diff --git a/Content.ModuleManager/ModuleManifest.cs b/Content.ModuleManager/ModuleManifest.cs index 8fb7ce1ed1..e900aaef78 100644 --- a/Content.ModuleManager/ModuleManifest.cs +++ b/Content.ModuleManager/ModuleManifest.cs @@ -11,6 +11,8 @@ public sealed class ModuleManifest public string Version { get; set; } = string.Empty; public List Projects { get; set; } = new(); + public bool Disabled { get; set; } + public string ManifestPath { get; set; } = string.Empty; public string ModuleDirectory { get; set; } = string.Empty; diff --git a/Content.Packaging/ClientPackaging.cs b/Content.Packaging/ClientPackaging.cs index 9d7c41d590..ac46b7300d 100644 --- a/Content.Packaging/ClientPackaging.cs +++ b/Content.Packaging/ClientPackaging.cs @@ -79,21 +79,27 @@ private static List GetClientModules(string path) private static List FindAllModules(string path = ".") { - // Correct pathing to be in local folder if contentDir is empty. if (string.IsNullOrEmpty(path)) path = "."; var modules = new List { "Content.Client", "Content.Shared", "Content.Shared.Database", "Content.ModuleManager" }; - // Modules - Add modules from Modules/ directory - modules.AddRange( - ModuleDiscovery.DiscoverModules(path) - .Where(m => m.Type != ModuleRole.Server) - .Select(m => m.Name) - .Distinct() - ); + var coreDepsPath = Path.Combine(path, "bin", "Content.Client", "Content.Client.deps.json"); + + foreach (var mod in ModuleDiscovery.DiscoverModules(path).Where(m => m.Type != ModuleRole.Server).DistinctBy(m => m.Name)) + { + modules.Add(mod.Name); - return modules; + var moduleOutputDir = ModuleDiscovery.GetModuleOutputDir(mod.ProjectPath); + var moduleDepsPath = Path.Combine(moduleOutputDir, $"{mod.Name}.deps.json"); + + if (File.Exists(coreDepsPath) && File.Exists(moduleDepsPath)) + { + modules.AddRange(DepsHandler.GetModuleUniqueAssemblies(coreDepsPath, moduleDepsPath)); + } + } + + return modules.Distinct().ToList(); } public static async Task WriteResources( @@ -168,18 +174,26 @@ private static Task WriteClientContentAssemblies( { var mainBinDir = Path.Combine(contentDir, "bin", "Content.Client"); - var moduleAssemblyPaths = ModuleDiscovery.DiscoverModules(contentDir) - .Where(m => m.Type == ModuleRole.Client) - .ToDictionary( - m => m.Name, - m => Path.Combine(GetModuleRoot(m.ProjectPath), "bin", "Content.Client") - ); + var moduleOutputDirs = new Dictionary(); + + // Discover all non-Server modules + var allModules = ModuleDiscovery.DiscoverModules(contentDir) + .Where(m => m.Type != ModuleRole.Server) + .ToList(); + + // Map each module to its own build output directory + foreach (var module in allModules) + { + var moduleOutputDir = ModuleDiscovery.GetModuleOutputDir(module.ProjectPath); + moduleOutputDirs[module.Name] = moduleOutputDir; + } foreach (var asm in contentAssemblies) { cancel.ThrowIfCancellationRequested(); - var sourceDir = moduleAssemblyPaths.GetValueOrDefault(asm) ?? mainBinDir; + // Check module output dirs first, fall back to mainBinDir + var sourceDir = moduleOutputDirs.GetValueOrDefault(asm) ?? mainBinDir; var dllPath = Path.Combine(sourceDir, $"{asm}.dll"); if (File.Exists(dllPath)) @@ -192,13 +206,4 @@ private static Task WriteClientContentAssemblies( return Task.CompletedTask; } - - private static string GetModuleRoot(string projectPath) - { - // Extracts the module root from the project path - // e.g., "Modules/GoobStation/Content.Goobstation.Client/Content.Goobstation.Client.csproj" - // -> "Modules/GoobStation" - var projectDir = Path.GetDirectoryName(projectPath); - return Path.GetDirectoryName(projectDir)!; - } } diff --git a/Content.Packaging/DepsHandler.cs b/Content.Packaging/DepsHandler.cs index 9907b97ba9..eadc52de22 100644 --- a/Content.Packaging/DepsHandler.cs +++ b/Content.Packaging/DepsHandler.cs @@ -1,4 +1,8 @@ -using System.Text.Json; +// SPDX-FileCopyrightText: 2026 Space Station 14 Contributors +// +// SPDX-License-Identifier: MIT-WIZARDS + +using System.Text.Json; using System.Text.Json.Serialization; namespace Content.Packaging; @@ -57,6 +61,30 @@ private void RecursiveAddLibraries(string start, HashSet set) } } + public static HashSet GetModuleUniqueAssemblies(string coreDepsPath, string moduleDepsPath) + { + var core = Load(coreDepsPath); + var module = Load(moduleDepsPath); + + var unique = new HashSet(module.Libraries.Keys); + unique.ExceptWith(core.Libraries.Keys); + + return unique; + } + + public static IEnumerable GetModuleUniqueDlls(string coreDepsPath, string moduleDepsPath) + { + var core = Load(coreDepsPath); + var module = Load(moduleDepsPath); + + var uniqueLibs = new HashSet(module.Libraries.Keys); + uniqueLibs.ExceptWith(core.Libraries.Keys); + + return uniqueLibs + .Where(lib => module.Libraries.ContainsKey(lib)) + .SelectMany(lib => module.Libraries[lib].GetDllNames()); + } + public sealed class DepsData { [JsonInclude, JsonPropertyName("targets")] diff --git a/Content.Packaging/ModuleDiscovery.cs b/Content.Packaging/ModuleDiscovery.cs index f554d7d839..22cdacdf99 100644 --- a/Content.Packaging/ModuleDiscovery.cs +++ b/Content.Packaging/ModuleDiscovery.cs @@ -10,6 +10,15 @@ public static class ModuleDiscovery { public record ModuleInfo(string Name, string ProjectPath, ModuleRole Type); + public static string GetModuleOutputDir(string projectPath) + { + var projectDir = Path.GetDirectoryName(projectPath)!; + var withTfm = Path.Combine(projectDir, "bin", "Debug", "net10.0"); + if (Directory.Exists(withTfm)) + return withTfm; + return Path.Combine(projectDir, "bin", "Debug"); + } + /// /// Discovers all modules by scanning for module.yml files in the Modules/ directory /// @@ -35,6 +44,9 @@ public static IEnumerable DiscoverModules(string basePath = ".") continue; } + if (manifest.Disabled) + continue; + foreach (var project in manifest.Projects) { var projectPath = ModuleManifestLoader.GetProjectPath(project, manifest.ModuleDirectory); diff --git a/Content.Packaging/ModuleManifestLoader.cs b/Content.Packaging/ModuleManifestLoader.cs index 061611b121..97aa1af79d 100644 --- a/Content.Packaging/ModuleManifestLoader.cs +++ b/Content.Packaging/ModuleManifestLoader.cs @@ -35,6 +35,7 @@ public static ModuleManifest LoadFromFile(string manifestPath) Name = GetRequiredString(root, "name", manifestPath), Id = GetRequiredString(root, "id", manifestPath), Version = GetRequiredString(root, "version", manifestPath), + Disabled = root.TryGet("disabled", out var disabledNode) && disabledNode is ValueDataNode disabledVal && bool.TryParse(disabledVal.Value, out var disabled) && disabled, }; if (!IsValidId(manifest.Id)) diff --git a/Content.Packaging/ServerPackaging.cs b/Content.Packaging/ServerPackaging.cs index 26c3c98182..8b62fed435 100644 --- a/Content.Packaging/ServerPackaging.cs +++ b/Content.Packaging/ServerPackaging.cs @@ -39,15 +39,6 @@ public static class ServerPackaging .Select(o => o.Rid) .ToList(); - private static readonly List CoreServerContentAssemblies = new() - { - "Content.Server.Database", - "Content.Server", - "Content.Shared", - "Content.Shared.Database", - "Content.ModuleManager", // I cant be fucked to figure out how to this dynamically - }; - private static readonly List ServerExtraAssemblies = new() { // Python script had Npgsql. though we want Npgsql.dll as well soooo @@ -170,16 +161,27 @@ private static List FindServerModules(string path = ".") private static List FindAllServerModules(string path = ".") { - var modules = new List(CoreServerContentAssemblies); + if (string.IsNullOrEmpty(path)) + path = "."; - // Modules - Add modules from Modules/ directory - modules.AddRange(ModuleDiscovery.DiscoverModules(path) - .Where(m => m.Type != ModuleRole.Client) - .Select(m => m.Name) - .Distinct() - ); + var modules = new List { "Content.Server.Database", "Content.Server", "Content.Shared", "Content.Shared.Database", "Content.ModuleManager" }; + + var coreDepsPath = Path.Combine(path, "bin", "Content.Server", "Content.Server.deps.json"); + + foreach (var mod in ModuleDiscovery.DiscoverModules(path).Where(m => m.Type != ModuleRole.Client).DistinctBy(m => m.Name)) + { + modules.Add(mod.Name); + + var moduleOutputDir = ModuleDiscovery.GetModuleOutputDir(mod.ProjectPath); + var moduleDepsPath = Path.Combine(moduleOutputDir, $"{mod.Name}.deps.json"); - return modules; + if (File.Exists(coreDepsPath) && File.Exists(moduleDepsPath)) + { + modules.AddRange(DepsHandler.GetModuleUniqueAssemblies(coreDepsPath, moduleDepsPath)); + } + } + + return modules.Distinct().ToList(); } private static async Task PublishClientServer(string runtime, string targetOs, string configuration) @@ -272,10 +274,10 @@ private static Task WriteServerContentAssemblies( var mainBinDir = Path.Combine(contentDir, "bin", "Content.Server"); var moduleAssemblyPaths = ModuleDiscovery.DiscoverModules(contentDir) - .Where(m => m.Type == ModuleRole.Server) + .Where(m => m.Type != ModuleRole.Client) .ToDictionary( m => m.Name, - m => Path.Combine(GetModuleRoot(m.ProjectPath), "bin", "Content.Server") + m => ModuleDiscovery.GetModuleOutputDir(m.ProjectPath) ); foreach (var asm in contentAssemblies) @@ -296,14 +298,5 @@ private static Task WriteServerContentAssemblies( return Task.CompletedTask; } - private static string GetModuleRoot(string projectPath) - { - // Extracts the module root from the project path - // e.g., "Modules/GoobStation/Content.Goobstation.Server/Content.Goobstation.Server.csproj" - // -> "Modules/GoobStation" - var projectDir = Path.GetDirectoryName(projectPath); - return Path.GetDirectoryName(projectDir)!; - } - private readonly record struct PlatformReg(string Rid, string TargetOs, bool BuildByDefault); } diff --git a/Goobstation.Bootstrap/BootstrapBuilder.cs b/Goobstation.Bootstrap/BootstrapBuilder.cs new file mode 100644 index 0000000000..7b1a2f3905 --- /dev/null +++ b/Goobstation.Bootstrap/BootstrapBuilder.cs @@ -0,0 +1,141 @@ +// SPDX-FileCopyrightText: 2026 Space Station 14 Contributors +// +// SPDX-License-Identifier: MIT-WIZARDS + +using System.Diagnostics; +using System.Runtime.InteropServices; +using Content.Packaging; +using Content.ModuleManager; + +namespace Goobstation.Bootstrap; + +public static class BootstrapBuilder +{ + public static readonly string DotnetPath = FindDotnet(); + + private static string FindDotnet() + { + // Runtime dir is like /usr/share/dotnet/shared/Microsoft.NETCore.App/10.0.x/ + var runtimeDir = RuntimeEnvironment.GetRuntimeDirectory(); + var dotnetRoot = Path.GetFullPath(Path.Combine(runtimeDir, "..", "..", "..")); + var exe = Path.Combine(dotnetRoot, "dotnet"); + if (File.Exists(exe)) + return exe; + return "dotnet"; + } + + private static string FindRepoRoot() + { + var dir = AppContext.BaseDirectory; + while (dir != null) + { + if (File.Exists(Path.Combine(dir, "SpaceStation14.slnx"))) + return dir; + dir = Path.GetDirectoryName(dir); + } + throw new Exception("Could not find repo root (SpaceStation14.slnx)"); + } + + public static Task BuildAll() + { + var repoRoot = FindRepoRoot(); + Environment.CurrentDirectory = repoRoot; + + var modules = ModuleDiscovery.DiscoverModules().ToList(); + + Console.WriteLine("Building core projects..."); + RunDotnetBuild("Content.Client/Content.Client.csproj"); + RunDotnetBuild("Content.Server/Content.Server.csproj"); + + foreach (var module in modules) + { + Console.WriteLine($"Building module {module.Name}..."); + RunDotnetBuild(module.ProjectPath); + } + + foreach (var module in modules) + { + Console.WriteLine($"Copying {module.Name} outputs..."); + + switch (module.Type) + { + case ModuleRole.Client: + CopyModuleOutputs(module, "bin/Content.Client"); + break; + case ModuleRole.Server: + CopyModuleOutputs(module, "bin/Content.Server"); + break; + case ModuleRole.Shared: + case ModuleRole.Common: + CopyModuleOutputs(module, "bin/Content.Client", "bin/Content.Server"); + break; + } + } + + Console.WriteLine("Build complete."); + return Task.CompletedTask; + } + + private static void RunDotnetBuild(string projectPath) + { + var psi = new ProcessStartInfo + { + FileName = DotnetPath, + Arguments = $"build {projectPath} -c Debug --nologo /v:m /m", + UseShellExecute = false + }; + + using var process = Process.Start(psi); + if (process == null) + throw new Exception($"Failed to start dotnet build for {projectPath}"); + + process.WaitForExit(); + + if (process.ExitCode != 0) + throw new Exception($"Build failed for {projectPath} with exit code {process.ExitCode}"); + } + + private static void CopyModuleOutputs(ModuleDiscovery.ModuleInfo module, params string[] targetBinDirs) + { + var moduleOutputDir = ModuleDiscovery.GetModuleOutputDir(module.ProjectPath); + + foreach (var targetBinDir in targetBinDirs) + { + var coreName = Path.GetFileName(targetBinDir); + var coreDepsPath = Path.Combine(targetBinDir, $"{coreName}.deps.json"); + var moduleDepsPath = Path.Combine(moduleOutputDir, $"{module.Name}.deps.json"); + + var uniqueDlls = DepsHandler.GetModuleUniqueDlls(coreDepsPath, moduleDepsPath); + + foreach (var dll in uniqueDlls) + { + var sourceDll = Path.Combine(moduleOutputDir, dll); + var targetDll = Path.Combine(targetBinDir, dll); + + if (File.Exists(sourceDll)) + File.Copy(sourceDll, targetDll, overwrite: true); + + var sourcePdb = Path.ChangeExtension(sourceDll, ".pdb"); + var targetPdb = Path.ChangeExtension(targetDll, ".pdb"); + + if (File.Exists(sourcePdb)) + File.Copy(sourcePdb, targetPdb, overwrite: true); + } + + var moduleAssembly = $"{module.Name}.dll"; + var moduleAssemblyPdb = $"{module.Name}.pdb"; + + var sourceModuleDll = Path.Combine(moduleOutputDir, moduleAssembly); + var targetModuleDll = Path.Combine(targetBinDir, moduleAssembly); + + if (File.Exists(sourceModuleDll)) + File.Copy(sourceModuleDll, targetModuleDll, overwrite: true); + + var sourceModulePdb = Path.Combine(moduleOutputDir, moduleAssemblyPdb); + var targetModulePdb = Path.Combine(targetBinDir, moduleAssemblyPdb); + + if (File.Exists(sourceModulePdb)) + File.Copy(sourceModulePdb, targetModulePdb, overwrite: true); + } + } +} diff --git a/Goobstation.Bootstrap/Goobstation.Bootstrap.csproj b/Goobstation.Bootstrap/Goobstation.Bootstrap.csproj new file mode 100644 index 0000000000..dbb60288e4 --- /dev/null +++ b/Goobstation.Bootstrap/Goobstation.Bootstrap.csproj @@ -0,0 +1,12 @@ + + + Exe + enable + enable + + + + + + + diff --git a/Goobstation.Bootstrap/Program.cs b/Goobstation.Bootstrap/Program.cs new file mode 100644 index 0000000000..2bc3f6f477 --- /dev/null +++ b/Goobstation.Bootstrap/Program.cs @@ -0,0 +1,65 @@ +// SPDX-FileCopyrightText: 2026 Space Station 14 Contributors +// +// SPDX-License-Identifier: MIT-WIZARDS + +using System.Diagnostics; +using Goobstation.Bootstrap; + +await BootstrapBuilder.BuildAll(); + +var command = args.Length > 0 ? args[0].ToLowerInvariant() : null; + +switch (command) +{ + case null: + var server = StartProject("Content.Server/Content.Server.csproj"); + var client = StartProject("Content.Client/Content.Client.csproj"); + if (server == null || client == null) + return 1; + server.WaitForExit(); + client.WaitForExit(); + return 0; + + case "run-client": + return RunProject("Content.Client/Content.Client.csproj"); + + case "run-server": + return RunProject("Content.Server/Content.Server.csproj"); + + default: + PrintUsage(); + return 1; +} + +static void PrintUsage() +{ + Console.WriteLine("Usage: Goobstation.Bootstrap [command]"); + Console.WriteLine(); + Console.WriteLine(" (no args) - Build and run client + server"); + Console.WriteLine(" run-client - Build and run the client only"); + Console.WriteLine(" run-server - Build and run the server only"); +} + +static int RunProject(string projectPath) +{ + using var process = StartProject(projectPath); + if (process == null) + return 1; + process.WaitForExit(); + return process.ExitCode; +} + +static Process? StartProject(string projectPath) +{ + var process = Process.Start(new ProcessStartInfo + { + FileName = BootstrapBuilder.DotnetPath, + Arguments = $"run --project {projectPath}", + UseShellExecute = false + }); + + if (process == null) + Console.Error.WriteLine($"Failed to start process for {projectPath}"); + + return process; +} diff --git a/Modules/GoobStation/Content.Goobstation.Client/Content.Goobstation.Client.csproj b/Modules/GoobStation/Content.Goobstation.Client/Content.Goobstation.Client.csproj index 303f596a9b..bc29964a68 100644 --- a/Modules/GoobStation/Content.Goobstation.Client/Content.Goobstation.Client.csproj +++ b/Modules/GoobStation/Content.Goobstation.Client/Content.Goobstation.Client.csproj @@ -3,7 +3,6 @@ $(TargetFramework) false false - ..\..\..\bin\Content.Client\ Exe nullable enable diff --git a/Modules/GoobStation/Content.Goobstation.Server/Content.Goobstation.Server.csproj b/Modules/GoobStation/Content.Goobstation.Server/Content.Goobstation.Server.csproj index 02c6dff511..275751ac15 100644 --- a/Modules/GoobStation/Content.Goobstation.Server/Content.Goobstation.Server.csproj +++ b/Modules/GoobStation/Content.Goobstation.Server/Content.Goobstation.Server.csproj @@ -4,7 +4,6 @@ net10.0 false false - ..\..\..\bin\Content.Server\ true Exe 1998 diff --git a/Modules/GoobStation/Content.Goobstation.Shared/Content.Goobstation.Shared.csproj b/Modules/GoobStation/Content.Goobstation.Shared/Content.Goobstation.Shared.csproj index c04cc9949c..0b6797ddcd 100644 --- a/Modules/GoobStation/Content.Goobstation.Shared/Content.Goobstation.Shared.csproj +++ b/Modules/GoobStation/Content.Goobstation.Shared/Content.Goobstation.Shared.csproj @@ -3,7 +3,6 @@ net10.0 enable - ..\..\..\bin\Content.Shared\ enable diff --git a/Modules/GoobStation/module.yml b/Modules/GoobStation/module.yml index 54db871fbf..f09613f75f 100644 --- a/Modules/GoobStation/module.yml +++ b/Modules/GoobStation/module.yml @@ -1,11 +1,12 @@ -name: GoobStation +# SPDX-FileCopyrightText: 2026 Space Station 14 Contributors +# +# SPDX-License-Identifier: MIT-WIZARDS + +name: GoobStation id: goobstation version: 1.0.0 projects: - - path: Content.Goobstation.Maths - role: Common - - path: Content.Goobstation.Common role: Common @@ -15,5 +16,8 @@ projects: - path: Content.Goobstation.Server role: Server + - path: Content.Goobstation.Server.Database + role: Server + - path: Content.Goobstation.Client role: Client diff --git a/SpaceStation14.slnx b/SpaceStation14.slnx index 1aaeb19265..fe7777592b 100644 --- a/SpaceStation14.slnx +++ b/SpaceStation14.slnx @@ -58,6 +58,10 @@ + + + +