Files
BT411/docs/REVOLVING_DOOR_PLAN.md
T
arcattackandClaude Opus 5 66d1410118 Revolving-door mode: feasibility findings + two-phase implementation plan (docs only)
Phase 1 = console-only round carousel (relay door mode, no new zip);
Phase 2 = true drop-in (engine make re-send on late PEER_UP), gated on a
2-node spike.  No code modified -- plan for the next work session.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0166KTsC7ADm7VXEi1HF1jNg
2026-07-24 15:05:06 -05:00

13 KiB
Raw Blame History

Revolving-Door Mode — Implementation Plan

Status: PLANNED (not started). Written 2026-07-24 from a read-only feasibility study. No code has been modified. Implement starting with Phase 1; do NOT start Phase 2 without the Phase-2 spike passing on a 2-node rig.

Goal (operator's words): "an infinite long mission that players can just spawn in and out of as they please... they can just go in and out as they please and i'd just leave it running" — so players generate gameplay logs without coordinating everyone's timeline.


Two tiers

What the player sees Code touched Effort New zip?
Phase 1 — Round Carousel Short rounds (1015 min) run back-to-back forever with whoever is seated; a joiner waits at most one round-length for the next launch tools/btconsole.py (+ small tools/btoperator.py UI) only ~half a day NO — works with 4.11.518 zips as shipped
Phase 2 — True Drop-In One endless mission; join and leave instantly mid-mission engine (L4NET.CPP, HOSTMGR.cpp, game-side make re-send) + relay 1-day spike, then 24 days YES

Phase 1 fully solves the stated goal (unattended logging, no timeline coordination). Phase 2 is the pod-faithful dream and also the path a rebooting pod would use mid-session — build it later, on top of a proven Phase 1.


Feasibility findings (2026-07-24, all read-only — file:line anchors)

What ALREADY works:

  1. Back-to-back auto re-arm_check_launch_gate (tools/btconsole.py:780-813): after a round (launches_sent==2), when all seats re-ACK a fresh egg the relay resets launch state and re-arms. In auto mode (manual_launch=False) it then schedules the next LAUNCH pair itself (:810-813). The carousel loop essentially exists; it is just gated on the full roster re-ACKing.
  2. Mission clock is console-side_tick_stop (btconsole.py:959-983) sends StopMission at [mission] length expiry. Round length is an egg edit, nothing else.
  3. Mid-round LEAVING works today — PEER_DOWN teardown, survivors continue, matchlog auto-uploads on exit. Hardened all through the 4.11.49x51x work (issues #33/#34/#35; the interest-teardown crash on this exact path is fixed, commit 210bdb0).
  4. Round reset / seat identity_maybe_reset_round (btconsole.py:734-771) restores orig_roster + template egg when all pods leave; seat_identity keeps seats static per (ip,callsign) (#34). LAUNCH trims the roster to present seats (_trim_roster :651-677, writes _relay_trimmed.egg).
  5. Transport tolerates late host-online — relay PEER_UP is bidirectional for newcomers (btconsole.py:1202-1209 "newcomer learns everyone; everyone learns newcomer"); engine HostConnectedMessageHandler (engine/MUNGA_L4/L4NET.CPP:1551+) flips an existing ROSTER host to OnLine whenever it connects — different arrival times are normal (that's how slow loaders work).
  6. The world learns about a JOINER for free — a joiner creates its player/mech AFTER connecting, so its entity-creation broadcast reaches everyone present.

The Phase-2 gap (the ONLY hard part):

  1. A joiner never learns about the EXISTING world. Entity MakeMessages are broadcast once at creation and never retained. InterestManager::NotifyOfEntityCreation (engine/MUNGA/INTEREST.cpp:694-763) routes to DynamicEntityCreation (:839+) which sends immediately; interest-zone tracking is #if 0 "HACK - Partial implementation" (:877-898). HostManager::NotifyOfReplacementEntityCreation (engine/MUNGA/HOSTMGR.cpp) is literally Fail("under construction") — the 1995 team designed late-sync and never finished it. [T1]
  2. The engine launch gate stalls on absent roster seats — an endless mission with an 8-seat roster cannot launch with 2 present (every pod waits on every roster host). Phase 1 sidesteps this with the existing launch-time trim; Phase 2 must relax it.

Concept

A relay door mode (--door / GUI checkbox → env for the spawned relay): rounds launch automatically with whoever is seated, run for a short fixed length, and the relay re-forms the next round forever. The operator leaves it running. Players run the normal join.bat from the 518 zip — join between rounds, leave whenever.

The one rule door mode changes

Everywhere the relay today waits for the full roster, door mode waits for at least MIN_PLAYERS (default 12) + a quiet-period join window:

door gate:  _pods_ready() >= DOOR_MIN_PLAYERS
            and (now - last_new_ack) >= DOOR_JOIN_WINDOW   # let a 2nd/3rd joiner in
            and now >= round_hold_until                     # keep the #33 settle

Changes (all in tools/btconsole.py unless noted)

  1. --door flag / BT_DOOR=1self.door_mode, plus knobs DOOR_MIN_PLAYERS (default 2; 1 allowed for solo soak), DOOR_JOIN_WINDOW (default 45 s), DOOR_ROUND_SECONDS (default 600; overrides the egg [mission] length via the existing prefs-rewrite path — same mechanism as the console's mission fields, :603-622).
  2. Gate swap — in _check_launch_gate (:780-813) and the ALL-READY hold (:910-934): in door mode replace >= len(self.roster) with the door gate above. Track last_new_ack on every egg ACK. Auto mode implied (manual_launch ignored, or rather: door mode forces auto).
  3. Roster reset at round END, not all-gone — today _maybe_reset_round (:734) waits for every pod to vanish. In door mode, ALSO reset (restore orig_roster + template egg) as soon as stop_sent fires and the post-round rejoin churn begins, so a new player's walk-up can claim a seat for the next round even while last-round players are still relaunching. Care: re-key live beacons exactly as :737-771 already does; keep seat_identity so returning players get the same seat (#34).
  4. Trim stays — the existing LAUNCH trim (:651) is what makes a partial roster launchable (finding 8). No change.
  5. Stop → re-arm loop closure — after stop_sent, clear stop_sent/stop_requested/ mission_started_at when the round resets (the :750-757 and :791-796 blocks already do this; verify both fire in door mode's earlier-reset ordering).
  6. Operator visibility[door] log lines: round number, seated players, next-launch countdown; surface in the GUI via the existing _StampedOut tee / remote control channel (no new protocol — get mission already reports state; add a door status line to it under the existing whitelist mechanism).
  7. tools/btoperator.py — a "Revolving door" checkbox that adds the flag when spawning the relay (local) or is simply visible state when remote (the remote control set whitelist gains door, door_min, door_round keys via CONTROL_SET_KEYS).
  8. Idle behavior — 0 players seated: relay just sits at the join wait (it already does); no special case. 1 player with DOOR_MIN_PLAYERS=2: waits, logs [door] 1 seated, need 2.

What does NOT change

  • Player zips/exe/bats — 518 as shipped. Joining is the normal join.bat flow.
  • Classic mode — every door branch is if self.door_mode:; default behavior untouched (tonight's round runs the unmodified console).
  • The #33 abort-settle, REJOIN-on-bad-HELLO, storm damper, seat identity — all kept; door mode layers on top.

Risks / edge cases to test

  • Mid-round joiner walk-up: a player joining while a round is RUNNING must be parked cleanly at the join wait (held egg) until the round ends + resets — verify the hold path doesn't hand them an egg mid-round (today's held-egg logic should cover it; test it).
  • Round reset racing the relaunch churn (change 3 is the riskiest): last-round pods relaunch and re-HELLO while the roster is being restored. The #33/#34 machinery was built for exactly this; run the 3-client churn rig against door mode.
  • _relay_trimmed.egg staleness between rounds — reset must re-point egg_path at the template (already done at :766-771 region; verify in the earlier-reset ordering).
  • Endless operator absence: crash of one client mid-round → existing abort/settle path → door mode must re-form the next round without operator input (this is the whole point — soak it).

Test plan (before fielding)

  1. 3-node loopback rig, DOOR_ROUND_SECONDS=120: 3 clients seated → autolaunch → round end → all relaunch → next round autolaunches. 5+ consecutive rounds unattended.
  2. Joiner test: start round with 2, connect a 3rd mid-round → parked → seated next round.
  3. Leaver test: kill one client mid-round (its own PID only) → survivors finish → next round forms with 2 → the leaver rejoins a round later.
  4. DOOR_MIN_PLAYERS=1 solo soak overnight: round count in the relay log, zero stalls.
  5. Classic-mode regression: run one normal manual-launch round — byte-identical behavior (no door branches taken; grep the log for [door] = zero lines).

Phase 2 — True Drop-In (engine work; LATER)

Do not start without the spike. Everything below is sketch-level [T4 until spiked].

The design

On a mid-mission PEER_UP for a host with no entities, each machine re-sends the MakeMessages for the masters it owns (its BTPlayer + mech — projectiles are transient and self-expire). This finishes what NotifyOfReplacementEntityCreation left "under construction", but at the game layer where we control both ends.

Pieces:

  1. Late-attach detectionHostConnectedMessageHandler (L4NET.CPP:1551+): if GetApplicationState()==RunningMission when a roster host comes online, fire the re-make pass for locally-owned masters.
  2. Make re-send — each master rebuilds its Entity::MakeMessage (same path HostManager::NotifyOfEntityCreation used at creation, HOSTMGR.cpp:360-410) and sends it addressed/broadcast. NOTE: must verify a make can be regenerated post-creation (serialize current state, not birth state — that's actually BETTER for a joiner: current position/damage).
  3. Duplicate-make guard — peers that already know the entity ID must ignore (or idempotently accept) the re-make. Check the replicant-creation path for existing-ID behavior; add a guard if it double-creates.
  4. Relaxed launch gate — a door-mode env (BT_DOOR_LAUNCH=1?) letting the mission launch with roster hosts still Opening; absent seats stay pending instead of blocking. (Find the exact all-connected wait first — it's the stall the relay warns about.)
  5. Relay door mode v2 — skip the all-ACK egg hold entirely: hand a joiner the egg mid-mission, send them RunMission solo (the pair-timing needs thought), PEER_UP does the rest.
  6. Joiner-side world catch-up — terrain/map entities are static (loaded from the egg, StaticEntityCreation path) so only dynamic masters need sync. Score state rides the existing update stream (Player::WriteUpdateRecord ForceUpdate — already fixed for #29).

The 1-day spike (go/no-go)

2-node rig: node A in-mission; node B connects late with a hacked-in gate bypass; node A manually re-sends its mech make on PEER_UP. Pass = B sees A's mech and can shoot it. If the make can't be cleanly regenerated post-birth, or replicant creation of an existing ID corrupts state, stop and re-scope.

Risk register

  • This is exactly the interest-teardown crash neighborhood (the #35 NULL-vehicle AV, fixed in L4AUDIO.cpp) — every join/leave exercises Orphan/RemoveUninteresting paths. Soak with join/leave churn + the crash self-report armed.
  • #if 0 partial-implementation interest zones mean interest filtering is effectively global — fine for our player counts.
  • Wire format is a compatibility surface: any new route/message = new exe for EVERYONE (no mixed-version doors).

Rollout order

  1. Phase 1 next work session (console-only, no rebuild) → soak on the operator machine → field it on the next casual night with DOOR_ROUND_SECONDS=600.
  2. Collect a week of carousel logs (that's the goal being served).
  3. Phase 2 spike when there's appetite; commit only on a green spike.

Morning checklist (verify before coding Phase 1)

  • Re-read _check_launch_gate + ALL-READY hold ordering with door gate in mind (btconsole.py:780-813, :910-934).
  • Trace the held-egg path for a walk-up arriving mid-round (change-3 risk).
  • Confirm which of the two reset blocks (:750-757 vs :791-796) must fire on the door-mode early reset, and that egg_path re-points at the template.
  • Confirm parse_egg_mission_length override path for DOOR_ROUND_SECONDS (prefs-rewrite at :603-622).
  • Grep for every len(self.roster) comparison — each is a door-mode decision point.