MP: authentic update-record deadbands streamed from the model record (task #3)

Binary ctor @0x4a26a7-0x4a26ef: mech+0x768/76c/770 <- raw record words
0x26/0x27/0x28 (UpdatePositionDiffrence / UpdateTurnVelocityDiffrence /
UpdateTurnDegreeDiffrence x pi/180 [const @0x4a2d44]).  Read at RAW record
offsets -- the Mech__ModelResource struct is layout-skewed (field98 gave
-1.5 where the record holds 0.3; audit filed as task #4, also explains the
old forwardCycleRate "floor 25" hack).  Sender triggers now use the
authored constants: pos-deadband type 0, quat-Y delta + yaw-rate delta +
stopped-turn edge type 4 (binary @0x4aad35/@0x4aac2b/@0x4aac6c).
Verified 2-node: madcat streams 0.3/0.045/3deg, type-4 spam gone, walking
replicant clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
arcattack
2026-07-11 11:55:25 -05:00
co-authored by Claude Fable 5
parent e8b9f71025
commit a652ddcdbc
4 changed files with 105 additions and 17 deletions
+13 -3
View File
@@ -1267,9 +1267,19 @@ Mech::Mech(
Wword(0x15a) = (int)(model->lookRightAngle * DegreesToRadians); // +0x54 Wword(0x15a) = (int)(model->lookRightAngle * DegreesToRadians); // +0x54
Wword(0x15b) = (int)(model->lookFrontAngle * DegreesToRadians); // +0x58 Wword(0x15b) = (int)(model->lookFrontAngle * DegreesToRadians); // +0x58
Wword(0x15c) = (int)(model->lookBackAngle * DegreesToRadians); // +0x5c Wword(0x15c) = (int)(model->lookBackAngle * DegreesToRadians); // +0x5c
Wword(0x1dc) = (int)(model->fieldA0 * DegreesToRadians); // The update-record deadbands, read at the RAW record offsets the binary
Wword(0x1db) = model->field9c; // itself uses ([esi+0x98/0x9c/0xa0], disasm @0x4a26a7-0x4a26ef) -- the
Wword(0x1da) = model->field98; // Mech__ModelResource struct fieldXX members are LAYOUT-SKEWED (audit
// task: struct field98 read -1.5 where raw word 0x26 holds 0.3). A
// resource record is a byte blob, so word-indexed reads are the correct
// idiom (not the object databinding trap).
updateTurnAngleDeadband = ((Scalar *)model)[0x28] * DegreesToRadians; // @0x770 <- UpdateTurnDegreeDiffrence
updateTurnVelocityDeadband = ((Scalar *)model)[0x27]; // @0x76c <- UpdateTurnVelocityDiffrence
updatePositionDeadband = ((Scalar *)model)[0x26]; // @0x768 <- UpdatePositionDiffrence
if (getenv("BT_REPL_LOG"))
DEBUG_STREAM << "[deadband] pos=" << updatePositionDeadband
<< " turnVel=" << updateTurnVelocityDeadband
<< " turnAngle=" << updateTurnAngleDeadband << " (rad)" << std::endl;
Wword(0x10c) = model->fieldA4; Wword(0x10c) = model->fieldA4;
// NOTE: the decomp writes word slots this[0x15d]/this[0x15e]. Routed to the // NOTE: the decomp writes word slots this[0x15d]/this[0x15e]. Routed to the
// scratch bank (unnamed pose-angle fields) -- writing through (float*)(this + // scratch bank (unnamed pose-angle fields) -- writing through (float*)(this +
+8
View File
@@ -581,6 +581,14 @@ public:
Scalar fallScalar; // binary @0x4b4 -- same record family; ctor 0 [T4 name] Scalar fallScalar; // binary @0x4b4 -- same record family; ctor 0 [T4 name]
int heatLevelSnapshot; // binary @0x780 -- heatAlarm level at frame int heatLevelSnapshot; // binary @0x780 -- heatAlarm level at frame
// entry; the type-7 impact-record deadband // entry; the type-7 impact-record deadband
// The AUTHENTIC update-record deadbands (task #3): streamed by the ctor
// from the model record (disasm @0x4a26d7-0x4a26ef: 0x770 <- rec+0xa0 x
// pi/180 [const @0x4a2d44 = 0.01745329], 0x76c <- rec+0x9c raw, 0x768 <-
// rec+0x98 raw = UpdateTurnDegreeDiffrence / UpdateTurnVelocityDiffrence /
// UpdatePositionDiffrence). Consumed by the perf-loop senders. [T1]
Scalar updatePositionDeadband; // binary @0x768 -- type-0 pose trigger
Scalar updateTurnVelocityDeadband; // binary @0x76c -- type-4 yaw-rate trigger
Scalar updateTurnAngleDeadband; // binary @0x770 -- type-4 orientation trigger (rad)
// AUTHENTIC GROUND MODEL ctor products (task #15, ground-model-decode; // AUTHENTIC GROUND MODEL ctor products (task #15, ground-model-decode;
// binary part_012.c:9938-9940 + 9974-9975). By-name access only; declared // binary part_012.c:9938-9940 + 9974-9975). By-name access only; declared
// after the layout-locked fields so nothing shifts. // after the layout-locked fields so nothing shifts.
+45 -14
View File
@@ -2540,23 +2540,54 @@ void
angular_deviation.Subtract( angular_deviation.Subtract(
projectedOrigin.angularPosition, projectedOrigin.angularPosition,
localOrigin.angularPosition); localOrigin.angularPosition);
if ( // Position deadband (binary @0x4aad35: |pos - lastSent|^2 vs the
error.LengthSquared() > 0.04f // model's UpdatePositionDiffrence @0x768) + the 2s heartbeat.
|| lastPerformance - lastUpdate > 2.0f // Guard against an unstreamed/zero deadband with the old stand-in.
)
{ {
ForceUpdate(); // type 0: position deadband + heartbeat Scalar posDb = (updatePositionDeadband > 0.0f)
? updatePositionDeadband : 0.04f;
if (
error.LengthSquared() > posDb
|| lastPerformance - lastUpdate > 2.0f
)
{
ForceUpdate(); // type 0: pose record
}
} }
// ORIENTATION rides ONLY the type-4 resync record now (the // ORIENTATION rides ONLY the type-4 resync record (the authentic
// authentic case-0 writer/reader save-restore strips it from the // case-0 writer/reader save-restore strips it from the pose
// pose record) -- request it on angular deviation or the turn // record). Binary triggers (@0x4aac2b / @0x4aac6c):
// never reaches the replicant. Binary deadbands @0x4aac2b/ // |localOrigin.angular.y - projectedOrigin.angular.y| > @0x770
// @0x4aac6c diff the live vs last-replicated angular channels // |localVelocity.angular.y - projectedVelocity.angular.y| > @0x76c
// against model constants (+0x76c/+0x770, not yet streamed); // or (live yaw-rate == 0 && replicated yaw-rate != 0)
// the quat-deviation test stands in. [T3 threshold only] // -- the quaternion Y component and the yaw rate, against the
if (Abs(angular_deviation.w) < 0.997f) // model's UpdateTurnDegreeDiffrence (deg->rad) and
// UpdateTurnVelocityDiffrence. [T1 expressions; zero-deadband
// guard falls back to the old quat-w stand-in]
{ {
ForceUpdate(1 << MechResyncUpdateModelBit); // type 4 Scalar angDb = (updateTurnAngleDeadband > 0.0f) ? updateTurnAngleDeadband : -1.0f;
Scalar velDb = (updateTurnVelocityDeadband > 0.0f) ? updateTurnVelocityDeadband : -1.0f;
Logical resync = False;
if (angDb > 0.0f)
{
if (Abs(localOrigin.angularPosition.y
- projectedOrigin.angularPosition.y) > angDb)
resync = True;
if (Abs((Scalar)localVelocity.angularMotion.y
- (Scalar)projectedVelocity.angularMotion.y) > velDb)
resync = True;
if ((Scalar)localVelocity.angularMotion.y == 0.0f
&& (Scalar)updateVelocity.angularMotion.y != 0.0f)
resync = True;
}
else if (Abs(angular_deviation.w) < 0.997f)
{
resync = True; // stand-in when unstreamed
}
if (resync)
{
ForceUpdate(1 << MechResyncUpdateModelBit); // type 4
}
} }
// Commanded-speed deadband (binary @0x4aac88): the mapper's live // Commanded-speed deadband (binary @0x4aac88): the mapper's live
// speedDemand vs the last-replicated bodyTargetSpeed -> the tiny // speedDemand vs the last-replicated bodyTargetSpeed -> the tiny
+39
View File
@@ -0,0 +1,39 @@
import capstone, struct, sys
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
data = open("../content/BTL4OPT.EXE","rb").read()
# established mapping (from dis_4b2980.py usage): file offset = VA - 0x400000 - 0xc00 adjustments;
# use the section table: .text typically RVA 0x1000, raw 0x400/0x600. Recover from PE header.
pe = struct.unpack_from("<I", data, 0x3c)[0]
nsec = struct.unpack_from("<H", data, pe+6)[0]
opt = pe+24; szopt = struct.unpack_from("<H", data, pe+20)[0]
sec0 = opt+szopt
imgbase = struct.unpack_from("<I", data, opt+28)[0]
secs=[]
for i in range(nsec):
off=sec0+i*40
name=data[off:off+8].rstrip(b"\0").decode()
vsz,va,rsz,ra=struct.unpack_from("<IIII",data,off+8)
secs.append((name,va,rsz,ra))
text=[s for s in secs if s[0]=="CODE"][0]
va0=imgbase+text[1]; ra0=text[3]; n=text[2]
md=capstone.Cs(capstone.CS_ARCH_X86, capstone.CS_MODE_32)
md.detail=True
code=data[ra0:ra0+n]
hits=[]
# quick byte-pattern prescan for disp32 0x768/0x76c/0x770 little-endian
import re
for target,label in [(0x768,"0x768"),(0x76c,"0x76c"),(0x770,"0x770")]:
pat=struct.pack("<I",target)
for m in re.finditer(re.escape(pat),code):
start=max(0,m.start()-8)
for ins in md.disasm(code[start:m.start()+12], va0+start):
if ins.op_str and label in ins.op_str and ("ptr [" in ins.op_str):
# writes: mov [reg+disp], src OR fstp
if ins.mnemonic in ("mov","fstp","fst") and ins.op_str.index("[")<ins.op_str.index(","):
hits.append((ins.address,f"{ins.mnemonic} {ins.op_str}"))
elif ins.mnemonic in ("fld","fcomp","fcom","mov") :
pass
seen=set()
for a,t in sorted(hits):
if (a,t) not in seen:
seen.add((a,t)); print(f"0x{a:x}: {t}")