Compare commits

...

11 Commits

Author SHA1 Message Date
Matt 50007e1109 pack: track pack/overrides/defaultconfigs/ + ship Waystones override
.gitignore previously ignored all of pack/overrides/ because that dir is
the build output of Build-Tweaks.ps1. But defaultconfigs/ inside it holds
hand-written source-of-truth configs that should be in git (otherwise
they don't survive cross-machine deploys). Whitelisting that subdir.

Adds pack/overrides/defaultconfigs/waystones-common.toml -- the same
file the empty 0.16.1 commit was supposed to ship but couldn't because
of the .gitignore rule. Bumps to 0.16.2.

The override changes just one key:
- spawnInVillages: REGULAR -> DISABLED

All other Waystones settings stay at mod default (ACTIVATION visibility,
PLAYER restriction, empty allowedVisibilities) which already provides
the base-intrusion protections we discussed.
2026-05-23 12:56:13 +00:00
Matt d4dfcb1e11 Merge pull request 'Tune Waystones config + ship via defaultconfigs' (#11) from feature/waystones-config into main 2026-05-23 12:55:19 +00:00
Matt 3dc0767a24 pack: tune Waystones config + bump to 0.16.1
Ships an override of waystones-common.toml under pack/overrides/
defaultconfigs/ so first-run installs (clients + fresh servers) get the
intended defaults. NeoForge auto-copies defaultconfigs/<file> into
config/<file> on first run only -- player-tweaked configs after that
are preserved across pack updates.

The only override vs mod-default in this commit:
- spawnInVillages: REGULAR -> DISABLED

Everything else stays at mod default, which already gives the security
posture we want:
- defaultVisibility = ACTIVATION  -> each player must physically visit
  a waystone before they can teleport TO it. Solves the "player B
  teleports into player A's base" concern without a hard per-player
  placement cap (which v21.x dropped).
- restrictedWaystones = [PLAYER]  -> only owners can edit their own
  waystones.
- allowedVisibilities = []        -> no player UI can create global
  waystones; OPs use the /waystone command for server-public ones.

Wild waystones (in the open world, not villages) stay on -- they're
public-good fast travel, not tied to bases.
2026-05-23 12:55:18 +00:00
Matt c996d4edff Merge pull request 'Add Waystones + Balm' (#10) from feature/add-waystones into main 2026-05-23 12:53:27 +00:00
Matt b080dfe325 pack: add Waystones + Balm, bump to 0.16.0
Waystones 21.1.33: fast-travel teleport network.
Balm 21.0.58: required common lib for Waystones (and other BlayTheNinth mods).

Config will be tweaked in a follow-up after the server generates its
default config files. Intended config:
- worldgen village waystones DISABLED
- maxWaystonesPerPlayer = 1
- defaultPrivacy = Private (player-placed)
- OP-placed global waystones via /waystone command
2026-05-23 12:53:26 +00:00
Matt ce28a44e89 Merge pull request 'Add Check-Deps pre-flight (mods.toml ground truth)' (#9) from feature/check-deps-preflight into main 2026-05-23 11:55:38 +00:00
Matt 40dd06b8cf scripts: add Check-Deps pre-flight (mods.toml ground truth)
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.
2026-05-23 11:55:38 +00:00
Matt 06d6a5ef45 Merge pull request 'Hotfix: add Kotlin for Forge (Slice & Dice dep)' (#8) from fix/kotlin-for-forge-dep into main 2026-05-22 21:00:38 +00:00
Matt 4ea919432e pack: add Kotlin for Forge 5.11.0, bump to 0.15.1
Hotfix for 0.15.0: Slice & Dice declares 'kotlinforforge >= 5.8' as a
hard dependency and the server crashed on load without it. Adding KFF
satisfies the dep. 6.9 MB, both sides.
2026-05-22 21:00:37 +00:00
Matt 6ccd83da5f Merge pull request 'Add Farmer's Delight + Slice & Dice' (#7) from feature/add-farmers-delight into main 2026-05-22 20:58:38 +00:00
Matt 89c41dfe4d pack: add Farmer's Delight + Slice & Dice, bump to 0.15.0
- Farmer's Delight 1.3.2 (3.2 MB): cooking expansion -- new crops
  (tomato, onion, rice, cabbage), cooking pot, stove, cutting board,
  knife, ~50+ food recipes. Both sides required.
- Create Slice & Dice 4.2.4 (407 KB): FD<->Create automation bridge.
  Adds a Slicer machine (Mechanical Press/Mixer-style) that automates
  the knife/cutting-board step so Create kinetic networks can feed FD
  prep. Loader-shared jar (forge+neoforge from one file).

Both tagged "both". Brings live mod count to 41.
2026-05-22 20:58:37 +00:00
4 changed files with 340 additions and 1 deletions
+6
View File
@@ -60,3 +60,9 @@ pack/overrides/
# (default OutputPath) and copied to the deploy share by Deploy-Brass.ps1
scripts/manifest.json
pack/manifest.json
# pack/overrides/ is build output, but defaultconfigs/ holds hand-written
# source-of-truth configs we ship as first-run defaults (see Build-Pack.ps1).
# Whitelist that subdir so it lives in git like other source files.
!pack/overrides/defaultconfigs/
!pack/overrides/defaultconfigs/**
+56 -1
View File
@@ -1,7 +1,7 @@
{
"$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.14.0",
"version": "0.16.2",
"minecraft": "1.21.1",
"loader": {
"type": "neoforge",
@@ -426,6 +426,61 @@
"sha1": "6dae57f4f50f7808d2ed9a18f6cd1c0d4640c6bc",
"size": 73938,
"side": "client"
},
{
"source": "modrinth",
"slug": "farmers-delight",
"versionId": "GbNuOZ4S",
"version": "1.21.1-1.3.2",
"path": "mods/FarmersDelight-1.21.1-1.3.2.jar",
"url": "https://cdn.modrinth.com/data/R2OftAxM/versions/GbNuOZ4S/FarmersDelight-1.21.1-1.3.2.jar",
"sha1": "bf7e7dede99e832de20e191b1f6c11f7a9b9a622",
"size": 3163042,
"side": "both"
},
{
"source": "modrinth",
"slug": "slice-and-dice",
"versionId": "tyVnEa75",
"version": "4.2.4",
"path": "mods/sliceanddice-forge-4.2.4.jar",
"url": "https://cdn.modrinth.com/data/GmjmRQ0A/versions/tyVnEa75/sliceanddice-forge-4.2.4.jar",
"sha1": "13d8835813b3851d4818c59ec8f7124e4a826c14",
"size": 406699,
"side": "both"
},
{
"source": "modrinth",
"slug": "kotlin-for-forge",
"versionId": "NrSebcsG",
"version": "5.11.0",
"path": "mods/kotlinforforge-5.11.0-all.jar",
"url": "https://cdn.modrinth.com/data/ordsPcFz/versions/NrSebcsG/kotlinforforge-5.11.0-all.jar",
"sha1": "4d83e8a8e8cb06b306d2cea1e903680fe513f1f3",
"size": 6869790,
"side": "both"
},
{
"source": "modrinth",
"slug": "balm",
"versionId": "3XiinKbh",
"version": "21.0.58+neoforge-1.21.1",
"path": "mods/balm-neoforge-1.21.1-21.0.58.jar",
"url": "https://cdn.modrinth.com/data/MBAkmtvl/versions/3XiinKbh/balm-neoforge-1.21.1-21.0.58.jar",
"sha1": "47b6dbe51693d5a2b3eac15c6322f357de365f15",
"size": 701868,
"side": "both"
},
{
"source": "modrinth",
"slug": "waystones",
"versionId": "PXntW965",
"version": "21.1.33+neoforge-1.21.1",
"path": "mods/waystones-neoforge-1.21.1-21.1.33.jar",
"url": "https://cdn.modrinth.com/data/LOpKHB2A/versions/PXntW965/waystones-neoforge-1.21.1-21.1.33.jar",
"sha1": "488bc70db3730f209e65cbda04c36dbc3918c448",
"size": 878265,
"side": "both"
}
],
"defaultServer": {
+267
View File
@@ -0,0 +1,267 @@
#requires -Version 7
<#
.SYNOPSIS
Audit pack.lock.json for missing required mod dependencies by reading
each jar's mods.toml (and any jar-in-jar bundles) directly. Loader-truth,
not Modrinth-truth.
.DESCRIPTION
Each NeoForge/Forge mod declares its real dependencies in
META-INF/neoforge.mods.toml (or META-INF/mods.toml) inside the jar.
This is what the loader actually checks at startup; the Modrinth
dependency field is metadata authors fill in (or don't).
The script also recurses into META-INF/jarjar/ bundles. Big mods like
Create ship their deps (Registrate, Flywheel, Ponder) embedded as
jar-in-jar -- the loader auto-loads them, so their modIds count as
"present" without explicit lockfile entries.
Slug filter: only entries under "mods/" are treated as mod jars.
Shaderpacks and other managed-but-non-mod files are skipped.
Language-provider jars (e.g. Kotlin for Forge) ship without a standard
mods.toml; their modIds are added via a tiny override map.
Jars are cached in /tmp/bns-check-deps-cache/<sha1>.jar so subsequent
runs are instant.
.PARAMETER LockPath
.PARAMETER CacheDir
.PARAMETER WarnOnly
Print missing deps but exit 0. Deploy-Brass.ps1 calls without it.
#>
[CmdletBinding()]
param(
[string]$LockPath = $(Join-Path (Split-Path -Parent $MyInvocation.MyCommand.Path) '..\pack\pack.lock.json'),
[string]$CacheDir = '/tmp/bns-check-deps-cache',
[switch]$WarnOnly
)
$ErrorActionPreference = 'Stop'
$ProgressPreference = 'SilentlyContinue'
$LockPath = (Resolve-Path $LockPath).Path
$lock = Get-Content $LockPath -Raw | ConvertFrom-Json -Depth 20
if (-not $lock.mods) { throw "pack.lock.json has no 'mods' array." }
if (-not (Test-Path $CacheDir)) { New-Item -ItemType Directory -Path $CacheDir | Out-Null }
# Vanilla / loader IDs are always satisfied -- they're never modpack mods.
$vanillaIds = @('minecraft','neoforge','forge','fabric','fabricloader','quilt','java','javafml','lowcodefml')
# Language-provider / library jars that ship without a standard mods.toml.
# Map their lockfile slug to the modId they register at runtime. Add entries
# here as new ones are encountered.
$slugToImplicitModId = @{
'kotlin-for-forge' = 'kotlinforforge'
}
Add-Type -AssemblyName System.IO.Compression
Add-Type -AssemblyName System.IO.Compression.FileSystem
function Get-JarPath {
param($mod)
$cached = Join-Path $CacheDir ("$($mod.sha1).jar")
if (Test-Path $cached) { return $cached }
Write-Host " downloading $($mod.slug) ..." -ForegroundColor DarkGray
try { Invoke-WebRequest -Uri $mod.url -OutFile $cached -TimeoutSec 60 }
catch { throw "Failed to download $($mod.url): $($_.Exception.Message)" }
return $cached
}
# Read a specific entry's bytes from an already-open ZipArchive into memory.
function Read-ZipEntry {
param([System.IO.Compression.ZipArchive]$Zip, [string]$EntryName)
$entry = $Zip.GetEntry($EntryName)
if (-not $entry) { return $null }
$ms = New-Object System.IO.MemoryStream
$s = $entry.Open()
try { $s.CopyTo($ms) } finally { $s.Dispose() }
return $ms.ToArray()
}
function Read-ZipEntryText {
param([System.IO.Compression.ZipArchive]$Zip, [string]$EntryName)
$entry = $Zip.GetEntry($EntryName)
if (-not $entry) { return $null }
$reader = New-Object System.IO.StreamReader($entry.Open())
try { return $reader.ReadToEnd() }
finally { $reader.Dispose() }
}
# Tiny TOML reader -- only the bits we need (sections + key="value" lines).
function Parse-ModsToml {
param([string]$Text)
$modIds = @()
$deps = @()
$section = $null
$current = $null
function Flush-Record {
param($refModIds, $refDeps, $sec, $cur)
if (-not $cur) { return }
if ($sec -eq 'mods' -and $cur.modId) {
$refModIds.Value += $cur.modId
} elseif ($sec -like 'dep:*' -and $cur.modId) {
$refDeps.Value += [pscustomobject]@{
parent = $sec.Substring(4)
modId = $cur.modId
type = $cur.type
mandatory = $cur.mandatory
versionRange = $cur.versionRange
}
}
}
$modIdsRef = [ref]$modIds
$depsRef = [ref]$deps
foreach ($raw in ($Text -split "`n")) {
$line = $raw.Trim()
if (-not $line -or $line.StartsWith('#')) { continue }
if ($line.StartsWith('[[')) {
Flush-Record $modIdsRef $depsRef $section $current
$current = $null
if ($line -match '^\[\[mods\]\]') {
$section = 'mods'; $current = @{}
} elseif ($line -match '^\[\[dependencies\.([^\]]+)\]\]') {
$section = "dep:$($Matches[1])"; $current = @{}
} else {
$section = $null
}
continue
}
if (-not $current) { continue }
if ($line -match '^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.+?)\s*$') {
$k = $Matches[1]; $v = $Matches[2].Trim()
if ($v.StartsWith('"') -and $v.EndsWith('"')) { $v = $v.Substring(1, $v.Length - 2) }
$current[$k] = $v
}
}
Flush-Record $modIdsRef $depsRef $section $current
return [pscustomobject]@{ ModIds = $modIdsRef.Value; Deps = $depsRef.Value }
}
function Is-Required {
param($dep)
if ($dep.type) { return ($dep.type -eq 'required' -or $dep.type -eq 'mandatory') }
if ($dep.mandatory) { return ($dep.mandatory -eq 'true') }
return $true # NeoForge default
}
# Recursively walk a jar (and its META-INF/jarjar/ embedded jars) collecting
# every modId declared by mods.toml inside. Used to satisfy deps that ship
# bundled (e.g. ponder/registrate/flywheel inside Create).
function Collect-Jar-Identity {
param([string]$JarPath)
$modIds = @()
$deps = @()
$zip = [System.IO.Compression.ZipFile]::OpenRead($JarPath)
try {
# 1) This jar's own mods.toml (if any).
$toml = Read-ZipEntryText -Zip $zip -EntryName 'META-INF/neoforge.mods.toml'
if (-not $toml) { $toml = Read-ZipEntryText -Zip $zip -EntryName 'META-INF/mods.toml' }
if ($toml) {
$parsed = Parse-ModsToml -Text $toml
$modIds += $parsed.ModIds
$deps += $parsed.Deps
}
# 2) jar-in-jar bundles -- recursively extract + collect modIds (we only
# need their modIds; their deps are the parent's problem to satisfy).
$jarjarEntries = $zip.Entries | Where-Object {
$_.FullName -like 'META-INF/jarjar/*.jar'
}
foreach ($e in $jarjarEntries) {
$tmpInner = [System.IO.Path]::GetTempFileName() + '.jar'
$s = $e.Open()
try {
$fs = [System.IO.File]::Open($tmpInner, [System.IO.FileMode]::Create)
try { $s.CopyTo($fs) } finally { $fs.Dispose() }
} finally { $s.Dispose() }
$inner = Collect-Jar-Identity -JarPath $tmpInner
$modIds += $inner.ModIds # bundled deps count as present
# Don't propagate bundled deps -- they're satisfied within this jar
Remove-Item $tmpInner -Force -ErrorAction SilentlyContinue
}
} finally {
$zip.Dispose()
}
return [pscustomobject]@{ ModIds = ($modIds | Select-Object -Unique); Deps = $deps }
}
Write-Host ""
Write-Host "Auditing $($lock.mods.Count) lockfile entries (parsing each jar's mods.toml + jar-in-jar bundles)..."
Write-Host ""
$presentModIds = New-Object 'System.Collections.Generic.HashSet[string]' ([System.StringComparer]::OrdinalIgnoreCase)
foreach ($v in $vanillaIds) { [void]$presentModIds.Add($v) }
# parent modId -> list of dep records (so the missing report blames a modId, not a slug)
$jarDepsByParentModId = @{}
$skipped = @() # informational
$count = 0
foreach ($m in $lock.mods) {
$count++
# Only consider entries that live under mods/ -- shaderpacks, configs etc.
# are managed by the launcher but aren't mods.
if (-not ($m.path -like 'mods/*')) {
$skipped += "$($m.slug): not under mods/ ($($m.path))"
continue
}
# Language-provider override (kotlin-for-forge etc.)
if ($slugToImplicitModId.ContainsKey($m.slug)) {
[void]$presentModIds.Add($slugToImplicitModId[$m.slug])
}
try {
$jar = Get-JarPath -mod $m
$ident = Collect-Jar-Identity -JarPath $jar
foreach ($id in $ident.ModIds) {
[void]$presentModIds.Add($id)
}
foreach ($id in $ident.ModIds) {
$jarDepsByParentModId[$id] = $ident.Deps
}
if (-not $ident.ModIds -and -not $slugToImplicitModId.ContainsKey($m.slug)) {
$skipped += "$($m.slug): no modId discovered (no mods.toml; consider adding to override map)"
}
} catch {
$skipped += "$($m.slug): $($_.Exception.Message)"
}
if ($count % 10 -eq 0) { Write-Host " ...processed $count/$($lock.mods.Count)" -ForegroundColor DarkGray }
}
# Pass 2: check each parent's required deps against the present set.
$missing = @{}
foreach ($parentId in $jarDepsByParentModId.Keys) {
foreach ($dep in $jarDepsByParentModId[$parentId]) {
if (-not (Is-Required $dep)) { continue }
if (-not $presentModIds.Contains($dep.modId)) {
if (-not $missing.ContainsKey($dep.modId)) {
$missing[$dep.modId] = @{ parents = @() }
}
$missing[$dep.modId].parents += $parentId
}
}
}
Write-Host ""
Write-Host ("=" * 60)
if ($missing.Count -eq 0) {
Write-Host "All required deps satisfied. ($($presentModIds.Count) mod IDs in pack, incl. bundled jar-in-jar)" -ForegroundColor Green
} else {
Write-Host "MISSING REQUIRED DEPS: $($missing.Count) unique" -ForegroundColor Red
Write-Host ""
foreach ($depId in ($missing.Keys | Sort-Object)) {
$parents = ($missing[$depId].parents | Sort-Object -Unique) -join ', '
Write-Host " $depId" -ForegroundColor Red
Write-Host " needed by: $parents" -ForegroundColor DarkGray
}
}
if ($skipped.Count -gt 0) {
Write-Host ""
Write-Host "Informational (skipped / unanalysable):" -ForegroundColor DarkGray
foreach ($s in $skipped) { Write-Host " - $s" -ForegroundColor DarkGray }
}
if ($missing.Count -gt 0 -and -not $WarnOnly) {
throw "Missing required dependencies. Add them to pack/pack.lock.json (or pass -WarnOnly to bypass)."
}
+11
View File
@@ -118,6 +118,17 @@ if (($shouldRunPack -or $shouldRunLauncher) -and -not $DryRun -and -not $Force)
}
}
# ─── Pre-flight: required-dependency check ─────────────────────────────────
# Walks each Modrinth mod in pack.lock.json, fetches its 'required'
# dependencies, and refuses to deploy if any aren't already in the lockfile.
# Catches the failure mode where adding a mod that hard-deps on (say)
# kotlinforforge crash-loops the daemon at startup. Only runs when pack
# content is changing; -Force skips it (rarely needed).
if ($shouldRunPack -and -not $DryRun -and -not $Force) {
Write-Host "Pre-flight: validating Modrinth dependency graph..." -ForegroundColor DarkGray
& (Join-Path $here 'Check-Deps.ps1')
}
# ─── Pre-flight: was the daemon already running? ───────────────────────────
# We don't auto-stop it (kicks active players to fix a problem that isn't
# actually broken -- the atomic swap is safe with the daemon running). But