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:
@@ -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.
|
||||
|
||||
@@ -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 |
|
||||
|
||||
84
docs/re/data/mission-scoring.txt
Normal file
84
docs/re/data/mission-scoring.txt
Normal 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
|
||||
80
docs/re/structures/mission-scoring.md
Normal file
80
docs/re/structures/mission-scoring.md
Normal 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 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.
|
||||
96
tools/re-capture/mission_scoring.py
Normal file
96
tools/re-capture/mission_scoring.py
Normal 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()
|
||||
Reference in New Issue
Block a user