mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-10-14 11:17:16 -04:00
Preserve native Linux and FEX compatibility choices (#749)
This commit is contained in:
1 parent
0c3444afbc
commit
06b09b37bd
5 files changed
+533
-25
No files matched your search
@@ -442,27 +442,49 @@ class Helper:
|
||||
installed = COMPAT["installed_apps"](self.steam_root)
|
||||
self.installed = installed
|
||||
picks = COMPAT["valve_picks"](self.steam_root)
|
||||
apps = sorted(set(installed) | set(picks) | set(state.get("auto", {})), key=COMPAT["app_order"])
|
||||
prior_auto = state.get("auto", {})
|
||||
if not isinstance(prior_auto, dict):
|
||||
prior_auto = {}
|
||||
prior_native = state.get("native", ())
|
||||
native_choices = set(prior_native if isinstance(prior_native, (list, tuple, set)) else ())
|
||||
apps = sorted(set(installed) | set(picks) | set(prior_auto) | native_choices, key=COMPAT["app_order"])
|
||||
overrides = COMPAT["requested_games"](
|
||||
COMPAT["game_requests"](), catalog, [t["name"] for t in live])
|
||||
apps = sorted(set(apps) | set(overrides), key=COMPAT["app_order"])
|
||||
current = dict(page.compat("many(%s)" % json.dumps(apps)) or {})
|
||||
current["0"] = top["selected"]
|
||||
native = COMPAT["native_targets"](self.steam_root, installed)
|
||||
native = COMPAT["native_targets"](self.steam_root, apps)
|
||||
raw_mapping = COMPAT["read_config_mapping"](
|
||||
os.path.join(self.steam_root, "config", "config.vdf"))
|
||||
if raw_mapping is None:
|
||||
for app in native_choices:
|
||||
current[app] = ""
|
||||
else:
|
||||
for app in set(native) | native_choices:
|
||||
if app in raw_mapping:
|
||||
current[app] = raw_mapping[app]
|
||||
elif app in prior_auto or app in native_choices:
|
||||
current[app] = ""
|
||||
changes, auto = COMPAT["plan_mapping"](
|
||||
current, installed, [t["name"] for t in live], default,
|
||||
COMPAT["followers"](state, current), native, picks, overrides)
|
||||
COMPAT["followers"](state, current), native, picks, overrides,
|
||||
prior_auto.keys(), native_choices)
|
||||
for app, tool in sorted(changes.items(), key=lambda item: COMPAT["app_order"](item[0])):
|
||||
page.compat("set(%d, %s)" % (int(app), json.dumps(tool)))
|
||||
say("%s -> %s" % ("default" if app == "0" else "app %s" % app, tool))
|
||||
state.update(auto=auto, tools=catalog, live=True)
|
||||
page.compat("set(%d, %s)" % (int(app), json.dumps(tool if tool is not None else "")))
|
||||
say("%s -> %s" % ("default" if app == "0" else "app %s" % app,
|
||||
tool if tool is not None else "Steam native default"))
|
||||
state.update(
|
||||
auto={app: tool for app, tool in auto.items() if tool is not COMPAT["NATIVE_CHOICE"]},
|
||||
native=sorted(app for app, tool in auto.items() if tool is COMPAT["NATIVE_CHOICE"]),
|
||||
tools=catalog, live=True)
|
||||
if waiting:
|
||||
state["pending"] = wanted
|
||||
say("%s is new to the running client; it applies when Steam next starts" % wanted)
|
||||
else:
|
||||
state.pop("pending", None)
|
||||
COMPAT["write_json"](state_file, state)
|
||||
if native or os.path.exists(COMPAT["fex_wanted_marker"]()):
|
||||
if (any(app in installed and tool == COMPAT["FEX_TOOL"] for app, tool in native.items()) or
|
||||
os.path.exists(COMPAT["fex_wanted_marker"]())):
|
||||
for app in COMPAT["request_fex"](self.steam_root, self.fex_asked):
|
||||
say("asked the client to install %s for x86 Linux titles and programs" % app)
|
||||
|
||||
|
||||
@@ -787,19 +787,18 @@ def native_targets(steam_root, apps):
|
||||
return {}
|
||||
targets = {}
|
||||
for app, (oslist, osarch, kind) in appinfo_platforms(os.path.join(steam_root, "appcache", "appinfo.vdf"), wanted).items():
|
||||
if kind not in PLAYABLE_TYPES or "windows" in oslist or "linux" not in oslist:
|
||||
if kind not in PLAYABLE_TYPES or "linux" not in oslist:
|
||||
continue
|
||||
if "arm64" in osarch or "aarch64" in osarch:
|
||||
targets[app] = None
|
||||
continue
|
||||
if "windows" in oslist:
|
||||
targets[app] = NATIVE_CHOICE
|
||||
continue
|
||||
targets[app] = FEX_TOOL
|
||||
return targets
|
||||
|
||||
|
||||
def linux_runtime_tool(name):
|
||||
low = name.lower()
|
||||
return low == "fex" or low.startswith("steamlinuxruntime")
|
||||
|
||||
|
||||
def fex_missing(steam_root):
|
||||
missing = []
|
||||
for app in FEX_DEPENDENCIES:
|
||||
@@ -877,18 +876,22 @@ def app_order(app):
|
||||
|
||||
|
||||
DEFERRED_GAME_TOOL = object()
|
||||
NATIVE_CHOICE = object()
|
||||
|
||||
|
||||
def plan_mapping(current, installed, runnable, default, auto, native=None, picks=(), overrides=None):
|
||||
def plan_mapping(current, installed, runnable, default, auto, native=None, picks=(), overrides=None, managed=None,
|
||||
native_choices=()):
|
||||
runnable = {n.lower() for n in runnable}
|
||||
installed = set(installed)
|
||||
picks = set(picks)
|
||||
native = native or {}
|
||||
overrides = overrides or {}
|
||||
managed = set(auto) if managed is None else set(managed)
|
||||
native_choices = set(native_choices)
|
||||
changes, kept = {}, {}
|
||||
if current.get("0", "") != default:
|
||||
changes["0"] = default
|
||||
for app in sorted((set(current) | installed | picks | set(auto) | set(overrides)) - {"0"}, key=app_order):
|
||||
for app in sorted((set(current) | installed | picks | set(auto) | set(overrides) | native_choices) - {"0"}, key=app_order):
|
||||
name = current.get(app, "")
|
||||
if app in overrides:
|
||||
target = overrides[app]
|
||||
@@ -902,9 +905,26 @@ def plan_mapping(current, installed, runnable, default, auto, native=None, picks
|
||||
continue
|
||||
mine = auto.get(app)
|
||||
target = native.get(app, default)
|
||||
stale = unrunnable(name, runnable) or (app in native and linux_runtime_tool(name))
|
||||
if not name and app in native_choices:
|
||||
target = NATIVE_CHOICE
|
||||
stale = unrunnable(name, runnable)
|
||||
if name and not stale and (mine is None or name != mine):
|
||||
continue
|
||||
if target is NATIVE_CHOICE:
|
||||
if not name and (app in native_choices or app in current or app in managed):
|
||||
kept[app] = NATIVE_CHOICE
|
||||
continue
|
||||
target = default
|
||||
if target is None:
|
||||
if stale:
|
||||
target = default
|
||||
if name != target:
|
||||
changes[app] = target
|
||||
continue
|
||||
else:
|
||||
if name and app in managed and name == mine:
|
||||
changes[app] = None
|
||||
continue
|
||||
if not name and app not in installed and app not in picks:
|
||||
continue
|
||||
if name != target:
|
||||
@@ -1043,8 +1063,37 @@ def read_mapping(tokens, mapping):
|
||||
return current
|
||||
|
||||
|
||||
def read_config_mapping(config_path):
|
||||
"""Read per-app choices without creating or rewriting the client's mapping block."""
|
||||
try:
|
||||
with open(config_path, encoding="utf-8", errors="replace") as f:
|
||||
tokens = tokenize(f.read())
|
||||
except OSError:
|
||||
return None
|
||||
depth = 0
|
||||
for token in tokens:
|
||||
if token == "{":
|
||||
depth += 1
|
||||
elif token == "}":
|
||||
depth -= 1
|
||||
if depth < 0:
|
||||
return None
|
||||
if depth != 0:
|
||||
return None
|
||||
steam = find_block(tokens, ["InstallConfigStore", "Software", "Valve", "Steam"])
|
||||
if steam is None:
|
||||
return None
|
||||
mapping = find_block(tokens[steam + 1:], ["CompatToolMapping"])
|
||||
return read_mapping(tokens, mapping + steam + 1) if mapping is not None else {}
|
||||
|
||||
|
||||
def set_mapping(tokens, mapping, app, tool):
|
||||
block = find_block(tokens[mapping + 1:], [app])
|
||||
if tool is None:
|
||||
if block is not None:
|
||||
block += mapping + 1
|
||||
del tokens[block - 1:block_end(tokens, block) + 1]
|
||||
return
|
||||
if block is None:
|
||||
tokens[mapping + 1:mapping + 1] = [
|
||||
'"%s"' % app, "{", '"name"', '"%s"' % tool, '"config"', '""', '"priority"', '"75"' if app == "0" else '"250"', "}"]
|
||||
@@ -1109,13 +1158,17 @@ def register_default(config_path, apps=(), protect=(), default=None, catalog=Non
|
||||
steam_root = os.path.dirname(os.path.dirname(os.path.abspath(config_path)))
|
||||
overrides = requested_games(game_requests(directory), catalog)
|
||||
changes, auto = plan_mapping(
|
||||
current, apps, names, default, followers(state, current), native_targets(steam_root, apps), picks, overrides)
|
||||
current, apps, names, default, followers(state, current),
|
||||
native_targets(steam_root, set(apps) | set(picks)), picks,
|
||||
overrides, state.get("auto", {}).keys() if isinstance(state.get("auto"), dict) else (),
|
||||
state.get("native", ()))
|
||||
for app, tool in changes.items():
|
||||
set_mapping(tokens, mapping, app, tool)
|
||||
text = serialize(tokens)
|
||||
if text != serialize(tokenize(original)):
|
||||
write_changed(config_path, text)
|
||||
state["auto"] = auto
|
||||
state["auto"] = {app: tool for app, tool in auto.items() if tool is not NATIVE_CHOICE}
|
||||
state["native"] = sorted(app for app, tool in auto.items() if tool is NATIVE_CHOICE)
|
||||
state["tools"] = catalog
|
||||
state.setdefault("default", default)
|
||||
write_json(state_path("state.json", directory), state)
|
||||
|
||||
@@ -499,19 +499,23 @@ class AppInfoTest(unittest.TestCase):
|
||||
table = struct.pack("<I", len(strings)) + b"".join(s.encode() + b"\0" for s in strings)
|
||||
return struct.pack("<IIq", magic, 1, table_at) + body + table
|
||||
|
||||
def test_linux_only_titles_are_sent_to_fex(self):
|
||||
def test_linux_only_titles_are_sent_to_fex_and_dual_platform_is_deferred(self):
|
||||
for magic in (0x07564427, 0x07564428, 0x07564429):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
steam = Path(tmp)
|
||||
(steam / "appcache").mkdir()
|
||||
(steam / "appcache/appinfo.vdf").write_bytes(self.appinfo({
|
||||
10: ("windows", "", "Game"), 20: ("linux", "64", "Game"), 30: ("windows,linux,macos", "", "game"),
|
||||
40: ("linux", "arm64", "game"), 50: ("linux", "", "Demo"), 70: ("linux", "", "Tool"), 80: ("linux", "", "config"),
|
||||
40: ("linux", "arm64", "game"), 41: ("windows,linux", "aarch64", "game"),
|
||||
50: ("linux", "", "Demo"), 70: ("linux", "", "Tool"), 80: ("linux", "", "config"),
|
||||
90: ("linux", "", ""),
|
||||
}, magic))
|
||||
self.COMPAT["APPINFO_CACHE"].clear()
|
||||
targets = self.COMPAT["native_targets"](str(steam), ["10", "20", "30", "40", "50", "70", "80", "90", "1628350", "60"])
|
||||
self.assertEqual(targets, {"20": "droiddeck-fex", "50": "droiddeck-fex"}, hex(magic))
|
||||
targets = self.COMPAT["native_targets"](str(steam), ["10", "20", "30", "40", "41", "50", "70", "80", "90", "1628350", "60"])
|
||||
self.assertEqual(targets, {
|
||||
"20": "droiddeck-fex", "30": self.COMPAT["NATIVE_CHOICE"],
|
||||
"40": None, "41": None, "50": "droiddeck-fex",
|
||||
}, hex(magic))
|
||||
|
||||
def test_titles_with_a_valve_pick_are_found_installed_or_not(self):
|
||||
def deck(oslist, runtime, kind="game"):
|
||||
@@ -545,14 +549,34 @@ class AppInfoTest(unittest.TestCase):
|
||||
self.assertEqual(changes, {"30": "GE-Proton11-7", "32": "GE-Proton11-7"})
|
||||
self.assertEqual(auto, {"30": "GE-Proton11-7", "32": "GE-Proton11-7"})
|
||||
|
||||
def test_the_mapping_sends_native_titles_to_fex_and_keeps_picks(self):
|
||||
def test_automatic_native_mapping_uses_fex_and_keeps_explicit_tools(self):
|
||||
tool, fex = self.COMPAT["TOOL"], self.COMPAT["FEX_TOOL"]
|
||||
changes, auto = self.COMPAT["plan_mapping"](
|
||||
{"0": tool, "20": "fex", "21": "steamlinuxruntime_sniper", "22": "GE-Proton11-7"},
|
||||
["20", "21", "22", "23", "24"], [tool, "GE-Proton11-7"], tool, {},
|
||||
{"20": fex, "21": fex, "22": fex, "23": fex})
|
||||
self.assertEqual(changes, {"20": fex, "21": fex, "23": fex, "24": tool})
|
||||
self.assertEqual(auto, {"20": fex, "21": fex, "23": fex, "24": tool})
|
||||
self.assertEqual(changes, {"23": fex, "24": tool})
|
||||
self.assertEqual(auto, {"23": fex, "24": tool})
|
||||
|
||||
def test_explicit_linux_runtime_and_fex_selections_survive_default_maintenance(self):
|
||||
tool, fex = self.COMPAT["TOOL"], self.COMPAT["FEX_TOOL"]
|
||||
changes, auto = self.COMPAT["plan_mapping"](
|
||||
{"0": tool, "20": "steamlinuxruntime_arm64", "21": fex},
|
||||
["20", "21"], [tool], tool, {}, {"20": None, "21": fex}, ["20", "21"])
|
||||
self.assertEqual(changes, {})
|
||||
self.assertEqual(auto, {})
|
||||
|
||||
def test_native_arm64_default_stays_unmapped_and_old_auto_proton_is_cleared(self):
|
||||
tool = self.COMPAT["TOOL"]
|
||||
changes, auto = self.COMPAT["plan_mapping"](
|
||||
{"0": tool, "20": tool}, ["20"], [tool], tool, {"20": tool}, {"20": None}, ["20"])
|
||||
self.assertEqual(changes, {"20": None})
|
||||
self.assertEqual(auto, {})
|
||||
|
||||
changes, auto = self.COMPAT["plan_mapping"](
|
||||
{"0": tool}, ["20"], [tool], tool, {}, {"20": None}, ["20"])
|
||||
self.assertEqual(changes, {})
|
||||
self.assertEqual(auto, {})
|
||||
|
||||
def test_the_tool_files(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
|
||||
@@ -26,6 +26,22 @@ def config_text(entries):
|
||||
return '"InstallConfigStore" { "Software" { "Valve" { "Steam" { "CompatToolMapping" { %s } } } } }' % mapping
|
||||
|
||||
|
||||
class ReadConfigMappingTest(unittest.TestCase):
|
||||
def test_missing_parent_or_unbalanced_vdf_is_not_treated_as_absent_choices(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
config = Path(tmp) / "config.vdf"
|
||||
config.write_text('"InstallConfigStore" { "Software" { "Other" {} } }')
|
||||
self.assertIsNone(COMPAT["read_config_mapping"](str(config)))
|
||||
config.write_text('"InstallConfigStore" { "Software" { "Valve" { "Steam" {} }')
|
||||
self.assertIsNone(COMPAT["read_config_mapping"](str(config)))
|
||||
|
||||
def test_valid_steam_parent_without_compat_mapping_has_no_per_app_choices(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
config = Path(tmp) / "config.vdf"
|
||||
config.write_text('"InstallConfigStore" { "Software" { "Valve" { "Steam" {} } } }')
|
||||
self.assertEqual(COMPAT["read_config_mapping"](str(config)), {})
|
||||
|
||||
|
||||
class GameEnvironmentTest(unittest.TestCase):
|
||||
def test_the_install_script_evaluator_gets_its_scripts_marked_first(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
@@ -415,6 +431,218 @@ class ProtonDefaultTest(unittest.TestCase):
|
||||
self.assertEqual(changes, {"0": "GE-Proton11-7", "10": "GE-Proton11-7", "11": "GE-Proton11-7", "14": "GE-Proton11-7"})
|
||||
self.assertEqual(auto, {"10": "GE-Proton11-7", "11": "GE-Proton11-7", "14": "GE-Proton11-7"})
|
||||
|
||||
def test_windows_only_titles_still_follow_the_default(self):
|
||||
changes, auto = COMPAT["plan_mapping"](
|
||||
{"0": self.TOOL}, ["42"], self.NAMES, self.TOOL, {})
|
||||
self.assertEqual(changes, {"42": self.TOOL})
|
||||
self.assertEqual(auto, {"42": self.TOOL})
|
||||
|
||||
def test_native_arm64_default_clears_only_our_auto_mapping(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
config = Path(tmp) / "config.vdf"
|
||||
config.write_text(config_text([("0", self.TOOL), ("42", self.TOOL)]))
|
||||
(Path(tmp) / "state.json").write_text(json.dumps({
|
||||
"default": self.TOOL,
|
||||
"auto": {"42": self.TOOL},
|
||||
}))
|
||||
globals_ = COMPAT["register_default"].__globals__
|
||||
original_native_targets = globals_["native_targets"]
|
||||
globals_["native_targets"] = lambda _root, _apps: {"42": None}
|
||||
try:
|
||||
COMPAT["register_default"](
|
||||
str(config), ["42"], catalog=self.CATALOG, directory=tmp)
|
||||
first = config.read_bytes()
|
||||
self.assertEqual(mapping_of(first.decode()), {"0": self.TOOL})
|
||||
self.assertEqual(COMPAT["tokenize"](first.decode()),
|
||||
COMPAT["tokenize"](config_text([("0", self.TOOL)])))
|
||||
self.assertEqual(json.loads((Path(tmp) / "state.json").read_text())["auto"], {})
|
||||
|
||||
COMPAT["register_default"](
|
||||
str(config), ["42"], catalog=self.CATALOG, directory=tmp)
|
||||
self.assertEqual(config.read_bytes(), first)
|
||||
finally:
|
||||
globals_["native_targets"] = original_native_targets
|
||||
|
||||
def test_saved_native_choice_survives_valve_pick_without_appinfo(self):
|
||||
app = "452280"
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
root = Path(tmp)
|
||||
steam = root / "Steam"
|
||||
config = steam / "config/config.vdf"
|
||||
config.parent.mkdir(parents=True)
|
||||
(steam / "steamapps").mkdir()
|
||||
(steam / "steamapps" / ("appmanifest_%s.acf" % app)).write_text("")
|
||||
config.write_text(config_text([("0", self.TOOL)]))
|
||||
(root / "state.json").write_text(json.dumps({
|
||||
"default": self.TOOL,
|
||||
"auto": {},
|
||||
"native": [app],
|
||||
}))
|
||||
|
||||
globals_ = COMPAT["register_default"].__globals__
|
||||
original_native_targets = globals_["native_targets"]
|
||||
globals_["native_targets"] = lambda _root, _apps: {}
|
||||
try:
|
||||
COMPAT["register_default"](
|
||||
str(config), [], catalog=self.CATALOG, directory=tmp, picks=[app])
|
||||
self.assertEqual(mapping_of(config.read_text()), {"0": self.TOOL})
|
||||
state = json.loads((root / "state.json").read_text())
|
||||
self.assertEqual(state["auto"], {})
|
||||
self.assertEqual(state["native"], [app])
|
||||
finally:
|
||||
globals_["native_targets"] = original_native_targets
|
||||
|
||||
def test_shortcuts_scanner_keeps_native_choice_for_unrelated_app_list(self):
|
||||
app, added_shortcut = "452280", "3537741571"
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
root = Path(tmp)
|
||||
steam = root / "Steam"
|
||||
config = steam / "config/config.vdf"
|
||||
config.parent.mkdir(parents=True)
|
||||
(steam / "steamapps").mkdir()
|
||||
(steam / "steamapps" / ("appmanifest_%s.acf" % app)).write_text("")
|
||||
config.write_text(config_text([("0", self.TOOL)]))
|
||||
(root / "state.json").write_text(json.dumps({
|
||||
"default": self.TOOL,
|
||||
"auto": {},
|
||||
"native": [app],
|
||||
}))
|
||||
|
||||
globals_ = COMPAT["register_default"].__globals__
|
||||
original_native_targets = globals_["native_targets"]
|
||||
globals_["native_targets"] = lambda _root, wanted: {
|
||||
app: COMPAT["NATIVE_CHOICE"]
|
||||
} if app in wanted else {}
|
||||
try:
|
||||
COMPAT["register_default"](
|
||||
str(config), [app], catalog=self.CATALOG, directory=tmp)
|
||||
self.assertEqual(json.loads((root / "state.json").read_text())["native"], [app])
|
||||
|
||||
COMPAT["register_default"](
|
||||
str(config), [added_shortcut], catalog=self.CATALOG, directory=tmp)
|
||||
state = json.loads((root / "state.json").read_text())
|
||||
self.assertEqual(state["native"], [app])
|
||||
self.assertEqual(state["auto"], {added_shortcut: self.TOOL})
|
||||
self.assertEqual(mapping_of(config.read_text()), {
|
||||
"0": self.TOOL,
|
||||
added_shortcut: self.TOOL,
|
||||
})
|
||||
finally:
|
||||
globals_["native_targets"] = original_native_targets
|
||||
|
||||
def test_factorio_preserves_an_observed_native_choice_despite_ambiguous_metadata(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
steam = Path(tmp) / "Steam"
|
||||
config = steam / "config/config.vdf"
|
||||
config.parent.mkdir(parents=True)
|
||||
config.write_text(config_text([("0", self.TOOL)]))
|
||||
state = {"default": self.TOOL, "auto": {}}
|
||||
(Path(tmp) / "state.json").write_text(json.dumps(state))
|
||||
|
||||
globals_ = COMPAT["native_targets"].__globals__
|
||||
original_platforms = globals_["appinfo_platforms"]
|
||||
globals_["appinfo_platforms"] = lambda _path, wanted: {
|
||||
"427520": ({"linux", "macos", "windows"}, "", "game")
|
||||
} if "427520" in wanted else {}
|
||||
try:
|
||||
native = COMPAT["native_targets"](str(steam), ["427520"])
|
||||
self.assertIs(native["427520"], COMPAT["NATIVE_CHOICE"])
|
||||
|
||||
changes, auto = COMPAT["plan_mapping"](
|
||||
{"0": self.TOOL}, ["427520"], self.NAMES, self.TOOL, {}, native)
|
||||
self.assertEqual(changes, {"427520": self.TOOL})
|
||||
self.assertEqual(auto, {"427520": self.TOOL})
|
||||
|
||||
COMPAT["register_default"](
|
||||
str(config), ["427520"], catalog=self.CATALOG, directory=tmp)
|
||||
self.assertEqual(mapping_of(config.read_text()), {"0": self.TOOL, "427520": self.TOOL})
|
||||
|
||||
state["auto"] = {"427520": self.TOOL}
|
||||
(Path(tmp) / "state.json").write_text(json.dumps(state))
|
||||
config.write_text(config_text([("0", self.TOOL)]))
|
||||
COMPAT["register_default"](
|
||||
str(config), ["427520"], catalog=self.CATALOG, directory=tmp)
|
||||
first = config.read_bytes()
|
||||
self.assertEqual(mapping_of(first.decode()), {"0": self.TOOL})
|
||||
self.assertEqual(COMPAT["tokenize"](first.decode()),
|
||||
COMPAT["tokenize"](config_text([("0", self.TOOL)])))
|
||||
self.assertEqual(json.loads((Path(tmp) / "state.json").read_text())["auto"], {})
|
||||
self.assertEqual(json.loads((Path(tmp) / "state.json").read_text())["native"], ["427520"])
|
||||
|
||||
COMPAT["register_default"](
|
||||
str(config), ["427520"], catalog=self.CATALOG, directory=tmp)
|
||||
self.assertEqual(config.read_bytes(), first)
|
||||
|
||||
config.write_text(config_text([("0", self.TOOL), ("427520", "")]))
|
||||
COMPAT["register_default"](
|
||||
str(config), ["427520"], catalog=self.CATALOG, directory=tmp)
|
||||
native_choice = config.read_bytes()
|
||||
self.assertEqual(mapping_of(native_choice.decode()), {"0": self.TOOL, "427520": ""})
|
||||
self.assertEqual(json.loads((Path(tmp) / "state.json").read_text())["auto"], {})
|
||||
self.assertEqual(json.loads((Path(tmp) / "state.json").read_text())["native"], ["427520"])
|
||||
COMPAT["register_default"](
|
||||
str(config), ["427520"], catalog=self.CATALOG, directory=tmp)
|
||||
self.assertEqual(config.read_bytes(), native_choice)
|
||||
|
||||
for choice in (COMPAT["FEX_TOOL"], COMPAT["TOOL_11"], "GE-Proton11-7"):
|
||||
config.write_text(config_text([("0", self.TOOL), ("427520", choice)]))
|
||||
COMPAT["register_default"](
|
||||
str(config), ["427520"], catalog=self.CATALOG, directory=tmp)
|
||||
self.assertEqual(mapping_of(config.read_text()), {"0": self.TOOL, "427520": choice})
|
||||
self.assertEqual(json.loads((Path(tmp) / "state.json").read_text())["native"], [])
|
||||
|
||||
state = json.loads((Path(tmp) / "state.json").read_text())
|
||||
state["native"] = ["427520"]
|
||||
state["auto"] = {}
|
||||
(Path(tmp) / "state.json").write_text(json.dumps(state))
|
||||
config.write_text(config_text([("0", self.TOOL), ("427520", "proton_experimental")]))
|
||||
COMPAT["register_default"](
|
||||
str(config), ["427520"], catalog=self.CATALOG, directory=tmp)
|
||||
self.assertEqual(mapping_of(config.read_text()), {"0": self.TOOL, "427520": self.TOOL})
|
||||
saved = json.loads((Path(tmp) / "state.json").read_text())
|
||||
self.assertEqual(saved["native"], [])
|
||||
self.assertEqual(saved["auto"], {"427520": self.TOOL})
|
||||
finally:
|
||||
globals_["appinfo_platforms"] = original_platforms
|
||||
|
||||
def test_native_arm64_preserves_a_proton_mapping_without_managed_state(self):
|
||||
changes, auto = COMPAT["plan_mapping"](
|
||||
{"0": self.TOOL, "42": self.TOOL}, ["42"], self.NAMES, self.TOOL,
|
||||
{"42": self.TOOL}, {"42": None}, managed=())
|
||||
self.assertEqual(changes, {})
|
||||
self.assertEqual(auto, {})
|
||||
|
||||
def test_native_arm64_keeps_a_chosen_valve_proton_through_its_wrapper(self):
|
||||
changes, auto = COMPAT["plan_mapping"](
|
||||
{"0": self.TOOL, "42": "proton_experimental"}, ["42"], self.NAMES,
|
||||
self.TOOL, {}, {"42": None})
|
||||
self.assertEqual(changes, {"42": self.TOOL})
|
||||
self.assertEqual(auto, {})
|
||||
|
||||
changes, auto = COMPAT["plan_mapping"](
|
||||
{"0": self.TOOL, "42": self.TOOL}, ["42"], self.NAMES,
|
||||
self.TOOL, {}, {"42": None})
|
||||
self.assertEqual(changes, {})
|
||||
self.assertEqual(auto, {})
|
||||
|
||||
def test_native_arm64_keeps_explicit_runtime_fex_and_proton_profiles(self):
|
||||
fex = COMPAT["FEX_TOOL"]
|
||||
current = {
|
||||
"0": self.TOOL,
|
||||
"42": COMPAT["TOOL_11"],
|
||||
"43": "GE-Proton11-7",
|
||||
"44": "steamlinuxruntime_arm64",
|
||||
"45": fex,
|
||||
"46": "",
|
||||
}
|
||||
changes, auto = COMPAT["plan_mapping"](
|
||||
current, ["42", "43", "44", "45", "46"], self.NAMES, self.TOOL, {},
|
||||
{app: None for app in ("42", "43", "44", "45", "46")},
|
||||
overrides={"46": "GE-Proton11-7"},
|
||||
)
|
||||
self.assertEqual(changes, {"46": "GE-Proton11-7"})
|
||||
self.assertEqual(auto, {})
|
||||
|
||||
def test_titles_following_the_default_move_with_it_and_picks_stay(self):
|
||||
current = {"0": self.TOOL, "10": self.TOOL, "11": "GE-Proton11-7", "12": self.TOOL}
|
||||
auto = {"10": self.TOOL, "11": self.TOOL, "12": self.TOOL}
|
||||
|
||||
@@ -221,6 +221,187 @@ class ReconcileTest(unittest.TestCase):
|
||||
self.assertEqual(page.sets, [("10", TOOL_11), ("11", TOOL)])
|
||||
self.assertEqual(state["auto"], {"11": TOOL})
|
||||
|
||||
def test_native_arm64_title_drops_auto_proton_once_and_keeps_explicit_choices(self):
|
||||
(self.steam / "steamapps/appmanifest_11.acf").unlink()
|
||||
(self.state / "state.json").write_text(json.dumps({
|
||||
"default": TOOL,
|
||||
"auto": {"10": TOOL},
|
||||
}))
|
||||
page = FakePage({"0": TOOL, "10": TOOL}, [TOOL, TOOL_11])
|
||||
original_native_targets = COMPAT["native_targets"]
|
||||
COMPAT["native_targets"] = lambda _root, _apps: {"10": None}
|
||||
try:
|
||||
state = self.reconcile(page)
|
||||
self.assertEqual(page.sets, [("10", "")])
|
||||
self.assertEqual(page.mapping["10"], "")
|
||||
self.assertEqual(state["auto"], {})
|
||||
|
||||
page.sets.clear()
|
||||
self.reconcile(page)
|
||||
self.assertEqual(page.sets, [])
|
||||
|
||||
(self.state / "games.json").write_text(json.dumps({
|
||||
"version": 1,
|
||||
"games": {"10": {"valve": True, "dir": "Proton 11.0 (ARM64)"}},
|
||||
}))
|
||||
state = self.reconcile(page)
|
||||
self.assertEqual(page.sets, [("10", TOOL_11)])
|
||||
self.assertEqual(page.mapping["10"], TOOL_11)
|
||||
self.assertEqual(state["auto"], {})
|
||||
(self.state / "games.json").unlink()
|
||||
|
||||
page.sets.clear()
|
||||
page.mapping["10"] = "steamlinuxruntime_arm64"
|
||||
self.reconcile(page)
|
||||
self.assertEqual(page.mapping["10"], "steamlinuxruntime_arm64")
|
||||
self.assertEqual(page.sets, [])
|
||||
|
||||
page.mapping["10"] = TOOL_11
|
||||
self.reconcile(page)
|
||||
self.assertEqual(page.mapping["10"], TOOL_11)
|
||||
self.assertEqual(page.sets, [])
|
||||
|
||||
page.mapping["10"] = COMPAT["FEX_TOOL"]
|
||||
self.reconcile(page)
|
||||
self.assertEqual(page.mapping["10"], COMPAT["FEX_TOOL"])
|
||||
self.assertEqual(page.sets, [])
|
||||
|
||||
page.mapping["10"] = "GE-Proton11-7"
|
||||
self.reconcile(page)
|
||||
self.assertEqual(page.mapping["10"], "GE-Proton11-7")
|
||||
self.assertEqual(page.sets, [])
|
||||
finally:
|
||||
COMPAT["native_targets"] = original_native_targets
|
||||
|
||||
def test_factorio_native_choice_survives_inherited_live_selection_and_restart(self):
|
||||
app = "427520"
|
||||
(self.steam / ("steamapps/appmanifest_%s.acf" % app)).write_text("")
|
||||
config = self.steam / "config/config.vdf"
|
||||
config.parent.mkdir(parents=True)
|
||||
config.write_text(
|
||||
'"InstallConfigStore" { "Software" { "Valve" { "Steam" { '
|
||||
'"CompatToolMapping" { "0" { "name" "%s" } } } } } }' % TOOL)
|
||||
(self.state / "state.json").write_text(json.dumps({
|
||||
"default": TOOL,
|
||||
"auto": {app: TOOL},
|
||||
}))
|
||||
|
||||
original_native_targets = COMPAT["native_targets"]
|
||||
COMPAT["native_targets"] = lambda _root, apps: {
|
||||
app: COMPAT["NATIVE_CHOICE"]
|
||||
} if app in apps else {}
|
||||
try:
|
||||
page = FakePage({"0": TOOL, "10": TOOL, "11": TOOL, app: "proton"}, [TOOL, TOOL_11])
|
||||
state = self.reconcile(page)
|
||||
self.assertEqual(page.sets, [])
|
||||
self.assertEqual(state["auto"], {})
|
||||
self.assertEqual(state["native"], [app])
|
||||
self.assertEqual(COMPAT["read_config_mapping"](str(config)), {"0": TOOL})
|
||||
|
||||
page.sets.clear()
|
||||
state = self.reconcile(page)
|
||||
self.assertEqual(page.sets, [])
|
||||
self.assertEqual(state["native"], [app])
|
||||
restarted = LIVE["Helper"](str(self.steam), 1)
|
||||
restarted.page = page
|
||||
restarted.reconcile()
|
||||
state = json.loads((self.state / "state.json").read_text())
|
||||
self.assertEqual(page.sets, [])
|
||||
self.assertEqual(state["native"], [app])
|
||||
|
||||
config.write_text(
|
||||
'"InstallConfigStore" { "Software" { "Valve" { "Steam" { '
|
||||
'"CompatToolMapping" { "0" { "name" "%s" } '
|
||||
'"%s" { "name" "GE-Proton11-7" } } } } } }' % (TOOL, app))
|
||||
page.mapping[app] = "GE-Proton11-7"
|
||||
state = self.reconcile(page)
|
||||
self.assertEqual(page.mapping[app], "GE-Proton11-7")
|
||||
self.assertEqual(page.sets, [])
|
||||
self.assertEqual(state["native"], [])
|
||||
|
||||
for choice in ("steamlinuxruntime", COMPAT["FEX_TOOL"], TOOL_11):
|
||||
(self.state / "state.json").write_text(json.dumps({
|
||||
"default": TOOL,
|
||||
"auto": {},
|
||||
"native": [app],
|
||||
}))
|
||||
config.write_text(
|
||||
'"InstallConfigStore" { "Software" { "Valve" { "Steam" { '
|
||||
'"CompatToolMapping" { "0" { "name" "%s" } '
|
||||
'"%s" { "name" "%s" } } } } } }' % (TOOL, app, choice))
|
||||
page.mapping[app] = choice
|
||||
page.sets.clear()
|
||||
state = self.reconcile(page)
|
||||
self.assertEqual(page.mapping[app], choice)
|
||||
self.assertEqual(page.sets, [])
|
||||
self.assertEqual(state["native"], [], choice)
|
||||
finally:
|
||||
COMPAT["native_targets"] = original_native_targets
|
||||
|
||||
def test_saved_native_choice_survives_live_restart_without_manifest_or_appinfo(self):
|
||||
app = "452280"
|
||||
config = self.steam / "config/config.vdf"
|
||||
config.parent.mkdir(parents=True)
|
||||
config.write_text(
|
||||
'"InstallConfigStore" { "Software" { "Valve" { "Steam" { '
|
||||
'"CompatToolMapping" { "0" { "name" "%s" } } } } } }' % TOOL)
|
||||
(self.state / "state.json").write_text(json.dumps({
|
||||
"default": TOOL,
|
||||
"auto": {},
|
||||
"native": [app],
|
||||
}))
|
||||
|
||||
original_native_targets = COMPAT["native_targets"]
|
||||
COMPAT["native_targets"] = lambda _root, _apps: {}
|
||||
try:
|
||||
page = FakePage({"0": TOOL, app: "proton"}, [TOOL, TOOL_11])
|
||||
state = self.reconcile(page)
|
||||
self.assertNotIn(app, self.helper.installed)
|
||||
self.assertEqual(state["native"], [app])
|
||||
self.assertNotIn(app, state["auto"])
|
||||
self.assertNotIn((app, TOOL), page.sets)
|
||||
self.assertEqual(COMPAT["read_config_mapping"](str(config)), {"0": TOOL})
|
||||
|
||||
config.write_text('"InstallConfigStore" { "Software" { "Other" {} } }')
|
||||
state = self.reconcile(page)
|
||||
self.assertEqual(state["native"], [app])
|
||||
self.assertNotIn((app, TOOL), page.sets)
|
||||
|
||||
config.write_text(
|
||||
'"InstallConfigStore" { "Software" { "Valve" { "Steam" { '
|
||||
'"CompatToolMapping" { "0" { "name" "%s" } '
|
||||
'"%s" { "name" "GE-Proton11-7" } } } } } }' % (TOOL, app))
|
||||
page.mapping[app] = "proton"
|
||||
state = self.reconcile(page)
|
||||
self.assertEqual(state["native"], [])
|
||||
self.assertNotIn((app, TOOL), page.sets)
|
||||
self.assertEqual(COMPAT["read_config_mapping"](str(config))[app], "GE-Proton11-7")
|
||||
finally:
|
||||
COMPAT["native_targets"] = original_native_targets
|
||||
|
||||
def test_factorio_without_prior_auto_state_uses_the_default_proton(self):
|
||||
app = "427520"
|
||||
(self.steam / ("steamapps/appmanifest_%s.acf" % app)).write_text("")
|
||||
config = self.steam / "config/config.vdf"
|
||||
config.parent.mkdir(parents=True)
|
||||
config.write_text(
|
||||
'"InstallConfigStore" { "Software" { "Valve" { "Steam" { '
|
||||
'"CompatToolMapping" { "0" { "name" "%s" } } } } } }' % TOOL)
|
||||
(self.state / "state.json").write_text(json.dumps({"default": TOOL, "auto": {}}))
|
||||
page = FakePage({"0": TOOL, "10": TOOL, "11": TOOL, app: "proton"}, [TOOL, TOOL_11])
|
||||
original_native_targets = COMPAT["native_targets"]
|
||||
COMPAT["native_targets"] = lambda _root, apps: {
|
||||
app: COMPAT["NATIVE_CHOICE"]
|
||||
} if app in apps else {}
|
||||
try:
|
||||
state = self.reconcile(page)
|
||||
self.assertEqual(page.sets, [(app, TOOL)])
|
||||
self.assertEqual(state["auto"], {app: TOOL})
|
||||
self.assertEqual(state["native"], [])
|
||||
self.assertEqual(COMPAT["read_config_mapping"](str(config)), {"0": TOOL})
|
||||
finally:
|
||||
COMPAT["native_targets"] = original_native_targets
|
||||
|
||||
def test_missing_per_game_proton_stays_deferred_live(self):
|
||||
page = FakePage({"0": TOOL, "10": "GE-Proton9-1", "11": TOOL_11}, [TOOL, TOOL_11])
|
||||
(self.state / "games.json").write_text(json.dumps({
|
||||
|
||||
Reference in new issue
Block a user