import json import os from pathlib import Path from types import SimpleNamespace import pytest from app.converter.nani_expr import assignment, parse from app.games.tomie.profile import ( ASSEMBLY_HASH, FUNCTIONS, constant_value, extract_tables, identify, ) def instruction(op, operand=None): return SimpleNamespace(mnemonic=op, operand=operand) def test_il_table_reader_accepts_constants_and_refuses_code(): code = [instruction("ldc.i4.2"), instruction("newarr"), instruction("dup"), instruction("ldc.i4.0"), instruction("ldstr", SimpleNamespace(rid=1)), instruction("stelem.ref"), instruction("dup"), instruction("ldc.i4.1"), instruction("ldstr", SimpleNamespace(rid=2)), instruction("stelem.ref")] assert constant_value(code, lambda rid: {1: "A", 2: "B"}[rid]) == ["A", "B"] with pytest.raises(ValueError, match="инициализация"): constant_value([instruction("call")], lambda _: "") with pytest.raises(ValueError, match="Неполная"): constant_value([instruction("ldc.i4.1"), instruction("newarr")], lambda _: "") def test_tomie_profile_requires_exact_assembly(tmp_path): path = tmp_path / "TomieWGM_Data/Managed/Assembly-CSharp.dll" path.parent.mkdir(parents=True) path.write_bytes(b"a renamed unrelated game") assert identify(tmp_path) is None with pytest.raises(ValueError, match="не соответствует"): extract_tables(tmp_path) def test_profile_expression_functions_are_opt_in(): with pytest.raises(ValueError, match="WillpowerDamage"): parse("WillpowerDamage(100, 50, 20, 0, 100)") assert parse("WillpowerDamage(100, 50, 20, 0, 100)", FUNCTIONS)[0] == "call" assert parse('\\"quoted\\"', FUNCTIONS) == ["const", "quoted"] assert parse('If(false, 1/0, 5)', FUNCTIONS)[0] == "if" assert assignment("points+=3", FUNCTIONS)["v"]["ast"] == ["bin", "+", ["name", "points"], ["const", 3]] def test_full_vm_validation_rejects_unimplemented_and_broken_commands(): 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}, {"op": "PlayBgm", "args": {"BgmPath": "song", "CustomPlugin": True}, "line": 3}, {"op": "NativeCode", "args": {}, "line": 4}, {"op": "BeginIf", "args": {"Expression": "true"}, "line": 5}, {"op": "AddChoice", "args": {"OnSelected": ['@set points+1.5']}, "line": 6}, ]}}} issues = validate_runtime(data) assert {item.code for item in issues} == {"missing_target", "unsupported_argument", "unsupported_command", "invalid_branch", "unsupported_choice"} assert not any(item.location == "Main:1" for item in issues) def test_choice_templates_and_quoted_commas_are_validated_without_evaluating_them(): 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}"'}}}, {"op": "AddChoice", "args": {"OnSelected": ['@goto Other.Start']}}, ]}, "Other": {"labels": {"Start": 0}, "commands": []}}} assert validate_runtime(data) == [] with pytest.raises(ValueError, match="Незакрытая"): inline_commands('@set x=If(true, 1') def test_native_effect_and_ui_extensions_are_rejected_before_media_export(): from app.games.tomie.validation import validate_runtime data = {"scripts": {"Main": {"labels": {}, "commands": [ {"op": "Spawn", "args": {"Path": "NativePlugin"}}, {"op": "ShowUI", "args": {"UINames": ["CustomScreen"]}}, {"op": "EndIf", "args": {}}, ]}}} assert {issue.code for issue in validate_runtime(data)} == {"unsupported_effect", "unsupported_ui"} @pytest.mark.skipif(not os.getenv("PGAMES_TOMIE_ORIGINAL"), reason="Requires the user's original Tomie build") def test_original_il_tables_match_the_recovered_port(): from hashlib import sha256 root = Path(os.environ["PGAMES_TOMIE_ORIGINAL"]) assert sha256((root / "TomieWGM_Data/Managed/Assembly-CSharp.dll").read_bytes()).hexdigest() == ASSEMBLY_HASH tables = extract_tables(root) assert {key: len(value) for key, value in tables.items()} == {"names": 2738, "normalComment": 55, "lewdComment": 181, "videos": 191} if os.getenv("PGAMES_TOMIE_PORT"): reference = json.loads((Path(os.environ["PGAMES_TOMIE_PORT"]) / "story/function_data.json").read_text(encoding="utf-8")) assert tables == reference