Skip to content

savegame

Core save file parsing — load and inspect a Terra Invicta .gz save.

tiargus.savegame

Save-file loading and the pluggable module system.

SavegameModule is the abstract base class for all parsers. Concrete subclasses self-register at import time, so TerraInvictaSave can locate them by key without any manual wiring. Only requested modules and their transitive dependencies are parsed.

Example
from tiargus.savegame import TerraInvictaSave
from tiargus.habs import HabSiteModule

save = TerraInvictaSave("Autosave.gz", HabSiteModule)
sites = save.get_module(HabSiteModule).hab_sites

SavegameModule

Bases: ABC

Abstract base class for a self-contained unit of save-file parsing.

Each concrete subclass handles one logical slice of the gamestate (e.g. space bodies, factions, hab sites). Subclasses self-register in _REGISTRY at class-definition time, so TerraInvictaSave can locate them by key during dependency resolution without any manual wiring.

Subclassing contract

  1. Define a unique KEY class variable (a plain string).
  2. Override requires() if this module depends on data from other modules.
  3. Implement parse() to populate the module's state from the raw gamestate dict.
Example
class MyModule(SavegameModule):
    KEY = "my_module"

    @classmethod
    def requires(cls) -> list[str]:
        return [SpaceBodyModule.KEY]

    def parse(self, gs: dict, modules: dict[str, SavegameModule]) -> None:
        bodies = modules[SpaceBodyModule.KEY]
        ...
Source code in src/tiargus/savegame.py
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
class SavegameModule(ABC):
    """Abstract base class for a self-contained unit of save-file parsing.

    Each concrete subclass handles one logical slice of the gamestate (e.g.
    space bodies, factions, hab sites).  Subclasses self-register in
    ``_REGISTRY`` at class-definition time, so ``TerraInvictaSave`` can locate
    them by key during dependency resolution without any manual wiring.

    **Subclassing contract**

    1. Define a unique ``KEY`` class variable (a plain string).
    2. Override ``requires()`` if this module depends on data from other modules.
    3. Implement ``parse()`` to populate the module's state from the raw
       gamestate dict.

    Example:
        ```python
        class MyModule(SavegameModule):
            KEY = "my_module"

            @classmethod
            def requires(cls) -> list[str]:
                return [SpaceBodyModule.KEY]

            def parse(self, gs: dict, modules: dict[str, SavegameModule]) -> None:
                bodies = modules[SpaceBodyModule.KEY]
                ...
        ```
    """

    KEY: ClassVar[str]
    """Unique string identifier for this module.

    Must be defined on every concrete subclass.  Used as the lookup key in
    ``_REGISTRY`` and as the argument to ``requires()``.

    Treat ``KEY`` as a stable public identifier — it is the identity of this
    module at every boundary that isn't Python code (YAML configs, web
    requests, serialised dependency graphs).  Renaming it is a breaking change:
    any external config or stored data that references the old name will break
    silently at runtime.  If a rename is unavoidable, keep the old value as an
    alias in ``_REGISTRY`` and document the deprecation.
    """

    _REGISTRY: ClassVar[dict[str, type[SavegameModule]]] = {}
    """Global map of KEY → class, populated automatically by ``__init_subclass__``.

    All concrete subclasses are registered here the moment their class body is
    executed (i.e. at import time).  ``TerraInvictaSave`` uses this registry to
    instantiate dependency modules that were not explicitly requested.
    """

    def __init_subclass__(cls, **kwargs: object) -> None:
        """Register subclasses in ``_REGISTRY`` at definition time.

        Raises:
            TypeError: If a subclass does not define ``KEY``.
        """
        super().__init_subclass__(**kwargs)
        if not hasattr(cls, "KEY"):
            raise TypeError(f"{cls.__name__} must define KEY as a class variable")
        SavegameModule._REGISTRY[cls.KEY] = cls

    @classmethod
    def requires(cls) -> list[str]:
        """Return the KEYs of modules that must be parsed before this one.

        Override this in subclasses that need data from other modules.
        ``TerraInvictaSave`` resolves and parses all declared dependencies
        (and their transitive dependencies) before calling ``parse()`` on this
        module.

        Returns:
            A list of ``KEY`` strings.  The default implementation returns an
            empty list (no dependencies).
        """
        return []

    @abstractmethod
    def parse(self, gs: dict, modules: dict[str, SavegameModule]) -> None:
        """Populate this module's state from the raw gamestate dict.

        Called by ``TerraInvictaSave`` after all modules listed in
        ``requires()`` have already been parsed.  Implementations should read
        from ``gs`` and store whatever data they expose via properties.

        Args:
            gs: The top-level ``"gamestates"`` dict from the save file,
                keyed by fully-qualified Unity type name (e.g.
                ``"PavonisInteractive.TerraInvicta.TISpaceBodyState"``).
            modules: Already-parsed modules, keyed by their ``KEY``.  Only
                modules declared in ``requires()`` (and their dependencies) are
                guaranteed to be present.
        """
        raise NotImplementedError

KEY class-attribute

Unique string identifier for this module.

Must be defined on every concrete subclass. Used as the lookup key in _REGISTRY and as the argument to requires().

Treat KEY as a stable public identifier — it is the identity of this module at every boundary that isn't Python code (YAML configs, web requests, serialised dependency graphs). Renaming it is a breaking change: any external config or stored data that references the old name will break silently at runtime. If a rename is unavoidable, keep the old value as an alias in _REGISTRY and document the deprecation.

__init_subclass__(**kwargs)

Register subclasses in _REGISTRY at definition time.

Raises:

Type Description
TypeError

If a subclass does not define KEY.

Source code in src/tiargus/savegame.py
141
142
143
144
145
146
147
148
149
150
def __init_subclass__(cls, **kwargs: object) -> None:
    """Register subclasses in ``_REGISTRY`` at definition time.

    Raises:
        TypeError: If a subclass does not define ``KEY``.
    """
    super().__init_subclass__(**kwargs)
    if not hasattr(cls, "KEY"):
        raise TypeError(f"{cls.__name__} must define KEY as a class variable")
    SavegameModule._REGISTRY[cls.KEY] = cls

parse(gs, modules) abstractmethod

Populate this module's state from the raw gamestate dict.

Called by TerraInvictaSave after all modules listed in requires() have already been parsed. Implementations should read from gs and store whatever data they expose via properties.

Parameters:

Name Type Description Default
gs dict

The top-level "gamestates" dict from the save file, keyed by fully-qualified Unity type name (e.g. "PavonisInteractive.TerraInvicta.TISpaceBodyState").

required
modules dict[str, SavegameModule]

Already-parsed modules, keyed by their KEY. Only modules declared in requires() (and their dependencies) are guaranteed to be present.

required
Source code in src/tiargus/savegame.py
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
@abstractmethod
def parse(self, gs: dict, modules: dict[str, SavegameModule]) -> None:
    """Populate this module's state from the raw gamestate dict.

    Called by ``TerraInvictaSave`` after all modules listed in
    ``requires()`` have already been parsed.  Implementations should read
    from ``gs`` and store whatever data they expose via properties.

    Args:
        gs: The top-level ``"gamestates"`` dict from the save file,
            keyed by fully-qualified Unity type name (e.g.
            ``"PavonisInteractive.TerraInvicta.TISpaceBodyState"``).
        modules: Already-parsed modules, keyed by their ``KEY``.  Only
            modules declared in ``requires()`` (and their dependencies) are
            guaranteed to be present.
    """
    raise NotImplementedError

requires() classmethod

Return the KEYs of modules that must be parsed before this one.

Override this in subclasses that need data from other modules. TerraInvictaSave resolves and parses all declared dependencies (and their transitive dependencies) before calling parse() on this module.

Returns:

Type Description
list[str]

A list of KEY strings. The default implementation returns an

list[str]

empty list (no dependencies).

Source code in src/tiargus/savegame.py
152
153
154
155
156
157
158
159
160
161
162
163
164
165
@classmethod
def requires(cls) -> list[str]:
    """Return the KEYs of modules that must be parsed before this one.

    Override this in subclasses that need data from other modules.
    ``TerraInvictaSave`` resolves and parses all declared dependencies
    (and their transitive dependencies) before calling ``parse()`` on this
    module.

    Returns:
        A list of ``KEY`` strings.  The default implementation returns an
        empty list (no dependencies).
    """
    return []

TerraInvictaSave

A parsed Terra Invicta save file.

Loads a save file and runs the requested SavegameModule instances in dependency order. Only the modules you ask for (and their transitive dependencies) are parsed, so callers pay only for what they use.

Both gzip-compressed (.gz) and uncompressed (extensionless JSON) save formats are supported; the format is detected from the file suffix.

Parameters:

Name Type Description Default
path str | Path | None

Path to the save file (.gz or extensionless JSON), or None to auto-detect: the saves directory is searched for Autosave.gz and Autosave; if both exist the more-recently modified file wins.

required
*modules type[SavegameModule] | SavegameModule

One or more SavegameModule subclasses (or pre-built instances) to parse from the save. Dependencies declared via requires() are resolved and parsed automatically.

()

Raises:

Type Description
ValueError

If a circular dependency or an unregistered dependency key is detected among the requested modules.

FileNotFoundError

If path does not exist.

KeyError

If the save file is missing an expected gamestate key.

Example
save = TerraInvictaSave("Autosave.gz", HabSiteModule)
sites = save.get_module(HabSiteModule).hab_sites
Source code in src/tiargus/savegame.py
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
class TerraInvictaSave:
    """A parsed Terra Invicta save file.

    Loads a save file and runs the requested ``SavegameModule`` instances in
    dependency order.  Only the modules you ask for (and their transitive
    dependencies) are parsed, so callers pay only for what they use.

    Both gzip-compressed (``.gz``) and uncompressed (extensionless JSON) save
    formats are supported; the format is detected from the file suffix.

    Args:
        path: Path to the save file (``.gz`` or extensionless JSON), or
            ``None`` to auto-detect: the saves directory is searched for
            ``Autosave.gz`` and ``Autosave``; if both exist the more-recently
            modified file wins.
        *modules: One or more ``SavegameModule`` subclasses (or pre-built
            instances) to parse from the save.  Dependencies declared via
            ``requires()`` are resolved and parsed automatically.

    Raises:
        ValueError: If a circular dependency or an unregistered dependency key
            is detected among the requested modules.
        FileNotFoundError: If ``path`` does not exist.
        KeyError: If the save file is missing an expected gamestate key.

    Example:
        ```python
        save = TerraInvictaSave("Autosave.gz", HabSiteModule)
        sites = save.get_module(HabSiteModule).hab_sites
        ```
    """

    def __init__(
        self,
        path: str | Path | None,
        *modules: type[SavegameModule] | SavegameModule,
        config: "Config | None" = None,
    ) -> None:
        resolved = _resolve_save_path(path, config)
        with _open_save(resolved) as f:
            raw = json.load(f)
        gs = raw["gamestates"]

        self._modules: dict[str, SavegameModule] = {}
        for module in _resolve_modules(modules):
            module.parse(gs, self._modules)
            self._modules[module.KEY] = module

    @classmethod
    def from_modules(
        cls,
        path: str | Path | None,
        modules: Iterable[type[SavegameModule] | SavegameModule],
    ) -> TerraInvictaSave:
        """Construct from an iterable of modules instead of varargs.

        Useful when the module list is built dynamically.

        Args:
            path: Path to the save file, or ``None`` to auto-detect.
            modules: Iterable of ``SavegameModule`` subclasses or instances.

        Returns:
            A fully parsed ``TerraInvictaSave``.
        """
        return cls(path, *modules)

    @overload
    def get_module(self, key: str) -> SavegameModule: ...

    @overload
    def get_module(self, key: type[_T]) -> _T: ...

    def get_module(self, key: str | type[SavegameModule]) -> SavegameModule:
        """Retrieve a parsed module by its string key or class.

        Args:
            key: Either the string ``KEY`` of the module, or the module class
                itself (preferred — enables type inference).

        Returns:
            The parsed ``SavegameModule`` instance.

        Raises:
            KeyError: If the requested module was not included when the save
                was constructed.

        Example:
            ```python
            hab_mod = save.get_module(HabSiteModule)
            faction_mod = save.get_module("factions")
            ```
        """
        if isinstance(key, str):
            return self._modules[key]
        return self._modules[key.KEY]  # type: ignore[return-value]

from_modules(path, modules) classmethod

Construct from an iterable of modules instead of varargs.

Useful when the module list is built dynamically.

Parameters:

Name Type Description Default
path str | Path | None

Path to the save file, or None to auto-detect.

required
modules Iterable[type[SavegameModule] | SavegameModule]

Iterable of SavegameModule subclasses or instances.

required

Returns:

Type Description
TerraInvictaSave

A fully parsed TerraInvictaSave.

Source code in src/tiargus/savegame.py
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
@classmethod
def from_modules(
    cls,
    path: str | Path | None,
    modules: Iterable[type[SavegameModule] | SavegameModule],
) -> TerraInvictaSave:
    """Construct from an iterable of modules instead of varargs.

    Useful when the module list is built dynamically.

    Args:
        path: Path to the save file, or ``None`` to auto-detect.
        modules: Iterable of ``SavegameModule`` subclasses or instances.

    Returns:
        A fully parsed ``TerraInvictaSave``.
    """
    return cls(path, *modules)

get_module(key)

get_module(key: str) -> SavegameModule
get_module(key: type[_T]) -> _T

Retrieve a parsed module by its string key or class.

Parameters:

Name Type Description Default
key str | type[SavegameModule]

Either the string KEY of the module, or the module class itself (preferred — enables type inference).

required

Returns:

Type Description
SavegameModule

The parsed SavegameModule instance.

Raises:

Type Description
KeyError

If the requested module was not included when the save was constructed.

Example
hab_mod = save.get_module(HabSiteModule)
faction_mod = save.get_module("factions")
Source code in src/tiargus/savegame.py
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
def get_module(self, key: str | type[SavegameModule]) -> SavegameModule:
    """Retrieve a parsed module by its string key or class.

    Args:
        key: Either the string ``KEY`` of the module, or the module class
            itself (preferred — enables type inference).

    Returns:
        The parsed ``SavegameModule`` instance.

    Raises:
        KeyError: If the requested module was not included when the save
            was constructed.

    Example:
        ```python
        hab_mod = save.get_module(HabSiteModule)
        faction_mod = save.get_module("factions")
        ```
    """
    if isinstance(key, str):
        return self._modules[key]
    return self._modules[key.KEY]  # type: ignore[return-value]