crouch gate to the LIVE drive value
The myomer system was ALREADY COMPLETE (2026-07-31 seek audit): Performance
wrapper @004b8b9c, AvailableOutput @004b8ac0 (gear clamp x quadratic heat
degrade x (1 - zone damage)), and the master-perf chain walk + speedDemand
scale + turn freeze in mechmppr.cpp:990 -- the same @0x4a9cf2-0x4a9da4 bytes
the crouch dig re-decoded. The 2026-08-05 banners calling the feeder dark
were an export-gap-blind grep (named members, not offsets). Fixes:
mechmppr publishes the chain MAX into mech->myomerEffectiveness (the
binary's +0x79C home) so the crouch posture gate reads the live factor
(dead/overheated myomers now genuinely refuse squat/rise -- previously the
gate read a neutral 1.0 and never fired); the duplicate speedDemand multiply
in the posture block is removed (mechmppr's is the one application); banners
and locomotion.md corrected. Squat re-benched green on the live wiring.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The WIP's 'pose does not hold' was a chain of bench-instrument errors, not a
code bug: every capture ran in COCKPIT view (the pilot cannot see their own
legs; the eye-height residual masked as reversion). Joint probes prove the
park holds indefinitely (knee 1.138, root -2.219 steady); the 2-node bench
shows the observer's replicant fully crouched and held (duckmpA_031 -- the
type-3 state record carries it with zero new replication code); the second
scripted press (new BT_BTNTEST2 env) verifies RISE -> standing zeros.
MP button delivery confirmed mode-mask-clean (the one miss was round-start
jitter). Diags added, all BT_DUCK_LOG-gated: SetLegAnimation re-arm tracer,
1 Hz joint probe, RIO press mode-mask, BT_TREE_LOG topology dump, and the
squat-park log. RESIDUAL filed: pilot's own cockpit eye does not ride the
root drop (DPLEyeRenderable chain composition; cosmetic).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Raw-disasm of the dark master-perf region (@0x4a9cf0-0x4aa0af): the myomer
effectiveness factor (+0x79c, MAX over heatables' +0x31c, scales speedDemand
-- feeder @004b8be3 unreconstructed, neutral 1.0 [T3]), the posture selector
(+0x3f8: mode/novice/leg-state/myomer gates -- novices cannot crouch), and
the DuckRequest consumer (standing -> SetLegAnimation(2) 'sqd', ducked ->
SetLegAnimation(3) 'squ', ForceUpdate 8+1 ships the type-3 state record,
stability alarm flips, request consumed). +0x1DC = mountSegment... er, the
searchlight learned that one; here: mapPosture @0x3f8 + myomerEffectiveness
@0x79c members land; value-space note (port normal mode == 1, binary 0).
VERIFIED: request->consumer chain fires ([duck] SQUAT), the sqd clip plays
(22kf/7joint, ends root -2.22 crouched -- clip data parsed from BTL4.RES,
squ is its exact mirror; loader slot map re-verified byte-exact). OPEN: the
parked crouch pose does not HOLD on screen (reverts ~1 frame after clip end
with NO SetLegAnimation re-arm logged) -- the hold's render path is the
remaining dig; [duck] re-arm tracer left in SetLegAnimation.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
SPOT.BGF decoded: a 7-vert cone from the mount, ~50u forward and ~35deg DOWN
(a ground-pool lamp, not an air beam), verts tinted cyan-white, material
class 'brighten' smuggling its additive factor in DIFFUSE.r (0.25) with a
warm emissive on the night page. The loader had never met the class -- it
drew as an opaque dark-red blob. Now: brightenFactor parsed (name-gated to
brighten*), batch -> L4DRAWOP.brightenAlpha, drawn in the blend pass as an
additive veil (dest += vertexRGB x factor), unlit. [T3] tint compose
(vertex cyan vs night emissive warm) noted in the draw branch -- field
eyeball accepted the current look.
BT_SPOT_SELF=1 (bench-only): builds the cone on the own-cockpit tree so the
BT_CAM=face view can inspect it solo. KB: view toggle is BACKTICK ('V' is
the rear-view hold since #68 -- the toggle skips bound keys); stale V-toggle
claims swept.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The subsystem (sim/toggle/attribute/replication) was already complete; this
lands the missing VISUALS, decoded from MakeMechRenderables @004cef28 case
0xbd8 (raw pseudocode part_014):
- COCKPIT: the 1995 searchlight is a FOG SWAP -- the @00456778/@00456814
watcher switches DPLRenderer::SetFogStyle between the authored fog= (lit)
and nosearchlightfog= (dark) sets per map/time page in BTDPL.INI. The
engine kept the whole system under its real names; completed the stubbed
plane application (currentFogNear/Far) and transcribed the watcher (with
its inverted-cache seed) into TickSearchlight. CONSEQUENCE: night now
STARTS on the authentic dark set (near-plane 5u on arena pages) -- our
builds had rendered the searchlight-ON fog permanently.
- EXTERNAL: spot.bgf beam cone hung on the searchlight SITE joint, shown/
hidden from the replicated LightOn attribute (@0045612c watcher). Site
segments now build geometry-less DCS children (posed + parentable, as the
1995 graph did) -- previously they were skipped entirely.
- searchlight.hpp: commandedOn @0x1DC identified as mountSegment (resource
segmentIndex; the cone's mount joint).
Benches: searchfog.sh (solo cockpit: first-tick dark sync, F5/0x14 press ->
SetFogStyle(2), red-fog probe end-to-end), spotcone.sh (2-node: B's button ->
lightState replication -> A logs "[spot] cone SHOWN (seg 20)"). Cone look
(size/aim on the mount) pending an eyeball pass.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Reconstructs the dark-gap tail of Mech::TakeDamageMessageHandler
(@0x4a02f4-0x4a0890, raw disasm): the three id-0x16 score reports (kill to
the shooter's player / type-0 wire-fidelity / received to the victim's
player) and the BT 0x38-byte VehicleDeadMessage extension {killed-by player,
kill zone} dispatched from the death tail. Retires BTPostDamageScore /
BTPostKillScore and the per-hit inflicted credit (never existed in 1995:
@0x4c0200 is bound in no handler-table entry -- byte-scan receipt in
decomp-reference). Suicides now dispatch and the handler negates the award
(the #134 panic penalty). Collision divert falls through to the death tail
per @0x4a0375 (wall deaths respawn + blast; no score). ScoreMessage fields
renamed to decoded truth (vitalHit/zoneIndex/subsysID) + wire asserts;
console VTVDamaged points_transfered corrected (Round(award), not Now()).
Benches: scorekill.sh cross-node kill (kills=1 award=4.88, killedBy=2:1
zone=3, single death cycle), scoreself.sh suicide (type=2 award=-39.00
kills=0), deathblast2.sh re-verified (72 bursts at ~9u).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The missing half of Advanced Damage, found by call-scanning
Explosion::SplashDamage @0042fad0: TWO callers, not one -- Missile::Perform
(the known #62 path) and 0x4a0bda, the UN-EXPORTED tail of
Mech::TakeDamageMessageHandler itself. Raw disasm @0x4a07b8-0x4a0bda:
when the victim ENTERS dead(9)/eject(10) during the applications, the
binary sets the wreck burning (id 0x17, deferred -- handler not yet
reconstructed), spawns the death Explosion (model 0x31 -- our death-list
visuals stand in), and SPLASHES:
gates : owning player's advancedDamageOn (+0x264) AND NOT
suppressConsole (+0x258 -- eject sets it: PUNCH-OUTS NEVER
BLAST, the authentic anti-suicide-bomb rule)
damage: type 2 Explosive, amount = deathSplashDamage (mech+0x520),
bursts = round(0.001 * moverMass * 15.0) -- scales with tonnage
radius: deathSplashRadius (mech+0x524); per-victim falloff
bursts/dist^1.25 in the shared core
Draco's collision-divert suspicion is settled: the blast is TYPE 2, the
divert never touched it -- the tail was simply never reconstructed.
Port: deathSplashDamage/Radius PROMOTED from the Wword scratch bank to
named Mech members (the bank is one GLOBAL array -- authored per-chassis
values were clobbered to the last-loaded mech); BTSplashCore split out of
the #62 weapon splash and shared; BTApplyDeathSplash + the death-edge arm
in the handler tail; BTPlayerConsoleSuppressed bridge (friend).
Bench (2-node, B parked 8.9u from a self-destructing A): blast fired with
authored madcat data (radius=50, amount=5, mass=75000 -> 1125 base
bursts), B took 73 bursts (falloff exact: 1125/8.91^1.25), cross-pod
delivery + cylinder spray verified on B's own log ([dmghit] type=2
burst=73 across zones). ~365 damage at 9u -- Draco's 'double kills on
drops' economy restored.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Provocations run under the verified [skate] detector: sustained healthy
walking, 7x respawn-while-moving, gimp onset at speed, ~21s of sustained
GIMPED walking (aimed-leg self-damage: BT_SELF_DAMAGE_ZONE=dz_ldleg),
leg-destruction death (authentic: lvl 1.0 -> leg gone -> fall/death; the
mid-session "died of the gimp edge" reading was a capped-print artifact,
retracted), and respawn. ZERO skate anywhere. Peer gimp replication
VERIFIED live: observer reads gl=3 + sim=3 with the gimp bodyStates
cycling for the whole master limp window (#82 remains fixed). Conclusion:
the field skating does not reproduce at lab scale; the detector + SKATE
matchlog record ship with the next cut and the field names the failing
case. Bench scripts archived (skatebench3-7; 7 is the clean-room one --
the sed-derived chains dropped envs twice).
Also: BT_LAMP_LOG=1 joins the field bats (#135 -- lamp/annunciator edge
forensics; near-zero noise, answers leak-no-flash reports in one grep).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The night-12 field logs eliminated record starvation (zero [ghost] during
three observed skating windows), so the bug lives in gait APPLICATION on
peers. This adds the [skate] detector to the death-handler tick: a
replicant moving >0.08 u/frame for 90+ frames with BOTH animation
channels idle (legCycleSpeed + bodyCycleSpeed ~ 0) logs one line per
episode + a SKATE matchlog record carrying the discriminating inputs
(legCyc/bodyCyc/cmdSpd/destroyed/mode).
Honest history: the first build keyed on legCycleSpeed alone and
false-fired on every healthy movement phase -- the current peer
architecture poses joints from the BODY channel (s_peerLegCh=0,
AdvanceBodyAnimation mj=1), so legCycleSpeed==0 is NORMAL there. Caught
same-session by the [gimpfeed] silence (AdvanceLegAnimation never runs
on peers); corrected to channel-agnostic before anything shipped.
Bench (skatebench2.sh, 2-node, autodrive walker + kill every ~40s):
7 death/respawn cycles, ZERO skate hits either side -- no false fires,
and light local conditions do NOT reproduce the field skating. Next
provocations: leg-GIMPED walker (the #82 family transition) and 6-player
load; otherwise the detector rides the next cut and the field names the
failing case for us.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Night-12 field report (Ronin/Conn Man/Oracle, blackhawk-correlated): lock
ring lit with the reticle visibly off the mech + no-reg complaints. Root
cause: TWO port stand-ins answered where the 1995 card (which cast against
the DRAWN geometry) would miss -- the pick's any-object sphere fallback and
the caller's whole-mech AABB fallback. The regime that exposes them: a
LEVEL boresight over a SHORT mech -- the blackhawk's mesh tops out below
eye-ray height, so the ray clears every triangle but pierces the fat cull
spheres; the ring lights with the reticle above the mech's head (the
operator watched exactly this on the sweep bench). Careful aimed-down fire
rides triangles, which is why Oracle's per-panel audit passed on the same
build.
Fix: MechSegmentPick returns 1=drawn-geometry hit / 0=TRUE MISS / -1=no
render tree; the sphere may answer ONLY for a mesh the reader cannot parse
(pm==0 -- currently none exist: counters objs/invFail/noTri all clean);
the AABB survives ONLY as the pre-tree replicant grace. A readable mesh
the ray misses is a MISS -- no lock.
Verified (2-node vs bhk1 at 100u, all runs on force-relinked string-
verified exes after today's stale-link flake):
- LEVEL lock-sweep: 0 locks all run (pre-fix: lock band from 168 sphere
answers; picksrc tri=0 sphereFB=168).
- DOWN-PITCHED sweep: locks return 100%% tri-sourced (tri=158 sphereFB=0),
landing on real parts (rarm/ldleg/rdleg) with honest gaps.
- Full zone-walk matrix: tri=18874 sphereFB=0 box=0; victim took 156 hits
across 16 zones incl. both side torsos -- combat un-regressed.
New instruments (all env-gated): BT_LOCK_SWEEP=<axis> torso pan (the
operator-visible lock-envelope bench), [locksweep] transition log,
BT_LOCK_ENVELOPE synthetic unit-sweep probe, [picksrc]/[pickbox] source
telemetry with objs/invFail/noTri localization counters.
Bench: scratchpad/night12/zonewalk_bhk.sh.
NOTE for the field: locking is now strictly TIGHTER (ring = reticle truly
on the machine). If era testers feel the pods were more forgiving,
Draco's "slight lock linger" memory becomes a deliberate investigation
(sourced hysteresis), not an accidental sphere halo.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The desktop plasma window (L4PLASMAWIN, "BattleTech - Plasma") is created by a
different TU than the per-display panels, so it couldn't be a gWins[] entry and
sat outside the sticky-layout / ,noframe system. Add a tiny extern-window
registry to L4GLASSWIN so it rides the same glass_layout.cfg:
- BTGlassLayout_QueryWindow(title, rect, noframe) -- read a title's saved rect
and ,noframe flag (used by the plasma on creation, BEFORE sizing, since
WS_POPUP's frame extent differs from the framed tool window).
- BTGlassLayout_RegisterExtern(hwnd, title) -- register an externally-created
window so SaveLayout writes its line (with a last-known-rect cache so a
teardown before the save still preserves the line).
- BTGlassLayout_Save() -- public save trigger the plasma WndProc calls on
WM_EXITSIZEMOVE / teardown.
L4PLASMAWIN now reads its saved rect+flag on create (WS_POPUP when ,noframe,
position restored), registers itself, and saves on finished-drag/teardown. All
BT_GLASS-gated; the pod is untouched. The plasma blits directly every frame, so
it has no WM_TIMER focus-throttle.
Verified: Release links clean; drag wrote "BattleTech - Plasma=321,222,528,167",
reload with ,noframe brought it up WS_POPUP (WS_CAPTION absent) at 321,222.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two glass-panel indicator fixes reported by playtesters.
1. FLASH SLOW UNLESS FOCUSED. The per-display panel windows repaint off a
62ms WM_TIMER, and Windows coalesces/throttles timer + paint messages for a
window that is in the BACKGROUND (unfocused) -- so with the game window
holding focus the panels' lamp flash (a repaint-driven animation) crawled,
and giving a panel focus un-throttled it. (My earlier surround fix drew in
the main D3D frame and never touched these separate windows.) New
BTGlassPanels_Tick(), called once per frame from the main render loop
(L4VIDEO), drives a synchronous InvalidateRect+UpdateWindow at the ~16Hz
flash cadence -- the main loop runs every frame regardless of which window
has focus, and the forced paint bypasses the throttled timer-message path.
The WM_TIMER stays for its one-shot re-snap.
2. RED PANIC/EJECT. 0x3D is the Panic/Eject button; on the blue flight panel
it was just another blue block. The shared flight-grid geometry (L4RIOBANK)
now tags 0x3D as colorClass 0 (red); L4GLASSWIN's AdoptBank maps colorClass
0 -> ClrRed explicitly (the flight bank's ClrBlue default no longer swallows
it), and the surround (L4VB16) already renders non-blue/yellow as red -- so
the eject stands out red in both the surround and the exploded window.
Verified: Release links clean (40 tolerated /FORCE externals only); exploded
panels boot + run 13s no crash under the per-frame repaint pump; riobank dump
confirms 0x3d class=0 (red), neighbours class=2 (blue).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The 08-03 zone rule (struck SEGMENT's SKL dzone, ALWAYS -> every aimed
torso hit = center torso) was wrong, and era players' pushback caught it.
Byte-level proof: MAD_TOR.BGF zone-tags the hull PER PANEL (dz_utorso x36,
dz_ltorso/rtorso x18, dz_dtorso x16, all four rear panels, searchlight);
the dpl hit result kept GEOGROUP granularity (dplHitGeoGroup, T0); and the
binary's segment->zone map @49db20 has NO runtime caller (raw call-scan:
sole caller = CreateStreamedDamageZone, load time) -- no segment-level
collapse mechanism exists. Oracle's night-10 'only LCT gets hits' audit
was the BUG's fingerprint, not the pod's design.
Fix: MechSegmentPick attributes the struck triangle to its draw op (index
range) and takes the op's .DZM-bound zone -- the #87 armour-darkening
bindings, the same authored patch->zone mapping that already paints the
panels -- with the segment dzone as the untagged fallback. ZoneAimPoint
now aims hull zones at their patch CENTROIDS (all hull zones previously
shared the chest cull-center), which also upgrades the zone walker.
Bench (2-node zone-walk vs spinning madcat, zonewalk_madcat.sh): every
hull panel resolves individually -- utorso 11/12 in-zone, no L/R
mirroring, all four rear panels register; misses are the panel facing the
shooter mid-spin (correct geometry, not misattribution). Victim applied
254 hits spread across every panel family. Was 102/102 hull aims ->
dtorso before the fix.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The night-11 instrumentation that makes the next ghost/K-D report
diagnosable instead of anecdotal:
- UNGATED [ghost] stale-replicant detector: ReadUpdateRecord stamps every
applied record; the death-handler tick logs ONE line per starvation
episode (>600 frames, unburied) with entity id + mode + last position,
plus a GHOST matchlog record. Verified both ways: zero false positives
on a healthy 2-node session; fires on both nodes at frame 601 after a
mid-session relay kill.
- Entity IDs on the render forensics (MakeMechRenderables / RemakeEntity /
wreck-swap fallbacks) and the replicant un-wreck line un-gated -- ghost
triage no longer needs players to set envs.
- players/*.bat (steam + both joins): BT_MATCHLOG/BT_SCORE_LOG/BT_DEATH_LOG
on for every field session.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Operator reports audited vs the binary (full matrix in RESPAWN_REARM_PLAN
addendum):
- VALVES (real gap): Condenser reset @004ae534 was missing from the decomp
export -- raw disasm shows it chains HEATSINK (coolant refill runs; the
old body chained HeatableSubsystem per the stale TCP shard) then, respawn-
side, resets valveState to detent 1 and restores massScale from
refrigerationFactor. Mech::Reset now also runs the binary's tail call
(@0049f788 BTRecomputeCondenserValves) so flow fractions rebuild from the
reset detents. Bench: detent 5 -> death -> "[respawn] Condenser1 valve
detent 5 -> 1".
- #129 SMOKE (real gap, peers-only): the replicant un-wreck edge rebuilt
the model without the @004d0c14 per-entity effect cleanup, so the
observer's last 10s wreck-plume window rode the teleport onto the fresh
mech. BTStopEntityPfx now runs on the edge; bench shows no plume line
after any un-wreck until the next death.
- AUTHENTIC (no fix): weapon->generator taps persist (@004b0e6c only
resolves the link) and MFD display/control modes persist (mapper vtables
0050f45c/0051e440 slots 8-11 = plain root bodies, read from the exe).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The frame-adapter's inferred twist-sign flip (SelectSlice theta -= twist,
vs the binary's += pre-reflection) was the last unverified half of #124 --
every earlier probe ran at twist 0. Bench: stationary madcat target with
the torso PINNED at 0 / +140 / -140 deg, LRM salvos from a fixed shooter
(missiles = the authentic cylinder path; the binary DROPPED zone -1 beam
damage). Result: slice picks track the physically-facing flank in BOTH
directions (twist-left -> right-family zones for left-flank impacts,
twist-right -> left-family), deterministic, wrong-sign outcome (slice 7
vs observed slice 1) clearly excluded. No game-code change needed.
Instrumentation added (all env-gated):
- torso.cpp BT_FORCE_TWIST=<-1..1>: HOLD the sim's analogTwistAxis (the
input-level pin was dead -- live input rides the CONTROLS.MAP device
push, and Basic mode auto-centers; sim-level is plumbing-independent).
- dmgtable.cpp [slice] line: rot flag, live twist, thetaIn/thetaAdj,
chosen slice -- the weighted leaf roll made zone-only logs ambiguous.
- [dmgresolve] now names the zone (BTMechZoneSegAndName).
- mech4.cpp BT_FORCE_TWIST input pin + one-shot mode cycle (kept as doc
of the dead path), mechmppr [mppr] mode probe.
- scratchpad/night11/twistsign.sh: the 3-config bench.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The binary's inside renderable build sets type A (=4, part_014.c:5077) and
its per-segment loop looks up EVERY segment's type-A mesh (FUN_00424084,
:5570) with no torso/cop filter -- the arcade drew all authored own-body
meshes (thor pod, owens legs). No opt-out exists in the binary.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Three testers reported a black rectangle swaying with the footsteps in the
thor cockpit (Summoner = the thr1 label, same THX canopy -- no isolation).
Decode: the inside view is a PER-MECH AUTHORED type-A set, not the cop
alone (no fallback in EntitySegment::GetVideoObjectName -- authored data):
madcat/vulture/bhk1 = cop; sunder/loki/avatar = +tor; thor = +tor +MSL
(the shoulder pod, the reported rectangle); owens = +both legs +tshd.
The pod/leg pilot-facing surfaces use the SAME "<pfx>skin:blakskn_dz_*"
interior-structure material as the canopy frame, but the unlit frame
constant was keyed on the _cop FILENAME -- identical material rendered
(0.13,0.12,0.15) on the canopy and pure (0,0,0) on the pod/legs
([matlog]: owx_cop blakskn vcol=FF211F26 vs owx_lule vcol=FF000000).
Fix (bgfload.cpp): the frame-constant treatment keys on meshIsCop OR
material contains "skin:blakskn_dz_". mechfx:blakskn_mtl (tshd shadow
quads) deliberately excluded. Verified: zero pure-black px in the lower
view band across walk captures, the pod plate renders frame-toned and
blends with the bar at rest (the reported anomaly dissolves), owens legs
read as coherent structure, canopy/terrain un-regressed.
Diags added: BT_MAT_LOG=<stem> per-batch material routing dump (bgfload),
BT_HIDE_INSIDE_SEG=<substr> inside-mesh hide (btl4vid), [view] per-segment
inside-roster names. KB: cockpit-view.md exactly-one claim corrected +
the #91 section; bench scratchpad/night11/thorrect.sh.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A failed join returns 1 to the FE, which QUITS the exe -- so every
rejection so far was a log line plus "the game just closed" (#68's exact
complaint). New LobbyNotice() = same text in the day log (flattened, still
greppable) + blocking MessageBox. Wired to every join-side bail:
- BUILD MISMATCH: when the version-filtered search is empty, probe once
without the version filter; if a lobby IS up, the box names the host's
build vs ours (lobby data rides the list result -- no join needed).
Post-entry verify mismatch gets the same box.
- NO LOBBY FOUND: probe empty too -> plain no-lobby box naming our build.
- STEAM UNAVAILABLE: transport install failed.
- LEFT BEHIND: host launched without us (no token in btl4map).
Old exes still exit silently on rejection -- nothing shipped today can
add text to a binary players already have; the host's roster marker +
REJECT log line remain the operator's view of those.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The joiner-side filter only runs on builds that HAVE it -- a stale-zip
player runs an old exe with no filter, finds the lobby, and joins anyway.
The host must do the rejecting, and the lever already ships in every old
build: a member omitted from btl4map hits its own "the host's map is
missing us" path and fails the join cleanly.
1. PublishSelf stamps per-member data bv=BT_VERSION_STRING; an old exe
cannot fake a key it never sets.
2. Host GO mint: any non-self member with absent/mismatched bv gets NO
token -- omitted from the map, loud REJECT LobbyLog with both builds.
3. Room screen: mismatched members show [WRONG BUILD -- WILL NOT LAUNCH]
so the host sees who's stale BEFORE pressing GO, not after the match
starts short-handed.
Verified in the deployed exe by string scan (btl4ver, REJECT-at-GO,
BUILD MISMATCH ascii + WRONG BUILD utf16 all present). Field behavior to
verify on the next Steam night.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Mixed-build lobbies silently corrupt raw-struct replication (night-9: one
stale-zip player, one desynced stream, ghost mechs + K/D doubt). Three
additive edits in btl4lobby.cpp:
1. HOST stamps the lobby with its exact build (btl4ver = BT_VERSION_STRING).
2. JOINER's lobby search filters on build equality -- a stale-zip player
simply finds no lobby, and the "no lobby found" log line NAMES the local
build so the report is self-diagnosing (#68's silent-exit lesson).
3. Post-entry verify (covers invites/direct joins + unstamped older hosts):
mismatch -> log both versions loudly, LeaveLobby, fail the join.
LAN/relay (join.bat) handshake remains a separate 717 item -- this covers
the Steam path the operator asked about.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>