From 892b31d04c50753bb06d9f37af237bc7753a0b9c Mon Sep 17 00:00:00 2001 From: Atchbo <86882671+Atchbo2001@users.noreply.github.com> Date: Thu, 30 Jul 2026 21:04:58 -0500 Subject: [PATCH 1/8] Validate and generate curated map manifest --- assets/ci | 132 +++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 100 insertions(+), 32 deletions(-) diff --git a/assets/ci b/assets/ci index 566ed45b..0cfb0596 100755 --- a/assets/ci +++ b/assets/ci @@ -14,64 +14,132 @@ mkdir -p cache ./setup -outdir=${ASSET_OUTPUT_DIR:-output} -asset_maps=${ASSET_MAPS:-"complex dust2 turbine"} -read -r -a packaged_maps <<< "$asset_maps" - -if [ "${#packaged_maps[@]}" -eq 0 ]; then - echo "ASSET_MAPS did not contain any maps" >&2 - exit 1 -fi - -# Sauerbraten loads autoexec.cfg after its stock menu catalog. Generate that -# native config from the same exact map list passed to the asset packager, so -# every in-game map picker only exposes maps that are actually in this build. -PACKAGED_MAPS="$asset_maps" python3 - <<'PY' +outdir=${ASSET_OUTPUT_DIR:-dist} +asset_maps=${ASSET_MAPS:-"complex turbine dust2"} +ffa_maps=${ASSET_FFA_MAPS:-"$asset_maps"} +capture_maps=${ASSET_CAPTURE_MAPS:-""} +ctf_maps=${ASSET_CTF_MAPS:-""} + +# Validate the curated manifest against the stock Sauerbraten catalog before +# downloading anything. This prevents typos or invented map names from silently +# entering a release. Mode-specific lists must also be subsets of ASSET_MAPS. +PACKAGED_MAPS="$asset_maps" \ +FFA_MAPS="$ffa_maps" \ +CAPTURE_MAPS="$capture_maps" \ +CTF_MAPS="$ctf_maps" \ +python3 - <<'PY' import os +import re from pathlib import Path -maps = [] -for value in os.environ["PACKAGED_MAPS"].split(): - value = value.strip() - if value and value not in maps: - maps.append(value) -if not maps: - raise SystemExit("packaged map manifest is empty") +def unique_words(name: str) -> list[str]: + values: list[str] = [] + for value in os.environ.get(name, "").split(): + value = value.strip() + if value and value not in values: + values.append(value) + return values + + +packaged = unique_words("PACKAGED_MAPS") +ffa = unique_words("FFA_MAPS") +capture = unique_words("CAPTURE_MAPS") +ctf = unique_words("CTF_MAPS") -catalog = " ".join(maps) +if not packaged: + raise SystemExit("ASSET_MAPS did not contain any maps") + +for label, values in (("FFA", ffa), ("Capture", capture), ("CTF", ctf)): + missing = sorted(set(values) - set(packaged)) + if missing: + raise SystemExit(f"{label} map list contains maps not in ASSET_MAPS: {', '.join(missing)}") + +menus = Path("sour/data/menus.cfg").read_text() +stock: set[str] = set() +for prefix in ("ffamaps", "capturemaps", "ctfmaps"): + for match in re.finditer(rf'^{prefix}\d*\s*=\s*"([^"]*)"', menus, re.MULTILINE): + stock.update(match.group(1).split()) + +unknown = sorted(set(packaged) - stock) +if unknown: + raise SystemExit( + "ASSET_MAPS contains names that are not in the stock Sauerbraten multiplayer catalog: " + + ", ".join(unknown) + ) + +catalog = " ".join(packaged) lines = [ - "// Generated by assets/ci from ASSET_MAPS. Do not edit manually.", + "// Generated by assets/ci from the validated packaged-map manifest.", + "// Do not edit manually.", f'packagedmaps = "{catalog}"', - 'ffamaps1 = $packagedmaps', + f'ffamaps1 = "{" ".join(ffa)}"', ] for index in range(2, 12): lines.append(f'ffamaps{index} = ""') -for index in range(1, 8): + +lines.append(f'capturemaps1 = "{" ".join(capture)}"') +for index in range(2, 8): lines.append(f'capturemaps{index} = ""') -for index in range(1, 8): + +lines.append(f'ctfmaps1 = "{" ".join(ctf)}"') +for index in range(2, 8): lines.append(f'ctfmaps{index} = ""') lines.extend([ 'conceptmaps = ""', 'spmaps = ""', 'rpgmaps = ""', - 'allffamaps = $packagedmaps', - 'allcapturemaps = ""', - 'allctfmaps = ""', + 'allffamaps = $ffamaps1', + 'allcapturemaps = $capturemaps1', + 'allctfmaps = $ctfmaps1', 'allmaps = $packagedmaps', '', ]) -path = Path("sour/autoexec.cfg") -path.write_text("\n".join(lines)) -print(f"native packaged map catalog ({len(maps)}): {catalog}") +Path("sour/autoexec.cfg").write_text("\n".join(lines)) +print(f"validated packaged maps ({len(packaged)}): {catalog}") +print(f"FFA ({len(ffa)}): {' '.join(ffa)}") +print(f"Capture ({len(capture)}): {' '.join(capture)}") +print(f"CTF ({len(ctf)}): {' '.join(ctf)}") PY +read -r -a packaged_maps <<< "$asset_maps" + +# A clean output directory makes every release reproducible and prevents stale +# map bundles from an older build from surviving after the manifest changes. +rm -rf "$outdir" +mkdir -p "$outdir" + python3 base.py \ --root https://static.sourga.me/blobs/6481/.index.source \ --models \ --download \ - --outdir dist \ + --outdir "$outdir" \ "${packaged_maps[@]}" + +PACKAGED_MAPS="$asset_maps" \ +FFA_MAPS="$ffa_maps" \ +CAPTURE_MAPS="$capture_maps" \ +CTF_MAPS="$ctf_maps" \ +MAP_OUTPUT_DIR="$outdir" \ +python3 - <<'PY' +import json +import os +from pathlib import Path + + +def words(name: str) -> list[str]: + return [value for value in os.environ.get(name, "").split() if value] + +manifest = { + "maps": words("PACKAGED_MAPS"), + "ffa": words("FFA_MAPS"), + "capture": words("CAPTURE_MAPS"), + "ctf": words("CTF_MAPS"), +} +path = Path(os.environ["MAP_OUTPUT_DIR"]) / "packaged-maps.json" +path.write_text(json.dumps(manifest, indent=2) + "\n") +print(f"wrote {path}") +PY From f48dc872777f3ea75b57e8ebe70b8d9ef8b8fe58 Mon Sep 17 00:00:00 2001 From: Atchbo <86882671+Atchbo2001@users.noreply.github.com> Date: Thu, 30 Jul 2026 21:05:37 -0500 Subject: [PATCH 2/8] Fail releases when requested maps are not actually packaged --- assets/ci | 48 ++++++++++++++++++++++++++++++++++-------------- 1 file changed, 34 insertions(+), 14 deletions(-) diff --git a/assets/ci b/assets/ci index 0cfb0596..f1ecf0b5 100755 --- a/assets/ci +++ b/assets/ci @@ -33,19 +33,18 @@ import re from pathlib import Path -def unique_words(name: str) -> list[str]: - values: list[str] = [] - for value in os.environ.get(name, "").split(): - value = value.strip() - if value and value not in values: - values.append(value) - return values +def manifest_words(name: str) -> list[str]: + raw = [value.strip() for value in os.environ.get(name, "").split() if value.strip()] + if len(raw) != len(set(raw)): + duplicates = sorted({value for value in raw if raw.count(value) > 1}) + raise SystemExit(f"{name} contains duplicate map names: {', '.join(duplicates)}") + return raw -packaged = unique_words("PACKAGED_MAPS") -ffa = unique_words("FFA_MAPS") -capture = unique_words("CAPTURE_MAPS") -ctf = unique_words("CTF_MAPS") +packaged = manifest_words("PACKAGED_MAPS") +ffa = manifest_words("FFA_MAPS") +capture = manifest_words("CAPTURE_MAPS") +ctf = manifest_words("CTF_MAPS") if not packaged: raise SystemExit("ASSET_MAPS did not contain any maps") @@ -129,17 +128,38 @@ import json import os from pathlib import Path +import cbor2 + def words(name: str) -> list[str]: return [value for value in os.environ.get(name, "").split() if value] + +output = Path(os.environ["MAP_OUTPUT_DIR"]) +requested = words("PACKAGED_MAPS") +with (output / ".index.source").open("rb") as handle: + index = cbor2.load(handle) + +built = [entry["name"] for entry in index.get("maps", [])] +missing = [name for name in requested if name not in built] +unexpected = [name for name in built if name not in requested] +if missing or unexpected or len(built) != len(requested): + details = [] + if missing: + details.append("missing: " + ", ".join(missing)) + if unexpected: + details.append("unexpected: " + ", ".join(unexpected)) + if len(built) != len(requested): + details.append(f"requested {len(requested)} but built {len(built)}") + raise SystemExit("packaged map verification failed (" + "; ".join(details) + ")") + manifest = { - "maps": words("PACKAGED_MAPS"), + "maps": requested, "ffa": words("FFA_MAPS"), "capture": words("CAPTURE_MAPS"), "ctf": words("CTF_MAPS"), } -path = Path(os.environ["MAP_OUTPUT_DIR"]) / "packaged-maps.json" +path = output / "packaged-maps.json" path.write_text(json.dumps(manifest, indent=2) + "\n") -print(f"wrote {path}") +print(f"verified all {len(built)} requested maps and wrote {path}") PY From ec43571e235cecb0a1feebc91b3b725d635facbd Mon Sep 17 00:00:00 2001 From: Atchbo <86882671+Atchbo2001@users.noreply.github.com> Date: Thu, 30 Jul 2026 21:06:00 -0500 Subject: [PATCH 3/8] Add curated 20-map rotations and Capture server --- pterodactyl.yaml | 98 ++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 90 insertions(+), 8 deletions(-) diff --git a/pterodactyl.yaml b/pterodactyl.yaml index 481debfb..8d2c0bb0 100644 --- a/pterodactyl.yaml +++ b/pterodactyl.yaml @@ -29,18 +29,46 @@ server: defaultMap: "complex" matchLength: 300 maps: - - "turbine" - "complex" + - "turbine" - "dust2" + - "academy" + - "metl3" + - "metl4" + - "frostbyte" + - "gorge" + - "hades" + - "injustice" + - "lostinspace" + - "oasis" + - "paradigm" + - "phosgene" + - "ruby" + - "stronghold" + - "suburb" - name: "insta" config: defaultMode: "insta" defaultMap: "complex" matchLength: 300 maps: - - "turbine" - "complex" + - "turbine" - "dust2" + - "academy" + - "metl3" + - "metl4" + - "frostbyte" + - "gorge" + - "hades" + - "injustice" + - "lostinspace" + - "oasis" + - "paradigm" + - "phosgene" + - "ruby" + - "stronghold" + - "suburb" - name: "insta-dust2" config: defaultMode: "insta" @@ -55,6 +83,24 @@ server: matchLength: 300 maps: - "dust2" + - name: "capture" + config: + defaultMode: "capture" + defaultMap: "dust2" + matchLength: 300 + maps: + - "dust2" + - "forge" + - "frostbyte" + - "gorge" + - "hades" + - "lostinspace" + - "paradigm" + - "reissen" + - "ruby" + - "stronghold" + - "suburb" + - "venice" - name: "ctf" config: defaultMode: "ctf" @@ -62,15 +108,33 @@ server: matchLength: 300 maps: - "dust2" + - "forge" + - "reissen" + - "stronghold" + - "suburb" - name: "effic" config: defaultMode: "effic" - defaultMap: "dust2" + defaultMap: "complex" matchLength: 300 maps: - - "dust2" - "complex" - "turbine" + - "dust2" + - "academy" + - "metl3" + - "metl4" + - "frostbyte" + - "gorge" + - "hades" + - "injustice" + - "lostinspace" + - "oasis" + - "paradigm" + - "phosgene" + - "ruby" + - "stronghold" + - "suburb" - name: "ffa-bots" config: @@ -80,9 +144,16 @@ server: botCount: 5 botSkill: 70 maps: - - "turbine" - "complex" - - "dust2" + - "turbine" + - "academy" + - "metl3" + - "metl4" + - "frostbyte" + - "gorge" + - "injustice" + - "oasis" + - "phosgene" - name: "insta-bots" config: defaultMode: "insta" @@ -91,9 +162,16 @@ server: botCount: 4 botSkill: 78 maps: - - "turbine" - "complex" - - "dust2" + - "turbine" + - "academy" + - "metl3" + - "metl4" + - "frostbyte" + - "gorge" + - "injustice" + - "oasis" + - "phosgene" ingress: desktop: @@ -129,6 +207,10 @@ server: config: alias: "ffa-dust2" description: "FFA Dust 2" + - preset: "capture" + config: + alias: "capture" + description: "Capture" - preset: "ctf" config: alias: "ctf" From e987db385835dbc46efb4bca38b8995b37d6f9b9 Mon Sep 17 00:00:00 2001 From: Atchbo <86882671+Atchbo2001@users.noreply.github.com> Date: Thu, 30 Jul 2026 21:06:25 -0500 Subject: [PATCH 4/8] Add three-player relay regression coverage --- pkg/gameserver/relay/relay_test.go | 89 +++++++++++++++++++++++++----- 1 file changed, 74 insertions(+), 15 deletions(-) diff --git a/pkg/gameserver/relay/relay_test.go b/pkg/gameserver/relay/relay_test.go index 449808ed..de8789a4 100644 --- a/pkg/gameserver/relay/relay_test.go +++ b/pkg/gameserver/relay/relay_test.go @@ -12,6 +12,16 @@ type delivered struct { payload []protocol.Message } +func payloadHasText(payload []protocol.Message, expected string) bool { + for _, message := range payload { + text, ok := message.(protocol.Text) + if ok && text.Text == expected { + return true + } + } + return false +} + func TestSingleClientPacketsAreNotReplayedToLateJoiner(t *testing.T) { r := New() first, _ := r.AddClient(1, func(uint8, []protocol.Message) {}) @@ -31,20 +41,10 @@ func TestSingleClientPacketsAreNotReplayedToLateJoiner(t *testing.T) { if packet.channel != 1 { t.Fatalf("expected channel 1, got %d", packet.channel) } - foundFresh := false - for _, message := range packet.payload { - text, ok := message.(protocol.Text) - if !ok { - continue - } - if text.Text == "stale" { - t.Fatal("late joiner received stale packet") - } - if text.Text == "fresh" { - foundFresh = true - } + if payloadHasText(packet.payload, "stale") { + t.Fatal("late joiner received stale packet") } - if !foundFresh { + if !payloadHasText(packet.payload, "fresh") { t.Fatal("fresh packet was not delivered") } case <-time.After(time.Second): @@ -95,9 +95,9 @@ func TestHumanPacketsReachClientZero(t *testing.T) { case packet := <-firstMessages: switch packet.channel { case 0: - seenMovement = true + seenMovement = payloadHasText(packet.payload, "movement") || seenMovement case 1: - seenReliable = true + seenReliable = payloadHasText(packet.payload, "spawn") || seenReliable } case <-timeout: t.Fatalf("client zero missed relayed traffic: movement=%t reliable=%t", seenMovement, seenReliable) @@ -105,6 +105,65 @@ func TestHumanPacketsReachClientZero(t *testing.T) { } } +func TestThreeHumanClientsReceiveEveryOtherReliablePacket(t *testing.T) { + r := New() + received := []chan delivered{ + make(chan delivered, 8), + make(chan delivered, 8), + make(chan delivered, 8), + } + + publishers := make([]*Publisher, 3) + for cn := uint32(0); cn < 3; cn++ { + index := int(cn) + _, publishers[index] = r.AddClient(cn, func(channel uint8, payload []protocol.Message) { + received[index] <- delivered{channel: channel, payload: payload} + }) + } + + for cn, publisher := range publishers { + publisher.Publish(protocol.Text{Text: "from-" + string(rune('0'+cn))}) + } + + for receiver := 0; receiver < 3; receiver++ { + expected := map[string]bool{} + for sender := 0; sender < 3; sender++ { + if sender != receiver { + expected["from-"+string(rune('0'+sender))] = false + } + } + + timeout := time.After(time.Second) + for { + complete := true + for _, seen := range expected { + complete = complete && seen + } + if complete { + break + } + + select { + case packet := <-received[receiver]: + if packet.channel != 1 { + continue + } + own := "from-" + string(rune('0'+receiver)) + if payloadHasText(packet.payload, own) { + t.Fatalf("client %d received its own reliable packet", receiver) + } + for text := range expected { + if payloadHasText(packet.payload, text) { + expected[text] = true + } + } + case <-timeout: + t.Fatalf("client %d missed reliable packets: %#v", receiver, expected) + } + } + } +} + func TestBotSourceDoesNotEchoToOwner(t *testing.T) { r := New() ownerMessages := make(chan delivered, 2) From 5b23dcfa93514f60d53011afe3ce84cadc750fbc Mon Sep 17 00:00:00 2001 From: Atchbo <86882671+Atchbo2001@users.noreply.github.com> Date: Thu, 30 Jul 2026 21:06:38 -0500 Subject: [PATCH 5/8] Document the verified stock map pack --- MAPS.md | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 MAPS.md diff --git a/MAPS.md b/MAPS.md new file mode 100644 index 00000000..87e2fe73 --- /dev/null +++ b/MAPS.md @@ -0,0 +1,48 @@ +# Void Arena map pack + +This build packages 20 maps from the stock Cube 2: Sauerbraten multiplayer catalog used by the pinned r6481 game assets. The build rejects unknown names and fails if any requested `.ogz` map cannot actually be packaged, so a release cannot silently ship a menu entry without its map data. + +## Free For All / Instagib / Efficiency + +- academy +- complex +- dust2 +- frostbyte +- gorge +- hades +- injustice +- lostinspace +- metl3 +- metl4 +- oasis +- paradigm +- phosgene +- ruby +- stronghold +- suburb +- turbine + +## Capture + +- dust2 +- forge +- frostbyte +- gorge +- hades +- lostinspace +- paradigm +- reissen +- ruby +- stronghold +- suburb +- venice + +## Capture the Flag + +- dust2 +- forge +- reissen +- stronghold +- suburb + +The native in-game map menus are generated from these same lists during the asset build. `assets/dist/packaged-maps.json` records the exact manifest included in a release. From 951bde2f79e141e011757cd9185ce8231f9ff469 Mon Sep 17 00:00:00 2001 From: Atchbo <86882671+Atchbo2001@users.noreply.github.com> Date: Thu, 30 Jul 2026 21:07:05 -0500 Subject: [PATCH 6/8] Build 20 verified maps and run multiplayer regressions --- .github/workflows/ci.yml | 76 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 72 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1130fd61..9ddd8721 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,6 +17,11 @@ jobs: timeout-minutes: 120 permissions: contents: read + env: + ASSET_MAPS: "complex turbine dust2 academy metl3 metl4 frostbyte gorge hades injustice lostinspace oasis paradigm phosgene ruby stronghold suburb forge reissen venice" + ASSET_FFA_MAPS: "complex turbine dust2 academy metl3 metl4 frostbyte gorge hades injustice lostinspace oasis paradigm phosgene ruby stronghold suburb" + ASSET_CAPTURE_MAPS: "dust2 forge frostbyte gorge hades lostinspace paradigm reissen ruby stronghold suburb venice" + ASSET_CTF_MAPS: "dust2 forge reissen stronghold suburb" steps: - name: Check out the full source fork @@ -54,17 +59,79 @@ jobs: make -C pkg/enet/enet clean make -C pkg/enet/enet - - name: Build game assets - env: - ASSET_MAPS: "complex dust2 turbine" + - name: Build and verify curated game maps run: ./scripts/build assets + - name: Verify map manifest and server rotations + run: | + python3 - <<'PY' + import json + import re + from pathlib import Path + + manifest = json.loads(Path("assets/dist/packaged-maps.json").read_text()) + packaged = manifest["maps"] + if len(packaged) != 20: + raise SystemExit(f"expected exactly 20 packaged maps, got {len(packaged)}") + if len(packaged) != len(set(packaged)): + raise SystemExit("packaged map manifest contains duplicates") + + text = Path("pterodactyl.yaml").read_text().splitlines() + referenced = set() + in_maps = False + maps_indent = -1 + for line in text: + stripped = line.strip() + indent = len(line) - len(line.lstrip()) + default_match = re.fullmatch(r'defaultMap:\s*"([^"]+)"', stripped) + if default_match: + referenced.add(default_match.group(1)) + if stripped == "maps:": + in_maps = True + maps_indent = indent + continue + if in_maps: + if stripped and indent <= maps_indent: + in_maps = False + else: + map_match = re.fullmatch(r'-\s*"([^"]+)"', stripped) + if map_match: + referenced.add(map_match.group(1)) + + missing = sorted(referenced - set(packaged)) + if missing: + raise SystemExit("server configuration references unpackaged maps: " + ", ".join(missing)) + + expected_categories = { + "ffa": 17, + "capture": 12, + "ctf": 5, + } + for category, expected in expected_categories.items(): + actual = len(manifest[category]) + if actual != expected: + raise SystemExit(f"expected {expected} {category} maps, got {actual}") + + print(f"verified {len(packaged)} real packaged maps and {len(referenced)} configured map references") + PY + - name: Build WebAssembly game engine run: ./scripts/build game - name: Build web client run: ./scripts/build web + - name: Set up Go for multiplayer regression tests + uses: actions/setup-go@v5 + with: + go-version-file: go.mod + cache: true + + - name: Run multiplayer protocol and relay regressions + run: | + go test ./pkg/gameserver/relay -count=20 + go test ./pkg/game/protocol -run TestBotPositionUsesUnsignedClientNumber -count=20 + - name: Build Go multiplayer backend run: ./scripts/build server @@ -72,6 +139,7 @@ jobs: run: | test -x bin/sour test -f assets/dist/.index.source + test -f assets/dist/packaged-maps.json test -f pkg/server/static/site/index.html file bin/sour ldd bin/sour || true @@ -116,7 +184,7 @@ jobs: cp bin/sour release/void-arena/bin/sour cp -a assets/dist release/void-arena/assets/dist - cp start.sh package.json pterodactyl.yaml PTERODACTYL.md LICENSE release/void-arena/ + cp start.sh package.json pterodactyl.yaml PTERODACTYL.md MAPS.md LICENSE release/void-arena/ chmod +x release/void-arena/bin/sour release/void-arena/start.sh ( From 9b7ca9f779aefde5de1afcd566fff716497f5ee7 Mon Sep 17 00:00:00 2001 From: Atchbo <86882671+Atchbo2001@users.noreply.github.com> Date: Thu, 30 Jul 2026 21:23:44 -0500 Subject: [PATCH 7/8] Fix map rotation validation --- .github/workflows/ci.yml | 26 ++++++++------------------ 1 file changed, 8 insertions(+), 18 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9ddd8721..10c985d6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -76,27 +76,17 @@ jobs: if len(packaged) != len(set(packaged)): raise SystemExit("packaged map manifest contains duplicates") - text = Path("pterodactyl.yaml").read_text().splitlines() + # Validate only actual map fields. A quoted list item consisting solely + # of a Sauerbraten-style identifier is a map rotation entry; asset URLs + # and mapping entries such as '- preset:' cannot match this pattern. referenced = set() - in_maps = False - maps_indent = -1 - for line in text: - stripped = line.strip() - indent = len(line) - len(line.lstrip()) - default_match = re.fullmatch(r'defaultMap:\s*"([^"]+)"', stripped) + for line in Path("pterodactyl.yaml").read_text().splitlines(): + default_match = re.fullmatch(r'\s*defaultMap:\s*"([a-z0-9_-]+)"\s*', line) if default_match: referenced.add(default_match.group(1)) - if stripped == "maps:": - in_maps = True - maps_indent = indent - continue - if in_maps: - if stripped and indent <= maps_indent: - in_maps = False - else: - map_match = re.fullmatch(r'-\s*"([^"]+)"', stripped) - if map_match: - referenced.add(map_match.group(1)) + list_match = re.fullmatch(r'\s*-\s*"([a-z0-9_-]+)"\s*', line) + if list_match: + referenced.add(list_match.group(1)) missing = sorted(referenced - set(packaged)) if missing: From 22e04efea7be4259c12601a3a487b719a3b36e3d Mon Sep 17 00:00:00 2001 From: Atchbo <86882671+Atchbo2001@users.noreply.github.com> Date: Fri, 31 Jul 2026 08:38:24 -0500 Subject: [PATCH 8/8] Run curated map manifest build inside asset container --- scripts/build-assets | 34 +++++++++++++++------------------- 1 file changed, 15 insertions(+), 19 deletions(-) diff --git a/scripts/build-assets b/scripts/build-assets index f7c565a6..1dcf316a 100755 --- a/scripts/build-assets +++ b/scripts/build-assets @@ -4,35 +4,31 @@ set -euo pipefail source "$(dirname "$0")/buildenv-vars" +# assets/ci is the single source of truth for the packaged map manifest, +# native Sauerbraten map menus, downloaded map assets, and release metadata. +# Keep every categorized rotation available inside the build container. +ASSET_FFA_MAPS=${ASSET_FFA_MAPS:-$ASSET_MAPS} +ASSET_CAPTURE_MAPS=${ASSET_CAPTURE_MAPS:-} +ASSET_CTF_MAPS=${ASSET_CTF_MAPS:-} + $CONTAINER_CMD run \ --rm \ --user "$HOST_UID:$HOST_GID" \ --env-file "$PROJECT_ROOT/docker/common.env" \ - -e ASSET_OUTPUT_DIR="$ASSET_OUTPUT_DIR" \ + -e ASSET_OUTPUT_DIR=dist \ -e ASSET_CACHE_DIR="$ASSET_CACHE_DIR" \ -e ASSET_MAPS="$ASSET_MAPS" \ + -e ASSET_FFA_MAPS="$ASSET_FFA_MAPS" \ + -e ASSET_CAPTURE_MAPS="$ASSET_CAPTURE_MAPS" \ + -e ASSET_CTF_MAPS="$ASSET_CTF_MAPS" \ -v "$PROJECT_ROOT":/workspace \ -w /workspace/assets \ "$IMAGE_TAG" \ bash -lc ' - set -e - rm -rf /workspace/assets/dist || true - mkdir -p "$HOME" "$PIP_CACHE_DIR" + set -euo pipefail + mkdir -p "$HOME" "$PIP_CACHE_DIR" "$ASSET_CACHE_DIR" python3 -m pip install --user -r requirements.txt || true - mkdir -p "$ASSET_CACHE_DIR" - ./setup - # OPTIONAL: Rebuild sourdump to ensure latest code is used - # rm -f sourdump - # go build -o sourdump ../cmd/sourdump/main.go - # Build base assets; configure maps via ASSET_MAPS - # Expand ASSET_MAPS into positional args - set -- $ASSET_MAPS - python3 base.py \ - --root https://static.sourga.me/blobs/6481/.index.source \ - --models \ - --download \ - --outdir dist \ - "$@" + ./ci ' -echo "Successfully built assets" +echo "Successfully built and verified assets"