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.
Files changed (37) hide show
  1. steamlayer_core-0.1.0/LICENSE +21 -0
  2. steamlayer_core-0.1.0/PKG-INFO +188 -0
  3. steamlayer_core-0.1.0/README.md +171 -0
  4. steamlayer_core-0.1.0/pyproject.toml +54 -0
  5. steamlayer_core-0.1.0/setup.cfg +4 -0
  6. steamlayer_core-0.1.0/steamlayer_core/__init__.py +110 -0
  7. steamlayer_core-0.1.0/steamlayer_core/_version.py +6 -0
  8. steamlayer_core-0.1.0/steamlayer_core/adapters.py +78 -0
  9. steamlayer_core-0.1.0/steamlayer_core/api.py +508 -0
  10. steamlayer_core-0.1.0/steamlayer_core/constants.py +14 -0
  11. steamlayer_core-0.1.0/steamlayer_core/discovery/__init__.py +19 -0
  12. steamlayer_core-0.1.0/steamlayer_core/discovery/dlc.py +189 -0
  13. steamlayer_core-0.1.0/steamlayer_core/discovery/engine.py +391 -0
  14. steamlayer_core-0.1.0/steamlayer_core/discovery/local.py +51 -0
  15. steamlayer_core-0.1.0/steamlayer_core/discovery/matcher.py +143 -0
  16. steamlayer_core-0.1.0/steamlayer_core/discovery/query_strategy.py +99 -0
  17. steamlayer_core-0.1.0/steamlayer_core/discovery/repository.py +130 -0
  18. steamlayer_core-0.1.0/steamlayer_core/discovery/web.py +37 -0
  19. steamlayer_core-0.1.0/steamlayer_core/domain/__init__.py +0 -0
  20. steamlayer_core-0.1.0/steamlayer_core/domain/exceptions.py +148 -0
  21. steamlayer_core-0.1.0/steamlayer_core/domain/models.py +248 -0
  22. steamlayer_core-0.1.0/steamlayer_core/events.py +81 -0
  23. steamlayer_core-0.1.0/steamlayer_core/http_client.py +135 -0
  24. steamlayer_core-0.1.0/steamlayer_core/patching/__init__.py +66 -0
  25. steamlayer_core-0.1.0/steamlayer_core/patching/config.py +95 -0
  26. steamlayer_core-0.1.0/steamlayer_core/patching/engine.py +338 -0
  27. steamlayer_core-0.1.0/steamlayer_core/patching/models.py +87 -0
  28. steamlayer_core-0.1.0/steamlayer_core/patching/scanner.py +217 -0
  29. steamlayer_core-0.1.0/steamlayer_core/patching/vault.py +225 -0
  30. steamlayer_core-0.1.0/steamlayer_core/patching/vendors.py +145 -0
  31. steamlayer_core-0.1.0/steamlayer_core/protocols.py +196 -0
  32. steamlayer_core-0.1.0/steamlayer_core/utils.py +18 -0
  33. steamlayer_core-0.1.0/steamlayer_core.egg-info/PKG-INFO +188 -0
  34. steamlayer_core-0.1.0/steamlayer_core.egg-info/SOURCES.txt +35 -0
  35. steamlayer_core-0.1.0/steamlayer_core.egg-info/dependency_links.txt +1 -0
  36. steamlayer_core-0.1.0/steamlayer_core.egg-info/requires.txt +1 -0
  37. 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
+ [![PyPI](https://img.shields.io/pypi/v/steamlayer-core)](https://pypi.org/project/steamlayer-core/)
23
+ [![Python](https://img.shields.io/pypi/pyversions/steamlayer-core)](https://pypi.org/project/steamlayer-core/)
24
+ [![CI](https://img.shields.io/github/actions/workflow/status/layeredtools/steamlayer-core/ci.yml?branch=main&label=ci)](https://github.com/layeredtools/steamlayer-core/actions)
25
+ [![License](https://img.shields.io/github/license/layeredtools/steamlayer-core)](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
+ [![PyPI](https://img.shields.io/pypi/v/steamlayer-core)](https://pypi.org/project/steamlayer-core/)
6
+ [![Python](https://img.shields.io/pypi/pyversions/steamlayer-core)](https://pypi.org/project/steamlayer-core/)
7
+ [![CI](https://img.shields.io/github/actions/workflow/status/layeredtools/steamlayer-core/ci.yml?branch=main&label=ci)](https://github.com/layeredtools/steamlayer-core/actions)
8
+ [![License](https://img.shields.io/github/license/layeredtools/steamlayer-core)](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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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,6 @@
1
+ from importlib.metadata import PackageNotFoundError, version
2
+
3
+ try:
4
+ __version__ = version("steamlayer-core")
5
+ except PackageNotFoundError:
6
+ __version__ = "dev"
@@ -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.")