Merge branch 'heads/upstream' into OBT/1.2.0(SpringUpdate)
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Barotrauma.LuaCs;
|
||||
|
||||
|
||||
// taken from: <https://gist.github.com/cajuncoding/a88f0d00847dcfc241ae80d1c7bafb1e?permalink_comment_id=4498792>
|
||||
public sealed class AsyncReaderWriterLock : IDisposable
|
||||
{
|
||||
readonly SemaphoreSlim _readSemaphore = new SemaphoreSlim(1, 1);
|
||||
readonly SemaphoreSlim _writeSemaphore = new SemaphoreSlim(1, 1);
|
||||
int _readerCount;
|
||||
|
||||
public async Task<IDisposable> AcquireWriterLock(CancellationToken token = default)
|
||||
{
|
||||
await _writeSemaphore.WaitAsync(token).ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
await _readSemaphore.WaitAsync(token).ConfigureAwait(false);
|
||||
}
|
||||
catch
|
||||
{
|
||||
_writeSemaphore.Release();
|
||||
throw;
|
||||
}
|
||||
|
||||
return new LockToken(ReleaseWriterLock);
|
||||
}
|
||||
|
||||
private void ReleaseWriterLock()
|
||||
{
|
||||
_readSemaphore.Release();
|
||||
_writeSemaphore.Release();
|
||||
}
|
||||
|
||||
public async Task<IDisposable> AcquireReaderLock(CancellationToken token = default)
|
||||
{
|
||||
await _writeSemaphore.WaitAsync(token).ConfigureAwait(false);
|
||||
if (Interlocked.Increment(ref _readerCount) == 1)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _readSemaphore.WaitAsync(token).ConfigureAwait(false);
|
||||
}
|
||||
catch
|
||||
{
|
||||
Interlocked.Decrement(ref _readerCount);
|
||||
_writeSemaphore.Release();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
_writeSemaphore.Release();
|
||||
return new LockToken(ReleaseReaderLock);
|
||||
}
|
||||
|
||||
private void ReleaseReaderLock()
|
||||
{
|
||||
if (Interlocked.Decrement(ref _readerCount) == 0)
|
||||
_readSemaphore.Release();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_writeSemaphore.Dispose();
|
||||
_readSemaphore.Dispose();
|
||||
}
|
||||
|
||||
private sealed class LockToken : IDisposable
|
||||
{
|
||||
private readonly Action _action;
|
||||
public LockToken(Action action) => _action = action;
|
||||
public void Dispose() => _action?.Invoke();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
using Barotrauma;
|
||||
using Barotrauma.Items.Components;
|
||||
using Barotrauma.Networking;
|
||||
using System;
|
||||
using System.Reflection;
|
||||
using static Barotrauma.Items.Components.Quality;
|
||||
|
||||
namespace Barotrauma;
|
||||
|
||||
static class MapEntityExtensions
|
||||
{
|
||||
public static void AddLinked(this MapEntity entity, MapEntity other)
|
||||
{
|
||||
entity.linkedTo.Add(other);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static class ClientExtensions
|
||||
{
|
||||
#if SERVER
|
||||
public static void SetClientCharacter(this Client client, Character character)
|
||||
{
|
||||
GameMain.Server.SetClientCharacter(client, character);
|
||||
}
|
||||
|
||||
public static void Kick(this Client client, string reason = "")
|
||||
{
|
||||
GameMain.Server.KickClient(client.Connection, reason);
|
||||
}
|
||||
|
||||
public static void Ban(this Client client, string reason = "", float seconds = -1)
|
||||
{
|
||||
if (seconds == -1)
|
||||
{
|
||||
GameMain.Server.BanClient(client, reason, null);
|
||||
}
|
||||
else
|
||||
{
|
||||
GameMain.Server.BanClient(client, reason, TimeSpan.FromSeconds(seconds));
|
||||
}
|
||||
}
|
||||
|
||||
public static bool CheckPermission(this Client client, ClientPermissions permissions)
|
||||
{
|
||||
return client.Permissions.HasFlag(permissions);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
static class ItemExtensions
|
||||
{
|
||||
public static object GetComponentString(this Item item, string component)
|
||||
{
|
||||
Type type = LuaCsSetup.Instance.PluginManagementService
|
||||
.GetType("Barotrauma.Items.Components." + component);
|
||||
|
||||
if (type == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
MethodInfo method = typeof(Item).GetMethod(nameof(Item.GetComponent));
|
||||
MethodInfo generic = method.MakeGenericMethod(type);
|
||||
return generic.Invoke(item, null);
|
||||
}
|
||||
|
||||
#if SERVER
|
||||
public static object CreateServerEventString(this Item item, string component)
|
||||
{
|
||||
var comp = item.GetComponentString(component);
|
||||
|
||||
if (comp == null)
|
||||
return null;
|
||||
|
||||
MethodInfo method = typeof(Item).GetMethod(
|
||||
nameof(Item.CreateServerEvent),
|
||||
new Type[] { Type.MakeGenericMethodParameter(0) });
|
||||
|
||||
MethodInfo generic = method.MakeGenericMethod(comp.GetType());
|
||||
return generic.Invoke(item, new object[] { comp });
|
||||
}
|
||||
|
||||
public static object CreateServerEventString(this Item item, string component, object[] extraData)
|
||||
{
|
||||
var comp = item.GetComponentString(component);
|
||||
|
||||
if (comp == null)
|
||||
return null;
|
||||
|
||||
MethodInfo method = typeof(Item).GetMethod(
|
||||
nameof(Item.CreateServerEvent),
|
||||
new Type[] { Type.MakeGenericMethodParameter(0), typeof(object[]) });
|
||||
|
||||
MethodInfo generic = method.MakeGenericMethod(comp.GetType());
|
||||
return generic.Invoke(item, new object[] { comp, extraData });
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
static class QualityExtensions
|
||||
{
|
||||
public static void SetValue(this Quality quality, StatType statType, float value)
|
||||
{
|
||||
quality.statValues[statType] = value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
using System;
|
||||
using System.Reflection;
|
||||
using Barotrauma.LuaCs;
|
||||
|
||||
namespace Barotrauma.LuaCs.Compatibility;
|
||||
|
||||
public interface ILuaCsHook : ILuaPatcher, ILuaCsShim
|
||||
{
|
||||
// Event Services
|
||||
[Obsolete("ACsMod is deprecated. Use ILuaEventService.Add() instead.")]
|
||||
void Add(string eventName, string identifier, LuaCsFunc callback, object owner = null);
|
||||
[Obsolete("ACsMod is deprecated. Use ILuaEventService.Add() instead.")]
|
||||
void Add(string eventName, LuaCsFunc callback, object owner = null);
|
||||
void Remove(string eventName, string identifier);
|
||||
// Does anyone use this? TODO: Analyze old Lua mods for usage scenarios.
|
||||
//bool Exists(string eventName, string identifier);
|
||||
object Call(string eventName, params object[] args);
|
||||
T Call<T>(string eventName, params object[] args);
|
||||
|
||||
// Needs to be here instead of ILuaPatcher for compatiility purposes
|
||||
public enum HookMethodType
|
||||
{
|
||||
Before, After
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace Barotrauma.LuaCs.Compatibility;
|
||||
|
||||
public interface ILuaCsLogger : ILuaCsShim
|
||||
{
|
||||
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
using Barotrauma.Networking;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Barotrauma.LuaCs.Compatibility;
|
||||
|
||||
internal interface ILuaCsNetworking : ILuaCsShim
|
||||
{
|
||||
void CreateEntityEvent(INetSerializable entity, NetEntityEvent.IData extraData);
|
||||
ushort LastClientListUpdateID { get; set; }
|
||||
void HttpRequest(string url, LuaCsAction callback, string data = null, string method = "POST", string contentType = "application/json", Dictionary<string, string> headers = null, string savePath = null);
|
||||
void HttpPost(string url, LuaCsAction callback, string data, string contentType = "application/json", Dictionary<string, string> headers = null, string savePath = null);
|
||||
void HttpGet(string url, LuaCsAction callback, Dictionary<string, string> headers = null, string savePath = null);
|
||||
void RequestGetHTTP(string url, LuaCsAction callback, Dictionary<string, string> headers = null, string savePath = null);
|
||||
void RequestPostHTTP(string url, LuaCsAction callback, string data, string contentType = "application/json", Dictionary<string, string> headers = null, string savePath = null);
|
||||
|
||||
void Receive(string netId, LuaCsAction action);
|
||||
#if SERVER
|
||||
int FileSenderMaxPacketsPerUpdate { get; set; }
|
||||
void ClientWriteLobby(Client client);
|
||||
void UpdateClientPermissions(Client client);
|
||||
IWriteMessage Start();
|
||||
void Send(IWriteMessage mesage, NetworkConnection connection = null, DeliveryMethod deliveryMethod = DeliveryMethod.Reliable);
|
||||
#elif CLIENT
|
||||
void Send(IWriteMessage mesage, DeliveryMethod deliveryMethod = DeliveryMethod.Reliable);
|
||||
#endif
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
using Barotrauma.LuaCs;
|
||||
|
||||
namespace Barotrauma.LuaCs.Compatibility;
|
||||
|
||||
public interface ILuaCsShim : IService
|
||||
{
|
||||
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Barotrauma.LuaCs.Compatibility;
|
||||
|
||||
internal partial interface ILuaCsTimer : IReusableService, ILuaCsShim
|
||||
{
|
||||
public static double Time => Timing.TotalTime;
|
||||
public static double GetTime() => Time;
|
||||
public static double AccumulatorMax { get; set; }
|
||||
|
||||
public void Clear();
|
||||
public void Wait(LuaCsAction action, int millisecondDelay);
|
||||
public void NextFrame(LuaCsAction action);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace Barotrauma.LuaCs.Compatibility;
|
||||
|
||||
public interface ILuaCsUtility : ILuaCsShim
|
||||
{
|
||||
public bool CanReadFromPath(string file);
|
||||
public bool CanWriteToPath(string file);
|
||||
internal bool IsPathAllowedException(string path, bool write = true,
|
||||
LuaCsMessageOrigin origin = LuaCsMessageOrigin.Unknown);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,305 @@
|
||||
using Barotrauma;
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Runtime.Loader;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace Barotrauma;
|
||||
|
||||
class LuaCsConfig
|
||||
{
|
||||
private enum ValueType
|
||||
{
|
||||
None,
|
||||
Text,
|
||||
Integer,
|
||||
Decimal,
|
||||
Boolean,
|
||||
Collection,
|
||||
Object,
|
||||
Enum
|
||||
}
|
||||
|
||||
private static Type[] LoadDocTypes(XElement typesElem)
|
||||
{
|
||||
var result = new List<Type>();
|
||||
var loadedTypes = AssemblyLoadContext.All
|
||||
.Where(alc => alc != AssemblyLoadContext.Default)
|
||||
.SelectMany(alc => alc.Assemblies)
|
||||
.SelectMany(asm => asm.GetTypes())
|
||||
.ToImmutableArray();
|
||||
|
||||
foreach (var elem in typesElem.Elements())
|
||||
{
|
||||
var typesFound = loadedTypes.Where(t => t.FullName?.EndsWith(elem.Value) ?? false).ToImmutableList();
|
||||
if (!typesFound.Any())
|
||||
{
|
||||
ModUtils.Logging.PrintError(
|
||||
$"{nameof(LuaCsConfig)}::{nameof(LoadDocTypes)}() | Unable to find a matching type for {elem.Value}");
|
||||
continue;
|
||||
}
|
||||
result.AddRange(typesFound);
|
||||
}
|
||||
|
||||
return result.ToArray();
|
||||
}
|
||||
|
||||
private static IEnumerable<XElement> SaveDocTypes(IEnumerable<Type> types)
|
||||
{
|
||||
return types.Select(t => new XElement("Type", t.ToString()));
|
||||
}
|
||||
|
||||
private static Type GetTypeAttr(Type[] types, XElement elem)
|
||||
{
|
||||
var idx = elem.GetAttributeInt("Type", -1);
|
||||
if (idx < 0 || idx >= types.Length) throw new Exception($"Type index '{idx}' is outside of saved types bounds");
|
||||
return types[idx];
|
||||
}
|
||||
private static ValueType GetValueType(XElement elem)
|
||||
{
|
||||
Enum.TryParse(typeof(ValueType), elem.Attribute("Value")?.Value, out object result);
|
||||
if (result != null) return (ValueType)result;
|
||||
else return ValueType.None;
|
||||
}
|
||||
private static object ParseValue(Type[] types, XElement elem)
|
||||
{
|
||||
var type = GetValueType(elem);
|
||||
|
||||
if (elem.IsEmpty) return null;
|
||||
if (type == ValueType.Enum)
|
||||
{
|
||||
var tType = GetTypeAttr(types, elem);
|
||||
if (tType == null || !tType.IsSubclassOf(typeof(Enum))) return null;
|
||||
if (Enum.TryParse(tType, elem.Value, out object result)) return result;
|
||||
else return null;
|
||||
}
|
||||
if (type == ValueType.Collection)
|
||||
{
|
||||
var tType = GetTypeAttr(types, elem);
|
||||
var tInt = tType.GetInterfaces().FirstOrDefault(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IEnumerable<>));
|
||||
var gArg = tInt.GetGenericArguments()[0];
|
||||
if (tType == null || !tType.GetInterfaces().Any(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IEnumerable<>))) return null;
|
||||
|
||||
object result = null;
|
||||
|
||||
if (result == null)
|
||||
{
|
||||
var ctor = tType.GetConstructors(BindingFlags.Public | BindingFlags.Instance).FirstOrDefault(c =>
|
||||
{
|
||||
var param = c.GetParameters();
|
||||
return param.Count() == 1 && param.Any(p => p.ParameterType.IsGenericType && p.ParameterType.GetGenericTypeDefinition() == typeof(IEnumerable<>));
|
||||
});
|
||||
if (ctor != null)
|
||||
{
|
||||
var elements = elem.Elements().Select(x => ParseValue(types, x));
|
||||
var castElems = typeof(Enumerable).GetMethod("Cast").MakeGenericMethod(gArg).Invoke(elements, new object[] { elements });
|
||||
result = ctor.Invoke(new object[] { castElems });
|
||||
}
|
||||
}
|
||||
|
||||
if (result == null)
|
||||
{
|
||||
var ctor = tType.GetConstructors(BindingFlags.Public | BindingFlags.Instance).FirstOrDefault(c => c.GetParameters().Count() == 0);
|
||||
var addMethod = tType.GetMethods(BindingFlags.Instance | BindingFlags.Public).FirstOrDefault(m =>
|
||||
{
|
||||
if (m.Name != "Add") return false;
|
||||
var param = m.GetParameters();
|
||||
return param.Count() == 1 && param[0].ParameterType == gArg;
|
||||
});
|
||||
if (ctor != null && addMethod != null)
|
||||
{
|
||||
var elements = elem.Elements().Select(x => ParseValue(types, x));
|
||||
result = ctor.Invoke(null);
|
||||
foreach (var el in elements) addMethod.Invoke(result, new object[] { el });
|
||||
}
|
||||
}
|
||||
|
||||
if (result == null)
|
||||
{
|
||||
var ctor = tType.GetConstructors(BindingFlags.Public | BindingFlags.Instance).FirstOrDefault();
|
||||
var setMethod = tType.GetMethods(BindingFlags.Instance | BindingFlags.Public).FirstOrDefault(m =>
|
||||
{
|
||||
if (m.Name != "Set") return false;
|
||||
var param = m.GetParameters();
|
||||
return param.Count() == 2 && param[0].ParameterType == typeof(int) && param[1].ParameterType == gArg;
|
||||
});
|
||||
if (ctor != null || setMethod != null)
|
||||
{
|
||||
var elements = elem.Elements().Select(x => ParseValue(types, x));
|
||||
result = ctor.Invoke(new object[] { elements.Count() });
|
||||
int i = 0;
|
||||
foreach (var el in elements)
|
||||
{
|
||||
setMethod.Invoke(result, new object[] { i, el });
|
||||
i++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
else if (type == ValueType.Text) return elem.Value;
|
||||
else if (type == ValueType.Integer)
|
||||
{
|
||||
int.TryParse(elem.Value, out var num);
|
||||
return num;
|
||||
}
|
||||
else if (type == ValueType.Decimal)
|
||||
{
|
||||
float.TryParse(elem.Value, out var num);
|
||||
return num;
|
||||
}
|
||||
else if (type == ValueType.Boolean)
|
||||
{
|
||||
bool.TryParse(elem.Value, out var boolean);
|
||||
return boolean;
|
||||
}
|
||||
else if (type == ValueType.Object)
|
||||
{
|
||||
var tType = GetTypeAttr(types, elem);
|
||||
if (tType == null) return null;
|
||||
|
||||
IEnumerable<FieldInfo> fields = tType.GetFields(BindingFlags.Instance | BindingFlags.Public)
|
||||
.Concat(tType.GetFields(BindingFlags.Instance | BindingFlags.NonPublic));
|
||||
IEnumerable<PropertyInfo> properties = tType.GetProperties(BindingFlags.Instance | BindingFlags.Public).Where(p => p.GetSetMethod() != null)
|
||||
.Concat(tType.GetProperties(BindingFlags.Instance | BindingFlags.NonPublic).Where(p => p.GetSetMethod() != null));
|
||||
|
||||
object result = null;
|
||||
var ctor = tType.GetConstructors(BindingFlags.Public | BindingFlags.Instance).FirstOrDefault(c => c.GetParameters().Count() == 0);
|
||||
if (ctor == null)
|
||||
{
|
||||
if (!tType.IsValueType) return null;
|
||||
result = Activator.CreateInstance(tType);
|
||||
}
|
||||
else result = ctor.Invoke(null);
|
||||
|
||||
foreach (var el in elem.Elements())
|
||||
{
|
||||
var value = ParseValue(types, el);
|
||||
|
||||
var field = fields.FirstOrDefault(f => f.Name == el.Name.LocalName);
|
||||
if (field != null) field.SetValue(result, value);
|
||||
var property = properties.FirstOrDefault(p => p.Name == el.Name.LocalName);
|
||||
if (property != null) property.SetValue(result, value);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
else return elem.Value;
|
||||
|
||||
}
|
||||
|
||||
private static void AddTypeAttr(List<Type> types, Type type, XElement elem)
|
||||
{
|
||||
if (!types.Contains(type)) types.Add(type);
|
||||
elem.SetAttributeValue("Type", types.IndexOf(type));
|
||||
}
|
||||
|
||||
private static XElement ParseObject(List<Type> types, string name, object value)
|
||||
{
|
||||
XElement result = new XElement(name);
|
||||
|
||||
if (value != null)
|
||||
{
|
||||
var tType = value.GetType();
|
||||
|
||||
if (tType.IsEnum)
|
||||
{
|
||||
result.SetAttributeValue("Value", ValueType.Enum);
|
||||
AddTypeAttr(types, tType, result);
|
||||
|
||||
result.Value = Enum.GetName(tType, value) ?? "";
|
||||
}
|
||||
else if (value is string str)
|
||||
{
|
||||
result.SetAttributeValue("Value", ValueType.Text);
|
||||
result.Value = str;
|
||||
}
|
||||
else if (value is int integer)
|
||||
{
|
||||
result.SetAttributeValue("Value", ValueType.Integer);
|
||||
result.Value = integer.ToString();
|
||||
}
|
||||
else if (value is float || value is double)
|
||||
{
|
||||
result.SetAttributeValue("Value", ValueType.Decimal);
|
||||
result.Value = value.ToString();
|
||||
}
|
||||
else if (value is bool boolean)
|
||||
{
|
||||
result.SetAttributeValue("Value", ValueType.Boolean);
|
||||
result.Value = boolean.ToString();
|
||||
}
|
||||
else if (tType.GetInterfaces().Any(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IEnumerable<>)))
|
||||
{
|
||||
result.SetAttributeValue("Value", ValueType.Collection);
|
||||
AddTypeAttr(types, tType, result);
|
||||
|
||||
var enumerator = (IEnumerator)tType.GetMethod("GetEnumerator").Invoke(value, null);
|
||||
while (enumerator.MoveNext())
|
||||
{
|
||||
var elVal = ParseObject(types, "Item", enumerator.Current);
|
||||
result.Add(elVal);
|
||||
}
|
||||
}
|
||||
else if (tType.IsClass || tType.IsValueType)
|
||||
{
|
||||
result.SetAttributeValue("Value", ValueType.Object);
|
||||
AddTypeAttr(types, tType, result);
|
||||
|
||||
IEnumerable<FieldInfo> fields = tType.GetFields(BindingFlags.Instance | BindingFlags.Public)
|
||||
.Concat(tType.GetFields(BindingFlags.Instance | BindingFlags.NonPublic));
|
||||
IEnumerable<PropertyInfo> properties = tType.GetProperties(BindingFlags.Instance | BindingFlags.Public).Where(p => p.GetSetMethod() != null)
|
||||
.Concat(tType.GetProperties(BindingFlags.Instance | BindingFlags.NonPublic).Where(p => p.GetSetMethod() != null));
|
||||
|
||||
foreach (var field in fields) result.Add(ParseObject(types, field.Name, field.GetValue(value)));
|
||||
foreach (var property in properties) result.Add(ParseObject(types, property.Name, property.GetValue(value)));
|
||||
}
|
||||
else
|
||||
{
|
||||
result.SetAttributeValue("Value", ValueType.None);
|
||||
result.Value = value.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
public static T Load<T>(FileStream file)
|
||||
{
|
||||
var doc = XDocument.Load(file);
|
||||
|
||||
var rootElems = doc.Root.Elements().ToArray();
|
||||
var types = rootElems[0];
|
||||
var elem = rootElems[1];
|
||||
|
||||
var dict = ParseValue(LoadDocTypes(types), elem);
|
||||
if (dict.GetType() == typeof(T)) return (T)dict;
|
||||
else throw new Exception($"Loaded configuration is not of the type '{typeof(T).Name}'");
|
||||
}
|
||||
|
||||
public static void Save(FileStream file, object obj)
|
||||
{
|
||||
var types = new List<Type>();
|
||||
var elem = ParseObject(types, "Root", obj);
|
||||
var root = new XElement("Configuration", new XElement("Types", SaveDocTypes(types)), elem);
|
||||
|
||||
var doc = new XDocument(root);
|
||||
doc.Save(file);
|
||||
}
|
||||
|
||||
public static T Load<T>(string path)
|
||||
{
|
||||
using (var file = LuaCsFile.OpenRead(path)) return Load<T>(file);
|
||||
}
|
||||
|
||||
public static void Save(string path, object obj)
|
||||
{
|
||||
using (var file = LuaCsFile.OpenWrite(path)) Save(file, obj);
|
||||
}
|
||||
}
|
||||
+7
-2
@@ -1,9 +1,14 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
/// <summary>
|
||||
/// <b>[Obsolete]</b> Legacy compatibility only.
|
||||
/// </summary>
|
||||
[Obsolete("Deprecated.")]
|
||||
public class LuaCsPerformanceCounter
|
||||
{
|
||||
public bool EnablePerformanceCounter = false;
|
||||
@@ -33,4 +38,4 @@ namespace Barotrauma
|
||||
HookElapsedTime[eventName][hookName] = (double)ticks / Stopwatch.Frequency;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Xml.Linq;
|
||||
using System.Xml.Serialization;
|
||||
using Barotrauma.LuaCs;
|
||||
using Barotrauma.Steam;
|
||||
using OneOf;
|
||||
|
||||
namespace Barotrauma.LuaCs.Data;
|
||||
|
||||
#region ModConfigurationInfo
|
||||
|
||||
public partial record ModConfigInfo : IModConfigInfo
|
||||
{
|
||||
public ContentPackage Package { get; init; }
|
||||
public ImmutableArray<IAssemblyResourceInfo> Assemblies { get; init; }
|
||||
public ImmutableArray<ILuaScriptResourceInfo> LuaScripts { get; init; }
|
||||
public ImmutableArray<IConfigResourceInfo> Configs { get; init; }
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region DataContracts_Resources
|
||||
|
||||
public record BaseResourceInfo : IBaseResourceInfo
|
||||
{
|
||||
public Platform SupportedPlatforms { get; init; }
|
||||
public Target SupportedTargets { get; init; }
|
||||
public int LoadPriority { get; init; }
|
||||
public ImmutableArray<ContentPath> FilePaths { get; init; }
|
||||
public bool Optional { get; init; }
|
||||
public string InternalName { get; init; }
|
||||
public ContentPackage OwnerPackage { get; init; }
|
||||
public ImmutableArray<Identifier> RequiredPackages { get; init; }
|
||||
public ImmutableArray<Identifier> IncompatiblePackages { get; init; }
|
||||
}
|
||||
|
||||
public record AssemblyResourceInfo : BaseResourceInfo, IAssemblyResourceInfo
|
||||
{
|
||||
public string FriendlyName { get; init; }
|
||||
public bool IsScript { get; init; }
|
||||
public bool UseInternalAccessName { get; init; }
|
||||
public bool IsReferenceModeOnly { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Note: Config settings and settings-profiles are stored in the same files.
|
||||
/// </summary>
|
||||
public record ConfigResourceInfo : BaseResourceInfo, IConfigResourceInfo {}
|
||||
|
||||
public record LuaScriptsResourceInfo : BaseResourceInfo, ILuaScriptResourceInfo
|
||||
{
|
||||
public bool IsAutorun { get; init; }
|
||||
public bool RunUnrestricted { get; init; }
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region DataContracts_ParsedInfo
|
||||
|
||||
public record ConfigInfo : IConfigInfo
|
||||
{
|
||||
public string InternalName { get; init; }
|
||||
public ContentPackage OwnerPackage { get; init; }
|
||||
public string DataType { get; init; }
|
||||
public XElement Element { get; init; }
|
||||
public RunState EditableStates { get; init; }
|
||||
public NetSync NetSync { get; init; }
|
||||
|
||||
#if CLIENT // IConfigDisplayInfo
|
||||
public string DisplayName { get; init; }
|
||||
public string Description { get; init; }
|
||||
public string DisplayCategory { get; init; }
|
||||
public bool ShowInMenus { get; init; }
|
||||
public string Tooltip { get; init; }
|
||||
public ContentPath ImageIconPath { get; init; }
|
||||
#endif
|
||||
}
|
||||
|
||||
public record ConfigProfileInfo : IConfigProfileInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// Profile name.
|
||||
/// </summary>
|
||||
public string InternalName { get; init; }
|
||||
public ContentPackage OwnerPackage { get; init; }
|
||||
public IReadOnlyList<(string SettingName, XElement Element)> ProfileValues { get; init; }
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -0,0 +1,21 @@
|
||||
using System;
|
||||
// ReSharper disable InconsistentNaming
|
||||
|
||||
namespace Barotrauma.LuaCs.Data;
|
||||
|
||||
[Flags]
|
||||
public enum Platform
|
||||
{
|
||||
Linux = 0x1,
|
||||
OSX = 0x2,
|
||||
Windows = 0x4,
|
||||
Any = Linux | OSX | Windows
|
||||
}
|
||||
|
||||
[Flags]
|
||||
public enum Target
|
||||
{
|
||||
Client = 0x1,
|
||||
Server = 0x2,
|
||||
Any = Client | Server
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Globalization;
|
||||
using System.Xml.Serialization;
|
||||
|
||||
namespace Barotrauma.LuaCs.Data;
|
||||
|
||||
public interface IDependencyInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// List of dependency packages required by this resource.
|
||||
/// </summary>
|
||||
ImmutableArray<Identifier> RequiredPackages { get; }
|
||||
/// <summary>
|
||||
/// List of packages incompatible with this resource.
|
||||
/// </summary>
|
||||
ImmutableArray<Identifier> IncompatiblePackages { get; }
|
||||
}
|
||||
|
||||
public interface IPlatformInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// Platforms that these localization files should be loaded for.
|
||||
/// </summary>
|
||||
[Required]
|
||||
[XmlAttribute("Platform")]
|
||||
Platform SupportedPlatforms { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Targets that these localization files should be loaded for.
|
||||
/// </summary>
|
||||
[Required]
|
||||
[XmlAttribute("Target")]
|
||||
Target SupportedTargets { get; }
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// ResourceInfos contain metadata about a resource.
|
||||
/// </summary>
|
||||
public interface IResourceInfo : IPlatformInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// [Optional]
|
||||
/// Specifies the loading order for all assets of the same type (ie. styles, assemblies, etc.) from
|
||||
/// the same <see cref="ContentPackage"/>. Lower number is higher priority, see <see cref="System.Linq.Enumerable.OrderBy{TSource,TKey}(IEnumerable{TSource}, Func{TSource,TKey})"/>
|
||||
/// </summary>
|
||||
[XmlAttribute("LoadPriority")]
|
||||
int LoadPriority { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Resource absolute file paths.
|
||||
/// </summary>
|
||||
[Required]
|
||||
ImmutableArray<ContentPath> FilePaths { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Marks this resource as optional (ie. Cross-CP content). Setting this to true will allow the dependency system to
|
||||
/// try and order the loading but not fail if it runs into circular dependency issues.
|
||||
/// </summary>
|
||||
[XmlAttribute("Optional")]
|
||||
bool Optional { get; }
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
using System;
|
||||
using System.Xml.Linq;
|
||||
using Barotrauma.LuaCs;
|
||||
using Barotrauma.Networking;
|
||||
|
||||
namespace Barotrauma.LuaCs.Data;
|
||||
|
||||
/// <summary>
|
||||
/// Parsed data from a configuration xml.
|
||||
/// </summary>
|
||||
public partial interface IConfigInfo : IDataInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// Specifies the type initializer that will be used to instantiate the config var.
|
||||
/// </summary>
|
||||
string DataType { get; }
|
||||
/// <summary>
|
||||
/// The 'Setting' XML element.
|
||||
/// </summary>
|
||||
XElement Element { get; }
|
||||
/// <summary>
|
||||
/// In what <see cref="RunState"/>(s) is this config editable. Will be editable in the selected state, and lower value states.
|
||||
/// <br/><br/>
|
||||
/// <b>[Important]</b><br/> Setting this to value lower than 'Configuration` will render this config read-only.
|
||||
/// <br/><br/><b>Expected Behaviour</b>:
|
||||
/// <br/><b>[<see cref="RunState.Unloaded"/>|<see cref="RunState.Unloaded"/>]</b>: Read-Only.
|
||||
/// <br/><b>[<see cref="RunState.LoadedNoExec"/>]</b>: Can only be changed at the Main Menu (not in a lobby).
|
||||
/// <br/><b>[<see cref="RunState.Running"/>]</b>: Can be changed at the Main Menu and while a lobby is active.
|
||||
/// </summary>
|
||||
RunState EditableStates { get; }
|
||||
/// <summary>
|
||||
/// Network synchronization rules for this config.
|
||||
/// </summary>
|
||||
NetSync NetSync { get; }
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace Barotrauma.LuaCs.Data;
|
||||
|
||||
public interface IConfigProfileInfo : IDataInfo
|
||||
{
|
||||
IReadOnlyList<(string SettingName, XElement Element)> ProfileValues { get; }
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Xml.Serialization;
|
||||
|
||||
namespace Barotrauma.LuaCs.Data;
|
||||
|
||||
/// <summary>
|
||||
/// Serves as a compound-key to refer to all resources and information that comes from a specific source.
|
||||
/// </summary>
|
||||
public interface IDataInfo : IEqualityComparer<IDataInfo>, IEquatable<IDataInfo>
|
||||
{
|
||||
/// <summary>
|
||||
/// Internal name unique within the resources inside a package.
|
||||
/// </summary>
|
||||
[XmlAttribute("Name")]
|
||||
string InternalName { get; }
|
||||
/// <summary>
|
||||
/// The package this information belongs to.
|
||||
/// </summary>
|
||||
ContentPackage OwnerPackage { get; }
|
||||
|
||||
bool IEqualityComparer<IDataInfo>.Equals(IDataInfo x, IDataInfo y)
|
||||
{
|
||||
if (x is null || y is null)
|
||||
return false;
|
||||
if (x.OwnerPackage is null)
|
||||
throw new NullReferenceException($"ContentPackage not set for resource {x}!");
|
||||
if (y.OwnerPackage is null)
|
||||
throw new NullReferenceException($"ContentPackage not set for resource {y}!");
|
||||
if (x.InternalName.IsNullOrWhiteSpace())
|
||||
throw new NullReferenceException($"InternalName not set for resource {x}!");
|
||||
if (y.InternalName.IsNullOrWhiteSpace())
|
||||
throw new NullReferenceException($"InternalName not set for resource {y}!");
|
||||
return x.OwnerPackage == y.OwnerPackage && x.InternalName == y.InternalName;
|
||||
}
|
||||
|
||||
bool IEquatable<IDataInfo>.Equals(IDataInfo other)
|
||||
{
|
||||
return Equals(this, other);
|
||||
}
|
||||
|
||||
int IEqualityComparer<IDataInfo>.GetHashCode(IDataInfo obj)
|
||||
{
|
||||
if (obj.OwnerPackage is null)
|
||||
throw new NullReferenceException($"ContentPackage not set for resource {obj}!");
|
||||
if (obj.InternalName.IsNullOrWhiteSpace())
|
||||
throw new NullReferenceException($"InternalName is null for object {obj}!");
|
||||
return obj.InternalName.GetHashCode() + obj.OwnerPackage.GetHashCode();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
using System.Collections.Immutable;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace Barotrauma.LuaCs.Data;
|
||||
|
||||
public partial interface IModConfigInfo : IAssembliesResourcesInfo,
|
||||
ILuaScriptsResourcesInfo, IConfigsResourcesInfo
|
||||
{
|
||||
// package info
|
||||
ContentPackage Package { get; }
|
||||
}
|
||||
|
||||
public record ResourceParserInfo(
|
||||
[NotNull] ContentPackage Owner,
|
||||
[NotNull] XElement Element,
|
||||
ImmutableArray<Identifier> Required,
|
||||
ImmutableArray<Identifier> Incompatible);
|
||||
@@ -0,0 +1,79 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Globalization;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Xml.Serialization;
|
||||
|
||||
namespace Barotrauma.LuaCs.Data;
|
||||
|
||||
|
||||
public interface IBaseResourceInfo : IResourceInfo, IDataInfo, IDependencyInfo {}
|
||||
|
||||
public interface IConfigResourceInfo : IBaseResourceInfo {}
|
||||
|
||||
/// <summary>
|
||||
/// Represents loadable Lua files.
|
||||
/// </summary>
|
||||
public interface ILuaScriptResourceInfo : IBaseResourceInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// Should this script be run automatically.
|
||||
/// </summary>
|
||||
[XmlAttribute("IsAutorun")]
|
||||
public bool IsAutorun { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Indicates that this lua resources needs to run outside sandbox/requires unrestricted access.
|
||||
/// </summary>
|
||||
[XmlAttribute("RunUnrestricted")]
|
||||
public bool RunUnrestricted { get; }
|
||||
}
|
||||
|
||||
public interface IAssemblyResourceInfo : IBaseResourceInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// The friendly name of the assembly. Script files belonging to the same assembly should all have the same name.
|
||||
/// Legacy scripts will all be given the sanitized name of the Content Package they belong to.
|
||||
/// </summary>
|
||||
[XmlAttribute("FriendlyName")]
|
||||
public string FriendlyName { get; }
|
||||
/// <summary>
|
||||
/// Is this entry referring to a script file collection.
|
||||
/// </summary>
|
||||
[XmlAttribute("IsScript")]
|
||||
public bool IsScript { get; }
|
||||
|
||||
/// <summary>
|
||||
/// <b>[Required(IsScript: true)] Whether the internal compiled assembly name should be named to enabled use of the
|
||||
/// <see cref="InternalsVisibleToAttribute"/> attribute.</b>
|
||||
/// </summary>
|
||||
[XmlAttribute("UseInternalAccessName")]
|
||||
public bool UseInternalAccessName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Should the following resources only be used for Compilation MetadataReference.
|
||||
/// NOTE: Affects the entire package's assembly resources, meant for internal use only.
|
||||
/// </summary>
|
||||
[XmlAttribute("IsReferenceModeOnly")]
|
||||
public bool IsReferenceModeOnly { get; }
|
||||
}
|
||||
|
||||
|
||||
#region Collections
|
||||
|
||||
public interface IAssembliesResourcesInfo
|
||||
{
|
||||
ImmutableArray<IAssemblyResourceInfo> Assemblies { get; }
|
||||
}
|
||||
|
||||
public interface ILuaScriptsResourcesInfo
|
||||
{
|
||||
ImmutableArray<ILuaScriptResourceInfo> LuaScripts { get; }
|
||||
}
|
||||
|
||||
public interface IConfigsResourcesInfo
|
||||
{
|
||||
ImmutableArray<IConfigResourceInfo> Configs { get; }
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -0,0 +1,18 @@
|
||||
namespace Barotrauma.LuaCs.Data;
|
||||
|
||||
/// <summary>
|
||||
/// Legacy data contract for the old run configuration system. Should be deprecated
|
||||
/// once no longer needed.
|
||||
/// </summary>
|
||||
public interface IRunConfig
|
||||
{
|
||||
bool UseNonPublicizedAssemblies { get; }
|
||||
bool AutoGenerated { get; }
|
||||
bool UseInternalAssemblyName { get; }
|
||||
string Client { get; }
|
||||
string Server { get; }
|
||||
|
||||
bool IsForced();
|
||||
bool IsStandard();
|
||||
bool IsForcedOrStandard();
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Xml.Linq;
|
||||
using Barotrauma.LuaCs.Data;
|
||||
using Barotrauma.LuaCs;
|
||||
using Barotrauma.Networking;
|
||||
using OneOf;
|
||||
|
||||
namespace Barotrauma.LuaCs.Data;
|
||||
|
||||
public partial interface ISettingBase : IDataInfo, IEquatable<ISettingBase>, IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Settings production factory. Should be implemented by all types and registered with the Dependency Injector.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">An interface type derived from <see cref="ISettingBase"/>.</typeparam>
|
||||
public interface IFactory<out T> where T : ISettingBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates an instance of the given <see cref="ISettingBase"/> type.
|
||||
/// </summary>
|
||||
/// <param name="configInfo">Configuration information.</param>
|
||||
/// <param name="valueChangePredicate">Called before a new value is assigned. Returns a boolean whether to allow
|
||||
/// the value to be changed to the one given.</param>
|
||||
/// <returns></returns>
|
||||
T CreateInstance([NotNull]IConfigInfo configInfo, Func<OneOf<string, XElement, object>, bool> valueChangePredicate);
|
||||
}
|
||||
|
||||
IConfigInfo GetConfigInfo();
|
||||
#if CLIENT
|
||||
IConfigDisplayInfo GetDisplayInfo();
|
||||
#endif
|
||||
bool IsDisposed { get; }
|
||||
Type GetValueType();
|
||||
string GetStringValue();
|
||||
string GetDefaultStringValue();
|
||||
bool TrySetSerializedValue(OneOf<string, XElement> value);
|
||||
event Action<ISettingBase> OnValueChanged;
|
||||
OneOf.OneOf<string, XElement> GetSerializableValue();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a setting representing a value of the given <see cref="Type"/>. Must be a compatible listed type. <br/>
|
||||
/// </summary>
|
||||
/// <typeparam name="T">
|
||||
/// <b>Compatible Types:</b><br/>
|
||||
/// Any primitive type:<br/>
|
||||
/// - <see cref="byte"/><br/>
|
||||
/// - <see cref="sbyte"/><br/>
|
||||
/// - <see cref="ushort"/><br/>
|
||||
/// - <see cref="short"/><br/>
|
||||
/// - <see cref="int"/><br/>
|
||||
/// - <see cref="uint"/><br/>
|
||||
/// - <see cref="long"/><br/>
|
||||
/// - <see cref="ulong"/><br/>
|
||||
/// - <see cref="float"/><br/>
|
||||
/// - <see cref="double"/><br/>
|
||||
/// Extension types and Enums: <br/>
|
||||
/// - <see cref="string"/><br/>
|
||||
/// - <see cref="Enum"/><br/>
|
||||
/// </typeparam>
|
||||
public interface ISettingBase<T> : ISettingBase where T : IEquatable<T>, IConvertible
|
||||
{
|
||||
[NotNull]
|
||||
T Value { get; }
|
||||
[NotNull]
|
||||
T DefaultValue { get; }
|
||||
bool TrySetValue(T value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a setting representing a value of the given <see cref="Type"/> with a minimum and maximum value.
|
||||
/// Can only be either an <see cref="int"/> or a <see cref="float"/>.
|
||||
/// </summary>
|
||||
/// <remarks>The type selection is limited by the Undertow implementation of the GUI Slider.</remarks>
|
||||
/// <typeparam name="T">The value type, either <see cref="int"/> or <see cref="float"/></typeparam>
|
||||
public interface ISettingRangeBase<T> : ISettingBase<T> where T : IEquatable<T>, IConvertible
|
||||
{
|
||||
T MinValue { get; }
|
||||
T MaxValue { get; }
|
||||
int IncrementalSteps { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a setting representing a value of the given <see cref="Type"/> with a distinct list of selectable values.
|
||||
/// Must be a type compatible with <see cref="ISettingBase{T}"/>.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The value type. See <see cref="ISettingBase{T}"/></typeparam>
|
||||
public interface ISettingList<T> : ISettingBase<T> where T : IEquatable<T>, IConvertible
|
||||
{
|
||||
bool TrySetValueByIndex(int index);
|
||||
IReadOnlyList<T> Options { get; }
|
||||
IReadOnlyList<string> StringOptions { get; }
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Security.AccessControl;
|
||||
using Barotrauma.LuaCs;
|
||||
using Barotrauma.Networking;
|
||||
using FluentResults;
|
||||
using OneOf.Types;
|
||||
|
||||
namespace Barotrauma.LuaCs.Data;
|
||||
|
||||
|
||||
// --- Storage Service
|
||||
// TODO: Configs should not be services, add new registration path for them.
|
||||
public interface IStorageServiceConfig : IService
|
||||
{
|
||||
string LocalModsDirectory { get; }
|
||||
string WorkshopModsDirectory { get; }
|
||||
string GameSettingsConfigPath { get; }
|
||||
#if CLIENT
|
||||
string TempDownloadsDirectory { get; }
|
||||
#endif
|
||||
string LocalDataSavePath { get; }
|
||||
string LocalDataPathRegex { get; }
|
||||
string LocalPackageDataPath { get; }
|
||||
}
|
||||
|
||||
public record StorageServiceConfig : IStorageServiceConfig
|
||||
{
|
||||
private static readonly string ExecutionLocation = Directory.GetCurrentDirectory().CleanUpPathCrossPlatform();
|
||||
|
||||
public string LocalModsDirectory { get; init; } = System.IO.Path.GetFullPath(ContentPackage.LocalModsDir).CleanUpPath();
|
||||
public string WorkshopModsDirectory { get; init; } = System.IO.Path.GetFullPath(ContentPackage.WorkshopModsDir).CleanUpPath();
|
||||
public string GameSettingsConfigPath { get; init; } = System.IO.Path.GetFullPath(
|
||||
string.IsNullOrEmpty(GameSettings.CurrentConfig.SavePath)
|
||||
? SaveUtil.DefaultSaveFolder
|
||||
: GameSettings.CurrentConfig.SavePath).CleanUpPath();
|
||||
#if CLIENT
|
||||
public string TempDownloadsDirectory { get; init; } = System.IO.Path.GetFullPath(ModReceiver.DownloadFolder).CleanUpPath();
|
||||
#endif
|
||||
public string LocalDataSavePath => Path.Combine(ExecutionLocation, "Data/Mods").CleanUpPathCrossPlatform();
|
||||
public string LocalDataPathRegex => "%ModDir%";
|
||||
public string RunLocation => ExecutionLocation;
|
||||
|
||||
public string LocalPackageDataPath => Path.Combine(LocalDataSavePath, LocalDataPathRegex);
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
// cannot be disposed.
|
||||
}
|
||||
|
||||
public bool IsDisposed => false;
|
||||
}
|
||||
|
||||
// --- Config Service
|
||||
public interface IConfigServiceConfig : IService
|
||||
{
|
||||
string LocalConfigPathPartial { get; }
|
||||
string FileNamePattern { get; }
|
||||
}
|
||||
|
||||
public record ConfigServiceConfig : IConfigServiceConfig
|
||||
{
|
||||
public string LocalConfigPathPartial => $"/Config/{FileNamePattern}.xml";
|
||||
public string FileNamePattern => "<ConfigName>";
|
||||
public void Dispose()
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
public bool IsDisposed => false;
|
||||
}
|
||||
|
||||
|
||||
// --- Lua Scripts Service
|
||||
public interface ILuaScriptServicesConfig : IService
|
||||
{
|
||||
bool SafeLuaIOEnabled { get; }
|
||||
bool UseCaching { get; }
|
||||
}
|
||||
|
||||
public record LuaScriptServicesConfig : ILuaScriptServicesConfig
|
||||
{
|
||||
public bool SafeLuaIOEnabled => true;
|
||||
public bool UseCaching => true;
|
||||
public void Dispose()
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
|
||||
public bool IsDisposed => false;
|
||||
}
|
||||
|
||||
// --- Package Management Service
|
||||
public interface IPackageManagementServiceConfig : IService
|
||||
{
|
||||
bool IsCsEnabled { get; }
|
||||
}
|
||||
|
||||
public class PackageManagementServiceConfig : IPackageManagementServiceConfig
|
||||
{
|
||||
public void Dispose()
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
|
||||
public bool IsDisposed => false;
|
||||
public bool IsCsEnabled => true;
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Xml.Linq;
|
||||
using Barotrauma.LuaCs.Data;
|
||||
using Microsoft.Toolkit.Diagnostics;
|
||||
using Microsoft.Xna.Framework;
|
||||
using OneOf;
|
||||
|
||||
namespace Barotrauma.LuaCs.Data;
|
||||
|
||||
public abstract class SettingBase : ISettingBase
|
||||
{
|
||||
protected SettingBase(IConfigInfo configInfo)
|
||||
{
|
||||
Guard.IsNotNull(configInfo, nameof(configInfo));
|
||||
ConfigInfo = configInfo;
|
||||
}
|
||||
|
||||
protected IConfigInfo ConfigInfo { get; private set; }
|
||||
|
||||
public string InternalName => ConfigInfo.InternalName;
|
||||
public ContentPackage OwnerPackage => ConfigInfo.OwnerPackage;
|
||||
|
||||
public IConfigInfo GetConfigInfo() => ConfigInfo;
|
||||
#if CLIENT
|
||||
public IConfigDisplayInfo GetDisplayInfo() => ConfigInfo;
|
||||
#endif
|
||||
|
||||
public virtual bool Equals(ISettingBase other)
|
||||
{
|
||||
return other is not null && (
|
||||
ReferenceEquals(this, other) || !IsDisposed &&
|
||||
OwnerPackage == other.OwnerPackage &&
|
||||
InternalName.Equals(other.InternalName));
|
||||
}
|
||||
|
||||
private int _isDisposed = 0;
|
||||
public virtual bool IsDisposed
|
||||
{
|
||||
get => ModUtils.Threading.GetBool(ref _isDisposed);
|
||||
private set => ModUtils.Threading.SetBool(ref _isDisposed, value);
|
||||
}
|
||||
|
||||
protected abstract void OnDispose();
|
||||
|
||||
public virtual void Dispose()
|
||||
{
|
||||
if (!ModUtils.Threading.CheckIfClearAndSetBool(ref _isDisposed))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
OnDispose();
|
||||
ConfigInfo = null;
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
// -- Must be implemented
|
||||
|
||||
public abstract Type GetValueType();
|
||||
public abstract string GetStringValue();
|
||||
public abstract string GetDefaultStringValue();
|
||||
public abstract bool TrySetSerializedValue(OneOf<string, XElement> value);
|
||||
|
||||
public abstract event Action<ISettingBase> OnValueChanged;
|
||||
public abstract OneOf<string, XElement> GetSerializableValue();
|
||||
#if CLIENT
|
||||
public virtual void AddDisplayComponent(GUILayoutGroup layoutGroup, Vector2 relativeSize, Action<string> onSerializedValue)
|
||||
{
|
||||
new GUITextBox(new RectTransform(relativeSize, layoutGroup.RectTransform), font: GUIStyle.SmallFont)
|
||||
{
|
||||
Text = GetStringValue(),
|
||||
OnTextChangedDelegate = (box, txt) =>
|
||||
{
|
||||
onSerializedValue?.Invoke(txt);
|
||||
return true;
|
||||
}
|
||||
};
|
||||
}
|
||||
#endif
|
||||
}
|
||||
@@ -0,0 +1,395 @@
|
||||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Xml.Linq;
|
||||
using Barotrauma.LuaCs.Data;
|
||||
using Barotrauma.LuaCs;
|
||||
using Barotrauma.Networking;
|
||||
using Microsoft.Toolkit.Diagnostics;
|
||||
using Microsoft.Xna.Framework;
|
||||
using OneOf;
|
||||
|
||||
namespace Barotrauma.LuaCs.Data;
|
||||
|
||||
public partial class SettingEntry<T> : SettingBase, ISettingBase<T>, INetworkSyncVar where T : IEquatable<T>, IConvertible
|
||||
{
|
||||
public class Factory : ISettingBase.IFactory<ISettingBase<T>>
|
||||
{
|
||||
public ISettingBase<T> CreateInstance(IConfigInfo configInfo, Func<OneOf<string, XElement, object>, bool> valueChangePredicate)
|
||||
{
|
||||
Guard.IsNotNull(configInfo, nameof(configInfo));
|
||||
return new SettingEntry<T>(configInfo, valueChangePredicate);
|
||||
}
|
||||
}
|
||||
|
||||
public SettingEntry(IConfigInfo configInfo,
|
||||
Func<OneOf<string, XElement, object>, bool> valueChangePredicate)
|
||||
: base(configInfo)
|
||||
{
|
||||
if (!(
|
||||
typeof(T).IsEnum ||
|
||||
typeof(T).IsPrimitive ||
|
||||
typeof(T) == typeof(string)))
|
||||
{
|
||||
ThrowHelper.ThrowArgumentException($"{nameof(ISettingBase<T>)}: The type of {nameof(T)} is not an allowed type.");
|
||||
}
|
||||
ValueChangePredicate = valueChangePredicate;
|
||||
|
||||
try
|
||||
{
|
||||
Value = (T)Convert.ChangeType(ConfigInfo.Element.GetAttributeString("Value", null), typeof(T));
|
||||
DefaultValue = Value;
|
||||
}
|
||||
catch (Exception e) when (e is InvalidCastException or ArgumentNullException)
|
||||
{
|
||||
Value = default(T);
|
||||
DefaultValue = default(T);
|
||||
}
|
||||
}
|
||||
|
||||
protected Func<OneOf<string, XElement, object>, bool> ValueChangePredicate;
|
||||
public T Value { get; protected set; }
|
||||
|
||||
public T DefaultValue { get; protected set; }
|
||||
|
||||
public virtual bool TrySetValue(T value)
|
||||
{
|
||||
if (value is null || value.Equals(Value))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
#if CLIENT
|
||||
if (SyncType is NetSync.ServerAuthority && NetworkingService is not null
|
||||
&& GameMain.IsMultiplayer
|
||||
&& GameMain.Client is not null
|
||||
&& !GameMain.Client.HasPermission(this.WritePermissions))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
|
||||
if (!TrySetValueInternal(value))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
OnValueChanged?.Invoke(this);
|
||||
#if CLIENT
|
||||
if (GameMain.IsMultiplayer && SyncType is NetSync.ClientOneWay or NetSync.TwoWay)
|
||||
{
|
||||
NetworkingService?.SendNetVar(this);
|
||||
}
|
||||
#elif SERVER
|
||||
if (SyncType is NetSync.TwoWay or NetSync.ServerAuthority)
|
||||
{
|
||||
NetworkingService?.SendNetVar(this);
|
||||
}
|
||||
#endif
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool TrySetValueInternal(T value)
|
||||
{
|
||||
if (value is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (ValueChangePredicate != null && !ValueChangePredicate(value))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Value = value;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// handles internal networking rules after reading the net message (to avoid synchro issues).
|
||||
/// </summary>
|
||||
/// <param name="value"></param>
|
||||
/// <returns></returns>
|
||||
private bool TrySetValueNetwork(T value)
|
||||
{
|
||||
if (NetworkingService is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
#if CLIENT
|
||||
if (SyncType is NetSync.None or NetSync.ClientOneWay)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
#else
|
||||
if (SyncType is NetSync.None or NetSync.ServerAuthority)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
if (!TrySetValueInternal(value))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
#if SERVER
|
||||
if (SyncType is NetSync.TwoWay)
|
||||
{
|
||||
NetworkingService?.SendNetVar(this);
|
||||
}
|
||||
#endif
|
||||
|
||||
OnValueChanged?.Invoke(this);
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override void OnDispose()
|
||||
{
|
||||
ValueChangePredicate = null;
|
||||
NetworkingService?.DeregisterNetVar(this);
|
||||
}
|
||||
|
||||
public override Type GetValueType() => typeof(T);
|
||||
public override string GetStringValue() => Value?.ToString() ?? string.Empty;
|
||||
public override string GetDefaultStringValue() => DefaultValue?.ToString() ?? string.Empty;
|
||||
|
||||
public override bool TrySetSerializedValue(OneOf<string, XElement> value)
|
||||
{
|
||||
bool isFailed = false;
|
||||
var typeConvertedValue = value.Match<T>(
|
||||
(string val) =>
|
||||
{
|
||||
try
|
||||
{
|
||||
return (T)Convert.ChangeType(val, typeof(T));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
// ignored
|
||||
isFailed = true;
|
||||
return default(T);
|
||||
}
|
||||
},
|
||||
(XElement val) =>
|
||||
{
|
||||
try
|
||||
{
|
||||
return (T)Convert.ChangeType(val.GetAttributeString("Value", null), typeof(T));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
isFailed = true;
|
||||
return default(T);
|
||||
}
|
||||
});
|
||||
return !isFailed && TrySetValue(typeConvertedValue);
|
||||
}
|
||||
|
||||
public override event Action<ISettingBase> OnValueChanged;
|
||||
|
||||
public override OneOf<string, XElement> GetSerializableValue() => Value.ToString();
|
||||
|
||||
// -- Networking
|
||||
protected IEntityNetworkingService NetworkingService;
|
||||
public Guid InstanceId => NetworkingService?.GetNetworkIdForInstance(this) ?? Guid.Empty;
|
||||
public void SetNetworkOwner(IEntityNetworkingService networkingService)
|
||||
{
|
||||
NetworkingService = networkingService;
|
||||
}
|
||||
|
||||
public NetSync SyncType => ConfigInfo?.NetSync ?? NetSync.None;
|
||||
// needs to be added IConfigInfo
|
||||
public ClientPermissions WritePermissions => ClientPermissions.ManageSettings;
|
||||
|
||||
public void ReadNetMessage(IReadMessage message)
|
||||
{
|
||||
if (SyncType == NetSync.None || NetworkingService is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (typeof(T).IsEnum)
|
||||
{
|
||||
TrySetValueInternal((T)(object)message.ReadInt32());
|
||||
}
|
||||
|
||||
// No...there's no better way to do this...
|
||||
var typeCode = Type.GetTypeCode(typeof(T));
|
||||
switch (typeCode)
|
||||
{
|
||||
case TypeCode.Boolean:
|
||||
TrySetValueNetwork((T)Convert.ChangeType(message.ReadBoolean(), typeCode));
|
||||
return;
|
||||
case TypeCode.Byte:
|
||||
TrySetValueNetwork((T)Convert.ChangeType(message.ReadByte(), typeCode));
|
||||
return;
|
||||
// SByte not supported by interface
|
||||
case TypeCode.SByte:
|
||||
TrySetValueNetwork((T)Convert.ChangeType(message.ReadInt16(), typeCode));
|
||||
return;
|
||||
case TypeCode.Int16:
|
||||
TrySetValueNetwork((T)Convert.ChangeType(message.ReadInt16(), typeCode));
|
||||
return;
|
||||
case TypeCode.Char:
|
||||
case TypeCode.UInt16:
|
||||
TrySetValueNetwork((T)Convert.ChangeType(message.ReadUInt16(), typeCode));
|
||||
return;
|
||||
case TypeCode.Int32:
|
||||
TrySetValueNetwork((T)Convert.ChangeType(message.ReadInt32(), typeCode));
|
||||
return;
|
||||
case TypeCode.UInt32:
|
||||
TrySetValueNetwork((T)Convert.ChangeType(message.ReadUInt32(), typeCode));
|
||||
return;
|
||||
case TypeCode.Int64:
|
||||
TrySetValueNetwork((T)Convert.ChangeType(message.ReadInt64(), typeCode));
|
||||
return;
|
||||
case TypeCode.UInt64:
|
||||
TrySetValueNetwork((T)Convert.ChangeType(message.ReadUInt64(), typeCode));
|
||||
return;
|
||||
case TypeCode.Single:
|
||||
TrySetValueNetwork((T)Convert.ChangeType(message.ReadSingle(), typeCode));
|
||||
return;
|
||||
case TypeCode.Double:
|
||||
TrySetValueNetwork((T)Convert.ChangeType(message.ReadDouble(), typeCode));
|
||||
return;
|
||||
case TypeCode.String:
|
||||
TrySetValueNetwork((T)Convert.ChangeType(message.ReadString(), typeCode));
|
||||
return;
|
||||
case TypeCode.Decimal:
|
||||
default:
|
||||
ThrowHelper.ThrowNotSupportedException($"{nameof(SettingEntry<T>)}: The type {typeof(T).Name} is not supported.");
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
// Suppress unless we're testing.
|
||||
#if DEBUG
|
||||
throw;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
public void WriteNetMessage(IWriteMessage message)
|
||||
{
|
||||
if (SyncType == NetSync.None || NetworkingService is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (typeof(T).IsEnum)
|
||||
{
|
||||
message.WriteInt32((int)((IConvertible)Value));
|
||||
}
|
||||
|
||||
// No...there's no better way to do this...
|
||||
var typeCode = Type.GetTypeCode(typeof(T));
|
||||
switch (typeCode)
|
||||
{
|
||||
case TypeCode.Boolean:
|
||||
message.WriteBoolean((bool)Convert.ChangeType(Value, typeCode)!);
|
||||
return;
|
||||
case TypeCode.Byte:
|
||||
message.WriteByte((byte)Convert.ChangeType(Value, typeCode)!);
|
||||
return;
|
||||
// SByte not supported by interface
|
||||
case TypeCode.SByte:
|
||||
message.WriteInt16((short)Convert.ChangeType(Value, typeCode)!);
|
||||
return;
|
||||
case TypeCode.Int16:
|
||||
message.WriteInt16((short)Convert.ChangeType(Value, typeCode)!);
|
||||
return;
|
||||
case TypeCode.Char:
|
||||
case TypeCode.UInt16:
|
||||
message.WriteUInt16((ushort)Convert.ChangeType(Value, typeCode)!);
|
||||
return;
|
||||
case TypeCode.Int32:
|
||||
message.WriteInt32((int)Convert.ChangeType(Value, typeCode)!);
|
||||
return;
|
||||
case TypeCode.UInt32:
|
||||
message.WriteUInt32((uint)Convert.ChangeType(Value, typeCode)!);
|
||||
return;
|
||||
case TypeCode.Int64:
|
||||
message.WriteInt64((long)Convert.ChangeType(Value, typeCode)!);
|
||||
return;
|
||||
case TypeCode.UInt64:
|
||||
message.WriteUInt64((ulong)Convert.ChangeType(Value, typeCode)!);
|
||||
return;
|
||||
case TypeCode.Single:
|
||||
message.WriteSingle((float)Convert.ChangeType(Value, typeCode)!);
|
||||
return;
|
||||
case TypeCode.Double:
|
||||
message.WriteDouble((double)Convert.ChangeType(Value, typeCode)!);
|
||||
return;
|
||||
case TypeCode.String:
|
||||
message.WriteString((string)Convert.ChangeType(Value, typeCode)!);
|
||||
return;
|
||||
case TypeCode.Decimal:
|
||||
default:
|
||||
ThrowHelper.ThrowNotSupportedException($"{nameof(SettingEntry<T>)}: The type {typeof(T).Name} is not supported.");
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
// Suppress unless we're testing.
|
||||
#if DEBUG
|
||||
throw;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
#if CLIENT
|
||||
public override void AddDisplayComponent(GUILayoutGroup layoutGroup, Vector2 relativeSize, Action<string> onSerializedValue)
|
||||
{
|
||||
switch (Type.GetTypeCode(typeof(T)))
|
||||
{
|
||||
case TypeCode.Boolean:
|
||||
new GUITickBox(new RectTransform(relativeSize, layoutGroup.RectTransform), "")
|
||||
{
|
||||
Selected = (bool)Convert.ChangeType(this.Value, TypeCode.Boolean),
|
||||
OnSelected = (box) =>
|
||||
{
|
||||
onSerializedValue?.Invoke(box.Selected.ToString());
|
||||
return true;
|
||||
}
|
||||
};
|
||||
break;
|
||||
case TypeCode.Byte:
|
||||
case TypeCode.SByte:
|
||||
case TypeCode.Int16:
|
||||
case TypeCode.Char:
|
||||
case TypeCode.UInt16:
|
||||
case TypeCode.Int32:
|
||||
case TypeCode.UInt32:
|
||||
case TypeCode.Int64:
|
||||
case TypeCode.UInt64:
|
||||
new GUINumberInput(new RectTransform(relativeSize, layoutGroup.RectTransform), NumberType.Int)
|
||||
{
|
||||
IntValue = (int)Convert.ChangeType(this.Value, TypeCode.Int32)!,
|
||||
OnValueChanged = (num) =>
|
||||
{
|
||||
onSerializedValue?.Invoke(num.IntValue.ToString());
|
||||
}
|
||||
};
|
||||
break;
|
||||
case TypeCode.Single:
|
||||
case TypeCode.Double:
|
||||
new GUINumberInput(new RectTransform(relativeSize, layoutGroup.RectTransform), NumberType.Float)
|
||||
{
|
||||
FloatValue = (float)Convert.ChangeType(this.Value, TypeCode.Single)!,
|
||||
OnValueChanged = (num) =>
|
||||
{
|
||||
onSerializedValue?.Invoke(num.FloatValue.ToString());
|
||||
}
|
||||
};
|
||||
break;
|
||||
case TypeCode.String:
|
||||
default:
|
||||
base.AddDisplayComponent(layoutGroup, relativeSize, onSerializedValue);
|
||||
break;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Xml;
|
||||
using System.Xml.Linq;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using Microsoft.Toolkit.Diagnostics;
|
||||
using Microsoft.Xna.Framework;
|
||||
|
||||
namespace Barotrauma.LuaCs.Data;
|
||||
|
||||
public class SettingList<T> : SettingEntry<T>, ISettingList<T> where T : IEquatable<T>, IConvertible
|
||||
{
|
||||
public class LFactory : ISettingBase.IFactory<ISettingList<T>>
|
||||
{
|
||||
public ISettingList<T> CreateInstance(IConfigInfo configInfo, Func<OneOf<string, XElement, object>, bool> valueChangePredicate)
|
||||
{
|
||||
Guard.IsNotNull(configInfo, nameof(configInfo));
|
||||
return new SettingList<T>(configInfo, valueChangePredicate);
|
||||
}
|
||||
}
|
||||
|
||||
public SettingList(IConfigInfo configInfo, Func<OneOf<string, XElement, object>, bool> valueChangePredicate) : base(configInfo, valueChangePredicate)
|
||||
{
|
||||
if (!(
|
||||
typeof(T).IsEnum ||
|
||||
typeof(T).IsPrimitive ||
|
||||
typeof(T) == typeof(string)))
|
||||
{
|
||||
ThrowHelper.ThrowArgumentException($"{nameof(ISettingBase<T>)}: The type of {nameof(T)} is not an allowed type.");
|
||||
}
|
||||
ValueChangePredicate = valueChangePredicate;
|
||||
|
||||
var valuesElements = ConfigInfo.Element.GetChildElement("Values")?.GetChildElements("Value")?.ToImmutableArray();
|
||||
|
||||
Guard.IsNotNull(valuesElements, this.InternalName);
|
||||
if (valuesElements.Value.IsEmpty)
|
||||
{
|
||||
ThrowHelper.ThrowArgumentNullException($"{this.InternalName}: Could not find any values in list!");
|
||||
}
|
||||
|
||||
foreach (var element in valuesElements.Value)
|
||||
{
|
||||
if (!TryConvert(element, out var v1))
|
||||
{
|
||||
ThrowHelper.ThrowArgumentException($"{this.InternalName}: Error while parsing list values");
|
||||
}
|
||||
_valuesList.Add(v1);
|
||||
}
|
||||
|
||||
if (TryConvert(ConfigInfo.Element, out var v) && _valuesList.Contains(v))
|
||||
{
|
||||
Value = v;
|
||||
DefaultValue = v;
|
||||
}
|
||||
else
|
||||
{
|
||||
Value = _valuesList[0];
|
||||
DefaultValue = _valuesList[0];
|
||||
}
|
||||
|
||||
|
||||
bool TryConvert(XElement element, out T value)
|
||||
{
|
||||
try
|
||||
{
|
||||
value = (T)Convert.ChangeType(element.GetAttributeString("Value", null), typeof(T));
|
||||
return true;
|
||||
}
|
||||
catch (Exception e) when (e is InvalidCastException or ArgumentNullException)
|
||||
{
|
||||
value = default(T);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private readonly List<T> _valuesList = new();
|
||||
|
||||
public override bool TrySetValue(T value)
|
||||
{
|
||||
if (!_valuesList.Contains(value))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return base.TrySetValue(value);
|
||||
}
|
||||
|
||||
public bool TrySetValueByIndex(int index)
|
||||
{
|
||||
if (_valuesList.Count <= index)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return base.TrySetValue(_valuesList[index]);
|
||||
}
|
||||
|
||||
public IReadOnlyList<T> Options => _valuesList.AsReadOnly();
|
||||
|
||||
public IReadOnlyList<string> StringOptions => _valuesList.Select(e => e.ToString()).ToImmutableArray();
|
||||
|
||||
#if CLIENT
|
||||
public override void AddDisplayComponent(GUILayoutGroup layoutGroup, Vector2 relativeSize, Action<string> onSerializedValue)
|
||||
{
|
||||
GUIUtil.Dropdown(layoutGroup, (T val) => GetLocalizedString(val.ToString(), val.ToString()), null, Options, Value, (T val) =>
|
||||
{
|
||||
onSerializedValue?.Invoke(val.ToString());
|
||||
}, new Vector2(relativeSize.X, 1f));
|
||||
|
||||
string GetLocalizedString(string identifier, string defaultValue)
|
||||
{
|
||||
var lstr = TextManager.Get($"{XmlConvert.EncodeLocalName(OwnerPackage.Name)}.{InternalName}.{identifier}.DisplayName");
|
||||
return lstr.IsNullOrWhiteSpace() ? defaultValue : lstr.Value;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Xml.Linq;
|
||||
using Barotrauma.LuaCs.Data;
|
||||
using Microsoft.Toolkit.Diagnostics;
|
||||
using Microsoft.Xna.Framework;
|
||||
using OneOf;
|
||||
|
||||
namespace Barotrauma.LuaCs.Data;
|
||||
|
||||
public abstract class SettingRangeBase<T> : SettingEntry<T>, ISettingRangeBase<T> where T : IEquatable<T>, IConvertible
|
||||
{
|
||||
public SettingRangeBase(IConfigInfo configInfo, Func<OneOf<string, XElement, object>, bool> valueChangePredicate) : base(configInfo, valueChangePredicate)
|
||||
{
|
||||
}
|
||||
|
||||
public T MinValue { get; protected init; }
|
||||
public T MaxValue { get; protected init; }
|
||||
public int IncrementalSteps { get; protected init; }
|
||||
}
|
||||
|
||||
public class SettingRangeFloat : SettingRangeBase<float>
|
||||
{
|
||||
public class RangeFactory : ISettingBase.IFactory<SettingRangeFloat>
|
||||
{
|
||||
public SettingRangeFloat CreateInstance(IConfigInfo configInfo, Func<OneOf<string, XElement, object>, bool> valueChangePredicate)
|
||||
{
|
||||
Guard.IsNotNull(configInfo, nameof(configInfo));
|
||||
return new SettingRangeFloat(configInfo, valueChangePredicate);
|
||||
}
|
||||
}
|
||||
|
||||
public SettingRangeFloat(IConfigInfo configInfo, Func<OneOf<string, XElement, object>, bool> valueChangePredicate) : base(configInfo, valueChangePredicate)
|
||||
{
|
||||
// funny values in case they forget to set them in the config.
|
||||
MinValue = configInfo.Element.GetAttributeFloat("Min", float.MinValue);
|
||||
MaxValue = configInfo.Element.GetAttributeFloat("Max", float.MaxValue);
|
||||
IncrementalSteps = configInfo.Element.GetAttributeInt("Steps", 3);
|
||||
}
|
||||
|
||||
public override bool TrySetValue(float value)
|
||||
{
|
||||
if (value > MaxValue || value < MinValue)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return base.TrySetValue(value);
|
||||
}
|
||||
|
||||
#if CLIENT
|
||||
public override void AddDisplayComponent(GUILayoutGroup layoutGroup, Vector2 relativeSize, Action<string> onSerializedValue)
|
||||
{
|
||||
GUIUtil.Slider(layoutGroup, new Vector2(MinValue, MaxValue), IncrementalSteps, labelFunc: val =>
|
||||
{
|
||||
return val.ToString("G4", CultureInfo.InvariantCulture);
|
||||
}, Value, setter: val =>
|
||||
{
|
||||
onSerializedValue?.Invoke(val.ToString());
|
||||
}, TextManager.Get(this.GetDisplayInfo().Tooltip), relativeSize);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
public class SettingRangeInt : SettingRangeBase<int>
|
||||
{
|
||||
public class RangeFactory : ISettingBase.IFactory<SettingRangeInt>
|
||||
{
|
||||
public SettingRangeInt CreateInstance(IConfigInfo configInfo, Func<OneOf<string, XElement, object>, bool> valueChangePredicate)
|
||||
{
|
||||
Guard.IsNotNull(configInfo, nameof(configInfo));
|
||||
return new SettingRangeInt(configInfo, valueChangePredicate);
|
||||
}
|
||||
}
|
||||
|
||||
public SettingRangeInt(IConfigInfo configInfo, Func<OneOf<string, XElement, object>, bool> valueChangePredicate) : base(configInfo, valueChangePredicate)
|
||||
{
|
||||
// funny values in case they forget to set them in the config.
|
||||
MinValue = configInfo.Element.GetAttributeInt("Min", int.MinValue);
|
||||
MaxValue = configInfo.Element.GetAttributeInt("Max", int.MaxValue);
|
||||
IncrementalSteps = configInfo.Element.GetAttributeInt("Steps", 3);
|
||||
}
|
||||
|
||||
public override bool TrySetValue(int value)
|
||||
{
|
||||
if (value > MaxValue || value < MinValue)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return base.TrySetValue(value);
|
||||
}
|
||||
|
||||
#if CLIENT
|
||||
public override void AddDisplayComponent(GUILayoutGroup layoutGroup, Vector2 relativeSize, Action<string> onSerializedValue)
|
||||
{
|
||||
GUIUtil.Slider(layoutGroup, new Vector2(MinValue, MaxValue), IncrementalSteps, labelFunc: val =>
|
||||
{
|
||||
return ((int)val).ToString();
|
||||
}, Value, setter: val =>
|
||||
{
|
||||
onSerializedValue?.Invoke(((int)val).ToString());
|
||||
}, TextManager.Get(this.GetDisplayInfo().Tooltip), relativeSize);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
+123
@@ -0,0 +1,123 @@
|
||||
using System;
|
||||
using System.Xml.Linq;
|
||||
using Barotrauma.LuaCs.Data;
|
||||
using Barotrauma.LuaCs;
|
||||
using OneOf;
|
||||
|
||||
namespace Barotrauma.LuaCs.Data;
|
||||
|
||||
public interface ISettingsRegistrationProvider : IService
|
||||
{
|
||||
void RegisterTypeProviders(IConfigService configService, Func<OneOf<string, XElement, object>, bool> valueChangePredicate);
|
||||
}
|
||||
|
||||
public class SettingsEntryRegistrar : ISettingsRegistrationProvider
|
||||
{
|
||||
private ILuaCsInfoProvider _infoProvider;
|
||||
|
||||
public SettingsEntryRegistrar(ILuaCsInfoProvider infoProvider)
|
||||
{
|
||||
_infoProvider = infoProvider;
|
||||
}
|
||||
|
||||
public void RegisterTypeProviders(IConfigService configService, Func<OneOf<string, XElement, object>, bool> valueChangePredicate)
|
||||
{
|
||||
RegisterSettingEntry<bool>(configService, "bool", valueChangePredicate);
|
||||
RegisterSettingEntry<byte>(configService, "byte", valueChangePredicate);
|
||||
RegisterSettingEntry<sbyte>(configService, "sbyte", valueChangePredicate);
|
||||
RegisterSettingEntry<short>(configService, "short", valueChangePredicate);
|
||||
RegisterSettingEntry<ushort>(configService, "ushort", valueChangePredicate);
|
||||
RegisterSettingEntry<int>(configService, "int", valueChangePredicate);
|
||||
RegisterSettingEntry<uint>(configService, "uint", valueChangePredicate);
|
||||
RegisterSettingEntry<long>(configService, "long", valueChangePredicate);
|
||||
RegisterSettingEntry<ulong>(configService, "ulong", valueChangePredicate);
|
||||
RegisterSettingEntry<string>(configService, "string", valueChangePredicate);
|
||||
RegisterSettingEntry<float>(configService, "float", valueChangePredicate);
|
||||
RegisterSettingEntry<float>(configService, "single", valueChangePredicate);
|
||||
RegisterSettingEntry<double>(configService, "double", valueChangePredicate);
|
||||
|
||||
// ISettingRangeBase<T>
|
||||
configService.RegisterSettingTypeInitializer("rangeInt", cfgInfo =>
|
||||
{
|
||||
return new SettingRangeInt.RangeFactory().CreateInstance(cfgInfo.Info, (val) =>
|
||||
IsValueChangeAllowed(cfgInfo.Info, val, valueChangePredicate));
|
||||
});
|
||||
|
||||
configService.RegisterSettingTypeInitializer("rangeFloat", cfgInfo =>
|
||||
{
|
||||
return new SettingRangeFloat.RangeFactory().CreateInstance(cfgInfo.Info, (val) =>
|
||||
IsValueChangeAllowed(cfgInfo.Info, val, valueChangePredicate));
|
||||
});
|
||||
|
||||
#if CLIENT
|
||||
configService.RegisterSettingTypeInitializer("control" , cfgInfo =>
|
||||
{
|
||||
return new SettingControl.Factory().CreateInstance(cfgInfo.Info, val =>
|
||||
IsValueChangeAllowed(cfgInfo.Info, val, valueChangePredicate));
|
||||
});
|
||||
#endif
|
||||
|
||||
RegisterSettingList<bool>(configService, "listBool", valueChangePredicate);
|
||||
RegisterSettingList<byte>(configService, "listByte", valueChangePredicate);
|
||||
RegisterSettingList<sbyte>(configService, "listSbyte", valueChangePredicate);
|
||||
RegisterSettingList<short>(configService, "listShort", valueChangePredicate);
|
||||
RegisterSettingList<ushort>(configService, "listUshort", valueChangePredicate);
|
||||
RegisterSettingList<int>(configService, "listInt", valueChangePredicate);
|
||||
RegisterSettingList<uint>(configService, "listUint", valueChangePredicate);
|
||||
RegisterSettingList<long>(configService, "listLong", valueChangePredicate);
|
||||
RegisterSettingList<ulong>(configService, "listUlong", valueChangePredicate);
|
||||
RegisterSettingList<string>(configService, "listString", valueChangePredicate);
|
||||
RegisterSettingList<float>(configService, "listFloat", valueChangePredicate);
|
||||
RegisterSettingList<float>(configService, "listSingle", valueChangePredicate);
|
||||
RegisterSettingList<double>(configService, "listDouble", valueChangePredicate);
|
||||
}
|
||||
|
||||
private void RegisterSettingList<T>(IConfigService configService, string typeName, Func<OneOf<string, XElement, object>, bool> valueChangePredicate) where T : IEquatable<T>, IConvertible
|
||||
{
|
||||
configService.RegisterSettingTypeInitializer(typeName, cfgInfo =>
|
||||
{
|
||||
return new SettingList<T>.LFactory().CreateInstance(cfgInfo.Info, (val) =>
|
||||
IsValueChangeAllowed(cfgInfo.Info, val, valueChangePredicate));
|
||||
});
|
||||
}
|
||||
|
||||
private void RegisterSettingEntry<T>(IConfigService configService, string typeName, Func<OneOf<string, XElement, object>, bool> valueChangePredicate) where T : IEquatable<T>, IConvertible
|
||||
{
|
||||
configService.RegisterSettingTypeInitializer(typeName, cfgInfo =>
|
||||
{
|
||||
return new SettingEntry<T>.Factory().CreateInstance(cfgInfo.Info, (val) =>
|
||||
IsValueChangeAllowed(cfgInfo.Info, val, valueChangePredicate));
|
||||
});
|
||||
}
|
||||
|
||||
private bool IsValueChangeAllowed(IConfigInfo info, OneOf<string, XElement, object> newValue,
|
||||
Func<OneOf<string, XElement, object>, bool> valueChangePredicate)
|
||||
{
|
||||
#if CLIENT
|
||||
return !info.Element.GetAttributeBool("ReadOnly", false)
|
||||
|| info.EditableStates < _infoProvider.CurrentRunState
|
||||
|| valueChangePredicate is null
|
||||
|| valueChangePredicate.Invoke(newValue);
|
||||
#else
|
||||
// Server has absolute authority.
|
||||
return !info.Element.GetAttributeBool("ReadOnly", false);
|
||||
#endif
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (!ModUtils.Threading.CheckIfClearAndSetBool(ref _isDisposed))
|
||||
{
|
||||
return;
|
||||
}
|
||||
_infoProvider.Dispose();
|
||||
_infoProvider = null;
|
||||
}
|
||||
|
||||
private int _isDisposed;
|
||||
public bool IsDisposed
|
||||
{
|
||||
get => ModUtils.Threading.GetBool(ref _isDisposed);
|
||||
private set => ModUtils.Threading.SetBool(ref _isDisposed, value);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,110 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using MoonSharp.Interpreter;
|
||||
using Microsoft.Xna.Framework;
|
||||
using Barotrauma.Networking;
|
||||
|
||||
namespace Barotrauma.Networking
|
||||
{
|
||||
partial class Client
|
||||
{
|
||||
public static IReadOnlyList<Client> ClientList
|
||||
{
|
||||
get
|
||||
{
|
||||
if (GameMain.IsSingleplayer) { return new List<Client>(); }
|
||||
|
||||
#if SERVER
|
||||
return GameMain.Server.ConnectedClients;
|
||||
#else
|
||||
return GameMain.Client.ConnectedClients;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
public ulong SteamID
|
||||
{
|
||||
get
|
||||
{
|
||||
if (AccountId.TryUnwrap(out AccountId outValue) && outValue is SteamId steamId)
|
||||
{
|
||||
return steamId.Value;
|
||||
}
|
||||
else
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
using Barotrauma.Networking;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
|
||||
|
||||
|
||||
partial class Character
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
partial class Item
|
||||
{
|
||||
public object GetComponentString(string component)
|
||||
{
|
||||
Type type = LuaUserData.GetType("Barotrauma.Items.Components." + component);
|
||||
|
||||
if (type == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
MethodInfo method = typeof(Item).GetMethod(nameof(Item.GetComponent));
|
||||
MethodInfo generic = method.MakeGenericMethod(type);
|
||||
return generic.Invoke(this, null);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
partial class ItemPrefab
|
||||
{
|
||||
|
||||
public static ItemPrefab GetItemPrefab(string itemNameOrId)
|
||||
{
|
||||
ItemPrefab itemPrefab =
|
||||
(MapEntityPrefab.Find(itemNameOrId, identifier: null, showErrorMessages: false) ??
|
||||
MapEntityPrefab.Find(null, identifier: itemNameOrId, showErrorMessages: false)) as ItemPrefab;
|
||||
|
||||
return itemPrefab;
|
||||
}
|
||||
}
|
||||
|
||||
abstract partial class MapEntity
|
||||
{
|
||||
public void AddLinked(MapEntity entity)
|
||||
{
|
||||
linkedTo.Add(entity);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
namespace Barotrauma.Items.Components
|
||||
{
|
||||
using Barotrauma.Networking;
|
||||
|
||||
partial class CustomInterface
|
||||
{
|
||||
}
|
||||
|
||||
partial struct Signal
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -1,391 +0,0 @@
|
||||
using MoonSharp.Interpreter;
|
||||
using MoonSharp.Interpreter.Interop;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
partial class LuaUserData
|
||||
{
|
||||
public static ReadOnlyDictionary<string, IUserDataDescriptor> Descriptors => new ReadOnlyDictionary<string, IUserDataDescriptor>(descriptors);
|
||||
private static ConcurrentDictionary<string, IUserDataDescriptor> descriptors = new ConcurrentDictionary<string, IUserDataDescriptor>();
|
||||
|
||||
public IUserDataDescriptor this[string index]
|
||||
{
|
||||
get => Descriptors.GetValueOrDefault(index);
|
||||
}
|
||||
|
||||
public static Type GetType(string typeName) => LuaCsSetup.GetType(typeName);
|
||||
|
||||
public static IUserDataDescriptor RegisterType(string typeName)
|
||||
{
|
||||
Type type = GetType(typeName);
|
||||
|
||||
if (type == null)
|
||||
{
|
||||
throw new ScriptRuntimeException($"tried to register a type that doesn't exist: {typeName}.");
|
||||
}
|
||||
|
||||
var descriptor = UserData.RegisterType(type);
|
||||
descriptors.TryAdd(typeName, descriptor);
|
||||
|
||||
return descriptor;
|
||||
}
|
||||
|
||||
public static IUserDataDescriptor RegisterTypeBarotrauma(string typeName)
|
||||
{
|
||||
return RegisterType($"Barotrauma.{typeName}");
|
||||
}
|
||||
|
||||
public static void RegisterExtensionType(string typeName)
|
||||
{
|
||||
Type type = GetType(typeName);
|
||||
|
||||
if (type == null)
|
||||
{
|
||||
throw new ScriptRuntimeException($"tried to register a type that doesn't exist: {typeName}.");
|
||||
}
|
||||
|
||||
UserData.RegisterExtensionType(type);
|
||||
}
|
||||
|
||||
public static bool IsRegistered(string typeName)
|
||||
{
|
||||
Type type = GetType(typeName);
|
||||
|
||||
if (type == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return UserData.GetDescriptorForType(type, true) != null;
|
||||
}
|
||||
|
||||
public static void UnregisterType(string typeName, bool deleteHistory = false)
|
||||
{
|
||||
Type type = GetType(typeName);
|
||||
|
||||
if (type == null)
|
||||
{
|
||||
throw new ScriptRuntimeException($"tried to unregister a type that doesn't exist: {typeName}.");
|
||||
}
|
||||
|
||||
UserData.UnregisterType(type, deleteHistory);
|
||||
}
|
||||
public static IUserDataDescriptor RegisterGenericType(string typeName, params string[] typeNameArguements)
|
||||
{
|
||||
Type type = GetType(typeName);
|
||||
Type[] typeArguements = typeNameArguements.Select(x => GetType(x)).ToArray();
|
||||
Type genericType = type.MakeGenericType(typeArguements);
|
||||
return UserData.RegisterType(genericType);
|
||||
}
|
||||
|
||||
public static void UnregisterGenericType(string typeName, params string[] typeNameArguements)
|
||||
{
|
||||
Type type = GetType(typeName);
|
||||
Type[] typeArguements = typeNameArguements.Select(x => GetType(x)).ToArray();
|
||||
Type genericType = type.MakeGenericType(typeArguements);
|
||||
UserData.UnregisterType(genericType);
|
||||
}
|
||||
|
||||
public static bool IsTargetType(object obj, string typeName)
|
||||
{
|
||||
if (obj == null) { throw new ScriptRuntimeException("userdata is nil"); }
|
||||
Type targetType = GetType(typeName);
|
||||
if (targetType == null) { throw new ScriptRuntimeException("target type not found"); }
|
||||
|
||||
Type type = obj is Type ? (Type)obj : obj.GetType();
|
||||
return targetType.IsAssignableFrom(type);
|
||||
}
|
||||
|
||||
public static string TypeOf(object obj)
|
||||
{
|
||||
if (obj == null) { throw new ScriptRuntimeException("userdata is nil"); }
|
||||
|
||||
return obj.GetType().FullName;
|
||||
}
|
||||
|
||||
public static object CreateStatic(string typeName)
|
||||
{
|
||||
Type type = GetType(typeName);
|
||||
|
||||
if (type == null)
|
||||
{
|
||||
throw new ScriptRuntimeException($"tried to create a static userdata of a type that doesn't exist: {typeName}.");
|
||||
}
|
||||
|
||||
MethodInfo method = typeof(UserData).GetMethod(nameof(UserData.CreateStatic), 1, new Type[0]);
|
||||
MethodInfo generic = method.MakeGenericMethod(type);
|
||||
var result = generic.Invoke(null, null);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public static object CreateEnumTable(string typeName)
|
||||
{
|
||||
Type type = GetType(typeName);
|
||||
|
||||
if (type == null)
|
||||
{
|
||||
throw new ScriptRuntimeException($"tried to create an enum table with a type that doesn't exist:: {typeName}.");
|
||||
}
|
||||
|
||||
Dictionary<string, object> result = new Dictionary<string, object>();
|
||||
|
||||
foreach (var value in Enum.GetValues(type))
|
||||
{
|
||||
string name = Enum.GetName(type, value);
|
||||
|
||||
result[name] = value;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static FieldInfo FindFieldRecursively(Type type, string fieldName)
|
||||
{
|
||||
var field = type.GetField(fieldName, BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static);
|
||||
|
||||
if (field == null && type.BaseType != null)
|
||||
{
|
||||
return FindFieldRecursively(type.BaseType, fieldName);
|
||||
}
|
||||
|
||||
return field;
|
||||
}
|
||||
|
||||
public static void MakeFieldAccessible(IUserDataDescriptor IUUD, string fieldName)
|
||||
{
|
||||
if (IUUD == null)
|
||||
{
|
||||
throw new ScriptRuntimeException($"tried to use a UserDataDescriptor that is null to make {fieldName} accessible.");
|
||||
}
|
||||
|
||||
var descriptor = (StandardUserDataDescriptor)IUUD;
|
||||
FieldInfo field = FindFieldRecursively(IUUD.Type, fieldName);
|
||||
|
||||
if (field == null)
|
||||
{
|
||||
throw new ScriptRuntimeException($"tried to make field '{fieldName}' accessible, but the field doesn't exist.");
|
||||
}
|
||||
|
||||
descriptor.RemoveMember(fieldName);
|
||||
descriptor.AddMember(fieldName, new FieldMemberDescriptor(field, InteropAccessMode.Default));
|
||||
}
|
||||
|
||||
private static MethodInfo FindMethodRecursively(Type type, string methodName, Type[] types = null)
|
||||
{
|
||||
MethodInfo method;
|
||||
|
||||
if (types == null)
|
||||
{
|
||||
method = type.GetMethod(methodName, BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static);
|
||||
}
|
||||
else
|
||||
{
|
||||
method = type.GetMethod(methodName, BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static, types);
|
||||
}
|
||||
|
||||
if (method == null && type.BaseType != null)
|
||||
{
|
||||
return FindMethodRecursively(type.BaseType, methodName, types);
|
||||
}
|
||||
|
||||
return method;
|
||||
}
|
||||
|
||||
public static void MakeMethodAccessible(IUserDataDescriptor IUUD, string methodName, string[] parameters = null)
|
||||
{
|
||||
if (IUUD == null)
|
||||
{
|
||||
throw new ScriptRuntimeException($"tried to use a UserDataDescriptor that is null to make {methodName} accessible.");
|
||||
}
|
||||
|
||||
Type[] parameterTypes = null;
|
||||
|
||||
|
||||
if (parameters != null)
|
||||
{
|
||||
parameterTypes = new Type[parameters.Length];
|
||||
|
||||
for (int i = 0; i < parameters.Length; i++)
|
||||
{
|
||||
Type type = LuaUserData.GetType(parameters[i]);
|
||||
if (type == null)
|
||||
{
|
||||
throw new ScriptRuntimeException($"invalid parameter type '{parameters[i]}'");
|
||||
}
|
||||
parameterTypes[i] = type;
|
||||
}
|
||||
}
|
||||
|
||||
var descriptor = (StandardUserDataDescriptor)IUUD;
|
||||
|
||||
MethodBase method;
|
||||
|
||||
try
|
||||
{
|
||||
method = FindMethodRecursively(IUUD.Type, methodName, parameterTypes);
|
||||
}
|
||||
catch (AmbiguousMatchException ex)
|
||||
{
|
||||
throw new ScriptRuntimeException("ambiguous method signature.");
|
||||
}
|
||||
|
||||
if (method == null)
|
||||
{
|
||||
throw new ScriptRuntimeException($"tried to make method '{methodName}' accessible, but the method doesn't exist.");
|
||||
}
|
||||
|
||||
descriptor.AddMember(methodName, new MethodMemberDescriptor(method, InteropAccessMode.Default));
|
||||
}
|
||||
|
||||
private static PropertyInfo FindPropertyRecursively(Type type, string propertyName)
|
||||
{
|
||||
var property = type.GetProperty(propertyName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static);
|
||||
|
||||
if (property == null && type.BaseType != null)
|
||||
{
|
||||
return FindPropertyRecursively(type.BaseType, propertyName);
|
||||
}
|
||||
|
||||
return property;
|
||||
}
|
||||
|
||||
public static void MakePropertyAccessible(IUserDataDescriptor IUUD, string propertyName)
|
||||
{
|
||||
if (IUUD == null)
|
||||
{
|
||||
throw new ScriptRuntimeException($"tried to use a UserDataDescriptor that is null to make {propertyName} accessible.");
|
||||
}
|
||||
|
||||
var descriptor = (StandardUserDataDescriptor)IUUD;
|
||||
PropertyInfo property = FindPropertyRecursively(IUUD.Type, propertyName);
|
||||
|
||||
if (property == null)
|
||||
{
|
||||
throw new ScriptRuntimeException($"tried to make property '{propertyName}' accessible, but the property doesn't exist.");
|
||||
}
|
||||
|
||||
descriptor.RemoveMember(propertyName);
|
||||
descriptor.AddMember(propertyName, new PropertyMemberDescriptor(property, InteropAccessMode.Default, property.GetGetMethod(true), property.GetSetMethod(true)));
|
||||
}
|
||||
|
||||
public static void AddMethod(IUserDataDescriptor IUUD, string methodName, object function)
|
||||
{
|
||||
if (IUUD == null)
|
||||
{
|
||||
throw new ScriptRuntimeException($"tried to use a UserDataDescriptor that is null to add method {methodName}.");
|
||||
}
|
||||
|
||||
var descriptor = (StandardUserDataDescriptor)IUUD;
|
||||
|
||||
descriptor.RemoveMember(methodName);
|
||||
descriptor.AddMember(methodName, new ObjectCallbackMemberDescriptor(methodName, (object arg1, ScriptExecutionContext arg2, CallbackArguments arg3) =>
|
||||
{
|
||||
if (GameMain.LuaCs != null)
|
||||
return GameMain.LuaCs.CallLuaFunction(function, arg3.GetArray());
|
||||
return null;
|
||||
}));
|
||||
}
|
||||
|
||||
public static void AddField(IUserDataDescriptor IUUD, string fieldName, DynValue value)
|
||||
{
|
||||
if (IUUD == null)
|
||||
{
|
||||
throw new ScriptRuntimeException($"tried to use a UserDataDescriptor that is null to add field {fieldName}.");
|
||||
}
|
||||
|
||||
var descriptor = (StandardUserDataDescriptor)IUUD;
|
||||
descriptor.RemoveMember(fieldName);
|
||||
descriptor.AddMember(fieldName, new DynValueMemberDescriptor(fieldName, value));
|
||||
}
|
||||
|
||||
public static void RemoveMember(IUserDataDescriptor IUUD, string memberName)
|
||||
{
|
||||
if (IUUD == null)
|
||||
{
|
||||
throw new ScriptRuntimeException($"tried to use a UserDataDescriptor that is null to remove the member {memberName}.");
|
||||
}
|
||||
|
||||
var descriptor = (StandardUserDataDescriptor)IUUD;
|
||||
descriptor.RemoveMember(memberName);
|
||||
}
|
||||
|
||||
public static bool HasMember(object obj, string memberName)
|
||||
{
|
||||
if (obj == null) { throw new ScriptRuntimeException("object is nil"); }
|
||||
|
||||
Type type;
|
||||
if (obj is Type)
|
||||
{
|
||||
type = (Type)obj;
|
||||
}
|
||||
else if(obj is IUserDataDescriptor descriptor)
|
||||
{
|
||||
type = descriptor.Type;
|
||||
|
||||
if (((StandardUserDataDescriptor)descriptor).HasMember(memberName))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
type = obj.GetType();
|
||||
}
|
||||
|
||||
if (type.GetMember(memberName).Length == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// See <see cref="CreateUserDataFromType"/>.
|
||||
/// </summary>
|
||||
/// <param name="scriptObject">Lua value to convert and wrap in a userdata.</param>
|
||||
/// <param name="desiredTypeDescriptor">Descriptor of the type of the object to convert the Lua value to. Uses MoonSharp ScriptToClr converters.</param>
|
||||
/// <returns>A userdata that wraps the Lua value converted to an object of the desired type as described by <paramref name="desiredTypeDescriptor"/>.</returns>
|
||||
public static DynValue CreateUserDataFromDescriptor(DynValue scriptObject, IUserDataDescriptor desiredTypeDescriptor)
|
||||
{
|
||||
return UserData.Create(scriptObject.ToObject(desiredTypeDescriptor.Type), desiredTypeDescriptor);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a Lua value to a CLR object of a desired type and wraps it in a userdata.
|
||||
/// If the type is not registered, then a new <see cref="MoonSharp.Interpreter.Interop.StandardUserDataDescriptor"/> will be created and used.
|
||||
/// The goal of this method is to allow Lua scripts to create userdata to wrap certain data without having to register types.
|
||||
/// <remarks>Wrapping the value in a userdata preserves the original type during script-to-CLR conversions.</remarks>
|
||||
/// <example>A Lua script needs to pass a List`1 to a CLR method expecting System.Object, MoonSharp gets
|
||||
/// in the way by converting the List`1 to a MoonSharp.Interpreter.Table and breaking everything.
|
||||
/// Registering the List`1 type can break other scripts relying on default converters, so instead
|
||||
/// it is better to manually wrap the List`1 object into a userdata.
|
||||
/// </example>
|
||||
/// </summary>
|
||||
/// <param name="scriptObject">Lua value to convert and wrap in a userdata.</param>
|
||||
/// <param name="desiredType">Type describing the CLR type of the object to convert the Lua value to.</param>
|
||||
/// <returns>A userdata that wraps the Lua value converted to an object of the desired type.</returns>
|
||||
public static DynValue CreateUserDataFromType(DynValue scriptObject, Type desiredType)
|
||||
{
|
||||
IUserDataDescriptor descriptor = UserData.GetDescriptorForType(desiredType, true);
|
||||
descriptor ??= new StandardUserDataDescriptor(desiredType, InteropAccessMode.Default);
|
||||
return CreateUserDataFromDescriptor(scriptObject, descriptor);
|
||||
}
|
||||
|
||||
public static void AddCallMetaTable(object userdata) { }
|
||||
|
||||
|
||||
public static void Clear()
|
||||
{
|
||||
descriptors.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.IO;
|
||||
using MoonSharp.Interpreter;
|
||||
using MoonSharp.Interpreter.Loaders;
|
||||
using System.Linq;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
class LuaScriptLoader : ScriptLoaderBase
|
||||
{
|
||||
|
||||
public override object LoadFile(string file, Table globalContext)
|
||||
{
|
||||
if (!LuaCsFile.IsPathAllowedLuaException(file, false)) return null;
|
||||
|
||||
return File.ReadAllText(file);
|
||||
}
|
||||
|
||||
public override bool ScriptFileExists(string file)
|
||||
{
|
||||
if (!LuaCsFile.IsPathAllowedLuaException(file, false)) return false;
|
||||
|
||||
return File.Exists(file);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,10 +9,19 @@ namespace Barotrauma
|
||||
{
|
||||
private static string[] trackingFiles = new string[]
|
||||
{
|
||||
"Barotrauma.dll", "Barotrauma.deps.json", "Barotrauma.pdb", "BarotraumaCore.dll", "BarotraumaCore.pdb",
|
||||
"0Harmony.dll", "Mono.Cecil.dll",
|
||||
/* Barotrauma */
|
||||
"Barotrauma.dll",
|
||||
"Barotrauma.deps.json",
|
||||
"Barotrauma.pdb",
|
||||
"BarotraumaCore.dll",
|
||||
"BarotraumaCore.pdb",
|
||||
|
||||
/* HarmonyX Package */
|
||||
"0Harmony.dll",
|
||||
"Mono.Cecil.dll",
|
||||
"Sigil.dll",
|
||||
"Mono.Cecil.Mdb.dll", "Mono.Cecil.Pdb.dll",
|
||||
"Mono.Cecil.Mdb.dll",
|
||||
"Mono.Cecil.Pdb.dll",
|
||||
"Mono.Cecil.Rocks.dll",
|
||||
"MonoMod.Backports.dll",
|
||||
"MonoMod.Core.dll",
|
||||
@@ -20,15 +29,32 @@ namespace Barotrauma
|
||||
"MonoMod.RuntimeDetour.dll",
|
||||
"MonoMod.Utils.dll",
|
||||
"MonoMod.Iced.dll",
|
||||
"MoonSharp.Interpreter.dll", "MoonSharp.VsCodeDebugger.dll",
|
||||
|
||||
/* MoonSharp */
|
||||
"MoonSharp.Interpreter.dll",
|
||||
"MoonSharp.VsCodeDebugger.dll",
|
||||
|
||||
"Microsoft.CodeAnalysis.dll", "Microsoft.CodeAnalysis.CSharp.dll",
|
||||
"Microsoft.CodeAnalysis.CSharp.Scripting.dll", "Microsoft.CodeAnalysis.Scripting.dll",
|
||||
|
||||
"System.Reflection.Metadata.dll", "System.Collections.Immutable.dll",
|
||||
/* Microsoft SDKs */
|
||||
"Microsoft.CodeAnalysis.dll",
|
||||
"Microsoft.CodeAnalysis.CSharp.dll",
|
||||
"Microsoft.CodeAnalysis.CSharp.Scripting.dll",
|
||||
"Microsoft.CodeAnalysis.Scripting.dll",
|
||||
"Microsoft.Toolkit.Diagnostics.dll",
|
||||
"Microsoft.Extensions.Logging.Abstractions.dll",
|
||||
"System.Reflection.Metadata.dll",
|
||||
"System.Collections.Immutable.dll",
|
||||
"System.Runtime.CompilerServices.Unsafe.dll",
|
||||
|
||||
"Publicized/DedicatedServer.dll", "Publicized/Barotrauma.dll"
|
||||
/* Assembly Script Dependencies */
|
||||
"Publicized/DedicatedServer.dll",
|
||||
"Publicized/Barotrauma.dll",
|
||||
"Publicized/BarotraumaCore.dll",
|
||||
|
||||
/* Other NuGet Packages */
|
||||
"Basic.Reference.Assemblies.Net80.dll",
|
||||
"FluentResults.dll",
|
||||
"LightInject.dll",
|
||||
"OneOf.dll"
|
||||
};
|
||||
|
||||
private static void CreateMissingDirectory()
|
||||
|
||||
@@ -1,181 +0,0 @@
|
||||
using System;
|
||||
using Barotrauma.Networking;
|
||||
using Microsoft.Xna.Framework;
|
||||
using MoonSharp.Interpreter;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
internal enum LuaCsMessageOrigin
|
||||
{
|
||||
LuaCs,
|
||||
Unknown,
|
||||
LuaMod,
|
||||
CSharpMod,
|
||||
}
|
||||
|
||||
partial class LuaCsLogger
|
||||
{
|
||||
public static bool HideUserNames = true;
|
||||
|
||||
#if SERVER
|
||||
private const string LogPrefix = "SV";
|
||||
private const int NetMaxLength = 1024;
|
||||
private const int NetMaxMessages = 60;
|
||||
|
||||
// This is used so its possible to call logging functions inside the serverLog
|
||||
// hook without creating an infinite loop
|
||||
private static bool lockLog = false;
|
||||
#else
|
||||
private const string LogPrefix = "CL";
|
||||
#endif
|
||||
|
||||
public static LuaCsMessageLogger MessageLogger;
|
||||
public static LuaCsExceptionHandler ExceptionHandler;
|
||||
|
||||
public static void HandleException(Exception ex, LuaCsMessageOrigin origin)
|
||||
{
|
||||
string errorString = "";
|
||||
switch (ex)
|
||||
{
|
||||
case NetRuntimeException netRuntimeException:
|
||||
if (netRuntimeException.DecoratedMessage == null)
|
||||
{
|
||||
errorString = netRuntimeException.ToString();
|
||||
}
|
||||
else
|
||||
{
|
||||
// FIXME: netRuntimeException.ToString() doesn't print the InnerException's stack trace...
|
||||
errorString = $"{netRuntimeException.DecoratedMessage}: {netRuntimeException}";
|
||||
}
|
||||
break;
|
||||
case InterpreterException interpreterException:
|
||||
if (interpreterException.DecoratedMessage == null)
|
||||
{
|
||||
errorString = interpreterException.ToString();
|
||||
}
|
||||
else
|
||||
{
|
||||
errorString = interpreterException.DecoratedMessage;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
errorString = ex.StackTrace != null
|
||||
? ex.ToString()
|
||||
: $"{ex}\n{Environment.StackTrace}";
|
||||
break;
|
||||
}
|
||||
|
||||
LogError(Environment.UserName + " " + errorString, origin);
|
||||
}
|
||||
|
||||
public static void LogError(string message, LuaCsMessageOrigin origin)
|
||||
{
|
||||
if (HideUserNames && !Environment.UserName.IsNullOrEmpty())
|
||||
{
|
||||
message = message.Replace(Environment.UserName, "USERNAME");
|
||||
}
|
||||
|
||||
switch (origin)
|
||||
{
|
||||
case LuaCsMessageOrigin.LuaCs:
|
||||
case LuaCsMessageOrigin.Unknown:
|
||||
LogError($"[{LogPrefix} ERROR] {message}");
|
||||
break;
|
||||
case LuaCsMessageOrigin.LuaMod:
|
||||
LogError($"[{LogPrefix} LUA ERROR] {message}");
|
||||
break;
|
||||
case LuaCsMessageOrigin.CSharpMod:
|
||||
LogError($"[{LogPrefix} CS ERROR] {message}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public static void LogError(string message)
|
||||
{
|
||||
Log($"{message}", Color.Red, ServerLog.MessageType.Error);
|
||||
}
|
||||
|
||||
public static void LogMessage(string message, Color? serverColor = null, Color? clientColor = null)
|
||||
{
|
||||
if (serverColor == null) { serverColor = Color.MediumPurple; }
|
||||
if (clientColor == null) { clientColor = Color.Purple; }
|
||||
|
||||
#if SERVER
|
||||
Log(message, serverColor);
|
||||
#else
|
||||
Log(message, clientColor);
|
||||
#endif
|
||||
}
|
||||
|
||||
public static void Log(string message, Color? color = null, ServerLog.MessageType messageType = ServerLog.MessageType.ServerMessage)
|
||||
{
|
||||
MessageLogger?.Invoke(message);
|
||||
|
||||
DebugConsole.NewMessage(message, color);
|
||||
|
||||
#if SERVER
|
||||
void broadcastMessage(string m)
|
||||
{
|
||||
foreach (var client in GameMain.Server.ConnectedClients)
|
||||
{
|
||||
//if (client.ChatMsgQueue.Count > NetMaxMessages)
|
||||
//{
|
||||
// If there's an error or message happening many times per second (inside Update loop for example)
|
||||
// we will need to discart some messages so the client doesn't get overloaded by all
|
||||
// those net messages.
|
||||
// continue;
|
||||
//}
|
||||
|
||||
ChatMessage consoleMessage = ChatMessage.Create("", m, ChatMessageType.Console, null, textColor: color);
|
||||
GameMain.Server.SendDirectChatMessage(consoleMessage, client);
|
||||
|
||||
if (!GameMain.Server.ServerSettings.SaveServerLogs || !client.HasPermission(ClientPermissions.ServerLog))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
ChatMessage logMessage = ChatMessage.Create(messageType.ToString(), "[LuaCs] " + m, ChatMessageType.ServerLog, null);
|
||||
GameMain.Server.SendDirectChatMessage(logMessage, client);
|
||||
}
|
||||
}
|
||||
|
||||
if (GameMain.Server != null)
|
||||
{
|
||||
if (GameMain.Server.ServerSettings.SaveServerLogs)
|
||||
{
|
||||
string logMessage = "[LuaCs] " + message;
|
||||
GameMain.Server.ServerSettings.ServerLog.WriteLine(logMessage, messageType, false);
|
||||
|
||||
if (!lockLog)
|
||||
{
|
||||
lockLog = true;
|
||||
GameMain.LuaCs?.Hook?.Call("serverLog", logMessage, messageType);
|
||||
lockLog = false;
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < message.Length; i += NetMaxLength)
|
||||
{
|
||||
string subStr = message.Substring(i, Math.Min(1024, message.Length - i));
|
||||
|
||||
broadcastMessage(subStr);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
partial class LuaCsSetup
|
||||
{
|
||||
// Compatibility with cs mods that use this method.
|
||||
public static void PrintLuaError(object message) => LuaCsLogger.LogError($"{message}", LuaCsMessageOrigin.LuaMod);
|
||||
public static void PrintCsError(object message) => LuaCsLogger.LogError($"{message}", LuaCsMessageOrigin.CSharpMod);
|
||||
public static void PrintGenericError(object message) => LuaCsLogger.LogError($"{message}", LuaCsMessageOrigin.LuaCs);
|
||||
|
||||
internal void PrintMessage(object message) => LuaCsLogger.LogMessage($"{message}");
|
||||
|
||||
public static void PrintCsMessage(object message) => LuaCsLogger.LogMessage($"{message}");
|
||||
|
||||
internal void HandleException(Exception ex, LuaCsMessageOrigin origin) => LuaCsLogger.HandleException(ex, origin);
|
||||
}
|
||||
}
|
||||
@@ -1,114 +0,0 @@
|
||||
using MoonSharp.Interpreter;
|
||||
using MoonSharp.Interpreter.Interop;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
partial class LuaCsSetup
|
||||
{
|
||||
public class LuaCsModStore
|
||||
{
|
||||
public abstract class ModStore<T, TStore>
|
||||
{
|
||||
protected Dictionary<string, TStore> store;
|
||||
|
||||
public TStore Set(string name, TStore value) => store[name] = value;
|
||||
public TStore Get(string name) => store[name];
|
||||
|
||||
public ModStore(Dictionary<string, TStore> store) => this.store = store;
|
||||
|
||||
public abstract bool Equals(T value);
|
||||
}
|
||||
public class LuaModStore : ModStore<string, DynValue>
|
||||
{
|
||||
public string Name;
|
||||
|
||||
public LuaModStore(Dictionary<string, DynValue> store) : base(store) { }
|
||||
public override bool Equals(string value) => Name == value;
|
||||
}
|
||||
public class CsModStore : ModStore<ACsMod, object>
|
||||
{
|
||||
public ACsMod Mod;
|
||||
|
||||
public CsModStore(Dictionary<string, object> store) : base(store) { }
|
||||
public override bool Equals(ACsMod value) => Mod == value;
|
||||
}
|
||||
|
||||
private HashSet<LuaModStore> luaModInterface;
|
||||
private HashSet<CsModStore> csModInterface;
|
||||
|
||||
public LuaCsModStore()
|
||||
{
|
||||
luaModInterface = new HashSet<LuaModStore>();
|
||||
csModInterface = new HashSet<CsModStore>();
|
||||
}
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
UserData.RegisterType<LuaModStore>();
|
||||
UserData.RegisterType<CsModStore>();
|
||||
var msType = UserData.RegisterType<LuaCsModStore>();
|
||||
var msDesc = (StandardUserDataDescriptor)msType;
|
||||
|
||||
typeof(StandardUserDataDescriptor).GetMethods(BindingFlags.NonPublic | BindingFlags.Instance).ToList().ForEach(m =>
|
||||
{
|
||||
if (
|
||||
m.Name.Contains("Register")
|
||||
)
|
||||
{
|
||||
msDesc.AddMember(m.Name, new MethodMemberDescriptor(m, InteropAccessMode.Default));
|
||||
}
|
||||
});
|
||||
}
|
||||
public void Clear()
|
||||
{
|
||||
luaModInterface.Clear();
|
||||
csModInterface.Clear();
|
||||
}
|
||||
|
||||
protected LuaModStore Register(string modName)
|
||||
{
|
||||
if (luaModInterface.Any(i => i.Equals(modName)))
|
||||
{
|
||||
LuaCsLogger.HandleException(new ArgumentException($"'{modName}' entry already registered"), LuaCsMessageOrigin.LuaMod);
|
||||
return null;
|
||||
}
|
||||
|
||||
var newHandle = new LuaModStore(new Dictionary<string, DynValue>());
|
||||
if (luaModInterface.Add(newHandle)) return newHandle;
|
||||
else return null;
|
||||
}
|
||||
[MoonSharpHidden]
|
||||
public CsModStore Register(ACsMod mod)
|
||||
{
|
||||
if (csModInterface.Any(i => i.Equals(mod)))
|
||||
{
|
||||
LuaCsLogger.HandleException(new ArgumentException($"'{mod.GetType().FullName}' entry already registered"), LuaCsMessageOrigin.CSharpMod);
|
||||
return null;
|
||||
}
|
||||
|
||||
var newHandle = new CsModStore(new Dictionary<string, object>());
|
||||
if (csModInterface.Add(newHandle)) return newHandle;
|
||||
else return null;
|
||||
}
|
||||
|
||||
public CsModStore GetCsStore(string modName) {
|
||||
var result = csModInterface.Where(i => i.Mod.GetType().FullName == modName).FirstOrDefault();
|
||||
if (result != null)
|
||||
{
|
||||
if (!result.Mod.IsDisposed) return result;
|
||||
else
|
||||
{
|
||||
csModInterface.Remove(result);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
else return null;
|
||||
}
|
||||
protected LuaModStore GetLuaStore(string modName) => luaModInterface.Where(i => i.Name == modName).FirstOrDefault();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,173 +0,0 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Collections.Generic;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using Barotrauma.Networking;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
partial class LuaCsNetworking
|
||||
{
|
||||
private static readonly HttpClient client = new HttpClient();
|
||||
|
||||
private enum LuaCsClientToServer
|
||||
{
|
||||
NetMessageId,
|
||||
NetMessageString,
|
||||
RequestSingleId,
|
||||
RequestAllIds,
|
||||
}
|
||||
|
||||
private enum LuaCsServerToClient
|
||||
{
|
||||
NetMessageId,
|
||||
NetMessageString,
|
||||
ReceiveIds
|
||||
}
|
||||
|
||||
public bool RestrictMessageSize = true;
|
||||
|
||||
private Dictionary<string, LuaCsAction> netReceives = new Dictionary<string, LuaCsAction>();
|
||||
private Dictionary<ushort, string> idToString = new Dictionary<ushort, string>();
|
||||
private Dictionary<string, ushort> stringToId = new Dictionary<string, ushort>();
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
#if CLIENT
|
||||
SendSyncMessage();
|
||||
#endif
|
||||
}
|
||||
|
||||
public void Remove(string netMessageName)
|
||||
{
|
||||
netReceives.Remove(netMessageName);
|
||||
}
|
||||
|
||||
public IWriteMessage Start()
|
||||
{
|
||||
return new WriteOnlyMessage();
|
||||
}
|
||||
|
||||
public string IdToString(ushort id)
|
||||
{
|
||||
if (idToString.ContainsKey(id)) { return idToString[id]; }
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public ushort StringToId(string name)
|
||||
{
|
||||
if (stringToId.ContainsKey(name)) { return stringToId[name]; }
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
private void HandleNetMessage(IReadMessage netMessage, string name, Client client = null)
|
||||
{
|
||||
if (netReceives.ContainsKey(name))
|
||||
{
|
||||
try
|
||||
{
|
||||
netReceives[name](netMessage, client);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
LuaCsLogger.LogError($"Exception thrown inside NetMessageReceive({name})", LuaCsMessageOrigin.CSharpMod);
|
||||
LuaCsLogger.HandleException(e, LuaCsMessageOrigin.CSharpMod);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (GameSettings.CurrentConfig.VerboseLogging)
|
||||
{
|
||||
#if SERVER
|
||||
LuaCsLogger.LogError($"Received NetMessage for unknown name {name} from {GameServer.ClientLogName(client)}.");
|
||||
#else
|
||||
LuaCsLogger.LogError($"Received NetMessage for unknown name {name} from server.");
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleNetMessageString(IReadMessage netMessage, Client client = null)
|
||||
{
|
||||
string name = netMessage.ReadString();
|
||||
|
||||
HandleNetMessage(netMessage, name, client);
|
||||
}
|
||||
|
||||
public async void HttpRequest(string url, LuaCsAction callback, string data = null, string method = "POST", string contentType = "application/json", Dictionary<string, string> headers = null, string savePath = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
HttpRequestMessage request = new HttpRequestMessage(new HttpMethod(method), url);
|
||||
|
||||
if (headers != null)
|
||||
{
|
||||
foreach (var header in headers)
|
||||
{
|
||||
request.Headers.Add(header.Key, header.Value);
|
||||
}
|
||||
}
|
||||
|
||||
if (data != null)
|
||||
{
|
||||
request.Content = new StringContent(data, Encoding.UTF8, contentType);
|
||||
}
|
||||
|
||||
HttpResponseMessage response = await client.SendAsync(request);
|
||||
|
||||
if (savePath != null)
|
||||
{
|
||||
if (LuaCsFile.IsPathAllowedException(savePath))
|
||||
{
|
||||
byte[] responseData = await response.Content.ReadAsByteArrayAsync();
|
||||
|
||||
using (var fileStream = new FileStream(savePath, FileMode.Create, FileAccess.Write))
|
||||
{
|
||||
fileStream.Write(responseData, 0, responseData.Length);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
string responseBody = await response.Content.ReadAsStringAsync();
|
||||
|
||||
GameMain.LuaCs.Timer.Wait((object[] par) =>
|
||||
{
|
||||
callback(responseBody, (int)response.StatusCode, response.Headers);
|
||||
}, 0);
|
||||
}
|
||||
catch (HttpRequestException e)
|
||||
{
|
||||
GameMain.LuaCs.Timer.Wait((object[] par) => { callback(e.Message, e.StatusCode, null); }, 0);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
GameMain.LuaCs.Timer.Wait((object[] par) => { callback(e.Message, null, null); }, 0);
|
||||
}
|
||||
}
|
||||
|
||||
public void HttpPost(string url, LuaCsAction callback, string data, string contentType = "application/json", Dictionary<string, string> headers = null, string savePath = null)
|
||||
{
|
||||
HttpRequest(url, callback, data, "POST", contentType, headers, savePath);
|
||||
}
|
||||
|
||||
|
||||
public void HttpGet(string url, LuaCsAction callback, Dictionary<string, string> headers = null, string savePath = null)
|
||||
{
|
||||
HttpRequest(url, callback, null, "GET", null, headers, savePath);
|
||||
}
|
||||
|
||||
public void CreateEntityEvent(INetSerializable entity, NetEntityEvent.IData extraData)
|
||||
{
|
||||
GameMain.NetworkMember.CreateEntityEvent(entity, extraData);
|
||||
}
|
||||
|
||||
public ushort LastClientListUpdateID
|
||||
{
|
||||
get { return GameMain.NetworkMember.LastClientListUpdateID; }
|
||||
set { GameMain.NetworkMember.LastClientListUpdateID = value; }
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,538 +0,0 @@
|
||||
using Barotrauma.Items.Components;
|
||||
using Barotrauma.Networking;
|
||||
using MoonSharp.Interpreter;
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Reflection;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
partial class LuaCsFile
|
||||
{
|
||||
public static bool CanReadFromPath(string path)
|
||||
{
|
||||
string getFullPath(string p) => System.IO.Path.GetFullPath(p).CleanUpPath();
|
||||
|
||||
path = getFullPath(path);
|
||||
|
||||
bool pathStartsWith(string prefix) => path.StartsWith(prefix, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
string localModsDir = getFullPath(ContentPackage.LocalModsDir);
|
||||
string workshopModsDir = getFullPath(ContentPackage.WorkshopModsDir);
|
||||
#if CLIENT
|
||||
string tempDownloadDir = getFullPath(ModReceiver.DownloadFolder);
|
||||
#endif
|
||||
if (pathStartsWith(getFullPath(string.IsNullOrEmpty(GameSettings.CurrentConfig.SavePath) ? SaveUtil.DefaultSaveFolder : GameSettings.CurrentConfig.SavePath)))
|
||||
return true;
|
||||
|
||||
if (pathStartsWith(localModsDir))
|
||||
return true;
|
||||
|
||||
if (pathStartsWith(workshopModsDir))
|
||||
return true;
|
||||
|
||||
#if CLIENT
|
||||
if (pathStartsWith(tempDownloadDir))
|
||||
return true;
|
||||
#endif
|
||||
|
||||
if (pathStartsWith(getFullPath(".")))
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static bool CanWriteToPath(string path)
|
||||
{
|
||||
string getFullPath(string p) => System.IO.Path.GetFullPath(p).CleanUpPath();
|
||||
|
||||
path = getFullPath(path);
|
||||
|
||||
bool pathStartsWith(string prefix) => path.StartsWith(prefix, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
foreach (var package in ContentPackageManager.AllPackages)
|
||||
{
|
||||
if (package.UgcId.ValueEquals(LuaCsSetup.LuaForBarotraumaId) && pathStartsWith(getFullPath(package.Path)))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (pathStartsWith(getFullPath(string.IsNullOrEmpty(GameSettings.CurrentConfig.SavePath) ? SaveUtil.DefaultSaveFolder : GameSettings.CurrentConfig.SavePath)))
|
||||
return true;
|
||||
|
||||
if (pathStartsWith(getFullPath(ContentPackage.LocalModsDir)))
|
||||
return true;
|
||||
|
||||
if (pathStartsWith(getFullPath(ContentPackage.WorkshopModsDir)))
|
||||
return true;
|
||||
#if CLIENT
|
||||
if (pathStartsWith(getFullPath(ModReceiver.DownloadFolder)))
|
||||
return true;
|
||||
#endif
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static bool IsPathAllowedException(string path, bool write = true, LuaCsMessageOrigin origin = LuaCsMessageOrigin.Unknown)
|
||||
{
|
||||
if (write)
|
||||
{
|
||||
if (CanWriteToPath(path))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new Exception("File access to \"" + path + "\" not allowed.");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (CanReadFromPath(path))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new Exception("File access to \"" + path + "\" not allowed.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static bool IsPathAllowedLuaException(string path, bool write = true) =>
|
||||
IsPathAllowedException(path, write, LuaCsMessageOrigin.LuaMod);
|
||||
public static bool IsPathAllowedCsException(string path, bool write = true) =>
|
||||
IsPathAllowedException(path, write, LuaCsMessageOrigin.CSharpMod);
|
||||
|
||||
public static string Read(string path)
|
||||
{
|
||||
if (!IsPathAllowedException(path, false))
|
||||
return "";
|
||||
|
||||
return File.ReadAllText(path);
|
||||
}
|
||||
|
||||
public static void Write(string path, string text)
|
||||
{
|
||||
if (!IsPathAllowedException(path))
|
||||
return;
|
||||
|
||||
File.WriteAllText(path, text);
|
||||
}
|
||||
|
||||
public static void Delete(string path)
|
||||
{
|
||||
if (!IsPathAllowedException(path))
|
||||
return;
|
||||
|
||||
File.Delete(path);
|
||||
}
|
||||
|
||||
public static void DeleteDirectory(string path)
|
||||
{
|
||||
if (!IsPathAllowedException(path))
|
||||
return;
|
||||
|
||||
Directory.Delete(path, true);
|
||||
}
|
||||
|
||||
public static void Move(string path, string destination)
|
||||
{
|
||||
if (!IsPathAllowedException(path))
|
||||
return;
|
||||
|
||||
if (!IsPathAllowedException(destination))
|
||||
return;
|
||||
|
||||
File.Move(path, destination, true);
|
||||
}
|
||||
|
||||
public static FileStream OpenRead(string path)
|
||||
{
|
||||
if (!IsPathAllowedException(path))
|
||||
return null;
|
||||
|
||||
return File.Open(path, FileMode.Open, FileAccess.Read);
|
||||
}
|
||||
public static FileStream OpenWrite(string path)
|
||||
{
|
||||
if (!IsPathAllowedException(path))
|
||||
return null;
|
||||
|
||||
if (File.Exists(path)) return File.Open(path, FileMode.Truncate, FileAccess.Write);
|
||||
else return File.Open(path, FileMode.Create, FileAccess.Write);
|
||||
}
|
||||
|
||||
public static bool Exists(string path)
|
||||
{
|
||||
if (!IsPathAllowedException(path, false))
|
||||
return false;
|
||||
|
||||
return File.Exists(path);
|
||||
}
|
||||
|
||||
public static bool CreateDirectory(string path)
|
||||
{
|
||||
if (!IsPathAllowedException(path))
|
||||
return false;
|
||||
|
||||
Directory.CreateDirectory(path);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool DirectoryExists(string path)
|
||||
{
|
||||
if (!IsPathAllowedException(path, false))
|
||||
return false;
|
||||
|
||||
return Directory.Exists(path);
|
||||
}
|
||||
|
||||
public static string[] GetFiles(string path)
|
||||
{
|
||||
if (!IsPathAllowedException(path, false))
|
||||
return null;
|
||||
|
||||
return Directory.GetFiles(path);
|
||||
}
|
||||
|
||||
public static string[] GetDirectories(string path)
|
||||
{
|
||||
if (!IsPathAllowedException(path, false))
|
||||
return new string[] { };
|
||||
|
||||
return Directory.GetDirectories(path);
|
||||
}
|
||||
|
||||
public static string[] DirSearch(string sDir)
|
||||
{
|
||||
if (!IsPathAllowedException(sDir, false))
|
||||
return new string[] { };
|
||||
|
||||
List<string> files = new List<string>();
|
||||
|
||||
try
|
||||
{
|
||||
foreach (string f in Directory.GetFiles(sDir))
|
||||
{
|
||||
files.Add(f);
|
||||
}
|
||||
|
||||
foreach (string d in Directory.GetDirectories(sDir))
|
||||
{
|
||||
foreach (string f in Directory.GetFiles(d))
|
||||
{
|
||||
files.Add(f);
|
||||
}
|
||||
DirSearch(d);
|
||||
}
|
||||
}
|
||||
catch (System.Exception excpt)
|
||||
{
|
||||
Console.WriteLine(excpt.Message);
|
||||
}
|
||||
|
||||
return files.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class LuaCsConfig
|
||||
{
|
||||
private enum ValueType
|
||||
{
|
||||
None,
|
||||
Text,
|
||||
Integer,
|
||||
Decimal,
|
||||
Boolean,
|
||||
Collection,
|
||||
Object,
|
||||
Enum
|
||||
}
|
||||
|
||||
private static Type[] LoadDocTypes(XElement typesElem)
|
||||
{
|
||||
var result = new List<Type>();
|
||||
var loadedTypes = LuaCsSetup.AssemblyManager
|
||||
.GetAllTypesInLoadedAssemblies()
|
||||
.ToImmutableHashSet();
|
||||
|
||||
foreach (var elem in typesElem.Elements())
|
||||
{
|
||||
var typesFound = loadedTypes.Where(t => t.FullName?.EndsWith(elem.Value) ?? false).ToImmutableList();
|
||||
if (!typesFound.Any())
|
||||
{
|
||||
ModUtils.Logging.PrintError(
|
||||
$"{nameof(LuaCsConfig)}::{nameof(LoadDocTypes)}() | Unable to find a matching type for {elem.Value}");
|
||||
continue;
|
||||
}
|
||||
result.AddRange(typesFound);
|
||||
}
|
||||
|
||||
return result.ToArray();
|
||||
}
|
||||
|
||||
private static IEnumerable<XElement> SaveDocTypes(IEnumerable<Type> types)
|
||||
{
|
||||
return types.Select(t => new XElement("Type", t.ToString()));
|
||||
}
|
||||
|
||||
private static Type GetTypeAttr(Type[] types, XElement elem)
|
||||
{
|
||||
var idx = elem.GetAttributeInt("Type", -1);
|
||||
if (idx < 0 || idx >= types.Length) throw new Exception($"Type index '{idx}' is outside of saved types bounds");
|
||||
return types[idx];
|
||||
}
|
||||
private static ValueType GetValueType(XElement elem)
|
||||
{
|
||||
Enum.TryParse(typeof(ValueType), elem.Attribute("Value")?.Value, out object result);
|
||||
if (result != null) return (ValueType)result;
|
||||
else return ValueType.None;
|
||||
}
|
||||
private static object ParseValue(Type[] types, XElement elem)
|
||||
{
|
||||
var type = GetValueType(elem);
|
||||
|
||||
if (elem.IsEmpty) return null;
|
||||
if (type == ValueType.Enum)
|
||||
{
|
||||
var tType = GetTypeAttr(types, elem);
|
||||
if (tType == null || !tType.IsSubclassOf(typeof(Enum))) return null;
|
||||
if (Enum.TryParse(tType, elem.Value, out object result)) return result;
|
||||
else return null;
|
||||
}
|
||||
if (type == ValueType.Collection)
|
||||
{
|
||||
var tType = GetTypeAttr(types, elem);
|
||||
var tInt = tType.GetInterfaces().FirstOrDefault(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IEnumerable<>));
|
||||
var gArg = tInt.GetGenericArguments()[0];
|
||||
if (tType == null || !tType.GetInterfaces().Any(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IEnumerable<>))) return null;
|
||||
|
||||
object result = null;
|
||||
|
||||
if (result == null) {
|
||||
var ctor = tType.GetConstructors(BindingFlags.Public | BindingFlags.Instance).FirstOrDefault(c =>
|
||||
{
|
||||
var param = c.GetParameters();
|
||||
return param.Count() == 1 && param.Any(p => p.ParameterType.IsGenericType && p.ParameterType.GetGenericTypeDefinition() == typeof(IEnumerable<>));
|
||||
});
|
||||
if (ctor != null)
|
||||
{
|
||||
var elements = elem.Elements().Select(x => ParseValue(types, x));
|
||||
var castElems = typeof(Enumerable).GetMethod("Cast").MakeGenericMethod(gArg).Invoke(elements, new object[] { elements });
|
||||
result = ctor.Invoke(new object[] { castElems });
|
||||
}
|
||||
}
|
||||
|
||||
if (result == null)
|
||||
{
|
||||
var ctor = tType.GetConstructors(BindingFlags.Public | BindingFlags.Instance).FirstOrDefault(c => c.GetParameters().Count() == 0);
|
||||
var addMethod = tType.GetMethods(BindingFlags.Instance | BindingFlags.Public).FirstOrDefault(m =>
|
||||
{
|
||||
if (m.Name != "Add") return false;
|
||||
var param = m.GetParameters();
|
||||
return param.Count() == 1 && param[0].ParameterType == gArg;
|
||||
});
|
||||
if (ctor != null && addMethod != null)
|
||||
{
|
||||
var elements = elem.Elements().Select(x => ParseValue(types, x));
|
||||
result = ctor.Invoke(null);
|
||||
foreach (var el in elements) addMethod.Invoke(result, new object[] { el });
|
||||
}
|
||||
}
|
||||
|
||||
if (result == null)
|
||||
{
|
||||
var ctor = tType.GetConstructors(BindingFlags.Public | BindingFlags.Instance).FirstOrDefault();
|
||||
var setMethod = tType.GetMethods(BindingFlags.Instance | BindingFlags.Public).FirstOrDefault(m =>
|
||||
{
|
||||
if (m.Name != "Set") return false;
|
||||
var param = m.GetParameters();
|
||||
return param.Count() == 2 && param[0].ParameterType == typeof(int) && param[1].ParameterType == gArg;
|
||||
});
|
||||
if (ctor != null || setMethod != null)
|
||||
{
|
||||
var elements = elem.Elements().Select(x => ParseValue(types, x));
|
||||
result = ctor.Invoke(new object[] { elements.Count() });
|
||||
int i = 0;
|
||||
foreach (var el in elements)
|
||||
{
|
||||
setMethod.Invoke(result, new object[] { i, el });
|
||||
i++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
else if (type == ValueType.Text) return elem.Value;
|
||||
else if (type == ValueType.Integer)
|
||||
{
|
||||
int.TryParse(elem.Value, out var num);
|
||||
return num;
|
||||
}
|
||||
else if (type == ValueType.Decimal)
|
||||
{
|
||||
float.TryParse(elem.Value, out var num);
|
||||
return num;
|
||||
}
|
||||
else if (type == ValueType.Boolean)
|
||||
{
|
||||
bool.TryParse(elem.Value, out var boolean);
|
||||
return boolean;
|
||||
}
|
||||
else if (type == ValueType.Object)
|
||||
{
|
||||
var tType = GetTypeAttr(types, elem);
|
||||
if (tType == null) return null;
|
||||
|
||||
IEnumerable<FieldInfo> fields = tType.GetFields(BindingFlags.Instance | BindingFlags.Public)
|
||||
.Concat(tType.GetFields(BindingFlags.Instance | BindingFlags.NonPublic));
|
||||
IEnumerable<PropertyInfo> properties = tType.GetProperties(BindingFlags.Instance | BindingFlags.Public).Where(p => p.GetSetMethod() != null)
|
||||
.Concat(tType.GetProperties(BindingFlags.Instance | BindingFlags.NonPublic).Where(p => p.GetSetMethod() != null));
|
||||
|
||||
object result = null;
|
||||
var ctor = tType.GetConstructors(BindingFlags.Public | BindingFlags.Instance).FirstOrDefault(c => c.GetParameters().Count() == 0);
|
||||
if (ctor == null)
|
||||
{
|
||||
if (!tType.IsValueType) return null;
|
||||
result = Activator.CreateInstance(tType);
|
||||
}
|
||||
else result = ctor.Invoke(null);
|
||||
|
||||
foreach(var el in elem.Elements())
|
||||
{
|
||||
var value = ParseValue(types, el);
|
||||
|
||||
var field = fields.FirstOrDefault(f => f.Name == el.Name.LocalName);
|
||||
if (field != null) field.SetValue(result, value);
|
||||
var property = properties.FirstOrDefault(p => p.Name == el.Name.LocalName);
|
||||
if (property != null) property.SetValue(result, value);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
else return elem.Value;
|
||||
|
||||
}
|
||||
|
||||
private static void AddTypeAttr(List<Type> types, Type type, XElement elem)
|
||||
{
|
||||
if (!types.Contains(type)) types.Add(type);
|
||||
elem.SetAttributeValue("Type", types.IndexOf(type));
|
||||
}
|
||||
|
||||
private static XElement ParseObject(List<Type> types, string name, object value)
|
||||
{
|
||||
XElement result = new XElement(name);
|
||||
|
||||
if (value != null)
|
||||
{
|
||||
var tType = value.GetType();
|
||||
|
||||
if (tType.IsEnum)
|
||||
{
|
||||
result.SetAttributeValue("Value", ValueType.Enum);
|
||||
AddTypeAttr(types, tType, result);
|
||||
|
||||
result.Value = Enum.GetName(tType, value) ?? "";
|
||||
}
|
||||
else if (value is string str)
|
||||
{
|
||||
result.SetAttributeValue("Value", ValueType.Text);
|
||||
result.Value = str;
|
||||
}
|
||||
else if (value is int integer)
|
||||
{
|
||||
result.SetAttributeValue("Value", ValueType.Integer);
|
||||
result.Value = integer.ToString();
|
||||
}
|
||||
else if (value is float || value is double)
|
||||
{
|
||||
result.SetAttributeValue("Value", ValueType.Decimal);
|
||||
result.Value = value.ToString();
|
||||
}
|
||||
else if (value is bool boolean)
|
||||
{
|
||||
result.SetAttributeValue("Value", ValueType.Boolean);
|
||||
result.Value = boolean.ToString();
|
||||
}
|
||||
else if (tType.GetInterfaces().Any(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IEnumerable<>)))
|
||||
{
|
||||
result.SetAttributeValue("Value", ValueType.Collection);
|
||||
AddTypeAttr(types, tType, result);
|
||||
|
||||
var enumerator = (IEnumerator)tType.GetMethod("GetEnumerator").Invoke(value, null);
|
||||
while (enumerator.MoveNext())
|
||||
{
|
||||
var elVal = ParseObject(types, "Item", enumerator.Current);
|
||||
result.Add(elVal);
|
||||
}
|
||||
}
|
||||
else if (tType.IsClass || tType.IsValueType)
|
||||
{
|
||||
result.SetAttributeValue("Value", ValueType.Object);
|
||||
AddTypeAttr(types, tType, result);
|
||||
|
||||
IEnumerable<FieldInfo> fields = tType.GetFields(BindingFlags.Instance | BindingFlags.Public)
|
||||
.Concat(tType.GetFields(BindingFlags.Instance | BindingFlags.NonPublic));
|
||||
IEnumerable<PropertyInfo> properties = tType.GetProperties(BindingFlags.Instance | BindingFlags.Public).Where(p => p.GetSetMethod() != null)
|
||||
.Concat(tType.GetProperties(BindingFlags.Instance | BindingFlags.NonPublic).Where(p => p.GetSetMethod() != null));
|
||||
|
||||
foreach(var field in fields) result.Add(ParseObject(types, field.Name, field.GetValue(value)));
|
||||
foreach (var property in properties) result.Add(ParseObject(types, property.Name, property.GetValue(value)));
|
||||
}
|
||||
else
|
||||
{
|
||||
result.SetAttributeValue("Value", ValueType.None);
|
||||
result.Value = value.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
public static T Load<T>(FileStream file)
|
||||
{
|
||||
var doc = XDocument.Load(file);
|
||||
|
||||
var rootElems = doc.Root.Elements().ToArray();
|
||||
var types = rootElems[0];
|
||||
var elem = rootElems[1];
|
||||
|
||||
var dict = ParseValue(LoadDocTypes(types), elem);
|
||||
if (dict.GetType() == typeof(T)) return (T)dict;
|
||||
else throw new Exception($"Loaded configuration is not of the type '{typeof(T).Name}'");
|
||||
}
|
||||
|
||||
public static void Save(FileStream file, object obj)
|
||||
{
|
||||
var types = new List<Type>();
|
||||
var elem = ParseObject(types, "Root", obj);
|
||||
var root = new XElement("Configuration", new XElement("Types", SaveDocTypes(types)), elem);
|
||||
|
||||
var doc = new XDocument(root);
|
||||
doc.Save(file);
|
||||
}
|
||||
|
||||
public static T Load<T>(string path)
|
||||
{
|
||||
using (var file = LuaCsFile.OpenRead(path)) return Load<T>(file);
|
||||
}
|
||||
|
||||
public static void Save(string path, object obj)
|
||||
{
|
||||
using (var file = LuaCsFile.OpenWrite(path)) Save(file, obj);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,341 +1,646 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Xml.Serialization;
|
||||
using Barotrauma;
|
||||
using Barotrauma.Items.Components;
|
||||
using Barotrauma.LuaCs.Data;
|
||||
using Barotrauma.Networking;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.Xna.Framework;
|
||||
using OneOf;
|
||||
using Platform = Barotrauma.LuaCs.Data.Platform;
|
||||
// ReSharper disable ConvertClosureToMethodGroup
|
||||
|
||||
namespace Barotrauma;
|
||||
|
||||
public static class ModUtils
|
||||
// This file is cursed, we put everything in it, and I'm not sorry about it.
|
||||
namespace Barotrauma
|
||||
{
|
||||
#region LOGGING
|
||||
|
||||
public static class Logging
|
||||
public static class ModUtils
|
||||
{
|
||||
public static void PrintMessage(string s)
|
||||
public static class ItemPrefab
|
||||
{
|
||||
#if SERVER
|
||||
LuaCsLogger.LogMessage($"[Server] {s}");
|
||||
#else
|
||||
LuaCsLogger.LogMessage($"[Client] {s}");
|
||||
#endif
|
||||
internal static Barotrauma.ItemPrefab GetItemPrefab(string itemNameOrId)
|
||||
{
|
||||
Barotrauma.ItemPrefab itemPrefab =
|
||||
(Barotrauma.MapEntityPrefab.Find(itemNameOrId, identifier: null, showErrorMessages: false) ??
|
||||
Barotrauma.MapEntityPrefab.Find(null, identifier: itemNameOrId, showErrorMessages: false)) as Barotrauma.ItemPrefab;
|
||||
|
||||
return itemPrefab;
|
||||
}
|
||||
}
|
||||
|
||||
public static void PrintWarning(string s)
|
||||
public static class Client
|
||||
{
|
||||
internal static ulong GetSteamId(Barotrauma.Networking.Client client)
|
||||
{
|
||||
if (client.AccountId.TryUnwrap(out AccountId outValue) && outValue is SteamId steamId)
|
||||
{
|
||||
return steamId.Value;
|
||||
}
|
||||
else
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
#if SERVER
|
||||
LuaCsLogger.Log($"[Server] {s}", Color.Yellow);
|
||||
#else
|
||||
LuaCsLogger.Log($"[Client] {s}", Color.Yellow);
|
||||
internal static void UnbanPlayer(string playerName)
|
||||
{
|
||||
GameMain.Server.UnbanPlayer(playerName);
|
||||
}
|
||||
|
||||
internal static void BanPlayer(string player, string reason, bool range = false, float seconds = -1)
|
||||
{
|
||||
if (seconds == -1)
|
||||
{
|
||||
GameMain.Server.BanPlayer(player, reason, null);
|
||||
}
|
||||
else
|
||||
{
|
||||
GameMain.Server.BanPlayer(player, reason, TimeSpan.FromSeconds(seconds));
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
internal static IReadOnlyList<Barotrauma.Networking.Client> ClientList
|
||||
{
|
||||
get
|
||||
{
|
||||
if (GameMain.IsSingleplayer) { return new List<Barotrauma.Networking.Client>(); }
|
||||
|
||||
#if SERVER
|
||||
return GameMain.Server.ConnectedClients;
|
||||
#else
|
||||
return GameMain.Client.ConnectedClients;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class Definitions
|
||||
{
|
||||
public const string LuaCsForBarotrauma = nameof(LuaCsForBarotrauma);
|
||||
}
|
||||
|
||||
public static void PrintError(string s)
|
||||
public static class Environment
|
||||
{
|
||||
#if SERVER
|
||||
LuaCsLogger.LogError($"[Server] {s}");
|
||||
internal static void SetCurrentThreadAsMain() => MainThreadId = Thread.CurrentThread.ManagedThreadId;
|
||||
public static int MainThreadId { get; private set; } = Int32.MinValue;
|
||||
public static bool IsMainThread
|
||||
{
|
||||
get
|
||||
{
|
||||
if (MainThreadId == Int32.MinValue)
|
||||
throw new ArgumentNullException("MainThread ID not set.");
|
||||
return Thread.CurrentThread.ManagedThreadId == MainThreadId;
|
||||
}
|
||||
}
|
||||
|
||||
public static readonly Platform CurrentPlatform =
|
||||
#if WINDOWS
|
||||
Platform.Windows;
|
||||
#elif MACOS
|
||||
Platform.MacOS;
|
||||
#elif LINUX
|
||||
Platform.Linux;
|
||||
#else
|
||||
LuaCsLogger.LogError($"[Client] {s}");
|
||||
Platform.Linux;
|
||||
#endif
|
||||
|
||||
public static readonly Target CurrentTarget =
|
||||
#if CLIENT
|
||||
Target.Client;
|
||||
#elif SERVER
|
||||
Target.Server;
|
||||
#else
|
||||
Target.Server;
|
||||
#endif
|
||||
|
||||
}
|
||||
|
||||
#region LOGGING
|
||||
|
||||
public static class Logging
|
||||
{
|
||||
public static void PrintMessage(string s)
|
||||
{
|
||||
#if SERVER
|
||||
LuaCsSetup.Instance.Logger.LogMessage($"{s}");
|
||||
#else
|
||||
LuaCsSetup.Instance.Logger.LogMessage($"{s}");
|
||||
#endif
|
||||
}
|
||||
|
||||
public static void PrintWarning(string s)
|
||||
{
|
||||
#if SERVER
|
||||
LuaCsSetup.Instance.Logger.Log($"{s}", Color.Yellow);
|
||||
#else
|
||||
LuaCsSetup.Instance.Logger.Log($"{s}", Color.Yellow);
|
||||
#endif
|
||||
}
|
||||
|
||||
public static void PrintError(string s)
|
||||
{
|
||||
#if SERVER
|
||||
LuaCsSetup.Instance.Logger.LogError($"{s}");
|
||||
#else
|
||||
LuaCsSetup.Instance.Logger.LogError($"{s}");
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region FILE_IO
|
||||
|
||||
// ReSharper disable once InconsistentNaming
|
||||
public static class IO
|
||||
{
|
||||
public static IEnumerable<string> FindAllFilesInDirectory(string folder, string pattern,
|
||||
SearchOption option)
|
||||
{
|
||||
try
|
||||
{
|
||||
return Directory.GetFiles(folder, pattern, option);
|
||||
}
|
||||
catch (DirectoryNotFoundException e)
|
||||
{
|
||||
return new string[] { };
|
||||
}
|
||||
}
|
||||
|
||||
public static string PrepareFilePathString(string filePath) =>
|
||||
PrepareFilePathString(Path.GetDirectoryName(filePath)!, Path.GetFileName(filePath));
|
||||
|
||||
public static string PrepareFilePathString(string path, string fileName) =>
|
||||
Path.Combine(SanitizePath(path), SanitizeFileName(fileName));
|
||||
|
||||
public static string SanitizeFileName(string fileName)
|
||||
{
|
||||
foreach (char c in Barotrauma.IO.Path.GetInvalidFileNameCharsCrossPlatform())
|
||||
fileName = fileName.Replace(c, '_');
|
||||
return fileName;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the sanitized path for the top-level directory for a given content package.
|
||||
/// </summary>
|
||||
/// <param name="package"></param>
|
||||
/// <returns></returns>
|
||||
public static string GetContentPackageDir(ContentPackage package)
|
||||
{
|
||||
return SanitizePath(Path.GetFullPath(package.Dir));
|
||||
}
|
||||
|
||||
public static string SanitizePath(string path)
|
||||
{
|
||||
foreach (char c in Path.GetInvalidPathChars())
|
||||
path = path.Replace(c.ToString(), "_");
|
||||
return path.CleanUpPath();
|
||||
}
|
||||
|
||||
public static IOActionResultState GetOrCreateFileText(string filePath, out string fileText,
|
||||
Func<string> fileDataFactory = null, bool createFile = true)
|
||||
{
|
||||
fileText = null;
|
||||
string fp = Path.GetFullPath(SanitizePath(filePath));
|
||||
|
||||
IOActionResultState ioActionResultState = IOActionResultState.Success;
|
||||
if (createFile)
|
||||
{
|
||||
ioActionResultState = CreateFilePath(SanitizePath(filePath), out fp, fileDataFactory);
|
||||
}
|
||||
else if (!File.Exists(fp))
|
||||
{
|
||||
return IOActionResultState.FileNotFound;
|
||||
}
|
||||
|
||||
if (ioActionResultState == IOActionResultState.Success)
|
||||
{
|
||||
try
|
||||
{
|
||||
fileText = File.ReadAllText(fp!);
|
||||
return IOActionResultState.Success;
|
||||
}
|
||||
catch (ArgumentNullException ane)
|
||||
{
|
||||
ModUtils.Logging.PrintError(
|
||||
$"ModUtils::CreateFilePath() | Exception: An argument is null. path: {fp ?? "null"} | Exception Details: {ane.Message}");
|
||||
return IOActionResultState.FilePathNull;
|
||||
}
|
||||
catch (ArgumentException ae)
|
||||
{
|
||||
ModUtils.Logging.PrintError(
|
||||
$"ModUtils::CreateFilePath() | Exception: An argument is invalid. path: {fp ?? "null"} | Exception Details: {ae.Message}");
|
||||
return IOActionResultState.FilePathInvalid;
|
||||
}
|
||||
catch (DirectoryNotFoundException dnfe)
|
||||
{
|
||||
ModUtils.Logging.PrintError(
|
||||
$"ModUtils::CreateFilePath() | Exception: Cannot find directory. path: {fp ?? "null"} | Exception Details: {dnfe.Message}");
|
||||
return IOActionResultState.DirectoryMissing;
|
||||
}
|
||||
catch (PathTooLongException ptle)
|
||||
{
|
||||
ModUtils.Logging.PrintError(
|
||||
$"ModUtils::CreateFilePath() | Exception: path length is over 200 characters. path: {fp ?? "null"} | Exception Details: {ptle.Message}");
|
||||
return IOActionResultState.PathTooLong;
|
||||
}
|
||||
catch (NotSupportedException nse)
|
||||
{
|
||||
ModUtils.Logging.PrintError(
|
||||
$"ModUtils::CreateFilePath() | Exception: Operation not supported on your platform/environment (permissions?). path: {fp ?? "null"} | Exception Details: {nse.Message}");
|
||||
return IOActionResultState.InvalidOperation;
|
||||
}
|
||||
catch (IOException ioe)
|
||||
{
|
||||
ModUtils.Logging.PrintError(
|
||||
$"ModUtils::CreateFilePath() | Exception: IO tasks failed (Operation not supported). path: {fp ?? "null"} | Exception Details: {ioe.Message}");
|
||||
return IOActionResultState.IOFailure;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
ModUtils.Logging.PrintError(
|
||||
$"ModUtils::CreateFilePath() | Exception: Unknown/Other Exception. path: {fp ?? "null"} | ExceptionMessage: {e.Message}");
|
||||
return IOActionResultState.UnknownError;
|
||||
}
|
||||
}
|
||||
|
||||
return ioActionResultState;
|
||||
}
|
||||
|
||||
public static IOActionResultState CreateFilePath(string filePath, out string formattedFilePath,
|
||||
Func<string> fileDataFactory = null)
|
||||
{
|
||||
string file = Path.GetFileName(filePath);
|
||||
string path = Path.GetDirectoryName(filePath)!;
|
||||
|
||||
formattedFilePath = IO.PrepareFilePathString(path, file);
|
||||
try
|
||||
{
|
||||
if (!Directory.Exists(path))
|
||||
Directory.CreateDirectory(path);
|
||||
if (!File.Exists(formattedFilePath))
|
||||
File.WriteAllText(formattedFilePath, fileDataFactory is null ? "" : fileDataFactory.Invoke());
|
||||
return IOActionResultState.Success;
|
||||
}
|
||||
catch (ArgumentNullException ane)
|
||||
{
|
||||
ModUtils.Logging.PrintError(
|
||||
$"ModUtils::CreateFilePath() | Exception: An argument is null. path: {formattedFilePath ?? "null"} | Exception Details: {ane.Message}");
|
||||
return IOActionResultState.FilePathNull;
|
||||
}
|
||||
catch (ArgumentException ae)
|
||||
{
|
||||
ModUtils.Logging.PrintError(
|
||||
$"ModUtils::CreateFilePath() | Exception: An argument is invalid. path: {formattedFilePath ?? "null"} | Exception Details: {ae.Message}");
|
||||
return IOActionResultState.FilePathInvalid;
|
||||
}
|
||||
catch (DirectoryNotFoundException dnfe)
|
||||
{
|
||||
ModUtils.Logging.PrintError(
|
||||
$"ModUtils::CreateFilePath() | Exception: Cannot find directory. path: {path ?? "null"} | Exception Details: {dnfe.Message}");
|
||||
return IOActionResultState.DirectoryMissing;
|
||||
}
|
||||
catch (PathTooLongException ptle)
|
||||
{
|
||||
ModUtils.Logging.PrintError(
|
||||
$"ModUtils::CreateFilePath() | Exception: path length is over 200 characters. path: {formattedFilePath ?? "null"} | Exception Details: {ptle.Message}");
|
||||
return IOActionResultState.PathTooLong;
|
||||
}
|
||||
catch (NotSupportedException nse)
|
||||
{
|
||||
ModUtils.Logging.PrintError(
|
||||
$"ModUtils::CreateFilePath() | Exception: Operation not supported on your platform/environment (permissions?). path: {formattedFilePath ?? "null"} | Exception Details: {nse.Message}");
|
||||
return IOActionResultState.InvalidOperation;
|
||||
}
|
||||
catch (IOException ioe)
|
||||
{
|
||||
ModUtils.Logging.PrintError(
|
||||
$"ModUtils::CreateFilePath() | Exception: IO tasks failed (Operation not supported). path: {formattedFilePath ?? "null"} | Exception Details: {ioe.Message}");
|
||||
return IOActionResultState.IOFailure;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
ModUtils.Logging.PrintError(
|
||||
$"ModUtils::CreateFilePath() | Exception: Unknown/Other Exception. path: {path ?? "null"} | Exception Details: {e.Message}");
|
||||
return IOActionResultState.UnknownError;
|
||||
}
|
||||
}
|
||||
|
||||
public static IOActionResultState WriteFileText(string filePath, string fileText)
|
||||
{
|
||||
IOActionResultState ioActionResultState = CreateFilePath(filePath, out var fp);
|
||||
if (ioActionResultState == IOActionResultState.Success)
|
||||
{
|
||||
try
|
||||
{
|
||||
File.WriteAllText(fp!, fileText);
|
||||
return IOActionResultState.Success;
|
||||
}
|
||||
catch (ArgumentNullException ane)
|
||||
{
|
||||
ModUtils.Logging.PrintError(
|
||||
$"ModUtils::WriteFileText() | Exception: An argument is null. path: {fp ?? "null"} | Exception Details: {ane.Message}");
|
||||
return IOActionResultState.FilePathNull;
|
||||
}
|
||||
catch (ArgumentException ae)
|
||||
{
|
||||
ModUtils.Logging.PrintError(
|
||||
$"ModUtils::WriteFileText() | Exception: An argument is invalid. path: {fp ?? "null"} | Exception Details: {ae.Message}");
|
||||
return IOActionResultState.FilePathInvalid;
|
||||
}
|
||||
catch (DirectoryNotFoundException dnfe)
|
||||
{
|
||||
ModUtils.Logging.PrintError(
|
||||
$"ModUtils::WriteFileText() | Exception: Cannot find directory. path: {fp ?? "null"} | Exception Details: {dnfe.Message}");
|
||||
return IOActionResultState.DirectoryMissing;
|
||||
}
|
||||
catch (PathTooLongException ptle)
|
||||
{
|
||||
ModUtils.Logging.PrintError(
|
||||
$"ModUtils::WriteFileText() | Exception: path length is over 200 characters. path: {fp ?? "null"} | Exception Details: {ptle.Message}");
|
||||
return IOActionResultState.PathTooLong;
|
||||
}
|
||||
catch (NotSupportedException nse)
|
||||
{
|
||||
ModUtils.Logging.PrintError(
|
||||
$"ModUtils::WriteFileText() | Exception: Operation not supported on your platform/environment (permissions?). path: {fp ?? "null"} | Exception Details: {nse.Message}");
|
||||
return IOActionResultState.InvalidOperation;
|
||||
}
|
||||
catch (IOException ioe)
|
||||
{
|
||||
ModUtils.Logging.PrintError(
|
||||
$"ModUtils::WriteFileText() | Exception: IO tasks failed (Operation not supported). path: {fp ?? "null"} | Exception Details: {ioe.Message}");
|
||||
return IOActionResultState.IOFailure;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
ModUtils.Logging.PrintError(
|
||||
$"ModUtils::WriteFileText() | Exception: Unknown/Other Exception. path: {fp ?? "null"} | ExceptionMessage: {e.Message}");
|
||||
return IOActionResultState.UnknownError;
|
||||
}
|
||||
}
|
||||
|
||||
return ioActionResultState;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="instance"></param>
|
||||
/// <param name="filepath"></param>
|
||||
/// <param name="typeFactory"></param>
|
||||
/// <param name="createFile"></param>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <returns></returns>
|
||||
public static bool LoadOrCreateTypeXml<T>(out T instance,
|
||||
string filepath, Func<T> typeFactory = null, bool createFile = true) where T : class, new()
|
||||
{
|
||||
instance = null;
|
||||
filepath = filepath.CleanUpPath();
|
||||
if (IOActionResultState.Success == GetOrCreateFileText(
|
||||
filepath, out string fileText, typeFactory is not null
|
||||
? () =>
|
||||
{
|
||||
using StringWriter sw = new StringWriter();
|
||||
T t = typeFactory?.Invoke();
|
||||
if (t is not null)
|
||||
{
|
||||
XmlSerializer s = new XmlSerializer(typeof(T));
|
||||
s.Serialize(sw, t);
|
||||
return sw.ToString();
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
: null, createFile))
|
||||
{
|
||||
XmlSerializer s = new XmlSerializer(typeof(T));
|
||||
try
|
||||
{
|
||||
using TextReader tr = new StringReader(fileText);
|
||||
instance = (T)s.Deserialize(tr);
|
||||
return true;
|
||||
}
|
||||
catch (InvalidOperationException ioe)
|
||||
{
|
||||
ModUtils.Logging.PrintError($"Error while parsing type data for {typeof(T)}.");
|
||||
#if DEBUG
|
||||
ModUtils.Logging.PrintError(
|
||||
$"Exception: {ioe.Message}. Details: {ioe.InnerException?.Message}");
|
||||
#endif
|
||||
instance = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public enum IOActionResultState
|
||||
{
|
||||
Success,
|
||||
FileNotFound,
|
||||
FilePathNull,
|
||||
FilePathInvalid,
|
||||
DirectoryMissing,
|
||||
PathTooLong,
|
||||
InvalidOperation,
|
||||
IOFailure,
|
||||
UnknownError
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region GAME
|
||||
|
||||
public static class Game
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns whether or not there is a round running.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public static bool IsRoundInProgress()
|
||||
{
|
||||
#if CLIENT
|
||||
if (Screen.Selected is not null
|
||||
&& Screen.Selected.IsEditor)
|
||||
return false;
|
||||
#endif
|
||||
return GameMain.GameSession is not null && Level.Loaded is not null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region THREADING
|
||||
|
||||
public static class Threading
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the boolean value of an integer with thread-safety via <code>Interlocked</code>.
|
||||
/// </summary>
|
||||
/// <param name="var"></param>
|
||||
/// <returns></returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool GetBool(ref int var) => Interlocked.CompareExchange(ref var, 1, 1) > 0;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void SetBool(ref int var, bool value)
|
||||
{
|
||||
if (value)
|
||||
{
|
||||
Interlocked.CompareExchange(ref var, 1, 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
Interlocked.CompareExchange(ref var, 0, 1);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets if the integer is under 1 (is zero/false) and, if so, sets the value to one/true.
|
||||
/// </summary>
|
||||
/// <param name="var"></param>
|
||||
/// <returns></returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool CheckIfClearAndSetBool(ref int var)
|
||||
{
|
||||
return Interlocked.CompareExchange(ref var, 1, 0) < 1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets if the integer is over 0 (is one/true) and, if so, sets the value to zero/false.
|
||||
/// </summary>
|
||||
/// <param name="var"></param>
|
||||
/// <returns></returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool CheckIfSetAndClearBool(ref int var)
|
||||
{
|
||||
return Interlocked.CompareExchange(ref var, 0, 1) > 0;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region UTILITIES_CORE
|
||||
|
||||
public static V TryGetOrSet<K, V>(this IDictionary<K,V> dict, K key, Func<V> valueFactory) where K : IEquatable<K>
|
||||
{
|
||||
if (dict.TryGetValue(key, out var dictValue)) return dictValue;
|
||||
if (valueFactory is not null)
|
||||
dict.Add(key, valueFactory());
|
||||
else
|
||||
return default;
|
||||
return dict[key];
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
public static class AssemblyExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets all types in the given assembly. Handles invalid type scenarios.
|
||||
/// </summary>
|
||||
/// <param name="assembly">The assembly to scan</param>
|
||||
/// <returns>An enumerable collection of types.</returns>
|
||||
public static IEnumerable<Type> GetSafeTypes(this Assembly assembly)
|
||||
{
|
||||
// Based on https://github.com/Qkrisi/ktanemodkit/blob/master/Assets/Scripts/ReflectionHelper.cs#L53-L67
|
||||
|
||||
try
|
||||
{
|
||||
return assembly.GetTypes();
|
||||
}
|
||||
catch (ReflectionTypeLoadException re)
|
||||
{
|
||||
try
|
||||
{
|
||||
return re.Types.Where(x => x != null)!;
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
return new List<Type>();
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return new List<Type>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region FILE_IO
|
||||
|
||||
// ReSharper disable once InconsistentNaming
|
||||
public static class IO
|
||||
public static class CollectionExtensions
|
||||
{
|
||||
public static IEnumerable<string> FindAllFilesInDirectory(string folder, string pattern,
|
||||
SearchOption option)
|
||||
{
|
||||
try
|
||||
{
|
||||
return Directory.GetFiles(folder, pattern, option);
|
||||
}
|
||||
catch (DirectoryNotFoundException e)
|
||||
{
|
||||
return new string[] { };
|
||||
}
|
||||
}
|
||||
|
||||
public static string PrepareFilePathString(string filePath) =>
|
||||
PrepareFilePathString(Path.GetDirectoryName(filePath)!, Path.GetFileName(filePath));
|
||||
|
||||
public static string PrepareFilePathString(string path, string fileName) =>
|
||||
Path.Combine(SanitizePath(path), SanitizeFileName(fileName));
|
||||
|
||||
public static string SanitizeFileName(string fileName)
|
||||
{
|
||||
foreach (char c in Barotrauma.IO.Path.GetInvalidFileNameCharsCrossPlatform())
|
||||
fileName = fileName.Replace(c, '_');
|
||||
return fileName;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the sanitized path for the top-level directory for a given content package.
|
||||
/// Executes a series of asynchronous tasks with limited parallelism to maintain execution efficiency.
|
||||
/// </summary>
|
||||
/// <param name="package"></param>
|
||||
/// <returns></returns>
|
||||
public static string GetContentPackageDir(ContentPackage package)
|
||||
{
|
||||
return SanitizePath(Path.GetFullPath(package.Dir));
|
||||
}
|
||||
|
||||
public static string SanitizePath(string path)
|
||||
{
|
||||
foreach (char c in Path.GetInvalidPathChars())
|
||||
path = path.Replace(c.ToString(), "_");
|
||||
return path.CleanUpPath();
|
||||
}
|
||||
|
||||
public static IOActionResultState GetOrCreateFileText(string filePath, out string fileText, Func<string> fileDataFactory = null, bool createFile = true)
|
||||
{
|
||||
fileText = null;
|
||||
string fp = Path.GetFullPath(SanitizePath(filePath));
|
||||
|
||||
IOActionResultState ioActionResultState = IOActionResultState.Success;
|
||||
if (createFile)
|
||||
{
|
||||
ioActionResultState = CreateFilePath(SanitizePath(filePath), out fp, fileDataFactory);
|
||||
}
|
||||
else if (!File.Exists(fp))
|
||||
{
|
||||
return IOActionResultState.FileNotFound;
|
||||
}
|
||||
|
||||
if (ioActionResultState == IOActionResultState.Success)
|
||||
{
|
||||
try
|
||||
{
|
||||
fileText = File.ReadAllText(fp!);
|
||||
return IOActionResultState.Success;
|
||||
}
|
||||
catch (ArgumentNullException ane)
|
||||
{
|
||||
ModUtils.Logging.PrintError($"ModUtils::CreateFilePath() | Exception: An argument is null. path: {fp ?? "null"} | Exception Details: {ane.Message}");
|
||||
return IOActionResultState.FilePathNull;
|
||||
}
|
||||
catch (ArgumentException ae)
|
||||
{
|
||||
ModUtils.Logging.PrintError($"ModUtils::CreateFilePath() | Exception: An argument is invalid. path: {fp ?? "null"} | Exception Details: {ae.Message}");
|
||||
return IOActionResultState.FilePathInvalid;
|
||||
}
|
||||
catch (DirectoryNotFoundException dnfe)
|
||||
{
|
||||
ModUtils.Logging.PrintError($"ModUtils::CreateFilePath() | Exception: Cannot find directory. path: {fp ?? "null"} | Exception Details: {dnfe.Message}");
|
||||
return IOActionResultState.DirectoryMissing;
|
||||
}
|
||||
catch (PathTooLongException ptle)
|
||||
{
|
||||
ModUtils.Logging.PrintError($"ModUtils::CreateFilePath() | Exception: path length is over 200 characters. path: {fp ?? "null"} | Exception Details: {ptle.Message}");
|
||||
return IOActionResultState.PathTooLong;
|
||||
}
|
||||
catch (NotSupportedException nse)
|
||||
{
|
||||
ModUtils.Logging.PrintError($"ModUtils::CreateFilePath() | Exception: Operation not supported on your platform/environment (permissions?). path: {fp ?? "null"} | Exception Details: {nse.Message}");
|
||||
return IOActionResultState.InvalidOperation;
|
||||
}
|
||||
catch (IOException ioe)
|
||||
{
|
||||
ModUtils.Logging.PrintError($"ModUtils::CreateFilePath() | Exception: IO tasks failed (Operation not supported). path: {fp ?? "null"} | Exception Details: {ioe.Message}");
|
||||
return IOActionResultState.IOFailure;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
ModUtils.Logging.PrintError($"ModUtils::CreateFilePath() | Exception: Unknown/Other Exception. path: {fp ?? "null"} | ExceptionMessage: {e.Message}");
|
||||
return IOActionResultState.UnknownError;
|
||||
}
|
||||
}
|
||||
|
||||
return ioActionResultState;
|
||||
}
|
||||
|
||||
public static IOActionResultState CreateFilePath(string filePath, out string formattedFilePath, Func<string> fileDataFactory = null)
|
||||
{
|
||||
string file = Path.GetFileName(filePath);
|
||||
string path = Path.GetDirectoryName(filePath)!;
|
||||
|
||||
formattedFilePath = IO.PrepareFilePathString(path, file);
|
||||
try
|
||||
{
|
||||
if (!Directory.Exists(path))
|
||||
Directory.CreateDirectory(path);
|
||||
if (!File.Exists(formattedFilePath))
|
||||
File.WriteAllText(formattedFilePath, fileDataFactory is null ? "" : fileDataFactory.Invoke());
|
||||
return IOActionResultState.Success;
|
||||
}
|
||||
catch (ArgumentNullException ane)
|
||||
{
|
||||
ModUtils.Logging.PrintError($"ModUtils::CreateFilePath() | Exception: An argument is null. path: {formattedFilePath ?? "null"} | Exception Details: {ane.Message}");
|
||||
return IOActionResultState.FilePathNull;
|
||||
}
|
||||
catch (ArgumentException ae)
|
||||
{
|
||||
ModUtils.Logging.PrintError($"ModUtils::CreateFilePath() | Exception: An argument is invalid. path: {formattedFilePath ?? "null"} | Exception Details: {ae.Message}");
|
||||
return IOActionResultState.FilePathInvalid;
|
||||
}
|
||||
catch (DirectoryNotFoundException dnfe)
|
||||
{
|
||||
ModUtils.Logging.PrintError($"ModUtils::CreateFilePath() | Exception: Cannot find directory. path: {path ?? "null"} | Exception Details: {dnfe.Message}");
|
||||
return IOActionResultState.DirectoryMissing;
|
||||
}
|
||||
catch (PathTooLongException ptle)
|
||||
{
|
||||
ModUtils.Logging.PrintError($"ModUtils::CreateFilePath() | Exception: path length is over 200 characters. path: {formattedFilePath ?? "null"} | Exception Details: {ptle.Message}");
|
||||
return IOActionResultState.PathTooLong;
|
||||
}
|
||||
catch (NotSupportedException nse)
|
||||
{
|
||||
ModUtils.Logging.PrintError($"ModUtils::CreateFilePath() | Exception: Operation not supported on your platform/environment (permissions?). path: {formattedFilePath ?? "null"} | Exception Details: {nse.Message}");
|
||||
return IOActionResultState.InvalidOperation;
|
||||
}
|
||||
catch (IOException ioe)
|
||||
{
|
||||
ModUtils.Logging.PrintError($"ModUtils::CreateFilePath() | Exception: IO tasks failed (Operation not supported). path: {formattedFilePath ?? "null"} | Exception Details: {ioe.Message}");
|
||||
return IOActionResultState.IOFailure;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
ModUtils.Logging.PrintError($"ModUtils::CreateFilePath() | Exception: Unknown/Other Exception. path: {path ?? "null"} | Exception Details: {e.Message}");
|
||||
return IOActionResultState.UnknownError;
|
||||
}
|
||||
}
|
||||
|
||||
public static IOActionResultState WriteFileText(string filePath, string fileText)
|
||||
{
|
||||
IOActionResultState ioActionResultState = CreateFilePath(filePath, out var fp);
|
||||
if (ioActionResultState == IOActionResultState.Success)
|
||||
{
|
||||
try
|
||||
{
|
||||
File.WriteAllText(fp!, fileText);
|
||||
return IOActionResultState.Success;
|
||||
}
|
||||
catch (ArgumentNullException ane)
|
||||
{
|
||||
ModUtils.Logging.PrintError($"ModUtils::WriteFileText() | Exception: An argument is null. path: {fp ?? "null"} | Exception Details: {ane.Message}");
|
||||
return IOActionResultState.FilePathNull;
|
||||
}
|
||||
catch (ArgumentException ae)
|
||||
{
|
||||
ModUtils.Logging.PrintError($"ModUtils::WriteFileText() | Exception: An argument is invalid. path: {fp ?? "null"} | Exception Details: {ae.Message}");
|
||||
return IOActionResultState.FilePathInvalid;
|
||||
}
|
||||
catch (DirectoryNotFoundException dnfe)
|
||||
{
|
||||
ModUtils.Logging.PrintError($"ModUtils::WriteFileText() | Exception: Cannot find directory. path: {fp ?? "null"} | Exception Details: {dnfe.Message}");
|
||||
return IOActionResultState.DirectoryMissing;
|
||||
}
|
||||
catch (PathTooLongException ptle)
|
||||
{
|
||||
ModUtils.Logging.PrintError($"ModUtils::WriteFileText() | Exception: path length is over 200 characters. path: {fp ?? "null"} | Exception Details: {ptle.Message}");
|
||||
return IOActionResultState.PathTooLong;
|
||||
}
|
||||
catch (NotSupportedException nse)
|
||||
{
|
||||
ModUtils.Logging.PrintError($"ModUtils::WriteFileText() | Exception: Operation not supported on your platform/environment (permissions?). path: {fp ?? "null"} | Exception Details: {nse.Message}");
|
||||
return IOActionResultState.InvalidOperation;
|
||||
}
|
||||
catch (IOException ioe)
|
||||
{
|
||||
ModUtils.Logging.PrintError($"ModUtils::WriteFileText() | Exception: IO tasks failed (Operation not supported). path: {fp ?? "null"} | Exception Details: {ioe.Message}");
|
||||
return IOActionResultState.IOFailure;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
ModUtils.Logging.PrintError($"ModUtils::WriteFileText() | Exception: Unknown/Other Exception. path: {fp ?? "null"} | ExceptionMessage: {e.Message}");
|
||||
return IOActionResultState.UnknownError;
|
||||
}
|
||||
}
|
||||
|
||||
return ioActionResultState;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="instance"></param>
|
||||
/// <param name="filepath"></param>
|
||||
/// <param name="typeFactory"></param>
|
||||
/// <param name="createFile"></param>
|
||||
/// <param name="source"></param>
|
||||
/// <param name="funcBody"></param>
|
||||
/// <param name="maxDegreeOfParallelism"></param>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <returns></returns>
|
||||
public static bool LoadOrCreateTypeXml<T>(out T instance,
|
||||
string filepath, Func<T> typeFactory = null, bool createFile = true) where T : class, new()
|
||||
public static Task ParallelForEachAsync<T>(this IEnumerable<T> source, Func<T, Task> funcBody, int maxDegreeOfParallelism = 4)
|
||||
{
|
||||
instance = null;
|
||||
filepath = filepath.CleanUpPath();
|
||||
if (IOActionResultState.Success == GetOrCreateFileText(
|
||||
filepath, out string fileText, typeFactory is not null ? () =>
|
||||
{
|
||||
using StringWriter sw = new StringWriter();
|
||||
T t = typeFactory?.Invoke();
|
||||
if (t is not null)
|
||||
{
|
||||
XmlSerializer s = new XmlSerializer(typeof(T));
|
||||
s.Serialize(sw, t);
|
||||
return sw.ToString();
|
||||
}
|
||||
return "";
|
||||
} : null, createFile))
|
||||
async Task AwaitParallelLimit(IEnumerator<T> partition)
|
||||
{
|
||||
XmlSerializer s = new XmlSerializer(typeof(T));
|
||||
try
|
||||
using (partition)
|
||||
{
|
||||
using TextReader tr = new StringReader(fileText);
|
||||
instance = (T)s.Deserialize(tr);
|
||||
return true;
|
||||
}
|
||||
catch(InvalidOperationException ioe)
|
||||
{
|
||||
ModUtils.Logging.PrintError($"Error while parsing type data for {typeof(T)}.");
|
||||
#if DEBUG
|
||||
ModUtils.Logging.PrintError($"Exception: {ioe.Message}. Details: {ioe.InnerException?.Message}");
|
||||
#endif
|
||||
instance = null;
|
||||
return false;
|
||||
while (partition.MoveNext())
|
||||
{
|
||||
await Task.Yield(); // prevents a sync/hot thread hangup
|
||||
await funcBody(partition.Current);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public enum IOActionResultState
|
||||
{
|
||||
Success, FileNotFound, FilePathNull, FilePathInvalid, DirectoryMissing, PathTooLong, InvalidOperation, IOFailure, UnknownError
|
||||
return Task.WhenAll(
|
||||
Partitioner
|
||||
.Create(source)
|
||||
.GetPartitions(maxDegreeOfParallelism)
|
||||
.AsParallel()
|
||||
.Select(p => AwaitParallelLimit(p)));
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region GAME
|
||||
|
||||
public static class Game
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns whether or not there is a round running.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public static bool IsRoundInProgress()
|
||||
{
|
||||
#if CLIENT
|
||||
if (Screen.Selected is not null
|
||||
&& Screen.Selected.IsEditor)
|
||||
return false;
|
||||
#endif
|
||||
return GameMain.GameSession is not null && Level.Loaded is not null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
|
||||
|
||||
#region ExceptionData
|
||||
|
||||
namespace FluentResults.LuaCs
|
||||
{
|
||||
public static class MetadataType
|
||||
{
|
||||
public static string ExceptionDetails = nameof(ExceptionDetails);
|
||||
/// <summary>
|
||||
/// The object that threw the exception.
|
||||
/// </summary>
|
||||
public static string ExceptionObject = nameof(ExceptionObject);
|
||||
/// <summary>
|
||||
/// The parameter-object responsible for the exception thrown (not the exception thrower).
|
||||
/// </summary>
|
||||
public static string RootObject = nameof(RootObject);
|
||||
/// <summary>
|
||||
/// Additional exception sources.
|
||||
/// </summary>
|
||||
public static string Sources = nameof(Sources);
|
||||
public static string StackTrace = nameof(StackTrace);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
namespace Barotrauma;
|
||||
|
||||
public enum ApplicationMode
|
||||
{
|
||||
Client, Server
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
namespace Barotrauma;
|
||||
|
||||
public enum AssemblyLoadingSuccessState
|
||||
{
|
||||
ACLLoadFailure,
|
||||
AlreadyLoaded,
|
||||
BadFilePath,
|
||||
CannotLoadFile,
|
||||
InvalidAssembly,
|
||||
NoAssemblyFound,
|
||||
PluginInstanceFailure,
|
||||
BadName,
|
||||
CannotLoadFromStream,
|
||||
Success
|
||||
}
|
||||
@@ -1,901 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.Loader;
|
||||
using System.Threading;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
|
||||
// ReSharper disable EventNeverSubscribedTo.Global
|
||||
// ReSharper disable InconsistentNaming
|
||||
|
||||
namespace Barotrauma;
|
||||
|
||||
/***
|
||||
* Note: This class was written to be thread-safe in order to allow parallelization in loading in the future if the need
|
||||
* becomes necessary as there is almost no serial performance overhead for adding threading protection.
|
||||
*/
|
||||
|
||||
/// <summary>
|
||||
/// Provides functionality for the loading, unloading and management of plugins implementing IAssemblyPlugin.
|
||||
/// All plugins are loaded into their own AssemblyLoadContext along with their dependencies.
|
||||
/// </summary>
|
||||
public class AssemblyManager
|
||||
{
|
||||
#region ExternalAPI
|
||||
|
||||
/// <summary>
|
||||
/// Called when an assembly is loaded.
|
||||
/// </summary>
|
||||
public event Action<Assembly> OnAssemblyLoaded;
|
||||
|
||||
/// <summary>
|
||||
/// Called when an assembly is marked for unloading, before unloading begins. You should use this to cleanup
|
||||
/// any references that you have to this assembly.
|
||||
/// </summary>
|
||||
public event Action<Assembly> OnAssemblyUnloading;
|
||||
|
||||
/// <summary>
|
||||
/// Called whenever an exception is thrown. First arg is a formatted message, Second arg is the Exception.
|
||||
/// </summary>
|
||||
public event Action<string, Exception> OnException;
|
||||
|
||||
/// <summary>
|
||||
/// For unloading issue debugging. Called whenever MemoryFileAssemblyContextLoader [load context] is unloaded.
|
||||
/// </summary>
|
||||
public event Action<Guid> OnACLUnload;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// [DEBUG ONLY]
|
||||
/// Returns a list of the current unloading ACLs.
|
||||
/// </summary>
|
||||
public ImmutableList<WeakReference<MemoryFileAssemblyContextLoader>> StillUnloadingACLs
|
||||
{
|
||||
get
|
||||
{
|
||||
OpsLockUnloaded.EnterReadLock();
|
||||
try
|
||||
{
|
||||
return UnloadingACLs.ToImmutableList();
|
||||
}
|
||||
finally
|
||||
{
|
||||
OpsLockUnloaded.ExitReadLock();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ReSharper disable once MemberCanBePrivate.Global
|
||||
/// <summary>
|
||||
/// Checks if there are any AssemblyLoadContexts still in the process of unloading.
|
||||
/// </summary>
|
||||
public bool IsCurrentlyUnloading
|
||||
{
|
||||
get
|
||||
{
|
||||
OpsLockUnloaded.EnterReadLock();
|
||||
try
|
||||
{
|
||||
return UnloadingACLs.Any();
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
finally
|
||||
{
|
||||
OpsLockUnloaded.ExitReadLock();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Old API compatibility
|
||||
public IEnumerable<Type> GetSubTypesInLoadedAssemblies<T>()
|
||||
{
|
||||
return GetSubTypesInLoadedAssemblies<T>(false);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Allows iteration over all non-interface types in all loaded assemblies in the AsmMgr that are assignable to the given type (IsAssignableFrom).
|
||||
/// Warning: care should be used when using this method in hot paths as performance may be affected.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type to compare against</typeparam>
|
||||
/// <param name="rebuildList">Forces caches to clear and for the lists of types to be rebuilt.</param>
|
||||
/// <returns>An Enumerator for matching types.</returns>
|
||||
public IEnumerable<Type> GetSubTypesInLoadedAssemblies<T>(bool rebuildList)
|
||||
{
|
||||
Type targetType = typeof(T);
|
||||
string typeName = targetType.FullName ?? targetType.Name;
|
||||
|
||||
// rebuild
|
||||
if (rebuildList)
|
||||
RebuildTypesList();
|
||||
|
||||
// check cache
|
||||
if (_subTypesLookupCache.TryGetValue(typeName, out var subTypeList))
|
||||
{
|
||||
return subTypeList;
|
||||
}
|
||||
|
||||
// build from scratch
|
||||
OpsLockLoaded.EnterReadLock();
|
||||
try
|
||||
{
|
||||
// build list
|
||||
var list1 = _defaultContextTypes
|
||||
.Where(kvp1 => targetType.IsAssignableFrom(kvp1.Value) && !kvp1.Value.IsInterface)
|
||||
.Concat(LoadedACLs
|
||||
.SelectMany(kvp => kvp.Value.AssembliesTypes)
|
||||
.Where(kvp2 => targetType.IsAssignableFrom(kvp2.Value) && !kvp2.Value.IsInterface))
|
||||
.Select(kvp3 => kvp3.Value)
|
||||
.ToImmutableList();
|
||||
|
||||
// only add if we find something
|
||||
if (list1.Count > 0)
|
||||
{
|
||||
if (!_subTypesLookupCache.TryAdd(typeName, list1))
|
||||
{
|
||||
ModUtils.Logging.PrintError(
|
||||
$"{nameof(AssemblyManager)}: Unable to add subtypes to cache of type {typeName}!");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
ModUtils.Logging.PrintMessage(
|
||||
$"{nameof(AssemblyManager)}: Warning: No types found during search for subtypes of {typeName}");
|
||||
}
|
||||
|
||||
return list1;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
this.OnException?.Invoke($"{nameof(AssemblyManager)}::{nameof(GetSubTypesInLoadedAssemblies)}() | Error: {e.Message}", e);
|
||||
return ImmutableList<Type>.Empty;
|
||||
}
|
||||
finally
|
||||
{
|
||||
OpsLockLoaded.ExitReadLock();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to get types assignable to type from the ACL given the Guid.
|
||||
/// </summary>
|
||||
/// <param name="id"></param>
|
||||
/// <param name="types"></param>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <returns></returns>
|
||||
public bool TryGetSubTypesFromACL<T>(Guid id, out IEnumerable<Type> types)
|
||||
{
|
||||
Type targetType = typeof(T);
|
||||
|
||||
if (TryGetACL(id, out var acl))
|
||||
{
|
||||
types = acl.AssembliesTypes
|
||||
.Where(kvp => targetType.IsAssignableFrom(kvp.Value) && !kvp.Value.IsInterface)
|
||||
.Select(kvp => kvp.Value);
|
||||
return true;
|
||||
}
|
||||
|
||||
types = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to get types from the ACL given the Guid.
|
||||
/// </summary>
|
||||
/// <param name="id"></param>
|
||||
/// <param name="types"></param>
|
||||
/// <returns></returns>
|
||||
public bool TryGetSubTypesFromACL(Guid id, out IEnumerable<Type> types)
|
||||
{
|
||||
if (TryGetACL(id, out var acl))
|
||||
{
|
||||
types = acl.AssembliesTypes.Select(kvp => kvp.Value);
|
||||
return true;
|
||||
}
|
||||
|
||||
types = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Allows iteration over all types, including interfaces, in all loaded assemblies in the AsmMgr who's names match the string.
|
||||
/// Note: Will return the by-reference equivalent type if the type name is prefixed with "out " or "ref ".
|
||||
/// </summary>
|
||||
/// <param name="typeName">The string name of the type to search for.</param>
|
||||
/// <returns>An Enumerator for matching types. List will be empty if bad params are supplied.</returns>
|
||||
public IEnumerable<Type> GetTypesByName(string typeName)
|
||||
{
|
||||
List<Type> types = new();
|
||||
if (typeName.IsNullOrWhiteSpace())
|
||||
return types;
|
||||
|
||||
bool byRef = false;
|
||||
if (typeName.StartsWith("out ") || typeName.StartsWith("ref "))
|
||||
{
|
||||
typeName = typeName.Remove(0, 4);
|
||||
byRef = true;
|
||||
}
|
||||
|
||||
|
||||
TypesListHelper();
|
||||
if (types.Count > 0)
|
||||
return types;
|
||||
|
||||
// we couldn't find it, rebuild and try one more time
|
||||
RebuildTypesList();
|
||||
TypesListHelper();
|
||||
|
||||
if (types.Count > 0)
|
||||
return types;
|
||||
|
||||
OpsLockLoaded.EnterReadLock();
|
||||
try
|
||||
{
|
||||
// fallback to Type.GetType
|
||||
Type t = Type.GetType(typeName, false, false);
|
||||
if (t is not null)
|
||||
{
|
||||
types.Add(byRef ? t.MakeByRefType() : t);
|
||||
return types;
|
||||
}
|
||||
|
||||
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
|
||||
{
|
||||
try
|
||||
{
|
||||
t = assembly.GetType(typeName, false, false);
|
||||
if (t is not null)
|
||||
types.Add(byRef ? t.MakeByRefType() : t);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
this.OnException?.Invoke(
|
||||
$"{nameof(AssemblyManager)}::{nameof(GetTypesByName)}() | Error: {e.Message}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
OpsLockLoaded.ExitReadLock();
|
||||
}
|
||||
|
||||
return types;
|
||||
|
||||
void TypesListHelper()
|
||||
{
|
||||
if (_defaultContextTypes.TryGetValue(typeName, out var type1))
|
||||
{
|
||||
if (type1 is not null)
|
||||
types.Add(byRef ? type1.MakeByRefType() : type1);
|
||||
}
|
||||
|
||||
OpsLockLoaded.EnterReadLock();
|
||||
try
|
||||
{
|
||||
foreach (KeyValuePair<Guid,LoadedACL> loadedAcl in LoadedACLs)
|
||||
{
|
||||
var at = loadedAcl.Value.AssembliesTypes;
|
||||
if (at.TryGetValue(typeName, out var type2))
|
||||
{
|
||||
if (type2 is not null)
|
||||
types.Add(byRef ? type2.MakeByRefType() : type2);
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
OpsLockLoaded.ExitReadLock();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Allows iteration over all types (including interfaces) in all loaded assemblies managed by the AsmMgr.
|
||||
/// Warning: High usage may result in performance issues.
|
||||
/// </summary>
|
||||
/// <returns>An Enumerator for iteration.</returns>
|
||||
public IEnumerable<Type> GetAllTypesInLoadedAssemblies()
|
||||
{
|
||||
OpsLockLoaded.EnterReadLock();
|
||||
try
|
||||
{
|
||||
return _defaultContextTypes
|
||||
.Select(kvp => kvp.Value)
|
||||
.Concat(LoadedACLs
|
||||
.SelectMany(kvp => kvp.Value?.AssembliesTypes.Select(kv => kv.Value)))
|
||||
.ToImmutableList();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return ImmutableList<Type>.Empty;
|
||||
}
|
||||
finally
|
||||
{
|
||||
OpsLockLoaded.ExitReadLock();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a list of all loaded ACLs.
|
||||
/// WARNING: References to these ACLs outside of the AssemblyManager should be kept in a WeakReference in order
|
||||
/// to avoid causing issues with unloading/disposal.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public IEnumerable<LoadedACL> GetAllLoadedACLs()
|
||||
{
|
||||
OpsLockLoaded.EnterReadLock();
|
||||
try
|
||||
{
|
||||
if (!LoadedACLs.Any())
|
||||
{
|
||||
return ImmutableList<LoadedACL>.Empty;
|
||||
}
|
||||
|
||||
return LoadedACLs.Select(kvp => kvp.Value).ToImmutableList();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return ImmutableList<LoadedACL>.Empty;
|
||||
}
|
||||
finally
|
||||
{
|
||||
OpsLockLoaded.ExitReadLock();
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region InternalAPI
|
||||
|
||||
/// <summary>
|
||||
/// [Unsafe] Warning: only for use in nested threading functions. Requires care to manage access.
|
||||
/// Does not make any guarantees about the state of the ACL after the list has been returned.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[MethodImpl(MethodImplOptions.Synchronized | MethodImplOptions.NoInlining)]
|
||||
internal ImmutableList<LoadedACL> UnsafeGetAllLoadedACLs()
|
||||
{
|
||||
if (LoadedACLs.IsEmpty)
|
||||
return ImmutableList<LoadedACL>.Empty;
|
||||
return LoadedACLs.Select(kvp => kvp.Value).ToImmutableList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Used by content package and plugin management to stop unloading of a given ACL until all plugins have gracefully closed.
|
||||
/// </summary>
|
||||
public event System.Func<LoadedACL, bool> IsReadyToUnloadACL;
|
||||
|
||||
/// <summary>
|
||||
/// Compiles an assembly from supplied references and syntax trees into the specified AssemblyContextLoader.
|
||||
/// A new ACL will be created if the Guid supplied is Guid.Empty.
|
||||
/// </summary>
|
||||
/// <param name="compiledAssemblyName"></param>
|
||||
/// <param name="syntaxTree"></param>
|
||||
/// <param name="externalMetadataReferences"></param>
|
||||
/// <param name="compilationOptions"></param>
|
||||
/// <param name="friendlyName">A non-unique name for later reference. Optional, set to null if unused.</param>
|
||||
/// <param name="id">The guid of the assembly </param>
|
||||
/// <param name="externFileAssemblyRefs"></param>
|
||||
/// <returns></returns>
|
||||
public AssemblyLoadingSuccessState LoadAssemblyFromMemory([NotNull] string compiledAssemblyName,
|
||||
[NotNull] IEnumerable<SyntaxTree> syntaxTree,
|
||||
IEnumerable<MetadataReference> externalMetadataReferences,
|
||||
[NotNull] CSharpCompilationOptions compilationOptions,
|
||||
string friendlyName,
|
||||
ref Guid id,
|
||||
IEnumerable<Assembly> externFileAssemblyRefs = null)
|
||||
{
|
||||
// validation
|
||||
if (compiledAssemblyName.IsNullOrWhiteSpace())
|
||||
return AssemblyLoadingSuccessState.BadName;
|
||||
|
||||
if (syntaxTree is null)
|
||||
return AssemblyLoadingSuccessState.InvalidAssembly;
|
||||
|
||||
if (!GetOrCreateACL(id, friendlyName, out var acl))
|
||||
return AssemblyLoadingSuccessState.ACLLoadFailure;
|
||||
|
||||
id = acl.Id; // pass on true id returned
|
||||
|
||||
// this acl is already hosting an in-memory assembly
|
||||
if (acl.Acl.CompiledAssembly is not null)
|
||||
return AssemblyLoadingSuccessState.AlreadyLoaded;
|
||||
|
||||
// compile
|
||||
AssemblyLoadingSuccessState state;
|
||||
string messages;
|
||||
try
|
||||
{
|
||||
state = acl.Acl.CompileAndLoadScriptAssembly(compiledAssemblyName, syntaxTree, externalMetadataReferences,
|
||||
compilationOptions, out messages, externFileAssemblyRefs);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
ModUtils.Logging.PrintError($"{nameof(AssemblyManager)}::{nameof(LoadAssemblyFromMemory)}() | Failed to compile and load assemblies for [ {compiledAssemblyName} / {friendlyName} ]! Details: {e.Message} | {e.StackTrace}");
|
||||
return AssemblyLoadingSuccessState.InvalidAssembly;
|
||||
}
|
||||
|
||||
// get types
|
||||
if (state is AssemblyLoadingSuccessState.Success)
|
||||
{
|
||||
_subTypesLookupCache.Clear();
|
||||
acl.RebuildTypesList();
|
||||
OnAssemblyLoaded?.Invoke(acl.Acl.CompiledAssembly);
|
||||
}
|
||||
else
|
||||
{
|
||||
ModUtils.Logging.PrintError($"Unable to compile assembly '{compiledAssemblyName}' due to errors: {messages}");
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Switches the ACL with the given Guid to Template Mode, which disables assembly name resolution for any assemblies loaded in it.
|
||||
/// These ACLs are intended to be used to host Assemblies for information only and not for code execution.
|
||||
/// WARNING: This process is irreversible.
|
||||
/// </summary>
|
||||
/// <param name="guid">Guid of the ACL.</param>
|
||||
/// <returns>Whether or not an ACL was found with the given ID.</returns>
|
||||
public bool SetACLToTemplateMode(Guid guid)
|
||||
{
|
||||
if (!TryGetACL(guid, out var acl))
|
||||
return false;
|
||||
acl.Acl.IsTemplateMode = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to load all assemblies at the supplied file paths list into the ACl with the given Guid.
|
||||
/// If the supplied Guid is Empty, then a new ACl will be created and the Guid will be assigned to it.
|
||||
/// </summary>
|
||||
/// <param name="filePaths">List of assemblies to try and load.</param>
|
||||
/// <param name="friendlyName">A non-unique name for later reference. Optional.</param>
|
||||
/// <param name="id">Guid of the ACL or Empty if none specified. Guid of ACL will be assigned to this var.</param>
|
||||
/// <returns>Operation success messages.</returns>
|
||||
/// <exception cref="ArgumentNullException"></exception>
|
||||
public AssemblyLoadingSuccessState LoadAssembliesFromLocations([NotNull] IEnumerable<string> filePaths,
|
||||
string friendlyName, ref Guid id)
|
||||
{
|
||||
|
||||
if (filePaths is null)
|
||||
{
|
||||
var exception = new ArgumentNullException(
|
||||
$"{nameof(AssemblyManager)}::{nameof(LoadAssembliesFromLocations)}() | file paths supplied is null!");
|
||||
this.OnException?.Invoke($"Error: {exception.Message}", exception);
|
||||
throw exception;
|
||||
}
|
||||
|
||||
ImmutableList<string> assemblyFilePaths = filePaths.ToImmutableList(); // copy the list before loading
|
||||
|
||||
if (!assemblyFilePaths.Any())
|
||||
{
|
||||
return AssemblyLoadingSuccessState.NoAssemblyFound;
|
||||
}
|
||||
|
||||
if (GetOrCreateACL(id, friendlyName, out var loadedAcl))
|
||||
{
|
||||
var state = loadedAcl.Acl.LoadFromFiles(assemblyFilePaths);
|
||||
// if failure, we dispose of the acl
|
||||
if (state != AssemblyLoadingSuccessState.Success)
|
||||
{
|
||||
DisposeACL(loadedAcl.Id);
|
||||
ModUtils.Logging.PrintError($"ACL {friendlyName} failed, unloading...");
|
||||
return state;
|
||||
}
|
||||
// build types list
|
||||
_subTypesLookupCache.Clear();
|
||||
loadedAcl.RebuildTypesList();
|
||||
id = loadedAcl.Id;
|
||||
foreach (Assembly assembly in loadedAcl.Acl.Assemblies)
|
||||
{
|
||||
OnAssemblyLoaded?.Invoke(assembly);
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
return AssemblyLoadingSuccessState.ACLLoadFailure;
|
||||
}
|
||||
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.Synchronized)]
|
||||
public bool TryBeginDispose()
|
||||
{
|
||||
OpsLockLoaded.EnterWriteLock();
|
||||
OpsLockUnloaded.EnterWriteLock();
|
||||
try
|
||||
{
|
||||
_subTypesLookupCache.Clear();
|
||||
_defaultContextTypes = _defaultContextTypes.Clear();
|
||||
|
||||
foreach (KeyValuePair<Guid, LoadedACL> loadedAcl in LoadedACLs)
|
||||
{
|
||||
if (loadedAcl.Value.Acl is not null)
|
||||
{
|
||||
if (IsReadyToUnloadACL is not null)
|
||||
{
|
||||
foreach (Delegate del in IsReadyToUnloadACL.GetInvocationList())
|
||||
{
|
||||
if (del is System.Func<LoadedACL, bool> { } func)
|
||||
{
|
||||
if (!func.Invoke(loadedAcl.Value))
|
||||
return false; // Not ready, exit
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach (Assembly assembly in loadedAcl.Value.Acl.Assemblies)
|
||||
{
|
||||
OnAssemblyUnloading?.Invoke(assembly);
|
||||
}
|
||||
|
||||
UnloadingACLs.Add(new WeakReference<MemoryFileAssemblyContextLoader>(loadedAcl.Value.Acl, true));
|
||||
loadedAcl.Value.ClearTypesList();
|
||||
loadedAcl.Value.Acl.Unload();
|
||||
loadedAcl.Value.ClearACLRef();
|
||||
OnACLUnload?.Invoke(loadedAcl.Value.Id);
|
||||
}
|
||||
}
|
||||
|
||||
LoadedACLs.Clear();
|
||||
return true;
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
// should never happen
|
||||
this.OnException?.Invoke($"{nameof(TryBeginDispose)}() | Error: {e.Message}", e);
|
||||
return false;
|
||||
}
|
||||
finally
|
||||
{
|
||||
OpsLockUnloaded.ExitWriteLock();
|
||||
OpsLockLoaded.ExitWriteLock();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
public bool FinalizeDispose()
|
||||
{
|
||||
bool isUnloaded;
|
||||
OpsLockUnloaded.EnterUpgradeableReadLock();
|
||||
try
|
||||
{
|
||||
GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced); // force the gc to collect unloaded acls.
|
||||
List<WeakReference<MemoryFileAssemblyContextLoader>> toRemove = new();
|
||||
foreach (WeakReference<MemoryFileAssemblyContextLoader> weakReference in UnloadingACLs)
|
||||
{
|
||||
if (!weakReference.TryGetTarget(out _))
|
||||
{
|
||||
toRemove.Add(weakReference);
|
||||
}
|
||||
}
|
||||
|
||||
if (toRemove.Any())
|
||||
{
|
||||
OpsLockUnloaded.EnterWriteLock();
|
||||
try
|
||||
{
|
||||
foreach (WeakReference<MemoryFileAssemblyContextLoader> reference in toRemove)
|
||||
{
|
||||
UnloadingACLs.Remove(reference);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
OpsLockUnloaded.ExitWriteLock();
|
||||
}
|
||||
}
|
||||
isUnloaded = !UnloadingACLs.Any();
|
||||
}
|
||||
finally
|
||||
{
|
||||
OpsLockUnloaded.ExitUpgradeableReadLock();
|
||||
}
|
||||
|
||||
return isUnloaded;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to retrieve the LoadedACL with the given ID or null if none is found.
|
||||
/// WARNING: External references to this ACL with long lifespans should be kept in a WeakReference
|
||||
/// to avoid causing unloading/disposal issues.
|
||||
/// </summary>
|
||||
/// <param name="id">GUID of the ACL.</param>
|
||||
/// <param name="acl">The found ACL or null if none was found.</param>
|
||||
/// <returns>Whether or not an ACL was found.</returns>
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
public bool TryGetACL(Guid id, out LoadedACL acl)
|
||||
{
|
||||
acl = null;
|
||||
OpsLockLoaded.EnterReadLock();
|
||||
try
|
||||
{
|
||||
if (id.Equals(Guid.Empty) || !LoadedACLs.ContainsKey(id))
|
||||
return false;
|
||||
acl = LoadedACLs[id];
|
||||
return true;
|
||||
}
|
||||
finally
|
||||
{
|
||||
OpsLockLoaded.ExitReadLock();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Gets or creates an AssemblyCtxLoader for the given ID. Creates if the ID is empty or no ACL can be found.
|
||||
/// [IMPORTANT] After calling this method, the id you use should be taken from the acl container (acl.Id).
|
||||
/// </summary>
|
||||
/// <param name="id"></param>
|
||||
/// <param name="friendlyName">A non-unique name for later reference. Optional.</param>
|
||||
/// <param name="acl"></param>
|
||||
/// <returns>Should only return false if an error occurs.</returns>
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private bool GetOrCreateACL(Guid id, string friendlyName, out LoadedACL acl)
|
||||
{
|
||||
OpsLockLoaded.EnterUpgradeableReadLock();
|
||||
try
|
||||
{
|
||||
if (id.Equals(Guid.Empty) || !LoadedACLs.ContainsKey(id) || LoadedACLs[id] is null)
|
||||
{
|
||||
OpsLockLoaded.EnterWriteLock();
|
||||
try
|
||||
{
|
||||
id = Guid.NewGuid();
|
||||
acl = new LoadedACL(id, this, friendlyName);
|
||||
LoadedACLs[id] = acl;
|
||||
return true;
|
||||
}
|
||||
finally
|
||||
{
|
||||
OpsLockLoaded.ExitWriteLock();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
acl = LoadedACLs[id];
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
this.OnException?.Invoke($"{nameof(GetOrCreateACL)}Error: {e.Message}", e);
|
||||
acl = null;
|
||||
return false;
|
||||
}
|
||||
finally
|
||||
{
|
||||
OpsLockLoaded.ExitUpgradeableReadLock();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private bool DisposeACL(Guid id)
|
||||
{
|
||||
OpsLockLoaded.EnterWriteLock();
|
||||
OpsLockUnloaded.EnterWriteLock();
|
||||
try
|
||||
{
|
||||
if (LoadedACLs.ContainsKey(id) && LoadedACLs[id] == null)
|
||||
{
|
||||
if (!LoadedACLs.TryRemove(id, out _))
|
||||
{
|
||||
ModUtils.Logging.PrintWarning($"An ACL with the GUID {id.ToString()} was found as null. Unable to remove null ACL entry.");
|
||||
}
|
||||
}
|
||||
|
||||
if (id.Equals(Guid.Empty) || !LoadedACLs.ContainsKey(id))
|
||||
{
|
||||
return false; // nothing to dispose of
|
||||
}
|
||||
|
||||
var acl = LoadedACLs[id];
|
||||
|
||||
foreach (Assembly assembly in acl.Acl.Assemblies)
|
||||
{
|
||||
OnAssemblyUnloading?.Invoke(assembly);
|
||||
}
|
||||
|
||||
_subTypesLookupCache.Clear();
|
||||
UnloadingACLs.Add(new WeakReference<MemoryFileAssemblyContextLoader>(acl.Acl, true));
|
||||
acl.Acl.Unload();
|
||||
acl.ClearACLRef();
|
||||
OnACLUnload?.Invoke(acl.Id);
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
this.OnException?.Invoke($"{nameof(DisposeACL)}() | Error: {e.Message}", e);
|
||||
return false;
|
||||
}
|
||||
finally
|
||||
{
|
||||
OpsLockLoaded.ExitWriteLock();
|
||||
OpsLockUnloaded.ExitWriteLock();
|
||||
}
|
||||
}
|
||||
|
||||
internal AssemblyManager()
|
||||
{
|
||||
RebuildTypesList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rebuilds the list of types in the default assembly load context.
|
||||
/// </summary>
|
||||
private void RebuildTypesList()
|
||||
{
|
||||
try
|
||||
{
|
||||
_defaultContextTypes = AssemblyLoadContext.Default.Assemblies
|
||||
.SelectMany(a => a.GetSafeTypes())
|
||||
.ToImmutableDictionary(t => t.FullName ?? t.Name, t => t);
|
||||
_subTypesLookupCache.Clear();
|
||||
}
|
||||
catch(ArgumentException ae)
|
||||
{
|
||||
this.OnException?.Invoke($"{nameof(RebuildTypesList)}() | Error: {ae.Message}", ae);
|
||||
try
|
||||
{
|
||||
// some types must've had duplicate type names, build the list while filtering
|
||||
Dictionary<string, Type> types = new();
|
||||
foreach (var type in AssemblyLoadContext.Default.Assemblies.SelectMany(a => a.GetSafeTypes()))
|
||||
{
|
||||
try
|
||||
{
|
||||
types.TryAdd(type.FullName ?? type.Name, type);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignore, null key exception
|
||||
}
|
||||
}
|
||||
|
||||
_defaultContextTypes = types.ToImmutableDictionary();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
this.OnException?.Invoke($"{nameof(RebuildTypesList)}() | Error: {e.Message}", e);
|
||||
ModUtils.Logging.PrintError($"{nameof(AssemblyManager)}: Unable to create list of default assembly types! Default AssemblyLoadContext types searching not available.");
|
||||
#if DEBUG
|
||||
ModUtils.Logging.PrintError($"{nameof(AssemblyManager)}: Exception Details :{e.Message} | {e.InnerException}");
|
||||
#endif
|
||||
_defaultContextTypes = ImmutableDictionary<string, Type>.Empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Data
|
||||
|
||||
private readonly ConcurrentDictionary<string, ImmutableList<Type>> _subTypesLookupCache = new();
|
||||
private ImmutableDictionary<string, Type> _defaultContextTypes;
|
||||
private readonly ConcurrentDictionary<Guid, LoadedACL> LoadedACLs = new();
|
||||
private readonly List<WeakReference<MemoryFileAssemblyContextLoader>> UnloadingACLs= new();
|
||||
private readonly ReaderWriterLockSlim OpsLockLoaded = new ();
|
||||
private readonly ReaderWriterLockSlim OpsLockUnloaded = new ();
|
||||
|
||||
#endregion
|
||||
|
||||
#region TypeDefs
|
||||
|
||||
|
||||
public sealed class LoadedACL
|
||||
{
|
||||
public readonly Guid Id;
|
||||
private ImmutableDictionary<string, Type> _assembliesTypes = ImmutableDictionary<string, Type>.Empty;
|
||||
public MemoryFileAssemblyContextLoader Acl { get; private set; }
|
||||
|
||||
internal LoadedACL(Guid id, AssemblyManager manager, string friendlyName)
|
||||
{
|
||||
this.Id = id;
|
||||
this.Acl = new(manager)
|
||||
{
|
||||
FriendlyName = friendlyName
|
||||
};
|
||||
}
|
||||
public ref readonly ImmutableDictionary<string, Type> AssembliesTypes => ref _assembliesTypes;
|
||||
|
||||
/// <summary>
|
||||
/// Warning: For use by the Assembly Manager only! Do not call this method otherwise.
|
||||
/// </summary>
|
||||
internal void ClearACLRef()
|
||||
{
|
||||
Acl = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rebuild the list of types from assemblies loaded in the AsmCtxLoader.
|
||||
/// </summary>
|
||||
internal void RebuildTypesList()
|
||||
{
|
||||
if (this.Acl is null)
|
||||
{
|
||||
ModUtils.Logging.PrintWarning($"{nameof(RebuildTypesList)}() | ACL with GUID {Id.ToString()} is null, cannot rebuild.");
|
||||
return;
|
||||
}
|
||||
|
||||
ClearTypesList();
|
||||
try
|
||||
{
|
||||
_assembliesTypes = this.Acl.Assemblies
|
||||
.SelectMany(a => a.GetSafeTypes())
|
||||
.ToImmutableDictionary(t => t.FullName ?? t.Name, t => t);
|
||||
}
|
||||
catch(ArgumentException)
|
||||
{
|
||||
// some types must've had duplicate type names, build the list while filtering
|
||||
Dictionary<string, Type> types = new();
|
||||
foreach (var type in this.Acl.Assemblies.SelectMany(a => a.GetSafeTypes()))
|
||||
{
|
||||
try
|
||||
{
|
||||
types.TryAdd(type.FullName ?? type.Name, type);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignore, null key exception
|
||||
}
|
||||
}
|
||||
|
||||
_assembliesTypes = types.ToImmutableDictionary();
|
||||
}
|
||||
}
|
||||
|
||||
internal void ClearTypesList()
|
||||
{
|
||||
_assembliesTypes = ImmutableDictionary<string, Type>.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
public static class AssemblyExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets all types in the given assembly. Handles invalid type scenarios.
|
||||
/// </summary>
|
||||
/// <param name="assembly">The assembly to scan</param>
|
||||
/// <returns>An enumerable collection of types.</returns>
|
||||
public static IEnumerable<Type> GetSafeTypes(this Assembly assembly)
|
||||
{
|
||||
// Based on https://github.com/Qkrisi/ktanemodkit/blob/master/Assets/Scripts/ReflectionHelper.cs#L53-L67
|
||||
|
||||
try
|
||||
{
|
||||
return assembly.GetTypes();
|
||||
}
|
||||
catch (ReflectionTypeLoadException re)
|
||||
{
|
||||
try
|
||||
{
|
||||
return re.Types.Where(x => x != null)!;
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
return new List<Type>();
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return new List<Type>();
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,22 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace Barotrauma;
|
||||
|
||||
public interface IAssemblyPlugin : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Called on plugin normal, use this for basic/core loading that does not rely on any other modded content.
|
||||
/// </summary>
|
||||
void Initialize();
|
||||
|
||||
/// <summary>
|
||||
/// Called once all plugins have been loaded. if you have integrations with any other mod, put that code here.
|
||||
/// </summary>
|
||||
void OnLoadCompleted();
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Called before Barotrauma initializes vanilla content. WARNING: This method may be called before Initialize()!
|
||||
/// </summary>
|
||||
void PreInitPatching();
|
||||
}
|
||||
-340
@@ -1,340 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.Loader;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
// ReSharper disable ConditionIsAlwaysTrueOrFalse
|
||||
|
||||
[assembly: InternalsVisibleTo("CompiledAssembly")]
|
||||
|
||||
namespace Barotrauma;
|
||||
|
||||
/// <summary>
|
||||
/// AssemblyLoadContext to compile from syntax trees in memory and to load from disk/file. Provides dependency resolution.
|
||||
/// [IMPORTANT] Only supports 1 in-memory compiled assembly at a time. Use more instances if you need more.
|
||||
/// [IMPORTANT] All file assemblies required for the compilation of syntax trees should be loaded first.
|
||||
/// </summary>
|
||||
public class MemoryFileAssemblyContextLoader : AssemblyLoadContext
|
||||
{
|
||||
// public
|
||||
public string FriendlyName { get; set; }
|
||||
// ReSharper disable MemberCanBePrivate.Global
|
||||
public Assembly CompiledAssembly { get; private set; }
|
||||
public byte[] CompiledAssemblyImage { get; private set; }
|
||||
// ReSharper restore MemberCanBePrivate.Global
|
||||
// internal
|
||||
private readonly Dictionary<string, AssemblyDependencyResolver> _dependencyResolvers = new(); // path-folder, resolver
|
||||
protected bool IsResolving; //this is to avoid circular dependency lookup.
|
||||
private AssemblyManager _assemblyManager;
|
||||
public bool IsTemplateMode { get; set; }
|
||||
public bool IsDisposed { get; private set; }
|
||||
|
||||
public MemoryFileAssemblyContextLoader(AssemblyManager assemblyManager) : base(isCollectible: true)
|
||||
{
|
||||
this._assemblyManager = assemblyManager;
|
||||
this.IsDisposed = false;
|
||||
base.Unloading += OnUnload;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Try to load the list of disk-file assemblies.
|
||||
/// </summary>
|
||||
/// <param name="assemblyFilePaths">Operation success or failure reason.</param>
|
||||
public AssemblyLoadingSuccessState LoadFromFiles([NotNull] IEnumerable<string> assemblyFilePaths)
|
||||
{
|
||||
if (assemblyFilePaths is null)
|
||||
throw new ArgumentNullException(
|
||||
$"{nameof(MemoryFileAssemblyContextLoader)}::{nameof(LoadFromFiles)}() | The supplied filepath list is null.");
|
||||
|
||||
foreach (string filepath in assemblyFilePaths)
|
||||
{
|
||||
// path verification
|
||||
if (filepath.IsNullOrWhiteSpace())
|
||||
continue;
|
||||
string sanitizedFilePath = System.IO.Path.GetFullPath(filepath.CleanUpPath());
|
||||
string directoryKey = System.IO.Path.GetDirectoryName(sanitizedFilePath);
|
||||
|
||||
if (directoryKey is null)
|
||||
return AssemblyLoadingSuccessState.BadFilePath;
|
||||
|
||||
// setup dep resolver if not available
|
||||
if (!_dependencyResolvers.ContainsKey(directoryKey) || _dependencyResolvers[directoryKey] is null)
|
||||
{
|
||||
_dependencyResolvers[directoryKey] = new AssemblyDependencyResolver(sanitizedFilePath); // supply the first assembly to be loaded
|
||||
}
|
||||
|
||||
// try loading the assemblies
|
||||
try
|
||||
{
|
||||
LoadFromAssemblyPath(sanitizedFilePath);
|
||||
}
|
||||
// on fail of any we're done because we assume that loaded files are related. This ACL needs to be unloaded and collected.
|
||||
catch (ArgumentNullException ane)
|
||||
{
|
||||
ModUtils.Logging.PrintError($"MemFileACL::{nameof(LoadFromFiles)}() | Error loading file path {sanitizedFilePath}. Details: {ane.Message} | {ane.StackTrace}");
|
||||
return AssemblyLoadingSuccessState.BadFilePath;
|
||||
}
|
||||
catch (ArgumentException ae)
|
||||
{
|
||||
ModUtils.Logging.PrintError($"MemFileACL::{nameof(LoadFromFiles)}() | Error loading file path {sanitizedFilePath}. Details: {ae.Message} | {ae.StackTrace}");
|
||||
return AssemblyLoadingSuccessState.BadFilePath;
|
||||
}
|
||||
catch (FileLoadException fle)
|
||||
{
|
||||
ModUtils.Logging.PrintError($"MemFileACL::{nameof(LoadFromFiles)}() | Error loading file path {sanitizedFilePath}. Details: {fle.Message} | {fle.StackTrace}");
|
||||
return AssemblyLoadingSuccessState.CannotLoadFile;
|
||||
}
|
||||
catch (FileNotFoundException fnfe)
|
||||
{
|
||||
ModUtils.Logging.PrintError($"MemFileACL::{nameof(LoadFromFiles)}() | Error loading file path {sanitizedFilePath}. Details: {fnfe.Message} | {fnfe.StackTrace}");
|
||||
return AssemblyLoadingSuccessState.NoAssemblyFound;
|
||||
}
|
||||
catch (BadImageFormatException bife)
|
||||
{
|
||||
ModUtils.Logging.PrintError($"MemFileACL::{nameof(LoadFromFiles)}() | Error loading file path {sanitizedFilePath}. Details: {bife.Message} | {bife.StackTrace}");
|
||||
return AssemblyLoadingSuccessState.InvalidAssembly;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
#if SERVER
|
||||
LuaCsLogger.LogError($"Unable to load dependency assembly file at {filepath.CleanUpPath()} for the assembly named {CompiledAssembly?.FullName}. | Data: {e.Message} | InnerException: {e.InnerException}");
|
||||
#elif CLIENT
|
||||
LuaCsLogger.ShowErrorOverlay($"Unable to load dependency assembly file at {filepath} for the assembly named {CompiledAssembly?.FullName}. | Data: {e.Message} | InnerException: {e.InnerException}");
|
||||
#endif
|
||||
return AssemblyLoadingSuccessState.ACLLoadFailure;
|
||||
}
|
||||
}
|
||||
|
||||
return AssemblyLoadingSuccessState.Success;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Compiles the supplied syntaxtrees and options into an in-memory assembly image.
|
||||
/// Builds metadata from loaded assemblies, only supply your own if you have in-memory images not managed by the
|
||||
/// AssemblyManager class.
|
||||
/// </summary>
|
||||
/// <param name="assemblyName">Name of the assembly. Must be supplied for in-memory assemblies.</param>
|
||||
/// <param name="syntaxTrees">Syntax trees to compile into the assembly.</param>
|
||||
/// <param name="externMetadataReferences">Metadata to be used for compilation.
|
||||
/// [IMPORTANT] This method builds metadata from loaded assemblies, only supply your own if you have in-memory
|
||||
/// images not managed by the AssemblyManager class.</param>
|
||||
/// <param name="compilationOptions">CSharp compilation options. This method automatically adds the 'IgnoreAccessChecks' property for compilation.</param>
|
||||
/// <param name="compilationMessages">Will contain any diagnostic messages for compilation failure.</param>
|
||||
/// <param name="externFileAssemblyReferences">Additional assemblies located in the FileSystem to build metadata references from.
|
||||
/// Assemblies here will have duplicates by the same name that are currently loaded filtered out.</param>
|
||||
/// <returns>Success state of the operation.</returns>
|
||||
/// <exception cref="ArgumentNullException">Throws exception if any of the required arguments are null.</exception>
|
||||
public AssemblyLoadingSuccessState CompileAndLoadScriptAssembly(
|
||||
[NotNull] string assemblyName,
|
||||
[NotNull] IEnumerable<SyntaxTree> syntaxTrees,
|
||||
IEnumerable<MetadataReference> externMetadataReferences,
|
||||
[NotNull] CSharpCompilationOptions compilationOptions,
|
||||
out string compilationMessages,
|
||||
IEnumerable<Assembly> externFileAssemblyReferences = null)
|
||||
{
|
||||
compilationMessages = "";
|
||||
|
||||
if (this.CompiledAssembly is not null)
|
||||
{
|
||||
return AssemblyLoadingSuccessState.AlreadyLoaded;
|
||||
}
|
||||
|
||||
var externAssemblyRefs = externFileAssemblyReferences is not null ? externFileAssemblyReferences.ToImmutableList() : ImmutableList<Assembly>.Empty;
|
||||
var externAssemblyNames = externAssemblyRefs.Any() ? externAssemblyRefs
|
||||
.Where(a => a.FullName is not null)
|
||||
.Select(a => a.FullName).ToImmutableHashSet()
|
||||
: ImmutableHashSet<string>.Empty;
|
||||
|
||||
// verifications
|
||||
if (assemblyName.IsNullOrWhiteSpace())
|
||||
throw new ArgumentNullException(
|
||||
$"{nameof(MemoryFileAssemblyContextLoader)}::{nameof(CompileAndLoadScriptAssembly)}() | The supplied assembly name is null!");
|
||||
|
||||
if (syntaxTrees is null)
|
||||
throw new ArgumentNullException(
|
||||
$"{nameof(MemoryFileAssemblyContextLoader)}::{nameof(CompileAndLoadScriptAssembly)}() | The supplied syntax tree is null!");
|
||||
|
||||
// add external references
|
||||
List<MetadataReference> metadataReferences = new();
|
||||
if (externMetadataReferences is not null)
|
||||
metadataReferences.AddRange(externMetadataReferences);
|
||||
|
||||
// build metadata refs from default where not an in-memory compiled assembly and not the same assembly as supplied.
|
||||
metadataReferences.AddRange(AssemblyLoadContext.Default.Assemblies
|
||||
.Where(a =>
|
||||
{
|
||||
if (a.IsDynamic || string.IsNullOrWhiteSpace(a.Location) || a.Location.Contains("xunit"))
|
||||
return false;
|
||||
if (a.FullName is null)
|
||||
return true;
|
||||
return !externAssemblyNames.Contains(a.FullName); // exclude duplicates
|
||||
})
|
||||
.Select(a => MetadataReference.CreateFromFile(a.Location) as MetadataReference)
|
||||
.Union(externAssemblyRefs // add custom supplied assemblies
|
||||
.Where(a => !(a.IsDynamic || string.IsNullOrEmpty(a.Location) || a.Location.Contains("xunit")))
|
||||
.Select(a => MetadataReference.CreateFromFile(a.Location) as MetadataReference)
|
||||
).ToList());
|
||||
|
||||
ImmutableList<AssemblyManager.LoadedACL> loadedAcls = _assemblyManager.GetAllLoadedACLs().ToImmutableList();
|
||||
if (loadedAcls.Any())
|
||||
{
|
||||
// build metadata refs from ACL assemblies from files/disk.
|
||||
foreach (AssemblyManager.LoadedACL loadedAcl in loadedAcls)
|
||||
{
|
||||
if(loadedAcl?.Acl is null || loadedAcl.Acl.IsTemplateMode || loadedAcl.Acl.IsDisposed)
|
||||
continue;
|
||||
metadataReferences.AddRange(loadedAcl.Acl.Assemblies
|
||||
.Where(a =>
|
||||
{
|
||||
if (a.IsDynamic || string.IsNullOrWhiteSpace(a.Location) || a.Location.Contains("xunit"))
|
||||
return false;
|
||||
if (a.FullName is null)
|
||||
return true;
|
||||
return !externAssemblyNames.Contains(a.FullName); // exclude duplicates
|
||||
})
|
||||
.Select(a => MetadataReference.CreateFromFile(a.Location) as MetadataReference)
|
||||
.Union(externAssemblyRefs // add custom supplied assemblies
|
||||
.Where(a => !(a.IsDynamic || string.IsNullOrEmpty(a.Location) || a.Location.Contains("xunit")))
|
||||
.Select(a => MetadataReference.CreateFromFile(a.Location) as MetadataReference)
|
||||
).ToList());
|
||||
}
|
||||
|
||||
// build metadata refs from in-memory images
|
||||
foreach (var loadedAcl in loadedAcls)
|
||||
{
|
||||
if (loadedAcl?.Acl?.CompiledAssemblyImage is null || loadedAcl.Acl.CompiledAssemblyImage.Length == 0)
|
||||
continue;
|
||||
metadataReferences.Add(MetadataReference.CreateFromImage(loadedAcl.Acl.CompiledAssemblyImage));
|
||||
}
|
||||
}
|
||||
|
||||
// Change inaccessible options to allow public access to restricted members
|
||||
var topLevelBinderFlagsProperty = typeof(CSharpCompilationOptions).GetProperty("TopLevelBinderFlags", BindingFlags.Instance | BindingFlags.NonPublic);
|
||||
topLevelBinderFlagsProperty?.SetValue(compilationOptions, (uint)1 << 22);
|
||||
|
||||
// begin compilation
|
||||
using var memoryCompilation = new MemoryStream();
|
||||
// compile, emit
|
||||
var result = CSharpCompilation.Create(assemblyName, syntaxTrees, metadataReferences, compilationOptions).Emit(memoryCompilation);
|
||||
// check for errors
|
||||
if (!result.Success)
|
||||
{
|
||||
IEnumerable<Diagnostic> failures = result.Diagnostics.Where(d => d.IsWarningAsError || d.Severity == DiagnosticSeverity.Error);
|
||||
foreach (Diagnostic diagnostic in failures)
|
||||
{
|
||||
compilationMessages += $"\n{diagnostic}";
|
||||
}
|
||||
|
||||
return AssemblyLoadingSuccessState.InvalidAssembly;
|
||||
}
|
||||
|
||||
// read compiled assembly from memory stream into an in-memory assembly & image
|
||||
memoryCompilation.Seek(0, SeekOrigin.Begin); // reset
|
||||
try
|
||||
{
|
||||
CompiledAssembly = LoadFromStream(memoryCompilation);
|
||||
CompiledAssemblyImage = memoryCompilation.ToArray();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
#if SERVER
|
||||
LuaCsLogger.LogError($"Unable to load memory assembly from stream. | Data: {e.Message} | InnerException: {e.InnerException}");
|
||||
#elif CLIENT
|
||||
LuaCsLogger.ShowErrorOverlay($"Unable to load memory assembly from stream. | Data: {e.Message} | InnerException: {e.InnerException}");
|
||||
#endif
|
||||
return AssemblyLoadingSuccessState.CannotLoadFromStream;
|
||||
}
|
||||
|
||||
return AssemblyLoadingSuccessState.Success;
|
||||
}
|
||||
|
||||
[SuppressMessage("ReSharper", "ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract")]
|
||||
protected override Assembly Load(AssemblyName assemblyName)
|
||||
{
|
||||
if (IsResolving)
|
||||
return null; //circular resolution fast exit.
|
||||
|
||||
try
|
||||
{
|
||||
IsResolving = true;
|
||||
|
||||
// resolve self collection
|
||||
Assembly ass = this.Assemblies.FirstOrDefault(a =>
|
||||
a.FullName is not null && a.FullName.Equals(assemblyName.FullName), null);
|
||||
if (ass is not null)
|
||||
return ass;
|
||||
|
||||
// resolve to local folders
|
||||
foreach (KeyValuePair<string,AssemblyDependencyResolver> pair in _dependencyResolvers)
|
||||
{
|
||||
var asspath = pair.Value.ResolveAssemblyToPath(assemblyName);
|
||||
if (asspath is null)
|
||||
continue;
|
||||
ass = LoadFromAssemblyPath(asspath);
|
||||
// ReSharper disable once ConditionIsAlwaysTrueOrFalse
|
||||
if (ass is not null)
|
||||
return ass;
|
||||
}
|
||||
|
||||
//try resolve against other loaded alcs
|
||||
ImmutableList<AssemblyManager.LoadedACL> list;
|
||||
try
|
||||
{
|
||||
list = _assemblyManager.UnsafeGetAllLoadedACLs();
|
||||
}
|
||||
catch
|
||||
{
|
||||
list = ImmutableList<AssemblyManager.LoadedACL>.Empty;
|
||||
}
|
||||
|
||||
if (!list.IsEmpty)
|
||||
{
|
||||
foreach (var loadedAcL in list)
|
||||
{
|
||||
if (loadedAcL.Acl is null || loadedAcL.Acl.IsTemplateMode || loadedAcL.Acl.IsDisposed)
|
||||
continue;
|
||||
|
||||
try
|
||||
{
|
||||
ass = loadedAcL.Acl.LoadFromAssemblyName(assemblyName);
|
||||
if (ass is not null)
|
||||
return ass;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// LoadFromAssemblyName throws, no need to propagate
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ass = AssemblyLoadContext.Default.LoadFromAssemblyName(assemblyName);
|
||||
if (ass is not null)
|
||||
return ass;
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsResolving = false;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
private void OnUnload(AssemblyLoadContext alc)
|
||||
{
|
||||
CompiledAssembly = null;
|
||||
CompiledAssemblyImage = null;
|
||||
_dependencyResolvers.Clear();
|
||||
_assemblyManager = null;
|
||||
base.Unloading -= OnUnload;
|
||||
this.IsDisposed = true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,399 @@
|
||||
using Microsoft.Xna.Framework;
|
||||
using Sigil;
|
||||
using Sigil.NonGeneric;
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq.Expressions;
|
||||
using System.Reflection;
|
||||
|
||||
namespace Barotrauma.LuaCs;
|
||||
|
||||
internal static class SigilExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Puts a type on the stack, as a <see cref="Type" /> object instead of a
|
||||
/// runtime type token.
|
||||
/// </summary>
|
||||
/// <param name="il">The IL emitter.</param>
|
||||
/// <param name="type">The type to put on the stack.</param>
|
||||
public static void LoadType(this Emit il, Type type)
|
||||
{
|
||||
if (type == null) throw new ArgumentNullException(nameof(type));
|
||||
il.LoadConstant(type); // ldtoken
|
||||
// This converts the type token into a Type object
|
||||
il.Call(typeof(Type).GetMethod(
|
||||
name: nameof(Type.GetTypeFromHandle),
|
||||
bindingAttr: BindingFlags.Public | BindingFlags.Static,
|
||||
binder: null,
|
||||
types: new Type[] { typeof(RuntimeTypeHandle) },
|
||||
modifiers: null));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts the value on the stack to <see cref="object" />.
|
||||
/// </summary>
|
||||
/// <param name="il">The IL emitter.</param>
|
||||
/// <param name="type">The type of the value on the stack.</param>
|
||||
public static void ToObject(this Emit il, Type type)
|
||||
{
|
||||
if (type == null) throw new ArgumentNullException(nameof(type));
|
||||
il.DerefIfByRef(ref type);
|
||||
if (type.IsValueType)
|
||||
{
|
||||
il.Box(type);
|
||||
}
|
||||
else if (type != typeof(object))
|
||||
{
|
||||
il.CastClass<object>();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deferences the value on stack if the provided type is ByRef.
|
||||
/// </summary>
|
||||
/// <param name="il">The IL emitter.</param>
|
||||
/// <param name="type">The type to check if ByRef.</param>
|
||||
public static void DerefIfByRef(this Emit il, Type type) => il.DerefIfByRef(ref type);
|
||||
|
||||
/// <summary>
|
||||
/// Deferences the value on stack if the provided type is ByRef.
|
||||
/// </summary>
|
||||
/// <param name="il">The IL emitter.</param>
|
||||
/// <param name="type">The type to check if ByRef.</param>
|
||||
public static void DerefIfByRef(this Emit il, ref Type type)
|
||||
{
|
||||
if (type == null) throw new ArgumentNullException(nameof(type));
|
||||
if (type.IsByRef)
|
||||
{
|
||||
type = type.GetElementType();
|
||||
if (type.IsValueType)
|
||||
{
|
||||
il.LoadObject(type);
|
||||
}
|
||||
else
|
||||
{
|
||||
il.LoadIndirect(type);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Copied from https://github.com/evilfactory/moonsharp/blob/5264656c6442e783f3c75082cce69a93d66d4cc0/src/MoonSharp.Interpreter/Interop/Converters/ScriptToClrConversions.cs#L79-L99
|
||||
private static MethodInfo GetImplicitOperatorMethod(Type baseType, Type targetType)
|
||||
{
|
||||
try
|
||||
{
|
||||
return Expression.Convert(Expression.Parameter(baseType, null), targetType).Method;
|
||||
}
|
||||
catch
|
||||
{
|
||||
if (baseType.BaseType != null)
|
||||
{
|
||||
return GetImplicitOperatorMethod(baseType.BaseType, targetType);
|
||||
}
|
||||
|
||||
if (targetType.BaseType != null)
|
||||
{
|
||||
return GetImplicitOperatorMethod(baseType, targetType.BaseType);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads a local variable and casts it to the target type.
|
||||
/// </summary>
|
||||
/// <param name="il">The IL emitter.</param>
|
||||
/// <param name="value">The value to cast. Must be of type <see cref="object" />.</param>
|
||||
/// <param name="targetType">The type to cast into.</param>
|
||||
public static void LoadLocalAndCast(this Emit il, Local value, Type targetType)
|
||||
{
|
||||
if (value == null) throw new ArgumentNullException(nameof(value));
|
||||
if (targetType == null) throw new ArgumentNullException(nameof(targetType));
|
||||
if (value.LocalType != typeof(object))
|
||||
{
|
||||
throw new ArgumentException($"Expected local type {typeof(object)}; got {value.LocalType}.", nameof(value));
|
||||
}
|
||||
|
||||
var guid = Guid.NewGuid().ToString("N");
|
||||
|
||||
if (targetType.IsByRef)
|
||||
{
|
||||
targetType = targetType.GetElementType();
|
||||
}
|
||||
|
||||
// IL: var baseType = value.GetType();
|
||||
var baseType = il.DeclareLocal(typeof(Type), $"cast_baseType_{guid}");
|
||||
il.LoadLocal(value);
|
||||
il.Call(typeof(object).GetMethod("GetType"));
|
||||
il.StoreLocal(baseType);
|
||||
|
||||
// IL: var implicitOperatorMethod = SigilExtensions.GetImplicitOperatorMethod(baseType, <targetType>);
|
||||
var implicitOperatorMethod = il.DeclareLocal(typeof(MethodInfo), $"cast_implicitOperatorMethod_{guid}");
|
||||
il.LoadLocal(baseType);
|
||||
il.LoadType(targetType);
|
||||
il.Call(typeof(SigilExtensions).GetMethod(nameof(GetImplicitOperatorMethod), BindingFlags.NonPublic | BindingFlags.Static));
|
||||
il.StoreLocal(implicitOperatorMethod);
|
||||
|
||||
// IL: <TargetType> castValue;
|
||||
var castValue = il.DeclareLocal(targetType, $"cast_castValue_{guid}");
|
||||
|
||||
// IL: if (implicitConversionMethod != null)
|
||||
il.LoadLocal(implicitOperatorMethod);
|
||||
il.Branch((il) =>
|
||||
{
|
||||
// IL: var methodInvokeParams = new object[1];
|
||||
var methodInvokeParams = il.DeclareLocal(typeof(object[]), $"cast_methodInvokeParams_{guid}");
|
||||
il.LoadConstant(1);
|
||||
il.NewArray(typeof(object));
|
||||
il.StoreLocal(methodInvokeParams);
|
||||
|
||||
// IL: methodInvokeParams[0] = value;
|
||||
il.LoadLocal(methodInvokeParams);
|
||||
il.LoadConstant(0);
|
||||
il.LoadLocal(value);
|
||||
il.StoreElement<object>();
|
||||
|
||||
// IL: castValue = (<TargetType>)implicitConversionMethod.Invoke(null, methodInvokeParams);
|
||||
il.LoadLocal(implicitOperatorMethod);
|
||||
il.LoadNull(); // first parameter is null because implicit cast operators are static
|
||||
il.LoadLocal(methodInvokeParams);
|
||||
il.Call(typeof(MethodInfo).GetMethod("Invoke", new[] { typeof(object), typeof(object[]) }));
|
||||
if (targetType.IsValueType)
|
||||
{
|
||||
il.UnboxAny(targetType);
|
||||
}
|
||||
else
|
||||
{
|
||||
il.CastClass(targetType);
|
||||
}
|
||||
il.StoreLocal(castValue);
|
||||
},
|
||||
(il) =>
|
||||
{
|
||||
// IL: castValue = (<TargetType>)value;
|
||||
il.LoadLocal(value);
|
||||
if (targetType.IsValueType)
|
||||
{
|
||||
il.UnboxAny(targetType);
|
||||
}
|
||||
else
|
||||
{
|
||||
il.CastClass(targetType);
|
||||
}
|
||||
il.StoreLocal(castValue);
|
||||
});
|
||||
|
||||
il.LoadLocal(castValue);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Emits a call to <see cref="string.Format(string, object[])"/>.
|
||||
/// </summary>
|
||||
/// <param name="il">The IL emitter.</param>
|
||||
/// <param name="format">The string format.</param>
|
||||
/// <param name="args">The local variables passed to string.Format.</param>
|
||||
public static void FormatString(this Emit il, string format, params Local[] args)
|
||||
{
|
||||
if (format == null) throw new ArgumentNullException(nameof(format));
|
||||
if (args == null) throw new ArgumentNullException(nameof(args));
|
||||
|
||||
var guid = Guid.NewGuid().ToString("N");
|
||||
|
||||
var listType = typeof(List<>).MakeGenericType(typeof(object));
|
||||
var list = il.DeclareLocal(listType, $"formatString_list_{guid}");
|
||||
il.NewObject(listType);
|
||||
il.StoreLocal(list);
|
||||
|
||||
foreach (var arg in args)
|
||||
{
|
||||
il.LoadLocal(list);
|
||||
il.LoadLocal(arg);
|
||||
il.ToObject(arg.LocalType);
|
||||
il.CallVirtual(listType.GetMethod("Add", new[] { typeof(object) }));
|
||||
}
|
||||
|
||||
var arr = il.DeclareLocal<object[]>($"formatString_arr_{guid}");
|
||||
il.LoadLocal(list);
|
||||
il.CallVirtual(listType.GetMethod("ToArray", new Type[0]));
|
||||
il.StoreLocal(arr);
|
||||
|
||||
il.LoadConstant(format);
|
||||
il.LoadLocal(arr);
|
||||
il.Call(typeof(string).GetMethod("Format", new[] { typeof(string), typeof(object[]) }));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Emits a call to <see cref="DebugConsole.NewMessage(string, Color?, bool)" />.
|
||||
/// </summary>
|
||||
/// <param name="il">The IL emitter.</param>
|
||||
/// <param name="message">The message to print.</param>
|
||||
public static void NewMessage(this Emit il, string message)
|
||||
{
|
||||
var newMessage = typeof(DebugConsole).GetMethod(
|
||||
name: nameof(DebugConsole.NewMessage),
|
||||
bindingAttr: BindingFlags.Public | BindingFlags.Static,
|
||||
binder: null,
|
||||
types: new Type[] { typeof(string), typeof(Color?), typeof(bool) },
|
||||
modifiers: null);
|
||||
il.LoadConstant(message);
|
||||
il.Call(typeof(Color).GetProperty(nameof(Color.LightBlue), BindingFlags.Public | BindingFlags.Static).GetGetMethod());
|
||||
il.LoadConstant(false);
|
||||
il.Call(newMessage);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Emits a call to <see cref="DebugConsole.NewMessage(string, Color?, bool)" />,
|
||||
/// using the string on the stack.
|
||||
/// </summary>
|
||||
/// <param name="il">The IL emitter.</param>
|
||||
public static void NewMessage(this Emit il)
|
||||
{
|
||||
var newMessage = typeof(DebugConsole).GetMethod(
|
||||
name: nameof(DebugConsole.NewMessage),
|
||||
bindingAttr: BindingFlags.Public | BindingFlags.Static,
|
||||
binder: null,
|
||||
types: new Type[] { typeof(string), typeof(Color?), typeof(bool) },
|
||||
modifiers: null);
|
||||
il.Call(typeof(Color).GetProperty(nameof(Color.LightBlue), BindingFlags.Public | BindingFlags.Static).GetGetMethod());
|
||||
il.LoadConstant(false);
|
||||
il.Call(newMessage);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Emits a <c>foreach</c> loop that iterates over an <see cref="IEnumerable{T}"/> local variable.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of elements in the enumerable.</typeparam>
|
||||
/// <param name="il">The IL emitter.</param>
|
||||
/// <param name="enumerable">The enumerable.</param>
|
||||
/// <param name="action">The body of code to run on each iteration.</param>
|
||||
public static void ForEachEnumerable<T>(this Emit il, Local enumerable, Action<Emit, Local, Sigil.Label> action)
|
||||
{
|
||||
if (enumerable == null) throw new ArgumentNullException(nameof(enumerable));
|
||||
if (action == null) throw new ArgumentNullException(nameof(action));
|
||||
if (!typeof(IEnumerable<T>).IsAssignableFrom(enumerable.LocalType))
|
||||
{
|
||||
throw new ArgumentException($"Expected local type {typeof(IEnumerator<T>)}; got {enumerable.LocalType}.", nameof(enumerable));
|
||||
}
|
||||
|
||||
var guid = Guid.NewGuid().ToString("N");
|
||||
|
||||
var enumerator = il.DeclareLocal<IEnumerator<T>>($"forEachEnumerable_enumerator_{guid}");
|
||||
il.LoadLocal(enumerable);
|
||||
il.CallVirtual(typeof(IEnumerable<T>).GetMethod("GetEnumerator"));
|
||||
il.StoreLocal(enumerator);
|
||||
ForEachEnumerator<T>(il, enumerator, action);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Emits a <c>foreach</c> loop that iterates over an <see cref="IEnumerator{T}"/> local variable.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of elements in the enumerable.</typeparam>
|
||||
/// <param name="il">The IL emitter.</param>
|
||||
/// <param name="enumerator">The enumerator.</param>
|
||||
/// <param name="action">The body of code to run on each iteration.</param>
|
||||
public static void ForEachEnumerator<T>(this Emit il, Local enumerator, Action<Emit, Local, Sigil.Label> action)
|
||||
{
|
||||
if (enumerator == null) throw new ArgumentNullException(nameof(enumerator));
|
||||
if (action == null) throw new ArgumentNullException(nameof(action));
|
||||
if (!typeof(IEnumerator<T>).IsAssignableFrom(enumerator.LocalType))
|
||||
{
|
||||
throw new ArgumentException($"Expected local type {typeof(IEnumerator<T>)}; got {enumerator.LocalType}.", nameof(enumerator));
|
||||
}
|
||||
|
||||
var guid = Guid.NewGuid().ToString("N");
|
||||
var labelLoopStart = il.DefineLabel($"forEach_loopStart_{guid}");
|
||||
var labelMoveNext = il.DefineLabel($"forEach_moveNext_{guid}");
|
||||
var labelLeave = il.DefineLabel($"forEach_leave_{guid}");
|
||||
|
||||
il.BeginExceptionBlock(out var exceptionBlock);
|
||||
il.Branch(labelMoveNext); // MoveNext() needs to be called at least once before iterating
|
||||
il.MarkLabel(labelLoopStart);
|
||||
|
||||
// IL: var current = enumerator.Current;
|
||||
var current = il.DeclareLocal<T>($"forEachEnumerator_current_{guid}");
|
||||
il.LoadLocal(enumerator);
|
||||
il.CallVirtual(enumerator.LocalType.GetProperty("Current").GetGetMethod());
|
||||
il.StoreLocal(current);
|
||||
|
||||
action(il, current, labelLeave);
|
||||
|
||||
il.MarkLabel(labelMoveNext);
|
||||
il.LoadLocal(enumerator);
|
||||
il.CallVirtual(typeof(IEnumerator).GetMethod("MoveNext"));
|
||||
il.BranchIfTrue(labelLoopStart); // loop if MoveNext() returns true
|
||||
|
||||
// IL: finally { enumerator.Dispose(); }
|
||||
il.BeginFinallyBlock(exceptionBlock, out var finallyBlock);
|
||||
il.LoadLocal(enumerator);
|
||||
il.CallVirtual(typeof(IDisposable).GetMethod("Dispose"));
|
||||
il.EndFinallyBlock(finallyBlock);
|
||||
|
||||
il.EndExceptionBlock(exceptionBlock);
|
||||
|
||||
il.MarkLabel(labelLeave);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Emits a branch that only executes if the last value on the stack
|
||||
/// is truthy (e.g. non-null references, 1, etc).
|
||||
/// </summary>
|
||||
/// <param name="il">The IL emitter.</param>
|
||||
/// <param name="action">The body of code to run if the value is truthy.</param>
|
||||
public static void If(this Emit il, Action<Emit> action)
|
||||
{
|
||||
if (action == null) throw new ArgumentNullException(nameof(action));
|
||||
il.Branch(@if: action);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Emits a branch that only executes if the last value on the stack
|
||||
/// is falsy (e.g. null references, 0, etc).
|
||||
/// </summary>
|
||||
/// <param name="il">The IL emitter.</param>
|
||||
/// <param name="action">The body of code to run if the value is falsy.</param>
|
||||
public static void IfNot(this Emit il, Action<Emit> action)
|
||||
{
|
||||
if (action == null) throw new ArgumentNullException(nameof(action));
|
||||
il.Branch(@else: action);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Emits two branches that diverge based on a condition -- analogous
|
||||
/// to an if-else statement. If either <paramref name="if"/>
|
||||
/// or <paramref name="else"/> are omitted, it behaves the same as
|
||||
/// <see cref="If(Emit, Action{Emit})"/>
|
||||
/// and <see cref="IfNot(Emit, Action{Emit})"/>.
|
||||
/// </summary>
|
||||
/// <param name="il">The IL emitter.</param>
|
||||
/// <param name="if">The body of code to run if the value is truthy.</param>
|
||||
/// <param name="else">The body of code to run if the value is falsy.</param>
|
||||
public static void Branch(this Emit il, Action<Emit> @if = null, Action<Emit> @else = null)
|
||||
{
|
||||
if (@if == null && @else == null) throw new ArgumentException("At least one of the two branches must be defined.");
|
||||
|
||||
var guid = Guid.NewGuid().ToString("N");
|
||||
var labelEnd = il.DefineLabel($"branch_end_{guid}");
|
||||
if (@if != null && @else != null)
|
||||
{
|
||||
var labelElse = il.DefineLabel($"branch_else_{guid}");
|
||||
il.BranchIfFalse(labelElse);
|
||||
@if(il);
|
||||
il.Branch(labelEnd);
|
||||
il.MarkLabel(labelElse);
|
||||
@else(il);
|
||||
}
|
||||
else if (@if != null)
|
||||
{
|
||||
il.BranchIfFalse(labelEnd);
|
||||
@if(il);
|
||||
}
|
||||
else
|
||||
{
|
||||
il.BranchIfTrue(labelEnd);
|
||||
@else(il);
|
||||
}
|
||||
il.MarkLabel(labelEnd);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.Toolkit.Diagnostics;
|
||||
|
||||
namespace Barotrauma.LuaCs;
|
||||
|
||||
public class StateMachine<T> where T : Enum
|
||||
{
|
||||
private readonly ConcurrentDictionary<T, State<T>> _states;
|
||||
private State<T> _currentState;
|
||||
public T CurrentState => _currentState.StateId;
|
||||
private bool _errorOnSameStateSelected;
|
||||
private readonly AsyncReaderWriterLock _operationsLock = new();
|
||||
|
||||
public StateMachine(bool errorOnSameState, T defaultState, Action<State<T>> onEnter, Action<State<T>> onExit)
|
||||
{
|
||||
_errorOnSameStateSelected = errorOnSameState;
|
||||
_states = new ConcurrentDictionary<T, State<T>>();
|
||||
var defState = new State<T>(defaultState, onEnter, onExit);
|
||||
_currentState = defState;
|
||||
_states[defaultState] = defState;
|
||||
}
|
||||
|
||||
public StateMachine<T> AddState(T stateId, Action<State<T>> onEnter, Action<State<T>> onExit)
|
||||
{
|
||||
using var lck = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
if (_states.TryGetValue(stateId, out _))
|
||||
{
|
||||
ThrowHelper.ThrowArgumentException($"State with id {stateId} already exists.");
|
||||
}
|
||||
|
||||
_states[stateId] = new State<T>(stateId, onEnterState: onEnter, onExitState: onExit);
|
||||
return this;
|
||||
}
|
||||
|
||||
public StateMachine<T> RemoveState(T stateId)
|
||||
{
|
||||
using var lck = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
if (EqualityComparer<T>.Default.Equals(stateId, CurrentState))
|
||||
{
|
||||
ThrowHelper.ThrowInvalidOperationException($"State with id {CurrentState} is active. Cannot remove.");
|
||||
}
|
||||
|
||||
_states.TryRemove(stateId, out _);
|
||||
return this;
|
||||
}
|
||||
|
||||
public StateMachine<T> AddOrReplaceState(T oldStateId, T newStateId, Action<State<T>> onEnter, Action<State<T>> onExit)
|
||||
{
|
||||
using var lck = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
if (EqualityComparer<T>.Default.Equals(oldStateId, CurrentState))
|
||||
{
|
||||
ThrowHelper.ThrowInvalidOperationException($"State with id {CurrentState} is active. Cannot replace.");
|
||||
}
|
||||
|
||||
_states[oldStateId] = new State<T>(newStateId, onEnter, onExit);
|
||||
return this;
|
||||
}
|
||||
|
||||
public StateMachine<T> GotoState(T stateId)
|
||||
{
|
||||
using var lck = _operationsLock.AcquireWriterLock().ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
if (EqualityComparer<T>.Default.Equals(stateId, CurrentState))
|
||||
{
|
||||
if (_errorOnSameStateSelected)
|
||||
{
|
||||
ThrowHelper.ThrowInvalidOperationException($"State with id {stateId} is already selected.");
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
if (!_states.TryGetValue(stateId, out var newState))
|
||||
{
|
||||
ThrowHelper.ThrowArgumentNullException($"Target state with id {stateId} does not exist.");
|
||||
}
|
||||
|
||||
_currentState.OnExit();
|
||||
_currentState = newState;
|
||||
_currentState.OnEnter();
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
public class State<T> where T : Enum
|
||||
{
|
||||
public T StateId;
|
||||
private Action<State<T>> _onEnter, _onExit;
|
||||
public State(T stateId, Action<State<T>> onEnterState, Action<State<T>> onExitState)
|
||||
{
|
||||
StateId = stateId;
|
||||
_onEnter = onEnterState;
|
||||
_onExit = onExitState;
|
||||
}
|
||||
|
||||
public virtual void OnEnter()
|
||||
{
|
||||
_onEnter?.Invoke(this);
|
||||
}
|
||||
|
||||
public virtual void OnExit()
|
||||
{
|
||||
_onExit?.Invoke(this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
using System;
|
||||
|
||||
namespace Barotrauma.LuaCs;
|
||||
|
||||
public partial interface INetCallback
|
||||
{
|
||||
public ushort CallbackId { get; }
|
||||
}
|
||||
|
||||
#if SERVER
|
||||
public partial interface INetCallback
|
||||
{
|
||||
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,37 @@
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Barotrauma.Items.Components;
|
||||
using Barotrauma.LuaCs.Data;
|
||||
|
||||
namespace Barotrauma.LuaCs;
|
||||
|
||||
/// <summary>
|
||||
/// Provides a deterministic ID for a given <see cref="IDataInfo"/> instance under multiple circumstances, for use with
|
||||
/// network synchronization.
|
||||
/// </summary>
|
||||
internal interface INetworkIdProvider : IService
|
||||
{
|
||||
/// <summary>
|
||||
/// Deterministically generates a GUID for the given parameters.
|
||||
/// </summary>
|
||||
/// <param name="instance">The instance.</param>
|
||||
/// <returns>The GUID for the entity.</returns>
|
||||
Guid GetNetworkIdForInstance([NotNull] IDataInfo instance);
|
||||
|
||||
/// <summary>
|
||||
/// Deterministically generates a GUID for the given parameters.
|
||||
/// </summary>
|
||||
/// <param name="instance">The instance.</param>
|
||||
/// <param name="attachedEntity">The <see cref="Entity"/> that this instance is attached to, if any.</param>
|
||||
/// <typeparam name="TEntity">The entity type, if any.</typeparam>
|
||||
/// <returns>The GUID for the entity.</returns>
|
||||
Guid GetNetworkIdForInstance<TEntity>([NotNull] IDataInfo instance, TEntity attachedEntity) where TEntity : Entity;
|
||||
|
||||
/// <summary>
|
||||
/// Deterministically generates a GUID for the given parameters.
|
||||
/// </summary>
|
||||
/// <param name="instance">The instance.</param>
|
||||
/// <param name="attachedItemComponent">The <see cref="ItemComponent"/> that this instance is attached to, if any.</param>
|
||||
/// <returns>The GUID for the entity.</returns>
|
||||
Guid GetNetworkIdForInstance([NotNull] IDataInfo instance, [MaybeNull] ItemComponent attachedItemComponent);
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
using System;
|
||||
using Barotrauma.LuaCs.Data;
|
||||
using Barotrauma.LuaCs;
|
||||
using Barotrauma.Networking;
|
||||
|
||||
namespace Barotrauma.LuaCs;
|
||||
|
||||
public interface INetworkSyncVar : IDataInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// Network-synchronized object ID. Used for networking send/receive message events.
|
||||
/// </summary>
|
||||
Guid InstanceId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Sets the <see cref="IEntityNetworkingService"/> that is currently managing this instance. The <see cref="InstanceId"/>
|
||||
/// is retrieved from here.
|
||||
/// </summary>
|
||||
/// <param name="networkingService">The networking service managing this instance or null to deregister.</param>
|
||||
void SetNetworkOwner(IEntityNetworkingService networkingService);
|
||||
|
||||
/// <summary>
|
||||
/// Synchronization type. See <see cref="NetSync"/> for more information.
|
||||
/// </summary>
|
||||
NetSync SyncType { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Permissions needed by clients to send net-events and/or receive net messages.
|
||||
/// </summary>
|
||||
ClientPermissions WritePermissions { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Called when an incoming net message has data for this network object, typically from the same entity on another
|
||||
/// machine.
|
||||
/// </summary>
|
||||
/// <param name="message">Wrapper for the internal type: <see cref="IReadMessage"/></param>
|
||||
void ReadNetMessage(IReadMessage message);
|
||||
|
||||
/// <summary>
|
||||
/// Called when a network send-event involving this entity is triggered. Any data expected to be read by the recipient
|
||||
/// network object on the other instance(s) should be written to the packet.
|
||||
/// </summary>
|
||||
/// <param name="message">Wrapper for the internal type: <see cref="IWriteMessage"/></param>
|
||||
void WriteNetMessage(IWriteMessage message);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Specifies the networking send/receive relationship for network object. Objects implementing this interface are
|
||||
/// expected to adhere to the contract or de-sync may occur.
|
||||
/// </summary>
|
||||
public enum NetSync
|
||||
{
|
||||
/// <summary>
|
||||
/// No network synchronization.
|
||||
/// </summary>
|
||||
None,
|
||||
/// <summary>
|
||||
/// Both the client and the server have 'send' and 'receive' permissions (limited by <see cref="ClientPermissions"/>). Can also be used to allow two-way communication
|
||||
/// with the server.
|
||||
/// </summary>
|
||||
TwoWay,
|
||||
/// <summary>
|
||||
/// Only the host/server has the authority to change this value.
|
||||
/// </summary>
|
||||
ServerAuthority,
|
||||
/// <summary>
|
||||
/// Only clients (with the required by <see cref="ClientPermissions"/>) may change the value and all value changes are communicated to the server/host.
|
||||
/// <br/><br/><b>[Important] The host/server will not send the value to other connected clients.</b><br/>
|
||||
/// Intended to allow clients to send one-way messages to the server.
|
||||
/// </summary>
|
||||
ClientOneWay
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
using System;
|
||||
using Barotrauma.Items.Components;
|
||||
using Barotrauma.LuaCs.Data;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
||||
namespace Barotrauma.LuaCs;
|
||||
|
||||
internal class NetworkingIdProvider : INetworkIdProvider
|
||||
{
|
||||
public void Dispose()
|
||||
{
|
||||
//stateless service
|
||||
}
|
||||
|
||||
public bool IsDisposed => false;
|
||||
|
||||
private Guid GetNetworkIdFromStringMd5(string id)
|
||||
{
|
||||
return new Guid(MD5.Create().ComputeHash(Encoding.ASCII.GetBytes(id)));
|
||||
}
|
||||
|
||||
public Guid GetNetworkIdForInstance(IDataInfo instance)
|
||||
{
|
||||
var str = $"{instance.OwnerPackage.Name}.{instance.InternalName}";
|
||||
return GetNetworkIdFromStringMd5(str);
|
||||
}
|
||||
|
||||
public Guid GetNetworkIdForInstance<TEntity>(IDataInfo instance, TEntity attachedEntity) where TEntity : Entity
|
||||
{
|
||||
var str = $"{nameof(TEntity)}({attachedEntity.ID}).{instance.OwnerPackage.Name}.{instance.InternalName}";
|
||||
return GetNetworkIdFromStringMd5(str);
|
||||
}
|
||||
|
||||
public Guid GetNetworkIdForInstance(IDataInfo instance, ItemComponent attachedItemComponent)
|
||||
{
|
||||
var attachedEntity = attachedItemComponent.Item;
|
||||
var str = $"{attachedEntity.GetType().Name}({attachedEntity.ID}).ComponentId({attachedEntity.Components.IndexOf(attachedItemComponent)}).{instance.OwnerPackage.Name}.{instance.InternalName}";
|
||||
return GetNetworkIdFromStringMd5(str);
|
||||
}
|
||||
}
|
||||
+5
-11
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using Barotrauma.LuaCs;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
@@ -8,9 +9,11 @@ namespace Barotrauma
|
||||
public abstract class ACsMod : IAssemblyPlugin
|
||||
{
|
||||
private static List<ACsMod> mods = new List<ACsMod>();
|
||||
[Obsolete("$This does nothing. Stop using it!")]
|
||||
public static List<ACsMod> LoadedMods { get => mods; }
|
||||
|
||||
private const string MOD_STORE = "LocalMods/.modstore";
|
||||
[Obsolete("$This does nothing. Stop using it!")]
|
||||
public static string GetStoreFolder<T>() where T : ACsMod
|
||||
{
|
||||
if (!Directory.Exists(MOD_STORE)) Directory.CreateDirectory(MOD_STORE);
|
||||
@@ -19,14 +22,7 @@ namespace Barotrauma
|
||||
return modFolder;
|
||||
}
|
||||
|
||||
public bool IsDisposed { get; private set; }
|
||||
|
||||
/// Mod initialization
|
||||
public ACsMod()
|
||||
{
|
||||
IsDisposed = false;
|
||||
LoadedMods.Add(this);
|
||||
}
|
||||
public bool IsDisposed { get; private set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Called as soon as plugin loading begins, use this for internal setup only.
|
||||
@@ -52,10 +48,8 @@ namespace Barotrauma
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
LuaCsLogger.HandleException(e, LuaCsMessageOrigin.CSharpMod);
|
||||
LuaCsSetup.Instance.Logger.HandleException(e);
|
||||
}
|
||||
|
||||
LoadedMods.Remove(this);
|
||||
IsDisposed = true;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,721 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Runtime.Loader;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using Barotrauma.Extensions;
|
||||
using Barotrauma.LuaCs;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using FluentResults;
|
||||
using FluentResults.LuaCs;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using OneOf;
|
||||
using Path = System.IO.Path;
|
||||
|
||||
[assembly: InternalsVisibleTo(IAssemblyLoaderService.InternalsAwareAssemblyName)]
|
||||
|
||||
namespace Barotrauma.LuaCs;
|
||||
public sealed class AssemblyLoader : AssemblyLoadContext, IAssemblyLoaderService
|
||||
{
|
||||
public class Factory : IAssemblyLoaderService.IFactory
|
||||
{
|
||||
public IAssemblyLoaderService CreateInstance(IAssemblyLoaderService.LoaderInitData initData)
|
||||
{
|
||||
return new AssemblyLoader(initData);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
//stateless service
|
||||
}
|
||||
public bool IsDisposed => false;
|
||||
}
|
||||
|
||||
public Guid Id { get; init; }
|
||||
public ContentPackage OwnerPackage { get; private set; }
|
||||
public bool IsReferenceOnlyMode { get; init; }
|
||||
public bool IsDisposed
|
||||
{
|
||||
get => ModUtils.Threading.GetBool(ref _isDisposed);
|
||||
private set => ModUtils.Threading.SetBool(ref _isDisposed, value);
|
||||
}
|
||||
private int _isDisposed;
|
||||
|
||||
/// <summary>
|
||||
/// This bool-int wrapper increments/decrements when set as true/false respectively and return true if the value > 0.
|
||||
/// </summary>
|
||||
private bool AreOperationRunning
|
||||
{
|
||||
get => Interlocked.CompareExchange(ref _operationsRunning, 0, 0) > 0;
|
||||
set // we use the set as our inc/decr
|
||||
{
|
||||
if (value)
|
||||
{
|
||||
Interlocked.Add(ref _operationsRunning, 1);
|
||||
}
|
||||
else
|
||||
{
|
||||
Interlocked.Add(ref _operationsRunning, -1);
|
||||
}
|
||||
}
|
||||
}
|
||||
private int _operationsRunning;
|
||||
|
||||
//internal
|
||||
private readonly Action<IAssemblyLoaderService> _onUnload;
|
||||
private readonly Func<IAssemblyLoaderService, AssemblyName, Assembly> _onResolvingManaged;
|
||||
private readonly Func<Assembly, string, IntPtr> _onResolvingUnmanagedDll;
|
||||
private readonly ConcurrentDictionary<string, AssemblyDependencyResolver> _dependencyResolvers = new();
|
||||
private readonly ConcurrentDictionary<AssemblyOrStringKey, AssemblyData> _loadedAssemblyData = new();
|
||||
|
||||
private readonly ThreadLocal<bool> _isResolving = new(static()=>false); // cyclic resolution exit
|
||||
private readonly ThreadLocal<bool> _isResolvingNative = new(static () => false);
|
||||
|
||||
public AssemblyLoader(IAssemblyLoaderService.LoaderInitData initData)
|
||||
: base(isCollectible: true, name: initData.Name)
|
||||
{
|
||||
Id = initData.InstanceId;
|
||||
IsReferenceOnlyMode = initData.IsReferenceMode;
|
||||
this._onUnload = initData.OnUnload;
|
||||
this._onResolvingManaged = initData.OnResolvingManaged;
|
||||
this._onResolvingUnmanagedDll = initData.OnResolvingUnmanagedDll;
|
||||
this.OwnerPackage = initData.OwnerPackage;
|
||||
base.Unloading += OnUnload;
|
||||
base.Resolving += OnResolvingManagedAssembly;
|
||||
base.ResolvingUnmanagedDll += OnResolvingUnmanagedDll;
|
||||
}
|
||||
|
||||
private IntPtr OnResolvingUnmanagedDll(Assembly invokingAssembly, string assemblyName)
|
||||
{
|
||||
if (IsDisposed)
|
||||
return 0;
|
||||
|
||||
if (_isResolvingNative.Value)
|
||||
return 0;
|
||||
|
||||
AreOperationRunning = true;
|
||||
_isResolvingNative.Value = true;
|
||||
try
|
||||
{
|
||||
if (!_dependencyResolvers.IsEmpty)
|
||||
{
|
||||
foreach (var resolver in _dependencyResolvers)
|
||||
{
|
||||
try
|
||||
{
|
||||
var path = resolver.Value.ResolveUnmanagedDllToPath(assemblyName);
|
||||
if (path.IsNullOrWhiteSpace())
|
||||
continue;
|
||||
return base.LoadUnmanagedDllFromPath(path);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignored
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (_onResolvingUnmanagedDll is not null)
|
||||
{
|
||||
try
|
||||
{
|
||||
return _onResolvingUnmanagedDll(invokingAssembly, assemblyName);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
finally
|
||||
{
|
||||
AreOperationRunning = false;
|
||||
_isResolvingNative.Value = false;
|
||||
}
|
||||
}
|
||||
|
||||
private Assembly OnResolvingManagedAssembly(AssemblyLoadContext assemblyLoadContext, AssemblyName assemblyName)
|
||||
{
|
||||
if (IsDisposed)
|
||||
return null;
|
||||
|
||||
if (_isResolving.Value)
|
||||
return null;
|
||||
|
||||
if (assemblyLoadContext != this)
|
||||
return null;
|
||||
|
||||
AreOperationRunning = true;
|
||||
_isResolving.Value = true;
|
||||
try
|
||||
{
|
||||
if (!_dependencyResolvers.IsEmpty)
|
||||
{
|
||||
foreach (var resolver in _dependencyResolvers)
|
||||
{
|
||||
try
|
||||
{
|
||||
var path = resolver.Value.ResolveAssemblyToPath(assemblyName);
|
||||
if (path.IsNullOrWhiteSpace())
|
||||
continue;
|
||||
return assemblyLoadContext.LoadFromAssemblyPath(path);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignored
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (_onResolvingManaged is not null)
|
||||
{
|
||||
try
|
||||
{
|
||||
return _onResolvingManaged(this, assemblyName);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
finally
|
||||
{
|
||||
AreOperationRunning = false;
|
||||
_isResolving.Value = false;
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerable<MetadataReference> AssemblyReferences
|
||||
{
|
||||
get
|
||||
{
|
||||
if (IsDisposed || _loadedAssemblyData.IsEmpty)
|
||||
yield return null;
|
||||
AreOperationRunning = true;
|
||||
foreach (var data in _loadedAssemblyData.Values)
|
||||
{
|
||||
if (data.AssemblyReference is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
yield return data.AssemblyReference;
|
||||
}
|
||||
AreOperationRunning = false;
|
||||
}
|
||||
}
|
||||
|
||||
public FluentResults.Result AddDependencyPaths(ImmutableArray<string> paths)
|
||||
{
|
||||
if (IsDisposed)
|
||||
return FluentResults.Result.Fail($"Loader is disposed!");
|
||||
AreOperationRunning = true;
|
||||
try
|
||||
{
|
||||
if (paths.Length == 0)
|
||||
return FluentResults.Result.Ok();
|
||||
var res = new FluentResults.Result();
|
||||
foreach (var path in paths)
|
||||
{
|
||||
try
|
||||
{
|
||||
var p = Path.GetFullPath(path.CleanUpPath());
|
||||
if (!_dependencyResolvers.ContainsKey(p))
|
||||
{
|
||||
_dependencyResolvers[p] = new AssemblyDependencyResolver(p);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
res = res.WithError(new ExceptionalError(ex)
|
||||
.WithMetadata(MetadataType.Sources, path));
|
||||
}
|
||||
}
|
||||
|
||||
if (res.Errors.Any())
|
||||
return FluentResults.Result.Fail(res.Errors);
|
||||
return FluentResults.Result.Ok();
|
||||
}
|
||||
finally
|
||||
{
|
||||
AreOperationRunning = false;
|
||||
}
|
||||
}
|
||||
|
||||
public Result<Assembly> CompileScriptAssembly([NotNull] string assemblyName,
|
||||
bool compileWithInternalAccess,
|
||||
ImmutableArray<SyntaxTree> syntaxTrees,
|
||||
ImmutableArray<MetadataReference> metadataReferences,
|
||||
CSharpCompilationOptions compilationOptions = null)
|
||||
{
|
||||
if (IsDisposed)
|
||||
return FluentResults.Result.Fail($"Loader is disposed!");
|
||||
AreOperationRunning = true;
|
||||
try
|
||||
{
|
||||
if (assemblyName.IsNullOrWhiteSpace())
|
||||
{
|
||||
return new Result<Assembly>().WithError(new Error($"The name provided is null!")
|
||||
.WithMetadata(MetadataType.ExceptionObject, this)
|
||||
.WithMetadata(MetadataType.RootObject, syntaxTrees));
|
||||
}
|
||||
|
||||
if (_loadedAssemblyData.ContainsKey(assemblyName))
|
||||
{
|
||||
return new Result<Assembly>().WithError(
|
||||
new Error($"The name provided is already assigned to an assembly!")
|
||||
.WithMetadata(MetadataType.ExceptionObject, this)
|
||||
.WithMetadata(MetadataType.RootObject, syntaxTrees));
|
||||
}
|
||||
|
||||
var compilationAssemblyName = compileWithInternalAccess
|
||||
? IAssemblyLoaderService.InternalsAwareAssemblyName
|
||||
: assemblyName;
|
||||
|
||||
compilationOptions ??= new CSharpCompilationOptions(
|
||||
outputKind: OutputKind.DynamicallyLinkedLibrary,
|
||||
optimizationLevel: OptimizationLevel.Release,
|
||||
concurrentBuild: true,
|
||||
reportSuppressedDiagnostics: false,
|
||||
warningLevel: 0,
|
||||
allowUnsafe: true);
|
||||
|
||||
if (!compileWithInternalAccess)
|
||||
{
|
||||
typeof(CSharpCompilationOptions)
|
||||
.GetProperty("TopLevelBinderFlags", BindingFlags.Instance | BindingFlags.NonPublic)
|
||||
?.SetValue(compilationOptions,
|
||||
(uint)1 << 25 // CSharp.BinderFlags.AllowAwaitInUnsafeContext
|
||||
| (uint)1 << 22 // CSharp.BinderFlags.IgnoreAccessibility
|
||||
| (uint)1 << 1 // CSharp.BinderFlags.SuppressObsoleteChecks
|
||||
);
|
||||
}
|
||||
|
||||
using var asmMemoryStream = new MemoryStream();
|
||||
var result = CSharpCompilation
|
||||
.Create(compilationAssemblyName, syntaxTrees,
|
||||
metadataReferences, compilationOptions)
|
||||
.Emit(asmMemoryStream);
|
||||
if (!result.Success)
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
foreach (var resultDiagnostic in result.Diagnostics)
|
||||
{
|
||||
if (resultDiagnostic.IsWarningAsError || resultDiagnostic.Severity == DiagnosticSeverity.Error)
|
||||
{
|
||||
//sb.AppendLine($">>> {resultDiagnostic.GetMessage()} | Location: {resultDiagnostic.Location.SourceTree?.GetLineSpan(resultDiagnostic.Location.SourceSpan)} ");
|
||||
sb.AppendLine($"\n{resultDiagnostic}");
|
||||
}
|
||||
}
|
||||
var res = new FluentResults.Result().WithError(
|
||||
new Error($"Package Error: {OwnerPackage.Name}: Compilation failed for assembly {assemblyName}!\n {sb.ToString()}\n")
|
||||
.WithMetadata(MetadataType.ExceptionObject, this)
|
||||
.WithMetadata(MetadataType.RootObject, syntaxTrees));
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
asmMemoryStream.Seek(0, SeekOrigin.Begin);
|
||||
var data = new AssemblyData(LoadFromStream(asmMemoryStream), asmMemoryStream.ToArray());
|
||||
_loadedAssemblyData[data.Assembly] = data;
|
||||
return new Result<Assembly>().WithSuccess($"Compiled assembly {assemblyName} successful.")
|
||||
.WithValue(data.Assembly);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new FluentResults.Result().WithError(new ExceptionalError(ex)
|
||||
.WithMetadata(MetadataType.ExceptionObject, this)
|
||||
.WithMetadata(MetadataType.RootObject, assemblyName)
|
||||
.WithMetadata(MetadataType.Sources, syntaxTrees));
|
||||
}
|
||||
finally
|
||||
{
|
||||
AreOperationRunning = false;
|
||||
}
|
||||
}
|
||||
|
||||
public FluentResults.Result<Assembly> LoadAssemblyFromFile(string assemblyFilePath,
|
||||
ImmutableArray<string> additionalDependencyPaths)
|
||||
{
|
||||
if (IsDisposed)
|
||||
return FluentResults.Result.Fail($"Loader is disposed!");
|
||||
|
||||
AreOperationRunning = true;
|
||||
try
|
||||
{
|
||||
if (assemblyFilePath.IsNullOrWhiteSpace())
|
||||
return new Result<Assembly>().WithError(new Error($"The path provided is empty."));
|
||||
|
||||
if (additionalDependencyPaths.Any())
|
||||
{
|
||||
var r = AddDependencyPaths(additionalDependencyPaths);
|
||||
if (r.IsFailed)
|
||||
{
|
||||
// we have errors, loading may not work.
|
||||
return FluentResults.Result.Fail(new Error($"Failed to load dependency paths for '{assemblyFilePath}' with paths: {additionalDependencyPaths.Aggregate((s, ac) => $"{ac}| P={s}")}.")
|
||||
.WithMetadata(MetadataType.ExceptionObject, this)
|
||||
.WithMetadata(MetadataType.RootObject, assemblyFilePath))
|
||||
.WithErrors(r.Errors);
|
||||
}
|
||||
}
|
||||
|
||||
string sanitizedFilePath = Path.GetFullPath(assemblyFilePath.CleanUpPath());
|
||||
string directoryKey = Path.GetDirectoryName(sanitizedFilePath);
|
||||
|
||||
if (directoryKey is null)
|
||||
{
|
||||
return FluentResults.Result.Fail(new Error($"Unable to load assembly: bath file path: {assemblyFilePath}")
|
||||
.WithMetadata(MetadataType.ExceptionObject, this)
|
||||
.WithMetadata(MetadataType.RootObject, sanitizedFilePath));
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var assembly = LoadFromAssemblyPath(sanitizedFilePath);
|
||||
_loadedAssemblyData[assembly] = new AssemblyData(assembly, assembly.Location);
|
||||
return new Result<Assembly>().WithSuccess($"Loaded assembly '{assembly.GetName()}'").WithValue(assembly);
|
||||
}
|
||||
catch (FileNotFoundException fnfe)
|
||||
{
|
||||
// last attempt
|
||||
try
|
||||
{
|
||||
var assemblyName = new AssemblyName(System.IO.Path.GetFileName(sanitizedFilePath));
|
||||
foreach (var resolver in _dependencyResolvers)
|
||||
{
|
||||
try
|
||||
{
|
||||
var path = resolver.Value.ResolveAssemblyToPath(assemblyName);
|
||||
return base.LoadFromAssemblyPath(path);
|
||||
}
|
||||
catch
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
return GenerateExceptionReturn(fnfe);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return GenerateExceptionReturn(fnfe);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return GenerateExceptionReturn(e);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
AreOperationRunning = false;
|
||||
}
|
||||
|
||||
FluentResults.Result<Assembly> GenerateExceptionReturn<T>(T exception) where T : Exception
|
||||
{
|
||||
return FluentResults.Result.Fail<Assembly>(new ExceptionalError(exception)
|
||||
.WithMetadata(MetadataType.ExceptionObject, this)
|
||||
.WithMetadata(MetadataType.RootObject, assemblyFilePath)
|
||||
.WithMetadata(MetadataType.ExceptionDetails, exception.Message)
|
||||
.WithMetadata(MetadataType.StackTrace, exception.StackTrace));
|
||||
}
|
||||
}
|
||||
|
||||
public FluentResults.Result<Assembly> GetAssemblyByName(string assemblyName)
|
||||
{
|
||||
if (IsDisposed)
|
||||
return FluentResults.Result.Fail(new Error($"Loader is disposed!"));
|
||||
if (assemblyName.IsNullOrWhiteSpace())
|
||||
{
|
||||
return FluentResults.Result.Fail(new Error($"Assembly name is empty.")
|
||||
.WithMetadata(MetadataType.ExceptionObject, this));
|
||||
}
|
||||
AreOperationRunning = true;
|
||||
try
|
||||
{
|
||||
if (_loadedAssemblyData.TryGetValue(assemblyName, out var data))
|
||||
{
|
||||
return new Result<Assembly>().WithSuccess(new Success($"Assembly found.")).WithValue(data.Assembly);
|
||||
}
|
||||
|
||||
// search any assemblies that were background loaded and we're unaware of.
|
||||
foreach (var assembly1 in this.Assemblies.Where(a => !_loadedAssemblyData.ContainsKey(a)))
|
||||
{
|
||||
if (assembly1.GetName().FullName == assemblyName)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!assembly1.Location.IsNullOrWhiteSpace())
|
||||
{
|
||||
_loadedAssemblyData[assembly1] = new AssemblyData(assembly1, assembly1.Location);
|
||||
}
|
||||
// we don't have the original byte array so we can't store it.
|
||||
}
|
||||
catch (NotSupportedException nse) // dynamic assembly or location property threw
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
|
||||
return new Result<Assembly>().WithSuccess(new Success($"Assembly found.")).WithValue(assembly1);
|
||||
}
|
||||
}
|
||||
|
||||
return FluentResults.Result.Fail(new Error($"Assembly named '{ assemblyName }' not found!"));
|
||||
}
|
||||
finally
|
||||
{
|
||||
AreOperationRunning = false;
|
||||
}
|
||||
}
|
||||
|
||||
public FluentResults.Result<ImmutableArray<Type>> GetTypesInAssemblies()
|
||||
{
|
||||
if (IsDisposed)
|
||||
return FluentResults.Result.Fail(new Error($"Loader is disposed!"));
|
||||
AreOperationRunning = true;
|
||||
try
|
||||
{
|
||||
return new FluentResults.Result<ImmutableArray<Type>>().WithValue(_loadedAssemblyData
|
||||
.SelectMany(kvp => kvp.Value.Types).ToImmutableArray());
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return FluentResults.Result.Fail(new ExceptionalError(e));
|
||||
}
|
||||
finally
|
||||
{
|
||||
AreOperationRunning = false;
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerable<Type> UnsafeGetTypesInAssemblies()
|
||||
{
|
||||
if (IsDisposed)
|
||||
yield return null;
|
||||
AreOperationRunning = true;
|
||||
try
|
||||
{
|
||||
if (_loadedAssemblyData.None())
|
||||
{
|
||||
yield return null;
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var assemblyData in _loadedAssemblyData.Values)
|
||||
{
|
||||
foreach (var type in assemblyData.Types)
|
||||
{
|
||||
yield return type;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
AreOperationRunning = false;
|
||||
}
|
||||
}
|
||||
|
||||
public Result<Type> GetTypeInAssemblies(string typeName)
|
||||
{
|
||||
if (IsDisposed)
|
||||
return FluentResults.Result.Fail(new Error($"Loader is disposed!"));
|
||||
AreOperationRunning = true;
|
||||
try
|
||||
{
|
||||
if (_loadedAssemblyData.IsEmpty)
|
||||
return FluentResults.Result.Fail(new Error($"No assemblies loaded!"));
|
||||
foreach (var assemblyData in _loadedAssemblyData)
|
||||
{
|
||||
if (assemblyData.Value.TypesByName.TryGetValue(typeName, out var type))
|
||||
return new FluentResults.Result<Type>().WithSuccess($"Found type.").WithValue(type);
|
||||
}
|
||||
return FluentResults.Result.Fail(new Error($"No matching types found for { typeName }!"));
|
||||
}
|
||||
finally
|
||||
{
|
||||
AreOperationRunning = false;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (IsDisposed)
|
||||
return; // we don't want to invoke events twice nor cause strong GC handles.
|
||||
IsDisposed = true;
|
||||
this.Unload();
|
||||
this.DisposeInternal();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
~AssemblyLoader()
|
||||
{
|
||||
this.DisposeInternal();
|
||||
}
|
||||
|
||||
private void OnUnload(AssemblyLoadContext context)
|
||||
{
|
||||
// Try to wait for loading ops on other threads if they happen to occur with a timeout.
|
||||
// This should be an edge, should it even occur.
|
||||
DateTime timeout = DateTime.Now.AddSeconds(2);
|
||||
while (timeout > DateTime.Now)
|
||||
{
|
||||
if (!AreOperationRunning)
|
||||
break;
|
||||
Thread.Sleep(1000/Timing.FixedUpdateRate-1);
|
||||
}
|
||||
|
||||
var wf = new WeakReference<IAssemblyLoaderService>(this);
|
||||
_onUnload?.Invoke(this);
|
||||
}
|
||||
|
||||
private void DisposeInternal()
|
||||
{
|
||||
IsDisposed = true;
|
||||
base.Resolving -= OnResolvingManagedAssembly;
|
||||
base.ResolvingUnmanagedDll -= OnResolvingUnmanagedDll;
|
||||
base.Unloading -= OnUnload;
|
||||
this._dependencyResolvers.Clear();
|
||||
this._loadedAssemblyData.Clear();
|
||||
}
|
||||
|
||||
protected override Assembly Load(AssemblyName assemblyName)
|
||||
{
|
||||
if (IsDisposed)
|
||||
return null;
|
||||
AreOperationRunning = true;
|
||||
try
|
||||
{
|
||||
if (_loadedAssemblyData.TryGetValue(assemblyName.FullName, out var assembly))
|
||||
return assembly.Assembly;
|
||||
return null;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
finally
|
||||
{
|
||||
AreOperationRunning = false;
|
||||
}
|
||||
}
|
||||
|
||||
protected override IntPtr LoadUnmanagedDll(string unmanagedDllName)
|
||||
{
|
||||
if (IsDisposed)
|
||||
return 0;
|
||||
|
||||
GCHandle? handle = null;
|
||||
AreOperationRunning = true;
|
||||
try
|
||||
{
|
||||
if (_loadedAssemblyData.TryGetValue(unmanagedDllName, out var assemblyData))
|
||||
{
|
||||
handle = GCHandle.Alloc(assemblyData.Assembly, GCHandleType.Pinned);
|
||||
nint asmPtr = GCHandle.ToIntPtr(handle.Value);
|
||||
return asmPtr;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
finally
|
||||
{
|
||||
AreOperationRunning = false;
|
||||
try
|
||||
{
|
||||
if (handle.HasValue)
|
||||
handle.Value.Free();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignored. We just want to ensure that free is called.
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
private readonly record struct AssemblyData
|
||||
{
|
||||
public readonly Assembly Assembly;
|
||||
public readonly OneOf<byte[], string> AssemblyImageOrPath;
|
||||
public readonly MetadataReference AssemblyReference;
|
||||
public readonly ImmutableArray<Type> Types;
|
||||
public readonly ImmutableDictionary<string, Type> TypesByName;
|
||||
|
||||
public AssemblyData(Assembly assembly, byte[] assemblyImage)
|
||||
{
|
||||
Assembly = assembly ?? throw new ArgumentNullException(nameof(assembly));
|
||||
AssemblyImageOrPath = assemblyImage ?? throw new ArgumentNullException(nameof(assemblyImage));
|
||||
AssemblyReference = MetadataReference.CreateFromImage(assemblyImage);
|
||||
Types = assembly.GetSafeTypes().ToImmutableArray();
|
||||
TypesByName = Types.ToImmutableDictionary(type => type.FullName, type => type);
|
||||
}
|
||||
|
||||
public AssemblyData(Assembly assembly, string path)
|
||||
{
|
||||
Assembly = assembly ?? throw new ArgumentNullException(nameof(assembly));
|
||||
AssemblyImageOrPath = path ?? throw new ArgumentNullException(nameof(path));
|
||||
AssemblyReference = MetadataReference.CreateFromFile(path);
|
||||
Types = assembly.GetSafeTypes().ToImmutableArray();
|
||||
TypesByName = Types.ToImmutableDictionary(type => type.FullName, type => type);
|
||||
}
|
||||
}
|
||||
|
||||
private readonly record struct AssemblyOrStringKey : IEquatable<AssemblyOrStringKey>, IEqualityComparer<AssemblyOrStringKey>
|
||||
{
|
||||
public Assembly Assembly { get; init; }
|
||||
public string AssemblyName { get; init; }
|
||||
public readonly int HashCode;
|
||||
|
||||
public AssemblyOrStringKey(Assembly assembly)
|
||||
{
|
||||
if(assembly == null)
|
||||
throw new ArgumentNullException(nameof(assembly));
|
||||
Assembly = assembly;
|
||||
AssemblyName = assembly.GetName().FullName;
|
||||
if (AssemblyName == null)
|
||||
throw new ArgumentNullException(nameof(AssemblyName));
|
||||
HashCode = AssemblyName.GetHashCode();
|
||||
}
|
||||
|
||||
public AssemblyOrStringKey(string assemblyName)
|
||||
{
|
||||
if (assemblyName.IsNullOrWhiteSpace())
|
||||
throw new ArgumentNullException(nameof(assemblyName));
|
||||
Assembly = null;
|
||||
AssemblyName = assemblyName;
|
||||
HashCode = AssemblyName.GetHashCode();
|
||||
}
|
||||
|
||||
public bool Equals(AssemblyOrStringKey x, AssemblyOrStringKey y)
|
||||
{
|
||||
if (x.Assembly is not null && y.Assembly is not null)
|
||||
return x.Assembly == y.Assembly;
|
||||
return x.AssemblyName == y.AssemblyName;
|
||||
}
|
||||
|
||||
public int GetHashCode(AssemblyOrStringKey obj)
|
||||
{
|
||||
return this.HashCode;
|
||||
}
|
||||
|
||||
public static implicit operator AssemblyOrStringKey(Assembly assembly) => new AssemblyOrStringKey(assembly);
|
||||
public static implicit operator AssemblyOrStringKey(string name) => new AssemblyOrStringKey(name);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using Barotrauma.LuaCs;
|
||||
using FluentResults;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
|
||||
namespace Barotrauma.LuaCs;
|
||||
|
||||
public interface IAssemblyLoaderService : IService
|
||||
{
|
||||
public interface IFactory : IService
|
||||
{
|
||||
IAssemblyLoaderService CreateInstance(LoaderInitData initData);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructor record for instancing.
|
||||
/// </summary>
|
||||
/// <param name="AssemblyManagementService"></param>
|
||||
/// <param name="InstanceId"></param>
|
||||
/// <param name="Name"></param>
|
||||
/// <param name="IsReferenceMode">Assemblies and Types in this context are for <see cref="MetadataReference"/> only.
|
||||
/// Execution of assembly data is forbidden.</param>
|
||||
/// <param name="OwnerPackage"></param>
|
||||
/// <param name="OnUnload"></param>
|
||||
/// <param name="OnResolvingManaged"></param>
|
||||
/// <param name="OnResolvingUnmanagedDll"></param>
|
||||
public record LoaderInitData(
|
||||
[Required] Guid InstanceId,
|
||||
[Required][NotNull] string Name,
|
||||
[Required] bool IsReferenceMode,
|
||||
ContentPackage OwnerPackage,
|
||||
Action<IAssemblyLoaderService> OnUnload,
|
||||
Func<IAssemblyLoaderService, AssemblyName, Assembly> OnResolvingManaged,
|
||||
Func<Assembly, string, IntPtr> OnResolvingUnmanagedDll);
|
||||
|
||||
/// <summary>
|
||||
/// ID for this instance.
|
||||
/// </summary>
|
||||
Guid Id { get; }
|
||||
/// <summary>
|
||||
/// The owner content package.
|
||||
/// </summary>
|
||||
ContentPackage OwnerPackage { get; }
|
||||
/// <summary>
|
||||
/// Indicates that the assemblies in this load context are metadata references only and not
|
||||
/// intended for execution.
|
||||
/// </summary>
|
||||
bool IsReferenceOnlyMode { get; }
|
||||
/// <summary>
|
||||
/// Runtime value of constant <see cref="InternalsAwareAssemblyName"/> for extensibility use.
|
||||
/// </summary>
|
||||
public static readonly string InternalsAccessAssemblyName = InternalsAwareAssemblyName;
|
||||
/// <summary>
|
||||
/// Name for all runtime-compiled assemblies requiring access to <c>internal</c> assembly components. <seealso cref="InternalsVisibleToAttribute"/>
|
||||
/// </summary>
|
||||
public const string InternalsAwareAssemblyName = "InternalsAwareAssembly";
|
||||
|
||||
/// <summary>
|
||||
/// Add additional locations for dependency resolution to use.
|
||||
/// </summary>
|
||||
/// <param name="paths"></param>
|
||||
/// <returns></returns>
|
||||
public FluentResults.Result AddDependencyPaths(ImmutableArray<string> paths);
|
||||
|
||||
/// <summary>
|
||||
/// Compiles the supplied syntaxtrees and options into an in-memory assembly image.
|
||||
/// Builds metadata from loaded assemblies, only supply your own if you have in-memory images not managed by the
|
||||
/// AssemblyManager class.
|
||||
/// </summary>
|
||||
/// <param name="assemblyName"><c>[NotNull]</c>Name reference of the assembly.
|
||||
/// <para><b>[IMPORTANT]</b> This is used to reference this assembly as the true name will be forced if
|
||||
/// publicized assemblies are not used (InternalsVisibleTo Attrib).</para>
|
||||
/// Must be supplied for in-memory assemblies.
|
||||
/// <para>Must be unique to all other assemblies explicitly loaded using this context.</para></param>
|
||||
/// <param name="compileWithInternalAccess">Forces the assembly name to <see cref="InternalsAccessAssemblyName"/> and grants access to <c>internal</c>.</param>
|
||||
/// <param name="syntaxTrees"><c>[NotNull]</c>Syntax trees to compile into the assembly.</param>
|
||||
/// <param name="metadataReferences">All <c>MetadataReference<c/>s to be used for compilation.
|
||||
/// [IMPORTANT] This method builds metadata from loaded assemblies, only supply your own if you have in-memory
|
||||
/// images not managed by the AssemblyManager class.</param>
|
||||
/// <param name="compilationOptions"><c>[NotNull]</c>CSharp compilation options. This method automatically adds the 'IgnoreAccessChecks' property for compilation.</param>
|
||||
/// <para><b>[IMPORTANT]</b>Cannot be null or empty if <see cref="compileWithInternalAccess"/> is false.</para></param>
|
||||
/// <returns>Success state of the operation.</returns>
|
||||
public Result<Assembly> CompileScriptAssembly([NotNull] string assemblyName,
|
||||
bool compileWithInternalAccess,
|
||||
ImmutableArray<SyntaxTree> syntaxTrees,
|
||||
ImmutableArray<MetadataReference> metadataReferences,
|
||||
CSharpCompilationOptions compilationOptions = null);
|
||||
|
||||
/// <summary>
|
||||
/// Loads the assembly from the provided location and registers all new paths provided with dependency resolution.
|
||||
/// </summary>
|
||||
/// <param name="assemblyFilePath">Absolute path to the managed assembly.</param>
|
||||
/// <param name="additionalDependencyPaths">Additional paths for dependency resolution.</param>
|
||||
/// <returns>Success and reference to the assembly if successful.</returns>
|
||||
public FluentResults.Result<Assembly> LoadAssemblyFromFile(string assemblyFilePath,
|
||||
ImmutableArray<string> additionalDependencyPaths);
|
||||
|
||||
/// <summary>
|
||||
/// Returns the already loaded assembly with the same name.
|
||||
/// </summary>
|
||||
/// <param name="assemblyName">Name of the assembly.</param>
|
||||
/// <returns>Operation success on assembly found and assembly.</returns>
|
||||
public FluentResults.Result<Assembly> GetAssemblyByName(string assemblyName);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the list of <c>Type</c>s from loaded assemblies.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public FluentResults.Result<ImmutableArray<Type>> GetTypesInAssemblies();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the list of <c>Type</c>s from loaded assemblies. Does not create a defensive copy and blocks loading/unloading.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public IEnumerable<Type> UnsafeGetTypesInAssemblies();
|
||||
|
||||
/// <summary>
|
||||
/// Returns the first found type given it's fully qualified name.
|
||||
/// </summary>
|
||||
/// <param name="typeName"></param>
|
||||
/// <returns></returns>
|
||||
public FluentResults.Result<Type> GetTypeInAssemblies(string typeName);
|
||||
|
||||
/// <summary>
|
||||
/// List of loaded assemblies.
|
||||
/// </summary>
|
||||
public IEnumerable<Assembly> Assemblies { get; }
|
||||
|
||||
public IEnumerable<MetadataReference> AssemblyReferences { get; }
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
using System;
|
||||
using Barotrauma.LuaCs.Events;
|
||||
|
||||
namespace Barotrauma.LuaCs;
|
||||
|
||||
public interface IAssemblyPlugin : IDisposable, IEventPluginPreInitialize, IEventPluginInitialize, IEventPluginLoadCompleted { }
|
||||
+5
-3
@@ -1,25 +1,27 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Xml.Serialization;
|
||||
using Barotrauma.LuaCs.Data;
|
||||
|
||||
namespace Barotrauma;
|
||||
|
||||
[Serializable]
|
||||
public sealed class RunConfig
|
||||
[Obsolete($"Use {nameof(IModConfigInfo)} instead. This class exists for legacy compatibility only.")]
|
||||
public sealed class RunConfig : IRunConfig
|
||||
{
|
||||
/// <summary>
|
||||
/// How should scripts be run on the server.
|
||||
/// </summary>
|
||||
[XmlElement(ElementName = "Server")]
|
||||
[DefaultValue("Standard")]
|
||||
public string Server;
|
||||
public string Server { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// How should scripts be run on the client.
|
||||
/// </summary>
|
||||
[XmlElement(ElementName = "Client")]
|
||||
[DefaultValue("Standard")]
|
||||
public string Client;
|
||||
public string Client { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// List of dependencies by either Steam Workshop ID or by Partial Inclusive Name (ie. "ModDep" will match a mod named "A ModDependency").
|
||||
@@ -0,0 +1,682 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Frozen;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Xml;
|
||||
using System.Xml.Linq;
|
||||
using Barotrauma.LuaCs.Data;
|
||||
using Barotrauma.LuaCs.Events;
|
||||
using Barotrauma.LuaCs;
|
||||
using FluentResults;
|
||||
using Microsoft.Toolkit.Diagnostics;
|
||||
using Microsoft.Xna.Framework;
|
||||
|
||||
namespace Barotrauma.LuaCs;
|
||||
|
||||
public sealed partial class ConfigService : IConfigService
|
||||
{
|
||||
#region Disposal_Locks_Reset
|
||||
|
||||
private readonly AsyncReaderWriterLock _operationLock = new ();
|
||||
private readonly AsyncReaderWriterLock _settingsByPackageLock = new ();
|
||||
private int _isDisposed = 0;
|
||||
public bool IsDisposed
|
||||
{
|
||||
get => ModUtils.Threading.GetBool(ref _isDisposed);
|
||||
private set => ModUtils.Threading.SetBool(ref _isDisposed, value);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
using var lck = _operationLock.AcquireWriterLock().ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
using var settingsLck = _settingsByPackageLock.AcquireWriterLock().ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
if (!ModUtils.Threading.CheckIfClearAndSetBool(ref _isDisposed))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_logger.LogDebug($"{nameof(ConfigService)}: Disposing.");
|
||||
|
||||
_configInfoParserService.Dispose();
|
||||
_configProfileInfoParserService.Dispose();
|
||||
|
||||
if (!_settingsInstances.IsEmpty)
|
||||
{
|
||||
foreach (var instance in _settingsInstances)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (instance.Value is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
_eventService.PublishEvent<IEventSettingInstanceLifetime>(sub =>
|
||||
// ReSharper disable once AccessToDisposedClosure
|
||||
sub.OnSettingInstanceDisposed(instance.Value));
|
||||
instance.Value.Dispose();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignored
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_settingsInstances.Clear();
|
||||
_instanceFactory.Clear();
|
||||
_settingsInstancesByPackage.Clear();
|
||||
_commandsService.Dispose();
|
||||
|
||||
_storageService = null;
|
||||
_logger = null;
|
||||
_eventService = null;
|
||||
_configInfoParserService = null;
|
||||
_configProfileInfoParserService = null;
|
||||
_commandsService = null;
|
||||
_infoProvider = null;
|
||||
}
|
||||
|
||||
public FluentResults.Result Reset()
|
||||
{
|
||||
using var lck = _operationLock.AcquireWriterLock().ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
IService.CheckDisposed(this);
|
||||
|
||||
var result = new FluentResults.Result();
|
||||
|
||||
if (!_settingsInstances.IsEmpty)
|
||||
{
|
||||
foreach (var instance in _settingsInstances)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (instance.Value is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
_eventService.PublishEvent<IEventSettingInstanceLifetime>(sub =>
|
||||
// ReSharper disable once AccessToDisposedClosure
|
||||
sub.OnSettingInstanceDisposed(instance.Value));
|
||||
instance.Value.Dispose();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
result.WithError(new ExceptionalError(e));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_settingsInstances.Clear();
|
||||
_instanceFactory.Clear();
|
||||
_settingsInstancesByPackage.Clear();
|
||||
_storageService.PurgeCache();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private const string SaveDataFileName = "SettingsData.xml";
|
||||
|
||||
// --- Settings
|
||||
private readonly ConcurrentDictionary<(ContentPackage OwnerPackage, string InternalName), ISettingBase>
|
||||
_settingsInstances = new();
|
||||
private readonly ConcurrentDictionary<string, Func<(IConfigService ConfigService, IConfigInfo Info), ISettingBase>>
|
||||
_instanceFactory = new();
|
||||
private readonly ConcurrentDictionary<ContentPackage, ConcurrentBag<ISettingBase>>
|
||||
_settingsInstancesByPackage = new();
|
||||
|
||||
// --- Profiles
|
||||
private readonly ConcurrentDictionary<(ContentPackage Package, string ProfileName), IConfigProfileInfo>
|
||||
_settingsProfiles = new();
|
||||
|
||||
private IStorageService _storageService;
|
||||
private ILoggerService _logger;
|
||||
private IEventService _eventService;
|
||||
private IConsoleCommandsService _commandsService;
|
||||
private ILuaCsInfoProvider _infoProvider;
|
||||
private IParserServiceOneToManyAsync<IConfigResourceInfo, IConfigInfo> _configInfoParserService;
|
||||
private IParserServiceOneToManyAsync<IConfigResourceInfo, IConfigProfileInfo> _configProfileInfoParserService;
|
||||
|
||||
public ConfigService(ILoggerService logger,
|
||||
IStorageService storageService,
|
||||
IParserServiceOneToManyAsync<IConfigResourceInfo, IConfigInfo> configInfoParserService,
|
||||
IParserServiceOneToManyAsync<IConfigResourceInfo, IConfigProfileInfo> configProfileInfoParserService,
|
||||
IEventService eventService,
|
||||
IConsoleCommandsService commandsService,
|
||||
ILuaCsInfoProvider infoProvider)
|
||||
{
|
||||
_logger = logger;
|
||||
_storageService = storageService;
|
||||
_configInfoParserService = configInfoParserService;
|
||||
_configProfileInfoParserService = configProfileInfoParserService;
|
||||
_eventService = eventService;
|
||||
_commandsService = commandsService;
|
||||
_infoProvider = infoProvider;
|
||||
|
||||
_storageService.UseCaching = false;
|
||||
InjectCommands(commandsService);
|
||||
}
|
||||
|
||||
private void InjectCommands(IConsoleCommandsService commandsService)
|
||||
{
|
||||
commandsService.RegisterCommand("cfg_getvalue", "cfg_getvalue [Content Package] [InternalName] [ValueString]: gets a config value.", (string[] args) =>
|
||||
{
|
||||
if (args.Length < 1)
|
||||
{
|
||||
_logger.LogError("Please specify the name of the package to set the config.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.Length < 2)
|
||||
{
|
||||
_logger.LogError("Please specify the name of the config.");
|
||||
return;
|
||||
}
|
||||
|
||||
var package = ContentPackageManager.RegularPackages.FirstOrDefault(p => p.Name == args[0]);
|
||||
if (package == null)
|
||||
{
|
||||
_logger.LogError($"Could not find the package {args[0]}!");
|
||||
return;
|
||||
}
|
||||
|
||||
string internalName = args[1];
|
||||
|
||||
if (!TryGetConfig(package, internalName, out ISettingBase setting))
|
||||
{
|
||||
_logger.LogError($"Could not get config with name {internalName}");
|
||||
return;
|
||||
}
|
||||
|
||||
_logger.LogMessage($"config {internalName} value is {setting.GetStringValue()}", Color.Green);
|
||||
}, getValidArgs: () => new[]
|
||||
{
|
||||
ContentPackageManager.RegularPackages.Select(p => p.Name).ToArray()
|
||||
});
|
||||
|
||||
commandsService.RegisterCommand("cfg_setvalue", "cfg_setvalue [Content Package] [InternalName] [ValueString]: sets a config.", (string[] args) =>
|
||||
{
|
||||
if (args.Length < 1)
|
||||
{
|
||||
_logger.LogError("Please specify the name of the package to set the config.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.Length < 2)
|
||||
{
|
||||
_logger.LogError("Please specify the name of the config.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.Length < 3)
|
||||
{
|
||||
_logger.LogError("Please specify the value to set the config to.");
|
||||
return;
|
||||
}
|
||||
|
||||
var package = ContentPackageManager.RegularPackages.FirstOrDefault(p => p.Name == args[0]);
|
||||
if (package == null)
|
||||
{
|
||||
_logger.LogError($"Could not find the package {args[0]}!");
|
||||
return;
|
||||
}
|
||||
|
||||
string internalName = args[1];
|
||||
string valueString = args[2];
|
||||
|
||||
if (!TryGetConfig(package, internalName, out ISettingBase setting))
|
||||
{
|
||||
_logger.LogError($"Could not get config with name {internalName}");
|
||||
return;
|
||||
}
|
||||
|
||||
if (setting.TrySetSerializedValue(valueString))
|
||||
{
|
||||
_logger.LogMessage($"Set config {internalName} value to {valueString}", Color.Green);
|
||||
if (SaveConfigValue(setting) is { IsFailed: true } res)
|
||||
{
|
||||
_logger.LogMessage($"Failed to save new config data to disk. Reasons: {res.ToString()}");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogError($"Failed to set config value");
|
||||
}
|
||||
}, getValidArgs: () => new[]
|
||||
{
|
||||
ContentPackageManager.RegularPackages.Select(p => p.Name).ToArray()
|
||||
});
|
||||
|
||||
commandsService.RegisterCommand("cfg_setprofile", "cfg_setprofile [ContentPackage] [InternalProfileName]",
|
||||
(string[] args) =>
|
||||
{
|
||||
if (args.Length < 1 || args[0].IsNullOrWhiteSpace())
|
||||
{
|
||||
_logger.LogError("Please specify the name of the package of the profile.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.Length < 2 || args[1].IsNullOrWhiteSpace())
|
||||
{
|
||||
_logger.LogError("Please specify the name of the profile.");
|
||||
return;
|
||||
}
|
||||
|
||||
var package = ContentPackageManager.RegularPackages.FirstOrDefault(p => p.Name == args[0], null);
|
||||
if (package == null)
|
||||
{
|
||||
_logger.LogError($"Could not find the package {args[0]}!");
|
||||
return;
|
||||
}
|
||||
|
||||
var res = ApplyConfigProfile(package, args[1]);
|
||||
if (res.IsFailed)
|
||||
{
|
||||
_logger.LogError($"Errors while applying profile {args[1]}!");
|
||||
_logger.LogResults(res);
|
||||
return;
|
||||
}
|
||||
_logger.Log($"Profile {args[1]} applied successfully!", Color.Green);
|
||||
}, getValidArgs: () => new[]
|
||||
{
|
||||
ContentPackageManager.RegularPackages.Select(p => p.Name).ToArray()
|
||||
}, false);
|
||||
}
|
||||
|
||||
public void RegisterSettingTypeInitializer<T>(string typeIdentifier, Func<(IConfigService ConfigService, IConfigInfo Info), T> settingFactory) where T : class, ISettingBase
|
||||
{
|
||||
Guard.IsNotNullOrWhiteSpace(typeIdentifier, nameof(typeIdentifier));
|
||||
Guard.IsNotNull(settingFactory, nameof(settingFactory));
|
||||
using var lck = _operationLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
IService.CheckDisposed(this);
|
||||
|
||||
if (_instanceFactory.ContainsKey(typeIdentifier))
|
||||
{
|
||||
ThrowHelper.ThrowArgumentException($"{nameof(RegisterSettingTypeInitializer)}: The type identifier {typeIdentifier} is already registered.");
|
||||
}
|
||||
|
||||
_instanceFactory[typeIdentifier] = settingFactory;
|
||||
}
|
||||
|
||||
private static ImmutableArray<T> SelectCompatible<T>(ImmutableArray<T> resources) where T : IBaseResourceInfo
|
||||
{
|
||||
return resources
|
||||
.Where(r => r.SupportedPlatforms.HasFlag(ModUtils.Environment.CurrentPlatform))
|
||||
.Where(r => r.SupportedTargets.HasFlag(ModUtils.Environment.CurrentTarget))
|
||||
.OrderBy(r => r.Optional ? 1 : 0) // optional content last
|
||||
.ThenBy(r => r.LoadPriority)
|
||||
.ToImmutableArray();
|
||||
}
|
||||
|
||||
public async Task<FluentResults.Result> LoadConfigsAsync(ImmutableArray<IConfigResourceInfo> configResources)
|
||||
{
|
||||
using var lck = await _operationLock.AcquireReaderLock();
|
||||
IService.CheckDisposed(this);
|
||||
if (configResources.IsDefaultOrEmpty)
|
||||
{
|
||||
return FluentResults.Result.Ok();
|
||||
}
|
||||
|
||||
var result = new FluentResults.Result();
|
||||
|
||||
var taskBuilder = ImmutableArray.CreateBuilder<Task<ImmutableArray<IConfigInfo>>>();
|
||||
var toProcessErrors = new ConcurrentStack<IError>();
|
||||
|
||||
foreach (var resource in SelectCompatible(configResources))
|
||||
{
|
||||
taskBuilder.Add(await Task.Factory.StartNew<Task<ImmutableArray<IConfigInfo>>>(async Task<ImmutableArray<IConfigInfo>> () =>
|
||||
{
|
||||
var r = await _configInfoParserService.TryParseResourcesAsync(resource);
|
||||
if (r.IsFailed)
|
||||
{
|
||||
toProcessErrors.PushRange(r.Errors.ToArray());
|
||||
return ImmutableArray<IConfigInfo>.Empty;
|
||||
}
|
||||
return r.Value;
|
||||
}));
|
||||
}
|
||||
|
||||
var taskResults = await Task.WhenAll(taskBuilder.ToImmutable());
|
||||
|
||||
if (toProcessErrors.Count > 0)
|
||||
{
|
||||
return FluentResults.Result.Fail($"{nameof(LoadConfigsAsync)}: Errors while loading configuration info: ").WithErrors(toProcessErrors.ToArray());
|
||||
}
|
||||
|
||||
var toProcessDocs = taskResults
|
||||
.Where(tr => !tr.IsDefaultOrEmpty)
|
||||
.SelectMany(tr => tr)
|
||||
.Where(icf => icf is not null)
|
||||
.ToImmutableArray();
|
||||
|
||||
var instanceQueue = new Queue<(IConfigInfo configInfo, Func<(IConfigService ConfigService, IConfigInfo Info), ISettingBase> factory)>();
|
||||
|
||||
foreach (var info in toProcessDocs)
|
||||
{
|
||||
if (!_instanceFactory.TryGetValue(info.DataType, out var factory))
|
||||
{
|
||||
result.WithError(
|
||||
$"{nameof(LoadConfigsAsync)}: Could not retrieve the instance factory for the data type of '{info.DataType}'!");
|
||||
continue;
|
||||
}
|
||||
if (_settingsInstances.ContainsKey((info.OwnerPackage, info.InternalName)))
|
||||
{
|
||||
// duplicate for some reason (ie. double loading). This should never happen.
|
||||
ThrowHelper.ThrowInvalidOperationException($"{nameof(LoadConfigsAsync)}: A setting for the [ContentPackage].[InternalName] of '[{info.OwnerPackage.Name}].[{info.InternalName}]' already exists!");
|
||||
}
|
||||
|
||||
instanceQueue.Enqueue((info, factory));
|
||||
}
|
||||
|
||||
var toProcessInstanceQueue = new Queue<(IConfigInfo info, ISettingBase instance)>();
|
||||
|
||||
while (instanceQueue.TryDequeue(out var instanceFactoryInfo))
|
||||
{
|
||||
try
|
||||
{
|
||||
toProcessInstanceQueue.Enqueue((instanceFactoryInfo.configInfo, instanceFactoryInfo.factory((this, instanceFactoryInfo.configInfo))));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
result.WithError(
|
||||
$"{nameof(LoadConfigsAsync)}: Error while instancing setting for '{instanceFactoryInfo.configInfo.OwnerPackage}.{instanceFactoryInfo.configInfo.InternalName}': {e.Message}!");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
using var settingsLck = await _settingsByPackageLock.AcquireWriterLock(); // block to protect new bag instance creation
|
||||
|
||||
while (toProcessInstanceQueue.TryDequeue(out var newInstanceData))
|
||||
{
|
||||
_settingsInstances[(newInstanceData.info.OwnerPackage, newInstanceData.info.InternalName)] = newInstanceData.instance;
|
||||
if (!_settingsInstancesByPackage.TryGetValue(newInstanceData.info.OwnerPackage, out _))
|
||||
{
|
||||
_settingsInstancesByPackage[newInstanceData.info.OwnerPackage] = new ConcurrentBag<ISettingBase>();
|
||||
}
|
||||
_settingsInstancesByPackage[newInstanceData.info.OwnerPackage].Add(newInstanceData.instance);
|
||||
result.WithReasons(_eventService.PublishEvent<IEventSettingInstanceLifetime>(sub =>
|
||||
sub.OnSettingInstanceCreated(newInstanceData.instance)).Reasons);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public async Task<FluentResults.Result> LoadConfigsProfilesAsync(ImmutableArray<IConfigResourceInfo> configProfileResources)
|
||||
{
|
||||
using var _ = await _operationLock.AcquireReaderLock();
|
||||
IService.CheckDisposed(this);
|
||||
if (configProfileResources.IsDefaultOrEmpty)
|
||||
{
|
||||
ThrowHelper.ThrowArgumentNullException($"{nameof(LoadConfigsProfilesAsync)}: {nameof(configProfileResources)} is empty.");
|
||||
}
|
||||
|
||||
var result = new FluentResults.Result();
|
||||
|
||||
foreach (var resource in SelectCompatible(configProfileResources))
|
||||
{
|
||||
var r = await _configProfileInfoParserService.TryParseResourcesAsync(resource);
|
||||
if (r.IsFailed)
|
||||
{
|
||||
result.WithErrors(r.Errors);
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (var info in r.Value)
|
||||
{
|
||||
if (!_settingsProfiles.TryAdd((info.OwnerPackage, info.InternalName), info))
|
||||
{
|
||||
result.WithErrors(r.Errors);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (info.InternalName.Equals("default", StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
//apply it
|
||||
foreach (var value in info.ProfileValues)
|
||||
{
|
||||
if (_settingsInstances.TryGetValue((info.OwnerPackage, value.SettingName), out var instance))
|
||||
{
|
||||
instance.TrySetSerializedValue(value.Element);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public FluentResults.Result LoadSavedValueForConfig(ISettingBase setting)
|
||||
{
|
||||
Guard.IsNotNull(setting, nameof(setting));
|
||||
using var lck = _operationLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
IService.CheckDisposed(this);
|
||||
|
||||
if (_storageService.LoadLocalXml(setting.OwnerPackage, SaveDataFileName) is not { } saveFileResult)
|
||||
{
|
||||
#if DEBUG
|
||||
return FluentResults.Result.Fail(
|
||||
$"{nameof(LoadSavedValueForConfig)}: Could not open save file for setting [{setting.OwnerPackage.Name}.{setting.InternalName}]");
|
||||
#endif
|
||||
return FluentResults.Result.Ok();
|
||||
}
|
||||
|
||||
if (saveFileResult is { IsFailed: true })
|
||||
{
|
||||
#if DEBUG
|
||||
_logger.LogResults(saveFileResult.ToResult());
|
||||
return FluentResults.Result.Fail(
|
||||
$"{nameof(LoadSavedValueForConfig)}: Could not open save file for setting [{setting.OwnerPackage.Name}.{setting.InternalName}]");
|
||||
#endif
|
||||
return FluentResults.Result.Ok();
|
||||
}
|
||||
|
||||
if (saveFileResult.Value.Root is not {} rootElement
|
||||
|| !string.Equals(rootElement.Name.LocalName, "Configuration", StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
return FluentResults.Result.Fail($"{nameof(LoadSavedValueForConfig)}: Root invalid for setting [{setting.OwnerPackage.Name}.{setting.InternalName}]");
|
||||
}
|
||||
|
||||
if (rootElement.GetChildElement(XmlConvert.EncodeLocalName(setting.OwnerPackage.Name.Trim()), StringComparison.InvariantCultureIgnoreCase)
|
||||
?.GetChildElement(setting.InternalName, StringComparison.InvariantCultureIgnoreCase) is not {} cfgValueElement)
|
||||
{
|
||||
#if DEBUG
|
||||
return FluentResults.Result.Fail($"{nameof(LoadSavedValueForConfig)}: Could not find saved value for setting:[{setting.OwnerPackage.Name}.{setting.InternalName}]");
|
||||
#endif
|
||||
return FluentResults.Result.Ok();
|
||||
}
|
||||
|
||||
return FluentResults.Result.OkIf(setting.TrySetSerializedValue(cfgValueElement), new Error($"Failed to set value for [{setting.OwnerPackage.Name}.{setting.InternalName}]"));
|
||||
}
|
||||
|
||||
public FluentResults.Result LoadSavedConfigsValues()
|
||||
{
|
||||
ImmutableArray<ISettingBase> cfgValues;
|
||||
using (var lck = _operationLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult())
|
||||
{
|
||||
IService.CheckDisposed(this);
|
||||
cfgValues = _settingsInstances.Select(kvp => kvp.Value).ToImmutableArray();
|
||||
}
|
||||
|
||||
var ret = new FluentResults.Result();
|
||||
|
||||
foreach (var settingBase in cfgValues)
|
||||
{
|
||||
#if DEBUG
|
||||
// log in debug only.
|
||||
ret.WithReasons(LoadSavedValueForConfig(settingBase).Reasons);
|
||||
#else
|
||||
LoadSavedValueForConfig(settingBase);
|
||||
#endif
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
public FluentResults.Result ApplyConfigProfile(ContentPackage package, string internalName)
|
||||
{
|
||||
Guard.IsNotNull(package, nameof(package));
|
||||
Guard.IsNotNullOrWhiteSpace(internalName, nameof(internalName));
|
||||
using var _ = _operationLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
IService.CheckDisposed(this);
|
||||
|
||||
if (!_settingsProfiles.TryGetValue((package, internalName), out var setting))
|
||||
{
|
||||
return FluentResults.Result.Fail($"{nameof(ApplyConfigProfile)}: Could not find profile [{package.Name}.{internalName}]");
|
||||
}
|
||||
|
||||
var result = new FluentResults.Result();
|
||||
|
||||
foreach (var profileValue in setting.ProfileValues)
|
||||
{
|
||||
if (!_settingsInstances.TryGetValue((package, profileValue.SettingName), out var instance))
|
||||
{
|
||||
result.WithError(new Error($"{nameof(ApplyConfigProfile)}: Could not find setting [{profileValue.SettingName}]."));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!instance.TrySetSerializedValue(profileValue.Element))
|
||||
{
|
||||
result.WithError(new Error($"{nameof(ApplyConfigProfile)}: Failed to set value for [{profileValue.SettingName}]."));
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public FluentResults.Result SaveConfigValue(ISettingBase setting)
|
||||
{
|
||||
XDocument cpCfgValues;
|
||||
if (_storageService.LoadLocalXml(setting.OwnerPackage, SaveDataFileName) is not {} saveFileResult)
|
||||
{
|
||||
return FluentResults.Result.Fail($"{nameof(SaveConfigValue)}: Storage Service Failure while trying to load file for setting [{setting.OwnerPackage.Name}.{setting.InternalName}]");
|
||||
}
|
||||
|
||||
// get Configuration
|
||||
if (saveFileResult.IsFailed)
|
||||
{
|
||||
cpCfgValues = new XDocument(new XDeclaration("1.0", "utf-8", "yes"), new XElement("Configuration"));
|
||||
}
|
||||
else
|
||||
{
|
||||
cpCfgValues = saveFileResult.Value;
|
||||
}
|
||||
|
||||
if (cpCfgValues.Root is null || cpCfgValues.Root.Name != "Configuration")
|
||||
{
|
||||
return FluentResults.Result.Fail($"{nameof(SaveConfigValue)}: Bad save file format for setting: [{setting.OwnerPackage.Name}.{setting.InternalName}]");
|
||||
}
|
||||
|
||||
XElement currentTarget = GetOrAddElement(cpCfgValues.Root, XmlConvert.EncodeLocalName(setting.OwnerPackage.Name.Trim()), name => new XElement(name));
|
||||
currentTarget = GetOrAddElement(currentTarget, setting.InternalName, name => new XElement(name));
|
||||
|
||||
var ret = setting.GetSerializableValue().Match(str =>
|
||||
{
|
||||
var tgt = currentTarget.Attribute("Value");
|
||||
if (tgt is null)
|
||||
{
|
||||
var attr = new XAttribute("Value", str);
|
||||
currentTarget.Add(attr);
|
||||
}
|
||||
else
|
||||
{
|
||||
tgt.Value = str;
|
||||
}
|
||||
|
||||
return FluentResults.Result.Ok();
|
||||
},
|
||||
elem =>
|
||||
{
|
||||
currentTarget.ReplaceNodes(new XElement("Value", elem));
|
||||
return FluentResults.Result.Ok();
|
||||
});
|
||||
|
||||
ret.WithReasons(_storageService.SaveLocalXml(setting.OwnerPackage, SaveDataFileName, cpCfgValues).Reasons);
|
||||
return ret;
|
||||
|
||||
XElement GetOrAddElement(XElement containerElement, string elementName, Func<string, XElement> factory)
|
||||
{
|
||||
var element = containerElement.Element(elementName);
|
||||
if (element is null)
|
||||
{
|
||||
element = factory(elementName);
|
||||
containerElement.Add(element);
|
||||
}
|
||||
return element;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public FluentResults.Result DisposePackageData(ContentPackage package)
|
||||
{
|
||||
Guard.IsNotNull(package, nameof(package));
|
||||
using var lck = _operationLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
IService.CheckDisposed(this);
|
||||
|
||||
ConcurrentBag<ISettingBase> toDispose;
|
||||
using (var settingsLck = _settingsByPackageLock.AcquireWriterLock().ConfigureAwait(false).GetAwaiter().GetResult())
|
||||
{
|
||||
if (!_settingsInstancesByPackage.TryRemove(package, out toDispose) || toDispose is null)
|
||||
{
|
||||
return FluentResults.Result.Ok();
|
||||
}
|
||||
}
|
||||
|
||||
var result = new FluentResults.Result();
|
||||
|
||||
foreach (var setting in toDispose)
|
||||
{
|
||||
result.WithReasons(_eventService.PublishEvent<IEventSettingInstanceLifetime>(sub => sub.OnSettingInstanceDisposed(setting)).Reasons);
|
||||
try
|
||||
{
|
||||
_settingsInstances.TryRemove((setting.OwnerPackage, setting.InternalName), out _);
|
||||
setting.Dispose();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
result.WithError(new ExceptionalError(e));
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public FluentResults.Result DisposeAllPackageData()
|
||||
{
|
||||
return this.Reset();
|
||||
}
|
||||
|
||||
public bool TryGetConfig<T>(ContentPackage package, string internalName, out T instance) where T : ISettingBase
|
||||
{
|
||||
Guard.IsNotNull(package, nameof(package));
|
||||
Guard.IsNotNullOrWhiteSpace(internalName, nameof(internalName));
|
||||
using var lck = _operationLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
using var settingsLck =
|
||||
_settingsByPackageLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
IService.CheckDisposed(this);
|
||||
|
||||
instance = default;
|
||||
|
||||
if(!_settingsInstances.TryGetValue((package, internalName), out var inst))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (inst is not T instanceT)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
instance = instanceT;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
using Barotrauma.LuaCs.Events;
|
||||
using Barotrauma.Networking;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
|
||||
namespace Barotrauma.LuaCs;
|
||||
|
||||
internal class ConsoleCommandsService : IConsoleCommandsService
|
||||
{
|
||||
private readonly List<DebugConsole.Command> _registeredCommands = new();
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (!ModUtils.Threading.CheckIfClearAndSetBool(ref _isDisposed))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var cmd in _registeredCommands.ToImmutableArray())
|
||||
{
|
||||
DebugConsole.Commands.Remove(cmd);
|
||||
}
|
||||
|
||||
_registeredCommands.Clear();
|
||||
}
|
||||
|
||||
private int _isDisposed = 0;
|
||||
public bool IsDisposed
|
||||
{
|
||||
get => ModUtils.Threading.GetBool(ref _isDisposed);
|
||||
private set => ModUtils.Threading.SetBool(ref _isDisposed, value);
|
||||
}
|
||||
|
||||
public void RegisterCommand(string name, string help, Action<string[]> onExecute, Func<string[][]> getValidArgs = null, bool isCheat = false)
|
||||
{
|
||||
IService.CheckDisposed(this);
|
||||
|
||||
if (DebugConsole.Commands.Any(cmd => cmd.Names.Contains(name)))
|
||||
{
|
||||
LuaCsSetup.Instance.Logger.LogWarning($"Registering console command {name} more than once!");
|
||||
}
|
||||
|
||||
var cmd = new DebugConsole.Command(name, help, onExecute, getValidArgs, isCheat);
|
||||
_registeredCommands.Add(cmd);
|
||||
DebugConsole.Commands.Add(cmd);
|
||||
}
|
||||
|
||||
public void AssignOnExecute(string names, Action<string[]> onExecute)
|
||||
{
|
||||
var matchingCommand = DebugConsole.Commands.Find(c => c.Names.Intersect(names.Split('|').ToIdentifiers()).Any());
|
||||
if (matchingCommand == null)
|
||||
{
|
||||
throw new Exception("AssignOnExecute failed. Command matching the name(s) \"" + names + "\" not found.");
|
||||
}
|
||||
else
|
||||
{
|
||||
matchingCommand.OnExecute = onExecute;
|
||||
}
|
||||
}
|
||||
|
||||
#if SERVER
|
||||
public void AssignOnClientRequestExecute(string names, Action<Client, Vector2, string[]> onClientRequestExecute)
|
||||
{
|
||||
var matchingCommand = DebugConsole.Commands.Find(c => c.Names.Intersect(names.Split('|').ToIdentifiers()).Any());
|
||||
if (matchingCommand == null)
|
||||
{
|
||||
throw new Exception("AssignOnClientRequestExecute failed. Command matching the name(s) \"" + names + "\" not found.");
|
||||
}
|
||||
else
|
||||
{
|
||||
matchingCommand.OnClientRequestExecute = onClientRequestExecute;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
public void RemoveCommand(string name)
|
||||
{
|
||||
IService.CheckDisposed(this);
|
||||
|
||||
_registeredCommands.RemoveAll(cmd => cmd.Names.Contains(name));
|
||||
DebugConsole.Commands.RemoveAll(cmd => cmd.Names.Contains(name));
|
||||
}
|
||||
|
||||
public void RemoveRegisteredCommands()
|
||||
{
|
||||
IService.CheckDisposed(this);
|
||||
foreach (var cmd in _registeredCommands.ToImmutableArray())
|
||||
{
|
||||
DebugConsole.Commands.Remove(cmd);
|
||||
}
|
||||
_registeredCommands.Clear();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,426 @@
|
||||
using Barotrauma.LuaCs.Events;
|
||||
using FluentResults;
|
||||
using Microsoft.Toolkit.Diagnostics;
|
||||
using MoonSharp.Interpreter;
|
||||
using OneOf;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Barotrauma.LuaCs;
|
||||
|
||||
public partial class EventService : IEventService
|
||||
{
|
||||
private readonly record struct TypeStringKey : IEqualityComparer<TypeStringKey>, IEquatable<TypeStringKey>
|
||||
{
|
||||
public Type Type { get; init; }
|
||||
public string TypeName { get; init; }
|
||||
public readonly int HashCode;
|
||||
|
||||
public TypeStringKey(Type type)
|
||||
{
|
||||
Type = type ?? throw new ArgumentNullException(nameof(type));
|
||||
TypeName = type.Name.ToLowerInvariant();
|
||||
HashCode = TypeName.GetHashCode();
|
||||
}
|
||||
|
||||
public TypeStringKey(string typeName)
|
||||
{
|
||||
Type = null;
|
||||
TypeName = typeName?.ToLowerInvariant() ?? throw new ArgumentNullException(nameof(typeName));
|
||||
HashCode = TypeName.GetHashCode();
|
||||
}
|
||||
|
||||
public bool Equals(TypeStringKey x, TypeStringKey y)
|
||||
{
|
||||
if (x.Type is not null && y.Type is not null)
|
||||
return x.Type == y.Type;
|
||||
return x.TypeName == y.TypeName;
|
||||
}
|
||||
|
||||
public int GetHashCode(TypeStringKey obj)
|
||||
{
|
||||
return obj.HashCode;
|
||||
}
|
||||
|
||||
public static implicit operator TypeStringKey(Type type) => new(type);
|
||||
public static implicit operator TypeStringKey(string typeName) => new(typeName);
|
||||
}
|
||||
|
||||
private readonly ILoggerService _loggerService;
|
||||
private readonly ILuaPatcher _luaPatcher;
|
||||
private readonly AsyncReaderWriterLock _operationsLock = new();
|
||||
private readonly ConcurrentDictionary<TypeStringKey, ConcurrentDictionary<OneOf<IEvent, string>, IEvent>> _subscribers = new();
|
||||
private readonly ConcurrentDictionary<TypeStringKey, (TypeStringKey Event, Func<LuaCsFunc, IEvent> RunnerFactory)> _luaAliasEventFactory = new();
|
||||
private readonly ConcurrentDictionary<TypeStringKey, ConcurrentDictionary<TypeStringKey, LuaCsFunc>> _luaLegacyEventsSubscribers = new();
|
||||
private readonly ConcurrentDictionary<IEventService, IEventService> _subscribedEventDispatchers = new();
|
||||
|
||||
#region LifeCycle
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
using var lck = _operationsLock.AcquireWriterLock().ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
if (!ModUtils.Threading.CheckIfClearAndSetBool(ref _isDisposed))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_luaLegacyEventsSubscribers.Clear();
|
||||
_luaAliasEventFactory.Clear();
|
||||
_subscribers.Clear();
|
||||
_luaPatcher.Dispose();
|
||||
}
|
||||
|
||||
private int _isDisposed;
|
||||
|
||||
public EventService(ILoggerService loggerService, ILuaPatcher luaPatcher)
|
||||
{
|
||||
_loggerService = loggerService;
|
||||
_luaPatcher = luaPatcher;
|
||||
}
|
||||
|
||||
public bool IsDisposed
|
||||
{
|
||||
get => ModUtils.Threading.GetBool(ref _isDisposed);
|
||||
private set => ModUtils.Threading.SetBool(ref _isDisposed, value);
|
||||
}
|
||||
public FluentResults.Result Reset()
|
||||
{
|
||||
using var lck = _operationsLock.AcquireWriterLock().ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
IService.CheckDisposed(this);
|
||||
_luaLegacyEventsSubscribers.Clear();
|
||||
_luaAliasEventFactory.Clear();
|
||||
_subscribers.Clear();
|
||||
_luaPatcher.Reset();
|
||||
return FluentResults.Result.Ok();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region LuaEventSystem
|
||||
|
||||
public void Add(string eventName, string identifier, LuaCsFunc callback, object owner = null)
|
||||
{
|
||||
Guard.IsNotNullOrWhiteSpace(eventName, nameof(eventName));
|
||||
Guard.IsNotNullOrWhiteSpace(identifier, nameof(identifier));
|
||||
Guard.IsNotNull(callback, nameof(callback));
|
||||
using var lck = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
IService.CheckDisposed(this);
|
||||
|
||||
if (_luaAliasEventFactory.TryGetValue(eventName, out var eventFunc))
|
||||
{
|
||||
var eventSubs = _subscribers.GetOrAdd(eventFunc.Event, key => new ConcurrentDictionary<OneOf<IEvent, string>, IEvent>());
|
||||
eventSubs[identifier] = eventFunc.RunnerFactory(callback);
|
||||
}
|
||||
else
|
||||
{
|
||||
var eventSubs = _luaLegacyEventsSubscribers.GetOrAdd(eventName, key => new ConcurrentDictionary<TypeStringKey, LuaCsFunc>());
|
||||
eventSubs[identifier] = callback;
|
||||
}
|
||||
}
|
||||
|
||||
public void Add(string eventName, LuaCsFunc callback, object owner = null)
|
||||
{
|
||||
// random ident, we hope for no conflicts :barodev:.
|
||||
Add(eventName, Random.Shared.NextInt64().ToString() ,callback);
|
||||
}
|
||||
|
||||
public object Call(string eventName, params object[] args)
|
||||
{
|
||||
return Call<object>(eventName, args);
|
||||
}
|
||||
|
||||
[MoonSharpHidden] // Needs to be hidden so Lua doesn't accidentally use this instead of the above
|
||||
public T Call<T>(string eventName, params object[] args)
|
||||
{
|
||||
Guard.IsNotNullOrWhiteSpace(eventName, nameof(eventName));
|
||||
using var lck = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
IService.CheckDisposed(this);
|
||||
|
||||
if (!_luaLegacyEventsSubscribers.TryGetValue(eventName, out var eventSubscribers)
|
||||
|| eventSubscribers.IsEmpty)
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
T returnValue = default;
|
||||
|
||||
foreach (var subscriber in eventSubscribers)
|
||||
{
|
||||
try
|
||||
{
|
||||
object result = subscriber.Value.Invoke(args);
|
||||
if (result is DynValue luaResult)
|
||||
{
|
||||
if (luaResult.Type == DataType.Tuple)
|
||||
{
|
||||
bool replaceNil = luaResult.Tuple.Length > 1 && luaResult.Tuple[1].CastToBool();
|
||||
|
||||
if (!luaResult.Tuple[0].IsNil() || replaceNil)
|
||||
{
|
||||
returnValue = luaResult.ToObject<T>();
|
||||
}
|
||||
}
|
||||
else if (!luaResult.IsNil())
|
||||
{
|
||||
returnValue = luaResult.ToObject<T>();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
returnValue = (T)result;
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_loggerService.LogError(e.Message);
|
||||
#if DEBUG
|
||||
throw;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
return returnValue;
|
||||
}
|
||||
|
||||
public void Subscribe<T>(string identifier, IDictionary<string, LuaCsFunc> callbacks) where T : class, IEvent<T>
|
||||
{
|
||||
Guard.IsNotNullOrWhiteSpace(identifier, nameof(identifier));
|
||||
Guard.IsNotNull(callbacks, nameof(callbacks));
|
||||
Guard.IsNotEmpty(callbacks, nameof(callbacks));
|
||||
using var lck = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
IService.CheckDisposed(this);
|
||||
|
||||
var eventSubs = _subscribers.GetOrAdd(typeof(T), key => new ConcurrentDictionary<OneOf<IEvent, string>, IEvent>());
|
||||
eventSubs[identifier] = T.GetLuaRunner(callbacks);
|
||||
}
|
||||
|
||||
public void Remove(string eventName, string identifier)
|
||||
{
|
||||
Guard.IsNotNullOrWhiteSpace(eventName, nameof(eventName));
|
||||
Guard.IsNotNullOrWhiteSpace(identifier, nameof(identifier));
|
||||
|
||||
using var lck = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
IService.CheckDisposed(this);
|
||||
|
||||
if (_luaAliasEventFactory.TryGetValue(eventName, out var eventFunc))
|
||||
{
|
||||
if (_subscribers.TryGetValue(eventFunc.Event, out var eventSubs))
|
||||
{
|
||||
eventSubs.TryRemove(identifier, out _);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_luaLegacyEventsSubscribers.TryGetValue(eventName, out var eventSubs))
|
||||
{
|
||||
eventSubs.TryRemove(identifier, out _);
|
||||
}
|
||||
}
|
||||
}
|
||||
public void Unsubscribe(string eventName, string identifier)
|
||||
{
|
||||
Guard.IsNotNullOrWhiteSpace(eventName, nameof(eventName));
|
||||
Guard.IsNotNullOrWhiteSpace(identifier, nameof(identifier));
|
||||
using var lck = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
|
||||
if (!_subscribers.TryGetValue(eventName, out var evtSubscribers))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
evtSubscribers.TryRemove(identifier, out _);
|
||||
}
|
||||
|
||||
public void PublishLuaEvent<T>(LuaCsFunc subscriberRunner) where T : class, IEvent<T>
|
||||
{
|
||||
this.PublishEvent<T>(sub => subscriberRunner(sub));
|
||||
}
|
||||
|
||||
public FluentResults.Result RegisterLuaEventAlias<T>(string luaEventName, string targetMethod) where T : class, IEvent<T>
|
||||
{
|
||||
Guard.IsNotNullOrWhiteSpace(luaEventName, nameof(luaEventName));
|
||||
Guard.IsNotNullOrWhiteSpace(targetMethod, nameof(targetMethod));
|
||||
using var lck = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
IService.CheckDisposed(this);
|
||||
|
||||
if (_luaAliasEventFactory.ContainsKey(luaEventName))
|
||||
{
|
||||
#if DEBUG
|
||||
ThrowHelper.ThrowInvalidOperationException($"{nameof(RegisterLuaEventAlias)}: An alias already exists for the event of {luaEventName}.");
|
||||
#endif
|
||||
return FluentResults.Result.Fail($"{nameof(RegisterLuaEventAlias)}: An alias already exists for the event of {luaEventName}.");
|
||||
}
|
||||
|
||||
var eventRunnerFactory = (LuaCsFunc function) => (IEvent)T.GetLuaRunner(new Dictionary<string, LuaCsFunc>
|
||||
{
|
||||
{ targetMethod, function }
|
||||
});
|
||||
|
||||
_luaAliasEventFactory[luaEventName] = (Event: typeof(T), RunnerFactory: eventRunnerFactory);
|
||||
// create the group
|
||||
_subscribers.GetOrAdd(typeof(T), key => new ConcurrentDictionary<OneOf<IEvent, string>, IEvent>());
|
||||
return FluentResults.Result.Ok();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
public FluentResults.Result Subscribe<T>(T subscriber) where T : class, IEvent<T>
|
||||
{
|
||||
Guard.IsNotNull(subscriber, nameof(subscriber));
|
||||
using var lck = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
IService.CheckDisposed(this);
|
||||
|
||||
var eventSubs =
|
||||
_subscribers.GetOrAdd(typeof(T), (type) => new ConcurrentDictionary<OneOf<IEvent, string>, IEvent>());
|
||||
|
||||
if (eventSubs.ContainsKey(subscriber))
|
||||
{
|
||||
ThrowHelper.ThrowInvalidOperationException($"{nameof(Subscribe)}: The instance is already registered!");
|
||||
}
|
||||
|
||||
return eventSubs.TryAdd(subscriber, subscriber)
|
||||
? FluentResults.Result.Ok()
|
||||
: FluentResults.Result.Fail($"{nameof(Subscribe)}: Failed to add subscriber.");
|
||||
}
|
||||
|
||||
public void Unsubscribe<T>(T subscriber) where T : class, IEvent
|
||||
{
|
||||
Guard.IsNotNull(subscriber, nameof(subscriber));
|
||||
using var lck = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
IService.CheckDisposed(this);
|
||||
|
||||
if (!_subscribers.TryGetValue(typeof(T), out var evtSubscribers))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
evtSubscribers.TryRemove(subscriber, out _);
|
||||
}
|
||||
|
||||
public void ClearAllEventSubscribers<T>() where T : class, IEvent
|
||||
{
|
||||
using var lck = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
IService.CheckDisposed(this);
|
||||
_subscribers.TryRemove(typeof(T), out _);
|
||||
}
|
||||
|
||||
public void ClearAllSubscribers()
|
||||
{
|
||||
using var lck = _operationsLock.AcquireWriterLock().ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
IService.CheckDisposed(this);
|
||||
_subscribers.Clear();
|
||||
}
|
||||
|
||||
public FluentResults.Result PublishEvent<T>(Action<T> action) where T : class, IEvent<T>
|
||||
{
|
||||
Guard.IsNotNull(action, nameof(action));
|
||||
using var lck = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
IService.CheckDisposed(this);
|
||||
|
||||
if (!_subscribers.TryGetValue(typeof(T), out var subs) || subs.IsEmpty)
|
||||
{
|
||||
return FluentResults.Result.Ok();
|
||||
}
|
||||
|
||||
var results = new FluentResults.Result();
|
||||
|
||||
foreach (var sub in subs)
|
||||
{
|
||||
try
|
||||
{
|
||||
action.Invoke(Unsafe.As<T>(sub.Value));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
results.WithError(new ExceptionalError(e));
|
||||
_loggerService.LogError(e.Message);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var dispatchers in _subscribedEventDispatchers.ToImmutableArray())
|
||||
{
|
||||
dispatchers.Value.PublishEvent(action);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
public void AddDispatcherEventService(IEventService eventService)
|
||||
{
|
||||
using var lck = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
IService.CheckDisposed(this);
|
||||
|
||||
_subscribedEventDispatchers.TryAdd(eventService, eventService);
|
||||
}
|
||||
|
||||
public void RemoveDispatcherEventService(IEventService eventService)
|
||||
{
|
||||
using var lck = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
IService.CheckDisposed(this);
|
||||
|
||||
_subscribedEventDispatchers.TryRemove(eventService, out _);
|
||||
}
|
||||
|
||||
#region LuaPatcherAdapter
|
||||
public string Patch(string identifier, string className, string methodName, string[] parameterTypes, LuaCsPatchFunc patch, LuaCsHook.HookMethodType hookType = LuaCsHook.HookMethodType.Before)
|
||||
{
|
||||
return _luaPatcher.Patch(identifier, className, methodName, parameterTypes, patch, hookType);
|
||||
}
|
||||
|
||||
public string Patch(string identifier, string className, string methodName, LuaCsPatchFunc patch, LuaCsHook.HookMethodType hookType = LuaCsHook.HookMethodType.Before)
|
||||
{
|
||||
return _luaPatcher.Patch(identifier, className, methodName, patch, hookType);
|
||||
}
|
||||
|
||||
public string Patch(string className, string methodName, string[] parameterTypes, LuaCsPatchFunc patch, LuaCsHook.HookMethodType hookType = LuaCsHook.HookMethodType.Before)
|
||||
{
|
||||
return _luaPatcher.Patch(className, methodName, parameterTypes, patch, hookType);
|
||||
}
|
||||
|
||||
public string Patch(string className, string methodName, LuaCsPatchFunc patch, LuaCsHook.HookMethodType hookType = LuaCsHook.HookMethodType.Before)
|
||||
{
|
||||
return _luaPatcher.Patch(className, methodName, patch, hookType);
|
||||
}
|
||||
|
||||
public bool RemovePatch(string identifier, string className, string methodName, string[] parameterTypes, LuaCsHook.HookMethodType hookType)
|
||||
{
|
||||
return _luaPatcher.RemovePatch(className, className, methodName, parameterTypes, hookType);
|
||||
}
|
||||
|
||||
public bool RemovePatch(string identifier, string className, string methodName, LuaCsHook.HookMethodType hookType)
|
||||
{
|
||||
return _luaPatcher.RemovePatch(className, className, methodName, hookType);
|
||||
}
|
||||
|
||||
public void HookMethod(string identifier, MethodBase method, LuaCsPatch patch, LuaCsHook.HookMethodType hookType = LuaCsHook.HookMethodType.Before, IAssemblyPlugin owner = null)
|
||||
{
|
||||
_luaPatcher.HookMethod(identifier, method, patch, hookType, owner);
|
||||
}
|
||||
|
||||
public void HookMethod(string identifier, string className, string methodName, string[] parameterNames, LuaCsPatch patch, LuaCsHook.HookMethodType hookMethodType = LuaCsHook.HookMethodType.Before)
|
||||
{
|
||||
_luaPatcher.HookMethod(identifier, className, methodName, parameterNames, patch, hookMethodType);
|
||||
}
|
||||
|
||||
public void HookMethod(string identifier, string className, string methodName, LuaCsPatch patch, LuaCsHook.HookMethodType hookMethodType = LuaCsHook.HookMethodType.Before)
|
||||
{
|
||||
_luaPatcher.HookMethod(identifier, className, methodName, patch, hookMethodType);
|
||||
}
|
||||
|
||||
public void HookMethod(string className, string methodName, LuaCsPatch patch, LuaCsHook.HookMethodType hookMethodType = LuaCsHook.HookMethodType.Before)
|
||||
{
|
||||
_luaPatcher.HookMethod(className, methodName, patch, hookMethodType);
|
||||
}
|
||||
|
||||
public void HookMethod(string className, string methodName, string[] parameterNames, LuaCsPatch patch, LuaCsHook.HookMethodType hookMethodType = LuaCsHook.HookMethodType.Before)
|
||||
{
|
||||
_luaPatcher.HookMethod(className, methodName, parameterNames, patch, hookMethodType);
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
+416
@@ -0,0 +1,416 @@
|
||||
using Barotrauma.Items.Components;
|
||||
using Barotrauma.LuaCs;
|
||||
using Barotrauma.LuaCs.Events;
|
||||
using Barotrauma.Networking;
|
||||
using Barotrauma.Steam;
|
||||
using HarmonyLib;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using static Barotrauma.ContentPackageManager;
|
||||
|
||||
namespace Barotrauma.LuaCs;
|
||||
|
||||
[HarmonyPatch]
|
||||
internal class HarmonyEventPatchesService : ISystem
|
||||
{
|
||||
public bool IsDisposed { get; private set; }
|
||||
public FluentResults.Result Reset()
|
||||
{
|
||||
Unpatch();
|
||||
Patch();
|
||||
return FluentResults.Result.Ok();
|
||||
}
|
||||
|
||||
private static IEventService _eventService;
|
||||
private static ILoggerService _loggerService;
|
||||
private readonly Harmony Harmony;
|
||||
|
||||
public HarmonyEventPatchesService(IEventService eventService, ILoggerService loggerService)
|
||||
{
|
||||
_eventService = eventService;
|
||||
_loggerService = loggerService;
|
||||
Harmony = new Harmony("LuaCsForBarotrauma.Events");
|
||||
Patch();
|
||||
}
|
||||
|
||||
private void Patch()
|
||||
{
|
||||
this.Harmony?.PatchAll(typeof(HarmonyEventPatchesService));
|
||||
#if SERVER
|
||||
this.Harmony?.PatchAll(typeof(HarmonyEventPatchesService.Patch_StartGame_End));
|
||||
#endif
|
||||
}
|
||||
|
||||
private void Unpatch()
|
||||
{
|
||||
this.Harmony?.UnpatchSelf();
|
||||
}
|
||||
|
||||
|
||||
[HarmonyPatch(typeof(CoroutineManager), nameof(CoroutineManager.Update)), HarmonyPostfix]
|
||||
public static void CoroutineManager_Update_Post()
|
||||
{
|
||||
_eventService.PublishEvent<IEventUpdate>(x => x.OnUpdate(CoroutineManager.DeltaTime));
|
||||
_loggerService.ProcessLogs();
|
||||
}
|
||||
|
||||
#if CLIENT
|
||||
[HarmonyPatch(typeof(GameSession), nameof(GameSession.StartRound), new Type[]
|
||||
{
|
||||
typeof(LevelData), typeof(bool), typeof(SubmarineInfo), typeof(SubmarineInfo)
|
||||
}), HarmonyPostfix]
|
||||
public static void GameSession_StartRound_Post()
|
||||
{
|
||||
_eventService.PublishEvent<IEventRoundStarted>(x => x.OnRoundStart());
|
||||
}
|
||||
#endif
|
||||
|
||||
[HarmonyPatch(typeof(GameSession), nameof(GameSession.EndRound)), HarmonyPrefix]
|
||||
public static void GameSession_EndRound_Pre()
|
||||
{
|
||||
_eventService.PublishEvent<IEventRoundEnded>(x => x.OnRoundEnd());
|
||||
}
|
||||
|
||||
[HarmonyPatch(typeof(GameSession), nameof(GameSession.LoadPreviousSave)), HarmonyPrefix]
|
||||
public static void GameSession_LoadPreviousSave_Pre()
|
||||
{
|
||||
_eventService.PublishEvent<IEventRoundEnded>(x => x.OnRoundEnd());
|
||||
}
|
||||
|
||||
[HarmonyPatch(typeof(GameSession), nameof(GameSession.EndMissions)), HarmonyPostfix]
|
||||
public static void GameSession_EndMission_Post(GameSession __instance)
|
||||
{
|
||||
_eventService.PublishEvent<IEventMissionsEnded>(x => x.OnMissionsEnded(__instance.Missions.ToList()));
|
||||
}
|
||||
|
||||
[HarmonyPatch(typeof(Screen), nameof(Screen.Select)), HarmonyPostfix]
|
||||
public static void Screen_Selected_Post(Screen __instance)
|
||||
{
|
||||
_eventService.PublishEvent<IEventScreenSelected>(x => x.OnScreenSelected(__instance));
|
||||
}
|
||||
|
||||
#if CLIENT
|
||||
[HarmonyPatch(typeof(MainMenuScreen), "StartGame"), HarmonyPostfix]
|
||||
public static void MainMenuScreen_StartGame_Pre(Screen __instance)
|
||||
{
|
||||
LuaCsSetup.Instance.SetRunState(RunState.Running);
|
||||
}
|
||||
|
||||
[HarmonyPatch(typeof(MainMenuScreen), "LoadGame"), HarmonyPostfix]
|
||||
public static void MainMenuScreen_LoadGame_Pre(Screen __instance)
|
||||
{
|
||||
LuaCsSetup.Instance.SetRunState(RunState.Running);
|
||||
}
|
||||
|
||||
[HarmonyPatch(typeof(MutableWorkshopMenu), nameof(MutableWorkshopMenu.Apply)), HarmonyPostfix]
|
||||
public static void MutableWorkshopMenu_Apply_Post(Screen __instance)
|
||||
{
|
||||
LuaCsSetup.Instance.PromptCSharpMods(selection => { }, joiningServer: false);
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
[HarmonyPatch(typeof(ContentPackageManager.PackageSource), nameof(ContentPackageManager.PackageSource.Refresh)), HarmonyPostfix]
|
||||
public static void PackageSource_Refresh_Post()
|
||||
{
|
||||
_eventService.PublishEvent<IEventAllPackageListChanged>(x => x.OnAllPackageListChanged(ContentPackageManager.CorePackages, ContentPackageManager.RegularPackages));
|
||||
}
|
||||
|
||||
[HarmonyPatch(typeof(ContentPackageManager), nameof(ContentPackageManager.Init)), HarmonyPostfix]
|
||||
public static void ContentPackageManager_Init_Post()
|
||||
{
|
||||
_eventService.PublishEvent<IEventAllPackageListChanged>(x => x.OnAllPackageListChanged(ContentPackageManager.CorePackages, ContentPackageManager.RegularPackages));
|
||||
_eventService.PublishEvent<IEventEnabledPackageListChanged>(sub => sub.OnEnabledPackageListChanged(EnabledPackages.Core, EnabledPackages.Regular));
|
||||
}
|
||||
|
||||
[HarmonyPatch(typeof(ContentPackageManager.EnabledPackages), nameof(ContentPackageManager.EnabledPackages.SetCore)), HarmonyPostfix]
|
||||
public static void EnabledPackages_SetCore_Post()
|
||||
{
|
||||
_eventService.PublishEvent<IEventEnabledPackageListChanged>(sub => sub.OnEnabledPackageListChanged(EnabledPackages.Core, EnabledPackages.Regular));
|
||||
}
|
||||
|
||||
[HarmonyPatch(typeof(ContentPackageManager.EnabledPackages), nameof(ContentPackageManager.EnabledPackages.SetRegular)), HarmonyPostfix]
|
||||
public static void EnabledPackages_SetRegular_Post()
|
||||
{
|
||||
_eventService.PublishEvent<IEventEnabledPackageListChanged>(sub => sub.OnEnabledPackageListChanged(EnabledPackages.Core, EnabledPackages.Regular));
|
||||
}
|
||||
|
||||
#if CLIENT
|
||||
[HarmonyPatch(typeof(GameClient), "ReadDataMessage"), HarmonyPrefix]
|
||||
public static bool GameClient_ReadDataMessage_Pre(IReadMessage inc)
|
||||
{
|
||||
int prevBitPosition = inc.BitPosition;
|
||||
ServerPacketHeader header = (ServerPacketHeader)inc.ReadByte();
|
||||
bool? skip = null;
|
||||
_eventService.PublishEvent<IEventServerRawNetMessageReceived>(x => skip = x.OnReceivedServerNetMessage(inc, header) ?? skip);
|
||||
|
||||
if (skip == true)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
inc.BitPosition = prevBitPosition; // rewind so the game can read the message
|
||||
return true;
|
||||
}
|
||||
|
||||
[HarmonyPatch(typeof(SubEditorScreen), nameof(SubEditorScreen.Select), new Type[] { }), HarmonyPostfix]
|
||||
public static void SubEditorScreen_Selected_Post(Screen __instance)
|
||||
{
|
||||
_eventService.PublishEvent<IEventScreenSelected>(x => x.OnScreenSelected(__instance));
|
||||
}
|
||||
|
||||
[HarmonyPatch(typeof(PlayerInput), nameof(PlayerInput.Update)), HarmonyPrefix]
|
||||
public static void PlayerInput_Update_Pre(double deltaTime)
|
||||
{
|
||||
_eventService.PublishEvent<IEventKeyUpdate>(x => x.OnKeyUpdate(deltaTime));
|
||||
}
|
||||
|
||||
[HarmonyPatch(typeof(DebugConsole), "IsCommandPermitted"), HarmonyPrefix]
|
||||
public static bool DebugConsole_IsCommandPermitted(Identifier command, ref bool __result)
|
||||
{
|
||||
DebugConsole.Command c = DebugConsole.FindCommand(command.Value);
|
||||
|
||||
if (DebugConsole.Commands.IndexOf(c) >= LuaCsSetup.DebugConsoleCommandVanillaIndex)
|
||||
{
|
||||
__result = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
#elif SERVER
|
||||
[HarmonyPatch(typeof(GameServer), "ReadDataMessage"), HarmonyPrefix]
|
||||
public static bool GameServer_ReadDataMessage_Pre(NetworkConnection sender, IReadMessage inc)
|
||||
{
|
||||
int prevBitPosition = inc.BitPosition;
|
||||
ClientPacketHeader header = (ClientPacketHeader)inc.ReadByte();
|
||||
|
||||
bool? skip = null;
|
||||
_eventService.PublishEvent<IEventClientRawNetMessageReceived>(x => skip = x.OnReceivedClientNetMessage(inc, header, sender) ?? skip);
|
||||
|
||||
if (skip == true)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
inc.BitPosition = prevBitPosition; // rewind so the game can read the message
|
||||
return true;
|
||||
}
|
||||
|
||||
[HarmonyPatch(typeof(GameServer), "OnInitializationComplete"), HarmonyPostfix]
|
||||
public static void GameServer_OnInitializationComplete_Post(GameServer __instance)
|
||||
{
|
||||
Client client = __instance.ConnectedClients.LastOrDefault();
|
||||
if (client == null) { return; }
|
||||
_eventService.PublishEvent<IEventClientConnected>(x => x.OnClientConnected(client));
|
||||
}
|
||||
|
||||
[HarmonyPatch(typeof(GameServer), nameof(GameServer.DisconnectClient), new Type[] { typeof(Client), typeof(PeerDisconnectPacket) }), HarmonyPrefix]
|
||||
public static void GameServer_DisconnectClient_Pre(Client client, PeerDisconnectPacket peerDisconnectPacket)
|
||||
{
|
||||
if (client == null) { return; }
|
||||
|
||||
_eventService.PublishEvent<IEventClientDisconnected>(x => x.OnClientDisconnected(client));
|
||||
}
|
||||
|
||||
[HarmonyPatch(typeof(GameServer), nameof(GameServer.AssignJobs)), HarmonyPostfix]
|
||||
public static void GameServer_AssignJobs_Post(List<Client> unassigned)
|
||||
{
|
||||
_eventService.PublishEvent<IEventJobsAssigned>(x => x.OnJobsAssigned(unassigned));
|
||||
}
|
||||
#endif
|
||||
|
||||
[HarmonyPatch(typeof(Character), nameof(Character.Create), new[] {
|
||||
typeof(CharacterPrefab),
|
||||
typeof(Vector2),
|
||||
typeof(string),
|
||||
typeof(CharacterInfo),
|
||||
typeof(ushort),
|
||||
typeof(bool),
|
||||
typeof(bool),
|
||||
typeof(bool),
|
||||
typeof(RagdollParams),
|
||||
typeof(bool)
|
||||
}), HarmonyPostfix]
|
||||
public static void Character_Create_Post(Character __result)
|
||||
{
|
||||
_eventService.PublishEvent<IEventCharacterCreated>(x => x.OnCharacterCreated(__result));
|
||||
}
|
||||
|
||||
[HarmonyPatch(typeof(Character), "KillProjSpecific"), HarmonyPostfix]
|
||||
public static void Character_Kill_Post(Character __instance, Affliction causeOfDeathAffliction, CauseOfDeathType causeOfDeath)
|
||||
{
|
||||
_eventService.PublishEvent<IEventCharacterDeath>(x => x.OnCharacterDeath(__instance, causeOfDeathAffliction, causeOfDeath));
|
||||
}
|
||||
|
||||
[HarmonyPatch(typeof(Character), nameof(Character.GiveJobItems)), HarmonyPostfix]
|
||||
public static void Character_GiveJobItems_Post(Character __instance, WayPoint spawnPoint, bool isPvPMode)
|
||||
{
|
||||
_eventService.PublishEvent<IEventGiveCharacterJobItems>(x => x.OnGiveCharacterJobItems(__instance, spawnPoint, isPvPMode));
|
||||
}
|
||||
|
||||
[HarmonyPatch(typeof(Character), nameof(Character.DamageLimb)), HarmonyPrefix]
|
||||
public static bool Character_DamageLimb_Pre(AttackResult __result, Character __instance, Vector2 worldPosition, Limb hitLimb, IEnumerable<Affliction> afflictions, float stun, bool playSound, Vector2 attackImpulse, Character attacker, float damageMultiplier, bool allowStacking, float penetration, bool shouldImplode, bool ignoreDamageOverlay, bool recalculateVitality)
|
||||
{
|
||||
AttackResult? result = null;
|
||||
_eventService.PublishEvent<IEventCharacterDamageLimb>(x => result = x.OnCharacterDamageLimb(__instance, worldPosition, hitLimb, afflictions, stun, playSound, attackImpulse, attacker, damageMultiplier, allowStacking, penetration, shouldImplode));
|
||||
if (result != null)
|
||||
{
|
||||
__result = (AttackResult)result;
|
||||
return false; // skip
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
[HarmonyPatch(typeof(Affliction), nameof(Affliction.Update)), HarmonyPostfix]
|
||||
public static void Affliction_Update_Post(Affliction __instance, CharacterHealth characterHealth, Limb targetLimb, float deltaTime)
|
||||
{
|
||||
_eventService.PublishEvent<IEventAfflictionUpdate>(x => x.OnAfflictionUpdate(__instance, characterHealth, targetLimb, deltaTime));
|
||||
}
|
||||
|
||||
[HarmonyPatch(typeof(Connection), nameof(Connection.SendSignal)), HarmonyPostfix]
|
||||
public static void Connection_SendSignal_Post(Connection __instance, Signal signal)
|
||||
{
|
||||
foreach (var wire in __instance.Wires)
|
||||
{
|
||||
Connection recipient = wire.OtherConnection(__instance);
|
||||
if (recipient == null) { continue; }
|
||||
|
||||
_eventService.PublishEvent<IEventSignalReceived>(x => x.OnSignalReceived(signal, recipient));
|
||||
_eventService.Call("signalReceived." + recipient.Item.Prefab.Identifier, signal, recipient);
|
||||
}
|
||||
|
||||
foreach (CircuitBoxConnection connection in __instance.CircuitBoxConnections)
|
||||
{
|
||||
_eventService.PublishEvent<IEventSignalReceived>(x => x.OnSignalReceived(signal, connection.Connection));
|
||||
_eventService.Call("signalReceived." + connection.Connection.Item.Prefab.Identifier, signal, connection.Connection);
|
||||
}
|
||||
}
|
||||
|
||||
[HarmonyPatch(typeof(Item), MethodType.Constructor, new Type[] { typeof(Rectangle), typeof(ItemPrefab), typeof(Submarine), typeof(bool), typeof(ushort) }), HarmonyPostfix]
|
||||
public static void Item_Ctor_Post(Item __instance)
|
||||
{
|
||||
_eventService.PublishEvent<IEventItemCreated>(x => x.OnItemCreated(__instance));
|
||||
}
|
||||
|
||||
[HarmonyPatch(typeof(Item), nameof(Item.Remove)), HarmonyPostfix]
|
||||
public static void Item_Remove_Post(Item __instance)
|
||||
{
|
||||
_eventService.PublishEvent<IEventItemRemoved>(x => x.OnItemRemoved(__instance));
|
||||
}
|
||||
|
||||
[HarmonyPatch(typeof(Item), nameof(Item.Remove)), HarmonyPostfix]
|
||||
public static void Item_ShallowRemove_Post(Item __instance)
|
||||
{
|
||||
_eventService.PublishEvent<IEventItemRemoved>(x => x.OnItemRemoved(__instance));
|
||||
}
|
||||
|
||||
[HarmonyPatch(typeof(Item), nameof(Item.Use)), HarmonyPrefix]
|
||||
public static bool Item_Use_Pre(Item __instance, Character user, Limb targetLimb, Entity useTarget)
|
||||
{
|
||||
if (__instance.RequireAimToUse && (user == null || !user.IsKeyDown(InputType.Aim)))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (__instance.Condition <= 0.0f) { return true; }
|
||||
|
||||
bool? result = null;
|
||||
_eventService.PublishEvent<IEventItemUse>(x => result = x.OnItemUsed(__instance, user, targetLimb, useTarget));
|
||||
if (result == true)
|
||||
{
|
||||
return false; // skip
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
[HarmonyPatch(typeof(Item), nameof(Item.SecondaryUse)), HarmonyPrefix]
|
||||
public static bool Item_SecondaryUse_Pre(Item __instance, Character character)
|
||||
{
|
||||
if (__instance.Condition <= 0.0f) { return true; }
|
||||
|
||||
bool? result = null;
|
||||
_eventService.PublishEvent<IEventItemSecondaryUse>(x => result = x.OnItemSecondaryUsed(__instance, character));
|
||||
if (result == true)
|
||||
{
|
||||
return false; // skip
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
[HarmonyPatch(typeof(Inventory), "PutItem"), HarmonyPrefix]
|
||||
public static bool Inventory_PutItem_Prefix(Inventory __instance, Item item, int i, Character user, bool removeItem)
|
||||
{
|
||||
bool? result = null;
|
||||
_eventService.PublishEvent<IEventInventoryPutItem>(x => result = x.OnInventoryPutItem(__instance, item, user, i, removeItem));
|
||||
if (result == true)
|
||||
{
|
||||
return false; // skip
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
[HarmonyPatch(typeof(Inventory), "TrySwapping"), HarmonyPrefix]
|
||||
public static bool Inventory_TrySwapping_Prefix(Inventory __instance, Item item, int index, Character user, bool swapWholeStack, ref bool __result)
|
||||
{
|
||||
// uncomment when we are plugin
|
||||
// if (item?.ParentInventory == null || !__instance.slots[index].Any()) { return false; }
|
||||
// if (__instance.slots[index].Items.Any(it => !it.IsInteractable(user))) { return false; }
|
||||
if (!__instance.AllowSwappingContainedItems) { return false; }
|
||||
|
||||
bool? result = null;
|
||||
_eventService.PublishEvent<IEventInventoryItemSwap>(x => result = x.OnInventoryItemSwap(__instance, item, user, index, swapWholeStack));
|
||||
if (result != null)
|
||||
{
|
||||
__result = (bool)result;
|
||||
return false; // skip
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
IsDisposed = true;
|
||||
this.Harmony?.UnpatchSelf();
|
||||
}
|
||||
|
||||
#if SERVER
|
||||
[HarmonyPatch]
|
||||
class Patch_StartGame_End
|
||||
{
|
||||
static MethodBase TargetMethod()
|
||||
{
|
||||
var original = AccessTools.Method(
|
||||
typeof(GameServer),
|
||||
"StartGame"
|
||||
);
|
||||
|
||||
return AccessTools.EnumeratorMoveNext(original);
|
||||
}
|
||||
|
||||
[HarmonyPostfix]
|
||||
static void Postfix(object __instance, bool __result)
|
||||
{
|
||||
if (!__result) { return; }
|
||||
|
||||
var enumerator = __instance as IEnumerator<CoroutineStatus>;
|
||||
if (enumerator == null) { return; }
|
||||
|
||||
if (enumerator.Current == CoroutineStatus.Success)
|
||||
{
|
||||
_eventService.PublishEvent<IEventRoundStarted>(x => x.OnRoundStart());
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
@@ -0,0 +1,218 @@
|
||||
using Barotrauma.LuaCs.Events;
|
||||
using Barotrauma.Networking;
|
||||
using FluentResults;
|
||||
using HarmonyLib;
|
||||
using Microsoft.Xna.Framework;
|
||||
using MoonSharp.Interpreter;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Barotrauma.LuaCs;
|
||||
|
||||
public partial class LoggerService : ILoggerService
|
||||
{
|
||||
private List<ILoggerSubscriber> logSubscribers = [];
|
||||
private ConcurrentQueue<PendingLog> logQueue = [];
|
||||
|
||||
#if SERVER
|
||||
private const string TargetPrefix = "[SV]";
|
||||
private const int NetMaxLength = 1024; // character limit of vanilla Barotrauma's chat system.
|
||||
private const int NetMaxMessages = 60;
|
||||
|
||||
// This is used so it's possible to call logging functions inside the serverLog
|
||||
// hook without creating an infinite loop
|
||||
private bool _isInsideLogCall = false;
|
||||
#else
|
||||
private const string TargetPrefix = "[CL]";
|
||||
#endif
|
||||
|
||||
public LoggerService() { }
|
||||
|
||||
public void Subscribe(ILoggerSubscriber subscriber)
|
||||
{
|
||||
logSubscribers.Add(subscriber);
|
||||
}
|
||||
|
||||
public void Unsubscribe(ILoggerSubscriber subscriber)
|
||||
{
|
||||
logSubscribers.Remove(subscriber);
|
||||
}
|
||||
|
||||
public void ProcessLogs()
|
||||
{
|
||||
while (logQueue.TryDequeue(out PendingLog log))
|
||||
{
|
||||
logSubscribers.ForEach(s => s.OnLog(log));
|
||||
|
||||
DebugConsole.NewMessage(log.Message, log.Color);
|
||||
|
||||
#if SERVER
|
||||
if (GameMain.Server != null)
|
||||
{
|
||||
if (GameMain.Server.ServerSettings.SaveServerLogs)
|
||||
{
|
||||
string logMessage = "[LuaCs] " + log.Message;
|
||||
GameMain.Server.ServerSettings.ServerLog.WriteLine(logMessage, log.MessageType, false);
|
||||
|
||||
if (!_isInsideLogCall)
|
||||
{
|
||||
_isInsideLogCall = true;
|
||||
LuaCsSetup.Instance?.EventService.PublishEvent<IEventServerLog>(x => x.OnServerLog(logMessage, log.MessageType));
|
||||
_isInsideLogCall = false;
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < log.Message.Length; i += NetMaxLength)
|
||||
{
|
||||
string subStr = log.Message.Substring(i, Math.Min(1024, log.Message.Length - i));
|
||||
BroadcastMessage(subStr);
|
||||
}
|
||||
}
|
||||
|
||||
void BroadcastMessage(string m)
|
||||
{
|
||||
foreach (var client in GameMain.Server.ConnectedClients)
|
||||
{
|
||||
ChatMessage consoleMessage = ChatMessage.Create("", m, ChatMessageType.Console, null, textColor: log.Color);
|
||||
GameMain.Server.SendDirectChatMessage(consoleMessage, client);
|
||||
|
||||
if (!GameMain.Server.ServerSettings.SaveServerLogs || !client.HasPermission(ClientPermissions.ServerLog))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
ChatMessage logMessage = ChatMessage.Create(log.MessageType.ToString(), "[LuaCs] " + m, ChatMessageType.ServerLog, null);
|
||||
GameMain.Server.SendDirectChatMessage(logMessage, client);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
public void Log(string message, Color? color = null, ServerLog.MessageType messageType = ServerLog.MessageType.ServerMessage)
|
||||
{
|
||||
if (LuaCsSetup.Instance.HideUserNamesInLogs && !Environment.UserName.IsNullOrEmpty())
|
||||
{
|
||||
message = message.Replace(Environment.UserName, "USERNAME");
|
||||
}
|
||||
|
||||
message = $"{TargetPrefix} {message}";
|
||||
|
||||
logQueue.Enqueue(new PendingLog(message, color, messageType));
|
||||
}
|
||||
|
||||
public void LogError(string message)
|
||||
{
|
||||
Log($"{message}", Color.Red, ServerLog.MessageType.Error);
|
||||
}
|
||||
|
||||
public void LogWarning(string message)
|
||||
{
|
||||
Log($"{message}", Color.Yellow, ServerLog.MessageType.ServerMessage);
|
||||
}
|
||||
|
||||
public void LogMessage(string message, Color? serverColor = null, Color? clientColor = null)
|
||||
{
|
||||
serverColor ??= Color.MediumPurple;
|
||||
clientColor ??= Color.Purple;
|
||||
|
||||
#if SERVER
|
||||
Log(message, serverColor);
|
||||
#else
|
||||
Log(message, clientColor);
|
||||
#endif
|
||||
}
|
||||
|
||||
public void HandleException(Exception exception, string prefix = null)
|
||||
{
|
||||
string errorString = "";
|
||||
switch (exception)
|
||||
{
|
||||
case NetRuntimeException netRuntimeException:
|
||||
if (netRuntimeException.DecoratedMessage == null)
|
||||
{
|
||||
errorString = $"{prefix ?? ""}{netRuntimeException.ToString()}";
|
||||
}
|
||||
else
|
||||
{
|
||||
// FIXME: netRuntimeException.ToString() doesn't print the InnerException's stack trace...
|
||||
errorString = $"{prefix ?? ""}{netRuntimeException.DecoratedMessage}: {netRuntimeException}";
|
||||
}
|
||||
break;
|
||||
case InterpreterException interpreterException:
|
||||
if (interpreterException.DecoratedMessage == null)
|
||||
{
|
||||
errorString = $"{prefix ?? ""}{interpreterException.ToString()}";
|
||||
}
|
||||
else
|
||||
{
|
||||
errorString = $"{prefix ?? ""}{interpreterException.DecoratedMessage}";
|
||||
}
|
||||
break;
|
||||
default:
|
||||
string s = exception.StackTrace != null ? exception.ToString() : $"{exception}\n{Environment.StackTrace}";
|
||||
errorString = $"{prefix ?? ""}{s}";
|
||||
break;
|
||||
}
|
||||
|
||||
LogError(prefix + Environment.UserName + " " + errorString);
|
||||
}
|
||||
|
||||
|
||||
public void LogResults(FluentResults.Result result)
|
||||
{
|
||||
if (result == null)
|
||||
{
|
||||
LogError("Result is null");
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.IsSuccess)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.IsFailed)
|
||||
{
|
||||
foreach (var error in result.Errors)
|
||||
{
|
||||
if (error is ExceptionalError exceptionalError)
|
||||
{
|
||||
HandleException(exceptionalError.Exception);
|
||||
}
|
||||
else
|
||||
{
|
||||
LogError($"FluentResults::IError: {error.Message}");
|
||||
/*if (error.Reasons != null)
|
||||
{
|
||||
foreach (var reason in error.Reasons)
|
||||
{
|
||||
LogError($" - {reason.Message}");
|
||||
}
|
||||
}*/
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void LogDebug(string message, Color? color = null)
|
||||
{
|
||||
Log(message, color ?? Color.Purple);
|
||||
}
|
||||
|
||||
public void LogDebugWarning(string message)
|
||||
{
|
||||
Log(message, Color.Yellow);
|
||||
}
|
||||
|
||||
public void LogDebugError(string message)
|
||||
{
|
||||
Log(message, Color.Red);
|
||||
}
|
||||
|
||||
public void Dispose() { }
|
||||
public FluentResults.Result Reset() => FluentResults.Result.Ok();
|
||||
|
||||
public bool IsDisposed { get; }
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Barotrauma.LuaCs;
|
||||
|
||||
public sealed class LuaCsInfoProvider : ILuaCsInfoProvider
|
||||
{
|
||||
public void Dispose()
|
||||
{
|
||||
// stateless service
|
||||
}
|
||||
|
||||
public bool IsDisposed => false;
|
||||
public bool IsCsEnabled => LuaCsSetup.Instance.IsCsEnabled;
|
||||
public bool HideUserNamesInLogs => LuaCsSetup.Instance.HideUserNamesInLogs;
|
||||
public bool UseCaching => LuaCsSetup.Instance.UseCaching;
|
||||
public RunState CurrentRunState => LuaCsSetup.Instance.CurrentRunState;
|
||||
public ContentPackage LuaCsForBarotraumaPackage
|
||||
{
|
||||
get
|
||||
{
|
||||
return ContentPackageManager.EnabledPackages.Regular.FirstOrDefault(cp => cp.NameMatches(LuaCsSetup.PackageName), null)
|
||||
?? ContentPackageManager.LocalPackages.FirstOrDefault(cp => cp.NameMatches(LuaCsSetup.PackageName))
|
||||
?? ContentPackageManager.WorkshopPackages.FirstOrDefault(cp => cp.NameMatches(LuaCsSetup.PackageName));
|
||||
}
|
||||
}
|
||||
}
|
||||
+670
@@ -0,0 +1,670 @@
|
||||
#nullable enable
|
||||
|
||||
using Barotrauma.LuaCs.Compatibility;
|
||||
using Barotrauma.LuaCs.Data;
|
||||
using Barotrauma.LuaCs.Events;
|
||||
using Barotrauma.Networking;
|
||||
using FluentResults;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.Toolkit.Diagnostics;
|
||||
using MoonSharp.Interpreter;
|
||||
using MoonSharp.Interpreter.Interop;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Barotrauma.LuaCs;
|
||||
|
||||
class LuaScriptManagementService : ILuaScriptManagementService, ILuaDataService, IEventAssemblyUnloading
|
||||
{
|
||||
public Script? InternalScript => _script;
|
||||
|
||||
private Script? _script;
|
||||
private bool _isRunning;
|
||||
[MemberNotNullWhen(true, nameof(_script))]
|
||||
public bool IsRunning => _isRunning;
|
||||
private List<ILuaScriptResourceInfo> _resourcesInfo = new List<ILuaScriptResourceInfo>();
|
||||
|
||||
private readonly AsyncReaderWriterLock _operationsLock = new ();
|
||||
|
||||
private readonly ILuaUserDataService _userDataService;
|
||||
private readonly ISafeLuaUserDataService _safeUserDataService;
|
||||
|
||||
private readonly ILuaScriptLoader _luaScriptLoader;
|
||||
private readonly ILuaScriptServicesConfig _luaScriptServicesConfig;
|
||||
private readonly ILoggerService _loggerService;
|
||||
private readonly LuaGame _luaGame;
|
||||
private readonly IEventService _eventService;
|
||||
private readonly ILuaCsTimer _luaCsTimer;
|
||||
private readonly IDefaultLuaRegistrar _defaultLuaRegistrar;
|
||||
private readonly IPluginManagementService _pluginManagementService;
|
||||
private readonly INetworkingService _networkingService;
|
||||
private readonly IConsoleCommandsService _commandsService;
|
||||
private readonly ILuaConfigService _configService;
|
||||
private readonly ILuaCsInfoProvider _luaCsInfoProvider;
|
||||
private readonly Lazy<IPackageManagementService> _packageManagementService;
|
||||
//private readonly ILuaCsUtility _luaCsUtility;
|
||||
|
||||
public LuaScriptManagementService(
|
||||
ILoggerService loggerService,
|
||||
ILuaScriptLoader loader,
|
||||
ILuaUserDataService userDataService,
|
||||
ISafeLuaUserDataService safeUserDataService,
|
||||
IDefaultLuaRegistrar defaultLuaRegistrar,
|
||||
ILuaScriptServicesConfig luaScriptServicesConfig,
|
||||
IPluginManagementService pluginManagementService,
|
||||
INetworkingService networkingService,
|
||||
LuaGame luaGame,
|
||||
IEventService eventService,
|
||||
//ILuaCsUtility luaCsUtility,
|
||||
ILuaCsTimer luaCsTimer,
|
||||
IConsoleCommandsService commandsService,
|
||||
ILuaCsInfoProvider luaCsInfoProvider,
|
||||
ILuaConfigService configService,
|
||||
Lazy<IPackageManagementService> packageManagementService)
|
||||
{
|
||||
_luaScriptLoader = loader;
|
||||
_userDataService = userDataService;
|
||||
_safeUserDataService = safeUserDataService;
|
||||
_defaultLuaRegistrar = defaultLuaRegistrar;
|
||||
_luaScriptServicesConfig = luaScriptServicesConfig;
|
||||
_loggerService = loggerService;
|
||||
_pluginManagementService = pluginManagementService;
|
||||
_networkingService = networkingService;
|
||||
|
||||
_luaGame = luaGame;
|
||||
_eventService = eventService;
|
||||
_commandsService = commandsService;
|
||||
_luaCsInfoProvider = luaCsInfoProvider;
|
||||
_configService = configService;
|
||||
_packageManagementService = packageManagementService;
|
||||
_luaCsTimer = luaCsTimer;
|
||||
|
||||
RegisterLuaEvents();
|
||||
RegisterConsoleCommands(_commandsService);
|
||||
}
|
||||
|
||||
private void RegisterConsoleCommands(IConsoleCommandsService commands)
|
||||
{
|
||||
#if CLIENT
|
||||
commands.RegisterCommand("cl_reloadlua|cl_reloadcs|cl_reloadluacs", "Re-initializes the LuaCs environment.", (string[] args) =>
|
||||
{
|
||||
LuaCsSetup.Instance.EventService.PublishEvent<IEventReloadAllPackages>(sub => sub.OnReloadAllPackages());
|
||||
});
|
||||
|
||||
commands.RegisterCommand("cl_lua", $"cl_lua: Runs a string on the client.", (string[] args) =>
|
||||
{
|
||||
if (GameMain.Client != null && !GameMain.Client.HasPermission(ClientPermissions.ConsoleCommands))
|
||||
{
|
||||
DebugConsole.ThrowError("Command not permitted.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (LuaCsSetup.Instance.CurrentRunState != RunState.Running)
|
||||
{
|
||||
DebugConsole.ThrowError("LuaCs not initialized, use the console command cl_reloadluacs to force initialization.");
|
||||
return;
|
||||
}
|
||||
|
||||
var result = LuaCsSetup.Instance.LuaScriptManagementService.DoString(string.Join(" ", args));
|
||||
LuaCsSetup.Instance.Logger.LogResults(result.ToResult());
|
||||
});
|
||||
|
||||
commands.RegisterCommand("cl_toggleluadebug", "Toggles the MoonSharp Debug Server.", (string[] args) =>
|
||||
{
|
||||
DebugConsole.Log($"This command is currently not implemented. Please open a github issue if you need this feature.");
|
||||
/*int port = 41912;
|
||||
|
||||
if (args.Length > 0)
|
||||
{
|
||||
int.TryParse(args[0], out port);
|
||||
}
|
||||
|
||||
throw new NotImplementedException();
|
||||
//GameMain.LuaCs.ToggleDebugger(port);*/
|
||||
});
|
||||
|
||||
#elif SERVER
|
||||
commands.RegisterCommand("lua", "lua: Runs a string.", (string[] args) =>
|
||||
{
|
||||
var result = LuaCsSetup.Instance.LuaScriptManagementService.DoString(string.Join(" ", args));
|
||||
LuaCsSetup.Instance.Logger.LogResults(result.ToResult());
|
||||
});
|
||||
|
||||
commands.RegisterCommand("reloadlua|reloadcs|reloadluacs", "Re-initializes the LuaCs environment.", (string[] args) =>
|
||||
{
|
||||
LuaCsSetup.Instance.EventService.PublishEvent<IEventReloadAllPackages>(sub => sub.OnReloadAllPackages());
|
||||
});
|
||||
|
||||
commands.RegisterCommand("toggleluadebug", "Toggles the MoonSharp Debug Server.", (string[] args) =>
|
||||
{
|
||||
int port = 41912;
|
||||
|
||||
if (args.Length > 0)
|
||||
{
|
||||
int.TryParse(args[0], out port);
|
||||
}
|
||||
|
||||
throw new NotImplementedException();
|
||||
//GameMain.LuaCs.ToggleDebugger(port);
|
||||
});
|
||||
#endif
|
||||
|
||||
#if SERVER
|
||||
commands.RegisterCommand("install_cl_lua|install_cl|install_cl_cs|install_cl_luacs", "Installs Client-Side LuaCs into your client.", (string[] args) =>
|
||||
{
|
||||
LuaCsInstaller.Install();
|
||||
});
|
||||
#endif
|
||||
}
|
||||
|
||||
public bool IsDisposed { get; private set; }
|
||||
|
||||
public void SetCachingPolicy(bool useCaching)
|
||||
{
|
||||
_luaScriptLoader?.SetCachingPolicy(useCaching);
|
||||
}
|
||||
|
||||
public async Task<FluentResults.Result> LoadScriptResourcesAsync(ImmutableArray<ILuaScriptResourceInfo> resourcesInfo)
|
||||
{
|
||||
if (!_luaCsInfoProvider.UseCaching)
|
||||
{
|
||||
return FluentResults.Result.Ok();
|
||||
}
|
||||
|
||||
// Do any exception checks you can before acquiring a lock to avoid needlessly holding up resources.
|
||||
if (resourcesInfo.IsDefaultOrEmpty)
|
||||
{
|
||||
ThrowHelper.ThrowArgumentNullException($"{nameof(LoadScriptResourcesAsync)}: The parameter is empty!");
|
||||
}
|
||||
|
||||
// Acquire a lock:
|
||||
// Reader = Allow parallel operations (try to avoid nesting acquiring the lock when possible)
|
||||
// Writer = Exclusive use (ie. executing scripts or Dispose())
|
||||
using var lck = await _operationsLock.AcquireWriterLock(); // IDisposable using with generate a try-finally and release for you.
|
||||
IService.CheckDisposed(this); // Check disposed after you have the lock
|
||||
|
||||
// If you use a ConcurrentDictionary instead of a List, it will handle threading issues for you.
|
||||
_resourcesInfo.AddRange(resourcesInfo.OrderBy(static r => r.LoadPriority));
|
||||
|
||||
// Use the StorageService's caching function by just loading the file with caching turned on.
|
||||
// Right now the LuaScriptLoader has this on by default.
|
||||
var cacheRes = await _luaScriptLoader.CacheResourcesAsync(resourcesInfo);
|
||||
|
||||
// Aggregate and return results to the caller to deal with. Optionally, log here if you want.
|
||||
// Automatically converted to a Task<T> when 'async' is in the method declaration.
|
||||
if (cacheRes.IsFailed)
|
||||
{
|
||||
return cacheRes.ToResult();
|
||||
}
|
||||
return new FluentResults.Result().WithReasons(cacheRes.Value.SelectMany(cr => cr.Item2.Reasons));
|
||||
}
|
||||
|
||||
public FluentResults.Result<DynValue> DoString(string code)
|
||||
{
|
||||
IService.CheckDisposed(this);
|
||||
if (_script == null || !IsRunning) { throw new Exception("Disposed"); }
|
||||
|
||||
try
|
||||
{
|
||||
var result = _script.DoString(code);
|
||||
return FluentResults.Result.Ok(result);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return FluentResults.Result.Fail(new ExceptionalError(ex));
|
||||
}
|
||||
}
|
||||
|
||||
private DynValue DoFile(string file, Table? globalContext = null, string? codeStringFriendly = null)
|
||||
{
|
||||
if (_script == null)
|
||||
{
|
||||
throw new Exception("Not running");
|
||||
}
|
||||
|
||||
if (!LuaCsFile.CanReadFromPath(file))
|
||||
{
|
||||
// TODO: Replace with LuaScriptLoader IsFileAccessible.
|
||||
throw new ScriptRuntimeException($"dofile: File access to {file} not allowed.");
|
||||
}
|
||||
|
||||
if (!LuaCsFile.Exists(file))
|
||||
{
|
||||
// TODO: Replace with LuaScriptLoader IsFileAccessible.
|
||||
throw new ScriptRuntimeException($"dofile: File {file} not found.");
|
||||
}
|
||||
|
||||
return _script.DoFile(file, globalContext, codeStringFriendly);
|
||||
}
|
||||
|
||||
private DynValue LoadFile(string file, Table? globalContext = null, string? codeStringFriendly = null)
|
||||
{
|
||||
if (_script == null)
|
||||
{
|
||||
throw new Exception("Not running");
|
||||
}
|
||||
|
||||
if (!LuaCsFile.CanReadFromPath(file))
|
||||
{
|
||||
throw new ScriptRuntimeException($"loadfile: File access to {file} not allowed.");
|
||||
}
|
||||
|
||||
if (!LuaCsFile.Exists(file))
|
||||
{
|
||||
throw new ScriptRuntimeException($"loadfile: File {file} not found.");
|
||||
}
|
||||
|
||||
return _script.LoadFile(file, globalContext, codeStringFriendly);
|
||||
}
|
||||
|
||||
private void RegisterLuaEvents()
|
||||
{
|
||||
_eventService.Subscribe<IEventAssemblyUnloading>(this);
|
||||
|
||||
_eventService.RegisterLuaEventAlias<IEventUpdate>("think", nameof(IEventUpdate.OnUpdate));
|
||||
_eventService.RegisterLuaEventAlias<IEventKeyUpdate>("keyUpdate", nameof(IEventKeyUpdate.OnKeyUpdate));
|
||||
_eventService.RegisterLuaEventAlias<IEventAfflictionUpdate>("afflictionUpdate", nameof(IEventAfflictionUpdate.OnAfflictionUpdate));
|
||||
|
||||
_eventService.RegisterLuaEventAlias<IEventCharacterCreated>("character.created", nameof(IEventCharacterCreated.OnCharacterCreated));
|
||||
_eventService.RegisterLuaEventAlias<IEventCharacterDeath>("character.death", nameof(IEventCharacterDeath.OnCharacterDeath));
|
||||
_eventService.RegisterLuaEventAlias<IEventCharacterDamageLimb>("character.damageLimb", nameof(IEventCharacterDamageLimb.OnCharacterDamageLimb));
|
||||
_eventService.RegisterLuaEventAlias<IEventGiveCharacterJobItems>("character.giveJobItems", nameof(IEventGiveCharacterJobItems.OnGiveCharacterJobItems));
|
||||
_eventService.RegisterLuaEventAlias<IEventHumanCPRSuccess>("character.CPRSuccess", nameof(IEventHumanCPRSuccess.OnCharacterCPRSuccess));
|
||||
_eventService.RegisterLuaEventAlias<IEventHumanCPRFailed>("character.CPRFailed", nameof(IEventHumanCPRFailed.OnCharacterCPRFailed));
|
||||
_eventService.RegisterLuaEventAlias<IEventHumanCPRSuccess>("human.CPRSuccess", nameof(IEventHumanCPRSuccess.OnCharacterCPRSuccess));
|
||||
_eventService.RegisterLuaEventAlias<IEventHumanCPRFailed>("human.CPRFailed", nameof(IEventHumanCPRFailed.OnCharacterCPRFailed));
|
||||
_eventService.RegisterLuaEventAlias<IEventCharacterApplyDamage>("character.applyDamage", nameof(IEventCharacterApplyDamage.OnCharacterApplyDamage));
|
||||
_eventService.RegisterLuaEventAlias<IEventCharacterApplyAffliction>("character.applyAffliction", nameof(IEventCharacterApplyAffliction.OnCharacterApplyAffliction));
|
||||
|
||||
_eventService.RegisterLuaEventAlias<IEventGapOxygenUpdate>("gapOxygenUpdate", nameof(IEventGapOxygenUpdate.OnGapOxygenUpdate));
|
||||
|
||||
_eventService.RegisterLuaEventAlias<IEventClientControlHusk>("husk.clientControlHusk", nameof(IEventClientControlHusk.OnClientControlHusk));
|
||||
|
||||
_eventService.RegisterLuaEventAlias<IEventMeleeWeaponHandleImpact>("meleeWeapon.handleImpact", nameof(IEventMeleeWeaponHandleImpact.OnMeleeWeaponHandleImpact));
|
||||
|
||||
_eventService.RegisterLuaEventAlias<IEventServerLog>("serverLog", nameof(IEventServerLog.OnServerLog));
|
||||
|
||||
_eventService.RegisterLuaEventAlias<IEventTryClientChangeName>("tryChangeClientName", nameof(IEventTryClientChangeName.OnTryClienChangeName));
|
||||
|
||||
_eventService.RegisterLuaEventAlias<IEventChangeFallDamage>("changeFallDamage", nameof(IEventChangeFallDamage.OnChangeFallDamage));
|
||||
|
||||
_eventService.RegisterLuaEventAlias<IEventChatMessage>("chatMessage", nameof(IEventChatMessage.OnChatMessage));
|
||||
|
||||
_eventService.RegisterLuaEventAlias<IEventCanUseVoiceRadio>("canUseVoiceRadio", nameof(IEventCanUseVoiceRadio.OnCanUseVoiceRadio));
|
||||
_eventService.RegisterLuaEventAlias<IEventChangeLocalVoiceRange>("changeLocalVoiceRange", nameof(IEventChangeLocalVoiceRange.OnChangeLocalVoiceRange));
|
||||
|
||||
_eventService.RegisterLuaEventAlias<IEventRoundStarted>("roundStart", nameof(IEventRoundStarted.OnRoundStart));
|
||||
_eventService.RegisterLuaEventAlias<IEventRoundEnded>("roundEnd", nameof(IEventRoundEnded.OnRoundEnd));
|
||||
_eventService.RegisterLuaEventAlias<IEventMissionsEnded>("missionsEnded", nameof(IEventMissionsEnded.OnMissionsEnded));
|
||||
|
||||
_eventService.RegisterLuaEventAlias<IEventSignalReceived>("signalReceived", nameof(IEventSignalReceived.OnSignalReceived));
|
||||
|
||||
_eventService.RegisterLuaEventAlias<IEventItemCreated>("item.created", nameof(IEventItemCreated.OnItemCreated));
|
||||
_eventService.RegisterLuaEventAlias<IEventItemRemoved>("item.removed", nameof(IEventItemRemoved.OnItemRemoved));
|
||||
_eventService.RegisterLuaEventAlias<IEventItemUse>("item.use", nameof(IEventItemUse.OnItemUsed));
|
||||
_eventService.RegisterLuaEventAlias<IEventItemSecondaryUse>("item.secondaryUse", nameof(IEventItemSecondaryUse.OnItemSecondaryUsed));
|
||||
_eventService.RegisterLuaEventAlias<IEventItemReadPropertyChange>("item.readPropertyChange", nameof(IEventItemReadPropertyChange.OnItemReadPropertyChange));
|
||||
_eventService.RegisterLuaEventAlias<IEventItemDeconstructed>("item.deconstructed", nameof(IEventItemDeconstructed.OnItemDeconstructed));
|
||||
|
||||
_eventService.RegisterLuaEventAlias<IEventInventoryPutItem>("inventoryPutItem", nameof(IEventInventoryPutItem.OnInventoryPutItem));
|
||||
_eventService.RegisterLuaEventAlias<IEventInventoryItemSwap>("inventoryItemSwap", nameof(IEventInventoryItemSwap.OnInventoryItemSwap));
|
||||
|
||||
// Compatibility
|
||||
_eventService.RegisterLuaEventAlias<IEventCharacterCreated>("characterCreated", nameof(IEventCharacterCreated.OnCharacterCreated));
|
||||
_eventService.RegisterLuaEventAlias<IEventCharacterDeath>("characterDeath", nameof(IEventCharacterDeath.OnCharacterDeath));
|
||||
|
||||
#if SERVER
|
||||
_eventService.RegisterLuaEventAlias<IEventClientConnected>("client.connected", nameof(IEventClientConnected.OnClientConnected));
|
||||
_eventService.RegisterLuaEventAlias<IEventClientDisconnected>("client.disconnected", nameof(IEventClientDisconnected.OnClientDisconnected));
|
||||
_eventService.RegisterLuaEventAlias<IEventJobsAssigned>("jobsAssigned", nameof(IEventJobsAssigned.OnJobsAssigned));
|
||||
|
||||
_eventService.RegisterLuaEventAlias<IEventClientRawNetMessageReceived>("netMessageReceived", nameof(IEventClientRawNetMessageReceived.OnReceivedClientNetMessage));
|
||||
|
||||
// Compatibility
|
||||
_eventService.RegisterLuaEventAlias<IEventClientConnected>("clientConnected", nameof(IEventClientConnected.OnClientConnected));
|
||||
_eventService.RegisterLuaEventAlias<IEventClientDisconnected>("clientDisconnected", nameof(IEventClientDisconnected.OnClientDisconnected));
|
||||
_eventService.RegisterLuaEventAlias<IEventModifyChatMessage>("modifyChatMessage", nameof(IEventModifyChatMessage.OnModifyMessagePredicate));
|
||||
#elif CLIENT
|
||||
_eventService.RegisterLuaEventAlias<IEventServerRawNetMessageReceived>("netMessageReceived", nameof(IEventServerRawNetMessageReceived.OnReceivedServerNetMessage));
|
||||
#endif
|
||||
}
|
||||
|
||||
private void SetupEnvironment(bool enableSandbox)
|
||||
{
|
||||
_script = new Script(CoreModules.Preset_SoftSandbox | CoreModules.Debug | CoreModules.IO | CoreModules.OS_System);
|
||||
_script.Options.DebugPrint = (string msg) =>
|
||||
{
|
||||
_loggerService.LogMessage($"[Lua] {msg}");
|
||||
};
|
||||
SetCachingPolicy(_luaCsInfoProvider.UseCaching);
|
||||
|
||||
_script.Options.ScriptLoader = _luaScriptLoader;
|
||||
_script.Options.CheckThreadAccess = false;
|
||||
|
||||
Script.GlobalOptions.ShouldPCallCatchException = (Exception ex) => { return true; };
|
||||
|
||||
UserData.RegisterType<ILuaCsHook.HookMethodType>();
|
||||
UserData.RegisterType(typeof(LuaGame));
|
||||
StandardUserDataDescriptor descriptor = (StandardUserDataDescriptor)UserData.RegisterType(typeof(EventService));
|
||||
descriptor.AddDynValue("HookMethodType", UserData.CreateStatic<ILuaCsHook.HookMethodType>());
|
||||
UserData.RegisterType(typeof(ILuaCsNetworking));
|
||||
UserData.RegisterType(typeof(ILuaCsUtility));
|
||||
UserData.RegisterType(typeof(ILuaCsTimer));
|
||||
UserData.RegisterType(typeof(LuaCsFile));
|
||||
UserData.RegisterType(typeof(ILuaScriptResourceInfo));
|
||||
UserData.RegisterType(typeof(IResourceInfo));
|
||||
UserData.RegisterType(typeof(IUserDataDescriptor));
|
||||
UserData.RegisterType(typeof(INetworkingService));
|
||||
UserData.RegisterType(typeof(ILuaConfigService));
|
||||
UserData.RegisterType(typeof(ILoggerService));
|
||||
|
||||
UserData.RegisterType(typeof(ISettingBase));
|
||||
UserData.RegisterType(typeof(IDataInfo));
|
||||
|
||||
Type[] settingBaseTypes = [
|
||||
typeof(ISettingBase<bool>),
|
||||
typeof(ISettingBase<string>),
|
||||
typeof(ISettingBase<byte>),
|
||||
typeof(ISettingBase<sbyte>),
|
||||
typeof(ISettingBase<ushort>),
|
||||
typeof(ISettingBase<short>),
|
||||
typeof(ISettingBase<char>),
|
||||
typeof(ISettingBase<uint>),
|
||||
typeof(ISettingBase<int>),
|
||||
typeof(ISettingBase<ulong>),
|
||||
typeof(ISettingBase<long>),
|
||||
typeof(ISettingBase<float>),
|
||||
typeof(ISettingBase<double>),
|
||||
|
||||
typeof(ISettingRangeBase<float>),
|
||||
typeof(ISettingRangeBase<int>),
|
||||
|
||||
typeof(ISettingList<string>),
|
||||
typeof(ISettingList<byte>),
|
||||
typeof(ISettingList<sbyte>),
|
||||
typeof(ISettingList<ushort>),
|
||||
typeof(ISettingList<short>),
|
||||
typeof(ISettingList<char>),
|
||||
typeof(ISettingList<uint>),
|
||||
typeof(ISettingList<int>),
|
||||
typeof(ISettingList<ulong>),
|
||||
typeof(ISettingList<long>),
|
||||
typeof(ISettingList<float>),
|
||||
typeof(ISettingList<double>),
|
||||
];
|
||||
|
||||
Dictionary<string, Dictionary<string, object>> settingsTable = [];
|
||||
|
||||
foreach (Type type in settingBaseTypes)
|
||||
{
|
||||
UserData.RegisterType(type);
|
||||
|
||||
string baseName = type.Name.RemoveFromEnd("`1").Substring(1);
|
||||
|
||||
if (!settingsTable.ContainsKey(baseName))
|
||||
{
|
||||
settingsTable[baseName] = new Dictionary<string, object>();
|
||||
}
|
||||
|
||||
settingsTable[baseName][type.GetGenericArguments()[0].Name] = UserData.CreateStatic(type);
|
||||
}
|
||||
|
||||
foreach (var keyPair in settingsTable)
|
||||
{
|
||||
_script.Globals[keyPair.Key] = keyPair.Value;
|
||||
}
|
||||
|
||||
UserData.RegisterType(typeof(ISettingRangeBase<int>));
|
||||
#if CLIENT
|
||||
UserData.RegisterType(typeof(ISettingControl));
|
||||
#endif
|
||||
|
||||
new LuaConverters(this).RegisterLuaConverters();
|
||||
|
||||
var luaRequire = new LuaRequire(_script);
|
||||
|
||||
_script.Globals["setmodulepaths"] = (string[] str) => ((LuaScriptLoader)_luaScriptLoader).ModulePaths = str;
|
||||
|
||||
_script.Globals["dofile"] = (Func<string, Table, string, DynValue>)DoFile;
|
||||
_script.Globals["loadfile"] = (Func<string, Table, string, DynValue>)LoadFile;
|
||||
_script.Globals["require"] = (Func<string, Table, DynValue>)luaRequire.Require;
|
||||
|
||||
_script.Globals["printerror"] = (DynValue o) => { _loggerService.LogError($"[Lua] {o.ToString()}"); };
|
||||
|
||||
_script.Globals["dostring"] = (Func<string, Table, string, DynValue>)_script.DoString;
|
||||
_script.Globals["load"] = (Func<string, Table, string, DynValue>)_script.LoadString;
|
||||
_script.Globals["Game"] = _luaGame;
|
||||
_script.Globals["Hook"] = _eventService;
|
||||
_script.Globals["Timer"] = _luaCsTimer;
|
||||
_script.Globals["File"] = UserData.CreateStatic<LuaCsFile>();
|
||||
_script.Globals["ConfigService"] = _configService;
|
||||
_script.Globals["Networking"] = _networkingService;
|
||||
_script.Globals["trygetpackage"] = (string name, out ContentPackage package) =>
|
||||
_packageManagementService.Value.TryGetLoadedPackageByName(name, out package);
|
||||
_script.Globals["Logger"] = _loggerService;
|
||||
//_script.Globals["Steam"] = Steam;
|
||||
|
||||
if (enableSandbox)
|
||||
{
|
||||
UserData.RegisterType(typeof(SafeLuaUserDataService));
|
||||
_script.Globals["LuaUserData"] = _safeUserDataService;
|
||||
}
|
||||
else
|
||||
{
|
||||
UserData.RegisterType(typeof(LuaUserDataService));
|
||||
_script.Globals["LuaUserData"] = _userDataService;
|
||||
}
|
||||
|
||||
Table eventsTable = new Table(_script);
|
||||
|
||||
var typesValue = _pluginManagementService.GetImplementingTypes<IEvent>(includeInterfaces: true, includeAbstractTypes: true);
|
||||
if (typesValue.IsSuccess)
|
||||
{
|
||||
foreach (var eventType in typesValue.Value)
|
||||
{
|
||||
if (eventType.IsGenericType) { continue; }
|
||||
if (!eventType.IsInterface) { continue; }
|
||||
|
||||
UserData.RegisterType(eventType);
|
||||
eventsTable[eventType.Name] = UserData.CreateStatic(eventType);
|
||||
}
|
||||
}
|
||||
|
||||
_script.Globals["Events"] = eventsTable;
|
||||
|
||||
_script.Globals["ExecutionNumber"] = 0;
|
||||
_script.Globals["CSActive"] = !enableSandbox;
|
||||
((Table)_script.Globals["debug"])["breakpoint"] = () => { Debugger.Break(); };
|
||||
|
||||
_script.Globals["SERVER"] = LuaCsSetup.IsServer;
|
||||
_script.Globals["CLIENT"] = LuaCsSetup.IsClient;
|
||||
|
||||
_defaultLuaRegistrar.RegisterAll();
|
||||
}
|
||||
|
||||
public FluentResults.Result ExecuteLoadedScripts(ImmutableArray<ILuaScriptResourceInfo> executionOrder, bool enableSandbox)
|
||||
{
|
||||
if (_isRunning)
|
||||
{
|
||||
return FluentResults.Result.Fail("Tried to execute Lua scripts without unloading first.");
|
||||
}
|
||||
|
||||
_loggerService.LogMessage("[Lua] Executing scripts");
|
||||
|
||||
SetupEnvironment(enableSandbox);
|
||||
|
||||
if (_script == null) { return FluentResults.Result.Ok(); } // never happens
|
||||
|
||||
var result = FluentResults.Result.Ok();
|
||||
|
||||
_isRunning = true;
|
||||
|
||||
var packages = executionOrder.Select(r => r.OwnerPackage)
|
||||
.Distinct()
|
||||
.Select(p => $"{p.Dir}/Lua/?.lua")
|
||||
.ToArray();
|
||||
|
||||
((LuaScriptLoader)_luaScriptLoader).ModulePaths = packages;
|
||||
Table package = (Table)_script.Globals["package"];
|
||||
package.Set("path", DynValue.FromObject(_script, packages));
|
||||
|
||||
#if CLIENT
|
||||
if (GameMain.NetworkMember is { IsClient: true })
|
||||
{
|
||||
var startMessage = _networkingService.Start("_luastart");
|
||||
|
||||
var packagesToReport = ContentPackageManager.EnabledPackages.All
|
||||
.Where(p => _packageManagementService.Value.PackageContainsAnyRunnableResource(p))
|
||||
.Where(p => !p.NameMatches(LuaCsSetup.PackageName))
|
||||
.ToList();
|
||||
|
||||
startMessage.WriteUInt16((UInt16)packagesToReport.Count());
|
||||
|
||||
foreach (var enabledPackage in packagesToReport)
|
||||
{
|
||||
var id = enabledPackage.UgcId;
|
||||
string hash = enabledPackage.Hash.StringRepresentation ?? "";
|
||||
|
||||
startMessage.WriteString(enabledPackage.Name);
|
||||
startMessage.WriteString(enabledPackage.ModVersion);
|
||||
if (id.TryUnwrap(out ContentPackageId? packageId) && packageId is SteamWorkshopId steamId)
|
||||
{
|
||||
startMessage.WriteUInt64(steamId.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
startMessage.WriteUInt64(0);
|
||||
}
|
||||
startMessage.WriteString(hash);
|
||||
}
|
||||
|
||||
_networkingService.Send(startMessage);
|
||||
}
|
||||
#elif SERVER
|
||||
_networkingService.Receive("_luastart", (message, client) =>
|
||||
{
|
||||
var num = message.ReadUInt16();
|
||||
List<Table> packages = new List<Table>();
|
||||
|
||||
for (int i = 0; i < num; i++)
|
||||
{
|
||||
Table table = new Table(_script);
|
||||
|
||||
table.Set("Name", DynValue.NewString(message.ReadString()));
|
||||
table.Set("Version", DynValue.NewString(message.ReadString()));
|
||||
table.Set("Id", DynValue.NewString(message.ReadUInt64().ToString()));
|
||||
table.Set("Hash", DynValue.NewString(message.ReadString()));
|
||||
|
||||
packages.Add(table);
|
||||
}
|
||||
|
||||
_eventService.Call("client.packages", client, packages);
|
||||
});
|
||||
#endif
|
||||
|
||||
|
||||
foreach (ILuaScriptResourceInfo resource in executionOrder.Where(l => l.IsAutorun))
|
||||
{
|
||||
foreach (ContentPath filePath in resource.FilePaths)
|
||||
{
|
||||
try
|
||||
{
|
||||
_loggerService.LogMessage($"[Lua] - Run {filePath.Value}");
|
||||
_script.Call(_script.LoadFile(filePath.FullPath), resource.OwnerPackage.Dir);
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
result = result.WithError(new ExceptionalError(e));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_eventService.Call("loaded");
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public DynValue? CallFunctionSafe(object luaFunction, params object[] args)
|
||||
{
|
||||
if (!IsRunning) { return null; }
|
||||
|
||||
lock (_script)
|
||||
{
|
||||
try
|
||||
{
|
||||
return _script.Call(luaFunction, args);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_loggerService.HandleException(e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public FluentResults.Result UnloadActiveScripts()
|
||||
{
|
||||
_isRunning = false;
|
||||
|
||||
_script = null;
|
||||
|
||||
return FluentResults.Result.Ok();
|
||||
}
|
||||
|
||||
public FluentResults.Result DisposePackageResources(ContentPackage package)
|
||||
{
|
||||
return FluentResults.Result.Ok();
|
||||
}
|
||||
|
||||
public FluentResults.Result DisposeAllPackageResources()
|
||||
{
|
||||
if (IsRunning)
|
||||
{
|
||||
UnloadActiveScripts();
|
||||
}
|
||||
|
||||
_resourcesInfo.Clear();
|
||||
_luaScriptLoader.ClearCaches();
|
||||
|
||||
return FluentResults.Result.Ok();
|
||||
}
|
||||
|
||||
public FluentResults.Result Reset()
|
||||
{
|
||||
IService.CheckDisposed(this);
|
||||
_luaScriptLoader.ClearCaches();
|
||||
_userDataService.Reset();
|
||||
_luaCsTimer.Reset();
|
||||
RegisterLuaEvents();
|
||||
return DisposeAllPackageResources();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
IsDisposed = true;
|
||||
_userDataService.Dispose();
|
||||
_luaScriptLoader.Dispose();
|
||||
_commandsService.Dispose();
|
||||
}
|
||||
|
||||
public object? GetGlobalTableValue(string tableName)
|
||||
{
|
||||
if (!IsRunning) { return null; }
|
||||
|
||||
return _script.Globals[tableName];
|
||||
}
|
||||
|
||||
public void OnAssemblyUnloading(Assembly assembly)
|
||||
{
|
||||
foreach (Type type in assembly.SafeGetTypes())
|
||||
{
|
||||
UserData.UnregisterType(type, deleteHistory: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
using Barotrauma;
|
||||
using Barotrauma.LuaCs;
|
||||
using Barotrauma.LuaCs.Events;
|
||||
using FluentResults;
|
||||
using HarmonyLib;
|
||||
using Microsoft.Xna.Framework;
|
||||
|
||||
[HarmonyPatch]
|
||||
internal class MainMenuPatch : ISystem, IEventScreenSelected
|
||||
{
|
||||
public bool IsDisposed { get; private set; }
|
||||
|
||||
private readonly IEventService _eventService;
|
||||
|
||||
private bool mainMenuUIAdded = false;
|
||||
|
||||
public MainMenuPatch(IEventService eventService)
|
||||
{
|
||||
_eventService = eventService;
|
||||
|
||||
RegisterEvents();
|
||||
|
||||
#if CLIENT
|
||||
if (Screen.Selected is MainMenuScreen mainMenuScreen)
|
||||
{
|
||||
AddToMainMenu(mainMenuScreen);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
public void OnScreenSelected(Screen screen)
|
||||
{
|
||||
#if CLIENT
|
||||
if (screen is MainMenuScreen mainMenuScreen)
|
||||
{
|
||||
AddToMainMenu(mainMenuScreen);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
#if CLIENT
|
||||
private void AddToMainMenu(MainMenuScreen screen)
|
||||
{
|
||||
if (mainMenuUIAdded) { return; }
|
||||
|
||||
var textBlock = new GUITextBlock(new RectTransform(new Point(300, 30), screen.Frame.RectTransform, Anchor.TopLeft) { AbsoluteOffset = new Point(10, 10) }, "", Color.Red)
|
||||
{
|
||||
IgnoreLayoutGroups = false
|
||||
};
|
||||
|
||||
textBlock.OnAddedToGUIUpdateList = (GUIComponent component) =>
|
||||
{
|
||||
string mode = LuaCsSetup.Instance.CsRunPolicyValue;
|
||||
|
||||
if (mode is "Prompt")
|
||||
{
|
||||
string sessionState = LuaCsSetup.Instance.IsCsEnabledForSession ? "yes" : "no";
|
||||
mode = $"enabled (prompt mode, allowed for this session: {sessionState})";
|
||||
}
|
||||
else if (mode is "Enabled")
|
||||
{
|
||||
mode = "always enabled";
|
||||
}
|
||||
else
|
||||
{
|
||||
mode = "disabled";
|
||||
}
|
||||
|
||||
textBlock.Text = $"LuaCsForBarotrauma active (revision {AssemblyInfo.GitRevision}), C# is currently {mode}\nNew settings available in the game settings menu.";
|
||||
};
|
||||
|
||||
mainMenuUIAdded = true;
|
||||
}
|
||||
#endif
|
||||
|
||||
private void RegisterEvents()
|
||||
{
|
||||
_eventService.Subscribe<IEventScreenSelected>(this);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_eventService.Unsubscribe<IEventScreenSelected>(this);
|
||||
|
||||
IsDisposed = true;
|
||||
}
|
||||
|
||||
public FluentResults.Result Reset()
|
||||
{
|
||||
RegisterEvents();
|
||||
|
||||
return FluentResults.Result.Ok();
|
||||
}
|
||||
}
|
||||
+281
@@ -0,0 +1,281 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using System.Xml.Linq;
|
||||
using Barotrauma.LuaCs.Data;
|
||||
using FarseerPhysics.Common;
|
||||
using FluentResults;
|
||||
using Microsoft.Toolkit.Diagnostics;
|
||||
|
||||
namespace Barotrauma.LuaCs;
|
||||
|
||||
public sealed partial class ModConfigFileParserService :
|
||||
IParserServiceAsync<ResourceParserInfo, IAssemblyResourceInfo>,
|
||||
IParserServiceAsync<ResourceParserInfo, ILuaScriptResourceInfo>,
|
||||
IParserServiceAsync<ResourceParserInfo, IConfigResourceInfo>
|
||||
{
|
||||
private IStorageService _storageService;
|
||||
private readonly AsyncReaderWriterLock _operationsLock = new();
|
||||
|
||||
public ModConfigFileParserService(IStorageService storageService)
|
||||
{
|
||||
_storageService = storageService;
|
||||
}
|
||||
|
||||
#region Dispose
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
using var lck = _operationsLock.AcquireWriterLock().ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
if (!ModUtils.Threading.CheckIfClearAndSetBool(ref _isDisposed))
|
||||
return;
|
||||
try
|
||||
{
|
||||
_storageService.Dispose();
|
||||
this._storageService = null;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
}
|
||||
|
||||
private int _isDisposed = 0;
|
||||
public bool IsDisposed
|
||||
{
|
||||
get => ModUtils.Threading.GetBool(ref _isDisposed);
|
||||
private set => ModUtils.Threading.SetBool(ref _isDisposed, value);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
// --- Assemblies
|
||||
async Task<Result<IAssemblyResourceInfo>> IParserServiceAsync<ResourceParserInfo, IAssemblyResourceInfo>.TryParseResourceAsync(ResourceParserInfo src)
|
||||
{
|
||||
using var lck = await _operationsLock.AcquireReaderLock();
|
||||
IService.CheckDisposed(this);
|
||||
|
||||
if (CheckThrowNullRefs(src, "Assembly") is { IsFailed: true } fail)
|
||||
return fail;
|
||||
|
||||
var isScript = src.Element.GetAttributeBool("IsScript", false);
|
||||
var runtimeEnv = GetRuntimeEnvironment(src.Element);
|
||||
var fileResults = await UnsafeGetCheckedFiles(src.Element, src.Owner, isScript ? ".cs" : ".dll");
|
||||
|
||||
if (fileResults.IsFailed)
|
||||
return FluentResults.Result.Fail(fileResults.Errors);
|
||||
|
||||
return new AssemblyResourceInfo()
|
||||
{
|
||||
SupportedPlatforms = runtimeEnv.Platform,
|
||||
SupportedTargets = runtimeEnv.Target,
|
||||
LoadPriority = src.Element.GetAttributeInt("LoadPriority", 0),
|
||||
FilePaths = fileResults.Value,
|
||||
Optional = src.Element.GetAttributeBool("Optional", false),
|
||||
InternalName = src.Element.GetAttributeString("Name", string.Empty),
|
||||
OwnerPackage = src.Owner,
|
||||
RequiredPackages = src.Required,
|
||||
IncompatiblePackages = src.Incompatible,
|
||||
// Type Specific
|
||||
FriendlyName = src.Element.GetAttributeString("FriendlyName", GetFallbackCompliantAssemblyName(src.Owner)),
|
||||
IsScript = isScript,
|
||||
UseInternalAccessName = src.Element.GetAttributeBool("UseInternalAccessName", false),
|
||||
IsReferenceModeOnly = src.Element.GetAttributeBool("IsReferenceModeOnly", false)
|
||||
};
|
||||
|
||||
|
||||
// helper methods
|
||||
string GetFallbackCompliantAssemblyName(ContentPackage package)
|
||||
{
|
||||
if (package.Name.IsNullOrWhiteSpace())
|
||||
{
|
||||
return "FallbackAssemblyName";
|
||||
}
|
||||
|
||||
// replace non az chars with '_'
|
||||
var sanitizedPackageName = Regex.Replace(package.Name, @"[^a-zA-Z0-9_]", "_");
|
||||
if (char.IsDigit(sanitizedPackageName[0]))
|
||||
{
|
||||
sanitizedPackageName = "ASM" + sanitizedPackageName;
|
||||
}
|
||||
|
||||
// replace consecutive '_'
|
||||
return Regex.Replace(sanitizedPackageName, @"[_.]{2,}", "_");
|
||||
}
|
||||
}
|
||||
|
||||
async Task<ImmutableArray<Result<IAssemblyResourceInfo>>> IParserServiceAsync<ResourceParserInfo, IAssemblyResourceInfo>.TryParseResourcesAsync(IEnumerable<ResourceParserInfo> sources)
|
||||
{
|
||||
return await this.TryParseGenericResourcesAsync<IAssemblyResourceInfo>(sources);
|
||||
}
|
||||
|
||||
// --- Config
|
||||
|
||||
async Task<Result<IConfigResourceInfo>> IParserServiceAsync<ResourceParserInfo, IConfigResourceInfo>.TryParseResourceAsync(ResourceParserInfo src)
|
||||
{
|
||||
using var lck = await _operationsLock.AcquireReaderLock();
|
||||
IService.CheckDisposed(this);
|
||||
|
||||
if (CheckThrowNullRefs(src, "Config") is { IsFailed: true } fail)
|
||||
return fail;
|
||||
|
||||
var runtimeEnv = GetRuntimeEnvironment(src.Element);
|
||||
var fileResults = await UnsafeGetCheckedFiles(src.Element, src.Owner, ".xml");
|
||||
|
||||
if (fileResults.IsFailed)
|
||||
return FluentResults.Result.Fail(fileResults.Errors);
|
||||
|
||||
return new ConfigResourceInfo()
|
||||
{
|
||||
SupportedPlatforms = runtimeEnv.Platform,
|
||||
SupportedTargets = runtimeEnv.Target,
|
||||
LoadPriority = src.Element.GetAttributeInt("LoadPriority", 0),
|
||||
FilePaths = fileResults.Value,
|
||||
Optional = src.Element.GetAttributeBool("Optional", false),
|
||||
InternalName = src.Element.GetAttributeString("Name", string.Empty),
|
||||
OwnerPackage = src.Owner,
|
||||
RequiredPackages = src.Required,
|
||||
IncompatiblePackages = src.Incompatible
|
||||
};
|
||||
}
|
||||
|
||||
async Task<ImmutableArray<Result<IConfigResourceInfo>>> IParserServiceAsync<ResourceParserInfo, IConfigResourceInfo>.TryParseResourcesAsync(IEnumerable<ResourceParserInfo> sources)
|
||||
{
|
||||
return await this.TryParseGenericResourcesAsync<IConfigResourceInfo>(sources);
|
||||
}
|
||||
|
||||
// --- Lua Scripts
|
||||
async Task<Result<ILuaScriptResourceInfo>> IParserServiceAsync<ResourceParserInfo, ILuaScriptResourceInfo>.TryParseResourceAsync(ResourceParserInfo src)
|
||||
{
|
||||
using var lck = await _operationsLock.AcquireReaderLock();
|
||||
IService.CheckDisposed(this);
|
||||
|
||||
if (CheckThrowNullRefs(src, "Lua") is { IsFailed: true } fail)
|
||||
return fail;
|
||||
|
||||
var runtimeEnv = GetRuntimeEnvironment(src.Element);
|
||||
var fileResults = await UnsafeGetCheckedFiles(src.Element, src.Owner, ".lua");
|
||||
|
||||
if (fileResults.IsFailed)
|
||||
return FluentResults.Result.Fail(fileResults.Errors);
|
||||
|
||||
return new LuaScriptsResourceInfo()
|
||||
{
|
||||
SupportedPlatforms = runtimeEnv.Platform,
|
||||
SupportedTargets = runtimeEnv.Target,
|
||||
LoadPriority = src.Element.GetAttributeInt("LoadPriority", 0),
|
||||
FilePaths = fileResults.Value,
|
||||
Optional = src.Element.GetAttributeBool("Optional", false),
|
||||
InternalName = src.Element.GetAttributeString("Name", string.Empty),
|
||||
OwnerPackage = src.Owner,
|
||||
RequiredPackages = src.Required,
|
||||
IncompatiblePackages = src.Incompatible,
|
||||
// Type Specific
|
||||
IsAutorun = src.Element.GetAttributeBool("IsAutorun", false),
|
||||
RunUnrestricted = src.Element.GetAttributeBool("RunUnrestricted", false)
|
||||
};
|
||||
}
|
||||
|
||||
private FluentResults.Result CheckThrowNullRefs(ResourceParserInfo src, string elementName)
|
||||
{
|
||||
Guard.IsNotNull(src, nameof(src));
|
||||
Guard.IsNotNull(src.Owner, nameof(src.Owner));
|
||||
Guard.IsNotNull(src.Element, nameof(src.Element));
|
||||
|
||||
if (src.Element.Name != elementName)
|
||||
{
|
||||
return FluentResults.Result.Fail($"Element name '{elementName}' is incorrect");
|
||||
}
|
||||
|
||||
return FluentResults.Result.Ok();
|
||||
}
|
||||
|
||||
async Task<ImmutableArray<Result<ILuaScriptResourceInfo>>> IParserServiceAsync<ResourceParserInfo, ILuaScriptResourceInfo>.TryParseResourcesAsync(IEnumerable<ResourceParserInfo> sources)
|
||||
{
|
||||
return await this.TryParseGenericResourcesAsync<ILuaScriptResourceInfo>(sources);
|
||||
}
|
||||
|
||||
// --- Helpers
|
||||
private async Task<Result<ImmutableArray<ContentPath>>> UnsafeGetCheckedFiles(XElement srcElement, ContentPackage srcOwner, string fileExtension)
|
||||
{
|
||||
var builder = ImmutableArray.CreateBuilder<ContentPath>();
|
||||
var filePath = srcElement.GetAttributeContentPath("File", srcOwner);
|
||||
var folderPath = srcElement.GetAttributeContentPath("Folder", srcOwner);
|
||||
|
||||
var res = new FluentResults.Result<ImmutableArray<ContentPath>>();
|
||||
|
||||
if ((!filePath?.Value.IsNullOrWhiteSpace()) ?? false)
|
||||
{
|
||||
if (_storageService.FileExists(filePath.FullPath) is { IsSuccess: true, Value: true })
|
||||
{
|
||||
builder.Add(filePath);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (srcElement.GetAttributeBool("IsFileRequired", true))
|
||||
{
|
||||
res.WithError($"{srcOwner.Name}: The file '{filePath}' is missing!");
|
||||
}
|
||||
else
|
||||
{
|
||||
res.WithSuccess($"Skipped missing not-required file: '{filePath}'");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ((!folderPath?.Value.IsNullOrWhiteSpace()) ?? false)
|
||||
{
|
||||
if (_storageService.DirectoryExists(folderPath.FullPath) is { IsSuccess: true, Value: true })
|
||||
{
|
||||
var searchLocation = System.IO.Path.GetRelativePath(srcOwner.Dir, folderPath.Value);
|
||||
var files = _storageService.FindFilesInPackage(srcOwner, searchLocation, "*"+fileExtension, true);
|
||||
if (files.IsFailed)
|
||||
{
|
||||
res.WithError($"{srcOwner.Name}: Failed to load files from {folderPath}!");
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var file in files.Value)
|
||||
{
|
||||
builder.Add(ContentPath.FromRaw(srcOwner, $"%ModDir%/{System.IO.Path.GetRelativePath(System.IO.Path.GetFullPath(srcOwner.Dir), file)}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (srcElement.GetAttributeBool("IsFileRequired", true))
|
||||
{
|
||||
res.WithError($"{srcOwner.Name}: The file '{folderPath}' is missing!");
|
||||
}
|
||||
else
|
||||
{
|
||||
res.WithSuccess($"Skipped missing not-required folder: '{folderPath}'");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return res.WithValue(builder.ToImmutable());
|
||||
}
|
||||
private (Platform Platform, Target Target) GetRuntimeEnvironment(XElement element)
|
||||
{
|
||||
return (
|
||||
Platform: element.GetAttributeEnum("Platform", Platform.Any),
|
||||
Target: element.GetAttributeEnum("Target", Target.Any));
|
||||
}
|
||||
|
||||
private async Task<ImmutableArray<Result<T>>> TryParseGenericResourcesAsync<T>(IEnumerable<ResourceParserInfo> sources)
|
||||
{
|
||||
// ReSharper disable once PossibleMultipleEnumeration
|
||||
Guard.IsNotNull(sources, nameof(IParserServiceAsync<ResourceParserInfo, T>.TryParseResourcesAsync));
|
||||
var builder = ImmutableArray.CreateBuilder<Result<T>>();
|
||||
foreach (var info in sources)
|
||||
{
|
||||
builder.Add(await Unsafe.As<IParserServiceAsync<ResourceParserInfo, T>>(this).TryParseResourceAsync(info));
|
||||
}
|
||||
return builder.ToImmutable();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,430 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Xml.Linq;
|
||||
using Barotrauma.Extensions;
|
||||
using Barotrauma.LuaCs.Data;
|
||||
using FluentResults;
|
||||
using Microsoft.Toolkit.Diagnostics;
|
||||
using MoonSharp.VsCodeDebugger.SDK;
|
||||
|
||||
namespace Barotrauma.LuaCs;
|
||||
|
||||
public sealed class ModConfigService : IModConfigService
|
||||
{
|
||||
private IStorageService _storageService;
|
||||
private ILoggerService _logger;
|
||||
private IParserServiceAsync<ResourceParserInfo, IAssemblyResourceInfo> _assemblyParserService;
|
||||
private IParserServiceAsync<ResourceParserInfo, ILuaScriptResourceInfo> _luaScriptParserService;
|
||||
private IParserServiceAsync<ResourceParserInfo, IConfigResourceInfo> _configParserService;
|
||||
#if CLIENT
|
||||
private IParserServiceAsync<ResourceParserInfo, IStylesResourceInfo> _stylesParserService;
|
||||
#endif
|
||||
private readonly AsyncReaderWriterLock _operationsLock = new();
|
||||
|
||||
public ModConfigService(IStorageService storageService,
|
||||
IParserServiceAsync<ResourceParserInfo, IAssemblyResourceInfo> assemblyParserService,
|
||||
IParserServiceAsync<ResourceParserInfo, ILuaScriptResourceInfo> luaScriptParserService,
|
||||
IParserServiceAsync<ResourceParserInfo, IConfigResourceInfo> configParserService,
|
||||
#if CLIENT
|
||||
IParserServiceAsync<ResourceParserInfo, IStylesResourceInfo> stylesParserService,
|
||||
#endif
|
||||
ILoggerService logger)
|
||||
{
|
||||
_storageService = storageService;
|
||||
_assemblyParserService = assemblyParserService;
|
||||
_luaScriptParserService = luaScriptParserService;
|
||||
_configParserService = configParserService;
|
||||
_logger = logger;
|
||||
#if CLIENT
|
||||
_stylesParserService = stylesParserService;
|
||||
#endif
|
||||
}
|
||||
|
||||
#region Dispose
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
using var lck = _operationsLock.AcquireWriterLock().ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
if (!ModUtils.Threading.CheckIfClearAndSetBool(ref _isDisposed))
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
_storageService.Dispose();
|
||||
_logger.Dispose();
|
||||
_assemblyParserService.Dispose();
|
||||
_luaScriptParserService.Dispose();
|
||||
_configParserService.Dispose();
|
||||
|
||||
_storageService = null;
|
||||
_logger = null;
|
||||
_assemblyParserService = null;
|
||||
_luaScriptParserService = null;
|
||||
_configParserService = null;
|
||||
|
||||
#if CLIENT
|
||||
_stylesParserService.Dispose();
|
||||
_stylesParserService = null;
|
||||
#endif
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
}
|
||||
|
||||
private int _isDisposed = 0;
|
||||
public bool IsDisposed
|
||||
{
|
||||
get => ModUtils.Threading.GetBool(ref _isDisposed);
|
||||
private set => ModUtils.Threading.SetBool(ref _isDisposed, value);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
public async Task<Result<IModConfigInfo>> CreateConfigAsync(ContentPackage src)
|
||||
{
|
||||
Guard.IsNotNull(src, nameof(src));
|
||||
using var lck = await _operationsLock.AcquireReaderLock();
|
||||
IService.CheckDisposed(this);
|
||||
|
||||
if (await TryGetModConfigXmlAsync(src) is { IsSuccess: true, Value: { } config })
|
||||
{
|
||||
return await CreateFromConfigXmlAsync(src, config);
|
||||
}
|
||||
|
||||
return await CreateFromLegacyAsync(src);
|
||||
}
|
||||
|
||||
public async Task<ImmutableArray<(ContentPackage Source, Result<IModConfigInfo> Config)>> CreateConfigsAsync(ImmutableArray<ContentPackage> src)
|
||||
{
|
||||
if (src.IsDefaultOrEmpty)
|
||||
ThrowHelper.ThrowArgumentNullException($"{nameof(CreateConfigsAsync)}: The supplied array is default or empty!");
|
||||
using var lck = await _operationsLock.AcquireReaderLock();
|
||||
IService.CheckDisposed(this);
|
||||
|
||||
var builder = ImmutableArray.CreateBuilder<Task<Task<Result<IModConfigInfo>>>>(src.Length);
|
||||
foreach (var srcItem in src)
|
||||
{
|
||||
builder.Add(Task.Factory.StartNew(async Task<Result<IModConfigInfo>> () => await CreateConfigAsync(srcItem)));
|
||||
}
|
||||
var taskResults = await Task.WhenAll(builder.ToImmutable());
|
||||
var returnResults = ImmutableArray.CreateBuilder<(ContentPackage Source, Result<IModConfigInfo> Config)>();
|
||||
foreach (var taskResult in taskResults)
|
||||
{
|
||||
if (taskResult.IsFaulted)
|
||||
{
|
||||
ThrowHelper.ThrowInvalidOperationException($"{nameof(CreateConfigsAsync)}: Task failed: {taskResult.Exception?.Message}");
|
||||
}
|
||||
|
||||
var r = await taskResult;
|
||||
returnResults.Add((r.Value.Package, r));
|
||||
}
|
||||
|
||||
return returnResults.ToImmutable();
|
||||
}
|
||||
|
||||
//--- Helpers
|
||||
private async Task<Result<XElement>> TryGetModConfigXmlAsync(ContentPackage src)
|
||||
{
|
||||
return await _storageService.LoadPackageXmlAsync(ContentPath.FromRaw(src, "%ModDir%/ModConfig.xml")) is { IsSuccess: true, Value: { Root: {} config} }
|
||||
? FluentResults.Result.Ok(config)
|
||||
: FluentResults.Result.Fail<XElement>("ModConfig.xml not found");
|
||||
}
|
||||
|
||||
private async Task<Result<IModConfigInfo>> CreateFromConfigXmlAsync(ContentPackage owner, XElement src)
|
||||
{
|
||||
var asmTask = Task.Factory.StartNew(async () => await GetAssembliesFromXml(owner, src));
|
||||
var cfgTask = Task.Factory.StartNew(async () => await GetConfigsFromXml(owner, src));
|
||||
var luaTask = Task.Factory.StartNew(async () => await GetLuaScriptsFromXml(owner, src));
|
||||
#if CLIENT
|
||||
var styleTask = Task.Factory.StartNew(async () => await GetStylesFromXml(owner, src));
|
||||
#endif
|
||||
|
||||
await Task.WhenAll(
|
||||
asmTask,
|
||||
cfgTask,
|
||||
#if CLIENT
|
||||
styleTask,
|
||||
#endif
|
||||
luaTask);
|
||||
|
||||
return FluentResults.Result.Ok<IModConfigInfo>(new ModConfigInfo()
|
||||
{
|
||||
Package = owner,
|
||||
Assemblies = await await asmTask,
|
||||
Configs = await await cfgTask,
|
||||
#if CLIENT
|
||||
Styles = await await styleTask,
|
||||
#endif
|
||||
LuaScripts = await await luaTask
|
||||
});
|
||||
|
||||
async Task<ImmutableArray<ILuaScriptResourceInfo>> GetLuaScriptsFromXml(ContentPackage contentPackage,
|
||||
XElement cfgElement)
|
||||
{
|
||||
return await GetResourceFromXml<ILuaScriptResourceInfo>(contentPackage, cfgElement, "Lua", "FileGroup", _luaScriptParserService);
|
||||
}
|
||||
|
||||
async Task<ImmutableArray<IConfigResourceInfo>> GetConfigsFromXml(ContentPackage contentPackage,
|
||||
XElement cfgElement)
|
||||
{
|
||||
return await GetResourceFromXml<IConfigResourceInfo>(contentPackage, cfgElement, "Config", "FileGroup", _configParserService);
|
||||
}
|
||||
|
||||
async Task<ImmutableArray<IAssemblyResourceInfo>> GetAssembliesFromXml(ContentPackage contentPackage,
|
||||
XElement cfgElement)
|
||||
{
|
||||
return await GetResourceFromXml<IAssemblyResourceInfo>(contentPackage, cfgElement, "Assembly", "FileGroup", _assemblyParserService);
|
||||
}
|
||||
|
||||
#if CLIENT
|
||||
async Task<ImmutableArray<IStylesResourceInfo>> GetStylesFromXml(ContentPackage contentPackage,
|
||||
XElement cfgElement)
|
||||
{
|
||||
return await GetResourceFromXml<IStylesResourceInfo>(contentPackage, cfgElement, "Style", "FileGroup", _stylesParserService);
|
||||
}
|
||||
#endif
|
||||
|
||||
async Task<ImmutableArray<T>> GetResourceFromXml<T>(ContentPackage contentPackage, XElement cfgElement, string elemName, string fileGroupName, IParserServiceAsync<ResourceParserInfo, T> resourceService)
|
||||
{
|
||||
var elems = GetResourceElementsWithName(owner, cfgElement, elemName, fileGroupName);
|
||||
if (elems.IsDefaultOrEmpty)
|
||||
return ImmutableArray<T>.Empty;
|
||||
|
||||
var results = await resourceService.TryParseResourcesAsync(elems);
|
||||
Guard.IsNotEmpty((IReadOnlyCollection<Result<T>>)results, nameof(results));
|
||||
|
||||
var resources = ImmutableArray.CreateBuilder<T>();
|
||||
foreach (var result in results)
|
||||
{
|
||||
if (result.Errors.Count > 0)
|
||||
{
|
||||
_logger.LogResults(result.ToResult());
|
||||
continue;
|
||||
}
|
||||
resources.Add(result.Value);
|
||||
}
|
||||
return resources.ToImmutable();
|
||||
}
|
||||
|
||||
ImmutableArray<ResourceParserInfo> GetResourceElementsWithName(ContentPackage package, XElement root, string elemName, string groupName)
|
||||
{
|
||||
var elems = ImmutableArray.CreateBuilder<ResourceParserInfo>();
|
||||
|
||||
elems.AddRange(root.GetChildElements(elemName)
|
||||
.Select(e => new ResourceParserInfo(package, e, ImmutableArray<Identifier>.Empty, ImmutableArray<Identifier>.Empty))
|
||||
.ToImmutableArray());
|
||||
|
||||
if (root.GetChildElements(groupName).ToImmutableArray() is { IsDefaultOrEmpty: false } fileGroups)
|
||||
{
|
||||
foreach (var fileGroup in fileGroups)
|
||||
{
|
||||
if (fileGroup.GetChildElements(elemName).ToImmutableArray() is { IsDefaultOrEmpty: false } subLuaElems)
|
||||
{
|
||||
var cond = GetDependencyIdentifiers(fileGroup, true);
|
||||
var negCond = GetDependencyIdentifiers(fileGroup, false);
|
||||
|
||||
foreach (var element in subLuaElems)
|
||||
{
|
||||
elems.Add(new ResourceParserInfo(package, element, cond, negCond));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return elems.ToImmutable();
|
||||
}
|
||||
|
||||
ImmutableArray<Identifier> GetDependencyIdentifiers(XElement fg, bool depsLoadedSetting)
|
||||
{
|
||||
return fg.GetChildElements("Conditional")
|
||||
.Where(cElem => bool.TryParse(cElem.GetAttribute("IsLoaded").Value, out bool isLoaded) && isLoaded == depsLoadedSetting)
|
||||
.SelectMany(cElem2 => cElem2.GetAttributeString("Dependencies", String.Empty)
|
||||
.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries)
|
||||
.Select(ident => new Identifier(ident)))
|
||||
.ToImmutableArray();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
private async Task<Result<IModConfigInfo>> CreateFromLegacyAsync(ContentPackage src)
|
||||
{
|
||||
return new ModConfigInfo()
|
||||
{
|
||||
Package = src,
|
||||
Assemblies = GetAssembliesLegacy(src),
|
||||
Configs = GetConfigsLegacy(src),
|
||||
LuaScripts = GetLuaScriptsLegacy(src)
|
||||
};
|
||||
|
||||
ImmutableArray<IAssemblyResourceInfo> GetAssembliesLegacy(ContentPackage srcPackage)
|
||||
{
|
||||
var binSearchInd = new (string SubFolder, Target Targets, Platform Platforms)[]
|
||||
{
|
||||
("bin/Client/Windows", Target.Client, Platform.Windows),
|
||||
("bin/Client/Linux", Target.Client, Platform.Linux),
|
||||
("bin/Client/OSX", Target.Client, Platform.OSX),
|
||||
("bin/Server/Windows", Target.Server, Platform.Windows),
|
||||
("bin/Server/Linux", Target.Server, Platform.Linux),
|
||||
("bin/Server/OSX", Target.Server, Platform.OSX)
|
||||
};
|
||||
|
||||
var builder = ImmutableArray.CreateBuilder<IAssemblyResourceInfo>();
|
||||
|
||||
foreach (var searchPathways in binSearchInd)
|
||||
{
|
||||
if (_storageService.FindFilesInPackage(srcPackage, searchPathways.SubFolder, "*.dll",
|
||||
true) is { IsSuccess: true, Value.IsDefaultOrEmpty: false } result)
|
||||
{
|
||||
builder.Add(new AssemblyResourceInfo()
|
||||
{
|
||||
OwnerPackage = srcPackage,
|
||||
InternalName = searchPathways.SubFolder,
|
||||
SupportedPlatforms = searchPathways.Platforms,
|
||||
SupportedTargets = searchPathways.Targets,
|
||||
LoadPriority = 0,
|
||||
FilePaths = result.Value.Select(fp => ContentPath.FromRaw(srcPackage, $"%ModDir%/{Path.GetRelativePath(srcPackage.Dir, fp)}".CleanUpPathCrossPlatform()))
|
||||
.ToImmutableArray(),
|
||||
FriendlyName = $"{srcPackage.Name}.{searchPathways.SubFolder.Replace('/','.')}",
|
||||
IncompatiblePackages = ImmutableArray<Identifier>.Empty,
|
||||
RequiredPackages = ImmutableArray<Identifier>.Empty,
|
||||
IsScript = false,
|
||||
IsReferenceModeOnly = false
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
var sharedResult = _storageService.FindFilesInPackage(srcPackage,
|
||||
Path.Combine("CSharp/Shared"),
|
||||
"*.cs", true);
|
||||
var sharedFiles = sharedResult.IsSuccess && !sharedResult.Value.IsDefaultOrEmpty
|
||||
? sharedResult.Value.Select(fp =>
|
||||
ContentPath.FromRaw(srcPackage, $"%ModDir%/{Path.GetRelativePath(srcPackage.Dir, fp)}".CleanUpPathCrossPlatform()))
|
||||
.ToImmutableArray()
|
||||
: ImmutableArray<ContentPath>.Empty;
|
||||
|
||||
var srcSearchInd = new (string SubFolder, Target Targets, Platform Platforms)[]
|
||||
{
|
||||
("CSharp/Client", Target.Client, Platform.Any),
|
||||
("CSharp/Server", Target.Server, Platform.Any)
|
||||
};
|
||||
|
||||
foreach (var searchPathways in srcSearchInd)
|
||||
{
|
||||
// we have architecture dependent files as well
|
||||
if (_storageService.FindFilesInPackage(srcPackage, searchPathways.SubFolder, "*.cs",
|
||||
true) is { IsSuccess: true, Value.IsDefaultOrEmpty: false } result)
|
||||
{
|
||||
builder.Add(new AssemblyResourceInfo()
|
||||
{
|
||||
OwnerPackage = srcPackage,
|
||||
InternalName = searchPathways.SubFolder,
|
||||
SupportedPlatforms = searchPathways.Platforms,
|
||||
SupportedTargets = searchPathways.Targets,
|
||||
LoadPriority = 0,
|
||||
FilePaths = result.Value
|
||||
.Select(fp => ContentPath.FromRaw(srcPackage,
|
||||
$"%ModDir%/{Path.GetRelativePath(srcPackage.Dir, fp)}".CleanUpPathCrossPlatform()))
|
||||
.Concat(sharedFiles).ToImmutableArray(),
|
||||
FriendlyName = IAssemblyLoaderService.InternalsAwareAssemblyName, // give the best chance of success (InternalsAware + Publicizer)
|
||||
IncompatiblePackages = ImmutableArray<Identifier>.Empty,
|
||||
RequiredPackages = ImmutableArray<Identifier>.Empty,
|
||||
UseInternalAccessName = false, //compile as public and then fallback to internals
|
||||
IsScript = true,
|
||||
IsReferenceModeOnly = false
|
||||
});
|
||||
}
|
||||
// add the shared files by themselves
|
||||
else if (!sharedFiles.IsDefaultOrEmpty)
|
||||
{
|
||||
builder.Add(new AssemblyResourceInfo()
|
||||
{
|
||||
OwnerPackage = srcPackage,
|
||||
InternalName = searchPathways.SubFolder,
|
||||
SupportedPlatforms = searchPathways.Platforms,
|
||||
SupportedTargets = searchPathways.Targets,
|
||||
LoadPriority = 0,
|
||||
FilePaths = sharedFiles,
|
||||
FriendlyName = IAssemblyLoaderService.InternalsAwareAssemblyName,
|
||||
IncompatiblePackages = ImmutableArray<Identifier>.Empty,
|
||||
RequiredPackages = ImmutableArray<Identifier>.Empty,
|
||||
UseInternalAccessName = false,
|
||||
IsScript = true,
|
||||
IsReferenceModeOnly = false
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return builder.ToImmutable();
|
||||
}
|
||||
|
||||
ImmutableArray<IConfigResourceInfo> GetConfigsLegacy(ContentPackage src)
|
||||
{
|
||||
return ImmutableArray<IConfigResourceInfo>.Empty;
|
||||
}
|
||||
|
||||
ImmutableArray<ILuaScriptResourceInfo> GetLuaScriptsLegacy(ContentPackage src)
|
||||
{
|
||||
var builder = ImmutableArray.CreateBuilder<ILuaScriptResourceInfo>();
|
||||
|
||||
if (_storageService.FindFilesInPackage(src, "Lua", "*.lua", true)
|
||||
is { IsSuccess: true, Value.IsDefaultOrEmpty: false } result)
|
||||
{
|
||||
ImmutableArray<string> cleanedResult = result.Value.Select(fp => fp.CleanUpPathCrossPlatform()).ToImmutableArray();
|
||||
|
||||
ImmutableArray<string> autorun = cleanedResult
|
||||
.Where(fp => fp.Contains("Lua/ForcedAutorun/") || fp.Contains("Lua/Autorun/"))
|
||||
.ToImmutableArray();
|
||||
|
||||
ImmutableArray<ContentPath> autorunFP = autorun.Select(fp => ContentPath.FromRaw(src,
|
||||
$"%ModDir%/{Path.GetRelativePath(src.Dir, fp)}".CleanUpPathCrossPlatform()))
|
||||
.ToImmutableArray();
|
||||
|
||||
ImmutableArray<ContentPath> reg = cleanedResult.Except(autorun)
|
||||
.Select(fp => ContentPath.FromRaw(src,
|
||||
$"%ModDir%/{Path.GetRelativePath(src.Dir, fp)}".CleanUpPathCrossPlatform()))
|
||||
.ToImmutableArray();
|
||||
|
||||
builder.Add(new LuaScriptsResourceInfo()
|
||||
{
|
||||
OwnerPackage = src,
|
||||
InternalName = "LegacyAutorun",
|
||||
SupportedPlatforms = Platform.Any,
|
||||
SupportedTargets = Target.Any,
|
||||
LoadPriority = 1, // autorun should be last to ensure that dependent code in other files are loaded first
|
||||
FilePaths = autorunFP,
|
||||
IncompatiblePackages = ImmutableArray<Identifier>.Empty,
|
||||
RequiredPackages = ImmutableArray<Identifier>.Empty,
|
||||
IsAutorun = true,
|
||||
RunUnrestricted = false
|
||||
});
|
||||
|
||||
builder.Add(new LuaScriptsResourceInfo()
|
||||
{
|
||||
OwnerPackage = src,
|
||||
InternalName = "Legacy",
|
||||
SupportedPlatforms = Platform.Any,
|
||||
SupportedTargets = Target.Any,
|
||||
LoadPriority = 0, // should be included first to ensure that dependent code in these files are available
|
||||
FilePaths = reg,
|
||||
IncompatiblePackages = ImmutableArray<Identifier>.Empty,
|
||||
RequiredPackages = ImmutableArray<Identifier>.Empty,
|
||||
IsAutorun = false,
|
||||
RunUnrestricted = false
|
||||
});
|
||||
}
|
||||
|
||||
return builder.ToImmutable();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,421 @@
|
||||
using Barotrauma.LuaCs;
|
||||
using Barotrauma.LuaCs.Compatibility;
|
||||
using Barotrauma.LuaCs.Events;
|
||||
using Barotrauma.Networking;
|
||||
using FluentResults;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using Barotrauma.LuaCs.Data;
|
||||
|
||||
namespace Barotrauma.LuaCs;
|
||||
|
||||
internal partial class NetworkingService : INetworkingService, IEventSettingInstanceLifetime
|
||||
{
|
||||
public readonly record struct NetId
|
||||
{
|
||||
private readonly string _value;
|
||||
|
||||
public NetId(string netId)
|
||||
{
|
||||
_value = netId;
|
||||
}
|
||||
|
||||
public static void Write(IWriteMessage message, NetId netId)
|
||||
{
|
||||
message.WriteString(netId._value);
|
||||
}
|
||||
|
||||
public static NetId Read(IReadMessage message)
|
||||
{
|
||||
return new NetId(message.ReadString());
|
||||
}
|
||||
}
|
||||
|
||||
private enum ClientToServer
|
||||
{
|
||||
NetMessageInternalId,
|
||||
NetMessageNetId,
|
||||
RequestSingleNetId,
|
||||
RequestSync,
|
||||
}
|
||||
|
||||
private enum ServerToClient
|
||||
{
|
||||
NetMessageInternalId,
|
||||
NetMessageNetId,
|
||||
ReceiveNetIds
|
||||
}
|
||||
|
||||
private ClientPacketHeader? clientHeader = null;
|
||||
public ClientPacketHeader ClientHeader
|
||||
{
|
||||
get
|
||||
{
|
||||
if (clientHeader == null)
|
||||
{
|
||||
byte lastHeader = (byte)Enum.GetValues(typeof(ClientPacketHeader)).Cast<ClientPacketHeader>().Last();
|
||||
clientHeader = (ClientPacketHeader)(lastHeader + 1);
|
||||
}
|
||||
|
||||
return (ClientPacketHeader)clientHeader;
|
||||
}
|
||||
}
|
||||
|
||||
private ServerPacketHeader? serverHeader = null;
|
||||
public ServerPacketHeader ServerHeader
|
||||
{
|
||||
get
|
||||
{
|
||||
if (serverHeader == null)
|
||||
{
|
||||
byte lastHeader = (byte)Enum.GetValues(typeof(ServerPacketHeader)).Cast<ServerPacketHeader>().Last();
|
||||
serverHeader = (ServerPacketHeader)(lastHeader + 1);
|
||||
}
|
||||
|
||||
return (ServerPacketHeader)serverHeader;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private ConcurrentDictionary<INetworkSyncVar, NetId> netVars = [];
|
||||
|
||||
private ConcurrentDictionary<NetId, NetMessageReceived> netReceives = [];
|
||||
private ConcurrentDictionary<ushort, NetId> packetToId = [];
|
||||
private ConcurrentDictionary<NetId, ushort> idToPacket = [];
|
||||
|
||||
public bool IsActive
|
||||
{
|
||||
get
|
||||
{
|
||||
return GameMain.NetworkMember != null;
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsSynchronized { get; private set; }
|
||||
public bool IsDisposed { get; private set; }
|
||||
|
||||
private readonly IEventService _eventService;
|
||||
private readonly ILoggerService _loggerService;
|
||||
private readonly INetworkIdProvider _networkIdProvider;
|
||||
|
||||
public NetworkingService(IEventService eventService, INetworkIdProvider networkIdProvider, ILoggerService loggerService)
|
||||
{
|
||||
_eventService = eventService;
|
||||
_networkIdProvider = networkIdProvider;
|
||||
_loggerService = loggerService;
|
||||
|
||||
#if SERVER
|
||||
IsSynchronized = true;
|
||||
#endif
|
||||
SubscribeToEvents();
|
||||
}
|
||||
|
||||
public void Receive(string netIdString, LuaCsAction callback)
|
||||
{
|
||||
#if SERVER
|
||||
Receive(new NetId(netIdString), (IReadMessage message, Client client) => callback(message, client));
|
||||
#elif CLIENT
|
||||
Receive(new NetId(netIdString), (IReadMessage message) => callback(message, null));
|
||||
#endif
|
||||
}
|
||||
|
||||
public void Receive(string netIdString, NetMessageReceived callback) => Receive(new NetId(netIdString), callback);
|
||||
public void Receive(Guid netIdGuid, NetMessageReceived callback) => Receive(new NetId(netIdGuid.ToString()), callback);
|
||||
public IWriteMessage Start(string netIdString)
|
||||
{
|
||||
if (netIdString == null)
|
||||
{
|
||||
// idk why but Lua calls this method with null instead of the Start method with no arguments
|
||||
return new WriteOnlyMessage();
|
||||
}
|
||||
|
||||
return Start(new NetId(netIdString));
|
||||
}
|
||||
public IWriteMessage Start(Guid netIdGuid) => Start(new NetId(netIdGuid.ToString()));
|
||||
public IWriteMessage Start() => new WriteOnlyMessage();
|
||||
|
||||
internal void Receive(NetId netId, NetMessageReceived callback)
|
||||
{
|
||||
#if SERVER
|
||||
RegisterId(netId);
|
||||
#elif CLIENT
|
||||
RequestId(netId);
|
||||
#endif
|
||||
netReceives[netId] = callback;
|
||||
}
|
||||
|
||||
private void HandleNetMessage(IReadMessage netMessage, NetId netId, Client client = null)
|
||||
{
|
||||
if (netReceives.ContainsKey(netId))
|
||||
{
|
||||
try
|
||||
{
|
||||
#if CLIENT
|
||||
netReceives[netId](netMessage);
|
||||
#elif SERVER
|
||||
netReceives[netId](netMessage, client);
|
||||
#endif
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_loggerService.LogResults(new ExceptionalError("Exception thrown inside NetMessageReceive({netId})", e));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (GameSettings.CurrentConfig.VerboseLogging)
|
||||
{
|
||||
#if SERVER
|
||||
_loggerService.LogError($"Received NetMessage for unknown netid {netId} from {GameServer.ClientLogName(client)}.");
|
||||
#else
|
||||
_loggerService.LogError($"Received NetMessage for unknown netid {netId} from server.");
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleNetMessageString(IReadMessage netMessage, Client client = null)
|
||||
{
|
||||
NetId netId = NetId.Read(netMessage);
|
||||
|
||||
HandleNetMessage(netMessage, netId, client);
|
||||
}
|
||||
|
||||
private void SubscribeToEvents()
|
||||
{
|
||||
_eventService.Subscribe<IEventSettingInstanceLifetime>(this);
|
||||
#if CLIENT
|
||||
_eventService.Subscribe<IEventServerConnected>(this);
|
||||
_eventService.Subscribe<IEventServerRawNetMessageReceived>(this);
|
||||
#elif SERVER
|
||||
_eventService.Subscribe<IEventClientRawNetMessageReceived>(this);
|
||||
#endif
|
||||
}
|
||||
|
||||
public Guid GetNetworkIdForInstance(INetworkSyncVar var)
|
||||
{
|
||||
return _networkIdProvider.GetNetworkIdForInstance(var);
|
||||
}
|
||||
|
||||
public void RegisterNetVar(INetworkSyncVar netVar)
|
||||
{
|
||||
netVar.SetNetworkOwner(this);
|
||||
|
||||
NetId netId = new NetId(netVar.InstanceId.ToString());
|
||||
netVars[netVar] = netId;
|
||||
|
||||
#if CLIENT
|
||||
Receive(netId, (IReadMessage message) =>
|
||||
{
|
||||
if (netVar.SyncType == NetSync.None)
|
||||
{
|
||||
_loggerService.LogWarning($"Received net var from server but {nameof(NetSync)} is {netVar.SyncType.ToString()}");
|
||||
return;
|
||||
}
|
||||
|
||||
netVar.ReadNetMessage(message);
|
||||
});
|
||||
#elif SERVER
|
||||
Receive(netId, (IReadMessage message, Client client) =>
|
||||
{
|
||||
if (netVar.SyncType == NetSync.None || netVar.SyncType == NetSync.ServerAuthority)
|
||||
{
|
||||
_loggerService.LogWarning($"Received net var from {GameServer.ClientLogName(client)} but {nameof(NetSync)} is {netVar.SyncType.ToString()}");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!client.HasPermission(netVar.WritePermissions))
|
||||
{
|
||||
_loggerService.LogWarning($"Received net var from {GameServer.ClientLogName(client)} but the client lacks permissions to modify it");
|
||||
return;
|
||||
}
|
||||
|
||||
netVar.ReadNetMessage(message);
|
||||
|
||||
// Sync back to all clients
|
||||
if (netVar.SyncType != NetSync.ClientOneWay)
|
||||
{
|
||||
SendNetVar(netVar);
|
||||
}
|
||||
});
|
||||
#endif
|
||||
}
|
||||
|
||||
public void DeregisterNetVar(INetworkSyncVar netVar)
|
||||
{
|
||||
if (netVar is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
netVar.SetNetworkOwner(null);
|
||||
netVars.TryRemove(netVar, out _);
|
||||
}
|
||||
|
||||
public void SendNetVar(INetworkSyncVar netVar) => SendNetVar(netVar, null);
|
||||
|
||||
public void SendNetVar(INetworkSyncVar netVar, NetworkConnection connection = null)
|
||||
{
|
||||
if (!netVars.TryGetValue(netVar, out NetId netId))
|
||||
{
|
||||
throw new InvalidOperationException("Tried to send net var across network without registering first");
|
||||
}
|
||||
|
||||
if (netVar.SyncType == NetSync.None) { return; }
|
||||
#if CLIENT
|
||||
if (netVar.SyncType == NetSync.ServerAuthority) { return; }
|
||||
#elif SERVER
|
||||
if (netVar.SyncType == NetSync.ClientOneWay) { return; }
|
||||
#endif
|
||||
|
||||
IWriteMessage message = Start(netId);
|
||||
netVar.WriteNetMessage(message);
|
||||
#if CLIENT
|
||||
SendToServer(message);
|
||||
#elif SERVER
|
||||
SendToClient(message, connection);
|
||||
#endif
|
||||
}
|
||||
|
||||
public FluentResults.Result Reset()
|
||||
{
|
||||
IsSynchronized = false;
|
||||
netReceives = new ConcurrentDictionary<NetId, NetMessageReceived>();
|
||||
packetToId = new ConcurrentDictionary<ushort, NetId>();
|
||||
idToPacket = new ConcurrentDictionary<NetId, ushort>();
|
||||
netVars = new ConcurrentDictionary<INetworkSyncVar, NetId>();
|
||||
|
||||
SubscribeToEvents();
|
||||
return FluentResults.Result.Ok();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
IsDisposed = true;
|
||||
}
|
||||
|
||||
#region Compatiblity
|
||||
|
||||
private static readonly HttpClient client = new HttpClient();
|
||||
|
||||
public async void HttpRequest(string url, LuaCsAction callback, string data = null, string method = "POST", string contentType = "application/json", Dictionary<string, string> headers = null, string savePath = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
HttpRequestMessage request = new HttpRequestMessage(new HttpMethod(method), url);
|
||||
|
||||
if (headers != null)
|
||||
{
|
||||
foreach (var header in headers)
|
||||
{
|
||||
request.Headers.Add(header.Key, header.Value);
|
||||
}
|
||||
}
|
||||
|
||||
if (data != null)
|
||||
{
|
||||
request.Content = new StringContent(data, Encoding.UTF8, contentType);
|
||||
}
|
||||
|
||||
HttpResponseMessage response = await client.SendAsync(request);
|
||||
|
||||
if (savePath != null)
|
||||
{
|
||||
if (LuaCsFile.IsPathAllowedException(savePath))
|
||||
{
|
||||
byte[] responseData = await response.Content.ReadAsByteArrayAsync();
|
||||
|
||||
using (var fileStream = new FileStream(savePath, FileMode.Create, FileAccess.Write))
|
||||
{
|
||||
fileStream.Write(responseData, 0, responseData.Length);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
string responseBody = await response.Content.ReadAsStringAsync();
|
||||
|
||||
CrossThread.RequestExecutionOnMainThread(() =>
|
||||
{
|
||||
callback(responseBody, (int)response.StatusCode, response.Headers);
|
||||
});
|
||||
}
|
||||
catch (HttpRequestException e)
|
||||
{
|
||||
CrossThread.RequestExecutionOnMainThread(() => { callback(e.Message, e.StatusCode, null); });
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
CrossThread.RequestExecutionOnMainThread(() => { callback(e.Message, null, null); });
|
||||
}
|
||||
}
|
||||
|
||||
public void HttpPost(string url, LuaCsAction callback, string data, string contentType = "application/json", Dictionary<string, string> headers = null, string savePath = null)
|
||||
{
|
||||
HttpRequest(url, callback, data, "POST", contentType, headers, savePath);
|
||||
}
|
||||
|
||||
public void RequestPostHTTP(string url, LuaCsAction callback, string data, string contentType = "application/json", Dictionary<string, string> headers = null, string savePath = null)
|
||||
{
|
||||
HttpRequest(url, callback, data, "POST", contentType, headers, savePath);
|
||||
}
|
||||
|
||||
public void HttpGet(string url, LuaCsAction callback, Dictionary<string, string> headers = null, string savePath = null)
|
||||
{
|
||||
HttpRequest(url, callback, null, "GET", null, headers, savePath);
|
||||
}
|
||||
|
||||
public void RequestGetHTTP(string url, LuaCsAction callback, Dictionary<string, string> headers = null, string savePath = null)
|
||||
{
|
||||
HttpRequest(url, callback, null, "GET", null, headers, savePath);
|
||||
}
|
||||
|
||||
public void CreateEntityEvent(INetSerializable entity, NetEntityEvent.IData extraData)
|
||||
{
|
||||
GameMain.NetworkMember.CreateEntityEvent(entity, extraData);
|
||||
}
|
||||
|
||||
public ushort LastClientListUpdateID
|
||||
{
|
||||
get { return GameMain.NetworkMember.LastClientListUpdateID; }
|
||||
set { GameMain.NetworkMember.LastClientListUpdateID = value; }
|
||||
}
|
||||
|
||||
#if SERVER
|
||||
public void ClientWriteLobby(Client client) => GameMain.Server.ClientWriteLobby(client);
|
||||
|
||||
public void UpdateClientPermissions(Client client)
|
||||
{
|
||||
GameMain.Server.UpdateClientPermissions(client);
|
||||
}
|
||||
|
||||
public int FileSenderMaxPacketsPerUpdate
|
||||
{
|
||||
get { return FileSender.FileTransferOut.MaxPacketsPerUpdate; }
|
||||
set { FileSender.FileTransferOut.MaxPacketsPerUpdate = value; }
|
||||
}
|
||||
#endif
|
||||
|
||||
#endregion
|
||||
|
||||
public void OnSettingInstanceCreated<T>(T configInstance) where T : ISettingBase
|
||||
{
|
||||
if (configInstance is INetworkSyncVar syncVar)
|
||||
{
|
||||
RegisterNetVar(syncVar);
|
||||
}
|
||||
}
|
||||
|
||||
public void OnSettingInstanceDisposed<T>(T configInstance) where T : ISettingBase
|
||||
{
|
||||
if (configInstance is INetworkSyncVar syncVar)
|
||||
{
|
||||
DeregisterNetVar(syncVar);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,558 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading.Tasks;
|
||||
using System.Xml;
|
||||
using Barotrauma.Extensions;
|
||||
using Barotrauma.LuaCs.Data;
|
||||
using FluentResults;
|
||||
using Microsoft.Toolkit.Diagnostics;
|
||||
|
||||
namespace Barotrauma.LuaCs;
|
||||
|
||||
public sealed class PackageManagementService : IPackageManagementService
|
||||
{
|
||||
// svc
|
||||
private ILoggerService _logger;
|
||||
private IModConfigService _modConfigService;
|
||||
private IConfigService _configService;
|
||||
private ILuaScriptManagementService _luaScriptManagementService;
|
||||
private IPluginManagementService _pluginManagementService;
|
||||
private IConsoleCommandsService _commandsService;
|
||||
#if CLIENT
|
||||
private IUIStylesService _uiStylesService;
|
||||
#endif
|
||||
private IPackageManagementServiceConfig _runConfig;
|
||||
// state
|
||||
private readonly ConcurrentDictionary<ContentPackage, IModConfigInfo> _loadedPackages = new();
|
||||
private readonly ConcurrentDictionary<ContentPackage, IModConfigInfo> _runningPackages = new();
|
||||
private readonly ConcurrentDictionary<string, ContentPackage> _packageNameCache = new();
|
||||
// control
|
||||
/// <summary>
|
||||
/// Service Disposal Lock.
|
||||
/// </summary>
|
||||
private readonly AsyncReaderWriterLock _operationsLock = new();
|
||||
/// <summary>
|
||||
/// Execution of packages lock.
|
||||
/// <br/> Read: Package loading/unloading (Multi-operation mode).
|
||||
/// <br/> Write: Package execution (exclusive mode).
|
||||
/// </summary>
|
||||
private readonly AsyncReaderWriterLock _executionLock = new();
|
||||
|
||||
public PackageManagementService(ILoggerService logger,
|
||||
IModConfigService modConfigService,
|
||||
ILuaScriptManagementService luaScriptManagementService,
|
||||
IPluginManagementService pluginManagementService,
|
||||
IConfigService configService,
|
||||
IConsoleCommandsService commandsService,
|
||||
#if CLIENT
|
||||
IUIStylesService uiStylesService,
|
||||
#endif
|
||||
IPackageManagementServiceConfig runConfig)
|
||||
{
|
||||
_logger = logger;
|
||||
_modConfigService = modConfigService;
|
||||
_luaScriptManagementService = luaScriptManagementService;
|
||||
_pluginManagementService = pluginManagementService;
|
||||
_configService = configService;
|
||||
_runConfig = runConfig;
|
||||
#if CLIENT
|
||||
_uiStylesService = uiStylesService;
|
||||
#endif
|
||||
_commandsService = commandsService;
|
||||
commandsService.RegisterCommand("pms_getxmlname",
|
||||
"Gets the XML encoded name for the given package, as used in localization.",
|
||||
onExecute: args =>
|
||||
{
|
||||
if (args.Length < 1)
|
||||
{
|
||||
_logger.LogError("Please specify the name of the package.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (ContentPackageManager.AllPackages.FirstOrDefault(p => p.Name == args[0]) is { } pkg)
|
||||
{
|
||||
_logger.Log($"Package Xml Name: '{XmlConvert.EncodeLocalName(pkg.Name)}'");
|
||||
return;
|
||||
}
|
||||
_logger.Log($"Could not find package with the name '{args[0]}'");
|
||||
},
|
||||
getValidArgs: () =>
|
||||
{
|
||||
return new[]
|
||||
{
|
||||
this._loadedPackages.Keys.Select(p => p.Name).ToArray()
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
using var lck = _operationsLock.AcquireWriterLock().ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
if (!ModUtils.Threading.CheckIfClearAndSetBool(ref _isDisposed))
|
||||
return;
|
||||
|
||||
_logger.LogMessage($"{nameof(PackageManagementService)} is disposing.");
|
||||
_luaScriptManagementService.Dispose();
|
||||
_pluginManagementService.Dispose();
|
||||
_modConfigService.Dispose();
|
||||
_logger.Dispose();
|
||||
#if CLIENT
|
||||
_uiStylesService.Dispose();
|
||||
#endif
|
||||
|
||||
_logger = null;
|
||||
_luaScriptManagementService = null;
|
||||
_pluginManagementService = null;
|
||||
_modConfigService = null;
|
||||
#if CLIENT
|
||||
_uiStylesService = null;
|
||||
#endif
|
||||
|
||||
|
||||
_loadedPackages.Clear();
|
||||
_runningPackages.Clear();
|
||||
}
|
||||
|
||||
private int _isDisposed = 0;
|
||||
public bool IsDisposed
|
||||
{
|
||||
get => ModUtils.Threading.GetBool(ref _isDisposed);
|
||||
set => ModUtils.Threading.SetBool(ref _isDisposed, value);
|
||||
}
|
||||
|
||||
public FluentResults.Result Reset()
|
||||
{
|
||||
using var lck = _operationsLock.AcquireWriterLock().ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
if (IsDisposed)
|
||||
return FluentResults.Result.Fail($"{nameof(PackageManagementService)}failed to reset. Has already been disposed.");
|
||||
|
||||
try
|
||||
{
|
||||
var operationResult = new FluentResults.Result();
|
||||
|
||||
operationResult.WithReasons(_luaScriptManagementService.Reset().Reasons);
|
||||
operationResult.WithReasons(_pluginManagementService.Reset().Reasons);
|
||||
operationResult.WithReasons(_configService.Reset().Reasons);
|
||||
#if CLIENT
|
||||
operationResult.WithReasons(_uiStylesService.Reset().Reasons);
|
||||
#endif
|
||||
_runningPackages.Clear();
|
||||
_loadedPackages.Clear();
|
||||
_packageNameCache.Clear();
|
||||
return operationResult;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return FluentResults.Result.Fail(new ExceptionalError(e));
|
||||
}
|
||||
}
|
||||
|
||||
public bool TryGetLoadedPackageByName(string name, out ContentPackage package)
|
||||
{
|
||||
package = null;
|
||||
if (name.IsNullOrWhiteSpace())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
using var _ = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
return _packageNameCache.TryGetValue(name, out package);
|
||||
}
|
||||
|
||||
public FluentResults.Result LoadPackageInfo(ContentPackage package)
|
||||
{
|
||||
Guard.IsNotNull(package, nameof(package));
|
||||
using var lck = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
using var executeLock = _executionLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
|
||||
IService.CheckDisposed(this);
|
||||
if (_loadedPackages.TryGetValue(package, out var result))
|
||||
{
|
||||
_logger.LogWarning($"{nameof(LoadPackageInfo)}: Tried to load already-loaded package {package.Name}.");
|
||||
return FluentResults.Result.Ok();
|
||||
}
|
||||
|
||||
var pkgCfgInfo = _modConfigService.CreateConfigAsync(package).ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
if (pkgCfgInfo.IsFailed)
|
||||
{
|
||||
_logger.LogResults(pkgCfgInfo.ToResult());
|
||||
return pkgCfgInfo.ToResult();
|
||||
}
|
||||
return UnsafeAddPackageInternal(package, pkgCfgInfo.Value);
|
||||
}
|
||||
|
||||
public FluentResults.Result LoadPackagesInfo(ImmutableArray<ContentPackage> packages)
|
||||
{
|
||||
if (packages.IsDefaultOrEmpty)
|
||||
ThrowHelper.ThrowArgumentException($"{nameof(LoadPackagesInfo)}: packages list is empty.");
|
||||
using var lck = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
using var executeLock = _executionLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
|
||||
IService.CheckDisposed(this);
|
||||
var result = new FluentResults.Result();
|
||||
var packages2 = packages.OrderBy(pkg => pkg.Name == "LuaCsForBarotrauma" ? 0 : 1) // always run lua cs first.
|
||||
.ThenBy(packages.IndexOf)
|
||||
.ToImmutableArray();
|
||||
|
||||
var pkgConfigs = _modConfigService.CreateConfigsAsync([..packages2]).ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
foreach (var pkgConfig in pkgConfigs)
|
||||
{
|
||||
result.WithReasons(pkgConfig.Config.Reasons);
|
||||
if (pkgConfig.Config.IsSuccess)
|
||||
{
|
||||
result.WithReasons(UnsafeAddPackageInternal(pkgConfig.Source, pkgConfig.Config.Value).Reasons);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private FluentResults.Result UnsafeAddPackageInternal(ContentPackage package, IModConfigInfo config)
|
||||
{
|
||||
if (_loadedPackages.TryGetValue(package, out _))
|
||||
{
|
||||
_logger.LogWarning($"Tried to load already-loaded package {package.Name}.");
|
||||
return FluentResults.Result.Ok();
|
||||
}
|
||||
|
||||
// We need to touch ContentPath.Fullpath once in a single-threaded context to make it thread-safe.
|
||||
foreach (var info in config.Assemblies)
|
||||
{
|
||||
TouchMeFullPaths(info);
|
||||
}
|
||||
|
||||
foreach (var info in config.Configs)
|
||||
{
|
||||
TouchMeFullPaths(info);
|
||||
}
|
||||
|
||||
foreach (var info in config.LuaScripts)
|
||||
{
|
||||
TouchMeFullPaths(info);
|
||||
}
|
||||
|
||||
// We need to touch ContentPath.Fullpath once in a single-threaded context to make it thread-safe.
|
||||
[MethodImpl(MethodImplOptions.NoOptimization | MethodImplOptions.PreserveSig)]
|
||||
void TouchMeFullPaths(IBaseResourceInfo info)
|
||||
{
|
||||
foreach (var contentPath in info.FilePaths)
|
||||
{
|
||||
var s = contentPath.FullPath;
|
||||
}
|
||||
}
|
||||
|
||||
_loadedPackages[package] = config;
|
||||
_packageNameCache[package.Name] = package;
|
||||
try
|
||||
{
|
||||
var res = new FluentResults.Result();
|
||||
var tasks = ImmutableArray.CreateBuilder<Task<Task<FluentResults.Result>>>();
|
||||
|
||||
if (!config.Configs.IsDefaultOrEmpty)
|
||||
{
|
||||
tasks.Add(Task.Factory.StartNew(async Task<FluentResults.Result> () =>
|
||||
new FluentResults.Result()
|
||||
.WithReasons((await _configService.LoadConfigsAsync(config.Configs)).Reasons)
|
||||
.WithReasons((await _configService.LoadConfigsProfilesAsync(config.Configs)).Reasons)));
|
||||
}
|
||||
|
||||
if (!config.LuaScripts.IsDefaultOrEmpty)
|
||||
{
|
||||
tasks.Add(Task.Factory.StartNew(async () =>
|
||||
await _luaScriptManagementService.LoadScriptResourcesAsync(config.LuaScripts)));
|
||||
}
|
||||
|
||||
if (tasks.Count == 0)
|
||||
{
|
||||
return FluentResults.Result.Ok();
|
||||
}
|
||||
|
||||
#if CLIENT
|
||||
if (!config.Styles.IsDefaultOrEmpty)
|
||||
{
|
||||
res.WithReasons(_uiStylesService.LoadAssets(config.Styles).Reasons);
|
||||
}
|
||||
#endif
|
||||
var r = Task.WhenAll(tasks.ToArray()).ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
|
||||
foreach (var task in r)
|
||||
{
|
||||
res.WithReasons(task.ConfigureAwait(false).GetAwaiter().GetResult().Reasons);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return FluentResults.Result.Fail(new ExceptionalError(e));
|
||||
}
|
||||
}
|
||||
|
||||
public FluentResults.Result ExecuteLoadedPackages(ImmutableArray<ContentPackage> executionOrder, bool executeCsAssemblies)
|
||||
{
|
||||
using var lck = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
using var executeLock = _executionLock.AcquireWriterLock().ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
IService.CheckDisposed(this);
|
||||
|
||||
if (executionOrder.IsDefaultOrEmpty)
|
||||
{
|
||||
return FluentResults.Result.Fail($"{nameof(ExecuteLoadedPackages)}: No packages in the execution order list.");
|
||||
}
|
||||
|
||||
if (!_runningPackages.IsEmpty)
|
||||
{
|
||||
return FluentResults.Result.Fail(
|
||||
$"{nameof(ExecuteLoadedPackages)}: There are already packages running! List: {
|
||||
_runningPackages.Aggregate(string.Empty, (acc, kvp) => "-" + kvp + "\n" + kvp.Key.Name)}");
|
||||
}
|
||||
|
||||
if (_loadedPackages.IsEmpty)
|
||||
{
|
||||
return FluentResults.Result.Fail($"{nameof(ExecuteLoadedPackages)}: No packages loaded. Nothing to run!)");
|
||||
}
|
||||
|
||||
var result = new FluentResults.Result();
|
||||
|
||||
// get loading order. Note: packages not in the execution order list will load first.
|
||||
var loadingOrderedPackages = _loadedPackages
|
||||
.OrderBy(pkg => pkg.Key.Name == "LuaCsForBarotrauma" ? 0 : 1) // always run lua cs first.
|
||||
.ThenBy(pkg => executionOrder.IndexOf(pkg.Key))
|
||||
.ToImmutableArray();
|
||||
var loadOrderByPackage = loadingOrderedPackages.Select(p => p.Key).ToImmutableArray();
|
||||
var toLoadPackagesIndents = loadingOrderedPackages
|
||||
.SelectMany(p => p.Key.AltNames.Union(new []{ p.Key.Name }).ToIdentifiers())
|
||||
.ToImmutableHashSet();
|
||||
|
||||
|
||||
// NOTE: Config/Settings are instanced in LoadPackages()
|
||||
|
||||
if (executeCsAssemblies)
|
||||
{
|
||||
var plugins = SelectCompatible(loadingOrderedPackages
|
||||
.SelectMany(pkg => pkg.Value.Assemblies)
|
||||
.ToImmutableArray(), toLoadPackagesIndents, loadOrderByPackage);
|
||||
|
||||
if (!plugins.IsDefaultOrEmpty)
|
||||
{
|
||||
result.WithReasons(_pluginManagementService.LoadAssemblyResources(plugins).Reasons);
|
||||
result.WithReasons(_pluginManagementService.ActivatePluginInstances(
|
||||
plugins.Select(p => p.OwnerPackage).ToImmutableArray(), false).Reasons);
|
||||
}
|
||||
}
|
||||
|
||||
//lua scripts
|
||||
var luaScripts = SelectCompatible(loadingOrderedPackages
|
||||
.Where(pkg => executeCsAssemblies
|
||||
|| !pkg.Value.LuaScripts.Any(scr => scr.RunUnrestricted))
|
||||
.SelectMany(pkg => pkg.Value.LuaScripts)
|
||||
.ToImmutableArray(), toLoadPackagesIndents, loadOrderByPackage);
|
||||
|
||||
if (!luaScripts.IsDefaultOrEmpty)
|
||||
{
|
||||
result.WithReasons(_luaScriptManagementService.ExecuteLoadedScripts(luaScripts, enableSandbox: !executeCsAssemblies).Reasons);
|
||||
}
|
||||
|
||||
foreach (var package in loadingOrderedPackages)
|
||||
{
|
||||
_runningPackages[package.Key] = package.Value;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static ImmutableArray<T> SelectCompatible<T>(ImmutableArray<T> resources,
|
||||
ImmutableHashSet<Identifier> enabledPackagesIdents,
|
||||
ImmutableArray<ContentPackage> loadingOrder)
|
||||
where T : IBaseResourceInfo
|
||||
{
|
||||
return resources
|
||||
.Where(r => r.SupportedPlatforms.HasFlag(ModUtils.Environment.CurrentPlatform))
|
||||
.Where(r => r.SupportedTargets.HasFlag(ModUtils.Environment.CurrentTarget))
|
||||
.Where(r => !r.Optional || (
|
||||
(r.RequiredPackages.IsDefaultOrEmpty || enabledPackagesIdents.Intersect(r.RequiredPackages).Any())
|
||||
&& (r.IncompatiblePackages.IsDefaultOrEmpty || enabledPackagesIdents.Intersect(r.IncompatiblePackages).None())))
|
||||
.OrderBy(r => r.Optional ? 1 : 0) // optional content last
|
||||
.ThenBy(r => loadingOrder.IndexOf(r.OwnerPackage))
|
||||
.ThenBy(r => r.LoadPriority)
|
||||
.ToImmutableArray();
|
||||
}
|
||||
|
||||
|
||||
public FluentResults.Result SyncLoadedPackagesList(ImmutableArray<ContentPackage> packages)
|
||||
{
|
||||
if (packages.IsDefaultOrEmpty)
|
||||
ThrowHelper.ThrowArgumentNullException(nameof(packages));
|
||||
if (!_runningPackages.IsEmpty)
|
||||
ThrowHelper.ThrowInvalidOperationException($"{nameof(SyncLoadedPackagesList)}: There are packages running!");
|
||||
|
||||
var toRemove = _loadedPackages.Keys.Except(packages).ToImmutableArray();
|
||||
var toAdd = packages.Except(_loadedPackages.Keys)
|
||||
.OrderBy(pack => packages.IndexOf(pack)).ToImmutableArray();
|
||||
|
||||
var result = new FluentResults.Result();
|
||||
|
||||
if (!toRemove.IsDefaultOrEmpty)
|
||||
{
|
||||
result.WithReasons(UnloadPackages(toRemove).Reasons);
|
||||
}
|
||||
|
||||
if (!toAdd.IsDefaultOrEmpty)
|
||||
{
|
||||
result.WithReasons(LoadPackagesInfo(toAdd).Reasons);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public FluentResults.Result StopRunningPackages()
|
||||
{
|
||||
using var lck = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
using var executeLock = _executionLock.AcquireWriterLock().ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
IService.CheckDisposed(this);
|
||||
|
||||
if (_loadedPackages.IsEmpty || _runningPackages.IsEmpty)
|
||||
{
|
||||
_logger.LogWarning($"{nameof(StopRunningPackages)}: No packages are currently executing.");
|
||||
return FluentResults.Result.Ok();
|
||||
}
|
||||
|
||||
var res = new FluentResults.Result();
|
||||
res.WithReasons(_luaScriptManagementService.UnloadActiveScripts().Reasons);
|
||||
res.WithReasons(_pluginManagementService.UnloadManagedAssemblies().Reasons);
|
||||
_runningPackages.Clear();
|
||||
return res;
|
||||
}
|
||||
|
||||
public FluentResults.Result UnloadPackage(ContentPackage package)
|
||||
{
|
||||
Guard.IsNotNull(package, nameof(package));
|
||||
using var lck = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
using var executeLock = _executionLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
IService.CheckDisposed(this);
|
||||
|
||||
if (!_loadedPackages.ContainsKey(package))
|
||||
{
|
||||
return FluentResults.Result.Fail($"{nameof(UnloadPackage)}: The package is not loaded.");
|
||||
}
|
||||
if (!_runningPackages.IsEmpty)
|
||||
{
|
||||
return FluentResults.Result.Fail($"{nameof(UnloadPackage)}: Packages are currently executing.");
|
||||
}
|
||||
var result = new FluentResults.Result();
|
||||
result.WithReasons(_luaScriptManagementService.DisposePackageResources(package).Reasons);
|
||||
result.WithReasons(_configService.DisposePackageData(package).Reasons);
|
||||
#if CLIENT
|
||||
result.WithReasons(_uiStylesService.UnloadPackage(package).Reasons);
|
||||
#endif
|
||||
_loadedPackages.TryRemove(package, out _);
|
||||
_packageNameCache.TryRemove(package.Name, out _);
|
||||
return result;
|
||||
}
|
||||
|
||||
public FluentResults.Result UnloadPackages(ImmutableArray<ContentPackage> packages)
|
||||
{
|
||||
if (packages.IsDefaultOrEmpty)
|
||||
return FluentResults.Result.Fail($"{nameof(UnloadPackages)}: Package list is empty.");
|
||||
|
||||
using var lck = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
using var executeLock = _executionLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
IService.CheckDisposed(this);
|
||||
|
||||
var result = new FluentResults.Result();
|
||||
foreach (var package in packages)
|
||||
{
|
||||
result.WithReasons(UnloadPackage(package).Reasons);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public FluentResults.Result UnloadAllPackages()
|
||||
{
|
||||
using var lck = _operationsLock.AcquireWriterLock().ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
using var executeLock = _executionLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
IService.CheckDisposed(this);
|
||||
|
||||
if (_loadedPackages.IsEmpty)
|
||||
return FluentResults.Result.Ok();
|
||||
if (!_runningPackages.IsEmpty)
|
||||
return FluentResults.Result.Fail($"{nameof(UnloadAllPackages)}: Packages are currently executing.");
|
||||
var result = new FluentResults.Result();
|
||||
result.WithReasons(_luaScriptManagementService.DisposeAllPackageResources().Reasons);
|
||||
result.WithReasons(_configService.DisposeAllPackageData().Reasons);
|
||||
_loadedPackages.Clear();
|
||||
return result;
|
||||
}
|
||||
|
||||
public ImmutableArray<ContentPackage> GetAllLoadedPackages()
|
||||
{
|
||||
using var lck = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
IService.CheckDisposed(this);
|
||||
return [.._loadedPackages.Keys];
|
||||
}
|
||||
|
||||
public bool IsPackageRunning(ContentPackage package)
|
||||
{
|
||||
Guard.IsNotNull(package, nameof(package));
|
||||
using var lck = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
IService.CheckDisposed(this);
|
||||
return _runningPackages.ContainsKey(package);
|
||||
}
|
||||
|
||||
public bool IsAnyPackageLoaded()
|
||||
{
|
||||
using var lck = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
IService.CheckDisposed(this);
|
||||
return !_loadedPackages.IsEmpty;
|
||||
}
|
||||
|
||||
public bool IsAnyPackageRunning()
|
||||
{
|
||||
using var lck = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
IService.CheckDisposed(this);
|
||||
return !_runningPackages.IsEmpty;
|
||||
}
|
||||
|
||||
public ImmutableArray<ContentPackage> GetLoadedUnrestrictedPackages()
|
||||
{
|
||||
using var lck = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
IService.CheckDisposed(this);
|
||||
if (_loadedPackages.IsEmpty)
|
||||
return ImmutableArray<ContentPackage>.Empty;
|
||||
return [.._loadedPackages.Values
|
||||
.Where(cfg => !cfg.Assemblies.IsDefaultOrEmpty || cfg.LuaScripts.Any(scr => scr.RunUnrestricted))
|
||||
.Select(cfg => cfg.Package)];
|
||||
}
|
||||
|
||||
public bool PackageContainsAnyRunnableResource(ContentPackage package)
|
||||
{
|
||||
using var lck = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
IService.CheckDisposed(this);
|
||||
|
||||
var result = GetModConfigForPackage(package);
|
||||
|
||||
if (result.IsSuccess)
|
||||
{
|
||||
return result.Value.Assemblies.Any() || result.Value.LuaScripts.Any();
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public Result<IModConfigInfo> GetModConfigForPackage(ContentPackage package)
|
||||
{
|
||||
using var lck = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
IService.CheckDisposed(this);
|
||||
|
||||
if (!_loadedPackages.TryGetValue(package, out var modConfig))
|
||||
{
|
||||
return FluentResults.Result.Fail($"Failed to find mod config for package {package.Name}");
|
||||
}
|
||||
|
||||
return new FluentResults.Result<IModConfigInfo>().WithValue(modConfig);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,962 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Linq.Expressions;
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Runtime.Loader;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Xml.Serialization;
|
||||
using Barotrauma.Extensions;
|
||||
using Barotrauma.IO;
|
||||
using Barotrauma.LuaCs.Data;
|
||||
using Barotrauma.LuaCs.Events;
|
||||
using FluentResults;
|
||||
using FluentResults.LuaCs;
|
||||
using LightInject;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.Text;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Toolkit.Diagnostics;
|
||||
using OneOf;
|
||||
|
||||
namespace Barotrauma.LuaCs;
|
||||
|
||||
public class PluginManagementService : IAssemblyManagementService
|
||||
{
|
||||
#region CSHARP_COMPILATION_OPTIONS
|
||||
|
||||
private static readonly CSharpParseOptions ScriptParseOptions = CSharpParseOptions.Default
|
||||
.WithPreprocessorSymbols(new[]
|
||||
{
|
||||
#if SERVER
|
||||
"SERVER"
|
||||
#elif CLIENT
|
||||
"CLIENT"
|
||||
#else
|
||||
"UNDEFINED"
|
||||
#endif
|
||||
#if DEBUG
|
||||
,"DEBUG"
|
||||
#endif
|
||||
});
|
||||
|
||||
#if WINDOWS
|
||||
private const string PLATFORM_TARGET = "Windows";
|
||||
#elif OSX
|
||||
private const string PLATFORM_TARGET = "OSX";
|
||||
#elif LINUX
|
||||
private const string PLATFORM_TARGET = "Linux";
|
||||
#endif
|
||||
|
||||
#if CLIENT
|
||||
private const string ARCHITECTURE_TARGET = "Client";
|
||||
#elif SERVER
|
||||
private const string ARCHITECTURE_TARGET = "Server";
|
||||
#endif
|
||||
|
||||
private static readonly CSharpCompilationOptions CompilationOptions = new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)
|
||||
.WithMetadataImportOptions(MetadataImportOptions.All)
|
||||
#if DEBUG
|
||||
.WithOptimizationLevel(OptimizationLevel.Debug)
|
||||
#else
|
||||
.WithOptimizationLevel(OptimizationLevel.Release)
|
||||
#endif
|
||||
.WithAllowUnsafe(true);
|
||||
|
||||
private static readonly SyntaxTree BaseAssemblyImports = CSharpSyntaxTree.ParseText(
|
||||
new StringBuilder()
|
||||
.AppendLine("global using LuaCsHook = Barotrauma.LuaCs.Compatibility.ILuaCsHook;")
|
||||
.AppendLine("global using System.Reflection;")
|
||||
.AppendLine("global using Barotrauma;")
|
||||
.AppendLine("global using Barotrauma.LuaCs;")
|
||||
.AppendLine("global using Barotrauma.LuaCs.Compatibility;")
|
||||
.AppendLine("using System.Runtime.CompilerServices;")
|
||||
.AppendLine("[assembly: IgnoresAccessChecksTo(\"BarotraumaCore\")]")
|
||||
#if CLIENT
|
||||
.AppendLine("[assembly: IgnoresAccessChecksTo(\"Barotrauma\")]")
|
||||
#elif SERVER
|
||||
.AppendLine("[assembly: IgnoresAccessChecksTo(\"DedicatedServer\")]")
|
||||
#endif
|
||||
.ToString(),
|
||||
ScriptParseOptions);
|
||||
|
||||
private ImmutableArray<MetadataReference> _baseMetadataReferences = ImmutableArray<MetadataReference>.Empty;
|
||||
private ImmutableArray<MetadataReference> _baseMetadataReferencesNonPublicized = ImmutableArray<MetadataReference>.Empty;
|
||||
|
||||
|
||||
private IEnumerable<MetadataReference> BaseMetadataReferences
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_baseMetadataReferences.IsDefaultOrEmpty)
|
||||
{
|
||||
_baseMetadataReferences = Basic.Reference.Assemblies.Net80.References.All
|
||||
.Union(AssemblyLoadContext.Default.Assemblies
|
||||
.Where(ass =>
|
||||
!ass.IsDynamic &&
|
||||
!ass.GetName().FullName.StartsWith("BarotraumaCore") &&
|
||||
!ass.GetName().FullName.StartsWith("Barotrauma") &&
|
||||
!ass.GetName().FullName.StartsWith("DedicatedServer"))
|
||||
.Where(ass => !ass.Location.IsNullOrWhiteSpace())
|
||||
.Select(MetadataReference (ass) => MetadataReference.CreateFromFile(ass.Location)))
|
||||
.Where(ar => ar is not null)
|
||||
.ToImmutableArray();
|
||||
}
|
||||
|
||||
return _baseMetadataReferences;
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerable<MetadataReference> BaseMetadataReferencesWithBarotrauma
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_baseMetadataReferencesNonPublicized.IsDefaultOrEmpty)
|
||||
{
|
||||
_baseMetadataReferencesNonPublicized = Basic.Reference.Assemblies.Net80.References.All
|
||||
.Union(AssemblyLoadContext.Default.Assemblies
|
||||
.Where(ass => !ass.IsDynamic)
|
||||
.Where(ass => !ass.Location.IsNullOrWhiteSpace())
|
||||
.Select(MetadataReference (ass) => MetadataReference.CreateFromFile(ass.Location)))
|
||||
.Where(ar => ar is not null)
|
||||
.ToImmutableArray();
|
||||
}
|
||||
|
||||
return _baseMetadataReferencesNonPublicized;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Disposal
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
using var lck = _operationsLock.AcquireWriterLock().ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
if (!ModUtils.Threading.CheckIfClearAndSetBool(ref _isDisposed))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
UnsafeDisposeResourcesInternal();
|
||||
_assemblyLoaderFactory = null;
|
||||
_storageService = null;
|
||||
_eventService = null;
|
||||
_logger = null;
|
||||
_configService = null;
|
||||
_luaScriptManagementService = null;
|
||||
_luaCsInfoProvider = null;
|
||||
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
private void UnsafeDisposeResourcesInternal()
|
||||
{
|
||||
foreach (var packPlugin in _pluginInstances.SelectMany(kvp => kvp.Value.Select(pluginInst => (kvp.Key, pluginInst))))
|
||||
{
|
||||
try
|
||||
{
|
||||
packPlugin.pluginInst.Dispose();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogError($"Error while disposing plugin for ContentPackage {packPlugin.Key.Name}: \n{e.Message}");
|
||||
}
|
||||
}
|
||||
_pluginInstances.Clear();
|
||||
_pluginPackageLookup.Clear();
|
||||
_pluginInjectorContainer?.Dispose();
|
||||
_pluginInjectorContainer = null;
|
||||
|
||||
foreach (var loader in _assemblyLoaders)
|
||||
{
|
||||
try
|
||||
{
|
||||
loader.Value.Dispose();
|
||||
_unloadingAssemblyLoaders.Add(loader.Value, loader.Key);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger?.LogError($"Failed to dispose of {nameof(IAssemblyLoaderService)} for ContentPackage {loader.Key.Name}: \n{e.Message}");
|
||||
if (loader.Value.Assemblies.Any())
|
||||
{
|
||||
foreach (var ass in loader.Value.Assemblies)
|
||||
{
|
||||
_logger?.LogWarning($"{nameof(PluginManagementService)}: Fallback manual unsubscription of assemblies: {ass.GetName()}");
|
||||
ReflectionUtils.RemoveAssemblyFromCache(ass);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_assemblyLoaders.Clear();
|
||||
}
|
||||
|
||||
private int _isDisposed = 0;
|
||||
public bool IsDisposed
|
||||
{
|
||||
get => ModUtils.Threading.GetBool(ref _isDisposed);
|
||||
private set => ModUtils.Threading.SetBool(ref _isDisposed, value);
|
||||
}
|
||||
public FluentResults.Result Reset()
|
||||
{
|
||||
using var lck = _operationsLock.AcquireWriterLock().ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
IService.CheckDisposed(this);
|
||||
UnsafeDisposeResourcesInternal();
|
||||
return FluentResults.Result.Ok();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private IAssemblyLoaderService.IFactory _assemblyLoaderFactory;
|
||||
private IStorageService _storageService;
|
||||
private ILoggerService _logger;
|
||||
private Lazy<IEventService> _eventService;
|
||||
private Lazy<IConfigService> _configService;
|
||||
private Lazy<ILuaScriptManagementService> _luaScriptManagementService;
|
||||
private IEventService _pluginEventService;
|
||||
private Lazy<ILuaPatcher> _pluginLuaPatcherService;
|
||||
private Func<IConsoleCommandsService> _consoleCommandServiceFactory;
|
||||
private ILuaCsInfoProvider _luaCsInfoProvider;
|
||||
private readonly ConcurrentDictionary<ContentPackage, IAssemblyLoaderService> _assemblyLoaders = new();
|
||||
private readonly ConcurrentDictionary<Type, ContentPackage> _pluginPackageLookup = new();
|
||||
private readonly ConcurrentDictionary<ContentPackage, ImmutableArray<IAssemblyPlugin>> _pluginInstances = new();
|
||||
private readonly ConditionalWeakTable<IAssemblyLoaderService, ContentPackage> _unloadingAssemblyLoaders = new();
|
||||
private readonly ConcurrentBag<IntPtr> _loadedNativeLibraries = new();
|
||||
private readonly AsyncReaderWriterLock _operationsLock = new();
|
||||
private ServiceContainer _pluginInjectorContainer;
|
||||
|
||||
public PluginManagementService(
|
||||
IAssemblyLoaderService.IFactory assemblyLoaderFactory,
|
||||
IStorageService storageService,
|
||||
ILoggerService logger,
|
||||
Lazy<IEventService> eventService,
|
||||
Lazy<ILuaScriptManagementService> luaScriptManagementService,
|
||||
Lazy<IConfigService> configService,
|
||||
Lazy<ILuaPatcher> pluginLuaPatcherService,
|
||||
Func<IConsoleCommandsService> consoleCommandServiceFactory,
|
||||
ILuaCsInfoProvider luaCsInfoProvider)
|
||||
{
|
||||
_assemblyLoaderFactory = assemblyLoaderFactory;
|
||||
_storageService = storageService;
|
||||
_logger = logger;
|
||||
_eventService = eventService;
|
||||
_luaScriptManagementService = luaScriptManagementService;
|
||||
_configService = configService;
|
||||
_pluginLuaPatcherService = pluginLuaPatcherService;
|
||||
_consoleCommandServiceFactory = consoleCommandServiceFactory;
|
||||
_luaCsInfoProvider = luaCsInfoProvider;
|
||||
}
|
||||
|
||||
private ServiceContainer CreatePluginServiceContainer()
|
||||
{
|
||||
var container = new ServiceContainer(new ContainerOptions()
|
||||
{
|
||||
EnablePropertyInjection = true
|
||||
});
|
||||
|
||||
_pluginEventService ??= new EventService(_logger, _pluginLuaPatcherService.Value);
|
||||
_eventService.Value.AddDispatcherEventService(_pluginEventService);
|
||||
|
||||
container.Register<ILoggerService>(fac => _logger);
|
||||
container.Register<IStorageService>(fac => _storageService);
|
||||
container.Register<IEventService>(fac => _pluginEventService);
|
||||
container.Register<IPluginManagementService>(fac => this);
|
||||
container.Register<ILuaScriptManagementService>(fac => _luaScriptManagementService.Value);
|
||||
container.Register<IConfigService>(fac => _configService.Value);
|
||||
container.Register<IConsoleCommandsService>(fac => _consoleCommandServiceFactory?.Invoke());
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
public Result<ImmutableArray<Type>> GetImplementingTypes<T>(bool includeInterfaces = false, bool includeAbstractTypes = false,
|
||||
bool includeDefaultContext = true)
|
||||
{
|
||||
if (includeInterfaces)
|
||||
{
|
||||
includeAbstractTypes = true;
|
||||
}
|
||||
|
||||
using var lck = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
IService.CheckDisposed(this);
|
||||
|
||||
var builder = ImmutableArray.CreateBuilder<Type>();
|
||||
|
||||
if (includeDefaultContext)
|
||||
{
|
||||
foreach (var ass in AssemblyLoadContext.Default.Assemblies)
|
||||
{
|
||||
AddTypesFromAssembly(ass);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var ass in _assemblyLoaders.Values.Where(al => !al.IsReferenceOnlyMode).SelectMany(al => al.Assemblies))
|
||||
{
|
||||
AddTypesFromAssembly(ass);
|
||||
}
|
||||
|
||||
return builder.ToImmutable();
|
||||
|
||||
|
||||
void AddTypesFromAssembly(Assembly assembly)
|
||||
{
|
||||
foreach (var type in assembly.GetSafeTypes())
|
||||
{
|
||||
if ((includeInterfaces || !type.IsInterface)
|
||||
&& (includeAbstractTypes || !type.IsAbstract)
|
||||
&& type.IsAssignableTo(typeof(T)))
|
||||
{
|
||||
builder.Add(type);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool TryGetPackageForPlugin<TPlugin>(out ContentPackage ownerPackage)
|
||||
{
|
||||
return _pluginPackageLookup.TryGetValue(typeof(TPlugin), out ownerPackage);
|
||||
}
|
||||
|
||||
public Type GetType(string typeName, bool isByRefType = false, bool includeInterfaces = false,
|
||||
bool includeDefaultContext = true)
|
||||
{
|
||||
if (typeName.StartsWith("out ") || typeName.StartsWith("ref "))
|
||||
{
|
||||
typeName = typeName.Remove(0, 4);
|
||||
isByRefType = true;
|
||||
}
|
||||
|
||||
if (includeDefaultContext)
|
||||
{
|
||||
var type = Type.GetType(typeName, false, false);
|
||||
if (type is not null && (includeInterfaces || !type.IsInterface))
|
||||
{
|
||||
if (isByRefType)
|
||||
{
|
||||
return type.MakeByRefType();
|
||||
}
|
||||
|
||||
return type;
|
||||
}
|
||||
|
||||
foreach (var ass in AssemblyLoadContext.Default.Assemblies)
|
||||
{
|
||||
if (ass.GetType(typeName, false, false) is not {} type2 || (!includeInterfaces && type2.IsInterface))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
return isByRefType ? type2.MakeByRefType() : type2;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var ass in AssemblyLoadContext.All
|
||||
.Where(alc => alc != AssemblyLoadContext.Default)
|
||||
.SelectMany(alc => alc.Assemblies))
|
||||
{
|
||||
if (ass.GetType(typeName, false, false) is not {} type || (!includeInterfaces && type.IsInterface))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
return isByRefType ? type.MakeByRefType() : type;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public FluentResults.Result ActivatePluginInstances(ImmutableArray<ContentPackage> executionOrder, bool excludeAlreadyRunningPackages = true)
|
||||
{
|
||||
if (executionOrder.IsDefaultOrEmpty)
|
||||
{
|
||||
ThrowHelper.ThrowArgumentNullException($"{nameof(ActivatePluginInstances)}: The ececution list provided is empty.");
|
||||
}
|
||||
using var lck = _operationsLock.AcquireWriterLock().ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
IService.CheckDisposed(this);
|
||||
|
||||
if (_assemblyLoaders.IsEmpty)
|
||||
{
|
||||
return FluentResults.Result.Ok();
|
||||
}
|
||||
|
||||
var results = new FluentResults.Result();
|
||||
|
||||
var toLoad = _assemblyLoaders
|
||||
.Where(al => executionOrder.Contains(al.Key))
|
||||
.Where(al => !excludeAlreadyRunningPackages || !_pluginInstances.ContainsKey(al.Key))
|
||||
.SelectMany(al => al.Value.Assemblies.Select(ass => (al.Key, ass)))
|
||||
.SelectMany<(ContentPackage Key, Assembly ass), (ContentPackage Key, Type type)>(kvp =>
|
||||
{
|
||||
try
|
||||
{
|
||||
return kvp.ass.GetTypes()
|
||||
.Where(type =>
|
||||
type is { IsInterface: false, IsAbstract: false, IsGenericType: false }
|
||||
&& type.IsAssignableTo(typeof(IAssemblyPlugin)))
|
||||
.Select(type => (kvp.Key, type));
|
||||
}
|
||||
catch (ReflectionTypeLoadException re)
|
||||
{
|
||||
results.WithError(new Error($"Failed to get types from Package '{kvp.Key.Name}'"));
|
||||
results.WithError(new ExceptionalError(re));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
results.WithError(new Error($"Failed to get types from Package '{kvp.Key.Name}'"));
|
||||
results.WithError(new ExceptionalError(e));
|
||||
}
|
||||
return new List<(ContentPackage Key, Type type)>();
|
||||
})
|
||||
.GroupBy(kvp => kvp.Key, kvp => kvp.type)
|
||||
.OrderBy(exeGrp => executionOrder.IndexOf(exeGrp.Key))
|
||||
.ToImmutableArray();
|
||||
|
||||
if (toLoad.Length == 0)
|
||||
{
|
||||
return results;
|
||||
}
|
||||
|
||||
_logger.LogMessage($"Activating {nameof(IAssemblyPlugin)} instances");
|
||||
|
||||
var loadedPackagePlugins =
|
||||
ImmutableArray.CreateBuilder<(ContentPackage Package, ImmutableArray<IAssemblyPlugin> Plugins)>();
|
||||
_pluginInjectorContainer ??= CreatePluginServiceContainer();
|
||||
|
||||
foreach (var packageTypes in toLoad)
|
||||
{
|
||||
var loadedTypes = ImmutableArray.CreateBuilder<IAssemblyPlugin>();
|
||||
foreach (var pluginType in packageTypes)
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.LogMessage($"- Instantiating {pluginType.Name}");
|
||||
var plugin = (IAssemblyPlugin)Activator.CreateInstance(pluginType);
|
||||
_pluginInjectorContainer.InjectProperties(plugin);
|
||||
_pluginInjectorContainer.Register(pluginType, fac => plugin);
|
||||
loadedTypes.Add(plugin);
|
||||
_pluginPackageLookup.TryAdd(pluginType, packageTypes.Key);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
results.WithError(new ExceptionalError($"Failed to instantiate mod: {packageTypes.Key.Name}", e));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
loadedPackagePlugins.Add((packageTypes.Key, loadedTypes.ToImmutable()));
|
||||
}
|
||||
|
||||
var packPluginGroups = loadedPackagePlugins.ToImmutable();
|
||||
foreach (var packagePluginGrp in packPluginGroups)
|
||||
{
|
||||
if (_pluginInstances.TryGetValue(packagePluginGrp.Package, out var plugins))
|
||||
{
|
||||
_pluginInstances[packagePluginGrp.Package] = plugins.Concat(packagePluginGrp.Plugins).ToImmutableArray();
|
||||
continue;
|
||||
}
|
||||
|
||||
_pluginInstances[packagePluginGrp.Package] = packagePluginGrp.Plugins;
|
||||
}
|
||||
|
||||
var pluginsToInit = packPluginGroups.SelectMany(ppg => ppg.Plugins).ToImmutableArray();
|
||||
|
||||
foreach (var plugin in pluginsToInit)
|
||||
{
|
||||
results.WithReasons(PluginInitRunner(plugin, p => p.PreInitPatching()).Reasons);
|
||||
}
|
||||
|
||||
_eventService.Value.PublishEvent<IEventPluginPreInitialize>(sub => sub.PreInitPatching());
|
||||
|
||||
foreach (var plugin in pluginsToInit)
|
||||
{
|
||||
results.WithReasons(PluginInitRunner(plugin, p => p.Initialize()).Reasons);
|
||||
}
|
||||
|
||||
_eventService.Value.PublishEvent<IEventPluginInitialize>(sub => sub.Initialize());
|
||||
|
||||
foreach (var plugin in pluginsToInit)
|
||||
{
|
||||
results.WithReasons(PluginInitRunner(plugin, p => p.OnLoadCompleted()).Reasons);
|
||||
}
|
||||
|
||||
_eventService.Value.PublishEvent<IEventPluginLoadCompleted>(sub => sub.OnLoadCompleted());
|
||||
|
||||
return results;
|
||||
|
||||
// helper
|
||||
FluentResults.Result PluginInitRunner(IAssemblyPlugin plugin, Action<IAssemblyPlugin> action)
|
||||
{
|
||||
try
|
||||
{
|
||||
action(plugin);
|
||||
return FluentResults.Result.Ok();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return FluentResults.Result.Fail(new ExceptionalError(e));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public FluentResults.Result LoadAssemblyResources(ImmutableArray<IAssemblyResourceInfo> resources)
|
||||
{
|
||||
if (resources.IsDefaultOrEmpty)
|
||||
{
|
||||
ThrowHelper.ThrowArgumentNullException($"{nameof(LoadAssemblyResources)} The resource list is empty.)");
|
||||
}
|
||||
using var lck = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
IService.CheckDisposed(this);
|
||||
|
||||
_storageService.UseCaching = _luaCsInfoProvider.UseCaching;
|
||||
if (!_luaCsInfoProvider.UseCaching)
|
||||
{
|
||||
_storageService.PurgeCache();
|
||||
}
|
||||
|
||||
var orderedContentPacks = resources.GroupBy(res => res.OwnerPackage)
|
||||
.OrderBy(res => resources.FindIndex(r2 => r2.OwnerPackage == res.Key))
|
||||
.ToImmutableArray();
|
||||
|
||||
var result = new FluentResults.Result();
|
||||
|
||||
foreach (var contentPack in orderedContentPacks)
|
||||
{
|
||||
LoadBinaries(contentPack);
|
||||
LoadAndCompileScriptAssemblies(contentPack);
|
||||
foreach (var ass in _assemblyLoaders[contentPack.Key].Assemblies)
|
||||
{
|
||||
ReflectionUtils.AddNonAbstractAssemblyTypes(ass);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
// --- helper methods
|
||||
void LoadBinaries(IGrouping<ContentPackage,IAssemblyResourceInfo> contentPackRes)
|
||||
{
|
||||
var binaries = contentPackRes.Where(cRes => !cRes.IsScript)
|
||||
.OrderBy(bin => bin.LoadPriority)
|
||||
.SelectMany(bin => bin.FilePaths)
|
||||
.ToImmutableArray();
|
||||
|
||||
if (binaries.IsDefaultOrEmpty)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var assemblyLoader = _assemblyLoaders.GetOrAdd(contentPackRes.Key, (cp) => _assemblyLoaderFactory.CreateInstance(
|
||||
new IAssemblyLoaderService.LoaderInitData(
|
||||
InstanceId: Guid.NewGuid(),
|
||||
contentPackRes.Key.Name,
|
||||
IsReferenceMode: contentPackRes.Any(r => r.IsReferenceModeOnly),
|
||||
OwnerPackage: contentPackRes.Key,
|
||||
OnUnload: OnAssemblyLoaderUnloading,
|
||||
OnResolvingManaged: OnAssemblyLoaderResolvingManaged,
|
||||
OnResolvingUnmanagedDll: OnAssemblyLoaderResolvingUnmanaged
|
||||
)));
|
||||
|
||||
var dependencyPaths = binaries
|
||||
.Select(bin => System.IO.Path.GetDirectoryName(bin.FullPath))
|
||||
.Distinct()
|
||||
.ToImmutableArray();
|
||||
|
||||
foreach (var binResource in binaries)
|
||||
{
|
||||
var res = assemblyLoader.LoadAssemblyFromFile(binResource.FullPath, dependencyPaths);
|
||||
result.WithReasons(res.Reasons);
|
||||
_logger.LogResults(res.ToResult());
|
||||
}
|
||||
}
|
||||
|
||||
void LoadAndCompileScriptAssemblies(IGrouping<ContentPackage, IAssemblyResourceInfo> contentPackRes)
|
||||
{
|
||||
var scriptsGrp = contentPackRes.Where(cRes => cRes.IsScript)
|
||||
.Select(scr => (scr.OwnerPackage, scr.FriendlyName, scr.FilePaths, scr.UseInternalAccessName, scr.LoadPriority))
|
||||
.OrderBy(scr => scr.LoadPriority)
|
||||
.GroupBy(scr => scr.FriendlyName)
|
||||
.ToImmutableArray();
|
||||
|
||||
if (scriptsGrp.IsDefaultOrEmpty)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var metadataReferences = GetMetadataReferences(false).ToImmutableArray();
|
||||
var metadataReferencesNonPublicized = GetMetadataReferences(true).ToImmutableArray();
|
||||
|
||||
var assemblyLoader = _assemblyLoaders.GetOrAdd(contentPackRes.Key, (cp) => _assemblyLoaderFactory.CreateInstance(
|
||||
new IAssemblyLoaderService.LoaderInitData(
|
||||
InstanceId: Guid.NewGuid(),
|
||||
contentPackRes.Key.Name,
|
||||
IsReferenceMode: contentPackRes.Any(r => r.IsReferenceModeOnly),
|
||||
OwnerPackage: contentPackRes.Key,
|
||||
OnUnload: OnAssemblyLoaderUnloading,
|
||||
OnResolvingManaged: OnAssemblyLoaderResolvingManaged,
|
||||
OnResolvingUnmanagedDll: OnAssemblyLoaderResolvingUnmanaged
|
||||
)));
|
||||
|
||||
// create syntax trees
|
||||
|
||||
foreach (var scripts in scriptsGrp)
|
||||
{
|
||||
var syntaxTreesBuilder = ImmutableArray.CreateBuilder<SyntaxTree>();
|
||||
|
||||
bool hasInternalsAwareBeenAdded = false;
|
||||
bool compileWithInternalName = true;
|
||||
|
||||
foreach (var resourceInfo in scripts)
|
||||
{
|
||||
// this should be the same for the entire collection of src files so we just grab it from the collection
|
||||
compileWithInternalName = resourceInfo.UseInternalAccessName;
|
||||
|
||||
if (!hasInternalsAwareBeenAdded)
|
||||
{
|
||||
hasInternalsAwareBeenAdded = true;
|
||||
syntaxTreesBuilder.Add(BaseAssemblyImports);
|
||||
}
|
||||
|
||||
if (resourceInfo.FilePaths.IsDefaultOrEmpty)
|
||||
{
|
||||
ThrowHelper.ThrowArgumentNullException($"{nameof(LoadAndCompileScriptAssemblies)} The resource list is empty for package {resourceInfo.OwnerPackage}.");
|
||||
}
|
||||
|
||||
foreach (var resourcePath in resourceInfo.FilePaths)
|
||||
{
|
||||
var loadRes = GetSourceFilesText(resourcePath);
|
||||
if (loadRes.IsFailed)
|
||||
{
|
||||
_logger.LogResults(loadRes.ToResult());
|
||||
continue;
|
||||
}
|
||||
|
||||
CancellationToken token = CancellationToken.None;
|
||||
|
||||
string sourceCode = loadRes.Value;
|
||||
sourceCode = DoSourceCodeTextCompatibilityPass(sourceCode);
|
||||
|
||||
syntaxTreesBuilder.Add(SyntaxFactory.ParseSyntaxTree(
|
||||
text: sourceCode,
|
||||
options: ScriptParseOptions,
|
||||
path: resourcePath.FullPath,
|
||||
encoding: Encoding.Default,
|
||||
cancellationToken: token
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if (syntaxTreesBuilder.Count < 1)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
_logger.LogMessage($"Compiling assembly for {scripts.Key}, in ContentPackage {contentPackRes.Key.Name}");
|
||||
|
||||
var res = assemblyLoader.CompileScriptAssembly(
|
||||
assemblyName: scripts.Key,
|
||||
compileWithInternalAccess: compileWithInternalName,
|
||||
syntaxTrees: syntaxTreesBuilder.ToImmutable(),
|
||||
metadataReferences: compileWithInternalName ? metadataReferencesNonPublicized : metadataReferences,
|
||||
compilationOptions: CompilationOptions);
|
||||
|
||||
// try with internal access instead for legacy mods
|
||||
if (!compileWithInternalName && res.IsFailed)
|
||||
{
|
||||
_logger.LogMessage($"Attempted compilation of {scripts.Key} for package {contentPackRes.Key.Name}. Trying fallback method.");
|
||||
var res2 = assemblyLoader.CompileScriptAssembly(
|
||||
assemblyName: scripts.Key,
|
||||
compileWithInternalAccess: true,
|
||||
syntaxTrees: syntaxTreesBuilder.ToImmutable(),
|
||||
metadataReferences: metadataReferencesNonPublicized,
|
||||
compilationOptions: CompilationOptions);
|
||||
|
||||
// overwrite result with good compilation
|
||||
if (res2.IsSuccess)
|
||||
{
|
||||
var reasonsStr = res.Reasons.Aggregate("", (accum, reason) => accum + "\n" + reason.Message);
|
||||
_logger.LogWarning($"Attempted compilation of {scripts.Key} for package {contentPackRes.Key.Name} succeeded. Original errors were: \n {reasonsStr}");
|
||||
res = res2;
|
||||
}
|
||||
}
|
||||
|
||||
result.WithReasons(res.Reasons);
|
||||
}
|
||||
}
|
||||
|
||||
Result<string> GetSourceFilesText(ContentPath resourceInfoFilePath)
|
||||
{
|
||||
if (_storageService.LoadPackageText(resourceInfoFilePath) is not { IsFailed: false } res)
|
||||
{
|
||||
_logger.LogError($"{nameof(GetSourceFilesText)}: Failed to load source file for ContentPackage {resourceInfoFilePath.ContentPackage?.Name}.");
|
||||
return FluentResults.Result.Fail($"{nameof(GetSourceFilesText)}: Failed to load source files for ContentPackage {resourceInfoFilePath.ContentPackage?.Name}.");
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
IEnumerable<MetadataReference> GetMetadataReferences(bool useNonPublicizedAssemblies)
|
||||
{
|
||||
var builder = ImmutableArray.CreateBuilder<MetadataReference>();
|
||||
if (useNonPublicizedAssemblies)
|
||||
{
|
||||
builder.AddRange(BaseMetadataReferencesWithBarotrauma);
|
||||
foreach (var loaderService in _assemblyLoaders
|
||||
.Where(asl => !asl.Key.Name.Equals("LuaCsForBarotrauma", StringComparison.InvariantCultureIgnoreCase))
|
||||
.ToImmutableArray())
|
||||
{
|
||||
builder.AddRange(loaderService.Value.AssemblyReferences.Where(ar => ar is not null));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
builder.AddRange(BaseMetadataReferences);
|
||||
foreach (var loaderService in _assemblyLoaders)
|
||||
{
|
||||
builder.AddRange(loaderService.Value.AssemblyReferences.Where(ar => ar is not null));
|
||||
}
|
||||
}
|
||||
|
||||
return builder.ToImmutable();
|
||||
}
|
||||
}
|
||||
|
||||
private string DoSourceCodeTextCompatibilityPass(string sourceCode)
|
||||
{
|
||||
return sourceCode
|
||||
.Replace("GameMain.LuaCs", "LuaCsSetup.Instance")
|
||||
.Replace(" Client.ClientList", " ModUtils.Client.ClientList")
|
||||
.Replace(" Barotrauma.Networking.Client.ClientList", " ModUtils.Client.ClientList")
|
||||
.Replace("ItemPrefab.GetItemPrefab", "ModUtils.ItemPrefab.GetItemPrefab");
|
||||
}
|
||||
|
||||
private IntPtr OnAssemblyLoaderResolvingUnmanaged(Assembly callerAssembly, string targetAssemblyName)
|
||||
{
|
||||
Guard.IsNull(callerAssembly, nameof(callerAssembly));
|
||||
Guard.IsNullOrWhiteSpace(targetAssemblyName, nameof(targetAssemblyName));
|
||||
|
||||
if (AssemblyLoadContext.GetLoadContext(callerAssembly) is not IAssemblyLoaderService loaderService)
|
||||
{
|
||||
return IntPtr.Zero;
|
||||
}
|
||||
|
||||
var targetDirectory = Path.GetFullPath(loaderService.OwnerPackage.Dir);
|
||||
if (!targetAssemblyName.TrimEnd().EndsWith(".dll"))
|
||||
{
|
||||
targetAssemblyName += ".dll";
|
||||
}
|
||||
|
||||
var res = _storageService.FindFilesInPackage(loaderService.OwnerPackage, string.Empty, targetAssemblyName, true);
|
||||
|
||||
if (res.IsFailed || !res.Value.Any())
|
||||
{
|
||||
return IntPtr.Zero;
|
||||
}
|
||||
|
||||
foreach (var path in res.Value)
|
||||
{
|
||||
if (System.Runtime.InteropServices.NativeLibrary.TryLoad(path, out IntPtr asmPtr))
|
||||
{
|
||||
_loadedNativeLibraries.Add(asmPtr);
|
||||
return asmPtr;
|
||||
}
|
||||
}
|
||||
|
||||
return IntPtr.Zero;
|
||||
}
|
||||
|
||||
private Assembly OnAssemblyLoaderResolvingManaged(IAssemblyLoaderService requestingLoader, AssemblyName searchName)
|
||||
{
|
||||
// This method is used during assembly instantiation, we cannot put a lock here.
|
||||
//using var lck = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
IService.CheckDisposed(this);
|
||||
|
||||
foreach (var loader in _assemblyLoaders.Where(kvp => kvp.Value != requestingLoader)
|
||||
.Select(kvp => kvp.Value).ToImmutableArray())
|
||||
{
|
||||
if (loader.IsReferenceOnlyMode || !loader.Assemblies.Any())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (var assembly in loader.Assemblies)
|
||||
{
|
||||
if (assembly.GetName().FullName == searchName.FullName)
|
||||
{
|
||||
return assembly;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private void OnAssemblyLoaderUnloading(IAssemblyLoaderService loader)
|
||||
{
|
||||
if (!loader.Assemblies.Any())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var assembly in loader.Assemblies)
|
||||
{
|
||||
_eventService?.Value?.PublishEvent<IEventAssemblyUnloading>(sub => sub.OnAssemblyUnloading(assembly));
|
||||
}
|
||||
}
|
||||
|
||||
public FluentResults.Result UnloadManagedAssemblies()
|
||||
{
|
||||
using var lck = _operationsLock.AcquireWriterLock().ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
IService.CheckDisposed(this);
|
||||
|
||||
if (_assemblyLoaders.Count == 0)
|
||||
{
|
||||
return FluentResults.Result.Ok();
|
||||
}
|
||||
|
||||
var results = new FluentResults.Result();
|
||||
|
||||
results.WithReasons(UnsafeDisposeManagedTypeInstances().Reasons);
|
||||
|
||||
ReflectionUtils.ResetCache();
|
||||
foreach (var loaderService in _assemblyLoaders)
|
||||
{
|
||||
try
|
||||
{
|
||||
loaderService.Value.Dispose();
|
||||
_unloadingAssemblyLoaders.Add(loaderService.Value, loaderService.Key);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
results.WithError(new ExceptionalError(e));
|
||||
}
|
||||
}
|
||||
|
||||
_assemblyLoaders.Clear();
|
||||
_storageService.PurgeCache();
|
||||
GC.Collect(GC.MaxGeneration, GCCollectionMode.Aggressive, true);
|
||||
|
||||
#if DEBUG
|
||||
// Print still loaded assembly load ctx after giving some time
|
||||
CoroutineManager.Invoke(() =>
|
||||
{
|
||||
if (!_unloadingAssemblyLoaders.Any())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
sb.AppendLine("The following ContentPackages have not unloaded their assemblies:");
|
||||
|
||||
foreach (var kvp in _unloadingAssemblyLoaders.ToImmutableArray())
|
||||
{
|
||||
sb.AppendLine($"- '{kvp.Value.Name}'");
|
||||
}
|
||||
|
||||
|
||||
// Use DebugConsole in case logger is null by the time this executes.
|
||||
if (_logger is null)
|
||||
{
|
||||
DebugConsole.LogError(sb.ToString());
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning(sb.ToString());
|
||||
}
|
||||
}, 3.0f);
|
||||
#endif
|
||||
|
||||
// clear native libraries
|
||||
if (_loadedNativeLibraries.Any())
|
||||
{
|
||||
foreach (var ptr in _loadedNativeLibraries)
|
||||
{
|
||||
try
|
||||
{
|
||||
System.Runtime.InteropServices.NativeLibrary.Free(ptr);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignored
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
_loadedNativeLibraries.Clear();
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
private FluentResults.Result UnsafeDisposeManagedTypeInstances()
|
||||
{
|
||||
var results = new FluentResults.Result();
|
||||
|
||||
if (!_pluginInstances.IsEmpty)
|
||||
{
|
||||
foreach (var instance in _pluginInstances.SelectMany(kvp => kvp.Value))
|
||||
{
|
||||
try
|
||||
{
|
||||
instance.Dispose();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
results.WithError(new ExceptionalError(e));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (_pluginEventService is not null)
|
||||
{
|
||||
_eventService.Value.RemoveDispatcherEventService(_pluginEventService);
|
||||
_pluginEventService = null;
|
||||
}
|
||||
_pluginInjectorContainer = null;
|
||||
|
||||
_pluginInstances.Clear();
|
||||
_pluginPackageLookup.Clear();
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
public Result<Assembly> GetLoadedAssembly(OneOf<AssemblyName, string> assemblyName, in Guid[] excludedContexts)
|
||||
{
|
||||
using var _ = _operationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
IService.CheckDisposed(this);
|
||||
|
||||
var guids = excludedContexts;
|
||||
return assemblyName.Match<Assembly>((AssemblyName asm) =>
|
||||
{
|
||||
foreach (var ass in _assemblyLoaders.Values
|
||||
.Where(al => guids.Length == 0 || !guids.Contains(al.Id))
|
||||
.SelectMany(al => al.Assemblies)
|
||||
.ToImmutableArray())
|
||||
{
|
||||
if (ass.GetName() == asm)
|
||||
{
|
||||
return ass;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
(string asmName) =>
|
||||
{
|
||||
foreach (var ass in _assemblyLoaders.Values.SelectMany(al => al.Assemblies))
|
||||
{
|
||||
if (ass.GetName().Name?.Equals(asmName) ?? ass.GetName().FullName.Equals(asmName))
|
||||
{
|
||||
return ass;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace Barotrauma.LuaCs;
|
||||
|
||||
public class PluginService
|
||||
{
|
||||
|
||||
}
|
||||
@@ -0,0 +1,230 @@
|
||||
using Barotrauma.IO;
|
||||
using Barotrauma.LuaCs.Data;
|
||||
using Barotrauma.Networking;
|
||||
using FarseerPhysics.Common;
|
||||
using FluentResults;
|
||||
using FluentResults.LuaCs;
|
||||
using Microsoft.Toolkit.Diagnostics;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Immutable;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Xml.Linq;
|
||||
using Path = System.IO.Path;
|
||||
|
||||
namespace Barotrauma.LuaCs;
|
||||
|
||||
public class SafeStorageService : StorageService, ISafeStorageService
|
||||
{
|
||||
private ConcurrentDictionary<string, byte>
|
||||
_fileListRead = new (),
|
||||
_fileListWrite = new();
|
||||
private readonly AsyncReaderWriterLock _higherOperationsLock = new();
|
||||
|
||||
public SafeStorageService(IStorageServiceConfig configData) : base(configData)
|
||||
{
|
||||
IsReadOperationAllowedEval = (fp) => IsFileAccessible(fp, true, true);
|
||||
IsWriteOperationAllowedEval = (fp) => IsFileAccessible(fp, false, true);
|
||||
}
|
||||
|
||||
private string GetFullPath(string path) => System.IO.Path.GetFullPath(path).CleanUpPathCrossPlatform();
|
||||
|
||||
public bool IsFileAccessible(string path, bool readOnly, bool checkWhitelistOnly = true)
|
||||
{
|
||||
Guard.IsNotNullOrWhiteSpace(path, nameof(path));
|
||||
using var lck = _higherOperationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
IService.CheckDisposed(this);
|
||||
|
||||
try
|
||||
{
|
||||
path = GetFullPath(path);
|
||||
|
||||
if (path.StartsWith(ConfigData.WorkshopModsDirectory)
|
||||
|| path.StartsWith(ConfigData.LocalModsDirectory)
|
||||
#if CLIENT
|
||||
|| path.StartsWith(ConfigData.TempDownloadsDirectory)
|
||||
#endif
|
||||
)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!_fileListRead.ContainsKey(path))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (!readOnly && !_fileListWrite.ContainsKey(path))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (checkWhitelistOnly)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
using var fs = System.IO.File.Open(
|
||||
path, FileMode.Open, readOnly ? FileAccess.Read : FileAccess.ReadWrite, FileShare.ReadWrite);
|
||||
return readOnly ? fs.CanRead : fs.CanWrite;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public void AddFileToWhitelist(string path, bool readOnly = true)
|
||||
{
|
||||
Guard.IsNotNullOrWhiteSpace(path, nameof(path));
|
||||
using var lck = _higherOperationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
IService.CheckDisposed(this);
|
||||
|
||||
try
|
||||
{
|
||||
path = GetFullPath(path);
|
||||
_fileListRead.AddOrUpdate(path, s => 0, (s, b) => 0);
|
||||
if (!readOnly)
|
||||
{
|
||||
_fileListWrite.AddOrUpdate(path, s => 0, (s, b) => 0);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
public void AddFilesToWhitelist(ImmutableArray<string> paths, bool readOnly = true)
|
||||
{
|
||||
if (paths.IsDefaultOrEmpty)
|
||||
ThrowHelper.ThrowArgumentNullException(nameof(paths));
|
||||
foreach (var path in paths)
|
||||
{
|
||||
AddFileToWhitelist(path, readOnly);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void RemoveFileFromAllWhitelists(string path)
|
||||
{
|
||||
Guard.IsNotNullOrWhiteSpace(path, nameof(path));
|
||||
using var lck = _higherOperationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
IService.CheckDisposed(this);
|
||||
|
||||
try
|
||||
{
|
||||
path = GetFullPath(path);
|
||||
_fileListRead.TryRemove(path, out _);
|
||||
_fileListWrite.TryRemove(path, out _);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
public FluentResults.Result SetReadOnlyWhitelist(ImmutableArray<string> filePaths)
|
||||
{
|
||||
using var lck = _higherOperationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
IService.CheckDisposed(this);
|
||||
if (filePaths.IsDefaultOrEmpty)
|
||||
{
|
||||
return FluentResults.Result.Fail($"{nameof(SetReadOnlyWhitelist)}: FilePaths cannot be empty.");
|
||||
}
|
||||
|
||||
_fileListRead.Clear();
|
||||
var res = new FluentResults.Result();
|
||||
foreach (var path in filePaths)
|
||||
{
|
||||
Guard.IsNotNullOrWhiteSpace(path, nameof(path));
|
||||
try
|
||||
{
|
||||
var p = Path.GetFullPath(path.CleanUpPathCrossPlatform());
|
||||
if (_fileListRead.ContainsKey(p))
|
||||
{
|
||||
res = res.WithReason(new Success($"Path already in whitelist: {p}"));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (_fileListRead.TryAdd(p, 0))
|
||||
{
|
||||
res = res.WithSuccess($"Added path successfully: {p}");
|
||||
continue;
|
||||
}
|
||||
|
||||
res = res.WithError(new Error($"Failed to add path to list: {p}"));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
res = res.WithError(new ExceptionalError(e)
|
||||
.WithMetadata(MetadataType.ExceptionObject, this)
|
||||
.WithMetadata(MetadataType.ExceptionDetails, e.Message)
|
||||
.WithMetadata(MetadataType.RootObject, path)
|
||||
);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
public FluentResults.Result SetReadWriteWhitelist(ImmutableArray<string> filePaths)
|
||||
{
|
||||
if (filePaths.IsDefaultOrEmpty)
|
||||
{
|
||||
return FluentResults.Result.Fail($"{nameof(SetReadOnlyWhitelist)}: FilePaths cannot be empty.");
|
||||
}
|
||||
using var lck = _higherOperationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
IService.CheckDisposed(this);
|
||||
|
||||
_fileListRead.Clear();
|
||||
_fileListWrite.Clear();
|
||||
var res = new FluentResults.Result();
|
||||
foreach (var path in filePaths)
|
||||
{
|
||||
Guard.IsNotNullOrWhiteSpace(path, nameof(path));
|
||||
try
|
||||
{
|
||||
var p = Path.GetFullPath(path.CleanUpPathCrossPlatform());
|
||||
TryAddToList(_fileListRead, p);
|
||||
TryAddToList(_fileListWrite, p);
|
||||
res = res.WithError(new Error($"Failed to add path to list: {p}"));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
res = res.WithError(new ExceptionalError(e)
|
||||
.WithMetadata(MetadataType.ExceptionObject, this)
|
||||
.WithMetadata(MetadataType.ExceptionDetails, e.Message)
|
||||
.WithMetadata(MetadataType.RootObject, path)
|
||||
);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
void TryAddToList(ConcurrentDictionary<string, byte> dict, string p)
|
||||
{
|
||||
if (dict.ContainsKey(p))
|
||||
{
|
||||
res = res.WithReason(new Success($"Path already in whitelist: {p}"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (dict.TryAdd(p, 0))
|
||||
{
|
||||
res = res.WithSuccess($"Added path successfully: {p}");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
public void ClearAllWhitelists()
|
||||
{
|
||||
using var lck = _higherOperationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
IService.CheckDisposed(this);
|
||||
_fileListRead.Clear();
|
||||
_fileListWrite.Clear();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,295 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading;
|
||||
using LightInject;
|
||||
using Microsoft.Toolkit.Diagnostics;
|
||||
|
||||
namespace Barotrauma.LuaCs;
|
||||
|
||||
|
||||
public class ServicesProvider : IServicesProvider
|
||||
{
|
||||
private ServiceContainer _serviceContainerInst;
|
||||
private ServiceContainer ServiceContainer => _serviceContainerInst;
|
||||
|
||||
/// <summary>
|
||||
/// Definition: [Key: ConcreteType, Value: TypeInstance]
|
||||
/// </summary>
|
||||
private ImmutableArray<ISystem> _systemInstances = ImmutableArray<ISystem>.Empty;
|
||||
private readonly ReaderWriterLockSlim _serviceLock = new();
|
||||
|
||||
public ServicesProvider()
|
||||
{
|
||||
_serviceContainerInst = new ServiceContainer(new ContainerOptions()
|
||||
{
|
||||
EnablePropertyInjection = false
|
||||
});
|
||||
|
||||
//_serviceContainerInst.Register<IServicesProvider>((f) => this);
|
||||
}
|
||||
|
||||
public void RegisterServiceType<TSvcInterface, TService>(ServiceLifetime lifetime, ILifetime lifetimeInstance = null) where TSvcInterface : class, IService where TService : class, IService, TSvcInterface
|
||||
{
|
||||
// ISystem services must run as a lifetime singleton
|
||||
if (typeof(TSvcInterface).IsAssignableTo(typeof(ISystem)))
|
||||
{
|
||||
lifetimeInstance = new PerContainerLifetime();
|
||||
}
|
||||
|
||||
if (lifetimeInstance is null)
|
||||
{
|
||||
switch (lifetime)
|
||||
{
|
||||
case ServiceLifetime.Singleton:
|
||||
lifetimeInstance = new PerContainerLifetime();
|
||||
break;
|
||||
case ServiceLifetime.PerThread:
|
||||
lifetimeInstance = new PerThreadLifetime();
|
||||
break;
|
||||
// treat these as transient
|
||||
case ServiceLifetime.Transient:
|
||||
case ServiceLifetime.Invalid:
|
||||
case ServiceLifetime.Custom:
|
||||
default:
|
||||
lifetimeInstance = null;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_serviceLock.EnterReadLock();
|
||||
if (lifetimeInstance is not null)
|
||||
ServiceContainer.Register<TSvcInterface, TService>(lifetimeInstance);
|
||||
else
|
||||
ServiceContainer.Register<TSvcInterface, TService>();
|
||||
}
|
||||
finally
|
||||
{
|
||||
_serviceLock.ExitReadLock();
|
||||
}
|
||||
}
|
||||
|
||||
public void RegisterServiceType<TSvcInterface, TService>(string name, ServiceLifetime lifetime,
|
||||
ILifetime lifetimeInstance = null) where TSvcInterface : class, IService where TService : class, IService, TSvcInterface
|
||||
{
|
||||
if (name.IsNullOrWhiteSpace())
|
||||
{
|
||||
throw new ArgumentNullException($"Tried to register a service of type {typeof(TService).Name} but the name provided is null or empty." );
|
||||
}
|
||||
|
||||
// ISystem services must run as a lifetime singleton
|
||||
if (typeof(TService).IsAssignableTo(typeof(ISystem)))
|
||||
{
|
||||
lifetimeInstance = new PerContainerLifetime();
|
||||
}
|
||||
|
||||
if (lifetimeInstance is null)
|
||||
{
|
||||
switch (lifetime)
|
||||
{
|
||||
case ServiceLifetime.Singleton:
|
||||
lifetimeInstance = new PerContainerLifetime();
|
||||
break;
|
||||
case ServiceLifetime.PerThread:
|
||||
lifetimeInstance = new PerThreadLifetime();
|
||||
break;
|
||||
// treat these as transient
|
||||
case ServiceLifetime.Transient:
|
||||
case ServiceLifetime.Invalid:
|
||||
case ServiceLifetime.Custom: // lifetime should not be null here
|
||||
default:
|
||||
lifetimeInstance = new PerRequestLifeTime();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_serviceLock.EnterReadLock();
|
||||
ServiceContainer.Register<TSvcInterface, TService>(name, lifetimeInstance);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_serviceLock.ExitReadLock();
|
||||
}
|
||||
}
|
||||
|
||||
public void RegisterServiceResolver<TSvcInterface>(Func<ServiceContainer, TSvcInterface> factory) where TSvcInterface : class, IService
|
||||
{
|
||||
try
|
||||
{
|
||||
_serviceLock.EnterReadLock();
|
||||
ServiceContainer.Register<TSvcInterface>(f => factory(ServiceContainer));
|
||||
}
|
||||
finally
|
||||
{
|
||||
_serviceLock.ExitReadLock();
|
||||
}
|
||||
}
|
||||
|
||||
public void CompileAndRun()
|
||||
{
|
||||
try
|
||||
{
|
||||
_serviceLock.EnterWriteLock();
|
||||
ServiceContainer!.Compile();
|
||||
if (!_systemInstances.IsDefaultOrEmpty)
|
||||
{
|
||||
ThrowHelper.ThrowInvalidOperationException($"Systems are already instanced!");
|
||||
}
|
||||
|
||||
_systemInstances = ServiceContainer.GetAllInstances(typeof(ISystem))
|
||||
.Select(obj => (ISystem)obj)
|
||||
.ToImmutableArray();
|
||||
}
|
||||
finally
|
||||
{
|
||||
_serviceLock.ExitWriteLock();
|
||||
}
|
||||
}
|
||||
|
||||
public void InjectServices<T>(T inst) where T : class
|
||||
{
|
||||
try
|
||||
{
|
||||
_serviceLock.EnterReadLock();
|
||||
ServiceContainer.InjectProperties(inst);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_serviceLock.ExitReadLock();
|
||||
}
|
||||
}
|
||||
|
||||
public bool TryGetService<TSvcInterface>(out TSvcInterface service) where TSvcInterface : class, IService
|
||||
{
|
||||
try
|
||||
{
|
||||
_serviceLock.EnterReadLock();
|
||||
service = ServiceContainer.TryGetInstance<TSvcInterface>();
|
||||
return service is not null;
|
||||
}
|
||||
catch
|
||||
{
|
||||
service = null;
|
||||
return false;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_serviceLock.ExitReadLock();
|
||||
}
|
||||
}
|
||||
|
||||
public TSvcInterface GetService<TSvcInterface>() where TSvcInterface : class, IService
|
||||
{
|
||||
try
|
||||
{
|
||||
_serviceLock.EnterReadLock();
|
||||
return ServiceContainer.GetInstance<TSvcInterface>();
|
||||
}
|
||||
finally
|
||||
{
|
||||
_serviceLock.ExitReadLock();
|
||||
}
|
||||
}
|
||||
|
||||
public bool TryGetService<TSvcInterface>(string name, out TSvcInterface service) where TSvcInterface : class, IService
|
||||
{
|
||||
try
|
||||
{
|
||||
_serviceLock.EnterReadLock();
|
||||
service = ServiceContainer.TryGetInstance<TSvcInterface>(name);
|
||||
return service is not null;
|
||||
}
|
||||
catch
|
||||
{
|
||||
service = null;
|
||||
return false;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_serviceLock.ExitReadLock();
|
||||
}
|
||||
}
|
||||
|
||||
public event Action<Type, IService> OnServiceInstanced;
|
||||
|
||||
public ImmutableArray<TSvc> GetAllServices<TSvc>() where TSvc : class, IService
|
||||
{
|
||||
try
|
||||
{
|
||||
_serviceLock.EnterReadLock();
|
||||
return ServiceContainer.GetAllInstances<TSvc>().ToImmutableArray();
|
||||
}
|
||||
finally
|
||||
{
|
||||
_serviceLock.ExitReadLock();
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.PreserveSig | MethodImplOptions.Synchronized)]
|
||||
public void DisposeAndReset()
|
||||
{
|
||||
// Plugins should never be allowed to execute this.
|
||||
if (Assembly.GetCallingAssembly() != Assembly.GetExecutingAssembly())
|
||||
{
|
||||
throw new MethodAccessException(
|
||||
$"Assembly {Assembly.GetCallingAssembly().FullName} attempted to call {nameof(DisposeAndReset)}().");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_serviceLock.EnterWriteLock();
|
||||
foreach (var system in _systemInstances)
|
||||
{
|
||||
try
|
||||
{
|
||||
system.Dispose();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
// ignored, no logging services available.
|
||||
}
|
||||
}
|
||||
_systemInstances = ImmutableArray<ISystem>.Empty;
|
||||
_serviceContainerInst?.Dispose();
|
||||
_serviceContainerInst = new ServiceContainer();
|
||||
}
|
||||
finally
|
||||
{
|
||||
_serviceLock.ExitWriteLock();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class PerThreadLifetime : ILifetime
|
||||
{
|
||||
private readonly ThreadLocal<object> _instance = new();
|
||||
|
||||
public object GetInstance(Func<object> createInstance, Scope scope)
|
||||
{
|
||||
if (_instance.Value is null)
|
||||
{
|
||||
var inst = createInstance.Invoke();
|
||||
// IDisposable dispatch
|
||||
if (inst is IDisposable disposable)
|
||||
{
|
||||
if (scope is null)
|
||||
{
|
||||
throw new InvalidOperationException("Attempt disposable object without a valid scope.");
|
||||
}
|
||||
scope.TrackInstance(disposable);
|
||||
}
|
||||
|
||||
_instance.Value = inst;
|
||||
}
|
||||
|
||||
return _instance.Value;
|
||||
}
|
||||
}
|
||||
+212
@@ -0,0 +1,212 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Xml;
|
||||
using System.Xml.Linq;
|
||||
using Barotrauma.LuaCs.Data;
|
||||
using FluentResults;
|
||||
using Microsoft.Toolkit.Diagnostics;
|
||||
using OneOf;
|
||||
|
||||
namespace Barotrauma.LuaCs;
|
||||
|
||||
public sealed class SettingsFileParserService :
|
||||
IParserServiceOneToManyAsync<IConfigResourceInfo, IConfigInfo>,
|
||||
IParserServiceOneToManyAsync<IConfigResourceInfo, IConfigProfileInfo>
|
||||
{
|
||||
#region DisposalControl
|
||||
|
||||
private AsyncReaderWriterLock _operationLock = new();
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
using var lck = _operationLock.AcquireWriterLock().ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
if (!ModUtils.Threading.CheckIfClearAndSetBool(ref _isDisposed))
|
||||
{
|
||||
return;
|
||||
}
|
||||
_storageService.Dispose();
|
||||
_storageService = null;
|
||||
}
|
||||
|
||||
private int _isDisposed = 0;
|
||||
public bool IsDisposed
|
||||
{
|
||||
get => ModUtils.Threading.GetBool(ref _isDisposed);
|
||||
private set => ModUtils.Threading.SetBool(ref _isDisposed, value);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private IStorageService _storageService;
|
||||
|
||||
public SettingsFileParserService(IStorageService storageService)
|
||||
{
|
||||
_storageService = storageService;
|
||||
}
|
||||
|
||||
async Task<Result<ImmutableArray<IConfigInfo>>> IParserServiceOneToManyAsync<IConfigResourceInfo, IConfigInfo>
|
||||
.TryParseResourcesAsync(IConfigResourceInfo src)
|
||||
{
|
||||
Guard.IsNotNull(src, nameof(src));
|
||||
Guard.IsNotNull(src.OwnerPackage, nameof(src.OwnerPackage));
|
||||
using var lck = await _operationLock.AcquireReaderLock();
|
||||
IService.CheckDisposed(this);
|
||||
|
||||
if (src.FilePaths.IsDefaultOrEmpty)
|
||||
{
|
||||
return ReturnFail($"The config file list is empty.");
|
||||
}
|
||||
|
||||
var parsedInfo = ImmutableArray.CreateBuilder<IConfigInfo>();
|
||||
|
||||
foreach ((ContentPath path, Result<XDocument> docLoadResult) res in await _storageService.LoadPackageXmlFilesAsync(src.FilePaths))
|
||||
{
|
||||
if (res.docLoadResult.IsFailed)
|
||||
{
|
||||
return ReturnFail($"Failed to load document for {src.OwnerPackage.Name}").WithErrors(res.docLoadResult.Errors);
|
||||
}
|
||||
|
||||
var settingElements = res.docLoadResult.Value.GetChildElement("Configuration")
|
||||
.GetChildElements("Settings").SelectMany(e => e.GetChildElements("Setting")).ToImmutableArray();
|
||||
if (settingElements.IsDefaultOrEmpty)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var packageIdent = XmlConvert.EncodeLocalName(res.path.ContentPackage!.Name);
|
||||
|
||||
foreach (var element in settingElements)
|
||||
{
|
||||
var name = element.GetAttributeString("Name", string.Empty);
|
||||
if (name.IsNullOrWhiteSpace())
|
||||
{
|
||||
return ReturnFail(
|
||||
$"The internal name for a setting in the config file '{res.path.FullPath}' is empty!");
|
||||
}
|
||||
|
||||
var newSetting = new ConfigInfo()
|
||||
{
|
||||
InternalName = name,
|
||||
OwnerPackage = res.path.ContentPackage,
|
||||
DataType = element.GetAttributeString("Type", string.Empty),
|
||||
Element = element,
|
||||
EditableStates = element.GetAttributeBool("ReadOnly", false) ? RunState.Unloaded :
|
||||
element.GetAttributeBool("AllowChangesWhileExecuting", true) ? RunState.Running :
|
||||
RunState.LoadedNoExec,
|
||||
NetSync = element.GetAttributeEnum("NetSync", NetSync.None),
|
||||
#if CLIENT
|
||||
DisplayName = $"{packageIdent}.{name}.DisplayName",
|
||||
Description = $"{packageIdent}.{name}.Description",
|
||||
DisplayCategory = $"{packageIdent}.{name}.DisplayCategory",
|
||||
ShowInMenus = element.GetAttributeBool("ShowInMenus", true),
|
||||
Tooltip = $"{packageIdent}.{name}.Tooltip",
|
||||
ImageIconPath = element.GetAttributeString("ImageIcon", string.Empty) is {} val && !val.IsNullOrWhiteSpace() ?
|
||||
ContentPath.FromRaw(res.path.ContentPackage, val) : ContentPath.Empty
|
||||
#endif
|
||||
};
|
||||
if (!IsInfoValid(newSetting))
|
||||
{
|
||||
return ReturnFail($"A setting was invalid. ContentPackage: {res.path.ContentPackage.Name}. Name: {newSetting?.InternalName}");
|
||||
}
|
||||
parsedInfo.Add(newSetting);
|
||||
}
|
||||
}
|
||||
|
||||
return FluentResults.Result.Ok(parsedInfo.ToImmutable());
|
||||
|
||||
// Helpers
|
||||
|
||||
FluentResults.Result ReturnFail(string msg)
|
||||
{
|
||||
return FluentResults.Result.Fail($"{nameof(IParserServiceOneToManyAsync<IConfigResourceInfo, IConfigInfo>.TryParseResourcesAsync)}: {msg}");
|
||||
}
|
||||
|
||||
bool IsInfoValid(ConfigInfo info)
|
||||
{
|
||||
return info.OwnerPackage != null
|
||||
&& !info.InternalName.IsNullOrWhiteSpace()
|
||||
&& !info.DataType.IsNullOrWhiteSpace()
|
||||
&& info.Element != null
|
||||
#if CLIENT
|
||||
&& !info.DisplayName.IsNullOrWhiteSpace()
|
||||
&& !info.Description.IsNullOrWhiteSpace()
|
||||
&& !info.DisplayCategory.IsNullOrWhiteSpace()
|
||||
&& !info.Tooltip.IsNullOrWhiteSpace()
|
||||
#endif
|
||||
;
|
||||
}
|
||||
}
|
||||
|
||||
async Task<Result<ImmutableArray<IConfigProfileInfo>>>
|
||||
IParserServiceOneToManyAsync<IConfigResourceInfo, IConfigProfileInfo>
|
||||
.TryParseResourcesAsync(IConfigResourceInfo src)
|
||||
{
|
||||
Guard.IsNotNull(src, nameof(src));
|
||||
Guard.IsNotNull(src.OwnerPackage, nameof(src.OwnerPackage));
|
||||
using var lck = await _operationLock.AcquireReaderLock();
|
||||
IService.CheckDisposed(this);
|
||||
|
||||
if (src.FilePaths.IsDefaultOrEmpty)
|
||||
{
|
||||
return ReturnFail($"The config file list is empty.");
|
||||
}
|
||||
|
||||
var parsedInfo = ImmutableArray.CreateBuilder<IConfigProfileInfo>();
|
||||
|
||||
foreach ((ContentPath path, Result<XDocument> docLoadResult) res in await _storageService
|
||||
.LoadPackageXmlFilesAsync(src.FilePaths))
|
||||
{
|
||||
if (res.docLoadResult.IsFailed)
|
||||
{
|
||||
return ReturnFail($"Failed to load document for {src.OwnerPackage.Name}")
|
||||
.WithErrors(res.docLoadResult.Errors);
|
||||
}
|
||||
|
||||
var profileCollection = res.docLoadResult.Value.GetChildElement("Configuration")
|
||||
.GetChildElement("Profiles");
|
||||
if (profileCollection == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (var profile in profileCollection.GetChildElements("Profile"))
|
||||
{
|
||||
var profileName = profile.GetAttributeString("Name", string.Empty);
|
||||
Guard.IsNotNullOrWhiteSpace(profileName, nameof(profileName));
|
||||
|
||||
var settingValues = profile.GetChildElements("SettingValue").ToImmutableArray();
|
||||
if (settingValues.IsDefaultOrEmpty)
|
||||
{
|
||||
ThrowHelper.ThrowArgumentNullException(nameof(settingValues));
|
||||
}
|
||||
|
||||
var profileValuesBuilder = ImmutableArray.CreateBuilder<(string ConfigName, XElement Value)>();
|
||||
|
||||
foreach (var settingValue in settingValues)
|
||||
{
|
||||
var cfgName = settingValue.GetAttributeString("Name", string.Empty);
|
||||
Guard.IsNotNullOrWhiteSpace(cfgName, nameof(cfgName));
|
||||
profileValuesBuilder.Add((cfgName, settingValue));
|
||||
}
|
||||
|
||||
parsedInfo.Add(new ConfigProfileInfo()
|
||||
{
|
||||
InternalName = profileName,
|
||||
OwnerPackage = res.path.ContentPackage,
|
||||
ProfileValues = profileValuesBuilder.ToImmutable()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return parsedInfo.ToImmutable();
|
||||
|
||||
FluentResults.Result ReturnFail(string msg)
|
||||
{
|
||||
return FluentResults.Result.Fail($"{nameof(IParserServiceOneToManyAsync<IConfigResourceInfo, IConfigProfileInfo>.TryParseResourcesAsync)}: {msg}");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,728 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Xml;
|
||||
using System.Xml.Linq;
|
||||
using Barotrauma.LuaCs.Data;
|
||||
using Barotrauma.Networking;
|
||||
using FluentResults;
|
||||
using FluentResults.LuaCs;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.Toolkit.Diagnostics;
|
||||
using Error = FluentResults.Error;
|
||||
using Path = System.IO.Path;
|
||||
|
||||
namespace Barotrauma.LuaCs;
|
||||
|
||||
public class StorageService : IStorageService
|
||||
{
|
||||
public StorageService(IStorageServiceConfig configData)
|
||||
{
|
||||
ConfigData = configData;
|
||||
IsReadOperationAllowedEval = bool (str) => true;
|
||||
IsWriteOperationAllowedEval = bool (str) => true;
|
||||
}
|
||||
|
||||
private readonly ConcurrentDictionary<string, OneOf.OneOf<byte[], string, XDocument>> _fsCache = new();
|
||||
protected readonly IStorageServiceConfig ConfigData;
|
||||
protected readonly AsyncReaderWriterLock OperationsLock = new();
|
||||
|
||||
private Func<string, bool> _isReadOperationAllowedEval;
|
||||
protected Func<string, bool> IsReadOperationAllowedEval
|
||||
{
|
||||
get => _isReadOperationAllowedEval;
|
||||
set
|
||||
{
|
||||
if (value is not null)
|
||||
_isReadOperationAllowedEval = value;
|
||||
}
|
||||
}
|
||||
|
||||
private Func<string, bool> _isWriteOperationAllowedEval;
|
||||
protected Func<string, bool> IsWriteOperationAllowedEval
|
||||
{
|
||||
get => _isWriteOperationAllowedEval;
|
||||
set
|
||||
{
|
||||
if (value is not null)
|
||||
_isWriteOperationAllowedEval = value;
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsDisposed => ModUtils.Threading.GetBool(ref _isDisposed);
|
||||
private int _isDisposed = 0;
|
||||
public virtual void Dispose()
|
||||
{
|
||||
using var lck = OperationsLock.AcquireWriterLock().ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
if (!ModUtils.Threading.CheckIfClearAndSetBool(ref _isDisposed))
|
||||
return;
|
||||
_fsCache.Clear();
|
||||
}
|
||||
|
||||
public void PurgeCache()
|
||||
{
|
||||
using var lck = OperationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
IService.CheckDisposed(this);
|
||||
_fsCache.Clear();
|
||||
}
|
||||
|
||||
public void PurgeFileFromCache(string absolutePath)
|
||||
{
|
||||
using var lck = OperationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
IService.CheckDisposed(this);
|
||||
|
||||
if (absolutePath.IsNullOrWhiteSpace())
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
//sanitation pass
|
||||
absolutePath = System.IO.Path.GetFullPath(absolutePath).CleanUpPath();
|
||||
_fsCache.Remove(absolutePath, out _);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignored
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
public void PurgeFilesFromCache(params string[] absolutePaths)
|
||||
{
|
||||
using var lck = OperationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
IService.CheckDisposed(this);
|
||||
|
||||
if (absolutePaths.Length < 1)
|
||||
return;
|
||||
|
||||
foreach (var path in absolutePaths)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (path.IsNullOrWhiteSpace())
|
||||
continue;
|
||||
|
||||
//sanitation pass
|
||||
var path2 = System.IO.Path.GetFullPath(path).CleanUpPath();
|
||||
_fsCache.Remove(path2, out _);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignored
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Local Game Content
|
||||
protected Result<string> GetAbsolutePathForLocal(ContentPackage package, string localFilePath)
|
||||
{
|
||||
if (Path.IsPathRooted(localFilePath))
|
||||
ThrowHelper.ThrowArgumentException($"{nameof(GetAbsolutePathForLocal)}: The path {localFilePath} is an absolute path.");
|
||||
|
||||
try
|
||||
{
|
||||
var path = System.IO.Path.GetFullPath(Path.Combine(
|
||||
ConfigData.LocalPackageDataPath.Replace(ConfigData.LocalDataPathRegex, XmlConvert.EncodeLocalName(package.Name)).CleanUpPathCrossPlatform(),
|
||||
localFilePath.CleanUpPathCrossPlatform()));
|
||||
if (!path.StartsWith(Path.GetFullPath(ConfigData.LocalDataSavePath)))
|
||||
ThrowHelper.ThrowUnauthorizedAccessException($"{nameof(GetAbsolutePathForLocal)}: The local path of '{path}' is not a local path!");
|
||||
return path;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
if (e is ArgumentNullException or ArgumentException or UnauthorizedAccessException)
|
||||
throw; // these are dev errors and should be propagated.
|
||||
return FluentResults.Result.Fail(new ExceptionalError(e));
|
||||
}
|
||||
}
|
||||
|
||||
private Result<T> LoadLocalData<T>(ContentPackage package, string localFilePath, Func<string, Result<T>> dataLoader)
|
||||
{
|
||||
Guard.IsNotNull(package, nameof(package));
|
||||
Guard.IsNotNullOrWhiteSpace(localFilePath, nameof(localFilePath));
|
||||
using var lck = OperationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
IService.CheckDisposed(this);
|
||||
var res = GetAbsolutePathForLocal(package, localFilePath);
|
||||
return res is { IsFailed: true } ? res.ToResult() : dataLoader(res.Value);
|
||||
}
|
||||
|
||||
public Result<XDocument> LoadLocalXml(ContentPackage package, string localFilePath) => LoadLocalData(package, localFilePath, TryLoadXml);
|
||||
public Result<byte[]> LoadLocalBinary(ContentPackage package, string localFilePath) => LoadLocalData(package, localFilePath, TryLoadBinary);
|
||||
public Result<string> LoadLocalText(ContentPackage package, string localFilePath) => LoadLocalData(package, localFilePath, TryLoadText);
|
||||
|
||||
|
||||
private FluentResults.Result SaveLocalData<T>(ContentPackage package, string localFilePath, in T data, Func<string, T, FluentResults.Result> dataSaver)
|
||||
{
|
||||
Guard.IsNotNull(package, nameof(package));
|
||||
Guard.IsNotNullOrWhiteSpace(localFilePath, nameof(localFilePath));
|
||||
using var lck = OperationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
IService.CheckDisposed(this);
|
||||
var res = GetAbsolutePathForLocal(package, localFilePath);
|
||||
return res is { IsFailed: true } ? res.ToResult() : dataSaver(res.Value, data);
|
||||
}
|
||||
|
||||
public FluentResults.Result SaveLocalXml(ContentPackage package, string localFilePath, XDocument document)
|
||||
=> SaveLocalData(package, localFilePath, document, (path, data) => TrySaveXml(path, in data));
|
||||
public FluentResults.Result SaveLocalBinary(ContentPackage package, string localFilePath, in byte[] bytes)
|
||||
=> SaveLocalData(package, localFilePath, bytes, (path, data) => TrySaveBinary(path, in data));
|
||||
public FluentResults.Result SaveLocalText(ContentPackage package, string localFilePath, in string text)
|
||||
=> SaveLocalData(package, localFilePath, text, (path, data) => TrySaveText(path, in data));
|
||||
|
||||
private async Task<Result<T>> LoadLocalDataAsync<T>(ContentPackage package, string localFilePath,
|
||||
Func<string, Task<Result<T>>> dataLoader)
|
||||
{
|
||||
Guard.IsNotNull(package, nameof(package));
|
||||
Guard.IsNotNullOrWhiteSpace(localFilePath, nameof(localFilePath));
|
||||
using var lck = await OperationsLock.AcquireReaderLock();
|
||||
IService.CheckDisposed(this);
|
||||
var res = GetAbsolutePathForLocal(package, localFilePath);
|
||||
return res is { IsFailed: true } ? res.ToResult() : await dataLoader(res.Value);
|
||||
}
|
||||
|
||||
public async Task<Result<XDocument>> LoadLocalXmlAsync(ContentPackage package, string localFilePath)
|
||||
=> await LoadLocalDataAsync(package, localFilePath, async path => await TryLoadXmlAsync(path));
|
||||
public async Task<Result<byte[]>> LoadLocalBinaryAsync(ContentPackage package, string localFilePath)
|
||||
=> await LoadLocalDataAsync(package, localFilePath, async path => await TryLoadBinaryAsync(path));
|
||||
public async Task<Result<string>> LoadLocalTextAsync(ContentPackage package, string localFilePath)
|
||||
=> await LoadLocalDataAsync(package, localFilePath, async path => await TryLoadTextAsync(path));
|
||||
|
||||
private async Task<FluentResults.Result> SaveLocalDataAsync<T>(ContentPackage package, string localFilePath,
|
||||
T data, Func<string, T, Task<FluentResults.Result>> dataSaver)
|
||||
{
|
||||
Guard.IsNotNull(package, nameof(package));
|
||||
Guard.IsNotNullOrWhiteSpace(localFilePath, nameof(localFilePath));
|
||||
IService.CheckDisposed(this);
|
||||
using var lck = await OperationsLock.AcquireReaderLock();
|
||||
var res = GetAbsolutePathForLocal(package, localFilePath);
|
||||
return res is { IsFailed: true } ? res.ToResult() : await dataSaver(res.Value, data);
|
||||
}
|
||||
|
||||
public async Task<FluentResults.Result> SaveLocalXmlAsync(ContentPackage package, string localFilePath, XDocument document)
|
||||
=> await SaveLocalDataAsync(package, localFilePath, document, async (path, doc) => await TrySaveXmlAsync(path, doc));
|
||||
public async Task<FluentResults.Result> SaveLocalBinaryAsync(ContentPackage package, string localFilePath, byte[] bytes)
|
||||
=> await SaveLocalDataAsync(package, localFilePath, bytes, async (path, bin) => await TrySaveBinaryAsync(path, bin));
|
||||
public async Task<FluentResults.Result> SaveLocalTextAsync(ContentPackage package, string localFilePath, string text)
|
||||
=> await SaveLocalDataAsync(package, localFilePath, text, async (path, txt) => await TrySaveTextAsync(path, txt));
|
||||
|
||||
private bool IsPackagePathValid(ContentPath contentPath)
|
||||
{
|
||||
return contentPath.FullPath.StartsWith(ConfigData.WorkshopModsDirectory)
|
||||
|| contentPath.FullPath.StartsWith(ConfigData.LocalModsDirectory)
|
||||
#if CLIENT
|
||||
|| contentPath.FullPath.StartsWith(ConfigData.TempDownloadsDirectory)
|
||||
#endif
|
||||
|| contentPath.FullPath.StartsWith(Path.GetFullPath(ContentPackageManager.VanillaCorePackage!.Dir).CleanUpPathCrossPlatform());
|
||||
}
|
||||
|
||||
// --- Package Content
|
||||
private Result<T> LoadPackageData<T>(ContentPath contentPath, Func<string, Result<T>> dataLoader)
|
||||
{
|
||||
Guard.IsNotNull(contentPath, nameof(contentPath));
|
||||
Guard.IsNotNullOrWhiteSpace(contentPath.FullPath, nameof(contentPath.FullPath));
|
||||
using var lck = OperationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
IService.CheckDisposed(this);
|
||||
if (!IsPackagePathValid(contentPath))
|
||||
{
|
||||
ThrowHelper.ThrowUnauthorizedAccessException($"{nameof(LoadPackageData)}: The filepath of `{contentPath.FullPath}' is not in a package directory!");
|
||||
}
|
||||
return dataLoader(contentPath.FullPath);
|
||||
}
|
||||
|
||||
public Result<XDocument> LoadPackageXml(ContentPath filePath)
|
||||
=> LoadPackageData(filePath, path => TryLoadXml(filePath.FullPath));
|
||||
public Result<byte[]> LoadPackageBinary(ContentPath filePath)
|
||||
=> LoadPackageData(filePath, path => TryLoadBinary(filePath.FullPath));
|
||||
public Result<string> LoadPackageText(ContentPath filePath)
|
||||
=> LoadPackageData(filePath, path => TryLoadText(filePath.FullPath));
|
||||
|
||||
private ImmutableArray<(ContentPath, Result<T>)> LoadPackageDataFiles<T>(ImmutableArray<ContentPath> filePaths, Func<string, Result<T>> dataLoader)
|
||||
{
|
||||
if (filePaths.IsDefaultOrEmpty)
|
||||
ThrowHelper.ThrowArgumentNullException($"{nameof(LoadPackageData)}: File paths is empty!");
|
||||
using var lck = OperationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
var builder = ImmutableArray.CreateBuilder<(ContentPath, Result<T>)>();
|
||||
foreach (var path in filePaths)
|
||||
{
|
||||
builder.Add((path, LoadPackageData(path, dataLoader)));
|
||||
}
|
||||
return builder.ToImmutable();
|
||||
}
|
||||
|
||||
public ImmutableArray<(ContentPath, Result<XDocument>)> LoadPackageXmlFiles(ImmutableArray<ContentPath> filePaths)
|
||||
=> LoadPackageDataFiles(filePaths, TryLoadXml);
|
||||
public ImmutableArray<(ContentPath, Result<byte[]>)> LoadPackageBinaryFiles(ImmutableArray<ContentPath> filePaths)
|
||||
=> LoadPackageDataFiles(filePaths, TryLoadBinary);
|
||||
public ImmutableArray<(ContentPath, Result<string>)> LoadPackageTextFiles(ImmutableArray<ContentPath> filePaths)
|
||||
=> LoadPackageDataFiles(filePaths, TryLoadText);
|
||||
|
||||
public Result<ImmutableArray<string>> FindFilesInPackage(ContentPackage package, string localSubfolder, string regexFilter, bool searchRecursively)
|
||||
{
|
||||
Guard.IsNotNull(package, nameof(package));
|
||||
try
|
||||
{
|
||||
var cp = ContentPath.FromRaw(package, package.Dir);
|
||||
var fullPath = localSubfolder.IsNullOrWhiteSpace()
|
||||
? Path.GetFullPath(cp.FullPath)
|
||||
: Path.GetFullPath(localSubfolder, cp.FullPath);
|
||||
return System.IO.Directory.GetFiles(fullPath, regexFilter,
|
||||
searchRecursively ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly)
|
||||
.ToImmutableArray();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
if (e is ArgumentNullException or ArgumentException)
|
||||
throw;
|
||||
return FluentResults.Result.Fail(new ExceptionalError(e)
|
||||
.WithMetadata(MetadataType.ExceptionObject, this)
|
||||
.WithMetadata(MetadataType.RootObject, package));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private async Task<Result<T>> LoadPackageDataAsync<T>(ContentPath contentPath, Func<string, Task<Result<T>>> dataLoader)
|
||||
{
|
||||
Guard.IsNotNull(contentPath, nameof(contentPath));
|
||||
Guard.IsNotNullOrWhiteSpace(contentPath.FullPath, nameof(contentPath.FullPath));
|
||||
using var lck = await OperationsLock.AcquireReaderLock();
|
||||
IService.CheckDisposed(this);
|
||||
if (!IsPackagePathValid(contentPath))
|
||||
{
|
||||
ThrowHelper.ThrowUnauthorizedAccessException($"{nameof(LoadPackageDataAsync)}: The filepath of `{contentPath.FullPath}' is not in a package directory!");
|
||||
}
|
||||
return await dataLoader(contentPath.FullPath);
|
||||
}
|
||||
|
||||
public async Task<Result<XDocument>> LoadPackageXmlAsync(ContentPath filePath)
|
||||
=> await LoadPackageDataAsync(filePath, async path => await TryLoadXmlAsync(path));
|
||||
public async Task<Result<byte[]>> LoadPackageBinaryAsync(ContentPath filePath)
|
||||
=> await LoadPackageDataAsync(filePath, async path => await TryLoadBinaryAsync(path));
|
||||
public async Task<Result<string>> LoadPackageTextAsync(ContentPath filePath)
|
||||
=> await LoadPackageDataAsync(filePath, async path => await TryLoadTextAsync(path));
|
||||
|
||||
private async Task<ImmutableArray<(ContentPath, Result<T>)>> LoadPackageDataFilesAsync<T>(
|
||||
ImmutableArray<ContentPath> filePaths, Func<string, Task<Result<T>>> dataLoader)
|
||||
{
|
||||
if (filePaths.IsDefaultOrEmpty)
|
||||
{
|
||||
ThrowHelper.ThrowArgumentNullException($"{nameof(LoadPackageData)}: File paths is empty!");
|
||||
}
|
||||
using var lck = await OperationsLock.AcquireReaderLock();
|
||||
var builder = ImmutableArray.CreateBuilder<(ContentPath, Result<T>)>();
|
||||
foreach (var path in filePaths)
|
||||
{
|
||||
builder.Add((path, await LoadPackageDataAsync(path, dataLoader)));
|
||||
}
|
||||
return builder.ToImmutable();
|
||||
}
|
||||
|
||||
public async Task<ImmutableArray<(ContentPath, Result<XDocument>)>> LoadPackageXmlFilesAsync(ImmutableArray<ContentPath> filePaths)
|
||||
=> await LoadPackageDataFilesAsync(filePaths, async path => await TryLoadXmlAsync(path));
|
||||
public async Task<ImmutableArray<(ContentPath, Result<byte[]>)>> LoadPackageBinaryFilesAsync(ImmutableArray<ContentPath> filePaths)
|
||||
=> await LoadPackageDataFilesAsync(filePaths, async path => await TryLoadBinaryAsync(path));
|
||||
public async Task<ImmutableArray<(ContentPath, Result<string>)>> LoadPackageTextFilesAsync(ImmutableArray<ContentPath> filePaths)
|
||||
=> await LoadPackageDataFilesAsync(filePaths, async path => await TryLoadTextAsync(path));
|
||||
|
||||
|
||||
private int _useCaching;
|
||||
public bool UseCaching
|
||||
{
|
||||
get => ModUtils.Threading.GetBool(ref _useCaching);
|
||||
set => ModUtils.Threading.SetBool(ref _useCaching, value);
|
||||
}
|
||||
|
||||
// Method group redirect
|
||||
private FluentResults.Result<XDocument> TryLoadXml(string filePath) => TryLoadXml(filePath, null);
|
||||
|
||||
public virtual FluentResults.Result<XDocument> TryLoadXml(string filePath, Encoding encoding)
|
||||
{
|
||||
Guard.IsNotNullOrWhiteSpace(filePath, nameof(filePath));
|
||||
using var lck = OperationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
IService.CheckDisposed(this);
|
||||
|
||||
var r = TryLoadText(filePath, encoding);
|
||||
if (r is { IsSuccess: true, Value: not null })
|
||||
return XDocument.Parse(r.Value);
|
||||
else
|
||||
{
|
||||
return r.ToResult<XDocument>(s => null)
|
||||
.WithError(GetGeneralError(nameof(LoadLocalXml), filePath));
|
||||
}
|
||||
}
|
||||
|
||||
// Method group redirect
|
||||
private FluentResults.Result<string> TryLoadText(string filePath) => TryLoadText(filePath, null);
|
||||
public virtual FluentResults.Result<string> TryLoadText(string filePath, Encoding encoding)
|
||||
{
|
||||
Guard.IsNotNullOrWhiteSpace(filePath, nameof(filePath));
|
||||
using var lck = OperationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
IService.CheckDisposed(this);
|
||||
|
||||
if (IsReadOperationAllowedEval?.Invoke(filePath) is not true)
|
||||
{
|
||||
return FluentResults.Result.Fail($"{nameof(TryLoadText)}: File '{filePath}' is not allowed.");
|
||||
}
|
||||
|
||||
if (UseCaching && _fsCache.TryGetValue(filePath, out var result)
|
||||
&& result.TryPickT1(out var cachedVal, out _))
|
||||
{
|
||||
return FluentResults.Result.Ok(cachedVal);
|
||||
}
|
||||
|
||||
return IOExceptionsOperationRunner(nameof(TryLoadText), filePath, () =>
|
||||
{
|
||||
var fp = filePath.CleanUpPath();
|
||||
fp = System.IO.Path.IsPathRooted(fp) ? fp : System.IO.Path.GetFullPath(fp);
|
||||
var fileText = encoding is null ? System.IO.File.ReadAllText(fp) : System.IO.File.ReadAllText(fp, encoding);
|
||||
if (UseCaching)
|
||||
_fsCache[filePath] = fileText;
|
||||
return new FluentResults.Result<string>().WithSuccess($"Loaded file successfully").WithValue(fileText);
|
||||
});
|
||||
}
|
||||
|
||||
public virtual FluentResults.Result<byte[]> TryLoadBinary(string filePath)
|
||||
{
|
||||
Guard.IsNotNullOrWhiteSpace(filePath, nameof(filePath));
|
||||
using var lck = OperationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
IService.CheckDisposed(this);
|
||||
|
||||
if (IsReadOperationAllowedEval?.Invoke(filePath) is not true)
|
||||
{
|
||||
return FluentResults.Result.Fail($"{nameof(TryLoadBinary)}: File '{filePath}' is not allowed.");
|
||||
}
|
||||
|
||||
if (UseCaching && _fsCache.TryGetValue(filePath, out var result)
|
||||
&& result.TryPickT0(out var cachedVal, out _))
|
||||
{
|
||||
return FluentResults.Result.Ok(cachedVal);
|
||||
}
|
||||
|
||||
return IOExceptionsOperationRunner(nameof(TryLoadBinary), filePath, () =>
|
||||
{
|
||||
var fp = filePath.CleanUpPath();
|
||||
fp = System.IO.Path.IsPathRooted(fp) ? fp : System.IO.Path.GetFullPath(fp);
|
||||
var fileData = System.IO.File.ReadAllBytes(fp);
|
||||
if (UseCaching)
|
||||
{
|
||||
_fsCache[filePath] = fileData;
|
||||
}
|
||||
return new FluentResults.Result<byte[]>().WithSuccess($"Loaded file successfully").WithValue(fileData);
|
||||
});
|
||||
}
|
||||
|
||||
public virtual FluentResults.Result TrySaveXml(string filePath, in XDocument document, Encoding encoding = null) => TrySaveText(filePath, document.ToString(), encoding);
|
||||
public virtual FluentResults.Result TrySaveText(string filePath, in string text, Encoding encoding = null)
|
||||
{
|
||||
Guard.IsNotNullOrWhiteSpace(text, nameof(text));
|
||||
using var lck = OperationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
IService.CheckDisposed(this);
|
||||
|
||||
if (IsWriteOperationAllowedEval?.Invoke(filePath) is not true)
|
||||
{
|
||||
return FluentResults.Result.Fail($"{nameof(TrySaveText)}: File '{filePath}' is not allowed.");
|
||||
}
|
||||
|
||||
string t = text; //copy
|
||||
return IOExceptionsOperationRunner(nameof(TrySaveText), filePath, () =>
|
||||
{
|
||||
var fp = filePath.CleanUpPath();
|
||||
fp = System.IO.Path.IsPathRooted(fp) ? fp : System.IO.Path.GetFullPath(fp);
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(fp)!);
|
||||
System.IO.File.WriteAllText(fp, t, encoding ?? Encoding.UTF8);
|
||||
if (UseCaching)
|
||||
_fsCache[filePath] = t;
|
||||
return new FluentResults.Result().WithSuccess($"Saved to file successfully");
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
public virtual FluentResults.Result TrySaveBinary(string filePath, in byte[] bytes)
|
||||
{
|
||||
Guard.IsNotNullOrWhiteSpace(filePath, nameof(filePath));
|
||||
Guard.IsNotNull(bytes, nameof(bytes));
|
||||
Guard.HasSizeGreaterThanOrEqualTo(bytes, 1, nameof(bytes));
|
||||
using var lck = OperationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult();
|
||||
IService.CheckDisposed(this);
|
||||
|
||||
if (IsWriteOperationAllowedEval?.Invoke(filePath) is not true)
|
||||
{
|
||||
return FluentResults.Result.Fail($"{nameof(TrySaveBinary)}: File '{filePath}' is not allowed.");
|
||||
}
|
||||
|
||||
byte[] b = new byte[bytes.Length];
|
||||
System.Buffer.BlockCopy(bytes, 0, b, 0, bytes.Length);
|
||||
return IOExceptionsOperationRunner(nameof(TrySaveBinary), filePath, () =>
|
||||
{
|
||||
var fp = filePath.CleanUpPath();
|
||||
fp = System.IO.Path.IsPathRooted(fp) ? fp : System.IO.Path.GetFullPath(fp);
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(fp)!);
|
||||
System.IO.File.WriteAllBytes(fp, b);
|
||||
if (UseCaching)
|
||||
_fsCache[filePath] = b;
|
||||
return new FluentResults.Result().WithSuccess($"Saved to file successfully");
|
||||
});
|
||||
}
|
||||
|
||||
public virtual FluentResults.Result<bool> FileExists(string filePath)
|
||||
{
|
||||
Guard.IsNotNullOrWhiteSpace(filePath, nameof(filePath));
|
||||
IService.CheckDisposed(this);
|
||||
// lock not needed
|
||||
if (IsReadOperationAllowedEval?.Invoke(filePath) is not true)
|
||||
{
|
||||
return FluentResults.Result.Fail($"{nameof(FileExists)}: File '{filePath}' is not allowed.");
|
||||
}
|
||||
|
||||
return IOExceptionsOperationRunner<bool>(nameof(FileExists), filePath, () =>
|
||||
{
|
||||
var fp = filePath.CleanUpPath();
|
||||
fp = System.IO.Path.IsPathRooted(fp) ? fp : System.IO.Path.GetFullPath(fp);
|
||||
return System.IO.File.Exists(fp);
|
||||
});
|
||||
}
|
||||
|
||||
public virtual FluentResults.Result<bool> DirectoryExists(string directoryPath)
|
||||
{
|
||||
Guard.IsNotNullOrWhiteSpace(directoryPath, nameof(directoryPath));
|
||||
IService.CheckDisposed(this);
|
||||
// lock not needed
|
||||
if (IsReadOperationAllowedEval?.Invoke(directoryPath) is not true)
|
||||
{
|
||||
return FluentResults.Result.Fail($"{nameof(DirectoryExists)}: File '{directoryPath}' is not allowed.");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var di = new DirectoryInfo(directoryPath);
|
||||
return di.Exists;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new FluentResults.Result<bool>().WithError(ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
public virtual async Task<FluentResults.Result<XDocument>> TryLoadXmlAsync(string filePath, Encoding encoding = null)
|
||||
{
|
||||
Guard.IsNotNullOrWhiteSpace(filePath, nameof(filePath));
|
||||
using var lck = await OperationsLock.AcquireReaderLock();
|
||||
IService.CheckDisposed(this);
|
||||
if (IsReadOperationAllowedEval.Invoke(filePath) is not true)
|
||||
{
|
||||
return FluentResults.Result.Fail($"{nameof(TryLoadXmlAsync)}: File '{filePath}' is not allowed.");
|
||||
}
|
||||
|
||||
if (UseCaching && _fsCache.TryGetValue(filePath, out var cachedVal)
|
||||
&& cachedVal.TryPickT2(out var cachedDoc, out _))
|
||||
{
|
||||
return FluentResults.Result.Ok(cachedDoc);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await using var fs = new FileStream(filePath, FileMode.Open, FileAccess.Read);
|
||||
var doc = await XDocument.LoadAsync(fs, LoadOptions.PreserveWhitespace, CancellationToken.None);
|
||||
if (UseCaching)
|
||||
_fsCache[filePath] = doc;
|
||||
return FluentResults.Result.Ok(doc);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return FluentResults.Result.Fail<XDocument>(GetGeneralError(nameof(TryLoadXmlAsync), filePath));
|
||||
}
|
||||
}
|
||||
|
||||
public virtual async Task<FluentResults.Result<string>> TryLoadTextAsync(string filePath, Encoding encoding = null)
|
||||
{
|
||||
Guard.IsNotNullOrWhiteSpace(filePath, nameof(filePath));
|
||||
using var lck = await OperationsLock.AcquireReaderLock();
|
||||
IService.CheckDisposed(this);
|
||||
if (IsReadOperationAllowedEval.Invoke(filePath) is not true)
|
||||
{
|
||||
return FluentResults.Result.Fail($"{nameof(TryLoadTextAsync)}: File '{filePath}' is not allowed.");
|
||||
}
|
||||
|
||||
if (UseCaching && _fsCache.TryGetValue(filePath, out var cachedVal)
|
||||
&& cachedVal.TryPickT1(out var cachedTxt, out _))
|
||||
{
|
||||
return FluentResults.Result.Ok(cachedTxt);
|
||||
}
|
||||
|
||||
return await IOExceptionsOperationRunnerAsync<string>(nameof(TryLoadTextAsync), filePath, async () =>
|
||||
{
|
||||
var fp = filePath.CleanUpPath();
|
||||
fp = System.IO.Path.IsPathRooted(fp) ? fp : System.IO.Path.GetFullPath(fp);
|
||||
var txt = await System.IO.File.ReadAllTextAsync(fp);
|
||||
if (UseCaching)
|
||||
_fsCache[filePath] = txt;
|
||||
return FluentResults.Result.Ok(txt);
|
||||
});
|
||||
}
|
||||
|
||||
public virtual async Task<FluentResults.Result<byte[]>> TryLoadBinaryAsync(string filePath)
|
||||
{
|
||||
Guard.IsNotNullOrWhiteSpace(filePath, nameof(filePath));
|
||||
using var lck = await OperationsLock.AcquireReaderLock();
|
||||
IService.CheckDisposed(this);
|
||||
if (IsReadOperationAllowedEval.Invoke(filePath) is not true)
|
||||
{
|
||||
return FluentResults.Result.Fail($"{nameof(TryLoadBinaryAsync)}: File '{filePath}' is not allowed.");
|
||||
}
|
||||
|
||||
if (UseCaching && _fsCache.TryGetValue(filePath, out var cachedVal)
|
||||
&& cachedVal.TryPickT0(out var cachedBin, out _))
|
||||
{
|
||||
return cachedBin;
|
||||
}
|
||||
|
||||
return await IOExceptionsOperationRunnerAsync<byte[]>(nameof(TryLoadTextAsync), filePath, async () =>
|
||||
{
|
||||
var fp = filePath.CleanUpPath();
|
||||
fp = System.IO.Path.IsPathRooted(fp) ? fp : System.IO.Path.GetFullPath(fp);
|
||||
return await System.IO.File.ReadAllBytesAsync(fp);
|
||||
});
|
||||
}
|
||||
|
||||
// method group overload
|
||||
public virtual async Task<FluentResults.Result> TrySaveXmlAsync(string filePath, XDocument document, Encoding encoding = null) => await TrySaveTextAsync(filePath, document.ToString(), encoding);
|
||||
public virtual async Task<FluentResults.Result> TrySaveTextAsync(string filePath, string text, Encoding encoding = null)
|
||||
{
|
||||
Guard.IsNotNullOrWhiteSpace(text, nameof(text));
|
||||
using var lck = await OperationsLock.AcquireReaderLock();
|
||||
IService.CheckDisposed(this);
|
||||
if (IsWriteOperationAllowedEval.Invoke(filePath) is not true)
|
||||
{
|
||||
return FluentResults.Result.Fail($"{nameof(TrySaveTextAsync)}: File '{filePath}' is not allowed.");
|
||||
}
|
||||
|
||||
string t = text.ToString(); //copy
|
||||
return await IOExceptionsOperationRunnerAsync(nameof(TrySaveText), filePath, async () =>
|
||||
{
|
||||
var fp = filePath.CleanUpPath();
|
||||
fp = System.IO.Path.IsPathRooted(fp) ? fp : System.IO.Path.GetFullPath(fp);
|
||||
await System.IO.File.WriteAllTextAsync(fp, t, encoding);
|
||||
if (UseCaching)
|
||||
_fsCache[filePath] = t;
|
||||
return new FluentResults.Result().WithSuccess($"Saved to file successfully");
|
||||
});
|
||||
}
|
||||
|
||||
public virtual async Task<FluentResults.Result> TrySaveBinaryAsync(string filePath, byte[] bytes)
|
||||
{
|
||||
Guard.IsNotNullOrWhiteSpace(filePath, nameof(filePath));
|
||||
Guard.IsNotNull(bytes, nameof(bytes));
|
||||
Guard.HasSizeGreaterThanOrEqualTo(bytes, 1, nameof(bytes));
|
||||
using var lck = await OperationsLock.AcquireReaderLock();
|
||||
IService.CheckDisposed(this);
|
||||
if (IsWriteOperationAllowedEval.Invoke(filePath) is not true)
|
||||
{
|
||||
return FluentResults.Result.Fail($"{nameof(TrySaveBinaryAsync)}: File '{filePath}' is not allowed.");
|
||||
}
|
||||
|
||||
byte[] b = new byte[bytes.Length];
|
||||
System.Buffer.BlockCopy(bytes, 0, b, 0, bytes.Length);
|
||||
return await IOExceptionsOperationRunnerAsync(nameof(TrySaveBinary), filePath, async () =>
|
||||
{
|
||||
var fp = filePath.CleanUpPath();
|
||||
fp = System.IO.Path.IsPathRooted(fp) ? fp : System.IO.Path.GetFullPath(fp);
|
||||
await System.IO.File.WriteAllBytesAsync(fp, b);
|
||||
if (UseCaching)
|
||||
_fsCache[filePath] = b;
|
||||
return new FluentResults.Result().WithSuccess($"Saved to file successfully");
|
||||
});
|
||||
}
|
||||
|
||||
private async Task<FluentResults.Result<T>> IOExceptionsOperationRunnerAsync<T>(string funcName, string filepath, Func<Task<FluentResults.Result<T>>> operation)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await operation?.Invoke()!;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
if (e is ArgumentException or ArgumentNullException)
|
||||
throw;
|
||||
return ReturnException(e, filepath).WithError(GetGeneralError(funcName, filepath));
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<FluentResults.Result> IOExceptionsOperationRunnerAsync(string funcName, string filepath, Func<Task<FluentResults.Result>> operation)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await operation?.Invoke()!;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
if (e is ArgumentException or ArgumentNullException)
|
||||
throw;
|
||||
return ReturnException(e, filepath).WithError(GetGeneralError(funcName, filepath));
|
||||
}
|
||||
}
|
||||
|
||||
private FluentResults.Result<T> IOExceptionsOperationRunner<T>(string funcName, string filepath, Func<FluentResults.Result<T>> operation)
|
||||
{
|
||||
try
|
||||
{
|
||||
return operation?.Invoke();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
if (e is ArgumentException or ArgumentNullException)
|
||||
throw;
|
||||
return ReturnException(e, filepath).WithError(GetGeneralError(funcName, filepath));
|
||||
}
|
||||
}
|
||||
|
||||
private FluentResults.Result IOExceptionsOperationRunner(string funcName, string filepath, Func<FluentResults.Result> operation)
|
||||
{
|
||||
try
|
||||
{
|
||||
return operation?.Invoke();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
if (e is ArgumentException or ArgumentNullException)
|
||||
throw;
|
||||
return ReturnException(e, filepath).WithError(GetGeneralError(funcName, filepath));
|
||||
}
|
||||
}
|
||||
|
||||
private Error GetGeneralError(string funcName, string localfp, ContentPackage package) =>
|
||||
new Error($"{funcName}: Failed to load local file.")
|
||||
.WithMetadata(MetadataType.ExceptionObject, this)
|
||||
.WithMetadata(MetadataType.Sources, localfp)
|
||||
.WithMetadata(MetadataType.RootObject, package);
|
||||
|
||||
private Error GetGeneralError(string funcName, string localfp) =>
|
||||
new Error($"{funcName}: Failed to load local file.")
|
||||
.WithMetadata(MetadataType.ExceptionObject, this)
|
||||
.WithMetadata(MetadataType.Sources, localfp);
|
||||
|
||||
private FluentResults.Result<TReturn> ReturnException<TReturn, TException>(TException exception, ContentPackage package) where TException : Exception
|
||||
{
|
||||
return new FluentResults.Result<TReturn>().WithError(new ExceptionalError(exception)
|
||||
.WithMetadata(MetadataType.ExceptionObject, this)
|
||||
.WithMetadata(MetadataType.RootObject, package));
|
||||
}
|
||||
|
||||
private FluentResults.Result ReturnException<TException>(TException exception, ContentPackage package) where TException : Exception
|
||||
{
|
||||
return new FluentResults.Result().WithError(new ExceptionalError(exception)
|
||||
.WithMetadata(MetadataType.ExceptionObject, this)
|
||||
.WithMetadata(MetadataType.RootObject, package));
|
||||
}
|
||||
|
||||
private FluentResults.Result ReturnException<TException>(TException exception, string filePath) where TException : Exception
|
||||
{
|
||||
return new FluentResults.Result().WithError(new ExceptionalError(exception)
|
||||
.WithMetadata(MetadataType.ExceptionObject, this)
|
||||
.WithMetadata(MetadataType.RootObject, filePath));
|
||||
}
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using OneOf;
|
||||
|
||||
// ReSharper disable InconsistentNaming
|
||||
|
||||
namespace Barotrauma.LuaCs;
|
||||
|
||||
public interface IAssemblyManagementService : IPluginManagementService
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Searches for an assembly given it's fully qualified name, while excluding the contexts with the given Guids, if supplied.
|
||||
/// </summary>
|
||||
/// <param name="assemblyName">The assembly info.</param>
|
||||
/// <param name="excludedContexts">Guids of excluded contexts.</param>
|
||||
/// <returns><b>On Success:</b> The assembly. <br/><b>On Failure:</b> nothing.</returns>
|
||||
FluentResults.Result<Assembly> GetLoadedAssembly(OneOf<AssemblyName, string> assemblyName, in Guid[] excludedContexts);
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Threading.Tasks;
|
||||
using System.Xml.Linq;
|
||||
using Barotrauma.LuaCs.Data;
|
||||
using Barotrauma.LuaCs;
|
||||
using Barotrauma.Networking;
|
||||
using FluentResults;
|
||||
|
||||
namespace Barotrauma.LuaCs;
|
||||
|
||||
public partial interface IConfigService : IReusableService, ILuaConfigService
|
||||
{
|
||||
void RegisterSettingTypeInitializer<T>(string typeIdentifier, Func<(IConfigService ConfigService, IConfigInfo Info), T> settingFactory)
|
||||
where T : class, ISettingBase;
|
||||
Task<FluentResults.Result> LoadConfigsAsync(ImmutableArray<IConfigResourceInfo> configResources);
|
||||
Task<FluentResults.Result> LoadConfigsProfilesAsync(ImmutableArray<IConfigResourceInfo> configProfileResources);
|
||||
FluentResults.Result LoadSavedConfigsValues();
|
||||
FluentResults.Result ApplyConfigProfile(ContentPackage package, string internalName);
|
||||
FluentResults.Result DisposePackageData(ContentPackage package);
|
||||
FluentResults.Result DisposeAllPackageData();
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
using Barotrauma.Networking;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
|
||||
namespace Barotrauma.LuaCs;
|
||||
|
||||
public interface IConsoleCommandsService : IService
|
||||
{
|
||||
void RegisterCommand(string name, string help, Action<string[]> onExecute, Func<string[][]> getValidArgs = null, bool isCheat = false);
|
||||
void AssignOnExecute(string names, Action<string[]> onExecute);
|
||||
#if SERVER
|
||||
internal void AssignOnClientRequestExecute(string names, Action<Client, Vector2, string[]> onClientRequestExecute);
|
||||
#endif
|
||||
void RemoveCommand(string name);
|
||||
void RemoveRegisteredCommands();
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
using System;
|
||||
using System.Reflection;
|
||||
using Barotrauma.LuaCs.Events;
|
||||
using Barotrauma.LuaCs.Compatibility;
|
||||
using Barotrauma.LuaCs;
|
||||
|
||||
namespace Barotrauma.LuaCs;
|
||||
|
||||
public interface IEventService : IReusableService, ILuaEventService
|
||||
{
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="subscriber"></param>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <returns></returns>
|
||||
FluentResults.Result Subscribe<T>(T subscriber) where T : class, IEvent<T>;
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="subscriber"></param>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
void Unsubscribe<T>(T subscriber) where T : class, IEvent;
|
||||
/// <summary>
|
||||
/// Clears all subscribers for a given event type and removes any registration to the type.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The event type.</typeparam>
|
||||
void ClearAllEventSubscribers<T>() where T : class, IEvent;
|
||||
/// <summary>
|
||||
/// Clears all subscribers lists.
|
||||
/// </summary>
|
||||
void ClearAllSubscribers();
|
||||
/// <summary>
|
||||
/// Invokes all alive subscribers of the given event using the provided invocation factory.
|
||||
/// </summary>
|
||||
/// <param name="action"></param>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <returns></returns>
|
||||
FluentResults.Result PublishEvent<T>(Action<T> action) where T : class, IEvent<T>;
|
||||
|
||||
/// <summary>
|
||||
/// Adds an event service that will receive all published events.
|
||||
/// </summary>
|
||||
/// <param name="eventService"></param>
|
||||
void AddDispatcherEventService(IEventService eventService);
|
||||
|
||||
/// <summary>
|
||||
/// Removes an event service from the dispatcher list.
|
||||
/// </summary>
|
||||
/// <param name="eventService"></param>
|
||||
void RemoveDispatcherEventService(IEventService eventService);
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Threading.Tasks;
|
||||
using System.Xml.Linq;
|
||||
using Barotrauma.LuaCs.Data;
|
||||
using FluentResults;
|
||||
|
||||
namespace Barotrauma.LuaCs;
|
||||
|
||||
public interface IParserService<in TSrc, TOut> : IService
|
||||
{
|
||||
Result<TOut> TryParseResource(TSrc src);
|
||||
ImmutableArray<Result<TOut>> TryParseResources(IEnumerable<TSrc> sources);
|
||||
}
|
||||
|
||||
public interface IParserServiceAsync<in TSrc, TOut> : IService
|
||||
{
|
||||
Task<Result<TOut>> TryParseResourceAsync(TSrc src);
|
||||
Task<ImmutableArray<Result<TOut>>> TryParseResourcesAsync(IEnumerable<TSrc> sources);
|
||||
}
|
||||
|
||||
public interface IParserServiceOneToManyAsync<in TSrc, TOut> : IService
|
||||
{
|
||||
Task<Result<ImmutableArray<TOut>>> TryParseResourcesAsync(TSrc src);
|
||||
}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
using System;
|
||||
using Barotrauma.Networking;
|
||||
using FluentResults;
|
||||
using Microsoft.Xna.Framework;
|
||||
|
||||
namespace Barotrauma.LuaCs;
|
||||
|
||||
public readonly record struct PendingLog(string Message, Color? Color, ServerLog.MessageType MessageType);
|
||||
|
||||
public interface ILoggerSubscriber
|
||||
{
|
||||
void OnLog(PendingLog pendingLog);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Provides console and debug logging services
|
||||
/// </summary>
|
||||
public interface ILoggerService : IReusableService
|
||||
{
|
||||
void Subscribe(ILoggerSubscriber subscriber);
|
||||
void Unsubscribe(ILoggerSubscriber subscriber);
|
||||
void ProcessLogs();
|
||||
void HandleException(Exception exception, string prefix = null);
|
||||
void LogError(string message);
|
||||
void LogWarning(string message);
|
||||
void LogMessage(string message, Color? serverColor = null, Color? clientColor = null);
|
||||
void Log(string message, Color? color = null, ServerLog.MessageType messageType = ServerLog.MessageType.ServerMessage);
|
||||
void LogResults(FluentResults.Result result);
|
||||
|
||||
#region DebugBuilds
|
||||
|
||||
void LogDebug(string message, Color? color = null);
|
||||
void LogDebugWarning(string message);
|
||||
void LogDebugError(string message);
|
||||
|
||||
#endregion
|
||||
|
||||
#region LegacyCompat_LuaCsLogger
|
||||
|
||||
public void HandleException(Exception ex, LuaCsMessageOrigin origin)
|
||||
{
|
||||
HandleException(ex, origin.ToString());
|
||||
}
|
||||
|
||||
public void LogError(string message, LuaCsMessageOrigin origin)
|
||||
{
|
||||
LogError(message);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
public enum LuaCsMessageOrigin
|
||||
{
|
||||
LuaCs,
|
||||
Unknown,
|
||||
LuaMod,
|
||||
CSharpMod,
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
namespace Barotrauma.LuaCs;
|
||||
|
||||
/// <summary>
|
||||
/// Provides access to data from the current <see cref="LuaCsSetup"/>.
|
||||
/// </summary>
|
||||
public interface ILuaCsInfoProvider : IService
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether C# plugin code is enabled.
|
||||
/// </summary>
|
||||
public bool IsCsEnabled { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether usernames are anonymized or show in logs.
|
||||
/// </summary>
|
||||
public bool HideUserNamesInLogs { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether file system caching is enabled.
|
||||
/// </summary>
|
||||
public bool UseCaching { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The current state of the Execution State Machine.
|
||||
/// </summary>
|
||||
public RunState CurrentRunState { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Returns the best-matching LuaCsForBarotrauma package (enabled list > localMods > WorkshopMods).
|
||||
/// </summary>
|
||||
public ContentPackage LuaCsForBarotraumaPackage { get; }
|
||||
}
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
#nullable enable
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using System.Threading.Tasks;
|
||||
using Barotrauma.LuaCs.Data;
|
||||
using MoonSharp.Interpreter;
|
||||
|
||||
namespace Barotrauma.LuaCs;
|
||||
|
||||
public interface ILuaScriptManagementService : IReusableService
|
||||
{
|
||||
/// <summary>
|
||||
/// The running <see cref="Script"/> instance, if available.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// It is recommended to avoid using this directly if another API is available for the intended purposes.
|
||||
/// </remarks>
|
||||
Script? InternalScript { get; }
|
||||
|
||||
object? GetGlobalTableValue(string tableName);
|
||||
FluentResults.Result<DynValue> DoString(string code);
|
||||
DynValue? CallFunctionSafe(object luaFunction, params object[] args);
|
||||
|
||||
/// <summary>
|
||||
/// Whether to enable/disable the file system caching for lua.
|
||||
/// </summary>
|
||||
/// <param name="useCaching"></param>
|
||||
void SetCachingPolicy(bool useCaching);
|
||||
|
||||
/// <summary>
|
||||
/// Parses and loads script sources (code) into a memory cache without executing it.
|
||||
/// </summary>
|
||||
/// <param name="resourcesInfo"></param>
|
||||
/// <returns></returns>
|
||||
// [Required]
|
||||
Task<FluentResults.Result> LoadScriptResourcesAsync(ImmutableArray<ILuaScriptResourceInfo> resourcesInfo);
|
||||
|
||||
/// <summary>
|
||||
/// Executes already loaded into memory scripts data, in the supplied order.
|
||||
/// </summary>
|
||||
/// <param name="executionOrder"></param>
|
||||
/// <returns></returns>
|
||||
// [Required]
|
||||
FluentResults.Result ExecuteLoadedScripts(ImmutableArray<ILuaScriptResourceInfo> executionOrder, bool enableSandbox);
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="package"></param>
|
||||
/// <returns></returns>
|
||||
// [Required]
|
||||
FluentResults.Result DisposePackageResources(ContentPackage package);
|
||||
|
||||
/// <summary>
|
||||
/// Calls dispose on, and clears active refs for, currently running scripts. Does not clear caches.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
FluentResults.Result UnloadActiveScripts();
|
||||
|
||||
/// <summary>
|
||||
/// Unloads all scripts and clears all caches/references.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
/// <remarks>May be functionally equivalent to <see cref="IReusableService.Reset"/></remarks>
|
||||
FluentResults.Result DisposeAllPackageResources();
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Threading.Tasks;
|
||||
using Barotrauma.LuaCs.Data;
|
||||
using Barotrauma.LuaCs;
|
||||
using FluentResults;
|
||||
|
||||
namespace Barotrauma.LuaCs;
|
||||
|
||||
public interface IModConfigService : IService
|
||||
{
|
||||
/// <summary>
|
||||
/// Loads or dynamically generates a <see cref="IModConfigInfo"/> for the given <see cref="ContentPackage"/>.
|
||||
/// <br/> Throws a <see cref="NullReferenceException"/> if the package is null.
|
||||
/// </summary>
|
||||
/// <param name="src"></param>
|
||||
/// <returns></returns>
|
||||
Task<Result<IModConfigInfo>> CreateConfigAsync([NotNull]ContentPackage src);
|
||||
Task<ImmutableArray<(ContentPackage Source, Result<IModConfigInfo> Config)>> CreateConfigsAsync(ImmutableArray<ContentPackage> src);
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
using System;
|
||||
using Barotrauma.LuaCs.Data;
|
||||
using Barotrauma.LuaCs;
|
||||
using Barotrauma.LuaCs.Compatibility;
|
||||
using Barotrauma.Networking;
|
||||
|
||||
namespace Barotrauma.LuaCs;
|
||||
|
||||
#if CLIENT
|
||||
public delegate void NetMessageReceived(IReadMessage netMessage);
|
||||
#elif SERVER
|
||||
internal delegate void NetMessageReceived(IReadMessage netMessage, Client connection);
|
||||
#endif
|
||||
|
||||
internal interface INetworkingService : IReusableService, ILuaCsNetworking, IEntityNetworkingService
|
||||
{
|
||||
bool IsActive { get; }
|
||||
bool IsSynchronized { get; }
|
||||
|
||||
IWriteMessage Start(string netId);
|
||||
IWriteMessage Start(Guid netId);
|
||||
void Receive(string netId, NetMessageReceived action);
|
||||
void Receive(Guid netId, NetMessageReceived action);
|
||||
#if SERVER
|
||||
void SendToClient(IWriteMessage netMessage, NetworkConnection connection = null, DeliveryMethod deliveryMethod = DeliveryMethod.Reliable);
|
||||
#elif CLIENT
|
||||
void SendToServer(IWriteMessage netMessage, DeliveryMethod deliveryMethod = DeliveryMethod.Reliable);
|
||||
#endif
|
||||
|
||||
}
|
||||
|
||||
public interface IEntityNetworkingService
|
||||
{
|
||||
Guid GetNetworkIdForInstance(INetworkSyncVar var);
|
||||
void RegisterNetVar(INetworkSyncVar netVar);
|
||||
void DeregisterNetVar(INetworkSyncVar netVar);
|
||||
void SendNetVar(INetworkSyncVar netVar);
|
||||
void SendNetVar(INetworkSyncVar netVar, NetworkConnection connection);
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Globalization;
|
||||
using System.Threading.Tasks;
|
||||
using Barotrauma.Extensions;
|
||||
using Barotrauma.LuaCs.Data;
|
||||
using FluentResults;
|
||||
|
||||
namespace Barotrauma.LuaCs;
|
||||
|
||||
public interface IPackageManagementService : IReusableService
|
||||
{
|
||||
public bool TryGetLoadedPackageByName(string name, out ContentPackage package);
|
||||
public FluentResults.Result LoadPackageInfo(ContentPackage package);
|
||||
public FluentResults.Result LoadPackagesInfo(ImmutableArray<ContentPackage> packages);
|
||||
public FluentResults.Result ExecuteLoadedPackages(ImmutableArray<ContentPackage> executionOrder, bool executeCsAssemblies);
|
||||
public FluentResults.Result SyncLoadedPackagesList(ImmutableArray<ContentPackage> packages);
|
||||
public FluentResults.Result StopRunningPackages();
|
||||
public FluentResults.Result UnloadPackage(ContentPackage package);
|
||||
public FluentResults.Result UnloadPackages(ImmutableArray<ContentPackage> packages);
|
||||
public FluentResults.Result UnloadAllPackages();
|
||||
public ImmutableArray<ContentPackage> GetAllLoadedPackages();
|
||||
public ImmutableArray<ContentPackage> GetLoadedUnrestrictedPackages();
|
||||
public bool IsPackageRunning(ContentPackage package);
|
||||
public bool IsAnyPackageLoaded();
|
||||
public bool IsAnyPackageRunning();
|
||||
public bool PackageContainsAnyRunnableResource(ContentPackage package);
|
||||
public Result<IModConfigInfo> GetModConfigForPackage(ContentPackage package);
|
||||
}
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Reflection;
|
||||
using Barotrauma.LuaCs.Data;
|
||||
using Microsoft.CodeAnalysis;
|
||||
|
||||
namespace Barotrauma.LuaCs;
|
||||
|
||||
public interface IPluginManagementService : IReusableService
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets all types in searched <see cref="IAssemblyLoaderService"/> that implement the type supplied.
|
||||
/// </summary>
|
||||
/// <param name="includeInterfaces"></param>
|
||||
/// <param name="includeAbstractTypes"></param>
|
||||
/// <param name="includeDefaultContext"></param>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <returns></returns>
|
||||
FluentResults.Result<ImmutableArray<Type>> GetImplementingTypes<T>(
|
||||
bool includeInterfaces = false,
|
||||
bool includeAbstractTypes = false,
|
||||
bool includeDefaultContext = true);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="ContentPackage"/> that contains the plugin type.
|
||||
/// </summary>
|
||||
/// <param name="ownerPackage"></param>
|
||||
/// <typeparam name="TPlugin"></typeparam>
|
||||
/// <returns></returns>
|
||||
bool TryGetPackageForPlugin<TPlugin>(out ContentPackage ownerPackage);
|
||||
|
||||
/// <summary>
|
||||
/// Tries to find the type given the fully qualified name and filters.
|
||||
/// </summary>
|
||||
/// <param name="typeName"></param>
|
||||
/// <param name="isByRefType"></param>
|
||||
/// <param name="includeInterfaces"></param>
|
||||
/// <param name="includeDefaultContext"></param>
|
||||
/// <returns></returns>
|
||||
Type GetType(string typeName, bool isByRefType = false, bool includeInterfaces = false, bool includeDefaultContext = true);
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="executionOrder"></param>
|
||||
/// <param name="excludeAlreadyRunningPackages"></param>
|
||||
/// <returns></returns>
|
||||
FluentResults.Result ActivatePluginInstances(ImmutableArray<ContentPackage> executionOrder, bool excludeAlreadyRunningPackages = true);
|
||||
|
||||
/// <summary>
|
||||
/// Loads the provided assembly resources in the order of their dependencies and intra-mod priority load order.
|
||||
/// </summary>
|
||||
/// <param name="resources"></param>
|
||||
/// <returns>Success/Failure and list of failed resources, if any.</returns>
|
||||
FluentResults.Result LoadAssemblyResources(ImmutableArray<IAssemblyResourceInfo> resources);
|
||||
|
||||
/// <summary>
|
||||
/// Unloads all managed <see cref="IAssemblyPlugin"/>, <see cref="Assembly"/>, and <see cref="IAssemblyLoaderService"/>s.
|
||||
/// </summary>
|
||||
/// <returns>Success of the operation. <br/><b>Note: does not guarantee .NET runtime assembly unloading success.</b></returns>
|
||||
FluentResults.Result UnloadManagedAssemblies();
|
||||
}
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Reflection;
|
||||
using Barotrauma.LuaCs.Data;
|
||||
|
||||
namespace Barotrauma.LuaCs;
|
||||
|
||||
public interface IPluginService : IReusableService
|
||||
{
|
||||
bool IsAssemblyLoaded(string friendlyName);
|
||||
/// <summary>
|
||||
/// Loads the assemblies for the given information
|
||||
/// </summary>
|
||||
/// <param name="assemblyResourcesInfo"></param>
|
||||
/// <param name="injectServices"></param>
|
||||
/// <param name="typeInstances"></param>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <returns></returns>
|
||||
FluentResults.Result LoadAndInstanceTypes<T>(IEnumerable<IAssemblyResourceInfo> assemblyResourcesInfo, bool injectServices, out ImmutableArray<T> typeInstances) where T : class, IAssemblyPlugin;
|
||||
FluentResults.Result<ImmutableArray<T>> GetLoadedPluginTypesInPackage<T>() where T : class, IAssemblyPlugin;
|
||||
/// <summary>
|
||||
/// Advances the loading/execution state of the plugin. IMPORTANT: You cannot set the execution state of plugins
|
||||
/// to 'Disposed'. You must instead call the 'DisposePlugins' method.
|
||||
/// </summary>
|
||||
/// <param name="newState"></param>
|
||||
/// <returns></returns>
|
||||
FluentResults.Result AdvancePluginStates(PluginRunState newState);
|
||||
|
||||
/// <summary>
|
||||
/// Disposes of all running plugins hosted by the service and releases their references to allow unloading.
|
||||
/// </summary>
|
||||
/// <returns>Success of the operation. Returns false if any plugin threw errors during disposal.</returns>
|
||||
FluentResults.Result DisposePlugins();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current plugin execution state.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
PluginRunState GetPluginRunState();
|
||||
}
|
||||
|
||||
public enum PluginRunState
|
||||
{
|
||||
Instanced=0,
|
||||
PreInitialization=1,
|
||||
Initialized=2,
|
||||
LoadingCompleted=3,
|
||||
Disposed=4
|
||||
}
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
using System.Collections.Immutable;
|
||||
|
||||
namespace Barotrauma.LuaCs;
|
||||
|
||||
public interface ISafeStorageService : IStorageService, ISafeStorageValidation { }
|
||||
|
||||
public interface ISafeStorageValidation
|
||||
{
|
||||
/// <summary>
|
||||
/// Checks the given file path to see if it can be read. This includes any permissions, whitelists and OS checks.
|
||||
/// </summary>
|
||||
/// <param name="path">The absolute path to the file.</param>
|
||||
/// <param name="readOnly">Whether to only check for read permissions only, or full RWM if false.</param>
|
||||
/// <param name="checkWhitelistOnly">Whether to only check if the file is safe to access, without checking accessibility at the OS level.</param>
|
||||
/// <returns>Whether the file is accessible.</returns>
|
||||
bool IsFileAccessible(string path, bool readOnly, bool checkWhitelistOnly = true);
|
||||
|
||||
/// <summary>
|
||||
/// Adds the given path to the specified whitelists.
|
||||
/// </summary>
|
||||
/// <param name="path">The path to the file, exactly as it will be passed to the Try(Load|Save) methods in <see cref="StorageService"/>.</param>
|
||||
/// <param name="readOnly">Whether to add it to the read whitelist only, or Read+Write whitelists.</param>
|
||||
void AddFileToWhitelist(string path, bool readOnly = true);
|
||||
|
||||
/// <summary>
|
||||
/// Adds the given collection of file paths to whitelists (Read|+Write)
|
||||
/// </summary>
|
||||
/// <param name="paths">The paths to the files, formatted exactly as it will be passed to the Try(Load|Save) methods in <see cref="StorageService"/>.</param>
|
||||
/// <param name="readOnly">Whether to add it to the read whitelist only, or Read+Write whitelists.</param>
|
||||
void AddFilesToWhitelist(ImmutableArray<string> paths, bool readOnly = true);
|
||||
|
||||
/// <summary>
|
||||
/// Removes the given path from all whitelists (Read|+Write).
|
||||
/// </summary>
|
||||
/// <param name="path"></param>
|
||||
void RemoveFileFromAllWhitelists(string path);
|
||||
|
||||
/// <summary>
|
||||
/// Sets the whitelist filtering for read-only file permissions for the instance. Overwrites previous list.
|
||||
/// </summary>
|
||||
/// <param name="filePaths">List of file paths allowed, as will be passed to the <see cref="StorageService"/> Try(Load|Save) methods.</param>
|
||||
FluentResults.Result SetReadOnlyWhitelist(ImmutableArray<string> filePaths);
|
||||
|
||||
/// <summary>
|
||||
/// Sets the whitelist filtering for read & write file permissions for the instance. Overwrites previous lists.
|
||||
/// </summary>
|
||||
/// <param name="filePaths">List of file paths allowed, as will be passed to the <see cref="StorageService"/> Try(Load|Save) methods.</param>
|
||||
FluentResults.Result SetReadWriteWhitelist(ImmutableArray<string> filePaths);
|
||||
|
||||
/// <summary>
|
||||
/// Deletes all paths from all white lists.
|
||||
/// </summary>
|
||||
void ClearAllWhitelists();
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
using System;
|
||||
using Microsoft.Toolkit.Diagnostics;
|
||||
|
||||
namespace Barotrauma.LuaCs;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a <see cref="IReusableService"/> that is automatically instantiated at startup for the lifetime of the
|
||||
/// <see cref="IServiceProvider"/> instance.
|
||||
/// </summary>
|
||||
public interface ISystem : IReusableService { }
|
||||
|
||||
/// <summary>
|
||||
/// Defines a service that can be reset to it's post-constructor state and reused without needing to be disposed.
|
||||
/// Intended for persistent services.
|
||||
/// </summary>
|
||||
public interface IReusableService : IService
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns the service to its original state (post-instantiation).
|
||||
/// Allows a service instance to be reused without disposing of the instance.
|
||||
/// </summary>
|
||||
FluentResults.Result Reset();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Base interface inherited by all services.
|
||||
/// </summary>
|
||||
/// <exception cref="ObjectDisposedException">Throws exception if `IsDisposed` return true.</exception>
|
||||
public interface IService : IDisposable
|
||||
{
|
||||
bool IsDisposed { get; }
|
||||
public void CheckDisposed()
|
||||
{
|
||||
if (IsDisposed)
|
||||
ThrowHelper.ThrowObjectDisposedException($"Tried to call method on disposed object '{this.GetType().Name}'!");
|
||||
}
|
||||
|
||||
static void CheckDisposed(IService service)
|
||||
{
|
||||
if (service.IsDisposed)
|
||||
ThrowHelper.ThrowObjectDisposedException($"Tried to call method on disposed object '{service.GetType().Name}'!");
|
||||
}
|
||||
}
|
||||
+118
@@ -0,0 +1,118 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using LightInject;
|
||||
|
||||
namespace Barotrauma.LuaCs;
|
||||
|
||||
/// <summary>
|
||||
/// Provides instancing and management of <see cref="IService"/>, <see cref="IReusableService"/>, and <see cref="ISystem"/>
|
||||
/// instances.
|
||||
/// </summary>
|
||||
public interface IServicesProvider
|
||||
{
|
||||
#region Type_Registration
|
||||
|
||||
/// <summary>
|
||||
/// Registers a type as a service for a given interface.
|
||||
/// </summary>
|
||||
/// <remarks>NOTE: <see cref="ISystem"/> services are forced to <see cref="ServiceLifetime.Singleton"/></remarks>
|
||||
/// <param name="lifetime">The <see cref="ServiceLifetime"/> of the service when requested.</param>
|
||||
/// <param name="lifetimeInstance">Custom lifetime instance.</param>
|
||||
/// <typeparam name="TSvcInterface">Service interface.</typeparam>
|
||||
/// <typeparam name="TService">Implementing service type.</typeparam>
|
||||
void RegisterServiceType<TSvcInterface, TService>(ServiceLifetime lifetime, ILifetime lifetimeInstance = null) where TSvcInterface : class, IService where TService : class, IService, TSvcInterface;
|
||||
|
||||
/// <summary>
|
||||
/// Registers a type as a service for a given interface that can be requested by name.
|
||||
/// </summary>
|
||||
/// <remarks>NOTE: <see cref="ISystem"/> services are forced to <see cref="ServiceLifetime.Singleton"/></remarks>
|
||||
/// <param name="name">Name of the service for lookup.</param>
|
||||
/// <param name="lifetime">The <see cref="ServiceLifetime"/> of the service when requested.</param>
|
||||
/// <param name="lifetimeInstance">Custom lifetime instance.</param>
|
||||
/// <typeparam name="TSvcInterface">Service interface.</typeparam>
|
||||
/// <typeparam name="TService">Implementing service type.</typeparam>
|
||||
void RegisterServiceType<TSvcInterface, TService>(string name, ServiceLifetime lifetime, ILifetime lifetimeInstance = null) where TSvcInterface : class, IService where TService : class, IService, TSvcInterface;
|
||||
|
||||
/// <summary>
|
||||
/// Registers a factory for resolving the service type.
|
||||
/// </summary>
|
||||
/// <param name="factory"></param>
|
||||
/// <typeparam name="TSvcInterface"></typeparam>
|
||||
void RegisterServiceResolver<TSvcInterface>(Func<ServiceContainer, TSvcInterface> factory) where TSvcInterface : class, IService;
|
||||
|
||||
/// <summary>
|
||||
/// Compiles/Generates IL for registered services and instantiates all registered <see cref="ISystem"/> types.
|
||||
/// </summary>
|
||||
public void CompileAndRun();
|
||||
|
||||
#endregion
|
||||
|
||||
#region Services_Instancing_Injection
|
||||
|
||||
/// <summary>
|
||||
/// Injects services into the properties of already instanced objects.
|
||||
/// </summary>
|
||||
/// <param name="inst"></param>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
void InjectServices<T>(T inst) where T : class;
|
||||
|
||||
/// <summary>
|
||||
/// Tries to get a service for the given interface, returns success/failure.
|
||||
/// </summary>
|
||||
/// <param name="service"></param>
|
||||
/// <typeparam name="TSvcInterface"></typeparam>
|
||||
/// <returns></returns>
|
||||
bool TryGetService<TSvcInterface>(out TSvcInterface service) where TSvcInterface : class, IService;
|
||||
|
||||
/// <summary>
|
||||
/// Tries to get a service for the given interface, throws an exception upon failure.
|
||||
/// </summary>
|
||||
/// <typeparam name="TSvcInterface"></typeparam>
|
||||
/// <returns></returns>
|
||||
TSvcInterface GetService<TSvcInterface>() where TSvcInterface : class, IService;
|
||||
|
||||
/// <summary>
|
||||
/// Tries to get a service for the given name and interface, returns success/failure.
|
||||
/// </summary>
|
||||
/// <param name="name"></param>
|
||||
/// <param name="service"></param>
|
||||
/// <typeparam name="TSvcInterface"></typeparam>
|
||||
/// <returns></returns>
|
||||
bool TryGetService<TSvcInterface>(string name, out TSvcInterface service) where TSvcInterface : class, IService;
|
||||
|
||||
/// <summary>
|
||||
/// Called whenever a new service is created/instanced.
|
||||
/// Args[0]: The interface type of the service.
|
||||
/// Args[1]: The instance of the service.
|
||||
/// </summary>
|
||||
event System.Action<Type, IService> OnServiceInstanced;
|
||||
|
||||
#endregion
|
||||
|
||||
#region ActiveServices
|
||||
|
||||
/// <summary>
|
||||
/// Returns all services for the given interface.
|
||||
/// </summary>
|
||||
/// <typeparam name="TSvc"></typeparam>
|
||||
/// <returns></returns>
|
||||
ImmutableArray<TSvc> GetAllServices<TSvc>() where TSvc : class, IService;
|
||||
|
||||
#endregion
|
||||
|
||||
// Notes: Left public due to the common use of Publicizers
|
||||
#region Internal_Use
|
||||
|
||||
/// <summary>
|
||||
/// Notes: Internal use only if hosted by LuaCsForBarotrauma. Disposes of all services and resets DI container. Warning: unable to dispose of services held by other objects.
|
||||
/// </summary>
|
||||
void DisposeAndReset();
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
public enum ServiceLifetime
|
||||
{
|
||||
Transient, Singleton, PerThread, Invalid, Custom
|
||||
}
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
using System;
|
||||
using System.Collections.Immutable;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Xml.Linq;
|
||||
using FluentResults;
|
||||
|
||||
namespace Barotrauma.LuaCs;
|
||||
|
||||
public interface IStorageService : IService
|
||||
{
|
||||
|
||||
bool UseCaching { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Deletes all cached file data.
|
||||
/// </summary>
|
||||
void PurgeCache();
|
||||
|
||||
/// <summary>
|
||||
/// Deletes the data for the supplied file path from the data cache.
|
||||
/// </summary>
|
||||
/// <param name="absolutePath"></param>
|
||||
void PurgeFileFromCache(string absolutePath);
|
||||
|
||||
/// <summary>
|
||||
/// Deletes the data from the supplied file paths from the data cache.
|
||||
/// </summary>
|
||||
/// <param name="absolutePaths"></param>
|
||||
void PurgeFilesFromCache(params string[] absolutePaths);
|
||||
|
||||
// -- local game folder storage
|
||||
FluentResults.Result<XDocument> LoadLocalXml(ContentPackage package, string localFilePath);
|
||||
FluentResults.Result<byte[]> LoadLocalBinary(ContentPackage package, string localFilePath);
|
||||
FluentResults.Result<string> LoadLocalText(ContentPackage package, string localFilePath);
|
||||
FluentResults.Result SaveLocalXml(ContentPackage package, string localFilePath, XDocument document);
|
||||
FluentResults.Result SaveLocalBinary(ContentPackage package, string localFilePath, in byte[] bytes);
|
||||
FluentResults.Result SaveLocalText(ContentPackage package, string localFilePath, in string text);
|
||||
// async
|
||||
Task<FluentResults.Result<XDocument>> LoadLocalXmlAsync(ContentPackage package, string localFilePath);
|
||||
Task<FluentResults.Result<byte[]>> LoadLocalBinaryAsync(ContentPackage package, string localFilePath);
|
||||
Task<FluentResults.Result<string>> LoadLocalTextAsync(ContentPackage package, string localFilePath);
|
||||
Task<FluentResults.Result> SaveLocalXmlAsync(ContentPackage package, string localFilePath, XDocument document);
|
||||
Task<FluentResults.Result> SaveLocalBinaryAsync(ContentPackage package, string localFilePath, byte[] bytes);
|
||||
Task<FluentResults.Result> SaveLocalTextAsync(ContentPackage package, string localFilePath, string text);
|
||||
|
||||
// -- package directory
|
||||
// singles
|
||||
Result<XDocument> LoadPackageXml(ContentPath filePath);
|
||||
Result<byte[]> LoadPackageBinary(ContentPath filePath);
|
||||
Result<string> LoadPackageText(ContentPath filePath);
|
||||
// collections
|
||||
ImmutableArray<(ContentPath, Result<XDocument>)> LoadPackageXmlFiles(ImmutableArray<ContentPath> filePaths);
|
||||
ImmutableArray<(ContentPath, Result<byte[]>)> LoadPackageBinaryFiles(ImmutableArray<ContentPath> filePaths);
|
||||
ImmutableArray<(ContentPath, Result<string>)> LoadPackageTextFiles(ImmutableArray<ContentPath> filePaths);
|
||||
FluentResults.Result<ImmutableArray<string>> FindFilesInPackage(ContentPackage package, string localSubfolder, string regexFilter, bool searchRecursively);
|
||||
// async
|
||||
// singles
|
||||
Task<Result<XDocument>> LoadPackageXmlAsync(ContentPath filePath);
|
||||
Task<Result<byte[]>> LoadPackageBinaryAsync(ContentPath filePath);
|
||||
Task<Result<string>> LoadPackageTextAsync(ContentPath filePath);
|
||||
// collections
|
||||
Task<ImmutableArray<(ContentPath, Result<XDocument>)>> LoadPackageXmlFilesAsync(ImmutableArray<ContentPath> filePaths);
|
||||
Task<ImmutableArray<(ContentPath, Result<byte[]>)>> LoadPackageBinaryFilesAsync(ImmutableArray<ContentPath> filePaths);
|
||||
Task<ImmutableArray<(ContentPath, Result<string>)>> LoadPackageTextFilesAsync(ImmutableArray<ContentPath> filePaths);
|
||||
|
||||
// -- absolute paths
|
||||
FluentResults.Result<XDocument> TryLoadXml(string filePath, Encoding encoding = null);
|
||||
FluentResults.Result<string> TryLoadText(string filePath, Encoding encoding = null);
|
||||
FluentResults.Result<byte[]> TryLoadBinary(string filePath);
|
||||
FluentResults.Result TrySaveXml(string filePath, in XDocument document, Encoding encoding = null);
|
||||
FluentResults.Result TrySaveText(string filePath, in string text, Encoding encoding = null);
|
||||
FluentResults.Result TrySaveBinary(string filePath, in byte[] bytes);
|
||||
FluentResults.Result<bool> FileExists(string filePath);
|
||||
FluentResults.Result<bool> DirectoryExists(string directoryPath);
|
||||
|
||||
//async
|
||||
Task<FluentResults.Result<XDocument>> TryLoadXmlAsync(string filePath, Encoding encoding = null);
|
||||
Task<FluentResults.Result<string>> TryLoadTextAsync(string filePath, Encoding encoding = null);
|
||||
Task<FluentResults.Result<byte[]>> TryLoadBinaryAsync(string filePath);
|
||||
Task<FluentResults.Result> TrySaveXmlAsync(string filePath, XDocument document, Encoding encoding = null);
|
||||
Task<FluentResults.Result> TrySaveTextAsync(string filePath, string text, Encoding encoding = null);
|
||||
Task<FluentResults.Result> TrySaveBinaryAsync(string filePath, byte[] bytes);
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
using Barotrauma.LuaCs.Data;
|
||||
using Barotrauma.Networking;
|
||||
using MoonSharp.Interpreter;
|
||||
using MoonSharp.Interpreter.Interop;
|
||||
using MoonSharp.Interpreter.Interop.BasicDescriptors;
|
||||
using Sigil;
|
||||
using System.Collections.Generic;
|
||||
using System.Data;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Barotrauma.LuaCs;
|
||||
|
||||
public interface IDefaultLuaRegistrar : IService
|
||||
{
|
||||
public void RegisterAll();
|
||||
}
|
||||
|
||||
public class DefaultLuaRegistrar : IDefaultLuaRegistrar
|
||||
{
|
||||
public bool IsDisposed { get; private set; }
|
||||
|
||||
private readonly ILuaUserDataService _userDataService;
|
||||
private readonly ISafeLuaUserDataService _safeUserDataService;
|
||||
private readonly ILoggerService _loggerService;
|
||||
|
||||
private class SteamIDMemberDescriptor : IMemberDescriptor
|
||||
{
|
||||
public bool IsStatic => false;
|
||||
|
||||
public string Name => "SteamID";
|
||||
|
||||
public MemberDescriptorAccess MemberAccess => MemberDescriptorAccess.CanRead;
|
||||
|
||||
public DynValue GetValue(Script script, object obj)
|
||||
{
|
||||
if (obj is Client client)
|
||||
{
|
||||
return DynValue.FromObject(script, ModUtils.Client.GetSteamId(client));
|
||||
}
|
||||
|
||||
throw new System.NotImplementedException();
|
||||
}
|
||||
|
||||
public void SetValue(Script script, object obj, DynValue value)
|
||||
{
|
||||
throw new System.NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
||||
public DefaultLuaRegistrar(ILoggerService loggerService, ILuaUserDataService userDataService, ISafeLuaUserDataService safeUserDataService)
|
||||
{
|
||||
_userDataService = userDataService;
|
||||
_safeUserDataService = safeUserDataService;
|
||||
_loggerService = loggerService;
|
||||
}
|
||||
|
||||
private void RegisterShared()
|
||||
{
|
||||
_userDataService.RegisterType("System.TimeSpan");
|
||||
_userDataService.RegisterType("System.Exception");
|
||||
_userDataService.RegisterType("System.Console");
|
||||
_userDataService.RegisterType("System.Exception");
|
||||
|
||||
_userDataService.RegisterType("Barotrauma.Success`2");
|
||||
_userDataService.RegisterType("Barotrauma.Failure`2");
|
||||
_userDataService.RegisterType("Barotrauma.Range`1");
|
||||
_userDataService.RegisterType("Barotrauma.ItemPrefab");
|
||||
|
||||
_userDataService.RegisterType("Barotrauma.InputType");
|
||||
|
||||
List<Assembly> assembliesToScan = [typeof(DefaultLuaRegistrar).Assembly, typeof(Identifier).Assembly, typeof(Microsoft.Xna.Framework.Vector2).Assembly];
|
||||
|
||||
foreach (var type in assembliesToScan.SelectMany(a => a.GetTypes()))
|
||||
{
|
||||
if (type.IsEnum || type.Name.StartsWith("<") || type.IsDefined(typeof(CompilerGeneratedAttribute)) || !_safeUserDataService.IsAllowed(type.FullName))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
_userDataService.RegisterType(type.FullName);
|
||||
}
|
||||
|
||||
_userDataService.RegisterType("Barotrauma.LuaSByte");
|
||||
_userDataService.RegisterType("Barotrauma.LuaByte");
|
||||
_userDataService.RegisterType("Barotrauma.LuaInt16");
|
||||
_userDataService.RegisterType("Barotrauma.LuaUInt16");
|
||||
_userDataService.RegisterType("Barotrauma.LuaInt32");
|
||||
_userDataService.RegisterType("Barotrauma.LuaUInt32");
|
||||
_userDataService.RegisterType("Barotrauma.LuaInt64");
|
||||
_userDataService.RegisterType("Barotrauma.LuaUInt64");
|
||||
_userDataService.RegisterType("Barotrauma.LuaSingle");
|
||||
_userDataService.RegisterType("Barotrauma.LuaDouble");
|
||||
|
||||
_userDataService.RegisterType("Barotrauma.Level+InterestingPosition");
|
||||
_userDataService.RegisterType("Barotrauma.Networking.RespawnManager+TeamSpecificState");
|
||||
|
||||
_userDataService.RegisterType("Barotrauma.CharacterParams+AIParams");
|
||||
_userDataService.RegisterType("Barotrauma.CharacterParams+TargetParams");
|
||||
_userDataService.RegisterType("Barotrauma.CharacterParams+InventoryParams");
|
||||
_userDataService.RegisterType("Barotrauma.CharacterParams+HealthParams");
|
||||
_userDataService.RegisterType("Barotrauma.CharacterParams+ParticleParams");
|
||||
_userDataService.RegisterType("Barotrauma.CharacterParams+SoundParams");
|
||||
|
||||
_userDataService.RegisterType("Barotrauma.FabricationRecipe+RequiredItemByIdentifier");
|
||||
_userDataService.RegisterType("Barotrauma.FabricationRecipe+RequiredItemByTag");
|
||||
|
||||
_userDataService.MakeFieldAccessible(_userDataService.RegisterType("Barotrauma.StatusEffect"), "user");
|
||||
|
||||
|
||||
_userDataService.RegisterType("Barotrauma.ContentPackageManager+PackageSource");
|
||||
_userDataService.RegisterType("Barotrauma.ContentPackageManager+EnabledPackages");
|
||||
|
||||
_userDataService.RegisterType("System.Xml.Linq.XElement");
|
||||
_userDataService.RegisterType("System.Xml.Linq.XName");
|
||||
_userDataService.RegisterType("System.Xml.Linq.XAttribute");
|
||||
_userDataService.RegisterType("System.Xml.Linq.XContainer");
|
||||
_userDataService.RegisterType("System.Xml.Linq.XDocument");
|
||||
_userDataService.RegisterType("System.Xml.Linq.XNode");
|
||||
|
||||
|
||||
_userDataService.RegisterType("Barotrauma.Networking.ServerSettings+SavedClientPermission");
|
||||
_userDataService.RegisterType("Barotrauma.Inventory+ItemSlot");
|
||||
|
||||
|
||||
_userDataService.MakeFieldAccessible(_userDataService.RegisterType("Barotrauma.Items.Components.CustomInterface"), "customInterfaceElementList");
|
||||
_userDataService.RegisterType("Barotrauma.Items.Components.CustomInterface+CustomInterfaceElement");
|
||||
|
||||
_userDataService.RegisterType("Barotrauma.DebugConsole+Command");
|
||||
|
||||
{
|
||||
var descriptor = _userDataService.RegisterType("Barotrauma.NetLobbyScreen");
|
||||
|
||||
#if SERVER
|
||||
_userDataService.MakeFieldAccessible(descriptor, "subs");
|
||||
#endif
|
||||
}
|
||||
|
||||
_userDataService.RegisterType("FarseerPhysics.Dynamics.Body");
|
||||
_userDataService.RegisterType("FarseerPhysics.Dynamics.World");
|
||||
_userDataService.RegisterType("FarseerPhysics.Dynamics.Fixture");
|
||||
_userDataService.RegisterType("FarseerPhysics.ConvertUnits");
|
||||
_userDataService.RegisterType("FarseerPhysics.Collision.AABB");
|
||||
_userDataService.RegisterType("FarseerPhysics.Collision.ContactFeature");
|
||||
_userDataService.RegisterType("FarseerPhysics.Collision.ManifoldPoint");
|
||||
_userDataService.RegisterType("FarseerPhysics.Collision.ContactID");
|
||||
_userDataService.RegisterType("FarseerPhysics.Collision.Manifold");
|
||||
_userDataService.RegisterType("FarseerPhysics.Collision.RayCastInput");
|
||||
_userDataService.RegisterType("FarseerPhysics.Collision.ClipVertex");
|
||||
_userDataService.RegisterType("FarseerPhysics.Collision.RayCastOutput");
|
||||
_userDataService.RegisterType("FarseerPhysics.Collision.EPAxis");
|
||||
_userDataService.RegisterType("FarseerPhysics.Collision.ReferenceFace");
|
||||
_userDataService.RegisterType("FarseerPhysics.Collision.Collision");
|
||||
|
||||
_userDataService.RegisterType("Voronoi2.DoubleVector2");
|
||||
_userDataService.RegisterType("Voronoi2.Site");
|
||||
_userDataService.RegisterType("Voronoi2.Edge");
|
||||
_userDataService.RegisterType("Voronoi2.Halfedge");
|
||||
_userDataService.RegisterType("Voronoi2.VoronoiCell");
|
||||
_userDataService.RegisterType("Voronoi2.GraphEdge");
|
||||
|
||||
_userDataService.RegisterType("Barotrauma.PrefabCollection`1");
|
||||
_userDataService.RegisterType("Barotrauma.PrefabSelector`1");
|
||||
_userDataService.RegisterType("Barotrauma.Pair`2");
|
||||
|
||||
_userDataService.RegisterExtensionType("Barotrauma.MathUtils");
|
||||
_userDataService.RegisterExtensionType("Barotrauma.XMLExtensions");
|
||||
|
||||
var itemPrefabDescriptor = (StandardUserDataDescriptor)_userDataService.RegisterType("Barotrauma.ItemPrefab");
|
||||
itemPrefabDescriptor.AddMember("GetItemPrefab", new MethodMemberDescriptor(typeof(ModUtils.ItemPrefab).GetMethod(nameof(ModUtils.ItemPrefab.GetItemPrefab), BindingFlags.NonPublic | BindingFlags.Static)));
|
||||
|
||||
var clientDescriptor = (StandardUserDataDescriptor)_userDataService.RegisterType("Barotrauma.Networking.Client");
|
||||
clientDescriptor.AddMember("ClientList", new PropertyMemberDescriptor(typeof(ModUtils.Client).GetProperty(nameof(ModUtils.Client.ClientList), BindingFlags.NonPublic | BindingFlags.Static), InteropAccessMode.LazyOptimized));
|
||||
clientDescriptor.AddMember("SteamID", new SteamIDMemberDescriptor());
|
||||
|
||||
|
||||
#if SERVER
|
||||
clientDescriptor.AddMember("UnbanPlayer", new MethodMemberDescriptor(typeof(ModUtils.Client).GetMethod(nameof(ModUtils.Client.UnbanPlayer), BindingFlags.NonPublic | BindingFlags.Static), InteropAccessMode.LazyOptimized));
|
||||
clientDescriptor.AddMember("BanPlayer", new MethodMemberDescriptor(typeof(ModUtils.Client).GetMethod(nameof(ModUtils.Client.BanPlayer), BindingFlags.NonPublic | BindingFlags.Static), InteropAccessMode.LazyOptimized));
|
||||
#endif
|
||||
|
||||
_userDataService.RegisterExtensionType(typeof(ClientExtensions).FullName);
|
||||
_userDataService.RegisterExtensionType(typeof(ItemExtensions).FullName);
|
||||
_userDataService.RegisterExtensionType(typeof(MapEntityExtensions).FullName);
|
||||
_userDataService.RegisterExtensionType(typeof(QualityExtensions).FullName);
|
||||
|
||||
|
||||
var toolBox = UserData.RegisterType(typeof(ToolBox));
|
||||
#if CLIENT
|
||||
_userDataService.RemoveMember(toolBox, "OpenFileWithShell");
|
||||
#endif
|
||||
}
|
||||
|
||||
#if CLIENT
|
||||
private void RegisterClient()
|
||||
{
|
||||
_userDataService.RegisterType("Microsoft.Xna.Framework.Graphics.Effect");
|
||||
_userDataService.RegisterType("Microsoft.Xna.Framework.Graphics.EffectParameterCollection");
|
||||
_userDataService.RegisterType("Microsoft.Xna.Framework.Graphics.EffectParameter");
|
||||
|
||||
_userDataService.RegisterType("Microsoft.Xna.Framework.Graphics.SpriteBatch");
|
||||
_userDataService.RegisterType("Microsoft.Xna.Framework.Graphics.Texture2D");
|
||||
_userDataService.RegisterType("EventInput.KeyboardDispatcher");
|
||||
_userDataService.RegisterType("EventInput.KeyEventArgs");
|
||||
_userDataService.RegisterType("Microsoft.Xna.Framework.Input.Keys");
|
||||
_userDataService.RegisterType("Microsoft.Xna.Framework.Input.KeyboardState");
|
||||
|
||||
_userDataService.RegisterType("Barotrauma.Anchor");
|
||||
_userDataService.RegisterType("Barotrauma.Alignment");
|
||||
_userDataService.RegisterType("Barotrauma.Pivot");
|
||||
_userDataService.RegisterType("Barotrauma.Key");
|
||||
_userDataService.RegisterType("Barotrauma.PlayerInput");
|
||||
|
||||
|
||||
_userDataService.RegisterType("Barotrauma.Inventory+SlotReference");
|
||||
}
|
||||
#elif SERVER
|
||||
private void RegisterServer()
|
||||
{
|
||||
_userDataService.RegisterType("Barotrauma.Character+TeamChangeEventData");
|
||||
}
|
||||
#endif
|
||||
|
||||
public void RegisterAll()
|
||||
{
|
||||
RegisterShared();
|
||||
#if CLIENT
|
||||
RegisterClient();
|
||||
#elif SERVER
|
||||
RegisterServer();
|
||||
#endif
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
IsDisposed = true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
using System.Collections.Generic;
|
||||
using Barotrauma.LuaCs.Data;
|
||||
using Microsoft.Xna.Framework;
|
||||
|
||||
namespace Barotrauma.LuaCs;
|
||||
|
||||
public interface ILuaConfigService : ILuaService
|
||||
{
|
||||
FluentResults.Result LoadSavedValueForConfig(ISettingBase setting);
|
||||
bool TryGetConfig<T>(ContentPackage package, string internalName, out T instance) where T : ISettingBase;
|
||||
FluentResults.Result SaveConfigValue(ISettingBase setting);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
using MoonSharp.Interpreter;
|
||||
|
||||
namespace Barotrauma.LuaCs;
|
||||
|
||||
/// <summary>
|
||||
/// Service for providing stateful functions and in-memory storage for lua functions
|
||||
/// </summary>
|
||||
public interface ILuaDataService : ILuaService
|
||||
{
|
||||
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Barotrauma.LuaCs.Events;
|
||||
using Barotrauma.LuaCs.Compatibility;
|
||||
|
||||
namespace Barotrauma.LuaCs;
|
||||
|
||||
public interface ILuaSafeEventService : ILuaService, ILuaCsHook
|
||||
{
|
||||
/// <summary>
|
||||
/// Subscribes lua scripts via <see cref="ImpromptuInterface"/> for the given <see cref="IEvent{T}"/> interface.
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <param name="identifier"></param>
|
||||
/// <param name="callbacks">A 'method name'=='signature action' dictionary matching the interface method list.</param>
|
||||
void Subscribe<T>(string identifier, IDictionary<string, LuaCsFunc> callbacks) where T : class, IEvent<T>;
|
||||
/// <summary>
|
||||
/// Removes a subscriber from an event that subscribed under the given identifier.
|
||||
/// </summary>
|
||||
/// <param name="eventName"></param>
|
||||
/// <param name="identifier"></param>
|
||||
void Unsubscribe(string eventName, string identifier);
|
||||
/// <summary>
|
||||
/// Send an event to all subscribers to an interface.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Interface type.</typeparam>
|
||||
/// <param name="subscriberRunner">Execution runner, the subscriber is provided as the first argument in the lua runner.</param>
|
||||
/// <returns></returns>
|
||||
void PublishLuaEvent<T>(LuaCsFunc subscriberRunner) where T : class, IEvent<T>;
|
||||
|
||||
/// <summary>
|
||||
/// Defines the target method name for legacy <see cref="ILuaCsHook.Add(string, LuaCsFunc)"/> to target on new <see cref="IEvent{T}"/>
|
||||
/// interfaces.
|
||||
/// </summary>
|
||||
/// <param name="luaEventName">The <see cref="ILuaCsHook.Add(string, LuaCsFunc)"/> legacy event name.</param>
|
||||
/// <param name="targetMethod">.</param>
|
||||
/// <typeparam name="T">The event interface type.</typeparam>
|
||||
/// <returns>Operation success.</returns>
|
||||
/// <exception cref="ArgumentNullException">The <see cref="luaEventName"/> is <b>null or empty.</b></exception>
|
||||
public FluentResults.Result RegisterLuaEventAlias<T>(string luaEventName, string targetMethod) where T : class, IEvent<T>;
|
||||
}
|
||||
|
||||
public interface ILuaEventService : ILuaSafeEventService
|
||||
{
|
||||
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace Barotrauma.LuaCs;
|
||||
|
||||
public interface ILuaNetworkingService : ILuaService
|
||||
{
|
||||
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
namespace Barotrauma.LuaCs;
|
||||
|
||||
public interface ILuaPackageManagementService : ILuaService
|
||||
{
|
||||
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace Barotrauma.LuaCs;
|
||||
|
||||
public interface ILuaPackageService : ILuaService
|
||||
{
|
||||
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
using Barotrauma.LuaCs.Compatibility;
|
||||
using System;
|
||||
using System.Reflection;
|
||||
using static Barotrauma.LuaCs.Compatibility.ILuaCsHook;
|
||||
using LuaCsCompatPatchFunc = Barotrauma.LuaCsPatch;
|
||||
|
||||
namespace Barotrauma.LuaCs;
|
||||
|
||||
public interface ILuaPatcher : IReusableService
|
||||
{
|
||||
string Patch(string identifier, string className, string methodName, string[] parameterTypes, LuaCsPatchFunc patch, HookMethodType hookType = HookMethodType.Before);
|
||||
string Patch(string identifier, string className, string methodName, LuaCsPatchFunc patch, HookMethodType hookType = HookMethodType.Before);
|
||||
string Patch(string className, string methodName, string[] parameterTypes, LuaCsPatchFunc patch, HookMethodType hookType = HookMethodType.Before);
|
||||
string Patch(string className, string methodName, LuaCsPatchFunc patch, HookMethodType hookType = HookMethodType.Before);
|
||||
bool RemovePatch(string identifier, string className, string methodName, string[] parameterTypes, HookMethodType hookType);
|
||||
bool RemovePatch(string identifier, string className, string methodName, HookMethodType hookType);
|
||||
|
||||
void HookMethod(string identifier, MethodBase method, LuaCsCompatPatchFunc patch, HookMethodType hookType = HookMethodType.Before, IAssemblyPlugin owner = null);
|
||||
public void HookMethod(string identifier, string className, string methodName, string[] parameterNames, LuaCsCompatPatchFunc patch, ILuaCsHook.HookMethodType hookMethodType = ILuaCsHook.HookMethodType.Before);
|
||||
public void HookMethod(string identifier, string className, string methodName, LuaCsCompatPatchFunc patch, ILuaCsHook.HookMethodType hookMethodType = ILuaCsHook.HookMethodType.Before);
|
||||
public void HookMethod(string className, string methodName, LuaCsCompatPatchFunc patch, ILuaCsHook.HookMethodType hookMethodType = ILuaCsHook.HookMethodType.Before);
|
||||
public void HookMethod(string className, string methodName, string[] parameterNames, LuaCsCompatPatchFunc patch, ILuaCsHook.HookMethodType hookMethodType = ILuaCsHook.HookMethodType.Before);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
using System.Collections.Immutable;
|
||||
using System.Threading.Tasks;
|
||||
using Barotrauma.LuaCs.Data;
|
||||
using FluentResults;
|
||||
using MoonSharp.Interpreter.Loaders;
|
||||
|
||||
namespace Barotrauma.LuaCs;
|
||||
|
||||
public interface ILuaScriptLoader : IService, IScriptLoader, ISafeStorageValidation
|
||||
{
|
||||
void ClearCaches();
|
||||
/// <summary>
|
||||
/// Whether caching is enabled/disabled.
|
||||
/// </summary>
|
||||
/// <param name="useCaching"></param>
|
||||
void SetCachingPolicy(bool useCaching);
|
||||
Task<Result<ImmutableArray<(ContentPath Path, Result<string>)>>> CacheResourcesAsync(ImmutableArray<ILuaScriptResourceInfo> resourceInfos);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace Barotrauma.LuaCs;
|
||||
|
||||
public interface ILuaService : IService
|
||||
{
|
||||
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user