steam-engine 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- steam_engine/__init__.py +7 -0
- steam_engine/cdp.py +105 -0
- steam_engine/cli.py +58 -0
- steam_engine/client.py +105 -0
- steam_engine/enable.py +64 -0
- steam_engine/jsproxy.py +64 -0
- steam_engine-0.1.0.dist-info/METADATA +87 -0
- steam_engine-0.1.0.dist-info/RECORD +10 -0
- steam_engine-0.1.0.dist-info/WHEEL +4 -0
- steam_engine-0.1.0.dist-info/entry_points.txt +2 -0
steam_engine/__init__.py
ADDED
steam_engine/cdp.py
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""Minimal synchronous Chrome DevTools Protocol client for Steam's CEF webhelper."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import urllib.request
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
|
|
9
|
+
from websockets.sync.client import connect as ws_connect
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class Target:
|
|
14
|
+
kind: str
|
|
15
|
+
title: str
|
|
16
|
+
url: str
|
|
17
|
+
ws_url: str
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def list_targets(port: int = 1337) -> list[Target]:
|
|
21
|
+
"""GET http://127.0.0.1:<port>/json — all debuggable targets."""
|
|
22
|
+
with urllib.request.urlopen(f"http://127.0.0.1:{port}/json", timeout=5) as r:
|
|
23
|
+
raw = json.load(r)
|
|
24
|
+
return [
|
|
25
|
+
Target(
|
|
26
|
+
kind=t.get("type", ""),
|
|
27
|
+
title=t.get("title", ""),
|
|
28
|
+
url=t.get("url", ""),
|
|
29
|
+
ws_url=t.get("webSocketDebuggerUrl", ""),
|
|
30
|
+
)
|
|
31
|
+
for t in raw
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def pick_ui_target(targets: list[Target]) -> Target | None:
|
|
36
|
+
"""The SharedJSContext page — where collectionStore/appStore/SteamClient live."""
|
|
37
|
+
for t in targets:
|
|
38
|
+
if t.title == "SharedJSContext":
|
|
39
|
+
return t
|
|
40
|
+
for t in targets:
|
|
41
|
+
if "steamloopback.host" in t.url:
|
|
42
|
+
return t
|
|
43
|
+
return None
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class Session:
|
|
47
|
+
"""A CDP session attached to one target. Sequential request/response only —
|
|
48
|
+
events are discarded. Not thread-safe."""
|
|
49
|
+
|
|
50
|
+
def __init__(self, ws):
|
|
51
|
+
self._ws = ws
|
|
52
|
+
self._id = 0
|
|
53
|
+
|
|
54
|
+
@classmethod
|
|
55
|
+
def connect(cls, port: int = 1337, target: Target | None = None) -> Session:
|
|
56
|
+
if target is None:
|
|
57
|
+
target = pick_ui_target(list_targets(port))
|
|
58
|
+
if target is None or not target.ws_url:
|
|
59
|
+
raise RuntimeError(
|
|
60
|
+
"SharedJSContext target not found — is Steam running with "
|
|
61
|
+
"--remote-debugging-port open?"
|
|
62
|
+
)
|
|
63
|
+
return cls(ws_connect(target.ws_url, open_timeout=10))
|
|
64
|
+
|
|
65
|
+
def eval(self, expression: str):
|
|
66
|
+
"""Runtime.evaluate with returnByValue+awaitPromise. Returns result.value."""
|
|
67
|
+
self._id += 1
|
|
68
|
+
my_id = self._id
|
|
69
|
+
self._ws.send(
|
|
70
|
+
json.dumps(
|
|
71
|
+
{
|
|
72
|
+
"id": my_id,
|
|
73
|
+
"method": "Runtime.evaluate",
|
|
74
|
+
"params": {
|
|
75
|
+
"expression": expression,
|
|
76
|
+
"returnByValue": True,
|
|
77
|
+
"awaitPromise": True,
|
|
78
|
+
},
|
|
79
|
+
}
|
|
80
|
+
)
|
|
81
|
+
)
|
|
82
|
+
while True:
|
|
83
|
+
msg = json.loads(self._ws.recv())
|
|
84
|
+
if msg.get("id") != my_id:
|
|
85
|
+
continue # event or unrelated response
|
|
86
|
+
result = msg.get("result", {})
|
|
87
|
+
exc = result.get("exceptionDetails")
|
|
88
|
+
if exc:
|
|
89
|
+
desc = (
|
|
90
|
+
exc.get("exception", {}).get("description")
|
|
91
|
+
or exc.get("text")
|
|
92
|
+
or "js exception"
|
|
93
|
+
)
|
|
94
|
+
raise RuntimeError(f"JS exception: {desc}")
|
|
95
|
+
return result.get("result", {}).get("value")
|
|
96
|
+
|
|
97
|
+
def eval_json(self, expression: str):
|
|
98
|
+
"""Evaluate an expression that returns a JSON.stringify'ed string."""
|
|
99
|
+
v = self.eval(expression)
|
|
100
|
+
if not isinstance(v, str):
|
|
101
|
+
raise RuntimeError(f"expected JSON string result, got {type(v).__name__}")
|
|
102
|
+
return json.loads(v)
|
|
103
|
+
|
|
104
|
+
def close(self):
|
|
105
|
+
self._ws.close()
|
steam_engine/cli.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""steam-engine CLI — generic driver commands only."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import sys
|
|
9
|
+
|
|
10
|
+
from .cdp import list_targets
|
|
11
|
+
from .client import SteamEngine
|
|
12
|
+
from .enable import enable as _enable
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _parser() -> argparse.ArgumentParser:
|
|
16
|
+
p = argparse.ArgumentParser(
|
|
17
|
+
prog="steam-engine",
|
|
18
|
+
description="Drive a running Steam client's internal JS (SteamClient.*, window.* stores) over CDP",
|
|
19
|
+
)
|
|
20
|
+
p.add_argument("--port", type=int,
|
|
21
|
+
default=int(os.environ.get("STEAM_CDP_PORT", "1337")),
|
|
22
|
+
help="remote-debugging port (env STEAM_CDP_PORT)")
|
|
23
|
+
sub = p.add_subparsers(dest="cmd", required=True)
|
|
24
|
+
|
|
25
|
+
sub.add_parser("status", help="probe the CDP endpoint")
|
|
26
|
+
sub.add_parser("targets", help="list debuggable targets")
|
|
27
|
+
e = sub.add_parser("enable", help="patch webhelper wrapper (Linux)")
|
|
28
|
+
e.add_argument("--restart", action="store_true",
|
|
29
|
+
help="kill steamwebhelper so Steam respawns it patched")
|
|
30
|
+
ev = sub.add_parser("eval", help="evaluate raw JS in SharedJSContext")
|
|
31
|
+
ev.add_argument("expr")
|
|
32
|
+
|
|
33
|
+
return p
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def main(argv: list[str] | None = None) -> int:
|
|
37
|
+
args = _parser().parse_args(argv)
|
|
38
|
+
|
|
39
|
+
if args.cmd == "enable":
|
|
40
|
+
print(_enable(args.port, restart=args.restart))
|
|
41
|
+
return 0
|
|
42
|
+
if args.cmd == "status":
|
|
43
|
+
st = SteamEngine.status(args.port)
|
|
44
|
+
print(json.dumps(st, indent=2))
|
|
45
|
+
return 0 if st["reachable"] else 1
|
|
46
|
+
if args.cmd == "targets":
|
|
47
|
+
for t in list_targets(args.port):
|
|
48
|
+
print(f"{t.kind:<10} {t.title:<35} {t.url[:80]}")
|
|
49
|
+
return 0
|
|
50
|
+
if args.cmd == "eval":
|
|
51
|
+
with SteamEngine.connect(args.port) as se:
|
|
52
|
+
print(json.dumps(se.eval(args.expr), indent=2, default=str))
|
|
53
|
+
return 0
|
|
54
|
+
return 1
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
if __name__ == "__main__":
|
|
58
|
+
sys.exit(main())
|
steam_engine/client.py
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""SteamEngine — main entry point.
|
|
2
|
+
|
|
3
|
+
from steam_engine import SteamEngine
|
|
4
|
+
|
|
5
|
+
with SteamEngine.connect() as se:
|
|
6
|
+
se.client.Apps.SetAppLaunchOptions(292030, "-fullscreen")
|
|
7
|
+
apps = se.window.appStore.allApps.get()
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
from . import cdp
|
|
16
|
+
from .enable import enable as _enable
|
|
17
|
+
from .jsproxy import JsProxy
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class SteamEngine:
|
|
21
|
+
def __init__(self, session: cdp.Session):
|
|
22
|
+
self._session = session
|
|
23
|
+
self._name_cache: dict[tuple[str, str], str | None] = {}
|
|
24
|
+
# SteamClient.* — PascalCase bridge into the native client.
|
|
25
|
+
self.client = JsProxy(self, "SteamClient", convert=True)
|
|
26
|
+
# window.* — every store (collectionStore, appStore, downloadsStore, ...).
|
|
27
|
+
self.window = JsProxy(self, "window")
|
|
28
|
+
|
|
29
|
+
@classmethod
|
|
30
|
+
def connect(cls, port: int = 1337) -> SteamEngine:
|
|
31
|
+
"""Attach to Steam's SharedJSContext via the remote-debugging port."""
|
|
32
|
+
return cls(cdp.Session.connect(port))
|
|
33
|
+
|
|
34
|
+
def __enter__(self) -> SteamEngine:
|
|
35
|
+
return self
|
|
36
|
+
|
|
37
|
+
def __exit__(self, *exc):
|
|
38
|
+
self.close()
|
|
39
|
+
|
|
40
|
+
def close(self):
|
|
41
|
+
self._session.close()
|
|
42
|
+
|
|
43
|
+
# ---- low level ------------------------------------------------------
|
|
44
|
+
|
|
45
|
+
def eval(self, expression: str) -> Any:
|
|
46
|
+
"""Evaluate raw JS in SharedJSContext. Returns result.value."""
|
|
47
|
+
return self._session.eval(expression)
|
|
48
|
+
|
|
49
|
+
def eval_json(self, expression: str) -> Any:
|
|
50
|
+
return self._session.eval_json(expression)
|
|
51
|
+
|
|
52
|
+
def get(self, path: str) -> Any:
|
|
53
|
+
return self._session.eval_json(f"JSON.stringify({path})")
|
|
54
|
+
|
|
55
|
+
def _resolve_name(self, parent: str, name: str) -> str | None:
|
|
56
|
+
"""Find the real member name on `parent`, ignoring case/underscores —
|
|
57
|
+
so `get_os_type` finds `GetOSType`."""
|
|
58
|
+
key = name.replace("_", "").lower()
|
|
59
|
+
cache_key = (parent, key)
|
|
60
|
+
if cache_key not in self._name_cache:
|
|
61
|
+
self._name_cache[cache_key] = self._session.eval_json(
|
|
62
|
+
f"JSON.stringify(Object.getOwnPropertyNames({parent})"
|
|
63
|
+
f".find(n=>n.toLowerCase()==={json.dumps(key)}) ?? null)"
|
|
64
|
+
)
|
|
65
|
+
return self._name_cache[cache_key]
|
|
66
|
+
|
|
67
|
+
def call(self, path: str, *args: Any, convert: bool = False) -> Any:
|
|
68
|
+
"""Call `path(...args)`; args are JSON-serialized. Awaits promises.
|
|
69
|
+
Returns the JSON-decoded result, or None for undefined/unserializable."""
|
|
70
|
+
if convert:
|
|
71
|
+
parent, _, leaf = path.rpartition(".")
|
|
72
|
+
resolved = self._resolve_name(parent, leaf)
|
|
73
|
+
if resolved:
|
|
74
|
+
path = f"{parent}.{resolved}"
|
|
75
|
+
arglist = ",".join(json.dumps(a) for a in args)
|
|
76
|
+
expr = (
|
|
77
|
+
"(async()=>{"
|
|
78
|
+
f"const r = await {path}({arglist});"
|
|
79
|
+
"try { return JSON.stringify({v: r === undefined ? null : r}); }"
|
|
80
|
+
"catch { return JSON.stringify({v: null, unserializable: typeof r}); }"
|
|
81
|
+
"})()"
|
|
82
|
+
)
|
|
83
|
+
out = self._session.eval(expr)
|
|
84
|
+
return json.loads(out).get("v") if isinstance(out, str) else out
|
|
85
|
+
|
|
86
|
+
# ---- enable / status ------------------------------------------------
|
|
87
|
+
|
|
88
|
+
@staticmethod
|
|
89
|
+
def status(port: int = 1337) -> dict:
|
|
90
|
+
"""Probe the debug endpoint without attaching a session."""
|
|
91
|
+
try:
|
|
92
|
+
targets = cdp.list_targets(port)
|
|
93
|
+
except Exception as e:
|
|
94
|
+
return {"reachable": False, "error": str(e), "port": port}
|
|
95
|
+
return {
|
|
96
|
+
"reachable": True,
|
|
97
|
+
"port": port,
|
|
98
|
+
"targets": len(targets),
|
|
99
|
+
"shared_js_context": any(t.title == "SharedJSContext" for t in targets),
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
@staticmethod
|
|
103
|
+
def enable(port: int = 1337, restart: bool = False) -> str:
|
|
104
|
+
"""Patch the webhelper wrapper so it opens the debug port (Linux)."""
|
|
105
|
+
return _enable(port, restart)
|
steam_engine/enable.py
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""Enable the CEF remote-debugging port on the Steam client."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import subprocess
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
WRAP = "ubuntu12_64/steamwebhelper_sniper_wrap.sh"
|
|
10
|
+
EXEC_LINE = 'exec ./steamwebhelper "$@"'
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _steam_dir() -> Path | None:
|
|
14
|
+
home = Path.home()
|
|
15
|
+
candidates = [
|
|
16
|
+
home / ".local/share/Steam",
|
|
17
|
+
home / ".steam/steam",
|
|
18
|
+
home / ".var/app/com.valvesoftware.Steam/.local/share/Steam", # flatpak
|
|
19
|
+
]
|
|
20
|
+
for c in candidates:
|
|
21
|
+
if (c / WRAP).exists():
|
|
22
|
+
return c
|
|
23
|
+
return None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def enable(port: int = 1337, restart: bool = False) -> str:
|
|
27
|
+
"""Patch steamwebhelper_sniper_wrap.sh to pass --remote-debugging-port.
|
|
28
|
+
Returns a human-readable status message."""
|
|
29
|
+
if sys.platform != "linux":
|
|
30
|
+
raise RuntimeError(
|
|
31
|
+
"on this platform, launch Steam with -cef-enable-debugging "
|
|
32
|
+
"(Windows/macOS) or inject --remote-debugging-port into the "
|
|
33
|
+
"steamwebhelper command line"
|
|
34
|
+
)
|
|
35
|
+
steam = _steam_dir()
|
|
36
|
+
if steam is None:
|
|
37
|
+
raise RuntimeError("could not locate " + WRAP)
|
|
38
|
+
wrap = steam / WRAP
|
|
39
|
+
body = wrap.read_text()
|
|
40
|
+
if "--remote-debugging-port" in body:
|
|
41
|
+
msg = f"{wrap} already patched"
|
|
42
|
+
else:
|
|
43
|
+
if EXEC_LINE not in body:
|
|
44
|
+
raise RuntimeError(
|
|
45
|
+
f"unexpected wrap script contents - edit {wrap} manually"
|
|
46
|
+
)
|
|
47
|
+
patched = (
|
|
48
|
+
"exec ./steamwebhelper --remote-debugging-address=127.0.0.1 "
|
|
49
|
+
f"--remote-debugging-port={port} \"$@\""
|
|
50
|
+
)
|
|
51
|
+
wrap.with_suffix(".sh.bak").write_text(body)
|
|
52
|
+
wrap.write_text(body.replace(EXEC_LINE, patched))
|
|
53
|
+
msg = f"patched {wrap}"
|
|
54
|
+
if restart:
|
|
55
|
+
# the browser process is the only one carrying -uimode in argv
|
|
56
|
+
r = subprocess.run(["pkill", "-f", "steamwebhelper.*-uimode"])
|
|
57
|
+
msg += (
|
|
58
|
+
"; steamwebhelper killed - Steam will respawn it with the debug port"
|
|
59
|
+
if r.returncode == 0
|
|
60
|
+
else "; no running steamwebhelper found"
|
|
61
|
+
)
|
|
62
|
+
else:
|
|
63
|
+
msg += "; restart Steam (or pass restart=True) to activate"
|
|
64
|
+
return msg
|
steam_engine/jsproxy.py
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""Lazy JS path proxy — `se.client.Apps.RunGame(123)` evaluates
|
|
2
|
+
`SteamClient.Apps.RunGame(123)` in the page; `se.window.appStore.allApps.get()`
|
|
3
|
+
reads the property. Nothing is bound until called, so the entire
|
|
4
|
+
SteamClient.* / window.* surface is reachable without hand-written bindings."""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
from typing import Any, Protocol
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Engine(Protocol):
|
|
13
|
+
"""What a proxy needs from its host — any object providing these works,
|
|
14
|
+
including test doubles."""
|
|
15
|
+
|
|
16
|
+
def call(self, path: str, *args: Any, convert: bool = False) -> Any: ...
|
|
17
|
+
def get(self, path: str) -> Any: ...
|
|
18
|
+
def eval_json(self, expression: str) -> Any: ...
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _to_js_name(name: str, convert: bool) -> str:
|
|
22
|
+
"""snake_case/lowerCamel -> PascalCase, for SteamClient.* ergonomics."""
|
|
23
|
+
if not convert:
|
|
24
|
+
return name
|
|
25
|
+
if "_" in name:
|
|
26
|
+
return "".join(p.title() for p in name.split("_") if p)
|
|
27
|
+
if name[:1].islower():
|
|
28
|
+
return name[0].upper() + name[1:]
|
|
29
|
+
return name
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class JsProxy:
|
|
33
|
+
__slots__ = ("_se", "_path", "_convert")
|
|
34
|
+
|
|
35
|
+
def __init__(self, se: Engine, path: str, convert: bool = False):
|
|
36
|
+
self._se = se
|
|
37
|
+
self._path = path
|
|
38
|
+
self._convert = convert
|
|
39
|
+
|
|
40
|
+
def __getattr__(self, name: str) -> JsProxy:
|
|
41
|
+
if name.startswith("_"):
|
|
42
|
+
raise AttributeError(name)
|
|
43
|
+
return JsProxy(
|
|
44
|
+
self._se, f"{self._path}.{_to_js_name(name, self._convert)}", self._convert
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
def __getitem__(self, key) -> JsProxy:
|
|
48
|
+
return JsProxy(self._se, f"{self._path}[{json.dumps(key)}]", self._convert)
|
|
49
|
+
|
|
50
|
+
def __call__(self, *args: Any) -> Any:
|
|
51
|
+
return self._se.call(self._path, *args, convert=self._convert)
|
|
52
|
+
|
|
53
|
+
def get(self) -> Any:
|
|
54
|
+
"""Read the property value (must be JSON-serializable)."""
|
|
55
|
+
return self._se.get(self._path)
|
|
56
|
+
|
|
57
|
+
def keys(self) -> list[str]:
|
|
58
|
+
"""Own property names — handy when .get() hits unserializable objects."""
|
|
59
|
+
return self._se.eval_json(
|
|
60
|
+
f"JSON.stringify(Object.getOwnPropertyNames({self._path}))"
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
def __repr__(self) -> str:
|
|
64
|
+
return f"<JsProxy {self._path}>"
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: steam-engine
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Drive a running Steam client's internals over Chrome DevTools Protocol (collections, apps, downloads, all of SteamClient.*)
|
|
5
|
+
License: MIT
|
|
6
|
+
Requires-Python: >=3.11
|
|
7
|
+
Requires-Dist: websockets>=12
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
|
|
10
|
+
# steam-engine
|
|
11
|
+
|
|
12
|
+
Call a running Steam client’s internal JavaScript API from Python.
|
|
13
|
+
|
|
14
|
+
`steam-engine` connects to Steam’s CEF renderer over Chrome DevTools Protocol. It exposes `SteamClient.*` methods and `window.*` stores through a Python proxy, so operations run through Steam itself.
|
|
15
|
+
|
|
16
|
+
For a higher-level collections API, see the sibling project `steam-collections`.
|
|
17
|
+
|
|
18
|
+
## Get started
|
|
19
|
+
|
|
20
|
+
Install from the repository, then enable debugging in Steam:
|
|
21
|
+
|
|
22
|
+
```sh
|
|
23
|
+
pip install -e .
|
|
24
|
+
steam-engine enable --restart
|
|
25
|
+
steam-engine status
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
`enable` is Linux-only. It patches `ubuntu12_64/steamwebhelper_sniper_wrap.sh` and backs up the original to `.sh.bak`. Steam updates can replace this wrapper; run `enable` again if the connection stops working.
|
|
29
|
+
|
|
30
|
+
On Windows or macOS, launch Steam with `-cef-enable-debugging` or add `--remote-debugging-port` to the webhelper command line.
|
|
31
|
+
|
|
32
|
+
The default port is `1337`. Set `STEAM_CDP_PORT` to use another port.
|
|
33
|
+
|
|
34
|
+
## Use from Python
|
|
35
|
+
|
|
36
|
+
Steam must be running with debugging enabled.
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
from steam_engine import SteamEngine
|
|
40
|
+
|
|
41
|
+
with SteamEngine.connect() as se:
|
|
42
|
+
# Call Steam's native bridge.
|
|
43
|
+
se.client.Apps.SetAppLaunchOptions(292030, "-fullscreen")
|
|
44
|
+
|
|
45
|
+
# Snake_case works too.
|
|
46
|
+
se.client.Apps.specify_compat_tool(105600, "proton_experimental")
|
|
47
|
+
|
|
48
|
+
# Read a JavaScript property.
|
|
49
|
+
apps = se.window.appStore.allApps.get()
|
|
50
|
+
|
|
51
|
+
# Run JavaScript directly.
|
|
52
|
+
count = se.eval("collectionStore.userCollections.length")
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
The proxy supports:
|
|
56
|
+
|
|
57
|
+
| Syntax | Behavior |
|
|
58
|
+
| --------------------- | ------------------------------------------------------------ |
|
|
59
|
+
| `proxy.method(*args)` | Call a method with JSON-serialized arguments; await promises |
|
|
60
|
+
| `proxy.prop.get()` | Read a JSON-serializable property |
|
|
61
|
+
| `proxy.prop.keys()` | List own property names |
|
|
62
|
+
| `proxy.prop[index]` | Access an indexed value |
|
|
63
|
+
|
|
64
|
+
Method names ignore case and underscores. Results are JSON-decoded; `undefined` and unserializable results become `None`.
|
|
65
|
+
|
|
66
|
+
## Use from the terminal
|
|
67
|
+
|
|
68
|
+
```sh
|
|
69
|
+
steam-engine status # Check the connection
|
|
70
|
+
steam-engine targets # List debuggable targets
|
|
71
|
+
steam-engine enable --restart # Enable debugging and restart Steam (Linux)
|
|
72
|
+
steam-engine eval 'collectionStore.userCollections.length'
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## API reference
|
|
76
|
+
|
|
77
|
+
The generated reference in `docs/` lists the discovered methods and stores. To refresh and browse it:
|
|
78
|
+
|
|
79
|
+
```sh
|
|
80
|
+
python scripts/dump_api_surface.py
|
|
81
|
+
mkdocs serve
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Raw data is available at `docs/data/api_surface.json`.
|
|
85
|
+
|
|
86
|
+
Steam’s internal API is undocumented. Method names and arguments can change between client builds.
|
|
87
|
+
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
steam_engine/__init__.py,sha256=fw8dMIs-pakiaY7A4l0LtQLvN81ESOsLyq8aH0TR9js,196
|
|
2
|
+
steam_engine/cdp.py,sha256=qdROL3SwuEOpuzY_1A-EJFPA6ZVEDE5VkSnapINUz9o,3298
|
|
3
|
+
steam_engine/cli.py,sha256=q1FAhWHltWg1iIvLFpVP7iIzYxy0oIHPQfMSZpW6--k,1871
|
|
4
|
+
steam_engine/client.py,sha256=N7kksLvOeZC-rDdQoKvR_ru0nUh_IqOF0rol5nZ2HT4,3873
|
|
5
|
+
steam_engine/enable.py,sha256=BLIcE4gNanVs5zhmn1dN23cMl_0ZXafxoc3j6PoPVkc,2165
|
|
6
|
+
steam_engine/jsproxy.py,sha256=D7iE1qiIAbYpHHonnACCAVMZ2b46D3vomctrjQHWEW4,2160
|
|
7
|
+
steam_engine-0.1.0.dist-info/METADATA,sha256=MG_oSMQlBVxZ2Ijy8ULfEsHTpkTsuc8gAcIfG2wX_xE,3060
|
|
8
|
+
steam_engine-0.1.0.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
|
|
9
|
+
steam_engine-0.1.0.dist-info/entry_points.txt,sha256=RdL_xC83JMOIk2pePftOlnQswaKGyfiQNh4CcWDFuJU,55
|
|
10
|
+
steam_engine-0.1.0.dist-info/RECORD,,
|