diff --git a/.github/workflows/build-artifacts.yaml b/.github/workflows/build-artifacts.yaml new file mode 100644 index 0000000..6aa1113 --- /dev/null +++ b/.github/workflows/build-artifacts.yaml @@ -0,0 +1,48 @@ +name: Build Artifacts + +permissions: + contents: write + +on: + push: + branches: + - '*' + +jobs: + build-artifacts: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up .NET Core + uses: actions/setup-dotnet@v3.0.3 + with: + dotnet-version: 6 + + - name: NuGet Restore + run: dotnet restore + + - name: Build and Publish project + run: dotnet publish BattleBitAPIRunner/BattleBitAPIRunner.csproj -c Release -o ./publish --self-contained false --framework net6.0 --runtime win-x64 + + - name: Build nuget + run: dotnet build BattleBitAPIRunner/BattleBitAPIRunner.csproj -c Release + + - name: Create NuGet package + run: dotnet pack BBRAPIModules/BBRAPIModules.csproj --configuration Release --output ./nuget + + - name: Upload NuGet package artifact + uses: actions/upload-artifact@v2 + with: + name: NuGet Package + path: ./nuget/*.nupkg + + - name: Upload Release artifact + uses: actions/upload-artifact@v2 + with: + name: BattleBitAPIRunner-beta + path: ./publish/* \ No newline at end of file diff --git a/BBRAPIModuleVerfication/BBRAPIModuleVerification.csproj b/BBRAPIModuleVerfication/BBRAPIModuleVerification.csproj new file mode 100644 index 0000000..af16098 --- /dev/null +++ b/BBRAPIModuleVerfication/BBRAPIModuleVerification.csproj @@ -0,0 +1,15 @@ + + + + Exe + net6.0 + enable + enable + + + + + + + + diff --git a/BBRAPIModuleVerfication/Program.cs b/BBRAPIModuleVerfication/Program.cs new file mode 100644 index 0000000..cc1e105 --- /dev/null +++ b/BBRAPIModuleVerfication/Program.cs @@ -0,0 +1,99 @@ +using BattleBitAPIRunner; +using BBRAPIModules; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis; +using Newtonsoft.Json; +using Microsoft.CodeAnalysis.CSharp; +using System.Text; +using System.Collections.ObjectModel; + +namespace BBRAPIModuleVerification; + +internal class Program +{ + static void Main(string[] args) + { + if (args.Length != 1) + { + Console.WriteLine("Usage: BBRAPIModuleVerification "); + return; + } + + string filePath = args[0]; + + Module.logToConsole = false; + + Module module; + try + { + module = new Module(filePath); + } + catch (Exception e) + { + Console.WriteLine(JsonConvert.SerializeObject(new VerificationResponse(false, Path.GetFileNameWithoutExtension(filePath), null, null, null, null, e.Message))); + return; + } + + List missingDependencies = new(); + List references = new(); + + foreach (string dependency in module.RequiredDependencies) + { + if (!Directory.Exists($"./cache/modules/{dependency}")) + { + missingDependencies.Add(dependency); + } + else + { + references.Add(MetadataReference.CreateFromFile($"./cache/modules/{dependency}/{dependency}.dll")); + } + } + + if (missingDependencies.Count > 0) + { + Console.WriteLine(JsonConvert.SerializeObject(new VerificationResponse(false, module.Name, module.Description, module.Version, module.RequiredDependencies, module.OptionalDependencies, $"Missing dependencies: {string.Join(", ", missingDependencies)}"))); + return; + } + + try + { + module.Compile(references.ToArray()); + if (!Directory.Exists($"./cache/modules/{module.Name}")) + { + Directory.CreateDirectory($"./cache/modules/{module.Name}"); + } + File.WriteAllBytes($"./cache/modules/{module.Name}/{module.Name}.dll", module.AssemblyBytes); + + Console.WriteLine(JsonConvert.SerializeObject(new VerificationResponse(true, module.Name, module.Description, module.Version, module.RequiredDependencies, module.OptionalDependencies, null))); + } + catch (Exception e) + { + Console.WriteLine(JsonConvert.SerializeObject(new VerificationResponse(false, module.Name, module.Description, module.Version, module.RequiredDependencies, module.OptionalDependencies, e.Message))); + return; + } + } + + +} + +internal class VerificationResponse +{ + public VerificationResponse(bool success, string name, string description, string version, string[] requiredDependencies, string[] optionalDependencies, string errors) + { + this.Success = success; + this.Name = name; + this.Description = description; + this.Version = version; + this.RequiredDependencies = requiredDependencies; + this.OptionalDependencies = optionalDependencies; + this.Errors = errors; + } + + public bool Success { get; set; } + public string? Name { get; set; } + public string? Description { get; set; } + public string? Version { get; set; } + public string[]? RequiredDependencies { get; set; } + public string[]? OptionalDependencies { get; set; } + public string? Errors { get; set; } +} \ No newline at end of file diff --git a/BBRAPIModuleVerfication/Properties/launchSettings.json b/BBRAPIModuleVerfication/Properties/launchSettings.json new file mode 100644 index 0000000..2f09f78 --- /dev/null +++ b/BBRAPIModuleVerfication/Properties/launchSettings.json @@ -0,0 +1,8 @@ +{ + "profiles": { + "BBRAPIModuleVerification": { + "commandName": "Project", + "commandLineArgs": "E:\\Create\\Development\\Software\\C#\\BattleBit\\BattleBitBaseModules\\SpectateControl.cs" + } + } +} \ No newline at end of file diff --git a/BBRAPIModules/BBRAPIModules.csproj b/BBRAPIModules/BBRAPIModules.csproj index 07c3b0d..ff30b00 100644 --- a/BBRAPIModules/BBRAPIModules.csproj +++ b/BBRAPIModules/BBRAPIModules.csproj @@ -6,7 +6,7 @@ enable True BattleBit Remastered Modular Community Server API - 0.4.11 + 0.4.13 $(AssemblyVersion) $(AssemblyVersion) RainOrigami diff --git a/BBRAPIModules/BattleBitModule.cs b/BBRAPIModules/BattleBitModule.cs index 9444326..81e0684 100644 --- a/BBRAPIModules/BattleBitModule.cs +++ b/BBRAPIModules/BattleBitModule.cs @@ -1,7 +1,5 @@ -using BattleBitAPI; -using BattleBitAPI.Common; +using BattleBitAPI.Common; using BattleBitAPI.Server; -using System.Reflection; using System.Runtime.CompilerServices; [assembly: InternalsVisibleTo("BattleBitAPIRunner")] @@ -19,41 +17,6 @@ internal void SetServer(RunnerServer server) this.Server = server; } - public void Call(string methodName, params object?[]? parameters) - { - this.Call(methodName, parameters); - } - - public T Call(string methodName, params object?[]? parameters) - { - var method = this.GetType().GetMethod(methodName); - if (method == null) - { - return default(T); - } - - ParameterInfo[] methodParameters = method.GetParameters(); - - List fullParameters = new(parameters ?? new object?[] { }); - - if (methodParameters.Length > 0 && methodParameters.Length != parameters?.Length) - { - for (int i = parameters?.Length ?? 0; i < methodParameters.Length; i++) - { - if (methodParameters[i].IsOptional || methodParameters[i].HasDefaultValue) - { - fullParameters.Add(methodParameters[i].DefaultValue); - } - else - { - throw new ArgumentException($"Parameter {methodParameters[i].Name} is not optional and does not have a default value."); - } - } - } - - return (T)method.Invoke(this, fullParameters.ToArray()); - } - public void Unload() { this.IsLoaded = false; @@ -63,6 +26,7 @@ public void Unload() public virtual void OnModulesLoaded() { } // sighs silently public virtual void OnModuleUnloading() { } + public virtual void OnConsoleCommand(string command) { } #region GameServer.cs copy-paste // TODO: there must be a better way to do this!? diff --git a/BBRAPIModules/ModuleAttribute.cs b/BBRAPIModules/ModuleAttribute.cs new file mode 100644 index 0000000..b033a29 --- /dev/null +++ b/BBRAPIModules/ModuleAttribute.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BBRAPIModules; + +[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)] +public class ModuleAttribute : Attribute +{ + public string Description { get; set; } + public string Version { get; set; } + + public ModuleAttribute(string description, string version) + { + this.Description = description; + this.Version = version; + } +} diff --git a/BBRAPIModules/RunnerServer.cs b/BBRAPIModules/RunnerServer.cs index 2ebdc5f..cef8442 100644 --- a/BBRAPIModules/RunnerServer.cs +++ b/BBRAPIModules/RunnerServer.cs @@ -1,6 +1,7 @@ using BattleBitAPI.Common; using BattleBitAPI.Server; using System.Diagnostics; +using System.Runtime.CompilerServices; namespace BBRAPIModules { @@ -114,7 +115,7 @@ private async Task invokeOnModulesWithBoolReturnValue(string method, param return result; } - private async Task invokeOnModules(string method, params object?[] parameters) + internal async Task invokeOnModules(string method, params object?[] parameters) { Stopwatch stopwatch = new(); foreach (BattleBitModule module in this.modules) diff --git a/BattleBitAPIRunner.sln b/BattleBitAPIRunner.sln index 7bc842d..16ee168 100644 --- a/BattleBitAPIRunner.sln +++ b/BattleBitAPIRunner.sln @@ -7,6 +7,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BattleBitAPIRunner", "Battl EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BBRAPIModules", "BBRAPIModules\BBRAPIModules.csproj", "{B17890B8-4D8C-4FA7-BFFB-20679A42AF7B}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BBRAPIModuleVerification", "BBRAPIModuleVerfication\BBRAPIModuleVerification.csproj", "{2A5EC0BE-9755-4383-A4B6-A3D2275DA9A7}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -21,6 +23,10 @@ Global {B17890B8-4D8C-4FA7-BFFB-20679A42AF7B}.Debug|Any CPU.Build.0 = Debug|Any CPU {B17890B8-4D8C-4FA7-BFFB-20679A42AF7B}.Release|Any CPU.ActiveCfg = Release|Any CPU {B17890B8-4D8C-4FA7-BFFB-20679A42AF7B}.Release|Any CPU.Build.0 = Release|Any CPU + {2A5EC0BE-9755-4383-A4B6-A3D2275DA9A7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2A5EC0BE-9755-4383-A4B6-A3D2275DA9A7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2A5EC0BE-9755-4383-A4B6-A3D2275DA9A7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2A5EC0BE-9755-4383-A4B6-A3D2275DA9A7}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/BattleBitAPIRunner/BattleBitAPIRunner.csproj b/BattleBitAPIRunner/BattleBitAPIRunner.csproj index adfb4cc..64e27f6 100644 --- a/BattleBitAPIRunner/BattleBitAPIRunner.csproj +++ b/BattleBitAPIRunner/BattleBitAPIRunner.csproj @@ -5,7 +5,7 @@ net6.0 enable enable - 0.4.12.6 + 0.4.13 $(AssemblyVersion) diff --git a/BattleBitAPIRunner/Module.cs b/BattleBitAPIRunner/Module.cs index 0ccfd63..4da62f2 100644 --- a/BattleBitAPIRunner/Module.cs +++ b/BattleBitAPIRunner/Module.cs @@ -1,4 +1,5 @@ -using BattleBitAPI; +using BattleBitAPI.Common; +using BattleBitAPI; using BBRAPIModules; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; @@ -14,6 +15,9 @@ using System.Text; using System.Threading.Tasks; using System.Xml.Linq; +using BattleBitAPI.Server; + +[assembly: InternalsVisibleTo("BBRAPIModuleVerification")] namespace BattleBitAPIRunner { @@ -26,6 +30,8 @@ internal class Module public AssemblyLoadContext? Context { get; private set; } public Type? ModuleType { get; private set; } public string? Name { get; private set; } + public string? Description { get; private set; } + public string? Version { get; private set; } public string[]? RequiredDependencies { get; private set; } public string[]? OptionalDependencies { get; private set; } public byte[] AssemblyBytes { get; private set; } = null!; @@ -33,6 +39,7 @@ internal class Module public string ModuleFilePath { get; } public Assembly? ModuleAssembly { get; private set; } + internal static bool logToConsole = true; private SyntaxTree syntaxTree = null!; private string code = null!; @@ -46,6 +53,8 @@ public static void LoadContext(string[] dependencies) loadDepedencies(dependencies); } + + private static void loadReferences(string[] dependencies) { List references = new() @@ -102,26 +111,52 @@ public Module(string moduleFilePath) private void initialize() { - Console.Write("Parsing module from file "); - Console.ForegroundColor = ConsoleColor.White; - Console.WriteLine(Path.GetFileName(this.ModuleFilePath)); - Console.ResetColor(); + if (logToConsole) + { + Console.Write("Parsing module from file "); + Console.ForegroundColor = ConsoleColor.White; + Console.WriteLine(Path.GetFileName(this.ModuleFilePath)); + Console.ResetColor(); + } this.code = File.ReadAllText(this.ModuleFilePath); this.syntaxTree = CSharpSyntaxTree.ParseText(code, null, this.ModuleFilePath, Encoding.UTF8); this.Name = this.getName(); + if (Path.GetFileNameWithoutExtension(this.ModuleFilePath) != this.Name) { throw new Exception($"Module {Path.GetFileName(this.ModuleFilePath)} does not have the same name as the class {this.Name} that inherits from {nameof(BattleBitModule)}"); } + this.getDependencies(); + this.getMetadata(); - Console.Write("Module "); - Console.ForegroundColor = ConsoleColor.White; - Console.Write(this.Name); - Console.ResetColor(); - Console.WriteLine($" has {this.RequiredDependencies!.Length} required and {this.OptionalDependencies!.Length} optional dependencies"); - Console.WriteLine(); + if (logToConsole) + { + Console.Write("Module "); + Console.ForegroundColor = ConsoleColor.White; + Console.Write(this.Name); + Console.ResetColor(); + Console.WriteLine($" has {this.RequiredDependencies.Length} required and {this.OptionalDependencies.Length} optional dependencies"); + Console.WriteLine(); + } + } + + private void getMetadata() + { + IEnumerable attributeSyntaxes = syntaxTree.GetRoot().DescendantNodes().OfType(); + IEnumerable moduleAttributes = attributeSyntaxes.Where(x => x.Name.ToString() + "Attribute" == nameof(ModuleAttribute)); + if (moduleAttributes.Count() != 1) + { + throw new Exception("Module must have exactly one ModuleAttribute"); + } + + AttributeSyntax moduleAttribute = moduleAttributes.First(); + IEnumerable moduleAttributeArguments = moduleAttribute.ArgumentList?.Arguments ?? throw new Exception("ModuleAttribute must have arguments"); + AttributeArgumentSyntax descriptionArgument = moduleAttributeArguments.ElementAtOrDefault(0) ?? throw new Exception("ModuleAttribute must have a description argument"); + AttributeArgumentSyntax versionArgument = moduleAttributeArguments.ElementAtOrDefault(1) ?? throw new Exception("ModuleAttribute must have a version argument"); + this.Description = descriptionArgument.Expression.ToString().Trim('"'); + this.Version = versionArgument.Expression.ToString().Trim('"'); } private void getDependencies() @@ -184,7 +219,7 @@ public void Load() modules.Add(this); } - public static PortableExecutableReference[]? baseReferences = null; + public static PortableExecutableReference[] baseReferences = null!; public void Compile(PortableExecutableReference[]? extraReferences = null) { @@ -193,11 +228,15 @@ public void Compile(PortableExecutableReference[]? extraReferences = null) return; } - Console.Write("Compiling module "); - Console.ForegroundColor = ConsoleColor.White; - Console.WriteLine(this.Name); - Console.ResetColor(); - List references = new(baseReferences!); + if (logToConsole) + { + Console.Write("Compiling module "); + Console.ForegroundColor = ConsoleColor.White; + Console.WriteLine(this.Name); + Console.ResetColor(); + } + + List references = new(baseReferences); foreach (Module module in modules) { diff --git a/BattleBitAPIRunner/Program.cs b/BattleBitAPIRunner/Program.cs index ecc0737..c7af270 100644 --- a/BattleBitAPIRunner/Program.cs +++ b/BattleBitAPIRunner/Program.cs @@ -182,6 +182,27 @@ private void consoleCommandHandler() continue; } + foreach (RunnerServer server in this.servers) + { + List instances = new(); + foreach (Module module in Module.Modules) + { + BattleBitModule? moduleInstance = server.GetModule(module.ModuleType!); + if (moduleInstance is null) + { + continue; + } + + instances.Add(moduleInstance); + moduleInstance.OnConsoleCommand(command); + } + + foreach (BattleBitModule moduleInstance in instances) + { + moduleInstance.Unload(); + } + } + switch (commandParts[0]) { case "servers": @@ -453,6 +474,8 @@ private void loadServerModules(RunnerServer server, IPAddress? ip = null, ushort } } + battleBitModules = battleBitModules.Where(m => m.Server is not null).ToList(); + foreach (BattleBitModule battleBitModule in battleBitModules) { // Resolve references @@ -540,22 +563,11 @@ private void ModuleConfiguration_OnLoadingRequest(object? sender, BattleBitModul // Create instance of type of the property if it doesn't exist ModuleConfiguration? configurationValue = property.GetValue(module) as ModuleConfiguration; - if (configurationValue is null) - { - configurationValue = Activator.CreateInstance(property.PropertyType) as ModuleConfiguration; - configurationValue!.Initialize(module, property, serverName); - configurationValue.OnLoadingRequest += ModuleConfiguration_OnLoadingRequest; - configurationValue.OnSavingRequest += ModuleConfiguration_OnSavingRequest; - - if (!File.Exists(filePath)) - { - File.WriteAllText(filePath, JsonConvert.SerializeObject(configurationValue, Formatting.Indented)); - } - } if (File.Exists(filePath)) { - configurationValue = JsonConvert.DeserializeObject(File.ReadAllText(filePath), property.PropertyType) as ModuleConfiguration; + configurationValue = JsonConvert.DeserializeObject(File.ReadAllText(filePath), property.PropertyType, new JsonSerializerSettings() { ObjectCreationHandling = ObjectCreationHandling.Replace }) as ModuleConfiguration; + if (configurationValue is null) { Console.ForegroundColor = ConsoleColor.Red; @@ -565,11 +577,22 @@ private void ModuleConfiguration_OnLoadingRequest(object? sender, BattleBitModul module.Unload(); return; } - configurationValue.Initialize(module, property, serverName); - configurationValue.OnLoadingRequest += ModuleConfiguration_OnLoadingRequest; - configurationValue.OnSavingRequest += ModuleConfiguration_OnSavingRequest; - property.SetValue(module, configurationValue); } + + if (configurationValue is null) + { + configurationValue = Activator.CreateInstance(property.PropertyType) as ModuleConfiguration; + + if (!File.Exists(filePath)) + { + File.WriteAllText(filePath, JsonConvert.SerializeObject(configurationValue, Formatting.Indented)); + } + } + + configurationValue!.Initialize(module, property, serverName); + configurationValue.OnLoadingRequest += ModuleConfiguration_OnLoadingRequest; + configurationValue.OnSavingRequest += ModuleConfiguration_OnSavingRequest; + property.SetValue(module, configurationValue); } private void startServerListener()