From 192d3bf67f24afd9dd668f5428bf61d488ba789c Mon Sep 17 00:00:00 2001 From: "Claude (auto)" Date: Thu, 27 Aug 2026 18:22:36 +0000 Subject: [PATCH] re: mission scoring and the S/A/B/C/D rank thresholds sub_8230D1F8 -- the loader that contaminated the AA_/AV_ offset search -- is the stage-settings loader. Reading its 122 field NAMES instead of its offsets found the scoring block, which nothing in docs/re owned. 24 IDXD objects per language pack x 6 = 144, each holding Score_Easy / Score_Normal / Score_Hard: 72 records per pack on one 22-field schema, no variants. Difficulty moves 10 of the 22 fields and never the five RankScore_* thresholds -- the rank bar is per stage, difficulty scales the earning rate (x0.5 / x1.0 / x2.0) and the penalties. 23 of 24 objects differ from the commonest Normal record; 9 of 72 records zero the scoring entirely. Not settled: which object is which stage. None of AUTO_SETTINGS's 28 filenames resolves to any of the 24 under 19 prefixes, and 24 vs 28 is unexplained. New doc structures/mission-scoring.md, regenerator mission_scoring.py, artefact data/mission-scoring.txt. Twelve artefacts now. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE --- docs/re/BACKLOG.md | 51 ++++++++++++++ docs/re/INDEX.md | 1 + docs/re/data/mission-scoring.txt | 84 +++++++++++++++++++++++ docs/re/structures/mission-scoring.md | 80 ++++++++++++++++++++++ tools/re-capture/mission_scoring.py | 96 +++++++++++++++++++++++++++ 5 files changed, 312 insertions(+) create mode 100644 docs/re/data/mission-scoring.txt create mode 100644 docs/re/structures/mission-scoring.md create mode 100644 tools/re-capture/mission_scoring.py diff --git a/docs/re/BACKLOG.md b/docs/re/BACKLOG.md index 274b9e5..de3f458 100644 --- a/docs/re/BACKLOG.md +++ b/docs/re/BACKLOG.md @@ -4524,3 +4524,54 @@ a future iteration. `sub_82398CC0` by eye before writing the six-line name-resolution script that settled both candidates. **Write the script first** — it is in the standing rules and I broke it again. + +--- + +## ✅ 2026-08-27 — mission scoring and the S/A/B/C/D rank thresholds + +The side finding from the blocked `AA_`/`AV_` item, followed up. New doc +[`structures/mission-scoring.md`](structures/mission-scoring.md); regenerator +`tools/re-capture/mission_scoring.py`; artefact `data/mission-scoring.txt` +(**TWELVE artefacts now**). + +**Docs checked first:** `isl-condition-builtins.md` owns `AUTO_SETTINGS` (the 28 +`stageNN_settings.tbl` **filenames**); `unit-datasheet-static.md` and +`unit-struct-runtime.md` own the *per-unit* `ScorePoint`/`DamageScore`/ +`MassScore`. **`RankScore`, `TimeBonus`, `KillBonus`, `FFPenalty` and +`Difficulty_*` appear nowhere in `docs/re/`** — the scoring rules were unowned. + +✅ **`sub_8230D1F8` is the stage-settings loader**, not just a rank table: its +122 name→offset pairs cover cameras, post-processing (bloom/DOF/fog/colour), +supply squadrons and movies, phase names — and the scoring block. Reading its +**names** rather than its offsets is what identified it; the offsets were the +contaminant that blocked the `AA_`/`AV_` item. + +✅ **24 IDXD objects per language pack × 6 = 144**, each holding exactly +`Score_Easy` / `Score_Normal` / `Score_Hard` ⇒ 72 records per pack, **one** +22-field schema, no variants. Groups: five `RankScore_*` thresholds; three +`*Score_Adjustment` multipliers; `MainMissionCount`/`Bonus`; three `TimeBonus_*`; +`KillBonus_Maximum`, `FriendlyVesselDamageBonus`, `GettingScoreBasis_*`; five +penalties. Two of the game's typos are join keys: **`TimeBonus_Maximun`** and +**`FFPenalty_Zessel_Maximum`**. + +✅ **Difficulty changes the earning rate, never the bar.** Comparing the three +records inside each object, **10 of 22 fields move and 12 never do — and the 12 +include all five `RankScore_*`**. Multipliers run `×0.5 / ×1.0 / ×2.0`. So a rank +bar is per stage and identical on every difficulty; difficulty scales how fast +you earn against it and how hard penalties bite. Measured over all 24 objects, +not inferred from the names. + +✅ **23 of 24 objects differ** from the commonest `Score_Normal` record, mostly in +the thresholds and the objective count/bonus. **Nine of the 72 records** (three +objects × three difficulties) zero the multipliers, penalties and time bonus — +unscored missions. + +🔴 **Which object is which stage is NOT settled.** None of `AUTO_SETTINGS`'s 28 +filenames resolves to any of the 24 objects under **19** prefixes. **24 objects +vs 28 declared files is itself unexplained** — and the standing rule applies: do +not assume the missing four are the tutorials without opening them. + +**Method note, and it worked:** the offsets that made `sub_8230D1F8` a false +positive last iteration are the same offsets that made it findable — but only +after switching from *offset* to *name*. Read what a loader names, not where it +stores. diff --git a/docs/re/INDEX.md b/docs/re/INDEX.md index f83035a..e4e1d00 100644 --- a/docs/re/INDEX.md +++ b/docs/re/INDEX.md @@ -99,6 +99,7 @@ files, which is how the same ground got covered twice. | [`structures/sound-cue-table.md`](structures/sound-cue-table.md) | The cue index in `tables.pak` — message id -> cue -> sound id -> `.slb` bank | ✅ CONFIRMED, 1 326/1 338 script message ids bind to a bank; SOUNDS and FILES agree on the same 12 absentees, 0 orphan files | | [`structures/cutscene-message-table.md`](structures/cutscene-message-table.md) | Cutscene dialogue — speaker, portrait, on-screen seconds, audio cue per page | ✅ CONFIRMED, field count = 9·PageCount+2 for all 7 PageCounts, 1 252/1 252 caption keys match, 138 ids close both ways | | [`structures/preset-message-rules.md`](structures/preset-message-rules.md) | The reactive-chatter rule table — 64 events, the (message, Yes/No) pair payload, the `Sperkers` roster | ✅ CONFIRMED, the seven fields read off `sub_82213980`, phase tables merge non-additively (224 variants discarded); 8 firing sites share one entry, Probability is a per-cent roll against rand*100.0; Yes = one-shot line; queue is Priority-ordered and capped at 128; the cooldown is Interval + Fluctuation*rand held in the lingering queue node; the 14 player-facing events are voiced by the player set alone (13/15 Katana tables, 0/129 others) (three are seconds ×60, the Yes/No element is a u32 mask, hence the 32 clamp); MessageCount·2 == positional count in 9 216/9 216; five naming routes union to 144/144 (six names predicted from the speaker roster before hashing, control 0/4); 2 388/2 405 message ids bind to a cue | +| [`structures/mission-scoring.md`](structures/mission-scoring.md) | Mission scoring and the S/A/B/C/D rank thresholds — 24 tables x 3 difficulties | ✅ CONFIRMED, one 22-field schema over 72 records; difficulty moves 10 of 22 fields and never the RankScore thresholds; 🔴 which object is which stage unresolved (0/28 names under 19 prefixes) | | [`structures/isl-mission-timer.md`](structures/isl-mission-timer.md) | The mission timer — two sequential countdowns, not a limit and a warning | ✅ CONFIRMED — read from `sub_822639B8`; the threshold reading is | | [`structures/archive-naming.md`](structures/archive-naming.md) | Which archives the disc can name — 100% for menu paks, **0.0%** for the six 2D paks and `GP_READY_ROOM` | ✅ CONFIRMED — 6027 names × 16 prefixes, bimodal | | [`structures/hud-config.md`](structures/hud-config.md) | The in-game HUD configuration — 16 records, 419 asset paths, and a per-stage `ResourceTable` | ✅ CONFIRMED — 0 of 419 config paths resolve as pak entries | diff --git a/docs/re/data/mission-scoring.txt b/docs/re/data/mission-scoring.txt new file mode 100644 index 0000000..c8e2df5 --- /dev/null +++ b/docs/re/data/mission-scoring.txt @@ -0,0 +1,84 @@ +# Mission scoring / rank thresholds (regenerated by mission_scoring.py) + +IDXD objects carrying RankScore_S, per pack: {'GP_MAIN_GAME_D.pak': 24, 'GP_MAIN_GAME_E.pak': 24, 'GP_MAIN_GAME_F.pak': 24, 'GP_MAIN_GAME_I.pak': 24, 'GP_MAIN_GAME_J.pak': 24, 'GP_MAIN_GAME_S.pak': 24} +total across the disc : 144 +difficulty records per object : [('Score_Easy', 'Score_Hard', 'Score_Normal')] +distinct field sets : 1 +records : 72 (22 fields each) + +value census per field (over 72 records): + CraftScore_Adjustment 4 distinct 0.5=21 1.0=21 2.0=21 0.0=9 + FFPenalty_Craft_Basis 2 distinct 1.0=63 0.0=9 + FFPenalty_Craft_Maximum 3 distinct 10000=42 5000=21 0=9 + FFPenalty_Vessel_Basis 2 distinct 1.0=63 0.0=9 + FFPenalty_Zessel_Maximum 3 distinct 10000=42 5000=21 0=9 + FriendlyVesselDamageBonus 4 distinct 0=33 1000=13 2000=13 500=13 + GettingScoreBasis_Maximum 7 distinct 10=48 6=6 7=6 4=3 5=3 8=3 9=3 + GettingScoreBasis_Minimum 2 distinct 3=51 2=21 + KillBonus_Maximum 4 distinct 0=18 1000=18 2000=18 500=18 + LostWingmanPenalty 3 distinct 1000=42 500=21 0=9 + MainMissionBonus 8 distinct 0=18 1000=14 2000=14 4000=9 500=5 1500=4 3000=4 6000=4 + MainMissionCount 3 distinct 1=33 2=27 3=12 + OtherScore_Adjustment 4 distinct 0.5=21 1.0=21 2.0=21 0.0=9 + RankScore_A 21 distinct 10350=6 12500=6 4000=6 100001=3 112350=3 14200=3 15400=3 200001=3 21150=3 22350=3 25000=3 25950=3 27150=3 2930 + RankScore_B 21 distinct 3000=6 7900=6 9000=6 10800=3 11600=3 150001=3 15100=3 15900=3 18000=3 18300=3 19100=3 20200=3 30800=3 35400=3 + RankScore_C 21 distinct 2000=6 5450=6 5500=6 100001=3 10650=3 11000=3 11050=3 11100=3 17400=3 18700=3 20001=3 3333=3 39450=3 4138=3 41 + RankScore_D 9 distinct 3000=24 2000=12 4000=12 1000=6 50001=6 10001=3 1667=3 690=3 833=3 + RankScore_S 19 distinct 9999=9 10000=6 12800=6 16000=6 11200=3 120001=3 148800=3 17600=3 19200=3 27200=3 28800=3 300001=3 32000=3 3360 + TimeBonus_Basis_Maximum 2 distinct 300=63 1200=9 + TimeBonus_Basis_Minimum 3 distinct 120=63 480=6 330=3 + TimeBonus_Maximun 5 distinct 1000=18 2000=18 500=18 0=9 10000=9 + VesselScore_Adjustment 4 distinct 0.5=21 1.0=21 2.0=21 0.0=9 + +fields that differ between the three difficulty records: + {'CraftScore_Adjustment': 21, 'FFPenalty_Craft_Maximum': 21, 'FFPenalty_Zessel_Maximum': 21, 'KillBonus_Maximum': 18, 'LostWingmanPenalty': 21, 'MainMissionBonus': 18, 'OtherScore_Adjustment': 21, 'TimeBonus_Maximun': 18, 'VesselScore_Adjustment': 21, 'FriendlyVesselDamageBonus': 13} + +objects whose Normal record differs from the commonest Normal record: + 23 of 24 objects + 0xd4c05c15 {'MainMissionBonus': '1000', 'MainMissionCount': '1'} + 0xe88492fc {'RankScore_A': '9150', 'RankScore_B': '7100', 'RankScore_C': '5050', 'RankScore_D': '3000', 'RankScore_S': '11200'} + 0xe8a78ef2 {'MainMissionBonus': '1000', 'MainMissionCount': '1', 'RankScore_A': '12500', 'RankScore_B': '9000', 'RankScore_C': '5500', 'RankScore_D': '2000', 'RankScore_S': '16000'} + 0xe98abbfc {'FriendlyVesselDamageBonus': '1000', 'MainMissionBonus': '3000', 'MainMissionCount': '3', 'RankScore_A': '14200', 'RankScore_B': '10800', 'RankScore_C': '7400', 'RankScore_D': '4000', 'RankScore_S': '17600'} + 0xe9adb7f2 {'RankScore_A': '27150', 'RankScore_B': '19100', 'RankScore_C': '11050', 'RankScore_D': '3000', 'RankScore_S': '35200'} + 0xea90e4fc {'FriendlyVesselDamageBonus': '1000', 'GettingScoreBasis_Maximum': '7', 'GettingScoreBasis_Minimum': '2', 'MainMissionBonus': '3000', 'MainMissionCount': '3', 'RankScore_A': '15400', 'RankScore_B': '11600', 'RankScore_C': '7800', 'RankScore_D': '4000', 'RankScore_S': '19200'} + 0xeab3e0f2 {'FriendlyVesselDamageBonus': '1000', 'GettingScoreBasis_Maximum': '5', 'GettingScoreBasis_Minimum': '2', 'MainMissionBonus': '1000', 'MainMissionCount': '1', 'RankScore_A': '52100', 'RankScore_B': '35400', 'RankScore_C': '18700', 'RankScore_D': '2000', 'RankScore_S': '68800'} + 0xeb970dfc {'FriendlyVesselDamageBonus': '1000', 'GettingScoreBasis_Maximum': '4', 'GettingScoreBasis_Minimum': '2', 'RankScore_A': '10350', 'RankScore_B': '7900', 'RankScore_C': '5450', 'RankScore_D': '3000', 'RankScore_S': '12800'} + 0xebba09f2 {'FriendlyVesselDamageBonus': '1000', 'GettingScoreBasis_Maximum': '6', 'GettingScoreBasis_Minimum': '2', 'MainMissionBonus': '1000', 'MainMissionCount': '1', 'RankScore_A': '12500', 'RankScore_B': '9000', 'RankScore_C': '5500', 'RankScore_D': '2000', 'RankScore_S': '16000'} + 0xec9d36fc {'RankScore_A': '10350', 'RankScore_B': '7900', 'RankScore_C': '5450', 'RankScore_D': '3000', 'RankScore_S': '12800'} + 0xecc032f2 {'RankScore_A': '22350', 'RankScore_B': '15900', 'RankScore_C': '9450', 'RankScore_D': '3000', 'RankScore_S': '28800'} + 0xeda35ffc {'FriendlyVesselDamageBonus': '1000', 'GettingScoreBasis_Maximum': '8', 'GettingScoreBasis_Minimum': '2', 'MainMissionBonus': '3000', 'MainMissionCount': '3', 'RankScore_A': '25000', 'RankScore_B': '18000', 'RankScore_C': '11000', 'RankScore_D': '4000', 'RankScore_S': '32000'} + 0xedc65bf2 {'FriendlyVesselDamageBonus': '1000', 'GettingScoreBasis_Maximum': '9', 'RankScore_A': '112350', 'RankScore_B': '75900', 'RankScore_C': '39450', 'RankScore_D': '3000', 'RankScore_S': '148800'} + 0xede957e8 {'CraftScore_Adjustment': '0.0', 'FFPenalty_Craft_Basis': '0.0', 'FFPenalty_Craft_Maximum': '0', 'FFPenalty_Vessel_Basis': '0.0', 'FFPenalty_Zessel_Maximum': '0', 'KillBonus_Maximum': '0', 'LostWingmanPenalty': '0', 'MainMissionBonus': '0', 'MainMissionCount': '1', 'OtherScore_Adjustment': '0.0', 'RankScore_A': '8333', 'RankScore_B': '5833', 'RankScore_C': '3333', 'RankScore_D': '833', 'RankScore_S': '9999', 'TimeBonus_Basis_Maximum': '1200', 'TimeBonus_Basis_Minimum': '480', 'TimeBonus_Maximun': '10000', 'VesselScore_Adjustment': '0.0'} + 0xeea988fc {'FriendlyVesselDamageBonus': '1000', 'RankScore_A': '25950', 'RankScore_B': '18300', 'RankScore_C': '10650', 'RankScore_D': '3000', 'RankScore_S': '33600'} + 0xeecc84f2 {'MainMissionBonus': '1000', 'MainMissionCount': '1', 'RankScore_A': '29300', 'RankScore_B': '20200', 'RankScore_C': '11100', 'RankScore_D': '2000', 'RankScore_S': '38400'} + 0xeeef80e8 {'FriendlyVesselDamageBonus': '1000', 'KillBonus_Maximum': '0', 'MainMissionBonus': '0', 'MainMissionCount': '1', 'RankScore_A': '200001', 'RankScore_B': '150001', 'RankScore_C': '100001', 'RankScore_D': '50001', 'RankScore_S': '300001', 'TimeBonus_Maximun': '0'} + 0xefafb1fc {'FriendlyVesselDamageBonus': '1000', 'GettingScoreBasis_Maximum': '7', 'GettingScoreBasis_Minimum': '2', 'RankScore_A': '21150', 'RankScore_B': '15100', 'RankScore_C': '9050', 'RankScore_D': '3000', 'RankScore_S': '27200'} + 0xeff5a9e8 {'FriendlyVesselDamageBonus': '1000', 'KillBonus_Maximum': '0', 'MainMissionBonus': '0', 'MainMissionCount': '1', 'RankScore_A': '60001', 'RankScore_B': '40001', 'RankScore_C': '20001', 'RankScore_D': '10001', 'RankScore_S': '80001', 'TimeBonus_Maximun': '0'} + 0xf0b5dafc {'FriendlyVesselDamageBonus': '1000', 'GettingScoreBasis_Maximum': '6', 'GettingScoreBasis_Minimum': '2', 'MainMissionBonus': '3000', 'MainMissionCount': '3', 'RankScore_A': '44200', 'RankScore_B': '30800', 'RankScore_C': '17400', 'RankScore_D': '4000', 'RankScore_S': '57600'} + 0xf0fbd2e8 {'CraftScore_Adjustment': '0.0', 'FFPenalty_Craft_Basis': '0.0', 'FFPenalty_Craft_Maximum': '0', 'FFPenalty_Vessel_Basis': '0.0', 'FFPenalty_Zessel_Maximum': '0', 'KillBonus_Maximum': '0', 'LostWingmanPenalty': '0', 'MainMissionBonus': '0', 'MainMissionCount': '1', 'OtherScore_Adjustment': '0.0', 'RankScore_A': '8750', 'RankScore_B': '6667', 'RankScore_C': '4167', 'RankScore_D': '1667', 'RankScore_S': '9999', 'TimeBonus_Basis_Maximum': '1200', 'TimeBonus_Basis_Minimum': '480', 'TimeBonus_Maximun': '10000', 'VesselScore_Adjustment': '0.0'} + 0xf1020211 {'CraftScore_Adjustment': '0.0', 'FFPenalty_Craft_Basis': '0.0', 'FFPenalty_Craft_Maximum': '0', 'FFPenalty_Vessel_Basis': '0.0', 'FFPenalty_Zessel_Maximum': '0', 'KillBonus_Maximum': '0', 'LostWingmanPenalty': '0', 'MainMissionBonus': '0', 'MainMissionCount': '1', 'OtherScore_Adjustment': '0.0', 'RankScore_A': '8276', 'RankScore_B': '6897', 'RankScore_C': '4138', 'RankScore_D': '690', 'RankScore_S': '9999', 'TimeBonus_Basis_Maximum': '1200', 'TimeBonus_Basis_Minimum': '330', 'TimeBonus_Maximun': '10000', 'VesselScore_Adjustment': '0.0'} + 0xf2082b11 {'FriendlyVesselDamageBonus': '1000', 'KillBonus_Maximum': '0', 'MainMissionBonus': '0', 'MainMissionCount': '1', 'RankScore_A': '100001', 'RankScore_B': '80001', 'RankScore_C': '65001', 'RankScore_D': '50001', 'RankScore_S': '120001', 'TimeBonus_Maximun': '0'} + +the commonest Normal record: + CraftScore_Adjustment 1.0 + FFPenalty_Craft_Basis 1.0 + FFPenalty_Craft_Maximum 10000 + FFPenalty_Vessel_Basis 1.0 + FFPenalty_Zessel_Maximum 10000 + FriendlyVesselDamageBonus 0 + GettingScoreBasis_Maximum 10 + GettingScoreBasis_Minimum 3 + KillBonus_Maximum 1000 + LostWingmanPenalty 1000 + MainMissionBonus 2000 + MainMissionCount 2 + OtherScore_Adjustment 1.0 + RankScore_A 4000 + RankScore_B 3000 + RankScore_C 2000 + RankScore_D 1000 + RankScore_S 10000 + TimeBonus_Basis_Maximum 300 + TimeBonus_Basis_Minimum 120 + TimeBonus_Maximun 1000 + VesselScore_Adjustment 1.0 diff --git a/docs/re/structures/mission-scoring.md b/docs/re/structures/mission-scoring.md new file mode 100644 index 0000000..79b2bb0 --- /dev/null +++ b/docs/re/structures/mission-scoring.md @@ -0,0 +1,80 @@ +# Mission scoring and the S/A/B/C/D rank thresholds + +**✅ Settled 2026-08-27** (static; regenerator +`tools/re-capture/mission_scoring.py`, artefact +[`data/mission-scoring.txt`](../data/mission-scoring.txt)). + +Found by following `sub_8230D1F8` — the loader that the `AA_`/`AV_` investigation +turned up as a *contaminant*, because it stores at the same object offsets the +unit definition uses. Reading its field names instead of its offsets identified +it as the **stage-settings loader**, and the scoring block is the part of it the +corpus did not have. + +**Docs checked:** `isl-condition-builtins.md` owns `AUTO_SETTINGS` — the 28 +`stageNN_settings.tbl` / `tutorialNN` / `challengeNN` **filenames**; +`unit-datasheet-static.md` and `unit-struct-runtime.md` own the *per-unit* +`ScorePoint` / `DamageScore` / `MassScore` fields. **What none of them says is how +a mission score is assembled or what earns a rank** — `RankScore`, `TimeBonus`, +`KillBonus`, `FFPenalty`, `Difficulty_*` appear nowhere in `docs/re/`. + +## The tables + +**24 IDXD objects per language pack × 6 packs = 144.** Each holds exactly three +records — `Score_Easy`, `Score_Normal`, `Score_Hard` — so **72 scoring records +per pack**, and they share **one** 22-field schema with no variants. + +| group | fields | +|---|---| +| rank thresholds | `RankScore_S` `_A` `_B` `_C` `_D` | +| score multipliers | `CraftScore_Adjustment`, `VesselScore_Adjustment`, `OtherScore_Adjustment` | +| objective bonus | `MainMissionCount`, `MainMissionBonus` | +| time bonus | `TimeBonus_Maximun` (sic), `TimeBonus_Basis_Minimum`, `TimeBonus_Basis_Maximum` | +| kill / escort bonus | `KillBonus_Maximum`, `FriendlyVesselDamageBonus`, `GettingScoreBasis_Minimum`, `GettingScoreBasis_Maximum` | +| penalties | `LostWingmanPenalty`, `FFPenalty_Craft_Basis`, `FFPenalty_Craft_Maximum`, `FFPenalty_Vessel_Basis`, `FFPenalty_Zessel_Maximum` (sic — "Vessel") | + +Two of the game's own misspellings are load-bearing as join keys: +**`TimeBonus_Maximun`** and **`FFPenalty_Zessel_Maximum`**. + +## ✅ Difficulty changes the earning rate, not the bar + +Comparing the three difficulty records inside each object, **10 of the 22 fields +move and 12 never do** — and the ones that never move include **all five +`RankScore_*` thresholds**: + +``` +differ between Easy/Normal/Hard CraftScore_Adjustment 21 VesselScore_Adjustment 21 +(count = objects out of 24) OtherScore_Adjustment 21 LostWingmanPenalty 21 + FFPenalty_Craft_Maximum 21 FFPenalty_Zessel_Maximum 21 + KillBonus_Maximum 18 MainMissionBonus 18 + TimeBonus_Maximun 18 FriendlyVesselDamageBonus 13 +never differ RankScore_S/A/B/C/D, MainMissionCount, + GettingScoreBasis_*, TimeBonus_Basis_*, + FFPenalty_*_Basis +``` + +So a rank bar is **per stage**, identical on every difficulty; what difficulty +changes is how fast you earn against it (`×0.5` / `×1.0` / `×2.0` on the three +`*Score_Adjustment` multipliers) and how hard the penalties bite. That is a +design statement a port has to reproduce exactly, and it is measured, not read +off the names. + +## Per-stage variation + +**23 of the 24 objects differ from the commonest `Score_Normal` record**, almost +always in the `RankScore_*` thresholds and the objective count/bonus — as +expected for per-mission tuning. Nine of the 72 records (three objects × three +difficulties) zero the multipliers, the penalties and `TimeBonus_Maximun` +outright: missions that are not scored. Full per-object diffs are in the +artefact. + +## ❔ Which object is which stage + +Not settled. `AUTO_SETTINGS` names 28 files (`stage01…16`, `tutorial01…06`, +`challenge01…06`, tags 1–16/18–23/24–29 — tag 17 absent, the cut S17), but +**none of those 28 names resolves to any of the 24 objects** under 19 prefixes +(bare, `stage\`, `Stage\`, `message\`, `settings\`, `2d\`, `ui\`, `system\`, +`dat\`, `prt\`, `view\`, `language\`, `eng\`, `script\`, `Script\`, `mission\`, +`Mission\`, `param\`, `Param\`). So either the settings files are loaded outside +the pak namespace, or the scoring objects are keyed by something else entirely. +**24 objects against 28 declared files is itself unexplained** — do not assume +the missing four are the tutorials without checking. diff --git a/tools/re-capture/mission_scoring.py b/tools/re-capture/mission_scoring.py new file mode 100644 index 0000000..a3a092e --- /dev/null +++ b/tools/re-capture/mission_scoring.py @@ -0,0 +1,96 @@ +#!/usr/bin/env python3 +"""Regenerate docs/re/data/mission-scoring.txt. + +The per-stage mission scoring / rank-threshold tables: 24 IDXD objects per +language pack, each holding Score_Easy / Score_Normal / Score_Hard with one +22-field schema. Loader: sub_8230D1F8. + +Run: python3 mission_scoring.py > ../../docs/re/data/mission-scoring.txt +""" +import sys, os, glob, collections +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from unit_substructures import pak_entries +import unitgroup as U + +DAT = '/work/sylph_extract/dat' +KEY = 'RankScore_S' +DIFFS = ('Score_Easy', 'Score_Normal', 'Score_Hard') + + +def main(): + percopy = {} + for p in sorted(glob.glob(os.path.join(DAT, '*.pak'))): + n = sum(1 for _, s in pak_entries(p) + if s[:4] == b'IDXD' and KEY.encode() in s) + if n: + percopy[os.path.basename(p)] = n + + P = os.path.join(DAT, 'GP_MAIN_GAME_E.pak') + tables = [] + for h, s in pak_entries(P): + if s[:4] != b'IDXD' or KEY.encode() not in s: + continue + try: + recs = U.parse(s) + except Exception: + continue + tables.append((h, {r['squadron']: U.named(r) for r in recs + if r['squadron'] in DIFFS})) + tables.sort() + + print('# Mission scoring / rank thresholds (regenerated by mission_scoring.py)') + print() + print('IDXD objects carrying %s, per pack: %s' % (KEY, percopy)) + print('total across the disc : %d' % sum(percopy.values())) + print('difficulty records per object : %s' % sorted( + {tuple(sorted(t)) for _, t in tables})) + + schemas = collections.Counter() + for _, t in tables: + for d in DIFFS: + schemas[tuple(sorted(t[d]))] += 1 + print('distinct field sets : %d' % len(schemas)) + fields = sorted(schemas.most_common(1)[0][0]) + print('records : %d (%d fields each)' % ( + sum(schemas.values()), len(fields))) + + # how much does each field actually vary? + print() + print('value census per field (over %d records):' % sum(schemas.values())) + for f in fields: + c = collections.Counter(t[d][f] for _, t in tables for d in DIFFS) + vals = ' '.join('%s=%d' % (k, v) for k, v in sorted( + c.items(), key=lambda kv: (-kv[1], kv[0]))) + print(' %-32s %2d distinct %s' % (f, len(c), vals[:110])) + + # does difficulty move anything? + print() + print('fields that differ between the three difficulty records:') + moved = collections.Counter() + for _, t in tables: + for f in fields: + if len({t[d][f] for d in DIFFS}) > 1: + moved[f] += 1 + print(' %s' % (dict(moved) if moved else 'NONE — Easy/Normal/Hard are identical everywhere')) + + # per-object variation + print() + print('objects whose Normal record differs from the commonest Normal record:') + base = collections.Counter(tuple(sorted(t['Score_Normal'].items())) + for _, t in tables).most_common(1)[0][0] + odd = [(h, t) for h, t in tables + if tuple(sorted(t['Score_Normal'].items())) != base] + print(' %d of %d objects' % (len(odd), len(tables))) + for h, t in odd: + diff = {f: t['Score_Normal'][f] for f in fields + if t['Score_Normal'][f] != dict(base)[f]} + print(' %#010x %s' % (h, diff)) + + print() + print('the commonest Normal record:') + for k, v in sorted(dict(base).items()): + print(' %-32s %s' % (k, v)) + + +if __name__ == '__main__': + main()