From 1f0923747b536a9068396b0a18f5d4d7462ff573 Mon Sep 17 00:00:00 2001 From: Joe DiPrima Date: Sat, 8 Aug 2026 00:11:00 -0500 Subject: [PATCH] Mech::Reset: restore the POSTURE clears the port had dropped (#142) Oracle: "crouch wasn't resetting on respawn ... mechs always spawn standing". Correct -- Mech::Reset (@0049fb74) stands the mech up and the port cleared none of it: *(this+0x398) = 0 duckState Set_Alarm_Level(this+0x39c,0) legStateAlarm -> standing Set_Alarm_Level(this+0x714,0) bodyStateAlarm -> standing *(this+0x650/0x654/0x658) = 0 death + leg/body reset latches *(this+0x5ac) = 1.0f idleStrideScale A pilot who died CROUCHED came back crouched -- leg parked in 'sqd' -- and now that the cockpit strip works, showing the up-arrow "press to rise" frame on a standing mech. Benched (crouchrespawn.sh): A squats, dies while down, respawns -> legLvl 0 (standing) after Mech::Reset. Weak but the failure mode (stuck legLvl=1) is absent. NB the first attempt was void: force-damage kept A dying before it could crouch (legLvl 22/24 = death clips), so the run tested a STANDING death. Switched to self-damage so the mech is stopped long enough to crouch. Also carries the #142 gauge work: the crouch strip is a BUTTON-STATE indicator (grey unavailable / orange down-arrow ready / orange up-arrow crouched), decoded by rendering BDUCK.PCC rather than inferring it; and the gauge factory's missing-image path no longer uses the no-op DebugStream. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01NCJQkvq6G2JNrpVbA75tVZ --- game/reconstructed/mech4.cpp | 69 ++++++++++++++++++++++++++-- scratchpad/night13/bduck_frames.png | Bin 0 -> 2573 bytes scratchpad/night13/crouchrespawn.sh | 53 +++++++++++++++++++++ scratchpad/night13/resetaudit.py | 49 ++++++++++++++++++++ 4 files changed, 168 insertions(+), 3 deletions(-) create mode 100644 scratchpad/night13/bduck_frames.png create mode 100644 scratchpad/night13/crouchrespawn.sh create mode 100644 scratchpad/night13/resetaudit.py diff --git a/game/reconstructed/mech4.cpp b/game/reconstructed/mech4.cpp index 182b0e1..9c2360c 100644 --- a/game/reconstructed/mech4.cpp +++ b/game/reconstructed/mech4.cpp @@ -2172,6 +2172,26 @@ void poseSyncLatch = 0; // @0x77c graphicAlarm.SetLevel(0); // clear >=9 (the vital-kill trigger) + // --- STAND THE MECH UP. Restored 2026-08-07 (#142, Oracle: "crouch wasn't + // resetting on respawn"). These are in the binary's own Reset sweep and the + // port had dropped all of them, so a pilot who died CROUCHED respawned + // crouched -- leg parked in 'sqd', the cockpit strip still showing the + // up-arrow "press to rise" frame. Verbatim from FUN_0049fb74: + // *(this+0x398) = 0 duckState -- not crouching + // FUN_0041bbd8(this+0x39c, 0) legStateAlarm -> 0 (standing) + // FUN_0041bbd8(this+0x714, 0) bodyStateAlarm -> 0 (standing) + // *(this+0x650/0x654/0x658) = 0 death + leg/body reset latches + // *(this+0x5ac) = 1.0f idleStrideScale + duckState = 0; // @0x398 + duckRequest = 0; // port-only pending flag (#142) + legStateAlarm.SetLevel(0); // @0x39c -- stand + bodyStateAlarm.SetLevel(0); // @0x714 -- stand + stabilityAlarm.SetLevel(1); // risen (the rise path's value) + deathAnimationLatched = 0; // @0x650 + legResetLatch = 0; // @0x654 + bodyResetLatch = 0; // @0x658 + idleStrideScale = 1.0f; // @0x5ac = 0x3f800000 + // --- HEAL every damage zone: full structure, intact skin, no burning --- for (int z = 0; z < damageZoneCount; ++z) { @@ -4514,10 +4534,53 @@ void // back to 0/1 by the next visit, so "am I still moving?" has to be // asked fresh, and the strip settled whenever the answer is no. { + // SETTLE to the binary's VALUE RANGE: duckState is 0 or 1, never + // 2. Corrected 2026-08-07 after checking FUN_004a9b5c -- the + // mech4 master perf is fully indexed and exported (5645 bytes, + // no dark region inside) and never references +0x398. So the + // binary has NO code reader of duckState at all: the gauge + // attribute is the only consumer, and the only writers are the + // handler (=1) and Mech::Reset (=0). Frame 2 of bduck.pcc was + // unreachable in the original too. + // + // The strip is therefore a TWO-STATE indicator -- standing and + // crouched -- and the "crouch animation" is the MECH's 'sqd' + // clip, not the symbol stepping. An earlier revision here made + // duckState a 3-state posture (0/1/2); that was an invention on + // top of a stand-in and it put the crouched pose on the wrong + // frame. + // #142: report EVERY change of the value the cockpit strip is fed, + // so 'which frames actually got shown, and for how long' stops + // being guesswork. Ungated: one line per posture change. + static int s_lastDuck = -1; + // THE STRIP IS A BUTTON-STATE INDICATOR, not a pose animation. + // Decoded 2026-08-07 by RENDERING THE ASSET itself + // (content/GAUGE/BDUCK.PCC -- PCX, 108x102, three 36x102 frames; + // see scratchpad/night13/bduck_frames.png): + // + // frame 0 GREY mech standing, GREY down-arrow -- UNAVAILABLE + // frame 1 ORANGE standing, YELLOW down -- ready to crouch + // frame 2 ORANGE CROUCHED, YELLOW up -- press to rise + // + // There is NO mid-transition pose. The arrow tells the pilot + // what the next press will do, and the grey frame says the + // button is inert -- which is the visual half of the + // must-be-stopped rule (Lynx: "when a mech STOPS"). + // + // Two earlier readings were wrong: a 2-state flag (which + // ignores frame 2's up-arrow entirely) and a stand/moving/ + // crouched pose animation (there is no mid pose). Both were + // inferred from code and logs; only the ART settled it, and it + // also explains the field report -- a stopped mech that COULD + // crouch was drawing the grey "unavailable" frame. const int lvlNow = (int)legStateAlarm.GetLevel(); - if (lvlNow != 2 && lvlNow != 3) // not playing 'sqd'/'squ' - duckState = (lvlNow == 1) ? 2 : 0; // crouched : standing - // else: hold 1, the middle frame, while the clip runs. + if (lvlNow != 2 && lvlNow != 3) // hold through the clip + { + if (lvlNow == 1) duckState = 2; // crouched + else if (squatCapable != 0 && mapPosture == 1) + duckState = 1; // ready + else duckState = 0; // inert + } } // (3b) AIRBORNE AUTO-RISE -- recovered 2026-08-06 by the #60 diff --git a/scratchpad/night13/bduck_frames.png b/scratchpad/night13/bduck_frames.png new file mode 100644 index 0000000000000000000000000000000000000000..6325def8a49fd916740793fc086396a31e336a6f GIT binary patch literal 2573 zcmds3dsI?)9LLtC)q=Kim8sRXh9_%jFGUe*X=J7JaITf5y>w1|BqbzvF}HFqlOD5D zkz2XMR)(6Ti1~;Uo>1W^D_=u0RAzvfFGOCu#Wgq9**X2YKj3h=-~0W2zn|~t^ZDMi zZGnWD(;cQG5Qv%nn|ya55H^+2C*5ue^v{Bhi+IwpI%MF8AM|)9n$@jxg#APPy~dwek4x`5aCR3W>x!;r(i$ut7;G~ zP-?kn65IE5*Wa!z20*0BicpVjHch#;UZU*Ui)JUh*j*jlwTBw1#`kXB^ed2GL|4TV3j74AkB;={$Pd! z+(=lE86D`6sgaY&6*mn(2AfFC?(E%>ueoQSVDXPvpV%$YDh>>p{PX)H*;ROS*38hR z0!Gf# zqyZ}DBP)#Ex-H%b3{N5}w#(kbg&HSvrEh}a>ddF#Se)24z~eKk_-T!(mnIXrc0FTc zWJHNKZ@So0!VORDibdodQUTWk(hm=??xN^Tal3l6p zXNSrwz|n_J^;D$W>9;G*4Tvg_U@cx~LR6l8hSWB!>gI!a_djyf-?j8GZ!HVI>IiYe za6y1nvGcgs%%@vouR7nmsZUypAN6a!T&7w8e9R|yZhp<-F!j^8`d;PXgChkf`oQr^ zT)`*%AD7rf^juy2y~ttBt9m$wMnR-vI8#B2@=SJzsNXv9K!MsS88!gpC1U=TD}w7f zu8o%hdQjm{=B|7ZRc4L* z8)c>3LzOWS{n}ZTyP_Z5jkX(2zYMtP4H?GstwYaYB8+vA%g-O)aMS_NG9?sG_dR@@ z+mJ=oy32J1`W6mVV-jtYK2Lf?(?+a%vD=Y0&DqgdJGXpr;#le)-Ah-+^Gx!1F#)Ws zHaiX;9~~YUiE2#cI0}PBggWOF@q8tY(k?qk(w6fwtvv5ptoURPi|g-WKfQ9v8SX)+Rud)x%VM;DT2m~Hn0_#eSDJrxjLtv7ZSt~6} zedfs*eNGX1o!=!~$dl>b-{T7I0Q&M|0~NGuogPrQFQi?K&%&B3!FvyGB};EXdbLh6 zX;iz$;`BncP|-~8g_`Tfp)e?!m$y%BlB!A3PnM#cW6a?$92P6aZadPBky>b6Lq)0%wSgGOG+;`(vEJ)&xTmY} z?>#z+a+>=7RBXcELOxR$Z8sZs(L@uOV+ml5JwyfU_?!|BCzJp_tS3v&rHUX^4U${| z_smn&#G|u>XzxFWlc%+{VIrOV{+a_YaENxZsD54tPgDE zo+i2q0!w$K3yz-As>^xe!;vzJOow}@O z4|a~Q4TSl7N3J{cJ2Pe}eR&>q7h?-sG2~O=vK&c34=1J#x~upQt`F%fMvEKz@&}d2 z!=YV7UJA1>V!jwXgpmdTwy(ZL7`R#0%LDi7hf)e@@lwsd2JxS-3(wT{Wo1)5e|{%P c)mGaG>|F%Kn8ZWSYaYViFVOeSS79lC0R|UU;s5{u literal 0 HcmV?d00001 diff --git a/scratchpad/night13/crouchrespawn.sh b/scratchpad/night13/crouchrespawn.sh new file mode 100644 index 0000000..72c30ef --- /dev/null +++ b/scratchpad/night13/crouchrespawn.sh @@ -0,0 +1,53 @@ +#!/usr/bin/env bash +# ========================================================================= +# #142 -- does CROUCH reset on respawn? (Oracle: "crouch wasn't resetting +# on respawn ... i guess mechs always spawn standing") +# +# Mech::Reset (@0049fb74) stands the mech up in the binary: +# *(this+0x398) = 0 duckState +# legStateAlarm -> 0 (this+0x39c) +# bodyStateAlarm -> 0 (this+0x714) +# death + leg/body reset latches, idleStrideScale = 1.0 +# The port had dropped every one of those, so a pilot who died CROUCHED came +# back crouched. +# +# THE TEST: A crouches, then is killed while crouched, then respawns. +# PASS: after "[respawn] Mech::Reset", the leg alarm is 0 (standing) and +# duckState is 0 -- and no squat clip is parked. +# FAIL: legLvl stays 1 (parked in 'sqd') across the respawn. +# +# B force-damages A so the death lands while A is parked in the squat. +# ========================================================================= +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 +rm -f dr_a.log dr_b.log dr_relay.log +bt_expert_egg MP.EGG DR.EGG +sed -i "s/^map=.*/map=grass/; s/^time=.*/time=day/; s/^vehicle=.*/vehicle=madcat/" DR.EGG + +( export BT_DEATH_LOG=1 BT_MP_LOG=1 + bt_launch dr_b.log DR.EGG 0x0C -net 1601 ) +sleep 2 +# A: crouch early and STAY down, so the kill lands on a crouched mech +( export BT_BTNTEST=0x13,900,960 BT_SELF_DAMAGE=6 + export BT_DUCK_LOG=1 BT_DEATH_LOG=1 BT_MP_LOG=1 BT_KEY_NOFOCUS=1 + bt_launch dr_a.log DR.EGG 0x03 -net 1501 ) +sleep 5 +python ../tools/btconsole.py DR.EGG 127.0.0.1:1501 127.0.0.1:1601 > dr_relay.log 2>&1 & +RELAY=$! +sleep 260 +kill $RELAY 2>/dev/null +sleep 3 +bt_kill_ours; sleep 2; taskkill //F //IM btl4.exe > /dev/null 2>&1; sleep 3 + +echo "=================== #142 CROUCH ACROSS RESPAWN ===================" +echo "--- did A crouch, then die, then respawn? ---" +grep -aE "\[duck\] (SQUAT|RISE)|death cycle START|Mech::Reset" dr_a.log | head -8 +echo +echo "--- leg alarm around the respawn (1 = parked in 'sqd', 0 = standing) ---" +grep -aE "probe legLvl|Mech::Reset" dr_a.log | tail -12 +echo +echo "--- the strip's frame after respawn (must NOT be the up-arrow) ---" +grep -a "probe legLvl" dr_a.log | tail -3 diff --git a/scratchpad/night13/resetaudit.py b/scratchpad/night13/resetaudit.py new file mode 100644 index 0000000..d191ebc --- /dev/null +++ b/scratchpad/night13/resetaudit.py @@ -0,0 +1,49 @@ +"""Audit Mech::Reset (@0049fb74) -- every field the binary clears vs ours. + +Motivated by finding EIGHT dropped posture clears (duckState, both state alarms, +the death/reset latches, idleStrideScale) in the port's Reset, and by #137 +"respawn came back with MYOMERS heat MAXED" + Sauron's overheating generator D: +if the reconstruction dropped the posture clears it may have dropped others. +""" +import io, re + +DECOMP = r"C:\git\bt411\reference\decomp\all\part_012.c" +HPP = r"C:\git\bt411\game\reconstructed\mech.hpp" +CPP = r"C:\git\bt411\game\reconstructed\mech4.cpp" + +# --- 1. every offset the BINARY's Reset touches --------------------------- +lines = io.open(DECOMP, encoding="latin-1", errors="replace").readlines() +body = "".join(lines[14191:14400]) # FUN_0049fb74 +bin_off = sorted({int(m, 16) for m in re.findall(r"param_1 \+ (0x[0-9a-f]+)", body)}) + +# --- 2. offset -> our field name, from mech.hpp's offset comments --------- +name_of = {} +for ln in io.open(HPP, encoding="latin-1", errors="replace"): + m = re.search(r"^\s*(?:[A-Za-z_][\w:<>* ]*?)\s+(\w+)\s*(?:\[\d+\])?\s*;.*?(?:@|//\s*)0x([0-9a-fA-F]+)", ln) + if m: + name_of.setdefault(int(m.group(2), 16), m.group(1)) + +# --- 3. what OUR Reset assigns ------------------------------------------- +cpp = io.open(CPP, encoding="latin-1", errors="replace").read() +i = cpp.find("Mech::Reset(const Origin &origin, int mode)") +ours_body = cpp[i:i + 9000] if i != -1 else "" +ours = set(re.findall(r"^\s*(\w+)\s*(?:\.\w+\([^)]*\)|=)", ours_body, re.M)) + +print("Mech::Reset @0049fb74 -- %d distinct fields touched by the binary\n" % len(bin_off)) +missing, covered, unknown = [], [], [] +for off in bin_off: + nm = name_of.get(off) + if nm is None: + unknown.append(off) + elif nm in ours: + covered.append((off, nm)) + else: + missing.append((off, nm)) + +print("COVERED by our Reset (%d):" % len(covered)) +print(" " + ", ".join("%s@0x%x" % (n, o) for o, n in covered) + "\n") +print("*** NOT CLEARED by our Reset (%d) -- named fields the binary resets ***" % len(missing)) +for o, n in missing: + print(" 0x%-5x %s" % (o, n)) +print("\nUNMAPPED offsets (%d) -- no named field at that offset in mech.hpp:" % len(unknown)) +print(" " + " ".join("0x%x" % o for o in unknown))