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..50965cd 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)
@@ -156,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
@@ -167,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."
+ }
}
}
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)
{