BT410 5.3.68: THE LIVE-RIO FAULT IS FIXED -- it and the TM crash were one defect
The [map] audit named it in one run: subsystem 17 = Torso, attribute ids 12/13 unresolved. The donor's decompiled torso.hpp carries the authentic enum with binary offsets -- ids 3..15, including StickPosition(9), TorsoUp/Down/ Left/Right(10-13), TorsoCenter(14), MotionState(15). Our table stopped at seven entries with MotionState at id 9, on the strength of a comment claiming the rest were messages. The streamed control mappings bind BY ID as direct WRITE destinations, so the truncation produced two different crashes from one cause: in TM mode the button ids 12/13 resolved NULL (the boot write-fault the moment the joystick polled), and in RIO mode the analog id 9 resolved to our motionState StateIndicator -- every analog packet from a live vRIO wrote raw floats over a watcher-socketed object, and the corrupted chains walked into unmapped memory ~30-40s later. That was the 'intermittent' pod fault at the fixed DPMI-host address. vRIO down = no analog = no corruption, which is why the norio conf launched: every observation from the whole hunt drops out of this mechanism. Fix: the full 13-entry donor table; five int command members carved from the dynamicsState reserve (class size unchanged); ctor zeros them. Verified, two agreeing runs each way: TM mode launches with a clean audit and the Thrustmaster joystick driving the torso (stickY=0.907 -> torsoElev 0.349); RIO mode with vRIO STREAMING launches, zero faults, weapons cycling. pod_render_rec is no longer poisoned and the norio workaround is obsolete. The audit-before-the-archive-call pattern (BT_MAP_LOG walks the same streamed table CreateStreamedMappings consumes, naming what will not resolve) turned a two-day intermittent-corruption hunt into a one-run lookup. The streamed RES tables are binding contracts on our attribute enums. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,197 @@
|
||||
# BT 4.10 — the damage model, end to end
|
||||
|
||||
How a shot becomes lost armor, dead equipment, and a mech kill. Synthesized
|
||||
from the surviving 1995 engine source (`CODE/RP/MUNGA/`), the reconstructed
|
||||
BT-side TUs (`restoration/source410/BT/`), and the BT411 binary reversal.
|
||||
Per-TU depth lives in the sibling `*.NOTES.md` files; this doc is the
|
||||
cross-cutting flow.
|
||||
|
||||
```
|
||||
weapon fire ──► Damage record ──► TakeDamageMessage ──► victim handler
|
||||
│ (unaimed? cylinder table)
|
||||
▼
|
||||
damageZones[zone]->TakeDamage
|
||||
│
|
||||
armor economy: level += amount × scale[type]
|
||||
│
|
||||
┌──────────────────┬────────────────────────┤
|
||||
▼ ▼ ▼
|
||||
level ≥ 1.0 Energy special level < 1.0
|
||||
zone destroyed (generator short) (leg ≥ 0.5 → limp)
|
||||
│
|
||||
┌──────────┼──────────────┐
|
||||
▼ ▼ ▼
|
||||
VITAL LEG other zone
|
||||
mech kill mech down SendSubsystemDamage (crit allotments → equipment)
|
||||
+ RecurseSegmentTable (SIBS / DESCEND)
|
||||
```
|
||||
|
||||
## 1. The Damage record (engine: `MUNGA/DAMAGE.HPP`)
|
||||
|
||||
Every hit travels as one `Damage` struct:
|
||||
|
||||
| field | meaning |
|
||||
|---|---|
|
||||
| `damageType` | 0 Collision · 1 Ballistic · 2 Explosive · 3 Laser · 4 Energy |
|
||||
| `damageAmount` | points (the only field in the armor formula) |
|
||||
| `damageForce` | impulse vector (physics/feel, not armor) |
|
||||
| `surfaceNormal`, `impactPoint` | world-space impact geometry |
|
||||
| `burstCount` | "times to apply" — **NOT in the armor formula**; one message = one application. Feeds splash falloff and the gyro bounce only. |
|
||||
|
||||
## 2. Producers — where Damage records are born
|
||||
|
||||
**Beam weapons** (`EMITTER.CPP`, PPC/lasers): instant-hit at the owner's
|
||||
current target when `rangeToTarget <= effectiveRange`. The discharge energy
|
||||
splits by the authored ratio `damageFraction = dmg/(dmg+heat)`; the delivered
|
||||
amount scales with charge: `damagePortion = authored × chargeRatio²` (an
|
||||
undercharged PPC hits soft). The heat portion goes into the FIRER's own heat
|
||||
sinks. Delivery = `MechWeapon::SendDamage`.
|
||||
|
||||
**Ballistic / missile weapons** (`PROJWEAP.CPP`, `MISLANCH.CPP`,
|
||||
`MISSILE.CPP`): the weapon FSM's Loaded case pulls ammo (`FeedAmmo`), checks
|
||||
jam, then `FireWeapon` spawns ONE cluster `Missile` entity per salvo
|
||||
(salvo-split `damageAmount`, `burstCount` = missile count). The round flies
|
||||
guided (seeker + thruster); the proximity fuse delivers the whole record
|
||||
once, `impactPoint` = the round's world position, zone = −1 (unaimed).
|
||||
|
||||
**Explosions / splash** (`MUNGA/EXPLODE.CPP`): a boxed splash volume gathers
|
||||
movers + cultural objects sorted by distance; each target gets
|
||||
`burstCount = original / r^1.2` (min 1), force along the radius vector,
|
||||
zone = −1.
|
||||
|
||||
**Collisions** (`MUNGA/MOVER.CPP` `ProcessCollisionList`): type Collision,
|
||||
amount computed by the bounce resolver (`StaticBounce` from velocity,
|
||||
elasticity, friction), impact point from the collision slice. Multiple
|
||||
same-frame collisions are averaged, damage summed.
|
||||
|
||||
## 3. Delivery — `Entity::TakeDamageMessage` (`ENTITY3.HPP`)
|
||||
|
||||
Fields: `inflictingEntity`, `damageZone` (+`invalidDamageZone` = zone < 0),
|
||||
`damageData`, `inflictingSubsystemID` (so the BT message manager can bundle
|
||||
explosion resource IDs). Dispatched AT the victim entity.
|
||||
|
||||
The authored contract (ENTITY3.HPP warning): **only reticle-based (aimed)
|
||||
weapons carry a valid zone**. Everything else — missiles, splash, rams —
|
||||
arrives zone = −1 and must be resolved by the victim.
|
||||
|
||||
The attacker also posts a `ScoreInflicted` message to its own player per
|
||||
delivered hit (the damage score).
|
||||
|
||||
## 4. Victim routing — `Mech::TakeDamageMessageHandler` (`MECH.CPP`)
|
||||
|
||||
Binary hub @004a0230, in order:
|
||||
|
||||
1. Feed the RAW record to the gyro (cockpit bounce — even an invalid-zone
|
||||
hit shakes the pilot). *(staged: feel wave)*
|
||||
2. Latch `lastInflictingID` — keys the LOD damage-clustering below.
|
||||
3. **Unaimed resolve**: if `invalidDamageZone`, map `impactPoint` through the
|
||||
cylinder hit-location table (`DMGTABLE.CPP`, type-29 resource, cached
|
||||
mech+0x444): world → mech-local; local height picks a ROW (feet rows are
|
||||
chassis-fixed, upper rows add the LIVE torso twist to the impact angle);
|
||||
`atan2(z,x)` picks the angular CELL; a uniform roll walks the cell's
|
||||
cumulative distribution (the BattleTech dice scatter) → hull zone.
|
||||
4. Chain to `Entity::TakeDamageMessageHandler`: zone −1 is DROPPED (base
|
||||
contract), otherwise `damageZones[zone]->TakeDamage(damageData)`.
|
||||
|
||||
## 5. The hull zone — `Mech__DamageZone::TakeDamage` (`MECHDMG.CPP`)
|
||||
|
||||
**Artifact (LOD) zones** — a zone with a non-empty redirect table is a
|
||||
low-LOD hull shell: it never takes damage itself, it routes to a real child
|
||||
zone. Same-attacker clustering: within 0.25 s the SAME child is hit again;
|
||||
past 10 s re-roll; in between, 33 % reuse. The artifact's displayed level =
|
||||
mean of its children.
|
||||
|
||||
**Real zones — the armor economy**:
|
||||
|
||||
damageLevel += damageAmount × damageScale[damageType] clamp [0,1]
|
||||
|
||||
Authoring streams armor POINTS per type; the ctor normalizes
|
||||
`scale[type] = 1/(points[type] × armorPoints)`, so 1.0 = the zone's full
|
||||
point budget spent. **Leg zones halve every scale** (legs effectively carry
|
||||
double points). 1.0 → BurningState + DestroyedGraphicState.
|
||||
|
||||
**Per-hit specials**: an Energy hit on a zone with critical subsystems rolls
|
||||
ONE of them; if the pick is a Generator it is force-shorted (screens
|
||||
flicker, weapons drop dead until recovery; novice cockpits exempt).
|
||||
|
||||
**State outcomes** by the new level:
|
||||
|
||||
| condition | result |
|
||||
|---|---|
|
||||
| VITAL zone hits 1.0 | mech kill (`statusAlarm` 9) |
|
||||
| leg zone hits 1.0 | fall → mech kill |
|
||||
| leg zone ≥ 0.5 | limp gait graphic (left 3 / right 4) |
|
||||
| non-leg, non-vital hits 1.0 | destruction descent (below) |
|
||||
|
||||
## 6. Criticals and equipment — `MECHDMG.CPP` + `MECHSUB.CPP`
|
||||
|
||||
Each hull zone streams a critical table: `{weight, damagePercentage
|
||||
allotment, roster subsystem index}` per entry (plug binding is
|
||||
master-authoritative — replicants never bind).
|
||||
|
||||
Every subsystem owns a PRIVATE `DamageZone` (index 0, not in the hull
|
||||
array) with its own points + per-type scales (`structureReference` +
|
||||
`armorByFacing[5]`, same normalization rule). That's what makes criticals
|
||||
measurable.
|
||||
|
||||
- **`CriticalHit`** (aimed/critical fire; not yet on the weapon path —
|
||||
reticle wave): HALF the damage (cap 1.0) is carved off as the critical
|
||||
bite, applied to ONE weight-rolled crit subsystem via
|
||||
`ApplyDamageAndMeasure`, charged against that entry's allotment; the
|
||||
remainder runs the normal zone armor model.
|
||||
- **Zone death → `SendSubsystemDamage`**: pins the zone at 1.0 and pushes
|
||||
each entry's UNUSED allotment into its subsystem's private zone. A
|
||||
subsystem at 1.0 → `ForceCriticalFailure`: alarm level 1 (Destroyed),
|
||||
`SetSimulationState(DestroyedState)` — the hard gate every weapon FSM
|
||||
polls, so destroyed weapons fall silent; a VITAL subsystem kills the mech.
|
||||
Repeat hits on a dead zone re-run the push (binary-authentic) — contained
|
||||
equipment keeps degrading under continued fire.
|
||||
|
||||
## 7. The destruction cascade — `RecurseSegmentTable`
|
||||
|
||||
A destroyed zone walks the skeleton by its streamed flags:
|
||||
|
||||
- **SIBS** (`destroySiblingsOnDestruction`): destroy the other zones on the
|
||||
same segment.
|
||||
- **DESCEND** (`descendOnDestruction`): destroy every zone on the child
|
||||
segments, recursing.
|
||||
|
||||
Fight-verified chain: arm zone dies → SIBS kills the searchlight zone (its
|
||||
Searchlight/ThermalSight crits destroyed) → DESCEND kills the gun zone →
|
||||
PPC_2 + ERMLaser_2 + Condenser6 destroyed and STOP FIRING, while the
|
||||
untouched PPC_1 keeps shooting.
|
||||
|
||||
## 8. Data authoring (the resource chain)
|
||||
|
||||
- **Type-20 DamageZoneStream** (per mech): zone count, then per zone the
|
||||
engine record (name, 5 effect-site segment lists, `defaultArmorPoints`,
|
||||
`damageScale[5]`, material lists) + the BT tail (descend/sibs flags,
|
||||
`segmentIndex`, leftLeg/rightLeg/vital, crit table, LOD redirect table).
|
||||
Authored in `.dmg` notation files: `WeaponDamagePoints` default with
|
||||
`Collision/Ballistic/Explosive/Laser/EnergyDamagePoints` overrides
|
||||
(stored as 1/points).
|
||||
- **Type-29 DamageLookupTableStream**: the cylinder table (rows ×
|
||||
angular cells × cumulative zone distributions; 18 tables shipped).
|
||||
- **Weapon subsystem resources**: `damageAmount`, `damageType`,
|
||||
`heatCostToFire`, range, recharge.
|
||||
- Reference magnitudes (bhk1): legs 70 pts, upper torso 124, searchlights
|
||||
25; PPC ~12 (Energy), ER-M laser 3.43 (Laser), SRM 5.83 × 6 (Explosive).
|
||||
|
||||
## 9. Replication and respawn
|
||||
|
||||
- Criticals resolve on the MASTER instance only; `DamageZone` state
|
||||
replicates via update records (`damageLevel`, zone state, graphic state,
|
||||
changed flags). `SubsystemMessageManager` consolidates per-frame damage
|
||||
and bundles explosion resources (not yet reconstructed).
|
||||
- `Mech::Reset` (respawn) heals every hull zone and DeathResets the roster,
|
||||
but crit `damagePercentageUsed` PERSISTS across lives — spent crit
|
||||
budgets stay spent (binary-authentic).
|
||||
|
||||
## 10. Known deltas from the 1995 binary (staged)
|
||||
|
||||
- Aimed fire: beam `SendDamage` currently rolls a uniform random hull zone;
|
||||
authentic = the reticle hit. `CriticalHit` is unwired for the same reason.
|
||||
- Cylinder height = 10.0 constant (binary reads the collision cylinder).
|
||||
- Leg-branch gates use "not already destroyed" instead of the live
|
||||
MovementMode/IsDisabled checks (gait FSM pending).
|
||||
- Gyro hit-feed and destroyed-skin graphics are log stubs (feel/render waves).
|
||||
@@ -29,6 +29,20 @@ Derivation
|
||||
"Torso"
|
||||
);
|
||||
|
||||
//
|
||||
// The full 13-entry authentic table (ids 3..15) -- donor torso.cpp:152 with
|
||||
// binary offsets, cross-confirmed by BTL4.RES's streamed control mappings
|
||||
// binding ids 12/13 on subsystem 17. The five command members and
|
||||
// analogTwistAxis are DIRECT WRITE DESTINATIONS for device polls
|
||||
// (L4CTRL.CPP:1936), which is why every id from 9 up must exist and be the
|
||||
// right kind of storage: id 9 short of this table landed the RIO's analog
|
||||
// stick on the motionState StateIndicator.
|
||||
//
|
||||
// SpeedOf* still point at the base rates rather than the donor's live
|
||||
// velocity accumulators (elevationVelocity/twistVelocity @0x1EC/0x1E8) --
|
||||
// those members arrive with TorsoSimulation; the authored audio watcher on
|
||||
// SpeedOfTorsoHorizontal just reads a constant until then.
|
||||
//
|
||||
const Torso::IndexEntry
|
||||
Torso::AttributePointers[]=
|
||||
{
|
||||
@@ -38,6 +52,12 @@ const Torso::IndexEntry
|
||||
ATTRIBUTE_ENTRY(Torso, HorizontalLimitLeft, horizontalLimitLeft),
|
||||
ATTRIBUTE_ENTRY(Torso, SpeedOfTorsoVertical, baseElevationRate),
|
||||
ATTRIBUTE_ENTRY(Torso, SpeedOfTorsoHorizontal, baseTwistRate),
|
||||
ATTRIBUTE_ENTRY(Torso, StickPosition, analogTwistAxis),
|
||||
ATTRIBUTE_ENTRY(Torso, TorsoUp, elevateUpCommand),
|
||||
ATTRIBUTE_ENTRY(Torso, TorsoDown, elevateDownCommand),
|
||||
ATTRIBUTE_ENTRY(Torso, TorsoLeft, twistLeftCommand),
|
||||
ATTRIBUTE_ENTRY(Torso, TorsoRight, twistRightCommand),
|
||||
ATTRIBUTE_ENTRY(Torso, TorsoCenter, centerCommand),
|
||||
ATTRIBUTE_ENTRY(Torso, MotionState, motionState)
|
||||
};
|
||||
|
||||
@@ -89,6 +109,16 @@ Torso::Torso(
|
||||
buttonRamp = 0.0f;
|
||||
horizontalEnabled = r->torsoHorizontalEnabled;
|
||||
|
||||
//
|
||||
// The device-poll write destinations (published ids 9..14). Zero =
|
||||
// stick centred, no button held -- their state before the first poll.
|
||||
//
|
||||
elevateUpCommand = 0;
|
||||
elevateDownCommand = 0;
|
||||
twistLeftCommand = 0;
|
||||
twistRightCommand = 0;
|
||||
centerCommand = 0;
|
||||
|
||||
//
|
||||
// Resolve the named torso-twist skeleton joints (twist body + shadow) from
|
||||
// the mech skeleton (now live). A mech with a fixed torso, or one whose
|
||||
|
||||
@@ -108,12 +108,24 @@
|
||||
// HorizontalLimitRight HorizontalLimitLeft
|
||||
// SpeedOfTorsoVertical SpeedOfTorsoHorizontal
|
||||
//
|
||||
// (the TorsoUp/Down/Left/Right/Center that follow are MESSAGES, not
|
||||
// attributes). All six map onto members we already carry. An
|
||||
// authored AttributeWatcher binds SpeedOfTorsoHorizontal.
|
||||
// The FULL authentic table, ids 3..15, confirmed twice over: the BT411
|
||||
// donor's decompiled enum carries these names WITH binary member offsets
|
||||
// (torso.hpp 111-124, @0x1E4..@0x20C), and BTL4.RES's own streamed
|
||||
// control mappings bind ids 12/13 as DIRECT button destinations on
|
||||
// subsystem 17 = Torso (the [map] audit).
|
||||
//
|
||||
// An earlier revision declared "TorsoUp/Down/Left/Right/Center are
|
||||
// MESSAGES, not attributes" and stopped at 7 entries with MotionState at
|
||||
// id 9. That was WRONG in a specifically nasty way: the ids are the
|
||||
// binding key for the streamed mappings, so Thrustmaster's buttons
|
||||
// (12/13) resolved NULL -- the boot write-fault at
|
||||
// ControlsInstanceDirectOf<int>::Update+0xE -- and the RIO's ANALOG
|
||||
// StickPosition (9) resolved to our motionState StateIndicator, letting
|
||||
// every analog update from a live RIO smash a watcher-socketed object
|
||||
// with raw floats.
|
||||
//
|
||||
// The chain -- PowerWatcher -> HeatWatcher -> MechSubsystem -- publishes
|
||||
// nothing of its own, so these start at Subsystem::NextAttributeID.
|
||||
// nothing of its own, so these start at MechSubsystem::NextAttributeID.
|
||||
//
|
||||
public:
|
||||
enum {
|
||||
@@ -123,11 +135,12 @@
|
||||
HorizontalLimitLeftAttributeID,
|
||||
SpeedOfTorsoVerticalAttributeID,
|
||||
SpeedOfTorsoHorizontalAttributeID,
|
||||
//
|
||||
// Bound by an authored watcher (BTL4.RES pairs it as
|
||||
// Torso/MotionState). Appended at the END of this leaf class's
|
||||
// own range, so no downstream id moves.
|
||||
//
|
||||
StickPositionAttributeID,
|
||||
TorsoUpAttributeID,
|
||||
TorsoDownAttributeID,
|
||||
TorsoLeftAttributeID,
|
||||
TorsoRightAttributeID,
|
||||
TorsoCenterAttributeID,
|
||||
MotionStateAttributeID,
|
||||
NextAttributeID
|
||||
};
|
||||
@@ -169,10 +182,23 @@
|
||||
//
|
||||
StateIndicator motionState; // authored watcher
|
||||
//
|
||||
// The five BUTTON COMMAND destinations (donor torso.hpp @0x1F8-0x208).
|
||||
// The streamed control mappings bind these BY ID as DIRECT WRITE
|
||||
// targets -- LBE4ControlsManager::CreateStreamedMappings registers
|
||||
// GetAttributePointer(attributeID) as the destination a device poll
|
||||
// writes through (L4CTRL.CPP:1936). Carved from the front of the
|
||||
// dynamicsState reserve so the class size is unchanged.
|
||||
//
|
||||
int elevateUpCommand;
|
||||
int elevateDownCommand;
|
||||
int twistLeftCommand;
|
||||
int twistRightCommand;
|
||||
int centerCommand;
|
||||
//
|
||||
// Twist/elevation dynamics accumulators advanced by TorsoSimulation.
|
||||
// Reserved until the per-frame sim is reconstructed (phase 5).
|
||||
//
|
||||
Scalar dynamicsState[22];
|
||||
Scalar dynamicsState[17];
|
||||
};
|
||||
|
||||
#endif
|
||||
|
||||
@@ -434,6 +434,76 @@ Entity*
|
||||
if (description)
|
||||
{
|
||||
description->Lock();
|
||||
|
||||
//
|
||||
// MAPPING AUDIT (env BT_MAP_LOG) -- walk the same streamed
|
||||
// table CreateStreamedMappings is about to consume and name
|
||||
// every DirectMapping whose attribute does not resolve.
|
||||
//
|
||||
// For a DirectMapping the archive code registers
|
||||
// subsystem->GetAttributePointer(attributeID) as the WRITE
|
||||
// DESTINATION of a controls instance; an attribute our
|
||||
// reconstruction has not published resolves NULL, and the
|
||||
// first device poll that fires the control writes through
|
||||
// it -- the Thrustmaster-mode boot crash at
|
||||
// ControlsInstanceDirectOf<int>::Update+0xE (write to
|
||||
// [eax]=NULL, guest 00485E1A). RIO mode never trips it only
|
||||
// because the RIO's analog stream arrives by a different
|
||||
// path. This audit turns that crash into a named list of
|
||||
// missing attribute publications.
|
||||
//
|
||||
if (getenv("BT_MAP_LOG"))
|
||||
{
|
||||
const int
|
||||
*words = (const int *)description->resourceAddress;
|
||||
int
|
||||
map_count = *words;
|
||||
const ControlsMapping
|
||||
*map_entry = (const ControlsMapping *)(words + 1);
|
||||
int
|
||||
map_index;
|
||||
|
||||
DEBUG_STREAM << "[map] " << map_count
|
||||
<< " streamed mappings for '" << primary_mapping_name
|
||||
<< "'\n" << flush;
|
||||
for (map_index = 0; map_index < map_count; map_index++)
|
||||
{
|
||||
Simulation
|
||||
*map_subsystem =
|
||||
viewing_entity->GetSimulation(
|
||||
map_entry[map_index].subsystemID);
|
||||
void
|
||||
*map_attribute = NULL;
|
||||
|
||||
if (
|
||||
map_subsystem != NULL &&
|
||||
map_entry[map_index].mappingType
|
||||
== ControlsMapping::DirectMapping
|
||||
)
|
||||
{
|
||||
map_attribute =
|
||||
map_subsystem->GetAttributePointer(
|
||||
map_entry[map_index].attributeID);
|
||||
}
|
||||
if (
|
||||
map_entry[map_index].mappingType
|
||||
== ControlsMapping::DirectMapping &&
|
||||
(map_subsystem == NULL || map_attribute == NULL)
|
||||
)
|
||||
{
|
||||
DEBUG_STREAM << "[map] BAD direct: idx="
|
||||
<< map_index
|
||||
<< " group=" << (int)map_entry[map_index].controlsGroup
|
||||
<< " elem=" << (int)map_entry[map_index].controlsElement
|
||||
<< " subsysID=" << (int)map_entry[map_index].subsystemID
|
||||
<< " attrID=" << (int)map_entry[map_index].attributeID
|
||||
<< (map_subsystem == NULL
|
||||
? " (NO SUBSYSTEM)" : " (attr NULL)")
|
||||
<< "\n" << flush;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
GetControlsManager()->CreateStreamedMappings(
|
||||
viewing_entity,
|
||||
(int*)description->resourceAddress
|
||||
|
||||
@@ -1393,3 +1393,57 @@ TOOLING: pose_probe.py replaces the throwaway harness -- updir=+1 default
|
||||
pose_chase_upfixed.png shows the MadCat right side up -- chicken-walker legs,
|
||||
purple knee actuators, feet planted, shadow quad UNDER the feet, weapon-pod
|
||||
arms. The milestone stands, now with the correct picture.
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
THE LIVE-RIO FAULT AND THE TM BOOT CRASH WERE ONE DEFECT: THE TORSO TABLE
|
||||
--------------------------------------------------------------------------------
|
||||
The [map] audit (BT_MAP_LOG in BTL4APP.CPP, printed just before the archive's
|
||||
CreateStreamedMappings consumes the streamed table) named it in one run:
|
||||
|
||||
[map] BAD direct: idx=1 group=3 elem=68 subsysID=17 attrID=12 (attr NULL)
|
||||
[map] BAD direct: idx=2 group=3 elem=67 subsysID=17 attrID=13 (attr NULL)
|
||||
|
||||
Subsystem 17 = Torso (the [roster] dump). The donor's decompiled torso.hpp
|
||||
carries the authentic enum WITH binary offsets: ids 3..15 =
|
||||
RotationOfTorsoVertical..SpeedOfTorsoHorizontal (3-8), then StickPosition(9),
|
||||
TorsoUp(10), TorsoDown(11), TorsoLeft(12), TorsoRight(13), TorsoCenter(14),
|
||||
MotionState(15). Our table had SEVEN entries with MotionState at id 9, on
|
||||
the strength of a comment that said "TorsoUp/Down/etc are MESSAGES, not
|
||||
attributes". Wrong -- and wrong in a specifically nasty way, because the
|
||||
streamed control mappings bind BY ID as DIRECT WRITE DESTINATIONS
|
||||
(L4CTRL.CPP:1936 registers GetAttributePointer(attributeID) as the pointer a
|
||||
device poll writes through):
|
||||
|
||||
* TM mode: button ids 12/13 resolved NULL -> the boot write-fault at
|
||||
ControlsInstanceDirectOf<int>::Update+0xE the moment the PC joystick
|
||||
polled. Instant, clean, easy.
|
||||
* RIO mode: analog id 9 (StickPosition) resolved to our motionState
|
||||
STATEINDICATOR -- so every analog packet from a live vRIO wrote raw
|
||||
floats over a watcher-socketed object. Corrupted socket chains walked
|
||||
into unmapped memory ~30-40s later: the "intermittent" pod fault at the
|
||||
fixed DPMI-host address, the one that ate two days. vRIO down = no
|
||||
analog stream = no corruption = mission runs. Every observation from
|
||||
the whole hunt drops out of this one mechanism.
|
||||
|
||||
FIX: Torso publishes the full 13-entry donor table; the five int command
|
||||
members are carved from the front of the dynamicsState reserve (class size
|
||||
unchanged); ctor zeros them. SpeedOf* still point at base rates until
|
||||
TorsoSimulation brings the live velocity accumulators.
|
||||
|
||||
VERIFIED, two agreeing runs each way:
|
||||
* TM mode: launches, [map] audit clean, and the Thrustmaster joystick
|
||||
DRIVES the torso -- stickY=0.907 -> torsoElev/eyePitch=0.349. The
|
||||
non-RIO controls path lives.
|
||||
* RIO mode with vRIO STREAMING: launches, zero faults, mission runs deep
|
||||
enough that weapons cycle. pod_render_rec is no longer poisoned; the
|
||||
norio workaround is obsolete.
|
||||
|
||||
Incidentally the [euler] probe now prints during missions with sane
|
||||
stack-local pointers -- EulerAngles::operator=(const LinearMatrix&) was
|
||||
always innocent, now visibly so.
|
||||
|
||||
METHOD NOTE for the file: the audit-before-the-archive-call pattern (walk the
|
||||
same streamed table our code hands to archive code, name what will not
|
||||
resolve) turned a two-day intermittent heap-corruption hunt into a one-run
|
||||
lookup. The streamed RES tables are BINDING CONTRACTS on our attribute
|
||||
enums; every subsystem the RES maps should get the same audit treatment.
|
||||
|
||||
Reference in New Issue
Block a user