from dataclasses import dataclass import os from typing import TYPE_CHECKING, Dict, List, Optional, cast import zipfile from BaseClasses import Location from worlds.Files import APPlayerContainer from .Enum import CivVICheckType from .Locations import CivVILocation, CivVILocationData if TYPE_CHECKING: from . import CivVIWorld # Python fstrings don't allow backslashes, so we use this workaround nl = "\n" tab = "\t" apo = "\'" @dataclass class CivTreeItem: name: str cost: int ui_tree_row: int class CivVIContainer(APPlayerContainer): """ Responsible for generating the dynamic mod files for the Civ VI multiworld """ game: Optional[str] = "Civilization VI" patch_file_ending = ".apcivvi" def __init__(self, patch_data: Dict[str, str], base_path: str = "", output_directory: str = "", player: Optional[int] = None, player_name: str = "", server: str = ""): self.patch_data = patch_data self.file_path = base_path container_path = os.path.join(output_directory, base_path + ".apcivvi") super().__init__(container_path, player, player_name, server) def write_contents(self, opened_zipfile: zipfile.ZipFile) -> None: for filename, yml in self.patch_data.items(): opened_zipfile.writestr(filename, yml) super().write_contents(opened_zipfile) def sanitize_value(value: str) -> str: """Removes values that can cause issues in XML""" return value.replace('"', "'").replace('&', 'and') def get_cost(world: 'CivVIWorld', location: CivVILocationData) -> int: """ Returns the cost of the item based on the game options """ # Research cost is between 50 and 150 where 100 equals the default cost multiplier = world.options.research_cost_multiplier / 100 return int(world.location_table[location.name].cost * multiplier) def get_formatted_player_name(world: 'CivVIWorld', player: int) -> str: """ Returns the name of the player in the world """ if player != world.player: return sanitize_value(f"{world.multiworld.player_name[player]}{apo}s") return "Your" def get_advisor_type(world: 'CivVIWorld', location: Location) -> str: if world.options.advisor_show_progression_items and location.item and location.item.advancement: return "ADVISOR_PROGRESSIVE" return "ADVISOR_GENERIC" def generate_new_items(world: 'CivVIWorld') -> str: """ Generates the XML for the new techs/civics as well as the blockers used to prevent players from researching their own items """ locations: List[CivVILocation] = cast(List[CivVILocation], world.multiworld.get_filled_locations(world.player)) techs = [location for location in locations if location.location_type == CivVICheckType.TECH] civics = [location for location in locations if location.location_type == CivVICheckType.CIVIC] boost_techs = [] boost_civics = [] if world.options.boostsanity: boost_techs = [location for location in locations if location.location_type == CivVICheckType.BOOST and location.name.split("_")[1] == "TECH"] boost_civics = [location for location in locations if location.location_type == CivVICheckType.BOOST and location.name.split("_")[1] == "CIVIC"] techs += boost_techs civics += boost_civics return f""" {"".join([f'{tab}{nl}' for tech in techs])} {"".join([f'{tab}{nl}' for civic in civics])} {"".join([f'{tab}{nl}' for location in techs if location.item])} {"".join([f'{tab}{nl}' for location in boost_techs])} {"".join([f'{tab}{nl}' for location in civics if location.item])} {"".join([f'{tab}{nl}' for location in boost_civics])} {"".join([f'{tab}{nl}' for location in civics if world.options.hide_item_names])} {"".join([f'{tab}{nl}' for location in techs if world.options.hide_item_names])} """ def generate_setup_file(world: 'CivVIWorld') -> str: """ Generates the Lua for the setup file. This sets initial variables and state that affect gameplay around Progressive Eras """ setup = "-- Setup" if world.options.progression_style == "eras_and_districts": setup += f""" -- Init Progressive Era Value if it hasn't been set already if Game.GetProperty("MaxAllowedEra") == nil then print("Setting MaxAllowedEra to 0") Game.SetProperty("MaxAllowedEra", 0) end """ if world.options.boostsanity: setup += f""" -- Init Boosts if Game.GetProperty("BoostsAsChecks") == nil then print("Setting Boosts As Checks to True") Game.SetProperty("BoostsAsChecks", true) end """ return setup def generate_goody_hut_sql(world: 'CivVIWorld') -> str: """ Generates the SQL for the goody huts or an empty string if they are disabled since the mod expects the file to be there """ if world.options.shuffle_goody_hut_rewards: return f""" UPDATE GoodyHutSubTypes SET Description = NULL WHERE GoodyHut NOT IN ('METEOR_GOODIES', 'GOODYHUT_SAILOR_WONDROUS', 'DUMMY_GOODY_BUILDIER') AND Weight > 0; INSERT INTO Modifiers (ModifierId, ModifierType, RunOnce, Permanent, SubjectRequirementSetId) SELECT ModifierID||'_AI', ModifierType, RunOnce, Permanent, 'PLAYER_IS_AI' FROM Modifiers WHERE EXISTS ( SELECT ModifierId FROM GoodyHutSubTypes WHERE Modifiers.ModifierId = GoodyHutSubTypes.ModifierId AND GoodyHutSubTypes.GoodyHut NOT IN ('METEOR_GOODIES', 'GOODYHUT_SAILOR_WONDROUS', 'DUMMY_GOODY_BUILDIER') AND GoodyHutSubTypes.Weight > 0); INSERT INTO ModifierArguments (ModifierId, Name, Type, Value) SELECT ModifierID||'_AI', Name, Type, Value FROM ModifierArguments WHERE EXISTS ( SELECT ModifierId FROM GoodyHutSubTypes WHERE ModifierArguments.ModifierId = GoodyHutSubTypes.ModifierId AND GoodyHutSubTypes.GoodyHut NOT IN ('METEOR_GOODIES', 'GOODYHUT_SAILOR_WONDROUS', 'DUMMY_GOODY_BUILDIER') AND GoodyHutSubTypes.Weight > 0); UPDATE GoodyHutSubTypes SET ModifierID = ModifierID||'_AI' WHERE GoodyHut NOT IN ('METEOR_GOODIES', 'GOODYHUT_SAILOR_WONDROUS', 'DUMMY_GOODY_BUILDIER') AND Weight > 0; """ return "-- Goody Huts are disabled, no changes needed" def generate_update_boosts_sql(world: 'CivVIWorld') -> str: """ Generates the SQL for existing boosts in boostsanity or an empty string if they are disabled since the mod expects the file to be there """ if world.options.boostsanity: return f""" UPDATE Boosts SET TechnologyType = 'BOOST_' || TechnologyType WHERE TechnologyType IS NOT NULL; UPDATE Boosts SET CivicType = 'BOOST_' || CivicType WHERE CivicType IS NOT NULL AND CivicType NOT IN ('CIVIC_CORPORATE_LIBERTARIANISM', 'CIVIC_DIGITAL_DEMOCRACY', 'CIVIC_SYNTHETIC_TECHNOCRACY', 'CIVIC_NEAR_FUTURE_GOVERNANCE'); """ return "-- Boostsanity is disabled, no changes needed"