URGENT client-side fix. brassandsigil_tweaks-1.0.0.jar (built from
pack/tweaks/ into pack/overrides/mods/) declares lithostitched as a
hard dependency in its mods.toml. lithostitched is tagged side=server
in the lockfile because it's worldgen-only. brassandsigil_tweaks had
no explicit side tag so Build-Pack defaulted it to side=both. Clients
downloaded the tweak jar but not the dep -> NeoForge refused to start
with 'Mod brassandsigil_tweaks requires lithostitched 1.7 or above'.
Fix: Build-Pack now injects side='server' on any local-override file
under mods/. All tweak jars are data-only (worldgen modifiers, recipe
overrides) and pure server-side. If a future tweak is actually client-
relevant, this rule will need a per-file exception.
Tested in build output -- the brassandsigil_tweaks entry now shows
side='server' in the generated manifest.
Adds scripts/Check-Deps.ps1 and wires it into Deploy-Brass.ps1 as a
pre-flight gate when Stage includes Pack. The script parses each lockfile
jar's META-INF/neoforge.mods.toml directly -- the same source the loader
reads at startup -- and refuses to deploy if any [[dependencies]] block
with type=required (default) names a modId that nobody in the pack
provides.
Handles three real edge cases discovered during initial run:
1. Jar-in-jar bundled deps: Create ships Ponder, Registrate, Flywheel
embedded under META-INF/jarjar/. The script recurses into those and
counts their modIds as present, since the loader auto-loads them.
2. Language-provider jars (Kotlin for Forge) have no standard mods.toml
-- they declare via a different mechanism. Tiny slugToImplicitModId
override map (currently 1 entry) covers them.
3. Non-mod lockfile entries (shaderpacks under shaderpacks/, configs
under config/, etc.) are skipped explicitly by path prefix check.
Vanilla deps (minecraft, neoforge, forge, fabric, java, javafml, etc.)
are pre-seeded as present.
Jars cached at /tmp/bns-check-deps-cache/<sha1>.jar; second-run cost is
~1.5s. First run downloads ~150MB.
The script would have caught the v0.15.0 -> v0.15.1 incident: Slice &
Dice's jar declares kotlinforforge required in mods.toml even though
Modrinth's dep field only lists Create. The Modrinth-only check (earlier
draft) wouldn't have helped; the mods.toml-based check does.
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 <Version>
when Get-Item.VersionInfo.FileVersion returns empty. Linux PowerShell
can't parse PE FileVersion from a Windows .exe, so the previous code
threw "set <Version> in ModpackLauncher.csproj and republish" -- but
the csproj <Version> *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.
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
When ServerSshHost = 'local' the server-binary stage skips scp/ssh and
does an in-place Copy-Item + chmod + Move-Item on the local filesystem.
Pre-flight pgrep also runs locally. Allows running the deploy script on
the server itself (glados in our case) without setting up ssh-to-self.
Other deploy modes (remote scp/ssh) unchanged.
The previous check covered version, launcherVersion/Url, and a single
sampled self-hosted URL. Expanded to cover every field that has ever
been a foot-gun:
Required (must always be present and match pack.lock.json):
name, version, minecraft.version, loader.type, loader.version,
launcherVersion, launcherUrl
Optional but consistency-checked: if pack.lock.json declares the
field, the manifest MUST also have it (and defaultServer must include
a non-empty ip). Catches the case where someone removes the field
from pack.lock or Build-Pack drops it silently:
panelUrl, defaultShader, defaultServer
files[]: every entry validated for path/url/sha1/size shape; every
self-hosted (non-CDN) url checked for the expected host. Catches
partial-update bugs where some URLs migrate hosts but others don't.
URL host checks use the configured $ManifestPublicUrl host as the
source of truth; CDN-hosted mods (modrinth/forgecdn/curseforge) are
intentionally exempt.
Two safeguards so a -Stage Pack run never strips launcherVersion/
launcherUrl from the deployed manifest (the original bug that left
old launchers unable to see upgrade prompts):
1. Always pass -LauncherExePath to Build-Pack.ps1 when a local
publish exists, regardless of stage. Previously this only fired
for stages that included Launcher, so Pack-only deploys regressed
the manifest to a version with no launcher metadata.
2. New post-deploy verification step fetches the published manifest
and asserts: pack version matches lockfile, launcherVersion +
launcherUrl present, all self-hosted URLs use the configured
manifest host. Throws on any mismatch.
Adds three structural assertions that run after the zip is built:
1. No backslash separators in any entry name. NeoForge needs
"META-INF/neoforge.mods.toml" to be at exactly that forward-slash
path; PowerShell 5.1's [ZipFile]::CreateFromDirectory() puts
"META-INF\neoforge.mods.toml" instead, which the loader silently
rejects. The current build code uses CreateEntry with explicit
forward slashes, but this guard fires if anyone reverts to the
simpler-looking CreateFromDirectory.
2. META-INF/neoforge.mods.toml exists at the canonical path. Without
it, NeoForge skips the jar with a bland "not a valid mod file"
warning that's easy to miss in a 500-line game log.
3. The modId declared in the embedded TOML matches the source folder's
modId. Catches the case where a tweak folder is renamed but its
neoforge.mods.toml isn't updated, which would otherwise ship a jar
whose declared identity differs from its filename.
Each tweak jar's build line now tags "[validated]" so a casual reader
of the log sees that the post-build checks ran. Failure raises a
specific exception with the offending entries listed, so the build
fails loudly at the source instead of producing a pack that mysteriously
doesn't apply its tweaks at runtime.
Stage 5 of Deploy-Brass.ps1 was gated only on \$shouldRunLauncher, so a
"-Stage Pack" run regenerated the manifest locally + mirrored
pack/overrides/ to the share, but never copied the new manifest.json
itself. Result: tweak jars/configs landed on the share, but clients
fetching the (still-old) manifest never knew about the new SHA-1s and
skipped the re-sync. Caught when fixing the brassandsigil_tweaks jar:
the public manifest stayed at 0.9.2 even though local was 0.9.3.
Split into two stages -- launcher exe stays gated on \$shouldRunLauncher,
manifest.json now publishes whenever \$shouldRunPack (so any Pack, All,
or Launcher deploy includes it).
NeoForge's mod scanner rejected brassandsigil_tweaks-1.0.0.jar because
PowerShell 5.1's [ZipFile]::CreateFromDirectory() writes Windows-native
path separators into ZIP entry names on Windows. Entries came out as
"META-INF\neoforge.mods.toml" instead of the spec-required forward-slash
form, so the loader couldn't find the manifest and silently dropped the
jar with "not a valid mod file".
Build-Tweaks.ps1 now opens the archive in 'Create' mode and writes each
file as an explicit ZipArchiveEntry whose name is built from the relative
path with backslashes replaced by forward slashes. Verified the rebuilt
jar lists "META-INF/neoforge.mods.toml" etc.
Pack version 0.9.2 -> 0.9.3 so launchers cached at 0.9.2 see "pack
changed" and re-sync the new tweak jar (their bytes differ; SHA-1 in the
manifest will reflect that).