#87: mech armour PANELS now darken with damage -- the .DZM material system was loaded and unused

Players: "the actual enemy mech in external view is not showing darkened armor
panels".  We swapped destroyed LIMB meshes but never darkened a panel.

The 1995 game darkens armour through the MATERIALS.  MakeMechRenderables
(FUN_004cef28) built, per (damage zone, material), a watcher
FUN_004573e4(material, &zone->damageLevel, 0.1f) that snapshotted the materials
This commit is contained in:
Joe DiPrima
2026-07-31 19:52:55 -05:00
parent 5410371b0c
commit 137c151951
14 changed files with 589 additions and 1 deletions
+29
View File
@@ -0,0 +1,29 @@
#!/usr/bin/env bash
# #87 PIXEL A/B: render the SAME standing scene twice -- every armour zone pinned
# to level 0 (pristine) and level 1 (fully damaged) via BT_ARMOR_FORCE -- so the
# only difference between the two frame sets is the darkening itself. No damage,
# no smoke, no motion, no death to confound the comparison.
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
sed "s/^map=.*/map=grass/; s/^time=.*/time=day/" MP.EGG > ARMOR.EGG
run () { # run <level> <prefix>
taskkill //F //IM btl4.exe > /dev/null 2>&1; sleep 2
rm -f ${2}_*.png
( bt_player_env
unset BT_START_INSIDE # presence-only gate -- external view
export BT_LOG=armor_ab_$2.log BT_AFFINITY=0x03
export BT_ARMOR_FORCE=$1 BT_ARMOR_LOG=1
export BT_SHOT_EVERY=400 BT_SHOT_PREFIX=$2
"$BT_EXE" -egg ARMOR.EGG & )
sleep 45
taskkill //F //IM btl4.exe > /dev/null 2>&1
sleep 2
}
run 0.0 abpristine
run 1.0 abdamaged
ls abpristine_*.png abdamaged_*.png 2>/dev/null | head
+78
View File
@@ -0,0 +1,78 @@
#!/usr/bin/env bash
# #87 bench: MECH ARMOUR DARKENING.
#
# The 1995 renderer lerped every material a damage zone paints from its authored
# colour toward 0.1x as that zone's damageLevel ran 0->1 (FUN_004573e4 built one
# watcher per (zone, material) from the mech's .DZM list; FUN_00457784 re-pushed
# the blend on every change). Our renderer swapped destroyed LIMB meshes but
# never darkened a panel -- "the actual enemy mech in external view is not
# showing darkened armor panels".
#
# Two legs:
# self -- self-damage one named zone on the player's own mech; the cleanest
# read of the level->brightness chain (deterministic, one zone).
# enemy -- walk up to a spawned target and shoot it: the REPORTED case, an
# enemy hull darkening under fire, with screenshots to pixel-verify.
#
# Verdict lines:
# [armor] zone N '<dzname>' -> M draw op(s) .DZM list resolved onto geometry
# [armor] '<mat>' zone N level a -> b (brightness kx) level reaching the ops
#
# usage: armor_darken.sh [self|enemy] [zone]
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
LEG="${1:-self}"
ZONE="${2:-dz_ltorso}"
LOG=armor_${LEG}.log
rm -f "$LOG" armorshot_*.tga
sed "s/^map=.*/map=grass/; s/^time=.*/time=day/" MP.EGG > ARMOR.EGG
if [ "$LEG" = "enemy" ]; then
# The reported case: an ENEMY hull taking fire. Beeline to the spawned target
# and hold the trigger; BT_SHOT_EVERY dumps backbuffer frames so the panels can
# be compared pixel-wise before/after.
BT_ARMOR_LOG=1 BT_DMG_LOG=1 \
BT_SPAWN_ENEMY=1 BT_GOTO=enemy BT_GOTO_STOP=2 BT_AUTOFIRE=1 \
BT_SHOT_EVERY=300 BT_SHOT_PREFIX=armorshot \
bt_launch "$LOG" ARMOR.EGG 0x03
elif [ "$LEG" = "ext" ]; then
# PIXEL leg. Same single-zone ramp, but in the EXTERNAL chase camera so the
# hull fills the frame and one zone can be compared before/after.
#
# NOTE: this deliberately overrides the shipped player env's BT_START_INSIDE=1
# (bench_common) -- players start in the cockpit and cannot see their own hull.
# This leg exists ONLY to photograph the panels; the `self` and `enemy` legs
# are the player-authentic ones.
( bt_player_env
# presence-only gate (btl4vid.cpp:657) -- "=0" still reads as set, so UNSET it
unset BT_START_INSIDE
export BT_LOG="$LOG" BT_AFFINITY=0x03
export BT_ARMOR_LOG=1 BT_DMG_LOG=1
export BT_SELF_DAMAGE=3 BT_SELF_DAMAGE_ZONE="$ZONE"
export BT_SHOT_EVERY=600 BT_SHOT_PREFIX=armorext
"$BT_EXE" -egg ARMOR.EGG & )
else
# Deterministic single-zone ramp on our own mech. Small dose so the level
# climbs in steps instead of saturating in one tick.
BT_ARMOR_LOG=1 BT_DMG_LOG=1 \
BT_SELF_DAMAGE=3 BT_SELF_DAMAGE_ZONE="$ZONE" \
bt_launch "$LOG" ARMOR.EGG 0x03
fi
sleep 90
taskkill //F //IM btl4.exe > /dev/null 2>&1
sleep 2
echo "=== per-entity binding totals ==="
grep -E "^\[armor\] entity" "$LOG" | sort -u
echo "=== level changes ==="
grep -E "^\[armor\] '" "$LOG" | head -30
echo "=== distinct materials darkened ==="
grep -E "^\[armor\] '" "$LOG" | sed "s/.*\['armor'\] //" | awk '{print $2}' | sort -u | head -20
echo "=== shots ==="
ls armorshot_*.tga 2>/dev/null | head
+57
View File
@@ -0,0 +1,57 @@
"""Raw-image xref sweep (gotcha #21: the decomp export has gaps).
Finds every CALL/JMP rel32 in .text whose target is one of the VAs given on the
command line, plus any absolute immediate referencing them.
python xref.py 0x4902b0 0x490308
"""
import struct, sys
PATH = r"C:\git\bt411\content\BTL4OPT.EXE"
data = open(PATH, "rb").read()
e_lfanew = struct.unpack_from("<I", data, 0x3C)[0]
coff = e_lfanew + 4
num_sec = struct.unpack_from("<H", data, coff + 2)[0]
opt_size = struct.unpack_from("<H", data, coff + 16)[0]
opt = coff + 20
image_base = struct.unpack_from("<I", data, opt + 28)[0]
sec_tbl = opt + opt_size
secs = []
for i in range(num_sec):
off = sec_tbl + i * 40
name = data[off:off + 8].rstrip(b"\0").decode("latin1")
vsize, va, rawsize, rawptr = struct.unpack_from("<IIII", data, off + 8)
secs.append((name, va, vsize, rawptr, rawsize))
def off_to_va(off):
for name, va, vsize, rawptr, rawsize in secs:
if rawptr <= off < rawptr + rawsize:
return image_base + va + (off - rawptr)
return None
targets = set(int(a, 16) for a in sys.argv[1:])
hits = []
for name, va, vsize, rawptr, rawsize in secs:
blob = data[rawptr:rawptr + rawsize]
base_va = image_base + va
# rel32 CALL (E8) / JMP (E9)
for i in range(len(blob) - 5):
op = blob[i]
if op not in (0xE8, 0xE9):
continue
rel = struct.unpack_from("<i", blob, i + 1)[0]
tgt = base_va + i + 5 + rel
if tgt in targets:
hits.append((base_va + i, "call" if op == 0xE8 else "jmp", tgt, name))
# absolute immediates (vtable slots, push offset)
for i in range(len(blob) - 4):
v = struct.unpack_from("<I", blob, i)[0]
if v in targets:
hits.append((base_va + i, "abs32", v, name))
for addr, kind, tgt, sec in sorted(hits):
print("%08x %-6s -> %08x [%s]" % (addr, kind, tgt, sec))
print("\n%d xrefs" % len(hits))