Skip to content

Commit 284d2e8

Browse files
committed
feat(saves): save sync v2 service refactoring (#184)
Refactor SaveService to use new adapter and domain capabilities: - Wire domain save path resolution (resolve_save_dir with roms_base) and per-platform save extensions into _find_save_files - Use build_emulator_tag(active_core) via CoreResolverFn protocol instead of hardcoded "retroarch" - Extend state format: server_device_id, active_core, active_slot, tracked_save_id (all backward-compat via .get() defaults) - Server device registration on v4.7 (register_device API), local UUID fallback on v4.6 - Pass device_id and slot to list_saves/upload_save on v4.7 - Use determine_sync_action with device_syncs is_current for v4.7 conflict detection, avoiding expensive server hash downloads - Read plugin version from package.json at bootstrap 20 new tests, 1531 total passing.
1 parent b3bab71 commit 284d2e8

11 files changed

Lines changed: 697 additions & 34 deletions

File tree

main.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,7 @@ async def _main(self): # Decky lifecycle — must be async
148148
runtime_dir=decky.DECKY_PLUGIN_RUNTIME_DIR,
149149
emit=decky.emit,
150150
get_saves_path=retrodeck_config.get_saves_path,
151+
get_roms_path=retrodeck_config.get_roms_path,
151152
save_state=self._save_state,
152153
save_settings_to_disk=self._save_settings_to_disk,
153154
save_metadata_cache=self._save_metadata_cache,

py_modules/bootstrap.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
DebugLogger,
3535
EventEmitter,
3636
RommApiProtocol,
37+
RomsPathProvider,
3738
SavesPathProvider,
3839
SettingsPersister,
3940
StatePersister,
@@ -71,6 +72,7 @@ class WiringConfig:
7172

7273
# Callbacks
7374
get_saves_path: SavesPathProvider
75+
get_roms_path: RomsPathProvider
7476
save_state: StatePersister
7577
save_settings_to_disk: SettingsPersister
7678
save_metadata_cache: StatePersister
@@ -127,6 +129,18 @@ def bootstrap(
127129
}
128130

129131

132+
def _read_plugin_version(plugin_dir: str) -> str:
133+
"""Read plugin version from package.json."""
134+
import json
135+
import os
136+
137+
try:
138+
with open(os.path.join(plugin_dir, "package.json")) as f:
139+
return json.load(f).get("version", "0.0.0")
140+
except (OSError, json.JSONDecodeError):
141+
return "0.0.0"
142+
143+
130144
def wire_services(cfg: WiringConfig) -> dict:
131145
"""Create service instances after plugin state is initialised.
132146
@@ -148,6 +162,9 @@ def wire_services(cfg: WiringConfig) -> dict:
148162
logger=cfg.logger,
149163
runtime_dir=cfg.runtime_dir,
150164
get_saves_path=cfg.get_saves_path,
165+
get_roms_path=cfg.get_roms_path,
166+
get_active_core=_es_de_config.get_active_core,
167+
plugin_version=_read_plugin_version(cfg.plugin_dir),
151168
)
152169

153170
playtime_service = PlaytimeService(

py_modules/domain/save_path.py

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ def resolve_save_dir(
1616
saves_base: str,
1717
system: str,
1818
*,
19+
roms_base: str | None = None,
1920
sort_by_content: bool = True,
2021
sort_by_core: bool = False,
2122
core_name: str | None = None,
@@ -25,12 +26,17 @@ def resolve_save_dir(
2526
Parameters
2627
----------
2728
rom_path:
28-
Relative ROM file path as stored in RomM (e.g. "gba/Game.gba" or
29+
ROM file path — absolute or relative (e.g. "gba/Game.gba",
30+
"/home/deck/retrodeck/roms/gba/Game.gba", or
2931
"psx/Game (USA)/Game.m3u").
3032
saves_base:
3133
Absolute path to the RetroArch saves root directory.
3234
system:
3335
Platform/system slug (e.g. "gba", "psx"). Used as fallback subdir.
36+
roms_base:
37+
If provided, strip this absolute prefix from rom_path before
38+
computing the content directory. Allows callers to pass absolute
39+
ROM paths directly without pre-stripping.
3440
sort_by_content:
3541
If True, save subdir = last folder component of rom_path.
3642
RetroDECK default: True.
@@ -45,11 +51,21 @@ def resolve_save_dir(
4551
str
4652
Absolute path to the directory where save files should be found/placed.
4753
"""
54+
# Strip roms_base prefix if provided to get a relative path
55+
effective_path = rom_path
56+
if roms_base is not None:
57+
norm_rom = os.path.normpath(rom_path)
58+
norm_base = os.path.normpath(roms_base)
59+
if norm_rom.startswith(norm_base + os.sep):
60+
effective_path = norm_rom[len(norm_base) + 1 :]
61+
elif norm_rom.startswith(norm_base):
62+
effective_path = norm_rom[len(norm_base) :]
63+
4864
parts: list[str] = [saves_base]
4965

5066
if sort_by_content:
51-
# Last folder component of the rom_path (i.e. the directory containing the ROM file)
52-
rom_dir = os.path.dirname(rom_path)
67+
# Last folder component of the effective_path (i.e. the directory containing the ROM file)
68+
rom_dir = os.path.dirname(effective_path)
5369
content_dir = os.path.basename(rom_dir) if rom_dir else system
5470
parts.append(content_dir)
5571

py_modules/services/protocols.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,18 @@ class SavesPathProvider(Protocol):
318318
def __call__(self) -> str: ...
319319

320320

321+
class RomsPathProvider(Protocol):
322+
"""Return the current RetroDECK roms directory path."""
323+
324+
def __call__(self) -> str: ...
325+
326+
327+
class CoreResolverFn(Protocol):
328+
"""Resolve the active RetroArch core for a system/game."""
329+
330+
def __call__(self, system_name: str, rom_filename: str | None = None) -> tuple[str | None, str | None]: ...
331+
332+
321333
class SyncStateRef(Protocol):
322334
"""Return the current sync state value (used by ArtworkService)."""
323335

0 commit comments

Comments
 (0)