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
{% for war in wars %}
- - {{ links[war] | safe }}
+ - {{ war.start_date }}{% if war.end_date %} - {{ war.end_date }}{% endif %}: {{ links[war] | safe }}
{% endfor %}
{% endif %}
@@ -47,6 +47,7 @@
Technologies
Edicts
+ Policies
Traditions
Colonizations
Army Combat
@@ -66,8 +67,7 @@
{% 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 @@
{% 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 @@
{% 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 @@
- {% 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 @@
{% 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 @@
+ {% 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 @@
+ {% 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 @@
{{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 @@
{{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",