Files
spoopyghosty0 277dd99645 Cpp2IL crashes aren't "code not found"; triage orders replayed logcat lines by date (#293)
- il2cpp._run: a non-zero exit or no .cs file under DiffableCs is an unreadable game. The release Cpp2IL dies on
  BattleTalent (Unity 2021.3.45f2, exit 134, IndexOutOfRangeException in CsFileUtils.GetTypeName) after making the
  folder tree; every class was recorded "missing" and cached.
- find_methods: any release failure gets one try with the development build (it reads BattleTalent), not only an
  unsupported metadata version; never with a caller's Cpp2IL or FRAMEPORT_CPP2IL. Errors are cached as errors,
  never as missing classes; cache entries with no class found and every wanted class missing are run again.
- Il2cppReturnPatch: Cpp2IL failures become the same WARN check as "code not found" but with Cpp2IL's error line;
  the method list in the WARN has no duplicates.
- triage: logcat times include the date (logcat replays kernel lines dated days earlier ahead of the game's; by time
  of day the "past midnight" guess gave a healthy run frames-stopped 83261 s). Year wrap handled.
2026-10-17 01:52:47 -04:00

485 lines
25 KiB
Python
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Unity XR settings fixed in libil2cpp.so: OVRManager's runtime MSAA and the Oculus XR Plugin's multiview."""
from pathlib import Path
from test_patches import _analysis
from frameport.analysis import il2cpp
from frameport.patches import base
from frameport.patches.frame import unity_runtime as R
from frameport.patches.frame import unity_text_input as U
from frameport.validate.triage import triage
def _lib() -> bytes:
return sorted(Path(__file__).parent.joinpath("fixtures").glob("libfake*_arm64.so"))[0].read_bytes()
def test_all_il2cpp_patches_share_one_cpp2il_lookup():
base.load_all()
for p in (U.UnityTextInput, R.UnityRuntimeMsaa, R.UnityMultiPass):
for cls, methods in p.targets.items():
assert set(methods) <= set(U.ALL_TARGETS[cls])
# keyed by the build's metadata: an earlier patch's bytes in libil2cpp.so don't force another Cpp2IL run
assert il2cpp._cache_file(b"a" * 10, b"meta") == il2cpp._cache_file(b"b" * 10, b"meta")
assert il2cpp._cache_file(b"a" * 10, b"meta") != il2cpp._cache_file(b"a" * 10, b"other")
def test_runtime_patches_write_zero_returns():
lib = _lib()
lo, _hi = U._executable(lib)[0]
a, b = (lo + 0x40) & ~3, (lo + 0x80) & ~3
found = {"Assembly-CSharp/OVRDisplay.cs": {"get_recommendedMSAALevel": (a, 0x5C)},
"Unity.XR.Oculus/Unity/XR/Oculus/OculusSettings.cs": {"GetStereoRenderingMode": (b, 8)}}
msaa, notes = U.patch_methods(lib, found, R.UnityRuntimeMsaa.targets)
assert msaa[a:a + 8] == R.RET_ZERO and msaa[b:b + 8] == lib[b:b + 8] and "OVRDisplay" in notes[0]
multi, _ = U.patch_methods(lib, found, R.UnityMultiPass.targets)
assert multi[b:b + 8] == R.RET_ZERO and multi[a:a + 8] == lib[a:a + 8]
def test_runtime_msaa_suggested_for_gles_ovr_games_only():
base.load_all()
p = base.get("frame.unity_runtime_msaa_off")
gles = _analysis(libs=["libil2cpp.so", "libunity.so"], graphics="GLES or unknown (no Vulkan declaration)",
extra={"ovr_runtime_msaa": True})
assert p.detect(gles).recommended
assert p.detect(_analysis(libs=["libil2cpp.so"], extra={"ovr_runtime_msaa": True})) is None # Vulkan
gles.extra["ovr_runtime_msaa"] = False
assert p.detect(gles) is None and p.applies(gles)
assert base.get("frame.unity_multipass").detect(gles) is None # opt-in: only for the symptom
assert not base.get("frame.unity_multipass").applies(_analysis(libs=["libunity.so"])) # Mono: no Cpp2IL
def test_triage_points_runtime_msaa_to_the_patch():
log = ("09-28 17:39:01.000 1000 1000 I ActivityManager: Start proc 1147:com.example.game/u0a55 for activity\n"
"09-28 17:39:04.000 1147 1174 I Unity : The current MSAA level is 0, but the recommended MSAA level is "
"4. Switching to the recommended level.\n")
f = {x.id: x for x in triage(log, "RUNNING", "com.example.game").findings}
assert "frame.unity_runtime_msaa_off" in f["unity-runtime-msaa"].suggest
def test_library_recipes_follow_their_catalog_entry(tmp_path, monkeypatch):
import json
from frameport.core import library
from frameport.recommend import catalog, engine
monkeypatch.setattr(library, "_path", lambda: tmp_path / "library.json")
entry = catalog.CatalogEntry.from_dict({"package": "com.x.game", "title": "X", "status": "issues",
"frame": ["frame.unity_multipass"], "updated": "2026-10-04"}, "bundled")
monkeypatch.setattr(catalog, "lookup", lambda pkg: entry if pkg in ("com.x.game", "com.x.mine") else None)
an = {"package": "com.x.game", "engine": "Unity", "libs": ["libil2cpp.so", "libunity.so"], "abis": ["arm64-v8a"]}
games = {"com.x.game": {"analysis": an, "recipe": {"package": "com.x.game", "source": "heuristics"}},
"com.x.mine": {"analysis": dict(an, package="com.x.mine"),
"recipe": {"package": "com.x.mine", "source": "user", "patches": {"frame.adapter": {}}}}}
(tmp_path / "library.json").write_text(json.dumps({"games": games, "settings": {}}))
got = library.load()["games"]
r = got["com.x.game"]["recipe"]
assert "frame.unity_multipass" in r["patches"] and r["status"] == "issues" and r["catalog_rev"] == entry.rev()
mine = got["com.x.mine"]["recipe"] # the user's own choice stays (one-time migrations aside)
assert "frame.adapter" in mine["patches"] and "frame.unity_multipass" not in mine["patches"]
assert not library._follow_catalog(library.load()) # nothing changed since: no re-derive on every load
assert engine.suggest(library.analysis_from_dict(an)).catalog_rev == entry.rev()
def test_maintained_entry_verified_later_beats_the_users_shared_one():
from frameport.recommend import catalog
def e(origin, **kw):
return catalog.CatalogEntry.from_dict({"package": "p.q", "title": "P", **kw}, origin)
user = e("user", verified={"date": "2026-10-03"})
fixed, old = e("bundled", updated="2026-10-04"), e("bundled", verified={"date": "2026-09-28"})
assert catalog._newer(fixed, user) and not catalog._newer(old, user) and not catalog._newer(None, user)
def test_inlined_getter_field_read_is_rewritten():
lib = bytearray(_lib())
lo, _hi = U._executable(bytes(lib))[0]
m = (lo + 0x40) & ~3
lib[m:m + 4] = (0xB9401C00 | 20 << 5 | 22).to_bytes(4, "little") # ldr w22, [x20, #0x1c]
lib[m + 4:m + 8] = (0x39408000 | 20 << 5 | 8).to_bytes(4, "little") # ldrb w8, [x20, #0x20]: another field
out, notes = U.patch_field_loads(bytes(lib), (m, 16), 0x1C, 0, "Initialize")
assert out[m:m + 4] == (0x52800000 | 22).to_bytes(4, "little") and out[m + 4:m + 8] == lib[m + 4:m + 8]
assert U.patch_field_loads(out, (m, 16), 0x1C, 0, "Initialize")[0] is None # idempotent
import pytest
with pytest.raises(RuntimeError, match="no load"):
U.patch_field_loads(bytes(_lib()), (m, 16), 0x1C, 0, "Initialize")
def test_cpp2il_field_offsets_are_parsed():
cs = ("public class OculusSettings : ScriptableObject\n{\n\t[SerializeField]\n"
"\tpublic StereoRenderingModeAndroid m_StereoRenderingModeAndroid; //Field offset: 0x1C\n}\n")
assert il2cpp.parse_methods(cs, ["field:m_StereoRenderingModeAndroid", "Missing"]) == {
"field:m_StereoRenderingModeAndroid": (0x1C, 0)}
assert "field:m_StereoRenderingModeAndroid" in U.ALL_TARGETS[R.UnityMultiPass.SETTINGS]
def test_recipe_changes_and_revised_patches_mark_builds_outdated():
from frameport.patches.base import recipe_fingerprint, revised_since_unrecorded
from frameport.ui.components import recipe_changed
base.load_all()
r = {"patches": {"frame.unity_text_input": {}}, "use_alt": False}
game = {"recipe": r, "build": {"sha256": "a", "recipe_fp": recipe_fingerprint(r)}}
assert not recipe_changed(game)
r["use_alt"] = True # e.g. a catalog fix switched to the alternate build
assert recipe_changed(game)
assert not revised_since_unrecorded({"patches": {"frame.unity_text_input": {}}})
assert revised_since_unrecorded({"patches": {"frame.unity_multipass": {}}}) # fixed after 0.6.3
assert revised_since_unrecorded({"patches": {"adapter.vk_shader_fix": {"value": "x"}}}) # needs the new shim
def test_packaged_app_shows_artwork_by_file_path(tmp_path, monkeypatch):
from frameport.artwork import thumbs
monkeypatch.setattr(thumbs, "user_data_dir", lambda: tmp_path)
art = tmp_path / "artwork" / "p.q" / "cover.jpg"
assert thumbs.asset_url(art) == "/artwork/p.q/cover.jpg" # web view / source run: URL under assets_dir
thumbs.use_file_paths(True)
try:
assert thumbs.asset_url(art) == str(art.resolve())
finally:
thumbs.use_file_paths(False)
def test_unity_oculus_check_for_unity_with_the_check():
base.load_all()
p = base.get("frame.unity_oculus_check")
old = _analysis(libs=["libunity.so", "libOVRPlugin.so"],
extra={"unity_version": "2017.4.23f1", "unity_oculus_check": True})
assert p.applies(old) and p.detect(old).recommended
new = _analysis(libs=["libunity.so", "libOVRPlugin.so"],
extra={"unity_version": "2019.4.35f1", "unity_oculus_check": True})
# 2019 too (BattleSisters, Unity 2019.4, stayed a 2D app without it)
assert p.applies(new) and p.detect(new).recommended
# the frame-wait shim: Unity's built-in VR loop (2017-2018, 2019 without the Oculus XR Plugin), not XR Plugin games
assert p.legacy_loop(old) and p.legacy_loop(new)
assert not p.legacy_loop(_analysis(libs=["libunity.so", "libOVRPlugin.so", "libOculusXRPlugin.so"],
extra={"unity_version": "2019.4.35f1"}))
assert not p.legacy_loop(_analysis(libs=["libunity.so"], extra={"unity_version": "2022.3.19f1"}))
none = _analysis(libs=["libunity.so", "libOVRPlugin.so"], extra={"unity_version": "2019.4.35f1"})
assert not p.applies(none) and p.detect(none) is None # no check in libunity.so: nothing to change
log = "10-04 15:08:01.000 1213 1235 I Unity : [NewtonVR] Critical Error: Oculus / SteamVR not setup properly"
assert "frame.unity_oculus_check" in {s for f in triage(log, "RUNNING", None).findings for s in f.suggest}
def test_app_update_refreshes_derived_recipes_once(tmp_path, monkeypatch):
import json
from frameport.core import library
monkeypatch.setattr(library, "_path", lambda: tmp_path / "library.json")
monkeypatch.setattr(library, "REFRESH_ON_UPDATE", True)
an = {"package": "com.own.engine", "engine": "Other", "libs": ["libgame.so"], "abis": ["arm64-v8a"]}
custom = {"frame.adapter": {}, "adapter.scale": {"value": 1.5}}
games = {"com.own.engine": {"analysis": an, "recipe": {"package": "com.own.engine", "source": "heuristics",
"patches": dict(custom)}},
"com.mine": {"analysis": dict(an, package="com.mine"),
"recipe": {"package": "com.mine", "source": "user", "patches": dict(custom)}}}
(tmp_path / "library.json").write_text(json.dumps({"games": games, "settings": {"recipes.app_version": "0.0.1"}}))
got = library.load()["games"]
assert "frame.vk_sanitize" in got["com.own.engine"]["recipe"]["patches"] # a newer automatic fix reaches it
assert "adapter.scale" not in got["com.own.engine"]["recipe"]["patches"] # derived again from scratch
assert got["com.mine"]["recipe"]["patches"]["adapter.scale"] == {"value": 1.5} # the user's own recipe stays
assert not library._follow_catalog(library.load()) # once per app version
def test_sdl_clipboard_patch_for_2d_sdl_apps():
from frameport.recommend import engine
base.load_all()
a = _analysis(package="org.love2d.android", engine="Other", libs=["liblove.so"], is_overport_output=False,
extra={"sdl_java": True, "vr_kind": "none"})
r = engine.suggest(a)
assert "frame.sdl_clipboard" in r.patches and not r.as_is # an APK edit: not installed unchanged
a.extra["sdl_java"] = False
assert "frame.sdl_clipboard" not in engine.suggest(a).patches
def test_uninstall_can_delete_the_games_files_on_this_pc(tmp_path):
from frameport import pipeline
from frameport.core import library
from frameport.core.paths import output_dir, user_data_dir
game = tmp_path / "Game v1.2"
(game / "com.x.game").mkdir(parents=True)
(game / "com.x.game.apk").write_bytes(b"PK" * 10)
(game / "com.x.game/main.obb").write_bytes(b"o" * 30)
shared = tmp_path / "Shared"
(shared / "data").mkdir(parents=True)
(shared / "b.apk").write_bytes(b"PK")
conv = output_dir() / "Game v1.2"
conv.mkdir(parents=True)
(conv / "com.x.game.apk").write_bytes(b"c")
key = user_data_dir() / "overport-workspace/signatures/com.x.game.keystore"
key.parent.mkdir(parents=True)
key.write_bytes(b"k")
library.upsert_game("com.x.game", apk=str(game / "com.x.game.apk"), data_dir=str(game / "com.x.game"),
build={"apk": str(conv / "com.x.game.apk")})
library.upsert_game("com.y.other", apk=str(shared / "b.apk"), data_dir=str(shared / "data"))
library.upsert_game("com.z.third", apk=str(shared / "c.apk"), data_dir=str(shared / "data"))
assert set(pipeline.local_game_files("com.x.game")) == {game / "com.x.game.apk", game / "com.x.game",
conv / "com.x.game.apk"}
assert pipeline.local_game_files("com.y.other") == [shared / "b.apk"] # its data folder is another game's too
done, freed = pipeline.delete_local_files("com.x.game")
assert not game.exists() and freed == 51 and library.game("com.x.game") is None
assert key.exists() and shared.exists() # signing keys and other games' files stay
def test_deleting_a_games_files_takes_its_download_manifest(tmp_path):
from frameport import pipeline
from frameport.core import library
def release(name, extra=""):
d = tmp_path / name
(d / "com.x.game").mkdir(parents=True)
(d / "com.x.game.apk").write_bytes(b"PK")
(d / "com.x.game/main.1.com.x.game.obb").write_bytes(b"o")
(d / "release.manifest").write_text(
"#VRPRELEASEMANIFEST 1.0\nGame Name;Release Name\nGame;Game v1\n\n#filelist\ntype;name;size\n"
"f;./com.x.game.apk;2\nd;./com.x.game;0\nf;./com.x.game/main.1.com.x.game.obb;1\n" + extra,
encoding="utf-8")
return d
game = release("Game v1")
library.upsert_game("com.x.game", apk=str(game / "com.x.game.apk"), data_dir=str(game / "com.x.game"))
assert game / "release.manifest" in pipeline.local_game_files("com.x.game")
pipeline.delete_local_files("com.x.game")
assert not game.exists() # the folder went too: a download manager no longer lists the game
other = release("Game v2", extra="f;./notes.txt;5\n") # lists a file that isn't the game's: stays
library.upsert_game("com.x.game", apk=str(other / "com.x.game.apk"), data_dir=str(other / "com.x.game"))
assert other / "release.manifest" not in pipeline.local_game_files("com.x.game")
def test_scan_finds_games_in_download_manager_layouts(tmp_path, monkeypatch):
import zipfile
from frameport.sources import quest_dump
monkeypatch.setattr(quest_dump, "_apk_ids", lambda apk: (Path(apk).stem, None))
def apk(path):
path.parent.mkdir(parents=True, exist_ok=True)
with zipfile.ZipFile(path, "w") as z:
z.writestr("AndroidManifest.xml", b"x")
root = tmp_path / "VR"
apk(root / "stray.app.apk") # a loose APK next to game folders: not "one game"
apk(root / "Manager/Game A v1/com.a.game.apk")
(root / "Manager/Game A v1/com.a.game").mkdir()
(root / "Manager/Game A v1/com.a.game/main.obb").write_bytes(b"o")
apk(root / "Manager/data/downloads/Game B v2/com.b.game.apk") # 4 levels down
(root / "PC Game/Binaries").mkdir(parents=True)
(root / "PC Game/game.exe").write_bytes(b"MZ")
apk(root / "PC Game/Binaries/never.apk") # PC program folders aren't searched
found = {g.apk.name: g for g in quest_dump.scan(root)}
assert set(found) == {"stray.app.apk", "com.a.game.apk", "com.b.game.apk"}
assert found["com.a.game.apk"].data_dir == root / "Manager/Game A v1/com.a.game"
def test_user_recipes_get_a_catalog_update_offer(tmp_path, monkeypatch):
"""GitHub #10: a recipe whose Game settings were saved once ("user") never followed a catalog fix again; now
the game is flagged, and taking the offer re-derives it keeping the user's own FrameBridge settings."""
from frameport import pipeline
from frameport.core import library
from frameport.recommend import catalog
monkeypatch.setattr(library, "_path", lambda: tmp_path / "library.json")
entry = catalog.CatalogEntry(package="com.x.vr4", title="VR4", adapter={"vk_shader_fix": "1:2:3:4"})
monkeypatch.setattr(catalog, "lookup", lambda pkg: entry if pkg == "com.x.vr4" else None)
an = {"package": "com.x.vr4", "engine": "Unreal", "libs": ["libUE4.so"], "abis": ["arm64-v8a"]}
recipe = {"package": "com.x.vr4", "patches": {"adapter.scale": {"value": 0.9}}, "source": "user",
"catalog_rev": "old"}
(tmp_path / "library.json").write_text(__import__("json").dumps(
{"games": {"com.x.vr4": {"package": "com.x.vr4", "analysis": an, "recipe": recipe}}, "settings": {}}))
assert library.game("com.x.vr4")["catalog_update"] == entry.rev()
pipeline.apply_catalog_update("com.x.vr4")
g = library.game("com.x.vr4")
assert "catalog_update" not in g and g["recipe"]["patches"]["adapter.scale"] == {"value": 0.9}
assert g["recipe"].get("source") != "user" and "adapter.vk_shader_fix" in g["recipe"]["patches"]
def test_oculus_model_checks_accept_any_device_but_the_go():
import struct
from frameport.patches.frame import unity_oculus_check as C
# .text: adrp x1, page(0x1000); add x1, x1, #0x10; orr w2, wzr, #6; bl ... -> strncmp(model, "Oculus", 6)
code = struct.pack("<4I", 0xB0000001, 0x91004021, C.ORR_W2_6, 0x94000000)
other = struct.pack("<4I", 0xB0000001, 0x91008021, C.ORR_W2_6, 0x94000000) # a different string: untouched
data = bytearray(0x2000)
data[0:16], data[16:32] = code, other
data[0x100F:0x1017] = b"\0Oculus\0" # the literal at 0x1010
data[0x1020:0x1028] = b"OculusX\0"
out, n = C.oculus_model_checks(bytes(data))
assert n == 1
assert struct.unpack_from("<I", out, 8)[0] == C.MOV_W2_0 and struct.unpack_from("<I", out, 24)[0] == C.ORR_W2_6
assert out[0x1010:0x1016] == b"Oculus" # the string itself stays (Unity's VR device name)
def test_install_builds_a_game_that_was_never_built(tmp_path, monkeypatch):
from types import SimpleNamespace
from frameport import pipeline
from frameport.core import library
from frameport.core.models import Recipe
library.upsert_game("com.x.fresh", apk=str(tmp_path / "in.apk"),
recipe=library.recipe_to_dict(Recipe("com.x.fresh")))
out = tmp_path / "built.apk"
out.write_bytes(b"PK")
def build(package, reporter):
library.upsert_game(package, build={"apk": str(out)})
monkeypatch.setattr(pipeline, "build_game", build)
monkeypatch.setattr(pipeline, "remove_converted_copies", lambda package: 0)
installed = []
target = SimpleNamespace(label="frame", install=lambda *a, **k: installed.append(a[2]) or {"ok": True},
add_to_library=lambda pkgs, rep: None)
pipeline.install_game("com.x.fresh", target, None) # used to fail with KeyError 'apk'
assert installed == [out]
def test_frame_wait_follows_unitys_frame_begins():
"""Sniper Elite VR deadlocked at a scene switch: the shim's xrWaitFrame waited for a frame Unity never began. The
begin/end lookups in libunity.so go to the shim too (same-length, in-place renames) and the shim exports
them."""
import io
from elftools.elf.elffile import ELFFile
from frameport.patches.frame import artifact
from frameport.patches.frame import unity_oculus_check as C
for old, new in ((C.UPDATE, C.SHIM_UPDATE), (C.BEGIN, C.SHIM_BEGIN), (C.END, C.SHIM_END)):
assert len(old) == len(new)
shim = ELFFile(io.BytesIO(artifact("arm64-v8a", C.SHIM)))
exported = {s.name for s in shim.get_section_by_name(".dynsym").iter_symbols() if s["st_shndx"] != "SHN_UNDEF"}
assert {C.SHIM_UPDATE, C.SHIM_BEGIN, C.SHIM_END} <= exported
log = ("10-06 15:01:12.256 1232 1255 I FrameBridge: ovrp frame loop shim: frame 2175: the last waited frame "
"wasn't begun within 50 ms, not waiting (2 times)")
assert "unity-frame-not-begun" in {f.id for f in triage(log, "RUNNING", None).findings}
def test_frame_loop_fixes_are_per_game_settings():
"""The frame-begin gate and the held-back physics update (Sniper Elite VR) are off by default: BattleSisters'
hands lagged and its loading screen stuttered with them; Sniper's recipe turns them on."""
from pathlib import Path
import yaml
from frameport.patches import settings
src = Path(settings.__file__).read_text()
assert '("ovrp_begin_gate", "int", 0,' in src and '("ovrp_hold_physics", "int", 0,' in src
shim = (Path(__file__).parents[1] / "native/ovrpshim/ovrpshim.c").read_text()
assert "if (!begin_gate) return 1;" in shim and "if (hold_physics && step == STEP_PHYSICS" in shim
recipe = yaml.safe_load((Path(__file__).parents[1] / "catalog/games/com.JustAddWater.SniperEliteVR.yaml")
.read_text())
assert recipe["adapter"]["ovrp_begin_gate"] == 1 and recipe["adapter"]["ovrp_hold_physics"] == 1
def test_vermillion_canvas_stays_in_the_scene():
"""Vermillion (GitHub #200): the canvas layer hid the hand and brush; both compositor checks return false (method
starts as Cpp2IL reports them for 1.7.2), and only Vermillion is offered the patch."""
from frameport.analysis.il2cpp import parse_methods
from frameport.patches.frame import vermillion_canvas as V
base.load_all()
lib = _lib()
lo, _hi = U._executable(lib)[0]
a, b = (lo + 0x40) & ~3, (lo + 0x80) & ~3
cs = (f"\t[Address(RVA = \"0x{a:X}\", Offset = \"0x{a:X}\", Length = \"0x90\")]\n"
"\t[Token(Token = \"0x600148D\")]\n"
"\tpublic bool get_HasCompositorLayerAvailable() { }\n\n"
f"\t[Address(RVA = \"0x{b:X}\", Offset = \"0x{b:X}\", Length = \"0x330\")]\n"
"\t[Token(Token = \"0x60014B0\")]\n"
"\tpublic bool get_ShouldUseCompositorOverlay() { }\n")
found = {V.PAINTABLE: parse_methods(cs, list(V.VermillionSceneCanvas.targets[V.PAINTABLE]))}
assert found[V.PAINTABLE] == {"get_HasCompositorLayerAvailable": (a, 0x90),
"get_ShouldUseCompositorOverlay": (b, 0x330)}
out, notes = U.patch_methods(lib, found, V.VermillionSceneCanvas.targets)
assert out[a:a + 8] == U.RET_FALSE and out[b:b + 8] == U.RET_FALSE and len(notes) == 2
assert U.patch_methods(out, found, V.VermillionSceneCanvas.targets)[0] is None # already applied
assert set(V.VermillionSceneCanvas.targets[V.PAINTABLE]) <= set(U.ALL_TARGETS[V.PAINTABLE])
p = base.get("frame.vermillion_scene_canvas")
game = _analysis(package=V.PACKAGE, libs=["libil2cpp.so", "libunity.so"])
assert p.applies(game) and p.detect(game) is None
assert not p.applies(_analysis(package="com.other.game", libs=["libil2cpp.so"]))
class _FakeWs:
abi = "arm64-v8a"
def __init__(self, lib):
self.files = {"lib/arm64-v8a/libil2cpp.so": lib, U.METADATA: b"meta"}
def lib(self, name):
return f"lib/arm64-v8a/{name}"
def has(self, name):
return name in self.files
def read(self, name):
return self.files[name]
def put(self, name, data):
self.files[name] = data
def __enter__(self):
return self
def __exit__(self, *a):
return False
def write(self, path):
pass
def test_il2cpp_patch_with_no_code_found_warns_instead_of_a_silent_skip(monkeypatch):
"""GitHub #293: BattleTalent's build had unity_runtime_msaa_off/multipass on, Cpp2IL found none of their methods
and the build said nothing. Now a WARN check (not a failure) names the patch."""
from frameport import build
from frameport.core.events import Reporter
base.load_all()
monkeypatch.setattr(il2cpp, "find_methods", lambda *a, **kw: {})
events = []
game = _analysis(extra={"unity_version": "2021.3.45f2"})
ctx = base.ApkContext(_FakeWs(_lib()), game, {}, Reporter([events.append]), {})
patch = base.get("frame.unity_runtime_msaa_off")
assert patch.apply(ctx) is False and not ctx.notes
(name, ok, detail), = ctx.checks
assert name == patch.check_name and ok is None
assert "frame.unity_runtime_msaa_off" in detail and "code not found" in detail
assert "get_recommendedMSAALevel" in detail and detail.count("OVRDisplay.get_recommendedMSAALevel") == 1
assert any("code not found" in e.message for e in events if e.kind == "log")
# the build turns it into a build check: a WARN, so BuildResult.ok stays true
monkeypatch.setattr(build, "ApkWorkspace", lambda path: _FakeWs(_lib()))
monkeypatch.setattr(build.upstream, "resolve", lambda ws, patches, reporter: build.upstream.Resolution(patches))
recipe = type("Recipe", (), {"patches": {"frame.unity_runtime_msaa_off": {}}})()
applied, checks, _ = build.apply_frame_fixes(Path("in.apk"), Path("out.apk"), game, recipe, Reporter())
assert applied == [] and checks == [{"name": patch.check_name, "ok": None, "detail": detail}]
def test_il2cpp_patch_says_when_cpp2il_couldnt_read_the_game(monkeypatch):
"""Both Cpp2IL builds failed: the WARN names Cpp2IL's error, not "code not found" (GitHub #293)."""
from frameport.core.events import Reporter
base.load_all()
error = ("Cpp2IL couldn't read this game's code: System.IndexOutOfRangeException: Index was outside the bounds "
"of the array. (exit code 134)")
def fail(*a, **kw):
raise RuntimeError(error)
monkeypatch.setattr(il2cpp, "find_methods", fail)
ctx = base.ApkContext(_FakeWs(_lib()), _analysis(extra={"unity_version": "2021.3.45f2"}), {}, Reporter(), {})
patch = base.get("frame.unity_multipass")
assert patch.apply(ctx) is False
(name, ok, detail), = ctx.checks
assert ok is None and "frame.unity_multipass" in detail and error in detail and "code not found" not in detail