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
9 changes: 8 additions & 1 deletion src/nnnotes/export.py
Original file line number Diff line number Diff line change
Expand Up @@ -389,6 +389,7 @@ def __init__(self, cat: Catalog, out_dir: Path, player=None, inline_meshes: bool
self._path_sources: list[tuple[str, dict]] = []
self._crcs: dict[str, int] = {}
self.unresolved: dict[str, int] = {} # bundle file name -> references exported as null
self._by_pid: dict[int, list] = {} # path id -> loaded objects with it (named-file fallback)
self._boot_env = None # the boot scene of the player data (builtin_root)
self._boot_graph: SceneGraph | None = None
self._boot_files: set[int] = set()
Expand Down Expand Up @@ -446,6 +447,7 @@ def load(self, key: str):
for o in env.objects:
self._graph_of_file.setdefault(id(o.assets_file), graph)
self._objects[_key(o)] = o
self._by_pid.setdefault(o.path_id, []).append(o)
if o.type.name == "Shader":
self._shader(o)
elif o.type.name == "GameObject":
Expand Down Expand Up @@ -493,7 +495,12 @@ def deref(self, owner, pptr: dict):
name = external_path(owner, pptr)
if not name.lower().rsplit("/", 1)[-1].startswith("cab-"):
raise
# a bundle file no bundle of the catalog provides: the reference is exported as null
# a bundle file no bundle of the catalog provides: its objects may sit in a loaded bundle under another
# file name (a bundle rebuilt after the one that refers to it); a path id held by one loaded object is it
found = self._by_pid.get(pptr["m_PathID"], ())
if len(found) == 1:
return found[0]
# no such object: the reference is exported as null
if name not in self.unresolved:
warnings.warn(f"{name}: no bundle of the catalog holds this file; references to it are null", stacklevel=2)
self.unresolved[name] = self.unresolved.get(name, 0) + 1
Expand Down
2 changes: 1 addition & 1 deletion src/nnnotes/musicdata.py
Original file line number Diff line number Diff line change
Expand Up @@ -1084,7 +1084,7 @@ def record(sid: int) -> dict:
"catalog": {"resourceVersion": catalog.get("resourceVersion"), "sha256": catalog.get("sha256"),
**({"resourceHash": catalog["resourceHash"]} if catalog.get("resourceHash") else {})},
"master": {"source": master_source, "version": master_version,
"tables": {t: {"sha256": table_sha[t]} for t in read}},
"tables": {t: {"sha256": table_sha[t]} for t in read if t in table_sha}},
"exporter": exporter,
"deck": deck.info() if deck is not None else None,
},
Expand Down
41 changes: 41 additions & 0 deletions tests/test_export_deref.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
"""Exporter.deref: references into a bundle file that no catalog bundle provides."""
import pytest

from nnnotes import export


class Obj:
def __init__(self, path_id):
self.path_id = path_id


def make(tmp_path, monkeypatch, objs):
ex = export.Exporter(None, tmp_path)
for o in objs:
ex._by_pid.setdefault(o.path_id, []).append(o)

def missing(owner, pptr):
raise FileNotFoundError("CAB-1")
monkeypatch.setattr(export, "deref", missing)
monkeypatch.setattr(export, "external_path", lambda owner, pptr: "archive:/CAB-1/CAB-1")
return ex


def test_dangling_reference_resolves_to_the_only_loaded_object(tmp_path, monkeypatch):
target = Obj(77)
ex = make(tmp_path, monkeypatch, [target, Obj(78)])
assert ex.deref(None, {"m_FileID": 3, "m_PathID": 77}) is target
assert ex.unresolved == {}


def test_dangling_reference_without_a_holder_is_null(tmp_path, monkeypatch):
ex = make(tmp_path, monkeypatch, [Obj(78)])
with pytest.warns(UserWarning, match="no bundle of the catalog"):
assert ex.deref(None, {"m_FileID": 3, "m_PathID": 77}) is None
assert ex.unresolved == {"archive:/CAB-1/CAB-1": 1}


def test_dangling_reference_held_twice_is_null(tmp_path, monkeypatch):
ex = make(tmp_path, monkeypatch, [Obj(77), Obj(77)])
with pytest.warns(UserWarning):
assert ex.deref(None, {"m_FileID": 3, "m_PathID": 77}) is None
13 changes: 13 additions & 0 deletions tests/test_musicdata.py
Original file line number Diff line number Diff line change
Expand Up @@ -1050,3 +1050,16 @@ def test_aptitude_disabled(tmp_path):
assert doc['deck']['gekisouAptitude'] is None
assert all(c['deck']['gekisouAptitude'] is None for s in doc['songs'] for c in s['charts'])
schema_validator().validate(doc)


def test_master_data_without_the_arena_music_table(tmp_path):
d = master_dir(tmp_path)
(d / "MasterArenaMusic.bin").unlink()
manifest = json.loads((d / "MasterManifest.json").read_text(encoding="utf-8"))
manifest["files"] = [f for f in manifest["files"] if f["name"] != "MasterArenaMusic.bin"]
(d / "MasterManifest.json").write_text(json.dumps(manifest), encoding="utf-8")
musicdata.export(tmp_path / "music.json", deckdata.master_files(d), KEY, CHARTS.__getitem__, bgm, **PROV,
deck=musicdata.Deck(module=FakeDeck(), workers=3))
doc = json.loads((tmp_path / "music.json").read_bytes())
assert "MasterArenaMusic" not in doc["provenance"]["master"]["tables"]
assert "MasterBand" in doc["provenance"]["master"]["tables"]
Loading