diff --git a/docs/music-data.md b/docs/music-data.md index 64c7e00..75abf3c 100644 --- a/docs/music-data.md +++ b/docs/music-data.md @@ -353,6 +353,8 @@ column should be parsed as 32-bit floats. Tables with a column list export those columns, and every row must have them. Tables marked "all" export every column their rows have, in the order the rows first have them; when such a table has no rows, `columns` is empty (the master data carries no field list for an empty table). A reader treats a table missing from `master` as empty. +`MasterCharacter._isNonPlayable` is the game's flag for a non-playable character; a row without it is written as +`false`. | Table | Columns | |---|---| @@ -364,7 +366,7 @@ column their rows have, in the order the rows first have them; when such a table | `MasterSupportCard` | `_id` `_characterIDs` `_rarity` `_cardType` `_performancePowerMax` `_technicPowerMax` `_visualPowerMax` `_supportCardLevelGroup` `_supportCardRankGroup` `_supportSkillId01` `_supportSkillId02` `_gekisouSupportSkillId01` `_gekisouSupportSkillId02` | | `MasterSupportCardLevel` | `_id` `_group` `_level` `_exp` `_performanceRate` `_technicRate` `_visualRate` | | `MasterSupportCardRank` | `_id` `_group` `_rank` `_limitLevel` `_cardTypeLinkBonusRate` `_supportSkill01Level` `_supportSkill02Level` `_gekisouSupportSkill01Level` `_gekisouSupportSkill02Level` | -| `MasterCharacter` | `_id` `_bandID` | +| `MasterCharacter` | `_id` `_bandID` `_isNonPlayable` | | `MasterBand` | `_id` | | `MasterCharacterRank` | `_id` `_rank` `_exp` `_bonus` | | `MasterCharacterTotalRank` | `_id` `_totalRank` `_bonus` | diff --git a/src/nnnotes/deckdata.py b/src/nnnotes/deckdata.py index f0855c7..8c28e07 100644 --- a/src/nnnotes/deckdata.py +++ b/src/nnnotes/deckdata.py @@ -28,7 +28,7 @@ from pathlib import Path from .apkset import ApkSet -from typing import Callable +from typing import Any, Callable DECK_FORMAT = "nnnotes.deck-data/1" # the deck model's input format (ournotes-sim data::FORMAT) CHART_FORMAT = "nnnotes.live-score/1" # score.convert's format: the converter the notes come from @@ -44,6 +44,9 @@ "_effectExecuteLimitResetConditionGroup") _WHOLE = None # every column of the table, in the order the rows first have them +# {table: {column: value}}: columns exported with this value when a row does not have them +DEFAULTS: dict[str, dict[str, Any]] = {"MasterCharacter": {"_isNonPlayable": False}} + # (table, columns), in file order. A table with a column list exports those columns; a whole table all of its own. TABLES: tuple[tuple[str, tuple[str, ...] | None], ...] = tuple( (t, None if c is _WHOLE else tuple(c.split())) for t, c in ( @@ -61,7 +64,7 @@ ("MasterSupportCardLevel", "_id _group _level _exp _performanceRate _technicRate _visualRate"), ("MasterSupportCardRank", "_id _group _rank _limitLevel _cardTypeLinkBonusRate _supportSkill01Level " "_supportSkill02Level _gekisouSupportSkill01Level _gekisouSupportSkill02Level"), - ("MasterCharacter", "_id _bandID"), + ("MasterCharacter", "_id _bandID _isNonPlayable"), ("MasterBand", "_id"), ("MasterCharacterRank", "_id _rank _exp _bonus"), ("MasterCharacterTotalRank", "_id _totalRank _bonus"), @@ -280,7 +283,8 @@ def _value(v, where: str): def master_subset(tables: dict[str, list[dict]]) -> dict: """{table: {"columns", "rows"}} for TABLES, rows in the served order. A table with a column list must have each - of them in every row; a whole table exports the columns its rows have (none when it has no rows).""" + of them in every row, except the columns of DEFAULTS (written as their default when a row lacks them); a whole + table exports the columns its rows have (none when it has no rows).""" out = {} for name, cols in TABLES: rows = tables[name] @@ -289,12 +293,14 @@ def master_subset(tables: dict[str, list[dict]]) -> dict: for r in rows: seen.update(dict.fromkeys(r)) cols = tuple(seen) + defaults = DEFAULTS.get(name, {}) for i, r in enumerate(rows): - missing = [c for c in cols if c not in r] + missing = [c for c in cols if c not in r and c not in defaults] if missing: raise DeckDataError(f"{name}: row {i} (_id {r.get('_id')}) has no column {missing[0]}") out[name] = {"columns": list(cols), - "rows": [[_value(r[c], f"{name} row {i} {c}") for c in cols] for i, r in enumerate(rows)]} + "rows": [[_value(r[c] if c in r else defaults[c], f"{name} row {i} {c}") for c in cols] + for i, r in enumerate(rows)]} return out diff --git a/tests/test_deckdata.py b/tests/test_deckdata.py index da68109..fbd39a2 100644 --- a/tests/test_deckdata.py +++ b/tests/test_deckdata.py @@ -214,6 +214,22 @@ def rows(name): assert document["provenance"]["tables"]["MasterVip"] == hashlib.sha256((tmp_path / "m/MasterVip.bin").read_bytes()).hexdigest() +def test_character_non_playable_flag(tmp_path): + def rows(name): + if name == "MasterCharacter": + return [ + {"_id": 1, "_bandID": 1, "_isNonPlayable": True}, + {"_id": 2, "_bandID": 2, "_isNonPlayable": False}, + {"_id": 3, "_bandID": 1}, # a master without the column + ] + return rows_of(name) + + export(tmp_path, rows=rows) + raw = (tmp_path / "deck.json").read_bytes() + expected = '"MasterCharacter":{"columns":["_id","_bandID","_isNonPlayable"],"rows":[[1,1,true],[2,2,false],[3,1,false]]}' + assert expected.encode() in raw + + def test_character_rank_carries_its_experience_threshold(tmp_path): def rows(name): if name == "MasterCharacterRank":