fix: keep another emulator's tree out of resolution

This commit is contained in:
Abdessamad Derraz committed 2026-10-09 22:20:00 +02:00
1 parent 5ebb141dbf
commit 5090208a64
2 files changed
+126 -17

No files matched your search

+54 -17
View File
@@ -489,6 +489,51 @@ def _owner_tokens(file_entry: dict) -> set[str]:
return tokens return tokens
def _foreign_tree(path: str, owners: set[str]) -> bool:
"""Whether a path lives in the tree of an emulator that is not the asker.
bios/Other/<emulator>/ holds what one emulator ships. A same-named file
found there answers that emulator's entry, not another's: EmuCoreX's
unlock.wav was served from NetherSX2's tree, and upstream PCSX2's
GameIndex.yaml from EmuCoreX's, while the system directory held a copy.
"""
parts = path.split("/")
if not owners or len(parts) < 4 or parts[0] != "bios" or parts[1] != "Other":
return False
return not (owners & _affinity_tokens(parts[2]))
def _rank_tail_candidates(
candidates: list[str], start: int, tail: str, files_db: dict, owners: set[str]
) -> list[str]:
"""Order the files a destination tail designates, the likeliest first.
A shortened tail found only in another emulator's tree is that
emulator's asset, not evidence about this entry. A full tail claimed
twice goes to the file whose path carries the least on top of the
destination, then to the owner's own copy (super3's Games.xml was served
to Supermodel-Dojo by index order alone), then to any copy outside
another emulator's tree (upstream PCSX2 took EmuCoreX's GameIndex.yaml
over the system directory's).
"""
def path_of(sha1: str) -> str:
return files_db.get(sha1, {}).get("path", "")
if start:
candidates = [h for h in candidates if not _foreign_tree(path_of(h), owners)]
if len(candidates) < 2:
return candidates
depth = len(tail.split("/"))
return sorted(
candidates,
key=lambda h: (
len(path_of(h).split("/")) - depth,
not owners & _affinity_tokens(path_of(h).rsplit("/", 1)[0]),
_foreign_tree(path_of(h), owners),
),
)
def _by_affinity(paths: list[str], file_entry: dict, dest_hint: str) -> list[str]: def _by_affinity(paths: list[str], file_entry: dict, dest_hint: str) -> list[str]:
"""Same-named candidates, the likeliest first. """Same-named candidates, the likeliest first.
@@ -507,14 +552,15 @@ def _by_affinity(paths: list[str], file_entry: dict, dest_hint: str) -> list[str
owners = _owner_tokens(file_entry) owners = _owner_tokens(file_entry)
sized = any(file_entry.get(k) for k in ("size", "min_size", "max_size")) sized = any(file_entry.get(k) for k in ("size", "min_size", "max_size"))
def score(path: str) -> tuple[int, bool, int]: def score(path: str) -> tuple[int, bool, bool, int]:
fits = 0 fits = 0
if sized and os.path.exists(path): if sized and os.path.exists(path):
fits = int(size_fits(file_entry, os.path.getsize(path))) fits = int(size_fits(file_entry, os.path.getsize(path)))
tokens = _affinity_tokens(path.rsplit("/", 1)[0]) tokens = _affinity_tokens(path.rsplit("/", 1)[0])
# The owner's own copy ranks above any shared tree segment: NetherSX2's # The owner's own copy ranks above any shared tree segment: NetherSX2's
# shaders/common/ beat armsx2's own fxaa.fx on two directory tokens. # shaders/common/ beat armsx2's own fxaa.fx on two directory tokens.
return fits, bool(owners & tokens), len(wanted & tokens) # Another emulator's own tree ranks below everything else.
return fits, bool(owners & tokens), not _foreign_tree(path, owners | wanted), len(wanted & tokens)
return sorted(paths, key=score, reverse=True) return sorted(paths, key=score, reverse=True)
@@ -716,9 +762,14 @@ def resolve_local_file(
# stops describing this file and starts describing whichever emulator # stops describing this file and starts describing whichever emulator
# happens to store the same asset tree. # happens to store the same asset tree.
hint_parts = dest_hint.split("/") hint_parts = dest_hint.split("/")
# The asking profile and the destination's own directories both name
# whose tree a copy may come from.
owners = _owner_tokens(file_entry) | _affinity_tokens("/".join(hint_parts[:-1]))
for start in range(min(2, len(hint_parts) - 1)): for start in range(min(2, len(hint_parts) - 1)):
tail = "/".join(hint_parts[start:]) tail = "/".join(hint_parts[start:])
candidates = by_path_suffix.get(tail, []) candidates = _rank_tail_candidates(
by_path_suffix.get(tail, []), start, tail, files_db, owners
)
if start and len(candidates) > 1: if start and len(candidates) > 1:
continue continue
# The index names the file this destination designates. Not on # The index names the file this destination designates. Not on
@@ -729,20 +780,6 @@ def resolve_local_file(
for h in candidates for h in candidates
): ):
own_file_absent = True own_file_absent = True
if len(candidates) > 1:
depth = len(tail.split("/"))
# At equal depth the owner's own copy: super3's Games.xml was
# served to Supermodel-Dojo by index order alone.
owners = _owner_tokens(file_entry)
candidates = sorted(
candidates,
key=lambda h: (
len(files_db.get(h, {}).get("path", "").split("/")) - depth,
not owners & _affinity_tokens(
files_db.get(h, {}).get("path", "").rsplit("/", 1)[0]
),
),
)
for match_sha1 in candidates: for match_sha1 in candidates:
if match_sha1 not in files_db: if match_sha1 not in files_db:
continue continue
+72
View File
@@ -0,0 +1,72 @@
"""A file in another emulator's tree does not answer for this one.
bios/Other/<emulator>/ holds what that emulator ships. The path step took a
shortened tail that only NetherSX2's tree answered as proof for EmuCoreX's
unlock.wav, and broke a full-tail tie between EmuCoreX's GameIndex.yaml and
the system directory's copy by index order, serving a fork's game database
to upstream PCSX2.
"""
from __future__ import annotations
import hashlib
import os
import sys
import tempfile
import unittest
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(REPO_ROOT / "scripts"))
import generate_db # noqa: E402
from common import resolve_local_file # noqa: E402
class ForeignTree(unittest.TestCase):
def setUp(self):
self._tmp = tempfile.TemporaryDirectory()
self._cwd = os.getcwd()
os.chdir(self._tmp.name)
files = {}
for rel, payload in (
("bios/Other/forkemu/resources/GameIndex.yaml", b"fork db"),
("bios/Sony/PlayStation 2/resources/GameIndex.yaml", b"system copy"),
("bios/Other/forkemu/resources/sounds/unlock.wav", b"fork sound"),
):
path = Path(rel)
path.parent.mkdir(parents=True, exist_ok=True)
path.write_bytes(payload)
sha1 = hashlib.sha1(payload).hexdigest()
files[sha1] = {
"path": rel, "name": path.name, "size": len(payload), "sha1": sha1,
"md5": hashlib.md5(payload).hexdigest(),
"sha256": hashlib.sha256(payload).hexdigest(), "crc32": "00000001",
}
self.db = {"files": files, "indexes": generate_db.build_indexes(files, {})}
def tearDown(self):
os.chdir(self._cwd)
self._tmp.cleanup()
def _resolve(self, owner: str, path: str):
entry = {"name": path.rsplit("/", 1)[-1], "path": path, "source_profile": owner}
return resolve_local_file(entry, self.db, {}, dest_hint=path)
def test_a_full_tail_tie_goes_to_the_system_directory(self):
local, status = self._resolve("pcsx2", "resources/GameIndex.yaml")
self.assertEqual(status, "path_exact")
self.assertTrue(local.startswith("bios/Sony/"), local)
def test_the_fork_still_gets_its_own_copy(self):
local, status = self._resolve("forkemu", "resources/GameIndex.yaml")
self.assertEqual(status, "path_exact")
self.assertTrue(local.startswith("bios/Other/forkemu/"), local)
def test_a_shortened_tail_in_a_foreign_tree_is_not_path_evidence(self):
_local, status = self._resolve("otheremu", "assets/sounds/unlock.wav")
self.assertNotEqual(status, "path_exact")
if __name__ == "__main__":
unittest.main()