Files
BT411/context/experience-levels.md
T
arcattackandClaude Opus 5 f3bdb3b85a Searchlight + ThermalSight ToggleLamp WIRED (#61) -- and a swapped table attribution ROOT-CAUSED, retracting a false "1995 latent bug"
Both classes' Receiver::MessageHandlerSet were default-constructed blackholes
(input-path audit systemic cause #1): entryCount 0, no parent chain, Find()
returns NullHandler for every id, Receive drops silently.  The new unhandled-
message trace printed it on the first press:

    [btntest] PRESS 0x14 at poll 400
    [msg] UNHANDLED: Searchlight has no handler for message id 3

Each class now has a GetMessageHandlers() function-local static chained to
PowerWatcher's (which name-resolves through HeatWatcher/MechSubsystem to
Receiver's empty ROOT set, so id 3 does NOT collide with HeatSink's
ToggleCooling), plus a correctly-typed forwarder -- Receiver::Handler is
void(const Message*) while the decomp shape is Logical(Message&), so a cast
would be UB that merely happens to work on x86 __thiscall.  DefaultData
re-pointed off the dead empty sets.  MessageArg now reads the NAMED
ReceiverDataMessageOf<int>::dataContents with a static_assert locking it to the
binary's message+0xC (was a raw offset read -- databinding rule).

ROOT CAUSE of a long-standing misattribution: @004b860c is **ThermalSight's**
ToggleLamp (table @0x51120C), NOT Searchlight's.  The two TUs emit parallel
shared-data blocks with identical stride (msg entry, +0x5C attrs, +0x84
Performance triple); what pins each entry to its TU is that "ToggleLamp" is NOT
pooled across them -- two copies exist, each emitted immediately before its own
class-name string ("Searchlight"@0x51144B, "ThermalSight"@0x511475).
[T1: reference/decomp/section_dump.txt:69661-69711]

Searchlight's own handler is @004b838c, which sits in a Ghidra EXPORT GAP (#60).
RECOVERED by raw disassembly of content/BTL4OPT.EXE (scratchpad/dis838c.py, the
EjectAmmo technique) -- and it corrected two things I had inferred wrong:
  * NO ControlsAllowLights/+0x25C novice gate.  Searchlight tests the press
    alone; that lock is ThermalSight-only.  A NOVICE pilot CAN work the lamp.
  * `or word ptr [this+0x18],1` sits AT the jle target -> the graphics-dirty bit
    (updateModel == ForceUpdate(), per #59) is raised UNCONDITIONALLY.

Consequence: the "ORIGINAL 1995 LATENT BUG -- the searchlight can never light"
claim is RETRACTED.  It compared Searchlight's Performance (@004b841c, reads
requestedOn@0x1E0) against ThermalSight's toggle (0x1DC) -- two different
classes -- and so invented a missing 0x1DC->0x1E0 bridge.  Every sibling toggles
the field its own Performance reads.  The searchlight DID light in the arcade,
the searchlight->fog swap was NOT inert there, and building PullFogRenderable is
FAITHFUL rather than a designer-intent deviation (the 2026-07-13 "left as-is"
decision is void; the sim needs no repair).

Verified live, real click seam (BT_BTNTEST):
  0x14 -> [light] requested ON -> reported lightState 0 -> 1     (lamp lights)
  0x12 -> [light] thermal sight requested ON -> thermalActive 0 -> 1
UNHANDLED lines for both classes gone; 40 LNK2019 unchanged (the pre-existing
CreateStreamedSubsystem + Entity__SharedData::DefaultData families only).

Swept the misattribution out of searchlight.cpp/.hpp, thermalsight.cpp/.hpp,
hud.cpp:282 (it had claimed @00511180/@004b838c as HUD's), btplayer.cpp's +0x25C
consumer list, context/{decomp-reference,subsystems,rendering,open-questions,
pod-hardware,experience-levels}.md, docs/{GLASS_COCKPIT,INPUT_PATH_AUDIT}.md.
checkctx.py CLEAN.

Still open (documented, not fixed): ThermalSight has no visible IR effect
(ToggleGlobalThermalVision is a marked no-op, pvision unported, IsLocallyViewed
returns False); ThermalSight publishes "LightState"->0x1D8 where the binary has
"LightOn"->0x1D8 and "LightState"->0x1E0; Searchlight's commandedOn@0x1DC has no
identified role and measured 21 live, hinting our SubsystemResource +0x28 differs
from the binary's.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25 10:46:59 -05:00

9.9 KiB

id, title, status, source_sections, related_topics, key_terms, open_questions
id title status source_sections related_topics key_terms open_questions
experience-levels Experience levels / simulation modes — novice, standard, veteran, expert established decomp part_013.c:10816-10871 (@4c0bc8), part_014.c:7830-7900 (@4d2f3e), FUN_004ad7d4/FUN_004ac9c8/FUN_004bbfcc; BTMSSN.HPP (survived); Tesla40_BT_manual.pdf pp.7,11-14,21,22,24
subsystems
combat-damage
gauges-hud
pod-hardware
multiplayer
decomp-reference
open-questions
master
replicant
player+0x26c (std+vet only) and +0x270 (all but novice) consumers not located — 0x26c hypothesis: myomer supercharge clamp (manual p22) [T4]
does a remote pilot's experience matter cross-pod? flags are computed only on the master branch of the BTPlayer ctor [T4]
movement-heat magnitude veteran vs expert (4.0 manual says expert-only; 4.10 gate is vet+expert) [T4]

Experience levels / simulation modes

The egg's per-pilot experience field is the pod's simulation-fidelity tier — the thing the 1995 site operator set per player at sign-up. It is NOT the BAS/MID/ADV control mode (a free in-cockpit choice, locomotion/pod-hardware) and NOT the scenario role (scoring modifiers). Researched end-to-end 2026-07-18; this file supersedes the older "role display toggles" / "ROOKIE-role lockout" readings (see Corrections below).

The data path [T1]

  1. Egg: pilot page key experience ∈ {novice,standard,veteran,expert}; required unless vehicle=camera; unknown value = hard abort. Parse @4d2f3e (part_014.c:7830-7900, strings @51e6f1); tools/eggmodel.py knows the value set.
  2. BTMission (BTMSSN.HPP survived, T0): stored at mission+0xe4 (experienceLevel, accessor ExperienceLevel()); the neighboring egg key advancedDamagemission+0xf0 (advancedDamageOn) — the separate site-technician splash/collision switch (manual p13).
  3. BTPlayer ctor @4c0bc8 (part_013.c:10816-10871): on the MASTER branch ((simulationFlags & 0xc) != 4; the replicant branch instead swaps in the console-update Performance) it reads btMission(+0x1f8)->experienceLevel(+0xe4) and fans it out:
egg value +0x25c +0x260 +0x26c +0x270 +0x274 (raw)
novice 0 0 0 0 0
standard 1 0 1 1 1
veteran 1 1 1 1 2
expert 1 1 0 1 3

then unconditionally +0x264 = +0x268 = mission->advancedDamageOn(+0xf0) (both copies). [T1]

What each flag gates (binary consumers, all reached via mech+0x190 → BTPlayer) [T1]

  • +0x25c — "sim live", off only for novice. Consumers: ballistic jam roll CheckForJam @4bbfcc early-returns NO-JAM when 0 (projweap.cpp calls this LiveFireEnabled); ThermalSight ToggleLamp @4b860c (thermalsight.hpp ControlsAllowLights) — ⚠ CORRECTED 2026-07-25 (#61): this was listed as Searchlight's gate. It is not. Raw disassembly of Searchlight's real handler @004b838c (a Ghidra export gap) shows it gates on the PRESS ALONE and never reads +0x25C, so a NOVICE pilot CAN work the searchlight; only the thermal sight is locked [T1, scratchpad/dis838c.py]; PoweredSubsystem message handler @4b0efc (powersub.cpp's "message-manager short-event flag" — same flag, misattributed); the mech combat-ineffective evaluation (part_012.c:9364: mech state ∈ {3,4} && novice is an extra OR-term setting mech+0x414). The #if 0 block in surviving PPC.CPP:63-95 (novice PPCs deal no damage) shows the original intent of the tier.
  • +0x260 — the HEAT-MODEL master switch, ON for veteran+expert. This is FUN_004ad7d4 (*(*(sub+0xD0)+0x190)+0x260), the single most-called gate in the subsystem family: Myomers work→heat (movement heat, MyomersSimulation @4b8d18), Emitter/energy-weapon firing heat, missile launch heat, the heat.cpp simulation, projectile-weapon heat, and the heat-scaled part of the jam chance. Standard mode has NO heat consequences; that is authentic, not a port gap.
  • +0x274 — the raw level. FUN_004ac9c8 = (level == 0), the NOVICE lockout used as the condenser-valve guard (gauges-hud task #13) and by the PoweredSubsystem handler @4b21d0 (not-novice gate). The port's BTPlayerRoleLocksAdvanced bridge implements this predicate (correct behavior, wrong seeding — see Port state).
  • +0x264/+0x268 — advancedDamage copies. Read in the heat-family sim (part_013.c:8757). Independent of experience.
  • +0x26c / +0x270 — consumers NOT located via the mech→player hop. 0x26c (standard+veteran only) best fits the manual-p22 myomer supercharge clamp (standard locked, expert unlocked) [T4].

Player-facing behavior (Tesla 4.0 manual) [T1 primary source]

Manual pp. 7, 11-14, 21, 22, 24 (the dedicated Simulation Modes page, manual p2, is MISSING from the Tesla40_BT_manual.pdf scan along with p3):

  • Standard: armor-only damage — no criticals, no generator outages, no jams, no heat concerns; MechTech limited to the leg-damage audio warning; viewscreen hunting aids (red hot box + guide arrow, 60° FOV) on; myomer supercharge locked.
  • Veteran: critical hits (any hit can go internal; likelihood rises as armor thins; a crit can kill the mech), generators take damage/go offline (reroute power), ballistic jams (manual clearing), full MechTech audio + smart buttons; viewscreen hunting aids OFF (radar hot box stays).
  • Expert: veteran + heat as a core mechanic: movement generates heat (accel/decel worst), heat slows recharge/top speed, incoming fire causes coolant leaks, ammo-bay fires/cook-off; myomer supercharge allowed.
  • Scoring identical in all modes (p4). Splash/collision damage = a separate technician setting.

4.0 → 4.10 drift [T1]

  • The viewscreen hunting-aid gate is GONE in 4.10: part_014 (HUD/btl4vid TU) reads none of the player flags — the target hotbox/edge arrows (@4cdf6f) are gated on lock state only, every mode. Fits the 4.10 arcade-cities positioning.
  • Movement heat is veteran+expert in 4.10 (the +0x260 gate), not expert-only as the 4.0 manual states. Whether a coefficient still differentiates them is open.

Port state (as of 2026-07-20) — WIRED (issue #2) [T2]

The wiring landed (uncommitted during the coordination pause; issue #2):

  • Seed: the BTPlayer master ctor (btplayer.cpp ~1305) now reads ((BTMission *)playerMission)->ExperienceLevel() + ->AdvancedDamageOn(). The binary's +0x1f8 mission pointer IS the inherited Player::playerMission (master: set by the base ctor; replicant: back-filled from Application::GetCurrentMission() = app+0xc8) — the old separate btMission member was a phantom duplicate, never set on the master branch; removed. The two switch slot errors are fixed (binary rows verified against part_013.c:10843-10870). A [exp] sentinel logs the seeded flags per master player.
  • Renames (btplayer.hpp): showDamageReceived→simLive (0x25c), showKills→heatModelOn (0x260), showDamageInflicted→advancedDamageOn (0x264), roleReturnDelay→advancedDamageOn2 (0x268, was mistyped Scalar), roleReturnDelay2→levelFlag26c (0x26c), showScore→levelFlag270 (0x270), roleClassIndex→experienceLevel (0x274).
  • Bridges (btplayer.cpp, complete-type TU): BTPlayerExperienceSimLive (+0x25c) and BTPlayerExperienceHeatModelOn (+0x260) join BTPlayerRoleLocksAdvanced (+0x274==0). All consumers route mech→GetPlayerLink()→named member; NULL player reads permissive [T3].
  • Gates unified: HeatSink::HeatModelActive() (heat.hpp; also serves mislanch/projweap/ heatfamily call sites) + Myomers::OwnerAdvancedDamage() (myomers.hpp) → the +0x260 bridge; ProjectileWeapon::LiveFireEnabled() (projweap.cpp) + ThermalSight::ControlsAllowLights() (thermalsight.hpp) → the +0x25c bridge (not Searchlight::ControlsAllowLights(), which #61 showed is unused — its handler has no novice gate; the accessor is retained but dead); PoweredSubsystem::HandleMessage @4b0efc (powersub.cpp) → not-novice + the +0x25c bridge (the old "message-manager short-event flag" misattribution and its dead BT_GetMessageManager guard are gone).
  • FUN_004ac9c8 family corrected: MechSubsystem::IsDamaged (mechsub.cpp — was a live raw-offset databinding trap *(*(*(owner+0xd0)+0x190)+0x274)), BT_IsBusLive (btstubs.cpp — was return True), and powersub's IsDamaged() call sites @4b11bc/@4b21d0/@4b179c now all route through BTPlayerRoleLocksAdvanced. HeatSink::IsDamaged (heat.hpp) deliberately keeps the simulationState shadow for the gauge tint (btl4gaug.cpp:172).
  • SHIPPED EGGS ARE ALL expert (every pilot page in all 19 content/*.EGG, camera pages included) — so the default felt behavior is unchanged by the wiring (expert and the old hardwired veteran agree on every LOCATED consumer: sim live, heat on, controls unlocked; they differ only in the unlocated +0x26c flag). The glass front-end writes veteran when a pilot's experience is empty (btl4fe.cpp:307).
  • Myomers movement heat remains arithmetically inert (the Owner* motion accessors still return neutral 0s — the gate is wired, the couplings are the reconciled follow-up).

Corrections (swept 2026-07-18)

  • btplayer.hpp "per-role display toggles from returnFromDeath" for 0x25c-0x274 was WRONG — the source is btMission+0xe4/+0xf0, not the role; the showKills/roleClassIndex names date from the scoring work and don't match the gate semantics (kept in code for now, comments corrected).
  • gauges-hud.md "ROOKIE-role lockout" for FUN_004ac9c8 → novice-EXPERIENCE lockout.
  • open-questions.md "true semantic of player+0x260/0x274 not yet pinned" → pinned here.

Key Relationships