Files
brass-and-sigil/scripts/Build-Pack.ps1
T
Matt 135ecf4a5a Build-Pack: default tweak jars to side=server, 0.22.2 -> 0.22.3
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.
2026-05-23 17:40:34 +00:00

221 lines
9.8 KiB
PowerShell

#requires -Version 5
<#
.SYNOPSIS
Generates manifest.json deterministically from pack.lock.json.
.DESCRIPTION
The lockfile (pack.lock.json) is the source of truth for every mod's exact
version, URL, SHA-1, and size. Running this script does not change versions.
To intentionally bump versions, use Update-Pack.ps1 with the -Refresh flag.
To see what new versions are available, use Check-Updates.ps1.
Workflow:
1. Edit pack.lock.json (manually or via Update-Pack.ps1 -Refresh)
2. Run Build-Pack.ps1 -OutputPath ...\manifest.json
3. Update the server to match
4. Deploy manifest to your hosted URL
.PARAMETER OutputPath
Where to write manifest.json.
.PARAMETER LocalPackSource
Optional folder containing local override files (configs, resourcepacks, etc.).
.PARAMETER SelfHostBaseUrl
Public URL prefix for self-hosted files (only used when LocalPackSource is set).
#>
[CmdletBinding()]
param(
[string]$OutputPath = ".\manifest.json",
[string]$LocalPackSource = "",
[string]$SelfHostBaseUrl = "https://bns.sijbers.uk/pack/files",
# Optional -- point at a published launcher .exe to embed launcherVersion + launcherUrl
# in the manifest. The launcher displays a "newer version available" banner when
# its embedded version is lower than this. Skip entirely to leave both fields out.
[string]$LauncherExePath = "",
[string]$LauncherPublicUrl = "https://bns.sijbers.uk/launcher/BrassAndSigil-Launcher.exe",
# Skip the auto-invoke of Build-Tweaks.ps1 (only touch this if you're debugging).
[switch]$SkipTweaks
)
$ErrorActionPreference = 'Stop'
$ProgressPreference = 'SilentlyContinue'
$here = Split-Path -Parent $MyInvocation.MyCommand.Path
$lockPath = Join-Path $here '..\pack\pack.lock.json'
# Build any data-only tweak jars first, then fold the resulting overrides
# folder into the manifest. Set -SkipTweaks to bypass. The "overrides" name
# matches CurseForge/Modrinth modpack conventions: files that override or
# augment the standard mod set, hosted by us.
$tweaksRoot = Join-Path $here '..\pack\tweaks'
$overridesRoot = Join-Path $here '..\pack\overrides'
if (-not $SkipTweaks -and (Test-Path $tweaksRoot)) {
& (Join-Path $here 'Build-Tweaks.ps1') -TweaksRoot $tweaksRoot -OutputRoot (Join-Path $overridesRoot 'mods')
}
if (-not $LocalPackSource -and (Test-Path $overridesRoot)) {
$LocalPackSource = $overridesRoot
}
if (-not (Test-Path $lockPath)) {
throw "pack.lock.json not found at $lockPath. Run Update-Pack.ps1 -Refresh first to bootstrap it."
}
$lock = Get-Content $lockPath -Raw | ConvertFrom-Json
Write-Host ""
Write-Host "Building manifest from pack.lock.json:"
Write-Host (" Pack: {0} v{1}" -f $lock.name, $lock.version)
Write-Host (" MC: {0} ({1} {2})" -f $lock.minecraft, $lock.loader.type, $lock.loader.version)
Write-Host (" Locked: {0}" -f $lock.lockedAt)
Write-Host (" Mods: {0}" -f $lock.mods.Count)
Write-Host ""
$files = @()
$totalBytes = 0L
foreach ($mod in $lock.mods) {
# `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
$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)
if ($LocalPackSource -and (Test-Path $LocalPackSource)) {
Write-Host ""
Write-Host "Adding local overrides from $LocalPackSource..."
$managedRoots = @('mods', 'config', 'resourcepacks', 'shaderpacks', 'kubejs', 'defaultconfigs')
$base = $SelfHostBaseUrl.TrimEnd('/')
$sourceFull = (Resolve-Path $LocalPackSource).Path.TrimEnd('\','/')
foreach ($root in $managedRoots) {
$rootDir = Join-Path $LocalPackSource $root
if (-not (Test-Path $rootDir)) { continue }
Get-ChildItem -Path $rootDir -Recurse -File | ForEach-Object {
$rel = $_.FullName.Substring($sourceFull.Length).TrimStart('\','/') -replace '\\','/'
$sha1 = (Get-FileHash -Algorithm SHA1 -Path $_.FullName).Hash.ToLowerInvariant()
# Tweak jars (built by Build-Tweaks.ps1 into pack/overrides/mods/)
# are pure data-only NeoForge mods -- worldgen modifiers, recipe
# overrides, etc. They typically depend on server-only libraries
# (Lithostitched, etc.) and have no client-side rendering. Default
# them to side="server" so the client doesn't try to load them
# without their server-only deps -> the loader otherwise refuses to
# start with "Mod X requires Y or above" errors. If a future tweak
# is genuinely client-relevant, override by editing this rule.
$entry = [ordered]@{
path = $rel
url = "$base/$rel"
sha1 = $sha1
size = $_.Length
}
if ($root -eq 'mods') { $entry.side = 'server' }
$files += [pscustomobject]$entry
$totalBytes += $_.Length
$sideTag = if ($entry.side) { " [$($entry.side)]" } else { '' }
Write-Host (" [local] {0}{1}" -f $rel, $sideTag)
}
}
}
$manifest = [ordered]@{
name = $lock.name
version = $lock.version
minecraft = [ordered]@{ version = $lock.minecraft }
loader = [ordered]@{ type = $lock.loader.type; version = $lock.loader.version }
files = $files
}
# Optional: copy a defaultServer block through from the lockfile so the launcher
# can pre-populate friends' multiplayer list. Lockfile schema:
# "defaultServer": { "name": "Brass & Sigil", "ip": "bns.sijbers.uk" }
if ($lock.PSObject.Properties.Name -contains "defaultServer" -and $lock.defaultServer) {
$manifest.defaultServer = [ordered]@{
name = $lock.defaultServer.name
ip = $lock.defaultServer.ip
}
Write-Host (" Default server: {0} ({1})" -f $lock.defaultServer.name, $lock.defaultServer.ip)
}
# Optional: shader pack to enable by default on fresh installs. Lockfile schema:
# "defaultShader": "ComplementaryReimagined_r5.7.1.zip"
if ($lock.PSObject.Properties.Name -contains "defaultShader" -and $lock.defaultShader) {
$manifest.defaultShader = $lock.defaultShader
Write-Host (" Default shader: {0}" -f $lock.defaultShader)
}
# Optional: public URL of the brass-sigil-server panel. Used by the launcher
# to send friend-side whitelist requests. Lockfile schema:
# "panelUrl": "https://bns-admin.sijbers.uk"
if ($lock.PSObject.Properties.Name -contains "panelUrl" -and $lock.panelUrl) {
$manifest.panelUrl = $lock.panelUrl
Write-Host (" Panel URL: {0}" -f $lock.panelUrl)
}
# Optional launcher metadata: when -LauncherExePath is supplied, read the exe's
# FileVersion (set via <Version> in ModpackLauncher.csproj) and embed it. The
# launcher compares its assembly version to this value and shows an upgrade
# banner pointing at LauncherPublicUrl when older.
if ($LauncherExePath) {
if (-not (Test-Path $LauncherExePath)) {
throw "LauncherExePath '$LauncherExePath' does not exist."
}
$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 <Version> (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)) {
$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 <Version> (fallback)"
}
}
}
if ([string]::IsNullOrWhiteSpace($launcherVersion)) {
throw "Launcher exe has no FileVersion and csproj <Version> couldn't be read either -- set <Version> in ModpackLauncher.csproj and republish."
}
# FileVersion is the four-component form (e.g. "0.1.0.0" for csproj <Version>0.1.0</Version>).
# Embed as-is -- the launcher's Version.Parse handles it directly and avoids ambiguous
# comparisons between trimmed and untrimmed forms.
$manifest.launcherVersion = $launcherVersion
$manifest.launcherUrl = $LauncherPublicUrl
Write-Host ""
Write-Host ("Launcher metadata embedded:")
Write-Host (" Version: {0} ({1})" -f $launcherVersion, $versionSource)
Write-Host (" Url: {0}" -f $LauncherPublicUrl)
Write-Host (" Source: {0}" -f $launcherFile.FullName)
}
$json = $manifest | ConvertTo-Json -Depth 10
$outDir = Split-Path -Parent $OutputPath
if ($outDir -and -not (Test-Path $outDir)) { New-Item -ItemType Directory -Path $outDir | Out-Null }
[System.IO.File]::WriteAllText($OutputPath, $json, [System.Text.UTF8Encoding]::new($false))
Write-Host ""
Write-Host "Manifest written: $OutputPath"
Write-Host (" Files: {0}" -f $files.Count)
Write-Host (" Total: {0:N1} MB (download size)" -f ($totalBytes / 1MB))