Compare commits

..

11 Commits

Author SHA1 Message Date
Matt 0bdc48c86f gitignore: scope overrides ignore to mods/, track defaultconfigs/
The previous rule 'pack/overrides/' ignored the whole directory, which
made it impossible to re-include subpaths via ! exception (git's rule).

Narrow the ignore to 'pack/overrides/mods/' -- the only path that
Build-Tweaks.ps1 actually writes -- and drop the broken whitelist
block. Other subdirs under pack/overrides/ (defaultconfigs/, etc.) now
live in git like normal source.

Adds the Waystones override that 0.16.1 attempted (spawnInVillages =
DISABLED). Bumps to 0.16.3 so launcher caches re-sync.
2026-05-23 12:57:41 +00:00
Matt 02e50f2fe5 Merge pull request 'Track pack/overrides/defaultconfigs/ + Waystones override' (#12) from feature/track-defaultconfigs into main 2026-05-23 12:56:13 +00:00
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
5 changed files with 401 additions and 2 deletions
+2 -1
View File
@@ -55,8 +55,9 @@ CLAUDE.md
# ─── Build artifacts that get regenerated ─────────────────────────────────
# Tweak jars rebuilt by scripts/Build-Tweaks.ps1
pack/overrides/
pack/overrides/mods/
# manifest.json regenerated by scripts/Build-Pack.ps1 -- produced at scripts/
# (default OutputPath) and copied to the deploy share by Deploy-Brass.ps1
scripts/manifest.json
pack/manifest.json
@@ -0,0 +1,87 @@
[general]
#List of waystone origins that should prevent others from editing. PLAYER is special in that it allows only edits by the owner of the waystone.
restrictedWaystones = ["PLAYER"]
#Set to "GLOBAL" to have newly placed or found waystones be global by default.
#Allowed Values: ACTIVATION, GLOBAL, SHARD_ONLY, ORANGE_SHARESTONE, MAGENTA_SHARESTONE, LIGHT_BLUE_SHARESTONE, YELLOW_SHARESTONE, LIME_SHARESTONE, PINK_SHARESTONE, GRAY_SHARESTONE, LIGHT_GRAY_SHARESTONE, CYAN_SHARESTONE, PURPLE_SHARESTONE, BLUE_SHARESTONE, BROWN_SHARESTONE, GREEN_SHARESTONE, RED_SHARESTONE, BLACK_SHARESTONE
defaultVisibility = "ACTIVATION"
#Add "GLOBAL" to allow every player to create global waystones.
allowedVisibilities = []
#The time in ticks that it takes to use a warp stone. This is the charge-up time when holding right-click.
warpStoneUseTime = 32
#The time in ticks that it takes to use a warp plate. This is the time the player has to stand on top for.
warpPlateUseTime = 15
#The time in ticks it takes to use a scroll. This is the charge-up time when holding right-click.
scrollUseTime = 32
[inventoryButton]
#Set to 'NONE' for no inventory button. Set to 'NEAREST' for an inventory button that teleports to the nearest waystone. Set to 'ANY' for an inventory button that opens the waystone selection menu. Set to a waystone name for an inventory button that teleports to a specifically named waystone.
inventoryButton = ""
#The x position of the inventory button in the inventory.
inventoryButtonX = 58
#The y position of the inventory button in the inventory.
inventoryButtonY = 60
#The y position of the inventory button in the creative menu.
creativeInventoryButtonX = 88
#The y position of the inventory button in the creative menu.
creativeInventoryButtonY = 33
[worldGen]
#Set to 'DEFAULT' to only generate the normally textured waystones. Set to 'MOSSY' or 'SANDY' to generate all as that variant. Set to 'BIOME' to make the style depend on the biome it is generated in.
#Allowed Values: DEFAULT, MOSSY, SANDY, BLACKSTONE, DEEPSLATE, END_STONE, BIOME
wildWaystoneStyle = "BIOME"
#Approximate chunk distance between wild waystones being generated. Set to 0 to disable generation.
chunksBetweenWildWaystones = 25
#List of dimensions that wild waystones are allowed to spawn in. If left empty, all dimensions except those in wildWaystonesDimensionDenyList will be able to spawn waystones, provided the biomes are in the `has_structure/waystone` tag.
wildWaystonesDimensionAllowList = ["minecraft:the_nether", "minecraft:overworld", "minecraft:the_end"]
#List of dimensions that wild waystones are not allowed to spawn in, even if the biome is in the `has_structure/waystone` tag. Only used if wildWaystonesDimensionAllowList is empty.
wildWaystonesDimensionDenyList = []
#Set to 'PRESET_FIRST' to first use names from the nameGenerationPresets. Set to 'PRESET_ONLY' to use only those custom names. Set to 'MIXED' to have some waystones use custom names, and others random names.
#Allowed Values: PRESET_FIRST, RANDOM_ONLY, PRESET_ONLY, MIXED
nameGenerationMode = "PRESET_FIRST"
#The template to use when generating new names. Supported placeholders are {Biome} (english biome name) and {MrPork} (the default name generator).
nameGenerationTemplate = "{MrPork}"
#These names will be used for the PRESET name generation mode. See the nameGenerationMode option for more info.
nameGenerationPresets = []
#Set to REGULAR to have waystones spawn in some villages. Set to FREQUENT to have waystones spawn in most villages. Set to DISABLED to disable waystone generation in villages. Waystones will only spawn in vanilla or supported villages.
#Allowed Values: DISABLED, REGULAR, FREQUENT
spawnInVillages = "DISABLED"
[teleports]
#Set to false to simply disable all xp costs. See warpRequirements for more fine-grained control.
enableCosts = true
#Set to false to simply disable all cooldowns. See warpRequirements for more fine-grained control.
enableCooldowns = true
#List of warp requirements with comma-separated parameters in parentheses. Conditions can be defined as comma-separated list in square brackets. Will be applied in order.
warpRequirements = ["[is_not_interdimensional] scaled_add_xp_cost(distance, 0.01)", "[is_interdimensional] add_xp_cost(27)", "[source_is_warp_plate] multiply_xp_cost(0)", "[target_is_global] multiply_xp_cost(0)", "min_xp_cost(0)", "max_xp_cost(27)", "[source_is_inventory_button] add_cooldown(inventory_button, 300)"]
#Set to ENABLED to have nearby pets teleport with you. Set to SAME_DIMENSION to have nearby pets teleport with you only if you're not changing dimensions. Set to DISABLED to disable.
#Allowed Values: ENABLED, SAME_DIMENSION, DISABLED
transportPets = "DISABLED"
#Set to ENABLED to have leashed mobs teleport with you. Set to SAME_DIMENSION to have leashed mobs teleport with you only if you're not changing dimensions. Set to DISABLED to disable.
#Allowed Values: ENABLED, SAME_DIMENSION, DISABLED
transportLeashed = "ENABLED"
#List of entities that cannot be teleported, either as pet, leashed, or on warp plates.
entityDenyList = ["minecraft:wither"]
#Set to true to enable warp modifier items for applying status effects on teleports.
enableModifiers = true
[client]
#If enabled, the text overlay on waystones will no longer always render at full brightness.
disableTextGlow = false
[compatibility]
#If enabled, JourneyMap waypoints will be created for each activated waystone.
journeyMap = true
#If enabled, JourneyMap waypoints will only be created if the mod 'JourneyMap Integration' is not installed
preferJourneyMapIntegrationMod = true
#If enabled, Waystones will add markers for waystones and sharestones to Dynmap.
dynmap = true
[blueMap]
#Controls whether Waystones' BlueMap integration should be loaded
enabled = true
#If enabled, waystones will be tracked as markers on BlueMap
includeWaystones = true
#If enabled, sharestones will be tracked as markers on BlueMap.
includeSharestones = true
#If enabled, undiscovered waystones will be tracked as markers on BlueMap.
includeUndiscoveredWaystones = false
+34 -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.15.0",
"version": "0.16.3",
"minecraft": "1.21.1",
"loader": {
"type": "neoforge",
@@ -448,6 +448,39 @@
"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