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>
143 lines
9.9 KiB
Markdown
143 lines
9.9 KiB
Markdown
---
|
|
id: experience-levels
|
|
title: "Experience levels / simulation modes — novice, standard, veteran, expert"
|
|
status: established
|
|
source_sections: "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"
|
|
related_topics: [subsystems, combat-damage, gauges-hud, pod-hardware, multiplayer, decomp-reference, open-questions]
|
|
key_terms: [master, replicant]
|
|
open_questions:
|
|
- "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 `advancedDamage` → `mission+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
|
|
- Feeds: [[subsystems]] (the heat model + jam/valve gates) · [[combat-damage]] (criticals tier intent) · [[gauges-hud]] (valve guard)
|
|
- Sourced from: the egg ([[multiplayer]] egg format) · `BTMission` (BTMSSN.HPP, T0)
|
|
- Offsets: [[decomp-reference]] §3 (mech+0x190 row + BTPlayer flag block)
|
|
- Manual audit: [[pod-hardware]] §Manual
|