83b3f31957603ebb32b56da8c81d5a71be6295ce
14
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
83b3f31957 |
Whirlwind respawn: authentic death->drop-zone->recreate cycle (task #52)
Death transition (mech4) now dispatches VehicleDead(-1) to the owning player; BTPlayer::VehicleDeadMessageHandler restructured to the authentic @004c05c4 three-way branch: -1 = death bookkeeping + sever playerVehicle (wreck stays) + 5s re-post; >=0 = engine drop-zone hunt -> DropZoneReply -> CreatePlayerVehicle (new mech); -2 = the acquire probe. Guarded on a DropZones group so a zone-less mission stays dead instead of aborting. Three latent bugs the respawn path exposed, all fixed: - IsMechDestroyed latched on graphicAlarm>=9 alone; a later leg hit on the wreck rewrites the alarm to 4/3, un-latching -> the death transition re-ran (double kill/score, abort). Now latches on movementMode 2||9. - Score handlers dereferenced the severed playerVehicle during the dead window; guarded. - The console score flush routed a NetworkClient::Message through the player's Entity::Dispatch, which stamps entityID past the smaller struct -> /RTC1 stack overflow. Sent via application->SendMessage instead. - Renderer LoadMission re-entry (per viewpoint-make) re-read the env INI; its light block Fail'd on stale sceneLightCount. Reset it in DPLReadEnvironment so the respawn's second read is clean. Verified 2-node self-drive: B killed repeatedly by A respawns each time (wreck stays), ticks + reloads + takes fresh damage on the new mech, no abort/hang across multiple death->respawn cycles. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
e430e474cb |
Death gate: a destroyed mech takes no pilot input (ghost-drive fix)
User report from 2-window play: after dying, the pilot could still steer the wreck around invisibly. Zero throttle/turn/fire and the forced harnesses at the gBTDrive source when IsMechDestroyed() -- deliberately INPUT-only, so the gait/animation machinery keeps running and the death collapse plays out. Also recorded in the KB: the killer's screen shows the wreck SWAP but not the death SINK (needs the Mech-level death/knockdown update records, disasm preserved from task #51), and the respawn-cycle scouting (type-12 intro scripts are binary ref-records; adrop; DropZone + Mech::Reset machinery) for the recovery-whirlwind reconstruction (task #52). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
29035028fd |
Cross-pod beams: replicate emitter discharge via subsystem update records
User report: lasers only visible on the window firing them. The peer's replicant emitters never learned the master fired. THE AUTHENTIC PIPE (decomp-verified): - FUN_0041c350 (the "beam keepalive" ServiceDischarge/ContinueDischarge call) does TWO things: queue the LOCAL deferred beam-effect callback (@0x4bac0c) on the app+0x34 manager -- our per-weapon render walk already plays that role -- and set the subsystem DIRTY bit, which maps to the 2007 engine's updateModel / ForceUpdate(). - Replication rides SUBSYSTEM UPDATE RECORDS inside the mech's update message: the roster walk already hands the entity's stream to every subsystem's PerformAndWatch; Simulation::WriteSimulationUpdate serializes each requested updateModel bit; Entity::UpdateMessageHandler routes received records by subsystemID to the subsystem's ReadUpdateRecord. All engine machinery -- the missing pieces were the Emitter's serialize/apply pair + the triggers. CORRECTIONS to the dormant task-33-era transcriptions (never exercised -- nothing ever set updateModel -- so the latent misreads never surfaced): - The weapon-family VTABLE SLOT MAP was swapped: slot 6 = ReadUpdateRecord, 7 = WriteUpdateRecord, 9 = TakeDamage (evidence: Mech hierarchy symmetry + body semantics; @004ba568 resolves an EntityID at rec+0x30 through the entity index -- record semantics, not Damage). Renamed across MechWeapon / Emitter / ProjectileWeapon; the real Emitter::TakeDamage @004bafc8 is undecoded (inherits MechWeapon for now). - Emitter/MechWeapon Write: `*record = 0x38/0x18` is the record LENGTH, not recordID; rec+0x30 is the TARGET's EntityID (GetEntityID()), not a colour -- the old `CopyColor(targetEntity+0x184)` was also a databinding trap. - OVERRIDE-SIGNATURE TRAP: the decls used each class's own shadowing UpdateRecord typedef as the param type, silently NOT overriding the engine virtual (the base ran instead; nothing would ever have serialized). Base-typed params (Simulation__UpdateRecord*), casts inside. - Emitter::ReadUpdateRecord reconstructed (@004ba568): target EntityID resolve (drop unknown non-null targets), MechWeapon alarm apply chain, beam fields. - ServiceDischarge/ContinueDischarge: ForceUpdate() per keepalive tick + one final record at beam end (turns the peer's beam off). - Mech::DrawWeaponBeams extracted from the player-only drive block so the walk runs for REPLICANTS (+ per-mech gun-port cache -- the process-wide statics would have served the player's segment pointers as the replicant's muzzles). VERIFIED 2-node: A fires 57 volleys -> 225 emitter records -> B applies all 225 -> B draws 414 beams (PPC blue / laser red, from A's replicant's own gun ports). Solo un-regressed (150 beams, kill chain, no crash). Also preserved: the full Mech::WriteUpdateRecord @0x4a0c2c recovery (reference/decomp/mech_writeupdate_004a0c2c.disasm.txt) with all 9 record types decoded (pose/alarm/leg-state+heat with the body-channel write-through re-sync, knockdown, death, impact, movementMode) -- transcription deferred; it replicates remote knockdown/death/heat and was not needed for beams. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
a110041a70 |
Fix "can't turn in MP": pilotArray[1] overrun stomped controlMode
User report from the first live 2-window session: throttle worked, A/D turn
did nothing. [mppr] showed the input REACHING the mapper (stickX=-1) but
turnDemand=0 and controlMode = pointer garbage (73583232 / 217478632 --
different per instance), from frame 1, MP only (solo mode=0).
Hunted with a cdb hardware write-breakpoint (ba w4 armed at ctor time on
&controlMode, compiled offset +0x128 from the [mode-ctor] probe). Caught the
writers red-handed: MechControlsMapper::BuildPilotArray / FillPilotArray.
ROOT CAUSE -- a shrunk-span array: the binary reserves a FIXED 10-slot pilot
block @0x15C..0x183 ((pilotArrayBuilt@0x184 - 0x15C)/4 = 10); the recon
declared `Pilot *pilotArray[1]` ("variable length"), so every write past
slot 0 stomps the members declared after it. In MP, FillPilotArray wrote the
PEER's Player pointer into slot 1 = over controlMode -> the turn shaping
dispatched on pointer garbage -> turnDemand 0. MASKED in solo: the zero-loop
overrun wrote 0 there, and 0 == BasicMode. This was ALSO the real cause of
task #48's "turnDemand zeroes out in -net" observation (worked around then
with the direct goto-turn; attribution corrected in the KB).
Fix: pilotArray sized to the binary's own 10-slot span; ctor zeroes all
slots; BuildPilotArray clamps pilotCount to capacity (local + 9 remotes; an
8-pod game fits). Diagnostics kept (BT_MODE_LOG ctor probe; [mppr] line now
prints mapper/&mode).
VERIFIED: MP session holds mode=0 throughout, turnDemand flows nonzero, the
mech turns (heading -1.33) and closes on the peer; solo kill chain intact.
KB: new "shrunk-span array" gotcha (reconstruction-gotchas 5) with the cdb
ba-w4 recipe; multiplayer.md task-#48 note corrected.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
d09cda6c36 |
MP: replicant gait animation -- peer mechs walk instead of sliding (task #50)
A peer's replicant mech slid around as a frozen statue: DeadReckon moved localOrigin but nothing animated the legs. Reconstruct the replicated-motion display loop: after DeadReckon, derive a SIGNED speed demand from the replicated velocity (the master publishes worldLinearVelocity in every update record, Mover::WriteUpdateRecord MOVER.cpp:759; the replicant stores it in updateVelocity.linearMotion :712; sign = forward dot against the -Z facing), write it to controlsMapper->speedDemand (the LEG channel's live demand source), and advance the LEG state machine for its JOINT writes only -- travel stays with DeadReckon, so position always follows the master and the clip cadence matches the replicated speed. Two replicant-only initialization holes found live (the master's equivalents are primed per-frame in the PLAYER-only drive block): - globalTimeScale/idleStrideScale read 0 -> clip advance time dt*0, the leg machine engaged (state 11) but the clip froze (legCycle stuck 0); - reverseSpeedMax2 (the run-cycle rise-clamp @0x7a0; LoadLocomotionClips does NOT set it) read debug-heap garbage -> legCycleSpeed clamped to -4.3e8 on entering run state 13. Primed both; legCycleSpeed sane-banded once. VERIFIED 2-node (BT_GOTO driver + BT_REPL_LOG observer): the replicant runs the full gait lifecycle -- stand -> walk(5,7) -> run(10,12), legCycle 7.5->34 tracking the master's speed, wind-down(8), Standing(0) when the master stops. Solo un-regressed (goto->aim->kill chain intact, no crash). projectedVelocity source-swap (the binary publishes the BODY channel's smoothed projection instead of the actual travel velocity) evaluated and DEFERRED: it risks a verified working replication feed for a smoothing nuance; recorded in multiplayer.md. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
9353d59eb9 |
Fix drive-to-range stall: BT_GOTO self-drives + steers correctly cross-net
The BT_GOTO beeline never actually closed on a target -- three separate faults: 1. It was gated behind `if(gBTDrive.forced)`, and `forced` was set ONLY by BT_AUTODRIVE -- a chicken-and-egg where the block that would drive was gated on the drive already being on. So BT_GOTO alone just turned in place (the "stall"). Fix: BT_GOTO enables forced mode itself AND supplies its own forward throttle (a beeline must drive, not only steer), cutting the throttle inside a stop radius (weapon range for "enemy", so it holds + shoots). 2. Steering used the gDriveHeading SCALAR MIRROR (seeded to 0), but MP pilots spawn facing a non-zero heading -> it steered the WRONG way. Fix: heading error from the mech's ACTUAL forward (localToWorld's -Z axis), signed angle to the target. 3. The goto turn was routed through controlsMapper->turnDemand, which zeroes out in -net mode (the mapper key-bridge only shapes the local viewpoint mech there) -- the heading froze and A drove straight past B. Fix: apply the goto turn to the orientation integration DIRECTLY (if gBTGotoActive, turn= gBTGotoTurn), after the mapper read. Verified 2-node one-box: A with BT_GOTO="enemy" ALONE (no autodrive) drives dist 1673->231m, holds at weapon range, mechPicks=59, autofire -> B hdlr=192, DESTROYED -- a fully automated human-style cross-pod KILL via the real beam path (not the FORCE_DMG hook). Solo un-regressed (plain autodrive still drives; goto="enemy" resolves the enemy, mechPicks=59, no crash). BT_GOTO_LOG traces the beeline. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
0dfbb97dcc |
Fix torso-pitch aim ray: gyro-stabilized boresight from the mech torso
The aim/pick ray was the active RENDER EYE's view direction, which made a
mech unable to aim at a distant enemy:
(a) it inherited the mech BODY pitch -- on a slope the body tilts ~8 deg to
conform to terrain, so the boresight pointed that far DOWN and the pick
ray hit the ground ~50-100m short of the target (groundPicks, never
mechPicks); and
(b) in CHASE view (the default) it was the elevated behind-the-shoulder
camera, so the ray started over the mech's shoulder and flew high.
Fix (L4VIDRND DPLEyeRenderable::Execute, the BTSetAimCamera publish):
- LEVEL the boresight: drop the view-direction pitch and rebuild an upright
basis (world +Y up) -- the guns fire along the mech's gyro-stabilized
HORIZONTAL heading, not the terrain-pitched body. Reticle X still carries
the torso twist (BTTwistToReticleX); reticle Y any elevation.
- ANCHOR the ray origin at the mech's TORSO (myEntity->localToWorld
translation + ~5) instead of the render eye, so it is correct in BOTH the
chase (default) and cockpit views.
Verified end-to-end: solo, enemy 120m ahead, BOTH chase and cockpit view ->
[target] "MECH under boresight ... mechPicks=59 groundPicks=0" -> beam ->
*** DESTROYED. Also proved PickRayHit resolves a REPLICANT when aimed at it
(hit=1 at 757m, 2-node run).
mech4: BT_GOTO="enemy" test aid now chases the nearest peer of ANY instance
(solo dummy OR replicant), not just replicants.
Still open (locomotion, not aim/MP): BT_GOTO/BT_AUTODRIVE stall short of the
enemy, so an AUTOMATED 2-node kill via the real beam isn't demonstrated yet
(the FORCE_DMG hook still is). A human can now aim correctly in the default
view. Solo un-regressed; build clean; KB validator clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
bac45851ad |
MP task #48 (partial): -net aim+drive already work; localize the real gap
Re-measured interactive -net combat on a 2-node run. The KB's "aim ray dead / drive dead in -net" was STALE: A's aim ray is live (noRay=0, the active eye publishes the aim camera) and A drives (BT_AUTODRIVE/BT_GOTO move it). The real blocker to a human cross-pod kill via the beam path is NOT the netcode: - Boresight inherits the mech BODY pitch. The aim ray = the active eye's view dir; on sloped terrain the body pitches to conform and the boresight points ~8 deg down ([EYE] up=(-.03,.99,-.14) == [pick] ldir.y=-0.145 every frame), so at range the ray hits terrain ~50-100m short of the enemy (mechPicks=0). - Drive/goto stalls ~700m short before closing to pick/weapon range. - PickRayHit vs a replicant is thus unconfirmed, but the replicant localToWorld IS correct (lstart magnitude == true A->B distance; the DeadReckon + localToWorld=localOrigin block already tracks the replicated position). Change: add a gated BT_GOTO="enemy" test mode (beeline toward the nearest live replicant) -- MP spawn coords vary per run so a fixed "x z" can't reliably face the peer. KB (multiplayer.md) updated with the corrected findings + the real remaining work (torso-pitch/level-boresight aim ray + drive-to-range). Solo un-regressed; btl4.exe builds; KB validator clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
54b6663b4d |
MP task #47 SOLVED: cross-pod damage + kill (validate the master mech)
Full cross-pod KILL now works: instance A (host 2) shoots B's mech (host 3) dead over the network; B runs the death transition on its own screen. Verified on a 2-node one-box run (BT_MP_FORCE_DMG on A): B's mech takes ~36 remote hits of 12 and is DESTROYED, with wreck swap + explosion. ROOT CAUSE (one bug): B's own MASTER mech was INVALID. Entity::Receive(Event*) does `if(!IsValid()) event->Defer()` -- so a cross-pod-delivered TakeDamage that arrived, resolved to B's real mech, and Posted, was DEFERRED forever; the handler never ran. The reconstructed Mech::Make force-set ValidFlag only for replicants (the P6 bring-up hack for the partial MakeReady handshake); a locally-created master never got validated. FIX (one line): Mech::Make now sets ValidFlag for the master too -- the reconstructed ctor builds the mech synchronously, so it's safe. Nothing else changed: targeting, the virtual victim->Dispatch, the replicant reroute, the wire, receive, and id resolution were ALL already correct. Two prior root causes were BOTH mis-diagnoses from the WRONG messageID: Entity::TakeDamageMessageID is 18, not 21. Every earlier probe filtered msgID==21, never matched a real TakeDamage, and mis-correlated the stray 21 hits' 3:19 / classID-48 against the real 3:22 hit -- inventing (a) a "cross-TU Entity layout divergence" (disproven: offsetof identical in both TUs, sizeof 444, entityID@0x17C) and (b) a "localID corrupted on the wire". Both FALSE; the id is correct end-to-end (3:22 = B's master). - game/reconstructed/mech.cpp: validate master + replicant at Make. - game/reconstructed/mech4.cpp: BT_MP_FORCE_DMG cleaned to a concise gated 2-node cross-pod reproducer (real virtual Dispatch path). - context/multiplayer.md, reconstruction-gotchas.md: corrected root cause + the validity-defers-cross-pod gotcha. Engine diagnostics reverted to clean. Solo un-regressed (the solo enemy was already force-valid; the change only ADDS ValidFlag). btl4.exe builds; full mission loop clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
04a2049ce2 |
MP task #47: DISPROVE the Entity-layout root cause; correct the record
The prior commit ( |
||
|
|
c78662a0f1 |
MP cross-pod damage ROOT-CAUSED: EntityID localID corrupted by hostID on the wire (task #47)
Traced the dispatched-message delivery end to end with BT_MP_NET / BT_MP_FORCE_DMG. Everything works except one wire step: - A's Entity::Dispatch reroutes the replicant's TakeDamage (application->SendMessage(ownerID=3, EntityManager, msg)); POST-Dispatch the message carries entityID=3:22 (host:local) -- the replicant's own ID, matching B's master (GetEntityID()==3:22). VERIFIED sent. - On B the message ARRIVES, GetEntityPointer finds an entity, Posts it, the event drains (ProcessEventTask = ProcessOneEvent(0)), Event::Process runs, Receive finds+calls a handler. VERIFIED the full deliver chain. - BUG: B receives entityID=3:19, NOT 3:22 -- the localID dropped by exactly the hostID (3) between A's send and B's receive. So GetEntityPointer(3:19) returns the WRONG entity (classID 48, not the mech 0xBB9), whose base handler ignores the unaimed hit -> 0 damage. Auto-replicated UPDATE records (msgID 18) arrive with the correct 3:22 and find the mech, so the corruption is specific to the dispatched- message wire path/direction. Next: the host-relative EntityID (de)serialization on the dispatched-msg path (RoutePacket / packet EntityID encoding) -- diff vs the update path which translates correctly. Diagnostics retained (all BT_MP_NET-gated, off in solo -- verified: solo 22 hits, 0 probe noise). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
a9c3e96f20 |
MP combat step 1 (target any peer) DONE; step 2 (cross-pod damage) localised (task #46)
STEP 1 -- target any peer mech: DONE + verified. A live-mech registry (BTRegisterMech/BTDeregisterMech from the Mech ctor/dtor) collects every mech -- player, dummy, and peer replicants. The boresight world-pick walks BTGetTargetCandidates (all mechs != shooter, closest ray hit) and the whole fire/damage block retargets from the solo gEnemyMech to the picked hotTarget; missile/projectile validation generalised via BTIsRegisteredMech. Verified one-box: instance A enumerates B's mech as a live ReplicantInstance (20 zones, ownerID=3). Solo un-regressed (pick->damage->kill clean, 28 hits + DESTROYED). STEP 2 -- cross-pod damage: dispatch + engine reroute are CORRECT and invoked with a valid owner, but the dispatched message doesn't reach the master. Entity::Dispatch (ENTITY.cpp:244) reroutes a replicant's msg via application->SendMessage(ownerID,...); BT_MP_FORCE_DMG proves A dispatches at B's replicant with ownerID=3, yet B takes 0 STEP-6 damage. Corrects the KB's [T3] "Dispatch already reroutes": the reroute FIRES; the break is downstream in the transport/delivery of a dispatched entity-message (update records flow fine -- net-rx works). Next MP task = that wire delivery. Diagnostics BT_MP_LOG / BT_MP_FORCE_DMG retained. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
48191d6fdf |
MP-front scout (task #45): P6 survives the combat rework; LAN-fight gap map
One-box smoke test re-run on the current build: console egg -> mesh -> RunningMission both instances, 174+ x144-byte update records each side, 2 mech trees per instance, 0 crashes (~6 min). The movement-replication milestone is intact after all the targeting/weapon/death changes. Gap map to a first playable LAN fight recorded in context/multiplayer.md (wiring order): (1) generalize the world-pick from gEnemyMech to all peer mechs; (2) exercise the Dispatch reroute for cross-pod TakeDamage; (3) victim state visuals on the shooter's screen (zone replication or a death event); (4) cross-pod beam visuals via the AUTHENTIC beam-keepalive messages (FUN_0041c350, templates @0x511e6c/78 -- already stubbed in emitter.cpp); (5) 2-window driving + DEATHS scoring; respawn deferred to the P5 teardown debt. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
f914fc040a |
context-system: complete migration -> CLAUDE.md is now a 160-line router (zero context lost)
Full migration of the 2236-line monolithic CLAUDE.md into the progressive-context knowledge graph (per spark-lesson / expert-seed.md), so the deep RE knowledge loads on-demand instead of every session. ZERO CONTEXT LOST: - docs/PROGRESS_LOG.md = the complete old CLAUDE.md, VERBATIM (byte-identical) -- the lossless safety net + the "full detail" quick-lookup fallback. - 18 context/*.md topic files (1343 lines) digest every section (§1-3 -> project-overview, §4 -> content-archives, §5 -> asset-formats/bgf-format, §5a -> source-completeness, §5b/§8 -> wintesla-port, §7/§10 -> locomotion, §10a -> build-and-run, §10b -> reconstruction-method, §10c -> combat-damage + reconstruction-gotchas, §10d -> subsystems, render notes -> rendering, gauges -> gauges-hud, MP -> multiplayer, §9 -> open-questions). - reference/glossary.yaml (53 terms). decomp-reference.md = the offsets/ClassIDs/addresses hub. CLAUDE.md (160 lines) = router: identity, answer/reason protocols, quick-lookup table, evidence tiers (T0 engine-truth / T1 decompiled+verified / T2 reconstructed+runtime / T3 guarded / T4 hypothesis), conventions + DO-NOT (the systemic bug classes), structure. Retains the load-bearing work directives (build recipe pointer, "keep current" mandate). Knowledge graph validates CLEAN (scratchpad/checkctx.py -- all [[links]] + quick-lookup + docs refs resolve; [[name]] -> topic file or glossary term). docs/*.md ledgers stay as the detailed logs; context/*.md are the curated digests that route into them. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |