map dropdown = the AUTHENTIC console catalog; the relay names a phantom-map stall instead of holding silently
THE 40-MINUTE OUTAGE'S REAL FIX. The GUI's map dropdown offered every RES
name passing the type-14+26 existence check -- which includes INTERNAL
FRAGMENTS: artrucks is an include-node of arena1 (PROGRESS_LOG map anatomy:
arena1 -> {arenall -> cavern, artrucks}), not a mission. Picking it stalled
every pod's mission load forever with no error anywhere, and End/Re-arm kept
restoring the poisoned egg -- 22:02..22:49, three "different" failures, one
cause.
* eggmodel.CONSOLE_MAPS: the Mac 4.10 operator console's own adventure-tree
catalog (Console.ini; the same 8 the solo menu ships in btl4fe.cpp kMaps):
cavern grass rav polar3 polar4 arena1 arena2 dbase. ValueSets.maps now
offers exactly that, catalog order, filtered to what the RES carries
(permissive fallback only if the intersection is empty -- a foreign RES
must not present zero maps). artrucks is the one fragment the old filter
let through; now hidden.
* The relay WARNS at egg load/reload when the mission names a non-catalog
map (the camo-color-warning precedent: hand-edited eggs still work, the
operator just cannot miss it).
* The launch hold gains the PHANTOM-MISSION SIGNATURE diagnostic: 0/N ready
for 60s means every pod is stalled in shared mission content -- one line
naming the egg's map and the recovery (End Mission -> fix map -> Re-arm),
instead of the silent 10s HELD drumbeat the operator stared at for 40
minutes. Hint re-arms per launch.
VERIFIED: new suite scratchpad/test_map_guard.py (5 checks: catalog exact +
ordered, artrucks hidden, warning fires on a doctored egg, silent on a real
one, the 60s hint names the map); all five console suites pass. Python-only.
(While placing the hint, a blind text-insert broke the launch-received block's
indentation -- caught by py_compile before anything ran, repaired, and the
whole area re-verified by the rearm suite.)
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
becaddb662
commit
ef0ec48f9a
@@ -0,0 +1,71 @@
|
||||
"""Map-catalog guard regression (the 2026-07-26 artrucks outage).
|
||||
|
||||
1. ValueSets.maps offers EXACTLY the Mac 4.10 console catalog (8 maps,
|
||||
catalog order) -- internal RES fragments (artrucks...) hidden.
|
||||
2. The relay warns loudly when an egg names an unplayable map.
|
||||
3. A real map stays silent.
|
||||
4. The 60s all-pods-stalled hint names the egg's map.
|
||||
"""
|
||||
import contextlib
|
||||
import io
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import sys
|
||||
import time
|
||||
|
||||
sys.path.insert(0, r"C:\git\bt411\tools")
|
||||
os.chdir(r"C:\git\bt411\content")
|
||||
import btconsole # noqa: E402
|
||||
import eggmodel # noqa: E402
|
||||
|
||||
fails = []
|
||||
|
||||
|
||||
def check(label, ok, extra=""):
|
||||
print(" %-56s %s %s" % (label, "OK" if ok else "*** FAIL ***", extra))
|
||||
if not ok:
|
||||
fails.append(label)
|
||||
|
||||
|
||||
v = eggmodel.ValueSets()
|
||||
check("dropdown = the console catalog, in order",
|
||||
v.maps == list(eggmodel.CONSOLE_MAPS), repr(v.maps))
|
||||
check("artrucks hidden from the dropdown", "artrucks" not in v.maps)
|
||||
|
||||
shutil.copyfile("FOGDAY.EGG", "_maptest.EGG")
|
||||
raw = re.sub(rb"^map=\S+", b"map=artrucks",
|
||||
open("_maptest.EGG", "rb").read(), count=1, flags=re.M)
|
||||
open("_maptest.EGG", "wb").write(raw)
|
||||
buf = io.StringIO()
|
||||
with contextlib.redirect_stdout(buf):
|
||||
btconsole.Relay(1500, "_maptest.EGG", "127.0.0.1", 0, manual_launch=True)
|
||||
check("relay warns on a phantom map",
|
||||
"NOT a playable mission" in buf.getvalue())
|
||||
|
||||
buf = io.StringIO()
|
||||
with contextlib.redirect_stdout(buf):
|
||||
r = btconsole.Relay(1500, "FOGDAY.EGG", "127.0.0.1", 0, manual_launch=True)
|
||||
check("real map stays silent", "NOT a playable mission" not in buf.getvalue())
|
||||
|
||||
|
||||
class C:
|
||||
ready = False
|
||||
|
||||
|
||||
r.launches_sent = 0
|
||||
r.launch_at = time.time() - 61
|
||||
r.eggs_released = True
|
||||
r.by_host = {2: C(), 3: C()}
|
||||
r._hold_notice_at = 0
|
||||
buf = io.StringIO()
|
||||
with contextlib.redirect_stdout(buf):
|
||||
r._tick_launch()
|
||||
check("60s all-stalled hint names the map",
|
||||
"stalled in MISSION LOAD" in buf.getvalue()
|
||||
and "map=" in buf.getvalue())
|
||||
|
||||
os.remove("_maptest.EGG")
|
||||
print()
|
||||
print("ALL PASS" if not fails else "FAILURES: %s" % fails)
|
||||
sys.exit(1 if fails else 0)
|
||||
@@ -523,6 +523,7 @@ class Relay:
|
||||
# LAUNCH trims the roster to the seats present.
|
||||
self.seat_beacons = {}
|
||||
self.roster = parse_egg_roster(egg_path)
|
||||
self._warn_unplayable_map()
|
||||
self.orig_roster = list(self.roster) # round-reset restore point
|
||||
self.expected_ids = set(range(FIRST_GAME_HOST_ID,
|
||||
FIRST_GAME_HOST_ID + len(self.roster)))
|
||||
@@ -686,6 +687,33 @@ class Relay:
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
# The Mac 4.10 console's playable-map catalog (one source of truth in
|
||||
# eggmodel.CONSOLE_MAPS; duplicated here only as an import-failure
|
||||
# fallback). The RES also carries INTERNAL FRAGMENTS (artrucks, arenall,
|
||||
# adrop...) that are include-nodes, not missions -- 2026-07-26: map=artrucks
|
||||
# in the egg held six players at 0/N ready for 40 minutes, every pod
|
||||
# stalled in mission load with no error anywhere.
|
||||
_CONSOLE_MAPS_FALLBACK = ("cavern", "grass", "rav", "polar3", "polar4",
|
||||
"arena1", "arena2", "dbase")
|
||||
|
||||
def _warn_unplayable_map(self):
|
||||
try:
|
||||
import re as _re
|
||||
raw = open(self.orig_egg_path, "rb").read()
|
||||
m = _re.search(rb"^map=(\S+)", raw, _re.M)
|
||||
if not m:
|
||||
return
|
||||
map_name = m.group(1).decode("ascii", "replace").strip().lower()
|
||||
playable = tuple(getattr(eggmodel, "CONSOLE_MAPS", ())) or self._CONSOLE_MAPS_FALLBACK
|
||||
if map_name and map_name not in playable:
|
||||
print(f"[relay] *** WARNING: egg map='{map_name}' is NOT a "
|
||||
f"playable mission (playable: {', '.join(playable)}). "
|
||||
f"An art/include fragment STALLS EVERY POD'S LOAD "
|
||||
f"forever (the 2026-07-26 outage). Fix the map before "
|
||||
f"launching.", flush=True)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
def _reload_egg_file(self):
|
||||
"""BETWEEN-ROUNDS MISSION EDITS (2026-07-23): re-read the session egg
|
||||
FILE so operator changes (arena/time/weather/length via the console's
|
||||
@@ -703,6 +731,7 @@ class Relay:
|
||||
open(self.orig_egg_path, "rb").read())
|
||||
self.egg_bytes = self.orig_egg_bytes
|
||||
self.egg_path = self.orig_egg_path
|
||||
self._warn_unplayable_map()
|
||||
new_length = parse_egg_mission_length(self.orig_egg_path)
|
||||
if new_length != self.mission_length:
|
||||
print(f"[relay] mission length now {new_length:.0f}s",
|
||||
@@ -1187,6 +1216,7 @@ class Relay:
|
||||
self.launch_at = max(time.time(),
|
||||
self.eggs_done_at + LAUNCH_SETTLE_SECONDS)
|
||||
wait = max(0.0, self.launch_at - time.time())
|
||||
self._stall_hint_shown = False # fresh hint per launch
|
||||
print(f"[relay] operator LAUNCH received; firing in "
|
||||
f"{wait:.0f}s", flush=True)
|
||||
self._log_launch_readiness()
|
||||
@@ -1227,6 +1257,28 @@ class Relay:
|
||||
print(f"[relay] launch HELD -- still loading: {names} "
|
||||
f"({len(self.by_host) - len(not_ready)}/"
|
||||
f"{len(self.by_host)} ready)", flush=True)
|
||||
# PHANTOM-MISSION SIGNATURE (2026-07-26): EVERY pod stuck at
|
||||
# 0/N for 60s+ means they are all stalled in mission load --
|
||||
# shared content, not a slow machine. Name the map once so
|
||||
# the operator is not left guessing for 40 minutes again.
|
||||
if (len(not_ready) == len(self.by_host)
|
||||
and now >= self.launch_at + 60.0
|
||||
and not getattr(self, "_stall_hint_shown", False)):
|
||||
self._stall_hint_shown = True
|
||||
try:
|
||||
import re as _re
|
||||
raw = open(self.orig_egg_path, "rb").read()
|
||||
m = _re.search(rb"^map=(\S+)", raw, _re.M)
|
||||
map_name = (m.group(1).decode("ascii", "replace")
|
||||
if m else "?")
|
||||
except OSError:
|
||||
map_name = "?"
|
||||
print(f"[relay] *** 0/{len(self.by_host)} ready after "
|
||||
f"60s: every pod is stalled in MISSION LOAD. "
|
||||
f"Suspect the mission content itself -- the egg "
|
||||
f"says map={map_name}. (An unplayable map hangs "
|
||||
f"all pods with no error; End Mission -> fix the "
|
||||
f"map -> Re-arm -> LAUNCH.)", flush=True)
|
||||
return
|
||||
if not_ready:
|
||||
print(f"[relay] launch FORCED after 180s hold -- "
|
||||
|
||||
+17
-1
@@ -45,6 +45,17 @@ DROPZONES = ["one"] # only confirmed-valid name (embedded in map streams)
|
||||
# crash). Canonical 8-mech names first for the operator dropdown; the
|
||||
# short variants (bhk1/mad1/lok2/...) are alternate ModelList entries of
|
||||
# the same stable. "camera" = the spectator CameraShip, not a mech.
|
||||
# The AUTHENTIC operator map catalog -- the Mac 4.10 console's adventure tree
|
||||
# (Console.ini; provenance docs/GLASS_COCKPIT.md 2026-07-19; same list the solo
|
||||
# menu ships in game/glass/btl4fe.cpp kMaps). The RES also holds INTERNAL
|
||||
# FRAGMENTS (artrucks, arenall, adrop, pol4sfx...) that pass the type-14+26
|
||||
# existence check but are include-nodes, not missions -- picking one stalls
|
||||
# every pod's load forever (the 2026-07-26 games-night outage: map=artrucks
|
||||
# held six players at 0/6 ready for 40 minutes). Offer what the 1995 console
|
||||
# offered.
|
||||
CONSOLE_MAPS = ["cavern", "grass", "rav", "polar3", "polar4",
|
||||
"arena1", "arena2", "dbase"]
|
||||
|
||||
KNOWN_GOOD_VEHICLES = ["bhk1", "madcat", "avatar", "blkhawk", "loki",
|
||||
"owens", "sunder", "thor", "vulture"]
|
||||
CANDIDATE_VEHICLES = ["ava1", "lok1", "lok2", "mad1", "mad2", "own1",
|
||||
@@ -105,7 +116,12 @@ class ValueSets:
|
||||
# maps: need BOTH type 14 and type 26
|
||||
maps14 = {r.name for r in by_type[14]}
|
||||
maps26 = {r.name for r in by_type[26]}
|
||||
self.maps = sorted(maps14 & maps26)
|
||||
present = maps14 & maps26
|
||||
# catalog order, filtered to what THIS RES actually carries; fall back
|
||||
# to the permissive set only if the intersection is somehow empty (a
|
||||
# foreign RES), so the tool never presents zero maps.
|
||||
self.maps = [m for m in CONSOLE_MAPS if m in present] or sorted(present)
|
||||
self.all_map_streams = sorted(present) # fragments included (debug)
|
||||
# vehicletable (type 25)
|
||||
self.colors, self.badges, self.patches = [], [], []
|
||||
for r in by_type[25]:
|
||||
|
||||
Reference in New Issue
Block a user