mirror of
https://github.com/MarioSpore/Grinch-AP.git
synced 2025-10-21 12:11:33 -06:00

* move to playercontainer * moves patch_file_ending handling to APPlayerContainer and updates the worlds using it to define their extensions * give oot a patch_file_ending as well
225 lines
9.6 KiB
Python
225 lines
9.6 KiB
Python
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"""<?xml version="1.0" encoding="utf-8"?>
|
|
<GameInfo>
|
|
<Types>
|
|
<Row Type="TECH_BLOCKER" Kind="KIND_TECH" />
|
|
<Row Type="CIVIC_BLOCKER" Kind="KIND_CIVIC" />
|
|
{"".join([f'{tab}<Row Type="{tech.name}" Kind="KIND_TECH" />{nl}' for
|
|
tech in techs])}
|
|
{"".join([f'{tab}<Row Type="{civic.name}" Kind="KIND_CIVIC" />{nl}' for
|
|
civic in civics])}
|
|
</Types>
|
|
<Technologies>
|
|
<Row TechnologyType="TECH_BLOCKER" Name="TECH_BLOCKER" EraType="ERA_ANCIENT" UITreeRow="0" Cost="99999" AdvisorType="ADVISOR_GENERIC" Description="Archipelago Tech created to prevent players from researching their own tech. If you can read this, then congrats you have reached the end of your tree before beating the game!"/>
|
|
{"".join([f'{tab}<Row TechnologyType="{location.name}" '
|
|
f'Name="{get_formatted_player_name(world, location.item.player)} '
|
|
f'{sanitize_value(location.item.name)}" '
|
|
f'EraType="{world.location_table[location.name].era_type}" '
|
|
f'UITreeRow="{world.location_table[location.name].uiTreeRow}" '
|
|
f'Cost="{get_cost(world, world.location_table[location.name])}" '
|
|
f'Description="{location.name}" '
|
|
f'AdvisorType="{get_advisor_type(world, location)}"'
|
|
f'/>{nl}'
|
|
for location in techs if location.item])}
|
|
</Technologies>
|
|
<TechnologyPrereqs>
|
|
{"".join([f'{tab}<Row Technology="{location.name}" PrereqTech="TECH_BLOCKER" />{nl}' for location in boost_techs])}
|
|
</TechnologyPrereqs>
|
|
<Civics>
|
|
<Row CivicType="CIVIC_BLOCKER" Name="CIVIC_BLOCKER" EraType="ERA_ANCIENT" UITreeRow="0" Cost="99999" AdvisorType="ADVISOR_GENERIC" Description="Archipelago Civic created to prevent players from researching their own civics. If you can read this, then congrats you have reached the end of your tree before beating the game!"/>
|
|
{"".join([f'{tab}<Row CivicType="{location.name}" '
|
|
f'Name="{get_formatted_player_name(world, location.item.player)} '
|
|
f'{sanitize_value(location.item.name)}" '
|
|
f'EraType="{world.location_table[location.name].era_type}" '
|
|
f'UITreeRow="{world.location_table[location.name].uiTreeRow}" '
|
|
f'Cost="{get_cost(world, world.location_table[location.name])}" '
|
|
f'Description="{location.name}" '
|
|
f'AdvisorType="{get_advisor_type(world, location)}"'
|
|
f'/>{nl}'
|
|
for location in civics if location.item])}
|
|
</Civics>
|
|
<CivicPrereqs>
|
|
{"".join([f'{tab}<Row Civic="{location.name}" PrereqCivic="CIVIC_BLOCKER" />{nl}' for location in boost_civics])}
|
|
</CivicPrereqs>
|
|
|
|
<Civics_XP2>
|
|
{"".join([f'{tab}<Row CivicType="{location.name}" HiddenUntilPrereqComplete="true" RandomPrereqs="false"/>{nl}' for location in civics if world.options.hide_item_names])}
|
|
</Civics_XP2>
|
|
|
|
<Technologies_XP2>
|
|
{"".join([f'{tab}<Row TechnologyType="{location.name}" HiddenUntilPrereqComplete="true" RandomPrereqs="false"/>{nl}' for location in techs if world.options.hide_item_names])}
|
|
</Technologies_XP2>
|
|
|
|
</GameInfo>
|
|
"""
|
|
|
|
|
|
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"
|