diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-steam-compat b/tools/linuxfs/overlay/usr/local/bin/droiddeck-steam-compat index 24b2ad88..e1ab89bf 100755 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-steam-compat +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-steam-compat @@ -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) diff --git a/tools/linuxfs/overlay/usr/local/bin/steam-compatibility b/tools/linuxfs/overlay/usr/local/bin/steam-compatibility index 71557cf0..39149751 100755 --- a/tools/linuxfs/overlay/usr/local/bin/steam-compatibility +++ b/tools/linuxfs/overlay/usr/local/bin/steam-compatibility @@ -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) diff --git a/tools/tests/test_droiddeck_fex.py b/tools/tests/test_droiddeck_fex.py index 20ccad02..8142e1ab 100644 --- a/tools/tests/test_droiddeck_fex.py +++ b/tools/tests/test_droiddeck_fex.py @@ -499,19 +499,23 @@ class AppInfoTest(unittest.TestCase): table = struct.pack("