Created
December 6, 2025 14:38
-
-
Save haifengkao/e449cf8459248a2abd8a3ec4bc17dab9 to your computer and use it in GitHub Desktop.
Automatic Mod Patcher for VP 5.0
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env python3 | |
| """ | |
| Batch rename helper for VP 5.0 alpha changes. | |
| Defaults: | |
| - Scans every mod folder under the default Civ5 MODS directory. | |
| - Skips VP核心模組(但保留 Community Events),除非使用 --include-vp。 | |
| - Applies a set of string replacements to text files (xml, sql, lua, modinfo, civ5proj, txt). | |
| This only rewrites file contents; it does not rename files. | |
| """ | |
| from __future__ import annotations | |
| import argparse | |
| from pathlib import Path | |
| import re | |
| from typing import Iterable | |
| DEFAULT_ROOT = Path.cwd() | |
| # Mods to skip by default (VP core pieces). Case-insensitive match. | |
| DEFAULT_SKIP_NAMES = { | |
| "(1) community patch", | |
| "(2) vox populi", | |
| "(3a) vp - eui compatibility files", | |
| "(4a) squads for vp", | |
| } | |
| # Extensions we will process. | |
| DEFAULT_EXTS = {".xml", ".sql", ".lua"} | |
| # Replacement map: old -> new | |
| REPLACEMENTS = { | |
| # Font icons | |
| # "ICON_RES_MARRIAGE": "ICON_MARRIAGE", # not used | |
| "ICON_VP_SAPPED": "ICON_SAPPED", | |
| "ICON_VP_WRITER": "ICON_WRITER", | |
| "ICON_VP_ARTIST": "ICON_ARTIST", | |
| "ICON_VP_MUSICIAN": "ICON_MUSICIAN", | |
| "ICON_VP_SCIENTIST": "ICON_SCIENTIST", | |
| "ICON_VP_MERCHANT": "ICON_MERCHANT", | |
| "ICON_VP_ENGINEER": "ICON_ENGINEER", | |
| "ICON_CSD_CIVIL_SERVANT": "ICON_CIVIL_SERVANT", | |
| "ICON_VP_AUTOMATION": "ICON_AUTOMATON", | |
| "ICON_VP_BORDER_OBSTRUCTION": "ICON_BORDER_OBSTRUCTION", | |
| "ICON_VP_NOINFLUENCE": "ICON_NOINFLUENCE", | |
| "ICON_VP_GREATWRITING": "ICON_GW_WRITING", | |
| "ICON_VP_GREATART": "ICON_GW_ART", | |
| "ICON_VP_GREATMUSIC": "ICON_GW_MUSIC", | |
| "ICON_VP_ARTIFACT": "ICON_ARTIFACT", | |
| "ICON_VP_SPY_POINTS": "ICON_SPY_POINT", | |
| "ICON_VP_VISION": "ICON_VISION", | |
| "ICON_VP_MONOPOLY": "ICON_MONOPOLY", | |
| "ICON_VP_OFFICE": "ICON_OFFICE", | |
| "ICON_VP_FRANCHISE": "ICON_FRANCHISE", | |
| "ICON_ITP_HAPPINESS_CONTENT": "ICON_HAPPINESS_CONTENT", | |
| "ICON_ITP_HAPPINESS_NEUTRAL": "ICON_HAPPINESS_NEUTRAL", | |
| "ICON_ITP_PANTHEON": "ICON_PANTHEON_TP", | |
| "ICON_ITP_RELIGION": "ICON_PROPHET_TP", | |
| "ICON_ITP_RELIGIOUS_BUILDING": "ICON_RELIGIOUS_BUILDING", | |
| "ICON_ITP_CITIZEN_CONTENT": "ICON_CITIZEN_CONTENT", | |
| "ICON_ITP_CITIZEN_NEUTRAL": "ICON_CITIZEN_NEUTRAL", | |
| "ICON_ITP_CITIZEN_UNHAPPY": "ICON_CITIZEN_UNHAPPY", | |
| "ICON_ITP_CITIZEN_VERY_UNHAPPY": "ICON_CITIZEN_VERY_UNHAPPY", | |
| "ICON_ITP_RELIGIOUS_UNIT": "ICON_STRENGTH_TP", | |
| "ICON_ITP_RELIGIOUS_ENGINEER": "ICON_GREAT_ENGINEER_GRAY", | |
| "ICON_ITP_RELIGIOUS_GENERAL": "ICON_GREAT_GENERAL_GRAY", | |
| "ICON_ITP_RELIGIOUS_SCIENTIST": "ICON_GREAT_SCIENTIST_GRAY", | |
| "ICON_ITP_RELIGIOUS_MERCHANT": "ICON_GREAT_MERCHANT_GRAY", | |
| "ICON_ITP_RELIGIOUS_ARTIST": "ICON_GREAT_ARTIST_GRAY", | |
| "ICON_ITP_RELIGIOUS_MUSICIAN": "ICON_GREAT_MUSICIAN_GRAY", | |
| "ICON_ITP_RELIGIOUS_WRITER": "ICON_GREAT_WRITER_GRAY", | |
| "ICON_ITP_RELIGIOUS_ADMIRAL": "ICON_GREAT_ADMIRAL_GRAY", | |
| "ICON_ITP_RELIGIOUS_VENETIAN": "ICON_GREAT_MERCHANT_VENICE_GRAY", | |
| "ICON_ITP_RELIGIOUS_DIPLOMAT": "ICON_DIPLOMAT_GRAY", | |
| # Corporations | |
| "CORPORATION_LANDSEA_EXTRACTORS": "CORPORATION_CENTAURUS_EXTRACTORS", | |
| # Unit + art rename | |
| "UNITCLASS_VP_SLINGER": "UNITCLASS_SLINGER", | |
| "UNITCLASS_HORSE_ARCHER": "UNITCLASS_SKIRMISHER", | |
| "UNITCLASS_MOUNTED_BOWMAN": "UNITCLASS_HEAVY_SKIRMISHER", | |
| "UNITCLASS_MISSILE_DESTROYER": "UNITCLASS_SENSOR_COMBAT_SHIP", | |
| "UNITCLASS_FCOMPANY": "UNITCLASS_FREE_COMPANY", | |
| "UNITCLASS_GUERILLA": "UNITCLASS_MERCENARY", | |
| "UNITCLASS_PANZER": "UNITCLASS_T34", | |
| "UNITCLASS_ASSYRIAN_SIEGE_TOWER": "UNITCLASS_SIEGE_TOWER", | |
| "UNITCLASS_BRAZILIAN_PRACINHA": "UNITCLASS_PRACINHA", | |
| "UNITCLASS_SKI_INFANTRY": "UNITCLASS_NORWEGIAN_SKI_INFANTRY", | |
| "UNITCLASS_COMPANIONCAVALRY": "UNITCLASS_COMPANION_CAVALRY", | |
| "UNITCLASS_HUNNIC_BATTERING_RAM": "UNITCLASS_BATTERING_RAM", | |
| "UNIT_MOUNTED_BOWMAN": "UNIT_HEAVY_SKIRMISHER", | |
| "UNIT_MISSILE_DESTROYER": "UNIT_SENSOR_COMBAT_SHIP", | |
| "UNIT_FCOMPANY": "UNIT_FREE_COMPANY", | |
| "UNIT_GUERILLA": "UNIT_MERCENARY", | |
| "UNIT_BANDEIRANTES": "UNIT_BANDEIRANTE", | |
| "ART_DEF_UNIT_VP_SLINGER": "ART_DEF_UNIT_SLINGER", | |
| "ART_DEF_UNIT_HEAVY_SKIRMISH": "ART_DEF_UNIT_HEAVY_SKIRMISHER", | |
| "ART_DEF_UNIT_EXPLORER_CBP": "ART_DEF_UNIT_VP_EXPLORER", | |
| "ART_DEF_UNIT_MISSILE_DESTROYER": "ART_DEF_UNIT_SENSOR_COMBAT_SHIP", | |
| "ART_DEF_UNIT_VP_LIBURNA": "ART_DEF_UNIT_LIBURNA", | |
| "ART_DEF_UNIT_BANDEIRANTES": "ART_DEF_UNIT_BANDEIRANTE", | |
| "ART_DEF_UNIT_MEMBER_VP_SLINGER": "ART_DEF_UNIT_MEMBER_SLINGER", | |
| "ART_DEF_UNIT_MEMBER_HEAVY_SKIRMISH": "ART_DEF_UNIT_MEMBER_HEAVY_SKIRMISHER", | |
| "ART_DEF_UNIT_MEMBER_MISSILE_DESTROYER": "ART_DEF_UNIT_MEMBER_SENSOR_COMBAT_SHIP", | |
| "ART_DEF_UNIT_MEMBER_VP_LIBURNA": "ART_DEF_UNIT_MEMBER_LIBURNA", | |
| # UNIT_INCAN_SLINGER not changed in 4.22-> 5.0 | |
| # 22UNIT_INCAN_SLINGER: [(UnitFlavorSweeps.sql, UNIT_INCAN_SLINGER), (Inca.sql, UNIT_INCAN_SLINGER), (PreUnitChanges.sql, UNIT_INCAN_SLINGER), (CivilizationTextChanges.sql, TXT_KEY_UNIT_INCAN_SLINGER)] | |
| # UNIT_INCAN_SLINGER: [(UnitFlavorSweeps.sql, UNIT_INCAN_SLINGER), (Inca.sql, UNIT_INCAN_SLINGER), (NewUnits.xml, TXT_KEY_UNIT_INCAN_SLINGER), (PreUnitChanges.sql, UNIT_INCAN_SLINGER), (UnitChanges.sql, UNIT_INCAN_SLINGER), (NewCivilizationText.xml, TXT_KEY_UNIT_INCAN_SLINGER)] | |
| # 22UNIT_SLINGER: [] | |
| # UNIT_SLINGER: [(NewUnitArts.xml, ART_DEF_UNIT_SLINGER), (UnitArtChanges.sql, ART_DEF_UNIT_SLINGER), (UnitChanges.sql, ART_DEF_UNIT_SLINGER)] | |
| # "UNIT_INCAN_SLINGER": "UNIT_SLINGER", | |
| "UNIT_WARAKAQ": "UNIT_WARAKAQ", # listed as new; included for completeness | |
| # Build | |
| "BUILD_ENCAMPMENT_SHOSHONE": "BUILD_ENCAMPMENT", | |
| "BUILD_SPAIN_HACIENDA": "BUILD_HACIENDA", | |
| "BUILD_POLYNESIAN_BOATS": "BUILD_FISHING_BOATS_EMBARKED", | |
| # Improvement | |
| "IMPROVEMENT_JFD_MACHU_PICCHU": "IMPROVEMENT_MOUNTAIN_CITY", | |
| "IMPROVEMENT_ENCAMPMENT_SHOSHONE": "IMPROVEMENT_VP_ENCAMPMENT", | |
| "IMPROVEMENT_SPAIN_HACIENDA": "IMPROVEMENT_HACIENDA", | |
| "IMPROVEMENT_MONGOLIA_ORDO": "IMPROVEMENT_ORDO", | |
| "ART_DEF_IMPROVEMENT_JFD_MACHU": "ART_DEF_IMPROVEMENT_MOUNTAIN_CITY", | |
| "ART_DEF_IMPROVEMENT_SPAIN_HACIENDA": "ART_DEF_IMPROVEMENT_HACIENDA", | |
| "ART_DEF_IMPROVEMENT_MONGOLIA_ORDO": "ART_DEF_IMPROVEMENT_ORDO", | |
| # Name standardization singular/plural tweaks | |
| # "Mercenaries": "Mercenary", # I like Mercenary more | |
| # 22Bandeirantes: [((2) Vox Populi (v 17).modinfo, bandeirantes_flagbearer), (flagbearer.fxsxml, bandeirantes_flagbearer), (UnitFlavorSweeps.sql, UNIT_BANDEIRANTES), (NewIcons.xml, BANDEIRANTES_FLAG_ATLAS), (NewUnitArts.xml, ART_DEF_UNIT_BANDEIRANTES), (UnitArtChanges.sql, ART_DEF_UNIT_BANDEIRANTES), (Brazil.sql, Bandeirantes), (NewPromotions.xml, TXT_KEY_PROMOTION_RECON_BANDEIRANTES_HELP), (PromotionChanges.sql, Bandeirantes), (PromotionDisplaySweeps.sql, PROMOTION_RECON_BANDEIRANTES)] | |
| # Bandeirantes: [((2) Vox Populi (v 17).modinfo, bandeirantes_flagbearer), (flagbearer.fxsxml, bandeirantes_flagbearer), (NewUnitArts.xml, svbandeirantes), (NewCivilizationText.xml, Bandeirantes)] | |
| # 22Bandeirante: [((2) Vox Populi (v 17).modinfo, bandeirantes_flagbearer), (bandeirante_1.fxsxml, bandeirante_1), (bandeirante_2.fxsxml, bandeirante_2), (bandeirante_3.fxsxml, bandeirante_3), (flagbearer.fxsxml, bandeirantes_flagbearer), (UnitFlavorSweeps.sql, UNIT_BANDEIRANTES), (NewIcons.xml, BANDEIRANTES_FLAG_ATLAS), (NewUnitArts.xml, bandeirante_3), (UnitArtChanges.sql, ART_DEF_UNIT_BANDEIRANTES), (Brazil.sql, Bandeirantes)] | |
| # Bandeirante: [((2) Vox Populi (v 17).modinfo, Bandeirante), (bandeirante_1.fxsxml, bandeirante_1), (bandeirante_2.fxsxml, bandeirante_2), (bandeirante_3.fxsxml, bandeirante_3), (flagbearer.fxsxml, bandeirantes_flagbearer), (UnitFlavorSweeps.sql, UNIT_BANDEIRANTE), (NewUnitArts.xml, ART_DEF_UNIT_MEMBER_BANDEIRANTE_3), (UnitArtChanges.sql, ART_DEF_UNIT_BANDEIRANTE), (Brazil.sql, UNIT_BANDEIRANTE), (PromotionChanges.sql, Bandeirante)] | |
| # "Bandeirantes": "Bandeirante", # cannot understand the replacement rule | |
| "Comanche Riders": "Comanche Rider", | |
| "Seir Morb": "Suea Mop", | |
| "Bräuhaus": "Brewhouse", | |
| "Waraq'ak": "Warak'aq", | |
| "Baochuan": "Treasure Ship", | |
| "E-temenanki": "Etemenanki", | |
| "Smithsonian Institute": "Smithsonian Institution", | |
| "Splash Damage I": "Splash I", | |
| "Splash Damage II": "Splash II", | |
| # "Noble": "Nobility", # I like Noble more | |
| "Superior Flank Attack": "Heavy Flanking", | |
| "Taikan Kyohosyugi": "Taikan Kyoho", | |
| "Burger": "Burgher", | |
| "Flag Bearers": "Flag Bearer", | |
| "Grenadiers": "Grenadier", | |
| "Siege Volleys": "Siege Volley", | |
| "Concussive Hits": "Concussive Hit", | |
| "White Walkers": "White Walker", | |
| "Marsh Walkers": "Marsh Walker", | |
| "Woods Walkers": "Woods Walker", | |
| "Desert Walkers": "Desert Walker", | |
| "Hill Walkers": "Hill Walker", | |
| "Oceanic Perils": "Oceanic Peril", | |
| "Highlanders": "Highlander", | |
| "Lost Codices": "Lost Codex", | |
| # Diplomacy responses | |
| "RESPONSE_HOSTILE_REPEAT_SHARE_OPINION_NO": "RESPONSE_HOSTILE_REPEAT_SHARE_APPROACH_NO", | |
| "RESPONSE_REPEAT_SHARE_OPINION_NO": "RESPONSE_REPEAT_SHARE_APPROACH_NO", | |
| "RESPONSE_SHARE_OPINION_NO": "RESPONSE_SHARE_APPROACH_NO", | |
| "RESPONSE_SHARE_OPINION_FRIENDLY": "RESPONSE_SHARE_APPROACH_FRIENDLY", | |
| "RESPONSE_SHARE_OPINION_NEUTRAL": "RESPONSE_SHARE_APPROACH_NEUTRAL", | |
| "RESPONSE_SHARE_OPINION_GUARDED": "RESPONSE_SHARE_APPROACH_GUARDED", | |
| "RESPONSE_SHARE_OPINION_HOSTILE": "RESPONSE_SHARE_APPROACH_HOSTILE", | |
| "RESPONSE_SHARE_OPINION_WAR": "RESPONSE_SHARE_APPROACH_WAR", | |
| "RESPONSE_SHARE_OPINION_AFRAID": "RESPONSE_SHARE_APPROACH_AFRAID", | |
| "RESPONSE_SHARE_OPINION_PLANNING_WAR": "RESPONSE_SHARE_APPROACH_PLANNING_WAR", | |
| "RESPONSE_SHARE_OPINION_DECEPTIVE": "RESPONSE_SHARE_APPROACH_DECEPTIVE", | |
| "RESPONSE_TOO_SOON_FOR_SHARE_OPINION": "RESPONSE_TOO_SOON_FOR_SHARE_APPROACH", | |
| # CustomModOptions | |
| "API_ACHIEVEMENTS": "ENABLE_ACHIEVEMENTS", | |
| "CIVILIANS_RETREAT_WITH_MILITARY": "CORE_CIVILIANS_RETREAT_WITH_MILITARY", | |
| "BALANCE_DEFENSIVE_PACTS_AGGRESSION_ONLY": "CORE_PERSISTENT_DEFENSIVE_PACTS", | |
| "BALANCE_CORE_ENGINEER_HURRY": "CORE_ENGINEER_HURRY", | |
| "NO_REPAIR_FOREIGN_LANDS": "CORE_NO_REPAIRING_FOREIGN_LANDS", | |
| "NO_MAJORCIV_GIFTING": "CORE_NO_INTERMAJOR_UNIT_GIFTING", | |
| "NO_HEALING_ON_MOUNTAINS": "CORE_NO_HEALING_ON_MOUNTAINS", | |
| "NO_YIELD_ICE": "CORE_NO_YIELD_ICE", | |
| "CORE_REDUCE_NOTIFICATIONS": "COREUI_REDUCE_NOTIFICATIONS", | |
| "BALANCE_CORE_DIPLOMACY_ERA_INFLUENCE": "COREUI_DIPLOMACY_ERA_INFLUENCE", | |
| "BALANCE_CORE_LUXURIES_TRAIT": "BALANCE_ALTERNATE_INDONESIA_TRAIT", | |
| "BALANCE_CORE_MAYA_CHANGE": "BALANCE_ALTERNATE_MAYA_TRAIT", | |
| "ALTERNATE_SIAM_TRAIT": "BALANCE_ALTERNATE_SIAM_TRAIT", | |
| "BALANCE_CORE_UNIQUE_BELIEFS_ONLY_FOR_CIV": "BALANCE_UNIQUE_BELIEFS_ONLY_FOR_CIV", | |
| "BALANCE_CORE_VICTORY_GAME_CHANGES": "BALANCE_CULTURE_VICTORY_CHANGES", | |
| "BALANCE_CORE_BUILDING_INVESTMENTS": "BALANCE_BUILDING_INVESTMENTS", | |
| "BALANCE_CORE_RESOURCE_MONOPOLIES": "BALANCE_RESOURCE_MONOPOLIES", | |
| "BALANCE_CORE_RESOURCE_MONOPOLIES_STRATEGIC": "BALANCE_STRATEGIC_RESOURCE_MONOPOLIES", | |
| "BALANCE_CORE_MINOR_VARIABLE_BULLYING": "BALANCE_HEAVY_TRIBUTE", | |
| "BALANCE_CORE_MINOR_PTP_MINIMUM_VALUE": "BALANCE_MINOR_PROTECTION_REQUIREMENTS", | |
| "BALANCE_CORE_CITY_DEFENSE_SWITCH": "BALANCE_CITY_STRENGTH_SWITCH", | |
| "RIVER_CITY_CONNECTIONS": "BALANCE_RIVER_CITY_CONNECTIONS", | |
| "BALANCE_CORE_SPIES": "BALANCE_SPY_POINTS", | |
| "BALANCE_CORE_RANGED_ATTACK_PENALTY": "BALANCE_RANGED_DEFENSE_UNIT_HEALTH", | |
| "ADJACENT_BLOCKADE": "BALANCE_LAND_UNITS_ADJACENT_BLOCKADE", | |
| "BALANCE_CORE_GOODY_RECON_ONLY": "BALANCE_RECON_ONLY_ANCIENT_RUINS", | |
| "BALANCE_CORE_INQUISITOR_TWEAKS": "BALANCE_INQUISITOR_NERF", | |
| "BALANCE_CORE_MILITARY_RESOURCES": "BALANCE_RESOURCE_SHORTAGE_UNIT_HEALING", | |
| "BALANCE_CORE_BUILDING_RESOURCE_MAINTENANCE": "BALANCE_RESOURCE_SHORTAGE_BUILDING_REFUND", | |
| "BALANCE_RETROACTIVE_PROMOS": "BALANCE_RETROACTIVE_PROMOTIONS", | |
| "BALANCE_CORE_PUPPET_CHANGES": "BALANCE_PUPPET_CHANGES", | |
| "BALANCE_CORE_HALF_XP_PURCHASE": "BALANCE_HALF_XP_GOLD_PURCHASES", | |
| "BALANCE_CORE_UNIT_CREATION_DAMAGED": "BALANCE_PURCHASED_UNIT_DAMAGE", | |
| "BALANCE_CORE_SETTLERS_CONSUME_POP": "BALANCE_SETTLERS_CONSUME_POPULATION", | |
| "BALANCE_CORE_BARBARIAN_THEFT": "BALANCE_BARBARIAN_THEFT", | |
| "BALANCE_CORE_WONDER_COST_INCREASE": "BALANCE_WORLD_WONDER_COST_INCREASE", | |
| "BALANCE_CORE_SCALING_TRADE_DISTANCE": "BALANCE_TRADE_ROUTE_PROXIMITY_PENALTY", | |
| "BALANCE_CORE_WONDERS_VARIABLE_REWARD": "BALANCE_WONDERS_VARIABLE_CONSOLATION", | |
| "BALANCE_FLIPPED_TOURISM_MODIFIER_OPEN_BORDERS": "BALANCE_FLIPPED_OPEN_BORDERS_TOURISM", | |
| "BALANCE_CORE_PURCHASE_COST_INCREASE": "BALANCE_PURCHASE_COST_ADJUSTMENTS", | |
| "BALANCE_CORE_QUEST_CHANGES": "BALANCE_QUEST_CHANGES", | |
| "CORE_RESILIENT_PANTHEONS": "BALANCE_RESILIENT_PANTHEONS", | |
| "ALTERNATE_ASSYRIA_TRAIT": "BALANCE_ALTERNATE_ASSYRIA_TRAIT", | |
| "ALTERNATE_CELTS": "BALANCE_ALTERNATE_CELTS_TRAIT", | |
| "ANY_PANTHEON": "BALANCE_ANY_PANTHEON", | |
| "CITY_STATE_SCALE": "BALANCE_CITY_STATE_SCALE", | |
| "ERA_RESTRICTED_GENERALS": "BALANCE_ERA_RESTRICTED_GENERALS", | |
| "ERA_RESTRICTION": "BALANCE_ERA_RESTRICTION", | |
| "GP_ERA_SCALING": "BALANCE_GREAT_PEOPLE_ERA_SCALING", | |
| "UNIT_SUPPLY_MINORS_USE_HANDICAP": "BALANCE_MINOR_UNIT_SUPPLY_HANDICAP", | |
| "NO_AUTO_SPAWN_PROPHET": "BALANCE_NO_AUTO_SPAWN_PROPHET", | |
| "CORE_NO_RANGED_ATTACK_FROM_CITIES": "BALANCE_NO_CITY_RANGED_ATTACK", | |
| "RELIGION_PASSIVE_SPREAD_WITH_CONNECTION_ONLY": "BALANCE_PASSIVE_SPREAD_BY_CONNECTION", | |
| "RELIGION_PERMANENT_PANTHEON": "BALANCE_PERMANENT_PANTHEONS", | |
| "PILLAGE_PERMANENT_IMPROVEMENTS": "BALANCE_PILLAGE_PERMANENT_IMPROVEMENTS", | |
| "BALANCE_CORE_RANDOMIZED_GREAT_PROPHET_SPAWNS": "BALANCE_RANDOMIZED_GREAT_PROPHET_SPAWNS", | |
| "CORE_RELAXED_BORDER_CHECK": "BALANCE_RELAXED_BORDER_CHECK", | |
| "SANE_UNIT_MOVEMENT_COST": "BALANCE_SANE_UNIT_MOVEMENT_COST", | |
| "BALANCE_CORE_SETTLER_RESET_FOOD": "BALANCE_SETTLERS_RESET_GROWTH", | |
| "BALANCE_CORE_UNCAPPED_HAPPINESS": "BALANCE_UNCAPPED_UNHAPPINESS", | |
| "BALANCE_CORE_UNIT_INVESTMENTS": "BALANCE_UNIT_INVESTMENTS", | |
| "BALANCE_CORE_XP_ON_FIRST_ATTACK": "BALANCE_XP_ON_FIRST_ATTACK", | |
| "NO_RANDOM_TEXT_CIVS": "UI_NO_RANDOM_CIV_DIALOGUE", | |
| "CORE_TWO_PASS_DANGER": "COMBATAI_TWO_PASS_DANGER", | |
| "CORE_AREA_EFFECT_PROMOTIONS": "API_AREA_EFFECT_PROMOTIONS", | |
| "CORE_DISABLE_LUA_HOOKS": "API_DISABLE_LUA_HOOKS", | |
| "CORE_HOVERING_UNITS": "API_HOVERING_UNITS", | |
| "BALANCE_CORE_GOLD_INTERNAL_TRADE_ROUTES": "TRADE_INTERNAL_GOLD_ROUTES", | |
| "BALANCE_CORE_BOMBARD_RANGE_BUILDINGS": "BALANCE_BOMBARD_RANGE_BUILDINGS", | |
| "BALANCE_CORE_NEW_GP_ATTRIBUTES": "BALANCE_NEW_GREAT_PERSON_ATTRIBUTES", | |
| # Defines | |
| "BALANCE_MOD_POLICY_BRANCHES_NEEDED_IDEOLOGY": "IDEOLOGY_UNLOCK_NUM_POLICY_BRANCHES_NEEDED", | |
| "BALANCE_MOD_POLICIES_NEEDED_IDEOLOGY": "IDEOLOGY_UNLOCK_NUM_POLICIES_NEEDED", | |
| "BALANCE_CORE_WORLD_WONDER_SAME_ERA_COST_MODIFIER": "BALANCE_WORLD_WONDER_SAME_ERA_COST_MODIFIER", | |
| "BALANCE_CORE_WORLD_WONDER_PREVIOUS_ERA_COST_MODIFIER": "BALANCE_WORLD_WONDER_PREVIOUS_ERA_COST_MODIFIER", | |
| "BALANCE_CORE_WORLD_WONDER_SECOND_PREVIOUS_ERA_COST_MODIFIER": "BALANCE_WORLD_WONDER_SECOND_PREVIOUS_ERA_COST_MODIFIER", | |
| "BALANCE_CORE_WORLD_WONDER_EARLIER_ERA_COST_MODIFIER": "BALANCE_WORLD_WONDER_EARLIER_ERA_COST_MODIFIER", | |
| "BALANCE_CORE_CORP_OFFICE_FRANCHISE_CONVERSION": "BALANCE_CORP_OFFICE_FRANCHISE_CONVERSION", | |
| "BALANCE_CORE_CORP_OFFICE_TR_CONVERSION": "BALANCE_CORP_OFFICE_TR_CONVERSION", | |
| "BALANCE_CORE_PRODUCTION_DESERT_IMPROVEMENT": "BALANCE_PRODUCTION_DESERT_IMPROVEMENT", | |
| "SHARE_OPINION_TURN_BUFFER": "SHARE_APPROACH_TURN_BUFFER", | |
| # Unit columns | |
| "NoMinorGifts": "NoMinorCivUU", | |
| } | |
| BUILDING_BASE_RENAMES = { | |
| "GROVE": "COUNCIL", | |
| "LODGE": "SMOKEHOUSE", | |
| "FORTRESS": "BASTION_FORT", | |
| "STOCKYARD": "AGRIBUSINESS", | |
| "COAL_PLANT": "REFINERY", | |
| "BASILICA": "TETRACONCH", | |
| "JELLING_STONES": "RUNESTONE", | |
| "ODEON": "GYMNASION", | |
| "INDUS_CANAL": "HARAPPAN_RESERVOIR", | |
| "YURT": "GER", | |
| "SIEGE_WORKSHOP": "SIEGE_FOUNDRY", | |
| "COURT_SCRIBE": "SCRIVENERS_OFFICE", | |
| "FOREIGN_OFFICE": "FOREIGN_BUREAU", | |
| "PALACE_SCIENCE_CULTURE": "PALACE_CULTURE_SCIENCE", | |
| "FINANCE_CENTER": "INTERNATIONAL_FINANCE_CENTER", | |
| "EHRENHALLE": "HALL_OF_HONOR", | |
| "AMERICA_INDEPENDENCEHALL": "INDEPENDENCE_HALL", | |
| "VENETIAN_ARSENALE": "ARSENALE_DI_VENEZIA", | |
| "FORUM": "ROMAN_FORUM", | |
| "MOTHERLAND_STATUE": "MOTHERLAND_CALLS", | |
| "AMERICA_WESTPOINT": "WEST_POINT", | |
| "AMERICA_SMITHSONIAN": "SMITHSONIAN_INSTITUTION", | |
| "AMERICA_SLATERMILL": "SLATER_MILL", | |
| "UN": "UNITED_NATIONS", | |
| "PALACE_TREASURY": "STATE_TREASURY", | |
| "CAPITAL_ENGINEER": "ROYAL_GUARDHOUSE", | |
| "PALACE_COURT_CHAPEL": "COURT_CHAPEL", | |
| "PALACE_ASTROLOGER": "ROYAL_ASTROLOGER", | |
| "HEAVENLY_THRONE": "CELESTIAL_THRONE", | |
| "RELIGIOUS_LIBRARY": "CHARTARIUM", | |
| "LANDSEA_EXTRACTORS_HQ": "CENTAURUS_EXTRACTORS_HQ", | |
| "LANDSEA_EXTRACTORS_FRANCHISE": "CENTAURUS_EXTRACTORS_FRANCHISE", | |
| } | |
| BUILDING_REPLACEMENT_KEYS = tuple(BUILDING_BASE_RENAMES.keys()) | |
| # Add prefixed Building/BuildingClass variants for every building rename entry. | |
| for old, new in BUILDING_BASE_RENAMES.items(): | |
| REPLACEMENTS[f"BUILDING_{old}"] = f"BUILDING_{new}" | |
| REPLACEMENTS[f"BUILDINGCLASS_{old}"] = f"BUILDINGCLASS_{new}" | |
| PROMOTION_REPLACEMENTS = { | |
| "WOODLAND_TRAILBLAZER": "TRAILBLAZER", | |
| "ANTIAIR_LAND": "AIR_DEFENSE", | |
| "SHOCK_4": "OVERRUN", | |
| "DRILL_4": "STALWART", | |
| "BARRAGE_4": "FIRING_DOCTRINE", | |
| "ACCURACY_4": "INFILTRATORS", | |
| "TARGETING_4": "INDOMITABLE", | |
| "COASTAL_RAIDER_4": "BLOCKADE", | |
| "BOARDING_PARTY_4": "PINCER", | |
| "DAMAGE_REDUCTION": "DAUNTLESS", | |
| "PRESS_GANGS": "COMMERCE_RAIDER", | |
| "SKIRMISHER_MOBILITY": "PARTHIAN_TACTICS", | |
| "SKIRMISHER_POWER": "COUP_DE_GRACE", | |
| "BETTER_BOMBARDMENT": "SHRAPNEL_ROUNDS", | |
| "COASTAL_TERROR": "VANGUARD", | |
| "TRUE_WOLFPACK": "VP_WOLFPACK", | |
| "KILL_HEAL": "ENDURANCE", | |
| "HONOR_BONUS": "CONSCRIPTION", | |
| "IMPERIALISM_OPENER": "IMPERIALISM", | |
| "BETTER_LEADERSHIP": "REGIMENTAL_TRADITIONS", | |
| "NAVAL_DEFENSE_BOOST": "BANZAI", | |
| "LIGHTNING_WARFARE_GUN": "LIGHTNING_WARFARE_GUNPOWDER", | |
| "LIGHTNING_WARFARE_ARMOR": "LIGHTNING_WARFARE_ARMORED", | |
| "ICE_BREAKERS": "ENGINEERING_CORP", | |
| "FALLOUT_REDUCTION": "FALLOUT_RESISTANCE", | |
| "SIGNET": "ROYAL_SIGNET", | |
| "EXPRESS": "WIRE_SERVICE", | |
| "IMMUNITY": "DIPLOMATIC_IMMUNITY", | |
| "PAX": "IMPERIAL_SEAL", | |
| "ALHAMBRA": "JINETE", | |
| "ARSENALE": "VENETIAN_CRAFTSMANSHIP", | |
| "BARBARIAN_BONUS": "BRUTE_FORCE", | |
| "NAVAL_MISFIRE": "NAVAL_TARGET_PENALTY", | |
| "MECH": "TITANIC", | |
| "ROUGH_TERRAIN_HALF_TURN": "BEAM_AXLE", | |
| "RECON_EXPERIENCE": "RECONNAISSANCE", | |
| "OCEAN_CROSSING": "OCEAN_EXPLORER", | |
| "SCOUT_XP_PILLAGE": "SCAVENGER", | |
| "OCEAN_HALF_MOVES": "SHALLOW_DRAFT", | |
| "AIR_MISFIRE": "STRAFING_RUNS", | |
| "RECON_RANGE": "AIR_RECON_RANGE", | |
| "SUPPLY_BOOST": "MILITARY_TRADITION", | |
| "NUCLEAR_SILO": "SHIELDED_SILO", | |
| "FLANK_ATTACK_BONUS_STRONG": "HEAVY_FLANKING", | |
| "ENSLAVEMENT": "GIFT_OF_THE_PHARAOH", | |
| "RECON_BANDEIRANTES": "FLAG_BEARER", | |
| "KNOCKOUT": "FASIMBA", | |
| "ESPRIT_DE_CORPS": "WITHERING_FIRE", | |
| "AOE_STRIKE_ON_KILL": "GRENADIER", | |
| "AOE_STRIKE_FORTIFY": "PILUM", | |
| "SLINGER": "CONCUSSIVE_HIT", | |
| "REPEATER": "SIEGE_VOLLEY", | |
| "HEAVY_SHIP": "HEAVY_ASSAULT", | |
| "MANY_GOLDEN_AGE_POINTS": "PRIDE_OF_THE_NATION", | |
| "BUSHIDO_HONOR": "HONOR", | |
| "PRISONER_WAR": "PRISONERS_OF_WAR", | |
| "MORALE_EVENT": "FERVOR", | |
| } | |
| for old, new in PROMOTION_REPLACEMENTS.items(): | |
| REPLACEMENTS[f"PROMOTION_{old}"] = f"PROMOTION_{new}" | |
| ICON_FALLBACK_SIZES = (64, 48, 32, 24, 16) | |
| # Scoped replacements (apply only in certain files) | |
| TRAIT_POLICY_COLUMNS = { | |
| "IgnoreTradeDistanceScaling": "NoTradeRouteProximityPenalty", | |
| "BasicNeedsMedianModifierGlobal": "BasicNeedsMedianModifier", | |
| "GoldMedianModifierGlobal": "GoldMedianModifier", | |
| "ScienceMedianModifierGlobal": "ScienceMedianModifier", | |
| "CultureMedianModifierGlobal": "CultureMedianModifier", | |
| "ReligiousUnrestModifierGlobal": "ReligiousUnrestModifier", | |
| "DistressFlatReductionGlobal": "DistressFlatReduction", | |
| "PovertyFlatReductionGlobal": "PovertyFlatReduction", | |
| "IlliteracyFlatReductionGlobal": "IlliteracyFlatReduction", | |
| "BoredomFlatReductionGlobal": "BoredomFlatReduction", | |
| "ReligiousUnrestFlatReductionGlobal": "ReligiousUnrestFlatReduction", | |
| } | |
| def should_skip_mod(mod_name: str, include_vp: bool) -> bool: | |
| if include_vp: | |
| return False | |
| lowered = mod_name.lower() | |
| # Always process Community Events even though it contains "vp". | |
| if "community events" in lowered: | |
| return False | |
| if lowered in DEFAULT_SKIP_NAMES: | |
| return True | |
| # Generic skip: names that clearly are VP core (prefix vp/, or contain vox populi/community patch) | |
| return lowered.startswith("vp ") or "vox populi" in lowered or "community patch" in lowered | |
| def iter_mod_dirs(root: Path, include_vp: bool) -> Iterable[Path]: | |
| for child in root.iterdir(): | |
| if not child.is_dir(): | |
| continue | |
| if should_skip_mod(child.name, include_vp): | |
| continue | |
| yield child | |
| def iter_target_files(mod_dir: Path, exts: set[str]) -> Iterable[Path]: | |
| for path in mod_dir.rglob("*"): | |
| if not path.is_file(): | |
| continue | |
| if path.suffix.lower() in exts: | |
| yield path | |
| def compile_patterns(replacements: dict[str, str]) -> list[tuple[re.Pattern[str], str]]: | |
| """ | |
| Compile patterns to match tokens safely with case-aware boundaries: | |
| - SCREAMING_SNAKE_CASE (all-caps/underscore/digit): match when followed by end or underscore. | |
| - PascalCase: match when followed by end, an uppercase letter, or a digit. | |
| - Other tokens: require non-word (no letter/digit/underscore) boundaries on both sides. | |
| """ | |
| compiled = [] | |
| for old, new in replacements.items(): | |
| escaped_old = re.escape(old) | |
| is_screaming_snake = bool(re.fullmatch(r"[A-Z0-9_]+", old)) | |
| is_pascal_case = bool( | |
| re.fullmatch(r"[A-Z][a-z0-9]*([A-Z][A-Za-z0-9]*)+", old) | |
| ) and not is_screaming_snake | |
| if is_screaming_snake: | |
| pattern = rf"(?<![A-Za-z0-9]){escaped_old}(?=$|_|[^A-Za-z0-9_])" | |
| elif is_pascal_case: | |
| pattern = rf"(?<![A-Za-z0-9_]){escaped_old}(?=$|[A-Z0-9])" | |
| else: | |
| pattern = rf"(?<![A-Za-z0-9_]){escaped_old}(?![A-Za-z0-9_])" | |
| compiled.append((re.compile(pattern), new)) | |
| # Sort longest first so longer tokens win when overlaps exist. | |
| compiled.sort(key=lambda x: len(x[0].pattern), reverse=True) | |
| return compiled | |
| def patch_ige_icon_lookup(root: Path) -> int: | |
| """ | |
| IGE calls IconLookup with size 45, but many atlases (Historical Religions) do not ship a 45px slice. | |
| Inject a SafeIconLookup wrapper with fallbacks and rewrite IconLookup calls to use it. | |
| Returns number of files patched. | |
| """ | |
| patched = 0 | |
| for path in root.rglob("IGE_API_Data.lua"): | |
| try: | |
| content = path.read_text(encoding="utf-8") | |
| except UnicodeDecodeError: | |
| continue | |
| if "SafeIconLookup" in content: | |
| continue | |
| updated = re.sub(r"\bIconLookup\(", "SafeIconLookup(", content) | |
| marker = "local largeSize = 64;\nlocal smallSize = 45;\n" | |
| helper_block = ( | |
| "\n" | |
| "local iconFallbackSizes = {" | |
| + ", ".join(str(size) for size in ICON_FALLBACK_SIZES) | |
| + "}\n\n" | |
| "local function SafeIconLookup(index, size, atlas)\n" | |
| " local offset, texture = IconLookup(index, size, atlas)\n" | |
| " if offset and texture then\n" | |
| " return offset, texture\n" | |
| " end\n" | |
| " for _, altSize in ipairs(iconFallbackSizes) do\n" | |
| " if altSize ~= size then\n" | |
| " offset, texture = IconLookup(index, altSize, atlas)\n" | |
| " if offset and texture then\n" | |
| " return offset, texture\n" | |
| " end\n" | |
| " end\n" | |
| " end\n" | |
| " return nil, nil\n" | |
| "end\n" | |
| "\n" | |
| ) | |
| if marker not in updated: | |
| continue | |
| updated = updated.replace(marker, marker + helper_block, 1) | |
| path.write_text(updated, encoding="utf-8") | |
| print(f"[ICON_FIX] Added SafeIconLookup to {path}") | |
| patched += 1 | |
| return patched | |
| def apply_replacements(text: str, patterns: list[tuple[re.Pattern[str], str]]) -> tuple[str, int]: | |
| """Return (new_text, total_replacements).""" | |
| total = 0 | |
| for pat, new in patterns: | |
| text, count = pat.subn(new, text) | |
| total += count | |
| return text, total | |
| def apply_scoped_replacements(text: str, path: Path) -> tuple[str, int]: | |
| """ | |
| Apply replacements that should only affect specific tables/files. | |
| Currently scoped to trait/policy column renames. | |
| """ | |
| lowered = path.as_posix().lower() | |
| is_trait_policy = any(key in lowered for key in ("trait", "policy")) | |
| total = 0 | |
| if is_trait_policy: | |
| for old, new in TRAIT_POLICY_COLUMNS.items(): | |
| pat = re.compile(rf"(?<![A-Za-z0-9_]){re.escape(old)}(?![A-Za-z0-9_])") | |
| text, count = pat.subn(new, text) | |
| total += count | |
| return text, total | |
| def run_tests() -> None: | |
| """Lightweight self-checks for boundary rules.""" | |
| def expect(src: str, expected: str, repl: dict[str, str]) -> None: | |
| compiled = compile_patterns(repl) | |
| out, _ = apply_replacements(src, compiled) | |
| assert out == expected, f"Expected {expected!r}, got {out!r} for {src!r}" | |
| # SCREAMING_SNAKE_CASE boundaries | |
| expect( | |
| "TXT_BUILDING_UN", | |
| "TXT_BUILDING_UNITED_NATIONS", | |
| {"BUILDING_UN": "BUILDING_UNITED_NATIONS"}, | |
| ) | |
| expect( | |
| "TXT_BUILDING_UN_TYPE1", | |
| "TXT_BUILDING_UNITED_NATIONS_TYPE1", | |
| {"BUILDING_UN": "BUILDING_UNITED_NATIONS"}, | |
| ) | |
| expect("BUILDING_UN2", "BUILDING_UN2", {"BUILDING_UN": "BUILDING_UNITED_NATIONS"}) | |
| expect( | |
| "BUILDINGCLASS_LANDSEA_EXTRACTORS_FRANCHISE</RequiredBuildingClass>", | |
| "BUILDINGCLASS_CENTAURUS_EXTRACTORS_FRANCHISE</RequiredBuildingClass>", | |
| {"BUILDINGCLASS_LANDSEA_EXTRACTORS_FRANCHISE": "BUILDINGCLASS_CENTAURUS_EXTRACTORS_FRANCHISE"}, | |
| ) | |
| expect( | |
| "BUILDINGCLASS_LANDSEA_EXTRACTORS_FRANCHISE>/RequiredBuildingClass>", | |
| "BUILDINGCLASS_CENTAURUS_EXTRACTORS_FRANCHISE>/RequiredBuildingClass>", | |
| {"BUILDINGCLASS_LANDSEA_EXTRACTORS_FRANCHISE": "BUILDINGCLASS_CENTAURUS_EXTRACTORS_FRANCHISE"}, | |
| ) | |
| expect( | |
| "BUILDINGCLASS_LANDSEA_EXTRACTORS_FRANCHISE /RequiredBuildingClass>", | |
| "BUILDINGCLASS_CENTAURUS_EXTRACTORS_FRANCHISE /RequiredBuildingClass>", | |
| {"BUILDINGCLASS_LANDSEA_EXTRACTORS_FRANCHISE": "BUILDINGCLASS_CENTAURUS_EXTRACTORS_FRANCHISE"}, | |
| ) | |
| # PascalCase boundaries | |
| expect( | |
| "BoredomFlatReductionGlobal123", | |
| "Pascal123", | |
| {"BoredomFlatReductionGlobal": "Pascal"}, | |
| ) | |
| expect( | |
| "Foo BoredomFlatReductionGlobalApple", | |
| "Foo PascalApple", | |
| {"BoredomFlatReductionGlobal": "Pascal"}, | |
| ) | |
| expect( | |
| "BoredomFlatReductionGlobales", | |
| "BoredomFlatReductionGlobales", | |
| {"BoredomFlatReductionGlobal": "Pascal"}, | |
| ) | |
| # Plain text boundaries (name standardization) | |
| expect( | |
| "Flag Bearers and Mercenaries.", | |
| "Flag Bearer and Mercenary.", | |
| {"Flag Bearers": "Flag Bearer", "Mercenaries": "Mercenary"}, | |
| ) | |
| expect( | |
| "Flag Bearers123", | |
| "Flag Bearers123", | |
| {"Flag Bearers": "Flag Bearer"}, | |
| ) | |
| def main() -> None: | |
| parser = argparse.ArgumentParser(description="VP 5.0 alpha rename helper") | |
| parser.add_argument( | |
| "--root", | |
| type=Path, | |
| default=DEFAULT_ROOT, | |
| help="Mods root folder (default: Civ5 MODS path)", | |
| ) | |
| parser.add_argument( | |
| "--include-vp", | |
| action="store_true", | |
| default=False, | |
| help="Also process VP core mods (default skips them, except Community Events).", | |
| ) | |
| parser.add_argument( | |
| "--patch-ige-icon-lookup", | |
| action="store_true", | |
| default=False, | |
| help="Patch IGE IconLookup calls with fallbacks for missing 45px atlas slices.", | |
| ) | |
| parser.add_argument( | |
| "--dry-run", action="store_true", help="Show what would change without writing" | |
| ) | |
| args = parser.parse_args() | |
| root = args.root | |
| if not root.exists(): | |
| raise SystemExit(f"Root does not exist: {root}") | |
| patched_icons = 0 | |
| if args.patch_ige_icon_lookup: | |
| patched_icons = patch_ige_icon_lookup(root) | |
| patterns = compile_patterns(REPLACEMENTS) | |
| modified_files = 0 | |
| total_hits = 0 | |
| for mod_dir in iter_mod_dirs(root, args.include_vp): | |
| for file_path in iter_target_files(mod_dir, DEFAULT_EXTS): | |
| read_encoding = "utf-8" | |
| try: | |
| content = file_path.read_text(encoding=read_encoding) | |
| except UnicodeDecodeError: | |
| read_encoding = "latin-1" | |
| content = file_path.read_text(encoding=read_encoding) | |
| new_content, hits = apply_replacements(content, patterns) | |
| scoped_content, scoped_hits = apply_scoped_replacements(new_content, file_path) | |
| if scoped_content != content: | |
| modified_files += 1 | |
| total_hits += hits + scoped_hits | |
| if not args.dry_run: | |
| file_path.write_text(scoped_content, encoding=read_encoding) | |
| print(f"[UPDATED] {file_path}") | |
| print(f"\nDone. Files modified: {modified_files}, replacements applied: {total_hits}") | |
| if patched_icons: | |
| print(f"SafeIconLookup added to {patched_icons} IGE file(s) for icon fallbacks.") | |
| if args.dry_run: | |
| print("Dry-run mode: no files were written.") | |
| if __name__ == "__main__": | |
| run_tests() | |
| main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment