diff --git a/README.md b/README.md index 79c76dc..9d044a4 100644 --- a/README.md +++ b/README.md @@ -114,11 +114,7 @@ If you installed Stellaris in the default location at (`C:/Program Files (x86)/S If you play the game in another language, you can change the "Stellaris language" and restart the dashboard. Note that this setting should be the language code used by the game (ie `l_braz_por`, `l_english`, `l_french`, `l_german`, `l_japanese`, `l_korean`, `l_polish`, `l_russian`, `l_simp_chinese`, `l_spanish`). -If you use mods that add new names, the dashboard *should* automatically find the required files. Note that if you restart change your mod list, any new modded names will not be loaded until you restart the dashboard. If modded names are not working at all, you might need to change your "Stellaris user data folder" setting and restart the dashboard. This should be the folder containing the `dlc_load.json` file as well as the `mod/` folder. - -## Colors - -Currently, all country colors shown in the dashboard are randomized and completely unrelated to the colors in-game. Unfortunately, it is not that easy to get the country color from the save files, but it could be possible to add this in a future release. +If you use mods that add new names, the dashboard *should* automatically find the required files. Note that if you change your mod list, any new modded names will not be loaded until you restart the dashboard. If modded names are not working at all, you might need to change your "Stellaris user data folder" setting and restart the dashboard. This should be the folder containing the `dlc_load.json` file as well as the `mod/` folder. ## How to improve performance @@ -133,7 +129,7 @@ If you find that the dashboard is too slow to browse, you can try some of these ### Multiplayer -Support for Multiplayer is **experimental**. The dashboard will avoid showing information about other player controlled empires, even if the "Show all empires" checkbox is ticked in the settings. To use the dashboard in multiplayer, you must first configure your multiplayer username in the dashboard settings menu. +Support for Multiplayer is **experimental**. The dashboard will avoid showing information about other player controlled empires, even if the "Show all empires" checkbox is ticked in the settings. To use the dashboard in multiplayer, you must first configure your multiplayer username in the dashboard settings menu. You can disable this behavior, uncheck the "Hide other players" setting. ### Observer Mode diff --git a/mod/stellaris_dashboard/descriptor.mod b/mod/stellaris_dashboard/descriptor.mod index beba90c..9a97bf6 100644 --- a/mod/stellaris_dashboard/descriptor.mod +++ b/mod/stellaris_dashboard/descriptor.mod @@ -5,5 +5,5 @@ tags={ "Gameplay" } picture="thumbnail.png" -supported_version="3.6.*" +supported_version="v3.12.*" remote_file_id="1466534202" \ No newline at end of file diff --git a/stellarisdashboard/cli.py b/stellarisdashboard/cli.py index 787c5f5..33141b1 100644 --- a/stellarisdashboard/cli.py +++ b/stellarisdashboard/cli.py @@ -64,7 +64,6 @@ def f_monitor_saves(save_path=None, stop_event: threading.Event = None): polling_interval = config.CONFIG.polling_interval save_reader = save_parser.ContinuousSavePathMonitor(save_path) save_reader.mark_all_existing_saves_processed() - tle = timeline.TimelineExtractor() show_wait_message = True while not stop_event.is_set(): @@ -80,6 +79,7 @@ def f_monitor_saves(save_path=None, stop_event: threading.Event = None): continue show_wait_message = True nothing_new = False + tle = timeline.TimelineExtractor() tle.process_gamestate(game_name, gamestate_dict) visualization_data.get_current_execution_plot_data(game_name) del gamestate_dict @@ -112,13 +112,13 @@ def f_parse_saves(threads=None, save_path=None, game_name_prefix="") -> None: save_path, game_name_prefix=game_name_prefix, ) - tle = timeline.TimelineExtractor() for ( game_name, gamestate_dict, ) in save_reader.get_gamestates_and_check_for_new_files(): if gamestate_dict is None: continue + tle = timeline.TimelineExtractor() tle.process_gamestate(game_name, gamestate_dict) del gamestate_dict diff --git a/stellarisdashboard/config.py b/stellarisdashboard/config.py index 172d377..2066d8f 100644 --- a/stellarisdashboard/config.py +++ b/stellarisdashboard/config.py @@ -40,7 +40,12 @@ def _get_default_stellaris_user_data_path(): # according to https://stellaris.paradoxwikis.com/Save-game_editing home = pathlib.Path.home() if platform.system() == "Windows": - return home / "Documents/Paradox Interactive/Stellaris/" + one_drive_path = home / "OneDrive/Documents/Paradox Interactive/Stellaris/" + non_one_drive_path = home / "OneDrive/Documents/Paradox Interactive/Stellaris/" + if one_drive_path.exists(): + return one_drive_path + else: + return non_one_drive_path elif platform.system() == "Linux": return home / ".local/share/Paradox Interactive/Stellaris/" else: @@ -177,6 +182,7 @@ def _get_default_base_output_path(): stellaris_user_data_path=_get_default_stellaris_user_data_path(), stellaris_language="l_english", mp_username="", + hide_other_players=True, base_output_path=_get_default_base_output_path(), threads=1, host="127.0.0.1", @@ -225,6 +231,7 @@ class Config: check_version: bool = None filter_events_by_type: bool = None show_everything: bool = None + hide_other_players: bool = None read_all_countries: bool = None show_all_country_types: bool = None include_id_in_names: bool = None @@ -242,16 +249,22 @@ class Config: production: bool = False - PATH_KEYS = { - "base_output_path", + EXISTING_PATH_KEYS = { + # these paths should already exist + # if not, the setting is reset to default + # this helps with issues related to OneDrive "save_file_path", "stellaris_install_path", "stellaris_user_data_path", } + PATH_KEYS = EXISTING_PATH_KEYS | { + "base_output_path", + } BOOL_KEYS = { "check_version", "filter_events_by_type", "show_everything", + "hide_other_players", "read_all_countries", "show_all_country_types", "log_to_file", @@ -311,6 +324,8 @@ def _process_path_keys(self, key, val): val = DEFAULT_SETTINGS[key] else: val = pathlib.Path(val) + if key in Config.EXISTING_PATH_KEYS and not val.exists(): + val = DEFAULT_SETTINGS[key] if key == "base_output_path": try: if not val.exists(): @@ -385,6 +400,7 @@ def __str__(self): f" base_output_path: {repr(self.base_output_path)}", f" threads: {repr(self.threads)}", f" show_everything: {repr(self.show_everything)}", + f" hide_other_players: {repr(self.hide_other_players)}", f" show_all_country_types: {repr(self.show_all_country_types)}", ] return "\n".join(lines) diff --git a/stellarisdashboard/dashboard_app/assets/style.css b/stellarisdashboard/dashboard_app/assets/style.css index f3c61f2..1c4a33f 100644 --- a/stellarisdashboard/dashboard_app/assets/style.css +++ b/stellarisdashboard/dashboard_app/assets/style.css @@ -39,10 +39,11 @@ input { font-size: 20px; } -a.button, input.button { +a.button, input.button, button.button { -webkit-appearance: button; -moz-appearance: button; appearance: button; + cursor: pointer; color: rgba(195, 133, 33, 1); font-family: verdana; @@ -83,19 +84,41 @@ li.toc-item { } form.settingsform { - padding: 0.25cm; + padding: 1rem } div.settings { width: 80%; + max-width: 64rem; + color: rgba(217, 217, 217, 1); + margin: 1rem auto 0 auto; +} + +.settingsbuttons { + margin-block-start: 1rem; + margin-inline-start: -1rem; + position: sticky; + bottom: 0; +} + +.settings label, +.settings dt { color: rgba(195, 133, 33, 1); - padding: 0.25cm; - margin: 0.25cm auto; } +.settings dl { + display: flex; + gap: 0.25rem; +} + +.settinginfo { + cursor: help; +} + + div.setting { padding: 0.1cm; - margin: 0.1cm; + margin: 0.1cm 0; align-self: flex-start; @@ -112,7 +135,12 @@ span.settingname { span.settinginput { align-self: flex-end; - flex: 1 0 20%; + flex: 1 0 40%; +} + +span.settinginput input:not([type=checkbox]) { + box-sizing: border-box; + width: 100%; } div.objectdetails { diff --git a/stellarisdashboard/dashboard_app/graph_ledger.py b/stellarisdashboard/dashboard_app/graph_ledger.py index 6bbb406..7dd4244 100644 --- a/stellarisdashboard/dashboard_app/graph_ledger.py +++ b/stellarisdashboard/dashboard_app/graph_ledger.py @@ -152,8 +152,7 @@ def update_country_select_options(search): for c in session.query(datamodel.Country): if ( c.is_real_country() - and (c.has_met_player() or config.CONFIG.show_everything) - and not c.is_other_player + and not c.is_hidden_country() ): options.append( {"label": c.rendered_name, "value": c.country_id_in_game} diff --git a/stellarisdashboard/dashboard_app/history_ledger.py b/stellarisdashboard/dashboard_app/history_ledger.py index cb0adb2..17c1b91 100644 --- a/stellarisdashboard/dashboard_app/history_ledger.py +++ b/stellarisdashboard/dashboard_app/history_ledger.py @@ -273,7 +273,7 @@ def collect_event_dicts(self, event_list, key_object): continue if ( event.country - and any(c.is_other_player for c in event.involved_countries()) + and any(c.is_hidden_country() for c in event.involved_countries()) and not event.event_is_known_to_player ): continue @@ -446,13 +446,7 @@ def system_details(self, system_model: datamodel.System) -> Dict[str, str]: if bypasses: details["Unknown Bypasses"] = ", ".join(bypasses) - if system_model.country is not None and ( - system_model.country.has_met_player() - or ( - config.CONFIG.show_everything - and not system_model.country.is_other_player - ) - ): + if system_model.country is not None and not system_model.country.is_hidden_country(): details["Owner"] = self._get_url_for(system_model.country) details["Planets"] = ", ".join( @@ -515,8 +509,7 @@ def country_details(self, country_model: datamodel.Country) -> Dict[str, str]: relations_to_display = [ self._get_url_for(c) for c in sorted(countries, key=lambda c: c.country_name) - if c.has_met_player() - or (config.CONFIG.show_everything and not c.is_other_player) + if not c.is_hidden_country() ] if relations_to_display: details[relation_type] = ", ".join(relations_to_display) diff --git a/stellarisdashboard/dashboard_app/settings.py b/stellarisdashboard/dashboard_app/settings.py index 9bb9d04..2b63c94 100644 --- a/stellarisdashboard/dashboard_app/settings.py +++ b/stellarisdashboard/dashboard_app/settings.py @@ -20,110 +20,124 @@ def _bool_to_lowercase(py_bool: bool) -> str: current_values = config.CONFIG.get_adjustable_settings_dict() settings = { - "check_version": { - "type": t_bool, - "value": _bool_to_lowercase(current_values["check_version"]), - "name": "Check for new versions (only when subscribed in the Steam Workshop)", - "description": "Check for new versions (only when subscribed in the Steam Workshop)", - }, - "save_file_path": { - "type": t_str, - "value": current_values["save_file_path"], - "name": "Save file path (applies after restart, set to empty to restore default, applies after restart)", - "description": "Where the dashboard will look for new Stellaris save files.", - }, - "stellaris_user_data_path": { - "type": t_str, - "value": current_values["stellaris_user_data_path"], - "name": "Stellaris user data folder", - "description": "Where the dashboard will look for user data such as enabled mods. It should contain the dlc_load.json file.", - }, - "stellaris_install_path": { - "type": t_str, - "value": current_values["stellaris_install_path"], - "name": "Stellaris install folder", - "description": "Where the dashboard will look for Stellaris game data.", - }, - "stellaris_language": { - "type": t_str, - "value": current_values["stellaris_language"], - "name": "Stellaris language", - "description": "The language to use for localizing Stellaris text. In the format l_ (for example, l_english)" - }, - "mp_username": { - "type": t_str, - "value": current_values["mp_username"], - "name": "Your Multiplayer username", - "description": "", - }, - "threads": { - "type": t_int, - "value": current_values["threads"], - "min": 1, - "name": "Number of threads (applies after restart)", - "description": "Number of threads for reading save files, updates apply after restarting the dashboard.", - }, - "show_everything": { - "type": t_bool, - "value": _bool_to_lowercase(current_values["show_everything"]), - "name": "Show information for all countries", - "description": "Show information for all countries, including unknown ones.", - }, - "show_all_country_types": { - "type": t_bool, - "value": _bool_to_lowercase(current_values["show_all_country_types"]), - "name": "Show all country types", - "description": "Check to include special countries like enclaves, leviathans or the shroud.", - }, - "filter_events_by_type": { - "type": t_bool, - "value": _bool_to_lowercase(current_values["filter_events_by_type"]), - "name": "Filter history ledger by event type", - "description": "If enabled, only a selection of relevant historical events are shown in the event ledger, depending on the page.", - }, - "save_name_filter": { - "type": t_str, - "value": current_values["save_name_filter"], - "name": "Save file name filter", - "description": "Only save files whose file names contain this string are processed.", - }, - "read_all_countries": { - "type": t_bool, - "value": _bool_to_lowercase(current_values["read_all_countries"]), - "name": "Store data of all countries", - "description": "Store budgets and internal stats of all countries. This makes a larger database and could slow things down.", - }, - "skip_saves": { - "type": t_int, - "value": current_values["skip_saves"], - "min": 0, - "max": 100, - "name": "Skip saves after processing (increase if save processing can't keep up with the game)", - "description": "Number of save files skipped for each save file that is processed.", - }, - "plot_time_resolution": { - "type": t_int, - "value": current_values["plot_time_resolution"], - "min": 0, - "max": 10000, - "name": "Initial graph resolution (set to 0 to load the full data)", - "description": "Number of points used when first loading the graphs.", - }, - "plot_width": { - "type": t_int, - "value": current_values["plot_width"], - "min": 300, - "max": 2000, - "name": "Graph width (pixels)", - "description": "Width of graphs.", - }, - "plot_height": { - "type": t_int, - "value": current_values["plot_height"], - "min": 300, - "max": 2000, - "name": "Graph height (pixels)", - "description": "Height of graphs.", + "File Locations": { + "save_file_path": { + "type": t_str, + "value": current_values["save_file_path"], + "name": "Save files folder *", + "description": "Where the dashboard will look for new Stellaris save files. Should contain a folder for each game, and each of those folder should contain one or .sav files. Leave blank to reset to default.", + }, + "stellaris_user_data_path": { + "type": t_str, + "value": current_values["stellaris_user_data_path"], + "name": "Stellaris user data folder *", + "description": "Where the dashboard will look for user data such as enabled mods. It should contain the dlc_load.json file. Leave blank to reset to default.", + }, + "stellaris_install_path": { + "type": t_str, + "value": current_values["stellaris_install_path"], + "name": "Stellaris install folder *", + "description": "Where the dashboard will look for Stellaris game data. Leave blank to reset to default.", + }, + }, + "Data Visibility": { + "show_everything": { + "type": t_bool, + "value": _bool_to_lowercase(current_values["show_everything"]), + "name": "Show information for all countries", + "description": "Show information for all countries, including unknown ones.", + }, + "show_all_country_types": { + "type": t_bool, + "value": _bool_to_lowercase(current_values["show_all_country_types"]), + "name": "Show all country types", + "description": "Check to include special countries like enclaves, leviathans or the shroud.", + }, + "filter_events_by_type": { + "type": t_bool, + "value": _bool_to_lowercase(current_values["filter_events_by_type"]), + "name": "Filter history ledger by event type", + "description": "If enabled, only a selection of relevant historical events are shown in the event ledger, depending on the page.", + }, + "mp_username": { + "type": t_str, + "value": current_values["mp_username"], + "name": "Your multiplayer username", + "description": "By default, data for countries controlled by players with any other username are hidden (see below setting). This setting does not affect single-player games.", + }, + "hide_other_players": { + "type": t_bool, + "value": _bool_to_lowercase(current_values["hide_other_players"]), + "name": "Hide other players", + "description": "If enabled, hides information for countries controlled by players that do not match the above multiplayer Username", + }, + }, + "Performance": { + "read_all_countries": { + "type": t_bool, + "value": _bool_to_lowercase(current_values["read_all_countries"]), + "name": "Store data of all countries", + "description": "Store budgets and internal stats of all countries. This makes a larger database and could slow things down.", + }, + "save_name_filter": { + "type": t_str, + "value": current_values["save_name_filter"], + "name": "Save file name filter", + "description": "Only save files whose file names contain this string are processed. For example, you could use \".01.01\" so only saves from the start of the year are processed. Alternatively, use \"Skip saves after processing\" below", + }, + "skip_saves": { + "type": t_int, + "value": current_values["skip_saves"], + "min": 0, + "max": 100, + "name": "Skip saves after processing", + "description": "After processing a save, the next X saves are skipped. Use this if the dashboard cannot keep up with autosaves. Alternatively, use the \"Save name filter\" above.", + }, + "plot_time_resolution": { + "type": t_int, + "value": current_values["plot_time_resolution"], + "min": 0, + "max": 10000, + "name": "Initial graph resolution", + "description": "Number of points used when first loading the graphs. Set to 0 to load full data.", + }, + "threads": { + "type": t_int, + "value": current_values["threads"], + "min": 1, + "name": "Number of threads *", + "description": "Number of threads for reading save files.", + }, + }, + "Interface": { + "check_version": { + "type": t_bool, + "value": _bool_to_lowercase(current_values["check_version"]), + "name": "Check for new versions", + "description": "When using the Workshop mod for in-game access, a message will be displayed if a newer version of the separate dashboard app is available.", + }, + "stellaris_language": { + "type": t_str, + "value": current_values["stellaris_language"], + "name": "Stellaris language *", + "description": "The language to use for localizing Stellaris text. In the format l_ (for example, l_english)" + }, + "plot_width": { + "type": t_int, + "value": current_values["plot_width"], + "min": 300, + "max": 2000, + "name": "Graph width", + "description": "Width of graphs in pixels.", + }, + "plot_height": { + "type": t_int, + "value": current_values["plot_height"], + "min": 300, + "max": 2000, + "name": "Graph height", + "description": "Height of graphs in pixels.", + }, }, } return render_template("settings_page.html", current_settings=settings) diff --git a/stellarisdashboard/dashboard_app/templates/settings_page.html b/stellarisdashboard/dashboard_app/templates/settings_page.html index 3188728..7ca8a86 100644 --- a/stellarisdashboard/dashboard_app/templates/settings_page.html +++ b/stellarisdashboard/dashboard_app/templates/settings_page.html @@ -5,30 +5,40 @@

Settings

- {% for key, setting in current_settings.items() %} -
- {{ setting["name"] }}: - - {% if setting["type"] == "bool" %} -
- {% endif %} - {% if setting["type"] == "int" %} -
- {% endif %} - {% if setting["type"] == "str" %} -
- {% endif %} - {% if setting["type"] == "float" %} -
- {% endif %} -
-
+
+
*
+
applies after restart
+
+ {% for category, settings in current_settings.items() %} +

{{ category }}

+ {% for key, setting in settings.items() %} +
+ + + 🛈 + + + {% if setting["type"] == "bool" %} +
+ {% endif %} + {% if setting["type"] == "int" %} +
+ {% endif %} + {% if setting["type"] == "str" %} +
+ {% endif %} + {% if setting["type"] == "float" %} +
+ {% endif %} +
+
+ {% endfor %} {% endfor %}
- - + +
{% endblock %} diff --git a/stellarisdashboard/dashboard_app/visualization_data.py b/stellarisdashboard/dashboard_app/visualization_data.py index 730be85..5b673c9 100644 --- a/stellarisdashboard/dashboard_app/visualization_data.py +++ b/stellarisdashboard/dashboard_app/visualization_data.py @@ -325,7 +325,7 @@ def _get_value_from_countrydata( def _override_visibility(cd: datamodel.CountryData): - return not cd.country.is_other_player and config.CONFIG.show_everything + return not cd.country.is_hidden_country() and config.CONFIG.show_everything class PlanetCountDataContainer(AbstractPerCountryDataContainer): @@ -443,7 +443,7 @@ def extract_data_from_gamestate(self, gs: datamodel.GameState): def _get_player_countrydata(self, gs: datamodel.GameState) -> datamodel.CountryData: player_cd = None for cd in gs.country_data: - if cd.country.is_other_player: + if cd.country.is_hidden_country(): continue if ( self._country_perspective is None and cd.country.is_player diff --git a/stellarisdashboard/datamodel.py b/stellarisdashboard/datamodel.py index 612d551..59b984b 100644 --- a/stellarisdashboard/datamodel.py +++ b/stellarisdashboard/datamodel.py @@ -732,6 +732,13 @@ def is_real_country(self): or self.country_type == "fallen_empire" or self.country_type == "awakened_fallen_empire" ) + + def is_hidden_country(self): + if self.is_other_player and config.CONFIG.hide_other_players: + return True + if not self.has_met_player() and not config.CONFIG.show_everything: + return True + return False def diplo_relation_details(self): countries_by_relation = {} diff --git a/stellarisdashboard/parsing/timeline.py b/stellarisdashboard/parsing/timeline.py index 6bdf0e1..c19ae8f 100644 --- a/stellarisdashboard/parsing/timeline.py +++ b/stellarisdashboard/parsing/timeline.py @@ -2,6 +2,7 @@ import collections import dataclasses import datetime +from functools import cache import itertools import json import logging @@ -127,8 +128,10 @@ def _get_or_add_game_to_db(self, game_id: str): player_country_name = dump_name( self._gamestate_dict["country"][player_country_id]["name"] ) - else: + elif len(self._other_players) == 0: player_country_name = "Observer Mode" + else: + player_country_name = "Player Not Found" galaxy_info = self._gamestate_dict["galaxy"] game = datamodel.Game( game_name=game_id, @@ -162,10 +165,6 @@ def _identify_player_country(self): return players[0]["country"] else: playercountry = None - if not config.CONFIG.mp_username: - raise ValueError( - "Please configure your Multiplayer username in the Dashboard settings for multiplayer games." - ) for player in players: if player["name"] == config.CONFIG.mp_username: playercountry = player["country"] @@ -173,7 +172,7 @@ def _identify_player_country(self): self._other_players.add(player["country"]) if playercountry is None: logger.warn( - f"Could not find player matching Multiplayer username {config.CONFIG.mp_username}" + f"Could not find player matching Multiplayer username \"{config.CONFIG.mp_username}\"" ) return playercountry # observer mode @@ -247,6 +246,7 @@ def data(self) -> Any: def extract_data_from_gamestate(self, dependencies: Dict[str, Any]): pass + @cache def _get_or_add_shared_description(self, text: str) -> datamodel.SharedDescription: matching_description = ( self._session.query(datamodel.SharedDescription) @@ -981,8 +981,6 @@ def _extract_country_economy( "engineering_research" ) - if country.country_id_in_game in self._basic_info.other_players: - continue if country.is_player or config.CONFIG.read_all_countries: self._session.add( datamodel.BudgetItem( @@ -2383,7 +2381,8 @@ def _get_current_policies(self, country_id) -> dict[str, (str, str)]: current_policies = [] current_stance_per_policy = { p.get("policy"): (p.get("selected"), p.get("date")) - for p in current_policies + for p in current_policies if isinstance(p, dict) # ambiguous {} is parsed as empty list, not empty dict + } return current_stance_per_policy @@ -3639,8 +3638,6 @@ def init_dict(): for country_id_in_game, country_model in countries_dict.items(): if not config.CONFIG.read_all_countries and not country_model.is_player: continue - if country_id_in_game in self._basic_info.other_players: - continue country_data = country_data_dict[country_id_in_game] stats_by_species = {} stats_by_faction = {}