MP live-play wave: collision economy, missiles, radar transform, panel polarity, comm ticker
The interactive 2-node playtest wave -- every fix decomp-grounded and live-verified: COLLISION ECONOMY (the ram one-shot): StaticBounce mutates worldLinearVelocity per contact and ProcessCollisionList walks EVERY touched solid per frame; with 2007 terrain-as-solids the reflections compounded x4-x40 within one frame and a walking bump one-shot a pristine mech for 112,375 pts (62-pt authentic economy). Fix: frameEntryWorldVelocity restore per contact (damage always priced at the real approach speed -- all the binary's physics ever saw); Mech::Reset zeroes the mover motion (respawn = teleport); [collide-tx]/[mp-hdlr] telemetry. Gotcha #16 (engine-facility drift class). MISSILES: peer-visible salvos (the launcher record extension carries a salvo counter + aim point; ForceUpdate actually enqueues it -- the dirty flag alone never serialized), the authentic arc (authored MuzzleVelocity vector + the Seeker's 200m/0.1/300 loft + gain-4 steering, decoded from @004beae4/@004bef78), world-impact bursts (rounds detonate on cave geometry instead of phasing through), contact-only damage (flight-cap expiry = fizzle, no more teleport damage), live re-lead, and ballistic (unguided) shells for autocannons. projweap's stale BTPushProjectile extern (the /FORCE signature trap, gotcha #6 corollary) crashed the Avatar's first AFC100 shot -- fixed + sweep rule recorded. RADAR: two transcription bugs made the scope permanently empty -- FUN_0040b244 is the affine INVERSE (not a copy) and FUN_0040adec writes ONLY the 3x3 rotation (never the translation); worldToView now Invert(view) built rotation-first. CulturalIcons sorted out of the moving grid (the phantom red pips were map props), visible-radius culls on all three draw passes, live pip verified at |delta| x ppm px. Gotcha #17 (verify the FUN_ body, not its call shape). WEAPON PANELS (the frozen-dial hunt): the binary's *(subsystem+0x40) means FAILED -- the recon's 'operating' name was backwards, inverting the destroyed-X lamps, the panel look, the children enable and the ready-lamp gate (which had NEVER executed). Polarity chain corrected end-to-end (failedState, fed by real damage saturation). Root cause of the freezes: MFD page-mode gating -- the dev composite shows ALL pages at once, so off-page dials legitimately stopped; under BT_DEV_GAUGES the 15 page-plane bits stay active (the exclusive secondary trio untouched). The SEH gauge guard now names its kills; repaint-heal resets the incremental arc after panel repaints; [panel]/[arc] probes added. COMM/SCORE: MessageBoard LIVE (the engine already shipped the whole Player__StatusMessage queue; wired the binary's one producer -- the kill branch, victim's name, 6s -- plus the consumer bridge and a lazy source bind); MP DEATHS counted via the observed-death tally (each node scores every pilot from locally observed events, the same model as the KILLS credit) and the -2/-1 engine seed clamped for display. DEV UX: node-tagged window titles (-net port), gauge panel reworked (1320x480, true 4:3 MFD cells, the portrait secondary UNROTATED upright, linear filtering, BT_GAUGE_SCALE), fixed close spawns via BT_SPAWN_XZ, Boreas flies an Avatar (first second-chassis live outing). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
dd27238ceb
commit
bb795e2805
+20
-4
@@ -1,8 +1,8 @@
|
||||
[mission]
|
||||
adventure=BattleTech
|
||||
map=grass
|
||||
map=cavern
|
||||
scenario=freeforall
|
||||
time=day
|
||||
time=night
|
||||
weather=clear
|
||||
temperature=27
|
||||
length=600
|
||||
@@ -156,8 +156,9 @@ x=128
|
||||
y=32
|
||||
width=8
|
||||
[pilots]
|
||||
pilot=200.0.0.96
|
||||
[200.0.0.96]
|
||||
pilot=127.0.0.1:1502
|
||||
pilot=127.0.0.1:1602
|
||||
[127.0.0.1:1502]
|
||||
hostType=0
|
||||
advancedDamage=1
|
||||
loadzones=1
|
||||
@@ -171,6 +172,20 @@ dropzone=one
|
||||
vehicle=bhk1
|
||||
vehicleValue=1000
|
||||
color=White
|
||||
[127.0.0.1:1602]
|
||||
hostType=0
|
||||
advancedDamage=1
|
||||
loadzones=1
|
||||
name=Boreas
|
||||
bitmapindex=2
|
||||
experience=expert
|
||||
badge=VGL
|
||||
patch=Yellow
|
||||
role=Role::Default
|
||||
dropzone=one
|
||||
vehicle=ava1
|
||||
vehicleValue=1000
|
||||
color=Red
|
||||
[largebitmap]
|
||||
bitmap=BitMap::Large::Aeolus
|
||||
[BitMap::Large::Aeolus]
|
||||
@@ -227,3 +242,4 @@ width=4
|
||||
model=dfltrole
|
||||
[Role::NoReturn]
|
||||
model=noretun
|
||||
|
||||
|
||||
+1
-1
@@ -183,7 +183,7 @@ badge=VGL
|
||||
patch=Yellow
|
||||
role=Role::Default
|
||||
dropzone=one
|
||||
vehicle=bhk1
|
||||
vehicle=ava1
|
||||
vehicleValue=1000
|
||||
color=Red
|
||||
[largebitmap]
|
||||
|
||||
@@ -233,6 +233,24 @@ Two non-field fixes were needed: **message-handler chaining** (an unchained set
|
||||
silently) + **entity validity** (`SetValidFlag()` on a manually-spawned entity) — see
|
||||
[[reconstruction-gotchas]] §9. [T2]
|
||||
|
||||
## Ram (mech-vs-mech collision) damage — the economy (2026-07-12)
|
||||
The binary's `Mech::ProcessCollision` (part_012.c:15280-15413) computes contact damage with the
|
||||
engine `Mover::StaticBounce` [T0 MOVER.cpp]: **damage = 0.0005 × (1−e²) × impact² × moverMass**
|
||||
(KE loss; `impact` = own `worldLinearVelocity·normal`; mass/elasticity/friction stream from the
|
||||
model gamedata `MoverMass` etc.). On a Mover owner it gates on separating-contact (`normal·rel <
|
||||
−1e-4 → return`), then on `ClassID==0xbb9` dispatches a `DamageMessage{id=0x64, zone=−1,
|
||||
CollisionDamageType}` at the OTHER mech (the receiver's cylinder table resolves the zone). Ported
|
||||
in `BTDispatchCollisionDamage` (mech4.cpp) [T1]. **Observed live economy [T2]:** weapons 3.4-11.8
|
||||
per hit; a walking/running ram = 13-62 per contact frame; a sustained ram killed a pristine mech
|
||||
at 102.5 total (4 contacts) via (presumably) a vital torso zone — per-zone HP = the authored
|
||||
`CollisionDamagePoints` (fallback `WeaponDamagePoints`), scale = 1/points, replayed from the
|
||||
type-0x14 stream. Whether ~100-pt ram lethality matches the pod is unverified (the authored
|
||||
collision points for the torso zones haven't been dumped) [T3].
|
||||
**⚠ The one-shot bug (fixed 2026-07-12):** multi-solid frames amplified the velocity StaticBounce
|
||||
priced damage from (62 → 112,375; killed a pristine mech through a single bump) — gotcha #16 in
|
||||
[[reconstruction-gotchas]]; fix = `frameEntryWorldVelocity` restore per contact + `Mech::Reset`
|
||||
motion zeroing. `[collide-tx]` (BT_MP_NET) logs every dispatched ram with the pricing velocity.
|
||||
|
||||
## Death — the wreck STAYS (P5 CLOSED)
|
||||
A killed mech does NOT disappear — BT death is a STATE transition
|
||||
(`SetGraphicState(DestroyedGraphicState)` + death anim + effect/splash), the mech becomes a persistent
|
||||
|
||||
+33
-1
@@ -6,8 +6,10 @@ source_sections: "docs/GAUGE_COMPOSITE.md (full map); CLAUDE.md §8"
|
||||
related_topics: [reconstruction-gotchas, decomp-reference, subsystems, pod-hardware]
|
||||
key_terms: [gauge, methodDescription, attribute-pointer, GaugeRenderer, MFD, PCC]
|
||||
open_questions:
|
||||
- "Status-message queue (StatusMessagePool) is a NULL stub -> MessageBoard is empty"
|
||||
- "MessageBoard LIVE 2026-07-12 (kill ticker); remaining: only strip 0 (kill) is produced -- survey the other btsmsgs.pcx strips for authentic producers"
|
||||
- "HUD binary attr table @005110c0 offsets conflict with the port HUD layout on 3 slots (0x1D8/0x1EC/0x1F8) -- re-dump before publishing"
|
||||
- "Secondary-view cycling (Damage/Critical/Heat) unreachable from the desktop keyboard: the mapper's 0x13d/0x13e cases are dead DOS F3/F4 codes (VK_F3/F4 = 0x72/0x73 collide with 'r'/'s') -- wire a free key to CycleControlMode"
|
||||
- "MP DEATHS resolved 2026-07-12 (observed-death tally + display clamp); remaining: verify multi-death tallies stay in sync across a long session (GAUGE_COMPOSITE.md)"
|
||||
---
|
||||
|
||||
# Cockpit Gauges / MFD HUD
|
||||
@@ -68,6 +70,36 @@ over one shared `SVGA16` pixelBuffer; `SVGA16::DrawDevSurface`). On the POD they
|
||||
intact). The `overlay` port (SectorDisplay lives there) shares the `sec` physical surface via a
|
||||
different bit-plane (0x00C0). [T2]
|
||||
|
||||
## The secondary screen's THREE views (Damage / Critical / Heat) — mode-gated [T0/T1]
|
||||
The `sec` port stacks three mode-gated mech-schematic layers at offset (50,0) over the
|
||||
always-on radar/heading/speed/messageBoard (`Secondary1`): **Damage** (`ModeSecondaryDamage`,
|
||||
`<mech>dama.pcc` + `colorMapArmor`/`colorMapperMultiArmor` — 4 silhouettes front/left/right/back,
|
||||
pixel-plane ids 60-63, per-DAMAGE-ZONE `dz_*` tint through the adpal→adpal2 ramp), **Critical**
|
||||
(`ModeSecondaryCritical`, `<mech>crit.pcc` + the cmCrit per-SUBSYSTEM list), **Heat**
|
||||
(`ModeSecondaryHeat` + cmHeat). Mode bits (BTL4MODE.HPP, `nextModeBit`=0 [T0]): Mapping=0x8000,
|
||||
NonMapping=0x10000, Intercom=0x20000, **SecondaryDamage=0x40000, SecondaryCritical=0x80000,
|
||||
SecondaryHeat=0x100000** (bits 18-20). `ModeInitial` includes **SecondaryDamage** → the ARMOR
|
||||
view is the default-on layer (our port creates `BTL4ModeManager(ModeInitial)`, btl4app.cpp:303).
|
||||
`L4MechControlsMapper::SetControlMode` @004d1ae4 is the SELECTOR — its mask table
|
||||
{0x40000,0x80000,0x100000} clears bits 18-20 and sets one — despite the "control mode" name it
|
||||
switches the secondary VIEW. ⚠ Desktop gap: the Keypress cases `0x13d`/`0x13e`
|
||||
(CycleControlMode/CycleDisplayMode) are the DOS Tesla extended F3/F4 codes and never fire under
|
||||
the WinTesla VK map (VK_F3=0x72 collides with 'r', VK_F4=0x73 with 's' — the same collision
|
||||
class as the documented 'p'/VK_F1 drop), so the desktop is PINNED on the Damage view. The
|
||||
schematic shows the pilot's OWN mech only — there is no target-damage readout in the cockpit.
|
||||
|
||||
## pilotList (Comm KILLS/DEATHS) row semantics + the −1 [T1/T2]
|
||||
One ROW PER PILOT in the mission (2-player MP = 2 rows — not duplicate displays). KILLS =
|
||||
`killCount` (the victim's ScoreMessageHandler credits the shooter cross-player, works for both
|
||||
rows); DEATHS = `Player::deathCount`: engine-inits to **−2** (PLAYER.cpp:759), the LOCAL
|
||||
vehicle-acquire branch zeroes it (btplayer.cpp:1118), then VehicleDead(-1) ++s per death. A
|
||||
REMOTE player's Player object never runs the local acquire → −2 +1 spawn increment = **−1
|
||||
locked**. **RESOLVED 2026-07-12 [T2]:** deaths now tally per node from LOCALLY OBSERVED events
|
||||
(the same model as the cross-pod KILLS credit) — a replicant's once-per-death transition calls
|
||||
`BTPlayerCountObservedDeath` on its owning player's local copy (replicant-gated: the master's
|
||||
own VehicleDead path counts its node), and `BTPilotDeaths` clamps the −2/−1 pre-acquire seed
|
||||
to 0 for display. Own row counts correctly as before.
|
||||
|
||||
## ConfigMapGauge (the weapon panel's regroup lamp column) — authentically DORMANT
|
||||
The per-weapon btjoy.pcc joystick image + 4 cm_* state lamps (off/other/only/both) showing,
|
||||
for each mappable fire button (Pinky/ThumbLow/Trigger/ThumbHigh), whether THIS panel's weapon
|
||||
|
||||
@@ -92,10 +92,17 @@ authentic path scoped.
|
||||
chain** ([0x31c]/[0x320] dpl objects + the PNAME1-8.bgf player-name meshes over the locked
|
||||
target — matters for MP). (c) the compass rotation SIGN is derived-not-verified [T3] (turn
|
||||
left → the stem should swing right; flip the sin sign in Draw if footage disagrees).
|
||||
- **StatusMessagePool (NULL stub, btstubs.cpp:62)** — the per-player status-message queue.
|
||||
BTPlayer+0x1dc is never populated → MessageBoard is empty (authentic for bring-up). What
|
||||
resolves it: wire the pool + decode `AddStatusMessage @0042e580` + the `Player__StatusMessage`
|
||||
record ({messageId@+0xc, nameEntity@+0x10}).
|
||||
- **✅ StatusMessagePool / MessageBoard — LIVE (2026-07-12) [T1/T2].** Decoded: FUN_0042e580 =
|
||||
the engine `Player::AddStatusMessage` (chain add at +0x1e4); FUN_0042e5a0 = `StatusMessageUpdate`
|
||||
(+0x1dc = the displayed message; +0x14 displayTime countdown, expiry-deleted) — ALL already in
|
||||
the engine [T0 PLAYER.cpp]. The ONE binary producer = BTPlayer::ScoreMessageHandler's KILL
|
||||
branch (@004c02e4 type-2): pool-alloc @00512f6c + ctor(victim's player, strip 0, 6.0s) +
|
||||
AddStatusMessage. Port: engine `new Player__StatusMessage` + Register_Object (the engine
|
||||
reclaims with delete — the binary pool is an allocation detail); consumer =
|
||||
BTResolveMessageBoard reads the mission player's statusMessagePointer + resolves the name via
|
||||
Mission::GetSmallNameBitmap; the board's SetSource binder (no recovered caller) is replaced by
|
||||
a lazy viewpoint-entity bind in Execute. Ticker pops on BOTH nodes (each node's death
|
||||
transition posts its local KillScore).
|
||||
- **Gyroscope (0xBC4) — RE-ENABLED LIVE (task #56, 2026-07-10).** The NaN revert's root causes
|
||||
are fixed byte-exact (ctor field map @004b3778 — springConstant@0x1E8/dampingConstant@0x1F4
|
||||
were mislabelled, the 0x254-0x2B3 block was missing, clamps/accumulators uninitialised; the
|
||||
@@ -292,6 +299,14 @@ authentic path scoped.
|
||||
under reconstruction: config-mode regrouping (task #6), generator reassignment (ids 4-8),
|
||||
coolant valves.
|
||||
|
||||
- **Subsystem-panel online/offline gate — fields unidentified [T4] (2026-07-12).** The binary's
|
||||
SubsystemCluster draw-state reads `*(subsystem+0x40)==1` and `*(subsystem+0x278)!=4`; neither
|
||||
1995 field is identified (weapon simulationState is 0/2/3/4 -- +0x40 isn't it). The port pins
|
||||
the panel ONLINE (GetDrawState()==1) because the old raw reads were layout NOISE that flapped
|
||||
the state and black-filled the panel over the recharge arc (the frozen-PPC-dial bug; the
|
||||
WeaponCluster repaint-heal also landed). Identify both fields to restore the authentic
|
||||
destroyed-subsystem dark-panel look.
|
||||
|
||||
## Locomotion / combat polish (non-gating)
|
||||
- Authentic per-mech TURN-RATE constant (currently a bring-up constant rate).
|
||||
- Body-callback gimp handlers (states 16-19, targets 0x70b2/0x7161 undecoded → fall back to stand);
|
||||
|
||||
@@ -113,6 +113,14 @@ near the image base, grep the link log for "unresolved external" — the "succes
|
||||
lying.** Corollary: a bridge fn / a `.data` fn-ptr callback MUST have a real (stub) definition.
|
||||
A `SetVideoPathPriority` defined in an anonymous namespace → internal linkage → unresolved in
|
||||
another TU → stubbed by /FORCE → AV in `LoadMissionImplementation`. [T2]
|
||||
**Signature-change corollary (user-hit crash 2026-07-12):** changing a shared free-function
|
||||
bridge's SIGNATURE changes its mangled name — every OTHER TU's local `extern` decl now
|
||||
references a symbol that no longer exists, /FORCE tolerates it, and the crash lands on the
|
||||
first call from the un-updated TU (the missile-arc wave updated `BTPushProjectile` +
|
||||
mislanch.cpp's extern but not projweap.cpp's → first AUTOCANNON shot AV'd). **Rule: after any
|
||||
bridge-signature change, `grep -rn "extern .*<name>"` and update every declaration; then grep
|
||||
the fresh link output for the symbol name** — the pre-existing LNK2019 wall camouflages new
|
||||
entries if you only eyeball it.
|
||||
|
||||
## 7. Dtor-epilogue rule — do not reconstruct compiler glue
|
||||
|
||||
@@ -293,6 +301,50 @@ corrupts the metric.
|
||||
canopies are open lattices. Namespace edge keys by patch identity (and remember l/r patches are
|
||||
MIRRORED — winding handedness flips, so no global winding choice can be right; orient per-face).
|
||||
|
||||
## 16. Engine-facility drift: 2007 terrain-solids amplify 1995 per-contact physics (the MP ram one-shot)
|
||||
|
||||
`Mover::StaticBounce` [T0] MUTATES `worldLinearVelocity` (`+= delta_v`, a ×(1+e) reflection) on
|
||||
every call, and `ProcessCollisionList` calls it once PER CONTACTED SOLID in the frame. In the 1995
|
||||
binary the ground was a heightfield probe (FUN_0040e5f0 lineage) — never a collision-list entry —
|
||||
so a mech's list held ~one solid and the mutation was harmless. The 2007 WinTesla engine models
|
||||
TERRAIN AS COLLISION SOLIDS: a mech touching ground + rock + another mech reflects 2-4× in ONE
|
||||
frame, compounding velocity ×4-×40, and the mech-vs-mech damage dispatch later in the list priced
|
||||
ram damage off the amplified value — a 62-point bump economy produced 1,074- and 112,375-point
|
||||
one-shots (mp_a.log:32651, 2026-07-12: a pristine mech killed by a walking bump). [T2]
|
||||
|
||||
- **Fix pattern:** snapshot the TRUE frame-entry motion (`frameEntryWorldVelocity`, set beside the
|
||||
`ProcessCollisionList` call sites) and restore it at the top of every `Mech::ProcessCollision` —
|
||||
each contact prices damage at the mech's real approach speed, which is all the binary's
|
||||
StaticBounce ever saw. The post-list velocity is discarded anyway (frame-rejection response /
|
||||
next frame's position-delta derive). Also: `Mech::Reset` must zero `worldLinearVelocity` +
|
||||
`localVelocity` (respawn is a TELEPORT; stale death-frame motion must not survive it).
|
||||
- **Tell:** damage amounts orders of magnitude outside the weapon economy (weapons 3-12/hit, rams
|
||||
~13-62), CONSTANT repeated values (a stable grind oscillation), or spikes scaling with how many
|
||||
solids surround the contact. Damage = `0.0005 × (1−e²) × impact² × moverMass` [T0 MOVER.cpp] —
|
||||
invert it to read the implied impact speed; >100 m/s means amplified/garbage velocity, not motion.
|
||||
- **Class rule:** when a 1995 per-event computation reads MUTABLE engine state, audit what ELSE the
|
||||
2007 engine feeds that state within the same event batch. (Family of gotcha #12's frame-pacing
|
||||
trap: the binary's physics assumed its own engine's event granularity.)
|
||||
|
||||
## 17. Engine-helper identity: verify the FUN_ body, not its call shape (the empty-radar bug)
|
||||
|
||||
Two adjacent matrix helpers in the radar's DrawDisplay were transcribed by CALL SHAPE and both
|
||||
were wrong — producing a plausible-looking but broken world→view transform that drew every pip
|
||||
hundreds of pixels off-scope (the radar looked simply "empty"; nothing crashed, nothing warned):
|
||||
- `FUN_0040b244(dst, src)` read as a COPY — it is the full affine INVERSE (cofactor expansion +
|
||||
determinant divide, part_001.c:172). → `worldToView.Invert(view)`.
|
||||
- `FUN_0040adec(matrix, quat)` read as a COMPOSE (`view *= yaw`) — it writes ONLY the 3×3
|
||||
rotation elements and NEVER touches [3]/[7]/[11] (the translation row). The engine's
|
||||
`operator*=(Quaternion)` composes fully (rotates the translation too) — the pre-set center got
|
||||
corrupted BEFORE the invert. → rotation-only assignment first, `SetFromAxis(W_Axis, center)`
|
||||
LAST. [T1 both, verified live: blip at exactly |delta|·ppm px after the fix]
|
||||
- **Tell:** a transform chain whose output is self-inconsistent — check whether the matrix maps
|
||||
its own reference point where it must (here: the viewer's position → the scope origin; it
|
||||
mapped to (−54, −599)). One logged matrix dump falsifies the whole chain in one frame.
|
||||
- **Rule:** for ANY engine-helper FUN_ in a reconstruction, read its BODY once (a 30-line
|
||||
decompile) before assigning it an engine method — a wrong-but-plausible identity survives
|
||||
every compile and every "it runs" test.
|
||||
|
||||
---
|
||||
|
||||
## Diagnostic recipe (the standard loop)
|
||||
|
||||
@@ -613,7 +613,13 @@ is a cluster-child (not config-called) — its 4 Seek* attributes are the only r
|
||||
VehicleDead(deathCount=-2) timer, and our handler ++deathCount + **re-posted it unconditionally** → an
|
||||
infinite loop (the drop-zone handshake never completes in bring-up). Fix: if the player already has a
|
||||
vehicle (drop zone acquired), the retry is moot → return without counting/re-posting → DEATHS=0, loop gone.
|
||||
MP real-death DEATHS (destroyed vehicle via @004c05c4) remains deferred.
|
||||
MP real-death DEATHS: RESOLVED 2026-07-12 (the observed-death tally). Each node scores every
|
||||
pilot from LOCALLY OBSERVED events (the same model as the cross-pod KILLS credit): a REPLICANT
|
||||
mech's once-per-death transition tallies onto its owning player's local copy
|
||||
(BTPlayerCountObservedDeath, btplayer.cpp; call site mech4.cpp death transition, replicant-gated
|
||||
against double-counting the master's own VehicleDead path). The display bridge BTPilotDeaths
|
||||
clamps the engine's -2/-1 pre-acquire seed to 0 (a remote player's copy never runs the local
|
||||
acquire that zeroes it -- the raw negative was the user-visible "DEATHS -1").
|
||||
- **duckState** writer (button-4 crouch lamp; only ctor-zeroed today).
|
||||
|
||||
### Phase 4 — DEEP/DEFERRED
|
||||
|
||||
@@ -579,8 +579,18 @@ Logical
|
||||
if (s_devGaugesUpd)
|
||||
{
|
||||
if (!GuardedExecute())
|
||||
{
|
||||
// LOUD KILL (2026-07-12): the silent version made a faulted
|
||||
// gauge simply FREEZE on screen (user-hit: a weapon panel's
|
||||
// dial + lamp died mid-session with no trace). Name the
|
||||
// casualty so the log convicts the faulting Execute.
|
||||
DEBUG_STREAM << "[gauge-fault] '"
|
||||
<< (identificationString ? identificationString : "?")
|
||||
<< "' Execute FAULTED -> gauge DISABLED (this="
|
||||
<< (void *)this << ")\n" << std::flush;
|
||||
Disable(True);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Execute();
|
||||
|
||||
@@ -3628,15 +3628,24 @@ void
|
||||
// Radar contacts are the DYNAMIC vehicles (mechs), not the props/terrain/
|
||||
// projectiles that AllEntityIterator would flood the grid with -- iterate the
|
||||
// dynamic masters (the local + AI/dummy vehicles) plus the dynamic replicants
|
||||
// (peer vehicles in multiplayer). Static beacons/props (a separate
|
||||
// classification) are left for a follow-up.
|
||||
// (peer vehicles in multiplayer).
|
||||
// CLASSIFICATION (the phantom-red-pip fix, 2026-07-12): the dynamic
|
||||
// iterators ALSO surface non-Mover world entities (the map's CulturalIcon
|
||||
// props, class 0x5E, register dynamic on this port) -- unsorted, they drew
|
||||
// through the radar's MOVING loop as RED contacts ("multiple pips with one
|
||||
// player"). Sort by Mover derivation: vehicles into the moving grid (red
|
||||
// pips), everything else into the static grid (the dim silhouette layer
|
||||
// DrawStatic draws) -- the follow-up the old note deferred.
|
||||
{
|
||||
HostManager::DynamicMasterEntityIterator master_iterator(host_manager);
|
||||
master_iterator.First();
|
||||
Entity *entity;
|
||||
while ((entity = master_iterator.ReadAndNext()) != NULL)
|
||||
{
|
||||
if (entity->IsDerivedFrom(*Mover::GetClassDerivations()))
|
||||
movingEntities.Add(entity);
|
||||
else
|
||||
staticEntities.Add(entity);
|
||||
}
|
||||
}
|
||||
{
|
||||
@@ -3645,7 +3654,10 @@ void
|
||||
Entity *entity;
|
||||
while ((entity = replicant_iterator.ReadAndNext()) != NULL)
|
||||
{
|
||||
if (entity->IsDerivedFrom(*Mover::GetClassDerivations()))
|
||||
movingEntities.Add(entity);
|
||||
else
|
||||
staticEntities.Add(entity);
|
||||
}
|
||||
}
|
||||
Check_Fpu();
|
||||
@@ -3665,6 +3677,22 @@ void
|
||||
Check(application->GetModeManager());
|
||||
ModeMask
|
||||
current_mode_mask = application->GetModeManager()->GetModeMask();
|
||||
// DEV-COMPOSITE (2026-07-12, the frozen-dial fix): page-gated gauges (mode
|
||||
// = their MFD page bit) freeze when their page isn't mode-active -- on the
|
||||
// pod you FLIP pages, but the dev window shows every page surface at once,
|
||||
// so six of seven weapon dials sat frozen on screen (user-hit; the arcs
|
||||
// executed exactly 8 times at startup then never again). Under
|
||||
// BT_DEV_GAUGES force the 15 MFD page-plane bits (BTL4 mode bits 0..14 =
|
||||
// MFD1/2/3 Quad+Eng1-4) active so every visible page runs. The secondary
|
||||
// trio (bits 18-20) stays authentic -- those views SHARE pixels and are
|
||||
// exclusive by design.
|
||||
{
|
||||
static int s_devAllPages = -1;
|
||||
if (s_devAllPages < 0)
|
||||
s_devAllPages = (getenv("BT_DEV_GAUGES") != NULL) ? 1 : 0;
|
||||
if (s_devAllPages)
|
||||
current_mode_mask |= (ModeMask)0x7FFF;
|
||||
}
|
||||
ModeMask
|
||||
previous_mode_mask = application->GetModeManager()->GetPreviousModeMask();
|
||||
ModeMask
|
||||
|
||||
@@ -912,6 +912,19 @@ void
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (getenv("BT_MAP_LOG"))
|
||||
{
|
||||
static int s_gi = 0;
|
||||
if ((s_gi++ % 30) == 0)
|
||||
{
|
||||
DEBUG_STREAM << "[gauima] lodVal=" << LOD_value
|
||||
<< " lodCount=" << LODCount << " chosen=" << lod_index
|
||||
<< " scales={";
|
||||
for (int gi = 0; gi < LODCount && gi < 4; ++gi)
|
||||
DEBUG_STREAM << LODScales[gi] << (gi + 1 < LODCount ? "," : "");
|
||||
DEBUG_STREAM << "} verts=" << vertexCount << "\n" << std::flush;
|
||||
}
|
||||
}
|
||||
//-------------------------------------------------------------
|
||||
// Continue if the index is within range
|
||||
//-------------------------------------------------------------
|
||||
|
||||
+123
-19
@@ -127,7 +127,8 @@ static bool DevGaugeDocked()
|
||||
// render-state save/restore is needed (the next frame resets device state).
|
||||
//===========================================================================//
|
||||
void SVGA16::DrawDevSurface(LPDIRECT3DDEVICE9 device, int slot, int mask, int paletteID,
|
||||
int monoTint, float dstX, float dstY, float dstW, float dstH)
|
||||
int monoTint, float dstX, float dstY, float dstW, float dstH,
|
||||
int rotateCCW)
|
||||
{
|
||||
int w = pixelBuffer.Data.Size.x; // 640
|
||||
int h = pixelBuffer.Data.Size.y; // 480
|
||||
@@ -181,6 +182,10 @@ void SVGA16::DrawDevSurface(LPDIRECT3DDEVICE9 device, int slot, int mask, int pa
|
||||
}
|
||||
|
||||
// Draw the surface quad at (dstX,dstY,dstW,dstH) in the current render target.
|
||||
// rotateCCW: the pod's SECONDARY screen was a physically ROTATED portrait
|
||||
// CRT -- the game authors its content sideways in the landscape buffer and
|
||||
// the cabinet monitor unrotated it. On the dev panel WE unrotate: display
|
||||
// the texture turned 90 degrees so the radar/secondary reads upright.
|
||||
struct InsetVert { float x, y, z, rhw, u, v; };
|
||||
InsetVert quad[4] =
|
||||
{
|
||||
@@ -189,8 +194,26 @@ void SVGA16::DrawDevSurface(LPDIRECT3DDEVICE9 device, int slot, int mask, int pa
|
||||
{ dstX + dstW, dstY + dstH, 0.0f, 1.0f, 1.0f, 1.0f },
|
||||
{ dstX, dstY + dstH, 0.0f, 1.0f, 0.0f, 1.0f },
|
||||
};
|
||||
if (rotateCCW == 1) // screen corner <- texture corner, turned CCW
|
||||
{
|
||||
quad[0].u = 1.0f; quad[0].v = 0.0f; // TL <- TR
|
||||
quad[1].u = 1.0f; quad[1].v = 1.0f; // TR <- BR
|
||||
quad[2].u = 0.0f; quad[2].v = 1.0f; // BR <- BL
|
||||
quad[3].u = 0.0f; quad[3].v = 0.0f; // BL <- TL
|
||||
}
|
||||
else if (rotateCCW == 3) // the other direction (CW), env-selectable
|
||||
{
|
||||
quad[0].u = 0.0f; quad[0].v = 1.0f; // TL <- BL
|
||||
quad[1].u = 0.0f; quad[1].v = 0.0f; // TR <- TL
|
||||
quad[2].u = 1.0f; quad[2].v = 0.0f; // BR <- TR
|
||||
quad[3].u = 1.0f; quad[3].v = 1.0f; // BL <- BR
|
||||
}
|
||||
device->SetTexture(0, tex);
|
||||
device->SetFVF(D3DFVF_XYZRHW | D3DFVF_TEX1);
|
||||
// LINEAR sampling: the cells scale the 8-bit surfaces (sec runs 0.75x at
|
||||
// scale 1.0); point sampling drops the radar's 1px strokes entirely.
|
||||
device->SetSamplerState(0, D3DSAMP_MINFILTER, D3DTEXF_LINEAR);
|
||||
device->SetSamplerState(0, D3DSAMP_MAGFILTER, D3DTEXF_LINEAR);
|
||||
device->SetTextureStageState(0, D3DTSS_COLOROP, D3DTOP_SELECTARG1);
|
||||
device->SetTextureStageState(0, D3DTSS_COLORARG1, D3DTA_TEXTURE);
|
||||
device->SetTextureStageState(0, D3DTSS_ALPHAOP, D3DTOP_SELECTARG1);
|
||||
@@ -218,18 +241,27 @@ struct BTGaugeSurfaceDesc
|
||||
int monoTint; // -1 = palette (sec); else R5G6B5 tint
|
||||
float cellX, cellY; // normalized cell origin within the panel (y down)
|
||||
float cellW, cellH; // normalized cell size
|
||||
int rotateCCW; // 1 = unrotate (portrait secondary CRT)
|
||||
};
|
||||
|
||||
// PANEL LAYOUT (2026-07-12 rework): the old 3x2 equal grid DOWNSCALED every
|
||||
// 640x480 surface into a 320x192 cell (blurry) and showed the SECONDARY
|
||||
// (radar) sideways -- on the pod that screen was a physically ROTATED
|
||||
// portrait CRT. New layout, logical 1320x480: a 2x2 block of mono MFDs on
|
||||
// the left at 320x240 (correct 4:3), the secondary UNROTATED in a portrait
|
||||
// 360x480 cell in the middle, Comm top-right. Scale the whole window with
|
||||
// BT_GAUGE_SCALE (e.g. 1.5) for more pixels -- content is a 1995 8-bit
|
||||
// buffer, so past ~1.5x you magnify, not sharpen.
|
||||
static const float kBTPanelW = 1320.0f;
|
||||
static const float kBTPanelH = 480.0f;
|
||||
static const BTGaugeSurfaceDesc kBTGaugeSurfaces[6] =
|
||||
{
|
||||
// top row: Heat (UL) | Mfd2 (UC) | Comm (UR)
|
||||
{ "Heat", 0xFFFF, 0.0f / 3, 0.0f, 1.0f / 3, 0.5f },
|
||||
{ "Mfd2", 0xFFFF, 1.0f / 3, 0.0f, 1.0f / 3, 0.5f },
|
||||
{ "Comm", 0xFFFF, 2.0f / 3, 0.0f, 1.0f / 3, 0.5f },
|
||||
// bottom row: Mfd1 (LL) | sec/radar (center) | Mfd3 (LR)
|
||||
{ "Mfd1", 0xFFFF, 0.0f / 3, 0.5f, 1.0f / 3, 0.5f },
|
||||
{ "sec", -1, 1.0f / 3, 0.5f, 1.0f / 3, 0.5f },
|
||||
{ "Mfd3", 0xFFFF, 2.0f / 3, 0.5f, 1.0f / 3, 0.5f },
|
||||
{ "Heat", 0xFFFF, 0.0f / kBTPanelW, 0.0f, 320.0f / kBTPanelW, 0.5f, 0 },
|
||||
{ "Mfd2", 0xFFFF, 320.0f / kBTPanelW, 0.0f, 320.0f / kBTPanelW, 0.5f, 0 },
|
||||
{ "Mfd1", 0xFFFF, 0.0f / kBTPanelW, 0.5f, 320.0f / kBTPanelW, 0.5f, 0 },
|
||||
{ "Mfd3", 0xFFFF, 320.0f / kBTPanelW, 0.5f, 320.0f / kBTPanelW, 0.5f, 0 },
|
||||
{ "sec", -1, 640.0f / kBTPanelW, 0.0f, 360.0f / kBTPanelW, 1.0f, 1 },
|
||||
{ "Comm", 0xFFFF, 1000.0f / kBTPanelW, 0.0f, 320.0f / kBTPanelW, 0.5f, 0 },
|
||||
};
|
||||
|
||||
//
|
||||
@@ -243,6 +275,16 @@ void BTDrawGaugeSurfaces(LPDIRECT3DDEVICE9 device, float px, float py, float pw,
|
||||
GaugeRenderer *gr = application ? application->GetGaugeRenderer() : NULL;
|
||||
if (gr == NULL) return;
|
||||
|
||||
// DEVICE-STATE ISOLATION (the "floating cavern rocks" bug): the per-surface
|
||||
// quad draw disables Z (D3DRS_ZENABLE=FALSE), culling and lighting, binds a
|
||||
// pre-transformed FVF and rewrites texture stage 0 -- and the world pass
|
||||
// does NOT re-assert these each frame, so the NEXT frame's world rendered
|
||||
// WITHOUT DEPTH TESTING (geometry resolved in submission order; rock
|
||||
// columns drew "floating / unconnected"). Snapshot the whole device state
|
||||
// and restore it after the composite -- dev-only path, cost irrelevant.
|
||||
IDirect3DStateBlock9 *stateBlock = NULL;
|
||||
device->CreateStateBlock(D3DSBT_ALL, &stateBlock);
|
||||
|
||||
// Reach the shared display once (any present port shares the same SVGA16/pixelBuffer).
|
||||
SVGA16 *svga = NULL;
|
||||
for (int i = 0; i < 6 && svga == NULL; i++)
|
||||
@@ -257,9 +299,30 @@ void BTDrawGaugeSurfaces(LPDIRECT3DDEVICE9 device, float px, float py, float pw,
|
||||
const BTGaugeSurfaceDesc &d = kBTGaugeSurfaces[i];
|
||||
L4GraphicsPort *port = static_cast<L4GraphicsPort*>(gr->GetGraphicsPort(d.portName));
|
||||
if (port == NULL) continue; // this surface's port isn't configured -> skip
|
||||
// Secondary-CRT unrotation direction is env-flippable (BT_GAUGE_SEC_ROT=3
|
||||
// for the other way) in case the assumed pod mounting is mirrored.
|
||||
int rot = d.rotateCCW;
|
||||
if (rot != 0)
|
||||
{
|
||||
static int s_secRot = -1;
|
||||
if (s_secRot < 0)
|
||||
{
|
||||
// Default 3 (CW): user-verified upright (the first guess, 1,
|
||||
// showed the secondary upside down). Env still overrides.
|
||||
const char *rv = getenv("BT_GAUGE_SEC_ROT");
|
||||
s_secRot = (rv != NULL && rv[0] == '1') ? 1 : 3;
|
||||
}
|
||||
rot = s_secRot;
|
||||
}
|
||||
svga->DrawDevSurface(
|
||||
device, i, port->GetBitMask(), port->paletteID, d.monoTint,
|
||||
px + d.cellX * pw, py + d.cellY * ph, d.cellW * pw, d.cellH * ph);
|
||||
px + d.cellX * pw, py + d.cellY * ph, d.cellW * pw, d.cellH * ph, rot);
|
||||
}
|
||||
|
||||
if (stateBlock != NULL)
|
||||
{
|
||||
stateBlock->Apply();
|
||||
stateBlock->Release();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -273,7 +336,8 @@ void BTDrawGaugeInset(LPDIRECT3DDEVICE9 device)
|
||||
{
|
||||
if (!DevGaugeComposite() || !DevGaugeDocked() || device == NULL) return;
|
||||
// DOCKED mode only: draw into the main backbuffer (this runs before EndScene).
|
||||
BTDrawGaugeSurfaces(device, 2.0f, 300.0f, 540.0f, 296.0f);
|
||||
// Keep the new panel's 1320:480 aspect (2.75) so nothing distorts.
|
||||
BTDrawGaugeSurfaces(device, 2.0f, 400.0f, 540.0f, 540.0f / 2.75f);
|
||||
}
|
||||
|
||||
//===========================================================================//
|
||||
@@ -286,13 +350,32 @@ void BTDrawGaugeInset(LPDIRECT3DDEVICE9 device)
|
||||
//===========================================================================//
|
||||
static HWND s_gaugeHwnd = NULL;
|
||||
static IDirect3DSwapChain9 *s_gaugeSwap = NULL;
|
||||
static const int s_gaugeW = 960;
|
||||
static const int s_gaugeH = 384;
|
||||
// Window = the logical 1320x480 panel (see kBTGaugeSurfaces) x BT_GAUGE_SCALE.
|
||||
// Resolved once at first ensure; default 1.0 (surfaces at/near native pixels).
|
||||
static int s_gaugeW = 1320;
|
||||
static int s_gaugeH = 480;
|
||||
static bool s_gaugeSized = false;
|
||||
|
||||
static void BTGaugeResolveSize()
|
||||
{
|
||||
if (s_gaugeSized) return;
|
||||
s_gaugeSized = true;
|
||||
const char *sv = getenv("BT_GAUGE_SCALE");
|
||||
float scale = (sv != NULL) ? (float)atof(sv) : 1.0f;
|
||||
if (scale < 0.5f || scale > 3.0f) scale = 1.0f;
|
||||
s_gaugeW = (int)(1320.0f * scale);
|
||||
s_gaugeH = (int)(480.0f * scale);
|
||||
}
|
||||
|
||||
static LRESULT CALLBACK BTGaugeWndProc(HWND h, UINT m, WPARAM w, LPARAM l)
|
||||
{
|
||||
if (m == WM_CLOSE) { ShowWindow(h, SW_HIDE); return 0; } // hide, never quit the app
|
||||
return DefWindowProc(h, m, w, l);
|
||||
// Fully-WIDE window (class + create + proc all W; the class/create below
|
||||
// are RegisterClassW/CreateWindowExW): mixed A/W pairings mangled the
|
||||
// title -- A-class + W-proc painted the ANSI bytes as UTF-16 (CJK-looking
|
||||
// mojibake), W-class + A-proc truncated at the first thunked NUL ("B").
|
||||
// One consistent character set ends the thunk roulette.
|
||||
return DefWindowProcW(h, m, w, l);
|
||||
}
|
||||
|
||||
// Lazily create the window + its additional swap chain (on the MAIN device).
|
||||
@@ -301,26 +384,47 @@ static bool BTGaugeWindowEnsure(LPDIRECT3DDEVICE9 device)
|
||||
if (s_gaugeSwap != NULL) return true;
|
||||
if (device == NULL) return false;
|
||||
|
||||
BTGaugeResolveSize(); // BT_GAUGE_SCALE -> window dims
|
||||
HINSTANCE hInst = GetModuleHandle(NULL);
|
||||
static bool s_classReg = false;
|
||||
if (!s_classReg)
|
||||
{
|
||||
WNDCLASSA wc;
|
||||
// WIDE APIs throughout: this TU compiles UNICODE, so BTGaugeWndProc's
|
||||
// DefWindowProc resolves to the W variant -- registering the class
|
||||
// with RegisterClassA made Windows reinterpret the ANSI title bytes
|
||||
// as UTF-16 (the "Korean garbage title" bug: byte-pairs of
|
||||
// "BattleTech - Cockpit MFDs" land in the CJK plane, only the
|
||||
// trailing 's'+NUL survives readable).
|
||||
WNDCLASSW wc;
|
||||
memset(&wc, 0, sizeof(wc));
|
||||
wc.lpfnWndProc = BTGaugeWndProc;
|
||||
wc.hInstance = hInst;
|
||||
wc.hCursor = LoadCursor(NULL, IDC_ARROW);
|
||||
wc.hbrBackground = (HBRUSH) GetStockObject(BLACK_BRUSH);
|
||||
wc.lpszClassName = "BTGaugeWnd";
|
||||
RegisterClassA(&wc);
|
||||
wc.lpszClassName = L"BTGaugeWnd";
|
||||
RegisterClassW(&wc);
|
||||
s_classReg = true;
|
||||
}
|
||||
if (s_gaugeHwnd == NULL)
|
||||
{
|
||||
RECT r = { 0, 0, s_gaugeW, s_gaugeH };
|
||||
AdjustWindowRect(&r, WS_OVERLAPPEDWINDOW, FALSE);
|
||||
s_gaugeHwnd = CreateWindowExA(
|
||||
0, "BTGaugeWnd", "BattleTech - Cockpit MFDs", WS_OVERLAPPEDWINDOW,
|
||||
// Window identity (MP dev): tag with the -net port so two side-by-side
|
||||
// nodes' MFD windows are distinguishable (matches the main window tag).
|
||||
wchar_t gaugeTitle[64];
|
||||
{
|
||||
int netPort = 0;
|
||||
const char *cmd = GetCommandLineA();
|
||||
const char *np = (cmd != NULL) ? strstr(cmd, "-net") : NULL;
|
||||
if (np != NULL)
|
||||
sscanf(np + 4, " %d", &netPort);
|
||||
if (netPort > 0)
|
||||
swprintf(gaugeTitle, 64, L"BattleTech MFDs \x2014 node %d", netPort);
|
||||
else
|
||||
wcscpy(gaugeTitle, L"BattleTech - Cockpit MFDs");
|
||||
}
|
||||
s_gaugeHwnd = CreateWindowExW(
|
||||
0, L"BTGaugeWnd", gaugeTitle, WS_OVERLAPPEDWINDOW,
|
||||
40, 40, r.right - r.left, r.bottom - r.top, NULL, NULL, hInst, NULL);
|
||||
if (s_gaugeHwnd == NULL) return false;
|
||||
ShowWindow(s_gaugeHwnd, SW_SHOWNOACTIVATE);
|
||||
|
||||
@@ -300,8 +300,11 @@ public:
|
||||
// dstW,dstH). monoTint < 0 -> palette-expand (sec/radar, case-0 LUT); monoTint >= 0
|
||||
// -> mono bit-plane expand ((word & mask) ? tint : 0) for an MFD plane. See
|
||||
// BTDrawGaugeSurfaces (L4VB16.cpp).
|
||||
// rotateCCW: 0 = as-is; 1/3 = display the texture turned 90 degrees (the
|
||||
// pod's portrait-mounted secondary CRT; the dev panel unrotates it).
|
||||
void DrawDevSurface(LPDIRECT3DDEVICE9 device, int slot, int mask, int paletteID,
|
||||
int monoTint, float dstX, float dstY, float dstW, float dstH);
|
||||
int monoTint, float dstX, float dstY, float dstW, float dstH,
|
||||
int rotateCCW = 0);
|
||||
};
|
||||
|
||||
//########################################################################
|
||||
|
||||
+18
-1
@@ -391,7 +391,24 @@ int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine
|
||||
}
|
||||
RECT wr = { 0, 0, winW, winH };
|
||||
AdjustWindowRect(&wr, WS_OVERLAPPEDWINDOW, FALSE);
|
||||
hWnd = CreateWindowEx(0, L"MainWndClass", L"BattleTech", WS_OVERLAPPEDWINDOW,
|
||||
|
||||
//
|
||||
// Window identity (MP dev): tag the title with the -net port so a player
|
||||
// running two nodes side-by-side can tell the windows apart when
|
||||
// reporting ("node 1501" = A / player 1, "node 1601" = B / player 2).
|
||||
//
|
||||
wchar_t winTitle[64];
|
||||
{
|
||||
int netPort = 0;
|
||||
const char *np = lpCmdLine ? strstr(lpCmdLine, "-net") : 0;
|
||||
if (np != 0)
|
||||
sscanf(np + 4, " %d", &netPort);
|
||||
if (netPort > 0)
|
||||
swprintf(winTitle, 64, L"BattleTech \x2014 node %d", netPort);
|
||||
else
|
||||
wcscpy(winTitle, L"BattleTech");
|
||||
}
|
||||
hWnd = CreateWindowEx(0, L"MainWndClass", winTitle, WS_OVERLAPPEDWINDOW,
|
||||
0, 0, wr.right - wr.left, wr.bottom - wr.top,
|
||||
(HWND)NULL, (HMENU)NULL, hInstance, (LPVOID)NULL);
|
||||
if (!hWnd) return FALSE;
|
||||
|
||||
@@ -1310,7 +1310,7 @@ SubsystemCluster::SubsystemCluster(
|
||||
*(int *)((char *)subsystem_in + 0x150) /*third*/, 3 /*frames*/,
|
||||
(Scalar *)coolantLeak, "LeakGauge"); // BEST-EFFORT raw (subsys+0x150)
|
||||
|
||||
operating = False; // @0xC4 this[0x31]
|
||||
failedState = False; // @0xC4 this[0x31] (1 = destroyed)
|
||||
subsystem = subsystem_in; // @0xC0 this[0x30]
|
||||
previousDrawState = 0; // @0xC8 this[0x32]
|
||||
}
|
||||
@@ -1362,10 +1362,12 @@ void SubsystemCluster::DrawPanelFrame(int color)
|
||||
//
|
||||
void SubsystemCluster::SetChildrenEnable(int enable)
|
||||
{
|
||||
// FUN_00444650 forwards visibility; enable==0 (offline) => Disable(True).
|
||||
if (temperatureBar) temperatureBar->Disable(enable == 0);
|
||||
if (coolingLoopA) coolingLoopA->Disable(enable == 0);
|
||||
if (powerSourceA) powerSourceA->Disable(enable == 0);
|
||||
// POLARITY (PPC-dial fix, take 3): the passed value is the DRAW STATE --
|
||||
// 0 = alive (children run), nonzero = dead panel (children off). The old
|
||||
// reading (`enable==0 => Disable`) had it backwards.
|
||||
if (temperatureBar) temperatureBar->Disable(enable != 0);
|
||||
if (coolingLoopA) coolingLoopA->Disable(enable != 0);
|
||||
if (powerSourceA) powerSourceA->Disable(enable != 0);
|
||||
}
|
||||
|
||||
//
|
||||
@@ -1374,11 +1376,13 @@ void SubsystemCluster::SetChildrenEnable(int enable)
|
||||
//
|
||||
int SubsystemCluster::GetDrawState()
|
||||
{
|
||||
if (operating != 0)
|
||||
return 1;
|
||||
if (*(int *)((char *)subsystem + 0x278) != 4) // BEST-EFFORT raw offset
|
||||
return 1;
|
||||
return 0;
|
||||
// POLARITY (PPC-dial fix, take 3): state 1 = the DEAD-panel presentation
|
||||
// (black fill + bright frame + X lamps lit); state 0 = ALIVE (frame 0 +
|
||||
// background art; the WeaponCluster lamp gate runs ONLY in state 0). The
|
||||
// binary's secondary condition (*(subsystem+0x278) != 4 -> 1) remains
|
||||
// unidentified [T4] -- a healthy subsystem reads ALIVE here until that
|
||||
// field is decoded (open-questions).
|
||||
return failedState ? 1 : 0;
|
||||
}
|
||||
|
||||
Logical SubsystemCluster::TestInstance() const
|
||||
@@ -1394,7 +1398,18 @@ Logical SubsystemCluster::TestInstance() const
|
||||
//
|
||||
void SubsystemCluster::Execute()
|
||||
{
|
||||
operating = (*(int *)((char *)subsystem + 0x40) == 1); // BEST-EFFORT raw offset
|
||||
// PPC-dial fix (2026-07-12, take 3 -- POLARITY): the binary's
|
||||
// *(subsystem+0x40)==1 means FAILED (it lights the X TwoStates, which the
|
||||
// engine draws bright on NONZERO, and selects the black-fill dead-panel
|
||||
// look). The recon's old name `operating` was backwards, which inverted
|
||||
// the X lamps, the panel look, the children enable AND the weapon lamp
|
||||
// gate (which runs only in draw-state 0 = alive). Truth source: the
|
||||
// subsystem's own damage saturation (DistributeCriticalHit destroys at
|
||||
// >= 1.0, mechdmg.cpp).
|
||||
{
|
||||
extern Scalar BTSubsystemDamageLevelOf(void *subsystem_in);
|
||||
failedState = (BTSubsystemDamageLevelOf(subsystem) >= 1.0f);
|
||||
}
|
||||
|
||||
int state = GetDrawState();
|
||||
if (state != previousDrawState)
|
||||
@@ -1450,7 +1465,7 @@ HeatSinkCluster::HeatSinkCluster(
|
||||
int engPort = renderer_in->FindGraphicsPort(eng_port_name);
|
||||
|
||||
heatFailLamp = new TwoState(ChildRate(), eng_mode, renderer_in, owner_ID, // @004739d8
|
||||
engPort, 200, 0x110, fail_lamp_image, 0, 0xff, (int *)&operating, "TwoState");
|
||||
engPort, 200, 0x110, fail_lamp_image, 0, 0xff, (int *)&failedState, "TwoState"); // bright when FAILED
|
||||
|
||||
failSubsystem = hud; // @0x33
|
||||
failFlag = 0; // @0x34
|
||||
@@ -1587,6 +1602,12 @@ WeaponCluster::WeaponCluster(
|
||||
warningState = 0; // @0x39
|
||||
AddConnection(new GaugeConnectionDirectOf<Scalar>(0, &percentDone, // @00474855
|
||||
(Scalar *)percentDoneAttr));
|
||||
|
||||
// PANEL DIAGNOSTIC (PPC-dial investigation 2026-07-12): remember the
|
||||
// weapon's name so the Execute probe can attribute its readings; tag the
|
||||
// recharge arc with the same name for its own [arc] probe.
|
||||
diagWeaponName = (subsystem_in != 0) ? subsystem_in->GetName() : "?";
|
||||
((SegmentArc270 *)rechargeArc)->diagName = diagWeaponName;
|
||||
}
|
||||
|
||||
WeaponCluster::~WeaponCluster()
|
||||
@@ -1630,7 +1651,33 @@ void WeaponCluster::BecameActive()
|
||||
//
|
||||
void WeaponCluster::Execute()
|
||||
{
|
||||
// PPC-dial fix (2026-07-12): the base Execute BLACK-FILLS + repaints the
|
||||
// panel on every draw-state flip -- wiping the recharge arc's pixels while
|
||||
// the arc's INCREMENTAL draw memory (previousSegment) still says "lit".
|
||||
// The stale arc then draws nothing until the value crosses new segments =
|
||||
// "tick marks frozen (at the backdrop's painted ring), dot still toggles"
|
||||
// (user-reported, intermittent). Detect the repaint and reset the arc's
|
||||
// draw memory so it fully redraws its true fill.
|
||||
int preDrawState = previousDrawState;
|
||||
SubsystemCluster::Execute();
|
||||
if (previousDrawState != preDrawState)
|
||||
{
|
||||
if (rechargeArc) rechargeArc->BecameActive();
|
||||
if (configMap) configMap->BecameActive();
|
||||
warningState = -2; // lamp: force redraw too
|
||||
}
|
||||
// PANEL DIAGNOSTIC (PPC-dial investigation): 1 Hz per-panel dump of the
|
||||
// exact value the recharge arc + warning lamp read. Answers whether a
|
||||
// frozen dial is a DATA fault (percentDone pegged) or a DRAW fault
|
||||
// (value cycles, segments don't).
|
||||
if (getenv("BT_PANEL_LOG"))
|
||||
{
|
||||
static int s_pl = 0;
|
||||
if ((s_pl++ % 60) == 0)
|
||||
DEBUG_STREAM << "[panel] " << (diagWeaponName ? diagWeaponName : "?")
|
||||
<< " percentDone=" << percentDone
|
||||
<< " warn=" << warningState << "\n" << std::flush;
|
||||
}
|
||||
if (previousDrawState == 0)
|
||||
{
|
||||
int warn = (WeaponWarnThreshold < percentDone) ? 1 : 0;
|
||||
@@ -1736,7 +1783,7 @@ BallisticWeaponCluster::BallisticWeaponCluster(
|
||||
"NumericDisplayScalarTwoState");
|
||||
|
||||
destroyedLamp = new TwoState(ChildRate(), eng_mode, renderer_in, owner_ID,
|
||||
engPort, 0xc2, 0x85, "edestryd.pcc", 0, 0xff, (int *)&operating, "TwoState");
|
||||
engPort, 0xc2, 0x85, "edestryd.pcc", 0, 0xff, (int *)&failedState, "TwoState"); // the destroyed X: bright when FAILED
|
||||
|
||||
// ejectWipe (BitMapInverseWipeScalar @004c61c8, eject-timer wipe reading
|
||||
// subsys+0x3f8). BRING-UP: the BitMapInverseWipeScalar class is not yet
|
||||
|
||||
@@ -390,7 +390,12 @@
|
||||
*stateLamp, // @0xB8 this[0x2E] OneOfSeveralStates
|
||||
*leakGauge; // @0xBC this[0x2F] LeakGauge
|
||||
Subsystem *subsystem; // @0xC0 this[0x30]
|
||||
Logical operating; // @0xC4 this[0x31]
|
||||
// POLARITY CORRECTED (PPC-dial fix, 2026-07-12): the binary's
|
||||
// *(subsystem+0x40)==1 means the subsystem FAILED -- it lights the
|
||||
// destroyed-X TwoStates (engine TwoState draws bright on NONZERO) and
|
||||
// selects the dead-panel look (GetDrawState 1 = black fill). The old
|
||||
// name `operating` had it backwards, inverting every consumer.
|
||||
Logical failedState; // @0xC4 this[0x31] 1 = subsystem destroyed
|
||||
int previousDrawState; // @0xC8 this[0x32] (Execute repaint key)
|
||||
|
||||
// internal helpers (non-virtual; @004c89c4 / @004c8820 / @004c8a28)
|
||||
@@ -472,6 +477,7 @@
|
||||
int warningCenterX; // @0xDC this[0x37]
|
||||
int warningCenterY; // @0xE0 this[0x38]
|
||||
int warningState; // @0xE4 this[0x39]
|
||||
const char *diagWeaponName; // PORT diagnostic (appended; clusters are plain-new)
|
||||
};
|
||||
|
||||
class EnergyWeaponCluster : // @004c93b0
|
||||
|
||||
@@ -1001,6 +1001,13 @@ void
|
||||
void
|
||||
MessageBoard::Execute()
|
||||
{
|
||||
// FEED LIVE (2026-07-12): the binary's SetSource binder has no recovered
|
||||
// caller, so trackedMech stayed NULL and the board never drew. The board
|
||||
// is the LOCAL cockpit's ticker -- bind the viewpoint mech lazily (the
|
||||
// same fallback the subsystem cluster uses, :226). The message resolve
|
||||
// itself reads the mission player's queue regardless.
|
||||
if (trackedMech == NULL && application != NULL)
|
||||
trackedMech = (Entity *)application->GetViewpointEntity();
|
||||
if (trackedMech == NULL) // binary gate :3949
|
||||
return;
|
||||
|
||||
@@ -1030,11 +1037,11 @@ void
|
||||
}
|
||||
}
|
||||
|
||||
// --- sender name cell (:3983-4009). STRUCTURAL SIMPLIFICATION (marked): the binary
|
||||
// tracks the previous name ENTITY pointer at 0x9c and erases it via +0x1e0->App+0xc8;
|
||||
// here previousNameId is a bool (0/1). Functionally identical while DEFERRED (name
|
||||
// always NULL); RESTORE the entity-pointer tracking + erase branch when the feed goes live.
|
||||
int nameState = (nameBitmap != NULL) ? 1 : 0;
|
||||
// --- sender name cell (:3983-4009). The binary tracks the previous name
|
||||
// ENTITY pointer at 0x9c; with the feed LIVE (2026-07-12) we track the
|
||||
// resolved BITMAP pointer -- same change-detection granularity (a new
|
||||
// victim = a different prebuilt raster), no entity re-deref.
|
||||
int nameState = (int)(size_t)nameBitmap;
|
||||
if (nameState != previousNameId)
|
||||
{
|
||||
localView.MoveToAbsolute(0x80, 0); // vtbl+0x24 (name cell x=128)
|
||||
|
||||
@@ -2404,8 +2404,28 @@ SegmentArc270::SegmentArc270(
|
||||
AddConnection(new GaugeConnectionDirectOf<Scalar>(0, ¤tValue, value_pointer));
|
||||
int n = (number_of_segs < 0) ? -number_of_segs : number_of_segs;
|
||||
segmentSpan = (Scalar)(n / (n - 1)) * 0.75f; // _DAT_004c62d4 = 0.75
|
||||
diagName = 0;
|
||||
}
|
||||
|
||||
SegmentArc270::~SegmentArc270() {} // @004c66b3 (base chain)
|
||||
|
||||
//
|
||||
// PORT diagnostic wrapper (frozen-dial hunt, 2026-07-12): log the exact value
|
||||
// and segment memory the arc runs with, then chain the engine draw untouched.
|
||||
//
|
||||
void
|
||||
SegmentArc270::Execute()
|
||||
{
|
||||
if (getenv("BT_PANEL_LOG") && diagName != 0)
|
||||
{
|
||||
static int s_al = 0;
|
||||
if ((s_al++ % 60) == 0)
|
||||
DEBUG_STREAM << "[arc] " << diagName
|
||||
<< " val=" << (float)currentValue
|
||||
<< " prevSeg=" << previousSegment
|
||||
<< " segs=" << numberOfSegments << "\n" << std::flush;
|
||||
}
|
||||
SegmentArc::Execute();
|
||||
}
|
||||
|
||||
// === btl4gau2.cpp begins at @004c6798 (SeekVoltage gauge) -- not part of this TU.
|
||||
|
||||
@@ -546,6 +546,8 @@
|
||||
Scalar *value_pointer, Logical use_thick_lines,
|
||||
const char *identification_string);
|
||||
~SegmentArc270(); // dtor thunk @004c66b3
|
||||
void Execute(); // PORT diag wrapper (chains SegmentArc::Execute)
|
||||
const char *diagName; // PORT diagnostic (frozen-dial hunt)
|
||||
protected:
|
||||
Scalar segmentSpan; // @0xC0 this[0x30]
|
||||
};
|
||||
|
||||
@@ -679,15 +679,23 @@ void
|
||||
if (currentPositionPointer != NULL)
|
||||
{
|
||||
Point3D center = *currentPositionPointer; // FUN_00408440(local_5c, ...)
|
||||
view.SetFromAxis(W_Axis, center); // FUN_0040b0ac(view,3,center)
|
||||
|
||||
// TRANSCRIPTION FIX #2 (the invisible-pip bug): FUN_0040adec writes
|
||||
// ONLY the 3x3 rotation elements ([0..2],[4..6],[8..10]) -- it never
|
||||
// touches the translation row. The binary therefore ends with
|
||||
// view = [R(q) | center]: rotation SET, center PRESERVED. Our old
|
||||
// `view *= yaw` was the engine's full COMPOSE, which also rotated the
|
||||
// translation (view = [R | center x R]) -- the subsequent Invert then
|
||||
// mapped center x R to the origin instead of the viewer, so every
|
||||
// blip landed hundreds of pixels off-scope. Rotation first
|
||||
// (rotation-only assignment), translation row LAST.
|
||||
if (currentAngularPointer != NULL)
|
||||
{
|
||||
if (!rockAndRoll)
|
||||
{
|
||||
//
|
||||
// Top-down: keep heading only. Extract the yaw, build a
|
||||
// yaw-only quaternion and concatenate it.
|
||||
// yaw-only quaternion, SET the rotation block from it.
|
||||
//
|
||||
EulerAngles euler;
|
||||
euler = *currentAngularPointer; // FUN_0040954c
|
||||
@@ -695,21 +703,27 @@ void
|
||||
SinCosPair sc;
|
||||
sc = Radian(heading); // FUN_00408328
|
||||
Quaternion yaw(0.0f, sc.sine, 0.0f, sc.cosine); // FUN_00409948
|
||||
view *= yaw; // FUN_0040adec
|
||||
view = yaw; // rotation-only (FUN_0040adec semantics)
|
||||
}
|
||||
else
|
||||
{
|
||||
//
|
||||
// "Rock and roll": use the full orientation matrix.
|
||||
// "Rock and roll": use the full orientation.
|
||||
//
|
||||
AffineMatrix full;
|
||||
full = *currentAngularPointer; // FUN_00409968
|
||||
view *= full; // FUN_0040adec
|
||||
view = *currentAngularPointer; // rotation-only build
|
||||
}
|
||||
}
|
||||
view.SetFromAxis(W_Axis, center); // FUN_0040b0ac(view,3,center) -- translation LAST
|
||||
}
|
||||
|
||||
worldToView = view; // FUN_0040b244(this+0x324, view)
|
||||
// TRANSCRIPTION FIX (the invisible-pip bug, 2026-07-12): FUN_0040b244 is
|
||||
// the full affine INVERSE (cofactor expansion + determinant divide,
|
||||
// part_001.c:172), not a copy. `view` above is the OPERATOR's local->
|
||||
// world pose (translate to the mech + yaw); the radar needs its inverse
|
||||
// -- world->scope, center subtracted, heading unrotated. Read as a copy,
|
||||
// every pip/name transformed to (world + center) x ppm = thousands of
|
||||
// pixels off-scope: the scope drew empty at all times.
|
||||
worldToView.Invert(view); // FUN_0040b244(this+0x324, view)
|
||||
|
||||
//
|
||||
// Bake the metres->pixels scale into the matrix.
|
||||
@@ -717,6 +731,18 @@ void
|
||||
Vector3D scale;
|
||||
scale.x = scale.y = scale.z = pixelsPerMeter;
|
||||
worldToView.Multiply(worldToView, scale); // FUN_0040b374
|
||||
if (getenv("BT_MAP_LOG"))
|
||||
{
|
||||
static int s_m = 0;
|
||||
if ((s_m++ % 30) == 0)
|
||||
DEBUG_STREAM << "[map] w2v ppm=" << pixelsPerMeter
|
||||
<< " halfW=" << halfWidth << " scale=" << currentScale
|
||||
<< " m=[" << worldToView(0,0) << "," << worldToView(0,1) << "," << worldToView(0,2)
|
||||
<< " | " << worldToView(1,0) << "," << worldToView(1,1) << "," << worldToView(1,2)
|
||||
<< " | " << worldToView(2,0) << "," << worldToView(2,1) << "," << worldToView(2,2)
|
||||
<< " | t " << worldToView(3,0) << "," << worldToView(3,1) << "," << worldToView(3,2)
|
||||
<< "]\n" << std::flush;
|
||||
}
|
||||
|
||||
BuildRadarShadow(worldToView); // FUN_004c228c
|
||||
|
||||
@@ -803,6 +829,14 @@ void
|
||||
{
|
||||
continue;
|
||||
}
|
||||
// VISIBLE-RADIUS cull (port): the capability radius above (RadarPercent
|
||||
// x 4000 m) exceeds the SCOPE radius (currentScale/2 at the current
|
||||
// zoom); an out-of-scope draw lands beyond the port rect on the shared
|
||||
// gauge surface. Skip what the scope can't show.
|
||||
if (delta.x*delta.x + delta.z*delta.z > currentScale * currentScale * 0.25f)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Pip *pip = (pips != NULL)
|
||||
? pips->LookUpPip(entity->GetResourceID()) // FUN_004688ce(pips+0x58,entity+0x1bc,0)
|
||||
@@ -888,10 +922,23 @@ void
|
||||
{
|
||||
continue;
|
||||
}
|
||||
// VISIBLE-RADIUS cull (port; see DrawStatic): don't draw past the scope.
|
||||
if (delta.x*delta.x + delta.z*delta.z > currentScale * currentScale * 0.25f)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Pip *pip = (pips != NULL)
|
||||
? pips->LookUpPip(entity->GetResourceID())
|
||||
: NULL;
|
||||
if (mlog)
|
||||
{
|
||||
Point3D sp;
|
||||
sp.Multiply(EntityPosition(entity), worldToView);
|
||||
DEBUG_STREAM << "[map] pip=" << (void*)pip
|
||||
<< " screen=(" << sp.x << "," << sp.y << "," << sp.z << ")"
|
||||
<< " lod=" << LODIndex << " mpp=" << metersPerPixel << "\n" << std::flush;
|
||||
}
|
||||
if (pip != NULL)
|
||||
{
|
||||
AffineMatrix blipXform;
|
||||
@@ -987,6 +1034,15 @@ void
|
||||
// "MapDisplay::DrawNames -- GetSize() / iterator mismatch"
|
||||
// "d:\\tesla\\bt\\bt_l4\\BTL4RDR.CPP", 0x408
|
||||
}
|
||||
else
|
||||
{
|
||||
// VISIBLE-RADIUS cull (port; see DrawStatic): a label for a
|
||||
// contact beyond the scope would blit outside the port rect.
|
||||
Vector3D d;
|
||||
d.Subtract(EntityPosition(entity), viewingPosition);
|
||||
if (d.x*d.x + d.z*d.z > currentScale * currentScale * 0.25f)
|
||||
entity = NULL; // extract guards NULL -> no label
|
||||
}
|
||||
nameArray[i].ExtractFromEntity(entity, worldToView, owner, target); // FUN_004c19fc
|
||||
}
|
||||
|
||||
|
||||
@@ -669,24 +669,25 @@ void
|
||||
}
|
||||
|
||||
//
|
||||
// Pop a "destroyed by <killer>" status message onto the pilot HUD.
|
||||
// Gated on a real attacker player (+ the status pool, a NULL stub in
|
||||
// bring-up), so the ownerless-dummy solo path skips it (no player to name).
|
||||
// Pop a "destroyed" status message naming the victim onto the comm
|
||||
// ticker (MessageBoard feed, LIVE 2026-07-12). ALLOCATION NOTE: the
|
||||
// binary drew from the MemoryBlock pool @00512f6c and spliced the BT
|
||||
// vtable; our engine's StatusMessageUpdate reclaims each expired
|
||||
// message with Unregister_Object + plain delete (PLAYER.cpp:864), so
|
||||
// the matching allocation here is the engine ctor + Register_Object
|
||||
// -- same record {playerInvolved, messageType 0, 6.0s}, same queue.
|
||||
// Gated on a real named player (the ownerless dummy has none).
|
||||
//
|
||||
if (sender_owner && StatusMessagePool)
|
||||
if (sender_owner)
|
||||
{
|
||||
StatusMessage *status = (StatusMessage *)StatusMessagePool->New(); // FUN_00402f74(0x512f6c)
|
||||
if (status)
|
||||
{
|
||||
new (status) Player__StatusMessage(
|
||||
sender_owner, // *(sender+0x190)
|
||||
0,
|
||||
Player__StatusMessage *status = new Player__StatusMessage(
|
||||
sender_owner, // *(sender+0x190) -- the victim's player
|
||||
0, // message strip 0 (the kill cell)
|
||||
STATUS_DISPLAY_TIME // 0x40c00000 == 6.0f
|
||||
);
|
||||
*(int *)((char *)status + 0x18) = 0; // status[6] = 0
|
||||
Register_Object(status);
|
||||
AddStatusMessage(status); // FUN_0042e580
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// If we just killed our designated objective mech, notify it.
|
||||
@@ -1359,7 +1360,41 @@ int BTPilotKills(void *pilot)
|
||||
DEBUG_STREAM << "[score] gauge read: pilot=" << pilot << " kills=" << k << "\n" << std::flush;
|
||||
return k;
|
||||
}
|
||||
int BTPilotDeaths(void *pilot) { return pilot ? ((BTPlayer *)pilot)->GetDeaths() : 0; }
|
||||
// DEATHS display (MP DEATHS fix): the engine seeds Player::deathCount at -2
|
||||
// (PLAYER.cpp:759) and only the LOCAL vehicle-acquire path zeroes it -- a REMOTE
|
||||
// player's copy on this node keeps the raw pre-acquire value (-2/-1) forever,
|
||||
// which the scoreboard showed verbatim ("DEATHS -1", user-reported). Those
|
||||
// negatives are the engine's internal spawn-cycle convention, not real deaths;
|
||||
// clamp them to the observable truth: no deaths yet = 0.
|
||||
int BTPilotDeaths(void *pilot)
|
||||
{
|
||||
if (pilot == 0)
|
||||
return 0;
|
||||
int deaths = ((BTPlayer *)pilot)->GetDeaths();
|
||||
return (deaths < 0) ? 0 : deaths;
|
||||
}
|
||||
|
||||
// OBSERVED-DEATH tally (MP DEATHS fix): each node maintains every player's
|
||||
// score from LOCALLY OBSERVED events -- exactly how the KILLS credit already
|
||||
// works (the victim's ScoreMessage handler ++s the SHOOTER's local Player
|
||||
// copy). This is the symmetric half: when a REPLICANT mech dies here (the
|
||||
// once-per-death transition runs on every node), tally the death onto its
|
||||
// owning player's LOCAL copy. The owner's own node counts through its real
|
||||
// VehicleDead(-1) path, so the caller gates this to replicant mechs -- each
|
||||
// node then counts every pilot self-consistently. Normalize the -2/-1
|
||||
// pre-acquire seed first so death #1 reads 1.
|
||||
void BTPlayerCountObservedDeath(void *player)
|
||||
{
|
||||
if (player == 0)
|
||||
return;
|
||||
BTPlayer *p = (BTPlayer *)player;
|
||||
if (p->deathCount < 0)
|
||||
p->deathCount = 0;
|
||||
++p->deathCount;
|
||||
if (getenv("BT_SCORE_LOG"))
|
||||
DEBUG_STREAM << "[score] observed death -> pilot " << player
|
||||
<< " deaths=" << p->deathCount << "\n" << std::flush;
|
||||
}
|
||||
|
||||
// Is `pilot` the owner of `local_player`'s current target? (the SELECT-TARGET row
|
||||
// highlight). Computed here via accessors -- the PilotList's raw-offset version
|
||||
@@ -1388,12 +1423,38 @@ int BTPilotIsSelected(void *pilot, void *local_player)
|
||||
// compiled BTPlayer/Player__StatusMessage members (decode AddStatusMessage @0042e580).
|
||||
// A real (stub) definition is REQUIRED or the /FORCE link AVs MessageBoard::Execute's call.
|
||||
class BitMap;
|
||||
// MessageBoard feed (LIVE 2026-07-12): the board is the LOCAL cockpit's comm
|
||||
// ticker -- read the mission player's engine status queue. statusMessagePointer
|
||||
// is maintained per frame by Player::StatusMessageUpdate [T0 PLAYER.cpp:520/822]:
|
||||
// it holds the newest queued Player__StatusMessage until its 6s displayTime runs
|
||||
// out. The name bitmap resolves through the SAME small-raster chain the radar
|
||||
// labels use (Mission::GetSmallNameBitmap keyed by the involved player's egg
|
||||
// bitmapindex).
|
||||
Logical BTResolveMessageBoard(Entity * /*tracked_mech*/, int *messageId, BitMap **nameBitmap)
|
||||
{
|
||||
*messageId = -1;
|
||||
*nameBitmap = 0;
|
||||
BTPlayer *local_player = (application != 0)
|
||||
? (BTPlayer *)application->GetMissionPlayer() : 0;
|
||||
if (local_player == 0)
|
||||
{
|
||||
return False;
|
||||
}
|
||||
Player::StatusMessage *m = local_player->statusMessagePointer;
|
||||
if (m == 0)
|
||||
{
|
||||
return False;
|
||||
}
|
||||
*messageId = m->messageType;
|
||||
Player *involved = m->playerInvolved;
|
||||
if (involved != 0)
|
||||
{
|
||||
Mission *mission = application->GetCurrentMission();
|
||||
if (mission != 0)
|
||||
*nameBitmap = mission->GetSmallNameBitmap(involved->playerBitmapIndex);
|
||||
}
|
||||
return True;
|
||||
}
|
||||
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
// gauge scoring wave: PRODUCERS -- posted by the combat path (mech4.cpp) to feed
|
||||
|
||||
@@ -374,6 +374,7 @@ class DropZone__ReplyMessage;
|
||||
int
|
||||
roleClassIndex; // @0x274 (role resource +0xe4)
|
||||
friend int BTPlayerRoleLocksAdvanced(void *); // the FUN_004ac9c8 bridge (task #12)
|
||||
friend void BTPlayerCountObservedDeath(void *); // observed-death tally (MP DEATHS fix)
|
||||
|
||||
int
|
||||
killCount; // @0x27c kills credited to this player
|
||||
|
||||
@@ -346,6 +346,7 @@ void
|
||||
<< " edge=" << (int)fireEdge
|
||||
<< " alarm=" << (int)weaponAlarm.GetState()
|
||||
<< " level=" << (float)currentLevel
|
||||
<< " pct=" << (float)rechargeLevel
|
||||
<< " tgt=" << (void *)((owner != 0)
|
||||
? *(Entity **)((char *)owner + 0x388) : 0)
|
||||
<< std::endl;
|
||||
|
||||
@@ -597,6 +597,11 @@ void
|
||||
<< " inst=" << (int)GetInstance()
|
||||
<< " invalidZone=" << (int)message->invalidDamageZone
|
||||
<< " amount=" << message->damageData.damageAmount
|
||||
<< " type=" << (int)message->damageData.damageType
|
||||
<< " from=" << message->inflictingEntity
|
||||
<< " at(" << message->damageData.impactPoint.x
|
||||
<< "," << message->damageData.impactPoint.y
|
||||
<< "," << message->damageData.impactPoint.z << ")"
|
||||
<< " table=" << (void*)damageLookupTable
|
||||
<< " zones=" << damageZoneCount << "\n" << std::flush;
|
||||
|
||||
@@ -1384,6 +1389,7 @@ Mech::Mech(
|
||||
standingTemplateMaxY = 0.0f;
|
||||
duckedTemplateMaxY = 0.0f;
|
||||
templateBottomLift = 0.0f;
|
||||
frameEntryWorldVelocity = Vector3D(0.0f, 0.0f, 0.0f); // collision-damage guard snapshot
|
||||
if (GroundReal() && GetCollisionVolumeCount() > 0
|
||||
&& collisionTemplate != 0 && collisionVolume != 0)
|
||||
{
|
||||
|
||||
@@ -625,6 +625,21 @@ public:
|
||||
Scalar duckedTemplateMaxY; // binary @0x51c 0.6 x standing (duck preset)
|
||||
Scalar templateBottomLift; // binary @0x4b8 0.05 x (volume maxX-minX)
|
||||
|
||||
// COLLISION-DAMAGE ECONOMY GUARD (the MP ram one-shot, 2026-07-12): the
|
||||
// mech's TRUE motion at frame entry, snapshotted just before the frame's
|
||||
// ProcessCollisionList walk. Each StaticBounce in that walk MUTATES
|
||||
// worldLinearVelocity (the += delta_v reflection); with several solids in
|
||||
// one frame's list (2007 WinTesla puts TERRAIN in the collision tree; the
|
||||
// 1995 ground was a heightfield probe, never a list entry) the reflections
|
||||
// COMPOUND x(1+e) per contact, and a mech-vs-mech contact later in the
|
||||
// list priced its dispatched damage off a velocity amplified 4x-40x
|
||||
// (62-pt bump economy -> 1074/112375-pt one-shots; mp_a.log:32651).
|
||||
// Mech::ProcessCollision restores worldLinearVelocity from this snapshot
|
||||
// per contact, so contact damage is always priced at the mech's REAL
|
||||
// approach speed -- which is all the binary's StaticBounce ever saw.
|
||||
// By-name access only; declared after the layout-locked fields.
|
||||
Vector3D frameEntryWorldVelocity;
|
||||
|
||||
// AUTHENTIC TARGETING (task #36): the engine Reticle struct (MUNGA/RETICLE.h
|
||||
// [T0]) -- the mech's "TargetReticle" attribute (id 0x1d) binds to it, per the
|
||||
// RP VTV analog (VTV.h: `Reticle targetReticle` + TargetReticleAttributeID).
|
||||
|
||||
+209
-11
@@ -764,13 +764,15 @@ int
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
struct BTProjectile {
|
||||
Point3D pos;
|
||||
Vector3D dir;
|
||||
Scalar speed;
|
||||
Vector3D vel; // world velocity (authored MuzzleVelocity, steered per frame)
|
||||
Scalar speed; // |vel| held constant through the steer
|
||||
Scalar traveled;
|
||||
Scalar range;
|
||||
Entity *target;
|
||||
Point3D targetPos;
|
||||
Scalar damage;
|
||||
Scalar aimOffsetY; // vertical aim offset vs the target's origin (live re-lead)
|
||||
Scalar damage; // <= 0 -> VISUAL-ONLY round (replicant-side salvo mirror)
|
||||
int guided; // 1 = missile (seeker loft + steering); 0 = ballistic (straight)
|
||||
int active;
|
||||
};
|
||||
static BTProjectile gProjectiles[64];
|
||||
@@ -782,7 +784,7 @@ extern void BTPushBeam(float,float,float, float,float,float, unsigned, float, fl
|
||||
// locked target entity + point, the launch speed (|muzzleVelocity|), and per-shot damage.
|
||||
void
|
||||
BTPushProjectile(const Point3D &muzzle, void *shooter, void *target, const Point3D &targetPos,
|
||||
Scalar speed, Scalar damage)
|
||||
Scalar speed, Scalar damage, const Vector3D *launch_velocity, int guided)
|
||||
{
|
||||
// The weapon's GetMuzzlePoint falls back to the mech ORIGIN (feet) when its mount
|
||||
// segment doesn't resolve, so a projectile appeared to launch from the mech's feet.
|
||||
@@ -824,7 +826,9 @@ void
|
||||
// like the energy weapons. (The old gEnemyMech fallback pre-dated the
|
||||
// acquisition and let missiles bypass it.)
|
||||
Point3D tpos = targetPos;
|
||||
if (target == 0)
|
||||
// A VISUAL round (damage <= 0, the replicant-side salvo mirror) flies on the
|
||||
// replicated aim POINT alone; a live round still requires the designation.
|
||||
if (target == 0 && damage > 0.0f)
|
||||
return;
|
||||
Vector3D d;
|
||||
d.x = tpos.x - mz.x; d.y = tpos.y - mz.y; d.z = tpos.z - mz.z;
|
||||
@@ -838,13 +842,45 @@ void
|
||||
if (gProjectiles[i].active) continue;
|
||||
BTProjectile &p = gProjectiles[i];
|
||||
p.pos = mz; // resolved launch port
|
||||
p.dir.x = d.x/len; p.dir.y = d.y/len; p.dir.z = d.z/len;
|
||||
p.speed = (speed > 1.0f) ? speed : 120.0f; // |launchVelocity|; sane fallback
|
||||
|
||||
// AUTHENTIC LAUNCH (missile-arc wave): the launcher's authored
|
||||
// MuzzleVelocity is a VECTOR in the shooter's frame (typically
|
||||
// up-tilted); rotate it into world by the shooter's pose so the round
|
||||
// leaves the rack climbing -- the front half of the pod's missile arc.
|
||||
// (The old code collapsed it to |v| along the straight line to the
|
||||
// target: dead-flat flight.) Fall back to the straight line when no
|
||||
// authored vector reaches us (non-missile callers).
|
||||
int haveLaunch = 0;
|
||||
if (launch_velocity != 0 && shooter != 0)
|
||||
{
|
||||
Mech *sm = (Mech *)shooter;
|
||||
UnitVector ax, ay, az;
|
||||
sm->localToWorld.GetFromAxis(X_Axis, &ax);
|
||||
sm->localToWorld.GetFromAxis(Y_Axis, &ay);
|
||||
sm->localToWorld.GetFromAxis(Z_Axis, &az);
|
||||
p.vel.x = ax.x*launch_velocity->x + ay.x*launch_velocity->y + az.x*launch_velocity->z;
|
||||
p.vel.y = ax.y*launch_velocity->x + ay.y*launch_velocity->y + az.y*launch_velocity->z;
|
||||
p.vel.z = ax.z*launch_velocity->x + ay.z*launch_velocity->y + az.z*launch_velocity->z;
|
||||
Scalar lv = (Scalar)sqrtf(p.vel.x*p.vel.x + p.vel.y*p.vel.y + p.vel.z*p.vel.z);
|
||||
if (lv > 1.0f) { p.speed = lv; haveLaunch = 1; }
|
||||
}
|
||||
if (!haveLaunch)
|
||||
{
|
||||
p.vel.x = d.x/len*p.speed; p.vel.y = d.y/len*p.speed; p.vel.z = d.z/len*p.speed;
|
||||
}
|
||||
p.traveled = 0.0f;
|
||||
p.range = len + 40.0f; // impact by the target, else expire just past it
|
||||
p.range = len * 1.3f + 60.0f; // arc margin; expire past the target
|
||||
p.target = (Entity *)target;
|
||||
p.targetPos = tpos; // resolved target position (fallback-aware)
|
||||
// live re-lead (authentic: the Seeker re-leads the MOVING target every
|
||||
// slice): remember the aim's height above the target's origin so the
|
||||
// per-frame refresh keeps striking the same body height.
|
||||
extern int BTIsRegisteredMech(Entity *e);
|
||||
p.aimOffsetY = (target != 0 && BTIsRegisteredMech((Entity *)target))
|
||||
? (tpos.y - ((Mech *)target)->localOrigin.linearPosition.y) : 0.0f;
|
||||
p.damage = damage;
|
||||
p.guided = guided; // autocannon shells fly straight (no seeker in the binary's plain Projectile)
|
||||
p.active = 1;
|
||||
return;
|
||||
}
|
||||
@@ -859,23 +895,117 @@ static void
|
||||
BTProjectile &p = gProjectiles[i];
|
||||
if (!p.active) continue;
|
||||
Point3D prev = p.pos;
|
||||
|
||||
// AUTHENTIC GUIDANCE (missile-arc wave; Seeker::LeadTarget @004beae4 +
|
||||
// the @004bef78 steering [T1]). The seeker LOFTS the aim point while
|
||||
// the round is far out -- beyond 200 m the aim rises by
|
||||
// 0.1 x clamp(range-200, 0, 300) (up to +30 m above the target) -- so
|
||||
// the missile climbs at range and dives as it closes: the pod's
|
||||
// "gravity arc". Steering is the port shape of the binary's
|
||||
// squared-error guidance: rotate the velocity toward the lofted aim at
|
||||
// the decomp turn gain (MissileTurnGain = 4.0, _DAT_004bf5a4), speed
|
||||
// held at the authored launch speed.
|
||||
if (p.guided)
|
||||
{
|
||||
// LIVE RE-LEAD (authentic: Seeker::LeadTarget runs every slice on
|
||||
// the MOVING target): refresh the aim from the target's current
|
||||
// position, preserving the launch aim's body height.
|
||||
extern int BTIsRegisteredMech(Entity *e);
|
||||
if (p.target != 0 && BTIsRegisteredMech(p.target)
|
||||
&& !((Mech *)p.target)->IsMechDestroyed())
|
||||
{
|
||||
p.targetPos = ((Mech *)p.target)->localOrigin.linearPosition;
|
||||
p.targetPos.y += p.aimOffsetY;
|
||||
}
|
||||
|
||||
Point3D aim = p.targetPos;
|
||||
Scalar rx = aim.x - p.pos.x, ry = aim.y - p.pos.y, rz = aim.z - p.pos.z;
|
||||
Scalar range = (Scalar)sqrtf(rx*rx + ry*ry + rz*rz);
|
||||
if (range > 200.0f) // SeekerLeadMinRange @004bec18
|
||||
{
|
||||
Scalar lead = range - 200.0f;
|
||||
if (lead > 300.0f) lead = 300.0f; // SeekerLeadMaxClamp @004bec24
|
||||
aim.y += 0.1f * lead; // SeekerLeadCoef @004bec28
|
||||
ry = aim.y - p.pos.y;
|
||||
}
|
||||
if (range > 0.001f)
|
||||
{
|
||||
Scalar inv = 1.0f / range;
|
||||
Scalar cx = p.vel.x / p.speed, cy = p.vel.y / p.speed, cz = p.vel.z / p.speed;
|
||||
// MissileTurnGain (4.0, _DAT_004bf5a4) far out; DOUBLED inside
|
||||
// the no-loft radius so the dive converges onto the contact
|
||||
// sphere (the port shape of the binary's squared-error terminal
|
||||
// aggressiveness -- error^2 steering climbs steeply near boresight).
|
||||
Scalar k = ((range < 200.0f) ? 8.0f : 4.0f) * dt;
|
||||
cx += (rx*inv - cx) * k; cy += (ry*inv - cy) * k; cz += (rz*inv - cz) * k;
|
||||
Scalar cl = (Scalar)sqrtf(cx*cx + cy*cy + cz*cz);
|
||||
if (cl > 0.001f)
|
||||
{
|
||||
p.vel.x = cx/cl*p.speed; p.vel.y = cy/cl*p.speed; p.vel.z = cz/cl*p.speed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
p.pos.x += p.vel.x*dt; p.pos.y += p.vel.y*dt; p.pos.z += p.vel.z*dt;
|
||||
p.traveled += p.speed * dt;
|
||||
|
||||
// WORLD IMPACT (authentic: the binary missile runs a world collision
|
||||
// query every frame, FUN_0042291c, and DETONATES on geometry). Ray the
|
||||
// flight step against the terrain/cave solids -- a lofted round in a
|
||||
// low cavern bursts on the CEILING instead of punching through it.
|
||||
{
|
||||
extern bool BTGroundRayHit(float,float,float, float,float,float,
|
||||
float, float*,float*,float*);
|
||||
extern Entity *gBTTerrainEntity; // captured by MakeEntityRenderables
|
||||
Scalar step = p.speed * dt;
|
||||
p.pos.x += p.dir.x*step; p.pos.y += p.dir.y*step; p.pos.z += p.dir.z*step;
|
||||
p.traveled += step;
|
||||
if (gBTTerrainEntity != 0 && step > 0.0001f)
|
||||
{
|
||||
Vector3D rd;
|
||||
rd.x = p.vel.x/p.speed; rd.y = p.vel.y/p.speed; rd.z = p.vel.z/p.speed;
|
||||
float hx, hy, hz;
|
||||
if (BTGroundRayHit(prev.x, prev.y, prev.z, rd.x, rd.y, rd.z,
|
||||
step + 1.0f, &hx, &hy, &hz))
|
||||
{
|
||||
// burst on the rock: a tight puff cluster at the hit, no damage
|
||||
extern void BTPfxTrailPuff(int, float, float, float, float, float, float, int);
|
||||
for (int pf = 0; pf < 4; ++pf)
|
||||
BTPfxTrailPuff(0, hx, hy, hz, -rd.x, -rd.y, -rd.z, 2);
|
||||
if (getenv("BT_PROJ_LOG"))
|
||||
DEBUG_STREAM << "[projectile] WORLD burst at(" << hx << ","
|
||||
<< hy << "," << hz << ")\n" << std::flush;
|
||||
p.active = 0;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
// AUTHENTIC missile look: the round is a short hot streak, and the trail
|
||||
// is the real dsrm smoke-trail effect (psfx 0, "the lrm smoke trail")
|
||||
// puffed along the flight path each frame with local +Z = backward (the
|
||||
// .PFX's velocities stream the smoke behind the round). This replaces
|
||||
// the old 3-segment white tracer lines (a bring-up placeholder).
|
||||
Vector3D bd = p.dir;
|
||||
Vector3D bd;
|
||||
bd.x = p.vel.x/p.speed; bd.y = p.vel.y/p.speed; bd.z = p.vel.z/p.speed;
|
||||
BTPushBeam(prev.x, prev.y, prev.z, p.pos.x, p.pos.y, p.pos.z, 0x00FFB040u, 0.10f, 0.9f); // the round (hot streak)
|
||||
{
|
||||
extern void BTPfxTrailPuff(int, float, float, float, float, float, float, int);
|
||||
BTPfxTrailPuff(0, prev.x, prev.y, prev.z, -bd.x, -bd.y, -bd.z, 2);
|
||||
}
|
||||
|
||||
// CONTACT-ONLY damage (user-verified bug: an arcing round that ran out
|
||||
// its flight cap while still descending "applied damage without making
|
||||
// contact"). Proximity = the hit; the flight-cap expiry is a FIZZLE --
|
||||
// no damage, matching the binary (a missile that dies mid-air detonates
|
||||
// nothing; only the world-collision hit spawns the Damage entity).
|
||||
Scalar dx = p.targetPos.x - p.pos.x, dy = p.targetPos.y - p.pos.y, dz = p.targetPos.z - p.pos.z;
|
||||
if (dx*dx + dy*dy + dz*dz < (10.0f*10.0f) || p.traveled >= p.range)
|
||||
const int contact = (dx*dx + dy*dy + dz*dz < (10.0f*10.0f));
|
||||
if (!contact && p.traveled >= p.range)
|
||||
{
|
||||
if (getenv("BT_PROJ_LOG"))
|
||||
DEBUG_STREAM << "[projectile] FIZZLE (flight cap, no contact)\n" << std::flush;
|
||||
p.active = 0;
|
||||
continue;
|
||||
}
|
||||
if (contact)
|
||||
{
|
||||
Entity *tgt = p.target;
|
||||
// Deliver to the projectile's target mech -- the launcher set p.target
|
||||
@@ -1053,6 +1183,12 @@ void
|
||||
projectedVelocity = Motion::Identity;
|
||||
updateVelocity = Motion::Identity;
|
||||
updateAcceleration = Motion::Identity;
|
||||
// The Mover motion scalars too (the binary Reset zeroes the whole motion
|
||||
// block): a respawn is a TELEPORT -- stale death-frame velocity must never
|
||||
// survive into the first post-respawn collision/publish.
|
||||
worldLinearVelocity = Vector3D(0.0f, 0.0f, 0.0f);
|
||||
localVelocity = Motion::Identity;
|
||||
frameEntryWorldVelocity = Vector3D(0.0f, 0.0f, 0.0f);
|
||||
|
||||
// --- our RELOCATED gait/motion accumulators -> identity (the 1995 offsets
|
||||
// the decomp zeroes map to these named members in our layout) ---
|
||||
@@ -1281,6 +1417,21 @@ void
|
||||
DEBUG_STREAM << "[damage] *** " << GetEntityID()
|
||||
<< " DESTROYED (death effects dispatched from the death transition) ***\n"
|
||||
<< std::flush;
|
||||
|
||||
// OBSERVED-DEATH tally (MP DEATHS fix): a REPLICANT's death observed
|
||||
// here counts onto its owning player's LOCAL score copy -- the
|
||||
// symmetric half of the cross-pod KILLS credit (see btplayer.cpp
|
||||
// BTPlayerCountObservedDeath). Masters count through their own
|
||||
// VehicleDead(-1) path; counting them here too would double-tally.
|
||||
if (GetInstance() == ReplicantInstance)
|
||||
{
|
||||
Player *owning_player = GetOwningPlayer(); // entity+0x190
|
||||
if (owning_player != 0)
|
||||
{
|
||||
extern void BTPlayerCountObservedDeath(void *);
|
||||
BTPlayerCountObservedDeath(owning_player);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
@@ -1398,6 +1549,20 @@ void
|
||||
const float fdot = -((float)wv.x * (float)zAxR.x
|
||||
+ (float)wv.z * (float)zAxR.z); // mech faces -Z
|
||||
replMppr->speedDemand = (fdot < 0.0f) ? -spd : spd;
|
||||
// REPLICANT TURN-STEP (user-reported: a turning peer
|
||||
// statue-rotates while the local mech steps): the leg SM's
|
||||
// Standing case arms the turn-in-place "trn" clip (state 4,
|
||||
// mech2.cpp:590) from mppr->turnDemand -- which nothing fed on
|
||||
// a replicant, so pivots never stepped. Feed it from the
|
||||
// REPLICATED yaw rate (updateVelocity.angularMotion.y, stored
|
||||
// by Mover::ReadUpdateRecord from the master's localVelocity
|
||||
// -- the same stream DeadReckon rotates the body with). Only
|
||||
// the +-0.05 threshold matters to the SM (the trn clip
|
||||
// advances at fixed cadence), so a signed unit demand
|
||||
// suffices; the deadband ignores dead-reckon jitter.
|
||||
const float yawRate = (float)updateVelocity.angularMotion.y;
|
||||
replMppr->turnDemand = (yawRate > 0.02f) ? 1.0f
|
||||
: (yawRate < -0.02f) ? -1.0f : 0.0f;
|
||||
// Prime the same clip-advance scalars the master's gait block sets
|
||||
// each frame -- uninitialized on a replicant they read 0, freezing
|
||||
// the clip at advance-time dt*0 (observed: legState engaged at 11,
|
||||
@@ -2406,6 +2571,7 @@ void
|
||||
if (cols)
|
||||
{
|
||||
Point3D before = localOrigin.linearPosition;
|
||||
frameEntryWorldVelocity = worldLinearVelocity; // collision-damage guard (see mech.hpp)
|
||||
Damage collisionDamage; // filled by ProcessCollisionList (unused)
|
||||
ProcessCollisionList(cols, dt, collisionOldPos, &collisionDamage); // pushes localOrigin out
|
||||
Scalar dx = localOrigin.linearPosition.x - before.x;
|
||||
@@ -3621,6 +3787,12 @@ void
|
||||
if (isPlayer && gBlockCooldown > 0.0f)
|
||||
gBlockCooldown -= dt; // decay the out-of-contact window
|
||||
// BINARY-TAIL-DEFERRED: collisionTemporaryState = 0 here (@4aa741).
|
||||
// COLLISION-DAMAGE ECONOMY GUARD: snapshot the TRUE frame motion for the
|
||||
// per-contact velocity restore in Mech::ProcessCollision (see mech.hpp) --
|
||||
// the list walk below reflects worldLinearVelocity at every StaticBounce,
|
||||
// and a multi-solid frame compounds those reflections into a garbage
|
||||
// velocity that priced mech-vs-mech damage 4x-40x too high (mp_a.log:32651).
|
||||
frameEntryWorldVelocity = savedWorldVel;
|
||||
Damage dmg; // ctor zeroes damageAmount
|
||||
if (cols != 0)
|
||||
ProcessCollisionList(cols, dt, old_position, &dmg);
|
||||
@@ -3823,6 +3995,20 @@ static void
|
||||
DEBUG_STREAM << "[collide] dmg=" << resolved->damageAmount
|
||||
<< " -> victim class=" << (int)victim->GetClassID()
|
||||
<< " (zone==-1, resolved by receiver)\n" << std::flush;
|
||||
|
||||
// RAM ECONOMY TELEMETRY (the MP ram one-shot investigation): every
|
||||
// dispatched ram, with the exact velocity StaticBounce priced it from --
|
||||
// the decisive evidence channel for any future out-of-economy amount.
|
||||
if (getenv("BT_MP_NET"))
|
||||
{
|
||||
const Vector3D &v = inflictor->GetWorldLinearVelocity();
|
||||
DEBUG_STREAM << "[collide-tx] " << inflictor->GetEntityID()
|
||||
<< " rams " << victim->GetEntityID()
|
||||
<< " dmg=" << resolved->damageAmount
|
||||
<< " |v|=" << (Scalar)sqrtf(v.x*v.x + v.y*v.y + v.z*v.z)
|
||||
<< " at(" << dmg.impactPoint.x << "," << dmg.impactPoint.y
|
||||
<< "," << dmg.impactPoint.z << ")\n" << std::flush;
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
@@ -3838,6 +4024,18 @@ void
|
||||
return;
|
||||
}
|
||||
|
||||
// COLLISION-DAMAGE ECONOMY GUARD (see mech.hpp frameEntryWorldVelocity):
|
||||
// this runs once PER CONTACT in the frame's ProcessCollisionList walk, and
|
||||
// the StaticBounce below reflects worldLinearVelocity (+= delta_v) every
|
||||
// time. Restore the TRUE frame-entry motion first so a multi-solid frame
|
||||
// (mech + terrain solids + debris) cannot compound the reflections into an
|
||||
// amplified velocity: every contact -- and every dispatched mech-vs-mech
|
||||
// damage amount -- is priced at the mech's real approach speed, which is
|
||||
// all the 1995 binary's StaticBounce ever saw (its ground was a heightfield
|
||||
// probe, never a collision-list entry). The post-list velocity is
|
||||
// overwritten by the response policy / next frame's derive regardless.
|
||||
worldLinearVelocity = frameEntryWorldVelocity;
|
||||
|
||||
// --- the BoxedSolid resolver (:15302-15304); miss => nothing runs -------
|
||||
Scalar penetration = 0.0f;
|
||||
if (!collisionVolume->ProcessCollision(collision, worldLinearVelocity,
|
||||
|
||||
@@ -511,3 +511,18 @@ void
|
||||
*message_id_out = sub->controlMessageID; // @0xEC
|
||||
*receiver_out = (Receiver *)sub;
|
||||
}
|
||||
|
||||
//
|
||||
// BTSubsystemDamageLevelOf -- complete-type bridge (PPC-dial fix, 2026-07-12):
|
||||
// feeds the gauge cluster's OPERATING flag (the "destroyed X" lamps). The
|
||||
// observable truth: a subsystem is out of action when its own damage level
|
||||
// saturates (DistributeCriticalHit destroys at >= 1.0, mechdmg.cpp) -- read
|
||||
// that instead of the unidentified binary +0x40 field the old raw read
|
||||
// guessed at (layout noise -> X's over healthy panels).
|
||||
//
|
||||
Scalar
|
||||
BTSubsystemDamageLevelOf(void *subsystem_in)
|
||||
{
|
||||
return (subsystem_in != 0)
|
||||
? ((MechSubsystem *)subsystem_in)->GetSubsystemDamageLevel() : 0.0f;
|
||||
}
|
||||
|
||||
@@ -384,6 +384,10 @@ void
|
||||
? owner->GetSubsystem(info->subsystemID) : 0;
|
||||
ResourceDescription::ResourceID explosionID =
|
||||
ResolveExplosionID(firingWeapon); // weapon+0x3E4
|
||||
if (getenv("BT_FIRE_LOG"))
|
||||
DEBUG_STREAM << "[boom-q] subsysID=" << info->subsystemID
|
||||
<< " weapon=" << (firingWeapon ? firingWeapon->GetName() : "<none>")
|
||||
<< " explosionID=" << explosionID << std::endl;
|
||||
if (explosionID != ResourceDescription::NullResourceID
|
||||
&& !weaponExplosions.Find(explosionID)) // chain+0xFC, slot 0xC
|
||||
{
|
||||
@@ -541,6 +545,10 @@ void
|
||||
explosion_id, Explosion::DefaultFlags, o,
|
||||
hitID, owner->GetEntityID());
|
||||
Explosion *e = Explosion::Make(&m);
|
||||
if (getenv("BT_FIRE_LOG"))
|
||||
DEBUG_STREAM << "[boom] id=" << explosion_id
|
||||
<< " at(" << o.linearPosition.x << "," << o.linearPosition.y
|
||||
<< "," << o.linearPosition.z << ") made=" << (void *)e << std::endl;
|
||||
if (e != 0)
|
||||
{
|
||||
Register_Object(e);
|
||||
|
||||
@@ -68,8 +68,49 @@
|
||||
// WAVE 7 Phase B -- the port-side flying-projectile service (defined in mech4.cpp, a
|
||||
// complete-Mech TU with the render + damage path). A fired missile becomes a tracked
|
||||
// projectile that flies to the target + delivers this weapon's damage on impact.
|
||||
// Missile-arc wave: launch_velocity = the authored MuzzleVelocity VECTOR in the
|
||||
// shooter's frame (up-tilted rack launch); damage <= 0 = a VISUAL-ONLY round.
|
||||
extern void BTPushProjectile(const Point3D &muzzle, void *shooter, void *target,
|
||||
const Point3D &targetPos, Scalar speed, Scalar damage);
|
||||
const Point3D &targetPos, Scalar speed, Scalar damage,
|
||||
const Vector3D *launch_velocity = 0, int guided = 1);
|
||||
|
||||
//###########################################################################
|
||||
// Port-side salvo replication state (missile-visibility wave).
|
||||
//
|
||||
// The launcher OBJECT is byte-locked at 0x44C (factory alloc) -- salvo state
|
||||
// cannot live on it. Keyed by launcher identity: on a MASTER, `fired` counts
|
||||
// FireWeapon salvos (serialized in the extended update record); on a
|
||||
// REPLICANT, `seen` tracks the last counter applied (-1 = unsynced, so a
|
||||
// freshly-joined peer does not replay the master's historical count).
|
||||
//###########################################################################
|
||||
namespace {
|
||||
struct BTSalvoState
|
||||
{
|
||||
const void *owner;
|
||||
int fired; // master: salvos launched
|
||||
int seen; // replicant: last counter applied (-1 = unsynced)
|
||||
Point3D target; // master: the last salvo's aim point
|
||||
};
|
||||
BTSalvoState gSalvoTable[64];
|
||||
|
||||
BTSalvoState &BTSalvoOf(const void *launcher)
|
||||
{
|
||||
int freeSlot = -1;
|
||||
for (int i = 0; i < 64; ++i)
|
||||
{
|
||||
if (gSalvoTable[i].owner == launcher)
|
||||
return gSalvoTable[i];
|
||||
if (gSalvoTable[i].owner == 0 && freeSlot < 0)
|
||||
freeSlot = i;
|
||||
}
|
||||
BTSalvoState &s = gSalvoTable[(freeSlot >= 0) ? freeSlot : 0];
|
||||
s.owner = launcher;
|
||||
s.fired = 0;
|
||||
s.seen = -1;
|
||||
s.target = Point3D(0.0f, 0.0f, 0.0f);
|
||||
return s;
|
||||
}
|
||||
}
|
||||
|
||||
//###########################################################################
|
||||
//###########################################################################
|
||||
@@ -258,15 +299,91 @@ void MissileLauncher::FireWeapon()
|
||||
+ launchVelocity.y*launchVelocity.y
|
||||
+ launchVelocity.z*launchVelocity.z);
|
||||
// Always launch -- BTPushProjectile falls back to gEnemyMech when the owner's target
|
||||
// slot isn't populated (bring-up); missileCount missiles per salvo.
|
||||
// slot isn't populated (bring-up); missileCount missiles per salvo. The authored
|
||||
// MuzzleVelocity VECTOR rides along (missile-arc wave): the round leaves the rack
|
||||
// on the authored up-tilt, then the seeker loft + steering arc it onto the target.
|
||||
int nmiss = (missileCount > 0 && missileCount < 40) ? missileCount : 1;
|
||||
for (int i = 0; i < nmiss; ++i)
|
||||
BTPushProjectile(muzzle, o, target, targetPos, speed, damageData.damageAmount);
|
||||
BTPushProjectile(muzzle, o, target, targetPos, speed, damageData.damageAmount,
|
||||
&launchVelocity);
|
||||
|
||||
// Salvo replication (missile-visibility wave): bump the fire counter + stamp
|
||||
// the aim point; the extended update record carries both to peer nodes.
|
||||
{
|
||||
BTSalvoState &s = BTSalvoOf(this);
|
||||
++s.fired;
|
||||
s.target = targetPos;
|
||||
}
|
||||
|
||||
simulationFlags |= 0x1; // replication-dirty
|
||||
// ENQUEUE the record (visibility fix): the dirty flag alone never
|
||||
// serializes -- ForceUpdate sets the updateModel bit that makes the next
|
||||
// PerformAndWatch write THIS subsystem's record into the mech's update
|
||||
// stream (the exact mechanism the Emitter's beam replication uses,
|
||||
// emitter.cpp:512). Without it the peer never saw a salvo record at all.
|
||||
ForceUpdate();
|
||||
Check_Fpu();
|
||||
}
|
||||
|
||||
//#############################################################################
|
||||
// WriteUpdateRecord / ReadUpdateRecord (PORT record extension -- missile-
|
||||
// visibility wave; see the mislanch.hpp banner). The master serializes the
|
||||
// salvo counter + aim point after the MechWeapon base fields; the replicant
|
||||
// edge-detects the counter and mirrors the salvo as VISUAL-ONLY rounds
|
||||
// (damage 0 -- the master's own rounds deliver the damage cross-pod).
|
||||
//
|
||||
void
|
||||
MissileLauncher::WriteUpdateRecord(Simulation__UpdateRecord *message, int update_model)
|
||||
{
|
||||
MechWeapon::WriteUpdateRecord(message, update_model); // @004b9690 (alarm fields)
|
||||
|
||||
MissileLauncher__UpdateRecord *rec = (MissileLauncher__UpdateRecord *)message;
|
||||
BTSalvoState &s = BTSalvoOf(this);
|
||||
rec->recordLength = sizeof(MissileLauncher__UpdateRecord);
|
||||
rec->salvoCounter = s.fired;
|
||||
rec->salvoRounds = missileCount;
|
||||
rec->salvoTarget = s.target;
|
||||
}
|
||||
|
||||
void
|
||||
MissileLauncher::ReadUpdateRecord(Simulation__UpdateRecord *message)
|
||||
{
|
||||
MechWeapon::ReadUpdateRecord(message); // @004b964c (alarm apply)
|
||||
|
||||
MissileLauncher__UpdateRecord *rec = (MissileLauncher__UpdateRecord *)message;
|
||||
BTSalvoState &s = BTSalvoOf(this);
|
||||
if (s.seen < 0)
|
||||
{
|
||||
// first sync after spawn/join: adopt the master's count silently --
|
||||
// no replay of salvos fired before we could see them.
|
||||
s.seen = rec->salvoCounter;
|
||||
}
|
||||
else if (rec->salvoCounter != s.seen)
|
||||
{
|
||||
s.seen = rec->salvoCounter;
|
||||
|
||||
// Mirror ONE salvo (a missed record collapses to one -- catch-up
|
||||
// replays would draw stale phantom volleys). Rounds fly the same
|
||||
// authored launch vector + seeker-loft arc as the master's, from
|
||||
// this replicant's own resolved rack ports, damage 0.
|
||||
Point3D mz;
|
||||
GetMuzzlePoint(mz); // @004b9948
|
||||
Scalar spd = (Scalar)sqrtf(launchVelocity.x*launchVelocity.x
|
||||
+ launchVelocity.y*launchVelocity.y
|
||||
+ launchVelocity.z*launchVelocity.z);
|
||||
int n = rec->salvoRounds;
|
||||
if (n < 1 || n > 40)
|
||||
n = (missileCount > 0 && missileCount < 40) ? missileCount : 1;
|
||||
for (int i = 0; i < n; ++i)
|
||||
BTPushProjectile(mz, owner, 0 /*no entity: aim point only*/,
|
||||
rec->salvoTarget, spd, 0.0f /*VISUAL*/, &launchVelocity);
|
||||
if (getenv("BT_PROJ_LOG"))
|
||||
DEBUG_STREAM << "[projectile] REPLICANT salvo x" << n
|
||||
<< " at(" << rec->salvoTarget.x << "," << rec->salvoTarget.y
|
||||
<< "," << rec->salvoTarget.z << ")\n" << std::flush;
|
||||
}
|
||||
}
|
||||
|
||||
//#############################################################################
|
||||
// TakeDamage / DeathReset
|
||||
//
|
||||
|
||||
@@ -90,6 +90,35 @@ class Missile;
|
||||
void TakeDamage(Damage &damage); // inherits MechWeapon::TakeDamage (vtable slot 6, @004b964c)
|
||||
void DeathReset(Logical full_reset); // best-effort -- chains ProjectileWeapon reset @004bbaf8
|
||||
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
// Replication Support (PORT record extension -- missile-visibility wave)
|
||||
//
|
||||
// In the 1995 binary a fired Missile was a REPLICATED WORLD ENTITY (its
|
||||
// own update records, tag 0x78), so the peer pod rendered the round from
|
||||
// entity replication and the launcher's record stayed the MechWeapon
|
||||
// base. The port's flying round is a LOCAL tracked projectile
|
||||
// (BTPushProjectile) -- invisible to peers -- so the launcher's record
|
||||
// carries the SALVO EVENT instead: a fire counter + the aim point + the
|
||||
// round count. The replicant-side reader edge-detects the counter and
|
||||
// launches a VISUAL-ONLY mirror salvo (damage 0; the master's own rounds
|
||||
// deliver damage cross-pod through TakeDamage). Both nodes run this
|
||||
// port, so the extended wire format is self-consistent. Salvo STATE
|
||||
// lives in a port-side table (mislanch.cpp) -- the object layout is
|
||||
// byte-locked to the 0x44C factory alloc and must not grow.
|
||||
//
|
||||
public:
|
||||
struct MissileLauncher__UpdateRecord:
|
||||
public MechWeapon::UpdateRecord
|
||||
{
|
||||
int salvoCounter; // fires since spawn (edge-detected by peers)
|
||||
int salvoRounds; // missiles in the salvo (authored missileCount)
|
||||
Point3D salvoTarget; // the aim point the salvo flew at
|
||||
};
|
||||
typedef MissileLauncher__UpdateRecord UpdateRecord;
|
||||
|
||||
void WriteUpdateRecord(Simulation__UpdateRecord *message, int update_model); // base-typed param (task #51 override rule)
|
||||
void ReadUpdateRecord(Simulation__UpdateRecord *message);
|
||||
|
||||
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
// Model Support
|
||||
//
|
||||
|
||||
@@ -88,8 +88,13 @@
|
||||
#include <math.h>
|
||||
|
||||
// WAVE 7 Phase B -- the port-side flying-projectile service (defined in mech4.cpp).
|
||||
// SIGNATURE MUST MATCH mech4.cpp EXACTLY (the missile-arc wave added
|
||||
// launch_velocity): a stale extern here mangles to a nonexistent symbol, /FORCE
|
||||
// fills the call with garbage, and the first autocannon shot AVs (user-hit
|
||||
// crash, 2026-07-12 -- gotcha #3).
|
||||
extern void BTPushProjectile(const Point3D &muzzle, void *shooter, void *target,
|
||||
const Point3D &targetPos, Scalar speed, Scalar damage);
|
||||
const Point3D &targetPos, Scalar speed, Scalar damage,
|
||||
const Vector3D *launch_velocity = 0, int guided = 1);
|
||||
|
||||
|
||||
//#############################################################################
|
||||
@@ -679,7 +684,8 @@ void
|
||||
Scalar speed = (Scalar)sqrtf(launchVelocity.x*launchVelocity.x
|
||||
+ launchVelocity.y*launchVelocity.y
|
||||
+ launchVelocity.z*launchVelocity.z);
|
||||
BTPushProjectile(muzzle, o, target, targetPos, speed, damageData.damageAmount);
|
||||
BTPushProjectile(muzzle, o, target, targetPos, speed, damageData.damageAmount,
|
||||
0 /*aim straight at the pick*/, 0 /*BALLISTIC: no seeker on a shell*/);
|
||||
|
||||
simulationFlags |= 0x1; // replication-dirty
|
||||
Check_Fpu();
|
||||
|
||||
Reference in New Issue
Block a user