steamlayer-core 0.1.0__tar.gz
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.
- steamlayer_core-0.1.0/LICENSE +21 -0
- steamlayer_core-0.1.0/PKG-INFO +188 -0
- steamlayer_core-0.1.0/README.md +171 -0
- steamlayer_core-0.1.0/pyproject.toml +54 -0
- steamlayer_core-0.1.0/setup.cfg +4 -0
- steamlayer_core-0.1.0/steamlayer_core/__init__.py +110 -0
- steamlayer_core-0.1.0/steamlayer_core/_version.py +6 -0
- steamlayer_core-0.1.0/steamlayer_core/adapters.py +78 -0
- steamlayer_core-0.1.0/steamlayer_core/api.py +508 -0
- steamlayer_core-0.1.0/steamlayer_core/constants.py +14 -0
- steamlayer_core-0.1.0/steamlayer_core/discovery/__init__.py +19 -0
- steamlayer_core-0.1.0/steamlayer_core/discovery/dlc.py +189 -0
- steamlayer_core-0.1.0/steamlayer_core/discovery/engine.py +391 -0
- steamlayer_core-0.1.0/steamlayer_core/discovery/local.py +51 -0
- steamlayer_core-0.1.0/steamlayer_core/discovery/matcher.py +143 -0
- steamlayer_core-0.1.0/steamlayer_core/discovery/query_strategy.py +99 -0
- steamlayer_core-0.1.0/steamlayer_core/discovery/repository.py +130 -0
- steamlayer_core-0.1.0/steamlayer_core/discovery/web.py +37 -0
- steamlayer_core-0.1.0/steamlayer_core/domain/__init__.py +0 -0
- steamlayer_core-0.1.0/steamlayer_core/domain/exceptions.py +148 -0
- steamlayer_core-0.1.0/steamlayer_core/domain/models.py +248 -0
- steamlayer_core-0.1.0/steamlayer_core/events.py +81 -0
- steamlayer_core-0.1.0/steamlayer_core/http_client.py +135 -0
- steamlayer_core-0.1.0/steamlayer_core/patching/__init__.py +66 -0
- steamlayer_core-0.1.0/steamlayer_core/patching/config.py +95 -0
- steamlayer_core-0.1.0/steamlayer_core/patching/engine.py +338 -0
- steamlayer_core-0.1.0/steamlayer_core/patching/models.py +87 -0
- steamlayer_core-0.1.0/steamlayer_core/patching/scanner.py +217 -0
- steamlayer_core-0.1.0/steamlayer_core/patching/vault.py +225 -0
- steamlayer_core-0.1.0/steamlayer_core/patching/vendors.py +145 -0
- steamlayer_core-0.1.0/steamlayer_core/protocols.py +196 -0
- steamlayer_core-0.1.0/steamlayer_core/utils.py +18 -0
- steamlayer_core-0.1.0/steamlayer_core.egg-info/PKG-INFO +188 -0
- steamlayer_core-0.1.0/steamlayer_core.egg-info/SOURCES.txt +35 -0
- steamlayer_core-0.1.0/steamlayer_core.egg-info/dependency_links.txt +1 -0
- steamlayer_core-0.1.0/steamlayer_core.egg-info/requires.txt +1 -0
- steamlayer_core-0.1.0/steamlayer_core.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 layeredtools
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: steamlayer-core
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Emulator-agnostic engine for identifying Steam games, patching DRM, and hydrating DLC metadata.
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Project-URL: Source, https://github.com/layeredtools/steamlayer-core
|
|
7
|
+
Project-URL: Issues, https://github.com/layeredtools/steamlayer-core/issues
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
11
|
+
Classifier: Environment :: Console
|
|
12
|
+
Requires-Python: >=3.13
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
License-File: LICENSE
|
|
15
|
+
Requires-Dist: requests>=2.33.1
|
|
16
|
+
Dynamic: license-file
|
|
17
|
+
|
|
18
|
+
<div align="center">
|
|
19
|
+
|
|
20
|
+
# steamlayer-core
|
|
21
|
+
|
|
22
|
+
[](https://pypi.org/project/steamlayer-core/)
|
|
23
|
+
[](https://pypi.org/project/steamlayer-core/)
|
|
24
|
+
[](https://github.com/layeredtools/steamlayer-core/actions)
|
|
25
|
+
[](LICENSE)
|
|
26
|
+
|
|
27
|
+
The emulator-agnostic engine behind SteamLayer. Handles Steam game identification,
|
|
28
|
+
DRM patching, and DLC hydration — with no opinion about which emulator you use.
|
|
29
|
+
</div>
|
|
30
|
+
|
|
31
|
+
## Overview
|
|
32
|
+
|
|
33
|
+
`steamlayer-core` is a Python library that provides a clean pipeline for:
|
|
34
|
+
|
|
35
|
+
1. **Resolving** a game directory to its Steam AppID (local files → app index → store search → disambiguation)
|
|
36
|
+
2. **Patching** the game by swapping Steam API DLLs with emulator binaries and writing config
|
|
37
|
+
3. **Unpatching** to restore the original files from a local backup vault
|
|
38
|
+
4. **Fetching DLC metadata** for a given AppID, with disk caching
|
|
39
|
+
|
|
40
|
+
The library is designed to be embedded inside higher-level tools. It has no `print()` or
|
|
41
|
+
`input()` calls anywhere — all user interaction is delegated to injected callbacks.
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## Installation
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
pip install steamlayer-core
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Python 3.13+ is required.
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## Quick start
|
|
56
|
+
|
|
57
|
+
### One-shot functions
|
|
58
|
+
|
|
59
|
+
The simplest way to use the library. Each call opens its own HTTP session and closes it
|
|
60
|
+
when done.
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
from pathlib import Path
|
|
64
|
+
from steamlayer_core.api import resolve_game, patch_game
|
|
65
|
+
|
|
66
|
+
# 1. Identify the game — resolve_game checks the directory for a steam_appid.txt
|
|
67
|
+
# or an appmanifest_*.acf file first. If found, no network call is made.
|
|
68
|
+
game = resolve_game(Path("C:/games/Euro Truck Simulator 2"))
|
|
69
|
+
print(game.appid, game.game_name) # 227300, Euro Truck Simulator 2
|
|
70
|
+
|
|
71
|
+
# 2. Patch it (requires a VendorProvider and ConfigWriter from your emulator layer)
|
|
72
|
+
result = patch_game(game, Path("C:/games/Euro Truck Simulator 2"), vendor=vendor, config_writer=writer)
|
|
73
|
+
print(result.patched_files)
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
If no local marker is found, the engine falls back to the app index and then a live
|
|
77
|
+
store search. You can also skip the path entirely and trigger a web search directly by
|
|
78
|
+
passing the game name as a string:
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
# No path — goes straight to index/store search
|
|
82
|
+
game = resolve_game("Euro Truck Simulator 2")
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Stateful client
|
|
86
|
+
|
|
87
|
+
Use `SteamLayerClient` when you need to reuse a single HTTP session across multiple
|
|
88
|
+
operations, wire progress callbacks once, or manage the lifecycle explicitly.
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
from pathlib import Path
|
|
92
|
+
from steamlayer_core import SteamLayerClient
|
|
93
|
+
|
|
94
|
+
with SteamLayerClient(vendor=vendor, config_writer=writer, progress=my_progress_hook) as client:
|
|
95
|
+
game = client.resolve(Path("C:/games/Euro Truck Simulator 2"))
|
|
96
|
+
dlcs = client.fetch_dlcs(game.appid)
|
|
97
|
+
result = client.patch(game, Path("C:/games/Euro Truck Simulator 2"))
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Reverting a patch
|
|
101
|
+
|
|
102
|
+
```python
|
|
103
|
+
from steamlayer_core.api import SteamLayerClient
|
|
104
|
+
|
|
105
|
+
with SteamLayerClient() as client:
|
|
106
|
+
if client.is_patched(game_path):
|
|
107
|
+
restored = client.unpatch(game_path)
|
|
108
|
+
print(f"Restored {len(restored)} file(s)")
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
## Core concepts
|
|
113
|
+
|
|
114
|
+
### Resolution waterfall
|
|
115
|
+
|
|
116
|
+
When you call `resolve()` or `resolve_game()`, the engine tries each strategy in order,
|
|
117
|
+
stopping as soon as a confident match is found:
|
|
118
|
+
|
|
119
|
+
```
|
|
120
|
+
Game directory
|
|
121
|
+
│
|
|
122
|
+
▼
|
|
123
|
+
Local file inspection (steam_appid.txt, appmanifest_*.acf, ...)
|
|
124
|
+
│ no match
|
|
125
|
+
▼
|
|
126
|
+
App index lookup (offline index bundled with the library)
|
|
127
|
+
│ no match / low confidence
|
|
128
|
+
▼
|
|
129
|
+
Steam store search (live web query, skipped if allow_network=False)
|
|
130
|
+
│ ambiguous
|
|
131
|
+
▼
|
|
132
|
+
Disambiguation callback (your on_disambiguation handler, or AmbiguousMatchError)
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### Emulator agnosticism
|
|
136
|
+
|
|
137
|
+
`steamlayer-core` never references a specific emulator. It interacts with two
|
|
138
|
+
protocols that you implement (or get from an emulator-specific package):
|
|
139
|
+
|
|
140
|
+
| Protocol | Responsibility |
|
|
141
|
+
|---|---|
|
|
142
|
+
| `VendorProvider` | Supplies the emulator DLL binaries to be written into the game directory |
|
|
143
|
+
| `ConfigWriter` | Defines what config files to write and where (AppID, DLC list, etc.) |
|
|
144
|
+
|
|
145
|
+
This means the same core library can drive Goldberg, or any future emulator, just by
|
|
146
|
+
swapping the injected implementations.
|
|
147
|
+
|
|
148
|
+
### Backup vault
|
|
149
|
+
|
|
150
|
+
Before any file is overwritten, `patch()` creates a local vault inside the game
|
|
151
|
+
directory. `unpatch()` reads exclusively from this vault — it does not need to know
|
|
152
|
+
which emulator was used. `purge_vault=True` (default) deletes the vault after a
|
|
153
|
+
successful restore.
|
|
154
|
+
|
|
155
|
+
### DLC caching
|
|
156
|
+
|
|
157
|
+
`fetch_dlcs()` caches results to `~/.steamlayer/.cache/dlcs_{appid}.json`. Subsequent
|
|
158
|
+
calls within the TTL window never touch the network. The cache path and TTL are
|
|
159
|
+
configurable via `SteamlayerOptions`.
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
## Design principles
|
|
163
|
+
|
|
164
|
+
- **No I/O side-effects.** No `print()`, no `input()`, no logging to stdout. Progress
|
|
165
|
+
and interaction are surfaced through injected callbacks.
|
|
166
|
+
- **Protocol-based injection.** `VendorProvider`, `ConfigWriter`, `HTTPClientProtocol`,
|
|
167
|
+
and the handler callbacks are all protocols. Mock any of them in tests without
|
|
168
|
+
subclassing.
|
|
169
|
+
implementation detail and may change between minor versions.
|
|
170
|
+
|
|
171
|
+
---
|
|
172
|
+
|
|
173
|
+
## Contributing
|
|
174
|
+
|
|
175
|
+
Issues and pull requests are welcome. Please open an issue before starting significant
|
|
176
|
+
work so we can discuss the approach.
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
git clone https://github.com/your-org/steamlayer-core
|
|
180
|
+
cd steamlayer-core
|
|
181
|
+
pip install -e ".[dev]"
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
## License
|
|
187
|
+
|
|
188
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
# steamlayer-core
|
|
4
|
+
|
|
5
|
+
[](https://pypi.org/project/steamlayer-core/)
|
|
6
|
+
[](https://pypi.org/project/steamlayer-core/)
|
|
7
|
+
[](https://github.com/layeredtools/steamlayer-core/actions)
|
|
8
|
+
[](LICENSE)
|
|
9
|
+
|
|
10
|
+
The emulator-agnostic engine behind SteamLayer. Handles Steam game identification,
|
|
11
|
+
DRM patching, and DLC hydration — with no opinion about which emulator you use.
|
|
12
|
+
</div>
|
|
13
|
+
|
|
14
|
+
## Overview
|
|
15
|
+
|
|
16
|
+
`steamlayer-core` is a Python library that provides a clean pipeline for:
|
|
17
|
+
|
|
18
|
+
1. **Resolving** a game directory to its Steam AppID (local files → app index → store search → disambiguation)
|
|
19
|
+
2. **Patching** the game by swapping Steam API DLLs with emulator binaries and writing config
|
|
20
|
+
3. **Unpatching** to restore the original files from a local backup vault
|
|
21
|
+
4. **Fetching DLC metadata** for a given AppID, with disk caching
|
|
22
|
+
|
|
23
|
+
The library is designed to be embedded inside higher-level tools. It has no `print()` or
|
|
24
|
+
`input()` calls anywhere — all user interaction is delegated to injected callbacks.
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Installation
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
pip install steamlayer-core
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Python 3.13+ is required.
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Quick start
|
|
39
|
+
|
|
40
|
+
### One-shot functions
|
|
41
|
+
|
|
42
|
+
The simplest way to use the library. Each call opens its own HTTP session and closes it
|
|
43
|
+
when done.
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
from pathlib import Path
|
|
47
|
+
from steamlayer_core.api import resolve_game, patch_game
|
|
48
|
+
|
|
49
|
+
# 1. Identify the game — resolve_game checks the directory for a steam_appid.txt
|
|
50
|
+
# or an appmanifest_*.acf file first. If found, no network call is made.
|
|
51
|
+
game = resolve_game(Path("C:/games/Euro Truck Simulator 2"))
|
|
52
|
+
print(game.appid, game.game_name) # 227300, Euro Truck Simulator 2
|
|
53
|
+
|
|
54
|
+
# 2. Patch it (requires a VendorProvider and ConfigWriter from your emulator layer)
|
|
55
|
+
result = patch_game(game, Path("C:/games/Euro Truck Simulator 2"), vendor=vendor, config_writer=writer)
|
|
56
|
+
print(result.patched_files)
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
If no local marker is found, the engine falls back to the app index and then a live
|
|
60
|
+
store search. You can also skip the path entirely and trigger a web search directly by
|
|
61
|
+
passing the game name as a string:
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
# No path — goes straight to index/store search
|
|
65
|
+
game = resolve_game("Euro Truck Simulator 2")
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Stateful client
|
|
69
|
+
|
|
70
|
+
Use `SteamLayerClient` when you need to reuse a single HTTP session across multiple
|
|
71
|
+
operations, wire progress callbacks once, or manage the lifecycle explicitly.
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
from pathlib import Path
|
|
75
|
+
from steamlayer_core import SteamLayerClient
|
|
76
|
+
|
|
77
|
+
with SteamLayerClient(vendor=vendor, config_writer=writer, progress=my_progress_hook) as client:
|
|
78
|
+
game = client.resolve(Path("C:/games/Euro Truck Simulator 2"))
|
|
79
|
+
dlcs = client.fetch_dlcs(game.appid)
|
|
80
|
+
result = client.patch(game, Path("C:/games/Euro Truck Simulator 2"))
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Reverting a patch
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
from steamlayer_core.api import SteamLayerClient
|
|
87
|
+
|
|
88
|
+
with SteamLayerClient() as client:
|
|
89
|
+
if client.is_patched(game_path):
|
|
90
|
+
restored = client.unpatch(game_path)
|
|
91
|
+
print(f"Restored {len(restored)} file(s)")
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
## Core concepts
|
|
96
|
+
|
|
97
|
+
### Resolution waterfall
|
|
98
|
+
|
|
99
|
+
When you call `resolve()` or `resolve_game()`, the engine tries each strategy in order,
|
|
100
|
+
stopping as soon as a confident match is found:
|
|
101
|
+
|
|
102
|
+
```
|
|
103
|
+
Game directory
|
|
104
|
+
│
|
|
105
|
+
▼
|
|
106
|
+
Local file inspection (steam_appid.txt, appmanifest_*.acf, ...)
|
|
107
|
+
│ no match
|
|
108
|
+
▼
|
|
109
|
+
App index lookup (offline index bundled with the library)
|
|
110
|
+
│ no match / low confidence
|
|
111
|
+
▼
|
|
112
|
+
Steam store search (live web query, skipped if allow_network=False)
|
|
113
|
+
│ ambiguous
|
|
114
|
+
▼
|
|
115
|
+
Disambiguation callback (your on_disambiguation handler, or AmbiguousMatchError)
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### Emulator agnosticism
|
|
119
|
+
|
|
120
|
+
`steamlayer-core` never references a specific emulator. It interacts with two
|
|
121
|
+
protocols that you implement (or get from an emulator-specific package):
|
|
122
|
+
|
|
123
|
+
| Protocol | Responsibility |
|
|
124
|
+
|---|---|
|
|
125
|
+
| `VendorProvider` | Supplies the emulator DLL binaries to be written into the game directory |
|
|
126
|
+
| `ConfigWriter` | Defines what config files to write and where (AppID, DLC list, etc.) |
|
|
127
|
+
|
|
128
|
+
This means the same core library can drive Goldberg, or any future emulator, just by
|
|
129
|
+
swapping the injected implementations.
|
|
130
|
+
|
|
131
|
+
### Backup vault
|
|
132
|
+
|
|
133
|
+
Before any file is overwritten, `patch()` creates a local vault inside the game
|
|
134
|
+
directory. `unpatch()` reads exclusively from this vault — it does not need to know
|
|
135
|
+
which emulator was used. `purge_vault=True` (default) deletes the vault after a
|
|
136
|
+
successful restore.
|
|
137
|
+
|
|
138
|
+
### DLC caching
|
|
139
|
+
|
|
140
|
+
`fetch_dlcs()` caches results to `~/.steamlayer/.cache/dlcs_{appid}.json`. Subsequent
|
|
141
|
+
calls within the TTL window never touch the network. The cache path and TTL are
|
|
142
|
+
configurable via `SteamlayerOptions`.
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
## Design principles
|
|
146
|
+
|
|
147
|
+
- **No I/O side-effects.** No `print()`, no `input()`, no logging to stdout. Progress
|
|
148
|
+
and interaction are surfaced through injected callbacks.
|
|
149
|
+
- **Protocol-based injection.** `VendorProvider`, `ConfigWriter`, `HTTPClientProtocol`,
|
|
150
|
+
and the handler callbacks are all protocols. Mock any of them in tests without
|
|
151
|
+
subclassing.
|
|
152
|
+
implementation detail and may change between minor versions.
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
## Contributing
|
|
157
|
+
|
|
158
|
+
Issues and pull requests are welcome. Please open an issue before starting significant
|
|
159
|
+
work so we can discuss the approach.
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
git clone https://github.com/your-org/steamlayer-core
|
|
163
|
+
cd steamlayer-core
|
|
164
|
+
pip install -e ".[dev]"
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
## License
|
|
170
|
+
|
|
171
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "steamlayer-core"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Emulator-agnostic engine for identifying Steam games, patching DRM, and hydrating DLC metadata."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
license-files = ["LICEN[CS]E*"]
|
|
8
|
+
requires-python = ">=3.13"
|
|
9
|
+
dependencies = [
|
|
10
|
+
"requests>=2.33.1",
|
|
11
|
+
]
|
|
12
|
+
classifiers = [
|
|
13
|
+
"Development Status :: 3 - Alpha",
|
|
14
|
+
"Programming Language :: Python :: 3",
|
|
15
|
+
"Operating System :: Microsoft :: Windows",
|
|
16
|
+
"Environment :: Console",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
[project.urls]
|
|
20
|
+
Source = "https://github.com/layeredtools/steamlayer-core"
|
|
21
|
+
Issues = "https://github.com/layeredtools/steamlayer-core/issues"
|
|
22
|
+
|
|
23
|
+
[dependency-groups]
|
|
24
|
+
dev = [
|
|
25
|
+
"mkdocs-material>=9.7.6",
|
|
26
|
+
"mkdocstrings[python]>=1.0.4",
|
|
27
|
+
"mypy>=1.20.0",
|
|
28
|
+
"pre-commit>=4.5.1",
|
|
29
|
+
"pytest>=9.0.3",
|
|
30
|
+
"rich>=15.0.0",
|
|
31
|
+
"ruff>=0.15.10",
|
|
32
|
+
"types-requests>=2.33.0.20260408",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
[tool.ruff]
|
|
36
|
+
line-length = 115
|
|
37
|
+
target-version = "py313"
|
|
38
|
+
fix = true
|
|
39
|
+
|
|
40
|
+
[tool.ruff.lint]
|
|
41
|
+
select = ["E", "F", "I", "UP"]
|
|
42
|
+
|
|
43
|
+
[tool.ruff.format]
|
|
44
|
+
quote-style = "double"
|
|
45
|
+
indent-style = "space"
|
|
46
|
+
|
|
47
|
+
[tool.mypy]
|
|
48
|
+
python_version = "3.13"
|
|
49
|
+
strict = false
|
|
50
|
+
warn_unused_ignores = false
|
|
51
|
+
|
|
52
|
+
[[tool.mypy.overrides]]
|
|
53
|
+
module = "steamlayer.tests.*"
|
|
54
|
+
ignore_errors = true
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""
|
|
2
|
+
steamlayer-core
|
|
3
|
+
===============
|
|
4
|
+
A headless, I/O-free library for Steam game identification, DLC hydration,
|
|
5
|
+
and emulator-agnostic patching.
|
|
6
|
+
|
|
7
|
+
Quick start
|
|
8
|
+
-----------
|
|
9
|
+
::
|
|
10
|
+
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from steamlayer_core import SteamLayerClient, GoldbergLocalVendorProvider, GoldbergConfigWriter
|
|
13
|
+
|
|
14
|
+
with SteamLayerClient(
|
|
15
|
+
vendor=GoldbergLocalVendorProvider(Path("./vendors")),
|
|
16
|
+
config_writer=GoldbergConfigWriter(),
|
|
17
|
+
) as client:
|
|
18
|
+
game = client.resolve(Path("/games/Portal 2"))
|
|
19
|
+
result = client.patch(game, Path("/games/Portal 2"))
|
|
20
|
+
print(result.targets_patched)
|
|
21
|
+
|
|
22
|
+
Public surface
|
|
23
|
+
--------------
|
|
24
|
+
Only the names imported here are considered stable API. Everything else is
|
|
25
|
+
an implementation detail subject to change without notice.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from steamlayer_core.adapters import (
|
|
29
|
+
FixedConfirmationHandler,
|
|
30
|
+
FixedDisambiguationHandler,
|
|
31
|
+
)
|
|
32
|
+
from steamlayer_core.api import SteamLayerClient, fetch_dlcs, patch_game, resolve_game
|
|
33
|
+
from steamlayer_core.domain.events import AmbiguousMatchEvent, LowConfidenceEvent
|
|
34
|
+
from steamlayer_core.domain.exceptions import (
|
|
35
|
+
AmbiguousMatchError,
|
|
36
|
+
AppIDNotFoundError,
|
|
37
|
+
ConfigurationError,
|
|
38
|
+
DLCCacheError,
|
|
39
|
+
DLCHydrationError,
|
|
40
|
+
EmulatorBinaryError,
|
|
41
|
+
LowConfidenceError,
|
|
42
|
+
PatchError,
|
|
43
|
+
SteamLayerError,
|
|
44
|
+
VaultError,
|
|
45
|
+
)
|
|
46
|
+
from steamlayer_core.domain.models import (
|
|
47
|
+
DiscoveryResult,
|
|
48
|
+
DLCInfo,
|
|
49
|
+
ResolutionSource,
|
|
50
|
+
ResolvedGame,
|
|
51
|
+
SteamlayerOptions,
|
|
52
|
+
)
|
|
53
|
+
from steamlayer_core.patching.config import GoldbergConfigWriter
|
|
54
|
+
from steamlayer_core.patching.engine import PatchEngine
|
|
55
|
+
from steamlayer_core.patching.models import PatchResult, PatchTarget
|
|
56
|
+
from steamlayer_core.patching.vendors import GoldbergLocalVendorProvider, LocalVendorProvider
|
|
57
|
+
from steamlayer_core.protocols import (
|
|
58
|
+
ConfigWriter,
|
|
59
|
+
ConfirmationHandler,
|
|
60
|
+
DisambiguationHandler,
|
|
61
|
+
ProgressCallback,
|
|
62
|
+
VendorProvider,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
__all__ = [
|
|
66
|
+
# Client
|
|
67
|
+
"SteamLayerClient",
|
|
68
|
+
# API functions
|
|
69
|
+
"resolve_game",
|
|
70
|
+
"patch_game",
|
|
71
|
+
"fetch_dlcs",
|
|
72
|
+
# Models
|
|
73
|
+
"SteamlayerOptions",
|
|
74
|
+
"ResolvedGame",
|
|
75
|
+
"DiscoveryResult",
|
|
76
|
+
"ResolutionSource",
|
|
77
|
+
"DLCInfo",
|
|
78
|
+
# Events
|
|
79
|
+
"AmbiguousMatchEvent",
|
|
80
|
+
"LowConfidenceEvent",
|
|
81
|
+
# Exceptions
|
|
82
|
+
"SteamLayerError",
|
|
83
|
+
"AppIDNotFoundError",
|
|
84
|
+
"AmbiguousMatchError",
|
|
85
|
+
"LowConfidenceError",
|
|
86
|
+
"PatchError",
|
|
87
|
+
"VaultError",
|
|
88
|
+
"EmulatorBinaryError",
|
|
89
|
+
"DLCHydrationError",
|
|
90
|
+
"DLCCacheError",
|
|
91
|
+
"ConfigurationError",
|
|
92
|
+
# Protocols
|
|
93
|
+
"ConfigWriter",
|
|
94
|
+
"ConfirmationHandler",
|
|
95
|
+
"DisambiguationHandler",
|
|
96
|
+
"ProgressCallback",
|
|
97
|
+
"VendorProvider",
|
|
98
|
+
# Adapters
|
|
99
|
+
"FixedDisambiguationHandler",
|
|
100
|
+
"FixedConfirmationHandler",
|
|
101
|
+
# Vendors
|
|
102
|
+
"LocalVendorProvider",
|
|
103
|
+
"GoldbergLocalVendorProvider",
|
|
104
|
+
# Patching
|
|
105
|
+
"PatchEngine",
|
|
106
|
+
"PatchResult",
|
|
107
|
+
"PatchTarget",
|
|
108
|
+
# Config writers
|
|
109
|
+
"GoldbergConfigWriter",
|
|
110
|
+
]
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
from steamlayer_core.domain.exceptions import AppIDNotFoundError, AppIDResolutionError
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from steamlayer_core.domain.events import AmbiguousMatchEvent, LowConfidenceEvent
|
|
10
|
+
from steamlayer_core.domain.models import DiscoveryResult
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class FixedDisambiguationHandler:
|
|
16
|
+
"""
|
|
17
|
+
Adapter used when the user has already provided a specific AppID (e.g., via CLI).
|
|
18
|
+
Instead of asking, it searches the candidates for a match and returns it.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(self, target_appid: int) -> None:
|
|
22
|
+
self.target_appid = target_appid
|
|
23
|
+
|
|
24
|
+
def handle_disambiguation(self, event: AmbiguousMatchEvent) -> DiscoveryResult:
|
|
25
|
+
logger.debug(f"Fixed handler looking for AppID {self.target_appid} in candidates.")
|
|
26
|
+
|
|
27
|
+
for candidate in event.candidates:
|
|
28
|
+
if candidate.appid == self.target_appid:
|
|
29
|
+
return candidate
|
|
30
|
+
|
|
31
|
+
raise ValueError(f"Provided AppID {self.target_appid} not found in resolution candidates.")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class FixedConfirmationHandler:
|
|
35
|
+
"""
|
|
36
|
+
Adapter used for 'non-interactive' mode (e.g., a --yes flag).
|
|
37
|
+
Automatically accepts or rejects low-confidence matches.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def __init__(self, auto_confirm: bool = True) -> None:
|
|
41
|
+
self.auto_confirm = auto_confirm
|
|
42
|
+
|
|
43
|
+
def handle_confirmation(self, event: LowConfidenceEvent) -> DiscoveryResult:
|
|
44
|
+
logger.info(
|
|
45
|
+
f"Auto-confirming ({self.auto_confirm}) match for {event.game_folder_name} "
|
|
46
|
+
f"with confidence {event.candidate.confidence:.2f}"
|
|
47
|
+
)
|
|
48
|
+
if self.auto_confirm:
|
|
49
|
+
return event.candidate
|
|
50
|
+
raise AppIDNotFoundError(event.game_folder_name)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class CLIHandler:
|
|
54
|
+
"""
|
|
55
|
+
The primary interactive adapter for terminal users.
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
def handle_disambiguation(self, event: AmbiguousMatchEvent) -> DiscoveryResult:
|
|
59
|
+
print(f"\n[!] Multiple matches found for folder: '{event.game_folder_name}'")
|
|
60
|
+
for i, c in enumerate(event.candidates, 1):
|
|
61
|
+
print(f" {i}) {c.game_name} (AppID: {c.appid}) [Confidence: {c.confidence:.2f}]")
|
|
62
|
+
|
|
63
|
+
while True:
|
|
64
|
+
choice = input("\nSelect the correct game (or 'q' to abort): ").strip().lower()
|
|
65
|
+
if choice == "q":
|
|
66
|
+
raise KeyboardInterrupt()
|
|
67
|
+
if choice.isdigit() and 1 <= int(choice) <= len(event.candidates):
|
|
68
|
+
return event.candidates[int(choice) - 1]
|
|
69
|
+
|
|
70
|
+
def handle_confirmation(self, event: LowConfidenceEvent) -> DiscoveryResult:
|
|
71
|
+
print(f"\n[?] Low confidence match for '{event.game_folder_name}':")
|
|
72
|
+
print(f" Suggested: {event.candidate.game_name} (AppID: {event.candidate.appid})")
|
|
73
|
+
print(f" Confidence: {event.candidate.confidence:.2f} (Threshold: {event.threshold:.2f})")
|
|
74
|
+
|
|
75
|
+
choice = input("Is this correct? [y/N]: ").strip().lower()
|
|
76
|
+
if choice == "y":
|
|
77
|
+
return event.candidate
|
|
78
|
+
raise AppIDResolutionError("User rejected candidate.")
|