#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://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://sijbers.uk/pack/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) { $files += [pscustomobject]@{ path = $mod.path url = $mod.url sha1 = $mod.sha1 size = $mod.size } $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)) } # 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() $files += [pscustomobject]@{ path = $rel url = "$base/$rel" sha1 = $sha1 size = $_.Length } $totalBytes += $_.Length Write-Host (" [local] {0}" -f $rel) } } } $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 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 if ([string]::IsNullOrWhiteSpace($launcherVersion)) { throw "Launcher exe has no FileVersion -- 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 # comparisons between trimmed and untrimmed forms. $manifest.launcherVersion = $launcherVersion $manifest.launcherUrl = $LauncherPublicUrl Write-Host "" Write-Host ("Launcher metadata embedded:") Write-Host (" Version: {0}" -f $launcherVersion) 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))