diff --git a/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Configuration/DisplayableData.cs b/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Configuration/DisplayableData.cs index 64649068e..59df05404 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Configuration/DisplayableData.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Configuration/DisplayableData.cs @@ -2,15 +2,17 @@ using Microsoft.Xna.Framework; namespace Barotrauma.LuaCs.Configuration; -public class DisplayableData : IDisplayableData +public record DisplayableData : IDisplayableData { - public string Name { get; private set; } - public string ModName { get; private set; } - public string DisplayName { get; private set; } - public string DisplayModName { get; private set; } - public string DisplayCategory { get; private set; } - public string Tooltip { get; private set; } - public string ImageIcon { get; private set; } - public Point IconResolution { get; private set; } - public bool ShowWhenNotLoaded { get; private set; } + public string InternalName { get; init; } + public ContentPackage OwnerPackage { get; init; } + public string FallbackPackageName { get; init; } + public string DisplayName { get; init; } + public string DisplayModName { get; init; } + public string DisplayCategory { get; init; } + public string Tooltip { get; init; } + public string ImageIcon { get; init; } + public Point IconResolution { get; init; } + public bool ShowWhenNotLoaded { get; init; } + public string Description { get; init; } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Configuration/IConfigControl.cs b/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Configuration/IConfigControl.cs new file mode 100644 index 000000000..4e3afbd93 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Configuration/IConfigControl.cs @@ -0,0 +1,12 @@ +using System; + +namespace Barotrauma.LuaCs.Configuration; + +public interface IConfigControl : IConfigBase +{ + event Action OnDown; + KeyOrMouse Value { get; } + bool IsAssignable(KeyOrMouse value); + bool TrySetValue(KeyOrMouse value); + bool IsDown(); +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Configuration/IDisplayableConfig.cs b/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Configuration/IDisplayables.cs similarity index 83% rename from Barotrauma/BarotraumaClient/ClientSource/LuaCs/Configuration/IDisplayableConfig.cs rename to Barotrauma/BarotraumaClient/ClientSource/LuaCs/Configuration/IDisplayables.cs index 69449ed6b..0422fb91c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Configuration/IDisplayableConfig.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Configuration/IDisplayables.cs @@ -1,4 +1,5 @@ using System.Numerics; +using Barotrauma.LuaCs.Data; using Microsoft.Xna.Framework; namespace Barotrauma.LuaCs.Configuration; @@ -6,16 +7,8 @@ namespace Barotrauma.LuaCs.Configuration; /// /// Contains the Display Data for use with Menus. /// -public interface IDisplayableData +public interface IDisplayableData : IDataInfo { - /// - /// Internal name of the instance. - /// - string Name { get; } - /// - /// Internal mod name of the instance. ContentPackage name will be used by default. - /// - string ModName { get; } /// /// The name to display in GUIs and Menus. /// @@ -44,6 +37,10 @@ public interface IDisplayableData /// Whether to show the entry in the menu when not loaded. /// bool ShowWhenNotLoaded { get; } + /// + /// What does this setting do? + /// + string Description { get; } } public interface IDisplayableInitialize @@ -53,8 +50,8 @@ public interface IDisplayableInitialize // copy this as needed /*public void Initialize(IDisplayableData values) { - this.Name = values.Name; - this.ModName = values.ModName; + this.InternalName = values.InternalName; + this.OwnerPackage = values.OwnerPackage; this.DisplayName = values.DisplayName; this.DisplayModName = values.DisplayModName; this.DisplayCategory = values.DisplayCategory; diff --git a/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Data/DataInterfaceDefinitions.cs b/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Data/DataInterfaceDefinitions.cs index d46048703..d7be1b0cf 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Data/DataInterfaceDefinitions.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Data/DataInterfaceDefinitions.cs @@ -17,5 +17,6 @@ public record StylesResourceInfo : IStylesResourceInfo public bool Optional { get; init; } public ImmutableArray SupportedCultures { get; init; } public string InternalName { get; init; } + public ContentPackage OwnerPackage { get; init; } public ImmutableArray Dependencies { get; init; } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Data/IResourceInfoDeclarations.cs b/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Data/IResourceInfoDeclarations.cs index 14974ac8c..bd9e60424 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Data/IResourceInfoDeclarations.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Data/IResourceInfoDeclarations.cs @@ -4,7 +4,7 @@ namespace Barotrauma.LuaCs.Data; public partial interface IModConfigInfo : IStylesResourcesInfo { } -public interface IStylesResourceInfo : IResourceInfo, IResourceCultureInfo, ILoadableResourceInfo, IPackageDependenciesInfo { } +public interface IStylesResourceInfo : IResourceInfo, IResourceCultureInfo, IDataInfo, IPackageDependenciesInfo { } public interface IStylesResourcesInfo { diff --git a/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Services/Compatibility/ILuaCsNetworking.cs b/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Services/Compatibility/ILuaCsNetworking.cs new file mode 100644 index 000000000..570debbaf --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Services/Compatibility/ILuaCsNetworking.cs @@ -0,0 +1,8 @@ +using Barotrauma.Networking; + +namespace Barotrauma.LuaCs.Services.Compatibility; + +internal partial interface ILuaCsNetworking : ILuaCsShim +{ + +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Services/IClientLoggerService.cs b/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Services/IClientLoggerService.cs index 21cc88352..54b5445cf 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Services/IClientLoggerService.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Services/IClientLoggerService.cs @@ -1,6 +1,6 @@ namespace Barotrauma.LuaCs.Services; -public interface IClientLoggerService : IService +public interface IClientLoggerService : IReusableService { void AddToGUIUpdateList(); void ShowErrorOverlay(string message, float time = 5f, float duration = 1.5f); diff --git a/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Services/IConfigService.cs b/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Services/IConfigService.cs new file mode 100644 index 000000000..6d817456c --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Services/IConfigService.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; +using Barotrauma.LuaCs.Configuration; +using Barotrauma.LuaCs.Networking; +using Barotrauma.Networking; + +namespace Barotrauma.LuaCs.Services; + +public partial interface IConfigService +{ + /* + * Immediate mode + */ + FluentResults.Result> AddConfigEntry(IDisplayableData data, + T defaultValue, + NetSync syncMode = NetSync.None, + ClientPermissions permissions = ClientPermissions.None, + Func valueChangePredicate = null, + Action> onValueChanged = null) where T : IConvertible, IEquatable; + + FluentResults.Result AddConfigList(IDisplayableData data, + int defaultIndex, IReadOnlyList values, + NetSync syncMode = NetSync.None, + ClientPermissions permissions = ClientPermissions.None, + Func valueChangePredicate = null, + Action onValueChanged = null); + + FluentResults.Result> AddConfigRangeEntry(IDisplayableData data, + T defaultValue, T minValue, T maxValue, + Func, int> getStepCount, + NetSync syncMode = NetSync.None, + ClientPermissions permissions = ClientPermissions.None, + Func valueChangePredicate = null, + Action> onValueChanged = null) where T : IConvertible, IEquatable; +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Services/IStylesService.cs b/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Services/IStylesService.cs index cd0b38829..05cde12b4 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Services/IStylesService.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Services/IStylesService.cs @@ -4,7 +4,7 @@ /// /// Loads XML Style assets from the given content package. /// -public interface IStylesService : IService +public interface IStylesService : IReusableService { /// /// Tries to load the styles file for the given contentpackage and path into a new UIStylesProcessor instance. @@ -12,11 +12,11 @@ public interface IStylesService : IService /// /// /// - bool TryLoadStylesFile(ContentPackage package, ContentPath path); + FluentResults.Result LoadStylesFile(ContentPackage package, ContentPath path); /// /// Unloads all styles assets and UIStyleProcessor instances. /// - void UnloadAllStyles(); + FluentResults.Result UnloadAllStyles(); /// /// Tries to the get the font asset by xml asset name, returns null on failure. diff --git a/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Services/NetworkingService.cs b/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Services/NetworkingService.cs new file mode 100644 index 000000000..8d9c0f0c0 --- /dev/null +++ b/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Services/NetworkingService.cs @@ -0,0 +1,135 @@ +using Barotrauma.LuaCs.Services; +using Barotrauma.Networking; +using System; +using System.Collections.Generic; + +namespace Barotrauma.LuaCs.Networking; + +partial class NetworkingService +{ + private Dictionary> receiveQueue = new Dictionary>(); + + public void SendSyncMessage() + { + if (GameMain.Client == null) { return; } + + WriteOnlyMessage message = new WriteOnlyMessage(); + message.WriteByte((byte)ClientPacketHeader.LUA_NET_MESSAGE); + message.WriteByte((byte)LuaCsClientToServer.RequestAllIds); + GameMain.Client.ClientPeer.Send(message, DeliveryMethod.Reliable); + } + + public void NetMessageReceived(IReadMessage netMessage, ServerPacketHeader header, Client client = null) + { + if (header != ServerPacketHeader.LUA_NET_MESSAGE) + { + return; + } + + LuaCsServerToClient luaCsHeader = (LuaCsServerToClient)netMessage.ReadByte(); + + switch (luaCsHeader) + { + case LuaCsServerToClient.NetMessageString: + HandleNetMessageString(netMessage); + break; + + case LuaCsServerToClient.NetMessageId: + HandleNetMessageId(netMessage); + break; + + case LuaCsServerToClient.ReceiveIds: + ReadIds(netMessage); + break; + } + } + + public INetWriteMessage Start(Guid netId) + { + var message = new WriteOnlyMessage(); + + message.WriteByte((byte)ClientPacketHeader.LUA_NET_MESSAGE); + + if (idToPacket.ContainsKey(netId)) + { + message.WriteByte((byte)LuaCsClientToServer.NetMessageId); + message.WriteUInt16(idToPacket[netId]); + } + else + { + message.WriteByte((byte)LuaCsClientToServer.NetMessageString); + message.WriteBytes(netId.ToByteArray(), 0, 16); + } + + return message.ToNetWriteMessage(); + } + + public void RequestId(Guid netId) + { + if (idToPacket.ContainsKey(netId)) { return; } + + if (GameMain.Client == null) { return; } + + WriteOnlyMessage message = new WriteOnlyMessage(); + message.WriteByte((byte)ClientPacketHeader.LUA_NET_MESSAGE); + message.WriteByte((byte)LuaCsClientToServer.RequestSingleId); + + message.WriteBytes(netId.ToByteArray(), 0, 16); + + Send(message, DeliveryMethod.Reliable); + } + + public void Send(IWriteMessage netMessage, DeliveryMethod deliveryMethod = DeliveryMethod.Reliable) + { + GameMain.Client.ClientPeer.Send(netMessage, deliveryMethod); + } + + private void HandleNetMessageId(IReadMessage netMessage, Client client = null) + { + ushort id = netMessage.ReadUInt16(); + + if (packetToId.ContainsKey(id)) + { + HandleNetMessage(netMessage, packetToId[id], client); + } + else + { + if (!receiveQueue.ContainsKey(id)) { receiveQueue[id] = new Queue(); } + receiveQueue[id].Enqueue(netMessage); + + if (GameSettings.CurrentConfig.VerboseLogging) + { + LuaCsLogger.LogMessage($"Received NetMessage with unknown id {id} from server, storing in queue in case we receive the id later."); + } + } + } + + private void ReadIds(IReadMessage netMessage) + { + ushort size = netMessage.ReadUInt16(); + + for (int i = 0; i < size; i++) + { + ushort packetId = netMessage.ReadUInt16(); + Guid netId = new Guid(netMessage.ReadBytes(16)); + + packetToId[packetId] = netId; + idToPacket[netId] = packetId; + + if (!receiveQueue.ContainsKey(packetId)) + { + continue; + } + + // We could have received messages before receiving the sync message, so we need to process them now + + while (receiveQueue[packetId].TryDequeue(out var queueMessage)) + { + if (netReceives.ContainsKey(netId)) + { + netReceives[netId](queueMessage); + } + } + } + } +} diff --git a/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Services/PackageService.cs b/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Services/PackageService.cs index a079ef1ec..d1ceb600c 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Services/PackageService.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Services/PackageService.cs @@ -9,10 +9,10 @@ namespace Barotrauma.LuaCs.Services; public partial class PackageService : IStylesResourcesInfo { private readonly Lazy _stylesService; + public IStylesService Styles => _stylesService.Value; public PackageService( - Lazy converterService, - Lazy legacyConfigService, + Lazy configParserService, Lazy luaScriptService, Lazy localizationService, Lazy pluginService, @@ -22,8 +22,7 @@ public partial class PackageService : IStylesResourcesInfo IStorageService storageService, ILoggerService loggerService) { - _modConfigConverterService = converterService; - _legacyConfigService = legacyConfigService; + _configParserService = configParserService; _luaScriptService = luaScriptService; _localizationService = localizationService; _pluginService = pluginService; @@ -36,7 +35,7 @@ public partial class PackageService : IStylesResourcesInfo public ImmutableArray StylesResourceInfos => ModConfigInfo?.StylesResourceInfos ?? ImmutableArray.Empty; - public void LoadStyles([NotNull]IStylesResourcesInfo stylesInfo) + public FluentResults.Result LoadStyles([NotNull]IStylesResourcesInfo stylesInfo) { throw new NotImplementedException(); } diff --git a/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Services/StylesService.cs b/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Services/StylesService.cs index f859dcd09..16c384157 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Services/StylesService.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/LuaCs/Services/StylesService.cs @@ -3,6 +3,8 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Xml.Linq; +using FluentResults; +using FluentResults.LuaCs; namespace Barotrauma.LuaCs.Services; @@ -18,44 +20,53 @@ public class StylesService : IStylesService _loggerService = loggerService; } - public bool TryLoadStylesFile(ContentPackage package, ContentPath path) + public FluentResults.Result LoadStylesFile(ContentPackage package, ContentPath path) { //check if file already in dict if (_loadedProcessors.ContainsKey(path.FullPath)) { - return true; + return FluentResults.Result.Ok(); } //check if file exists - if (_storageService.FileExists(path.FullPath)) + if (_storageService.FileExists(path.FullPath) is {} result + && result.IsFailed | (result.IsSuccess & result.Value == false)) { - try - { - var styleProcessor = new UIStyleProcessor(package, path); - styleProcessor.LoadFile(); - _loadedProcessors.Add(path.FullPath, styleProcessor); - } - catch (InvalidDataException exception) - { - _loggerService.LogError($"XmlAssetService.TryLoadStylesFile failed for ContentPackage {package.Name}: Exception: {exception.Message}"); - return false; - } - - return true; + return FluentResults.Result.Fail(result.Errors) + .WithError(new Error($"{nameof(StylesService)}.{nameof(LoadStylesFile)} file does not exist!") + .WithMetadata(MetadataType.ExceptionObject, this) + .WithMetadata(MetadataType.RootObject, package)); } - return false; + try + { + var styleProcessor = new UIStyleProcessor(package, path); + styleProcessor.LoadFile(); + _loadedProcessors.Add(path.FullPath, styleProcessor); + } + catch (InvalidDataException exception) + { + return FluentResults.Result.Fail(new Error($"{nameof(StylesService)}.{nameof(LoadStylesFile)} failed for ContentPackage {package.Name}: Exception: {exception.Message}") + .WithMetadata(MetadataType.ExceptionDetails, exception.Message) + .WithMetadata(MetadataType.ExceptionObject, this) + .WithMetadata(MetadataType.RootObject, package) + .WithMetadata(MetadataType.StackTrace, exception.StackTrace)); + } + + return FluentResults.Result.Ok(); } - public void UnloadAllStyles() + public FluentResults.Result UnloadAllStyles() { if (NoProcessorsLoaded) - return; + return FluentResults.Result.Fail(new Error($"{nameof(StylesService)}.{nameof(UnloadAllStyles)}: No processors have been loaded.") + .WithMetadata(MetadataType.ExceptionObject, this)); foreach (var processor in _loadedProcessors) { processor.Value.UnloadFile(); } _loadedProcessors.Clear(); + return FluentResults.Result.Ok(); } public GUIFont GetFont(string fontName) @@ -131,8 +142,8 @@ public class StylesService : IStylesService GC.SuppressFinalize(this); } - public void Reset() + public FluentResults.Result Reset() { - UnloadAllStyles(); + return UnloadAllStyles(); } } diff --git a/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs index c416d7a10..33b4a8aec 100644 --- a/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs +++ b/Barotrauma/BarotraumaClient/ClientSource/Networking/GameClient.cs @@ -603,7 +603,7 @@ namespace Barotrauma.Networking { ServerPacketHeader header = (ServerPacketHeader)inc.ReadByte(); - GameMain.LuaCs.Networking.NetMessageReceived(inc, header); + GameMain.LuaCs.NetworkingService.NetMessageReceived(inc, header); if (roundInitStatus == RoundInitStatus.WaitingForStartGameFinalize && header is not ( diff --git a/Barotrauma/BarotraumaClient/LinuxClient.csproj b/Barotrauma/BarotraumaClient/LinuxClient.csproj index a74a96392..371d9d527 100644 --- a/Barotrauma/BarotraumaClient/LinuxClient.csproj +++ b/Barotrauma/BarotraumaClient/LinuxClient.csproj @@ -14,6 +14,7 @@ Debug;Release;Unstable true ;NU1605;CS0114;CS0108;CS8597;CS8600;CS8601;CS8602;CS8603;CS8604;CS8605;CS8606;CS8607;CS8608;CS8609;CS8610;CS8611;CS8612;CS8613;CS8614;CS8615;CS8616;CS8617;CS8618;CS8619;CS8620;CS8621;CS8622;CS8624;CS8625;CS8626;CS8629;CS8631;CS8632;CS8633;CS8634;CS8638;CS8643;CS8644;CS8645;CS8653;CS8654;CS8655;CS8667;CS8669;CS8670;CS8714;CS8717;CS8765 + latest diff --git a/Barotrauma/BarotraumaClient/MacClient.csproj b/Barotrauma/BarotraumaClient/MacClient.csproj index 67009978a..39d3507cb 100644 --- a/Barotrauma/BarotraumaClient/MacClient.csproj +++ b/Barotrauma/BarotraumaClient/MacClient.csproj @@ -15,6 +15,7 @@ true ;NU1605;CS0114;CS0108;CS8597;CS8600;CS8601;CS8602;CS8603;CS8604;CS8605;CS8606;CS8607;CS8608;CS8609;CS8610;CS8611;CS8612;CS8613;CS8614;CS8615;CS8616;CS8617;CS8618;CS8619;CS8620;CS8621;CS8622;CS8624;CS8625;CS8626;CS8629;CS8631;CS8632;CS8633;CS8634;CS8638;CS8643;CS8644;CS8645;CS8653;CS8654;CS8655;CS8667;CS8669;CS8670;CS8714;CS8717;CS8765 false + latest diff --git a/Barotrauma/BarotraumaClient/WindowsClient.csproj b/Barotrauma/BarotraumaClient/WindowsClient.csproj index 43f2ab06c..37458baff 100644 --- a/Barotrauma/BarotraumaClient/WindowsClient.csproj +++ b/Barotrauma/BarotraumaClient/WindowsClient.csproj @@ -15,6 +15,7 @@ true app.manifest ;NU1605;CS0114;CS0108;CS8597;CS8600;CS8601;CS8602;CS8603;CS8604;CS8605;CS8606;CS8607;CS8608;CS8609;CS8610;CS8611;CS8612;CS8613;CS8614;CS8615;CS8616;CS8617;CS8618;CS8619;CS8620;CS8621;CS8622;CS8624;CS8625;CS8626;CS8629;CS8631;CS8632;CS8633;CS8634;CS8638;CS8643;CS8644;CS8645;CS8653;CS8654;CS8655;CS8667;CS8669;CS8670;CS8714;CS8717;CS8765 + latest diff --git a/Barotrauma/BarotraumaServer/LinuxServer.csproj b/Barotrauma/BarotraumaServer/LinuxServer.csproj index 18a89a450..e2dfeb5b2 100644 --- a/Barotrauma/BarotraumaServer/LinuxServer.csproj +++ b/Barotrauma/BarotraumaServer/LinuxServer.csproj @@ -14,6 +14,7 @@ Debug;Release;Unstable true ;NU1605;CS0114;CS0108;CS8597;CS8600;CS8601;CS8602;CS8603;CS8604;CS8605;CS8606;CS8607;CS8608;CS8609;CS8610;CS8611;CS8612;CS8613;CS8614;CS8615;CS8616;CS8617;CS8618;CS8619;CS8620;CS8621;CS8622;CS8624;CS8625;CS8626;CS8629;CS8631;CS8632;CS8633;CS8634;CS8638;CS8643;CS8644;CS8645;CS8653;CS8654;CS8655;CS8667;CS8669;CS8670;CS8714;CS8717;CS8765 + latest diff --git a/Barotrauma/BarotraumaServer/MacServer.csproj b/Barotrauma/BarotraumaServer/MacServer.csproj index fe5683a68..55ea36236 100644 --- a/Barotrauma/BarotraumaServer/MacServer.csproj +++ b/Barotrauma/BarotraumaServer/MacServer.csproj @@ -14,6 +14,7 @@ Debug;Release;Unstable true ;NU1605;CS0114;CS0108;CS8597;CS8600;CS8601;CS8602;CS8603;CS8604;CS8605;CS8606;CS8607;CS8608;CS8609;CS8610;CS8611;CS8612;CS8613;CS8614;CS8615;CS8616;CS8617;CS8618;CS8619;CS8620;CS8621;CS8622;CS8624;CS8625;CS8626;CS8629;CS8631;CS8632;CS8633;CS8634;CS8638;CS8643;CS8644;CS8645;CS8653;CS8654;CS8655;CS8667;CS8669;CS8670;CS8714;CS8717;CS8765 + latest diff --git a/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs b/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs index 0f09fa7e5..b95af9c57 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/GameMain.cs @@ -368,7 +368,7 @@ namespace Barotrauma performanceCounterTimer.Stop(); if (GameMain.LuaCs.PerformanceCounter.EnablePerformanceCounter) { - GameMain.LuaCs.PerformanceCounter.UpdateElapsedTime = (double)performanceCounterTimer.ElapsedTicks / Stopwatch.Frequency; + GameMain.LuaCs.PerformanceCounter.AddElapsedTicks(new SimplePerformanceData("Update", performanceCounterTimer.ElapsedTicks)); } performanceCounterTimer.Reset(); @@ -452,7 +452,7 @@ namespace Barotrauma public void Exit() { ShouldRun = false; - GameMain.LuaCs.Stop(); + GameMain.LuaCs.Dispose(); } } } diff --git a/Barotrauma/BarotraumaServer/ServerSource/LuaCs/Services/NetworkingService.cs b/Barotrauma/BarotraumaServer/ServerSource/LuaCs/Services/NetworkingService.cs new file mode 100644 index 000000000..5cdcff59a --- /dev/null +++ b/Barotrauma/BarotraumaServer/ServerSource/LuaCs/Services/NetworkingService.cs @@ -0,0 +1,176 @@ +using Barotrauma.LuaCs.Services; +using Barotrauma.Networking; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Barotrauma.LuaCs.Networking; + +partial class NetworkingService +{ + private const int MaxRegisterPerClient = 1000; + + private Dictionary clientRegisterCount = new Dictionary(); + + private ushort currentId = 0; + + public INetWriteMessage Start(Guid netId) + { + var message = new WriteOnlyMessage(); + + message.WriteByte((byte)ServerPacketHeader.LUA_NET_MESSAGE); + + if (idToPacket.ContainsKey(netId)) + { + message.WriteByte((byte)LuaCsServerToClient.NetMessageId); + message.WriteUInt16(idToPacket[netId]); + } + else + { + message.WriteByte((byte)LuaCsServerToClient.NetMessageString); + message.WriteBytes(netId.ToByteArray(), 0, 16); + } + + return message.ToNetWriteMessage(); + } + + public void NetMessageReceived(IReadMessage netMessage, ClientPacketHeader header, Client client = null) + { + if (header != ClientPacketHeader.LUA_NET_MESSAGE) + { + return; + } + + LuaCsClientToServer luaCsHeader = (LuaCsClientToServer)netMessage.ReadByte(); + + switch (luaCsHeader) + { + case LuaCsClientToServer.NetMessageString: + HandleNetMessageString(netMessage, client); + break; + + case LuaCsClientToServer.NetMessageId: + HandleNetMessageId(netMessage, client); + break; + + case LuaCsClientToServer.RequestAllIds: + WriteAllIds(client); + break; + + case LuaCsClientToServer.RequestSingleId: + RequestIdSingle(netMessage, client); + break; + } + } + + private void HandleNetMessageId(IReadMessage netMessage, Client client = null) + { + ushort id = netMessage.ReadUInt16(); + + if (packetToId.ContainsKey(id)) + { + Guid netId = packetToId[id]; + + HandleNetMessage(netMessage, netId, client); + } + else + { + if (GameSettings.CurrentConfig.VerboseLogging) + { + LuaCsLogger.LogError($"Received NetMessage for unknown id {id} from {GameServer.ClientLogName(client)}."); + } + } + } + + private ushort RegisterId(Guid netId) + { + if (idToPacket.ContainsKey(netId)) + { + return idToPacket[netId]; + } + + if (currentId >= ushort.MaxValue) + { + LuaCsLogger.LogError($"Tried to register more than {ushort.MaxValue} network ids!"); + return 0; + } + + currentId++; + + packetToId[currentId] = netId; + idToPacket[netId] = currentId; + + WriteIdToAll(currentId, netId); + + return currentId; + } + + private void RequestIdSingle(IReadMessage netMessage, Client client) + { + Guid netId = new Guid(netMessage.ReadBytes(16)); + + if (!idToPacket.ContainsKey(netId) && client.AccountId.TryUnwrap(out AccountId id)) + { + if (!clientRegisterCount.ContainsKey(id.StringRepresentation)) + { + clientRegisterCount[id.StringRepresentation] = 0; + } + + clientRegisterCount[id.StringRepresentation]++; + + if (clientRegisterCount[id.StringRepresentation] > MaxRegisterPerClient) + { + LuaCsLogger.Log($"{GameServer.ClientLogName(client)} Tried to register more than {MaxRegisterPerClient} Ids!"); + return; + } + } + + RegisterId(netId); + } + + private void WriteIdToAll(ushort packet, Guid netId) + { + WriteOnlyMessage message = new WriteOnlyMessage(); + message.WriteByte((byte)ServerPacketHeader.LUA_NET_MESSAGE); + message.WriteByte((byte)LuaCsServerToClient.ReceiveIds); + + message.WriteUInt16(1); + message.WriteUInt16(packet); + message.WriteBytes(netId.ToByteArray(), 0, 16); + + Send(message, null, DeliveryMethod.Reliable); + } + + private void WriteAllIds(Client client) + { + WriteOnlyMessage message = new WriteOnlyMessage(); + message.WriteByte((byte)ServerPacketHeader.LUA_NET_MESSAGE); + message.WriteByte((byte)LuaCsServerToClient.ReceiveIds); + + message.WriteUInt16((ushort)packetToId.Count()); + foreach ((ushort packet, Guid netId) in packetToId) + { + message.WriteUInt16(packet); + message.WriteBytes(netId.ToByteArray(), 0, 16); + } + + Send(message, client.Connection, DeliveryMethod.Reliable); + } + + public void ClientWriteLobby(Client client) => GameMain.Server.ClientWriteLobby(client); + + public void Send(IWriteMessage netMessage, NetworkConnection connection = null, DeliveryMethod deliveryMethod = DeliveryMethod.Reliable) + { + if (connection == null) + { + foreach (NetworkConnection conn in Client.ClientList.Select(c => c.Connection)) + { + GameMain.Server.ServerPeer.Send(netMessage, conn, deliveryMethod); + } + } + else + { + GameMain.Server.ServerPeer.Send(netMessage, connection, deliveryMethod); + } + } +} diff --git a/Barotrauma/BarotraumaServer/ServerSource/LuaCs/Services/PackageService.cs b/Barotrauma/BarotraumaServer/ServerSource/LuaCs/Services/PackageService.cs index 4b97459b1..e4d92ec2f 100644 --- a/Barotrauma/BarotraumaServer/ServerSource/LuaCs/Services/PackageService.cs +++ b/Barotrauma/BarotraumaServer/ServerSource/LuaCs/Services/PackageService.cs @@ -8,8 +8,7 @@ namespace Barotrauma.LuaCs.Services; public partial class PackageService { public PackageService( - Lazy converterService, - Lazy legacyConfigService, + Lazy configParserService, Lazy luaScriptService, Lazy localizationService, Lazy pluginService, @@ -18,8 +17,7 @@ public partial class PackageService IStorageService storageService, ILoggerService loggerService) { - _modConfigConverterService = converterService; - _legacyConfigService = legacyConfigService; + _configParserService = configParserService; _luaScriptService = luaScriptService; _localizationService = localizationService; _pluginService = pluginService; diff --git a/Barotrauma/BarotraumaServer/WindowsServer.csproj b/Barotrauma/BarotraumaServer/WindowsServer.csproj index 857c49007..fa071c43c 100644 --- a/Barotrauma/BarotraumaServer/WindowsServer.csproj +++ b/Barotrauma/BarotraumaServer/WindowsServer.csproj @@ -14,6 +14,7 @@ Debug;Release;Unstable true ;NU1605;CS0114;CS0108;CS8597;CS8600;CS8601;CS8602;CS8603;CS8604;CS8605;CS8606;CS8607;CS8608;CS8609;CS8610;CS8611;CS8612;CS8613;CS8614;CS8615;CS8616;CS8617;CS8618;CS8619;CS8620;CS8621;CS8622;CS8624;CS8625;CS8626;CS8629;CS8631;CS8632;CS8633;CS8634;CS8638;CS8643;CS8644;CS8645;CS8653;CS8654;CS8655;CS8667;CS8669;CS8670;CS8714;CS8717;CS8765 + latest diff --git a/Barotrauma/BarotraumaShared/Luatrauma.props b/Barotrauma/BarotraumaShared/Luatrauma.props index 62c907748..1adac0339 100644 --- a/Barotrauma/BarotraumaShared/Luatrauma.props +++ b/Barotrauma/BarotraumaShared/Luatrauma.props @@ -1,12 +1,15 @@ - - - + + + + + + diff --git a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs index 83b869ba4..e61097a03 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/Items/Item.cs @@ -1411,7 +1411,7 @@ namespace Barotrauma if (Components.Any(ic => ic is Wire) && Components.All(ic => ic is Wire || ic is Holdable)) { isWire = true; } if (HasTag(Barotrauma.Tags.LogicItem)) { isLogic = true; } - GameMain.LuaCs.Hook.Call("item.created", this); + GameMain.LuaCs.Hook.Call("item.created", this); ApplyStatusEffects(ActionType.OnSpawn, 1.0f); @@ -4622,7 +4622,7 @@ namespace Barotrauma body = null; } - GameMain.LuaCs.Hook.Call("item.removed", this); + GameMain.LuaCs.Hook.Call("item.removed", this); } public override void Remove() @@ -4708,7 +4708,7 @@ namespace Barotrauma RemoveProjSpecific(); - GameMain.LuaCs.Hook.Call("item.removed", this); + GameMain.LuaCs.Hook.Call("item.removed", this); } private void RemoveFromLists() diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Configuration/IConfigBase.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Configuration/IConfigBase.cs index 37e076506..87334e592 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Configuration/IConfigBase.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Configuration/IConfigBase.cs @@ -1,4 +1,5 @@ using System; +using Barotrauma.LuaCs.Data; using Barotrauma.Networking; namespace Barotrauma.LuaCs.Configuration; @@ -13,9 +14,7 @@ public partial interface IConfigBase : IVarId void Initialize(IVarId id, string defaultValue); } -public interface IVarId +public interface IVarId : IDataInfo { Guid InstanceId { get; } - string InternalName { get; } - ContentPackage OwnerPackage { get; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Configuration/IConfigEntry.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Configuration/IConfigEntry.cs index 30ba129c6..8f5325eca 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Configuration/IConfigEntry.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Configuration/IConfigEntry.cs @@ -9,4 +9,6 @@ public interface IConfigEntry : IConfigBase, INetVar where T : IConvertible, bool TrySetValue(T value); bool IsAssignable(T value); void Initialize(IVarId id, T defaultValue); + + } diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/DataInterfaceDefinitions.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/DataInterfaceDefinitions.cs index 7362f990f..f620bbbce 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/DataInterfaceDefinitions.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/DataInterfaceDefinitions.cs @@ -2,7 +2,9 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Globalization; +using System.Linq; using System.Runtime.InteropServices; +using System.Text; namespace Barotrauma.LuaCs.Data; @@ -29,6 +31,7 @@ public partial record ModConfigInfo : IModConfigInfo public record AssemblyResourceInfo : IAssemblyResourceInfo { public ContentPackage OwnerPackage { get; init; } + public string FallbackPackageName { get; init; } public string FriendlyName { get; init; } public bool IsScript { get; init; } public string InternalName { get; init; } @@ -44,16 +47,109 @@ public record AssemblyResourceInfo : IAssemblyResourceInfo public record DependencyInfo : IPackageDependencyInfo { + public string InternalName { get; init; } public ContentPackage OwnerPackage { get; init; } public string FolderPath { get; init; } - public string PackageName { get; init; } + public string FallbackPackageName { get; init; } public ulong SteamWorkshopId { get; init; } public ContentPackage DependencyPackage { get; init; } + public bool IsMissing { get; init; } + public bool IsWorkshopInstallation { get; init; } + + public virtual bool Equals(DependencyInfo other) => Equals(this, other); + + public override int GetHashCode() + { + if (DependencyPackage is not null) + return DependencyPackage.GetHashCode(); + if (SteamWorkshopId != 0) + return SteamWorkshopId.GetHashCode(); + if (!FallbackPackageName.IsNullOrWhiteSpace() && !FolderPath.IsNullOrWhiteSpace()) + return string.Concat(FallbackPackageName, FolderPath).GetHashCode(); + if (!InternalName.IsNullOrWhiteSpace() && !FolderPath.IsNullOrWhiteSpace()) + return string.Concat(InternalName, FolderPath).GetHashCode(); + + return base.GetHashCode(); + } + + bool IEqualityComparer.Equals(IPackageDependencyInfo x, IPackageDependencyInfo y) => DependencyInfo.Equals(x, y); + + public static bool operator ==(IPackageDependencyInfo x, DependencyInfo y) => y?.Equals(x) ?? false; + public static bool operator !=(IPackageDependencyInfo x, DependencyInfo y) => y?.Equals(x) ?? false; + public static bool Equals(IPackageDependencyInfo x, IPackageDependencyInfo y) + { + if (x is null) + return false; + if (y is null) + return false; + if (x == y) + return true; + + if (x.DependencyPackage is not null && y.DependencyPackage is not null) + return y.DependencyPackage == x.DependencyPackage; + + if (!x.FolderPath.IsNullOrWhiteSpace() + && !y.FolderPath.IsNullOrWhiteSpace() + && y.FolderPath == x.FolderPath) + return true; + + if (!x.FolderPath.IsNullOrWhiteSpace() != !y.FolderPath.IsNullOrWhiteSpace()) + return false; + + if (!x.FallbackPackageName.IsNullOrWhiteSpace() + && !y.FallbackPackageName.IsNullOrWhiteSpace() + && y.FallbackPackageName == x.FallbackPackageName) + return true; + + if (x.SteamWorkshopId != 0 && y.SteamWorkshopId == x.SteamWorkshopId) + return true; + + return false; + } + + /// + /// Returns the hash code unique for the package reference. + /// + /// + /// + /// The hash should only be collision-free when referring to different packages. + public int GetHashCode(IPackageDependencyInfo obj) + { + int hashCode = Seed; + hashCode = ApplyHashString(hashCode, obj.FallbackPackageName); + hashCode = ApplyHashString(hashCode, obj.InternalName); + if (obj.SteamWorkshopId > 0) + hashCode ^= (int)obj.SteamWorkshopId; + + + int ApplyHashString(int currentValue, string str) + { + try + { + if (str is null || str.Length < 1) + return currentValue; + byte[] b = Encoding.UTF8.GetBytes(str); + for (int i = 0; i < Math.Min(24, b.Length-1); i++) + currentValue ^= b[i]; + return currentValue; + } + catch + { + return currentValue; + } + } + + return hashCode; + } + + private static readonly int Seed = new Random().Next(436457, int.MaxValue-900); } public record LocalizationResourceInfo : ILocalizationResourceInfo { + public string InternalName { get; init; } public ContentPackage OwnerPackage { get; init; } + public string FallbackPackageName { get; init; } public CultureInfo TargetCulture { get; init; } public Platform SupportedPlatforms { get; init; } public Target SupportedTargets { get; init; } @@ -67,6 +163,7 @@ public record LocalizationResourceInfo : ILocalizationResourceInfo public readonly struct LuaScriptResourceInfo : ILuaResourceInfo { public ContentPackage OwnerPackage { get; init; } + public string FallbackPackageName { get; init; } public Platform SupportedPlatforms { get; init; } public Target SupportedTargets { get; init; } public int LoadPriority { get; init; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/EPlatformsTargets.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/EPlatformsTargets.cs index 42d702b7f..bdf4188dc 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/EPlatformsTargets.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/EPlatformsTargets.cs @@ -1,4 +1,5 @@ using System; +// ReSharper disable InconsistentNaming namespace Barotrauma.LuaCs.Data; @@ -6,7 +7,7 @@ namespace Barotrauma.LuaCs.Data; public enum Platform { Linux=0x1, - OSX=0x2, + MacOS=0x2, Windows=0x4 } diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/IBaseInfoDefinitions.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/IBaseInfoDefinitions.cs index e970aa9ce..4b269abe4 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/IBaseInfoDefinitions.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/IBaseInfoDefinitions.cs @@ -21,14 +21,10 @@ public interface IPlatformInfo Target SupportedTargets { get; } } - /// -/// Which package does the following data belong to? +/// All info we should have on a package for a given resource. /// -public interface IPackageInfo -{ - ContentPackage OwnerPackage { get; } -} +public interface IPackageInfo : IDataInfo { } /// @@ -65,13 +61,3 @@ public interface IResourceCultureInfo /// ImmutableArray SupportedCultures { get; } } - - -public interface ILoadableResourceInfo -{ - /// - /// [UNIQUE] The name that will be used when trying to reference this resource for execution or loading. - /// - [Required] - public string InternalName { get; } -} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/IConfigInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/IConfigInfo.cs index c56cb5aa1..fa1c487b7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/IConfigInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/IConfigInfo.cs @@ -1,26 +1,23 @@ using System; +using Barotrauma.LuaCs.Networking; using Barotrauma.Networking; namespace Barotrauma.LuaCs.Data; // TODO: Finish -public partial interface IConfigInfo +public partial interface IConfigInfo : IDataInfo { - string Name { get; } - string PackageName { get; } - ConfigDataType Type { get; } + /// + /// Specifies the data type this should be initialized to (ie. string, int, vector, etc.) + /// Custom types can be registered by mods. + /// + string DataType { get; } string DefaultValue { get; } + string StoredValue { get; } ClientPermissions RequiredPermissions { get; } -} - -public enum ConfigDataType -{ - Boolean, Int32, Int64, Single, Double, String, - Color, Vector2, Vector3, List, - RangeInt32, RangeSingle, ControlInput -} - -public enum NetSync -{ - None, TwoWay, ServerAuthority, ClientOneWay + /// + /// Whether a value can be changed at runtime. + /// + bool IsReadOnly { get; } + NetSync NetSync { get; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/IDataInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/IDataInfo.cs new file mode 100644 index 000000000..330b08ac4 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/IDataInfo.cs @@ -0,0 +1,20 @@ +namespace Barotrauma.LuaCs.Data; + +/// +/// Serves as a compound-key to refer to all resources and information that comes from a specific source. +/// +public interface IDataInfo +{ + /// + /// Package-Unique name to be used internally for all representations of, and references to, this information. + /// + string InternalName { get; } + /// + /// The package this information belongs to. + /// + ContentPackage OwnerPackage { get; } + /// + /// Used in place of the package data when the OwnerPackage is missing. + /// + string FallbackPackageName { get; } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/ILocalizationInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/ILocalizationInfo.cs index c05887e5a..b74b1f275 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/ILocalizationInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/ILocalizationInfo.cs @@ -1,6 +1,12 @@ -namespace Barotrauma.LuaCs.Data; +using System.Collections.Generic; +using System.Globalization; -public interface ILocalizationInfo +namespace Barotrauma.LuaCs.Data; + +public interface ILocalizationInfo : IDataInfo { - + string Symbol { get; } + IReadOnlyDictionary LocalizedValues { get; } + RawLString GetLocalizedString(CultureInfo locale); + RawLString GetLocalizedString(string cultureCode); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/IPackageDependencyInfo.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/IPackageDependencyInfo.cs index ef96428f1..7da431927 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/IPackageDependencyInfo.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/IPackageDependencyInfo.cs @@ -1,18 +1,17 @@ -using System.Collections.Immutable; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Immutable; namespace Barotrauma.LuaCs.Data; -public interface IPackageDependencyInfo : IPackageInfo +public interface IPackageDependencyInfo : IPackageInfo, + IEqualityComparer { /// /// Root folder of the content package. /// public string FolderPath { get; } /// - /// Name of the package. - /// - public string PackageName { get; } - /// /// Steam ID of the package. /// public ulong SteamWorkshopId { get; } @@ -20,6 +19,16 @@ public interface IPackageDependencyInfo : IPackageInfo /// The dependency package, if found in the ALL Packages List. /// public ContentPackage DependencyPackage { get; } + + /// + /// This dependency was not found. + /// + public bool IsMissing { get; } + + /// + /// Whether the package is installed from the workshop. False means installation is from local mods. + /// + public bool IsWorkshopInstallation { get; } } public interface IPackageDependenciesInfo diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/IResourceInfoDeclarations.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/IResourceInfoDeclarations.cs index 8b6f28e9b..c92fc1ec0 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/IResourceInfoDeclarations.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Data/IResourceInfoDeclarations.cs @@ -10,8 +10,8 @@ public interface ILocalizationResourceInfo : IResourceInfo, IResourceCultureInfo /// /// Represents loadable Lua files. /// -public interface ILuaResourceInfo : IResourceInfo, IResourceCultureInfo, IPackageDependenciesInfo, ILoadableResourceInfo, IPackageInfo { } -public interface IAssemblyResourceInfo : IResourceInfo, IResourceCultureInfo, IPackageDependenciesInfo, ILoadableResourceInfo, IPackageInfo +public interface ILuaResourceInfo : IResourceInfo, IResourceCultureInfo, IPackageDependenciesInfo, IPackageInfo { } +public interface IAssemblyResourceInfo : IResourceInfo, IResourceCultureInfo, IPackageDependenciesInfo, IPackageInfo { /// /// The friendly name of the assembly. Script files belonging to the same assembly should all have the same name. diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/IEvents.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/IEvents.cs new file mode 100644 index 000000000..b82eef22f --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/IEvents.cs @@ -0,0 +1,212 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using Barotrauma.LuaCs.Services; +using Barotrauma.Networking; +using Dynamitey; +using ImpromptuInterface; + +namespace Barotrauma.LuaCs.Events; + +/* + * The following is a collection of interfaces that types can implement to be registered events. + * Note: Internally-marked interfaces should be consumed using a publicizer. This is due to the Barotrauma source + * types being internal by default. +*/ + +public interface IEvent +{ + bool IsLuaRunner() => false; +} + +public interface IEvent : IEvent where T : IEvent +{ + static virtual T GetLuaRunner(IDictionary luaFunc) + { + // throw error if not overriden since we don't have 'static abstract'. + // Implementers must provide the runner. + throw new NotImplementedException(); + } +} + +#region GameEvents + +/// +/// Called as soon as round begins to load before any loading takes place. +/// +public interface IEventRoundStarting : IEvent +{ + void OnRoundStarting(); + static IEventRoundStarting IEvent.GetLuaRunner(IDictionary luaFunc) => new + { + IsLuaRunner = Return.Arguments(() => true), + OnRoundStarting = ReturnVoid.Arguments(() => luaFunc[nameof(OnRoundStarting)]()) + }.ActLike(); +} + +/// +/// Called when a round has started and fully loaded. +/// +public interface IEventRoundStarted : IEvent +{ + void OnRoundStart(); + static IEventRoundStarted IEvent.GetLuaRunner(IDictionary luaFunc) => new + { + IsLuaRunner = Return.Arguments(() => true), + OnRoundStart = ReturnVoid.Arguments(() => luaFunc[nameof(OnRoundStart)]()) + }.ActLike(); +} + +/// +/// Called on game loop normal update. +/// +public interface IEventUpdate : IEvent +{ + void OnUpdate(float fixedDeltaTime); + static IEventUpdate IEvent.GetLuaRunner(IDictionary luaFunc) => new + { + IsLuaRunner = Return.Arguments(() => true), + OnUpdate = ReturnVoid.Arguments((fixedDeltaTime) => luaFunc[nameof(OnUpdate)](fixedDeltaTime)) + }.ActLike(); +} + +/// +/// Called on game loop draw update. +/// +public interface IEventDrawUpdate : IEvent +{ + void OnDrawUpdate(float deltaTime); + static IEventDrawUpdate IEvent.GetLuaRunner(IDictionary luaFunc) => new + { + IsLuaRunner = Return.Arguments(() => true), + OnDrawUpdate = ReturnVoid.Arguments((deltaTime) => luaFunc[nameof(OnDrawUpdate)](deltaTime)) + }.ActLike(); +} + +#endregion + +#region Networking + + +#region Networking-Server +#if SERVER +/// +/// Called when a client connects to the server and has loaded into the lobby. +/// +interface IEventClientConnected : IEvent +{ + /// + /// Called when a client connects to the server. + /// + /// The connecting client. + void OnClientConnected(Client client); + static IEventClientConnected IEvent.GetLuaRunner(IDictionary luaFunc) => new + { + IsLuaRunner = Return.Arguments(() => true), + OnClientConnected = ReturnVoid.Arguments((client) => luaFunc[nameof(OnClientConnected)](client)) + }.ActLike(); +} +#endif +#endregion + +#region Networking-Client +#if CLIENT +/// +/// Called when the client has connected to the server and loaded to the lobby. +/// +public interface IEventServerConnected : IEvent +{ + void OnServerConnected(); + static IEventServerConnected IEvent.GetLuaRunner(IDictionary luaFunc) => new + { + IsLuaRunner = Return.Arguments(() => true), + OnServerConnected = ReturnVoid.Arguments(() => luaFunc[nameof(OnServerConnected)]()) + }.ActLike(); +} +#endif +#endregion + +#endregion + +#region Assembly_PluginEvents + +/// +/// Called on plugin normal, use this for basic/core loading that does not rely on any other modded content. +/// +public interface IEventPluginInitialize : IEvent +{ + void Initialize(); + static IEventPluginInitialize IEvent.GetLuaRunner(IDictionary luaFunc) => new + { + IsLuaRunner = Return.Arguments(() => true), + OnInitialize = ReturnVoid.Arguments(() => luaFunc[nameof(Initialize)]()) + }.ActLike(); +} + +/// +/// Called once all plugins have been loaded. if you have integrations with any other mod, put that code here. +/// +public interface IEventPluginLoadCompleted : IEvent +{ + void OnLoadCompleted(); + static IEventPluginLoadCompleted IEvent.GetLuaRunner(IDictionary luaFunc) => new + { + IsLuaRunner = Return.Arguments(() => true), + OnLoadCompleted = ReturnVoid.Arguments(() => luaFunc[nameof(OnLoadCompleted)]()) + }.ActLike(); +} + +/// +/// Called before Barotrauma initializes plugins. Use if you want to patch another plugin's behaviour 'unofficially'. +/// WARNING: This method is called before Initialize()! +/// +public interface IEventPluginPreInitialize : IEvent +{ + void PreInitPatching(); + static IEventPluginPreInitialize IEvent.GetLuaRunner(IDictionary luaFunc) => new + { + IsLuaRunner = Return.Arguments(() => true), + OnPreInitialize = ReturnVoid.Arguments(() => luaFunc[nameof(PreInitPatching)]()) + }.ActLike(); +} + +/// +/// Called whenever a new assembly is loaded. +/// +public interface IEventAssemblyLoaded : IEvent +{ + void OnAssemblyLoaded(Assembly assembly); + static IEventAssemblyLoaded IEvent.GetLuaRunner(IDictionary luaFunc) => new + { + IsLuaRunner = Return.Arguments(() => true), + OnAssemblyLoaded = ReturnVoid.Arguments((ass) => luaFunc[nameof(OnAssemblyLoaded)](ass)) + }.ActLike(); +} + +/// +/// Called whenever an is instanced. +/// +public interface IEventAssemblyContextCreated : IEvent +{ + void OnAssemblyCreated(IAssemblyLoaderService loaderService); + static IEventAssemblyContextCreated IEvent.GetLuaRunner(IDictionary luaFunc) => new + { + IsLuaRunner = Return.Arguments(() => true), + OnAssemblyContextCreated = ReturnVoid.Arguments((loader) => luaFunc[nameof(OnAssemblyCreated)](loader)) + }.ActLike(); +} + +/// +/// Called whenever an begins unloading. +/// +public interface IEventAssemblyContextUnloading : IEvent +{ + void OnAssemblyUnloading(WeakReference loaderService); + static IEventAssemblyContextUnloading IEvent.GetLuaRunner(IDictionary luaFunc) => new + { + IsLuaRunner = Return.Arguments(() => true), + OnAssemblyUnloading = ReturnVoid.Arguments>((loader) => luaFunc[nameof(OnAssemblyUnloading)](loader)) + }.ActLike(); +} + +#endregion diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Lua/LuaClasses/LuaGame.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Lua/LuaClasses/LuaGame.cs index fc515c8bd..5b6dba8d1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Lua/LuaClasses/LuaGame.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Lua/LuaClasses/LuaGame.cs @@ -463,7 +463,12 @@ namespace Barotrauma public List Commands => DebugConsole.Commands; - public void AssignOnExecute(string names, object onExecute) => DebugConsole.AssignOnExecute(names, (string[] a) => { GameMain.LuaCs.CallLuaFunction(onExecute, new object[] { a }); }); + public void AssignOnExecute(string names, object onExecute) => DebugConsole.AssignOnExecute(names, + (string[] a) => + { + //GameMain.LuaCs.CallLuaFunction(onExecute, new object[] { a }); + throw new NotImplementedException(); + }); public void SaveGame(string path) { diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/LuaCsHook.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/LuaCsHook.cs index cce9de419..11de83634 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/LuaCsHook.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/LuaCsHook.cs @@ -817,7 +817,7 @@ namespace Barotrauma if (luaCs.PerformanceCounter.EnablePerformanceCounter) { performanceMeasurement.Stop(); - luaCs.PerformanceCounter.SetHookElapsedTicks(name, key, performanceMeasurement.ElapsedTicks); + //luaCs.PerformanceCounter.SetHookElapsedTicks(name, key, performanceMeasurement.ElapsedTicks); TODO performanceMeasurement.Reset(); } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/LuaCsModStore.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/LuaCsModStore.cs deleted file mode 100644 index cdd77e17d..000000000 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/LuaCsModStore.cs +++ /dev/null @@ -1,114 +0,0 @@ -using MoonSharp.Interpreter; -using MoonSharp.Interpreter.Interop; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; - -namespace Barotrauma -{ - partial class LuaCsSetup - { - public class LuaCsModStore - { - public abstract class ModStore - { - protected Dictionary store; - - public TStore Set(string name, TStore value) => store[name] = value; - public TStore Get(string name) => store[name]; - - public ModStore(Dictionary store) => this.store = store; - - public abstract bool Equals(T value); - } - public class LuaModStore : ModStore - { - public string Name; - - public LuaModStore(Dictionary store) : base(store) { } - public override bool Equals(string value) => Name == value; - } - public class CsModStore : ModStore - { - public ACsMod Mod; - - public CsModStore(Dictionary store) : base(store) { } - public override bool Equals(ACsMod value) => Mod == value; - } - - private HashSet luaModInterface; - private HashSet csModInterface; - - public LuaCsModStore() - { - luaModInterface = new HashSet(); - csModInterface = new HashSet(); - } - - public void Initialize() - { - UserData.RegisterType(); - UserData.RegisterType(); - var msType = UserData.RegisterType(); - var msDesc = (StandardUserDataDescriptor)msType; - - typeof(StandardUserDataDescriptor).GetMethods(BindingFlags.NonPublic | BindingFlags.Instance).ToList().ForEach(m => - { - if ( - m.Name.Contains("Register") - ) - { - msDesc.AddMember(m.Name, new MethodMemberDescriptor(m, InteropAccessMode.Default)); - } - }); - } - public void Clear() - { - luaModInterface.Clear(); - csModInterface.Clear(); - } - - protected LuaModStore Register(string modName) - { - if (luaModInterface.Any(i => i.Equals(modName))) - { - LuaCsLogger.HandleException(new ArgumentException($"'{modName}' entry already registered"), LuaCsMessageOrigin.LuaMod); - return null; - } - - var newHandle = new LuaModStore(new Dictionary()); - if (luaModInterface.Add(newHandle)) return newHandle; - else return null; - } - [MoonSharpHidden] - public CsModStore Register(ACsMod mod) - { - if (csModInterface.Any(i => i.Equals(mod))) - { - LuaCsLogger.HandleException(new ArgumentException($"'{mod.GetType().FullName}' entry already registered"), LuaCsMessageOrigin.CSharpMod); - return null; - } - - var newHandle = new CsModStore(new Dictionary()); - if (csModInterface.Add(newHandle)) return newHandle; - else return null; - } - - public CsModStore GetCsStore(string modName) { - var result = csModInterface.Where(i => i.Mod.GetType().FullName == modName).FirstOrDefault(); - if (result != null) - { - if (!result.Mod.IsDisposed) return result; - else - { - csModInterface.Remove(result); - return null; - } - } - else return null; - } - protected LuaModStore GetLuaStore(string modName) => luaModInterface.Where(i => i.Name == modName).FirstOrDefault(); - } - } -} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/LuaCsPerformanceCounter.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/LuaCsPerformanceCounter.cs index 77c18a887..b59ae4dac 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/LuaCsPerformanceCounter.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/LuaCsPerformanceCounter.cs @@ -1,36 +1,81 @@ +using Barotrauma.LuaCs.Services; using System; using System.Collections.Generic; using System.Diagnostics; +using System.Linq; namespace Barotrauma { - public class LuaCsPerformanceCounter + public interface IPerformanceData { - public bool EnablePerformanceCounter = false; + public string Identifier { get; } + public long ElapsedTicks { get; } + } - public double UpdateElapsedTime; - public Dictionary> HookElapsedTime = new Dictionary>(); + public class SimplePerformanceData : IPerformanceData + { + public string Identifier { get; } + public long ElapsedTicks { get; } - public static float MemoryUsage + public SimplePerformanceData(string identifier, long elapsedTicks) { - get - { - Process proc = Process.GetCurrentProcess(); - float memory = MathF.Round(proc.PrivateMemorySize64 / (1024 * 1024), 2); - proc.Dispose(); - - return memory; - } - } - - public void SetHookElapsedTicks(string eventName, string hookName, long ticks) - { - if (!HookElapsedTime.ContainsKey(eventName)) - { - HookElapsedTime[eventName] = new Dictionary(); - } - - HookElapsedTime[eventName][hookName] = (double)ticks / Stopwatch.Frequency; + Identifier = identifier; + ElapsedTicks = elapsedTicks; } } -} \ No newline at end of file + + public class PerformanceCounterService : IReusableService + { + public bool EnablePerformanceCounter { get; set; } = false; + + private Dictionary> _data = new Dictionary>(); + + public void AddElapsedTicks(IPerformanceData data) + { + if (!EnablePerformanceCounter) { return; } + + if (!_data.ContainsKey(data.Identifier)) + { + _data.Add(data.Identifier, new List()); + } + + _data[data.Identifier].Add(data); + + Trim(data.Identifier, 100); + } + + public T GetLatestSnapshot(string identifier) where T : class, IPerformanceData + { + if (!_data.ContainsKey(identifier)) { return default; } + + return (T)_data[identifier].Last(); + } + + public T[] GetSnapshot(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().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>(); + return FluentResults.Result.Ok(); + } + + public void Dispose() { } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/LuaCsSetup.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/LuaCsSetup.cs index 4a479c11f..56a8536f7 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/LuaCsSetup.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/LuaCsSetup.cs @@ -1,17 +1,9 @@ using System; using System.IO; -using MoonSharp.Interpreter; -using MoonSharp.Interpreter.Interop; -using System.Runtime.CompilerServices; -using System.Linq; -using System.Threading; -using LuaCsCompatPatchFunc = Barotrauma.LuaCsPatch; -using System.Diagnostics; -using MoonSharp.VsCodeDebugger; -using System.Reflection; -using System.Runtime.Loader; -using System.Xml.Linq; +using Barotrauma.LuaCs.Configuration; using Barotrauma.LuaCs.Services; +using Barotrauma.LuaCs.Services.Compatibility; +using Barotrauma.LuaCs.Services.Processing; using Barotrauma.Networking; namespace Barotrauma @@ -42,23 +34,98 @@ namespace Barotrauma internal delegate void LuaCsErrorHandler(Exception ex, LuaCsMessageOrigin origin); internal delegate void LuaCsExceptionHandler(Exception ex, LuaCsMessageOrigin origin); - partial class LuaCsSetup + partial class LuaCsSetup : IDisposable { - public const string LuaSetupFile = "Lua/LuaSetup.lua"; - public const string VersionFile = "luacsversion.txt"; -#if WINDOWS - public static ContentPackageId LuaForBarotraumaId = new SteamWorkshopId(2559634234); -#elif LINUX - public static ContentPackageId LuaForBarotraumaId = new SteamWorkshopId(2970628943); -#elif OSX - public static ContentPackageId LuaForBarotraumaId = new SteamWorkshopId(2970890020); -#endif + public LuaCsSetup() + { + // load services + _servicesProvider = new ServicesProvider(); + RegisterServices(); + + // load manifest + if (!_servicesProvider.TryGetService(out IModConfigParserService modConfigSvc)) + throw new NullReferenceException("LuaCsSetup: Failed to get mod config parser service!"); // we should crash here + var luaConfig = modConfigSvc.BuildConfigFromManifest(Directory.GetCurrentDirectory() + LuaCsConfigFile); + if (!luaConfig.IsSuccess) + { + Logger.LogResults(luaConfig.ToResult()); + throw new FileLoadException("LuaCsSetup: Failed to load config file!"); + } + + // load resources + RegisterLocalizations(); + RegisterConfigs(); - public static ContentPackageId CsForBarotraumaId = new SteamWorkshopId(2795927223); + LuaForBarotraumaId = new SteamWorkshopId(LuaForBarotraumaSteamId.Value); + + return; + //--- + + void RegisterServices() + { + _servicesProvider.RegisterServiceType(ServiceLifetime.Singleton); + _servicesProvider.RegisterServiceType(ServiceLifetime.Singleton); + _servicesProvider.RegisterServiceType(ServiceLifetime.Singleton); + _servicesProvider.RegisterServiceType(ServiceLifetime.Singleton); + _servicesProvider.RegisterServiceType(ServiceLifetime.Singleton); + _servicesProvider.RegisterServiceType(ServiceLifetime.Singleton); + // TODO: IConfigService + // TODO: INetworkingService + // TODO: [Resource Converter/Parser Services] + // TODO: ILocalizationService + // TODO: IEventService + _servicesProvider.Compile(); + } + void RegisterLocalizations() + { + LocalizationService.LoadLocalizations(luaConfig.Value.Localizations); + } - private const string configFileName = "LuaCsSetupConfig.xml"; + void RegisterConfigs() + { + if (ConfigService.AddConfigs(luaConfig.Value.Configs) is { IsSuccess: false } res1) + { + Logger.LogResults(res1); + throw new Exception("LuaCsSetup: Failed to load config!"); + } + + if (ConfigService.AddConfigsProfiles(luaConfig.Value.ConfigProfiles) is { IsSuccess: false } res2) + { + Logger.LogResults(res2); + throw new Exception("LuaCsSetup: Failed to load config profiles!"); + } + IsCsEnabled = GetOrThrowForConfig(luaConfig.Value.PackageName, "IsCsEnabled"); + TreatForcedModsAsNormal = GetOrThrowForConfig(luaConfig.Value.PackageName, "TreatForcedModsAsNormal"); + PreferToUseWorkshopLuaSetup = GetOrThrowForConfig(luaConfig.Value.PackageName, "PreferToUseWorkshopLuaSetup"); + DisableErrorGUIOverlay = GetOrThrowForConfig(luaConfig.Value.PackageName, "DisableErrorGUIOverlay"); + EnableThreadedLoading = GetOrThrowForConfig(luaConfig.Value.PackageName, "EnableThreadedLoading"); + HideUserNamesInLogs = GetOrThrowForConfig(luaConfig.Value.PackageName, "HideUserNamesInLogs"); + LuaForBarotraumaSteamId = GetOrThrowForConfig(luaConfig.Value.PackageName, "LuaForBarotraumaSteamId"); + + return; + //--- + + IConfigEntry GetOrThrowForConfig(string packName, string internalName) where T : IConvertible, IEquatable + { + var cfgRes = ConfigService.GetConfig>(packName, internalName); + if (cfgRes.IsSuccess) + { + return cfgRes.Value; + } + Logger.LogResults(cfgRes.ToResult()); + throw new Exception($"LuaCsSetup: Failed to load config for {internalName}!"); + } + } + + + } + + #region CONST_DEF + + public const string LuaCsConfigFile = "LuaCsConfig.xml"; + #if SERVER public const bool IsServer = true; public const bool IsClient = false; @@ -67,6 +134,87 @@ namespace Barotrauma public const bool IsClient = true; #endif + #endregion + + #region Services_ConfigVars + + /* + * === Singleton Services + */ + + private readonly IServicesProvider _servicesProvider; + + public PerformanceCounterService PerformanceCounter => _servicesProvider.TryGetService(out var svc) + ? svc : throw new NullReferenceException("Performance counter service not found!"); + public ILoggerService Logger => _servicesProvider.TryGetService(out var svc) + ? svc : throw new NullReferenceException("Logger service not found!"); + public IConfigService ConfigService => _servicesProvider.TryGetService(out var svc) + ? svc : throw new NullReferenceException("Config Manager service not found!"); + public IPackageManagementService PackageManagementService => _servicesProvider.TryGetService(out var svc) + ? svc : throw new NullReferenceException("Package Manager service not found!"); + public IPluginManagementService PluginManagementService => _servicesProvider.TryGetService(out var svc) + ? svc : throw new NullReferenceException("Plugin Manager service not found!"); + public ILuaScriptManagementService LuaScriptService => _servicesProvider.TryGetService(out var svc) + ? svc : throw new NullReferenceException("Lua Script Manager service not found!"); + public ILocalizationService LocalizationService => _servicesProvider.TryGetService(out var svc) + ? svc : throw new NullReferenceException("Localization Manager service not found!"); + public INetworkingService NetworkingService => _servicesProvider.TryGetService(out var svc) + ? svc : throw new NullReferenceException("Networking Manager service not found!"); + public IEventService EventService => _servicesProvider.TryGetService(out var svc) + ? svc : throw new NullReferenceException("Networking Manager service not found!"); + + /* + * === Config Vars + */ + + /// + /// Whether C# plugin code is enabled. + /// + public IConfigEntry IsCsEnabled { get; private set; } + + /// + /// Whether mods marked as 'forced' or 'always load' should only be loaded if they're in the enabled mods list. + /// + public IConfigEntry TreatForcedModsAsNormal { get; private set; } + + /// + /// Whether the lua script runner from Workshop package should be used over the in-built version. + /// + public IConfigEntry PreferToUseWorkshopLuaSetup { get; private set; } + + /// + /// Whether the popup error GUI should be hidden/suppressed. + /// + public IConfigEntry DisableErrorGUIOverlay { get; private set; } + + /// + /// [Experimental] Whether multithreading should be used for loading. + /// + public IConfigEntry EnableThreadedLoading { get; private set; } + + /// + /// Whether usernames are anonymized or show in logs. + /// + public IConfigEntry HideUserNamesInLogs { get; private set; } + + private IConfigEntry LuaForBarotraumaSteamId { get; set; } + + #endregion + + #region LegacyRedirects + + public ILuaCsHook Hook => this.EventService; + + + #endregion + + /// + /// Whether mod content is loaded and being executed. + /// + public bool IsModContentRunning { get; private set; } + + public readonly ContentPackageId LuaForBarotraumaId; + public static bool IsRunningInsideWorkshop { get @@ -79,10 +227,7 @@ namespace Barotrauma } } - private static int executionNumber = 0; - - - public Script Lua { get; private set; } + /*public Script Lua { get; private set; } public LuaScriptLoader LuaScriptLoader { get; private set; } public LuaGame Game { get; private set; } @@ -90,7 +235,6 @@ namespace Barotrauma public LuaCsTimer Timer { get; private set; } public LuaCsNetworking Networking { get; private set; } public LuaCsSteam Steam { get; private set; } - public LuaCsPerformanceCounter PerformanceCounter { get; private set; } // must be available at anytime private static AssemblyManager _assemblyManager; @@ -98,12 +242,10 @@ namespace Barotrauma private CsPackageManager _pluginPackageManager; public CsPackageManager PluginPackageManager => _pluginPackageManager ??= new CsPackageManager(AssemblyManager, this); - - public LuaCsModStore ModStore { get; private set; } private LuaRequire Require { get; set; } public LuaCsSetupConfig Config { get; private set; } public MoonSharpVsCodeDebugServer DebugServer { get; private set; } - public bool IsInitialized { get; private set; } + public bool IsInitialized { get; private set; }*/ private bool ShouldRunCs { @@ -112,64 +254,21 @@ namespace Barotrauma #if SERVER if (GetPackage(CsForBarotraumaId, false, false) != null && GameMain.Server.ServerPeer is LidgrenServerPeer) { return true; } #endif - - return Config.EnableCsScripting; + return IsCsEnabled.Value; } } - public LuaCsSetup() - { - Script.GlobalOptions.Platform = new LuaPlatformAccessor(); - - Hook = new LuaCsHook(this); - ModStore = new LuaCsModStore(); - - Game = new LuaGame(); - Networking = new LuaCsNetworking(); - DebugServer = new MoonSharpVsCodeDebugServer(); - - ReadSettings(); - } + [Obsolete("Use AssemblyManager::GetTypesByName()")] public static Type GetType(string typeName, bool throwOnError = false, bool ignoreCase = false) { - return AssemblyManager.GetTypesByName(typeName).FirstOrDefault((Type)null); + throw new NotImplementedException(); + //return AssemblyManager.GetTypesByName(typeName).FirstOrDefault((Type)null); } - - public void ToggleDebugger(int port = 41912) - { - if (!GameMain.LuaCs.DebugServer.IsStarted) - { - DebugServer.Start(); - AttachDebugger(); - - LuaCsLogger.Log($"Lua Debug Server started on port {port}."); - } - else - { - DetachDebugger(); - DebugServer.Stop(); - - LuaCsLogger.Log($"Lua Debug Server stopped."); - } - } - - public void AttachDebugger() - { - DebugServer.AttachToScript(Lua, "Script", s => - { - if (s.Name.StartsWith("LocalMods") || s.Name.StartsWith("Lua")) - { - return Environment.CurrentDirectory + "/" + s.Name; - } - return s.Name; - }); - } - - public void DetachDebugger() => DebugServer.Detach(Lua); - - public void ReadSettings() + + // Old config ref + /*public void ReadSettings() { Config = new LuaCsSetupConfig(); @@ -205,7 +304,7 @@ namespace Barotrauma document.Root.SetAttributeValue("DisableErrorGUIOverlay", Config.DisableErrorGUIOverlay); document.Root.SetAttributeValue("HideUserNames", Config.HideUserNames); document.Save(configFileName); - } + }*/ public static ContentPackage GetPackage(ContentPackageId id, bool fallbackToAll = true, bool useBackup = false) { @@ -250,7 +349,8 @@ namespace Barotrauma return null; } - private DynValue DoFile(string file, Table globalContext = null, string codeStringFriendly = null) + // Old code ref + /*private DynValue DoFile(string file, Table globalContext = null, string codeStringFriendly = null) { if (!LuaCsFile.CanReadFromPath(file)) { @@ -324,51 +424,7 @@ namespace Barotrauma public void Stop() { - PluginPackageManager.UnloadPlugins(); - - // unregister types - foreach (Type type in AssemblyManager.GetAllLoadedACLs().SelectMany( - acl => acl.AssembliesTypes.Select(kvp => kvp.Value))) - { - UserData.UnregisterType(type, true); - } - if (Lua?.Globals is not null) - { - Lua.Globals.Remove("CsPackageManager"); - Lua.Globals.Remove("AssemblyManager"); - } - - if (Thread.CurrentThread == GameMain.MainThread) - { - Hook?.Call("stop"); - } - - if (Lua != null && DebugServer.IsStarted) - { - DebugServer.Detach(Lua); - } - - LuaUserData.Clear(); - - Game?.Stop(); - - Hook?.Clear(); - ModStore.Clear(); - LuaScriptLoader = null; - Lua = null; - - // we can only unload assemblies after clearing ModStore/references. - PluginPackageManager.Dispose(); -#pragma warning disable CS0618 - ACsMod.LoadedMods.Clear(); -#pragma warning restore CS0618 - - Game = new LuaGame(); - Networking = new LuaCsNetworking(); - Timer = new LuaCsTimer(); - Steam = new LuaCsSteam(); - PerformanceCounter = new LuaCsPerformanceCounter(); IsInitialized = false; } @@ -382,170 +438,22 @@ namespace Barotrauma IsInitialized = true; - LuaCsLogger.LogMessage("Lua! Version " + AssemblyInfo.GitRevision); + Logger.Log($"Initializing LuaCs, git revision = {AssemblyInfo.GitRevision}"); + }*/ + + public void Update() + { + throw new NotImplementedException(); + } - bool csActive = ShouldRunCs || forceEnableCs; + public void Reset() + { + throw new NotImplementedException(); + } - LuaScriptLoader = new LuaScriptLoader(); - LuaScriptLoader.ModulePaths = new string[] { }; - - RegisterLuaConverters(); - - Lua = new Script(CoreModules.Preset_SoftSandbox | CoreModules.Debug | CoreModules.IO | CoreModules.OS_System); - Lua.Options.DebugPrint = (o) => { LuaCsLogger.LogMessage(o); }; - Lua.Options.ScriptLoader = LuaScriptLoader; - Lua.Options.CheckThreadAccess = false; - Script.GlobalOptions.ShouldPCallCatchException = (Exception ex) => { return true; }; - - Require = new LuaRequire(Lua); - - Game = new LuaGame(); - Networking = new LuaCsNetworking(); - Timer = new LuaCsTimer(); - Steam = new LuaCsSteam(); - PerformanceCounter = new LuaCsPerformanceCounter(); - Hook.Initialize(); - ModStore.Initialize(); - Networking.Initialize(); - - UserData.RegisterType(); - UserData.RegisterType(); - UserData.RegisterType(); - UserData.RegisterType(); - UserData.RegisterType(); - UserData.RegisterType(); - UserData.RegisterType(); - UserData.RegisterType(); - UserData.RegisterType(); - UserData.RegisterType(); - UserData.RegisterType(); - UserData.RegisterType(); - var uuid = UserData.RegisterType(); - UserData.RegisterType(); - UserData.RegisterType(); - - Lua.Globals["printerror"] = (DynValue o) => { LuaCsLogger.LogError(o.ToString(), LuaCsMessageOrigin.LuaMod); }; - - Lua.Globals["setmodulepaths"] = (Action)SetModulePaths; - - Lua.Globals["dofile"] = (Func)DoFile; - Lua.Globals["loadfile"] = (Func)LoadFile; - Lua.Globals["require"] = (Func)Require.Require; - - Lua.Globals["dostring"] = (Func)Lua.DoString; - Lua.Globals["load"] = (Func)Lua.LoadString; - - Lua.Globals["Logger"] = UserData.CreateStatic(); - Lua.Globals["LuaUserData"] = UserData.CreateStatic(); - Lua.Globals["LuaUserDataIUUD"] = uuid; - Lua.Globals["Game"] = Game; - Lua.Globals["Hook"] = Hook; - Lua.Globals["ModStore"] = ModStore; - Lua.Globals["Timer"] = Timer; - Lua.Globals["File"] = UserData.CreateStatic(); - Lua.Globals["Networking"] = Networking; - Lua.Globals["Steam"] = Steam; - Lua.Globals["PerformanceCounter"] = PerformanceCounter; - Lua.Globals["LuaCsConfig"] = new LuaCsSetupConfig(Config); - - Lua.Globals["ExecutionNumber"] = executionNumber; - Lua.Globals["CSActive"] = csActive; - - Lua.Globals["SERVER"] = IsServer; - Lua.Globals["CLIENT"] = IsClient; - - if (DebugServer.IsStarted) - { - AttachDebugger(); - } - - if (csActive) - { - LuaCsLogger.LogMessage("Cs! Version " + AssemblyInfo.GitRevision); - - UserData.RegisterType(); - UserData.RegisterType(); - UserData.RegisterType(); - - Lua.Globals["PluginPackageManager"] = PluginPackageManager; - Lua.Globals["AssemblyManager"] = AssemblyManager; - - try - { - Stopwatch taskTimer = new(); - taskTimer.Start(); - ModStore.Clear(); - - var state = PluginPackageManager.LoadAssemblyPackages(); - if (state is AssemblyLoadingSuccessState.Success or AssemblyLoadingSuccessState.AlreadyLoaded) - { - if(!PluginPackageManager.PluginsInitialized) - PluginPackageManager.InstantiatePlugins(true); - if(!PluginPackageManager.PluginsPreInit) - PluginPackageManager.RunPluginsPreInit(); // this is intended to be called at startup in the future - if(!PluginPackageManager.PluginsLoaded) - PluginPackageManager.RunPluginsInit(); - state = AssemblyLoadingSuccessState.Success; - taskTimer.Stop(); - ModUtils.Logging.PrintMessage($"{nameof(LuaCsSetup)}: Completed assembly loading. Total time {taskTimer.ElapsedMilliseconds}ms."); - } - else - { - PluginPackageManager.Dispose(); // cleanup if there's an error - } - - if(state is not AssemblyLoadingSuccessState.Success) - { - ModUtils.Logging.PrintError($"{nameof(LuaCsSetup)}: Error while loading Cs-Assembly Mods | Err: {state}"); - taskTimer.Stop(); - } - } - catch (Exception e) - { - ModUtils.Logging.PrintError($"{nameof(LuaCsSetup)}::{nameof(Initialize)}() | Error while loading assemblies! Details: {e.Message} | {e.StackTrace}"); - } - } - - - ContentPackage luaPackage = GetPackage(LuaForBarotraumaId); - - void RunLocal() - { - LuaCsLogger.LogMessage("Using LuaSetup.lua from the Barotrauma Lua/ folder."); - string luaPath = LuaSetupFile; - CallLuaFunction(Lua.LoadFile(luaPath), Path.GetDirectoryName(Path.GetFullPath(luaPath))); - } - - void RunWorkshop() - { - LuaCsLogger.LogMessage("Using LuaSetup.lua from the content package."); - string luaPath = Path.Combine(Path.GetDirectoryName(luaPackage.Path), "Binary/Lua/LuaSetup.lua"); - CallLuaFunction(Lua.LoadFile(luaPath), Path.GetDirectoryName(Path.GetFullPath(luaPath))); - } - - void RunNone() - { - LuaCsLogger.LogError("LuaSetup.lua not found! Lua/LuaSetup.lua, no Lua scripts will be executed or work.", LuaCsMessageOrigin.LuaMod); - } - - if (Config.PreferToUseWorkshopLuaSetup) - { - if (luaPackage != null) { RunWorkshop(); } - else if (File.Exists(LuaSetupFile)) { RunLocal(); } - else { RunNone(); } - } - else - { - if (File.Exists(LuaSetupFile)) { RunLocal(); } - else if (luaPackage != null) { RunWorkshop(); } - else { RunNone(); } - } - -#if SERVER - GameMain.Server.ServerSettings.LoadClientPermissions(); -#endif - - executionNumber++; + public void Dispose() + { + // TODO release managed resources here } } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/LuaCsUtility.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/LuaCsUtility.cs index 3354449e3..6ed7c87de 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/LuaCsUtility.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/LuaCsUtility.cs @@ -11,6 +11,7 @@ using System.Linq; using System.Net; using System.Reflection; using System.Xml.Linq; +using Barotrauma.LuaCs; namespace Barotrauma { @@ -262,7 +263,8 @@ namespace Barotrauma private static Type[] LoadDocTypes(XElement typesElem) { - var result = new List(); + throw new NotImplementedException(); + /*var result = new List(); var loadedTypes = LuaCsSetup.AssemblyManager .GetAllTypesInLoadedAssemblies() .ToImmutableHashSet(); @@ -279,7 +281,7 @@ namespace Barotrauma result.AddRange(typesFound); } - return result.ToArray(); + return result.ToArray();*/ } private static IEnumerable SaveDocTypes(IEnumerable types) diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/ModUtils.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/ModUtils.cs index 089cea715..bc181f36e 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/ModUtils.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/ModUtils.cs @@ -1,341 +1,534 @@ using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; using System.Text; +using System.Threading; using System.Xml.Serialization; using Barotrauma; using Barotrauma.Items.Components; +using Barotrauma.LuaCs.Data; using Barotrauma.Networking; using Microsoft.CodeAnalysis; using Microsoft.Xna.Framework; +using OneOf; +using Platform = Barotrauma.LuaCs.Data.Platform; -namespace Barotrauma; - -public static class ModUtils +namespace Barotrauma.LuaCs { - #region LOGGING - public static class Logging + public static class ModUtils { - public static void PrintMessage(string s) + public static class Environment { -#if SERVER - LuaCsLogger.LogMessage($"[Server] {s}"); + internal static void SetCurrentThreadAsMain() => MainThreadId = Thread.CurrentThread.ManagedThreadId; + public static int MainThreadId { get; private set; } = Int32.MinValue; + public static bool IsMainThread + { + get + { + if (MainThreadId == Int32.MinValue) + throw new ArgumentNullException("MainThread ID not set."); + return Thread.CurrentThread.ManagedThreadId == MainThreadId; + } + } + + public static readonly Platform CurrentPlatform = +#if WINDOWS + Platform.Windows; +#elif MACOS + Platform.MacOS; +#elif LINUX + Platform.Linux; #else - LuaCsLogger.LogMessage($"[Client] {s}"); + Platform.Linux; #endif - } - public static void PrintWarning(string s) - { -#if SERVER - LuaCsLogger.Log($"[Server] {s}", Color.Yellow); + public static readonly Target CurrentTarget = +#if CLIENT + Target.Client; +#elif SERVER + Target.Server; #else - LuaCsLogger.Log($"[Client] {s}", Color.Yellow); + Target.Server; #endif + } - public static void PrintError(string s) + #region LOGGING + + public static class Logging { + public static void PrintMessage(string s) + { #if SERVER - LuaCsLogger.LogError($"[Server] {s}"); + LuaCsLogger.LogMessage($"[Server] {s}"); #else - LuaCsLogger.LogError($"[Client] {s}"); + LuaCsLogger.LogMessage($"[Client] {s}"); #endif - } - } - - #endregion - - #region FILE_IO - - // ReSharper disable once InconsistentNaming - public static class IO - { - public static IEnumerable FindAllFilesInDirectory(string folder, string pattern, - SearchOption option) - { - try - { - return Directory.GetFiles(folder, pattern, option); } - catch (DirectoryNotFoundException e) + + public static void PrintWarning(string s) { - return new string[] { }; +#if SERVER + LuaCsLogger.Log($"[Server] {s}", Color.Yellow); +#else + LuaCsLogger.Log($"[Client] {s}", Color.Yellow); +#endif + } + + public static void PrintError(string s) + { +#if SERVER + LuaCsLogger.LogError($"[Server] {s}"); +#else + LuaCsLogger.LogError($"[Client] {s}"); +#endif } } - public static string PrepareFilePathString(string filePath) => - PrepareFilePathString(Path.GetDirectoryName(filePath)!, Path.GetFileName(filePath)); + #endregion - public static string PrepareFilePathString(string path, string fileName) => - Path.Combine(SanitizePath(path), SanitizeFileName(fileName)); + #region FILE_IO - public static string SanitizeFileName(string fileName) + // ReSharper disable once InconsistentNaming + public static class IO { - foreach (char c in Barotrauma.IO.Path.GetInvalidFileNameCharsCrossPlatform()) - fileName = fileName.Replace(c, '_'); - return fileName; - } - - /// - /// Gets the sanitized path for the top-level directory for a given content package. - /// - /// - /// - public static string GetContentPackageDir(ContentPackage package) - { - return SanitizePath(Path.GetFullPath(package.Dir)); - } - - public static string SanitizePath(string path) - { - foreach (char c in Path.GetInvalidPathChars()) - path = path.Replace(c.ToString(), "_"); - return path.CleanUpPath(); - } - - public static IOActionResultState GetOrCreateFileText(string filePath, out string fileText, Func fileDataFactory = null, bool createFile = true) - { - fileText = null; - string fp = Path.GetFullPath(SanitizePath(filePath)); - - IOActionResultState ioActionResultState = IOActionResultState.Success; - if (createFile) - { - ioActionResultState = CreateFilePath(SanitizePath(filePath), out fp, fileDataFactory); - } - else if (!File.Exists(fp)) - { - return IOActionResultState.FileNotFound; - } - - if (ioActionResultState == IOActionResultState.Success) + public static IEnumerable FindAllFilesInDirectory(string folder, string pattern, + SearchOption option) { try { - fileText = File.ReadAllText(fp!); - return IOActionResultState.Success; + return Directory.GetFiles(folder, pattern, option); } - catch (ArgumentNullException ane) + catch (DirectoryNotFoundException e) { - ModUtils.Logging.PrintError($"ModUtils::CreateFilePath() | Exception: An argument is null. path: {fp ?? "null"} | Exception Details: {ane.Message}"); - return IOActionResultState.FilePathNull; - } - catch (ArgumentException ae) - { - ModUtils.Logging.PrintError($"ModUtils::CreateFilePath() | Exception: An argument is invalid. path: {fp ?? "null"} | Exception Details: {ae.Message}"); - return IOActionResultState.FilePathInvalid; - } - catch (DirectoryNotFoundException dnfe) - { - ModUtils.Logging.PrintError($"ModUtils::CreateFilePath() | Exception: Cannot find directory. path: {fp ?? "null"} | Exception Details: {dnfe.Message}"); - return IOActionResultState.DirectoryMissing; - } - catch (PathTooLongException ptle) - { - ModUtils.Logging.PrintError($"ModUtils::CreateFilePath() | Exception: path length is over 200 characters. path: {fp ?? "null"} | Exception Details: {ptle.Message}"); - return IOActionResultState.PathTooLong; - } - catch (NotSupportedException nse) - { - ModUtils.Logging.PrintError($"ModUtils::CreateFilePath() | Exception: Operation not supported on your platform/environment (permissions?). path: {fp ?? "null"} | Exception Details: {nse.Message}"); - return IOActionResultState.InvalidOperation; - } - catch (IOException ioe) - { - ModUtils.Logging.PrintError($"ModUtils::CreateFilePath() | Exception: IO tasks failed (Operation not supported). path: {fp ?? "null"} | Exception Details: {ioe.Message}"); - return IOActionResultState.IOFailure; - } - catch (Exception e) - { - ModUtils.Logging.PrintError($"ModUtils::CreateFilePath() | Exception: Unknown/Other Exception. path: {fp ?? "null"} | ExceptionMessage: {e.Message}"); - return IOActionResultState.UnknownError; + return new string[] { }; } } - return ioActionResultState; - } + public static string PrepareFilePathString(string filePath) => + PrepareFilePathString(Path.GetDirectoryName(filePath)!, Path.GetFileName(filePath)); - public static IOActionResultState CreateFilePath(string filePath, out string formattedFilePath, Func fileDataFactory = null) - { - string file = Path.GetFileName(filePath); - string path = Path.GetDirectoryName(filePath)!; + public static string PrepareFilePathString(string path, string fileName) => + Path.Combine(SanitizePath(path), SanitizeFileName(fileName)); - formattedFilePath = IO.PrepareFilePathString(path, file); - try + public static string SanitizeFileName(string fileName) { - if (!Directory.Exists(path)) - Directory.CreateDirectory(path); - if (!File.Exists(formattedFilePath)) - File.WriteAllText(formattedFilePath, fileDataFactory is null ? "" : fileDataFactory.Invoke()); - return IOActionResultState.Success; - } - catch (ArgumentNullException ane) - { - ModUtils.Logging.PrintError($"ModUtils::CreateFilePath() | Exception: An argument is null. path: {formattedFilePath ?? "null"} | Exception Details: {ane.Message}"); - return IOActionResultState.FilePathNull; - } - catch (ArgumentException ae) - { - ModUtils.Logging.PrintError($"ModUtils::CreateFilePath() | Exception: An argument is invalid. path: {formattedFilePath ?? "null"} | Exception Details: {ae.Message}"); - return IOActionResultState.FilePathInvalid; - } - catch (DirectoryNotFoundException dnfe) - { - ModUtils.Logging.PrintError($"ModUtils::CreateFilePath() | Exception: Cannot find directory. path: {path ?? "null"} | Exception Details: {dnfe.Message}"); - return IOActionResultState.DirectoryMissing; - } - catch (PathTooLongException ptle) - { - ModUtils.Logging.PrintError($"ModUtils::CreateFilePath() | Exception: path length is over 200 characters. path: {formattedFilePath ?? "null"} | Exception Details: {ptle.Message}"); - return IOActionResultState.PathTooLong; - } - catch (NotSupportedException nse) - { - ModUtils.Logging.PrintError($"ModUtils::CreateFilePath() | Exception: Operation not supported on your platform/environment (permissions?). path: {formattedFilePath ?? "null"} | Exception Details: {nse.Message}"); - return IOActionResultState.InvalidOperation; - } - catch (IOException ioe) - { - ModUtils.Logging.PrintError($"ModUtils::CreateFilePath() | Exception: IO tasks failed (Operation not supported). path: {formattedFilePath ?? "null"} | Exception Details: {ioe.Message}"); - return IOActionResultState.IOFailure; - } - catch (Exception e) - { - ModUtils.Logging.PrintError($"ModUtils::CreateFilePath() | Exception: Unknown/Other Exception. path: {path ?? "null"} | Exception Details: {e.Message}"); - return IOActionResultState.UnknownError; - } - } - - public static IOActionResultState WriteFileText(string filePath, string fileText) - { - IOActionResultState ioActionResultState = CreateFilePath(filePath, out var fp); - if (ioActionResultState == IOActionResultState.Success) - { - try - { - File.WriteAllText(fp!, fileText); - return IOActionResultState.Success; - } - catch (ArgumentNullException ane) - { - ModUtils.Logging.PrintError($"ModUtils::WriteFileText() | Exception: An argument is null. path: {fp ?? "null"} | Exception Details: {ane.Message}"); - return IOActionResultState.FilePathNull; - } - catch (ArgumentException ae) - { - ModUtils.Logging.PrintError($"ModUtils::WriteFileText() | Exception: An argument is invalid. path: {fp ?? "null"} | Exception Details: {ae.Message}"); - return IOActionResultState.FilePathInvalid; - } - catch (DirectoryNotFoundException dnfe) - { - ModUtils.Logging.PrintError($"ModUtils::WriteFileText() | Exception: Cannot find directory. path: {fp ?? "null"} | Exception Details: {dnfe.Message}"); - return IOActionResultState.DirectoryMissing; - } - catch (PathTooLongException ptle) - { - ModUtils.Logging.PrintError($"ModUtils::WriteFileText() | Exception: path length is over 200 characters. path: {fp ?? "null"} | Exception Details: {ptle.Message}"); - return IOActionResultState.PathTooLong; - } - catch (NotSupportedException nse) - { - ModUtils.Logging.PrintError($"ModUtils::WriteFileText() | Exception: Operation not supported on your platform/environment (permissions?). path: {fp ?? "null"} | Exception Details: {nse.Message}"); - return IOActionResultState.InvalidOperation; - } - catch (IOException ioe) - { - ModUtils.Logging.PrintError($"ModUtils::WriteFileText() | Exception: IO tasks failed (Operation not supported). path: {fp ?? "null"} | Exception Details: {ioe.Message}"); - return IOActionResultState.IOFailure; - } - catch (Exception e) - { - ModUtils.Logging.PrintError($"ModUtils::WriteFileText() | Exception: Unknown/Other Exception. path: {fp ?? "null"} | ExceptionMessage: {e.Message}"); - return IOActionResultState.UnknownError; - } + foreach (char c in Barotrauma.IO.Path.GetInvalidFileNameCharsCrossPlatform()) + fileName = fileName.Replace(c, '_'); + return fileName; } - return ioActionResultState; - } + /// + /// Gets the sanitized path for the top-level directory for a given content package. + /// + /// + /// + public static string GetContentPackageDir(ContentPackage package) + { + return SanitizePath(Path.GetFullPath(package.Dir)); + } - /// - /// - /// - /// - /// - /// - /// - /// - /// - public static bool LoadOrCreateTypeXml(out T instance, - string filepath, Func typeFactory = null, bool createFile = true) where T : class, new() - { - instance = null; - filepath = filepath.CleanUpPath(); - if (IOActionResultState.Success == GetOrCreateFileText( - filepath, out string fileText, typeFactory is not null ? () => + public static string SanitizePath(string path) + { + foreach (char c in Path.GetInvalidPathChars()) + path = path.Replace(c.ToString(), "_"); + return path.CleanUpPath(); + } + + public static IOActionResultState GetOrCreateFileText(string filePath, out string fileText, + Func fileDataFactory = null, bool createFile = true) + { + fileText = null; + string fp = Path.GetFullPath(SanitizePath(filePath)); + + IOActionResultState ioActionResultState = IOActionResultState.Success; + if (createFile) + { + ioActionResultState = CreateFilePath(SanitizePath(filePath), out fp, fileDataFactory); + } + else if (!File.Exists(fp)) + { + return IOActionResultState.FileNotFound; + } + + if (ioActionResultState == IOActionResultState.Success) + { + try { - using StringWriter sw = new StringWriter(); - T t = typeFactory?.Invoke(); - if (t is not null) - { - XmlSerializer s = new XmlSerializer(typeof(T)); - s.Serialize(sw, t); - return sw.ToString(); - } - return ""; - } : null, createFile)) + fileText = File.ReadAllText(fp!); + return IOActionResultState.Success; + } + catch (ArgumentNullException ane) + { + ModUtils.Logging.PrintError( + $"ModUtils::CreateFilePath() | Exception: An argument is null. path: {fp ?? "null"} | Exception Details: {ane.Message}"); + return IOActionResultState.FilePathNull; + } + catch (ArgumentException ae) + { + ModUtils.Logging.PrintError( + $"ModUtils::CreateFilePath() | Exception: An argument is invalid. path: {fp ?? "null"} | Exception Details: {ae.Message}"); + return IOActionResultState.FilePathInvalid; + } + catch (DirectoryNotFoundException dnfe) + { + ModUtils.Logging.PrintError( + $"ModUtils::CreateFilePath() | Exception: Cannot find directory. path: {fp ?? "null"} | Exception Details: {dnfe.Message}"); + return IOActionResultState.DirectoryMissing; + } + catch (PathTooLongException ptle) + { + ModUtils.Logging.PrintError( + $"ModUtils::CreateFilePath() | Exception: path length is over 200 characters. path: {fp ?? "null"} | Exception Details: {ptle.Message}"); + return IOActionResultState.PathTooLong; + } + catch (NotSupportedException nse) + { + ModUtils.Logging.PrintError( + $"ModUtils::CreateFilePath() | Exception: Operation not supported on your platform/environment (permissions?). path: {fp ?? "null"} | Exception Details: {nse.Message}"); + return IOActionResultState.InvalidOperation; + } + catch (IOException ioe) + { + ModUtils.Logging.PrintError( + $"ModUtils::CreateFilePath() | Exception: IO tasks failed (Operation not supported). path: {fp ?? "null"} | Exception Details: {ioe.Message}"); + return IOActionResultState.IOFailure; + } + catch (Exception e) + { + ModUtils.Logging.PrintError( + $"ModUtils::CreateFilePath() | Exception: Unknown/Other Exception. path: {fp ?? "null"} | ExceptionMessage: {e.Message}"); + return IOActionResultState.UnknownError; + } + } + + return ioActionResultState; + } + + public static IOActionResultState CreateFilePath(string filePath, out string formattedFilePath, + Func fileDataFactory = null) { - XmlSerializer s = new XmlSerializer(typeof(T)); + string file = Path.GetFileName(filePath); + string path = Path.GetDirectoryName(filePath)!; + + formattedFilePath = IO.PrepareFilePathString(path, file); try { - using TextReader tr = new StringReader(fileText); - instance = (T)s.Deserialize(tr); - return true; + if (!Directory.Exists(path)) + Directory.CreateDirectory(path); + if (!File.Exists(formattedFilePath)) + File.WriteAllText(formattedFilePath, fileDataFactory is null ? "" : fileDataFactory.Invoke()); + return IOActionResultState.Success; } - catch(InvalidOperationException ioe) + catch (ArgumentNullException ane) { - ModUtils.Logging.PrintError($"Error while parsing type data for {typeof(T)}."); - #if DEBUG - ModUtils.Logging.PrintError($"Exception: {ioe.Message}. Details: {ioe.InnerException?.Message}"); - #endif - instance = null; - return false; + ModUtils.Logging.PrintError( + $"ModUtils::CreateFilePath() | Exception: An argument is null. path: {formattedFilePath ?? "null"} | Exception Details: {ane.Message}"); + return IOActionResultState.FilePathNull; + } + catch (ArgumentException ae) + { + ModUtils.Logging.PrintError( + $"ModUtils::CreateFilePath() | Exception: An argument is invalid. path: {formattedFilePath ?? "null"} | Exception Details: {ae.Message}"); + return IOActionResultState.FilePathInvalid; + } + catch (DirectoryNotFoundException dnfe) + { + ModUtils.Logging.PrintError( + $"ModUtils::CreateFilePath() | Exception: Cannot find directory. path: {path ?? "null"} | Exception Details: {dnfe.Message}"); + return IOActionResultState.DirectoryMissing; + } + catch (PathTooLongException ptle) + { + ModUtils.Logging.PrintError( + $"ModUtils::CreateFilePath() | Exception: path length is over 200 characters. path: {formattedFilePath ?? "null"} | Exception Details: {ptle.Message}"); + return IOActionResultState.PathTooLong; + } + catch (NotSupportedException nse) + { + ModUtils.Logging.PrintError( + $"ModUtils::CreateFilePath() | Exception: Operation not supported on your platform/environment (permissions?). path: {formattedFilePath ?? "null"} | Exception Details: {nse.Message}"); + return IOActionResultState.InvalidOperation; + } + catch (IOException ioe) + { + ModUtils.Logging.PrintError( + $"ModUtils::CreateFilePath() | Exception: IO tasks failed (Operation not supported). path: {formattedFilePath ?? "null"} | Exception Details: {ioe.Message}"); + return IOActionResultState.IOFailure; + } + catch (Exception e) + { + ModUtils.Logging.PrintError( + $"ModUtils::CreateFilePath() | Exception: Unknown/Other Exception. path: {path ?? "null"} | Exception Details: {e.Message}"); + return IOActionResultState.UnknownError; } } - return false; + public static IOActionResultState WriteFileText(string filePath, string fileText) + { + IOActionResultState ioActionResultState = CreateFilePath(filePath, out var fp); + if (ioActionResultState == IOActionResultState.Success) + { + try + { + File.WriteAllText(fp!, fileText); + return IOActionResultState.Success; + } + catch (ArgumentNullException ane) + { + ModUtils.Logging.PrintError( + $"ModUtils::WriteFileText() | Exception: An argument is null. path: {fp ?? "null"} | Exception Details: {ane.Message}"); + return IOActionResultState.FilePathNull; + } + catch (ArgumentException ae) + { + ModUtils.Logging.PrintError( + $"ModUtils::WriteFileText() | Exception: An argument is invalid. path: {fp ?? "null"} | Exception Details: {ae.Message}"); + return IOActionResultState.FilePathInvalid; + } + catch (DirectoryNotFoundException dnfe) + { + ModUtils.Logging.PrintError( + $"ModUtils::WriteFileText() | Exception: Cannot find directory. path: {fp ?? "null"} | Exception Details: {dnfe.Message}"); + return IOActionResultState.DirectoryMissing; + } + catch (PathTooLongException ptle) + { + ModUtils.Logging.PrintError( + $"ModUtils::WriteFileText() | Exception: path length is over 200 characters. path: {fp ?? "null"} | Exception Details: {ptle.Message}"); + return IOActionResultState.PathTooLong; + } + catch (NotSupportedException nse) + { + ModUtils.Logging.PrintError( + $"ModUtils::WriteFileText() | Exception: Operation not supported on your platform/environment (permissions?). path: {fp ?? "null"} | Exception Details: {nse.Message}"); + return IOActionResultState.InvalidOperation; + } + catch (IOException ioe) + { + ModUtils.Logging.PrintError( + $"ModUtils::WriteFileText() | Exception: IO tasks failed (Operation not supported). path: {fp ?? "null"} | Exception Details: {ioe.Message}"); + return IOActionResultState.IOFailure; + } + catch (Exception e) + { + ModUtils.Logging.PrintError( + $"ModUtils::WriteFileText() | Exception: Unknown/Other Exception. path: {fp ?? "null"} | ExceptionMessage: {e.Message}"); + return IOActionResultState.UnknownError; + } + } + + return ioActionResultState; + } + + /// + /// + /// + /// + /// + /// + /// + /// + /// + public static bool LoadOrCreateTypeXml(out T instance, + string filepath, Func typeFactory = null, bool createFile = true) where T : class, new() + { + instance = null; + filepath = filepath.CleanUpPath(); + if (IOActionResultState.Success == GetOrCreateFileText( + filepath, out string fileText, typeFactory is not null + ? () => + { + using StringWriter sw = new StringWriter(); + T t = typeFactory?.Invoke(); + if (t is not null) + { + XmlSerializer s = new XmlSerializer(typeof(T)); + s.Serialize(sw, t); + return sw.ToString(); + } + + return ""; + } + : null, createFile)) + { + XmlSerializer s = new XmlSerializer(typeof(T)); + try + { + using TextReader tr = new StringReader(fileText); + instance = (T)s.Deserialize(tr); + return true; + } + catch (InvalidOperationException ioe) + { + ModUtils.Logging.PrintError($"Error while parsing type data for {typeof(T)}."); +#if DEBUG + ModUtils.Logging.PrintError( + $"Exception: {ioe.Message}. Details: {ioe.InnerException?.Message}"); +#endif + instance = null; + return false; + } + } + + return false; + } + + public enum IOActionResultState + { + Success, + FileNotFound, + FilePathNull, + FilePathInvalid, + DirectoryMissing, + PathTooLong, + InvalidOperation, + IOFailure, + UnknownError + } } - public enum IOActionResultState + #endregion + + #region GAME + + public static class Game { - Success, FileNotFound, FilePathNull, FilePathInvalid, DirectoryMissing, PathTooLong, InvalidOperation, IOFailure, UnknownError + /// + /// Returns whether or not there is a round running. + /// + /// + public static bool IsRoundInProgress() + { +#if CLIENT + if (Screen.Selected is not null + && Screen.Selected.IsEditor) + return false; +#endif + return GameMain.GameSession is not null && Level.Loaded is not null; + } + } + + #endregion + + #region THREADING + + public static class Threading + { + /// + /// Gets the boolean value of an integer with thread-safety via Interlocked. + /// + /// + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool GetBool(ref int var) => Interlocked.CompareExchange(ref var, 1, 1) > 0; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void SetBool(ref int var, bool value) + { + if (value) + { + Interlocked.CompareExchange(ref var, 1, 0); + } + else + { + Interlocked.CompareExchange(ref var, 0, 1); + } + } + + /// + /// Gets if the integer is under 1 (is zero/false) and, if so, sets the value to one/true. + /// + /// + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool CheckClearAndSetBool(ref int var) + { + return Interlocked.CompareExchange(ref var, 1, 0) < 1; + } + + /// + /// Gets if the integer is over 0 (is one/true) and, if so, sets the value to zero/false. + /// + /// + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool CheckSetAndClearBool(ref int var) + { + return Interlocked.CompareExchange(ref var, 0, 1) > 0; + } + } + + #endregion + + #region UTILITIES_CORE + + public static V TryGetOrSet(this IDictionary dict, K key, Func valueFactory) where K : IEquatable + { + if (dict.TryGetValue(key, out var dictValue)) return dictValue; + if (valueFactory is not null) + dict.Add(key, valueFactory()); + else + return default; + return dict[key]; + } + + #endregion } - #endregion - - #region GAME - - public static class Game + public static class AssemblyExtensions { /// - /// Returns whether or not there is a round running. + /// Gets all types in the given assembly. Handles invalid type scenarios. /// - /// - public static bool IsRoundInProgress() + /// The assembly to scan + /// An enumerable collection of types. + public static IEnumerable GetSafeTypes(this Assembly assembly) { -#if CLIENT - if (Screen.Selected is not null - && Screen.Selected.IsEditor) - return false; -#endif - return GameMain.GameSession is not null && Level.Loaded is not null; + // Based on https://github.com/Qkrisi/ktanemodkit/blob/master/Assets/Scripts/ReflectionHelper.cs#L53-L67 + + try + { + return assembly.GetTypes(); + } + catch (ReflectionTypeLoadException re) + { + try + { + return re.Types.Where(x => x != null)!; + } + catch (InvalidOperationException) + { + return new List(); + } + } + catch (Exception) + { + return new List(); + } } - } - - #endregion } + +#region ExceptionData + +namespace FluentResults.LuaCs +{ + public static class MetadataType + { + public static string ExceptionDetails = nameof(ExceptionDetails); + public static string ExceptionObject = nameof(ExceptionObject); + public static string RootObject = nameof(RootObject); + public static string Sources = nameof(Sources); + public static string StackTrace = nameof(StackTrace); + } +} + +#endregion diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Networking/INetVar.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Networking/INetVar.cs index b4a7d5885..38c962046 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Networking/INetVar.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Networking/INetVar.cs @@ -1,4 +1,5 @@ -using Barotrauma.LuaCs.Configuration; +using System; +using Barotrauma.LuaCs.Configuration; using Barotrauma.LuaCs.Data; using Barotrauma.LuaCs.Networking; using Barotrauma.Networking; @@ -7,10 +8,6 @@ namespace Barotrauma.LuaCs.Networking; public interface INetVar : IVarId { - /// - /// Synchronized network id, uninitialized if value is zero/0. Used by Networking service. - /// - ushort NetId { get; } /// /// Synchronization type /// @@ -19,7 +16,12 @@ public interface INetVar : IVarId /// Permissions needed by clients to send net-events or receive net messages. /// ClientPermissions WritePermissions { get; } + void ReadNetMessage(INetReadMessage message); void WriteNetMessage(INetWriteMessage message); - void Initialize(ushort netId, NetSync syncMode, ClientPermissions writePermissions); +} + +public enum NetSync +{ + None, TwoWay, ServerAuthority, ClientOneWay } diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Networking/NetInterfaceCompat.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Networking/NetInterfaceCompat.cs index c68b318ae..8799208df 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Networking/NetInterfaceCompat.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Networking/NetInterfaceCompat.cs @@ -11,7 +11,7 @@ namespace Barotrauma.LuaCs.Networking; public interface INetWriteMessage { internal IWriteMessage Message { get; } - internal void SetMessage(IWriteMessage msg); + internal INetWriteMessage SetMessage(IWriteMessage msg); void WriteBoolean(bool val) => Message.WriteBoolean(val); @@ -84,7 +84,7 @@ public interface INetWriteMessage public interface INetReadMessage { internal IReadMessage Message { get; } - internal void SetMessage(IReadMessage msg); + internal INetReadMessage SetMessage(IReadMessage msg); bool ReadBoolean() => Message.ReadBoolean(); void ReadPadBits() => Message.ReadPadBits(); @@ -131,20 +131,30 @@ public class NetWriteMessage : INetWriteMessage IWriteMessage INetWriteMessage.Message => Message; - void INetWriteMessage.SetMessage(IWriteMessage msg) + INetWriteMessage INetWriteMessage.SetMessage(IWriteMessage msg) { Message = msg; + return this; } } +internal static class NetHelperExtensions +{ + internal static INetWriteMessage ToNetWriteMessage(this IWriteMessage msg) => + ((INetWriteMessage)new NetWriteMessage()).SetMessage(msg); + internal static INetReadMessage ToNetReadMessage(this IReadMessage msg) => + ((INetReadMessage)new NetReadMessage()).SetMessage(msg); +} + public class NetReadMessage : INetReadMessage { private IReadMessage Message { get; set; } IReadMessage INetReadMessage.Message => Message; - void INetReadMessage.SetMessage(IReadMessage msg) + INetReadMessage INetReadMessage.SetMessage(IReadMessage msg) { Message = msg; + return this; } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Plugins/IAssemblyPlugin.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Plugins/IAssemblyPlugin.cs deleted file mode 100644 index 5a450ba74..000000000 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Plugins/IAssemblyPlugin.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System; - -namespace Barotrauma; - -public interface IAssemblyPlugin : IDisposable -{ - /// - /// Called on plugin normal, use this for basic/core loading that does not rely on any other modded content. - /// - void Initialize(); - - /// - /// Called once all plugins have been loaded. if you have integrations with any other mod, put that code here. - /// - void OnLoadCompleted(); - - - /// - /// Called before Barotrauma initializes vanilla content. WARNING: This method may be called before Initialize()! - /// - void PreInitPatching(); -} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/AssemblyManager.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/AssemblyManager.cs index 9d1b7c38f..e2017fbee 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/AssemblyManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/AssemblyManager.cs @@ -8,6 +8,8 @@ using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Loader; using System.Threading; +using FluentResults; +using FluentResults.LuaCs; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; @@ -25,7 +27,8 @@ namespace Barotrauma.LuaCs.Services; /// Provides functionality for the loading, unloading and management of plugins implementing IAssemblyPlugin. /// All plugins are loaded into their own AssemblyLoadContext along with their dependencies. /// -public class AssemblyManager : IAssemblyManagementService +[Obsolete] +public class AssemblyManager : IAssemblyManagementService, IPluginManagementService { #region ExternalAPI @@ -275,6 +278,11 @@ public class AssemblyManager : IAssemblyManagementService } } + public bool IsAssemblyLoadedGlobal(string friendlyName) + { + throw new NotImplementedException(); + } + #endregion #region InternalAPI @@ -740,41 +748,12 @@ public class AssemblyManager : IAssemblyManagementService TryBeginDispose(); } - public void Reset() + public FluentResults.Result Reset() { - TryBeginDispose(); + return TryBeginDispose() ? FluentResults.Result.Ok() + : FluentResults.Result.Fail(new Error($"{nameof(AssemblyManager)}: failed to Reset service.") + .WithMetadata(MetadataType.ExceptionObject, this)); } } -public static class AssemblyExtensions -{ - /// - /// Gets all types in the given assembly. Handles invalid type scenarios. - /// - /// The assembly to scan - /// An enumerable collection of types. - public static IEnumerable GetSafeTypes(this Assembly assembly) - { - // Based on https://github.com/Qkrisi/ktanemodkit/blob/master/Assets/Scripts/ReflectionHelper.cs#L53-L67 - try - { - return assembly.GetTypes(); - } - catch (ReflectionTypeLoadException re) - { - try - { - return re.Types.Where(x => x != null)!; - } - catch (InvalidOperationException) - { - return new List(); - } - } - catch (Exception) - { - return new List(); - } - } -} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/Compatibility/ILuaCsHook.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/Compatibility/ILuaCsHook.cs new file mode 100644 index 000000000..3eb723a2e --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/Compatibility/ILuaCsHook.cs @@ -0,0 +1,15 @@ +using System; + +namespace Barotrauma.LuaCs.Services.Compatibility; + +public interface ILuaCsHook : ILuaCsShim +{ + [Obsolete("ACsMod is deprecated. Use ILuaEventService.Add() instead.")] + void Add(string eventName, string identifier, LuaCsFunc callback, ACsMod mod = null); + [Obsolete("ACsMod is deprecated. Use ILuaEventService.Add() instead.")] + void Add(string eventName, LuaCsFunc callback, ACsMod mod = null); + bool Exists(string eventName, string identifier); + [Obsolete("Only Lua subscribers will receive events from call. Use ILuaEventService.Add() instead.")] + T Call(string eventName, params object[] args); + object Call(string eventName, params object[] args); +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/Compatibility/ILuaCsLogger.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/Compatibility/ILuaCsLogger.cs new file mode 100644 index 000000000..30c82860e --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/Compatibility/ILuaCsLogger.cs @@ -0,0 +1,6 @@ +namespace Barotrauma.LuaCs.Services.Compatibility; + +public interface ILuaCsLogger : ILuaCsShim +{ + +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/Compatibility/ILuaCsNetworking.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/Compatibility/ILuaCsNetworking.cs new file mode 100644 index 000000000..fe78085bd --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/Compatibility/ILuaCsNetworking.cs @@ -0,0 +1,6 @@ +namespace Barotrauma.LuaCs.Services.Compatibility; + +internal partial interface ILuaCsNetworking : ILuaCsShim +{ + +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/Compatibility/ILuaCsShim.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/Compatibility/ILuaCsShim.cs new file mode 100644 index 000000000..661b26360 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/Compatibility/ILuaCsShim.cs @@ -0,0 +1,6 @@ +namespace Barotrauma.LuaCs.Services.Compatibility; + +public interface ILuaCsShim +{ + +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/Compatibility/ILuaCsUtility.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/Compatibility/ILuaCsUtility.cs new file mode 100644 index 000000000..b5db0066c --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/Compatibility/ILuaCsUtility.cs @@ -0,0 +1,6 @@ +namespace Barotrauma.LuaCs.Services.Compatibility; + +public interface ILuaCsUtility : ILuaCsShim +{ + +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/EventService.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/EventService.cs new file mode 100644 index 000000000..59a1e983f --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/EventService.cs @@ -0,0 +1,376 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Collections.Specialized; +using System.Dynamic; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using System.Threading; +using Barotrauma.Extensions; +using Barotrauma.LuaCs.Events; +using Barotrauma.LuaCs.Services.Compatibility; +using Barotrauma.LuaCs.Services.Safe; +using Dynamitey; +using FluentResults; +using FluentResults.LuaCs; +using HarmonyLib; +using ImpromptuInterface; +using OneOf; + +namespace Barotrauma.LuaCs.Services; + +public class EventService : IEventService, IEventAssemblyContextUnloading +{ + private readonly record struct TypeStringKey : IEqualityComparer, IEquatable + { + public Type Type { get; init; } + public string TypeName { get; init; } + public readonly int HashCode; + + public TypeStringKey(Type type) + { + Type = type ?? throw new ArgumentNullException(nameof(type)); + TypeName = type.Name; + HashCode = TypeName.GetHashCode(); + } + + public TypeStringKey(string typeName) + { + Type = null; + TypeName = typeName ?? throw new ArgumentNullException(nameof(typeName)); + HashCode = TypeName.GetHashCode(); + } + + public bool Equals(TypeStringKey x, TypeStringKey y) + { + if (x.Type is not null && y.Type is not null) + return x.Type == y.Type; + return x.TypeName == y.TypeName; + } + + public int GetHashCode(TypeStringKey obj) + { + return obj.HashCode; + } + + public static implicit operator TypeStringKey(Type type) => new(type); + public static implicit operator TypeStringKey(string typeName) => new(typeName); + } + /// + /// Contains subscriber delegates by event and identifier. + /// Structure:
+ /// - Key: Type or String, TypeName == String Equality.
+ /// - Value: Dictionary
+ /// ---- Key: Either string identifier or subscriber instance pointer
+ /// ---- Value: Subscriber delegate
+ ///
+ private readonly Dictionary, IEvent>> _subscriptions = new(); + private readonly Dictionary _eventTypeNameAliases = new(); + private readonly Lazy _pluginManagementService; + private readonly Dictionary>> _luaSubscriptionFactories = new(); + /// + /// A collection of factories to produce subscribers from a single lua function handle. For legacy Add() API. + /// + private readonly Dictionary> _luaLegacySubscriptionFactories = new(); + /// + /// A collection of lua event subscribers from Add() that had neither a valid event name nor an event alias pointing to one. + /// Only actionable via Call(). + /// + private readonly Dictionary> _luaOrphanSubscribers = new(); + + public EventService(Lazy pluginManagementService) + { + _pluginManagementService = pluginManagementService ?? throw new ArgumentNullException(nameof(pluginManagementService)); + } + + public bool IsDisposed { get; private set; } = false; + + #region Compatibility + + [Obsolete("ACsMod is deprecated. Use ILuaEventService.Add() instead.")] + void ILuaCsHook.Add(string eventName, string identifier, LuaCsFunc callback, ACsMod mod = null) + { + Add(eventName, identifier, callback); + } + [Obsolete("ACsMod is deprecated. Use ILuaEventService.Add() instead.")] + void ILuaCsHook.Add(string eventName, LuaCsFunc callback, ACsMod mod = null) + { + Add(eventName, callback); + } + + public bool Exists(string eventName, string identifier) + { + ((IService)this).CheckDisposed(); + if (_subscriptions.ContainsKey(eventName) && _subscriptions[eventName].ContainsKey(identifier)) + return true; + if (_luaOrphanSubscribers.ContainsKey(eventName)) + return true; + return false; + } + + [Obsolete("Part of the legacy events API, only works for Lua-only custom events.")] + public T Call(string eventName, params object[] args) + { + ((IService)this).CheckDisposed(); + if (!_luaOrphanSubscribers.TryGetValue(eventName, out var dict)) + return default; + T returnValue = default; + foreach (var sub in dict.Values) + { + try + { + var r = sub(args); + if (r != default) + returnValue = (T)r; + } + catch + { + continue; + } + } + return returnValue; + } + + [Obsolete("Part of the legacy events API, only works for Lua-only custom events.")] + public object Call(string eventName, params object[] args) => Call(eventName, args); + + #endregion + + public void Add(string eventName, string identifier, LuaCsFunc callback) + { + var eventKey = eventName; + if (_eventTypeNameAliases.TryGetValue(eventName, out var aliasType)) + eventKey = aliasType; + if (_luaLegacySubscriptionFactories.TryGetValue(eventKey, out var factory)) + { + factory(identifier, callback); + return; + } + _luaOrphanSubscribers.TryGetOrSet(eventName, () => new Dictionary()) + .Add(identifier.IsNullOrWhiteSpace() ? string.Empty : identifier, callback); + } + + public void Add(string eventName, LuaCsFunc callback) + { + Add(eventName, string.Empty, callback); + } + + public void Remove(string eventName, string identifier) + { + if (_luaOrphanSubscribers.TryGetValue(eventName, out var dict)) + dict.Remove(identifier); + if (_subscriptions.TryGetValue(eventName, out var dict2)) + dict2.Remove(identifier); + } + + public void PublishLuaEvent(string interfaceName, LuaCsFunc runner) + { + ((IService)this).CheckDisposed(); + if (interfaceName.IsNullOrWhiteSpace()) + return; + if (!_subscriptions.TryGetValue(interfaceName, out var dict)) + return; + + var type = _subscriptions + .Select(x => x.Key) + .FirstOrNull(x => x.Type?.Name == interfaceName)?.Type; + + var errors = new Queue(); + foreach (var eventSub in dict.Values) + { + try + { + runner(type is null ? eventSub : Convert.ChangeType(eventSub, type)); // cast if possible + } + catch + { + continue; + } + } + } + + public FluentResults.Result RegisterSafeEvent() where T : IEvent + { + ((IService)this).CheckDisposed(); + var type = typeof(T); + if (_luaSubscriptionFactories.ContainsKey(type)) + return FluentResults.Result.Ok().WithReason(new Success($"The event {type.Name} is already registered.")); + try + { + _luaSubscriptionFactories.Add(type, (ident, funcDict) => + { + var runner = T.GetLuaRunner(funcDict); + var dict = _subscriptions.TryGetOrSet(type, () => new Dictionary, IEvent>()); + if (!ident.IsNullOrWhiteSpace()) + dict[ident] = runner; + else + dict[runner] = runner; + }); + return FluentResults.Result.Ok(); + } + catch (NullReferenceException e) + { + return FluentResults.Result.Fail(new Error($"The lua runner for {type.Name} is not registered.") + .WithMetadata(MetadataType.ExceptionObject, this) + .WithMetadata(MetadataType.RootObject, type)); + } + } + + public FluentResults.Result UnregisterSafeEvent() where T : IEvent + { + ((IService)this).CheckDisposed(); + _luaSubscriptionFactories.Remove(typeof(T)); + if (!_subscriptions.TryGetValue(typeof(T), out var dict)) + return FluentResults.Result.Ok(); + dict.Values.Where(value => value.IsLuaRunner()).ToImmutableArray().ForEach(Unsubscribe); + return FluentResults.Result.Ok(); + } + + // lua subscribe + public void Subscribe(string interfaceName, string identifier, IDictionary callbacks) + { + ((IService)this).CheckDisposed(); + if (_luaSubscriptionFactories.TryGetValue(interfaceName, out var subFactory)) + subFactory(identifier, callbacks); + } + + public FluentResults.Result SetLegacyLuaRunnerFactory(Func runnerFactory) where T : IEvent + { + var type = typeof(T); + if (!_luaSubscriptionFactories.TryGetValue(type, out var dict)) + return FluentResults.Result.Fail(new Error($"Tried to add legacy lua factory for an event not registered for lua subscriptions.")); + + _luaLegacySubscriptionFactories[type] = (ident, func) => + { + var runner = runnerFactory(func); + _subscriptions.TryGetOrSet(type, () => new Dictionary, IEvent>())[ident] = runner; + }; + return FluentResults.Result.Ok(); + } + + public void RemoveLegacyLuaRunnerFactory() where T : IEvent + { + _luaLegacySubscriptionFactories.Remove(typeof(T)); + } + + public void SetAliasToEvent(string alias) where T : IEvent + { + if (alias.IsNullOrWhiteSpace()) + return; + _eventTypeNameAliases[alias] = typeof(T).Name; + } + + public void RemoveEventAlias(string alias) + { + _eventTypeNameAliases.Remove(alias); + } + + public void RemoveAllEventAliases() where T : IEvent + { + foreach (var keys in _eventTypeNameAliases + .Where(kvp => kvp.Value.IsNullOrWhiteSpace() || kvp.Value == typeof(T).Name) + .Select(kvp => kvp.Key).ToImmutableArray()) + { + _eventTypeNameAliases.Remove(keys); + } + } + + public FluentResults.Result Subscribe(T subscriber) where T : IEvent + { + ((IService)this).CheckDisposed(); + var eventType = typeof(T); + var dict = _subscriptions.TryGetOrSet(eventType, () => new Dictionary, IEvent>()); + if (dict.ContainsKey(OneOf.FromT1(subscriber))) + { + return FluentResults.Result.Fail( + new Error($"The subscriber for {eventType.Name} is already registered to the event.") + .WithMetadata(MetadataType.ExceptionObject, this) + .WithMetadata(MetadataType.RootObject, subscriber)); + } + dict[subscriber] = subscriber; + return FluentResults.Result.Ok(); + } + + public void Unsubscribe(T subscriber) where T : IEvent + { + ((IService)this).CheckDisposed(); + if (!_subscriptions.TryGetValue(typeof(T), out var dict)) + return; + dict.Remove(OneOf.FromT1(subscriber)); + } + + public void ClearAllEventSubscribers() where T : IEvent => _subscriptions.Remove(typeof(T)); + public void ClearAllSubscribers() => _subscriptions.Clear(); + + public FluentResults.Result PublishEvent(Action action) where T : IEvent + { + ((IService)this).CheckDisposed(); + var eventType = typeof(T); + if (!_subscriptions.TryGetValue(eventType, out var dict)) + { + return FluentResults.Result.Fail(new Error($"The event {eventType.Name} is not registered.") + .WithMetadata(MetadataType.ExceptionObject, this)); + } + + var errors = new Queue(); + foreach (var eventSub in dict.Values) + { + try + { + action((T)eventSub); + } + catch (Exception e) + { + errors.Enqueue(new Error($"Error while executing runner for {eventType.Name} on type {eventSub.GetType().Name}.") + .WithMetadata(MetadataType.ExceptionObject, this) + .WithMetadata(MetadataType.RootObject, eventSub) + .WithMetadata(MetadataType.ExceptionDetails, e.Message) + .WithMetadata(MetadataType.StackTrace, e.StackTrace)); + } + } + + var result = errors.Count > 0 ? FluentResults.Result.Fail($"Errors while executing event type {eventType.Name}") : FluentResults.Result.Ok(); + while (errors.Count > 0) + result = result.WithError(errors.Dequeue()); + return result; + } + + public void Dispose() + { + IsDisposed = true; + _subscriptions.Clear(); + _luaSubscriptionFactories.Clear(); + _eventTypeNameAliases.Clear(); + GC.SuppressFinalize(this); + } + + public FluentResults.Result Reset() + { + ((IService)this).CheckDisposed(); + _subscriptions.Clear(); + _luaSubscriptionFactories.Clear(); + _eventTypeNameAliases.Clear(); + return FluentResults.Result.Ok(); + } + + public void OnAssemblyUnloading(WeakReference loaderService) + { + if (!loaderService.TryGetTarget(out var loader)) + return; + foreach (var assembly in loader.Assemblies) + { + var types = assembly.GetSafeTypes() + .Where(t => typeof(IEvent).IsAssignableFrom(t)) + .ToImmutableArray(); + if (!types.Any()) + continue; + foreach (var type in types) + { + _subscriptions.Remove(type); + _luaSubscriptionFactories.Remove(type); + } + } + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/IAssemblyManagementService.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/IAssemblyManagementService.cs deleted file mode 100644 index 1ba46fa9a..000000000 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/IAssemblyManagementService.cs +++ /dev/null @@ -1,189 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Diagnostics.CodeAnalysis; -using System.Reflection; -using System.Runtime.CompilerServices; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -// ReSharper disable InconsistentNaming - -namespace Barotrauma.LuaCs.Services; - - -public interface IAssemblyManagementService : IService -{ - #region Public API - - /// - /// Called when an assembly is loaded. - /// - public event Action OnAssemblyLoaded; - - /// - /// Called when an assembly is marked for unloading, before unloading begins. You should use this to cleanup - /// any references that you have to this assembly. - /// - public event Action OnAssemblyUnloading; - - /// - /// Called whenever an exception is thrown. First arg is a formatted message, Second arg is the Exception. - /// - public event Action OnException; - - /// - /// For unloading issue debugging. Called whenever MemoryFileAssemblyContextLoader [load context] is unloaded. - /// - // ReSharper disable once InconsistentNaming - public event Action OnACLUnload; - - - /// - /// [DEBUG ONLY] - /// Returns a list of the current unloading ACLs. - /// - // ReSharper disable once InconsistentNaming - public ImmutableList> StillUnloadingACLs { get; } - - // ReSharper disable once MemberCanBePrivate.Global - /// - /// Checks if there are any AssemblyLoadContexts still in the process of unloading. - /// - public bool IsCurrentlyUnloading { get; } - - /// - /// Allows iteration over all non-interface types in all loaded assemblies in the AsmMgr that are assignable to the given type (IsAssignableFrom). - /// Warning: care should be used when using this method in hot paths as performance may be affected. - /// - /// The type to compare against - /// Forces caches to clear and for the lists of types to be rebuilt. - /// An Enumerator for matching types. - public IEnumerable GetSubTypesInLoadedAssemblies(bool rebuildList); - - /// - /// Tries to get types assignable to type from the ACL given the Guid. - /// - /// - /// - /// - /// Operation success. - public bool TryGetSubTypesFromACL(Guid id, out IEnumerable types); - - /// - /// Tries to get types from the ACL given the Guid. - /// - /// - /// - /// - public bool TryGetSubTypesFromACL(Guid id, out IEnumerable types); - - /// - /// Allows iteration over all types, including interfaces, in all loaded assemblies in the AsmMgr who's names match the string. - /// Note: Will return the by-reference equivalent type if the type name is prefixed with "out " or "ref ". - /// - /// The string name of the type to search for. - /// An Enumerator for matching types. List will be empty if bad params are supplied. - public IEnumerable GetTypesByName(string typeName); - - /// - /// Allows iteration over all types (including interfaces) in all loaded assemblies managed by the AsmMgr. - /// Warning: High usage may result in performance issues. - /// - /// An Enumerator for iteration. - public IEnumerable GetAllTypesInLoadedAssemblies(); - - /// - /// Returns a list of all loaded ACLs. - /// WARNING: References to these ACLs outside the AssemblyManager should be kept in a WeakReference in order - /// to avoid causing issues with unloading/disposal. - /// - /// - public IEnumerable GetAllLoadedACLs(); - - #endregion - - #region InternalAPI - /*** Notes: Internal API uses the 'public' modifier because of the common and recommended use of publicized APIs - * by third-party add-ins. - */ - - /// - /// [Unsafe] Warning: only for use in nested threading functions. Requires care to manage access. - /// Does not make any guarantees about the state of the ACL after the list has been returned. - /// - /// - public ImmutableList UnsafeGetAllLoadedACLs(); - - /// - /// Used by content package and plugin management to stop unloading of a given ACL until all plugins have gracefully closed. - /// - public event System.Func IsReadyToUnloadACL; - - /// - /// Compiles an assembly from supplied references and syntax trees into the specified AssemblyContextLoader. - /// A new ACL will be created if the Guid supplied is Guid.Empty. - /// - /// - /// - /// - /// - /// A non-unique name for later reference. Optional, set to null if unused. - /// The guid of the assembly - /// - /// - public AssemblyLoadingSuccessState LoadAssemblyFromMemory([NotNull] string compiledAssemblyName, - [NotNull] IEnumerable syntaxTree, - IEnumerable externalMetadataReferences, - [NotNull] CSharpCompilationOptions compilationOptions, - string friendlyName, - ref Guid id, - IEnumerable externFileAssemblyRefs = null); - - /// - /// Switches the ACL with the given Guid to Template Mode, which disables assembly name resolution for any assemblies loaded in it. - /// These ACLs are intended to be used to host Assemblies for information only and not for code execution. - /// WARNING: This process is irreversible. - /// - /// Guid of the ACL. - /// Whether an ACL was found with the given ID. - public bool SetACLToTemplateMode(Guid guid); - - - /// - /// Tries to load all assemblies at the supplied file paths list into the ACl with the given Guid. - /// If the supplied Guid is Empty, then a new ACl will be created and the Guid will be assigned to it. - /// - /// List of assemblies to try and load. - /// A non-unique name for later reference. Optional. - /// Guid of the ACL or Empty if none specified. Guid of ACL will be assigned to this var. - /// Operation success messages. - /// - public AssemblyLoadingSuccessState LoadAssembliesFromLocations([NotNull] IEnumerable filePaths, - string friendlyName, ref Guid id); - - - /// - /// Tries to begin the disposal process of ACLs. - /// - /// Returns whether the unloading process could be initiated. - public bool TryBeginDispose(); - - - /// - /// Returns whether unloading is completed and updates the styate of the unloading cache. - /// - /// - public bool FinalizeDispose(); - - /// - /// Tries to retrieve the LoadedACL with the given ID or null if none is found. - /// WARNING: External references to this ACL with long lifespans should be kept in a WeakReference - /// to avoid causing unloading/disposal issues. - /// - /// GUID of the ACL. - /// The found ACL or null if none was found. - /// Whether an ACL was found. - public bool TryGetACL(Guid id, out AssemblyManager.LoadedACL acl); - - #endregion -} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/IConfigService.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/IConfigService.cs deleted file mode 100644 index ac6deedd5..000000000 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/IConfigService.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Diagnostics.CodeAnalysis; -using Barotrauma.LuaCs.Configuration; -using Barotrauma.LuaCs.Data; -using Barotrauma.Networking; - -namespace Barotrauma.LuaCs.Services; - -public interface IConfigService : IService -{ - /* - * Resource Files. - */ - bool TryAddConfigs(ImmutableArray configResources); - bool TryAddConfigsProfiles(ImmutableArray configProfileResources); - void RemoveConfigs(ImmutableArray configResources); - void RemoveConfigsProfiles(ImmutableArray configProfilesResources); - - - /* - * Already processed - */ - bool TryAddConfigs(ImmutableArray configs); - bool TryAddConfigsProfiles(ImmutableArray configProfiles); - void RemoveConfigs(ImmutableArray configs); - void RemoveConfigsProfiles(ImmutableArray configProfiles); - - /* - * Immediate mode, does not have displayable functionality - */ - IConfigEntry AddConfigEntry(ContentPackage package, string name, - T defaultValue, - NetSync syncMode = NetSync.None, - ClientPermissions permissions = ClientPermissions.None, - Func valueChangePredicate = null, - Action> onValueChanged = null) where T : IConvertible, IEquatable; - - IConfigList AddConfigList(ContentPackage package, string name, - int defaultIndex, IReadOnlyList values, - NetSync syncMode = NetSync.None, - ClientPermissions permissions = ClientPermissions.None, - Func valueChangePredicate = null, - Action onValueChanged = null); - - IReadOnlyDictionary GetConfigsForPackage(ContentPackage package); - IReadOnlyDictionary GetConfigsForPackage(string packageName); - IReadOnlyDictionary<(ContentPackage, string), IConfigBase> GetAllConfigs(); -} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/IHookManagementService.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/IHookManagementService.cs deleted file mode 100644 index 5428ed704..000000000 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/IHookManagementService.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Barotrauma.LuaCs.Services; - -public interface IHookManagementService : IService -{ - -} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/ILegacyConfigService.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/ILegacyConfigService.cs deleted file mode 100644 index a93559084..000000000 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/ILegacyConfigService.cs +++ /dev/null @@ -1,8 +0,0 @@ -using Barotrauma.LuaCs.Data; - -namespace Barotrauma.LuaCs.Services; - -public interface ILegacyConfigService : IService -{ - bool TryBuildModConfigFromLegacy(ContentPackage package, out IModConfigInfo configInfo); -} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/ILocalizationService.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/ILocalizationService.cs deleted file mode 100644 index 045d3e986..000000000 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/ILocalizationService.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System; -using System.Globalization; -using System.Collections.Generic; -using System.Collections.Immutable; -using Barotrauma.LuaCs.Data; - -namespace Barotrauma.LuaCs.Services; - -public interface ILocalizationService : IService -{ - IReadOnlyCollection GetLoadedLocales(); - void Remove(ImmutableArray localizations); - bool TrySetCurrentCulture(CultureInfo culture); - bool TrySetCurrentCulture(string cultureName); - bool TryLoadLocalizations(ImmutableArray localizationResources); - string GetLocalizedString(string key, string fallback); - string GetLocalizedString(string key, CultureInfo targetCulture); - bool TryRegisterLocalizationResolver(CultureInfo targetCulture, Func factoryResolver); - bool ReplaceSymbols(string text, string symbolExpr); - bool IsCurrentCultureSupported(IResourceCultureInfo culturesInfo); -} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/INetworkingService.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/INetworkingService.cs deleted file mode 100644 index 3b2c7269b..000000000 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/INetworkingService.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System; -using Barotrauma.LuaCs.Data; -using Barotrauma.LuaCs.Networking; -using Barotrauma.Networking; - -namespace Barotrauma.LuaCs.Services; - -public interface INetworkingService : IService -{ - bool IsActive { get; } - bool IsSynchronized { get; } - bool TryRegisterVar(INetVar var, NetSync mode, ClientPermissions permissions); - void UnregisterVar(Guid varId); - bool SendEvent(Guid varId); - void SendMessageGlobal(string id, string message); - void Synchronize(); - - #region LegacyAPI - - bool RestrictMessageSize { get; set; } - - #endregion -} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/IPackageManagementService.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/IPackageManagementService.cs deleted file mode 100644 index 3ef92394a..000000000 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/IPackageManagementService.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Globalization; -using Barotrauma.LuaCs.Data; - -namespace Barotrauma.LuaCs.Services; - -public interface IPackageManagementService : IService -{ - void AddPackages(ref ReadOnlySpan<(ContentPackage, bool)> packages, - bool executeImmediately = false, - bool errorOnFailures = false, - bool errorOnExistingPackageFound = false); - void LoadPackages(bool onlyUnloadedPackages = true, bool rescanPackages = false); - void UnloadPackages(bool errorOnFailures = true); - bool IsPackageLoaded(ContentPackage package); - bool CheckDependencyLoaded(IPackageDependencyInfo info); - bool CheckDependenciesLoaded(IEnumerable infos, out IReadOnlyList missingPackages); - bool CheckEnvironmentSupported(IPlatformInfo platform); -} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/IPluginManagementService.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/IPluginManagementService.cs deleted file mode 100644 index b5c505c83..000000000 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/IPluginManagementService.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Barotrauma.LuaCs.Services; - -public interface IPluginManagementService : IService -{ - bool IsAssemblyLoadedGlobal(string friendlyName); - -} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/IService.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/IService.cs deleted file mode 100644 index bd7595143..000000000 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/IService.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; - -namespace Barotrauma.LuaCs.Services; - -/// -/// Base interface inherited by all services -/// -public interface IService : IDisposable -{ - /// - /// Returns the service to its original state (post-instantiation). - /// Allows a service instance to be reused without disposing of the instance. - /// - void Reset(); -} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/IStorageService.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/IStorageService.cs deleted file mode 100644 index 608a5417c..000000000 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/IStorageService.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System.Collections.Immutable; -using System.Xml.Linq; - -namespace Barotrauma.LuaCs.Services; - -public interface IStorageService : IService -{ - #region LocalGameData - - bool TryLoadLocalXml(ContentPackage package, string localFilePath, out XDocument document); - bool TryLoadLocalBinary(ContentPackage package, string localFilePath, out byte[] bytes); - bool TryLoadLocalText(ContentPackage package, string localFilePath, out string text); - bool FileExistsInLocalData(ContentPackage package, string localFilePath); - - #endregion - - #region ContentPackageData - bool TryLoadPackageXml(ContentPackage package, string localFilePath, out XDocument document); - bool TryLoadPackageBinary(ContentPackage package, string localFilePath, out byte[] bytes); - bool TryLoadPackageText(ContentPackage package, string localFilePath, out string text); - - ImmutableArray TryLoadPackageXmlFiles(ContentPackage package, ImmutableArray localFilePath, out ImmutableArray document); - ImmutableArray TryLoadPackageBinaryFiles(ContentPackage package, ImmutableArray localFilePath, out ImmutableArray bytes); - ImmutableArray TryLoadPackageTextFiles(ContentPackage package, ImmutableArray localFilePath, out ImmutableArray text); - - bool FindFilesInPackage(ContentPackage package, string localSubfolder, string regexFilter, bool searchRecursively, out ImmutableArray localFilePaths); - bool FileExistsInPackage(ContentPackage package, string localFilePath); - - #endregion - - #region AbsolutePaths - - bool TryLoadXml(string filePath, out XDocument document); - bool TrySaveXml(string filePath, in XDocument document); - bool TryLoadBinary(string filePath, out byte[] bytes); - bool TrySaveBinary(string filePath, in byte[] bytes); - bool TryLoadText(string filePath, out string text); - bool TrySaveText(string filePath, string text); - bool FileExists(string filePath); - - #endregion -} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/LoggerService.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/LoggerService.cs index 25fb27542..a551aca9f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/LoggerService.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/LoggerService.cs @@ -129,6 +129,11 @@ public partial class LoggerService : ILoggerService #endif } + public void LogResults(FluentResults.Result result) + { + throw new NotImplementedException(); + } + public void LogDebug(string message, Color? color = null) { throw new NotImplementedException(); @@ -145,5 +150,5 @@ public partial class LoggerService : ILoggerService } public void Dispose() { } - public void Reset() { } + public FluentResults.Result Reset() => FluentResults.Result.Ok(); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/LuaScriptService.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/LuaScriptService.cs index b1c30f23e..eef266305 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/LuaScriptService.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/LuaScriptService.cs @@ -1,6 +1,176 @@ -namespace Barotrauma.LuaCs.Services; +using Barotrauma.LuaCs.Data; +using MoonSharp.Interpreter; +using MoonSharp.Interpreter.Interop; +using System; +using System.Collections.Immutable; +using System.Reflection; -public class LuaScriptService +namespace Barotrauma.LuaCs.Services; + +public class LuaScriptService : ILuaScriptService, ILuaScriptManagementService { - + public void AddField(IUserDataDescriptor descriptor, string fieldName, DynValue value) + { + throw new NotImplementedException(); + } + + public void AddMethod(IUserDataDescriptor descriptor, string methodName, object function) + { + throw new NotImplementedException(); + } + + public FluentResults.Result AddScriptFiles(ImmutableArray luaResource) + { + throw new System.NotImplementedException(); + } + + public object CreateEnumTable(string typeName) + { + throw new NotImplementedException(); + } + + public object CreateStatic(string typeName) + { + throw new NotImplementedException(); + } + + public DynValue CreateUserDataFromDescriptor(DynValue scriptObject, IUserDataDescriptor descriptor) + { + throw new NotImplementedException(); + } + + public DynValue CreateUserDataFromType(DynValue scriptObject, Type desiredType) + { + throw new NotImplementedException(); + } + + public void Dispose() + { + throw new System.NotImplementedException(); + } + + public FluentResults.Result ExecuteLoadedScripts(ContentPackage package, bool pauseExecutionOnError = false, bool verboseLogging = false) + { + throw new NotImplementedException(); + } + + public FluentResults.Result ExecuteLoadedScripts(ImmutableArray scripts, bool pauseExecutionOnError = false, bool verboseLogging = false) + { + throw new NotImplementedException(); + } + + public FluentResults.Result ExecuteLoadedScripts(bool pauseExecutionOnError = false, bool verboseLogging = false) + { + throw new NotImplementedException(); + } + + public FluentResults.Result ExecuteScripts(bool pauseExecutionOnScriptError = false, bool verboseLogging = false) + { + throw new System.NotImplementedException(); + } + + public FieldInfo FindFieldRecursively(Type type, string fieldName) + { + throw new NotImplementedException(); + } + + public MethodInfo FindMethodRecursively(Type type, string methodName, Type[] types = null) + { + throw new NotImplementedException(); + } + + public PropertyInfo FindPropertyRecursively(Type type, string propertyName) + { + throw new NotImplementedException(); + } + + public ImmutableArray GetScriptResources() + { + throw new System.NotImplementedException(); + } + + public bool HasMember(object obj, string memberName) + { + throw new NotImplementedException(); + } + + public bool IsRegistered(Type type) + { + throw new NotImplementedException(); + } + + public bool IsTargetType(object obj, string typeName) + { + throw new NotImplementedException(); + } + + public void MakeFieldAccessible(IUserDataDescriptor descriptor, string fieldName) + { + throw new NotImplementedException(); + } + + public void MakeMethodAccessible(IUserDataDescriptor descriptor, string methodName, string[] parameters = null) + { + throw new NotImplementedException(); + } + + public void MakePropertyAccessible(IUserDataDescriptor descriptor, string propertyName) + { + throw new NotImplementedException(); + } + + public IUserDataDescriptor RegisterGenericType(Type type) + { + throw new NotImplementedException(); + } + + public IUserDataDescriptor RegisterGenericType(string typeName, params string[] typeNameArgs) + { + throw new NotImplementedException(); + } + + public IUserDataDescriptor RegisterType(Type type) + { + throw new NotImplementedException(); + } + + public IUserDataDescriptor RegisterType(string typeName) + { + throw new NotImplementedException(); + } + + public void RemoveMember(IUserDataDescriptor descriptor, string memberName) + { + throw new NotImplementedException(); + } + + public void RemoveScriptFiles(ImmutableArray luaResource) + { + throw new System.NotImplementedException(); + } + + public FluentResults.Result Reset() + { + throw new System.NotImplementedException(); + } + + public string TypeOf(object obj) + { + throw new NotImplementedException(); + } + + public void UnregisterAllTypes() + { + throw new NotImplementedException(); + } + + public void UnregisterType(Type type) + { + throw new NotImplementedException(); + } + + public void UnregisterType(string typeName) + { + throw new NotImplementedException(); + } } diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/NetworkingService.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/NetworkingService.cs new file mode 100644 index 000000000..971661eec --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/NetworkingService.cs @@ -0,0 +1,128 @@ +using Barotrauma.LuaCs.Services; +using Barotrauma.Networking; +using System; +using System.Collections.Generic; + +namespace Barotrauma.LuaCs.Networking; + +internal partial class NetworkingService : INetworkingService +{ + private enum LuaCsClientToServer + { + NetMessageId, + NetMessageString, + RequestSingleId, + RequestAllIds, + } + + private enum LuaCsServerToClient + { + NetMessageId, + NetMessageString, + ReceiveIds + } + + private Dictionary netVars = new Dictionary(); + private Dictionary netReceives = new Dictionary(); + private Dictionary packetToId = new Dictionary(); + private Dictionary idToPacket = new Dictionary(); + + public bool IsActive + { + get + { + return GameMain.NetworkMember != null; // ehh? + } + } + public bool IsSynchronized { get; private set; } + public bool IsDisposed { get; private set; } + + public void Initialize() + { +#if SERVER + IsSynchronized = true; +#elif CLIENT + SendSyncMessage(); +#endif + } + + public void RegisterNetVar(INetVar netVar) + { + netVars[netVar.InstanceId] = netVar; + + netReceives[netVar.InstanceId] = (IReadMessage netMessage) => + { + INetReadMessage internalMind = new NetReadMessage(); + internalMind.SetMessage(netMessage); + netVar.ReadNetMessage(internalMind); + }; + } + + public void SendNetVar(INetVar netVar) + { + if (netVars.ContainsKey(netVar.InstanceId)) + { + INetWriteMessage message = Start(netVar.InstanceId); + netVar.WriteNetMessage(message); + Send(message.Message); + } + } + + public void Receive(Guid netId, NetMessageReceived callback) + { +#if SERVER + RegisterId(netId); +#elif CLIENT + RequestId(netId); +#endif + netReceives[netId] = callback; + } + + private void HandleNetMessage(IReadMessage netMessage, Guid netId, Client client = null) + { + if (netReceives.ContainsKey(netId)) + { + try + { + netReceives[netId](netMessage); + } + catch (Exception e) + { + LuaCsLogger.LogError($"Exception thrown inside NetMessageReceive({netId})", LuaCsMessageOrigin.CSharpMod); + LuaCsLogger.HandleException(e, LuaCsMessageOrigin.CSharpMod); + } + } + else + { + if (GameSettings.CurrentConfig.VerboseLogging) + { +#if SERVER + LuaCsLogger.LogError($"Received NetMessage for unknown netid {netId} from {GameServer.ClientLogName(client)}."); +#else + LuaCsLogger.LogError($"Received NetMessage for unknown netid {netId} from server."); +#endif + } + } + } + + private void HandleNetMessageString(IReadMessage netMessage, Client client = null) + { + Guid guid = new Guid(netMessage.ReadBytes(16)); + + HandleNetMessage(netMessage, guid, client); + } + + public FluentResults.Result Reset() + { + IsSynchronized = false; + netReceives = new Dictionary(); + packetToId = new Dictionary(); + idToPacket = new Dictionary(); + return FluentResults.Result.Ok(); + } + + public void Dispose() + { + IsDisposed = true; + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/PackageManagementService.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/PackageManagementService.cs index 2f5d5eb6f..8fda053e1 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/PackageManagementService.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/PackageManagementService.cs @@ -1,14 +1,41 @@ using System; +using System.Collections.Concurrent; using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; +using Barotrauma.Extensions; using Barotrauma.LuaCs.Data; +using Barotrauma.Steam; +using FluentResults; +using FluentResults.LuaCs; +using QuikGraph; namespace Barotrauma.LuaCs.Services; -public class PackageManagementService : IPackageManagementService, IPluginManagementService +public class PackageManagementService : IPackageManagementService { private readonly Func _contentPackageServiceFactory; private readonly Lazy _assemblyManagementService; + private readonly ConcurrentDictionary _contentPackages = new(); + private readonly ConcurrentQueue _queuedPackages = new(); + private readonly ConcurrentDictionary _packageDependencyInfos = new(); + /// + /// ConcurrentDictionary handles access/read synchronization. This is to ensure that we are not trying to + /// access the collection during a load/unload/modify operation. + /// + private readonly ReaderWriterLockSlim _contentPackagesModificationsLock = new(); + /// + /// This lock ensures that we are not adding new entries to the queue between when we read the contents and + /// empty the buffer. + /// + private readonly ReaderWriterLockSlim _packageQueueProcessingLock = new(); + public PackageManagementService( Func getPackageService, Lazy assemblyManagementService) @@ -17,55 +44,418 @@ public class PackageManagementService : IPackageManagementService, IPluginManage this._assemblyManagementService = assemblyManagementService; } + #region STATE_RESET public void Dispose() { // TODO release managed resources here } - public void Reset() + public FluentResults.Result Reset() { throw new NotImplementedException(); } - public bool IsAssemblyLoadedGlobal(string friendlyName) + #endregion + + public void QueuePackages(ImmutableArray packages) + { + _packageQueueProcessingLock.EnterReadLock(); + try + { + foreach (LoadablePackage package in packages) + _queuedPackages.Enqueue(package); + } + finally + { + _packageQueueProcessingLock.ExitReadLock(); + } + } + + public FluentResults.Result ParseQueuedPackages(bool loadParallel = true, bool reportFailOnDuplicates = false) + { + if (!ModUtils.Environment.IsMainThread) + throw new InvalidOperationException($"{nameof(ParseQueuedPackages)}: This method can only be called on the main thread."); + + ImmutableArray packagesToProcess = ImmutableArray.Empty; + + _packageQueueProcessingLock.EnterWriteLock(); + try + { + Interlocked.MemoryBarrier(); + if (_queuedPackages.IsEmpty) + return FluentResults.Result.Ok().WithSuccess($"{nameof(ParseQueuedPackages)}: The Queue is empty."); + packagesToProcess = _queuedPackages.Where(p => p.Package is not null) + .Distinct().ToImmutableArray(); + _queuedPackages.Clear(); + } + finally + { + _packageQueueProcessingLock.ExitWriteLock(); + } + + FluentResults.Result[] loadResults = new FluentResults.Result[packagesToProcess.Length]; + FluentResults.Result res = new FluentResults.Result(); + + // Load ModConfigInfo + _contentPackagesModificationsLock.EnterWriteLock(); + try + { + Stopwatch stopwatch = new Stopwatch(); + stopwatch.Start(); + + Interlocked.MemoryBarrier(); + if (loadParallel) + { + Parallel.For(0, loadResults.Length, new ParallelOptions() + { + /* + * This is an IO-bound operation. The purpose of parallelism here is to allow loaded package + * data to be processed while another package is waiting on the storage device for its info. + */ + MaxDegreeOfParallelism = 2 + },i => + { + loadResults[i] = LoadPackageInfo(packagesToProcess[i]); + }); + } + else + { + for (int i = 0; i < loadResults.Length; i++) + { + loadResults[i] = LoadPackageInfo(packagesToProcess[i]); + } + } + + stopwatch.Stop(); + + res.WithSuccess(new Success( + $"Completed parsing of {loadResults.Length} packages in {stopwatch.ElapsedMilliseconds} milliseconds.")); + + for (int i = 0; i < loadResults.Length; i++) + { + res = loadResults[i].IsSuccess + ? res.WithSuccesses(loadResults[i].Successes) + : res.WithErrors(loadResults[i].Errors); + } + + return res; + } + catch (AggregateException ae) + { + return FluentResults.Result.Fail(new Error($"{nameof(ParseQueuedPackages)}: Failed to load packages! AE.") + .WithMetadata(MetadataType.ExceptionDetails, ae.InnerException?.Message ?? ae.Message) + .WithMetadata(MetadataType.StackTrace, ae.StackTrace) + .WithMetadata(MetadataType.ExceptionObject, this)); + } + catch (ArgumentNullException ane) + { + return FluentResults.Result.Fail( + new Error($"{nameof(ParseQueuedPackages)}: Failed to load packages! ANE.") + .WithMetadata(MetadataType.ExceptionDetails, ane.InnerException?.Message ?? ane.Message) + .WithMetadata(MetadataType.StackTrace, ane.StackTrace) + .WithMetadata(MetadataType.ExceptionObject, this)); + } + finally + { + _contentPackagesModificationsLock.ExitWriteLock(); + } + + + /* + * Helper functions + */ + + // register in the list so we can check against it. + FluentResults.Result LoadPackageInfo(LoadablePackage package) + { + try + { + if (package.Package == null) + { + return FluentResults.Result.Fail( + new Error($"{nameof(LoadPackageInfo)}: Package is null!") + .WithMetadata(MetadataType.ExceptionObject, this) + .WithMetadata(MetadataType.RootObject, package)); + } + + if (_contentPackages.TryGetValue(package.Package, out var packageService)) + { + if (reportFailOnDuplicates) + { + return FluentResults.Result.Fail(new Error($"The package {package.Package?.Name} is already loaded.") + .WithMetadata(MetadataType.ExceptionObject, this) + .WithMetadata(MetadataType.RootObject, package.Package)); + } + + return FluentResults.Result.Ok(); + } + + packageService = _contentPackageServiceFactory.Invoke(); + _contentPackages[package.Package] = packageService; + return packageService.LoadResourcesInfo(package); + } + catch (NullReferenceException nre) + { + return FluentResults.Result.Fail(new Error($"{nameof(LoadPackageInfo)}: NRE while loading package {package.Package?.Name}!") + .WithMetadata(MetadataType.ExceptionObject, this) + .WithMetadata(MetadataType.StackTrace, nre.StackTrace ?? "StackTrace not available") + .WithMetadata(MetadataType.ExceptionDetails, nre.InnerException?.Message ?? nre.Message) + .WithMetadata(MetadataType.RootObject, package)); + } + } + } + + public FluentResults.Result LoadPackageConfigsResourcesGroup(bool loadParallel = true) { throw new NotImplementedException(); } - public void AddPackages(ref ReadOnlySpan<(ContentPackage, bool)> packages, bool executeImmediately = false, bool errorOnFailures = false, - bool errorOnExistingPackageFound = false) + public FluentResults.Result LoadAllPackageResources(bool loadParallel = true, bool safeResourcesOnly = true) { throw new NotImplementedException(); } - public void LoadPackages(bool onlyUnloadedPackages = true, bool rescanPackages = false) + public FluentResults.Result UnloadPackages() { + if (!ModUtils.Environment.IsMainThread) + { + return FluentResults.Result.Fail( + new ExceptionalError(new InvalidOperationException($"{nameof(UnloadPackages)}: This method can only be called on the main thread.")) + .WithMetadata(MetadataType.ExceptionObject, this)); + } + + var res = new FluentResults.Result(); + _contentPackagesModificationsLock.EnterWriteLock(); + try + { + // TODO: Finish him + } + finally + { + _contentPackagesModificationsLock.ExitWriteLock(); + } + throw new NotImplementedException(); } - public void UnloadPackages(bool errorOnFailures = true) - { - throw new NotImplementedException(); - } + public bool IsPackageLoaded(ContentPackage package) => package is not null && _contentPackages.ContainsKey(package); - public bool IsPackageLoaded(ContentPackage package) - { - throw new NotImplementedException(); - } + public bool CheckDependencyLoaded(IPackageDependencyInfo info) => + info is not null && IsPackageLoaded(info.DependencyPackage); - public bool CheckDependencyLoaded(IPackageDependencyInfo info) + public bool CheckDependenciesLoaded([NotNull]IEnumerable infos, out ImmutableArray missingPackages) { - throw new NotImplementedException(); + var missing = ImmutableArray.CreateBuilder(); + missing.AddRange(infos + .Where(i => i.DependencyPackage is not null) + .DistinctBy(i => i.DependencyPackage) + .Where(i => !CheckDependencyLoaded(i))); + missingPackages = missing.MoveToImmutable(); + return missingPackages.Length == 0; } - - public bool CheckDependenciesLoaded(IEnumerable infos, out IReadOnlyList missingPackages) - { - throw new NotImplementedException(); - } - + public bool CheckEnvironmentSupported(IPlatformInfo platform) { + return (platform.SupportedPlatforms & ModUtils.Environment.CurrentPlatform) > 0 + && (platform.SupportedTargets & ModUtils.Environment.CurrentTarget) > 0; + } + + public Result GetPackageDependencyInfoRecord(ContentPackage package, bool addIfMissing = false) + { + if (package is null) + { + return new FluentResults.Result() + .WithError(new Error($"{nameof(GetPackageDependencyInfoRecord)}: Package is null!") + .WithMetadata(MetadataType.ExceptionObject, this)); + } + + if (_packageDependencyInfos.TryGetValue(package, out var result)) + { + return new FluentResults.Result() + .WithValue(result); + } + + if (addIfMissing) + { + return AddDependencyRecord(package, package.Name, package.Path, + package.TryExtractSteamWorkshopId(out var id) ? id.Value : 0, + false); + } + + return FluentResults.Result.Fail(new Error($"Could not find package {package.Name}!") + .WithMetadata(MetadataType.ExceptionObject, this) + .WithMetadata(MetadataType.RootObject, package)); + } + + public Result GetPackageDependencyInfoRecord(ulong steamWorkshopId, string packageName, string folderPath = null, + bool addIfMissing = false) + { + if (packageName.IsNullOrWhiteSpace() || folderPath.IsNullOrWhiteSpace()) + { + return new FluentResults.Result() + .WithError(new Error($"{nameof(GetPackageDependencyInfoRecord)}: folder path and/or package name are null!") + .WithMetadata(MetadataType.ExceptionObject, this)); + } + + if (_packageDependencyInfos.TryGetValue((packageName,steamWorkshopId,folderPath), out var result)) + { + return new FluentResults.Result() + .WithValue(result); + } + + // TODO: Finish this throw new NotImplementedException(); } + + public Result GetPackageDependencyInfoRecord(string folderPath) + { + throw new NotImplementedException(); + } + + + public IPackageDependencyInfo CreateOrphanPackageDependencyInfoRecord( + string packageName, + string packagePath, + ulong steamWorkshopId) + { + return new DependencyInfo() + { + DependencyPackage = null, + FallbackPackageName = packageName, + FolderPath = packagePath.IsNullOrWhiteSpace() ? null : System.IO.Path.GetFullPath(packagePath), + SteamWorkshopId = steamWorkshopId, + IsMissing = true, + IsWorkshopInstallation = false + }; + } + + private Result AddDependencyRecord( + ContentPackage package, + string packageName, + string folderPath, + ulong steamWorkshopId, + bool isMissing) + { + // TODO: Redo + try + { + var dependencyInfo = new DependencyInfo() + { + DependencyPackage = package, + FallbackPackageName = packageName, + FolderPath = System.IO.Path.GetFullPath(folderPath), + SteamWorkshopId = steamWorkshopId, + IsMissing = isMissing, + IsWorkshopInstallation = steamWorkshopId != 0 + }; + if (package is not null) + { + _packageDependencyInfos.AddOrUpdate(package, pack => dependencyInfo, + (pack, dep) => dependencyInfo); + } + return new FluentResults.Result() + .WithValue(dependencyInfo) + .WithSuccess($"New value created."); + } + catch (Exception ex) + { + return new FluentResults.Result() + .WithError(new ExceptionalError(ex) + .WithMetadata(MetadataType.ExceptionObject, this) + .WithMetadata(MetadataType.ExceptionDetails, ex.Message) + .WithMetadata(MetadataType.RootObject, package) + .WithMetadata(MetadataType.StackTrace, ex.StackTrace ?? "StackTrace not available")); + } + } + + private readonly record struct DependencyEntryKey : IEqualityComparer, IEquatable + { + public ContentPackage Package { get; init; } + public string FolderPath { get; init; } + public string PackageName { get; init; } + public ulong SteamWorkshopId { get; init; } + + public DependencyEntryKey(ContentPackage package) + { + Package = package ?? throw new ArgumentNullException(nameof(package), $"{nameof(DependencyEntryKey)}.ctor: Package cannot be null!"); + PackageName = package.Name; + SteamWorkshopId = package.TryExtractSteamWorkshopId(out var id) ? id.Value : (ulong)0; + FolderPath = package.Path; + } + + public DependencyEntryKey(string packageName, string folderPath, ulong steamWorkshopId) + { + PackageName = packageName; + SteamWorkshopId = steamWorkshopId; + FolderPath = folderPath; + Package = null; + } + + public DependencyEntryKey(string packageName, ulong steamWorkshopId) + { + PackageName = packageName; + SteamWorkshopId = steamWorkshopId; + FolderPath = null; + Package = null; + } + + public bool Equals(DependencyEntryKey other) + { + return Equals(this, other); + } + + public override int GetHashCode() + { + return GetHashCode(this); + } + + public bool Equals(DependencyEntryKey x, DependencyEntryKey y) + { + if (x == y) + return true; + + if (x.Package is not null && y.Package is not null && x.Package == Package) + return true; + + // folder should be a unique key if not unset. + if (!x.FolderPath.IsNullOrWhiteSpace() && !y.FolderPath.IsNullOrWhiteSpace() && + x.FolderPath == FolderPath) + return true; + + if (!x.PackageName.IsNullOrWhiteSpace() && !y.PackageName.IsNullOrWhiteSpace() + && x.SteamWorkshopId != 0 && y.SteamWorkshopId != 0) + return x.PackageName == y.PackageName && x.SteamWorkshopId == y.SteamWorkshopId; + + if (!x.PackageName.IsNullOrWhiteSpace() && !y.PackageName.IsNullOrWhiteSpace() && x.PackageName == PackageName) + return true; + + if (x.SteamWorkshopId != 0 && y.SteamWorkshopId != 0 && + x.SteamWorkshopId == y.SteamWorkshopId) + return true; + + return false; + } + + public int GetHashCode(DependencyEntryKey obj) + { + if (!obj.PackageName.IsNullOrWhiteSpace()) + return obj.PackageName.GetHashCode(); + if (obj.SteamWorkshopId != 0) + return obj.SteamWorkshopId.GetHashCode(); + if (obj.Package is not null) + return obj.Package.GetHashCode(); + // We don't want to check the FolderPath because we want to resolve dependencies using packages + // that might be local instead in the workshop folder. + return 2342568; // random const value: collisions are fine as we want to call Equals() + } + + public static implicit operator DependencyEntryKey(ContentPackage package) => new(package); + public static implicit operator DependencyEntryKey((string packageName, ulong steamWorkshopId) tuple1) => + new (tuple1.packageName, tuple1.steamWorkshopId); + public static implicit operator DependencyEntryKey((string packageName, ulong steamWorkshopId, string folderPath) tuple1) => + new (tuple1.packageName, tuple1.folderPath, tuple1.steamWorkshopId); + } + + } diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/PackageService.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/PackageService.cs index 97e1f0cfe..219685d5b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/PackageService.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/PackageService.cs @@ -10,6 +10,9 @@ using System.Threading; using Barotrauma.Extensions; using Barotrauma.LuaCs.Data; using Barotrauma.LuaCs.Services.Processing; +using FluentResults; +using FluentResults.LuaCs; +using OneOf; namespace Barotrauma.LuaCs.Services; @@ -20,8 +23,7 @@ public partial class PackageService : IPackageService // mod config / package scanners/parsers - private readonly Lazy _modConfigConverterService; - private readonly Lazy _legacyConfigService; + private readonly Lazy _configParserService; private readonly Lazy _luaScriptService; private readonly Lazy _localizationService; private readonly Lazy _pluginService; @@ -35,31 +37,32 @@ public partial class PackageService : IPackageService // state monitors private int _configsLoaded, _localizationsLoaded, _luaScriptsLoaded, _pluginsLoaded, _isDisposed; private int _loadingOperationsRunning; + private int _isEnabledInModList; public bool ConfigsLoaded { - get => GetThreadSafeBool(ref _configsLoaded); - private set => SetThreadSafeBool(ref _configsLoaded, value); + get => ModUtils.Threading.GetBool(ref _configsLoaded); + private set => ModUtils.Threading.SetBool(ref _configsLoaded, value); } public bool LocalizationsLoaded { - get => GetThreadSafeBool(ref _localizationsLoaded); - private set => SetThreadSafeBool(ref _localizationsLoaded, value); + get => ModUtils.Threading.GetBool(ref _localizationsLoaded); + private set => ModUtils.Threading.SetBool(ref _localizationsLoaded, value); } public bool LuaScriptsLoaded { - get => GetThreadSafeBool(ref _luaScriptsLoaded); - private set => SetThreadSafeBool(ref _luaScriptsLoaded, value); + get => ModUtils.Threading.GetBool(ref _luaScriptsLoaded); + private set => ModUtils.Threading.SetBool(ref _luaScriptsLoaded, value); } public bool PluginsLoaded { - get => GetThreadSafeBool(ref _pluginsLoaded); - private set => SetThreadSafeBool(ref _pluginsLoaded, value); + get => ModUtils.Threading.GetBool(ref _pluginsLoaded); + private set => ModUtils.Threading.SetBool(ref _pluginsLoaded, value); } public bool IsDisposed { - get => GetThreadSafeBool(ref _isDisposed); - private set => SetThreadSafeBool(ref _isDisposed, value); + get => ModUtils.Threading.GetBool(ref _isDisposed); + private set => ModUtils.Threading.SetBool(ref _isDisposed, value); } private bool LoadingOperationsRunning @@ -146,6 +149,12 @@ public partial class PackageService : IPackageService } } + public bool IsEnabledInModList + { + get => ModUtils.Threading.GetBool(ref _isEnabledInModList); + private set => ModUtils.Threading.SetBool(ref _isEnabledInModList, value); + } + #endregion public ImmutableArray SupportedCultures => ModConfigInfo?.SupportedCultures ?? ImmutableArray.Empty; @@ -159,43 +168,48 @@ public partial class PackageService : IPackageService #region PublicAPI - public bool TryLoadResourcesInfo(ContentPackage package) + public FluentResults.Result LoadResourcesInfo(LoadablePackage cpackage) { + if (cpackage.Package == null) + { + return FluentResults.Result.Fail(new Error($"{nameof(LoadResourcesInfo)}: Package is null!") + .WithMetadata(MetadataType.ExceptionObject,this) + .WithMetadata(MetadataType.RootObject, cpackage)); + } + ContentPackage package = cpackage.Package; + _operationsUsageLock.EnterWriteLock(); LoadingOperationsRunning = true; try { if (IsDisposed) { - throw new ObjectDisposedException($"This package service instance is disposed!"); + return FluentResults.Result.Fail( + new Error("Service is disposed.") + .WithMetadata(MetadataType.ExceptionObject, this) + .WithMetadata(MetadataType.RootObject, package)); } - // try loading the ModConfig.xml. If it fails, use the Legacy loader to try and construct one from the package structure. - if (_storageService.TryLoadPackageXml(package, "ModConfig.xml", out var configXml) - && configXml.Root is not null) + var res = _configParserService.Value.BuildConfigForPackage(package); + + if (res.IsFailed) { - if (_modConfigConverterService.Value.TryParseResource(configXml.Root, out IModConfigInfo configInfo)) - { - ModConfigInfo = configInfo; - } - else - { - _loggerService.LogError( - $"Failed to parse ModConfig.xml for package {package.Name}, package mod content not loaded."); - return false; - } - } - else if (_legacyConfigService.Value.TryBuildModConfigFromLegacy(package, out var legacyConfig)) - { - ModConfigInfo = legacyConfig; - } - else - { - // vanilla mod or broken - return false; + return FluentResults.Result.Fail(res.Errors) + .WithError(new Error("PackageService failed to load ModConfigInfo") + .WithMetadata(MetadataType.ExceptionObject, _configParserService) + .WithMetadata(MetadataType.RootObject, package)); } - return true; + this.ModConfigInfo = res.Value; + this.IsEnabledInModList = cpackage.IsEnabled; + return FluentResults.Result.Ok(); + } + catch (Exception e) + { + return FluentResults.Result.Fail(new Error(e.Message) + .WithMetadata(MetadataType.ExceptionObject, this) + .WithMetadata(MetadataType.RootObject, package) + .WithMetadata(MetadataType.StackTrace, e.StackTrace)); } finally { @@ -204,31 +218,19 @@ public partial class PackageService : IPackageService } } - public void LoadPlugins([NotNull]IAssembliesResourcesInfo assembliesInfo, bool ignoreDependencySorting = false) + public FluentResults.Result LoadPlugins([NotNull]IAssembliesResourcesInfo assembliesInfo, bool ignoreDependencySorting = false) { _operationsUsageLock.EnterReadLock(); LoadingOperationsRunning = true; try { - if (IsDisposed) + if (CheckResourceSanitation(OneOf + .FromT0(assembliesInfo)) is { IsFailed: true } failed) { - throw new ObjectDisposedException($"This package service instance is disposed!"); + return failed; } - SanitationChecksCore(assembliesInfo, "assemblies", nameof(LoadPlugins)); - SanitationChecksEnumerable(assembliesInfo.Assemblies, "assemblies", nameof(LoadPlugins)); - -#if DEBUG - assembliesInfo.Assemblies.ForEach(ari => - { - if (!this.Assemblies.Contains(ari)) - { - throw new ArgumentException( - $"Package Service: tried to load the assembly resource {ari.InternalName} for package {this.Package.Name} but it is not in the list for this package."); - } - }); -#endif - // Order these assemblies by internal dependencies ImmutableArray resources; if (ignoreDependencySorting) @@ -243,12 +245,15 @@ public partial class PackageService : IPackageService } // Try loading them, throw on failure. - if (!_pluginService.Value.TryLoadAndInstanceTypes(resources, true, out var instancedTypes)) + if (_pluginService.Value.LoadAndInstanceTypes(resources, true, out var instancedTypes) is { IsFailed: true} failed2) { - throw new TypeLoadException($"PackageService: unable to load assemblies for package {this.Package.Name}! Aborting loading!"); + return failed2.WithError(new Error($"{nameof(LoadPlugins)}: Failed to load plugins for {this.Package.Name}") + .WithMetadata(MetadataType.ExceptionObject, this) + .WithMetadata(MetadataType.RootObject, assembliesInfo)); } PluginsLoaded = true; + return FluentResults.Result.Ok(); } finally { @@ -257,37 +262,28 @@ public partial class PackageService : IPackageService } } - public void LoadLocalizations([NotNull]ILocalizationsResourcesInfo localizationsInfo) + public FluentResults.Result LoadLocalizations([NotNull]ILocalizationsResourcesInfo localizationsInfo) { _operationsUsageLock.EnterReadLock(); LoadingOperationsRunning = true; try { - if (IsDisposed) + if (CheckResourceSanitation(OneOf + .FromT1(localizationsInfo)) is { IsFailed: true } failed) { - throw new ObjectDisposedException($"This package service instance is disposed!"); + return failed; } - SanitationChecksCore(localizationsInfo, "localizations", nameof(LoadLocalizations)); - SanitationChecksEnumerable(localizationsInfo.Localizations, "localizations", nameof(LoadLocalizations)); - -#if DEBUG - localizationsInfo.Localizations.ForEach(ri => + if (_localizationService.Value.LoadLocalizations(localizationsInfo.Localizations) is { IsFailed: true} failed2) { - if (!this.Localizations.Contains(ri)) - { - throw new ArgumentException( - $"Package Service: tried to load the localization resource for package {this.Package.Name} but it is not in the list for this package."); - } - }); -#endif - - if (!_localizationService.Value.TryLoadLocalizations(localizationsInfo.Localizations)) - { - throw new FileLoadException($"Package Service: unable to load localizations for package {this.Package.Name}! Aborting!"); + return failed2.WithError(new Error($"{nameof(LoadLocalizations)}: Failed to load localizations") + .WithMetadata(MetadataType.ExceptionObject, this) + .WithMetadata(MetadataType.RootObject, localizationsInfo)); } LocalizationsLoaded = true; + return FluentResults.Result.Ok(); } finally { @@ -296,38 +292,28 @@ public partial class PackageService : IPackageService } } - public void AddLuaScripts([NotNull]ILuaScriptsResourcesInfo luaScriptsInfo) + public FluentResults.Result AddLuaScripts([NotNull]ILuaScriptsResourcesInfo luaScriptsInfo) { _operationsUsageLock.EnterReadLock(); LoadingOperationsRunning = true; try { - if (IsDisposed) + if (CheckResourceSanitation(OneOf + .FromT4(luaScriptsInfo)) is { IsFailed: true } failed) { - throw new ObjectDisposedException($"This package service instance is disposed!"); + return failed; } - SanitationChecksCore(luaScriptsInfo, "luaScripts", nameof(AddLuaScripts)); - SanitationChecksEnumerable(luaScriptsInfo.LuaScripts, "luaScripts", nameof(AddLuaScripts)); - -#if DEBUG - luaScriptsInfo.LuaScripts.ForEach(ri => + if (_luaScriptService.Value.AddScriptFiles(luaScriptsInfo.LuaScripts) is { IsFailed: true} failed2) { - if (!this.LuaScripts.Contains(ri)) - { - throw new ArgumentException( - $"Package Service: tried to load the lua script resource for package {this.Package.Name} but it is not in the list for this package."); - } - }); -#endif - - if (!_luaScriptService.Value.TryAddScriptFiles(luaScriptsInfo.LuaScripts)) - { - throw new ArgumentException( - $"Package Service: unable to add lua files for package {this.Package.Name}! Aborting!"); + return failed2.WithError(new Error($"{nameof(LoadLocalizations)}: Failed to load lua scripts.") + .WithMetadata(MetadataType.ExceptionObject, this) + .WithMetadata(MetadataType.RootObject, luaScriptsInfo)); } LuaScriptsLoaded = true; + return FluentResults.Result.Ok(); } finally { @@ -336,7 +322,7 @@ public partial class PackageService : IPackageService } } - public void LoadConfig( + public FluentResults.Result LoadConfig( [NotNull]IConfigsResourcesInfo configsResourcesInfo, [NotNull]IConfigProfilesResourcesInfo configProfilesResourcesInfo) { @@ -344,48 +330,38 @@ public partial class PackageService : IPackageService LoadingOperationsRunning = true; try { - if (IsDisposed) + // register configs + if (CheckResourceSanitation(OneOf + .FromT2(configsResourcesInfo)) is { IsFailed: true } failed) { - throw new ObjectDisposedException($"This package service instance is disposed!"); + return failed; } - SanitationChecksCore(configsResourcesInfo, "config", nameof(LoadConfig)); - SanitationChecksCore(configProfilesResourcesInfo, "config profiles", nameof(LoadConfig)); - SanitationChecksEnumerable(configsResourcesInfo.Configs, "config", nameof(LoadConfig)); - SanitationChecksEnumerable(configProfilesResourcesInfo.ConfigProfiles, "config profiles", nameof(LoadConfig)); - -#if DEBUG - configsResourcesInfo.Configs.ForEach(ri => + if (_configService.Value.AddConfigs(configsResourcesInfo.Configs) is { IsFailed: true} failed2) { - if (!this.Configs.Contains(ri)) - { - throw new ArgumentException( - $"Package Service: tried to load the configs resource for package {this.Package.Name} but it is not in the list for this package."); - } - }); - - configProfilesResourcesInfo.ConfigProfiles.ForEach(ri => - { - if (!this.ConfigProfiles.Contains(ri)) - { - throw new ArgumentException( - $"Package Service: tried to load the localization resource for package {this.Package.Name} but it is not in the list for this package."); - } - }); -#endif - - if (!_configService.Value.TryAddConfigs(configsResourcesInfo.Configs)) - { - throw new ArgumentException( - $"Package Service: unable to add configs for package {this.Package.Name}! Aborting!"); + return failed2.WithError(new Error($"{nameof(LoadLocalizations)}: Failed to load configs.") + .WithMetadata(MetadataType.ExceptionObject, this) + .WithMetadata(MetadataType.RootObject, configsResourcesInfo)); } - if (!_configService.Value.TryAddConfigsProfiles(configProfilesResourcesInfo.ConfigProfiles)) + // register config profiles + if (CheckResourceSanitation(OneOf + .FromT3(configProfilesResourcesInfo)) is { IsFailed: true } failed3) { - throw new ArgumentException( - $"Package Service: unable to add configs profiles for package {this.Package.Name}! Aborting!"); + return failed3; } + + if (_configService.Value.AddConfigsProfiles(configProfilesResourcesInfo.ConfigProfiles) is { IsFailed: true} failed4) + { + return failed4.WithError(new Error($"{nameof(LoadLocalizations)}: Failed to load config profiles.") + .WithMetadata(MetadataType.ExceptionObject, this) + .WithMetadata(MetadataType.RootObject, configProfilesResourcesInfo)); + } + ConfigsLoaded = true; + return FluentResults.Result.Ok(); } finally { @@ -463,22 +439,24 @@ public partial class PackageService : IPackageService } } - public void Reset() + public FluentResults.Result Reset() { _operationsUsageLock.EnterWriteLock(); + try { if (this.Package is null) { - _loggerService.LogError( - $"Package Service: cannot Dispose of service as ContentPackage and info is not set!"); - return; + return FluentResults.Result.Fail(new Error($"Package Service: cannot Dispose of service as ContentPackage and info is not set!") + .WithMetadata(MetadataType.ExceptionDetails, nameof(Reset)) + .WithMetadata(MetadataType.ExceptionObject, this)); } if (this.ModConfigInfo is null) { - _loggerService.LogError($"Package Service: cannot Dispose of service as ModConfigInfo is not loaded!"); - return; + return FluentResults.Result.Fail(new Error($"Package Service: cannot Dispose of service as ModConfigInfo is not set!") + .WithMetadata(MetadataType.ExceptionDetails, nameof(Reset)) + .WithMetadata(MetadataType.ExceptionObject, this)); } Interlocked.MemoryBarrier(); //ensure cache states @@ -491,7 +469,7 @@ public partial class PackageService : IPackageService _operationsUsageLock.EnterWriteLock(); if (timeoutLimit < DateTime.Now) { - _loggerService.LogError($"Package Service: Dispose() time out reached while waiting for other operations. Continuing."); + _loggerService.LogError($"Package Service: Dispose() grace time-out reached while waiting for other operations. Continuing."); break; } } @@ -520,6 +498,7 @@ public partial class PackageService : IPackageService _localizationService.Value.Remove(this.Localizations); LocalizationsLoaded = false; } + return FluentResults.Result.Ok(); } finally { @@ -531,96 +510,176 @@ public partial class PackageService : IPackageService #region INTERNAL - private void SanitationChecksCore(object o, string resTypeInfoName, string callerName) + /// + /// [Thread Unsafe] Performs sanitation and null checks on resources and returns the results. + /// NOTE: Requires that resource locks be set by the caller. + /// + /// + /// + private FluentResults.Result CheckResourceSanitation( + OneOf.OneOf resourcesInfos) { - if (o is null) + // execute checks based on known types + return resourcesInfos.Match( + ass => ChecksDispatcher(ass, nameof(ass.Assemblies), nameof(LoadPlugins), + ass.Assemblies, this.Assemblies), + loc => ChecksDispatcher(loc, nameof(loc.Localizations), nameof(LoadLocalizations), + loc.Localizations, this.Localizations), + cfg => ChecksDispatcher(cfg, nameof(cfg.Configs), nameof(LoadConfig), + cfg.Configs, this.Configs), + cfp => ChecksDispatcher(cfp, nameof(cfp.ConfigProfiles), nameof(LoadConfig), + cfp.ConfigProfiles, this.ConfigProfiles), + lua => ChecksDispatcher(lua, nameof(lua.LuaScripts), nameof(AddLuaScripts), + lua.LuaScripts, this.LuaScripts)); + + + /* + * Helper functions + */ + FluentResults.Result ChecksDispatcher(object obj, string resName, string callerName, + ImmutableArray resList, ImmutableArray compareList) + where T : class, IPackageInfo, IResourceInfo, IResourceCultureInfo, IPackageDependenciesInfo { - _loggerService.LogError($"Package Service: {resTypeInfoName} resources list is null!"); - throw new NullReferenceException($"Package Service: {resTypeInfoName} resources list is null!"); + string errMsg = $"{callerName}: Failed to load {resName}."; + if (DisposeCheck(obj) is { IsFailed: true } failed) + return failed; + if (SanitationChecksCore(obj, resName, callerName) is { IsFailed: true } failed1) + return failed1.WithError(new Error(errMsg)); + if (SanitationChecksEnumerable(resList, resName, callerName) is { IsFailed: true } failed2) + return failed2.WithError(new Error(errMsg)); + if (DebugCheck(resList, compareList, resName) is {IsFailed: true} failed3) + return failed3.WithError(new Error(errMsg)); + return FluentResults.Result.Ok(); + } + + FluentResults.Result DisposeCheck(object obj) + { + if (IsDisposed) + { + return FluentResults.Result.Fail(new Error($"{nameof(PackageService)}: Tried to load resources when disposed.") + .WithMetadata(MetadataType.ExceptionObject, this) + .WithMetadata(MetadataType.RootObject, obj)); + } + return FluentResults.Result.Ok(); + } + + FluentResults.Result DebugCheck(ImmutableArray resList, ImmutableArray compareList, string resName) + where T : class, IPackageInfo + { +#if DEBUG + Stack errors = new(); + resList.ForEach(res => + { + if (!compareList.Contains(res)) + { + errors.Push(new Error($"Failed to load {resName} for: {this.Package.Name}") + .WithMetadata(MetadataType.ExceptionDetails, $"Tries to load {resName} resource {res.InternalName} but it is not from this package!") + .WithMetadata(MetadataType.ExceptionObject, this) + .WithMetadata(MetadataType.RootObject, res)); + } + }); + if (errors.Count > 0) + { + return FluentResults.Result.Fail(errors).WithError( + new Error($"{nameof(LoadPlugins)}: errors in {resName} resources.") + .WithMetadata(MetadataType.ExceptionObject, this) + .WithMetadata(MetadataType.RootObject, this.Package)); + } +#endif + return FluentResults.Result.Ok(); + } + } + + private FluentResults.Result SanitationChecksCore(object obj, string resTypeInfoName, string callerName) + { + Error e = null; + + if (obj is null) + { + e = new Error($"{nameof(SanitationChecksCore)}: null checks failed!") + .WithMetadata(MetadataType.ExceptionDetails, "Object is null!") + .WithMetadata(MetadataType.ExceptionObject, this) + .WithMetadata(MetadataType.Sources, new List() { resTypeInfoName, callerName }); } if (this.Package is null) { - _loggerService.LogError($"Package Service: package not set at {callerName}()!"); - throw new NullReferenceException($"Package Service: package not set at {callerName}()!"); + e = (e ?? new Error($"{nameof(SanitationChecksCore)}: null checks failed!")) + .WithMetadata(MetadataType.ExceptionDetails, "The Package is null!") + .WithMetadata(MetadataType.ExceptionObject, this) + .WithMetadata(MetadataType.Sources, new List() { resTypeInfoName, callerName }); } + + return e is null ? FluentResults.Result.Ok() : FluentResults.Result.Fail(e); } - private void SanitationChecksEnumerable(ImmutableArray resourceInfos, string resTypeInfoName, string callerName) where T : IResourceInfo, IResourceCultureInfo, IPackageInfo, IPackageDependenciesInfo + private FluentResults.Result SanitationChecksEnumerable(ImmutableArray resourceInfos, string resTypeInfoName, string callerName) where T : IResourceInfo, IResourceCultureInfo, IPackageInfo, IPackageDependenciesInfo { // Check if list is empty. Nothing more to do. if (resourceInfos.IsDefaultOrEmpty) - return; + return FluentResults.Result.Ok(); + Stack errors = new(); + // Check if all resources in the list are registered to this package, throw if not. foreach (var resourceInfo in resourceInfos) { // ownership checks if (resourceInfo.OwnerPackage is null) - { - throw new ArgumentException($"Package Service: {resTypeInfoName} info for resource does not have a package name set! Run by {this.Package.Name}."); + { + errors.Push(new Error($"Error for resource: {resTypeInfoName}. OwnerPackage is null!") + .WithMetadata(MetadataType.ExceptionObject, this) + .WithMetadata(MetadataType.RootObject, resourceInfo)); + continue; } if (resourceInfo.OwnerPackage != this.Package) { - throw new ArgumentException( - $"Package Service: {resTypeInfoName} info does not belong to this package! Owned by {resourceInfo.OwnerPackage.Name} but is run by {this.Package.Name}."); + errors.Push(new Error($"Error for resource: {resTypeInfoName}. $\"OwnerPackage {{resourceInfo.OwnerPackage?.Name}} is not the same as this package: {{this.Package}}") + .WithMetadata(MetadataType.ExceptionObject, this) + .WithMetadata(MetadataType.RootObject, resourceInfo)); + continue; } - // Check if external dependencies are loaded and if current environment is supported, throw if not if (resourceInfo.Dependencies.IsDefaultOrEmpty) continue; - - bool resourceMissing = false; - - resourceInfo.Dependencies.ForEach(pdi => + + // ReSharper disable once ForeachCanBePartlyConvertedToQueryUsingAnotherGetEnumerator + foreach (var pdi in resourceInfo.Dependencies) { - // for clarification: assemblies passed to the function should always be loaded. - // optional assemblies should be filtered out before the list is sent. + // for clarification: all resources passed to the function should always be loaded. + // unneeded optional resources should be filtered out before the list is sent. // left this as a reminder :) /*if (pdi.Optional) return;*/ if (!_packageManagementService.CheckDependencyLoaded(pdi)) { - resourceMissing = true; - _loggerService.LogError( - $"Package Service: the following dependency for package {resourceInfo.OwnerPackage.Name} is not loaded: {pdi.DependencyPackage?.Name ?? (pdi.PackageName.IsNullOrWhiteSpace() ? pdi.SteamWorkshopId.ToString() : pdi.PackageName)}"); + errors.Push(new Error($"Dependency missing for resource: {resourceInfo.OwnerPackage.Name}") + .WithMetadata(MetadataType.ExceptionDetails, $"Missing dependency: {pdi.DependencyPackage?.Name ?? (pdi.FallbackPackageName.IsNullOrWhiteSpace() ? pdi.SteamWorkshopId.ToString() : pdi.FallbackPackageName)}") + .WithMetadata(MetadataType.ExceptionObject, this) + .WithMetadata(MetadataType.RootObject, resourceInfo)); } - }); - - if (!resourceMissing) - { - throw new FileLoadException($"Package Service: dependencies for package {resourceInfo.OwnerPackage.Name} are not loaded."); } // check runtime platform if (!_packageManagementService.CheckEnvironmentSupported(resourceInfo)) { - throw new PlatformNotSupportedException($"Package service: the {resTypeInfoName} from {resourceInfo.OwnerPackage.Name} is not supported on this platform."); + errors.Push(new Error($"The resource {resourceInfo.OwnerPackage?.Name} does not support the current platform!") + .WithMetadata(MetadataType.ExceptionObject, this) + .WithMetadata(MetadataType.RootObject, resourceInfo)); } // check local culture if (!_localizationService.Value.IsCurrentCultureSupported(resourceInfo)) { - throw new PlatformNotSupportedException($"Package service: the {resTypeInfoName} from {resourceInfo.OwnerPackage.Name} is not supported in this culture."); + errors.Push(new Error($"The resource {resourceInfo.OwnerPackage?.Name} does not support the current culture/region!") + .WithMetadata(MetadataType.ExceptionObject, this) + .WithMetadata(MetadataType.RootObject, resourceInfo)); } } - } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool GetThreadSafeBool(ref int var) => Interlocked.CompareExchange(ref var, 1, 1) == 1; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void SetThreadSafeBool(ref int var, bool value) - { - if (value) - { - Interlocked.CompareExchange(ref var, 1, 0); - } - else - { - Interlocked.CompareExchange(ref var, 0, 1); - } + return errors.Count > 0 ? FluentResults.Result.Fail(errors) : FluentResults.Result.Ok(); } #endregion diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/PluginManagementService.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/PluginManagementService.cs new file mode 100644 index 000000000..64f36d63f --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/PluginManagementService.cs @@ -0,0 +1,52 @@ +using System.Collections.Immutable; +using Barotrauma.LuaCs.Data; +using FluentResults; +using Microsoft.CodeAnalysis; + +namespace Barotrauma.LuaCs.Services; + +public class PluginManagementService : IPluginManagementService +{ + + + public void Dispose() + { + throw new System.NotImplementedException(); + } + + public FluentResults.Result Reset() + { + throw new System.NotImplementedException(); + } + + public bool IsAssemblyLoadedGlobal(string friendlyName) + { + throw new System.NotImplementedException(); + } + + public Result> GetTypes(ContentPackage package = null, string namespacePrefix = null, bool includeInterfaces = false, + bool includeAbstractTypes = false, bool includeDefaultContext = true, bool includeExplicitAssembliesOnly = false) + { + throw new System.NotImplementedException(); + } + + public ImmutableArray GetStandardMetadataReferences() + { + throw new System.NotImplementedException(); + } + + public ImmutableArray GetPluginMetadataReferences() + { + throw new System.NotImplementedException(); + } + + public Result> GetCachedAssembliesForPackage(ContentPackage package) + { + throw new System.NotImplementedException(); + } + + public Result> LoadAssemblyResources(ImmutableArray resource) + { + throw new System.NotImplementedException(); + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/IEventService.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/PluginService.cs similarity index 61% rename from Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/IEventService.cs rename to Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/PluginService.cs index 3c0caef4e..098fa1954 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/IEventService.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/PluginService.cs @@ -1,6 +1,6 @@ namespace Barotrauma.LuaCs.Services; -public interface IEventService +public class PluginService { } diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/Processing/IConverterServiceDefinitions.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/Processing/IConverterServiceDefinitions.cs index dbb150ff0..0b8c8ad70 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/Processing/IConverterServiceDefinitions.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/Processing/IConverterServiceDefinitions.cs @@ -1,16 +1,17 @@ using System.Collections.Generic; using System.Xml.Linq; using Barotrauma.LuaCs.Data; +using FluentResults; namespace Barotrauma.LuaCs.Services.Processing; #region TypeDef // ReSharper disable once TypeParameterCanBeVariant -public interface IConverterService : IService +public interface IConverterService : IReusableService { - bool TryParseResource(TSrc src, out TOut resources); - bool TryParseResources(IEnumerable sources, out List resources); + Result TryParseResource(TSrc src); + Result TryParseResources(IEnumerable sources); } public interface IXmlResourceConverterService : IConverterService { } diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/Processing/IModConfigParserService.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/Processing/IModConfigParserService.cs new file mode 100644 index 000000000..e90f4d598 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/Processing/IModConfigParserService.cs @@ -0,0 +1,9 @@ +using Barotrauma.LuaCs.Data; + +namespace Barotrauma.LuaCs.Services.Processing; + +public interface IModConfigParserService : IReusableService +{ + FluentResults.Result BuildConfigForPackage(ContentPackage package); + FluentResults.Result BuildConfigFromManifest(string manifestPath); +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/Safe/ILuaConfigService.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/Safe/ILuaConfigService.cs index 2f4004e3b..781827ced 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/Safe/ILuaConfigService.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/Safe/ILuaConfigService.cs @@ -1,4 +1,6 @@ -namespace Barotrauma.LuaCs.Services.Safe; +using Barotrauma.LuaCs.Configuration; + +namespace Barotrauma.LuaCs.Services.Safe; public interface ILuaConfigService : ILuaService { diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/Safe/ILuaEventService.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/Safe/ILuaEventService.cs index 07f38202c..9a2f0d02b 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/Safe/ILuaEventService.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/Safe/ILuaEventService.cs @@ -1,6 +1,30 @@ -namespace Barotrauma.LuaCs.Services.Safe; +using System; +using System.Collections.Generic; +using Barotrauma.LuaCs.Events; +using Barotrauma.LuaCs.Services.Compatibility; -public interface ILuaEventService : ILuaService +namespace Barotrauma.LuaCs.Services.Safe; + +public interface ILuaSafeEventService : ILuaService, ILuaCsHook { - + void Subscribe(string interfaceName, string identifier, IDictionary callbacks); + /// + /// Removes a subscriber from an event that subscribed under the given identifier. + /// + /// + /// + void Remove(string eventName, string identifier); + /// + /// Send an event to all subscribers to an interface. + /// + /// Name of the interface (must be registered with Lua). + /// Execution runner, the subscriber is provided as the first argument in the lua runner. + /// + void PublishLuaEvent(string interfaceName, LuaCsFunc runner); +} + +public interface ILuaEventService : ILuaSafeEventService +{ + public FluentResults.Result RegisterSafeEvent() where T : IEvent; + public FluentResults.Result UnregisterSafeEvent() where T : IEvent; } diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/ServicesProvider.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/ServicesProvider.cs index b06b74940..49d45cb75 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/ServicesProvider.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/ServicesProvider.cs @@ -24,7 +24,7 @@ public class ServicesProvider : IServicesProvider private readonly ReaderWriterLockSlim _serviceLock = new(); - public void RegisterServiceType(ServiceLifetime lifetime, ILifetime lifetimeInstance = null) where TSvcInterface : class, IService where TService : class, IService, TSvcInterface, new() + public void RegisterServiceType(ServiceLifetime lifetime, ILifetime lifetimeInstance = null) where TSvcInterface : class, IService where TService : class, IService, TSvcInterface { if (lifetimeInstance is null) { @@ -50,7 +50,6 @@ public class ServicesProvider : IServicesProvider { _serviceLock.EnterReadLock(); ServiceContainer.Register(lifetimeInstance); - ServiceContainer.Compile(); OnServiceRegistered?.Invoke(typeof(TSvcInterface), typeof(TService)); } finally @@ -60,7 +59,7 @@ public class ServicesProvider : IServicesProvider } public void RegisterServiceType(string name, ServiceLifetime lifetime, - ILifetime lifetimeInstance = null) where TSvcInterface : class, IService where TService : class, IService, TSvcInterface, new() + ILifetime lifetimeInstance = null) where TSvcInterface : class, IService where TService : class, IService, TSvcInterface { if (name.IsNullOrWhiteSpace()) { @@ -91,7 +90,6 @@ public class ServicesProvider : IServicesProvider { _serviceLock.EnterReadLock(); ServiceContainer.Register(name, lifetimeInstance); - ServiceContainer.Compile(); OnServiceRegistered?.Invoke(typeof(TSvcInterface), typeof(TService)); } finally @@ -128,7 +126,7 @@ public class ServicesProvider : IServicesProvider } } - public bool TryGetService(out IService service) where TSvcInterface : class, IService + public bool TryGetService(out TSvcInterface service) where TSvcInterface : class, IService { try { @@ -147,7 +145,7 @@ public class ServicesProvider : IServicesProvider } } - public bool TryGetService(string name, out IService service) where TSvcInterface : class, IService + public bool TryGetService(string name, out TSvcInterface service) where TSvcInterface : class, IService { try { diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/_Interfaces/IAssemblyManagementService.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/_Interfaces/IAssemblyManagementService.cs new file mode 100644 index 000000000..b347a301f --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/_Interfaces/IAssemblyManagementService.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; +using System.Reflection; +using System.Runtime.CompilerServices; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +// ReSharper disable InconsistentNaming + +namespace Barotrauma.LuaCs.Services; + +public interface IAssemblyManagementService : IReusableService +{ + /// + /// Searches for an assembly given it's fully qualified name, while excluding the contexts with the given Guids, if supplied. + /// + /// The fully-qualified assembly name. + /// Guids of excluded contexts. + /// On Success: The assembly.
On Failure: nothing.
+ FluentResults.Result GetLoadedAssembly(string assemblyName, in Guid[] excludedContexts); + /// + /// Searches for an assembly given it's fully qualified name, while excluding the contexts with the given Guids, if supplied. + /// + /// The assembly info. + /// Guids of excluded contexts. + /// On Success: The assembly.
On Failure: nothing.
+ FluentResults.Result GetLoadedAssembly(AssemblyName assemblyName, in Guid[] excludedContexts); + + /// + /// Gets the assembly collection for the BCL and base game assemblies. + /// + /// collection, if any are found. Returns an empty collection otherwise. + ImmutableArray GetDefaultMetadataReferences(); + + /// + /// Gets the assembly collection for all add-in assemblies loaded. + /// + /// collection, if any are found. Returns an empty collection otherwise. + ImmutableArray GetAddInContextsMetadataReferences(); + + /// + /// + /// + ImmutableArray AssemblyLoaderServices { get; } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/_Interfaces/IConfigService.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/_Interfaces/IConfigService.cs new file mode 100644 index 000000000..b40a1da69 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/_Interfaces/IConfigService.cs @@ -0,0 +1,86 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; +using Barotrauma.LuaCs.Configuration; +using Barotrauma.LuaCs.Data; +using Barotrauma.LuaCs.Networking; +using Barotrauma.LuaCs.Services.Safe; +using Barotrauma.Networking; + +namespace Barotrauma.LuaCs.Services; + +public partial interface IConfigService : IReusableService, ILuaConfigService +{ + /* + * Resource Files. + */ + FluentResults.Result AddConfigs(ImmutableArray configResources); + FluentResults.Result AddConfigsProfiles(ImmutableArray configProfileResources); + FluentResults.Result RemoveConfigs(ImmutableArray configResources); + FluentResults.Result RemoveConfigsProfiles(ImmutableArray configProfilesResources); + + + /* + * From resources + */ + FluentResults.Result AddConfigs(ImmutableArray configs); + FluentResults.Result AddConfigsProfiles(ImmutableArray configProfiles); + FluentResults.Result RemoveConfigs(ImmutableArray configs); + FluentResults.Result RemoveConfigsProfiles(ImmutableArray configProfiles); + + /* + * Immediate mode + */ + FluentResults.Result> AddConfigEntry(ContentPackage package, string name, + T defaultValue, + NetSync syncMode = NetSync.None, + ClientPermissions permissions = ClientPermissions.None, + Func valueChangePredicate = null, + Action> onValueChanged = null) where T : IConvertible, IEquatable; + + FluentResults.Result AddConfigList(ContentPackage package, string name, + int defaultIndex, IReadOnlyList values, + NetSync syncMode = NetSync.None, + ClientPermissions permissions = ClientPermissions.None, + Func valueChangePredicate = null, + Action onValueChanged = null); + + FluentResults.Result> AddConfigRangeEntry(ContentPackage package, string name, + T defaultValue, T minValue, T maxValue, + Func, int> getStepCount, + NetSync syncMode = NetSync.None, + ClientPermissions permissions = ClientPermissions.None, + Func valueChangePredicate = null, + Action> onValueChanged = null) where T : IConvertible, IEquatable; + + FluentResults.Result> AddConfigEntry(string packageName, string name, + T defaultValue, + NetSync syncMode = NetSync.None, + ClientPermissions permissions = ClientPermissions.None, + Func valueChangePredicate = null, + Action> onValueChanged = null) where T : IConvertible, IEquatable; + + FluentResults.Result AddConfigList(string packageName, string name, + int defaultIndex, IReadOnlyList values, + NetSync syncMode = NetSync.None, + ClientPermissions permissions = ClientPermissions.None, + Func valueChangePredicate = null, + Action onValueChanged = null); + + FluentResults.Result> AddConfigRangeEntry(string packageName, string name, + T defaultValue, T minValue, T maxValue, + Func, int> getStepCount, + NetSync syncMode = NetSync.None, + ClientPermissions permissions = ClientPermissions.None, + Func valueChangePredicate = null, + Action> onValueChanged = null) where T : IConvertible, IEquatable; + + FluentResults.Result> GetConfigsForPackage(ContentPackage package); + FluentResults.Result> GetConfigsForPackage(string packageName); + IReadOnlyDictionary<(ContentPackage, string), IConfigBase> GetAllConfigs(); + FluentResults.Result GetConfig(ContentPackage package, string name); + FluentResults.Result GetConfig(string packageName, string name); + FluentResults.Result GetConfig(ContentPackage package, string name) where T : IConfigBase; + FluentResults.Result GetConfig(string packageName, string name) where T : IConfigBase; +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/_Interfaces/IEventService.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/_Interfaces/IEventService.cs new file mode 100644 index 000000000..08d612944 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/_Interfaces/IEventService.cs @@ -0,0 +1,45 @@ +using System; +using System.Reflection; +using Barotrauma.LuaCs.Events; +using Barotrauma.LuaCs.Services.Compatibility; +using Barotrauma.LuaCs.Services.Safe; + +namespace Barotrauma.LuaCs.Services; + +public interface IEventService : IReusableService, ILuaEventService +{ + FluentResults.Result SetLegacyLuaRunnerFactory(Func runnerFactory) where T : IEvent; + void RemoveLegacyLuaRunnerFactory() where T : IEvent; + void SetAliasToEvent(string alias) where T : IEvent; + void RemoveEventAlias(string alias); + void RemoveAllEventAliases() where T : IEvent; + /// + /// + /// + /// + /// + /// + FluentResults.Result Subscribe(T subscriber) where T : IEvent; + /// + /// + /// + /// + /// + void Unsubscribe(T subscriber) where T : IEvent; + /// + /// Clears all subscribers for a given event type and removes any registration to the type. + /// + /// The event type. + void ClearAllEventSubscribers() where T : IEvent; + /// + /// Clears all subscribers lists. + /// + void ClearAllSubscribers(); + /// + /// Invokes all alive subscribers of the given event using the provided invocation factory. + /// + /// + /// + /// + FluentResults.Result PublishEvent(Action action) where T : IEvent; +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/_Interfaces/ILocalizationService.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/_Interfaces/ILocalizationService.cs new file mode 100644 index 000000000..26575f720 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/_Interfaces/ILocalizationService.cs @@ -0,0 +1,32 @@ +using System; +using System.Globalization; +using System.Collections.Generic; +using System.Collections.Immutable; +using Barotrauma.LuaCs.Data; + +namespace Barotrauma.LuaCs.Services; + +public interface ILocalizationService : IReusableService +{ + IReadOnlyCollection GetLoadedLocales(); + void Remove(ImmutableArray localizations); + FluentResults.Result SetCurrentCulture(CultureInfo culture); + FluentResults.Result SetCurrentCulture(string cultureName); + FluentResults.Result LoadLocalizations(ImmutableArray localizationResources); + + /// + /// Tries to get a localized string without a fallback. Returns success/failure and associated data. + /// + /// Neutral localization key. + /// + FluentResults.Result GetLocalizedString(string key); + FluentResults.Result GetLocalizedString(string key, CultureInfo targetCulture); + string GetLocalizedString(string key, string fallback); + string GetLocalizedString(string key, string fallback, CultureInfo targetCulture); + FluentResults.Result GetLocalizedStringForPackage(ContentPackage package, string key); + FluentResults.Result GetLocalizedStringForPackage(ContentPackage package, string key, CultureInfo targetCulture); + string GetLocalizedStringForPackage(ContentPackage package, string key, string fallback); + string GetLocalizedStringForPackage(ContentPackage package, string key, string fallback, CultureInfo targetCulture); + FluentResults.Result RegisterLocalizationResolver(CultureInfo targetCulture, Func factoryResolver); + bool IsCurrentCultureSupported(IResourceCultureInfo culturesInfo); +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/ILoggerService.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/_Interfaces/ILoggerService.cs similarity index 85% rename from Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/ILoggerService.cs rename to Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/_Interfaces/ILoggerService.cs index eb457b7d3..e1b0805e5 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/ILoggerService.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/_Interfaces/ILoggerService.cs @@ -1,5 +1,6 @@ using System; using Barotrauma.Networking; +using FluentResults; using Microsoft.Xna.Framework; namespace Barotrauma.LuaCs.Services; @@ -7,14 +8,15 @@ namespace Barotrauma.LuaCs.Services; /// /// Provides console and debug logging services /// -public interface ILoggerService : IService +public interface ILoggerService : IReusableService { void HandleException(Exception exception, string prefix = null); void LogError(string message); void LogWarning(string message); void LogMessage(string message, Color? serverColor = null, Color? clientColor = null); void Log(string message, Color? color = null, ServerLog.MessageType messageType = ServerLog.MessageType.ServerMessage); - + void LogResults(FluentResults.Result result); + #region DebugBuilds void LogDebug(string message, Color? color = null); diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/ILuaScriptService.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/_Interfaces/ILuaScriptService.cs similarity index 79% rename from Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/ILuaScriptService.cs rename to Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/_Interfaces/ILuaScriptService.cs index 6d4a8e673..99f9fa516 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/ILuaScriptService.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/_Interfaces/ILuaScriptService.cs @@ -8,7 +8,7 @@ using MoonSharp.Interpreter.Interop; namespace Barotrauma.LuaCs.Services; -public interface ILuaScriptService : IService +public interface ILuaScriptService : IReusableService { #region Script_File_Collector @@ -17,7 +17,8 @@ public interface ILuaScriptService : IService /// /// /// - bool TryAddScriptFiles(ImmutableArray luaResource); + FluentResults.Result AddScriptFiles(ImmutableArray luaResource); + /// /// Removes the specific resources from the script runner. Important: Does not stop the /// execution of any code related to the files nor guarantee cleanup of resources! @@ -31,19 +32,20 @@ public interface ILuaScriptService : IService /// /// /// - bool TryExecuteScripts(bool pauseExecutionOnScriptError = false, bool verboseLogging = false); + FluentResults.Result ExecuteScripts(bool pauseExecutionOnScriptError = false, bool verboseLogging = false); + ImmutableArray GetScriptResources(); #endregion } -public interface ILuaScriptManagementService : IService +public interface ILuaScriptManagementService : IReusableService { #region Script_File_Execution - bool TryExecuteLoadedScripts(ContentPackage package, bool pauseExecutionOnError = false, bool verboseLogging = false); - bool TryExecuteLoadedScripts(ImmutableArray scripts, bool pauseExecutionOnError = false, bool verboseLogging = false); - bool TryExecuteLoadedScripts(bool pauseExecutionOnError = false, bool verboseLogging = false); + FluentResults.Result ExecuteLoadedScripts(ContentPackage package, bool pauseExecutionOnError = false, bool verboseLogging = false); + FluentResults.Result ExecuteLoadedScripts(ImmutableArray scripts, bool pauseExecutionOnError = false, bool verboseLogging = false); + FluentResults.Result ExecuteLoadedScripts(bool pauseExecutionOnError = false, bool verboseLogging = false); #endregion diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/_Interfaces/INetworkingService.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/_Interfaces/INetworkingService.cs new file mode 100644 index 000000000..e5c94a1d3 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/_Interfaces/INetworkingService.cs @@ -0,0 +1,25 @@ +using System; +using Barotrauma.LuaCs.Data; +using Barotrauma.LuaCs.Networking; +using Barotrauma.LuaCs.Services.Compatibility; +using Barotrauma.Networking; + +namespace Barotrauma.LuaCs.Services; + +internal delegate void NetMessageReceived(IReadMessage netMessage); + +internal interface INetworkingService : IReusableService, ILuaCsNetworking +{ + bool IsActive { get; } + bool IsSynchronized { get; } + + public INetWriteMessage Start(Guid netId); + public void Receive(Guid netId, NetMessageReceived action); +#if SERVER + public void Send(IWriteMessage netMessage, NetworkConnection connection = null, DeliveryMethod deliveryMethod = DeliveryMethod.Reliable); +#elif CLIENT + public void Send(IWriteMessage netMessage, DeliveryMethod deliveryMethod = DeliveryMethod.Reliable); +#endif + public void RegisterNetVar(INetVar netVar); + public void SendNetVar(INetVar netVar); +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/_Interfaces/IPackageManagementService.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/_Interfaces/IPackageManagementService.cs new file mode 100644 index 000000000..397a871b4 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/_Interfaces/IPackageManagementService.cs @@ -0,0 +1,91 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using Barotrauma.Extensions; +using Barotrauma.LuaCs.Data; + +namespace Barotrauma.LuaCs.Services; + +public interface IPackageManagementService : IReusableService +{ + /// + /// Adds packages to the queue of loadable packages without initializing them. + /// + /// + void QueuePackages(ImmutableArray packages); + + /// + /// Generates the ModConfigInfo for all queued packages and adds them to the store. + /// + /// Use multithreaded loading. + /// Whether duplicate packages should be reported as errors. + /// Failure/Success records for each package. + FluentResults.Result ParseQueuedPackages(bool loadParallel = true, bool reportFailOnDuplicates = false); + /// + /// Loads only the localizations, configs, and config profiles for stored packages. + /// + /// + /// + FluentResults.Result LoadPackageConfigsResourcesGroup(bool loadParallel = true); + /// + /// Loads all resources for stored packages. + /// + /// Use multithreaded loading. + /// Only load safe scripting resources, such as Lua. C# plugins disabled. + /// + FluentResults.Result LoadAllPackageResources(bool loadParallel = true, bool safeResourcesOnly = true); + FluentResults.Result UnloadPackages(); + bool IsPackageLoaded(ContentPackage package); + bool CheckDependencyLoaded(IPackageDependencyInfo info); + bool CheckDependenciesLoaded([NotNull]IEnumerable infos, out ImmutableArray missingPackages); + bool CheckEnvironmentSupported(IPlatformInfo platform); + /// + /// Tries to get the package dependency record to refer to that specific package if it exists, optionally create it. + /// + /// ContentPackage reference + /// Register a new IPackageDependencyInfo reference. + /// + FluentResults.Result GetPackageDependencyInfoRecord(ContentPackage package, + bool addIfMissing = false); + /// + /// Tries to get the package dependency record to refer to that specific package if it exists, optionally create it. + /// + /// The Steam Workshop ID, if available, if not enter zero ('0'). + /// The name of the package. + /// The folder path, as formatted in [ContentPackage.Path]. + /// Register a new IPackageDependencyInfo reference. + /// + FluentResults.Result GetPackageDependencyInfoRecord(ulong steamWorkshopId, + string packageName, string folderPath = null, bool addIfMissing = false); + /// + /// Tries to get the package dependency record to refer to that specific package if it exists. + /// Note: This overload does not allow the registration of a new dependency. + /// + /// The folder path, as formatted in [ContentPackage.Path]. + /// + FluentResults.Result GetPackageDependencyInfoRecord(string folderPath); + + IPackageDependencyInfo CreateOrphanPackageDependencyInfoRecord(string packageName, + string packagePath, ulong steamWorkshopId); +} + +public readonly record struct LoadablePackage +{ + public ContentPackage Package { get; } + public bool IsEnabled { get; } + + public LoadablePackage(ContentPackage package, bool isEnabled) + { + Package = package; + IsEnabled = isEnabled; + } + + public static ImmutableArray FromEnumerable(IEnumerable packages, bool isEnabled) + { + var builder = ImmutableArray.CreateBuilder(); + packages.ForEach(p => builder.Add(new LoadablePackage(p, isEnabled))); + return builder.ToImmutable(); + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/IPackageService.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/_Interfaces/IPackageService.cs similarity index 58% rename from Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/IPackageService.cs rename to Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/_Interfaces/IPackageService.cs index 1411cb313..3f1189250 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/IPackageService.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/_Interfaces/IPackageService.cs @@ -3,21 +3,23 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; using Barotrauma.LuaCs.Data; +using FluentResults; namespace Barotrauma.LuaCs.Services; -public interface IPackageService : IService, +public interface IPackageService : IReusableService, // These allow us the pass the IContentPackageService to anything that needs the data without having to directly reference the member IResourceCultureInfo, IAssembliesResourcesInfo, ILocalizationsResourcesInfo, ILuaScriptsResourcesInfo { ContentPackage Package { get; } IModConfigInfo ModConfigInfo { get; } + bool IsEnabledInModList { get; } /// /// Try to load the XML config and resources information from the given package. /// /// - /// Whether the package was parsed without errors and any information was found. Will return false for purely vanilla packages. - bool TryLoadResourcesInfo([NotNull]ContentPackage package); + /// Whether the package was parsed without errors. + FluentResults.Result LoadResourcesInfo([NotNull]LoadablePackage package); /// /// Tries to load all assemblies and instance plugins for the given resources list, regardless whether they're marked as optional and/or lazy load. /// Will sort by load priority unless overriden/bypassed. @@ -25,12 +27,12 @@ public interface IPackageService : IService, /// /// /// Whether loading is successful. Returns true on an empty list. - void LoadPlugins([NotNull]IAssembliesResourcesInfo assembliesInfo, bool ignoreDependencySorting = false); - void LoadLocalizations([NotNull]ILocalizationsResourcesInfo localizationsInfo); - void AddLuaScripts([NotNull]ILuaScriptsResourcesInfo luaScriptsInfo); + FluentResults.Result LoadPlugins([NotNull]IAssembliesResourcesInfo assembliesInfo, bool ignoreDependencySorting = false); + FluentResults.Result LoadLocalizations([NotNull]ILocalizationsResourcesInfo localizationsInfo); + FluentResults.Result AddLuaScripts([NotNull]ILuaScriptsResourcesInfo luaScriptsInfo); #if CLIENT - void LoadStyles([NotNull]IStylesResourcesInfo stylesInfo); + FluentResults.Result LoadStyles([NotNull]IStylesResourcesInfo stylesInfo); #endif - void LoadConfig([NotNull]IConfigsResourcesInfo configsResourcesInfo, [NotNull]IConfigProfilesResourcesInfo configProfilesResourcesInfo); + FluentResults.Result LoadConfig([NotNull]IConfigsResourcesInfo configsResourcesInfo, [NotNull]IConfigProfilesResourcesInfo configProfilesResourcesInfo); } diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/_Interfaces/IPluginManagementService.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/_Interfaces/IPluginManagementService.cs new file mode 100644 index 000000000..7769498e7 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/_Interfaces/IPluginManagementService.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Immutable; +using System.Reflection; +using Barotrauma.LuaCs.Data; +using Microsoft.CodeAnalysis; + +namespace Barotrauma.LuaCs.Services; + +public interface IPluginManagementService : IReusableService +{ + /// + /// Checks if an assembly with either the fully-qualified name globally or a 'friendly name' within loaded plugins + /// with the given name is loaded. + /// + /// + /// + bool IsAssemblyLoadedGlobal(string friendlyName); + + // TODO: Documentation. + FluentResults.Result> GetTypes( + ContentPackage package = null, + string namespacePrefix = null, + bool includeInterfaces = false, + bool includeAbstractTypes = false, + bool includeDefaultContext = true, + bool includeExplicitAssembliesOnly = false); + + /// + /// Gets the assembly MetadataReference collection for the BCL and base game assemblies. + /// + /// + ImmutableArray GetStandardMetadataReferences(); + + /// + /// + /// + /// + ImmutableArray GetPluginMetadataReferences(); + + /// + /// + /// + /// + /// + FluentResults.Result> GetCachedAssembliesForPackage(ContentPackage package); + + /// + /// + /// + /// + /// Success/Failure and list of failed resources, if any. + FluentResults.Result> LoadAssemblyResources(ImmutableArray resource); +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/IPluginService.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/_Interfaces/IPluginService.cs similarity index 74% rename from Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/IPluginService.cs rename to Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/_Interfaces/IPluginService.cs index cbdd9ba74..cfafe8e56 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/IPluginService.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/_Interfaces/IPluginService.cs @@ -6,7 +6,7 @@ using Barotrauma.LuaCs.Data; namespace Barotrauma.LuaCs.Services; -public interface IPluginService : IService +public interface IPluginService : IReusableService { bool IsAssemblyLoaded(string friendlyName); /// @@ -17,21 +17,21 @@ public interface IPluginService : IService /// /// /// - bool TryLoadAndInstanceTypes(IEnumerable assemblyResourcesInfo, bool injectServices, out ImmutableArray typeInstances) where T : class, IAssemblyPlugin; - ImmutableArray GetLoadedPluginTypesInPackage() where T : class, IAssemblyPlugin; + FluentResults.Result LoadAndInstanceTypes(IEnumerable assemblyResourcesInfo, bool injectServices, out ImmutableArray typeInstances) where T : class, IAssemblyPlugin; + FluentResults.Result> GetLoadedPluginTypesInPackage() where T : class, IAssemblyPlugin; /// /// Advances the loading/execution state of the plugin. IMPORTANT: You cannot set the execution state of plugins /// to 'Disposed'. You must instead call the 'DisposePlugins' method. /// /// /// - bool AdvancePluginStates(PluginRunState newState); + FluentResults.Result AdvancePluginStates(PluginRunState newState); /// /// Disposes of all running plugins hosted by the service and releases their references to allow unloading. /// /// Success of the operation. Returns false if any plugin threw errors during disposal. - bool DisposePlugins(); + FluentResults.Result DisposePlugins(); /// /// Gets the current plugin execution state. diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/_Interfaces/IService.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/_Interfaces/IService.cs new file mode 100644 index 000000000..680f51fc2 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/_Interfaces/IService.cs @@ -0,0 +1,29 @@ +using System; + +namespace Barotrauma.LuaCs.Services; + +/// +/// Defines a service that can be reset to it's post-constructor state and reused without needing to be disposed. +/// Intended for persistent services. +/// +public interface IReusableService : IService +{ + /// + /// Returns the service to its original state (post-instantiation). + /// Allows a service instance to be reused without disposing of the instance. + /// + FluentResults.Result Reset(); +} + +/// +/// Base interface inherited by all services. +/// +public interface IService : IDisposable +{ + bool IsDisposed { get; } + public void CheckDisposed() + { + if (IsDisposed) + throw new ObjectDisposedException($"Tried to call method on disposed object '{this.GetType().Name}'!"); + } +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/IServicesProvider.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/_Interfaces/IServicesProvider.cs similarity index 86% rename from Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/IServicesProvider.cs rename to Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/_Interfaces/IServicesProvider.cs index 0304b7da4..8cffa329f 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/IServicesProvider.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/_Interfaces/IServicesProvider.cs @@ -19,7 +19,7 @@ public interface IServicesProvider /// /// /// - void RegisterServiceType(ServiceLifetime lifetime, ILifetime lifetimeInstance = null) where TSvcInterface : class, IService where TService : class, IService, TSvcInterface, new(); + void RegisterServiceType(ServiceLifetime lifetime, ILifetime lifetimeInstance = null) where TSvcInterface : class, IReusableService where TService : class, IReusableService, TSvcInterface; /// /// Registers a type as a service for a given interface that can be requested by name. @@ -29,7 +29,7 @@ public interface IServicesProvider /// /// /// - void RegisterServiceType(string name, ServiceLifetime lifetime, ILifetime lifetimeInstance = null) where TSvcInterface : class, IService where TService : class, IService, TSvcInterface, new(); + void RegisterServiceType(string name, ServiceLifetime lifetime, ILifetime lifetimeInstance = null) where TSvcInterface : class, IReusableService where TService : class, IReusableService, TSvcInterface; /// /// Called whenever a new service type for a given interface is implemented. @@ -61,7 +61,7 @@ public interface IServicesProvider /// /// /// - bool TryGetService(out IService service) where TSvcInterface : class, IService; + bool TryGetService(out TSvcInterface service) where TSvcInterface : class, IReusableService; /// /// Tries to get a service for the given name and interface, returns success/failure. @@ -71,14 +71,14 @@ public interface IServicesProvider /// /// /// - bool TryGetService(string name, out IService service) where TSvcInterface : class, IService; + bool TryGetService(string name, out TSvcInterface service) where TSvcInterface : class, IReusableService; /// /// Called whenever a new service is created/instanced. /// Args[0]: The interface type of the service. /// Args[1]: The instance of the service. /// - event System.Action OnServiceInstanced; + event System.Action OnServiceInstanced; #endregion @@ -89,7 +89,7 @@ public interface IServicesProvider /// /// /// - ImmutableArray GetAllServices() where TSvc : class, IService; + ImmutableArray GetAllServices() where TSvc : class, IReusableService; #endregion diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/_Interfaces/IStorageService.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/_Interfaces/IStorageService.cs new file mode 100644 index 000000000..b43a595ba --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Services/_Interfaces/IStorageService.cs @@ -0,0 +1,42 @@ +using System.Collections.Immutable; +using System.Xml.Linq; + +namespace Barotrauma.LuaCs.Services; + +public interface IStorageService : IReusableService +{ + #region LocalGameData + + FluentResults.Result LoadLocalXml(ContentPackage package, string localFilePath); + FluentResults.Result LoadLocalBinary(ContentPackage package, string localFilePath); + FluentResults.Result LoadLocalText(ContentPackage package, string localFilePath); + FluentResults.Result FileExistsInLocalData(ContentPackage package, string localFilePath); + + #endregion + + #region ContentPackageData + FluentResults.Result LoadPackageXml(ContentPackage package, string localFilePath, out XDocument document); + FluentResults.Result LoadPackageBinary(ContentPackage package, string localFilePath, out byte[] bytes); + FluentResults.Result LoadPackageText(ContentPackage package, string localFilePath, out string text); + + FluentResults.Result> LoadPackageXmlFiles(ContentPackage package, ImmutableArray localFilePath); + FluentResults.Result> TryLoadPackageBinaryFiles(ContentPackage package, ImmutableArray localFilePath); + FluentResults.Result> TryLoadPackageTextFiles(ContentPackage package, ImmutableArray localFilePath); + + FluentResults.Result> FindFilesInPackage(ContentPackage package, string localSubfolder, string regexFilter, bool searchRecursively); + FluentResults.Result FileExistsInPackage(ContentPackage package, string localFilePath); + + #endregion + + #region AbsolutePaths + + FluentResults.Result TryLoadXml(string filePatht); + FluentResults.Result TrySaveXml(string filePath, in XDocument document); + FluentResults.Result TryLoadBinary(string filePath); + FluentResults.Result TrySaveBinary(string filePath, in byte[] bytes); + FluentResults.Result TryLoadText(string filePath); + FluentResults.Result TrySaveText(string filePath, string text); + FluentResults.Result FileExists(string filePath); + + #endregion +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Plugins/ACsMod.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Plugins/ACsMod.cs similarity index 100% rename from Barotrauma/BarotraumaShared/SharedSource/LuaCs/Plugins/ACsMod.cs rename to Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Plugins/ACsMod.cs diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Plugins/ApplicationMode.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Plugins/ApplicationMode.cs similarity index 100% rename from Barotrauma/BarotraumaShared/SharedSource/LuaCs/Plugins/ApplicationMode.cs rename to Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Plugins/ApplicationMode.cs diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Plugins/AssemblyLoader.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Plugins/AssemblyLoader.cs new file mode 100644 index 000000000..1dea21e4a --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Plugins/AssemblyLoader.cs @@ -0,0 +1,428 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; +using System.Dynamic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.Loader; +using System.Threading; +using Barotrauma.LuaCs; +using Barotrauma.LuaCs.Events; +using Microsoft.CodeAnalysis; +using Basic.Reference.Assemblies; +using FluentResults; +using FluentResults.LuaCs; +using LightInject; +using Microsoft.CodeAnalysis.CSharp; +using OneOf; +using Path = Barotrauma.IO.Path; + +[assembly: InternalsVisibleTo(IAssemblyLoaderService.InternalsAwareAssemblyName)] + +namespace Barotrauma.LuaCs.Services; +public sealed class AssemblyLoader : AssemblyLoadContext, IAssemblyLoaderService +{ + public Guid Id { get; init; } + public bool IsReferenceOnlyMode { get; init; } + public bool IsDisposed + { + get => ModUtils.Threading.GetBool(ref _isDisposed); + private set => ModUtils.Threading.SetBool(ref _isDisposed, value); + } + private int _isDisposed; + + //internal + private readonly IAssemblyManagementService _assemblyManagementService; + private readonly IEventService _eventService; + private readonly Action _onUnload; + /// + /// This lock is just to ensure that we do not load while disposing + /// + private readonly ReaderWriterLockSlim _operationsLock = new(LockRecursionPolicy.SupportsRecursion); + private readonly ConcurrentDictionary _dependencyResolvers = new(); + private readonly ConcurrentDictionary _loadedAssemblyData = new(); + + private ThreadLocal _isResolving = new(static()=>false); // cyclic resolution exit + + #region PublicAPI + + public AssemblyLoader(IAssemblyManagementService assemblyManagementService, + IEventService eventService, + Guid id, string name, + bool isReferenceOnlyMode, Action onUnload) + : base(isCollectible: true, name: name) + { + _assemblyManagementService = assemblyManagementService; + _eventService = eventService; + Id = id; + IsReferenceOnlyMode = isReferenceOnlyMode; + _onUnload = onUnload; + if (_onUnload is not null) + { + base.Unloading += OnUnload; + } + + } + + public FluentResults.Result AddDependencyPaths(ImmutableArray paths) + { + if (paths.Length == 0) + return FluentResults.Result.Ok(); + var res = new FluentResults.Result(); + foreach (var path in paths) + { + try + { + var p = Path.GetFullPath(path.CleanUpPath()); + _dependencyResolvers[p] = new AssemblyDependencyResolver(p); + } + catch (Exception ex) + { + return res.WithError(new ExceptionalError(ex) + .WithMetadata(MetadataType.Sources, path)); + } + } + return FluentResults.Result.Ok(); + } + + public FluentResults.Result CompileScriptAssembly( + [NotNull] string assemblyName, + bool compileWithInternalAccess, + ImmutableArray syntaxTrees, + ImmutableArray metadataReferences, + CSharpCompilationOptions compilationOptions = null) + { + if (assemblyName.IsNullOrWhiteSpace()) + { + return new FluentResults.Result().WithError(new Error($"The name provided is null!") + .WithMetadata(MetadataType.ExceptionObject, this) + .WithMetadata(MetadataType.RootObject, syntaxTrees)); + } + + if (_loadedAssemblyData.ContainsKey(assemblyName)) + { + return new FluentResults.Result().WithError(new Error($"The name provided is already assigned to an assembly!") + .WithMetadata(MetadataType.ExceptionObject, this) + .WithMetadata(MetadataType.RootObject, syntaxTrees)); + } + + var compilationAssemblyName = compileWithInternalAccess ? IAssemblyLoaderService.InternalsAwareAssemblyName : assemblyName; + + compilationOptions ??= new CSharpCompilationOptions( + outputKind: OutputKind.DynamicallyLinkedLibrary, + optimizationLevel: OptimizationLevel.Release, + concurrentBuild: true, + reportSuppressedDiagnostics: true, + allowUnsafe: true); + + if (!compileWithInternalAccess) + { + typeof(CSharpCompilationOptions) + .GetProperty("TopLevelBinderFlags", BindingFlags.Instance | BindingFlags.NonPublic) + ?.SetValue(compilationOptions, (uint)1 << 22); + } + + using var asmMemoryStream = new MemoryStream(); + var result = CSharpCompilation.Create(compilationAssemblyName, syntaxTrees, metadataReferences, compilationOptions).Emit(asmMemoryStream); + if (!result.Success) + { + var res = new FluentResults.Result().WithError( + new Error($"Compilation failed for assembly {assemblyName}!")); + var failuresDiag = result.Diagnostics.Where(d => d.IsWarningAsError || d.Severity == DiagnosticSeverity.Error); + foreach (var diag in failuresDiag) + { + res = res.WithError(new Error(diag.GetMessage()) + .WithMetadata(MetadataType.ExceptionObject, this) + .WithMetadata(MetadataType.ExceptionDetails, diag.Descriptor.Description)); + } + return res; + } + + asmMemoryStream.Seek(0, SeekOrigin.Begin); + try + { + var data = new AssemblyData(LoadFromStream(asmMemoryStream), asmMemoryStream.ToArray()); + _loadedAssemblyData[data.Assembly] = data; + return new FluentResults.Result().WithSuccess($"Compiled assembly {assemblyName} successful.").WithValue(data.Assembly); + } + catch (Exception ex) + { + return new FluentResults.Result().WithError(new ExceptionalError(ex)); + } + } + + public FluentResults.Result LoadAssemblyFromFile(string assemblyFilePath, + ImmutableArray additionalDependencyPaths) + { + if (assemblyFilePath.IsNullOrWhiteSpace()) + return new FluentResults.Result().WithError(new Error($"The path provided is null!")); + + if (additionalDependencyPaths.Any()) + { + var r = AddDependencyPaths(additionalDependencyPaths); + if (!r.IsFailed) + { + // we have errors, loading may not work. + return FluentResults.Result.Fail(new Error($"Failed to load dependency paths") + .WithMetadata(MetadataType.ExceptionObject, this) + .WithMetadata(MetadataType.RootObject, assemblyFilePath)) + .WithErrors(r.Errors); + } + } + + string sanitizedFilePath = Path.GetFullPath(assemblyFilePath.CleanUpPath()); + string directoryKey = Path.GetDirectoryName(sanitizedFilePath); + + if (directoryKey is null) + { + return FluentResults.Result.Fail(new Error($"Unable to load assembly: bath file path: {assemblyFilePath}") + .WithMetadata(MetadataType.ExceptionObject, this) + .WithMetadata(MetadataType.RootObject, sanitizedFilePath)); + } + + try + { + var assembly = LoadFromAssemblyPath(sanitizedFilePath); + _loadedAssemblyData[assembly] = new AssemblyData(assembly, sanitizedFilePath); + return new Result().WithSuccess($"Loaded assembly'{assembly.GetName()}'").WithValue(assembly); + } + catch (ArgumentNullException ane) + { + return FluentResults.Result.Fail(new ExceptionalError(ane) + .WithMetadata(MetadataType.ExceptionObject, this) + .WithMetadata(MetadataType.RootObject, assemblyFilePath) + .WithMetadata(MetadataType.ExceptionDetails, ane.Message) + .WithMetadata(MetadataType.StackTrace, ane.StackTrace)); + } + catch (ArgumentException ae) + { + return FluentResults.Result.Fail(new ExceptionalError(ae) + .WithMetadata(MetadataType.ExceptionObject, this) + .WithMetadata(MetadataType.RootObject, assemblyFilePath) + .WithMetadata(MetadataType.ExceptionDetails, ae.Message) + .WithMetadata(MetadataType.StackTrace, ae.StackTrace)); + } + catch (FileLoadException fle) + { + return FluentResults.Result.Fail(new ExceptionalError(fle) + .WithMetadata(MetadataType.ExceptionObject, this) + .WithMetadata(MetadataType.RootObject, assemblyFilePath) + .WithMetadata(MetadataType.ExceptionDetails, fle.Message) + .WithMetadata(MetadataType.StackTrace, fle.StackTrace)); + } + catch (FileNotFoundException fnfe) + { + return FluentResults.Result.Fail(new ExceptionalError(fnfe) + .WithMetadata(MetadataType.ExceptionObject, this) + .WithMetadata(MetadataType.RootObject, assemblyFilePath) + .WithMetadata(MetadataType.ExceptionDetails, fnfe.Message) + .WithMetadata(MetadataType.StackTrace, fnfe.StackTrace)); + } + catch (BadImageFormatException bife) + { + return FluentResults.Result.Fail(new ExceptionalError(bife) + .WithMetadata(MetadataType.ExceptionObject, this) + .WithMetadata(MetadataType.RootObject, assemblyFilePath) + .WithMetadata(MetadataType.ExceptionDetails, bife.Message) + .WithMetadata(MetadataType.StackTrace, bife.StackTrace)); + } + catch (Exception e) + { + return FluentResults.Result.Fail(new ExceptionalError(e) + .WithMetadata(MetadataType.ExceptionObject, this) + .WithMetadata(MetadataType.RootObject, assemblyFilePath) + .WithMetadata(MetadataType.ExceptionDetails, e.Message) + .WithMetadata(MetadataType.StackTrace, e.StackTrace)); + } + } + + public FluentResults.Result GetAssemblyByName(string assemblyName) + { + if (assemblyName.IsNullOrWhiteSpace()) + { + return FluentResults.Result.Fail(new Error($"Assembly name is null") + .WithMetadata(MetadataType.ExceptionObject, this)); + } + + if (_loadedAssemblyData.TryGetValue(assemblyName, out var data)) + { + return new FluentResults.Result().WithSuccess(new Success($"Assembly found")).WithValue(data.Assembly); + } + + foreach (var assembly1 in this.Assemblies.Where(a => !_loadedAssemblyData.ContainsKey(a))) + { + if (assembly1.GetName().FullName == assemblyName) + { + try + { + if (!assembly1.Location.IsNullOrWhiteSpace()) + { + _loadedAssemblyData[assembly1] = new AssemblyData(assembly1, assembly1.Location); + } + // we don't have the original byte array so we can't store it. + } + catch (NotSupportedException nse) // dynamic assembly or location property threw + { + // ignored + } + + return new FluentResults.Result().WithSuccess(new Success($"Assembly found")).WithValue(assembly1); + } + } + + return FluentResults.Result.Fail(new Error($"Assembly named { assemblyName } not found!")); + } + + public FluentResults.Result> GetTypesInAssemblies() + { + try + { + return new FluentResults.Result>().WithValue(_loadedAssemblyData.SelectMany(kvp=> kvp.Value.Types).ToImmutableArray()); + } + catch (Exception e) + { + return FluentResults.Result.Fail(new ExceptionalError(e)); + } + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + #endregion + + #region Internals + + protected override Assembly Load(AssemblyName assemblyName) + { + if (_isResolving.Value) + return null; + + _isResolving.Value = true; + try + { + if (_loadedAssemblyData.TryGetValue(assemblyName.FullName, out var data)) + return data.Assembly; + var idSpan = new[] { this.Id }; + if (_assemblyManagementService.GetLoadedAssembly(assemblyName, in idSpan) is { IsSuccess: true } ret) + return ret.Value; + return null; + } + catch (ArgumentNullException _) + { + return null; + } + finally + { + _isResolving.Value = false; + } + } + + // Use the default import resolver since native libraries are niche and not blocking for unloading. + // Implement if conflicts become an issue. + /*protected override IntPtr LoadUnmanagedDll(string unmanagedDllName) + { + // Implement NativeLibrary::InternalLoadUnmanagedDll() + throw new NotImplementedException(); + }*/ + + private void OnUnload(AssemblyLoadContext context) + { + base.Unloading -= OnUnload; + var wf = new WeakReference(this); + _eventService.PublishEvent((sub) => sub.OnAssemblyUnloading(wf)); + _onUnload?.Invoke(this); + this.Dispose(true); + } + + private void Dispose(bool disposing) + { + if (ModUtils.Threading.CheckClearAndSetBool(ref _isDisposed)) + { + _operationsLock.EnterWriteLock(); + try + { + _loadedAssemblyData.Clear(); + + } + finally + { + _operationsLock.ExitWriteLock(); + } + } + } + + private readonly record struct AssemblyData + { + public readonly Assembly Assembly; + public readonly OneOf AssemblyImageOrPath; + public readonly MetadataReference AssemblyReference; + public readonly ImmutableArray Types; + + public AssemblyData(Assembly assembly, byte[] assemblyImage) + { + Assembly = assembly ?? throw new ArgumentNullException(nameof(assembly)); + AssemblyImageOrPath = assemblyImage ?? throw new ArgumentNullException(nameof(assemblyImage)); + AssemblyReference = MetadataReference.CreateFromImage(assemblyImage); + Types = assembly.GetSafeTypes().ToImmutableArray(); + } + + public AssemblyData(Assembly assembly, string path) + { + Assembly = assembly ?? throw new ArgumentNullException(nameof(assembly)); + AssemblyImageOrPath = path ?? throw new ArgumentNullException(nameof(path)); + AssemblyReference = MetadataReference.CreateFromFile(path); + Types = assembly.GetSafeTypes().ToImmutableArray(); + } + } + + private readonly record struct AssemblyOrStringKey : IEquatable, IEqualityComparer + { + public Assembly Assembly { get; init; } + public string AssemblyName { get; init; } + public readonly int HashCode; + + public AssemblyOrStringKey(Assembly assembly) + { + if(assembly == null) + throw new ArgumentNullException(nameof(assembly)); + Assembly = assembly; + AssemblyName = assembly.GetName().FullName; + if (AssemblyName == null) + throw new ArgumentNullException(nameof(AssemblyName)); + HashCode = AssemblyName.GetHashCode(); + } + + public AssemblyOrStringKey(string assemblyName) + { + if (assemblyName.IsNullOrWhiteSpace()) + throw new ArgumentNullException(nameof(assemblyName)); + Assembly = null; + AssemblyName = assemblyName; + HashCode = AssemblyName.GetHashCode(); + } + + public bool Equals(AssemblyOrStringKey x, AssemblyOrStringKey y) + { + if (x.Assembly is not null && y.Assembly is not null) + return x.Assembly == y.Assembly; + return x.AssemblyName == y.AssemblyName; + } + + public int GetHashCode(AssemblyOrStringKey obj) + { + return obj.HashCode; + } + + public static implicit operator AssemblyOrStringKey(Assembly assembly) => new AssemblyOrStringKey(assembly); + public static implicit operator AssemblyOrStringKey(string name) => new AssemblyOrStringKey(name); + } + + #endregion +} diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Plugins/AssemblyLoadingSuccessState.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Plugins/AssemblyLoadingSuccessState.cs similarity index 100% rename from Barotrauma/BarotraumaShared/SharedSource/LuaCs/Plugins/AssemblyLoadingSuccessState.cs rename to Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Plugins/AssemblyLoadingSuccessState.cs diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Plugins/CsPackageManager.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Plugins/CsPackageManager.cs similarity index 99% rename from Barotrauma/BarotraumaShared/SharedSource/LuaCs/Plugins/CsPackageManager.cs rename to Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Plugins/CsPackageManager.cs index a23d70f7b..d323ac849 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Plugins/CsPackageManager.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Plugins/CsPackageManager.cs @@ -8,6 +8,7 @@ using System.Reflection; using System.Runtime.CompilerServices; using System.Text; using System.Threading; +using Barotrauma.LuaCs; using Barotrauma.LuaCs.Services; using Barotrauma.Steam; using Microsoft.CodeAnalysis; @@ -630,8 +631,9 @@ public sealed class CsPackageManager : IDisposable bool ShouldRunPackage(ContentPackage package, RunConfig config) { - return (!_luaCsSetup.Config.TreatForcedModsAsNormal && config.IsForced()) - || (ContentPackageManager.EnabledPackages.All.Contains(package) && config.IsForcedOrStandard()); + throw new NotImplementedException(); + /*return (!_luaCsSetup.Config.TreatForcedModsAsNormal && config.IsForced()) + || (ContentPackageManager.EnabledPackages.All.Contains(package) && config.IsForcedOrStandard());*/ } void UpdatePackagesToDisable(ref HashSet set, diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Plugins/IAssemblyLoaderService.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Plugins/IAssemblyLoaderService.cs new file mode 100644 index 000000000..629964ff7 --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Plugins/IAssemblyLoaderService.cs @@ -0,0 +1,106 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; +using System.Reflection; +using System.Runtime.CompilerServices; +using Barotrauma.LuaCs.Services; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; + +namespace Barotrauma.LuaCs; + +public interface IAssemblyLoaderService : IService +{ + /// + /// Assembly loader factory for DI registration. + /// + /// The assembly hosting management service. + /// The event service for publishing. + /// The referencing ID. Intended to be used to distinguish between instances. + /// The name of the friendly name instance, used for error messages. + /// Loaded assemblies are not intended for execution, just MetadataReferences. + delegate IAssemblyLoaderService AssemblyLoaderDelegate( + IAssemblyManagementService assemblyManagementService, + IEventService eventService, Guid id, string name, + bool isReferenceOnlyMode, Action onUnload); + + /// + /// ID for this instance. + /// + Guid Id { get; } + /// + /// Indicates that the assemblies in this load context are metadata references only and not + /// intended for execution. + /// + bool IsReferenceOnlyMode { get; } + /// + /// Runtime value of constant for extensibility use. + /// + public static readonly string InternalsAccessAssemblyName = InternalsAwareAssemblyName; + /// + /// Name for all runtime-compiled assemblies requiring access to internal assembly components. + /// + public const string InternalsAwareAssemblyName = "InternalsAwareAssembly"; + + /// + /// Add additional locations for dependency resolution to use. + /// + /// + /// + public FluentResults.Result AddDependencyPaths(ImmutableArray paths); + + /// + /// Compiles the supplied syntaxtrees and options into an in-memory assembly image. + /// Builds metadata from loaded assemblies, only supply your own if you have in-memory images not managed by the + /// AssemblyManager class. + /// + /// [NotNull]Name reference of the assembly. + /// [IMPORTANT] This is used to reference this assembly as the true name will be forced if + /// publicized assemblies are not used (InternalsVisibleTo Attrib). + /// Must be supplied for in-memory assemblies. + /// Must be unique to all other assemblies explicitly loaded using this context. + /// Forces the assembly name to and grants access to internal. + /// [IMPORTANT]Cannot be null or empty if is false. + /// [NotNull]Syntax trees to compile into the assembly. + /// All MetadataReferences to be used for compilation. + /// [IMPORTANT] This method builds metadata from loaded assemblies, only supply your own if you have in-memory + /// images not managed by the AssemblyManager class. + /// [NotNull]CSharp compilation options. This method automatically adds the 'IgnoreAccessChecks' property for compilation. + /// Success state of the operation. + public FluentResults.Result CompileScriptAssembly( + [NotNull] string assemblyName, + bool compileWithInternalAccess, + ImmutableArray syntaxTrees, + ImmutableArray metadataReferences, + CSharpCompilationOptions compilationOptions = null); + + /// + /// Loads the assembly from the provided location and registers all new paths provided with dependency resolution. + /// + /// Absolute path to the managed assembly. + /// Additional paths for dependency resolution. + /// Success and reference to the assembly if successful. + public FluentResults.Result LoadAssemblyFromFile(string assemblyFilePath, + ImmutableArray additionalDependencyPaths); + + /// + /// Returns the already loaded assembly with the same name. + /// + /// Name of the assembly. + /// Operation success on assembly found and assembly. + public FluentResults.Result GetAssemblyByName(string assemblyName); + + /// + /// Gets the list of Types from loaded assemblies. + /// + /// + public FluentResults.Result> GetTypesInAssemblies(); + + /// + /// List of loaded assemblies. + /// + public IEnumerable Assemblies { get; } +} + + diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Plugins/IAssemblyPlugin.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Plugins/IAssemblyPlugin.cs new file mode 100644 index 000000000..964ce312f --- /dev/null +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Plugins/IAssemblyPlugin.cs @@ -0,0 +1,6 @@ +using System; +using Barotrauma.LuaCs.Events; + +namespace Barotrauma; + +public interface IAssemblyPlugin : IDisposable, IEventPluginPreInitialize, IEventPluginInitialize, IEventPluginLoadCompleted { } diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Plugins/MemoryFileAssemblyContextLoader.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Plugins/MemoryFileAssemblyContextLoader.cs similarity index 99% rename from Barotrauma/BarotraumaShared/SharedSource/LuaCs/Plugins/MemoryFileAssemblyContextLoader.cs rename to Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Plugins/MemoryFileAssemblyContextLoader.cs index e01db5f4c..d5ccc6a86 100644 --- a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Plugins/MemoryFileAssemblyContextLoader.cs +++ b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Plugins/MemoryFileAssemblyContextLoader.cs @@ -14,7 +14,7 @@ using Microsoft.CodeAnalysis.CSharp; [assembly: InternalsVisibleTo("CompiledAssembly")] -namespace Barotrauma.LuaCs; +namespace Barotrauma.LuaCs.Services; /// /// AssemblyLoadContext to compile from syntax trees in memory and to load from disk/file. Provides dependency resolution. diff --git a/Barotrauma/BarotraumaShared/SharedSource/LuaCs/Plugins/RunConfig.cs b/Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Plugins/RunConfig.cs similarity index 100% rename from Barotrauma/BarotraumaShared/SharedSource/LuaCs/Plugins/RunConfig.cs rename to Barotrauma/BarotraumaShared/SharedSource/LuaCs/_Plugins/RunConfig.cs