refactor: move game-specific import logic into profiles; unblock Tomie import
- Add app/games: GameProfile base, registry, ReaderHooks and renpy/naninovel/ tomie profiles. pipeline, preflight, detect and mods dispatch via the registry - Move tomie_* converter modules to app/games/tomie and split out reader hooks, import, mod validation and known defects - Tolerate the 31 authoring defects of the pinned Tomie build as report warnings (exact list, per-script counts); any new defect still blocks import - Frontend: runtime registry (runtimes.ts) with lazy Tomie launcher - Add profile/registry tests and docs/GAME_PROFILES.md; update IMPORT.md Full media export of the original Tomie build is not yet verified. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
bbbcc6a5d8
commit
12ec82c7e6
30 files changed
+700
-196
No files matched your search
@@ -0,0 +1,54 @@
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from app.converter.compatibility import Issue
|
||||
from app.games import registry
|
||||
from app.games.base import GameProfile
|
||||
from app.games.tomie.defects import KNOWN_DEFECTS, split_known
|
||||
|
||||
|
||||
def test_builtin_profiles_are_registered():
|
||||
ids = {profile.id for profile in registry.all_profiles()}
|
||||
assert {"renpy", "naninovel", "tomie/expansion-1.3801"} <= ids
|
||||
assert registry.for_runtime("tomie/1").id == "tomie/expansion-1.3801"
|
||||
assert registry.for_runtime(None) is None
|
||||
assert registry.for_runtime("unknown/9") is None
|
||||
|
||||
|
||||
def test_specific_profile_wins_over_engine_profile(tmp_path, monkeypatch):
|
||||
assert registry.select("naninovel", tmp_path).id == "naninovel" # hash does not match
|
||||
monkeypatch.setattr("app.games.tomie.identify", lambda root: "tomie/expansion-1.3801")
|
||||
assert registry.select("naninovel", tmp_path).id == "tomie/expansion-1.3801"
|
||||
assert registry.select("renpy", tmp_path).id == "renpy"
|
||||
assert registry.select("godot", tmp_path) is None
|
||||
|
||||
|
||||
def test_new_profile_can_be_registered_once(monkeypatch):
|
||||
monkeypatch.setattr(registry, "_PROFILES", list(registry.all_profiles()))
|
||||
|
||||
class Custom(GameProfile):
|
||||
id = "custom/game-1"
|
||||
engine = "naninovel"
|
||||
specific = True
|
||||
|
||||
def claims(self, root: Path) -> bool:
|
||||
return root.name == "Custom"
|
||||
|
||||
registry.register(Custom())
|
||||
assert registry.select("naninovel", Path("x/Custom")).id == "custom/game-1"
|
||||
assert registry.select("naninovel", Path("x/Other")).id == "naninovel"
|
||||
with pytest.raises(ValueError, match="уже зарегистрирован"):
|
||||
registry.register(Custom())
|
||||
|
||||
|
||||
def test_known_defects_are_waived_only_up_to_the_recorded_count():
|
||||
(code, message, script), allowed = next(iter(KNOWN_DEFECTS.items()))
|
||||
known_issue = Issue(code, message, f"{script}:10")
|
||||
blocking, known = split_known([known_issue] * (allowed + 1))
|
||||
assert len(known) == allowed and len(blocking) == 1
|
||||
new = Issue("missing_target", "Не найден переход Nowhere.Start", "Common:1")
|
||||
blocking, known = split_known([new, known_issue])
|
||||
assert blocking == [new] and known == [known_issue]
|
||||
same_message_other_script = Issue(code, message, "SomewhereElse:1")
|
||||
assert split_known([same_message_other_script])[0] == [same_message_other_script]
|
||||
@@ -7,9 +7,9 @@ def test_native_translation_and_mod_edits_rebuild_the_same_vm_input(tmp_path, mo
|
||||
from app import mods
|
||||
from app.config import get_settings
|
||||
from app.converter.nani_package import Package, text_records
|
||||
from app.converter.naninovel import _convert_package
|
||||
from app.converter.tomie_format import unpack_modules
|
||||
from app.converter.tomie_profile import PROFILE
|
||||
from app.games.tomie.convert import convert_package
|
||||
from app.games.tomie.format import unpack_modules
|
||||
from app.games.tomie.profile import PROFILE
|
||||
from app.translation import apply_changes, locale_tables, text_index
|
||||
monkeypatch.setenv("DATA_DIR", str(tmp_path / "data"))
|
||||
get_settings.cache_clear()
|
||||
@@ -27,9 +27,9 @@ def test_native_translation_and_mod_edits_rebuild_the_same_vm_input(tmp_path, mo
|
||||
}
|
||||
package.texts = text_records("Tips", "tip:Title|Description {points}", "Tips")
|
||||
media = {key: {} for key in ("backgrounds", "characters", "textures", "videos", "audio", "fonts")}
|
||||
monkeypatch.setattr("app.converter.tomie_assets.export_media", lambda *_: ({}, media, {}))
|
||||
monkeypatch.setattr("app.games.tomie.convert.export_media", lambda *_: ({}, media, {}))
|
||||
try:
|
||||
result = _convert_package(tmp_path, tmp_path / "work", lambda _: None, package)
|
||||
result = convert_package(tmp_path, tmp_path / "work", lambda _: None, package)
|
||||
index = text_index(result.manifest)
|
||||
assert {line.source for line in index.lines} == {"Points: {points}", "Title", "Description {points}"}
|
||||
line = next(line for line in index.lines if line.source.startswith("Points:"))
|
||||
@@ -56,7 +56,7 @@ def test_native_translation_and_mod_edits_rebuild_the_same_vm_input(tmp_path, mo
|
||||
|
||||
|
||||
def test_video_recovery_checks_ranges_and_copies_only_declared_bytes(tmp_path):
|
||||
from app.converter.tomie_assets import recover_object
|
||||
from app.games.tomie.assets import recover_object
|
||||
data = tmp_path / "data"
|
||||
out = tmp_path / "out"
|
||||
data.mkdir(); out.mkdir()
|
||||
|
||||
@@ -6,7 +6,7 @@ from types import SimpleNamespace
|
||||
import pytest
|
||||
|
||||
from app.converter.nani_expr import assignment, parse
|
||||
from app.converter.tomie_profile import (
|
||||
from app.games.tomie.profile import (
|
||||
ASSEMBLY_HASH,
|
||||
FUNCTIONS,
|
||||
constant_value,
|
||||
@@ -49,7 +49,7 @@ def test_profile_expression_functions_are_opt_in():
|
||||
|
||||
|
||||
def test_full_vm_validation_rejects_unimplemented_and_broken_commands():
|
||||
from app.converter.tomie_validation import validate_runtime
|
||||
from app.games.tomie.validation import validate_runtime
|
||||
data = {"scripts": {"Main": {"labels": {"Start": 0}, "commands": [
|
||||
{"op": "AddChoice", "args": {"ChoiceSummary": "OK", "OnSelected": ['@set result=If(true, 1, 2)']}, "line": 1},
|
||||
{"op": "Goto", "args": {"Path": ".Missing"}, "line": 2},
|
||||
@@ -64,7 +64,7 @@ def test_full_vm_validation_rejects_unimplemented_and_broken_commands():
|
||||
|
||||
|
||||
def test_choice_templates_and_quoted_commas_are_validated_without_evaluating_them():
|
||||
from app.converter.tomie_validation import inline_commands, validate_runtime
|
||||
from app.games.tomie.validation import inline_commands, validate_runtime
|
||||
assert inline_commands('@set x=If(true, "a,b", "c"), @return') == ['@set x=If(true, "a,b", "c")', '@return']
|
||||
data = {"scripts": {"Main": {"labels": {}, "commands": [
|
||||
{"op": "AddChoice", "args": {"OnSelected": {"template": '@set result="{answer}"'}}},
|
||||
@@ -76,7 +76,7 @@ def test_choice_templates_and_quoted_commas_are_validated_without_evaluating_the
|
||||
|
||||
|
||||
def test_native_effect_and_ui_extensions_are_rejected_before_media_export():
|
||||
from app.converter.tomie_validation import validate_runtime
|
||||
from app.games.tomie.validation import validate_runtime
|
||||
data = {"scripts": {"Main": {"labels": {}, "commands": [
|
||||
{"op": "Spawn", "args": {"Path": "NativePlugin"}},
|
||||
{"op": "ShowUI", "args": {"UINames": ["CustomScreen"]}},
|
||||
|
||||
Reference in new issue
Block a user