From 4594cb9c00e32d65a11e8977142cb9ccb612f411 Mon Sep 17 00:00:00 2001 From: Matt Date: Fri, 22 May 2026 14:17:43 +0000 Subject: [PATCH 1/2] Add explicit client/server side field to manifest Bake each file's "side" (client / server / both) into manifest.json at build time so both the launcher and the server filter deterministically and offline. This replaces the launcher's previous "download everything" default and lets the server short-circuit its runtime Modrinth lookup when a file already has an authoritative tag. Schema: - ManifestFile.Side: "client" | "server" | "both" (null = "both" for backward compat with manifests pre-dating this field) - Files marked "both" omit the field entirely to keep the JSON tight Code changes: - launcher Manifest.cs + ManifestSyncService: filter out "server" files in both the prune+download path and FindMissingFiles - server Manifest.cs + ManifestSync: drop "client" files outright; only fall back to the existing runtime Modrinth lookup for files with no explicit side (legacy/un-tagged mods stay protected) - server LockedMod: side field propagates into manifest at build time - scripts/Build-Pack.ps1: propagate side from lockfile to manifest - scripts/Bootstrap-Sides.ps1: one-off populator; queries Modrinth's client_side/server_side per project, conservatively marks restricted only when one side is "unsupported", leaves ambiguous cases as "both" - pack/pack.lock.json: bootstrap-populated sides (3 client, 5 server, rest both); CurseForge mods default to "both" pending manual review; version bumped 0.9.3 -> 0.10.0 since clients must re-sync Compatibility: - Old launcher + new manifest: ignores unknown "side", downloads all - New launcher + old manifest: side null -> "both", installs all - Old server + new manifest: same -- runtime Modrinth lookup still works - New server + old manifest: side null -> runtime lookup, same behaviour --- launcher/Models/Manifest.cs | 9 + launcher/Services/ManifestSyncService.cs | 22 + pack/pack.lock.json | 729 ++++++++++++----------- scripts/Bootstrap-Sides.ps1 | 96 +++ scripts/Build-Pack.ps1 | 11 +- server/Models/Manifest.cs | 7 + server/Services/ManifestSync.cs | 17 +- 7 files changed, 538 insertions(+), 353 deletions(-) create mode 100644 scripts/Bootstrap-Sides.ps1 diff --git a/launcher/Models/Manifest.cs b/launcher/Models/Manifest.cs index d1cc985..6f6ea0e 100644 --- a/launcher/Models/Manifest.cs +++ b/launcher/Models/Manifest.cs @@ -95,6 +95,15 @@ public sealed class ManifestFile [JsonPropertyName("size")] public long? Size { get; set; } + + /// + /// "both" (default), "client", or "server". The launcher skips files + /// marked "server"; the server-tool skips files marked "client". + /// Null/missing/unknown = treated as "both" so manifests pre-dating + /// this field stay fully compatible. + /// + [JsonPropertyName("side")] + public string? Side { get; set; } } public sealed class PackVersionRecord diff --git a/launcher/Services/ManifestSyncService.cs b/launcher/Services/ManifestSyncService.cs index dd5c5b0..f21b257 100644 --- a/launcher/Services/ManifestSyncService.cs +++ b/launcher/Services/ManifestSyncService.cs @@ -66,6 +66,7 @@ public sealed class ManifestSyncService var missing = new System.Collections.Generic.List(); foreach (var file in manifest.Files) { + if (IsServerOnly(file)) continue; // never expected on disk client-side var dest = Path.Combine(installDir, file.Path); if (!File.Exists(dest)) missing.Add(file); } @@ -113,6 +114,19 @@ public sealed class ManifestSyncService $"Pack: {manifest.Name ?? "modpack"} v{manifest.Version ?? "?"} (local: {local?.Version ?? "none"})" )); + // Drop server-only files before computing wanted/download sets. Doing + // it once here means the rest of the sync (prune + download) doesn't + // need to know about sides. Unknown/missing/'both' = include on client. + var serverOnlyCount = manifest.Files.Count(f => IsServerOnly(f)); + if (serverOnlyCount > 0) + { + manifest.Files = manifest.Files.Where(f => !IsServerOnly(f)).ToList(); + progress.Report(new ProgressReport( + ProgressKind.Log, + $"Skipping {serverOnlyCount} server-only file(s) declared in manifest." + )); + } + var wantedPaths = new HashSet( manifest.Files.Select(f => NormalizePath(f.Path)), StringComparer.OrdinalIgnoreCase @@ -249,4 +263,12 @@ public sealed class ManifestSyncService } private static string NormalizePath(string p) => p.Replace('\\', '/').TrimStart('/'); + + /// + /// True if this manifest file is tagged as server-only and therefore + /// should not be installed on the client. Unknown/null/'both' = false + /// (always include) so manifests pre-dating the field still install. + /// + private static bool IsServerOnly(ManifestFile f) + => string.Equals(f.Side, "server", StringComparison.OrdinalIgnoreCase); } diff --git a/pack/pack.lock.json b/pack/pack.lock.json index 5330146..8f053ba 100644 --- a/pack/pack.lock.json +++ b/pack/pack.lock.json @@ -1,349 +1,382 @@ { - "$schema": "Brass-and-Sigil pack.lock.json - generated, do not edit by hand unless you know what you are doing", - "name": "Brass and Sigil", - "version": "0.9.3", - "minecraft": "1.21.1", - "loader": { - "type": "neoforge", - "version": "21.1.228" - }, - "lockedAt": "2026-05-04T14:22:58.7131203+01:00", - "mods": [ - { - "source": "modrinth", - "slug": "create", - "versionId": "UjX6dr61", - "version": "6.0.10+mc1.21.1", - "path": "mods/create-1.21.1-6.0.10.jar", - "url": "https://cdn.modrinth.com/data/LNytGWDc/versions/UjX6dr61/create-1.21.1-6.0.10.jar", - "sha1": "0e97e49837bed766e6f28a4c95b04885d6acc353", - "size": 19123767 - }, - { - "source": "modrinth", - "slug": "create-aeronautics", - "versionId": "YhZLrAFC", - "version": "1.2.1+mc1.21.1", - "path": "mods/create-aeronautics-bundled-1.21.1-1.2.1.jar", - "url": "https://cdn.modrinth.com/data/oWaK0Q19/versions/YhZLrAFC/create-aeronautics-bundled-1.21.1-1.2.1.jar", - "sha1": "fdf1ae69e8b6437e0196b3a35dd2325aa904aba9", - "size": 33030286 - }, - { - "source": "modrinth", - "slug": "sable", - "versionId": "3FMsUjO4", - "version": "1.2.2+mc1.21.1", - "path": "mods/sable-neoforge-1.21.1-1.2.2.jar", - "url": "https://cdn.modrinth.com/data/T9PomCSv/versions/3FMsUjO4/sable-neoforge-1.21.1-1.2.2.jar", - "sha1": "c5ecd3fcf60a31d84112c708abe29e341b2d1b73", - "size": 12719293 - }, - { - "source": "modrinth", - "slug": "create-big-cannons", - "versionId": "bsGaXKEd", - "version": "5.11.3", - "path": "mods/createbigcannons-5.11.3+mc.1.21.1.jar", - "url": "https://cdn.modrinth.com/data/GWp4jCJj/versions/bsGaXKEd/createbigcannons-5.11.3%2Bmc.1.21.1.jar", - "sha1": "8b61fa850e260bdeb5d360576123f98c260afa50", - "size": 3715787 - }, - { - "source": "modrinth", - "slug": "create-tfmg", - "versionId": "uDi14nbt", - "version": "1.2.0", - "path": "mods/tfmg-1.2.0.jar", - "url": "https://cdn.modrinth.com/data/USgVjXsk/versions/uDi14nbt/tfmg-1.2.0.jar", - "sha1": "b520f3687f60a69eb265ff5b9a16759b9e124103", - "size": 4924243 - }, - { - "source": "modrinth", - "slug": "distanthorizons", - "versionId": "KkaaQtTD", - "version": "3.0.2-b-1.21.1", - "path": "mods/DistantHorizons-3.0.2-b-1.21.1-fabric-neoforge.jar", - "url": "https://cdn.modrinth.com/data/uCdwusMi/versions/KkaaQtTD/DistantHorizons-3.0.2-b-1.21.1-fabric-neoforge.jar", - "sha1": "1ff0a8920e52add541471f7b32d0d389997145ba", - "size": 30019727 - }, - { - "source": "modrinth", - "slug": "sodium", - "versionId": "Pb3OXVqC", - "version": "mc1.21.1-0.6.13-neoforge", - "path": "mods/sodium-neoforge-0.6.13+mc1.21.1.jar", - "url": "https://cdn.modrinth.com/data/AANobbMI/versions/Pb3OXVqC/sodium-neoforge-0.6.13%2Bmc1.21.1.jar", - "sha1": "38af70fa4dc4b2aaac636e92fdba3bedd5a025e1", - "size": 1162994 - }, - { - "source": "modrinth", - "slug": "iris", - "versionId": "t3ruzodq", - "version": "1.8.12+1.21.1-neoforge", - "path": "mods/iris-neoforge-1.8.12+mc1.21.1.jar", - "url": "https://cdn.modrinth.com/data/YL57xq9U/versions/t3ruzodq/iris-neoforge-1.8.12%2Bmc1.21.1.jar", - "sha1": "a3e6355915c7d3b2bc392724795113e51d289378", - "size": 2438548 - }, - { - "source": "modrinth", - "slug": "modernfix", - "versionId": "6U8JVjdw", - "version": "5.27.4+mc1.21.1", - "path": "mods/modernfix-neoforge-5.27.4+mc1.21.1.jar", - "url": "https://cdn.modrinth.com/data/nmDcB62a/versions/6U8JVjdw/modernfix-neoforge-5.27.4%2Bmc1.21.1.jar", - "sha1": "2f39363f0d6d5a5ccc2a9e0f50ad3385611c3cb7", - "size": 562051 - }, - { - "source": "modrinth", - "slug": "ferrite-core", - "versionId": "x7kQWVju", - "version": "7.0.3-neoforge", - "path": "mods/ferritecore-7.0.3-neoforge.jar", - "url": "https://cdn.modrinth.com/data/uXXizFIs/versions/x7kQWVju/ferritecore-7.0.3-neoforge.jar", - "sha1": "9563692efb708b6b568df27a01ec52f6311928ef", - "size": 121559 - }, - { - "source": "modrinth", - "slug": "architectury-api", - "versionId": "ZxYGwlk0", - "version": "13.0.8+neoforge", - "path": "mods/architectury-13.0.8-neoforge.jar", - "url": "https://cdn.modrinth.com/data/lhGA9TYQ/versions/ZxYGwlk0/architectury-13.0.8-neoforge.jar", - "sha1": "6ca11d3cc136bf69bb8f4d56982481eb85b5100b", - "size": 584004 - }, - { - "source": "modrinth", - "slug": "rhino", - "versionId": "ZdLtebKH", - "version": "2101.2.7-build.81+Rhino-1.21", - "path": "mods/rhino-2101.2.7-build.81.jar", - "url": "https://cdn.modrinth.com/data/sk9knFPE/versions/ZdLtebKH/rhino-2101.2.7-build.81.jar", - "sha1": "480235a9f7749f68ce6fec3b9c3cac3428b92a4a", - "size": 882033 - }, - { - "source": "modrinth", - "slug": "rpl", - "versionId": "hZ6B2Z0x", - "version": "2.1.2", - "path": "mods/ritchiesprojectilelib-2.1.2+mc.1.21.1-neoforge.jar", - "url": "https://cdn.modrinth.com/data/B3pb093D/versions/hZ6B2Z0x/ritchiesprojectilelib-2.1.2%2Bmc.1.21.1-neoforge.jar", - "sha1": "ec2e4996f8bee8714173e603e379fef8a6901765", - "size": 76369 - }, - { - "source": "modrinth", - "slug": "kubejs", - "versionId": "Fe9CjPws", - "version": "2101.7.2-build.363", - "path": "mods/kubejs-neoforge-2101.7.2-build.363.jar", - "url": "https://cdn.modrinth.com/data/umyGl7zF/versions/Fe9CjPws/kubejs-neoforge-2101.7.2-build.363.jar", - "sha1": "d4e88254e8c26687d4c6aeb4dfa9c2ad70f260a2", - "size": 2270442 - }, - { - "source": "modrinth", - "slug": "jei", - "versionId": "YAcQ6elZ", - "version": "19.27.0.340", - "path": "mods/jei-1.21.1-neoforge-19.27.0.340.jar", - "url": "https://cdn.modrinth.com/data/u6dRKJwZ/versions/YAcQ6elZ/jei-1.21.1-neoforge-19.27.0.340.jar", - "sha1": "27d0d85e7e32e926fc3664ab6815df5cdabb7941", - "size": 1529391 - }, - { - "source": "modrinth", - "slug": "jade", - "versionId": "yd8FKCmx", - "version": "15.10.5+neoforge", - "path": "mods/Jade-1.21.1-NeoForge-15.10.5.jar", - "url": "https://cdn.modrinth.com/data/nvQzSEkH/versions/yd8FKCmx/Jade-1.21.1-NeoForge-15.10.5.jar", - "sha1": "d5bf134b3dbde9f5258666823900e21341dc0a50", - "size": 725742 - }, - { - "source": "modrinth", - "slug": "chunky", - "versionId": "LuFhm4eU", - "version": "1.4.23", - "path": "mods/Chunky-NeoForge-1.4.23.jar", - "url": "https://cdn.modrinth.com/data/fALzjamp/versions/LuFhm4eU/Chunky-NeoForge-1.4.23.jar", - "sha1": "ab0c74743a653020fe2dfc4986b43e893947f3e9", - "size": 340572 - }, - { - "source": "curseforge", - "slug": "ftb-library", - "fileId": "7746959", - "version": "2101.1.31", - "path": "mods/ftb-library-neoforge-2101.1.31.jar", - "url": "https://mediafilez.forgecdn.net/files/7746/959/ftb-library-neoforge-2101.1.31.jar", - "sha1": "686d4e784c28c14f7760cc22b2de6a8573b56b74", - "size": 1411181 - }, - { - "source": "curseforge", - "slug": "ftb-teams", - "fileId": "7369021", - "version": "2101.1.9", - "path": "mods/ftb-teams-neoforge-2101.1.9.jar", - "url": "https://mediafilez.forgecdn.net/files/7369/21/ftb-teams-neoforge-2101.1.9.jar", - "sha1": "328e04bf1a445870aacea8fe7637670f84272a8f", - "size": 291847 - }, - { - "source": "curseforge", - "slug": "ftb-chunks", - "fileId": "7608681", - "version": "2101.1.14", - "path": "mods/ftb-chunks-neoforge-2101.1.14.jar", - "url": "https://mediafilez.forgecdn.net/files/7608/681/ftb-chunks-neoforge-2101.1.14.jar", - "sha1": "908b63b11d0e00ae6c9557d3fe6440bdbcf21bb7", - "size": 642340 - }, - { - "source": "modrinth", - "slug": "ars-nouveau", - "versionId": "BmGGrC9A", - "version": "5.11.3+mc1.21.1", - "path": "mods/ars_nouveau-1.21.1-5.11.3.jar", - "url": "https://cdn.modrinth.com/data/TKB6INcv/versions/BmGGrC9A/ars_nouveau-1.21.1-5.11.3.jar", - "sha1": "0af12dd7fda63a4261ceb302c9bb57fc235641c6", - "size": 20689115 - }, - { - "source": "modrinth", - "slug": "terralith", - "versionId": "MuJMtPGQ", - "version": "2.5.8", - "path": "mods/Terralith_1.21.x_v2.5.8.jar", - "url": "https://cdn.modrinth.com/data/8oi3bsk5/versions/MuJMtPGQ/Terralith_1.21.x_v2.5.8.jar", - "sha1": "bee0cfb1a8cd4bf3d96bccea224fb45d74de9085", - "size": 3115385 - }, - { - "source": "modrinth", - "slug": "yungs-better-strongholds", - "versionId": "8U0dIfSM", - "version": "1.21.1-NeoForge-5.1.3", - "path": "mods/YungsBetterStrongholds-1.21.1-NeoForge-5.1.3.jar", - "url": "https://cdn.modrinth.com/data/kidLKymU/versions/8U0dIfSM/YungsBetterStrongholds-1.21.1-NeoForge-5.1.3.jar", - "sha1": "5d06a5850af7c577612d4592706a8e156bbe1cbf", - "size": 461244 - }, - { - "source": "modrinth", - "slug": "lithostitched", - "versionId": "IONexlgI", - "version": "1.7.2-neoforge-21.1", - "path": "mods/lithostitched-1.7.2-neoforge-21.1.jar", - "url": "https://cdn.modrinth.com/data/XaDC71GB/versions/IONexlgI/lithostitched-1.7.2-neoforge-21.1.jar", - "sha1": "ce35206214647131ebdf14212d1986349aeba79a", - "size": 810015 - }, - { - "source": "modrinth", - "slug": "c2me-neoforge", - "versionId": "9iPiN34N", - "version": "0.3.0+alpha.0.91+1.21.1", - "path": "mods/c2me-neoforge-mc1.21.1-0.3.0+alpha.0.91.jar", - "url": "https://cdn.modrinth.com/data/COlSi5iR/versions/9iPiN34N/c2me-neoforge-mc1.21.1-0.3.0%2Balpha.0.91.jar", - "sha1": "c858c8becfb5205eb12aaf0420eb82c307c2e6a7", - "size": 4508649 - }, - { - "source": "modrinth", - "slug": "noisium", - "versionId": "nJBE6tif", - "version": "2.3.0+mc1.21-1.21.1", - "path": "mods/noisium-neoforge-2.3.0+mc1.21-1.21.1.jar", - "url": "https://cdn.modrinth.com/data/KuNKN7d2/versions/nJBE6tif/noisium-neoforge-2.3.0%2Bmc1.21-1.21.1.jar", - "sha1": "1bea6b61378ba80f038256c4345d9ff3b67928c4", - "size": 60296 - }, - { - "source": "modrinth", - "slug": "async-locator-refined", - "versionId": "3BdGHbV2", - "version": "1.21.1-1.5.3", - "path": "mods/async-locator-refined-neoforge-1.21.1-1.5.3.jar", - "url": "https://cdn.modrinth.com/data/LUIHK4LD/versions/3BdGHbV2/async-locator-refined-neoforge-1.21.1-1.5.3.jar", - "sha1": "2993e3efc6d211ad8d4db179851dea6fdfff4e07", - "size": 273320 - }, - { - "source": "modrinth", - "slug": "servercore", - "versionId": "77MAnmOn", - "version": "1.5.10+1.21.1", - "path": "mods/servercore-neoforge-1.5.10+1.21.1.jar", - "url": "https://cdn.modrinth.com/data/4WWQxlQP/versions/77MAnmOn/servercore-neoforge-1.5.10%2B1.21.1.jar", - "sha1": "4524cd40cfa5019d8b5fbcb628b1616031838a0c", - "size": 1429522 - }, - { - "source": "modrinth", - "slug": "lithium", - "versionId": "RXHf27Wv", - "version": "mc1.21.1-0.15.3-neoforge", - "path": "mods/lithium-neoforge-0.15.3+mc1.21.1.jar", - "url": "https://cdn.modrinth.com/data/gvQqBUqZ/versions/RXHf27Wv/lithium-neoforge-0.15.3%2Bmc1.21.1.jar", - "sha1": "9fd5fa9076044180ae7f51672de74669196ec72e", - "size": 774148 - }, - { - "source": "modrinth", - "slug": "geckolib", - "versionId": "gFmrC8Ru", - "version": "4.8.4", - "path": "mods/geckolib-neoforge-1.21.1-4.8.4.jar", - "url": "https://cdn.modrinth.com/data/8BmcQJ2H/versions/gFmrC8Ru/geckolib-neoforge-1.21.1-4.8.4.jar", - "sha1": "eb854c8ec53ef922a5f3877a1aa4c1ce1352e0ce", - "size": 622582 - }, - { - "source": "modrinth", - "slug": "curios", - "versionId": "yohfFbgD", - "version": "9.5.1+1.21.1", - "path": "mods/curios-neoforge-9.5.1+1.21.1.jar", - "url": "https://cdn.modrinth.com/data/vvuO3ImH/versions/yohfFbgD/curios-neoforge-9.5.1%2B1.21.1.jar", - "sha1": "418fcd42e3a7844c9bdc71c9b6401fdb3894e0c4", - "size": 410690 - }, - { - "source": "modrinth", - "slug": "yungs-api", - "versionId": "ZB22DE9q", - "version": "1.21.1-NeoForge-5.1.6", - "path": "mods/YungsApi-1.21.1-NeoForge-5.1.6.jar", - "url": "https://cdn.modrinth.com/data/Ua7DFN59/versions/ZB22DE9q/YungsApi-1.21.1-NeoForge-5.1.6.jar", - "sha1": "e1c394779fb9e038e4f7a1b4558d0432607d263b", - "size": 388678 - }, - { - "source": "modrinth", - "slug": "complementary-reimagined", - "versionId": "836bPNGo", - "version": "r5.7.1", - "path": "shaderpacks/ComplementaryReimagined_r5.7.1.zip", - "url": "https://cdn.modrinth.com/data/HVnmMxH1/versions/836bPNGo/ComplementaryReimagined_r5.7.1.zip", - "sha1": "b560f646a124d5204b1fb7321fec373b9c346fa5", - "size": 522970 - } - ], - "defaultServer": { - "name": "Brass and Sigil", - "ip": "bns.sijbers.uk" - }, - "defaultShader": "ComplementaryReimagined_r5.7.1.zip", - "panelUrl": "https://bns-admin.sijbers.uk" -} \ No newline at end of file + "$schema": "Brass-and-Sigil pack.lock.json - generated, do not edit by hand unless you know what you are doing", + "name": "Brass and Sigil", + "version": "0.10.0", + "minecraft": "1.21.1", + "loader": { + "type": "neoforge", + "version": "21.1.228" + }, + "lockedAt": "2026-05-04T13:22:58.7131203+00:00", + "mods": [ + { + "source": "modrinth", + "slug": "create", + "versionId": "UjX6dr61", + "version": "6.0.10+mc1.21.1", + "path": "mods/create-1.21.1-6.0.10.jar", + "url": "https://cdn.modrinth.com/data/LNytGWDc/versions/UjX6dr61/create-1.21.1-6.0.10.jar", + "sha1": "0e97e49837bed766e6f28a4c95b04885d6acc353", + "size": 19123767, + "side": "both" + }, + { + "source": "modrinth", + "slug": "create-aeronautics", + "versionId": "YhZLrAFC", + "version": "1.2.1+mc1.21.1", + "path": "mods/create-aeronautics-bundled-1.21.1-1.2.1.jar", + "url": "https://cdn.modrinth.com/data/oWaK0Q19/versions/YhZLrAFC/create-aeronautics-bundled-1.21.1-1.2.1.jar", + "sha1": "fdf1ae69e8b6437e0196b3a35dd2325aa904aba9", + "size": 33030286, + "side": "both" + }, + { + "source": "modrinth", + "slug": "sable", + "versionId": "3FMsUjO4", + "version": "1.2.2+mc1.21.1", + "path": "mods/sable-neoforge-1.21.1-1.2.2.jar", + "url": "https://cdn.modrinth.com/data/T9PomCSv/versions/3FMsUjO4/sable-neoforge-1.21.1-1.2.2.jar", + "sha1": "c5ecd3fcf60a31d84112c708abe29e341b2d1b73", + "size": 12719293, + "side": "both" + }, + { + "source": "modrinth", + "slug": "create-big-cannons", + "versionId": "bsGaXKEd", + "version": "5.11.3", + "path": "mods/createbigcannons-5.11.3+mc.1.21.1.jar", + "url": "https://cdn.modrinth.com/data/GWp4jCJj/versions/bsGaXKEd/createbigcannons-5.11.3%2Bmc.1.21.1.jar", + "sha1": "8b61fa850e260bdeb5d360576123f98c260afa50", + "size": 3715787, + "side": "both" + }, + { + "source": "modrinth", + "slug": "create-tfmg", + "versionId": "uDi14nbt", + "version": "1.2.0", + "path": "mods/tfmg-1.2.0.jar", + "url": "https://cdn.modrinth.com/data/USgVjXsk/versions/uDi14nbt/tfmg-1.2.0.jar", + "sha1": "b520f3687f60a69eb265ff5b9a16759b9e124103", + "size": 4924243, + "side": "both" + }, + { + "source": "modrinth", + "slug": "distanthorizons", + "versionId": "KkaaQtTD", + "version": "3.0.2-b-1.21.1", + "path": "mods/DistantHorizons-3.0.2-b-1.21.1-fabric-neoforge.jar", + "url": "https://cdn.modrinth.com/data/uCdwusMi/versions/KkaaQtTD/DistantHorizons-3.0.2-b-1.21.1-fabric-neoforge.jar", + "sha1": "1ff0a8920e52add541471f7b32d0d389997145ba", + "size": 30019727, + "side": "both" + }, + { + "source": "modrinth", + "slug": "sodium", + "versionId": "Pb3OXVqC", + "version": "mc1.21.1-0.6.13-neoforge", + "path": "mods/sodium-neoforge-0.6.13+mc1.21.1.jar", + "url": "https://cdn.modrinth.com/data/AANobbMI/versions/Pb3OXVqC/sodium-neoforge-0.6.13%2Bmc1.21.1.jar", + "sha1": "38af70fa4dc4b2aaac636e92fdba3bedd5a025e1", + "size": 1162994, + "side": "client" + }, + { + "source": "modrinth", + "slug": "iris", + "versionId": "t3ruzodq", + "version": "1.8.12+1.21.1-neoforge", + "path": "mods/iris-neoforge-1.8.12+mc1.21.1.jar", + "url": "https://cdn.modrinth.com/data/YL57xq9U/versions/t3ruzodq/iris-neoforge-1.8.12%2Bmc1.21.1.jar", + "sha1": "a3e6355915c7d3b2bc392724795113e51d289378", + "size": 2438548, + "side": "client" + }, + { + "source": "modrinth", + "slug": "modernfix", + "versionId": "6U8JVjdw", + "version": "5.27.4+mc1.21.1", + "path": "mods/modernfix-neoforge-5.27.4+mc1.21.1.jar", + "url": "https://cdn.modrinth.com/data/nmDcB62a/versions/6U8JVjdw/modernfix-neoforge-5.27.4%2Bmc1.21.1.jar", + "sha1": "2f39363f0d6d5a5ccc2a9e0f50ad3385611c3cb7", + "size": 562051, + "side": "both" + }, + { + "source": "modrinth", + "slug": "ferrite-core", + "versionId": "x7kQWVju", + "version": "7.0.3-neoforge", + "path": "mods/ferritecore-7.0.3-neoforge.jar", + "url": "https://cdn.modrinth.com/data/uXXizFIs/versions/x7kQWVju/ferritecore-7.0.3-neoforge.jar", + "sha1": "9563692efb708b6b568df27a01ec52f6311928ef", + "size": 121559, + "side": "both" + }, + { + "source": "modrinth", + "slug": "architectury-api", + "versionId": "ZxYGwlk0", + "version": "13.0.8+neoforge", + "path": "mods/architectury-13.0.8-neoforge.jar", + "url": "https://cdn.modrinth.com/data/lhGA9TYQ/versions/ZxYGwlk0/architectury-13.0.8-neoforge.jar", + "sha1": "6ca11d3cc136bf69bb8f4d56982481eb85b5100b", + "size": 584004, + "side": "both" + }, + { + "source": "modrinth", + "slug": "rhino", + "versionId": "ZdLtebKH", + "version": "2101.2.7-build.81+Rhino-1.21", + "path": "mods/rhino-2101.2.7-build.81.jar", + "url": "https://cdn.modrinth.com/data/sk9knFPE/versions/ZdLtebKH/rhino-2101.2.7-build.81.jar", + "sha1": "480235a9f7749f68ce6fec3b9c3cac3428b92a4a", + "size": 882033, + "side": "both" + }, + { + "source": "modrinth", + "slug": "rpl", + "versionId": "hZ6B2Z0x", + "version": "2.1.2", + "path": "mods/ritchiesprojectilelib-2.1.2+mc.1.21.1-neoforge.jar", + "url": "https://cdn.modrinth.com/data/B3pb093D/versions/hZ6B2Z0x/ritchiesprojectilelib-2.1.2%2Bmc.1.21.1-neoforge.jar", + "sha1": "ec2e4996f8bee8714173e603e379fef8a6901765", + "size": 76369, + "side": "both" + }, + { + "source": "modrinth", + "slug": "kubejs", + "versionId": "Fe9CjPws", + "version": "2101.7.2-build.363", + "path": "mods/kubejs-neoforge-2101.7.2-build.363.jar", + "url": "https://cdn.modrinth.com/data/umyGl7zF/versions/Fe9CjPws/kubejs-neoforge-2101.7.2-build.363.jar", + "sha1": "d4e88254e8c26687d4c6aeb4dfa9c2ad70f260a2", + "size": 2270442, + "side": "both" + }, + { + "source": "modrinth", + "slug": "jei", + "versionId": "YAcQ6elZ", + "version": "19.27.0.340", + "path": "mods/jei-1.21.1-neoforge-19.27.0.340.jar", + "url": "https://cdn.modrinth.com/data/u6dRKJwZ/versions/YAcQ6elZ/jei-1.21.1-neoforge-19.27.0.340.jar", + "sha1": "27d0d85e7e32e926fc3664ab6815df5cdabb7941", + "size": 1529391, + "side": "both" + }, + { + "source": "modrinth", + "slug": "jade", + "versionId": "yd8FKCmx", + "version": "15.10.5+neoforge", + "path": "mods/Jade-1.21.1-NeoForge-15.10.5.jar", + "url": "https://cdn.modrinth.com/data/nvQzSEkH/versions/yd8FKCmx/Jade-1.21.1-NeoForge-15.10.5.jar", + "sha1": "d5bf134b3dbde9f5258666823900e21341dc0a50", + "size": 725742, + "side": "both" + }, + { + "source": "modrinth", + "slug": "chunky", + "versionId": "LuFhm4eU", + "version": "1.4.23", + "path": "mods/Chunky-NeoForge-1.4.23.jar", + "url": "https://cdn.modrinth.com/data/fALzjamp/versions/LuFhm4eU/Chunky-NeoForge-1.4.23.jar", + "sha1": "ab0c74743a653020fe2dfc4986b43e893947f3e9", + "size": 340572, + "side": "both" + }, + { + "source": "curseforge", + "slug": "ftb-library", + "fileId": "7746959", + "version": "2101.1.31", + "path": "mods/ftb-library-neoforge-2101.1.31.jar", + "url": "https://mediafilez.forgecdn.net/files/7746/959/ftb-library-neoforge-2101.1.31.jar", + "sha1": "686d4e784c28c14f7760cc22b2de6a8573b56b74", + "size": 1411181, + "side": "both" + }, + { + "source": "curseforge", + "slug": "ftb-teams", + "fileId": "7369021", + "version": "2101.1.9", + "path": "mods/ftb-teams-neoforge-2101.1.9.jar", + "url": "https://mediafilez.forgecdn.net/files/7369/21/ftb-teams-neoforge-2101.1.9.jar", + "sha1": "328e04bf1a445870aacea8fe7637670f84272a8f", + "size": 291847, + "side": "both" + }, + { + "source": "curseforge", + "slug": "ftb-chunks", + "fileId": "7608681", + "version": "2101.1.14", + "path": "mods/ftb-chunks-neoforge-2101.1.14.jar", + "url": "https://mediafilez.forgecdn.net/files/7608/681/ftb-chunks-neoforge-2101.1.14.jar", + "sha1": "908b63b11d0e00ae6c9557d3fe6440bdbcf21bb7", + "size": 642340, + "side": "both" + }, + { + "source": "modrinth", + "slug": "ars-nouveau", + "versionId": "BmGGrC9A", + "version": "5.11.3+mc1.21.1", + "path": "mods/ars_nouveau-1.21.1-5.11.3.jar", + "url": "https://cdn.modrinth.com/data/TKB6INcv/versions/BmGGrC9A/ars_nouveau-1.21.1-5.11.3.jar", + "sha1": "0af12dd7fda63a4261ceb302c9bb57fc235641c6", + "size": 20689115, + "side": "both" + }, + { + "source": "modrinth", + "slug": "terralith", + "versionId": "MuJMtPGQ", + "version": "2.5.8", + "path": "mods/Terralith_1.21.x_v2.5.8.jar", + "url": "https://cdn.modrinth.com/data/8oi3bsk5/versions/MuJMtPGQ/Terralith_1.21.x_v2.5.8.jar", + "sha1": "bee0cfb1a8cd4bf3d96bccea224fb45d74de9085", + "size": 3115385, + "side": "both" + }, + { + "source": "modrinth", + "slug": "yungs-better-strongholds", + "versionId": "8U0dIfSM", + "version": "1.21.1-NeoForge-5.1.3", + "path": "mods/YungsBetterStrongholds-1.21.1-NeoForge-5.1.3.jar", + "url": "https://cdn.modrinth.com/data/kidLKymU/versions/8U0dIfSM/YungsBetterStrongholds-1.21.1-NeoForge-5.1.3.jar", + "sha1": "5d06a5850af7c577612d4592706a8e156bbe1cbf", + "size": 461244, + "side": "server" + }, + { + "source": "modrinth", + "slug": "lithostitched", + "versionId": "IONexlgI", + "version": "1.7.2-neoforge-21.1", + "path": "mods/lithostitched-1.7.2-neoforge-21.1.jar", + "url": "https://cdn.modrinth.com/data/XaDC71GB/versions/IONexlgI/lithostitched-1.7.2-neoforge-21.1.jar", + "sha1": "ce35206214647131ebdf14212d1986349aeba79a", + "size": 810015, + "side": "server" + }, + { + "source": "modrinth", + "slug": "c2me-neoforge", + "versionId": "9iPiN34N", + "version": "0.3.0+alpha.0.91+1.21.1", + "path": "mods/c2me-neoforge-mc1.21.1-0.3.0+alpha.0.91.jar", + "url": "https://cdn.modrinth.com/data/COlSi5iR/versions/9iPiN34N/c2me-neoforge-mc1.21.1-0.3.0%2Balpha.0.91.jar", + "sha1": "c858c8becfb5205eb12aaf0420eb82c307c2e6a7", + "size": 4508649, + "side": "both" + }, + { + "source": "modrinth", + "slug": "noisium", + "versionId": "nJBE6tif", + "version": "2.3.0+mc1.21-1.21.1", + "path": "mods/noisium-neoforge-2.3.0+mc1.21-1.21.1.jar", + "url": "https://cdn.modrinth.com/data/KuNKN7d2/versions/nJBE6tif/noisium-neoforge-2.3.0%2Bmc1.21-1.21.1.jar", + "sha1": "1bea6b61378ba80f038256c4345d9ff3b67928c4", + "size": 60296, + "side": "server" + }, + { + "source": "modrinth", + "slug": "async-locator-refined", + "versionId": "3BdGHbV2", + "version": "1.21.1-1.5.3", + "path": "mods/async-locator-refined-neoforge-1.21.1-1.5.3.jar", + "url": "https://cdn.modrinth.com/data/LUIHK4LD/versions/3BdGHbV2/async-locator-refined-neoforge-1.21.1-1.5.3.jar", + "sha1": "2993e3efc6d211ad8d4db179851dea6fdfff4e07", + "size": 273320, + "side": "server" + }, + { + "source": "modrinth", + "slug": "servercore", + "versionId": "77MAnmOn", + "version": "1.5.10+1.21.1", + "path": "mods/servercore-neoforge-1.5.10+1.21.1.jar", + "url": "https://cdn.modrinth.com/data/4WWQxlQP/versions/77MAnmOn/servercore-neoforge-1.5.10%2B1.21.1.jar", + "sha1": "4524cd40cfa5019d8b5fbcb628b1616031838a0c", + "size": 1429522, + "side": "server" + }, + { + "source": "modrinth", + "slug": "lithium", + "versionId": "RXHf27Wv", + "version": "mc1.21.1-0.15.3-neoforge", + "path": "mods/lithium-neoforge-0.15.3+mc1.21.1.jar", + "url": "https://cdn.modrinth.com/data/gvQqBUqZ/versions/RXHf27Wv/lithium-neoforge-0.15.3%2Bmc1.21.1.jar", + "sha1": "9fd5fa9076044180ae7f51672de74669196ec72e", + "size": 774148, + "side": "both" + }, + { + "source": "modrinth", + "slug": "geckolib", + "versionId": "gFmrC8Ru", + "version": "4.8.4", + "path": "mods/geckolib-neoforge-1.21.1-4.8.4.jar", + "url": "https://cdn.modrinth.com/data/8BmcQJ2H/versions/gFmrC8Ru/geckolib-neoforge-1.21.1-4.8.4.jar", + "sha1": "eb854c8ec53ef922a5f3877a1aa4c1ce1352e0ce", + "size": 622582, + "side": "both" + }, + { + "source": "modrinth", + "slug": "curios", + "versionId": "yohfFbgD", + "version": "9.5.1+1.21.1", + "path": "mods/curios-neoforge-9.5.1+1.21.1.jar", + "url": "https://cdn.modrinth.com/data/vvuO3ImH/versions/yohfFbgD/curios-neoforge-9.5.1%2B1.21.1.jar", + "sha1": "418fcd42e3a7844c9bdc71c9b6401fdb3894e0c4", + "size": 410690, + "side": "both" + }, + { + "source": "modrinth", + "slug": "yungs-api", + "versionId": "ZB22DE9q", + "version": "1.21.1-NeoForge-5.1.6", + "path": "mods/YungsApi-1.21.1-NeoForge-5.1.6.jar", + "url": "https://cdn.modrinth.com/data/Ua7DFN59/versions/ZB22DE9q/YungsApi-1.21.1-NeoForge-5.1.6.jar", + "sha1": "e1c394779fb9e038e4f7a1b4558d0432607d263b", + "size": 388678, + "side": "both" + }, + { + "source": "modrinth", + "slug": "complementary-reimagined", + "versionId": "836bPNGo", + "version": "r5.7.1", + "path": "shaderpacks/ComplementaryReimagined_r5.7.1.zip", + "url": "https://cdn.modrinth.com/data/HVnmMxH1/versions/836bPNGo/ComplementaryReimagined_r5.7.1.zip", + "sha1": "b560f646a124d5204b1fb7321fec373b9c346fa5", + "size": 522970, + "side": "client" + } + ], + "defaultServer": { + "name": "Brass and Sigil", + "ip": "bns.sijbers.uk" + }, + "defaultShader": "ComplementaryReimagined_r5.7.1.zip", + "panelUrl": "https://bns-admin.sijbers.uk" +} diff --git a/scripts/Bootstrap-Sides.ps1 b/scripts/Bootstrap-Sides.ps1 new file mode 100644 index 0000000..2cbbc72 --- /dev/null +++ b/scripts/Bootstrap-Sides.ps1 @@ -0,0 +1,96 @@ +#requires -Version 7 +<# +.SYNOPSIS + Populate the `side` field on each mod entry in pack.lock.json by querying + Modrinth's project metadata. Run once after schema migration; thereafter, + Update-Pack.ps1 should set side automatically when adding new mods. + +.DESCRIPTION + Modrinth returns `client_side` and `server_side` per project, with values + `required` / `optional` / `unsupported` / `unknown`. We map: + server_side = unsupported -> side = "client" (skip on server) + client_side = unsupported -> side = "server" (skip on launcher) + both required/optional/unknown -> side = "both" (install everywhere) + + Conservative: only marks a mod side-restricted when the OTHER side is + explicitly `unsupported`. Anything ambiguous stays "both". + + CurseForge mods can't be queried (no API key here) -- left at "both" by + default, manual edit if needed. + +.PARAMETER LockPath + pack.lock.json to update in place. Defaults to ../pack/pack.lock.json. + +.PARAMETER DryRun + Print proposed changes without writing the file. +#> + +[CmdletBinding()] +param( + [string]$LockPath = $(Join-Path (Split-Path -Parent $MyInvocation.MyCommand.Path) '..\pack\pack.lock.json'), + [switch]$DryRun +) + +$ErrorActionPreference = 'Stop' +$ProgressPreference = 'SilentlyContinue' + +$LockPath = (Resolve-Path $LockPath).Path +$json = Get-Content $LockPath -Raw | ConvertFrom-Json -Depth 20 + +if (-not $json.mods) { throw "pack.lock.json has no 'mods' array." } + +function Get-ModrinthSide { + param([string]$Slug) + $proj = Invoke-RestMethod -Uri "https://api.modrinth.com/v2/project/$Slug" ` + -Headers @{ 'User-Agent' = 'BrassAndSigil-Launcher/0.1 (matt@sijbers.uk)' } + $cs = $proj.client_side; $ss = $proj.server_side + if ($ss -eq 'unsupported' -and $cs -ne 'unsupported') { return 'client' } + elseif ($cs -eq 'unsupported' -and $ss -ne 'unsupported') { return 'server' } + else { return 'both' } +} + +Write-Host "" +Write-Host "Bootstrapping side field on $($json.mods.Count) mods..." +Write-Host "" + +$changes = 0 +foreach ($mod in $json.mods) { + $existing = if ($mod.PSObject.Properties.Name -contains 'side') { $mod.side } else { $null } + if ($mod.source -ne 'modrinth') { + # CurseForge or other: default to 'both' if missing, leave alone otherwise. + if (-not $existing) { + $mod | Add-Member -NotePropertyName 'side' -NotePropertyValue 'both' -Force + Write-Host (" [{0}] {1,-26} {2}" -f $mod.source.PadRight(7).Substring(0,7), $mod.slug, 'both (default; non-modrinth)') + $changes++ + } + continue + } + try { + $side = Get-ModrinthSide -Slug $mod.slug + } catch { + Write-Warning (" $($mod.slug) lookup failed: $($_.Exception.Message) -- leaving as 'both'") + $side = 'both' + } + if ($existing -ne $side) { + $mod | Add-Member -NotePropertyName 'side' -NotePropertyValue $side -Force + Write-Host (" [{0}] {1,-26} {2}" -f $mod.source.PadRight(7).Substring(0,7), $mod.slug, $side) + $changes++ + } else { + Write-Host (" [{0}] {1,-26} {2} (unchanged)" -f $mod.source.PadRight(7).Substring(0,7), $mod.slug, $side) -ForegroundColor DarkGray + } + Start-Sleep -Milliseconds 100 # be polite to Modrinth's API +} + +Write-Host "" +Write-Host "$changes mod(s) updated." + +if ($DryRun) { + Write-Host "DryRun: not writing $LockPath." -ForegroundColor DarkGray + exit 0 +} + +# Round-trip via JSON: Modrinth-returned ConvertFrom-Json objects don't always +# round-trip with consistent property order, so we serialise with sorted keys +# at the mod level to keep the lockfile diff minimal. +$json | ConvertTo-Json -Depth 20 | Set-Content -Path $LockPath -Encoding utf8 +Write-Host "Wrote $LockPath" -ForegroundColor Green diff --git a/scripts/Build-Pack.ps1 b/scripts/Build-Pack.ps1 index 55d7056..12b305f 100644 --- a/scripts/Build-Pack.ps1 +++ b/scripts/Build-Pack.ps1 @@ -77,14 +77,21 @@ $files = @() $totalBytes = 0L foreach ($mod in $lock.mods) { - $files += [pscustomobject]@{ + # `side` is emitted only when explicitly set in the lockfile -- a missing or + # null lockfile side means "both", and we keep the manifest clean by omitting + # the field rather than serialising "both" everywhere. + $entry = [ordered]@{ path = $mod.path url = $mod.url sha1 = $mod.sha1 size = $mod.size } + $hasSide = ($mod.PSObject.Properties.Name -contains 'side') -and $mod.side -and $mod.side -ne 'both' + if ($hasSide) { $entry.side = $mod.side } + $files += [pscustomobject]$entry $totalBytes += $mod.size - Write-Host (" [{0}] {1,-26} {2,-22} {3,8:N0} KB" -f $mod.source.PadRight(7).Substring(0,7), $mod.slug, $mod.version, ($mod.size/1KB)) + $sideTag = if ($hasSide) { " [$($mod.side)]" } else { '' } + Write-Host (" [{0}] {1,-26} {2,-22} {3,8:N0} KB{4}" -f $mod.source.PadRight(7).Substring(0,7), $mod.slug, $mod.version, ($mod.size/1KB), $sideTag) } # Local overrides (configs, custom files not on Modrinth/CurseForge) diff --git a/server/Models/Manifest.cs b/server/Models/Manifest.cs index 63d184f..eafe635 100644 --- a/server/Models/Manifest.cs +++ b/server/Models/Manifest.cs @@ -37,6 +37,12 @@ public sealed class ManifestFile [JsonPropertyName("url")] public string Url { get; set; } = ""; [JsonPropertyName("sha1")] public string? Sha1 { get; set; } [JsonPropertyName("size")] public long? Size { get; set; } + + /// + /// "both" (default), "client", or "server". Server skips "client" files; + /// launcher skips "server" files. Null/missing = "both". + /// + [JsonPropertyName("side")] public string? Side { get; set; } } public sealed class PackLock @@ -59,4 +65,5 @@ public sealed class LockedMod [JsonPropertyName("url")] public string Url { get; set; } = ""; [JsonPropertyName("sha1")] public string Sha1 { get; set; } = ""; [JsonPropertyName("size")] public long Size { get; set; } + [JsonPropertyName("side")] public string? Side { get; set; } } diff --git a/server/Services/ManifestSync.cs b/server/Services/ManifestSync.cs index d6a7d49..099c2a4 100644 --- a/server/Services/ManifestSync.cs +++ b/server/Services/ManifestSync.cs @@ -38,12 +38,20 @@ public sealed class ManifestSync progress?.Report($"Pack: {manifest.Name} v{manifest.Version}"); Directory.CreateDirectory(serverDir); - // Resolve which mods are server-side. - var skipSlugs = await ResolveServerSideSkipListAsync(manifest, ct); + // Build-time tagging is authoritative: anything marked "client" is dropped + // outright, anything marked "server" or "both" is kept. We only fall back + // to the runtime Modrinth lookup for files with NO explicit side -- this + // keeps un-tagged (or CurseForge-only) mods safe and matches pre-side + // behaviour for them. + var needsLookup = manifest.Files.Where(f => string.IsNullOrEmpty(f.Side)).ToList(); + var skipSlugs = needsLookup.Count > 0 + ? await ResolveServerSideSkipListAsync( + new Manifest { Files = needsLookup }, ct) + : new HashSet(StringComparer.OrdinalIgnoreCase); // Build the filtered list of files to keep on the server. var keepFiles = manifest.Files - .Where(f => !ShouldSkipFile(f.Path, skipSlugs)) + .Where(f => !IsClientOnly(f) && (!string.IsNullOrEmpty(f.Side) || !ShouldSkipFile(f.Path, skipSlugs))) .ToList(); var skippedCount = manifest.Files.Count - keepFiles.Count; @@ -108,6 +116,9 @@ public sealed class ManifestSync || name.Equals(slug, StringComparison.OrdinalIgnoreCase)); } + private static bool IsClientOnly(ManifestFile f) + => string.Equals(f.Side, "client", StringComparison.OrdinalIgnoreCase); + /// Walk the manifest's mod URLs; for Modrinth ones, look up server_side; build a skip set. private async Task> ResolveServerSideSkipListAsync(Manifest manifest, CancellationToken ct) { -- 2.52.0 From 4ee4a2bb4335ade89efe1c9078c3cd2cb6a9932e Mon Sep 17 00:00:00 2001 From: Matt Date: Fri, 22 May 2026 14:23:09 +0000 Subject: [PATCH 2/2] scripts: make Build-Pack and Deploy-Brass cross-platform Two small fixes that surface when running the deploy pipeline on a non-Windows host (we now do this on glados via the Telegram bridge): - Build-Pack.ps1: fall back to launcher/ModpackLauncher.csproj when Get-Item.VersionInfo.FileVersion returns empty. Linux PowerShell can't parse PE FileVersion from a Windows .exe, so the previous code threw "set in ModpackLauncher.csproj and republish" -- but the csproj *was* set, just not readable from a foreign PE. Appends ".0" so the embedded value still matches the 4-part form the Windows compile bakes in. - Deploy-Brass.ps1: prefer rsync where available, fall back to robocopy otherwise. robocopy is Windows-only and bails on Linux/macOS hosts. --- scripts/Build-Pack.ps1 | 21 +++++++++++++++++++-- scripts/Deploy-Brass.ps1 | 25 ++++++++++++++++++++----- 2 files changed, 39 insertions(+), 7 deletions(-) diff --git a/scripts/Build-Pack.ps1 b/scripts/Build-Pack.ps1 index 12b305f..50965cd 100644 --- a/scripts/Build-Pack.ps1 +++ b/scripts/Build-Pack.ps1 @@ -163,8 +163,25 @@ if ($LauncherExePath) { } $launcherFile = Get-Item $LauncherExePath $launcherVersion = $launcherFile.VersionInfo.FileVersion + $versionSource = 'PE FileVersion' + + # Linux PowerShell can't read PE FileVersion from a Windows .exe. When that + # happens, fall back to launcher/ModpackLauncher.csproj's (the + # source of truth that the Windows compile bakes into FileVersion anyway). + # Append ".0" so callers get the same 4-part form as the PE metadata. if ([string]::IsNullOrWhiteSpace($launcherVersion)) { - throw "Launcher exe has no FileVersion -- set in ModpackLauncher.csproj and republish." + $csprojPath = Join-Path $here '..\launcher\ModpackLauncher.csproj' + if (Test-Path $csprojPath) { + [xml]$csproj = Get-Content $csprojPath + $csprojVersion = ($csproj.Project.PropertyGroup.Version | Where-Object { $_ }) | Select-Object -First 1 + if ($csprojVersion) { + $launcherVersion = if ($csprojVersion -match '^\d+\.\d+\.\d+$') { "$csprojVersion.0" } else { $csprojVersion } + $versionSource = "csproj (fallback)" + } + } + } + if ([string]::IsNullOrWhiteSpace($launcherVersion)) { + throw "Launcher exe has no FileVersion and csproj couldn't be read either -- set in ModpackLauncher.csproj and republish." } # FileVersion is the four-component form (e.g. "0.1.0.0" for csproj 0.1.0). # Embed as-is -- the launcher's Version.Parse handles it directly and avoids ambiguous @@ -174,7 +191,7 @@ if ($LauncherExePath) { Write-Host "" Write-Host ("Launcher metadata embedded:") - Write-Host (" Version: {0}" -f $launcherVersion) + Write-Host (" Version: {0} ({1})" -f $launcherVersion, $versionSource) Write-Host (" Url: {0}" -f $LauncherPublicUrl) Write-Host (" Source: {0}" -f $launcherFile.FullName) } diff --git a/scripts/Deploy-Brass.ps1 b/scripts/Deploy-Brass.ps1 index fe8c589..eab37c3 100644 --- a/scripts/Deploy-Brass.ps1 +++ b/scripts/Deploy-Brass.ps1 @@ -214,11 +214,26 @@ $overridesLocal = Join-Path $repoRoot 'pack\overrides' $shareFiles = Join-Path $DeployShare 'files' if ($shouldRunPack -and (Test-Path $overridesLocal)) { Step "Mirror pack/overrides/ -> $shareFiles" { - # /MIR makes destination match source (deletes orphan files in $shareFiles). - # /XJ skips junctions, /R:1 /W:1 keeps retry behaviour sane on flaky shares. - robocopy $overridesLocal $shareFiles /MIR /XJ /R:1 /W:1 /NFL /NDL /NJH /NJS /NP | Out-Host - # Robocopy returns 0-7 for success-with-info; 8+ is real failure. - if ($LASTEXITCODE -ge 8) { throw "robocopy failed with exit $LASTEXITCODE" } + # Prefer rsync where available (Linux/macOS); fall back to Windows robocopy. + # Both perform a mirror: orphan files in the destination are removed so + # the share matches the local overrides exactly. + if (Get-Command rsync -ErrorAction SilentlyContinue) { + $src = ($overridesLocal -replace '\\','/').TrimEnd('/') + '/' + $dst = ($shareFiles -replace '\\','/').TrimEnd('/') + '/' + if (-not (Test-Path $shareFiles)) { New-Item -ItemType Directory -Path $shareFiles -Force | Out-Null } + & rsync -a --delete $src $dst + if ($LASTEXITCODE -ne 0) { throw "rsync failed with exit $LASTEXITCODE" } + } + elseif (Get-Command robocopy -ErrorAction SilentlyContinue) { + # /MIR makes destination match source (deletes orphan files in $shareFiles). + # /XJ skips junctions, /R:1 /W:1 keeps retry behaviour sane on flaky shares. + robocopy $overridesLocal $shareFiles /MIR /XJ /R:1 /W:1 /NFL /NDL /NJH /NJS /NP | Out-Host + # Robocopy returns 0-7 for success-with-info; 8+ is real failure. + if ($LASTEXITCODE -ge 8) { throw "robocopy failed with exit $LASTEXITCODE" } + } + else { + throw "Neither rsync nor robocopy is available -- can't mirror pack overrides." + } } } -- 2.52.0