2025-03-10 07:53:26 -06:00
from dataclasses import dataclass
import os
from typing import TYPE_CHECKING , Dict , List , Optional , cast
import zipfile
from BaseClasses import Location
2025-06-02 10:53:18 -05:00
from worlds . Files import APPlayerContainer
2025-03-10 07:53:26 -06:00
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
2025-06-02 10:53:18 -05:00
class CivVIContainer ( APPlayerContainer ) :
2025-03-10 07:53:26 -06:00
"""
Responsible for generating the dynamic mod files for the Civ VI multiworld
"""
game : Optional [ str ] = " Civilization VI "
2025-03-20 13:28:16 -05:00
patch_file_ending = " .apcivvi "
2025-03-10 07:53:26 -06:00
2025-06-02 10:53:18 -05:00
def __init__ ( self , patch_data : Dict [ str , str ] , base_path : str = " " , output_directory : str = " " ,
2025-03-10 07:53:26 -06:00
player : Optional [ int ] = None , player_name : str = " " , server : str = " " ) :
2025-06-02 10:53:18 -05:00
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 )
2025-03-10 07:53:26 -06:00
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 )
2025-04-01 10:09:59 -06:00
def sanitize_value ( value : str ) - > str :
""" Removes values that can cause issues in XML """
return value . replace ( ' " ' , " ' " ) . replace ( ' & ' , ' and ' )
2025-03-10 07:53:26 -06:00
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 :
2025-04-01 10:09:59 -06:00
return sanitize_value ( f " { world . multiworld . player_name [ player ] } { apo } s " )
2025-03-10 07:53:26 -06:00
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 ) } '
2025-04-01 10:09:59 -06:00
f ' { sanitize_value ( location . item . name ) } " '
2025-03-10 07:53:26 -06:00
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 ) } '
2025-04-01 10:09:59 -06:00
f ' { sanitize_value ( location . item . name ) } " '
2025-03-10 07:53:26 -06:00
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 "