Skip to content

councilors

Data model and query module for councilors and the player's hiring pool.

tiargus.councilors

Data model and SavegameModule for councilors.

Provides:

  • Councilor — an individual councilor, including its profession, traits, and whether the player can currently hire it.
  • CouncilorModule (key: "councilors") — parses every councilor from the save and flags the ones in the player faction's hiring pool.
Example
from tiargus import TerraInvictaSave, CouncilorModule

save = TerraInvictaSave("Autosave.gz", CouncilorModule)
for c in save.get_module(CouncilorModule).councilors.values():
    if c.hireable:
        print(c.display_name, c.profession, sorted(c.traits))

Councilor dataclass

A councilor as recorded in the save file.

Attributes:

Name Type Description
id int

Numeric game-state ID.

display_name str

Councilor name as shown in-game (e.g. "Matt Price").

hireable bool

True iff this councilor's ID appears in the player faction's availableCouncilors hiring pool.

profession str

Internal profession identifier (typeTemplateName, e.g. "Astronaut").

traits frozenset[str]

Set of internal trait identifiers (traitTemplateNames).

missions frozenset[str]

Internal identifiers of the missions this councilor can currently perform, derived from its profession, traits, and assigned orgs (plus the universal base missions, minus any trait restrictions). Note: a few missions are additionally research- or faction-gated (e.g. alien missions) and are not modelled here.

persuasion int

Ability to shape the opinions of others to your own.

investigation int

Ability to discover information someone would prefer to keep hidden.

espionage int

Ability to move in secret and conduct covert operations.

command int

Ability to organize and lead combat operations.

administration int

Ability to manage large organizations.

science int

Ability to investigate and analyze phenomena to derive new understandings of the universe.

security int

Ability to survive violence.

loyalty int

How likely the councilor is to betray their faction — the councilor's true loyalty value (025).

apparent_loyalty int

The estimate of loyalty before the true loyalty is fully revealed.

Source code in src/tiargus/councilors.py
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 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
@dataclass
class Councilor:
    """A councilor as recorded in the save file.

    Attributes:
        id: Numeric game-state ID.
        display_name: Councilor name as shown in-game (e.g. ``"Matt Price"``).
        hireable: ``True`` iff this councilor's ID appears in the player
            faction's ``availableCouncilors`` hiring pool.
        profession: Internal profession identifier (``typeTemplateName``,
            e.g. ``"Astronaut"``).
        traits: Set of internal trait identifiers (``traitTemplateNames``).
        missions: Internal identifiers of the missions this councilor can
            currently perform, derived from its profession, traits, and assigned
            orgs (plus the universal base missions, minus any trait
            restrictions).  Note: a few missions are additionally research- or
            faction-gated (e.g. alien missions) and are not modelled here.
        persuasion: Ability to shape the opinions of others to your own.
        investigation: Ability to discover information someone would prefer to
            keep hidden.
        espionage: Ability to move in secret and conduct covert operations.
        command: Ability to organize and lead combat operations.
        administration: Ability to manage large organizations.
        science: Ability to investigate and analyze phenomena to derive new
            understandings of the universe.
        security: Ability to survive violence.
        loyalty: How likely the councilor is to betray their faction — the
            councilor's true loyalty value (``0``–``25``).
        apparent_loyalty: The estimate of loyalty before the true loyalty is
            fully revealed.
    """

    id: int
    display_name: str
    hireable: bool
    profession: str
    traits: frozenset[str]
    missions: frozenset[str]
    persuasion: int
    investigation: int
    espionage: int
    command: int
    administration: int
    science: int
    security: int
    loyalty: int
    apparent_loyalty: int

CouncilorAttribute

Bases: AliasedStrEnum

One of a councilor's eight core attributes.

Each member's value is the corresponding Councilor field name, so getattr(councilor, CouncilorAttribute.PERSUASION) reads that councilor's score. Members subclass str and so compare and hash equal to that field name (e.g. CouncilorAttribute.COMMAND == "command").

Each member also carries the in-game three-letter abbreviation as a curated synonym for interactive (REPL) use:

Member Field Synonym
PERSUASION persuasion PER
INVESTIGATION investigation INV
ESPIONAGE espionage ESP
COMMAND command CMD
ADMINISTRATION administration ADM
SCIENCE science SCI
SECURITY security SEC
LOYALTY loyalty LOY

Use from_alias to resolve any field name, member name, or abbreviation (case- and separator-insensitively) to a member, and the aliases property to list every accepted spelling for a member.

Members are declared as NAME = (field_name, abbreviation).

Source code in src/tiargus/councilors.py
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
class CouncilorAttribute(AliasedStrEnum):
    """One of a councilor's eight core attributes.

    Each member's value is the corresponding [Councilor][tiargus.councilors.Councilor]
    field name, so ``getattr(councilor, CouncilorAttribute.PERSUASION)`` reads
    that councilor's score.  Members subclass ``str`` and so compare and hash
    equal to that field name (e.g. ``CouncilorAttribute.COMMAND == "command"``).

    Each member also carries the in-game three-letter abbreviation as a curated
    **synonym** for interactive (REPL) use:

    | Member | Field | Synonym |
    | ------ | ----- | ------- |
    | ``PERSUASION`` | ``persuasion`` | ``PER`` |
    | ``INVESTIGATION`` | ``investigation`` | ``INV`` |
    | ``ESPIONAGE`` | ``espionage`` | ``ESP`` |
    | ``COMMAND`` | ``command`` | ``CMD`` |
    | ``ADMINISTRATION`` | ``administration`` | ``ADM`` |
    | ``SCIENCE`` | ``science`` | ``SCI`` |
    | ``SECURITY`` | ``security`` | ``SEC`` |
    | ``LOYALTY`` | ``loyalty`` | ``LOY`` |

    Use [from_alias][tiargus.councilors.CouncilorAttribute.from_alias] to
    resolve any field name, member name, or abbreviation (case- and
    separator-insensitively) to a member, and the
    [aliases][tiargus.councilors.CouncilorAttribute.aliases] property to
    list every accepted spelling for a member.

    Members are declared as ``NAME = (field_name, abbreviation)``.
    """

    PERSUASION = ("persuasion", "PER")
    INVESTIGATION = ("investigation", "INV")
    ESPIONAGE = ("espionage", "ESP")
    COMMAND = ("command", "CMD")
    ADMINISTRATION = ("administration", "ADM")
    SCIENCE = ("science", "SCI")
    SECURITY = ("security", "SEC")
    LOYALTY = ("loyalty", "LOY")

aliases property

Every spelling that resolves to this member, in declaration order.

Comprises the raw value, the member name, and the curated synonyms, de-duplicated. Handy for listing accepted inputs in a REPL.

synonyms property

The curated friendly aliases for this member (excludes value & name).

from_alias(name) classmethod

Resolve a flexible alias to its canonical member.

Lookup is case- and separator-insensitive (spaces, underscores, and hyphens are ignored) and a trailing "bonus" is optional. A member's raw value, its name, and its curated synonyms are all valid aliases.

Parameters:

Name Type Description Default
name str

The alias to resolve.

required

Returns:

Type Description
_AS

The matching member.

Raises:

Type Description
ValueError

If no member matches the normalized alias.

Source code in src/tiargus/_aliased_enum.py
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
@classmethod
def from_alias(cls: type[_AS], name: str) -> _AS:
    """Resolve a flexible alias to its canonical member.

    Lookup is case- and separator-insensitive (spaces, underscores, and
    hyphens are ignored) and a trailing ``"bonus"`` is optional.  A
    member's raw value, its name, and its curated ``synonyms`` are all valid
    aliases.

    Args:
        name: The alias to resolve.

    Returns:
        The matching member.

    Raises:
        ValueError: If no member matches the normalized alias.
    """
    try:
        return cls._alias_index()[_normalize_alias(name)]
    except KeyError:
        raise ValueError(f"Unknown {cls.__name__} alias: {name!r}") from None

CouncilorModule

Bases: SavegameModule

Parses all councilors and flags those in the player's hiring pool.

Depends on FactionModule to identify the player faction; a councilor is hireable iff its ID appears in that faction's availableCouncilors list.

Source code in src/tiargus/councilors.py
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
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
class CouncilorModule(SavegameModule):
    """Parses all councilors and flags those in the player's hiring pool.

    Depends on [FactionModule][tiargus.factions.FactionModule] to identify
    the player faction; a councilor is hireable iff its ID appears in that
    faction's ``availableCouncilors`` list.
    """

    KEY = "councilors"

    def __init__(self) -> None:
        self._councilors: dict[int, Councilor] = {}

    @property
    def councilors(self) -> dict[int, Councilor]:
        """All councilors keyed by their numeric game-state ID."""
        return self._councilors

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

    def parse(self, gs: dict, modules: dict[str, SavegameModule]) -> None:
        faction_mod: FactionModule = modules[FactionModule.KEY]  # type: ignore[assignment]
        org_mod: OrgModule = modules[OrgModule.KEY]  # type: ignore[assignment]
        hireable_ids = self._available_councilor_ids(gs, faction_mod.player_faction_id)
        mission_data = load_game_data("councilor_missions.yaml")
        for item in gs[GS_COUNCILOR]:
            cid = item["Key"]["value"]
            value = item["Value"]
            attrs = value.get("attributes", {})
            traits = frozenset(value.get("traitTemplateNames", []))
            self._councilors[cid] = Councilor(
                id=cid,
                display_name=value["displayName"],
                hireable=cid in hireable_ids,
                profession=value.get("typeTemplateName", ""),
                traits=traits,
                missions=self._missions(value, traits, org_mod, mission_data),
                persuasion=int(attrs.get("Persuasion", 0)),
                investigation=int(attrs.get("Investigation", 0)),
                espionage=int(attrs.get("Espionage", 0)),
                command=int(attrs.get("Command", 0)),
                administration=int(attrs.get("Administration", 0)),
                science=int(attrs.get("Science", 0)),
                security=int(attrs.get("Security", 0)),
                loyalty=int(attrs.get("Loyalty", 0)),
                apparent_loyalty=int(attrs.get("ApparentLoyalty", 0)),
            )

    @staticmethod
    def _missions(
        value: dict,
        traits: frozenset[str],
        org_mod: OrgModule,
        mission_data: dict,
    ) -> frozenset[str]:
        """Derive the missions a councilor can perform.

        Combines the universal ``base`` missions, the councilor's profession
        missions, and missions granted by its traits and assigned orgs, then
        subtracts any trait-restricted missions.
        """
        profession = mission_data.get("profession", {})
        trait_granted = mission_data.get("trait_granted", {})
        trait_restricted = mission_data.get("trait_restricted", {})

        missions: set[str] = set(mission_data.get("base", []))
        missions |= set(profession.get(value.get("typeTemplateName", ""), []))
        for trait in traits:
            missions |= set(trait_granted.get(trait, []))
        for ref in value.get("orgs", []):
            org = org_mod.orgs.get(ref["value"])
            if org is not None:
                missions |= org.missions_granted
        for trait in traits:
            missions -= set(trait_restricted.get(trait, []))
        return frozenset(missions)

    @staticmethod
    def _available_councilor_ids(gs: dict, player_faction_id: int) -> set[int]:
        """IDs in the player faction's ``availableCouncilors`` hiring pool.

        Read directly from the raw faction record, which
        [FactionModule][tiargus.factions.FactionModule] does not expose.
        """
        for item in gs[GS_FACTION]:
            if item["Key"]["value"] != player_faction_id:
                continue
            return {
                ref["value"] for ref in item["Value"].get("availableCouncilors", [])
            }
        return set()

councilors property

All councilors keyed by their numeric game-state ID.

__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