Preserve native Linux and FEX compatibility choices (#749)

This commit is contained in:
Kurt Himebauch authored and GitHub committed 2026-10-14 00:57:45 -04:00
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)
+31 -7
View File
@@ -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:
+228
View File
@@ -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}
+181
View File
@@ -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({