Merge branch 'heads/upstream' into OBT/1.2.0(SpringUpdate)

This commit is contained in:
NotAlwaysTrue
2026-04-25 13:08:16 +08:00
420 changed files with 24089 additions and 11191 deletions
@@ -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
{
}
@@ -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
{
}
@@ -0,0 +1,418 @@
using System;
using MoonSharp.Interpreter;
using Microsoft.Xna.Framework;
using FarseerPhysics.Dynamics;
using LuaCsCompatPatchFunc = Barotrauma.LuaCsPatch;
using Barotrauma.Networking;
using System.Collections.Immutable;
using Barotrauma.LuaCs;
namespace Barotrauma
{
public class LuaConverters
{
private readonly ILuaScriptManagementService _luaScriptManagementService;
public LuaConverters(ILuaScriptManagementService luaScriptManagementService)
{
_luaScriptManagementService = luaScriptManagementService;
}
private DynValue Call(object function, params object[] arguments) => _luaScriptManagementService.CallFunctionSafe(function, arguments);
public void RegisterLuaConverters()
{
RegisterAction<Item>();
RegisterAction<Character>();
RegisterAction<Character, Character>();
RegisterAction<Entity>();
RegisterAction<float>();
RegisterAction();
RegisterFunc<Fixture, Vector2, Vector2, float, float>();
RegisterFunc<AIObjective, bool>();
Script.GlobalOptions.CustomConverters.SetScriptToClrCustomConversion(DataType.Function, typeof(LuaCsAction), v => (LuaCsAction)(args =>
{
if (v.Function.OwnerScript == _luaScriptManagementService.InternalScript)
{
Call(v.Function, args);
}
}));
Script.GlobalOptions.CustomConverters.SetScriptToClrCustomConversion(DataType.Function, typeof(LuaCsFunc), v => (LuaCsFunc)(args =>
{
if (v.Function.OwnerScript == _luaScriptManagementService.InternalScript)
{
return Call(v.Function, args);
}
return default;
}));
Script.GlobalOptions.CustomConverters.SetScriptToClrCustomConversion(DataType.Function, typeof(LuaCsCompatPatchFunc), v => (LuaCsCompatPatchFunc)((self, args) =>
{
if (v.Function.OwnerScript == _luaScriptManagementService.InternalScript)
{
return Call(v.Function, self, args);
}
return default;
}));
Script.GlobalOptions.CustomConverters.SetScriptToClrCustomConversion(DataType.Function, typeof(LuaCsPatchFunc), v => (LuaCsPatchFunc)((self, args) =>
{
if (v.Function.OwnerScript == _luaScriptManagementService.InternalScript)
{
return Call(v.Function, self, args);
}
return default;
}));
void RegisterHandler<T>(Func<Closure, T> converter) => Script.GlobalOptions.CustomConverters.SetScriptToClrCustomConversion(DataType.Function, typeof(T), v => converter(v.Function));
RegisterHandler(f => (Character.OnDeathHandler)((a1, a2) => Call(f, a1, a2)));
RegisterHandler(f => (Character.OnAttackedHandler)((a1, a2) => Call(f, a1, a2)));
#if CLIENT
RegisterAction<Microsoft.Xna.Framework.Graphics.SpriteBatch, GUICustomComponent>();
RegisterAction<float, Microsoft.Xna.Framework.Graphics.SpriteBatch>();
RegisterAction<Microsoft.Xna.Framework.Graphics.SpriteBatch, float>();
{
RegisterHandler(f => (GUIComponent.SecondaryButtonDownHandler)(
(a1, a2) => Call(f, a1, a2)?.CastToBool() ?? default));
RegisterHandler(f => (GUIButton.OnClickedHandler)(
(a1, a2) => Call(f, a1, a2)?.CastToBool() ?? default));
RegisterHandler(f => (GUIButton.OnButtonDownHandler)(
() => Call(f)?.CastToBool() ?? default));
RegisterHandler(f => (GUIButton.OnPressedHandler)(
() => Call(f)?.CastToBool() ?? default));
RegisterHandler(f => (GUIColorPicker.OnColorSelectedHandler)(
(a1, a2) => Call(f, a1, a2)?.CastToBool() ?? default));
RegisterHandler(f => (GUIDropDown.OnSelectedHandler)(
(a1, a2) => Call(f, a1, a2)?.CastToBool() ?? default));
RegisterHandler(f => (GUIListBox.OnSelectedHandler)(
(a1, a2) => Call(f, a1, a2)?.CastToBool() ?? default));
RegisterHandler(f => (GUIListBox.OnRearrangedHandler)(
(a1, a2) => Call(f, a1, a2)));
RegisterHandler(f => (GUIListBox.CheckSelectedHandler)(
() => Call(f)?.ToObject() ?? default));
RegisterHandler(f => (GUINumberInput.OnValueEnteredHandler)(
(a1) => Call(f, a1)));
RegisterHandler(f => (GUINumberInput.OnValueChangedHandler)(
(a1) => Call(f, a1)));
RegisterHandler(f => (GUIProgressBar.ProgressGetterHandler)(
() => (float)(Call(f)?.CastToNumber() ?? default)));
RegisterHandler(f => (GUIRadioButtonGroup.RadioButtonGroupDelegate)(
(a1, a2) => Call(f, a1, a2)));
RegisterHandler(f => (GUIScrollBar.OnMovedHandler)(
(a1, a2) => Call(f, a1, a2)?.CastToBool() ?? default));
RegisterHandler(f => (GUIScrollBar.ScrollConversion)(
(a1, a2) => (float)(Call(f, a1, a2)?.CastToNumber() ?? default)));
RegisterHandler(f => (GUITextBlock.TextGetterHandler)(
() => Call(f, new object[0])?.CastToString() ?? default));
RegisterHandler(f => (GUITextBox.OnEnterHandler)(
(a1, a2) => Call(f, a1, a2)?.CastToBool() ?? default));
RegisterHandler(f => (GUITextBox.OnTextChangedHandler)(
(a1, a2) => Call(f, a1, a2)?.CastToBool() ?? default));
RegisterHandler(f => (TextBoxEvent)(
(a1, a2) => Call(f, a1, a2)));
RegisterHandler(f => (GUITickBox.OnSelectedHandler)(
(a1) => Call(f, a1)?.CastToBool() ?? default));
RegisterHandler(f => (GUITextBlock.ClickableArea.OnClickDelegate)(
(a1, a2) => Call(f, a1, a2)));
}
Script.GlobalOptions.CustomConverters.SetScriptToClrCustomConversion(DataType.Function, typeof(NetMessageReceived), v => (NetMessageReceived)((arg1) =>
{
if (v.Function.OwnerScript == _luaScriptManagementService.InternalScript)
{
Call(v.Function, arg1);
}
}));
#elif SERVER
Script.GlobalOptions.CustomConverters.SetScriptToClrCustomConversion(DataType.Function, typeof(NetMessageReceived), v => (NetMessageReceived)((arg1, arg2) =>
{
if (v.Function.OwnerScript == _luaScriptManagementService.InternalScript)
{
Call(v.Function, arg1, arg2);
}
}));
#endif
Script.GlobalOptions.CustomConverters.SetScriptToClrCustomConversion(DataType.Table, typeof(Pair<JobPrefab, int>), v =>
{
return new Pair<JobPrefab, int>((JobPrefab)v.Table.Get(1).ToObject(), (int)v.Table.Get(2).CastToNumber());
});
Script.GlobalOptions.CustomConverters.SetClrToScriptCustomConversion<ulong>((Script script, ulong v) =>
{
return DynValue.NewString(v.ToString());
});
Script.GlobalOptions.CustomConverters.SetScriptToClrCustomConversion(DataType.String, typeof(ulong), v =>
{
return ulong.Parse(v.String);
});
Script.GlobalOptions.CustomConverters.SetScriptToClrCustomConversion(
scriptDataType: DataType.UserData,
clrDataType: typeof(sbyte),
canConvert: luaValue => luaValue.UserData?.Object is LuaSByte,
converter: luaValue => luaValue.UserData.Object is LuaSByte v
? (sbyte)v
: throw new ScriptRuntimeException("use SByte(value) to pass primitive type 'sbyte' to C#"));
Script.GlobalOptions.CustomConverters.SetScriptToClrCustomConversion(
scriptDataType: DataType.UserData,
clrDataType: typeof(byte),
canConvert: luaValue => luaValue.UserData?.Object is LuaByte,
converter: luaValue => luaValue.UserData.Object is LuaByte v
? (byte)v
: throw new ScriptRuntimeException("use Byte(value) to pass primitive type 'byte' to C#"));
Script.GlobalOptions.CustomConverters.SetScriptToClrCustomConversion(
scriptDataType: DataType.UserData,
clrDataType: typeof(short),
canConvert: luaValue => luaValue.UserData?.Object is LuaInt16,
converter: luaValue => luaValue.UserData.Object is LuaInt16 v
? (short)v
: throw new ScriptRuntimeException("use Int16(value) to pass primitive type 'short' to C#"));
Script.GlobalOptions.CustomConverters.SetScriptToClrCustomConversion(
scriptDataType: DataType.UserData,
clrDataType: typeof(ushort),
canConvert: luaValue => luaValue.UserData?.Object is LuaUInt16,
converter: luaValue => luaValue.UserData.Object is LuaUInt16 v
? (ushort)v
: throw new ScriptRuntimeException("use UInt16(value) to pass primitive type 'ushort' to C#"));
Script.GlobalOptions.CustomConverters.SetScriptToClrCustomConversion(
scriptDataType: DataType.UserData,
clrDataType: typeof(int),
canConvert: luaValue => luaValue.UserData?.Object is LuaInt32,
converter: luaValue => luaValue.UserData.Object is LuaInt32 v
? (int)v
: throw new ScriptRuntimeException("use Int32(value) to pass primitive type 'int' to C#"));
Script.GlobalOptions.CustomConverters.SetScriptToClrCustomConversion(
scriptDataType: DataType.UserData,
clrDataType: typeof(uint),
canConvert: luaValue => luaValue.UserData?.Object is LuaUInt32,
converter: luaValue => luaValue.UserData.Object is LuaUInt32 v
? (uint)v
: throw new ScriptRuntimeException("use UInt32(value) to pass primitive type 'uint' to C#"));
Script.GlobalOptions.CustomConverters.SetScriptToClrCustomConversion(
scriptDataType: DataType.UserData,
clrDataType: typeof(long),
canConvert: luaValue => luaValue.UserData?.Object is LuaInt64,
converter: luaValue => luaValue.UserData.Object is LuaInt64 v
? (long)v
: throw new ScriptRuntimeException("use Int64(value) to pass primitive type 'long' to C#"));
Script.GlobalOptions.CustomConverters.SetScriptToClrCustomConversion(
scriptDataType: DataType.UserData,
clrDataType: typeof(ulong),
canConvert: luaValue => luaValue.UserData?.Object is LuaUInt64,
converter: luaValue => luaValue.UserData.Object is LuaUInt64 v
? (ulong)v
: throw new ScriptRuntimeException("use UInt64(value) to pass primitive type 'ulong' to C#"));
Script.GlobalOptions.CustomConverters.SetScriptToClrCustomConversion(
scriptDataType: DataType.UserData,
clrDataType: typeof(float),
canConvert: luaValue => luaValue.UserData?.Object is LuaSingle,
converter: luaValue => luaValue.UserData.Object is LuaSingle v
? (float)v
: throw new ScriptRuntimeException("use Single(value) to pass primitive type 'float' to C#"));
Script.GlobalOptions.CustomConverters.SetScriptToClrCustomConversion(
scriptDataType: DataType.UserData,
clrDataType: typeof(double),
canConvert: luaValue => luaValue.UserData?.Object is LuaDouble,
converter: luaValue => luaValue.UserData.Object is LuaDouble v
? (double)v
: throw new ScriptRuntimeException("use Double(value) to pass primitive type 'double' to C#"));
RegisterOption<Character>(DataType.UserData);
RegisterOption<AccountId>(DataType.UserData);
RegisterOption<ContentPackageId>(DataType.UserData);
RegisterOption<SteamId>(DataType.UserData);
RegisterOption<DateTime>(DataType.UserData);
RegisterOption<BannedPlayer>(DataType.UserData);
RegisterOption<Address>(DataType.UserData);
RegisterOption<int>(DataType.Number);
RegisterEither<Address, AccountId>();
RegisterImmutableArray<FactionPrefab.HireableCharacter>();
}
private static void RegisterImmutableArray<T>()
{
Script.GlobalOptions.CustomConverters.SetScriptToClrCustomConversion(DataType.Table, typeof(ImmutableArray<T>), v =>
{
return v.ToObject<T[]>().ToImmutableArray();
});
}
private static void RegisterEither<T1, T2>()
{
DynValue convertEitherIntoDynValue(Either<T1, T2> either)
{
if (either.TryGet(out T1 value1))
{
return UserData.Create(value1);
}
if (either.TryGet(out T2 value2))
{
return UserData.Create(value2);
}
return null;
}
Script.GlobalOptions.CustomConverters.SetClrToScriptCustomConversion(typeof(EitherT<T1, T2>), (Script v, object obj) =>
{
if (obj is EitherT<T1, T2> either)
{
return convertEitherIntoDynValue(either);
}
return null;
});
Script.GlobalOptions.CustomConverters.SetClrToScriptCustomConversion(typeof(EitherU<T1, T2>), (Script v, object obj) =>
{
if (obj is EitherU<T1, T2> either)
{
return convertEitherIntoDynValue(either);
}
return null;
});
}
private static void RegisterOption<T>(DataType dataType)
{
Script.GlobalOptions.CustomConverters.SetClrToScriptCustomConversion(typeof(Option<T>), (Script v, object obj) =>
{
if (obj is Option<T> option)
{
if (option.TryUnwrap(out T outValue))
{
return UserData.Create(outValue);
}
}
return DynValue.Nil;
});
Script.GlobalOptions.CustomConverters.SetScriptToClrCustomConversion(dataType, typeof(Option<T>), v =>
{
return Option<T>.Some(v.ToObject<T>());
});
Script.GlobalOptions.CustomConverters.SetScriptToClrCustomConversion(DataType.Nil, typeof(Option<T>), v =>
{
return Option<T>.None();
});
}
private void RegisterAction<T>()
{
Script.GlobalOptions.CustomConverters.SetScriptToClrCustomConversion(DataType.Function, typeof(Action<T>), v =>
{
var function = v.Function;
return (Action<T>)(p => Call(function, p));
});
Script.GlobalOptions.CustomConverters.SetScriptToClrCustomConversion(DataType.ClrFunction, typeof(Action<T>), v =>
{
var function = v.Function;
return (Action<T>)(p => Call(function, p));
});
}
private void RegisterAction<T1, T2>()
{
Script.GlobalOptions.CustomConverters.SetScriptToClrCustomConversion(DataType.Function, typeof(Action<T1, T2>), v =>
{
var function = v.Function;
return (Action<T1, T2>)((a1, a2) => Call(function, a1, a2));
});
Script.GlobalOptions.CustomConverters.SetScriptToClrCustomConversion(DataType.ClrFunction, typeof(Action<T1, T2>), v =>
{
var function = v.Function;
return (Action<T1, T2>)((a1, a2) => Call(function, a1, a2));
});
}
private void RegisterAction()
{
Script.GlobalOptions.CustomConverters.SetScriptToClrCustomConversion(DataType.Function, typeof(Action), v =>
{
var function = v.Function;
return (Action)(() => Call(function));
});
Script.GlobalOptions.CustomConverters.SetScriptToClrCustomConversion(DataType.ClrFunction, typeof(Action), v =>
{
var function = v.Function;
return (Action)(() => Call(function));
});
}
private void RegisterFunc<T1>()
{
Script.GlobalOptions.CustomConverters.SetScriptToClrCustomConversion(DataType.Function, typeof(Func<T1>), v =>
{
var function = v.Function;
return () => function.Call().ToObject<T1>();
});
Script.GlobalOptions.CustomConverters.SetScriptToClrCustomConversion(DataType.ClrFunction, typeof(Func<T1>), v =>
{
var function = v.Function;
return () => function.Call().ToObject<T1>();
});
}
private void RegisterFunc<T1, T2>()
{
Script.GlobalOptions.CustomConverters.SetScriptToClrCustomConversion(DataType.Function, typeof(Func<T1, T2>), v =>
{
var function = v.Function;
return (T1 a) => function.Call(a).ToObject<T2>();
});
Script.GlobalOptions.CustomConverters.SetScriptToClrCustomConversion(DataType.ClrFunction, typeof(Func<T1, T2>), v =>
{
var function = v.Function;
return (T1 a) => function.Call(a).ToObject<T2>();
});
}
private void RegisterFunc<T1, T2, T3, T4, T5>()
{
Script.GlobalOptions.CustomConverters.SetScriptToClrCustomConversion(DataType.Function, typeof(Func<T1, T2, T3, T4, T5>), v =>
{
var function = v.Function;
return (T1 a, T2 b, T3 c, T4 d) => function.Call(a, b, c, d).ToObject<T5>();
});
Script.GlobalOptions.CustomConverters.SetScriptToClrCustomConversion(DataType.Function, typeof(Func<T1, T2, T3, T4, T5>), v =>
{
var function = v.Function;
return (T1 a, T2 b, T3 c, T4 d) => function.Call(a, b, c, d).ToObject<T5>();
});
}
}
}
@@ -0,0 +1,51 @@
using System;
using Barotrauma.Networking;
using Microsoft.Xna.Framework;
using MoonSharp.Interpreter;
using Barotrauma.LuaCs;
namespace Barotrauma
{
public partial class LuaCsLogger
{
public static void HandleException(Exception ex, LuaCsMessageOrigin origin)
{
LuaCsSetup.Instance.Logger.HandleException(ex);
}
public static void LogError(string message, LuaCsMessageOrigin origin)
{
LuaCsSetup.Instance.Logger.LogError(message);
}
public static void LogError(string message)
{
LuaCsSetup.Instance.Logger.LogError(message);
}
public static void LogMessage(string message, Color? serverColor = null, Color? clientColor = null)
{
LuaCsSetup.Instance.Logger.LogMessage(message, serverColor, clientColor);
}
public static void Log(string message, Color? color = null, ServerLog.MessageType messageType = ServerLog.MessageType.ServerMessage)
{
LuaCsSetup.Instance.Logger.Log(message, color, messageType);
}
}
partial class LuaCsSetup
{
// Compatibility with cs mods that use this method.
public static void PrintLuaError(object message) => LuaCsSetup.Instance.Logger.LogError($"{message}");
public static void PrintCsError(object message) => LuaCsSetup.Instance.Logger.LogError($"{message}");
public static void PrintGenericError(object message) => LuaCsSetup.Instance.Logger.LogError($"{message}");
internal void PrintMessage(object message) => LuaCsSetup.Instance.Logger.LogMessage($"{message}");
public static void PrintCsMessage(object message) => LuaCsSetup.Instance.Logger.LogMessage($"{message}");
internal void HandleException(Exception ex, LuaCsMessageOrigin origin) => LuaCsSetup.Instance.Logger.HandleException(ex);
}
}
@@ -0,0 +1,82 @@
using Barotrauma.LuaCs;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
namespace Barotrauma
{
public interface IPerformanceData
{
public string Identifier { get; }
public long ElapsedTicks { get; }
}
public class SimplePerformanceData : IPerformanceData
{
public string Identifier { get; }
public long ElapsedTicks { get; }
public SimplePerformanceData(string identifier, long elapsedTicks)
{
Identifier = identifier;
ElapsedTicks = elapsedTicks;
}
}
public class PerformanceCounterService : IReusableService
{
public bool EnablePerformanceCounter { get; set; } = false;
private Dictionary<string, List<IPerformanceData>> _data = new Dictionary<string, List<IPerformanceData>>();
public void AddElapsedTicks(IPerformanceData data)
{
if (!EnablePerformanceCounter) { return; }
if (!_data.ContainsKey(data.Identifier))
{
_data.Add(data.Identifier, new List<IPerformanceData>());
}
_data[data.Identifier].Add(data);
Trim(data.Identifier, 100);
}
public T GetLatestSnapshot<T>(string identifier) where T : class, IPerformanceData
{
if (!_data.ContainsKey(identifier)) { return default; }
return (T)_data[identifier].Last();
}
public T[] GetSnapshot<T>(string identifier, int length) where T : class, IPerformanceData, new()
{
if (!_data.ContainsKey(identifier)) { return new T[] { }; }
length = Math.Min(length, _data[identifier].Count);
return _data[identifier].GetRange(_data[identifier].Count - length, length).Cast<T>().ToArray();
}
public void Trim(string identifier, int maxSize)
{
if (!_data.ContainsKey(identifier)) { return; }
if (_data[identifier].Count > maxSize)
{
_data[identifier].RemoveRange(0, _data[identifier].Count - maxSize);
}
}
public FluentResults.Result Reset()
{
_data = new Dictionary<string, List<IPerformanceData>>();
return FluentResults.Result.Ok();
}
public void Dispose() { }
public bool IsDisposed { get; }
}
}
@@ -0,0 +1,151 @@
using Steamworks;
using Steamworks.Data;
using Barotrauma.Steam;
using System.Threading;
using System.Collections.Generic;
using System.Linq;
using System.IO;
using System.Threading.Tasks;
using System;
using Steamworks.Ugc;
namespace Barotrauma
{
partial class LuaCsSteam
{
private struct WorkshopItemDownload
{
public Steamworks.Ugc.Item Item;
public string Destination;
public LuaCsAction Callback;
}
double lastTimeChecked = 0;
List<WorkshopItemDownload> itemsBeingDownloaded = new List<WorkshopItemDownload>();
public LuaCsSteam()
{
}
private static void CopyFolder(string sourceDirName, string destDirName, bool copySubDirs, bool overwriteExisting = false)
{
// Get the subdirectories for the specified directory.
DirectoryInfo dir = new DirectoryInfo(sourceDirName);
if (!dir.Exists)
{
throw new System.IO.DirectoryNotFoundException(
"Source directory does not exist or could not be found: "
+ sourceDirName);
}
IEnumerable<DirectoryInfo> dirs = dir.GetDirectories();
// If the destination directory doesn't exist, create it.
if (!Directory.Exists(destDirName))
{
Directory.CreateDirectory(destDirName);
}
// Get the files in the directory and copy them to the new location.
IEnumerable<FileInfo> files = dir.GetFiles();
foreach (FileInfo file in files)
{
string tempPath = Path.Combine(destDirName, file.Name);
if (!overwriteExisting && File.Exists(tempPath)) { continue; }
file.CopyTo(tempPath, true);
}
// If copying subdirectories, copy them and their contents to new location.
if (copySubDirs)
{
foreach (DirectoryInfo subdir in dirs)
{
string tempPath = Path.Combine(destDirName, subdir.Name);
CopyFolder(subdir.FullName, tempPath, copySubDirs, overwriteExisting);
}
}
}
private async void DownloadWorkshopItemAsync(WorkshopItemDownload download, bool startDownload = false)
{
if (startDownload)
{
SteamManager.Workshop.NukeDownload(download.Item);
SteamUGC.Download(download.Item.Id, true);
itemsBeingDownloaded.Add(download);
}
if (download.Item.IsInstalled && Directory.Exists(download.Item.Directory))
{
if (download.Callback != null)
{
download.Callback(download.Item);
}
itemsBeingDownloaded.Remove(download);
CopyFolder(download.Item.Directory, download.Destination, true, true);
return;
}
}
public async void DownloadWorkshopItem(ulong id, string destination, LuaCsAction callback)
{
if (!LuaCsFile.IsPathAllowedException(destination)) { return; }
Option<Steamworks.Ugc.Item> itemOption = await SteamManager.Workshop.GetItem(id);
if (itemOption.TryUnwrap(out Steamworks.Ugc.Item item))
{
DownloadWorkshopItemAsync(new WorkshopItemDownload()
{
Item = item,
Destination = destination,
Callback = callback
}, true);
}
else
{
throw new Exception($"Tried to download invalid workshop item {id}.");
}
}
public void DownloadWorkshopItem(Steamworks.Ugc.Item item, string destination, LuaCsAction callback)
{
DownloadWorkshopItemAsync(new WorkshopItemDownload()
{
Item = item,
Destination = destination,
Callback = callback
}, true);
}
public async void GetWorkshopItem(UInt64 id, LuaCsAction callback)
{
Option<Steamworks.Ugc.Item> itemOption = await SteamManager.Workshop.GetItem(id);
if (itemOption.TryUnwrap(out Steamworks.Ugc.Item item))
{
callback(item);
}
else
{
callback(null);
}
}
public void Update()
{
if (itemsBeingDownloaded.Count > 0 && Timing.TotalTime > lastTimeChecked) // SteamUGC.OnDownloadItemResult for some reason doesn't work, so i need to do this stupid thing.
{
foreach (var item in itemsBeingDownloaded.ToArray())
{
DownloadWorkshopItemAsync(item);
}
lastTimeChecked = Timing.TotalTime + 15;
}
}
}
}
@@ -0,0 +1,153 @@
using Barotrauma.LuaCs.Events;
using Barotrauma.LuaCs;
using Barotrauma.LuaCs.Compatibility;
using System;
using System.Collections.Generic;
using System.Diagnostics;
namespace Barotrauma
{
public class LuaCsTimer : ILuaCsTimer, IEventUpdate
{
public static double Time => Timing.TotalTime;
public static double GetTime() => Time;
public static double AccumulatorMax
{
get
{
return Timing.AccumulatorMax;
}
set
{
Timing.AccumulatorMax = value;
}
}
private class TimerComparer : IComparer<TimedAction>
{
public int Compare(TimedAction timedAction1, TimedAction timedAction2)
{
if (timedAction1 == null || timedAction2 == null)
return 0;
return -Math.Sign(timedAction2.ExecutionTime - timedAction1.ExecutionTime);
}
}
private class TimedAction
{
public LuaCsAction Action
{
get;
private set;
}
public double ExecutionTime
{
get;
private set;
}
public TimedAction(LuaCsAction action, int delayMs)
{
this.Action = action;
ExecutionTime = Time + (delayMs / 1000f);
}
}
private List<TimedAction> timedActions = new List<TimedAction>();
private readonly IEventService _eventService;
private readonly ILoggerService _loggerService;
public LuaCsTimer(IEventService eventService, ILoggerService loggerService)
{
_eventService = eventService;
_loggerService = loggerService;
SubscribeToEvents();
}
private void AddTimer(TimedAction timedAction)
{
if (timedAction == null)
{
throw new ArgumentNullException(nameof(timedAction));
}
lock (timedActions)
{
int insertionPoint = timedActions.BinarySearch(timedAction, new TimerComparer());
if (insertionPoint < 0)
{
insertionPoint = ~insertionPoint;
}
timedActions.Insert(insertionPoint, timedAction);
}
}
public void Clear()
{
timedActions = new List<TimedAction>();
}
public void Wait(LuaCsAction action, int millisecondDelay)
{
TimedAction timedAction = new TimedAction(action, millisecondDelay);
AddTimer(timedAction);
}
public void NextFrame(LuaCsAction action)
{
TimedAction timedAction = new TimedAction(action, 0);
AddTimer(timedAction);
}
public void OnUpdate(double fixedDeltaTime)
{
lock (timedActions)
{
TimedAction[] timedCopy = timedActions.ToArray();
for (int i = 0; i < timedCopy.Length; i++)
{
TimedAction timedAction = timedCopy[i];
if (Time >= timedAction.ExecutionTime)
{
try
{
timedAction.Action();
}
catch (Exception e)
{
_loggerService.HandleException(e);
}
timedActions.Remove(timedAction);
}
else
{
break;
}
}
}
}
private void SubscribeToEvents()
{
_eventService.Subscribe<IEventUpdate>(this);
}
public FluentResults.Result Reset()
{
SubscribeToEvents();
return FluentResults.Result.Ok();
}
public void Dispose()
{
_eventService.Unsubscribe<IEventUpdate>(this);
}
public bool IsDisposed => false;
}
}
@@ -0,0 +1,247 @@
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;
using Barotrauma.LuaCs;
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)
{
const long LuaCsPackageId = 2559634234;
string getFullPath(string p) => System.IO.Path.GetFullPath(p).CleanUpPath();
path = getFullPath(path);
bool pathStartsWith(string prefix) => path.StartsWith(prefix, StringComparison.OrdinalIgnoreCase);
if (pathStartsWith(getFullPath(LuaCsSetup.GetLuaCsPackage().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();
}
}
}
@@ -0,0 +1,560 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
using Barotrauma.Items.Components;
using Barotrauma.LuaCs;
using Barotrauma.Networking;
using FarseerPhysics.Dynamics;
using Microsoft.Xna.Framework;
using MoonSharp.Interpreter;
namespace Barotrauma.LuaCs
{
partial class LuaGame : IReusableService
{
public bool IsSingleplayer => GameMain.IsSingleplayer;
public bool IsMultiplayer => GameMain.IsMultiplayer;
public string SaveFolder => string.IsNullOrEmpty(GameSettings.CurrentConfig.SavePath) ? SaveUtil.DefaultSaveFolder : GameSettings.CurrentConfig.SavePath;
#if CLIENT
public GameClient Client
{
get
{
return GameMain.Client;
}
}
public bool Paused => GameMain.Instance?.Paused == true;
public byte SessionId => GameMain.Client.SessionId;
public byte MyID => SessionId; // compatibility
public ChatMode ActiveChatMode => GameMain.ActiveChatMode;
public ChatBox ChatBox
{
get
{
if (GameMain.IsSingleplayer)
return GameMain.GameSession.CrewManager.ChatBox;
else
return GameMain.Client.ChatBox;
}
}
public Sounds.SoundManager SoundManager
{
get
{
return GameMain.SoundManager;
}
}
public Lights.LightManager LightManager
{
get
{
return GameMain.LightManager;
}
}
public SubEditorScreen SubEditorScreen
{
get
{
return GameMain.SubEditorScreen;
}
}
public MainMenuScreen MainMenuScreen
{
get
{
return GameMain.MainMenuScreen;
}
}
public Particles.ParticleManager ParticleManager
{
get
{
return GameMain.ParticleManager;
}
}
public bool IsSubEditor
{
get
{
return Screen.Selected is SubEditorScreen;
}
}
#else
public GameServer Server
{
get
{
return GameMain.Server;
}
}
public bool IsDedicated
{
get
{
return GameMain.Server.ServerPeer is LidgrenServerPeer;
}
}
public bool Paused => false;
#endif
public ServerSettings ServerSettings
{
get
{
#if SERVER
return GameMain.Server.ServerSettings;
#else
return GameMain.Client.ServerSettings;
#endif
}
}
public RespawnManager RespawnManager
{
get
{
#if SERVER
return GameMain.Server.RespawnManager;
#else
return GameMain.Client.RespawnManager;
#endif
}
}
public List<DebugConsole.Command> Commands => DebugConsole.Commands;
public bool? ForceVoice = null;
public bool? ForceLocalVoice = null;
public DynValue Settings;
public bool allowWifiChat = false;
public bool overrideTraitors = false;
public bool overrideRespawnSub = false;
public bool overrideSignalRadio = false;
public bool disableSpamFilter = false;
public bool disableDisconnectCharacter = false;
public bool enableControlHusk = false;
public int MapEntityUpdateInterval
{
get { return MapEntity.MapEntityUpdateInterval; }
set { MapEntity.MapEntityUpdateInterval = value; }
}
public int GapUpdateInterval
{
get { return 1; }
set { }
}
public int PoweredUpdateInterval
{
get { return MapEntity.PoweredUpdateInterval; }
set { MapEntity.PoweredUpdateInterval = value; }
}
public int CharacterUpdateInterval
{
get { return Character.CharacterUpdateInterval; }
set { Character.CharacterUpdateInterval = value; }
}
public HashSet<Item> UpdatePriorityItems = new HashSet<Item>();
public HashSet<Character> UpdatePriorityCharacters = new HashSet<Character>();
public void AddPriorityItem(Item item)
{
UpdatePriorityItems.Add(item);
}
public void RemovePriorityItem(Item item)
{
UpdatePriorityItems.Remove(item);
}
public void ClearPriorityItem()
{
UpdatePriorityItems.Clear();
}
public void AddPriorityCharacter(Character character)
{
UpdatePriorityCharacters.Add(character);
}
public void RemovePriorityCharacter(Character character)
{
UpdatePriorityCharacters.Remove(character);
}
public void ClearPriorityCharacter()
{
UpdatePriorityCharacters.Clear();
}
public bool RoundStarted
{
get
{
if (GameMain.IsSingleplayer) { return GameMain.GameSession != null && GameMain.GameSession.IsRunning; }
#if SERVER
return GameMain.Server?.GameStarted == true;
#else
return GameMain.Client?.GameStarted == true;
#endif
}
}
public GameSession GameSession
{
get
{
return GameMain.GameSession;
}
}
public NetLobbyScreen NetLobbyScreen
{
get
{
return GameMain.NetLobbyScreen;
}
}
public GameScreen GameScreen
{
get
{
return GameMain.GameScreen;
}
}
public World World
{
get
{
return GameMain.World;
}
}
#if SERVER
public ServerPeer Peer
{
get
{
return GameMain.Server.ServerPeer;
}
}
#else
public ClientPeer Peer
{
get
{
return GameMain.Client.ClientPeer;
}
}
#endif
private readonly IConsoleCommandsService _consoleCommands;
public LuaGame(IConsoleCommandsService consoleCommands)
{
UserData.RegisterType(typeof(GameSettings));
Settings = UserData.CreateStatic(typeof(GameSettings));
_consoleCommands = consoleCommands;
}
public void OverrideTraitors(bool o)
{
overrideTraitors = o;
}
public void OverrideRespawnSub(bool o)
{
overrideRespawnSub = o;
}
public void AllowWifiChat(bool o)
{
allowWifiChat = o;
}
public void OverrideSignalRadio(bool o)
{
overrideSignalRadio = o;
}
public void DisableSpamFilter(bool o)
{
disableSpamFilter = o;
}
public void DisableDisconnectCharacter(bool o)
{
disableDisconnectCharacter = o;
}
public void EnableControlHusk(bool o)
{
enableControlHusk = o;
}
public static void Explode(Vector2 pos, float range = 100, float force = 30, float damage = 30, float structureDamage = 30, float itemDamage = 30, float empStrength = 0, float ballastFloraStrength = 0)
{
new Explosion(range, force, damage, structureDamage, itemDamage, empStrength, ballastFloraStrength).Explode(pos, null);
}
public static string SpawnItem(string name, Vector2 pos, bool inventory = false, Character character = null)
{
string error;
DebugConsole.SpawnItem(new string[] { name, inventory ? "inventory" : "cursor" }, pos, character, out error);
return error;
}
public static ContentPackage[] GetEnabledContentPackages()
{
return ContentPackageManager.EnabledPackages.All.ToArray();
}
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;
}
public static Submarine GetRespawnSub()
{
#if SERVER
if (GameMain.Server.RespawnManager == null) { return null; }
return GameMain.Server.RespawnManager.GetShuttle(CharacterTeamType.Team1);
#else
if (GameMain.Client.RespawnManager == null) { return null; }
return GameMain.Client.RespawnManager.GetShuttle(CharacterTeamType.Team1);
#endif
}
public static Items.Components.Steering GetSubmarineSteering(Submarine sub)
{
foreach (Item item in Item.ItemList)
{
if (item.Submarine != sub) continue;
var steering = item.GetComponent<Items.Components.Steering>();
if (steering != null)
{
return steering;
}
}
return null;
}
public static WifiComponent GetWifiComponent(Item item)
{
if (item == null) return null;
return item.GetComponent<WifiComponent>();
}
public static LightComponent GetLightComponent(Item item)
{
if (item == null) return null;
return item.GetComponent<LightComponent>();
}
public static CustomInterface GetCustomInterface(Item item)
{
if (item == null) return null;
return item.GetComponent<CustomInterface>();
}
public static Fabricator GetFabricatorComponent(Item item)
{
if (item == null) return null;
return item.GetComponent<Fabricator>();
}
public static Holdable GetHoldableComponent(Item item)
{
if (item == null) return null;
return item.GetComponent<Holdable>();
}
public static void ExecuteCommand(string command)
{
DebugConsole.ExecuteCommand(command);
}
public static Signal CreateSignal(string value, int stepsTaken = 1, Character sender = null, Item source = null, float power = 0, float strength = 1)
{
return new Signal(value, stepsTaken, sender, source, power, strength);
}
public void RemoveCommand(string name)
{
_consoleCommands.RemoveCommand(name);
for (var i = DebugConsole.Commands.Count - 1; i >= 0; i--)
{
foreach (var cmdname in DebugConsole.Commands[i].Names)
{
if (cmdname == name)
{
DebugConsole.Commands.RemoveAt(i);
continue;
}
}
}
}
public void AddCommand(string name, string help, LuaCsAction onExecute, LuaCsFunc getValidArgs = null, bool isCheat = false)
{
_consoleCommands.RegisterCommand(name, help,
(string[] args) =>
{
onExecute(new object[] { args });
},
() =>
{
if (getValidArgs == null) { return null; }
var validArgs = getValidArgs();
if (validArgs is DynValue luaValue)
{
return luaValue.ToObject<string[][]>();
}
return (string[][])validArgs;
}
);
}
public void AddCommand(string name, LuaCsAction onExecute, LuaCsFunc getValidArgs = null, bool isCheat = false)
{
_consoleCommands.RegisterCommand(name, "",
(string[] args) =>
{
onExecute(new object[] { args });
},
() =>
{
if (getValidArgs == null) { return null; }
var validArgs = getValidArgs();
if (validArgs is DynValue luaValue)
{
return luaValue.ToObject<string[][]>();
}
return (string[][])validArgs;
}
);
}
public bool IsDisposed => throw new NotImplementedException();
public void AssignOnExecute(string names, object onExecute) => DebugConsole.AssignOnExecute(names, (string[] args) =>
{
LuaCsSetup.Instance.LuaScriptManagementService.CallFunctionSafe(onExecute, new object[] { args });
});
public void SaveGame(string path)
{
if (!LuaCsFile.CanWriteToPath(path)) { throw new ScriptRuntimeException($"Saving files to {path} is disallowed."); }
SaveUtil.SaveGame(CampaignDataPath.CreateRegular(path));
}
public void LoadGame(string path)
{
SaveUtil.LoadGame(CampaignDataPath.CreateRegular(path));
}
#if SERVER
public void LoadCampaign(string path, Client client = null)
{
MultiPlayerCampaign.LoadCampaign(CampaignDataPath.CreateRegular(path), client);
}
public static void SendMessage(string msg, ChatMessageType? messageType = null, Client sender = null, Character character = null)
{
GameMain.Server.SendChatMessage(msg, messageType, sender, character);
}
public static void SendTraitorMessage(WriteOnlyMessage message, Client client)
{
GameMain.Server.SendTraitorMessage(message, client);
}
public static void SendDirectChatMessage(string sendername, string text, Character sender, ChatMessageType messageType = ChatMessageType.Private, Client client = null, string iconStyle = "")
{
ChatMessage cm = ChatMessage.Create(sendername, text, messageType, sender);
cm.IconStyle = iconStyle;
GameMain.Server.SendDirectChatMessage(cm, client);
}
public static void SendDirectChatMessage(ChatMessage chatMessage, Client client)
{
GameMain.Server.SendDirectChatMessage(chatMessage, client);
}
public static void Log(string message, ServerLog.MessageType type)
{
GameServer.Log(message, type);
}
public static void DispatchRespawnSub()
{
GameMain.Server.RespawnManager.DispatchShuttle(GameMain.Server.RespawnManager.GetTeamSpecificState(CharacterTeamType.Team1));
}
public static GameServer.TryStartGameResult StartGame()
{
return GameMain.Server.TryStartGame();
}
public static void EndGame()
{
GameMain.Server.EndGame();
}
public void AssignOnClientRequestExecute(string names, LuaCsAction onExecute) =>
_consoleCommands.AssignOnClientRequestExecute(names, (Client client, Vector2 position, string[] args) => onExecute(client, position, args));
#endif
public void Stop()
{
MapEntityUpdateInterval = 1;
CharacterUpdateInterval = 1;
_consoleCommands.RemoveRegisteredCommands();
}
public FluentResults.Result Reset()
{
Stop();
return FluentResults.Result.Ok();
}
public void Dispose()
{
Stop();
}
}
}
@@ -0,0 +1,116 @@
using MoonSharp.Interpreter.Platforms;
using MoonSharp.Interpreter;
using System;
using System.IO;
using System.Text;
namespace Barotrauma
{
public class LuaPlatformAccessor : PlatformAccessorBase
{
public static FileMode ParseFileMode(string mode)
{
mode = mode.Replace("b", "");
if (mode == "r")
return FileMode.Open;
else if (mode == "r+")
return FileMode.OpenOrCreate;
else if (mode == "w")
return FileMode.Create;
else if (mode == "w+")
return FileMode.Truncate;
else
return FileMode.Append;
}
public static FileAccess ParseFileAccess(string mode)
{
mode = mode.Replace("b", "");
if (mode == "r")
return FileAccess.Read;
else if (mode == "r+")
return FileAccess.ReadWrite;
else if (mode == "w")
return FileAccess.ReadWrite;
else if (mode == "w+")
return FileAccess.ReadWrite;
else
return FileAccess.Write;
}
public override string GetEnvironmentVariable(string envvarname)
{
return null;
}
public override CoreModules FilterSupportedCoreModules(CoreModules module)
{
return module;
}
public override Stream IO_OpenFile(Script script, string filename, Encoding encoding, string mode)
{
if (!LuaCsFile.IsPathAllowedLuaException(filename)) { return Stream.Null; }
FileStream stream = new FileStream(filename, ParseFileMode(mode), ParseFileAccess(mode), FileShare.ReadWrite | FileShare.Delete);
return stream;
}
public override Stream IO_GetStandardStream(StandardFileType type)
{
switch (type)
{
case StandardFileType.StdIn:
return Console.OpenStandardInput();
case StandardFileType.StdOut:
return Console.OpenStandardOutput();
case StandardFileType.StdErr:
return Console.OpenStandardError();
default:
throw new ArgumentException("type");
}
}
public override string IO_OS_GetTempFilename()
{
return "LocalMods/temp.txt";
}
public override void OS_ExitFast(int exitCode)
{
throw new ScriptRuntimeException("usage of os.exit is not allowed.");
}
public override bool OS_FileExists(string file)
{
return LuaCsFile.Exists(file);
}
public override void OS_FileDelete(string file)
{
LuaCsFile.Delete(file);
}
public override void OS_FileMove(string src, string dst)
{
LuaCsFile.Move(src, dst);
}
public override int OS_Execute(string cmdline)
{
throw new ScriptRuntimeException("usage of os.execute is not allowed.");
}
public override string GetPlatformNamePrefix()
{
return "lua";
}
public override void DefaultPrint(string content)
{
System.Diagnostics.Debug.WriteLine(content);
}
}
}
@@ -0,0 +1,120 @@
using System;
using System.Collections.Generic;
using System.IO;
using MoonSharp.Interpreter;
namespace Barotrauma
{
class LuaRequire
{
private Script lua { get; set; }
private Dictionary<string, DynValue> loadedModules { get; set; }
private bool GetExistingReturnValue(string moduleName, ref DynValue returnValue)
{
return loadedModules.TryGetValue(
moduleName,
out returnValue
);
}
private string FixContentPackagePath(string contentPackagePath)
{
contentPackagePath = Path.TrimEndingDirectorySeparator(
new FileInfo(contentPackagePath) // filelist.xml
.Directory
.FullName
.CleanUpPathCrossPlatform()
);
return contentPackagePath;
}
private string GetContentPackagePath(string path)
{
IEnumerable<ContentPackage> allContentPackages = ContentPackageManager.AllPackages;
foreach (ContentPackage contentPackage in allContentPackages)
{
string contentPackagePath = FixContentPackagePath(contentPackage.Path);
if (path.StartsWith(contentPackagePath))
{
return contentPackagePath;
}
}
// Return null if we can't find a content package that
// this module belongs to.
return null;
}
private string GetContentPackagePath(string moduleName, Table environment)
{
string filePath = lua.Options
.ScriptLoader
.ResolveModuleName(
moduleName,
environment
);
filePath = Path.TrimEndingDirectorySeparator(
new FileInfo(filePath)
.Directory
.FullName
.CleanUpPathCrossPlatform()
);
return GetContentPackagePath(filePath);
}
private void SaveReturnValue(string moduleName, DynValue returnValue)
{
loadedModules[moduleName] = returnValue;
}
private void ExecuteModule(string moduleName, Table environment, ref DynValue returnValue)
{
DynValue loadFunc = lua.RequireModule(
moduleName,
environment
);
string packagePath = GetContentPackagePath(
moduleName,
environment
);
returnValue = lua.Call(
loadFunc,
packagePath
);
}
// Lua modules that have been previously loaded by require() will
// not be loaded again; instead, their initial return value is
// preserved and returned again on subsequent attempts.
public DynValue Require(string moduleName, Table globalContext)
{
DynValue returnValue = null;
Table environment = globalContext ?? lua.Globals;
if (GetExistingReturnValue(moduleName, ref returnValue))
return returnValue;
ExecuteModule(moduleName, environment, ref returnValue);
if (
returnValue == null
|| returnValue.IsNil()
|| returnValue.IsVoid()
)
returnValue = DynValue.NewBoolean(true);
SaveReturnValue(moduleName, returnValue);
return returnValue;
}
public LuaRequire(Script lua)
{
this.lua = lua;
loadedModules = new Dictionary<string, DynValue>();
}
}
}
@@ -0,0 +1,198 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Reflection;
using MoonSharp.Interpreter;
using MoonSharp.Interpreter.Interop;
namespace Barotrauma
{
partial class LuaSafeUserData
{
public IUserDataDescriptor this[string index]
{
get => LuaUserData.Descriptors.GetValueOrDefault(index);
}
private static bool CanBeRegistered(string typeName)
{
if (typeName.StartsWith("Barotrauma.Lua", StringComparison.Ordinal) ||
typeName.StartsWith("Barotrauma.Cs", StringComparison.Ordinal) ||
typeName.StartsWith("Barotrauma.LuaCs", StringComparison.Ordinal))
{
return false;
}
if (typeName == "System.Single") { return true; }
if (typeName.StartsWith("System.Collections", StringComparison.Ordinal))
return true;
if (typeName.StartsWith("Microsoft.Xna", StringComparison.Ordinal))
return true;
if (typeName.StartsWith("Barotrauma.IO", StringComparison.Ordinal))
return false;
if (typeName.StartsWith("Barotrauma.ToolBox", StringComparison.Ordinal))
return false;
if (typeName.StartsWith("Barotrauma.SaveUtil", StringComparison.Ordinal))
return false;
if (typeName.StartsWith("Barotrauma.", StringComparison.Ordinal))
return true;
return false;
}
private static bool CanBeReRegistered(string typeName)
{
if (typeName.StartsWith("Barotrauma.Lua", StringComparison.Ordinal) ||
typeName.StartsWith("Barotrauma.Cs", StringComparison.Ordinal) ||
typeName.StartsWith("Barotrauma.LuaCs", StringComparison.Ordinal))
{
return false;
}
return true;
}
private static bool IsAllowed(string typeName)
{
if (!CanBeReRegistered(typeName) && LuaUserData.IsRegistered(typeName))
{
return false;
}
if (!CanBeRegistered(typeName) && !LuaUserData.IsRegistered(typeName))
{
return false;
}
return true;
}
private static void CheckAllowed(string typeName)
{
if (!IsAllowed(typeName))
{
throw new ScriptRuntimeException($"Type {typeName} can't be registered");
}
}
public static Type GetType(string typeName)
{
CheckAllowed(typeName);
return LuaUserData.GetType(typeName);
}
public static IUserDataDescriptor RegisterType(string typeName)
{
CheckAllowed(typeName);
return LuaUserData.RegisterType(typeName);
}
public static IUserDataDescriptor RegisterTypeBarotrauma(string typeName)
{
return RegisterType($"Barotrauma.{typeName}");
}
public static void RegisterExtensionType(string typeName)
{
CheckAllowed(typeName);
LuaUserData.RegisterExtensionType(typeName);
}
public static bool IsRegistered(string typeName)
{
return LuaUserData.IsRegistered(typeName);
}
public static void UnregisterType(string typeName, bool deleteHistory = false)
{
LuaUserData.UnregisterType(typeName, deleteHistory);
}
public static IUserDataDescriptor RegisterGenericType(string typeName, params string[] typeNameArguements)
{
CheckAllowed(typeName);
return LuaUserData.RegisterGenericType(typeName, typeNameArguements);
}
public static void UnregisterGenericType(string typeName, params string[] typeNameArguements)
{
LuaUserData.UnregisterGenericType(typeName, typeNameArguements);
}
public static bool IsTargetType(object obj, string typeName)
{
return LuaUserData.IsTargetType(obj, typeName);
}
public static string TypeOf(object obj)
{
return LuaUserData.TypeOf(obj);
}
public static object CreateStatic(string typeName)
{
CheckAllowed(typeName);
return LuaUserData.CreateStatic(typeName);
}
public static object CreateEnumTable(string typeName)
{
return LuaUserData.CreateEnumTable(typeName);
}
public static void MakeFieldAccessible(IUserDataDescriptor IUUD, string fieldName)
{
LuaUserData.MakeFieldAccessible(IUUD, fieldName);
}
public static void MakeMethodAccessible(IUserDataDescriptor IUUD, string methodName, string[] parameters = null)
{
LuaUserData.MakeMethodAccessible(IUUD, methodName, parameters);
}
public static void MakePropertyAccessible(IUserDataDescriptor IUUD, string propertyName)
{
LuaUserData.MakePropertyAccessible(IUUD, propertyName);
}
public static void AddMethod(IUserDataDescriptor IUUD, string methodName, object function)
{
LuaUserData.AddMethod(IUUD, methodName, function);
}
public static void AddField(IUserDataDescriptor IUUD, string fieldName, DynValue value)
{
LuaUserData.AddField(IUUD, fieldName, value);
}
public static void RemoveMember(IUserDataDescriptor IUUD, string memberName)
{
LuaUserData.RemoveMember(IUUD, memberName);
}
public static bool HasMember(object obj, string memberName)
{
return LuaUserData.HasMember(obj, memberName);
}
public static void AddCallMetaTable(object userdata) { }
public static DynValue CreateUserDataFromDescriptor(DynValue scriptObject, IUserDataDescriptor desiredTypeDescriptor)
{
return LuaUserData.CreateUserDataFromDescriptor(scriptObject, desiredTypeDescriptor);
}
public static DynValue CreateUserDataFromType(DynValue scriptObject, Type desiredType)
{
return LuaUserData.CreateUserDataFromType(scriptObject, desiredType);
}
}
}
@@ -0,0 +1,234 @@
using System;
namespace Barotrauma
{
public struct LuaSByte
{
public readonly sbyte Value;
public LuaSByte(double v)
{
Value = (sbyte)v;
}
public LuaSByte(string v, int radix = 10)
{
Value = Convert.ToSByte(v, radix);
}
public static implicit operator sbyte(LuaSByte luaValue) => luaValue.Value;
public override string ToString()
{
return Value.ToString();
}
}
public struct LuaByte
{
public readonly byte Value;
public LuaByte(double v)
{
Value = (byte)v;
}
public LuaByte(string v, int radix = 10)
{
Value = Convert.ToByte(v, radix);
}
public static implicit operator byte(LuaByte luaValue) => luaValue.Value;
public override string ToString()
{
return Value.ToString();
}
}
public struct LuaInt16
{
public readonly short Value;
public LuaInt16(double v)
{
Value = (short)v;
}
public LuaInt16(string v, int radix = 10)
{
Value = Convert.ToInt16(v, radix);
}
public static implicit operator short(LuaInt16 luaValue) => luaValue.Value;
public override string ToString()
{
return Value.ToString();
}
}
public struct LuaUInt16
{
public readonly ushort Value;
public LuaUInt16(double v)
{
Value = (ushort)v;
}
public LuaUInt16(string v, int radix = 10)
{
Value = Convert.ToUInt16(v, radix);
}
public static implicit operator ushort(LuaUInt16 luaValue) => luaValue.Value;
public override string ToString()
{
return Value.ToString();
}
}
public struct LuaInt32
{
public readonly int Value;
public LuaInt32(double v)
{
Value = (int)v;
}
public LuaInt32(string v, int radix = 10)
{
Value = Convert.ToInt32(v, radix);
}
public static implicit operator int(LuaInt32 luaValue) => luaValue.Value;
public override string ToString()
{
return Value.ToString();
}
}
public struct LuaUInt32
{
public readonly uint Value;
public LuaUInt32(double v)
{
Value = (uint)v;
}
public LuaUInt32(string v, int radix = 10)
{
Value = Convert.ToUInt32(v, radix);
}
public static implicit operator uint(LuaUInt32 luaValue) => luaValue.Value;
public override string ToString()
{
return Value.ToString();
}
}
public struct LuaInt64
{
public readonly long Value;
public LuaInt64(double v)
{
Value = (long)v;
}
public LuaInt64(double lo, double hi)
{
Value = Convert.ToUInt32(lo) | (long)Convert.ToInt32(hi) << 32;
}
public LuaInt64(string v, int radix = 10)
{
Value = Convert.ToInt64(v, radix);
}
public static implicit operator long(LuaInt64 luaValue) => luaValue.Value;
public override string ToString()
{
return Value.ToString();
}
}
public struct LuaUInt64
{
public readonly ulong Value;
public LuaUInt64(double v)
{
Value = (ulong)v;
}
public LuaUInt64(double lo, double hi)
{
Value = Convert.ToUInt32(lo) | (ulong)Convert.ToUInt32(hi) << 32;
}
public LuaUInt64(string v, int radix = 10)
{
Value = Convert.ToUInt64(v, radix);
}
public static implicit operator ulong(LuaUInt64 luaValue) => luaValue.Value;
public override string ToString()
{
return Value.ToString();
}
}
public struct LuaSingle
{
public readonly float Value;
public LuaSingle(double v)
{
Value = (float)v;
}
public LuaSingle(string v)
{
Value = float.Parse(v);
}
public static implicit operator float(LuaSingle luaValue) => luaValue.Value;
public override string ToString()
{
return Value.ToString();
}
}
public struct LuaDouble
{
public readonly double Value;
public LuaDouble(double v)
{
Value = v;
}
public LuaDouble(string v)
{
Value = double.Parse(v);
}
public static implicit operator double(LuaDouble luaValue) => luaValue.Value;
public override string ToString()
{
return Value.ToString();
}
}
}
@@ -0,0 +1,236 @@
global using LuaCsHook = Barotrauma.LuaCs.Compatibility.ILuaCsHook;
using System;
using System.Linq;
using System.Reflection;
using HarmonyLib;
using System.Collections.Generic;
using Barotrauma.LuaCs.Compatibility;
using MoonSharp.Interpreter;
using LuaCsCompatPatchFunc = Barotrauma.LuaCsPatch;
namespace Barotrauma
{
// XXX: this can't be renamed because of backward compatibility with C# mods
public delegate object LuaCsPatch(object self, Dictionary<string, object> args);
}
namespace Barotrauma.LuaCs
{
partial class LuaPatcherService
{
private static LuaPatcherService instance;
private Dictionary<long, HashSet<(string, LuaCsCompatPatchFunc)>> compatHookPrefixMethods = new Dictionary<long, HashSet<(string, LuaCsCompatPatchFunc)>>();
private Dictionary<long, HashSet<(string, LuaCsCompatPatchFunc)>> compatHookPostfixMethods = new Dictionary<long, HashSet<(string, LuaCsCompatPatchFunc)>>();
private static void _hookLuaCsPatch(MethodBase __originalMethod, object[] __args, object __instance, out object result, ILuaCsHook.HookMethodType hookType)
{
result = null;
try
{
var funcAddr = ((long)__originalMethod.MethodHandle.GetFunctionPointer());
HashSet<(string, LuaCsCompatPatchFunc)> methodSet = null;
switch (hookType)
{
case ILuaCsHook.HookMethodType.Before:
instance.compatHookPrefixMethods.TryGetValue(funcAddr, out methodSet);
break;
case ILuaCsHook.HookMethodType.After:
instance.compatHookPostfixMethods.TryGetValue(funcAddr, out methodSet);
break;
default:
throw new ArgumentException($"Invalid {nameof(ILuaCsHook.HookMethodType)} enum value.", nameof(hookType));
}
if (methodSet != null)
{
var @params = __originalMethod.GetParameters();
var args = new Dictionary<string, object>();
for (int i = 0; i < @params.Length; i++)
{
args.Add(@params[i].Name, __args[i]);
}
foreach (var tuple in methodSet)
{
var _result = tuple.Item2(__instance, args);
if (_result != null)
{
if (_result is DynValue res)
{
if (!res.IsNil())
{
if (__originalMethod is MethodInfo mi && mi.ReturnType != typeof(void))
{
result = res.ToObject(mi.ReturnType);
}
else
{
result = res.ToObject();
}
}
}
else
{
result = _result;
}
}
}
}
}
catch (Exception ex)
{
LuaCsLogger.LogError($"Error in {__originalMethod.Name}:", LuaCsMessageOrigin.Unknown);
LuaCsLogger.HandleException(ex, LuaCsMessageOrigin.Unknown);
}
}
private static bool HookLuaCsPatchPrefix(MethodBase __originalMethod, object[] __args, object __instance)
{
_hookLuaCsPatch(__originalMethod, __args, __instance, out object result, ILuaCsHook.HookMethodType.Before);
return result == null;
}
private static void HookLuaCsPatchPostfix(MethodBase __originalMethod, object[] __args, object __instance) =>
_hookLuaCsPatch(__originalMethod, __args, __instance, out object _, ILuaCsHook.HookMethodType.After);
private static bool HookLuaCsPatchRetPrefix(MethodBase __originalMethod, object[] __args, ref object __result, object __instance)
{
_hookLuaCsPatch(__originalMethod, __args, __instance, out object result, ILuaCsHook.HookMethodType.Before);
if (result != null)
{
__result = result;
return false;
}
else return true;
}
private static void HookLuaCsPatchRetPostfix(MethodBase __originalMethod, object[] __args, ref object __result, object __instance)
{
_hookLuaCsPatch(__originalMethod, __args, __instance, out object result, ILuaCsHook.HookMethodType.After);
if (result != null) __result = result;
}
private static MethodInfo _miHookLuaCsPatchPrefix = typeof(LuaPatcherService).GetMethod("HookLuaCsPatchPrefix", BindingFlags.NonPublic | BindingFlags.Static);
private static MethodInfo _miHookLuaCsPatchPostfix = typeof(LuaPatcherService).GetMethod("HookLuaCsPatchPostfix", BindingFlags.NonPublic | BindingFlags.Static);
private static MethodInfo _miHookLuaCsPatchRetPrefix = typeof(LuaPatcherService).GetMethod("HookLuaCsPatchRetPrefix", BindingFlags.NonPublic | BindingFlags.Static);
private static MethodInfo _miHookLuaCsPatchRetPostfix = typeof(LuaPatcherService).GetMethod("HookLuaCsPatchRetPostfix", BindingFlags.NonPublic | BindingFlags.Static);
// TODO: deprecate this
public void HookMethod(string identifier, MethodBase method, LuaCsCompatPatchFunc patch, ILuaCsHook.HookMethodType hookType = ILuaCsHook.HookMethodType.Before, IAssemblyPlugin owner = null)
{
if (identifier == null || method == null || patch == null)
{
LuaCsLogger.HandleException(new ArgumentNullException("Identifier, Method and Patch arguments must not be null."), LuaCsMessageOrigin.Unknown);
return;
}
ValidatePatchTarget(method);
var funcAddr = ((long)method.MethodHandle.GetFunctionPointer());
var patches = Harmony.GetPatchInfo(method);
if (hookType == ILuaCsHook.HookMethodType.Before)
{
if (method is MethodInfo mi && mi.ReturnType != typeof(void))
{
if (patches == null || patches.Prefixes == null || patches.Prefixes.Find(patch => patch.PatchMethod == _miHookLuaCsPatchRetPrefix) == null)
{
harmony.Patch(method, prefix: new HarmonyMethod(_miHookLuaCsPatchRetPrefix));
}
}
else
{
if (patches == null || patches.Prefixes == null || patches.Prefixes.Find(patch => patch.PatchMethod == _miHookLuaCsPatchPrefix) == null)
{
harmony.Patch(method, prefix: new HarmonyMethod(_miHookLuaCsPatchPrefix));
}
}
if (compatHookPrefixMethods.TryGetValue(funcAddr, out HashSet<(string, LuaCsCompatPatchFunc)> methodSet))
{
if (identifier != "")
{
methodSet.RemoveWhere(tuple => tuple.Item1 == identifier);
}
methodSet.Add((identifier, patch));
}
else if (patch != null)
{
compatHookPrefixMethods.Add(funcAddr, new HashSet<(string, LuaCsCompatPatchFunc)>() { (identifier, patch) });
}
}
else if (hookType == ILuaCsHook.HookMethodType.After)
{
if (method is MethodInfo mi && mi.ReturnType != typeof(void))
{
if (patches == null || patches.Postfixes == null || patches.Postfixes.Find(patch => patch.PatchMethod == _miHookLuaCsPatchRetPostfix) == null)
{
harmony.Patch(method, postfix: new HarmonyMethod(_miHookLuaCsPatchRetPostfix));
}
}
else
{
if (patches == null || patches.Postfixes == null || patches.Postfixes.Find(patch => patch.PatchMethod == _miHookLuaCsPatchPostfix) == null)
{
harmony.Patch(method, postfix: new HarmonyMethod(_miHookLuaCsPatchPostfix));
}
}
if (compatHookPostfixMethods.TryGetValue(funcAddr, out HashSet<(string, LuaCsCompatPatchFunc)> methodSet))
{
if (identifier != "")
{
methodSet.RemoveWhere(tuple => tuple.Item1 == identifier);
}
methodSet.Add((identifier, patch));
}
else if (patch != null)
{
compatHookPostfixMethods.Add(funcAddr, new HashSet<(string, LuaCsCompatPatchFunc)>() { (identifier, patch) });
}
}
}
public void HookMethod(string identifier, string className, string methodName, string[] parameterNames, LuaCsCompatPatchFunc patch, ILuaCsHook.HookMethodType hookMethodType = ILuaCsHook.HookMethodType.Before)
{
var method = ResolveMethod(className, methodName, parameterNames);
if (method == null) return;
if (method.GetParameters().Any(x => x.ParameterType.IsByRef))
{
throw new InvalidOperationException($"{nameof(HookMethod)} doesn't support ByRef parameters; use {nameof(Patch)} instead.");
}
HookMethod(identifier, method, patch, hookMethodType);
}
public void HookMethod(string identifier, string className, string methodName, LuaCsCompatPatchFunc patch, ILuaCsHook.HookMethodType hookMethodType = ILuaCsHook.HookMethodType.Before) =>
HookMethod(identifier, className, methodName, null, patch, hookMethodType);
public void HookMethod(string className, string methodName, LuaCsCompatPatchFunc patch, ILuaCsHook.HookMethodType hookMethodType = ILuaCsHook.HookMethodType.Before) =>
HookMethod("", className, methodName, null, patch, hookMethodType);
public void HookMethod(string className, string methodName, string[] parameterNames, LuaCsCompatPatchFunc patch, ILuaCsHook.HookMethodType hookMethodType = ILuaCsHook.HookMethodType.Before) =>
HookMethod("", className, methodName, parameterNames, patch, hookMethodType);
public void UnhookMethod(string identifier, MethodBase method, ILuaCsHook.HookMethodType hookType = ILuaCsHook.HookMethodType.Before)
{
var funcAddr = (long)method.MethodHandle.GetFunctionPointer();
Dictionary<long, HashSet<(string, LuaCsCompatPatchFunc)>> methods;
if (hookType == ILuaCsHook.HookMethodType.Before) methods = compatHookPrefixMethods;
else if (hookType == ILuaCsHook.HookMethodType.After) methods = compatHookPostfixMethods;
else throw null;
if (methods.ContainsKey(funcAddr)) methods[funcAddr]?.RemoveWhere(t => t.Item1 == identifier);
}
protected void UnhookMethod(string identifier, string className, string methodName, string[] parameterNames, ILuaCsHook.HookMethodType hookType = ILuaCsHook.HookMethodType.Before)
{
var method = ResolveMethod(className, methodName, parameterNames);
if (method == null) return;
UnhookMethod(identifier, method, hookType);
}
}
}
@@ -0,0 +1,816 @@
using Barotrauma.LuaCs;
using HarmonyLib;
using Microsoft.Xna.Framework;
using MoonSharp.Interpreter;
using MoonSharp.Interpreter.Interop;
using Sigil;
using Sigil.NonGeneric;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Reflection.Emit;
using System.Text;
using System.Text.RegularExpressions;
namespace Barotrauma
{
public delegate void LuaCsAction(params object[] args);
public delegate object LuaCsFunc(params object[] args);
public delegate DynValue LuaCsPatchFunc(object instance, LuaPatcherService.ParameterTable ptable);
}
namespace Barotrauma.LuaCs
{
public partial class LuaPatcherService : ILuaPatcher
{
private class LuaCsHookCallback
{
public string name;
public string hookName;
public LuaCsFunc func;
public LuaCsHookCallback(string name, string hookName, LuaCsFunc func)
{
this.name = name;
this.hookName = hookName;
this.func = func;
}
}
private class LuaCsPatch
{
public string Identifier { get; set; }
public LuaCsPatchFunc PatchFunc { get; set; }
}
private class PatchedMethod
{
public PatchedMethod(MethodInfo harmonyPrefix, MethodInfo harmonyPostfix)
{
HarmonyPrefixMethod = harmonyPrefix;
HarmonyPostfixMethod = harmonyPostfix;
Prefixes = new Dictionary<string, LuaCsPatch>();
Postfixes = new Dictionary<string, LuaCsPatch>();
}
public MethodInfo HarmonyPrefixMethod { get; }
public MethodInfo HarmonyPostfixMethod { get; }
public IEnumerator<LuaCsPatch> GetPrefixEnumerator() => Prefixes.Values.GetEnumerator();
public IEnumerator<LuaCsPatch> GetPostfixEnumerator() => Postfixes.Values.GetEnumerator();
public Dictionary<string, LuaCsPatch> Prefixes { get; }
public Dictionary<string, LuaCsPatch> Postfixes { get; }
}
public class ParameterTable
{
private readonly Dictionary<string, object> parameters;
private bool returnValueModified;
private object returnValue;
public ParameterTable(Dictionary<string, object> dict)
{
parameters = dict;
}
public object this[string paramName]
{
get
{
if (ModifiedParameters.TryGetValue(paramName, out var value))
{
return value;
}
return OriginalParameters[paramName];
}
set
{
ModifiedParameters[paramName] = value;
}
}
public object OriginalReturnValue { get; private set; }
public object ReturnValue
{
get
{
if (returnValueModified) return returnValue;
return OriginalReturnValue;
}
set
{
returnValueModified = true;
returnValue = value;
}
}
public bool PreventExecution { get; set; }
public Dictionary<string, object> OriginalParameters => parameters;
[MoonSharpHidden]
public Dictionary<string, object> ModifiedParameters { get; } = new Dictionary<string, object>();
}
private struct MethodKey : IEquatable<MethodKey>
{
public ModuleHandle ModuleHandle { get; set; }
public int MetadataToken { get; set; }
public override bool Equals(object obj)
{
return obj is MethodKey key && Equals(key);
}
public bool Equals(MethodKey other)
{
return ModuleHandle.Equals(other.ModuleHandle) && MetadataToken == other.MetadataToken;
}
public override int GetHashCode()
{
return HashCode.Combine(ModuleHandle, MetadataToken);
}
public static bool operator ==(MethodKey left, MethodKey right)
{
return left.Equals(right);
}
public static bool operator !=(MethodKey left, MethodKey right)
{
return !(left == right);
}
public static MethodKey Create(MethodBase method) => new MethodKey
{
ModuleHandle = method.Module.ModuleHandle,
MetadataToken = method.MetadataToken,
};
}
private static readonly string[] prohibitedHooks =
{
"Barotrauma.Lua",
"Barotrauma.Cs",
"Barotrauma.ContentPackageManager",
};
private Harmony harmony;
private Lazy<ModuleBuilder> patchModuleBuilder;
private readonly Dictionary<MethodKey, PatchedMethod> registeredPatches = new Dictionary<MethodKey, PatchedMethod>();
public LuaPatcherService()
{
instance = this;
harmony = new Harmony("LuaCsForBarotrauma");
patchModuleBuilder = new Lazy<ModuleBuilder>(CreateModuleBuilder);
UserData.RegisterType<ParameterTable>();
// whats this for?
/*
var hookType = UserData.RegisterType<EventService>();
var hookDesc = (StandardUserDataDescriptor)hookType;
typeof(EventService).GetMethods(BindingFlags.NonPublic | BindingFlags.Instance).ToList().ForEach(m => {
if (
m.Name.Contains("HookMethod") ||
m.Name.Contains("UnhookMethod") ||
m.Name.Contains("EnqueueFunction") ||
m.Name.Contains("EnqueueTimedFunction")
)
{
hookDesc.AddMember(m.Name, new MethodMemberDescriptor(m, InteropAccessMode.Default));
}
});
*/
}
private static void ValidatePatchTarget(MethodBase method)
{
if (prohibitedHooks.Any(h => method.DeclaringType.FullName.StartsWith(h)))
{
throw new ArgumentException("Hooks into the modding environment are prohibited.");
}
}
private static string NormalizeIdentifier(string identifier)
{
return identifier?.Trim().ToLowerInvariant();
}
private ModuleBuilder CreateModuleBuilder()
{
var assemblyName = $"LuaCsHookPatch-{Guid.NewGuid():N}";
var assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(new AssemblyName(assemblyName), AssemblyBuilderAccess.RunAndCollect);
var moduleBuilder = assemblyBuilder.DefineDynamicModule("LuaCsHookPatch");
// This code emits the Roslyn attribute
// "IgnoresAccessChecksToAttribute" so we can freely access
// the Barotrauma assembly from our dynamic patches.
// This is important because the generated IL references
// non-public types/members.
// class IgnoresAccessChecksToAttribute {
var typeBuilder = moduleBuilder.DefineType(
name: "System.Runtime.CompilerServices.IgnoresAccessChecksToAttribute",
attr: TypeAttributes.NotPublic | TypeAttributes.Sealed | TypeAttributes.Class,
parent: typeof(Attribute));
// [AttributeUsage(AllowMultiple = true)]
var attributeUsageAttribute = new CustomAttributeBuilder(
con: typeof(AttributeUsageAttribute).GetConstructor(new[] { typeof(AttributeTargets) }),
constructorArgs: new object[] { AttributeTargets.Assembly },
namedProperties: new[] { typeof(AttributeUsageAttribute).GetProperty("AllowMultiple") },
propertyValues: new object[] { true });
typeBuilder.SetCustomAttribute(attributeUsageAttribute);
// private readonly string assemblyName;
var attributeTypeFieldBuilder = typeBuilder.DefineField(
fieldName: "assemblyName",
type: typeof(string),
attributes: FieldAttributes.Private | FieldAttributes.InitOnly);
var ctor = Emit.BuildConstructor(
parameterTypes: new[] { typeof(string) },
type: typeBuilder,
attributes: MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.RTSpecialName,
callingConvention: CallingConventions.Standard | CallingConventions.HasThis);
// IL: this.assemblyName = arg;
ctor.LoadArgument(0);
ctor.LoadArgument(1);
ctor.StoreField(attributeTypeFieldBuilder);
ctor.Return();
ctor.CreateConstructor();
// public string AttributeName => this.assemblyName;
var attributeNameGetter = Emit.BuildMethod(
returnType: typeof(string),
parameterTypes: new Type[0],
type: typeBuilder,
name: "get_AttributeName",
attributes: MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.RTSpecialName,
callingConvention: CallingConventions.Standard | CallingConventions.HasThis);
attributeNameGetter.LoadArgument(0);
attributeNameGetter.LoadField(attributeTypeFieldBuilder);
attributeNameGetter.Return();
var attributeName = typeBuilder.DefineProperty(
name: "AttributeName",
attributes: PropertyAttributes.None,
returnType: typeof(string),
parameterTypes: null);
attributeName.SetGetMethod(attributeNameGetter.CreateMethod());
// }
var type = typeBuilder.CreateTypeInfo().AsType();
// The assembly names are hardcoded, otherwise it would
// break unit tests.
var assembliesToExpose = new[] { "Barotrauma", "DedicatedServer" };
foreach (var name in assembliesToExpose)
{
var attr = new CustomAttributeBuilder(
con: type.GetConstructor(new[] { typeof(string)}),
constructorArgs: new[] { name });
assemblyBuilder.SetCustomAttribute(attr);
}
return moduleBuilder;
}
private static MethodBase ResolveMethod(string className, string methodName, string[] parameters)
{
var classType = LuaCsSetup.Instance.PluginManagementService.GetType(className);
if (classType == null) throw new ScriptRuntimeException($"invalid class name '{className}'");
const BindingFlags BINDING_FLAGS = BindingFlags.Static | BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;
MethodBase method = null;
try
{
if (parameters != null)
{
Type[] parameterTypes = new Type[parameters.Length];
for (int i = 0; i < parameters.Length; i++)
{
Type type = LuaCsSetup.Instance.PluginManagementService.GetType(parameters[i]);
if (type == null)
{
throw new ScriptRuntimeException($"invalid parameter type '{parameters[i]}'");
}
parameterTypes[i] = type;
}
method = methodName switch
{
".cctor" => classType.TypeInitializer,
".ctor" => classType.GetConstructors(BINDING_FLAGS)
.Except(new[] { classType.TypeInitializer })
.Where(x => x.GetParameters().Select(x => x.ParameterType).SequenceEqual(parameterTypes))
.SingleOrDefault(),
_ => classType.GetMethod(methodName, BINDING_FLAGS, null, parameterTypes, null),
};
}
else
{
ConstructorInfo GetCtor()
{
var ctors = classType.GetConstructors(BINDING_FLAGS)
.Except(new[] { classType.TypeInitializer })
.GetEnumerator();
if (!ctors.MoveNext()) return null;
var ctor = ctors.Current;
if (ctors.MoveNext()) throw new AmbiguousMatchException();
return ctor;
}
method = methodName switch
{
".cctor" => throw new ScriptRuntimeException("type initializers can't have parameters"),
".ctor" => GetCtor(),
_ => classType.GetMethod(methodName, BINDING_FLAGS),
};
}
}
catch (AmbiguousMatchException)
{
throw new ScriptRuntimeException("ambiguous method signature");
}
if (method == null)
{
var parameterNamesStr = parameters == null ? "" : string.Join(", ", parameters);
throw new ScriptRuntimeException($"method '{methodName}({parameterNamesStr})' not found in class '{className}'");
}
return method;
}
private class DynamicParameterMapping
{
public DynamicParameterMapping(string name, Type originalMethodParamType, Type harmonyPatchParamType)
{
ParameterName = name;
OriginalMethodParamType = originalMethodParamType;
HarmonyPatchParamType = harmonyPatchParamType;
}
public string ParameterName { get; set; }
public Type OriginalMethodParamType { get; set; }
public Type HarmonyPatchParamType { get; set; }
}
private static readonly Regex InvalidIdentifierCharsRegex = new Regex(@"[^\w\d]", RegexOptions.Compiled);
private const string FIELD_LUACS = "LuaCs";
public bool IsDisposed { get; private set; }
// If you need to debug this:
// - use https://sharplab.io ; it's a very useful for resource for writing IL by hand.
// - use il.NewMessage("") or il.WriteLine("") to see where the IL crashes at runtime.
private MethodInfo CreateDynamicHarmonyPatch(string identifier, MethodBase original, LuaCsHook.HookMethodType hookType)
{
var parameters = new List<DynamicParameterMapping>
{
new DynamicParameterMapping("__originalMethod", null, typeof(MethodBase)),
new DynamicParameterMapping("__instance", null, typeof(object)),
};
var hasReturnType = original is MethodInfo mi && mi.ReturnType != typeof(void);
if (hasReturnType)
{
parameters.Add(new DynamicParameterMapping("__result", null, typeof(object).MakeByRefType()));
}
foreach (var parameter in original.GetParameters())
{
var paramName = parameter.Name;
var originalMethodParamType = parameter.ParameterType;
var harmonyPatchParamType = originalMethodParamType.IsByRef
? originalMethodParamType
// Make all parameters modifiable by the harmony patch
: originalMethodParamType.MakeByRefType();
parameters.Add(new DynamicParameterMapping(paramName, originalMethodParamType, harmonyPatchParamType));
}
static string MangleName(object o) => InvalidIdentifierCharsRegex.Replace(o?.ToString(), "_");
var moduleBuilder = patchModuleBuilder.Value;
var mangledName = original.DeclaringType != null
? $"{MangleName(original.DeclaringType)}-{MangleName(original)}"
: MangleName(original);
var typeBuilder = moduleBuilder.DefineType($"Patch_{identifier}_{Guid.NewGuid():N}_{mangledName}", TypeAttributes.Public);
var luaCsField = typeBuilder.DefineField(FIELD_LUACS, typeof(LuaCsSetup), FieldAttributes.Public | FieldAttributes.Static);
var methodName = hookType == LuaCsHook.HookMethodType.Before ? "HarmonyPrefix" : "HarmonyPostfix";
var il = Emit.BuildMethod(
returnType: hookType == LuaCsHook.HookMethodType.Before ? typeof(bool) : typeof(void),
parameterTypes: parameters.Select(x => x.HarmonyPatchParamType).ToArray(),
type: typeBuilder,
name: methodName,
attributes: MethodAttributes.Public | MethodAttributes.Static,
callingConvention: CallingConventions.Standard);
var labelReturn = il.DefineLabel("endOfFunction");
il.BeginExceptionBlock(out var exceptionBlock);
// IL: var harmonyReturnValue = true;
var harmonyReturnValue = il.DeclareLocal<bool>("harmonyReturnValue");
il.LoadConstant(true);
il.StoreLocal(harmonyReturnValue);
// IL: var patchKey = MethodKey.Create(__originalMethod);
var patchKey = il.DeclareLocal<MethodKey>("patchKey");
il.LoadArgument(0); // load __originalMethod
il.CastClass<MethodBase>();
il.Call(typeof(MethodKey).GetMethod(nameof(MethodKey.Create)));
il.StoreLocal(patchKey);
// IL: var patchExists = instance.registeredPatches.TryGetValue(patchKey, out MethodPatches patches)
var patchExists = il.DeclareLocal<bool>("patchExists");
var patches = il.DeclareLocal<PatchedMethod>("patches");
il.LoadField(typeof(LuaPatcherService).GetField(nameof(instance), BindingFlags.NonPublic | BindingFlags.Static));
il.LoadField(typeof(LuaPatcherService).GetField(nameof(registeredPatches), BindingFlags.NonPublic | BindingFlags.Instance));
il.LoadLocal(patchKey);
il.LoadLocalAddress(patches); // out parameter
il.Call(typeof(Dictionary<MethodKey, PatchedMethod>).GetMethod("TryGetValue"));
il.StoreLocal(patchExists);
// IL: if (!patchExists)
il.LoadLocal(patchExists);
il.IfNot((il) =>
{
// XXX: if we get here, it's probably because a patched
// method was running when `reloadlua` was executed.
// This can happen with a postfix on
// `Barotrauma.Networking.GameServer#Update`.
il.Leave(labelReturn);
});
// IL: var parameterDict = new Dictionary<string, object>(<paramCount>);
var parameterDict = il.DeclareLocal<Dictionary<string, object>>("parameterDict");
il.LoadConstant(parameters.Count(x => x.OriginalMethodParamType != null)); // preallocate the dictionary using the # of args
il.NewObject(typeof(Dictionary<string, object>), typeof(int));
il.StoreLocal(parameterDict);
for (ushort i = 0; i < parameters.Count; i++)
{
// Skip parameters that don't exist in the original method
if (parameters[i].OriginalMethodParamType == null) continue;
// IL: parameterDict.Add(<paramName>, <paramValue>);
il.LoadLocal(parameterDict);
il.LoadConstant(parameters[i].ParameterName);
il.LoadArgument(i);
il.ToObject(parameters[i].HarmonyPatchParamType);
il.Call(typeof(Dictionary<string, object>).GetMethod("Add"));
}
// IL: var ptable = new ParameterTable(parameterDict);
var ptable = il.DeclareLocal<ParameterTable>("ptable");
il.LoadLocal(parameterDict);
il.NewObject(typeof(ParameterTable), typeof(Dictionary<string, object>));
il.StoreLocal(ptable);
if (hasReturnType && hookType == LuaCsHook.HookMethodType.After)
{
// IL: ptable.OriginalReturnValue = __result;
il.LoadLocal(ptable);
il.LoadArgument(2); // ref __result
il.ToObject(parameters[2].HarmonyPatchParamType);
il.Call(typeof(ParameterTable).GetProperty(nameof(ParameterTable.OriginalReturnValue)).GetSetMethod(nonPublic: true));
}
// IL: var enumerator = patches.GetPrefixEnumerator();
var enumerator = il.DeclareLocal<IEnumerator<LuaCsPatch>>("enumerator");
il.LoadLocal(patches);
il.CallVirtual(typeof(PatchedMethod).GetMethod(
name: hookType == LuaCsHook.HookMethodType.Before
? nameof(PatchedMethod.GetPrefixEnumerator)
: nameof(PatchedMethod.GetPostfixEnumerator),
bindingAttr: BindingFlags.Public | BindingFlags.Instance));
il.StoreLocal(enumerator);
var labelUpdateParameters = il.DefineLabel("updateParameters");
// Iterate over prefixes/postfixes
il.ForEachEnumerator<LuaCsPatch>(enumerator, (il, current, labelLeave) =>
{
// IL: var luaReturnValue = current.PatchFunc.Invoke(__instance, ptable);
var luaReturnValue = il.DeclareLocal<DynValue>("luaReturnValue");
il.LoadLocal(current);
il.Call(typeof(LuaCsPatch).GetProperty(nameof(LuaCsPatch.PatchFunc)).GetGetMethod());
il.LoadArgument(1); // __instance
il.LoadLocal(ptable);
il.CallVirtual(typeof(LuaCsPatchFunc).GetMethod("Invoke"));
il.StoreLocal(luaReturnValue);
if (hasReturnType)
{
// IL: var ptableReturnValue = ptable.ReturnValue;
var ptableReturnValue = il.DeclareLocal<object>("ptableReturnValue");
il.LoadLocal(ptable);
il.Call(typeof(ParameterTable).GetProperty(nameof(ParameterTable.ReturnValue)).GetGetMethod());
il.StoreLocal(ptableReturnValue);
// IL: if (ptableReturnValue != null)
il.LoadLocal(ptableReturnValue);
il.If((il) =>
{
// IL: __result = ptableReturnValue;
il.LoadArgument(2); // ref __result
il.LoadLocal(ptableReturnValue);
il.StoreIndirect(typeof(object));
il.Break();
});
// IL: if (luaReturnValue != null)
il.LoadLocal(luaReturnValue);
il.If((il) =>
{
// IL: if (!luaReturnValue.IsVoid())
il.LoadLocal(luaReturnValue);
il.Call(typeof(DynValue).GetMethod(nameof(DynValue.IsVoid)));
il.IfNot((il) =>
{
// IL: var csReturnType = Type.GetTypeFromHandle(<original.ReturnType>);
var csReturnType = il.DeclareLocal<Type>("csReturnType");
il.LoadType(((MethodInfo)original).ReturnType);
il.StoreLocal(csReturnType);
// IL: var csReturnValue = luaReturnValue.ToObject(csReturnType);
var csReturnValue = il.DeclareLocal<object>("csReturnValue");
il.LoadLocal(luaReturnValue);
il.LoadLocal(csReturnType);
il.Call(typeof(DynValue).GetMethod(
name: nameof(DynValue.ToObject),
bindingAttr: BindingFlags.Public | BindingFlags.Instance,
binder: null,
types: new Type[] { typeof(Type) },
modifiers: null));
il.StoreLocal(csReturnValue);
// IL: __result = csReturnValue;
il.LoadArgument(2); // ref __result
il.LoadLocal(csReturnValue);
il.StoreIndirect(typeof(object));
});
});
}
// IL: if (ptable.PreventExecution)
il.LoadLocal(ptable);
il.Call(typeof(ParameterTable).GetProperty(nameof(ParameterTable.PreventExecution)).GetGetMethod());
il.If((il) =>
{
// IL: harmonyReturnValue = false;
il.LoadConstant(false);
il.StoreLocal(harmonyReturnValue);
// IL: break;
il.Leave(labelLeave);
});
});
// IL: var modifiedParameters = ptable.ModifiedParameters;
var modifiedParameters = il.DeclareLocal<Dictionary<string, object>>("modifiedParameters");
il.LoadLocal(ptable);
il.Call(typeof(ParameterTable).GetProperty(nameof(ParameterTable.ModifiedParameters)).GetGetMethod());
il.StoreLocal(modifiedParameters);
// IL: object modifiedValue;
var modifiedValue = il.DeclareLocal<object>("modifiedValue");
// Update the parameters
for (ushort i = 0; i < parameters.Count; i++)
{
// Skip parameters that don't exist in the original method
if (parameters[i].OriginalMethodParamType == null) continue;
// IL: if (modifiedParameters.TryGetValue("parameterName", out modifiedValue))
il.LoadLocal(modifiedParameters);
il.LoadConstant(parameters[i].ParameterName);
il.LoadLocalAddress(modifiedValue); // out parameter
il.Call(typeof(Dictionary<string, object>).GetMethod(nameof(Dictionary<string, object>.TryGetValue)));
il.If((il) =>
{
// XXX: GetElementType() gets the "real" type behind
// the ByRef. This is safe because all the parameters
// are made into ByRef to support modification.
var paramType = parameters[i].HarmonyPatchParamType.GetElementType();
// IL: ref argName = modifiedValue;
il.LoadArgument(i);
il.LoadLocalAndCast(modifiedValue, paramType);
if (paramType.IsValueType)
{
il.StoreObject(paramType);
}
else
{
il.StoreIndirect(paramType);
}
});
}
il.MarkLabel(labelReturn);
// IL: catch (Exception exception)
il.BeginCatchAllBlock(exceptionBlock, out var catchBlock);
var exception = il.DeclareLocal<Exception>("exception");
il.StoreLocal(exception);
// IL: if (LuaCs != null)
il.LoadField(luaCsField);
il.If((il) =>
{
// IL: LuaCs.HandleException(exception, LuaCsMessageOrigin.LuaMod);
il.LoadLocal(exception);
il.LoadConstant((int)LuaCsMessageOrigin.LuaMod); // underlying enum type is int
il.Call(typeof(LuaCsLogger).GetMethod(nameof(LuaCsLogger.HandleException), BindingFlags.Public | BindingFlags.Static));
});
il.EndCatchBlock(catchBlock);
il.EndExceptionBlock(exceptionBlock);
// Only prefixes return a bool
if (hookType == LuaCsHook.HookMethodType.Before)
{
il.LoadLocal(harmonyReturnValue);
}
il.Return();
var method = il.CreateMethod();
for (var i = 0; i < parameters.Count; i++)
{
method.DefineParameter(i + 1, ParameterAttributes.None, parameters[i].ParameterName);
}
var type = typeBuilder.CreateType();
type.GetField(FIELD_LUACS, BindingFlags.Public | BindingFlags.Static).SetValue(null, LuaCsSetup.Instance);
return type.GetMethod(methodName, BindingFlags.Public | BindingFlags.Static);
}
private string Patch(string identifier, MethodBase method, LuaCsPatchFunc patch, LuaCsHook.HookMethodType hookType = LuaCsHook.HookMethodType.Before)
{
if (method == null) throw new ArgumentNullException(nameof(method));
if (patch == null) throw new ArgumentNullException(nameof(patch));
ValidatePatchTarget(method);
identifier ??= Guid.NewGuid().ToString("N");
identifier = NormalizeIdentifier(identifier);
var patchKey = MethodKey.Create(method);
if (!registeredPatches.TryGetValue(patchKey, out var methodPatches))
{
var harmonyPrefix = CreateDynamicHarmonyPatch(identifier, method, LuaCsHook.HookMethodType.Before);
var harmonyPostfix = CreateDynamicHarmonyPatch(identifier, method, LuaCsHook.HookMethodType.After);
harmony.Patch(method, prefix: new HarmonyMethod(harmonyPrefix), postfix: new HarmonyMethod(harmonyPostfix));
methodPatches = registeredPatches[patchKey] = new PatchedMethod(harmonyPrefix, harmonyPostfix);
}
if (hookType == LuaCsHook.HookMethodType.Before)
{
if (methodPatches.Prefixes.Remove(identifier))
{
LuaCsLogger.LogMessage($"Replacing existing prefix: {identifier}");
}
methodPatches.Prefixes.Add(identifier, new LuaCsPatch
{
Identifier = identifier,
PatchFunc = patch,
});
}
else if (hookType == LuaCsHook.HookMethodType.After)
{
if (methodPatches.Postfixes.Remove(identifier))
{
LuaCsLogger.LogMessage($"Replacing existing postfix: {identifier}");
}
methodPatches.Postfixes.Add(identifier, new LuaCsPatch
{
Identifier = identifier,
PatchFunc = patch,
});
}
return identifier;
}
public string Patch(string identifier, string className, string methodName, string[] parameterTypes, LuaCsPatchFunc patch, LuaCsHook.HookMethodType hookType = LuaCsHook.HookMethodType.Before)
{
var method = ResolveMethod(className, methodName, parameterTypes);
return Patch(identifier, method, patch, hookType);
}
public string Patch(string identifier, string className, string methodName, LuaCsPatchFunc patch, LuaCsHook.HookMethodType hookType = LuaCsHook.HookMethodType.Before)
{
var method = ResolveMethod(className, methodName, null);
return Patch(identifier, method, patch, hookType);
}
public string Patch(string className, string methodName, string[] parameterTypes, LuaCsPatchFunc patch, LuaCsHook.HookMethodType hookType = LuaCsHook.HookMethodType.Before)
{
var method = ResolveMethod(className, methodName, parameterTypes);
return Patch(null, method, patch, hookType);
}
public string Patch(string className, string methodName, LuaCsPatchFunc patch, LuaCsHook.HookMethodType hookType = LuaCsHook.HookMethodType.Before)
{
var method = ResolveMethod(className, methodName, null);
return Patch(null, method, patch, hookType);
}
private bool RemovePatch(string identifier, MethodBase method, LuaCsHook.HookMethodType hookType)
{
if (identifier == null) throw new ArgumentNullException(nameof(identifier));
identifier = NormalizeIdentifier(identifier);
var patchKey = MethodKey.Create(method);
if (!registeredPatches.TryGetValue(patchKey, out var methodPatches))
{
return false;
}
return hookType switch
{
LuaCsHook.HookMethodType.Before => methodPatches.Prefixes.Remove(identifier),
LuaCsHook.HookMethodType.After => methodPatches.Postfixes.Remove(identifier),
_ => throw new ArgumentException($"Invalid {nameof(LuaCsHook.HookMethodType)} enum value.", nameof(hookType)),
};
}
public bool RemovePatch(string identifier, string className, string methodName, string[] parameterTypes, LuaCsHook.HookMethodType hookType)
{
var method = ResolveMethod(className, methodName, parameterTypes);
return RemovePatch(identifier, method, hookType);
}
public bool RemovePatch(string identifier, string className, string methodName, LuaCsHook.HookMethodType hookType)
{
var method = ResolveMethod(className, methodName, null);
return RemovePatch(identifier, method, hookType);
}
private void ClearAll()
{
harmony?.UnpatchSelf();
foreach (var (_, patch) in registeredPatches)
{
// Remove references stored in our dynamic types so the generated
// assembly can be garbage-collected.
patch.HarmonyPrefixMethod.DeclaringType
.GetField(FIELD_LUACS, BindingFlags.Public | BindingFlags.Static)
.SetValue(null, null);
patch.HarmonyPostfixMethod.DeclaringType
.GetField(FIELD_LUACS, BindingFlags.Public | BindingFlags.Static)
.SetValue(null, null);
}
registeredPatches.Clear();
compatHookPrefixMethods.Clear();
compatHookPostfixMethods.Clear();
}
public void Dispose()
{
IsDisposed = true;
ClearAll();
}
public FluentResults.Result Reset()
{
ClearAll();
return FluentResults.Result.Ok();
}
}
}
@@ -0,0 +1,167 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Text;
using System.IO;
using MoonSharp.Interpreter;
using MoonSharp.Interpreter.Loaders;
using System.Linq;
using System.Threading.Tasks;
using Barotrauma.LuaCs.Data;
using Barotrauma.LuaCs;
using FluentResults;
namespace Barotrauma.LuaCs
{
public class LuaScriptLoader : ScriptLoaderBase, ILuaScriptLoader
{
public LuaScriptLoader(ISafeStorageService storageService, Lazy<ILoggerService> loggerService)
{
this._storageService = storageService;
this._loggerService = loggerService;
storageService.UseCaching = true;
}
private readonly ISafeStorageService _storageService;
private readonly Lazy<ILoggerService> _loggerService;
public override object LoadFile(string file, Table globalContext)
{
IService.CheckDisposed(this);
if (file.IsNullOrWhiteSpace())
{
return null;
}
var res = _storageService.TryLoadText(file);
if (res.IsFailed || res is not { Value: { } script})
{
UnsafeLogErrors($"Failed to load file '{file}'.", res.ToResult());
return null;
}
if (script.IsNullOrWhiteSpace())
{
UnsafeLogErrors($"The file '{file}' is empty. ", res.ToResult());
return null;
}
return script;
}
public void ClearCaches()
{
IService.CheckDisposed(this);
_storageService?.PurgeCache();
}
public void SetCachingPolicy(bool useCaching)
{
if (_storageService is null)
{
return;
}
if (!useCaching)
{
_storageService.PurgeCache();
}
_storageService.UseCaching = useCaching;
}
public async Task<Result<ImmutableArray<(ContentPath Path, Result<string>)>>> CacheResourcesAsync(ImmutableArray<ILuaScriptResourceInfo> resourceInfos)
{
IService.CheckDisposed(this);
if (!_storageService.UseCaching)
{
return FluentResults.Result.Fail($"Caching is not enabled.");
}
return await this._storageService.LoadPackageTextFilesAsync([..resourceInfos.SelectMany(ri => ri.FilePaths)]);
}
public override bool ScriptFileExists(string file)
{
IService.CheckDisposed(this);
var result = _storageService.FileExists(file);
if (result is { IsFailed: true })
{
UnsafeLogErrors($"Unable to find and load file \"{file}\".", result.ToResult());
return false;
}
return result.Value;
}
private void UnsafeLogErrors(string message, FluentResults.Result result = null)
{
_loggerService.Value.LogError($"{nameof(LuaScriptLoader)}: {message}");
if (result is null || result.Errors.Count <= 0)
{
return;
}
foreach (var error in result.Errors)
{
_loggerService.Value.LogError($"{nameof(LuaScriptLoader)}: Error: {error.Message}.");
}
}
public void Dispose()
{
if (!ModUtils.Threading.CheckIfClearAndSetBool(ref _isDisposed))
{
return;
}
_storageService?.Dispose();
_loggerService?.Value.Dispose();
}
private int _isDisposed = 0;
public bool IsDisposed => ModUtils.Threading.GetBool(ref _isDisposed);
public bool IsFileAccessible(string path, bool readOnly, bool checkWhitelistOnly = true)
{
IService.CheckDisposed(this);
return _storageService.IsFileAccessible(path, readOnly, checkWhitelistOnly);
}
public void AddFileToWhitelist(string path, bool readOnly = true)
{
IService.CheckDisposed(this);
_storageService.AddFileToWhitelist(path, readOnly);
}
public void AddFilesToWhitelist(ImmutableArray<string> paths, bool readOnly = true)
{
IService.CheckDisposed(this);
_storageService.AddFilesToWhitelist(paths, readOnly);
}
public void RemoveFileFromAllWhitelists(string path)
{
IService.CheckDisposed(this);
_storageService.RemoveFileFromAllWhitelists(path);
}
public FluentResults.Result SetReadOnlyWhitelist(ImmutableArray<string> filePaths)
{
IService.CheckDisposed(this);
return _storageService.SetReadOnlyWhitelist(filePaths);
}
public FluentResults.Result SetReadWriteWhitelist(ImmutableArray<string> filePaths)
{
IService.CheckDisposed(this);
return _storageService.SetReadWriteWhitelist(filePaths);
}
public void ClearAllWhitelists()
{
IService.CheckDisposed(this);
_storageService.ClearAllWhitelists();
}
}
}
@@ -0,0 +1,413 @@
using MoonSharp.Interpreter;
using MoonSharp.Interpreter.Interop;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
namespace Barotrauma.LuaCs;
public interface ILuaUserDataService : IReusableService
{
IReadOnlyDictionary<string, IUserDataDescriptor> Descriptors { get; }
IUserDataDescriptor RegisterType(string typeName);
void RegisterExtensionType(string typeName);
bool IsRegistered(string typeName);
void UnregisterType(string typeName, bool deleteHistory = false);
object CreateStatic(string typeName);
bool IsTargetType(object obj, string typeName);
string TypeOf(object obj);
object CreateEnumTable(string typeName);
void MakeFieldAccessible(IUserDataDescriptor IUUD, string fieldName);
void MakeMethodAccessible(IUserDataDescriptor IUUD, string methodName, string[] parameters = null);
void MakePropertyAccessible(IUserDataDescriptor IUUD, string propertyName);
void AddMethod(IUserDataDescriptor IUUD, string methodName, object function);
void AddField(IUserDataDescriptor IUUD, string fieldName, DynValue value);
void RemoveMember(IUserDataDescriptor IUUD, string memberName);
bool HasMember(object obj, string memberName);
/// <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>
DynValue CreateUserDataFromDescriptor(DynValue scriptObject, IUserDataDescriptor 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>
DynValue CreateUserDataFromType(DynValue scriptObject, Type desiredType);
void AddCallMetaTable(object userdata);
}
public class LuaUserDataService : ILuaUserDataService
{
public bool IsDisposed { get; private set; }
public IReadOnlyDictionary<string, IUserDataDescriptor> Descriptors => descriptors;
private ConcurrentDictionary<string, IUserDataDescriptor> descriptors;
private readonly IPluginManagementService _pluginManagementService;
public LuaUserDataService(IPluginManagementService pluginManagementService)
{
descriptors = new ConcurrentDictionary<string, IUserDataDescriptor>();
_pluginManagementService = pluginManagementService;
}
public IUserDataDescriptor this[string key]
{
get
{
return descriptors.GetValueOrDefault(key);
}
}
private Type GetType(string typeName) => _pluginManagementService.GetType(typeName, includeInterfaces: true);
public 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 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 bool IsRegistered(string typeName)
{
Type type = GetType(typeName);
if (type == null)
{
return false;
}
return UserData.GetDescriptorForType(type, true) != null;
}
public 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 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 string TypeOf(object obj)
{
if (obj == null) { throw new ScriptRuntimeException("userdata is nil"); }
return obj.GetType().FullName;
}
public 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;
}
public 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);
return generic.Invoke(null, null);
}
private 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 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 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 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 = 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 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 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 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 (LuaCsSetup.Instance != null)
{
return LuaCsSetup.Instance.CallLuaFunction(function, arg3.GetArray());
}
return null;
}));
}
public 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 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 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;
}
public DynValue CreateUserDataFromDescriptor(DynValue scriptObject, IUserDataDescriptor desiredTypeDescriptor)
{
return UserData.Create(scriptObject.ToObject(desiredTypeDescriptor.Type), desiredTypeDescriptor);
}
public DynValue CreateUserDataFromType(DynValue scriptObject, Type desiredType)
{
IUserDataDescriptor descriptor = UserData.GetDescriptorForType(desiredType, true);
descriptor ??= new StandardUserDataDescriptor(desiredType, InteropAccessMode.Default);
return CreateUserDataFromDescriptor(scriptObject, descriptor);
}
public void AddCallMetaTable(object userdata) { }
public void Dispose()
{
IsDisposed = true;
descriptors.Clear();
}
public FluentResults.Result Reset()
{
descriptors.Clear();
return FluentResults.Result.Ok();
}
}
@@ -0,0 +1,236 @@
using Barotrauma;
using Barotrauma.LuaCs;
using MoonSharp.Interpreter;
using MoonSharp.Interpreter.Interop;
using System;
using System.Collections.Generic;
using System.Reflection;
namespace Barotrauma.LuaCs;
public interface ISafeLuaUserDataService : IService
{
bool IsAllowed(string typeName);
IUserDataDescriptor RegisterType(string typeName);
void RegisterExtensionType(string typeName);
bool IsRegistered(string typeName);
void UnregisterType(string typeName, bool deleteHistory = false);
object CreateStatic(string typeName);
bool IsTargetType(object obj, string typeName);
string TypeOf(object obj);
object CreateEnumTable(string typeName);
void MakeFieldAccessible(IUserDataDescriptor IUUD, string fieldName);
void MakeMethodAccessible(IUserDataDescriptor IUUD, string methodName, string[] parameters = null);
void MakePropertyAccessible(IUserDataDescriptor IUUD, string propertyName);
void AddMethod(IUserDataDescriptor IUUD, string methodName, object function);
void AddField(IUserDataDescriptor IUUD, string fieldName, DynValue value);
void RemoveMember(IUserDataDescriptor IUUD, string memberName);
bool HasMember(object obj, string memberName);
/// <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>
DynValue CreateUserDataFromDescriptor(DynValue scriptObject, IUserDataDescriptor 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>
DynValue CreateUserDataFromType(DynValue scriptObject, Type desiredType);
void AddCallMetaTable(object userdata);
}
public class SafeLuaUserDataService : ISafeLuaUserDataService
{
private readonly ILuaUserDataService _userDataService;
public bool IsDisposed { get; private set; }
public SafeLuaUserDataService(ILuaUserDataService userDataService)
{
_userDataService = userDataService;
}
public IUserDataDescriptor this[string key]
{
get
{
return _userDataService.Descriptors.GetValueOrDefault(key);
}
}
private bool CanBeRegistered(string typeName)
{
if (typeName.StartsWith("Barotrauma.Lua", StringComparison.Ordinal) ||
typeName.StartsWith("Barotrauma.Cs", StringComparison.Ordinal) ||
typeName.StartsWith("Barotrauma.LuaCs", StringComparison.Ordinal))
{
return false;
}
if (typeName == "System.Single") { return true; }
if (typeName == "System.Console") { return true; }
if (typeName.StartsWith("System.Collections", StringComparison.Ordinal))
return true;
if (typeName.StartsWith("Microsoft.Xna", StringComparison.Ordinal))
return true;
if (typeName.StartsWith("Barotrauma.IO", StringComparison.Ordinal))
return false;
if (typeName.StartsWith("Barotrauma.ToolBox", StringComparison.Ordinal))
return false;
if (typeName.StartsWith("Barotrauma.SaveUtil", StringComparison.Ordinal))
return false;
if (typeName.StartsWith("Barotrauma.", StringComparison.Ordinal))
return true;
return false;
}
private bool CanBeReRegistered(string typeName)
{
if (typeName.StartsWith("Barotrauma.Lua", StringComparison.Ordinal) ||
typeName.StartsWith("Barotrauma.Cs", StringComparison.Ordinal) ||
typeName.StartsWith("Barotrauma.LuaCs", StringComparison.Ordinal))
{
return false;
}
return true;
}
public bool IsAllowed(string typeName)
{
if (!CanBeReRegistered(typeName) && IsRegistered(typeName))
{
return false;
}
if (!CanBeRegistered(typeName))
{
return false;
}
return true;
}
private void CheckAllowed(string typeName)
{
if (!IsAllowed(typeName))
{
throw new ScriptRuntimeException($"Type {typeName} can't be registered");
}
}
public IUserDataDescriptor RegisterType(string typeName)
{
CheckAllowed(typeName);
return _userDataService.RegisterType(typeName);
}
public void RegisterExtensionType(string typeName)
{
CheckAllowed(typeName);
_userDataService.RegisterExtensionType(typeName);
}
public bool IsRegistered(string typeName)
{
return _userDataService.IsRegistered(typeName);
}
public void UnregisterType(string typeName, bool deleteHistory = false)
{
IsAllowed(typeName);
_userDataService.UnregisterType(typeName, deleteHistory);
}
public object CreateStatic(string typeName)
{
return _userDataService.CreateStatic(typeName);
}
public bool IsTargetType(object obj, string typeName)
{
return _userDataService.IsTargetType(obj, typeName);
}
public string TypeOf(object obj)
{
return _userDataService.TypeOf(obj);
}
public object CreateEnumTable(string typeName)
{
return _userDataService.CreateEnumTable(typeName);
}
public void MakeFieldAccessible(IUserDataDescriptor IUUD, string fieldName)
{
_userDataService.MakeFieldAccessible(IUUD, fieldName);
}
public void MakeMethodAccessible(IUserDataDescriptor IUUD, string methodName, string[] parameters = null)
{
_userDataService.MakeMethodAccessible(IUUD, methodName, parameters);
}
public void MakePropertyAccessible(IUserDataDescriptor IUUD, string propertyName)
{
_userDataService.MakePropertyAccessible(IUUD, propertyName);
}
public void AddMethod(IUserDataDescriptor IUUD, string methodName, object function)
{
_userDataService.AddMethod(IUUD, methodName, function);
}
public void AddField(IUserDataDescriptor IUUD, string fieldName, DynValue value)
{
_userDataService.AddField(IUUD, fieldName, value);
}
public void RemoveMember(IUserDataDescriptor IUUD, string memberName)
{
_userDataService.RemoveMember(IUUD, memberName);
}
public bool HasMember(object obj, string memberName)
{
return _userDataService.HasMember(obj, memberName);
}
public DynValue CreateUserDataFromDescriptor(DynValue scriptObject, IUserDataDescriptor desiredTypeDescriptor)
{
return _userDataService.CreateUserDataFromDescriptor(scriptObject, desiredTypeDescriptor);
}
public DynValue CreateUserDataFromType(DynValue scriptObject, Type desiredType)
{
return _userDataService.CreateUserDataFromType(scriptObject, desiredType);
}
public void AddCallMetaTable(object userdata) { }
public void Dispose()
{
IsDisposed = true;
}
}