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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
This commit is contained in:
Claude (auto)
2026-08-27 18:22:36 +00:00
parent 1a1a8d98d9
commit 192d3bf67f
5 changed files with 312 additions and 0 deletions

View File

@@ -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.

View File

@@ -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 |

View File

@@ -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

View File

@@ -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 116/1823/2429 — 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.

View File

@@ -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()