Players reported scoring and K/D going screwy on 4.11.817. Cause: build 787
(#45/#134) retired the port's per-hit inflicted crediting as an "invention",
on the strength of a KB claim that the type-0 score handler was dead code.
That claim was wrong.
BTPlayer overrides Dispatch -- vtable @00513300 slot 3 = FUN_004bffa0 -- and
splits type 0 off BEFORE base dispatch:
if (msg->id == 0x16 && msg->type == 0) FUN_004c0200(...); // ScoreInflicted
else base dispatch;
@004c0200 names itself in its own Verify string
("BTPlayer::ScoreInflictedMessageHandler") and computes
CalcInflicted(basis) -> negate if target==self -> x (targetTonnage/ownTonnage)
-> accumulate into +0x278. ScoreMessageHandler's type-0 arm Verify-rejects
precisely BECAUSE this interceptor guarantees type 0 never reaches it.
The port had the handler, faithfully reconstructed, and no interceptor -- so
Block B's inflicted reports all landed in the rejecting arm and banked 0.
Per-hit damage credit was silently deleted.
Independently corroborated by the ORIGINAL MANUAL'S SCORING CHART (filed as
reference/manual/scoring_chart.webp, from Lynx): "+1 each damage point scored
on opponent's armor" and "-1 each self-inflicted point of armor damage" -- the
negate-if-target-is-self arm exactly. Without that chart the dead-code note
would probably have stood.
Verified (scratchpad/night13/scoreverify.sh, cross-node kill, 2 nodes):
type-0 Verify rejections 0 (was firing on every non-lethal hit)
inflicted score rows 83 awards 0.98..25.00, all positive, tracking damage
kill path un-regressed type=2 award=4.88 kills=1, victim respawn x1
KB: combat-damage.md report B and the score-model paragraph rewritten, with
the full chart and THREE unreconciled rows flagged [T4] -- flat +500 kill vs
the benched 4.88, +1000 at game start, and -1000 eject / -500 ammo (which
would live in ScenarioRole::specialCaseDeathPenalty @role+0x20, read by the
port but authored nowhere in shipped content).
KNOWN, NOT FIXED HERE: in MP the running total does not persist -- currentScore
is flushed to the operator console and ZEROED (btplayer.cpp ~1219) because the
binary treats it as a console DELTA. Restoring the credit makes that very
visible (bench: totals climb to ~35 then reset). Needs its own decision; the
chart's "+1000 starting the game" implies a persistent total lives somewhere.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NCJQkvq6G2JNrpVbA75tVZ
96 lines
4.0 KiB
Bash
96 lines
4.0 KiB
Bash
#!/usr/bin/env bash
|
|
# =========================================================================
|
|
# SCORING verify -- the type-0 interceptor (@004bffa0) restored.
|
|
#
|
|
# Composition lifted from night12/scorekill.sh, which is known to produce a
|
|
# clean cross-node kill: A (madcat, shooter) zone-walk-hammers B (loki,
|
|
# spinner) at 90u until B dies.
|
|
#
|
|
# THE A/B IS READABLE IN ONE RUN, because the old behaviour left a receipt:
|
|
# BEFORE every inflicted report hit ScoreMessageHandler's type-0 arm and
|
|
# tripped Verify "ScoreMessageHandler should not be given
|
|
# DamageInflictedScoreMessages!" -- night12's bench listed those
|
|
# Verify prints as an expected PASS signal.
|
|
# AFTER the interceptor routes type 0 to ScoreInflictedMessageHandler, so
|
|
# those Verify prints must be GONE and matchlog SCORE type=0 rows
|
|
# with non-zero awards must appear instead.
|
|
#
|
|
# Chart cross-check (original manual, via Lynx): "+1 each damage point scored
|
|
# on opponent's armor". The handler scales by tonnage ratio and the role's
|
|
# damageInflictedModifier, so award != damage exactly -- but it must TRACK
|
|
# damage, not sit at zero, and must be NEGATIVE for self-damage.
|
|
# =========================================================================
|
|
set -x
|
|
. /c/git/bt411/scratchpad/night6/bench_common.sh
|
|
cd /c/git/bt411/content || exit 1
|
|
bt_assert_player_env
|
|
taskkill //F //IM btl4.exe > /dev/null 2>&1
|
|
sleep 2
|
|
rm -f sv_a.log sv_b.log sv_relay.log matchlog_*.txt
|
|
bt_expert_egg MP.EGG SV.EGG
|
|
sed -i "s/^map=.*/map=grass/; s/^time=.*/time=day/" SV.EGG
|
|
python - << 'EOF'
|
|
lines = open('SV.EGG').read().splitlines(True)
|
|
n = 0
|
|
for i, l in enumerate(lines):
|
|
if l.startswith('vehicle='):
|
|
n += 1
|
|
lines[i] = 'vehicle=madcat\n' if n == 1 else 'vehicle=loki\n'
|
|
open('SV.EGG', 'w').writelines(lines)
|
|
print('vehicles set:', n)
|
|
EOF
|
|
|
|
( export BT_DMG_LOG=1 BT_DEATH_LOG=1 BT_MP_LOG=1 BT_MATCHLOG=1 BT_SCORE_LOG=1
|
|
bt_launch sv_b.log SV.EGG 0x0C -net 1601 )
|
|
sleep 2
|
|
( export BT_ZONE_WALK=8 BT_WALK_ZONES=dz_ldleg
|
|
export BT_GOTO=enemy BT_GOTO_STOP=90 BT_KEY_NOFOCUS=1
|
|
export BT_DMG_LOG=1 BT_DEATH_LOG=1 BT_MP_LOG=1 BT_MATCHLOG=1 BT_SCORE_LOG=1
|
|
bt_launch sv_a.log SV.EGG 0x03 -net 1501 )
|
|
sleep 5
|
|
python ../tools/btconsole.py SV.EGG 127.0.0.1:1501 127.0.0.1:1601 > sv_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 "=================== SCORING VERIFY ==================="
|
|
echo "--- 0. did combat happen at all? (if 0 hits the run is VOID) ---"
|
|
echo -n "damage rows on victim B : "; grep -ac "dmghit\|DMG" sv_b.log
|
|
echo
|
|
echo "--- 1. THE OLD SYMPTOM: type-0 rejections (must be ZERO now) ---"
|
|
echo -n "'should not be given DamageInflictedScore' Verify prints: "
|
|
cat sv_a.log sv_b.log | grep -ac "should not be given DamageInflictedScore"
|
|
echo
|
|
echo "--- 2. THE FIX: inflicted score rows (matchlog SCORE type=0) ---"
|
|
echo -n "type=0 rows: "; cat matchlog_*.txt 2>/dev/null | grep -ac "type=0"
|
|
cat matchlog_*.txt 2>/dev/null | grep -a "type=0" | head -8
|
|
echo
|
|
echo "--- 3. award vs damage: does the credit TRACK damage? ---"
|
|
python - << 'EOF'
|
|
import glob, re
|
|
aw = []
|
|
for fn in glob.glob('matchlog_*.txt'):
|
|
for line in open(fn, errors='replace'):
|
|
m = re.search(r'type=0 award=(-?[\d.]+) total=(-?[\d.]+)', line)
|
|
if m:
|
|
aw.append((float(m.group(1)), float(m.group(2))))
|
|
if not aw:
|
|
print(' NO type=0 rows -- interceptor did not fire')
|
|
else:
|
|
pos = [a for a, t in aw if a > 0]
|
|
neg = [a for a, t in aw if a < 0]
|
|
print(' rows=%d positive=%d negative(self)=%d' % (len(aw), len(pos), len(neg)))
|
|
print(' award range: %.2f .. %.2f running total ends at %.2f'
|
|
% (min(a for a, t in aw), max(a for a, t in aw), aw[-1][1]))
|
|
EOF
|
|
echo
|
|
echo "--- 4. kill path un-regressed (type=2) + respawn ---"
|
|
echo -n "type=2 kill rows: "; cat matchlog_*.txt 2>/dev/null | grep -ac "type=2"
|
|
cat matchlog_*.txt 2>/dev/null | grep -a "type=2" | head -3
|
|
echo -n "victim death cycles: "; grep -ac "death cycle START" sv_b.log
|