#nullable enable using Barotrauma.LuaCs; using Barotrauma.LuaCs.Compatibility; using Barotrauma.LuaCs.Data; using Barotrauma.LuaCs.Events; using Barotrauma.Networking; using FluentResults; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.Toolkit.Diagnostics; using MonoMod.RuntimeDetour; using MoonSharp.Interpreter; using MoonSharp.Interpreter.Interop; using MoonSharp.Interpreter.Loaders; using RestSharp.Validation; using System; using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Reflection; using System.Threading.Tasks; namespace Barotrauma.LuaCs; class LuaScriptManagementService : ILuaScriptManagementService, ILuaDataService, IEventAssemblyUnloading { public Script? InternalScript => _script; private Script? _script; private bool _isRunning; [MemberNotNullWhen(true, nameof(_script))] public bool IsRunning => _isRunning; private List _resourcesInfo = new List(); private readonly AsyncReaderWriterLock _operationsLock = new (); private readonly ILuaUserDataService _userDataService; private readonly ISafeLuaUserDataService _safeUserDataService; private readonly ILuaScriptLoader _luaScriptLoader; private readonly ILuaScriptServicesConfig _luaScriptServicesConfig; private readonly ILoggerService _loggerService; private readonly LuaGame _luaGame; private readonly IEventService _eventService; private readonly ILuaCsTimer _luaCsTimer; private readonly IDefaultLuaRegistrar _defaultLuaRegistrar; private readonly IPluginManagementService _pluginManagementService; private readonly INetworkingService _networkingService; private readonly IConsoleCommandsService _commandsService; private readonly ILuaConfigService _configService; private readonly ILuaCsInfoProvider _luaCsInfoProvider; private readonly Lazy _packageManagementService; //private readonly ILuaCsUtility _luaCsUtility; public LuaScriptManagementService( ILoggerService loggerService, ILuaScriptLoader loader, ILuaUserDataService userDataService, ISafeLuaUserDataService safeUserDataService, IDefaultLuaRegistrar defaultLuaRegistrar, ILuaScriptServicesConfig luaScriptServicesConfig, IPluginManagementService pluginManagementService, INetworkingService networkingService, LuaGame luaGame, IEventService eventService, //ILuaCsUtility luaCsUtility, ILuaCsTimer luaCsTimer, IConsoleCommandsService commandsService, ILuaCsInfoProvider luaCsInfoProvider, ILuaConfigService configService, Lazy packageManagementService) { _luaScriptLoader = loader; _userDataService = userDataService; _safeUserDataService = safeUserDataService; _defaultLuaRegistrar = defaultLuaRegistrar; _luaScriptServicesConfig = luaScriptServicesConfig; _loggerService = loggerService; _pluginManagementService = pluginManagementService; _networkingService = networkingService; _luaGame = luaGame; _eventService = eventService; _commandsService = commandsService; _luaCsInfoProvider = luaCsInfoProvider; _configService = configService; _packageManagementService = packageManagementService; _luaCsTimer = luaCsTimer; RegisterLuaEvents(); RegisterConsoleCommands(_commandsService); } private void RegisterConsoleCommands(IConsoleCommandsService commands) { #if CLIENT commands.RegisterCommand("cl_reloadlua|cl_reloadcs|cl_reloadluacs", "Re-initializes the LuaCs environment.", (string[] args) => { LuaCsSetup.Instance.EventService.PublishEvent(sub => sub.OnReloadAllPackages()); }); commands.RegisterCommand("cl_lua", $"cl_lua: Runs a string on the client.", (string[] args) => { if (GameMain.Client != null && !GameMain.Client.HasPermission(ClientPermissions.ConsoleCommands)) { DebugConsole.ThrowError("Command not permitted."); return; } if (LuaCsSetup.Instance.CurrentRunState != RunState.Running) { DebugConsole.ThrowError("LuaCs not initialized, use the console command cl_reloadluacs to force initialization."); return; } var result = LuaCsSetup.Instance.LuaScriptManagementService.DoString(string.Join(" ", args)); LuaCsSetup.Instance.Logger.LogResults(result.ToResult()); }); commands.RegisterCommand("cl_toggleluadebug", "Toggles the MoonSharp Debug Server.", (string[] args) => { DebugConsole.Log($"This command is currently not implemented. Please open a github issue if you need this feature."); /*int port = 41912; if (args.Length > 0) { int.TryParse(args[0], out port); } throw new NotImplementedException(); //GameMain.LuaCs.ToggleDebugger(port);*/ }); #elif SERVER commands.RegisterCommand("lua", "lua: Runs a string.", (string[] args) => { var result = LuaCsSetup.Instance.LuaScriptManagementService.DoString(string.Join(" ", args)); LuaCsSetup.Instance.Logger.LogResults(result.ToResult()); }); commands.RegisterCommand("reloadlua|reloadcs|reloadluacs", "Re-initializes the LuaCs environment.", (string[] args) => { LuaCsSetup.Instance.EventService.PublishEvent(sub => sub.OnReloadAllPackages()); }); commands.RegisterCommand("toggleluadebug", "Toggles the MoonSharp Debug Server.", (string[] args) => { int port = 41912; if (args.Length > 0) { int.TryParse(args[0], out port); } throw new NotImplementedException(); //GameMain.LuaCs.ToggleDebugger(port); }); #endif #if SERVER commands.RegisterCommand("install_cl_lua|install_cl|install_cl_cs|install_cl_luacs", "Installs Client-Side LuaCs into your client.", (string[] args) => { LuaCsInstaller.Install(); }); #endif } public bool IsDisposed { get; private set; } public void SetCachingPolicy(bool useCaching) { _luaScriptLoader?.SetCachingPolicy(useCaching); } public async Task LoadScriptResourcesAsync(ImmutableArray resourcesInfo) { if (!_luaCsInfoProvider.UseCaching) { return FluentResults.Result.Ok(); } // Do any exception checks you can before acquiring a lock to avoid needlessly holding up resources. if (resourcesInfo.IsDefaultOrEmpty) { ThrowHelper.ThrowArgumentNullException($"{nameof(LoadScriptResourcesAsync)}: The parameter is empty!"); } // Acquire a lock: // Reader = Allow parallel operations (try to avoid nesting acquiring the lock when possible) // Writer = Exclusive use (ie. executing scripts or Dispose()) using var lck = await _operationsLock.AcquireWriterLock(); // IDisposable using with generate a try-finally and release for you. IService.CheckDisposed(this); // Check disposed after you have the lock // If you use a ConcurrentDictionary instead of a List, it will handle threading issues for you. _resourcesInfo.AddRange(resourcesInfo.OrderBy(static r => r.LoadPriority)); // Use the StorageService's caching function by just loading the file with caching turned on. // Right now the LuaScriptLoader has this on by default. var cacheRes = await _luaScriptLoader.CacheResourcesAsync(resourcesInfo); // Aggregate and return results to the caller to deal with. Optionally, log here if you want. // Automatically converted to a Task when 'async' is in the method declaration. if (cacheRes.IsFailed) { return cacheRes.ToResult(); } return new FluentResults.Result().WithReasons(cacheRes.Value.SelectMany(cr => cr.Item2.Reasons)); } public FluentResults.Result DoString(string code) { IService.CheckDisposed(this); if (_script == null || !IsRunning) { throw new Exception("Disposed"); } try { var result = _script.DoString(code); return FluentResults.Result.Ok(result); } catch (Exception ex) { return FluentResults.Result.Fail(new ExceptionalError(ex)); } } private DynValue DoFile(string file, Table? globalContext = null, string? codeStringFriendly = null) { if (_script == null) { throw new Exception("Not running"); } if (!LuaCsFile.CanReadFromPath(file)) { // TODO: Replace with LuaScriptLoader IsFileAccessible. throw new ScriptRuntimeException($"dofile: File access to {file} not allowed."); } if (!LuaCsFile.Exists(file)) { // TODO: Replace with LuaScriptLoader IsFileAccessible. throw new ScriptRuntimeException($"dofile: File {file} not found."); } return _script.DoFile(file, globalContext, codeStringFriendly); } private DynValue LoadFile(string file, Table? globalContext = null, string? codeStringFriendly = null) { if (_script == null) { throw new Exception("Not running"); } if (!LuaCsFile.CanReadFromPath(file)) { throw new ScriptRuntimeException($"loadfile: File access to {file} not allowed."); } if (!LuaCsFile.Exists(file)) { throw new ScriptRuntimeException($"loadfile: File {file} not found."); } return _script.LoadFile(file, globalContext, codeStringFriendly); } private void RegisterLuaEvents() { _eventService.Subscribe(this); _eventService.RegisterLuaEventAlias("think", nameof(IEventUpdate.OnUpdate)); _eventService.RegisterLuaEventAlias("keyUpdate", nameof(IEventKeyUpdate.OnKeyUpdate)); _eventService.RegisterLuaEventAlias("afflictionUpdate", nameof(IEventAfflictionUpdate.OnAfflictionUpdate)); _eventService.RegisterLuaEventAlias("character.created", nameof(IEventCharacterCreated.OnCharacterCreated)); _eventService.RegisterLuaEventAlias("character.death", nameof(IEventCharacterDeath.OnCharacterDeath)); _eventService.RegisterLuaEventAlias("character.damageLimb", nameof(IEventCharacterDamageLimb.OnCharacterDamageLimb)); _eventService.RegisterLuaEventAlias("character.giveJobItems", nameof(IEventGiveCharacterJobItems.OnGiveCharacterJobItems)); _eventService.RegisterLuaEventAlias("character.CPRSuccess", nameof(IEventHumanCPRSuccess.OnCharacterCPRSuccess)); _eventService.RegisterLuaEventAlias("character.CPRFailed", nameof(IEventHumanCPRFailed.OnCharacterCPRFailed)); _eventService.RegisterLuaEventAlias("character.applyDamage", nameof(IEventCharacterApplyDamage.OnCharacterApplyDamage)); _eventService.RegisterLuaEventAlias("character.applyAffliction", nameof(IEventCharacterApplyAffliction.OnCharacterApplyAffliction)); _eventService.RegisterLuaEventAlias("gapOxygenUpdate", nameof(IEventGapOxygenUpdate.OnGapOxygenUpdate)); _eventService.RegisterLuaEventAlias("husk.clientControlHusk", nameof(IEventClientControlHusk.OnClientControlHusk)); _eventService.RegisterLuaEventAlias("meleeWeapon.handleImpact", nameof(IEventMeleeWeaponHandleImpact.OnMeleeWeaponHandleImpact)); _eventService.RegisterLuaEventAlias("serverLog", nameof(IEventServerLog.OnServerLog)); _eventService.RegisterLuaEventAlias("tryChangeClientName", nameof(IEventTryClientChangeName.OnTryClienChangeName)); _eventService.RegisterLuaEventAlias("changeFallDamage", nameof(IEventChangeFallDamage.OnChangeFallDamage)); _eventService.RegisterLuaEventAlias("chatMessage", nameof(IEventChatMessage.OnChatMessage)); _eventService.RegisterLuaEventAlias("canUseVoiceRadio", nameof(IEventCanUseVoiceRadio.OnCanUseVoiceRadio)); _eventService.RegisterLuaEventAlias("changeLocalVoiceRange", nameof(IEventChangeLocalVoiceRange.OnChangeLocalVoiceRange)); _eventService.RegisterLuaEventAlias("roundStart", nameof(IEventRoundStarted.OnRoundStart)); _eventService.RegisterLuaEventAlias("roundEnd", nameof(IEventRoundEnded.OnRoundEnd)); _eventService.RegisterLuaEventAlias("missionsEnded", nameof(IEventMissionsEnded.OnMissionsEnded)); _eventService.RegisterLuaEventAlias("signalReceived", nameof(IEventSignalReceived.OnSignalReceived)); _eventService.RegisterLuaEventAlias("item.created", nameof(IEventItemCreated.OnItemCreated)); _eventService.RegisterLuaEventAlias("item.removed", nameof(IEventItemRemoved.OnItemRemoved)); _eventService.RegisterLuaEventAlias("item.use", nameof(IEventItemUse.OnItemUsed)); _eventService.RegisterLuaEventAlias("item.secondaryUse", nameof(IEventItemSecondaryUse.OnItemSecondaryUsed)); _eventService.RegisterLuaEventAlias("item.readPropertyChange", nameof(IEventItemReadPropertyChange.OnItemReadPropertyChange)); _eventService.RegisterLuaEventAlias("item.deconstructed", nameof(IEventItemDeconstructed.OnItemDeconstructed)); _eventService.RegisterLuaEventAlias("inventoryPutItem", nameof(IEventInventoryPutItem.OnInventoryPutItem)); _eventService.RegisterLuaEventAlias("inventoryItemSwap", nameof(IEventInventoryItemSwap.OnInventoryItemSwap)); // Compatibility _eventService.RegisterLuaEventAlias("characterCreated", nameof(IEventCharacterCreated.OnCharacterCreated)); _eventService.RegisterLuaEventAlias("characterDeath", nameof(IEventCharacterDeath.OnCharacterDeath)); #if SERVER _eventService.RegisterLuaEventAlias("client.connected", nameof(IEventClientConnected.OnClientConnected)); _eventService.RegisterLuaEventAlias("client.disconnected", nameof(IEventClientDisconnected.OnClientDisconnected)); _eventService.RegisterLuaEventAlias("jobsAssigned", nameof(IEventJobsAssigned.OnJobsAssigned)); _eventService.RegisterLuaEventAlias("netMessageReceived", nameof(IEventClientRawNetMessageReceived.OnReceivedClientNetMessage)); // Compatibility _eventService.RegisterLuaEventAlias("clientConnected", nameof(IEventClientConnected.OnClientConnected)); _eventService.RegisterLuaEventAlias("clientDisconnected", nameof(IEventClientDisconnected.OnClientDisconnected)); _eventService.RegisterLuaEventAlias("modifyChatMessage", nameof(IEventModifyChatMessage.OnModifyMessagePredicate)); #elif CLIENT _eventService.RegisterLuaEventAlias("netMessageReceived", nameof(IEventServerRawNetMessageReceived.OnReceivedServerNetMessage)); #endif } private void SetupEnvironment(bool enableSandbox) { _script = new Script(CoreModules.Preset_SoftSandbox | CoreModules.Debug | CoreModules.IO | CoreModules.OS_System); _script.Options.DebugPrint = (string msg) => { _loggerService.LogMessage($"[Lua] {msg}"); }; SetCachingPolicy(_luaCsInfoProvider.UseCaching); _script.Options.ScriptLoader = _luaScriptLoader; _script.Options.CheckThreadAccess = false; Script.GlobalOptions.ShouldPCallCatchException = (Exception ex) => { return true; }; UserData.RegisterType(); UserData.RegisterType(typeof(LuaGame)); StandardUserDataDescriptor descriptor = (StandardUserDataDescriptor)UserData.RegisterType(typeof(EventService)); descriptor.AddDynValue("HookMethodType", UserData.CreateStatic()); UserData.RegisterType(typeof(ILuaCsNetworking)); UserData.RegisterType(typeof(ILuaCsUtility)); UserData.RegisterType(typeof(ILuaCsTimer)); UserData.RegisterType(typeof(LuaCsFile)); UserData.RegisterType(typeof(ILuaScriptResourceInfo)); UserData.RegisterType(typeof(IResourceInfo)); UserData.RegisterType(typeof(IUserDataDescriptor)); UserData.RegisterType(typeof(INetworkingService)); UserData.RegisterType(typeof(ILuaConfigService)); UserData.RegisterType(typeof(ILoggerService)); //UserData.RegisterType(typeof(ISettingBase)); Type[] settingBaseTypes = [ typeof(ISettingBase), typeof(ISettingBase), typeof(ISettingBase), typeof(ISettingBase), typeof(ISettingBase), typeof(ISettingBase), typeof(ISettingBase), typeof(ISettingBase), typeof(ISettingBase), typeof(ISettingBase), typeof(ISettingBase), typeof(ISettingBase), typeof(ISettingBase), typeof(ISettingRangeBase), typeof(ISettingRangeBase), typeof(ISettingList), typeof(ISettingList), typeof(ISettingList), typeof(ISettingList), typeof(ISettingList), typeof(ISettingList), typeof(ISettingList), typeof(ISettingList), typeof(ISettingList), typeof(ISettingList), typeof(ISettingList), typeof(ISettingList), ]; Dictionary> settingsTable = []; foreach (Type type in settingBaseTypes) { UserData.RegisterType(type); string baseName = type.Name.RemoveFromEnd("`1").Substring(1); if (!settingsTable.ContainsKey(baseName)) { settingsTable[baseName] = new Dictionary(); } settingsTable[baseName][type.GetGenericArguments()[0].Name] = UserData.CreateStatic(type); } foreach (var keyPair in settingsTable) { _script.Globals[keyPair.Key] = keyPair.Value; } UserData.RegisterType(typeof(ISettingRangeBase)); #if CLIENT UserData.RegisterType(typeof(ISettingControl)); #endif new LuaConverters(this).RegisterLuaConverters(); var luaRequire = new LuaRequire(_script); _script.Globals["setmodulepaths"] = (string[] str) => ((LuaScriptLoader)_luaScriptLoader).ModulePaths = str; _script.Globals["dofile"] = (Func)DoFile; _script.Globals["loadfile"] = (Func)LoadFile; _script.Globals["require"] = (Func)luaRequire.Require; _script.Globals["printerror"] = (DynValue o) => { _loggerService.LogError($"[Lua] {o.ToString()}"); }; _script.Globals["dostring"] = (Func)_script.DoString; _script.Globals["load"] = (Func)_script.LoadString; _script.Globals["Game"] = _luaGame; _script.Globals["Hook"] = _eventService; _script.Globals["Timer"] = _luaCsTimer; _script.Globals["File"] = UserData.CreateStatic(); _script.Globals["ConfigService"] = _configService; _script.Globals["Networking"] = _networkingService; _script.Globals["trygetpackage"] = (string name, out ContentPackage package) => _packageManagementService.Value.TryGetLoadedPackageByName(name, out package); _script.Globals["Logger"] = _loggerService; //_script.Globals["Steam"] = Steam; if (enableSandbox) { UserData.RegisterType(typeof(SafeLuaUserDataService)); _script.Globals["LuaUserData"] = _safeUserDataService; } else { UserData.RegisterType(typeof(LuaUserDataService)); _script.Globals["LuaUserData"] = _userDataService; } Table eventsTable = new Table(_script); var typesValue = _pluginManagementService.GetImplementingTypes(includeInterfaces: true, includeAbstractTypes: true); if (typesValue.IsSuccess) { foreach (var eventType in typesValue.Value) { if (eventType.IsGenericType) { continue; } if (!eventType.IsInterface) { continue; } UserData.RegisterType(eventType); eventsTable[eventType.Name] = UserData.CreateStatic(eventType); } } _script.Globals["Events"] = eventsTable; _script.Globals["ExecutionNumber"] = 0; _script.Globals["CSActive"] = !enableSandbox; ((Table)_script.Globals["debug"])["breakpoint"] = () => { Debugger.Break(); }; _script.Globals["SERVER"] = LuaCsSetup.IsServer; _script.Globals["CLIENT"] = LuaCsSetup.IsClient; _defaultLuaRegistrar.RegisterAll(); } public FluentResults.Result ExecuteLoadedScripts(ImmutableArray executionOrder, bool enableSandbox) { if (_isRunning) { return FluentResults.Result.Fail("Tried to execute Lua scripts without unloading first."); } _loggerService.LogMessage("[Lua] Executing scripts"); SetupEnvironment(enableSandbox); if (_script == null) { return FluentResults.Result.Ok(); } // never happens var result = FluentResults.Result.Ok(); _isRunning = true; var packages = executionOrder.Select(r => r.OwnerPackage) .Distinct() .Select(p => $"{p.Dir}/Lua/?.lua") .ToArray(); ((LuaScriptLoader)_luaScriptLoader).ModulePaths = packages; Table package = (Table)_script.Globals["package"]; package.Set("path", DynValue.FromObject(_script, packages)); foreach (ILuaScriptResourceInfo resource in executionOrder.Where(l => l.IsAutorun)) { foreach (ContentPath filePath in resource.FilePaths) { try { _loggerService.LogMessage($"[Lua] - Run {filePath.Value}"); _script.Call(_script.LoadFile(filePath.FullPath), resource.OwnerPackage.Dir); } catch(Exception e) { result = result.WithError(new ExceptionalError(e)); } } } _eventService.Call("loaded"); return result; } public DynValue? CallFunctionSafe(object luaFunction, params object[] args) { if (!IsRunning) { return null; } lock (_script) { try { return _script.Call(luaFunction, args); } catch (Exception e) { _loggerService.HandleException(e); } return null; } } public FluentResults.Result UnloadActiveScripts() { _isRunning = false; _script = null; return FluentResults.Result.Ok(); } public FluentResults.Result DisposePackageResources(ContentPackage package) { return FluentResults.Result.Ok(); } public FluentResults.Result DisposeAllPackageResources() { if (IsRunning) { UnloadActiveScripts(); } _resourcesInfo.Clear(); _luaScriptLoader.ClearCaches(); return FluentResults.Result.Ok(); } public FluentResults.Result Reset() { IService.CheckDisposed(this); _luaScriptLoader.ClearCaches(); _userDataService.Reset(); _luaCsTimer.Reset(); RegisterLuaEvents(); return DisposeAllPackageResources(); } public void Dispose() { IsDisposed = true; _userDataService.Dispose(); _luaScriptLoader.Dispose(); _commandsService.Dispose(); } public object? GetGlobalTableValue(string tableName) { if (!IsRunning) { return null; } return _script.Globals[tableName]; } public void OnAssemblyUnloading(Assembly assembly) { foreach (Type type in assembly.SafeGetTypes()) { UserData.UnregisterType(type, deleteHistory: true); } } }