diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d4db66e..57e3139 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -64,7 +64,6 @@ jobs: cd dist powershell Compress-Archive stellarisdashboard-build/* ../stellarisdashboard-${{ matrix.os }}.zip cd .. - ls - name: package files if: matrix.os != 'windows-latest' run: | diff --git a/stellarisdashboard/dashboard_app/history_ledger.py b/stellarisdashboard/dashboard_app/history_ledger.py index dd76f2f..19a53d3 100644 --- a/stellarisdashboard/dashboard_app/history_ledger.py +++ b/stellarisdashboard/dashboard_app/history_ledger.py @@ -105,21 +105,21 @@ def query_args_info(self): datamodel.Leader, "leader", dict(leader_id=self.leader_filter), - datamodel.Leader.leader_id.asc(), + datamodel.Leader.leader_id_in_game.asc(), ) elif self.system_filter is not None: return ( datamodel.System, "system", dict(system_id=self.system_filter), - datamodel.System.system_id.asc(), + datamodel.System.system_id_in_game.asc(), ) elif self.planet_filter is not None: return ( datamodel.Planet, "planet", dict(planet_id=self.planet_filter), - datamodel.Planet.planet_id.asc(), + datamodel.Planet.planet_id_in_game.asc(), ) elif self.war_filter is not None: return ( @@ -136,7 +136,7 @@ def query_args_info(self): datamodel.Country, "country", filter_dict, - datamodel.Country.country_id.asc(), + datamodel.Country.country_id_in_game.asc(), ) @property @@ -235,7 +235,7 @@ def get_event_and_link_dicts(self): event_query_kwargs, key_obj_filter_dict, key_object_order_column, - ) = params = self.event_filter.query_args_info + ) = self.event_filter.query_args_info self._most_recent_date = utils.get_most_recent_date(self._session) key_objects = ( @@ -330,7 +330,10 @@ def collect_event_dicts(self, event_list, key_object): def get_war_list(self): if not self.event_filter.is_empty_filter: return [] - return [key for key in self._formatted_urls if isinstance(key, datamodel.War)] + return sorted( + [key for key in self._formatted_urls if isinstance(key, datamodel.War)], + key=lambda w: w.start_date_days, + ) def _get_details(self, key) -> Dict[str, str]: if isinstance(key, datamodel.Country): @@ -474,6 +477,8 @@ def leader_details(self, leader_model: datamodel.Leader) -> Dict[str, str]: "Gender": game_info.convert_id_to_name(leader_model.gender), "Species": leader_model.species.rendered_name, "Class": f"{game_info.convert_id_to_name(leader_model.leader_class)} in the {country_url}", + "Subclass": f"{game_info.lookup_key(leader_model.subclass)}" or "None", + "Traits": ", ".join(leader_model.rendered_traits), "Born": datamodel.days_to_date(leader_model.date_born), "Hired": datamodel.days_to_date(leader_model.date_hired), "Last active": datamodel.days_to_date( @@ -481,7 +486,7 @@ def leader_details(self, leader_model: datamodel.Leader) -> Dict[str, str]: if leader_model.is_active else leader_model.last_date ), - "Status": "Active" if leader_model.is_active else "Dead or Dismissed", + "Status": "Active" if leader_model.is_active else "Dead or retired", } return details @@ -494,23 +499,13 @@ def country_details(self, country_model: datamodel.Country) -> Dict[str, str]: details.update( { "Personality": game_info.convert_id_to_name(gov.personality), - "Government Type": game_info.convert_id_to_name( - gov.gov_type, remove_prefix="gov" - ), - "Authority": game_info.convert_id_to_name( - gov.authority, remove_prefix="auth" - ), + "Government Type": game_info.lookup_key(gov.gov_type), + "Authority": game_info.lookup_key(gov.authority), "Ethics": ", ".join( - [ - game_info.convert_id_to_name(e, remove_prefix="ethic") - for e in sorted(gov.ethics) - ] + [game_info.lookup_key(e) for e in sorted(gov.ethics)] ), "Civics": ", ".join( - [ - game_info.convert_id_to_name(c, remove_prefix="civic") - for c in sorted(gov.civics) - ] + [game_info.lookup_key(c) for c in sorted(gov.civics)] ), } ) diff --git a/stellarisdashboard/dashboard_app/templates/history_page.html b/stellarisdashboard/dashboard_app/templates/history_page.html index 61da8e6..d2d4456 100644 --- a/stellarisdashboard/dashboard_app/templates/history_page.html +++ b/stellarisdashboard/dashboard_app/templates/history_page.html @@ -11,7 +11,7 @@ War logs {% endif %} @@ -47,6 +47,7 @@

{{title[object] | safe}}

Technologies Edicts + Policies Traditions Colonizations Army Combat @@ -66,8 +67,7 @@

{{title[object] | safe}}

{% if event["is_active"] %}(A){% endif %} {{event["start_date"]}}{% if event["end_date"] != null %} - {{event["end_date"]}}{%endif%}: {{ event["leader"].leader_class.capitalize() }} {{ links[event["leader"]] | safe }} ruled the {{ links[event["country"]] | safe}} - {% if "planet" in event %} from the capital {{ links[event["planet"]] | safe }} in the {{ links[event["system"]] | safe }} system {% endif %} - with agenda "{{ event["leader"].agenda }}". + {% if "planet" in event %} from the capital {{ links[event["planet"]] | safe }} in the {{ links[event["system"]] | safe }} system {% endif %}.
@@ -79,7 +79,7 @@

{{title[object] | safe}}

{% if "leader" in event %} Under ruler {{ links[event["leader"]] | safe}}, the {% else %} The {% endif %} - {{ links[event["country"]] | safe }} relocated their capital to the {{ event["planet"].planetclass }} planet {{ links[event["planet"]] | safe }} + {{ links[event["country"]] | safe }} relocated their capital to the {{ event["planet"].planetclass }} {{ links[event["planet"]] | safe }} in the {{ links[event["system"]] | safe }} system.
@@ -90,7 +90,7 @@

{{title[object] | safe}}

{% if event["is_active"] %}(A){% endif %} {{event["start_date"]}}{% if event["end_date"] != null %} - {{event["end_date"]}}{%endif%}: The {{ links[event["country"]] | safe }} colonized the {{ event["planet"].planetclass }} - planet {{ links[event["planet"]] | safe }} in the {{ links[event["system"]] | safe }} system + {{ links[event["planet"]] | safe }} in the {{ links[event["system"]] | safe }} system {% if "leader" in event%} under Governor {{ links[event["leader"]] | safe}}{% endif %}. @@ -161,12 +161,32 @@

{{title[object] | safe}}

- {% elif event["event_type"] == "research_leader" %} -
  • + {% elif event["event_type"] == "councilor" %} +
  • {% if event["is_active"] %}(A){% endif %} {{event["start_date"]}}{% if event["end_date"] != null %} - {{event["end_date"]}}{%endif%}: - Scientist {{ links[event["leader"]] | safe }} led the {{ links[event["country"]] | safe}}'s {{event["description"]}} research efforts. + {{ links[event["leader"]] | safe }} served as {{event["description"]}} on the council of the {{ links[event["country"]] | safe}}. + +
    +
  • + {% elif event["event_type"] == "agenda_preparation" %} +
  • +
    + {% if event["is_active"] %}(A){% endif %} {{event["start_date"]}}{% if event["end_date"] != null %} - {{event["end_date"]}}{%endif%}: + + {% if "leader" in event %}Under ruler {{ links[event["leader"]] | safe}}, the {% else %}The {% endif %} + {{ links[event["country"]] | safe}} prepared a new agenda "{{event["description"]}}". + +
    +
  • + {% elif event["event_type"] == "agenda_launch" %} +
  • +
    + {{event["start_date"]}}: + + {% if "leader" in event %}Under ruler {{ links[event["leader"]] | safe}}, the {% else %}The {% endif %} + {{ links[event["country"]] | safe}} launched the agenda "{{event["description"]}}".
  • @@ -175,10 +195,7 @@

    {{title[object] | safe}}

    {% if event["is_active"] %}(A){% endif %} {{event["start_date"]}}{% if event["end_date"] != null %} - {{event["end_date"]}}{%endif%}: - {% if "leader" in event %} Scientist {{ links[event["leader"]] | safe}} - {% else %} The {{ links[event["country"]] | safe}} - {% endif %} - researched the "{{event["description"]}}" technology. + The {{ links[event["country"]] | safe}} researched the "{{event["description"]}}" technology.
    @@ -202,6 +219,26 @@

    {{title[object] | safe}}

    + {% elif event["event_type"] == "new_policy" %} +
  • +
    + {{event["start_date"]}}: + + {% if "leader" in event %}Under ruler {{ links[event["leader"]] | safe}}, the {% else %}The {% endif %} + {{ links[event["country"]] | safe}} enacted a new policy on {{event["description"]}}. + +
    +
  • + {% elif event["event_type"] == "changed_policy" %} +
  • +
    + {{event["start_date"]}}: + + {% if "leader" in event %}Under ruler {{ links[event["leader"]] | safe}}, the {% else %}The {% endif %} + {{ links[event["country"]] | safe}} changed their policy on {{event["description"]}}. + +
    +
  • {% elif event["event_type"] == "ascension_perk" %}
  • @@ -250,6 +287,24 @@

    {{title[object] | safe}}

  • + {% elif event["event_type"] == "gained_trait" %} +
  • +
    + {{event["start_date"]}}: + + {{ event["leader"].leader_class.capitalize() }} {{ links[event["leader"]] | safe }} gained the "{{ event["description"] }}" trait. + +
    +
  • + {% elif event["event_type"] == "lost_trait" %} +
  • +
    + {{event["start_date"]}}: + + {{ event["leader"].leader_class.capitalize() }} {{ links[event["leader"]] | safe }} lost the "{{ event["description"] }}" trait. + +
    +
  • {% elif event["event_type"] == "first_contact" %}
  • @@ -574,7 +629,7 @@

    {{title[object] | safe}}

    {{event["start_date"]}}: The planet {{ links[event["planet"]] | safe }} in the {{ links[event["system"]] | safe }} system, owned by the - {{ links[event["country"]] | safe }} was destroyed. It is now a {{ event["planet"].planet_class }} planet. + {{ links[event["country"]] | safe }} was destroyed. It is now a {{ event["planet"].planetclass }}.
  • @@ -583,7 +638,7 @@

    {{title[object] | safe}}

    {{event["start_date"]}}: - {{ event["leader"].leader_class.capitalize() }} {{ links[event["leader"]] | safe }} died or was fired. + {{ event["leader"].leader_class.capitalize() }} {{ links[event["leader"]] | safe }} died or retired.
    diff --git a/stellarisdashboard/dashboard_app/utils.py b/stellarisdashboard/dashboard_app/utils.py index 347167c..671cced 100644 --- a/stellarisdashboard/dashboard_app/utils.py +++ b/stellarisdashboard/dashboard_app/utils.py @@ -7,7 +7,7 @@ logger = logging.getLogger(__name__) -VERSION = "v4.4" +VERSION = "v5.2" def parse_version(version: str): diff --git a/stellarisdashboard/datamodel.py b/stellarisdashboard/datamodel.py index f7cbbf0..46c28d0 100644 --- a/stellarisdashboard/datamodel.py +++ b/stellarisdashboard/datamodel.py @@ -43,7 +43,6 @@ def get_db_session(game_id) -> sqlalchemy.orm.Session: s.close() -### Some enum types representing various in-game concepts @enum.unique class Attitude(enum.Enum): is_player = enum.auto() @@ -140,12 +139,14 @@ class HistoricalEventType(enum.Enum): # tied to a specific leader: ruled_empire = enum.auto() governed_sector = enum.auto() - research_leader = enum.auto() + councilor = enum.auto() faction_leader = enum.auto() leader_recruited = enum.auto() leader_died = enum.auto() # TODO level_up = enum.auto() fleet_command = enum.auto() + gained_trait = enum.auto() + lost_trait = enum.auto() # empire progress: researched_technology = enum.auto() @@ -153,6 +154,8 @@ class HistoricalEventType(enum.Enum): ascension_perk = enum.auto() edict = enum.auto() expanded_to_system = enum.auto() + agenda_preparation = enum.auto() + agenda_launch = enum.auto() # Planets and sectors: colonization = enum.auto() @@ -169,6 +172,8 @@ class HistoricalEventType(enum.Enum): new_faction = enum.auto() government_reform = enum.auto() species_rights_reform = enum.auto() # TODO + new_policy = enum.auto() + changed_policy = enum.auto() # Galactic community and council joined_galactic_community = enum.auto() @@ -254,7 +259,7 @@ def scope(self): HistoricalEventType.sector_creation, HistoricalEventType.planetary_unrest, HistoricalEventType.governed_sector, - HistoricalEventType.research_leader, + HistoricalEventType.councilor, HistoricalEventType.faction_leader, HistoricalEventType.leader_recruited, HistoricalEventType.leader_died, @@ -278,7 +283,6 @@ def scope(self): return HistoricalEventScope.all -### Some convenience functions def date_to_days(date_str: str) -> int: """Converts a date given in-game ("2200.03.01") to an integer counting the days passed since 2200.01.01. @@ -307,7 +311,6 @@ def days_to_date(days: float) -> str: return f"{year:04}.{month:02}.{day:02}" -### Some helper functions to conveniently access the databases. def get_last_modified_time(path: pathlib.Path) -> int: return path.stat().st_mtime @@ -364,7 +367,7 @@ def get_gamestates_since(game_name: str, date: float): class Game(Base): """Root object representing an entire game.""" - __tablename__ = "gametable" + __tablename__ = "game" game_id = Column(Integer, primary_key=True) game_name = Column(String(50)) @@ -400,15 +403,15 @@ def galaxy(self): @property def galaxy_template(self): - return game_info.convert_id_to_name(self.db_galaxy_template) + return game_info.lookup_key(self.db_galaxy_template) @property def galaxy_shape(self): - return game_info.convert_id_to_name(self.db_galaxy_shape) + return game_info.lookup_key(self.db_galaxy_shape) @property def difficulty(self): - return game_info.convert_id_to_name(self.db_difficulty) + return game_info.lookup_key(self.db_difficulty) @property def last_updated(self): @@ -418,7 +421,7 @@ def last_updated(self): class SharedDescription(Base): """Represents short descriptions like technology names that are likely to occur many times for various empires.""" - __tablename__ = "shareddescriptiontable" + __tablename__ = "shared_description" description_id = Column(Integer, primary_key=True) text = Column(String(500), index=True) @@ -427,10 +430,10 @@ class SharedDescription(Base): class System(Base): """Represents a single star system/galactic_object.""" - __tablename__ = "systemtable" + __tablename__ = "system" system_id = Column(Integer, primary_key=True) game_id = Column(ForeignKey(Game.game_id)) - country_id = Column(ForeignKey("countrytable.country_id"), index=True) + country_id = Column(ForeignKey("country.country_id"), index=True) name = Column(String(80)) system_id_in_game = Column(Integer, index=True) @@ -502,11 +505,11 @@ def __str__(self): class SystemOwnership(Base): """Represent the timespan during which some empire owned a given system.""" - __tablename__ = "systemownershiptable" + __tablename__ = "system_ownership" system_ownership_id = Column(Integer, primary_key=True) system_id = Column(ForeignKey(System.system_id), index=True) - owner_country_id = Column(ForeignKey("countrytable.country_id")) + owner_country_id = Column(ForeignKey("country.country_id")) start_date_days = Column(Integer, index=True) end_date_days = Column(Integer, index=True) @@ -532,7 +535,7 @@ class HyperLane(Base): represented as a directed edge, it should be interpreted as undirected. """ - __tablename__ = "hyperlanetable" + __tablename__ = "hyperlane" hyperlane_id = Column(Integer, primary_key=True) system_one_id = Column(ForeignKey(System.system_id)) @@ -555,7 +558,7 @@ class Bypass(Base): Represent bypasses. """ - __tablename__ = "bypasstable" + __tablename__ = "bypass" bypass_id = Column(Integer, primary_key=True) system_id = Column(ForeignKey(System.system_id), nullable=False, index=True) @@ -579,7 +582,7 @@ def name(self): class GameState(Base): """Represents the state of the game at a specific moment.""" - __tablename__ = "gamestatetable" + __tablename__ = "gamestate" gamestate_id = Column(Integer, primary_key=True) game_id = Column(ForeignKey(Game.game_id)) date = Column(Integer, index=True, nullable=False) # Days since 2200.1.1 @@ -599,15 +602,12 @@ def __str__(self): class Country(Base): - __tablename__ = "countrytable" + __tablename__ = "country" country_id = Column(Integer, primary_key=True) game_id = Column(ForeignKey(Game.game_id)) - capital_planet_id = Column(ForeignKey("planettable.planet_id")) + capital_planet_id = Column(ForeignKey("planet.planet_id")) - ruler_id = Column(ForeignKey("leadertable.leader_id")) - scientist_physics_id = Column(ForeignKey("leadertable.leader_id")) - scientist_society_id = Column(ForeignKey("leadertable.leader_id")) - scientist_engineering_id = Column(ForeignKey("leadertable.leader_id")) + ruler_id = Column(ForeignKey("leader.leader_id")) is_player = Column(Boolean) is_other_player = Column(Boolean) @@ -622,11 +622,6 @@ class Country(Base): capital = relationship("Planet", foreign_keys=[capital_planet_id], post_update=True) ruler = relationship("Leader", foreign_keys=[ruler_id]) - scientist_physics = relationship("Leader", foreign_keys=[scientist_physics_id]) - scientist_society = relationship("Leader", foreign_keys=[scientist_society_id]) - scientist_engineering = relationship( - "Leader", foreign_keys=[scientist_engineering_id] - ) governments = relationship( "Government", back_populates="country", cascade="all,delete,delete-orphan" @@ -658,6 +653,11 @@ class Country(Base): ascension_perks = relationship("AscensionPerk", cascade="all,delete,delete-orphan") technologies = relationship("Technology", cascade="all,delete,delete-orphan") + council_agendas = relationship( + "CouncilAgenda", back_populates="country", cascade="all,delete,delete-orphan" + ) + policies = relationship("Policy", foreign_keys=lambda: [Policy.country_id]) + historical_events = relationship( "HistoricalEvent", back_populates="country", @@ -706,27 +706,6 @@ def is_real_country(self): or self.country_type == "awakened_fallen_empire" ) - def get_research_leader(self, key: str): - if key == "physics": - return self.scientist_physics - elif key == "society": - return self.scientist_society - elif key == "engineering": - return self.scientist_engineering - else: - logger.warning(f"Unknown research leader ID: {key}") - return None - - def set_research_leader(self, key: str, new_leader: "Leader"): - if key == "physics": - self.scientist_physics = new_leader - elif key == "society": - self.scientist_society = new_leader - elif key == "engineering": - self.scientist_engineering = new_leader - else: - logger.warning(f"Could not set research leader for {key}") - def diplo_relation_details(self): countries_by_relation = {} for relation in self.outgoing_relations: @@ -738,7 +717,7 @@ def diplo_relation_details(self): class Tradition(Base): - __tablename__ = "traditionstable" + __tablename__ = "tradition" tradition_id = Column(Integer, primary_key=True) country_id = Column(ForeignKey(Country.country_id), index=True) tradition_name_id = Column(ForeignKey(SharedDescription.description_id)) @@ -752,7 +731,7 @@ def name(self): class AscensionPerk(Base): - __tablename__ = "ascensionperkstable" + __tablename__ = "ascension_perk" tradition_id = Column(Integer, primary_key=True) country_id = Column(ForeignKey(Country.country_id), index=True) perk_name_id = Column(ForeignKey(SharedDescription.description_id)) @@ -766,7 +745,7 @@ def name(self): class Technology(Base): - __tablename__ = "technologytable" + __tablename__ = "technology" technology_id = Column(Integer, primary_key=True) country_id = Column(ForeignKey(Country.country_id), index=True) @@ -789,7 +768,7 @@ class Government(Base): and civics. """ - __tablename__ = "govtable" + __tablename__ = "government" gov_id = Column(Integer, primary_key=True) country_id = Column(ForeignKey(Country.country_id), index=True) @@ -876,8 +855,54 @@ def __str__(self): return f"{self.authority} {self.gov_type} {self.civics} {self.ethics}" +class Policy(Base): + __tablename__ = "policy" + + policy_id = Column(Integer, primary_key=True) + country_id = Column(ForeignKey(Country.country_id), nullable=False, index=True) + + policy_date = Column(Integer) + is_active = Column(Boolean) + + policy_name_id = Column(ForeignKey(SharedDescription.description_id), index=True) + selected_id = Column(ForeignKey(SharedDescription.description_id), index=True) + + country_model = relationship( + Country, + back_populates="policies", + foreign_keys=[country_id], + ) + + policy_name = relationship(SharedDescription, foreign_keys=[policy_name_id]) + selected = relationship(SharedDescription, foreign_keys=[selected_id]) + + +class CouncilAgenda(Base): + __tablename__ = "council_agenda" + + agenda_id = Column(Integer, primary_key=True) + country_id = Column(ForeignKey(Country.country_id), nullable=False, index=True) + + start_date = Column(Integer) + launch_date = Column(Integer) + cooldown_date = Column(Integer) + + is_resolved = Column(Boolean) + + name_id = Column(ForeignKey(SharedDescription.description_id), index=True) + + country = relationship( + Country, foreign_keys=[country_id], back_populates="council_agendas" + ) + db_name = relationship(SharedDescription, foreign_keys=[name_id]) + + @property + def rendered_name(self) -> str: + return game_info.lookup_key(self.db_name.text + "_name") + + class DiplomaticRelation(Base): - __tablename__ = "diplo_relation_table" + __tablename__ = "diplomatic_relation" diplo_relation_id = Column(Integer, primary_key=True) @@ -946,7 +971,7 @@ def active_relations(self) -> Iterable[str]: class CountryData(Base): """Representation of the state of a single country at a specific time.""" - __tablename__ = "countrydatatable" + __tablename__ = "country_data" country_data_id = Column(Integer, primary_key=True) country_id = Column(ForeignKey(Country.country_id), index=True) game_state_id = Column(ForeignKey(GameState.gamestate_id), index=True) @@ -1084,7 +1109,7 @@ class GalacticMarketResource(Base): The name of the resource and its """ - __tablename__ = "galacticmarketresourcetable" + __tablename__ = "galactic_market_resource" galactic_market_resource_id = Column(Integer, primary_key=True) game_state_id = Column(ForeignKey(GameState.gamestate_id), index=True) country_data_id = Column(ForeignKey(CountryData.country_data_id), index=True) @@ -1106,7 +1131,7 @@ class GalacticMarketResource(Base): class InternalMarketResource(Base): - __tablename__ = "internalmarketresourcetable" + __tablename__ = "internal_market_resource" internal_market_resource_id = Column(Integer, primary_key=True) country_data_id = Column(ForeignKey(CountryData.country_data_id), index=True) @@ -1122,7 +1147,7 @@ class InternalMarketResource(Base): class BudgetItem(Base): - __tablename__ = "budgetitemtable" + __tablename__ = "budget_item" budget_item_id = Column(Integer, primary_key=True) country_data_id = Column(ForeignKey(CountryData.country_data_id), index=True) @@ -1163,11 +1188,11 @@ def name(self) -> str: class Species(Base): """Represents a species in a game. Not tied to any specific time.""" - __tablename__ = "speciestable" + __tablename__ = "species" species_id = Column(Integer, primary_key=True) game_id = Column(ForeignKey(Game.game_id)) species_id_in_game = Column(Integer) - home_planet_id = Column(ForeignKey("planettable.planet_id")) + home_planet_id = Column(ForeignKey("planet.planet_id")) species_name = Column(String(80)) species_class = Column(String(20)) @@ -1189,7 +1214,7 @@ def rendered_name(self): class SpeciesTrait(Base): - __tablename__ = "speciestraittable" + __tablename__ = "species_trait" trait_id = Column(Integer, primary_key=True) description_id = Column(ForeignKey(SharedDescription.description_id)) @@ -1206,7 +1231,7 @@ def name(self) -> str: class PoliticalFaction(Base): """Represents a single political faction in a game. Not tied to any specific time.""" - __tablename__ = "factiontable" + __tablename__ = "political_faction" faction_id = Column(Integer, primary_key=True) country_id = Column(ForeignKey(Country.country_id), index=True) @@ -1227,14 +1252,14 @@ def type(self): @property def rendered_name(self): - rendered = game_info.render_name(self.faction_name) + rendered = game_info.render_name(self.faction_name) return rendered class War(Base): """Wars are represented by a list of participants and a list of combat events.""" - __tablename__ = "wartable" + __tablename__ = "war" war_id = Column(Integer, primary_key=True) game_id = Column(ForeignKey(Game.game_id)) @@ -1276,9 +1301,21 @@ def defenders(self): if not p.is_attacker and p.call_type == "primary": yield p + @property + def start_date(self): + if self.start_date_days is not None: + return days_to_date(self.start_date_days) + return None + + @property + def end_date(self): + if self.end_date_days is not None: + return days_to_date(self.end_date_days) + return None + class WarParticipant(Base): - __tablename__ = "warparticipanttable" + __tablename__ = "war_participant" warparticipant_id = Column(Integer, primary_key=True) war_id = Column(ForeignKey(War.war_id), index=True) @@ -1306,11 +1343,11 @@ def get_war_goal(self): class Combat(Base): - __tablename__ = "combattable" + __tablename__ = "combat" combat_id = Column(Integer, primary_key=True) system_id = Column(ForeignKey(System.system_id), nullable=False) - planet_id = Column(ForeignKey("planettable.planet_id")) + planet_id = Column(ForeignKey("planet.planet_id")) war_id = Column(ForeignKey(War.war_id), nullable=False) date = Column(Integer, nullable=False, index=True) @@ -1324,14 +1361,16 @@ class Combat(Base): system = relationship("System") planet = relationship("Planet") war = relationship("War", back_populates="combat") - attackers = relationship( - "CombatParticipant", - primaryjoin="and_(Combat.combat_id==CombatParticipant.combat_id, CombatParticipant.is_attacker==True)", - ) - defenders = relationship( - "CombatParticipant", - primaryjoin="and_(Combat.combat_id==CombatParticipant.combat_id, CombatParticipant.is_attacker==False)", - ) + + participants = relationship("CombatParticipant") + + @property + def attackers(self): + return [p for p in self.participants if p.is_attacker] + + @property + def defenders(self): + return [p for p in self.participants if not p.is_attacker] def involved_countries(self) -> Iterable[Country]: for cp in itertools.chain(self.attackers, self.defenders): @@ -1348,7 +1387,7 @@ class CombatParticipant(Base): overall war. """ - __tablename__ = "combatparticipant" + __tablename__ = "combat_participant" combat_participant_id = Column(Integer, primary_key=True) combat_id = Column(ForeignKey(Combat.combat_id), index=True) @@ -1361,7 +1400,7 @@ class CombatParticipant(Base): war_participant = relationship( "WarParticipant", back_populates="combat_participation" ) - combat = relationship("Combat") + combat = relationship("Combat", back_populates="participants") @property def country(self): @@ -1369,7 +1408,7 @@ def country(self): class Leader(Base): - __tablename__ = "leadertable" + __tablename__ = "leader" leader_id = Column(Integer, primary_key=True) game_id = Column(ForeignKey(Game.game_id)) @@ -1382,14 +1421,16 @@ class Leader(Base): species_id = Column(ForeignKey(Species.species_id)) leader_class = Column(String(80)) gender = Column(String(20)) - leader_agenda = Column(String(80)) last_level = Column(Integer) + subclass = Column(String()) # optional + leader_traits = Column(String()) # comma-separated + date_hired = Column(Integer) # The date when this leader was first encountered date_born = Column(Integer) # estimated birthday last_date = Column(Integer) # estimated death / dismissal is_active = Column(Boolean, index=True) - fleet_id = Column(ForeignKey("fleettable.fleet_id"), nullable=True) + fleet_id = Column(ForeignKey("fleet.fleet_id"), nullable=True) game = relationship("Game", back_populates="leaders") country = relationship( @@ -1414,12 +1455,26 @@ def rendered_name(self): return rendered @property - def agenda(self): - return game_info.convert_id_to_name(self.leader_agenda, remove_prefix="agenda") + def rendered_traits(self) -> List[str]: + return [self.render_trait(t) for t in sorted(self.leader_traits.split("|"))] + + def render_trait(self, text: str): + key = text.rstrip("_0123456789") + description = game_info.lookup_key(key) + + if description == "[triggered_imperial_name]": + # logic from common/scripted_loc/08_scripted_loc_paragon.txt + key = "imperial_ruler" if self == self.country.ruler else "imperial_heir" + return game_info.lookup_key(key) + else: + level = text.removeprefix(key).lstrip("_") + if level: + description = f"{description} ({level})" + return description class Planet(Base): - __tablename__ = "planettable" + __tablename__ = "planet" planet_id = Column(Integer, primary_key=True) planet_name = Column(String(50)) @@ -1451,11 +1506,11 @@ def rendered_name(self): @property def planetclass(self): - return game_info.convert_id_to_name(self.planet_class, remove_prefix="pc") + return game_info.lookup_key(self.planet_class) class PlanetDistrict(Base): - __tablename__ = "planet_district_table" + __tablename__ = "planet_district" district_id = Column(Integer, primary_key=True) planet_id = Column(ForeignKey(Planet.planet_id), nullable=False, index=True) @@ -1474,7 +1529,7 @@ def name(self): class PlanetDeposit(Base): - __tablename__ = "planet_deposit_table" + __tablename__ = "planet_deposit" deposit_id = Column(Integer, primary_key=True) planet_id = Column(ForeignKey(Planet.planet_id), nullable=False, index=True) @@ -1499,7 +1554,7 @@ def is_resource_deposit(self): class PlanetBuilding(Base): - __tablename__ = "planet_building_table" + __tablename__ = "planet_building" building_id = Column(Integer, primary_key=True) planet_id = Column(ForeignKey(Planet.planet_id), nullable=False, index=True) @@ -1518,7 +1573,7 @@ def name(self): class PlanetModifier(Base): - __tablename__ = "planet_modifier_table" + __tablename__ = "planet_modifier" modifier_id = Column(Integer, primary_key=True) planet_id = Column(ForeignKey(Planet.planet_id), nullable=False, index=True) @@ -1537,7 +1592,7 @@ def name(self): class PopStatsBySpecies(Base): - __tablename__ = "popstats_species_table" + __tablename__ = "popstats_species" pop_stats_species_id = Column(Integer, primary_key=True) country_data_id = Column(ForeignKey(CountryData.country_data_id), index=True) species_id = Column(ForeignKey(Species.species_id)) @@ -1552,7 +1607,7 @@ class PopStatsBySpecies(Base): class PopStatsByFaction(Base): - __tablename__ = "popstats_faction_table" + __tablename__ = "popstats_faction" pop_stats_faction_id = Column(Integer, primary_key=True) country_data_id = Column(ForeignKey(CountryData.country_data_id), index=True) faction_id = Column(ForeignKey(PoliticalFaction.faction_id)) @@ -1570,7 +1625,7 @@ class PopStatsByFaction(Base): class PopStatsByJob(Base): - __tablename__ = "popstats_job_table" + __tablename__ = "popstats_job" pop_stats_job_id = Column(Integer, primary_key=True) country_data_id = Column(ForeignKey(CountryData.country_data_id), index=True) job_description_id = Column(ForeignKey(SharedDescription.description_id)) @@ -1589,7 +1644,7 @@ def job_description(self) -> str: class PopStatsByStratum(Base): - __tablename__ = "popstats_stratum_table" + __tablename__ = "popstats_stratum" pop_stats_stratum_id = Column(Integer, primary_key=True) country_data_id = Column(ForeignKey(CountryData.country_data_id), index=True) stratum_description_id = Column(ForeignKey(SharedDescription.description_id)) @@ -1608,7 +1663,7 @@ def stratum(self) -> str: class PopStatsByEthos(Base): - __tablename__ = "popstats_ethos_table" + __tablename__ = "popstats_ethos" pop_stats_stratum_id = Column(Integer, primary_key=True) country_data_id = Column(ForeignKey(CountryData.country_data_id), index=True) ethos_description_id = Column(ForeignKey(SharedDescription.description_id)) @@ -1627,7 +1682,7 @@ def ethos(self) -> str: class PlanetStats(Base): - __tablename__ = "planetstats_table" + __tablename__ = "planetstats" planet_stats_id = Column(Integer, primary_key=True) countrydata_id = Column(ForeignKey(CountryData.country_data_id), index=True) @@ -1648,7 +1703,7 @@ class PlanetStats(Base): class Fleet(Base): - __tablename__ = "fleettable" + __tablename__ = "fleet" fleet_id = Column(Integer, primary_key=True) fleet_id_in_game = Column(Integer, nullable=False, index=True) @@ -1670,7 +1725,7 @@ class HistoricalEvent(Base): certain columns may be null. """ - __tablename__ = "historicaleventtable" + __tablename__ = "historical_event" historical_event_id = Column(Integer, primary_key=True) event_type = Column(Enum(HistoricalEventType), nullable=False, index=True) @@ -1717,19 +1772,13 @@ def __str__(self): def description(self) -> str: """A brief description associated with the event, e.g. technology name or changes in government reform.""" if self.event_type == HistoricalEventType.tradition: - return game_info.convert_id_to_name( - self.db_description.text, remove_prefix="tr" - ) + return game_info.lookup_key(self.db_description.text) elif self.event_type == HistoricalEventType.researched_technology: - return game_info.convert_id_to_name( - self.db_description.text, remove_prefix="tech" - ) + return game_info.lookup_key(self.db_description.text) elif self.event_type == HistoricalEventType.edict: - return game_info.convert_id_to_name(self.db_description.text) + return game_info.lookup_key("edict_" + self.db_description.text) elif self.event_type == HistoricalEventType.ascension_perk: - return game_info.convert_id_to_name( - self.db_description.text, remove_prefix="ap" - ) + return game_info.lookup_key(self.db_description.text) elif self.event_type == HistoricalEventType.government_reform: old_gov = self.country.get_government_for_date(self.start_date_days - 1) new_gov = self.country.get_government_for_date(self.start_date_days) @@ -1742,15 +1791,15 @@ def description(self) -> str: return ref elif self.event_type == HistoricalEventType.terraforming: current_pc, target_pc = self.db_description.text.split(",") - current_pc = game_info.convert_id_to_name(current_pc, remove_prefix="pc") - target_pc = game_info.convert_id_to_name(target_pc, remove_prefix="pc") + current_pc = game_info.lookup_key(current_pc) + target_pc = game_info.lookup_key(target_pc) return f"from {current_pc} to {target_pc}" elif self.event_type in [ HistoricalEventType.envoy_federation, HistoricalEventType.formed_federation, HistoricalEventType.governed_sector, ]: - return game_info.render_name(self.db_description.text) + return game_info.lookup_key(self.db_description.text) elif self.event_type in [HistoricalEventType.war]: call_type = self.db_description.text if call_type == "primary": @@ -1765,9 +1814,33 @@ def description(self) -> str: return f" due to their alliance with the" else: return f" (called as {call_type})" - + elif self.event_type == HistoricalEventType.councilor: + return game_info.lookup_key(self.db_description.text) + elif self.event_type == HistoricalEventType.new_policy: + name, selected = self.db_description.text.split("|") + name_rendered = game_info.convert_id_to_name(name) + selected_rendered = game_info.lookup_key(selected) + return f"{name_rendered!r}: {selected_rendered!r}" + elif self.event_type == HistoricalEventType.changed_policy: + name, old, new = self.db_description.text.split("|") + name_rendered = game_info.convert_id_to_name(name) + old_rendered = game_info.lookup_key(old) + new_rendered = game_info.lookup_key(new) + return f"{name_rendered!r} from {old_rendered!r} to {new_rendered!r}" + elif self.event_type in ( + HistoricalEventType.agenda_launch, + HistoricalEventType.agenda_preparation, + ): + return game_info.lookup_key( + f"council_agenda_{self.db_description.text}_name" + ) + elif self.event_type in ( + HistoricalEventType.gained_trait, + HistoricalEventType.lost_trait, + ): + return self.leader.render_trait(self.db_description.text) elif self.db_description: - return game_info.convert_id_to_name(self.db_description.text) + return game_info.lookup_key(self.db_description.text) else: return "Unknown Event" diff --git a/stellarisdashboard/game_info.py b/stellarisdashboard/game_info.py index b0d06da..f397bcd 100644 --- a/stellarisdashboard/game_info.py +++ b/stellarisdashboard/game_info.py @@ -1,17 +1,23 @@ import json -import re import logging +import re +from typing import Iterable logger = logging.getLogger(__name__) -# Regex explanation: https://regex101.com/r/l76XGd/1 +# Regex explanation: https://regex101.com/r/qc0QhS/1 loc_re = re.compile(r'\s*(?P\S+?):\d*\s*"(?P.*)"\s*(#.*)?') +var_re = re.compile(r"\$(?P\S+)\$") + class NameRenderer: default_name = "Unknown name" def __init__(self, localization_files): self.localization_files = localization_files - self.name_mapping = None + self.name_mapping: dict[str, str] = {} + self.adjective_templates: dict[ + str, str + ] = {} # map noun suffixes to adjective suffix def load_name_mapping(self): """ @@ -20,30 +26,45 @@ def load_name_mapping(self): Localization files can be passed in, by default the dashboard tries to locate them from """ self.name_mapping = {"global_event_country": "Global event country"} - for p in self.localization_files: - # manually parse yaml, yaml.safe_load doesnt seem to work - with open(p, "rt", encoding="utf-8") as f: - for line in f: - try: - re_match = loc_re.match(line) - if re_match: - self.name_mapping[ re_match.group('key') ] = re_match.group('value') - else: - if not line.startswith( ('#', " ", " #", " #", " #", "\ufeffl_english:", "l_english:", "\n", " \n" ) ): - # This error prints the offending line as numbers because not only did we encounter whitespace, we encountered the Zero Width No-Break Space (BOM) - logger.debug(f"Unexpected unmatched localisation line found. Characters (as integers) follow: {[ord(x) for x in line]}") - except Exception as e: - logger.warning(f"Caught exception reading localisation files: {e}") + self.adjective_templates = {} + # manually parse the yaml files, yaml.safe_load doesn't seem to work + ignored_prefixes = ("#", "\ufeffl_english:", "l_english:") + for line in self._iter_localization_lines(): + try: + re_match = loc_re.match(line) + if re_match: + key = re_match.group("key") + value = re_match.group("value") + if key.startswith("adj_NN"): + key = key.removeprefix("adj_NN") + value = value.split("|")[0] + key = key.lstrip("*") + self.adjective_templates[key] = value + else: + self.name_mapping[key] = value + else: + if not line.startswith(ignored_prefixes): + # This error prints the offending line as numbers because not only did we encounter whitespace, + # we encountered the Zero Width No-Break Space (BOM) + logger.debug( + f"Unexpected unmatched localisation line found: {line=!r}" + ) + except Exception as e: + logger.warning(f"Caught exception reading localisation files: {e}") # Add missing format that is similar to but not the same as adj_format in practice if "%ADJECTIVE%" not in self.name_mapping: - self.name_mapping["%ADJECTIVE%"] = "$adjective$ $1$" + # Try to get it from game's localization config: + adj_format = self.name_mapping.get("adj_format", "adj $1$") + self.name_mapping["%ADJECTIVE%"] = adj_format.replace("adj", "%adjective%") # Alternate format with no template (meant to be concatenated?). Incomplete solution. -# if "%ADJ%" not in self.name_mapping: -# self.name_mapping["%ADJ%"] = "$1$" + # if "%ADJ%" not in self.name_mapping: + # self.name_mapping["%ADJ%"] = "$1$" if "%LEADER_1%" not in self.name_mapping: - self.name_mapping["%LEADER_1%"] = "$1$ $2$" + self.name_mapping["%LEADER_1%"] = "$1$ $2$" if "%LEADER_2%" not in self.name_mapping: - self.name_mapping["%LEADER_2%"] = "$1$ $2$" + self.name_mapping["%LEADER_2%"] = "$1$ $2$" + + logger.debug(f"Found adjective templates: {self.adjective_templates}") def render_from_json(self, name_json: str): try: @@ -69,47 +90,161 @@ def render_from_dict(self, name_dict: dict) -> str: return key render_template = self.name_mapping.get(key, key) - # The %ADJ% template is odd. See GitHub #90 - if render_template == "%ADJ%": - render_template = "$1$" - if "variables" in name_dict and "value" in name_dict["variables"][0] and "key" in name_dict["variables"][0]["value"] and "$1$" not in self.name_mapping.get(name_dict["variables"][0]["value"]["key"], ""): - name_dict["variables"][0]["value"]["key"] += " $1$" + render_template = self._preprocess_template(render_template, name_dict) - substitution_values = [] if "value" in name_dict: return self.render_from_dict(name_dict["value"]) + substitution_values = self._collect_substitution_values(name_dict) + render_template = self._substitute_variables( + render_template, substitution_values + ) + render_template = self._handle_unresolved_variables(render_template) + return render_template + + def _collect_substitution_values(self, name_dict): + substitution_values = [] for var in name_dict.get("variables", []): - if "key" not in var or "value" not in var: - continue - var_key = var.get("key") - substitution_values.append((var_key, self.render_from_dict(var["value"]))) + if "key" in var and "value" in var: + var_key = var.get("key") + substitution_values.append( + (var_key, self.render_from_dict(var["value"])) + ) + return substitution_values + + def _preprocess_template(self, render_template, name_dict): + """ + Handle some special keys. + """ + if render_template == "%ADJ%": + render_template = "$1$" + if ( + "variables" in name_dict + and "value" in name_dict["variables"][0] + and "key" in name_dict["variables"][0]["value"] + ): + # substitute predefined constants + tmp = name_dict["variables"][0]["value"]["key"] + if tmp in self.name_mapping: + name_dict["variables"][0]["value"]["key"] = self.name_mapping[tmp] + name_dict["variables"][0]["value"]["key"] += " $1$" + elif render_template == "%SEQ%": + render_template = "$fmt$" - # try all combinations of escaping identifiers to substitute the variables + return render_template + + def _substitute_variables(self, render_template, substitution_values): + if render_template == "%ACRONYM%": + for key, acronym_base in substitution_values: + if key == "base": + render_template = "".join( + s[0].upper() for s in acronym_base.split() + ) + render_template += acronym_base[-1].upper() + # try all combinations of escaping identifiers + parentheses = [ + ("<", ">"), + ("[", "]"), + ("$", "$"), + ("%", "%"), + ] for subst_key, subst_value in substitution_values: - for lparen, rparen in [ - ("<", ">"), - ("[", "]"), - ("$", "$"), - ("%", "%"), - ]: + if subst_key == "num": + try: + render_template = render_template.replace( + f"$ORD$", self._fmt_ord_number(int(subst_value)) + ) + except ValueError: + ... + if subst_key == "adjective": + subst_value = self._fmt_adjective(subst_value) + for l, r in parentheses: render_template = render_template.replace( - f"{lparen}{subst_key}{rparen}", subst_value + f"{l}{subst_key}{r}", subst_value, 1 ) return render_template + def _handle_unresolved_variables(self, render_template): + # remove any identifiers remaining after substitution: + for pattern in [ + r"\[[0-9]*\]", + r"\$[0-9]*\$", + r"%[0-9]*%", + r"<[0-9]*>", + ]: + # in some cases, the template can be left with some unresolved fields. Example from a game: + # LEADER_2 -> "$1$ $2$", then $2$ = MAM4_CHR_daughterofNagg -> "$1$, daughter of Nagg" + # This last template contains another $1$ which is never resolved. + render_template = re.sub(pattern, "", render_template) + # post-processing: The above issue can cause + render_template = ", ".join(s.strip() for s in render_template.split(",")) + render_template = ". ".join(s.strip() for s in render_template.split(".")) + + # Special case: tradition and technology have same name... + match_indirect_reference = re.match(r"\$([a-z0-9_]*)\$", render_template) + if match_indirect_reference: + second_lookup = match_indirect_reference.group(1) + render_template = lookup_key(second_lookup) + + # Find variables that were not resolved so far: + for match in var_re.findall(render_template): + if match == "ORD": + continue + resolved = lookup_key(match) + render_template = re.sub(f"\${match}\$", resolved, render_template) + + return render_template + + def _fmt_ord_number(self, num: int): + if num % 10 == 1: + return f"{num}st" + if num % 10 == 2: + return f"{num}nd" + if num % 10 == 3: + return f"{num}rd" + return f"{num}th" + + def _fmt_adjective(self, noun: str) -> str: + # {'i': '*ian $1$', 'r': '*ran $1$', 'a': '*an $1$', 'e': '*an $1$', 'us': '*an $1$', + # 'is': '*an $1$', 'es': '*an $1$', 'ss': '*an $1$', 'id': '*an $1$', 'ed': '*an $1$', + # 'ad': '*an $1$', 'od': '*an $1$', 'ud': '*an $1$', 'yd': '*an $1$'} + noun_suffix = "" + adj_template = "*" + for suffix in sorted(self.adjective_templates, key=len, reverse=True): + if noun.endswith(suffix): + noun_suffix = suffix + adj_template = self.adjective_templates[suffix] + break + return adj_template.replace("*", noun.removesuffix(noun_suffix)) + + def _iter_localization_lines(self) -> Iterable[str]: + for p in self.localization_files: + with open(p, "rt", encoding="utf-8") as f: + for line in f: + line = line.lstrip() + if line: + yield line + global_renderer: NameRenderer = None def render_name(json_str: str): + return get_global_renderer().render_from_json(json_str) + + +def lookup_key(key: str) -> str: + return get_global_renderer().render_from_dict({"key": key}) + + +def get_global_renderer() -> NameRenderer: global global_renderer if global_renderer is None: from stellarisdashboard import config global_renderer = NameRenderer(config.CONFIG.localization_files) global_renderer.load_name_mapping() - return global_renderer.render_from_json(json_str) + return global_renderer COLONIZABLE_PLANET_CLASSES_PLANETS = { diff --git a/stellarisdashboard/parsing/timeline.py b/stellarisdashboard/parsing/timeline.py index 1b04176..af8e38c 100644 --- a/stellarisdashboard/parsing/timeline.py +++ b/stellarisdashboard/parsing/timeline.py @@ -7,7 +7,7 @@ import logging import random import time -from typing import Dict, Any, Set, Iterable, Optional, Union, List, Tuple +from typing import Dict, Any, Set, Iterable, Optional, Union, List, Tuple, Collection import sqlalchemy @@ -197,7 +197,9 @@ def _data_processors(self) -> Iterable["AbstractGamestateDataProcessor"]: yield SectorColonyEventProcessor() yield PlanetUpdateProcessor() yield RulerEventProcessor() + yield CouncilProcessor() yield GovernmentProcessor() + yield PolicyProcessor() yield FactionProcessor() yield DiplomacyUpdatesProcessor() yield GalacticCommunityProcessor() @@ -277,7 +279,9 @@ def extract_data_from_gamestate(self, dependencies): s.system_id_in_game: s for s in self._session.query(datamodel.System) } self.starbase_systems = {} - for ingame_id, system_data in self._gamestate_dict["galactic_object"].items(): + for ingame_id, system_data in sorted( + self._gamestate_dict["galactic_object"].items() + ): if "starbases" in system_data: starbases = system_data["starbases"] if len(starbases) > 1: @@ -411,7 +415,9 @@ def data(self) -> Dict[int, datamodel.Country]: return self.countries_by_ingame_id def extract_data_from_gamestate(self, dependencies): - for country_id, country_data_dict in self._gamestate_dict["country"].items(): + for country_id, country_data_dict in sorted( + self._gamestate_dict["country"].items() + ): if not isinstance(country_data_dict, dict): continue country_type = country_data_dict.get("type") @@ -907,13 +913,19 @@ def _extract_ai_attitude_towards_player(self, country_id): return attitude_towards_player def _check_returned_number(self, item_name, value): - if not isinstance(value, (float, int)): - logger.warning(f"{self._basic_info.logger_str} {item_name}: Found unexpected type {type(value).__name__} with value {value}.") - if isinstance(value, list) and len(value) > 0 and isinstance(value[0], (float, int)): - value = value[0] - else: - value = 0.0 - return value + if not isinstance(value, (float, int)): + logger.warning( + f"{self._basic_info.logger_str} {item_name}: Found unexpected type {type(value).__name__} with value {value}." + ) + if ( + isinstance(value, list) + and len(value) > 0 + and isinstance(value[0], (float, int)) + ): + value = value[0] + else: + value = 0.0 + return value def _extract_country_economy( self, country_data: datamodel.CountryData, country_data_dict @@ -931,9 +943,22 @@ def _extract_country_economy( if not values: continue resources = {} - for resource in ["energy", "minerals", "alloys", "consumer_goods", "food", "unity", "influence", "physics_research", "society_research", "engineering_research",]: + for resource in [ + "energy", + "minerals", + "alloys", + "consumer_goods", + "food", + "unity", + "influence", + "physics_research", + "society_research", + "engineering_research", + ]: if resource in values: - resources[resource] = self._check_returned_number(item_name, values.get(resource)) + resources[resource] = self._check_returned_number( + item_name, values.get(resource) + ) else: resources[resource] = 0.0 @@ -946,7 +971,9 @@ def _extract_country_economy( country_data.net_influence += resources.get("influence") country_data.net_physics_research += resources.get("physics_research") country_data.net_society_research += resources.get("society_research") - country_data.net_engineering_research += resources.get("engineering_research") + country_data.net_engineering_research += resources.get( + "engineering_research" + ) if country.country_id_in_game in self._basic_info.other_players: continue @@ -997,9 +1024,9 @@ def data(self): return self._species_by_ingame_id, self._robot_species def extract_data_from_gamestate(self, dependencies): - for species_ingame_id, species_dict in self._gamestate_dict.get( - "species_db", {} - ).items(): + for species_ingame_id, species_dict in sorted( + self._gamestate_dict.get("species_db", {}).items() + ): species_model = self._get_or_add_species(species_ingame_id, species_dict) self._species_by_ingame_id[species_ingame_id] = species_model if species_dict.get("class") == "ROBOT": @@ -1138,6 +1165,7 @@ def _add_new_leader( - 360 * leader_dict.get("age", 0.0) + self._random_instance.randint(-15, 15) ) + subclass, leader_traits = self._get_leader_traits(leader_dict) leader = datamodel.Leader( country=country_model, leader_id_in_game=leader_id, @@ -1146,6 +1174,8 @@ def _add_new_leader( date_hired=date_hired, date_born=date_born, is_active=True, + subclass=subclass, + leader_traits=leader_traits, ) self._update_leader_attributes( leader=leader, leader_dict=leader_dict @@ -1167,9 +1197,7 @@ def get_leader_name(self, leader_dict): name_dict = leader_dict.get("name", {}) # Look for "first_name" then "full_names" (3.6+) first_name = dump_name( - name_dict.get("first_name", - name_dict.get("full_names", "Unknown Leader") - ) + name_dict.get("first_name", name_dict.get("full_names", "Unknown Leader")) ) last_name = dump_name(name_dict.get("second_name", "")) return first_name, last_name @@ -1180,11 +1208,11 @@ def _update_leader_attributes(self, leader: datamodel.Leader, leader_dict): else: leader_class = leader_dict.get("class", "unknown class") leader_gender = leader_dict.get("gender", "other") - leader_agenda = leader_dict.get("agenda", "unknown agenda") first_name, second_name = self.get_leader_name(leader_dict) level = leader_dict.get("level", -1) species_id = leader_dict.get("species", -1) leader_species = self._species_dict.get(species_id) + subclass, leader_traits = self._get_leader_traits(leader_dict) if leader_species is None: logger.warning( f"{self._basic_info.logger_str} Invalid species ID {species_id} for leader {leader_dict}" @@ -1193,30 +1221,91 @@ def _update_leader_attributes(self, leader: datamodel.Leader, leader_dict): leader.first_name != first_name or leader.second_name != second_name or leader.leader_class != leader_class + or leader.subclass != subclass or leader.gender != leader_gender - or leader.leader_agenda != leader_agenda or leader.species != leader_species + or leader.leader_traits != leader_traits ): + hist_event_kwargs = dict( + country=leader.country, + start_date_days=self._basic_info.date_in_days, + leader=leader, + event_is_known_to_player=leader.country.is_player, + ) if leader.last_level != level: self._session.add( datamodel.HistoricalEvent( + **hist_event_kwargs, event_type=datamodel.HistoricalEventType.level_up, - country=leader.country, - start_date_days=self._basic_info.date_in_days, - leader=leader, - event_is_known_to_player=leader.country.is_player, db_description=self._get_or_add_shared_description(str(level)), ) ) + if leader.leader_traits != leader_traits: + gained_traits, lost_traits = self._get_gained_lost_traits( + old_traits=leader.leader_traits, new_traits=leader_traits + ) + for event_type, trait in itertools.chain( + zip( + itertools.repeat(datamodel.HistoricalEventType.lost_trait), + lost_traits, + ), + zip( + itertools.repeat(datamodel.HistoricalEventType.gained_trait), + gained_traits, + ), + ): + self._session.add( + datamodel.HistoricalEvent( + **hist_event_kwargs, + event_type=event_type, + db_description=self._get_or_add_shared_description(trait), + ) + ) + leader.last_level = level leader.first_name = first_name leader.second_name = second_name leader.leader_class = leader_class + leader.subclass = subclass leader.gender = leader_gender - leader.leader_agenda = leader_agenda leader.species = leader_species + leader.leader_traits = leader_traits self._session.add(leader) + def _get_leader_traits(self, leader_dict) -> (str, str): + leader_traits = leader_dict.get("traits", []) + if not isinstance(leader_traits, list): + leader_traits = [leader_traits] + subclass = "" + for trait in leader_traits: + if trait.startswith("subclass"): + subclass = trait + break + + traits = "|".join( + t for t in sorted(leader_traits) if not t.startswith("subclass") + ) + return subclass, traits + + def _get_gained_lost_traits(self, old_traits: str, new_traits: str): + old_traits = set(old_traits.split("|")) + new_traits = set(new_traits.split("|")) + lost_traits = old_traits - new_traits + gained_traits = new_traits - old_traits + + def strip_level(t: str) -> str: + return t.rstrip("_0123456789") + + # Only consider a trait "lost" if it is not replaced by a direct upgrade, e.g. + # trait_ruler_charismatic -> trait_ruler_charismatic_2 is not considered a lost trait + lost_traits = { + t + for t in lost_traits + if all(strip_level(nt) != strip_level(t) for nt in new_traits) + } + + return gained_traits, lost_traits + class PlanetProcessor(AbstractGamestateDataProcessor): ID = "planet_models" @@ -1238,7 +1327,9 @@ def extract_data_from_gamestate(self, dependencies: Dict[str, Any]): } systems_by_id = dependencies[SystemProcessor.ID]["systems_by_ingame_id"] - for system_id, system_dict in self._gamestate_dict["galactic_object"].items(): + for system_id, system_dict in sorted( + self._gamestate_dict["galactic_object"].items() + ): planets = system_dict.get("planet", []) if isinstance(planets, int): planets = [planets] @@ -1460,9 +1551,10 @@ def extract_data_from_gamestate(self, dependencies): for country_id, country_model in self._countries_dict.items(): country_dict = self._gamestate_dict["country"][country_id] - # some countries have an empty object for "sectors", which gets parsed as an empty list - # so we need to wrap that in dict() - country_sectors = dict(country_dict.get("sectors", {})).get("owned", []) + country_sector_dict = country_dict.get("sectors") + if not isinstance(country_sector_dict, dict): + continue + country_sectors = country_sector_dict.get("owned", []) unprocessed_systems = set( s.system_id_in_game for s in self._systems_by_owner.get(country_id, []) ) @@ -1904,7 +1996,11 @@ def _extract_edict_events( if not isinstance(edict, dict): continue expiry_date = edict.get("date") - if not expiry_date or expiry_date == "1.01.01" or "perpetual" == "yes": + if ( + not expiry_date + or expiry_date == "1.01.01" + or edict.get("perpetual") == "yes" + ): expiry_date = None else: expiry_date = datamodel.date_to_days(expiry_date) @@ -1935,6 +2031,183 @@ def _extract_edict_events( ) +class CouncilProcessor(AbstractGamestateDataProcessor): + ID = "council" + DEPENDENCIES = [RulerEventProcessor.ID, LeaderProcessor.ID, CountryProcessor.ID] + + def __init__(self): + super().__init__() + self.planets_by_ingame_id = None + + def extract_data_from_gamestate(self, dependencies: Dict[str, Any]): + countries_by_id = dependencies[CountryProcessor.ID] + leaders_by_id = dependencies[LeaderProcessor.ID] + rulers_by_id = dependencies[RulerEventProcessor.ID] + + self._update_council_positions(countries_by_id, leaders_by_id) + self._update_council_agenda(countries_by_id, rulers_by_id) + + def _update_council_positions(self, countries_by_id, leaders_by_id): + for cp_id, council_position in sorted( + self._gamestate_dict["council_positions"]["council_positions"].items() + ): + if not isinstance(council_position, dict): + continue + country_model = countries_by_id.get(council_position.get("country")) + leader_model = leaders_by_id.get(council_position.get("leader")) + councilor_type = council_position.get("type", "unknown councilor type") + if not all([country_model, leader_model, councilor_type]): + logger.debug(f"No councilor assigned: %s", council_position) + continue + desc = self._get_or_add_shared_description(councilor_type) + + previous_event = ( + self._session.query(datamodel.HistoricalEvent) + .filter_by( + event_type=datamodel.HistoricalEventType.councilor, + country=country_model, + db_description=desc, + ) + .order_by(datamodel.HistoricalEvent.start_date_days.desc()) + .first() + ) + add_new_event = True + if previous_event is not None: + previous_event.event_is_known_to_player = country_model.has_met_player() + if previous_event.leader_id != leader_model.leader_id: + previous_event.end_date_days = self._basic_info.date_in_days - 1 + else: + add_new_event = False + self._session.add(previous_event) + + if add_new_event: + self._session.add( + datamodel.HistoricalEvent( + event_type=datamodel.HistoricalEventType.councilor, + country=country_model, + leader=leader_model, + db_description=desc, + start_date_days=self._basic_info.date_in_days, + event_is_known_to_player=country_model.has_met_player(), + ) + ) + + def _update_council_agenda(self, countries_by_id, rulers_by_id): + for country_id, country_model in countries_by_id.items(): + gov_dict = self._gamestate_dict["country"][country_id].get("government") + if not isinstance(gov_dict, dict): + continue + ruler = rulers_by_id.get(country_id) + + # 'agenda_finding_the_voice' + in_progress_agenda = gov_dict.get("council_agenda") + + # [{'council_agenda': 'agenda_gestalt_boost_scientist', 'start_date': '2349.01.01'}] + agenda_cooldowns = { + a.get("council_agenda"): a + for a in gov_dict.get("council_agenda_cooldowns", []) + if isinstance(a, dict) and "council_agenda" in a + } + + unresolved_db_agenda = ( + self._session.query(datamodel.CouncilAgenda) + .filter_by(country=country_model, is_resolved=False) + .one_or_none() + ) + dbagenda = ( + unresolved_db_agenda + if unresolved_db_agenda is None + else unresolved_db_agenda.db_name.text + ) + last_prep_event: Optional[datamodel.HistoricalEvent] = ( + self._session.query(datamodel.HistoricalEvent) + .filter_by( + country=country_model, + event_type=datamodel.HistoricalEventType.agenda_preparation, + ) + .order_by(datamodel.HistoricalEvent.start_date_days.desc()) + .first() + ) + if unresolved_db_agenda is None: + if in_progress_agenda is not None: + self._session.add( + datamodel.CouncilAgenda( + country=country_model, + start_date=self._basic_info.date_in_days, + is_resolved=False, + db_name=self._get_or_add_shared_description( + in_progress_agenda + ), + ) + ) + self._session.add( + datamodel.HistoricalEvent( + event_type=datamodel.HistoricalEventType.agenda_preparation, + country=country_model, + leader=ruler, + db_description=self._get_or_add_shared_description( + in_progress_agenda + ), + start_date_days=self._basic_info.date_in_days, + event_is_known_to_player=country_model.has_met_player(), + ) + ) + else: + a_key = unresolved_db_agenda.db_name.text + if a_key in agenda_cooldowns: + cooldown_date = datamodel.date_to_days( + agenda_cooldowns[a_key]["start_date"] + ) + unresolved_db_agenda.cooldown_date = cooldown_date + unresolved_db_agenda.launch_date = self._basic_info.date_in_days + unresolved_db_agenda.is_resolved = True + self._session.add(unresolved_db_agenda) + + if last_prep_event is not None: + last_prep_event.end_date_days = self._basic_info.date_in_days + self._session.add(last_prep_event) + self._session.add( + datamodel.HistoricalEvent( + event_type=datamodel.HistoricalEventType.agenda_launch, + country=country_model, + leader=ruler, + db_description=self._get_or_add_shared_description(a_key), + start_date_days=self._basic_info.date_in_days, + event_is_known_to_player=country_model.has_met_player(), + ) + ) + elif a_key != in_progress_agenda: + # agenda changed without launch + unresolved_db_agenda.is_resolved = True + self._session.add(unresolved_db_agenda) + if last_prep_event is not None: + last_prep_event.end_date_days = self._basic_info.date_in_days + self._session.add(last_prep_event) + if in_progress_agenda is not None: + self._session.add( + datamodel.CouncilAgenda( + country=country_model, + start_date=self._basic_info.date_in_days, + is_resolved=False, + db_name=self._get_or_add_shared_description( + in_progress_agenda + ), + ) + ) + self._session.add( + datamodel.HistoricalEvent( + event_type=datamodel.HistoricalEventType.agenda_preparation, + country=country_model, + leader=ruler, + db_description=self._get_or_add_shared_description( + in_progress_agenda + ), + start_date_days=self._basic_info.date_in_days, + event_is_known_to_player=country_model.has_met_player(), + ) + ) + + class GovernmentProcessor(AbstractGamestateDataProcessor): ID = "government" DEPENDENCIES = [CountryProcessor.ID, RulerEventProcessor.ID] @@ -2028,6 +2301,97 @@ def extract_data_from_gamestate(self, dependencies): ) +class PolicyProcessor(AbstractGamestateDataProcessor): + ID = "policy" + DEPENDENCIES = [CountryProcessor.ID, RulerEventProcessor.ID] + + def extract_data_from_gamestate(self, dependencies): + countries_dict = dependencies[CountryProcessor.ID] + rulers_dict = dependencies[RulerEventProcessor.ID] + + for country_id, country_model in countries_dict.items(): + current_stance_per_policy = self._get_current_policies(country_id) + + previous_policy_by_name = self._load_previous_policies(country_model) + + for policy_name, ( + current_selected, + date, + ) in current_stance_per_policy.items(): + policy_date_days = ( + datamodel.date_to_days(date) + if date + else self._basic_info.date_in_days + ) + add_new_policy = False + event_type = None + event_description = None + if policy_name not in previous_policy_by_name: + add_new_policy = True + event_type = datamodel.HistoricalEventType.new_policy + event_description = f"{policy_name}|{current_selected}" + else: + previous_policy = previous_policy_by_name[policy_name] + previous_selected = previous_policy.selected.text + if previous_selected != current_selected: + add_new_policy = True + previous_policy.is_active = False + self._session.add(previous_policy) + event_type = datamodel.HistoricalEventType.changed_policy + event_description = ( + f"{policy_name}|{previous_selected}|{current_selected}" + ) + if add_new_policy: + self._session.add( + datamodel.Policy( + country_model=country_model, + policy_date=policy_date_days, + is_active=True, + policy_name=self._get_or_add_shared_description( + policy_name + ), + selected=self._get_or_add_shared_description( + current_selected + ), + ) + ) + if event_type and event_description: + self._session.add( + datamodel.HistoricalEvent( + event_type=event_type, + country=country_model, + leader=rulers_dict.get(country_id), + start_date_days=policy_date_days, + db_description=self._get_or_add_shared_description( + event_description + ), + ) + ) + + def _get_current_policies(self, country_id) -> dict[str, (str, str)]: + country_gs_dict = self._gamestate_dict["country"][country_id] + current_policies = country_gs_dict.get("active_policies") + if not isinstance(current_policies, list): + current_policies = [] + current_stance_per_policy = { + p.get("policy"): (p.get("selected"), p.get("date")) + for p in current_policies + } + return current_stance_per_policy + + def _load_previous_policies(self, country_model): + previous_policy_by_name: dict[str, datamodel.Policy] = { + p.policy_name.text: p + for p in self._session.query(datamodel.Policy) + .filter_by( + country_model=country_model, + is_active=True, + ) + .all() + } + return previous_policy_by_name + + class FactionProcessor(AbstractGamestateDataProcessor): ID = "faction" DEPENDENCIES = [CountryProcessor.ID, LeaderProcessor.ID] @@ -2071,9 +2435,9 @@ def extract_data_from_gamestate(self, dependencies): countries_dict = dependencies[CountryProcessor.ID] self._leaders_dict = dependencies[LeaderProcessor.ID] - for faction_id, faction_dict in self._gamestate_dict.get( - "pop_factions", {} - ).items(): + for faction_id, faction_dict in sorted( + self._gamestate_dict.get("pop_factions", {}).items() + ): if not faction_dict or not isinstance(faction_dict, dict): continue country_model = countries_dict.get(faction_dict.get("country")) @@ -2572,10 +2936,6 @@ def _history_add_tech_events(self, country_model: datamodel.Country, country_dic if not isinstance(tech_status_dict, dict): return for tech_type in ["physics", "society", "engineering"]: - scientist_id = tech_status_dict.get("leaders", {}).get(tech_type) - scientist = self._leader_dict.get(scientist_id) - self.history_add_research_leader_events(country_model, scientist, tech_type) - progress_dict = tech_status_dict.get(f"{tech_type}_queue") if progress_dict and isinstance(progress_dict, list): progress_dict = progress_dict[0] @@ -2616,7 +2976,6 @@ def _history_add_tech_events(self, country_model: datamodel.Country, country_dic datamodel.HistoricalEvent( event_type=datamodel.HistoricalEventType.researched_technology, country=country_model, - leader=scientist, start_date_days=start_date, db_description=matching_description, event_is_known_to_player=country_model.has_met_player(), @@ -2652,51 +3011,6 @@ def _get_matching_historical_event(self, country_model, tech_name): ) return matching_event - def history_add_research_leader_events( - self, - country_model: datamodel.Country, - current_research_leader: datamodel.Leader, - tech_type: str, - ): - previous_research_leader = country_model.get_research_leader(tech_type) - if current_research_leader == previous_research_leader: - return - - country_model.set_research_leader(tech_type, current_research_leader) - self._session.add(country_model) - - if previous_research_leader is not None: - matching_event = ( - self._session.query(datamodel.HistoricalEvent) - .filter_by( - event_type=datamodel.HistoricalEventType.research_leader, - country=country_model, - leader=previous_research_leader, - ) - .order_by(datamodel.HistoricalEvent.start_date_days.desc()) - .first() - ) - matching_event.end_date_days = self._basic_info.date_in_days - 1 - matching_event.event_is_known_to_player = ( - matching_event.event_is_known_to_player - or country_model.has_met_player() - ) - self._session.add(matching_event) - if current_research_leader is not None: - description = self._get_or_add_shared_description( - text=tech_type.capitalize() - ) - self._session.add( - datamodel.HistoricalEvent( - event_type=datamodel.HistoricalEventType.research_leader, - country=country_model, - leader=current_research_leader, - start_date_days=self._basic_info.date_in_days, - db_description=description, - event_is_known_to_player=country_model.has_met_player(), - ) - ) - class EnvoyEventProcessor(AbstractGamestateDataProcessor): ID = "envoy_events" @@ -2706,7 +3020,9 @@ def extract_data_from_gamestate(self, dependencies): countries_dict = dependencies[CountryProcessor.ID] leaders = dependencies[LeaderProcessor.ID] - for envoy_id_ingame, raw_leader_dict in self._gamestate_dict["leaders"].items(): + for envoy_id_ingame, raw_leader_dict in sorted( + self._gamestate_dict["leaders"].items() + ): if not isinstance(raw_leader_dict, dict): continue if raw_leader_dict.get("class") != "envoy": @@ -2717,7 +3033,7 @@ def extract_data_from_gamestate(self, dependencies): country = envoy.country target_country = None location = raw_leader_dict.get("location", {}) - assignment = location.get("assignment") + assignment = location.get("assignment", "idle") description = None if assignment == "improve_relations": event_type = datamodel.HistoricalEventType.envoy_improving_relations @@ -2740,7 +3056,7 @@ def extract_data_from_gamestate(self, dependencies): else: event_type = None - event_is_known = country.is_player or country.has_met_player() + event_is_known = country.has_met_player() if target_country is not None: event_is_known &= target_country.has_met_player() @@ -2760,6 +3076,8 @@ def extract_data_from_gamestate(self, dependencies): self._session.add(previous_assignment) if not assignment_is_the_same and event_type is not None: + # print(f"{assignment_is_the_same=} {event_type=} {country.country_id} " + # f"{previous_assignment.event_type} {previous_assignment.target_country_id}") new_assignment_event = datamodel.HistoricalEvent( start_date_days=self._basic_info.date_in_days, country=country, @@ -2771,7 +3089,9 @@ def extract_data_from_gamestate(self, dependencies): ) self._session.add(new_assignment_event) - def _previous_assignment(self, envoy: datamodel.Leader): + def _previous_assignment( + self, envoy: datamodel.Leader + ) -> Optional[datamodel.HistoricalEvent]: return ( self._session.query(datamodel.HistoricalEvent) .filter(datamodel.HistoricalEvent.end_date_days.is_(None)) @@ -2806,7 +3126,7 @@ def extract_data_from_gamestate(self, dependencies: Dict[str, Any]): self._country_datas = dependencies[CountryDataProcessor.ID] self._fleet_owners = dependencies[FleetOwnershipProcessor.ID] - for fleet_id, fleet_dict in self._gamestate_dict["fleet"].items(): + for fleet_id, fleet_dict in sorted(self._gamestate_dict["fleet"].items()): if not isinstance(fleet_dict, dict): continue country = self._fleet_owners.get(fleet_id) @@ -3524,7 +3844,7 @@ def init_dict(): def _initialize_planet_owner_dict(self): self.country_by_planet_id = {} - for country_id, country_dict in self._gamestate_dict["country"].items(): + for country_id, country_dict in sorted(self._gamestate_dict["country"].items()): if not isinstance(country_dict, dict): continue for planet_id in country_dict.get("owned_planets", []): diff --git a/test/localization_test_files/english/empire_formats_l_english.yml b/test/localization_test_files/english/empire_formats_l_english.yml index a5fc216..a128a75 100644 --- a/test/localization_test_files/english/empire_formats_l_english.yml +++ b/test/localization_test_files/english/empire_formats_l_english.yml @@ -1,3 +1,18 @@ l_english: format.gen_imp.1:0 " [This.GetSpeciesName] " NAME_Ketling_Multitude:1 "Ketling Star Pack" + adj_format:0 "adj $1$" + adj_NNi:0 "*ian $1$" + adj_NNr:0 "*ran $1$" + adj_NNa:0 "*an $1$" + adj_NNe:0 "*an $1$" + adj_NNus:0 "*an $1$" + adj_NNis:0 "*an $1$" + adj_NNes:0 "*an $1$" + adj_NNss:0 "*an $1$" + adj_NNid:0 "*an $1$" + adj_NNed:0 "*an $1$" + adj_NNad:0 "*an $1$" + adj_NNod:0 "*an $1$" + adj_NNud:0 "*an $1$" + adj_NNyd:0 "*an $1$" \ No newline at end of file diff --git a/test/names_test.py b/test/names_test.py index b46eba0..8b4c22b 100644 --- a/test/names_test.py +++ b/test/names_test.py @@ -9,7 +9,6 @@ @dataclasses.dataclass class NameTestcase: name_dict: dict - context: dict = dataclasses.field(default_factory=dict) expected: str = "" description: str = "test" @@ -26,6 +25,11 @@ class NameTestcase: expected="Commonwealth of Man", description="empire, built-in", ), + NameTestcase( + {"key": "civic_selective_kinship"}, + expected="Selective Kinship", + description="yml line without number", + ), NameTestcase( dict( key="EMPIRE_DESIGN_humans2", @@ -118,6 +122,89 @@ class NameTestcase: expected="Qiramulan Trading Consortium Rit-Kwyr", description="fleet, in-game science fleet", ), + NameTestcase( + { + "key": "%LEADER_2%", + "variables": [ + {"key": "1", "value": {"key": "MAM4_CHR_Tibb"}}, + {"key": "2", "value": {"key": "MAM4_CHR_daughterofNagg"}}, + ], + }, + expected="Tibb, daughter of Nagg", + description="LEADER_2", + ), + NameTestcase( + { + "key": "%SEQ%", + "variables": [ + {"key": "fmt", "value": {"key": "MAM4_STARORDER"}}, + {"key": "num", "value": {"key": "73"}}, + ], + }, + expected="73rd Star Order", + description="SEQ", + ), + NameTestcase( + { + "key": "%ADJ%", + "variables": [ + { + "key": "1", + "value": { + "key": "Democratic", + "variables": [ + { + "key": "1", + "value": { + "key": "%ADJECTIVE%", + "variables": [ + { + "key": "adjective", + "value": {"key": "SPEC_Cevanti"}, + }, + {"key": "1", "value": {"key": "Assembly"}}, + ], + }, + } + ], + }, + } + ], + }, + expected="Democratic Cevantian Assembly", + description="nested_country_species_adjective", + ), + NameTestcase( + { + "key": "PREFIX_NAME_FORMAT", + "variables": [ + {"key": "NAME", "value": {"key": "PLANT2_SHIP_Perennia"}}, + { + "key": "PREFIX", + "value": { + "key": "%ACRONYM%", + "variables": [ + { + "key": "base", + "value": { + "key": "%ADJECTIVE%", + "variables": [ + { + "key": "adjective", + "value": {"key": "SPEC_Panaxala"}, + }, + {"key": "1", "value": {"key": "Theocracy"}}, + ], + }, + } + ], + }, + }, + ], + }, + expected="PTY Perennia", + description="ACRONYM test", + ), ], ids=lambda tc: tc.description, ) @@ -127,6 +214,8 @@ def test_name_rendering_with_game_files(test_case: NameTestcase): assert renderer.render_from_dict(test_case.name_dict) == test_case.expected + + @pytest.mark.parametrize( "test_case", [ @@ -215,13 +304,28 @@ def test_name_rendering_with_game_files(test_case: NameTestcase): dict( key="%ADJ%", variables=[ - {"key": "1", "value": {"key": "Allied", "variables": [ - {"key": "1", "value": {"key": "%ADJECTIVE%", "variables": [ - {"key": "adjective", "value": {"key": "Jing"}}, - {"key": "1", "value": {"key": "Systems"}} - ]}} - ]}} - ] + { + "key": "1", + "value": { + "key": "Allied", + "variables": [ + { + "key": "1", + "value": { + "key": "%ADJECTIVE%", + "variables": [ + { + "key": "adjective", + "value": {"key": "Jing"}, + }, + {"key": "1", "value": {"key": "Systems"}}, + ], + }, + } + ], + }, + } + ], ), expected="Allied Jing Systems", description="%ADJ% test", @@ -231,13 +335,24 @@ def test_name_rendering_with_game_files(test_case: NameTestcase): key="%ADJECTIVE%", variables=[ {"key": "adjective", "value": {"key": "Quetzan"}}, - {"key": "1", "value": {"key": "%ADJ%", "variables": [ - {"key": "1", "value": {"key": "Consolidated", "variables": [ - {"key": "1", "value": {"key": "Worlds"}} - ]}} - ]}} - ] - + { + "key": "1", + "value": { + "key": "%ADJ%", + "variables": [ + { + "key": "1", + "value": { + "key": "Consolidated", + "variables": [ + {"key": "1", "value": {"key": "Worlds"}} + ], + }, + } + ], + }, + }, + ], ), expected="Quetzan Consolidated Worlds", description="Nested %ADJECTIVE% and %ADJ% test",