Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion docs/music-data.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
|---|---|
Expand All @@ -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` |
Expand Down
16 changes: 11 additions & 5 deletions src/nnnotes/deckdata.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 (
Expand All @@ -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"),
Expand Down Expand Up @@ -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]
Expand All @@ -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


Expand Down
16 changes: 16 additions & 0 deletions tests/test_deckdata.py
Original file line number Diff line number Diff line change
Expand Up @@ -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":
Expand Down
Loading