#125 FIXED + 2-node benched: peers now see a killed arm WRECK + its gun pod DROP OFF on the FIRST hit. Root pair: (1) the MechDeathHandler Tick transcription carried an INVENTED write -- ApplyDamageGraphicState(descriptor->graphicState) -- where the raw @0042aa2c (part_003.c:10963-93) only PICKS an effect resource (level flag & 4 -> DescriptorForLevel; gstate flag & 8 -> DescriptorForGraphicState(the zone's CURRENT gstate), overwriting the level pick); with the port's pick hardcoded to Destroyed(1) the invention STOMPED every destruction-cascade child's authored Gone(2) back to 1 in the same tick, master-side, BEFORE the record shipped -- the wire itself carried the corruption, so no peer could ever render the pod dropping (receipts: legacy A remade jointrgun to gstate 2 then re-remade it to 1; legacy B received 'dz_rgun gstate 0->1'). Gotcha 36: an added write is worse than a stub -- it actively fights the authentic writers on every node. (2) the destroyed-mesh swap trigger was descriptor-gated (DescriptorForLevel returns NULL past the last threshold; cascade children have gstate edges with NO level edge) -- the binary needed no trigger at all (its renderer re-picked every segment's mesh from zone gstate PER FRAME); the port's built-once render tree now gets the event equivalent: per-zone gstate caches in Tick fire ONE coalesced RemakeEntityRenderables on any edge, which is also the PEER's only trigger for replicated gstate-only beats. Restored authored two-stage story: a directly-killed gun zone hangs its wrecked variant mesh (thrdrgun-family, exterior skeleton; every zone's descriptor table carries lvl=-1 gs=1/gs=2 terminals -- unreachable by level, existing exactly for the gstate picks); a killed ARM swaps to its wrecked variant AND the pod segment vanishes. Effects suppressed on ->Exists edges (the binary's heal writes the cell raw, no flag -> no respawn puff; regression benched 0 heal puffs across a full death/respawn cycle). 2-node benched both ways (night18/mp_zone125.sh + zonedump.sh): fix = peer receives dz_rarm gstate 0->1 + dz_rgun 0->2 on ONE beat, swaps thrdrarm.bgf and HIDES the pod, first-beat remake receipt; legacy = the stomp conviction above. KB: combat-damage.md MechDeathHandler section rewritten to the corrected reading; gotcha 36.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
f597f76a30
commit
91351b94bc
@@ -977,14 +977,33 @@ Full decomp map of the death path. Two tiers:
|
||||
`ApplyDamageAndMeasure` @4ac07c. Diag: `BT_CRIT_PROBE=<zone>`. Detail: [[open-questions]]
|
||||
(the resolved entry) + mechdmg.cpp/mechsub.cpp.
|
||||
- **`MechDeathHandler`** (ctor `FUN_0042a984` + Performance `FUN_0042aa2c`, cached mech+0x850 / `mech[0x214]`) —
|
||||
the **per-subsystem destroyed-skin + explosion engine**. Each tick walks the damage subsystems
|
||||
(mech+0x120[mech+0x11c]); as a subsystem's damage crosses a descriptor-table threshold (`FUN_0042a5f4`
|
||||
detects the crossing, `FUN_0042a664` looks up the state) it applies the `GraphicState` ("Destroyed" = enum 1,
|
||||
parsed @part_003.c:9099) to the subsystem graphic (`sub+0xd4`) AND dispatches an effect-creation message
|
||||
(`FUN_0043663c`→`FUN_004364e4` to the effect mgr `app+0x38`). ✅ Fully exported; was a no-op stub
|
||||
(mech.cpp:221) — now fully RECONSTRUCTED (mechdmg.cpp — ctor @0042a984 / Tick @0042aa2c; wired in the
|
||||
Mech ctor, mech.cpp:1469) [T2]. Gamedata `DeathSplashDamage`/`Radius`/`Effect` load into a descriptor @ +0x74/78/7c
|
||||
(`FUN_004a2da8`). Destroyed-skin variant set (`destroyed`, `destroyeds`…`destroyedd`) @part_003.c:5720.
|
||||
the per-ZONE **effect watcher**. ⚠ **Corrected reading (#125, 2026-08-14 [T1 raw
|
||||
part_003.c:10963-93]; supersedes the 07-12 note that had it "applying the GraphicState"):**
|
||||
each tick walks the zones (mech+0x120[mech+0x11c]); two changed-flag branches SELECT an
|
||||
effect descriptor — level moved (`+0xb8 & 4`) → `DescriptorForLevel` (`FUN_0042a664`; band
|
||||
crossing `FUN_0042a5f4` also dirties the zone record on a master), gstate moved (`& 8`) →
|
||||
`DescriptorForGraphicState(the zone's CURRENT gstate @+0x78)` (`FUN_0042a6c4`), the gstate
|
||||
pick OVERWRITING the level pick — then dispatches the effect message
|
||||
(`FUN_0043663c`→`FUN_004364e4` to `app+0x38`). **The descriptor picks the EFFECT only —
|
||||
the binary NEVER writes gstate here.** The gstate writers are the destruction cascade
|
||||
(`RecurseSegmentTable`: the dying zone → Destroyed(1), children/siblings → Gone(2)),
|
||||
replication, and Reset. The port's former `ApplyDamageGraphicState(d->graphicState)` was an
|
||||
INVENTION, and with its pick hardcoded to Destroyed it stomped every cascade child's
|
||||
authored Gone(2) back to 1 the same tick, master-side, pre-send — the gun pod never dropped
|
||||
off a killed arm on ANY node (#125). **The authored two-stage arm story (restored):** a
|
||||
directly-killed GUN zone shows its wrecked variant mesh (`thrdrgun.bgf`-family, EXTERIOR
|
||||
skeleton set — every zone's table carries `lvl=-1 {fx gs=1}` + `{fx gs=2}` terminals,
|
||||
unreachable by level, existing for exactly these gstate picks); a killed ARM swaps to its
|
||||
wrecked variant AND its pod segment VANISHES (Gone). Port mesh coupling: the binary's
|
||||
renderer re-picked segment meshes from gstate per frame; the port's built-once tree gets the
|
||||
event equivalent — per-zone gstate caches in Tick fire ONE coalesced `RemakeEntityRenderables`
|
||||
on any edge (also the peer's only trigger: replicated gstate-only beats have no level edge).
|
||||
Effects are suppressed on →Exists edges (the binary's heal writes the cell raw, no flag —
|
||||
no respawn puff). 2-node benched both ways (night18/mp_zone125.sh): peer receives
|
||||
`dz_rgun gstate 0->2` and hides the pod on the FIRST beat; legacy A/B receipt shows the
|
||||
stomp (`jointrgun → gstate 2` then re-remade `→ 1`). [T1/T2] Gamedata
|
||||
`DeathSplashDamage`/`Radius`/`Effect` load into a descriptor @ +0x74/78/7c (`FUN_004a2da8`).
|
||||
Destroyed-skin variant set (`destroyed`, `destroyeds`…`destroyedd`) @part_003.c:5720.
|
||||
**NOT EXPORTED (master-perf gap 0x4a9770-0x4ab188 + the un-exported TakeDamage) [T4]:**
|
||||
- The `movementMode` WRITER (sets 5-8 then 2/9 on death) + the fall-direction (5/6/7/8) selection.
|
||||
- The `Mech::DeathShutdown` roster loop (RP `VTV::DeathShutdown` analog — loop subsystems calling the
|
||||
|
||||
@@ -1151,3 +1151,26 @@ stale dispatch is exactly one frame of "no exit condition tested" -- the
|
||||
#102 class. (3) Sibling of gotcha 30 (one cell, many port stores) and the
|
||||
namedClip aliasing class -- grep for `= (int)` alarm syncs when auditing any
|
||||
split-cell machine.
|
||||
|
||||
## 36. AN INVENTED WRITE INSIDE A FAITHFUL TRANSCRIPTION: descriptor-as-authority (#125, 2026-08-14)
|
||||
|
||||
The MechDeathHandler tick was transcribed with one line the binary does not
|
||||
have: `zone->ApplyDamageGraphicState(d->graphicState)` -- the effect
|
||||
DESCRIPTOR (a lookup result) written back into the zone's graphic state (the
|
||||
authoritative cell). The raw @0042aa2c (part_003.c:10963-93) uses the
|
||||
descriptor ONLY to pick an effect resource; the gstate writers are the
|
||||
cascade, replication, and Reset. With the pick hardcoded to Destroyed(1),
|
||||
the invented write STOMPED every cascade child's authored Gone(2) back to 1
|
||||
in the same tick, master-side, before the record shipped -- so the wire
|
||||
itself carried the corruption and no peer could ever render the authored
|
||||
state. An added write is worse than a stub: a stub leaves the authentic
|
||||
writers alone; an invention actively fights them, and it fights them on
|
||||
every node that runs the transcription.
|
||||
**Rules:** (1) in a transcription, every WRITE to authoritative state must
|
||||
have a byte-level citation -- effects/lookups reading state is safe, state
|
||||
consuming a lookup result is the red flag; (2) convict with a before/after
|
||||
receipt AROUND the suspect pass (the [BTrender] seg lines caught gstate 2
|
||||
becoming 1 between two remakes -- one receipt, whole conviction); (3) when a
|
||||
lookup table carries entries UNREACHABLE by its primary key (the lvl=-1
|
||||
gs=1/gs=2 terminals), those entries name the binary's OTHER selector -- find
|
||||
it before assuming the reachable subset is the whole contract.
|
||||
|
||||
+121
-66
@@ -1106,9 +1106,19 @@ void
|
||||
damageDescriptors.push_back(d);
|
||||
}
|
||||
if (count != 0 && getenv("BT_DEATH_LOG"))
|
||||
DEBUG_STREAM << "[deathfx] zone descriptor table loaded: " << count
|
||||
<< " entries (first threshold=" << (count > 0 ? damageDescriptors[0].damageLevel : -1.0f)
|
||||
<< " effect=" << (count > 0 ? damageDescriptors[0].effectResource : -1) << ")\n" << std::flush;
|
||||
{
|
||||
// #125: full table (the summary-only line hid the question that
|
||||
// mattered -- whether a Destroyed-gstate entry exists per zone).
|
||||
DEBUG_STREAM << "[deathfx] zone " << damageZoneIndex
|
||||
<< " '" << (const char *)damageZoneName
|
||||
<< "' descriptors (" << count << "):";
|
||||
for (int di = 0; di < (int)damageDescriptors.size(); ++di)
|
||||
DEBUG_STREAM << " {lvl=" << damageDescriptors[di].damageLevel
|
||||
<< " fx=" << damageDescriptors[di].effectResource
|
||||
<< " gs=" << damageDescriptors[di].graphicState
|
||||
<< " dly=" << damageDescriptors[di].timeDelay << "}";
|
||||
DEBUG_STREAM << "\n" << std::flush;
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
@@ -1212,6 +1222,15 @@ MechDeathHandler::MechDeathHandler(Mech *mech) // @0042a984
|
||||
// per-zone last-damage cache, zeroed (binary this[0x10], size mech+0x11c).
|
||||
int count = (mech != 0 && mech->damageZoneCount > 0) ? mech->damageZoneCount : 0;
|
||||
lastLevel.assign(count, 0.0f);
|
||||
// #125: gstate cache seeded from the zones' CURRENT states (a handler
|
||||
// built on an already-damaged mech must not fire a spurious remake).
|
||||
lastGState.assign(count, (int)DamageZone::ExistsGraphicState);
|
||||
for (int i = 0; i < count; ++i)
|
||||
{
|
||||
Mech__DamageZone *z = (mech != 0) ? mech->Zone(i) : 0;
|
||||
if (z != 0)
|
||||
lastGState[i] = (int)z->GetGraphicState();
|
||||
}
|
||||
}
|
||||
|
||||
MechDeathHandler::~MechDeathHandler() // @0042a9f4
|
||||
@@ -1219,10 +1238,16 @@ MechDeathHandler::~MechDeathHandler() // @0042a9f4
|
||||
}
|
||||
|
||||
//
|
||||
// @0042aa2c -- each tick, walk the zones; where a zone's damageLevel rose since
|
||||
// last tick, fire the current damage-band descriptor's explosion (binary +0xb8 &
|
||||
// 4) and, on destruction, the Destroyed-graphic descriptor's explosion (+0xb8 &
|
||||
// 8), applying that descriptor's GraphicState (the destroyed skin) to the zone.
|
||||
// @0042aa2c -- each tick, walk the zones (raw: part_003.c:10963-93). Two
|
||||
// changed-flag branches select an effect descriptor: level moved (+0xb8 & 4)
|
||||
// -> DescriptorForLevel; gstate moved (& 8) -> DescriptorForGraphicState(the
|
||||
// zone's CURRENT gstate), overwriting the level pick. The descriptor picks
|
||||
// the EFFECT only -- the binary never writes gstate here (#125 corrected the
|
||||
// earlier reading, which had invented a descriptor->gstate apply). Port
|
||||
// additions on top: the per-zone level/gstate caches stand in for the
|
||||
// changed-flags (immune to consumption ordering), and a gstate edge fires
|
||||
// ONE coalesced RemakeEntity -- the event equivalent of the binary renderer's
|
||||
// per-frame mesh re-pick (the port render tree is built once).
|
||||
//
|
||||
void
|
||||
MechDeathHandler::Tick() // @0042aa2c
|
||||
@@ -1417,12 +1442,40 @@ void
|
||||
}
|
||||
|
||||
int n = owner->damageZoneCount;
|
||||
int needRemake = 0; // #125: one coalesced remake per tick
|
||||
for (int i = 0; i < n && i < (int)lastLevel.size(); ++i)
|
||||
{
|
||||
Mech__DamageZone *zone = owner->Zone(i);
|
||||
if (zone == 0)
|
||||
continue;
|
||||
|
||||
// #125: GRAPHIC-STATE EDGE watch (all instances) -- the mesh follows
|
||||
// the zone's gstate, not the descriptor tables. Three writers move a
|
||||
// gstate with NO level edge in this loop: a replicated record
|
||||
// (DamageZone::ReadUpdateRecord -- the peer path), the master's
|
||||
// destruction cascade (RecurseSegmentTable's SetGraphicState(1)/Gone(2)
|
||||
// marks on the zone + its children/siblings), and the descriptor apply
|
||||
// below. The binary needed no event -- its renderer re-picked every
|
||||
// segment's mesh from gstate PER FRAME; the port's built-once render
|
||||
// tree re-picks on this diff instead. The old trigger (the descriptor
|
||||
// branch below, level-rise + Destroyed-descriptor gated) missed both
|
||||
// the cascade children (gstate Gone, level unchanged) and any zone
|
||||
// whose authored table lacks a gs=1 entry (DescriptorForLevel returns
|
||||
// NULL past the last threshold) -- the #125 field shape: peers' arm
|
||||
// intact until a SECOND destroying edge finally fired a remake, whose
|
||||
// full-segment walk then applied the stale states too.
|
||||
int gstateEdged = 0; // the flag-8 (& 8) analog, this tick
|
||||
if (i < (int)lastGState.size())
|
||||
{
|
||||
const int g = (int)zone->GetGraphicState();
|
||||
if (g != lastGState[i])
|
||||
{
|
||||
lastGState[i] = g;
|
||||
gstateEdged = 1;
|
||||
needRemake = 1;
|
||||
}
|
||||
}
|
||||
|
||||
Scalar level = zone->GetStructureDamageLevel();
|
||||
Scalar prev = lastLevel[i];
|
||||
|
||||
@@ -1430,47 +1483,64 @@ void
|
||||
// (ForceUpdate(DamageZoneUpdateModelFlag)). Just refresh the cache: the
|
||||
// respawn un-wreck no longer rides this edge -- it fires from the
|
||||
// death-state exit watcher above, which also covers deaths that never
|
||||
// moved a zone (#94).
|
||||
// moved a zone (#94). (The gstate cache above saw the heal edge too --
|
||||
// the coalesced remake restores the segment; no EFFECT fires for it,
|
||||
// see the Exists suppression below.)
|
||||
if (level < prev)
|
||||
{
|
||||
lastLevel[i] = level;
|
||||
continue;
|
||||
}
|
||||
if (level <= prev) // unchanged -- no new damage
|
||||
continue;
|
||||
lastLevel[i] = level;
|
||||
const int levelRose = (level > prev) ? 1 : 0; // the flag-4 (& 4) analog
|
||||
if (levelRose)
|
||||
lastLevel[i] = level;
|
||||
if (!levelRose && !gstateEdged)
|
||||
continue; // nothing moved this tick
|
||||
|
||||
// AUTHENTIC (FUN_0042aa2c @0042aa2c, the binary's effect-watcher level
|
||||
// branch): when the rise CROSSES a band-descriptor threshold
|
||||
// (FUN_0042a5f4) and this node owns the master, raise the entity's
|
||||
// damage-zone update flag (*(entity+0x18) |= 2, master-gated on
|
||||
// (entity+0x28 & 0xc) == 0) -- the zone update records ship this
|
||||
// zone's damageLevel to every observer, so replicant dolls/hull tint
|
||||
// track the fight at band granularity. This send was MISSING from
|
||||
// the port (only the graphic-state branch below raised it): masters
|
||||
// consumed damage correctly while every observer's copy sat at 0.0
|
||||
// until destruction (the "no discoloration in MP" report, #87).
|
||||
if (zone->DescriptorCrossed(prev, level)
|
||||
&& owner->GetInstance() == Entity::MasterInstance)
|
||||
owner->ForceUpdate(Entity::DamageZoneUpdateModelFlag);
|
||||
|
||||
// AUTHENTIC GATE (decomp re-verified 2026-07-12, workflow): the binary
|
||||
// fires the CURRENT band descriptor's effect whenever the zone's
|
||||
// damage-CHANGED flag is set (+0xb8 & 4, set by DamageZone::TakeDamage,
|
||||
// coalesced per tick) -- i.e. EVERY hit on a damaged mech re-puffs its
|
||||
// band effect. Bands 3/4 are authored ember/bright-FIRE plumes, so a
|
||||
// nearly-destroyed mech under fire burns continuously (the demo look).
|
||||
// A level RISE this tick IS the flag analog -- no crossing test. (The
|
||||
// earlier metronome was SHKWAVE mispacing, not this path; an observer
|
||||
// node sees a few replication-step puffs per volley -- smoke, fine.)
|
||||
|
||||
const Mech__DamageZone::DamageDescriptor *d = zone->DescriptorForLevel(level);
|
||||
if (level >= 1.0f && prev < 1.0f) // just destroyed -> the Destroyed descriptor
|
||||
// AUTHENTIC (FUN_0042aa2c raw, part_003.c:10963-10993 -- re-read
|
||||
// 2026-08-14 for #125; this REPLACES the 07-12 reading, which had
|
||||
// invented a descriptor->gstate WRITE, see below):
|
||||
// flag & 4 (level moved): descriptor = DescriptorForLevel(level)
|
||||
// [FUN_0042a664]; if the rise CROSSED a band threshold
|
||||
// [FUN_0042a5f4] and this node owns the master, dirty |= 2
|
||||
// (the zone update record ships) -- every hit on a damaged
|
||||
// mech re-puffs its band effect (bands 3/4 are authored
|
||||
// ember/fire plumes: a nearly-dead mech burns continuously).
|
||||
if (levelRose)
|
||||
{
|
||||
const Mech__DamageZone::DamageDescriptor *dd =
|
||||
zone->DescriptorForGraphicState(DamageZone::DestroyedGraphicState); // enum 1
|
||||
if (dd != 0)
|
||||
d = dd;
|
||||
if (zone->DescriptorCrossed(prev, level)
|
||||
&& owner->GetInstance() == Entity::MasterInstance)
|
||||
owner->ForceUpdate(Entity::DamageZoneUpdateModelFlag);
|
||||
}
|
||||
// flag & 8 (gstate moved): descriptor = DescriptorForGraphicState(
|
||||
// the zone's CURRENT graphic state, zone+0x78) [FUN_0042a6c4] --
|
||||
// OVERWRITING the level pick (the raw stores into the same
|
||||
// local_c, null included) -- and dirty |= 2 unconditionally on
|
||||
// a master. The binary NEVER writes gstate here: the descriptor
|
||||
// chooses the EFFECT only. The gstate writers are the
|
||||
// destruction cascade (RecurseSegmentTable: self 1, children/
|
||||
// siblings 2), replication (ReadUpdateRecord) and Reset. The
|
||||
// port's former `ApplyDamageGraphicState(d->graphicState)` was
|
||||
// an INVENTION -- and with the pick hardcoded to Destroyed it
|
||||
// STOMPED every cascade child's authored Gone(2) back to 1 the
|
||||
// same tick, master-side, before the record shipped: the gun
|
||||
// pod never dropped off a killed arm on ANY node. Receipt that
|
||||
// convicted it: `[BTrender] seg 'jointrgun' -> gstate 1` on a
|
||||
// solo master right after `[cascade] zone 17 DESTROYED` (#125).
|
||||
const Mech__DamageZone::DamageDescriptor *d =
|
||||
levelRose ? zone->DescriptorForLevel(level) : 0;
|
||||
if (gstateEdged)
|
||||
{
|
||||
const int g = (int)zone->GetGraphicState();
|
||||
// Exists(0) suppression: the binary's heal path (Reset) writes the
|
||||
// gstate cell RAW (no changed-flag), so a heal never fires flag 8
|
||||
// -- our cache diff sees the ->Exists edge anyway (wanted, it
|
||||
// drives the mesh restore) but must not fire the gs=0 BAND entry
|
||||
// as a respawn "damage" puff.
|
||||
d = (g != (int)DamageZone::ExistsGraphicState)
|
||||
? zone->DescriptorForGraphicState(g) : 0;
|
||||
if (owner->GetInstance() == Entity::MasterInstance)
|
||||
owner->ForceUpdate(Entity::DamageZoneUpdateModelFlag);
|
||||
}
|
||||
if (d != 0)
|
||||
{
|
||||
@@ -1489,36 +1559,21 @@ void
|
||||
BTSpawnDamageEffect(owner, d->effectResource, // explosion AT the zone's
|
||||
zone->segmentIndex); // segment (world position)
|
||||
}
|
||||
zone->ApplyDamageGraphicState(d->graphicState); // destroyed skin (graphic state)
|
||||
// A graphic-state change means the segment's MODEL changed (intact ->
|
||||
// destroyed variant, keyed by GetVideoObjectName(skl, gstate)). Fire
|
||||
// the engine's model-rebuild flag so the renderer re-runs
|
||||
// MakeMechRenderables and loads the destroyed segment geometry -- the
|
||||
// same mechanism CulturalIcons use to visibly deform on damage
|
||||
// (CULTURAL.cpp:91; EXPTBL.cpp:549 does this per zone in the engine's
|
||||
// ExplosionTable). Without this the tree stays as built at spawn (intact).
|
||||
// The render's "RemakeEntity" state (which consumes this flag) was
|
||||
// NOT ported -- the mech tree is built once at spawn -- so we drive the
|
||||
// equivalent directly: set the flag (authentic signal; also flags the
|
||||
// zone for net damage replication) AND call the render bridge, which
|
||||
// swaps the wrecked segment mesh onto the already-built tree in place.
|
||||
// The single-threaded frame loop makes the immediate swap safe.
|
||||
if (d->graphicState != DamageZone::ExistsGraphicState)
|
||||
{
|
||||
// Master-gated like the binary's gstate branch (@0042aa2c:
|
||||
// (entity+0x28 & 0xc) == 0 guards the |= 2); the mesh swap
|
||||
// runs on EVERY instance -- an observer node executes this
|
||||
// same path when the replicated level/gstate lands.
|
||||
if (owner->GetInstance() == Entity::MasterInstance)
|
||||
owner->ForceUpdate(Entity::DamageZoneUpdateModelFlag);
|
||||
BTRemakeMechModel(owner); // RemakeEntity: swap in the destroyed mesh
|
||||
}
|
||||
if (getenv("BT_DEATH_LOG"))
|
||||
DEBUG_STREAM << "[deathfx] zone " << i << " level " << level
|
||||
<< " -> effect " << d->effectResource << " gstate " << d->graphicState
|
||||
<< " -> effect " << d->effectResource
|
||||
<< " (zone gstate " << (int)zone->GetGraphicState() << ")"
|
||||
<< "\n" << std::flush;
|
||||
}
|
||||
}
|
||||
|
||||
// #125: ONE remake per tick, after every zone's state has settled --
|
||||
// RemakeEntityRenderables walks ALL segments and re-picks each by its
|
||||
// zone's LIVE gstate, so a single call applies the destroying zone and
|
||||
// its whole cascade (children Gone, siblings) together, and the render
|
||||
// tree's own segGState diff makes repeats free.
|
||||
if (needRemake)
|
||||
BTRemakeMechModel(owner);
|
||||
}
|
||||
|
||||
//#############################################################################
|
||||
|
||||
@@ -372,6 +372,17 @@ class MechDeathHandler
|
||||
private:
|
||||
Mech *owner; // @0x14
|
||||
std::vector<Scalar> lastLevel; // @0x10 per-zone last damageLevel cache
|
||||
// PORT (#125): per-zone last GRAPHIC STATE cache. The binary's
|
||||
// renderer re-picked every segment's mesh from its zone's gstate PER
|
||||
// FRAME, so any gstate write (descriptor apply, the destruction
|
||||
// cascade's child/sibling Gone marks, a replicated record) showed the
|
||||
// next frame with no event plumbing. The port's built-once render
|
||||
// tree needs the event: Tick diffs each zone's gstate against this
|
||||
// cache and fires ONE coalesced RemakeEntity when anything moved --
|
||||
// the port equivalent of the per-frame re-pick, independent of the
|
||||
// descriptor tables (whose ForLevel lookup returns NULL past the last
|
||||
// threshold -- the #125 first-destroying-hit gap).
|
||||
std::vector<int> lastGState;
|
||||
// PORT (respawn replication): last-tick MovementMode, so a REPLICANT
|
||||
// observer can reverse the one-way wreck swap on the DEATH-STATE EXIT
|
||||
// edge (9/2 -> 0/1, only Mech::Reset ever leaves the death modes). The
|
||||
|
||||
@@ -100,3 +100,17 @@ turn-stop MID-PIVOT park (authentic, pod-invisible: no external self-view existe
|
||||
bench D reproduced it at trn legFrm=10 + twist rescue. Peer brake-rate select restored
|
||||
in the replicant motion path (IntegrateMotion:298 is dead code -- Simulate bypassed).
|
||||
Final build: 20 benign LNK2019s exact.
|
||||
NIGHT18 (cont): #125 FIXED + 2-node benched. Root pair: (1) the Tick transcription
|
||||
carried an INVENTED write -- ApplyDamageGraphicState(descriptor) -- the raw @0042aa2c
|
||||
(part_003.c:10963-93) only PICKS an effect (level flag -> ForLevel; gstate flag ->
|
||||
ForGraphicState(CURRENT gstate), overwriting); the hardcoded-Destroyed pick stomped
|
||||
every cascade child's authored Gone(2) -> 1 master-side pre-send: the gun pod NEVER
|
||||
dropped off a killed arm on any node (gotcha 36). (2) the mesh swap was descriptor-
|
||||
gated; now per-zone gstate caches fire ONE coalesced RemakeEntity on any edge (the
|
||||
binary re-picked meshes from gstate per frame). Restored authored story: killed gun
|
||||
= wrecked thrdrgun mesh hangs; killed arm = wrecked thrdrarm (exterior skel) + pod
|
||||
VANISHES -- peers see all of it on the FIRST beat ([zone-repl] dz_rgun gstate 0->2 +
|
||||
peer remake, mp_zone125.sh fix/legacy receipts). Heal edges fire no effects (Exists
|
||||
suppression; binary Reset writes raw). Respawn regression clean (0 heal puffs).
|
||||
SUNDAY build now: turn rates + #155 banks + #49 radar + #102 brake + #125 zone edge.
|
||||
Handout candidates: 'arms now visibly wreck + pods drop off on everyone's screen'.
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
#!/usr/bin/env bash
|
||||
# =========================================================================
|
||||
# ISSUE #125 -- peers' first destroyed-zone graphic (2-node, thor)
|
||||
# A (master, thor) self-destroys dz_rarm with ONE 120-pt hit at t=25.
|
||||
# B observes. PASS receipts on B (the peer):
|
||||
# [zone-repl] zone 10 'dz_rarm' ... gstate 0->1 (arm Destroyed arrives)
|
||||
# [zone-repl] zone 17 'dz_rgun' ... gstate 0->2 (pod GONE arrives -- the
|
||||
# pre-fix master STOMPED this to 1 before the record shipped)
|
||||
# [BTrender] seg 'jointrgun' slot 6 -> gstate 2 (peer hides the pod)
|
||||
# [BTrender] RemakeEntity: ... entity=<A's mech> (the peer-side remake
|
||||
# fired on the FIRST beat -- the pre-fix peer had no trigger at all
|
||||
# when descriptors misfired, and could never see gstate 2)
|
||||
# usage: mp_zone125.sh <tag>
|
||||
# =========================================================================
|
||||
set -x
|
||||
cd /c/git/bt411/content || exit 1
|
||||
taskkill //F //IM btl4.exe > /dev/null 2>&1
|
||||
sleep 2
|
||||
TAG=${1:-fix}
|
||||
rm -f z125_a_${TAG}.log z125_b_${TAG}.log
|
||||
sed 's/^experience=.*/experience=expert/' MP.EGG > Z125.EGG
|
||||
sed -i "s/^map=.*/map=grass/; s/^time=.*/time=day/; 0,/^vehicle=.*/s//vehicle=thor/" Z125.EGG
|
||||
|
||||
BT_PLATFORM=glass BT_START_INSIDE=1 BT_DEV_GAUGES=1 \
|
||||
BT_LOG=z125_b_${TAG}.log BT_AFFINITY=0x0C BT_MP_LOG=1 BT_DEATH_LOG=1 \
|
||||
../build/Release/btl4.exe -egg Z125.EGG -net 1601 &
|
||||
sleep 2
|
||||
BT_PLATFORM=glass BT_START_INSIDE=1 BT_DEV_GAUGES=1 \
|
||||
BT_LOG=z125_a_${TAG}.log BT_AFFINITY=0x03 BT_MP_LOG=1 BT_DEATH_LOG=1 BT_DMG_LOG=1 \
|
||||
BT_SELF_DAMAGE=120 BT_SELF_DAMAGE_ZONE=dz_rarm BT_SELF_DAMAGE_DELAY=25 BT_SELF_DAMAGE_TICKS=1 \
|
||||
../build/Release/btl4.exe -egg Z125.EGG -net 1501 &
|
||||
sleep 4
|
||||
python ../tools/btconsole.py Z125.EGG 127.0.0.1:1501 127.0.0.1:1601 &
|
||||
PC=$!
|
||||
|
||||
sleep 75
|
||||
kill $PC 2>/dev/null
|
||||
taskkill //F //IM btl4.exe > /dev/null 2>&1
|
||||
echo "=== A (master) ==="
|
||||
grep -a "dmghit\|cascade\|RemakeEntity\|seg 'jointr" z125_a_${TAG}.log | head -12
|
||||
echo "=== B (peer) ==="
|
||||
grep -a "zone-repl\|RemakeEntity\|seg 'jointr\|deathfx\] zone 1[07] level" z125_b_${TAG}.log | head -16
|
||||
@@ -0,0 +1,27 @@
|
||||
#!/usr/bin/env bash
|
||||
# =========================================================================
|
||||
# ISSUE #125 -- authored zone-descriptor dump (solo, any chassis)
|
||||
# usage: zonedump.sh <vehicle>
|
||||
# Prints each zone's [zone-armor] row + the full [deathfx] descriptor
|
||||
# table (lvl/fx/gs/dly per entry) at load. The #125 question: which zones
|
||||
# carry a gs=1 (Destroyed) entry -- the ONLY trigger the old remake had.
|
||||
# =========================================================================
|
||||
set -x
|
||||
. /c/git/bt411/scratchpad/night6/bench_common.sh
|
||||
cd /c/git/bt411/content || exit 1
|
||||
taskkill //F //IM btl4.exe > /dev/null 2>&1
|
||||
sleep 2
|
||||
|
||||
VEH=${1:-thor}
|
||||
bt_expert_egg MP.EGG ZD125.EGG
|
||||
sed -i "s/^map=.*/map=grass/; s/^time=.*/time=day/; 0,/^vehicle=.*/s//vehicle=$VEH/" ZD125.EGG
|
||||
LOG=zonedump_${VEH}.log
|
||||
rm -f "$LOG"
|
||||
( export BT_DEATH_LOG=1 BT_DMG_LOG=1
|
||||
bt_launch "$LOG" ZD125.EGG 0x03 )
|
||||
sleep 30
|
||||
bt_kill_ours
|
||||
sleep 2
|
||||
taskkill //F //IM btl4.exe > /dev/null 2>&1
|
||||
echo "--- $LOG"
|
||||
grep -ac "deathfx" "$LOG" || true
|
||||
Reference in New Issue
Block a user