diff --git a/.gitignore b/.gitignore index 41cc1372..9a8d0561 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ *.exp *.lib ref/ +agents.md __pycache__ temp.exe Thumbs.db diff --git a/Art/.gitignore b/Art/.gitignore index c85a63f9..5ab70449 100644 --- a/Art/.gitignore +++ b/Art/.gitignore @@ -1 +1,25 @@ -Seasons/ \ No newline at end of file +Seasons/ +0100/ +0200/ +0300/ +0400/ +0500/ +0600/ +0700/ +0800/ +0900/ +1000/ +1100/ + +1300/ +1400/ +1500/ +1600/ +1700/ +1800/ +1900/ +2000/ +2100/ +2200/ +2300/ +2400/ \ No newline at end of file diff --git a/Art/Districts/.gitignore b/Art/Districts/.gitignore deleted file mode 100644 index 9bbba50d..00000000 --- a/Art/Districts/.gitignore +++ /dev/null @@ -1,24 +0,0 @@ -0100/ -0200/ -0300/ -0400/ -0500/ -0600/ -0700/ -0800/ -0900/ -1000/ -1100/ - -1300/ -1400/ -1500/ -1600/ -1700/ -1800/ -1900/ -2000/ -2100/ -2200/ -2300/ -2400/ \ No newline at end of file diff --git a/Art/Districts/1200/.gitignore b/Art/Districts/1200/.gitignore index 34050988..3a7f50b6 100644 --- a/Art/Districts/1200/.gitignore +++ b/Art/Districts/1200/.gitignore @@ -1 +1 @@ -*_lights.PCX \ No newline at end of file +*_lights.pcx \ No newline at end of file diff --git a/Art/Districts/1200/Abandoned.PCX b/Art/Districts/1200/Abandoned.PCX new file mode 100644 index 00000000..8575052e Binary files /dev/null and b/Art/Districts/1200/Abandoned.PCX differ diff --git a/Art/Districts/1200/Bridge.PCX b/Art/Districts/1200/Bridge.PCX new file mode 100644 index 00000000..a61bd5c0 Binary files /dev/null and b/Art/Districts/1200/Bridge.PCX differ diff --git a/Art/Districts/1200/Campus.PCX b/Art/Districts/1200/Campus.PCX index a2d0c770..28774325 100644 Binary files a/Art/Districts/1200/Campus.PCX and b/Art/Districts/1200/Campus.PCX differ diff --git a/Art/Districts/1200/Canal.PCX b/Art/Districts/1200/Canal.PCX new file mode 100644 index 00000000..4de393f8 Binary files /dev/null and b/Art/Districts/1200/Canal.PCX differ diff --git a/Art/Districts/1200/CentralRailHub_AMER.PCX b/Art/Districts/1200/CentralRailHub_AMER.PCX new file mode 100644 index 00000000..2a1c81b9 Binary files /dev/null and b/Art/Districts/1200/CentralRailHub_AMER.PCX differ diff --git a/Art/Districts/1200/CentralRailHub_ASIAN.PCX b/Art/Districts/1200/CentralRailHub_ASIAN.PCX new file mode 100644 index 00000000..c5387dbe Binary files /dev/null and b/Art/Districts/1200/CentralRailHub_ASIAN.PCX differ diff --git a/Art/Districts/1200/CentralRailHub_EURO.PCX b/Art/Districts/1200/CentralRailHub_EURO.PCX new file mode 100644 index 00000000..b963b5db Binary files /dev/null and b/Art/Districts/1200/CentralRailHub_EURO.PCX differ diff --git a/Art/Districts/1200/CentralRailHub_MIDEAST.PCX b/Art/Districts/1200/CentralRailHub_MIDEAST.PCX new file mode 100644 index 00000000..5634c3fc Binary files /dev/null and b/Art/Districts/1200/CentralRailHub_MIDEAST.PCX differ diff --git a/Art/Districts/1200/CentralRailHub_ROMAN.PCX b/Art/Districts/1200/CentralRailHub_ROMAN.PCX new file mode 100644 index 00000000..5da251fe Binary files /dev/null and b/Art/Districts/1200/CentralRailHub_ROMAN.PCX differ diff --git a/Art/Districts/1200/CommercialHub_AMER.PCX b/Art/Districts/1200/CommercialHub_AMER.PCX index 32481d25..363a9df5 100644 Binary files a/Art/Districts/1200/CommercialHub_AMER.PCX and b/Art/Districts/1200/CommercialHub_AMER.PCX differ diff --git a/Art/Districts/1200/CommercialHub_ASIAN.PCX b/Art/Districts/1200/CommercialHub_ASIAN.PCX index bedc7543..1b33f24b 100644 Binary files a/Art/Districts/1200/CommercialHub_ASIAN.PCX and b/Art/Districts/1200/CommercialHub_ASIAN.PCX differ diff --git a/Art/Districts/1200/CommercialHub_EURO.PCX b/Art/Districts/1200/CommercialHub_EURO.PCX index 2e1f53de..9948fe0c 100644 Binary files a/Art/Districts/1200/CommercialHub_EURO.PCX and b/Art/Districts/1200/CommercialHub_EURO.PCX differ diff --git a/Art/Districts/1200/CommercialHub_MIDEAST.PCX b/Art/Districts/1200/CommercialHub_MIDEAST.PCX index 76ec0732..0b43f08b 100644 Binary files a/Art/Districts/1200/CommercialHub_MIDEAST.PCX and b/Art/Districts/1200/CommercialHub_MIDEAST.PCX differ diff --git a/Art/Districts/1200/CommercialHub_ROMAN.PCX b/Art/Districts/1200/CommercialHub_ROMAN.PCX index a8fbfdd4..eeb9e5a4 100644 Binary files a/Art/Districts/1200/CommercialHub_ROMAN.PCX and b/Art/Districts/1200/CommercialHub_ROMAN.PCX differ diff --git a/Art/Districts/1200/DataCenter.PCX b/Art/Districts/1200/DataCenter.PCX new file mode 100644 index 00000000..be33f924 Binary files /dev/null and b/Art/Districts/1200/DataCenter.PCX differ diff --git a/Art/Districts/1200/DistributionHub.PCX b/Art/Districts/1200/DistributionHub.PCX index c9add166..b042d8b0 100644 Binary files a/Art/Districts/1200/DistributionHub.PCX and b/Art/Districts/1200/DistributionHub.PCX differ diff --git a/Art/Districts/1200/Encampment.PCX b/Art/Districts/1200/Encampment.PCX index 94fffddb..202f5da0 100644 Binary files a/Art/Districts/1200/Encampment.PCX and b/Art/Districts/1200/Encampment.PCX differ diff --git a/Art/Districts/1200/EnergyGrid.PCX b/Art/Districts/1200/EnergyGrid.PCX new file mode 100644 index 00000000..d557c6e9 Binary files /dev/null and b/Art/Districts/1200/EnergyGrid.PCX differ diff --git a/Art/Districts/1200/GreatWall.pcx b/Art/Districts/1200/GreatWall.pcx new file mode 100644 index 00000000..2dd668f1 Binary files /dev/null and b/Art/Districts/1200/GreatWall.pcx differ diff --git a/Art/Districts/1200/HolySite_AMER.PCX b/Art/Districts/1200/HolySite_AMER.PCX index 27f6fd7b..763a0989 100644 Binary files a/Art/Districts/1200/HolySite_AMER.PCX and b/Art/Districts/1200/HolySite_AMER.PCX differ diff --git a/Art/Districts/1200/HolySite_ASIAN.PCX b/Art/Districts/1200/HolySite_ASIAN.PCX index 5b39ddff..b7b41a88 100644 Binary files a/Art/Districts/1200/HolySite_ASIAN.PCX and b/Art/Districts/1200/HolySite_ASIAN.PCX differ diff --git a/Art/Districts/1200/HolySite_EURO.PCX b/Art/Districts/1200/HolySite_EURO.PCX index b9f3ac96..1d6f1599 100644 Binary files a/Art/Districts/1200/HolySite_EURO.PCX and b/Art/Districts/1200/HolySite_EURO.PCX differ diff --git a/Art/Districts/1200/HolySite_MIDEAST.PCX b/Art/Districts/1200/HolySite_MIDEAST.PCX index 9d09853f..b6a5efb1 100644 Binary files a/Art/Districts/1200/HolySite_MIDEAST.PCX and b/Art/Districts/1200/HolySite_MIDEAST.PCX differ diff --git a/Art/Districts/1200/HolySite_ROMAN.PCX b/Art/Districts/1200/HolySite_ROMAN.PCX index 37d2f732..05428f1d 100644 Binary files a/Art/Districts/1200/HolySite_ROMAN.PCX and b/Art/Districts/1200/HolySite_ROMAN.PCX differ diff --git a/Art/Districts/1200/IndustrialZone.PCX b/Art/Districts/1200/IndustrialZone.PCX index f6a4c6c6..f463ddaa 100644 Binary files a/Art/Districts/1200/IndustrialZone.PCX and b/Art/Districts/1200/IndustrialZone.PCX differ diff --git a/Art/Districts/1200/MunicipalDistrict.PCX b/Art/Districts/1200/MunicipalDistrict.PCX new file mode 100644 index 00000000..ebeefb36 Binary files /dev/null and b/Art/Districts/1200/MunicipalDistrict.PCX differ diff --git a/Art/Districts/1200/NaturalWonders.pcx b/Art/Districts/1200/NaturalWonders.pcx index 9aa359bd..7f27b791 100644 Binary files a/Art/Districts/1200/NaturalWonders.pcx and b/Art/Districts/1200/NaturalWonders.pcx differ diff --git a/Art/Districts/1200/Neighborhood_Abandoned.PCX b/Art/Districts/1200/Neighborhood_Abandoned.PCX deleted file mode 100644 index 11ea0cb2..00000000 Binary files a/Art/Districts/1200/Neighborhood_Abandoned.PCX and /dev/null differ diff --git a/Art/Districts/1200/Neighborhood_MIDEAST.PCX b/Art/Districts/1200/Neighborhood_MIDEAST.PCX index bc7ad2bc..b8df8866 100644 Binary files a/Art/Districts/1200/Neighborhood_MIDEAST.PCX and b/Art/Districts/1200/Neighborhood_MIDEAST.PCX differ diff --git a/Art/Districts/1200/Neighborhood_ROMAN.PCX b/Art/Districts/1200/Neighborhood_ROMAN.PCX index b2ee8512..74bcc14d 100644 Binary files a/Art/Districts/1200/Neighborhood_ROMAN.PCX and b/Art/Districts/1200/Neighborhood_ROMAN.PCX differ diff --git a/Art/Districts/1200/OffshoreExtractionZone.PCX b/Art/Districts/1200/OffshoreExtractionZone.PCX new file mode 100644 index 00000000..bb1d750c Binary files /dev/null and b/Art/Districts/1200/OffshoreExtractionZone.PCX differ diff --git a/Art/Districts/1200/Park.PCX b/Art/Districts/1200/Park.PCX new file mode 100644 index 00000000..c968e079 Binary files /dev/null and b/Art/Districts/1200/Park.PCX differ diff --git a/Art/Districts/1200/Port_NE.PCX b/Art/Districts/1200/Port_NE.PCX new file mode 100644 index 00000000..e3707673 Binary files /dev/null and b/Art/Districts/1200/Port_NE.PCX differ diff --git a/Art/Districts/1200/Port_NW.PCX b/Art/Districts/1200/Port_NW.PCX new file mode 100644 index 00000000..3afef1e2 Binary files /dev/null and b/Art/Districts/1200/Port_NW.PCX differ diff --git a/Art/Districts/1200/Port_SE.PCX b/Art/Districts/1200/Port_SE.PCX new file mode 100644 index 00000000..18f1c155 Binary files /dev/null and b/Art/Districts/1200/Port_SE.PCX differ diff --git a/Art/Districts/1200/Port_SW.PCX b/Art/Districts/1200/Port_SW.PCX new file mode 100644 index 00000000..02ccf89f Binary files /dev/null and b/Art/Districts/1200/Port_SW.PCX differ diff --git a/Art/Districts/1200/SkiResort.PCX b/Art/Districts/1200/SkiResort.PCX new file mode 100644 index 00000000..3ebb4a0c Binary files /dev/null and b/Art/Districts/1200/SkiResort.PCX differ diff --git a/Art/Districts/1200/WaterPark.pcx b/Art/Districts/1200/WaterPark.pcx new file mode 100644 index 00000000..edcca3e3 Binary files /dev/null and b/Art/Districts/1200/WaterPark.pcx differ diff --git a/Art/Districts/1200/Wonders.PCX b/Art/Districts/1200/Wonders.PCX index e882b035..144373b7 100644 Binary files a/Art/Districts/1200/Wonders.PCX and b/Art/Districts/1200/Wonders.PCX differ diff --git a/Art/Districts/1200/Wonders_2.PCX b/Art/Districts/1200/Wonders_2.PCX index fe939a34..295a8829 100644 Binary files a/Art/Districts/1200/Wonders_2.PCX and b/Art/Districts/1200/Wonders_2.PCX differ diff --git a/Art/Districts/1200/Wonders_3.PCX b/Art/Districts/1200/Wonders_3.PCX new file mode 100644 index 00000000..5f646a17 Binary files /dev/null and b/Art/Districts/1200/Wonders_3.PCX differ diff --git a/Art/Districts/1200/_template.PCX b/Art/Districts/1200/_template.PCX index 01e2abc7..33118518 100644 Binary files a/Art/Districts/1200/_template.PCX and b/Art/Districts/1200/_template.PCX differ diff --git a/Art/Districts/Annotations/Bridge_lights.PCX b/Art/Districts/Annotations/Bridge_lights.PCX new file mode 100644 index 00000000..d67ac5f9 Binary files /dev/null and b/Art/Districts/Annotations/Bridge_lights.PCX differ diff --git a/Art/Districts/Annotations/Canal_lights.PCX b/Art/Districts/Annotations/Canal_lights.PCX new file mode 100644 index 00000000..0dd85820 Binary files /dev/null and b/Art/Districts/Annotations/Canal_lights.PCX differ diff --git a/Art/Districts/Annotations/CentralRailHub_AMER_lights.PCX b/Art/Districts/Annotations/CentralRailHub_AMER_lights.PCX new file mode 100644 index 00000000..2a1c81b9 Binary files /dev/null and b/Art/Districts/Annotations/CentralRailHub_AMER_lights.PCX differ diff --git a/Art/Districts/Annotations/CentralRailHub_ASIAN_lights.PCX b/Art/Districts/Annotations/CentralRailHub_ASIAN_lights.PCX new file mode 100644 index 00000000..c5387dbe Binary files /dev/null and b/Art/Districts/Annotations/CentralRailHub_ASIAN_lights.PCX differ diff --git a/Art/Districts/Annotations/CentralRailHub_EURO_lights.PCX b/Art/Districts/Annotations/CentralRailHub_EURO_lights.PCX new file mode 100644 index 00000000..b963b5db Binary files /dev/null and b/Art/Districts/Annotations/CentralRailHub_EURO_lights.PCX differ diff --git a/Art/Districts/Annotations/CentralRailHub_MIDEAST_lights.PCX b/Art/Districts/Annotations/CentralRailHub_MIDEAST_lights.PCX new file mode 100644 index 00000000..5634c3fc Binary files /dev/null and b/Art/Districts/Annotations/CentralRailHub_MIDEAST_lights.PCX differ diff --git a/Art/Districts/Annotations/CentralRailHub_ROMAN_lights.PCX b/Art/Districts/Annotations/CentralRailHub_ROMAN_lights.PCX new file mode 100644 index 00000000..5da251fe Binary files /dev/null and b/Art/Districts/Annotations/CentralRailHub_ROMAN_lights.PCX differ diff --git a/Art/Districts/Annotations/CommercialHub_AMER_lights.PCX b/Art/Districts/Annotations/CommercialHub_AMER_lights.PCX index 6ce56ebc..fe5c9edd 100644 Binary files a/Art/Districts/Annotations/CommercialHub_AMER_lights.PCX and b/Art/Districts/Annotations/CommercialHub_AMER_lights.PCX differ diff --git a/Art/Districts/Annotations/CommercialHub_ASIAN_lights.PCX b/Art/Districts/Annotations/CommercialHub_ASIAN_lights.PCX index 19cba785..aab9c94c 100644 Binary files a/Art/Districts/Annotations/CommercialHub_ASIAN_lights.PCX and b/Art/Districts/Annotations/CommercialHub_ASIAN_lights.PCX differ diff --git a/Art/Districts/Annotations/CommercialHub_EURO_lights.PCX b/Art/Districts/Annotations/CommercialHub_EURO_lights.PCX index 44671292..d21c1e36 100644 Binary files a/Art/Districts/Annotations/CommercialHub_EURO_lights.PCX and b/Art/Districts/Annotations/CommercialHub_EURO_lights.PCX differ diff --git a/Art/Districts/Annotations/CommercialHub_MIDEAST_lights.PCX b/Art/Districts/Annotations/CommercialHub_MIDEAST_lights.PCX index cb7eaca4..4d7d2d0a 100644 Binary files a/Art/Districts/Annotations/CommercialHub_MIDEAST_lights.PCX and b/Art/Districts/Annotations/CommercialHub_MIDEAST_lights.PCX differ diff --git a/Art/Districts/Annotations/CommercialHub_ROMAN_lights.PCX b/Art/Districts/Annotations/CommercialHub_ROMAN_lights.PCX index 85e9beac..7c29bd4d 100644 Binary files a/Art/Districts/Annotations/CommercialHub_ROMAN_lights.PCX and b/Art/Districts/Annotations/CommercialHub_ROMAN_lights.PCX differ diff --git a/Art/Districts/Annotations/DataCenter_lights.PCX b/Art/Districts/Annotations/DataCenter_lights.PCX new file mode 100644 index 00000000..11401750 Binary files /dev/null and b/Art/Districts/Annotations/DataCenter_lights.PCX differ diff --git a/Art/Districts/Annotations/Encampment_lights.PCX b/Art/Districts/Annotations/Encampment_lights.PCX index e1bfde16..362ea29f 100644 Binary files a/Art/Districts/Annotations/Encampment_lights.PCX and b/Art/Districts/Annotations/Encampment_lights.PCX differ diff --git a/Art/Districts/Annotations/EnergyGrid_lights.PCX b/Art/Districts/Annotations/EnergyGrid_lights.PCX new file mode 100644 index 00000000..5d75302b Binary files /dev/null and b/Art/Districts/Annotations/EnergyGrid_lights.PCX differ diff --git a/Art/Districts/Annotations/GreatWall_lights.pcx b/Art/Districts/Annotations/GreatWall_lights.pcx new file mode 100644 index 00000000..28e5490b Binary files /dev/null and b/Art/Districts/Annotations/GreatWall_lights.pcx differ diff --git a/Art/Districts/Annotations/HolySite_MIDEAST_lights.PCX b/Art/Districts/Annotations/HolySite_MIDEAST_lights.PCX index 4d9edae8..18dc89e7 100644 Binary files a/Art/Districts/Annotations/HolySite_MIDEAST_lights.PCX and b/Art/Districts/Annotations/HolySite_MIDEAST_lights.PCX differ diff --git a/Art/Districts/Annotations/IndustrialZone_lights.PCX b/Art/Districts/Annotations/IndustrialZone_lights.PCX index a7b27c79..b0fbb619 100644 Binary files a/Art/Districts/Annotations/IndustrialZone_lights.PCX and b/Art/Districts/Annotations/IndustrialZone_lights.PCX differ diff --git a/Art/Districts/Annotations/MunicipalDistrict_lights.PCX b/Art/Districts/Annotations/MunicipalDistrict_lights.PCX new file mode 100644 index 00000000..ebeefb36 Binary files /dev/null and b/Art/Districts/Annotations/MunicipalDistrict_lights.PCX differ diff --git a/Art/Districts/Annotations/Neighborhood_MIDEAST_lights.PCX b/Art/Districts/Annotations/Neighborhood_MIDEAST_lights.PCX index 1ce50e82..2366f91b 100644 Binary files a/Art/Districts/Annotations/Neighborhood_MIDEAST_lights.PCX and b/Art/Districts/Annotations/Neighborhood_MIDEAST_lights.PCX differ diff --git a/Art/Districts/Annotations/Neighborhood_ROMAN_lights.PCX b/Art/Districts/Annotations/Neighborhood_ROMAN_lights.PCX index 73674047..fa1c585c 100644 Binary files a/Art/Districts/Annotations/Neighborhood_ROMAN_lights.PCX and b/Art/Districts/Annotations/Neighborhood_ROMAN_lights.PCX differ diff --git a/Art/Districts/Annotations/OffshoreExtractionZone_lights.PCX b/Art/Districts/Annotations/OffshoreExtractionZone_lights.PCX new file mode 100644 index 00000000..38a718a9 Binary files /dev/null and b/Art/Districts/Annotations/OffshoreExtractionZone_lights.PCX differ diff --git a/Art/Districts/Annotations/Park_lights.PCX b/Art/Districts/Annotations/Park_lights.PCX new file mode 100644 index 00000000..5cc309cc Binary files /dev/null and b/Art/Districts/Annotations/Park_lights.PCX differ diff --git a/Art/Districts/Annotations/Port_NE_lights.PCX b/Art/Districts/Annotations/Port_NE_lights.PCX new file mode 100644 index 00000000..95f4f2b2 Binary files /dev/null and b/Art/Districts/Annotations/Port_NE_lights.PCX differ diff --git a/Art/Districts/Annotations/Port_NW_lights.PCX b/Art/Districts/Annotations/Port_NW_lights.PCX new file mode 100644 index 00000000..9381b17e Binary files /dev/null and b/Art/Districts/Annotations/Port_NW_lights.PCX differ diff --git a/Art/Districts/Annotations/Port_SE_lights.PCX b/Art/Districts/Annotations/Port_SE_lights.PCX new file mode 100644 index 00000000..79bb300c Binary files /dev/null and b/Art/Districts/Annotations/Port_SE_lights.PCX differ diff --git a/Art/Districts/Annotations/Port_SW_lights.PCX b/Art/Districts/Annotations/Port_SW_lights.PCX new file mode 100644 index 00000000..98a633cb Binary files /dev/null and b/Art/Districts/Annotations/Port_SW_lights.PCX differ diff --git a/Art/Districts/Annotations/SkiResort_lights.PCX b/Art/Districts/Annotations/SkiResort_lights.PCX new file mode 100644 index 00000000..0f6e3781 Binary files /dev/null and b/Art/Districts/Annotations/SkiResort_lights.PCX differ diff --git a/Art/Districts/Annotations/WaterPark_lights.pcx b/Art/Districts/Annotations/WaterPark_lights.pcx new file mode 100644 index 00000000..768ccbcb Binary files /dev/null and b/Art/Districts/Annotations/WaterPark_lights.pcx differ diff --git a/Art/Districts/Annotations/Wonders_2_lights.PCX b/Art/Districts/Annotations/Wonders_2_lights.PCX index ba58f68d..b2ac983a 100644 Binary files a/Art/Districts/Annotations/Wonders_2_lights.PCX and b/Art/Districts/Annotations/Wonders_2_lights.PCX differ diff --git a/Art/Districts/Annotations/Wonders_3_lights.PCX b/Art/Districts/Annotations/Wonders_3_lights.PCX new file mode 100644 index 00000000..9049590e Binary files /dev/null and b/Art/Districts/Annotations/Wonders_3_lights.PCX differ diff --git a/Art/Districts/Annotations/Wonders_lights.PCX b/Art/Districts/Annotations/Wonders_lights.PCX index 1ae656b6..e5fc1ecb 100644 Binary files a/Art/Districts/Annotations/Wonders_lights.PCX and b/Art/Districts/Annotations/Wonders_lights.PCX differ diff --git a/Art/Districts/DistrictIncomeIcons.pcx b/Art/Districts/DistrictIncomeIcons.pcx index 12646304..8468deae 100644 Binary files a/Art/Districts/DistrictIncomeIcons.pcx and b/Art/Districts/DistrictIncomeIcons.pcx differ diff --git a/Art/Districts/WorkerDistrictButtonsAlpha.pcx b/Art/Districts/WorkerDistrictButtonsAlpha.pcx index cafd3435..0a444f4f 100644 Binary files a/Art/Districts/WorkerDistrictButtonsAlpha.pcx and b/Art/Districts/WorkerDistrictButtonsAlpha.pcx differ diff --git a/Art/Districts/WorkerDistrictButtonsHighlighted.pcx b/Art/Districts/WorkerDistrictButtonsHighlighted.pcx index d756610d..acbed314 100644 Binary files a/Art/Districts/WorkerDistrictButtonsHighlighted.pcx and b/Art/Districts/WorkerDistrictButtonsHighlighted.pcx differ diff --git a/Art/Districts/WorkerDistrictButtonsNorm.pcx b/Art/Districts/WorkerDistrictButtonsNorm.pcx index 89c89c52..c2eaeb89 100644 Binary files a/Art/Districts/WorkerDistrictButtonsNorm.pcx and b/Art/Districts/WorkerDistrictButtonsNorm.pcx differ diff --git a/Art/Districts/WorkerDistrictButtonsRollover.pcx b/Art/Districts/WorkerDistrictButtonsRollover.pcx index cd3e482b..3b3f1aa6 100644 Binary files a/Art/Districts/WorkerDistrictButtonsRollover.pcx and b/Art/Districts/WorkerDistrictButtonsRollover.pcx differ diff --git a/Art/TileHighlights.pcx b/Art/TileHighlights.pcx index 833d95a3..e9b75320 100644 Binary files a/Art/TileHighlights.pcx and b/Art/TileHighlights.pcx differ diff --git a/C3X.h b/C3X.h index 77edda2f..d3b9c505 100644 --- a/C3X.h +++ b/C3X.h @@ -17,11 +17,14 @@ typedef unsigned char byte; #define MAX_BUILDING_PREREQS_FOR_UNIT 10 #define COUNT_SPECIAL_DISTRICT_TYPES 10 -#define USED_SPECIAL_DISTRICT_TYPES 5 +#define USED_SPECIAL_DISTRICT_TYPES 11 #define MAX_DYNAMIC_DISTRICT_TYPES 22 #define COUNT_DISTRICT_TYPES (COUNT_SPECIAL_DISTRICT_TYPES + MAX_DYNAMIC_DISTRICT_TYPES) #define MAX_WONDER_DISTRICT_TYPES 32 #define MAX_NATURAL_WONDER_DISTRICT_TYPES 32 +#define MAX_DISTRICT_VARIANT_COUNT 5 +#define MAX_DISTRICT_ERA_COUNT 4 +#define MAX_DISTRICT_COLUMN_COUNT 10 #define C3X_DISTRICT_COMMAND_BASE (-11000000) // Initialize to zero. Implementation is in common.c @@ -150,6 +153,21 @@ enum day_night_cycle_mode { DNCM_SPECIFIED }; +enum distribution_hub_yield_division_mode { + DHYDM_FLAT = 0, + DHYDM_SCALE_BY_CITY_COUNT +}; + +enum ai_distribution_hub_build_strategy { + ADHBS_AUTO = 0, + ADHBS_BY_CITY_COUNT +}; + +enum ai_auto_build_great_wall_strategy { + AAGWS_ALL_BORDERS = 0, + AAGWS_OTHER_CIV_BORDERED_ONLY +}; + enum perfume_kind { PK_PRODUCTION = 0, PK_TECHNOLOGY, @@ -206,6 +224,7 @@ struct c3x_config { bool warn_about_unrecognized_names; bool enable_ai_production_ranking; bool enable_ai_city_location_desirability_display; + bool show_ai_city_location_desirability_if_settler; bool zero_corruption_when_off; bool disallow_land_units_from_affecting_water_tiles; bool dont_end_units_turn_after_airdrop; @@ -285,6 +304,7 @@ struct c3x_config { bool allow_sale_of_aqueducts_and_hospitals; bool no_cross_shore_detection; int city_work_radius; + bool auto_zoom_city_screen_for_large_work_areas; enum work_area_limit work_area_limit; struct work_area_improvement * work_area_improvements; int count_work_area_improvements; @@ -338,6 +358,7 @@ struct c3x_config { bool patch_disease_stopping_tech_flag_bug; bool patch_division_by_zero_in_ai_alliance_eval; bool patch_empty_army_movement; + bool patch_empty_army_combat_crash; bool delete_off_map_ai_units; bool fix_overlapping_specialist_yield_icons; bool patch_premature_truncation_of_found_paths; @@ -350,7 +371,13 @@ struct c3x_config { bool prevent_autorazing; bool prevent_razing_by_players; + bool allow_extraterritorial_colonies; + int per_extraterritorial_colony_relation_penalty; + bool draw_forests_over_roads_and_railroads; + + bool enable_named_tiles; + char * aircraft_victory_animation; // NULL if set to "none" in config int day_night_cycle_mode; @@ -359,6 +386,7 @@ struct c3x_config { int pinned_hour_for_day_night_cycle; bool enable_natural_wonders; + bool add_natural_wonders_to_scenarios_if_none; bool show_natural_wonder_name_on_map; int minimum_natural_wonder_separation; @@ -367,25 +395,57 @@ struct c3x_config { bool enable_wonder_districts; bool enable_distribution_hub_districts; bool enable_aerodrome_districts; + bool enable_port_districts; + bool enable_bridge_districts; + bool enable_canal_districts; + bool enable_central_rail_hub_districts; + bool enable_energy_grid_districts; + bool enable_great_wall_districts; bool cities_with_mutual_district_receive_buildings; bool cities_with_mutual_district_receive_wonders; bool show_message_when_building_received_by_mutual_district; + bool show_message_when_building_lost_to_destroyed_district; bool air_units_use_aerodrome_districts_not_cities; + bool naval_units_use_port_districts_not_cities; int maximum_pop_before_neighborhood_needed; int per_neighborhood_pop_growth_enabled; int neighborhood_needed_message_frequency; + bool destroying_neighborhood_reduces_pop; bool completed_wonder_districts_can_be_destroyed; bool destroyed_wonders_can_be_built_again; + int distribution_hub_yield_division_mode; int distribution_hub_food_yield_divisor; int distribution_hub_shield_yield_divisor; + int ai_distribution_hub_build_strategy; int ai_ideal_distribution_hub_count_per_100_cities; + int max_distribution_hub_count_per_100_cities; + int central_rail_hub_distribution_food_bonus_percent; + int central_rail_hub_distribution_shield_bonus_percent; + + bool workers_can_enter_coast; + bool expand_water_tile_checks_to_city_work_area; + int max_contiguous_bridge_districts; + int max_contiguous_canal_districts; + int ai_canal_eval_min_bisected_land_tiles; + int ai_bridge_canal_eval_block_size; + int ai_bridge_eval_lake_tile_threshold; + bool ai_can_replace_existing_districts_with_canals; + bool ai_builds_bridges; + bool ai_builds_canals; bool ai_defends_districts; + int ai_city_district_max_build_wait_turns; + + bool disable_great_wall_city_defense_bonus; + bool great_wall_districts_impassible_by_others; + bool auto_build_great_wall_around_territory; + int great_wall_auto_build_wonder_improv_id; + int ai_auto_build_great_wall_strategy; bool enable_city_work_radii_highlights; }; @@ -498,13 +558,21 @@ enum c3x_label { CL_BUILDING, // Districts-related texts - CL_REQUIRES_NEIGHBORHOOD_TO_GROW, + CL_REQUIRES, + CL_TO_GROW, CL_DISTRICT_DESTROYED_BY_VOLCANO, CL_CONSTRUCTION_HALTED_DUE_TO_MISSING_DISTRICT, + CL_LOST_POPULATION_DUE_TO_DESTROYED_NEIGHBORHOOD, + CL_RECEIVED, CL_FROM_SHARED, CL_WITH, + CL_APOSTROPHE_S, + CL_AND, + CL_OTHER_BUILDINGS_HAVE_BEEN, + CL_LOST_DUE_TO_DESTROYED, + // Districts config mismatch checked on game load CL_DISTRICT_ID, CL_DISTRICT_IN_SAVE_BUT_MISSING_NOW, @@ -516,6 +584,13 @@ enum c3x_label { CL_DISTRICTS_IN_SAVE_FILE, CL_CURRENTLY_CONFIGURED_DISTRICTS, + // Maritime district pillaging button tooltip + CL_PILLAGE, + + // Tile naming + CL_NAME_TILE, + CL_RENAME_TILE, + // "Action" for passenger units CL_TRANSPORTED, @@ -579,20 +654,94 @@ enum { MAX_DISTRICT_DEPENDENTS = 64 }; +enum { + DEFAULT_DISTRICT_BUILDABLE_MASK = (1 << SQ_Desert) | (1 << SQ_Plains) | (1 << SQ_Grassland) | (1 << SQ_Tundra) | (1 << SQ_FloodPlain) | (1 << SQ_Hills) +}; + +enum { + MAX_DISTRICT_BONUS_ENTRIES = 16 +}; + +enum district_bonus_entry_type { + DBET_TILE = 0, + DBET_BUILDING = 1 +}; + +enum great_wall_auto_build_state { + GWABS_NOT_STARTED = 0, + GWABS_RUNNING, + GWABS_DONE +}; + +struct district_bonus_entry { + enum district_bonus_entry_type type; + int bonus; + enum SquareTypes tile_type; + int building_id; + char const * building_name; +}; + +struct district_bonus_list { + int count; + struct district_bonus_entry entries[MAX_DISTRICT_BONUS_ENTRIES]; +}; + +enum district_render_strategy { + DRS_BY_COUNT = 0, + DRS_BY_BUILDING = 1 +}; + +enum district_ai_build_strategy { + DABS_DISTRICT = 0, + DABS_TILE_IMPROVEMENT = 1 +}; + struct district_config { enum Unit_Command_Values command; char const * name; + char const * display_name; char const * tooltip; - char const * advance_prereq; + char const * advance_prereqs[MAX_DISTRICT_DEPENDENTS]; + int advance_prereq_count; + char const * obsoleted_by; + char const * resource_prereqs[MAX_DISTRICT_DEPENDENTS]; + char const * resource_prereq_on_tile; char const * dependent_improvements[MAX_DISTRICT_DEPENDENTS]; + char const * wonder_prereqs[MAX_DISTRICT_DEPENDENTS]; + char const * natural_wonder_prereqs[MAX_DISTRICT_DEPENDENTS]; + char const * buildable_on_districts[MAX_DISTRICT_DEPENDENTS]; + char const * buildable_adjacent_to_districts[MAX_DISTRICT_DEPENDENTS]; char const * img_paths[10]; + unsigned int buildable_square_types_mask; + unsigned int buildable_adjacent_to_square_types_mask; + unsigned int buildable_on_overlays_mask; + unsigned int buildable_only_on_overlays_mask; + unsigned int buildable_adjacent_to_overlays_mask; + bool buildable_adjacent_to_allows_city; bool allow_multiple; bool vary_img_by_era; bool vary_img_by_culture; + enum district_render_strategy render_strategy; + enum district_ai_build_strategy ai_build_strategy; bool is_dynamic; - int dependent_improvement_count; + bool align_to_coast; + bool draw_over_resources; + bool allow_irrigation_from; + bool auto_add_road; + bool auto_add_railroad; + int custom_width; + int custom_height; + int x_offset; + int y_offset; + int resource_prereq_count; + int dependent_improvement_max_index; + int wonder_prereq_count; + int natural_wonder_prereq_count; + int buildable_on_district_count; + int buildable_adjacent_to_district_count; int img_path_count; - int max_building_index; + int img_column_count; + bool has_img_column_count_override; int btn_tile_sheet_column; int btn_tile_sheet_row; int culture_bonus; @@ -600,7 +749,43 @@ struct district_config { int food_bonus; int gold_bonus; int shield_bonus; + int happiness_bonus; + struct district_bonus_list culture_bonus_extras; + struct district_bonus_list science_bonus_extras; + struct district_bonus_list food_bonus_extras; + struct district_bonus_list gold_bonus_extras; + struct district_bonus_list shield_bonus_extras; + struct district_bonus_list happiness_bonus_extras; + struct district_bonus_list defense_bonus_extras; int defense_bonus_percent; + bool heal_units_in_one_turn; + char const * generated_resource; + int generated_resource_id; + short generated_resource_flags; + int buildable_on_district_ids[MAX_DISTRICT_DEPENDENTS]; + int buildable_on_district_id_count; + bool has_buildable_on_districts; + int buildable_adjacent_to_district_ids[MAX_DISTRICT_DEPENDENTS]; + int buildable_adjacent_to_district_id_count; + bool has_buildable_adjacent_to; + bool has_buildable_adjacent_to_districts; + bool has_buildable_on_overlays; + bool has_buildable_only_on_overlays; + bool has_buildable_adjacent_to_overlays; + char const * buildable_by_civs[32]; + int buildable_by_civ_count; + bool has_buildable_by_civs; + int buildable_by_civ_traits_ids[8]; + int buildable_by_civ_traits_id_count; + bool has_buildable_by_civ_traits; + int buildable_by_civ_govs_ids[5]; + int buildable_by_civ_govs_id_count; + bool has_buildable_by_civ_govs; + int buildable_by_civ_cultures_ids[5]; + int buildable_by_civ_cultures_id_count; + bool has_buildable_by_civ_cultures; + bool buildable_by_war_allies; + bool buildable_by_pact_allies; }; struct wonder_district_config { @@ -610,13 +795,43 @@ struct wonder_district_config { img_row, img_column, img_construct_row, - img_construct_column; + img_construct_column, + img_alt_dir_construct_row, + img_alt_dir_construct_column, + img_alt_dir_row, + img_alt_dir_column; + unsigned int buildable_square_types_mask; + unsigned int buildable_only_on_overlays_mask; + unsigned int buildable_adjacent_to_square_types_mask; + unsigned int buildable_adjacent_to_overlays_mask; + bool buildable_adjacent_to_allows_city; + bool enable_img_alt_dir; bool is_dynamic; + bool has_buildable_only_on_overlays; + bool has_buildable_adjacent_to; + bool has_buildable_adjacent_to_overlays; }; enum square_type_extras { SQ_INVALID = -1, - SQ_RIVER = SQ_Ocean + 1 + SQ_RIVER = SQ_Ocean + 1, + SQ_SNOW_VOLCANO, + SQ_SNOW_FOREST, + SQ_SNOW_MOUNTAIN +}; + +enum district_overlay_mask_bits { + DOM_MINE = 1u << 0, + DOM_IRRIGATION = 1u << 1, + DOM_FORTRESS = 1u << 2, + DOM_BARRICADE = 1u << 3, + DOM_OUTPOST = 1u << 4, + DOM_RADAR_TOWER = 1u << 5, + DOM_JUNGLE = 1u << 6, + DOM_FOREST = 1u << 7, + DOM_SWAMP = 1u << 8, + DOM_RIVER = 1u << 9, + DOM_AIRFIELD = 1u << 10 }; struct natural_wonder_district_config { @@ -633,6 +848,7 @@ struct natural_wonder_district_config { int food_bonus; int gold_bonus; int shield_bonus; + int happiness_bonus; bool is_dynamic; }; @@ -654,80 +870,237 @@ struct wonder_location { const struct district_config special_district_defaults[USED_SPECIAL_DISTRICT_TYPES] = { { - .command = UCV_Build_Neighborhood, .name = "Neighborhood", .tooltip = "Build Neighborhood", - .advance_prereq = NULL, .allow_multiple = true, .vary_img_by_era = true, .vary_img_by_culture = true, .is_dynamic = false, .dependent_improvement_count = 0, .dependent_improvements = {0}, - .img_paths = {"Neighborhood_AMER.pcx", "Neighborhood_EURO.pcx", "Neighborhood_ROMAN.pcx", "Neighborhood_MIDEAST.pcx", "Neighborhood_ASIAN.pcx", "Neighborhood_Abandoned.pcx"}, - .img_path_count = 6, .max_building_index = 3, .btn_tile_sheet_column = 0, .btn_tile_sheet_row = 0, - .culture_bonus = 1, .science_bonus = 1, .food_bonus = 0, .gold_bonus = 1, .shield_bonus = 0, .defense_bonus_percent = 25 - + .command = UCV_Build_Neighborhood, .name = "Neighborhood", .tooltip = "Build Neighborhood", .display_name = "Neighborhood", + .advance_prereqs = {0}, .advance_prereq_count = 0, .resource_prereqs = {0}, .resource_prereq_on_tile = NULL, .allow_multiple = true, .vary_img_by_era = true, .vary_img_by_culture = true, .is_dynamic = false, .resource_prereq_count = 0, .dependent_improvement_max_index = 0, .dependent_improvements = {0}, + .img_paths = {"Neighborhood_AMER.pcx", "Neighborhood_EURO.pcx", "Neighborhood_ROMAN.pcx", "Neighborhood_MIDEAST.pcx", "Neighborhood_ASIAN.pcx"}, + .buildable_square_types_mask = DEFAULT_DISTRICT_BUILDABLE_MASK, + .img_path_count = 5, .img_column_count = 4, .btn_tile_sheet_column = 0, .btn_tile_sheet_row = 0, + .culture_bonus = 1, .science_bonus = 1, .food_bonus = 0, .gold_bonus = 1, .shield_bonus = 0, .happiness_bonus = 0, .defense_bonus_percent = 25, + .generated_resource = NULL, .generated_resource_id = -1, .generated_resource_flags = 0 + }, { - .command = UCV_Build_WonderDistrict, .name = "Wonder District", .tooltip = "Build Wonder District", - .advance_prereq = NULL, .allow_multiple = true, .vary_img_by_era = true, .vary_img_by_culture = false, .is_dynamic = false, .dependent_improvement_count = 0, .dependent_improvements = {0}, + .command = UCV_Build_WonderDistrict, .name = "Wonder District", .tooltip = "Build Wonder District", .display_name = "Wonder District", + .advance_prereqs = {0}, .advance_prereq_count = 0, .resource_prereqs = {0}, .resource_prereq_on_tile = NULL, .allow_multiple = true, .vary_img_by_era = true, .vary_img_by_culture = false, .is_dynamic = false, .resource_prereq_count = 0, .dependent_improvement_max_index = 0, .dependent_improvements = {0}, .img_paths = {"WonderDistrict.pcx"}, - .img_path_count = 1, .max_building_index = 0, .btn_tile_sheet_column = 1, .btn_tile_sheet_row = 0, - .culture_bonus = 0, .science_bonus = 0, .food_bonus = 0, .gold_bonus = 0, .shield_bonus = 0, .defense_bonus_percent = 0 - + .buildable_square_types_mask = (unsigned int)(DEFAULT_DISTRICT_BUILDABLE_MASK | (1 << SQ_Coast) | (1 << SQ_Mountains)), + .img_path_count = 1, .img_column_count = 1, .btn_tile_sheet_column = 1, .btn_tile_sheet_row = 0, + .culture_bonus = 0, .science_bonus = 0, .food_bonus = 0, .gold_bonus = 0, .shield_bonus = 0, .happiness_bonus = 0, .defense_bonus_percent = 0, + .generated_resource = NULL, .generated_resource_id = -1, .generated_resource_flags = 0 + }, { - .command = UCV_Build_DistributionHub, .name = "Distribution Hub", .tooltip = "Build Distribution Hub", - .advance_prereq = "Construction", .allow_multiple = true, .vary_img_by_era = true, .vary_img_by_culture = false, .is_dynamic = false, .dependent_improvement_count = 0, .dependent_improvements = {0}, + .command = UCV_Build_DistributionHub, .name = "Distribution Hub", .tooltip = "Build Distribution Hub", .display_name = "Distribution Hub", + .advance_prereqs = {"Construction"}, .advance_prereq_count = 1, .resource_prereqs = {0}, .resource_prereq_on_tile = NULL, .allow_multiple = true, .vary_img_by_era = true, .vary_img_by_culture = false, .is_dynamic = false, .resource_prereq_count = 0, .dependent_improvement_max_index = 0, .dependent_improvements = {0}, .img_paths = {"DistributionHub.pcx"}, - .img_path_count = 1, .max_building_index = 0, .btn_tile_sheet_column = 2, .btn_tile_sheet_row = 0, - .culture_bonus = 0, .science_bonus = 0, .food_bonus = 0, .gold_bonus = 0, .shield_bonus = 0, .defense_bonus_percent = 0 - + .buildable_square_types_mask = DEFAULT_DISTRICT_BUILDABLE_MASK, + .img_path_count = 1, .img_column_count = 1, .btn_tile_sheet_column = 2, .btn_tile_sheet_row = 0, + .culture_bonus = 0, .science_bonus = 0, .food_bonus = 0, .gold_bonus = 0, .shield_bonus = 0, .happiness_bonus = 0, .defense_bonus_percent = 0, + .generated_resource = NULL, .generated_resource_id = -1, .generated_resource_flags = 0 + }, { - .command = UCV_Build_Aerodrome, .name = "Aerodrome", .tooltip = "Build Aerodrome", - .advance_prereq = "Flight", .allow_multiple = true, .vary_img_by_era = true, .vary_img_by_culture = false, .is_dynamic = false, .dependent_improvement_count = 0, .dependent_improvements = {0}, + .command = UCV_Build_Aerodrome, .name = "Aerodrome", .tooltip = "Build Aerodrome", .display_name = "Aerodrome", + .advance_prereqs = {"Flight"}, .advance_prereq_count = 1, .resource_prereqs = {0}, .resource_prereq_on_tile = NULL, .allow_multiple = true, .vary_img_by_era = true, .vary_img_by_culture = false, .is_dynamic = false, .resource_prereq_count = 0, .dependent_improvement_max_index = 1, .img_paths = {"Aerodrome.pcx"}, .dependent_improvements = {"Airport"}, - .img_path_count = 1, .max_building_index = 0, .btn_tile_sheet_column = 3, .btn_tile_sheet_row = 0, - .culture_bonus = 0, .science_bonus = 0, .food_bonus = 0, .gold_bonus = 0, .shield_bonus = 0, .defense_bonus_percent = 0 + .buildable_square_types_mask = DEFAULT_DISTRICT_BUILDABLE_MASK, + .img_path_count = 1, .img_column_count = 2, .btn_tile_sheet_column = 3, .btn_tile_sheet_row = 0, + .culture_bonus = 0, .science_bonus = 0, .food_bonus = 0, .gold_bonus = 0, .shield_bonus = 0, .happiness_bonus = 0, .defense_bonus_percent = 0, + .generated_resource = NULL, .generated_resource_id = -1, .generated_resource_flags = 0 }, { - .command = -1, .name = "Natural Wonder", .tooltip = NULL, - .advance_prereq = NULL, .allow_multiple = true, .vary_img_by_era = false, .vary_img_by_culture = false, .is_dynamic = false, .dependent_improvement_count = 0, .dependent_improvements = {0}, + .command = -1, .name = "Natural Wonder", .tooltip = NULL, .display_name = "Natural Wonder", + .advance_prereqs = {0}, .advance_prereq_count = 0, .resource_prereqs = {0}, .resource_prereq_on_tile = NULL, .allow_multiple = true, .vary_img_by_era = false, .vary_img_by_culture = false, .is_dynamic = false, .resource_prereq_count = 0, .dependent_improvement_max_index = 0, .dependent_improvements = {0}, .img_paths = {0}, - .img_path_count = 0, .max_building_index = 0, .btn_tile_sheet_column = 0, .btn_tile_sheet_row = 0, - .culture_bonus = 0, .science_bonus = 0, .food_bonus = 0, .gold_bonus = 0, .shield_bonus = 0, .defense_bonus_percent = 0 + .buildable_square_types_mask = DEFAULT_DISTRICT_BUILDABLE_MASK, + .img_path_count = 0, .img_column_count = 0, .btn_tile_sheet_column = 0, .btn_tile_sheet_row = 0, + .culture_bonus = 0, .science_bonus = 0, .food_bonus = 0, .gold_bonus = 0, .shield_bonus = 0, .happiness_bonus = 0, .defense_bonus_percent = 0, + .generated_resource = NULL, .generated_resource_id = -1, .generated_resource_flags = 0 + }, + { + .command = UCV_Build_Port, .name = "Port", .tooltip = "Build Port", .display_name = "Port", + .advance_prereqs = {"Map Making"}, .advance_prereq_count = 1, .resource_prereqs = {0}, .resource_prereq_on_tile = NULL, .allow_multiple = true, .vary_img_by_era = true, .vary_img_by_culture = false, .is_dynamic = false, .resource_prereq_count = 0, .dependent_improvement_max_index = 2, .align_to_coast = true, + .img_paths = {"Port_NW.pcx", "Port_NE.pcx", "Port_SE.pcx", "Port_SW.pcx"}, .dependent_improvements = {"Harbor", "Commercial Dock"}, + .buildable_square_types_mask = (1 << SQ_Coast), + .img_path_count = 4, .img_column_count = 4, .btn_tile_sheet_column = 4, .btn_tile_sheet_row = 0, .align_to_coast = true, + .culture_bonus = 0, .science_bonus = 0, .food_bonus = 0, .gold_bonus = 0, .shield_bonus = 0, .happiness_bonus = 0, .defense_bonus_percent = 0, + .generated_resource = NULL, .generated_resource_id = -1, .generated_resource_flags = 0 + }, + { + .command = UCV_Build_CentralRailHub, .name = "Central Rail Hub", .tooltip = "Build Central Rail Hub", .display_name = "Central Rail Hub", + .advance_prereqs = {"Steam Power"}, .advance_prereq_count = 1, .resource_prereqs = {"Iron", "Coal"}, .resource_prereq_on_tile = NULL, .allow_multiple = true, .vary_img_by_era = true, .vary_img_by_culture = false, .is_dynamic = false, .resource_prereq_count = 2, .dependent_improvement_max_index = 0, + .img_paths = {"CentralRailHub_AMER.pcx", "CentralRailHub_EURO.pcx", "CentralRailHub_ROMAN.pcx", "CentralRailHub_MIDEAST.pcx", "CentralRailHub_ASIAN.pcx"}, + .buildable_square_types_mask = DEFAULT_DISTRICT_BUILDABLE_MASK, .auto_add_road = true, .auto_add_railroad = true, + .img_path_count = 5, .img_column_count = 2, .btn_tile_sheet_column = 5, .btn_tile_sheet_row = 0, + .culture_bonus = 0, .science_bonus = 0, .food_bonus = 0, .gold_bonus = 0, .shield_bonus = 0, .happiness_bonus = 0, .defense_bonus_percent = 0, + .generated_resource = NULL, .generated_resource_id = -1, .generated_resource_flags = 0 + }, + { + .command = UCV_Build_EnergyGrid, .name = "Energy Grid", .tooltip = "Build Energy Grid", .display_name = "Energy Grid", + .advance_prereqs = {"Industrialization"}, .advance_prereq_count = 1, .resource_prereqs = {0}, .resource_prereq_on_tile = NULL, .allow_multiple = true, .vary_img_by_era = true, .vary_img_by_culture = false, .is_dynamic = false, .resource_prereq_count = 0, .dependent_improvement_max_index = 4, + .img_paths = {"EnergyGrid.pcx"}, .dependent_improvements = {"Coal Plant", "Hydro Plant", "Nuclear Plant", "Solar Plant"}, + .buildable_square_types_mask = DEFAULT_DISTRICT_BUILDABLE_MASK, .custom_height = 84, + .img_path_count = 1, .img_column_count = 5, .btn_tile_sheet_column = 6, .btn_tile_sheet_row = 0, + .culture_bonus = 0, .science_bonus = 0, .food_bonus = 0, .gold_bonus = 0, .shield_bonus = 2, .happiness_bonus = 0, .defense_bonus_percent = 0, + .generated_resource = NULL, .generated_resource_id = -1, .generated_resource_flags = 0 + }, + { + .command = UCV_Build_Bridge, .name = "Bridge", .tooltip = "Build Bridge", .display_name = "Bridge", + .advance_prereqs = {"Industrialization"}, .advance_prereq_count = 1, .resource_prereqs = {0}, .resource_prereq_on_tile = NULL, .allow_multiple = true, .vary_img_by_era = true, .vary_img_by_culture = false, .is_dynamic = false, .resource_prereq_count = 0, .dependent_improvement_max_index = 0, + .img_paths = {"Bridge.pcx"}, .dependent_improvements = {0}, .custom_width = 176, .custom_height = 112, .y_offset = 24, .x_offset = 0, + .buildable_square_types_mask = (1 << SQ_Coast), .auto_add_road = true, + .img_path_count = 1, .img_column_count = 9, .btn_tile_sheet_column = 7, .btn_tile_sheet_row = 0, + .culture_bonus = 0, .science_bonus = 0, .food_bonus = 0, .gold_bonus = 0, .shield_bonus = 0, .happiness_bonus = 0, .defense_bonus_percent = 0, + .generated_resource = NULL, .generated_resource_id = -1, .generated_resource_flags = 0 + }, + { + .command = UCV_Build_Canal, .name = "Canal", .tooltip = "Build Canal", .display_name = "Canal", + .advance_prereqs = {"Industrialization"}, .advance_prereq_count = 1, .resource_prereqs = {0}, .resource_prereq_on_tile = NULL, .allow_multiple = true, .vary_img_by_era = true, .vary_img_by_culture = false, .is_dynamic = false, .resource_prereq_count = 0, .dependent_improvement_max_index = 0, + .img_paths = {"Canal.pcx"}, .dependent_improvements = {0}, .custom_width = 176, .custom_height = 112, .y_offset = 24, .x_offset = 0, + .buildable_square_types_mask = (1 << SQ_Desert) | (1 << SQ_Plains) | (1 << SQ_Grassland) | (1 << SQ_Tundra) | (1 << SQ_FloodPlain), + .img_path_count = 1, .img_column_count = 9, .btn_tile_sheet_column = 8, .btn_tile_sheet_row = 0, + .culture_bonus = 0, .science_bonus = 0, .food_bonus = 0, .gold_bonus = 0, .shield_bonus = 0, .happiness_bonus = 0, .defense_bonus_percent = 0, + .generated_resource = NULL, .generated_resource_id = -1, .generated_resource_flags = 0 + }, + { + .command = UCV_Build_GreatWall, .name = "Great Wall", .tooltip = "Build Great Wall", .display_name = "Great Wall", + .advance_prereqs = {0}, .advance_prereq_count = 0, .obsoleted_by = "Metallurgy", .resource_prereqs = {0}, .resource_prereq_on_tile = NULL, .allow_multiple = true, .vary_img_by_era = false, .vary_img_by_culture = false, .is_dynamic = false, .resource_prereq_count = 0, .dependent_improvement_max_index = 0, + .img_paths = {"GreatWall.pcx"}, .dependent_improvements = {0}, .custom_height = 88, .wonder_prereqs = {"The Great Wall"}, .wonder_prereq_count = 1, + .buildable_square_types_mask = (unsigned int)(DEFAULT_DISTRICT_BUILDABLE_MASK | (1 << SQ_Mountains) | (1 << SQ_Forest) | (1 << SQ_Swamp) | (1 << SQ_Jungle) | (1 << SQ_Volcano)), .draw_over_resources = true, + .img_path_count = 1, .img_column_count = 9, .btn_tile_sheet_column = 9, .btn_tile_sheet_row = 0, + .culture_bonus = 0, .science_bonus = 0, .food_bonus = 0, .gold_bonus = 0, .shield_bonus = 0, .happiness_bonus = 0, .defense_bonus_percent = 50, + .generated_resource = NULL, .generated_resource_id = -1, .generated_resource_flags = 0 } }; struct parsed_district_definition { char * name; + char * display_name; char * tooltip; - char * advance_prereq; + char * advance_prereqs[5]; + int advance_prereq_count; + char * obsoleted_by; + char * resource_prereqs[5]; + char * resource_prereq_on_tile; char * dependent_improvements[5]; + char * wonder_prereqs[5]; + char * natural_wonder_prereqs[5]; + char * buildable_on_districts[5]; + char * buildable_adjacent_to_districts[5]; char * img_paths[5]; - int dependent_improvement_count; + int resource_prereq_count; + int dependent_improvement_max_index; + int wonder_prereq_count; + int natural_wonder_prereq_count; + int buildable_on_district_count; + int buildable_adjacent_to_district_count; int img_path_count; + int img_column_count; bool allow_multiple; bool vary_img_by_era; bool vary_img_by_culture; + enum district_render_strategy render_strategy; + enum district_ai_build_strategy ai_build_strategy; + bool align_to_coast; + bool draw_over_resources; + bool allow_irrigation_from; + bool auto_add_road; + bool auto_add_railroad; + int custom_width; + int custom_height; + int x_offset; + int y_offset; int btn_tile_sheet_column; int btn_tile_sheet_row; int defense_bonus_percent; + bool heal_units_in_one_turn; int culture_bonus; int science_bonus; int food_bonus; int gold_bonus; int shield_bonus; + int happiness_bonus; + struct district_bonus_list culture_bonus_extras; + struct district_bonus_list science_bonus_extras; + struct district_bonus_list food_bonus_extras; + struct district_bonus_list gold_bonus_extras; + struct district_bonus_list shield_bonus_extras; + struct district_bonus_list happiness_bonus_extras; + struct district_bonus_list defense_bonus_extras; + unsigned int buildable_square_types_mask; + unsigned int buildable_adjacent_to_square_types_mask; + unsigned int buildable_on_overlays_mask; + unsigned int buildable_only_on_overlays_mask; + unsigned int buildable_adjacent_to_overlays_mask; + bool buildable_adjacent_to_allows_city; + char * buildable_by_civs[32]; + int buildable_by_civ_count; + bool buildable_by_war_allies; + bool buildable_by_pact_allies; bool has_name; bool has_tooltip; - bool has_advance_prereq; + bool has_advance_prereqs; + bool has_obsoleted_by; + bool has_resource_prereqs; bool has_dependent_improvements; + bool has_wonder_prereqs; + bool has_natural_wonder_prereqs; + bool has_display_name; bool has_img_paths; + bool has_img_column_count; bool has_allow_multiple; bool has_vary_img_by_era; bool has_vary_img_by_culture; + bool has_render_strategy; + bool has_ai_build_strategy; + bool has_align_to_coast; + bool has_draw_over_resources; + bool has_custom_width; + bool has_custom_height; + bool has_x_offset; + bool has_y_offset; bool has_btn_tile_sheet_column; bool has_btn_tile_sheet_row; bool has_defense_bonus_percent; + bool has_heal_units_in_one_turn; bool has_culture_bonus; bool has_science_bonus; bool has_food_bonus; bool has_gold_bonus; bool has_shield_bonus; + bool has_happiness_bonus; + bool has_buildable_on; + bool has_buildable_adjacent_to; + bool has_buildable_on_overlays; + bool has_buildable_only_on_overlays; + bool has_buildable_adjacent_to_overlays; + bool has_resource_prereq_on_tile; + bool has_buildable_by_civs; + bool has_buildable_by_war_allies; + bool has_buildable_by_pact_allies; + char * generated_resource; + short generated_resource_flags; + bool has_generated_resource; + char * buildable_by_civ_traits[10]; + int buildable_by_civ_traits_count; + int buildable_by_civ_traits_ids[10]; + int buildable_by_civ_traits_id_count; + bool has_buildable_by_civ_traits; + char * buildable_by_civ_govs[10]; + int buildable_by_civ_govs_count; + int buildable_by_civ_govs_ids[32]; + int buildable_by_civ_govs_id_count; + bool has_buildable_by_civ_govs; + char * buildable_by_civ_cultures[5]; + int buildable_by_civ_cultures_count; + int buildable_by_civ_cultures_ids[5]; + int buildable_by_civ_cultures_id_count; + bool has_buildable_by_civ_cultures; + bool has_buildable_on_districts; + bool has_buildable_adjacent_to_districts; + bool has_allow_irrigation_from; + bool has_auto_add_road; + bool has_auto_add_railroad; }; struct parsed_wonder_definition { @@ -737,12 +1110,31 @@ struct parsed_wonder_definition { int img_column; int img_construct_row; int img_construct_column; + int img_alt_dir_construct_row; + int img_alt_dir_construct_column; + int img_alt_dir_row; + int img_alt_dir_column; + bool enable_img_alt_dir; + unsigned int buildable_square_types_mask; + unsigned int buildable_only_on_overlays_mask; + unsigned int buildable_adjacent_to_square_types_mask; + unsigned int buildable_adjacent_to_overlays_mask; + bool buildable_adjacent_to_allows_city; bool has_name; bool has_img_path; bool has_img_row; bool has_img_column; bool has_img_construct_row; bool has_img_construct_column; + bool has_img_alt_dir_construct_row; + bool has_img_alt_dir_construct_column; + bool has_img_alt_dir_row; + bool has_img_alt_dir_column; + bool has_enable_img_alt_dir; + bool has_buildable_on; + bool has_buildable_only_on_overlays; + bool has_buildable_adjacent_to; + bool has_buildable_adjacent_to_overlays; }; struct parsed_natural_wonder_definition { @@ -758,6 +1150,7 @@ struct parsed_natural_wonder_definition { int food_bonus; int gold_bonus; int shield_bonus; + int happiness_bonus; bool has_name; bool has_img_path; bool has_img_row; @@ -770,6 +1163,7 @@ struct parsed_natural_wonder_definition { bool has_food_bonus; bool has_gold_bonus; bool has_shield_bonus; + bool has_happiness_bonus; }; struct scenario_district_entry { @@ -784,6 +1178,14 @@ struct scenario_district_entry { int has_wonder_name; }; +struct scenario_named_tile_entry { + int tile_x; + int tile_y; + int has_coordinates; + char * name; + int has_name; +}; + struct distribution_hub_record { Tile * tile; int tile_x; @@ -800,6 +1202,11 @@ struct ai_best_feasible_order { int value; }; +struct district_building_prereq_list { + int count; + int district_ids[MAX_DISTRICT_DEPENDENTS]; +}; + struct pending_district_request { City * city; int city_id; @@ -811,6 +1218,19 @@ struct pending_district_request { int worker_assigned_turn; }; +struct ai_candidate_bridge_or_canal_entry { + int district_id; + short owner_civ_id; + short * tile_x; + short * tile_y; + short tile_count; + short assigned_tile_index; + int assigned_worker_id; + bool completed; + struct pending_district_request pending_req; + int tile_capacity; +}; + struct district_worker_record { Unit * worker; int unit_id; @@ -819,10 +1239,10 @@ struct district_worker_record { }; enum wonder_district_state { - WDS_UNUSED = 0, // Wonder district built, no wonder assigned - WDS_UNDER_CONSTRUCTION, // Reserved by a city for wonder construction - WDS_COMPLETED, // Wonder completed on this district - WDS_RUINED // (Future) Wonder was destroyed + WDS_UNUSED = 0, // Wonder district built, no wonder assigned + WDS_UNDER_CONSTRUCTION, // Reserved by a city for wonder construction + WDS_COMPLETED, // Wonder completed on this district + WDS_RUINED // (Future) Wonder was destroyed }; struct wonder_district_info { @@ -843,11 +1263,39 @@ enum district_state { struct district_instance { enum district_state state; - int district_type; // Index into district_configs array + int district_id; // Index into district_configs array + int tile_x; + int tile_y; + int built_by_civ_id; + int completed_turn; + struct wonder_district_info wonder_info; // Only used if district_id is a wonder district + struct natural_wonder_district_info natural_wonder_info; // Only used if district_id is a natural wonder district +}; + +enum extra_resource_tile_type { + ERT_MILL_RESOURCE = 0, + ERT_DISTRICT_RESOURCE +}; + +struct extra_resource_tile { + Tile * tile; + enum extra_resource_tile_type type; + union { + struct { + City * city; + struct mill * mill; + } mill_info; + struct { + struct district_instance * inst; + struct district_config * cfg; + } district_info; + }; +}; + +struct named_tile_entry { int tile_x; int tile_y; - struct wonder_district_info wonder_info; // Only used if district_type is a wonder district - struct natural_wonder_district_info natural_wonder_info; // Only used if district_type is a natural wonder district + char name[100]; }; struct highlighted_city_radius_tile_info { @@ -994,6 +1442,9 @@ struct injected_state { struct table saved_code_areas; int * unit_menu_duplicates; // NULL initialized, allocated to an array of 0x100 ints when needed + bool named_tile_menu_active; + int named_tile_menu_tile_x; + int named_tile_menu_tile_y; // List of temporary ints. Initializes to NULL/0/0, used with functions "memoize" and "clear_memo" int * memo; @@ -1178,8 +1629,10 @@ struct injected_state { // first inside the loop over improvements then again inside a loop over unit types. The var is used by the intercept consideration functions // which run at the end of each loop iteration. City_Order ai_considering_order; + int handling_ai_district_fallback; // Used in the code that adds additional info to the tile info box + bool tile_info_open; int viewing_tile_info_x, viewing_tile_info_y; // Used in patch_Tile_m43_Get_field_30_for_city_loc_eval to change how the AI evaluates overlap between cities @@ -1193,15 +1646,11 @@ struct injected_state { int count_ai_prod_valuations; int ai_prod_valuations_capacity; - // Used for generating resources from buildings - struct mill_tile { - Tile * tile; - City * city; - struct mill * mill; - } * mill_tiles; - int count_mill_tiles; - int mill_tiles_capacity; - struct mill_tile * got_mill_tile; + // Used for generating resources from buildings and districts + struct extra_resource_tile * resource_tiles; + int count_resource_tiles; + int resource_tiles_capacity; + struct extra_resource_tile * got_resource_tile; int saved_tile_count; // Stores the actual tile count in case p_bic_data->Map.TileCount was temporarily overwritten. Set to -1 when empty. byte * mill_input_resource_bits; // Array of bits, one for each resource. Stores whether or not each one is an input to any mill. @@ -1319,6 +1768,12 @@ struct injected_state { bool temporarily_disallow_lethal_zoc; bool moving_unit_to_adjacent_tile; + // Tracks temporary transport bypass when letting workers step onto coast tiles without a boat + Unit * coast_walk_unit; + bool coast_walk_transport_override; + enum UnitStateType coast_walk_prev_state; + int coast_walk_prev_container; + // Used to record info about a defensive bomardment event during Fighter::fight. Gets set by Fighter::damage_by_defensive_bombardment and // cleared when Fighter::fight returns. struct defensive_bombard_event { @@ -1427,6 +1882,8 @@ struct injected_state { struct wonder_district_image_set { Sprite img; Sprite construct_img; + Sprite alt_dir_img; + Sprite alt_dir_construct_img; } wonder_district_img_sets[MAX_WONDER_DISTRICT_TYPES]; struct natural_wonder_district_image_set { @@ -1508,7 +1965,9 @@ struct injected_state { Sprite LM_Forests_Small_Images[10]; Sprite LM_Forests_Pines_Images[12]; Sprite LM_Hills_Images[16]; - Sprite District_Images[COUNT_DISTRICT_TYPES][10][4][6]; // [district][variant][era][building_stage] + Sprite District_Images[COUNT_DISTRICT_TYPES][MAX_DISTRICT_VARIANT_COUNT][4][MAX_DISTRICT_COLUMN_COUNT]; // [district][variant][era][column] + Sprite Abandoned_District_Image; + Sprite Abandoned_Maritime_District_Image; struct wonder_district_image_set Wonder_District_Images[MAX_WONDER_DISTRICT_TYPES]; struct natural_wonder_district_image_set Natural_Wonder_Images[MAX_NATURAL_WONDER_DISTRICT_TYPES]; } day_night_cycle_imgs[24]; @@ -1522,13 +1981,15 @@ struct injected_state { struct wonder_district_config wonder_district_configs[MAX_WONDER_DISTRICT_TYPES]; struct natural_wonder_district_config natural_wonder_configs[MAX_NATURAL_WONDER_DISTRICT_TYPES]; - struct district_image_set { - Sprite imgs[10][4][6]; // [variant][era][building_stage] - } district_img_sets[COUNT_DISTRICT_TYPES]; +struct district_image_set { + Sprite imgs[MAX_DISTRICT_VARIANT_COUNT][4][MAX_DISTRICT_COLUMN_COUNT]; // [variant][era][column] +} district_img_sets[COUNT_DISTRICT_TYPES]; + Sprite abandoned_district_img; + Sprite abandoned_maritime_district_img; - struct district_button_image_set { - Sprite imgs[4]; - } district_btn_img_sets[COUNT_DISTRICT_TYPES]; +struct district_button_image_set { + Sprite imgs[4]; +} district_btn_img_sets[COUNT_DISTRICT_TYPES]; // Tech ID keys -> district ID. If a tech (aka advance) ID is present in the // table that means that tech enables a district. This also means one tech can enable at most one district. @@ -1543,6 +2004,7 @@ struct injected_state { // For wonder districts, the wonder_info field tracks wonder-specific state (unused, reserved, completed, ruined), // which city reserved/completed the wonder, and which wonder index is on this district. struct table district_tile_map; + struct table named_tile_map; // Tracks per-turn airlift usage for aerodrome districts (tile pointer -> civ bitmask). struct table aerodrome_airlift_usage; @@ -1567,7 +2029,16 @@ struct injected_state { struct table building_name_to_id; struct district_infos { - int advance_prereq_id; // Tech ID that enables the district + int advance_prereq_ids[MAX_DISTRICT_DEPENDENTS]; // Tech IDs that enable the district (all required) + int advance_prereq_count; + int obsoleted_by_id; + int resource_prereq_ids[MAX_DISTRICT_DEPENDENTS]; + int resource_prereq_count; + int resource_prereq_on_tile_id; + int wonder_prereq_ids[MAX_DISTRICT_DEPENDENTS]; + int wonder_prereq_count; + int natural_wonder_prereq_ids[MAX_DISTRICT_DEPENDENTS]; + int natural_wonder_prereq_count; int dependent_building_count; int dependent_building_ids[MAX_DISTRICT_DEPENDENTS]; // Building types the district enables } district_infos[COUNT_DISTRICT_TYPES]; @@ -1603,7 +2074,7 @@ struct injected_state { int district_corruption_icons_remaining; int distribution_hub_corruption_icons_remaining; - // District UI sprites loaded from PCX files for rendering yield icons (science, commerce, shield, food, culture) + // District UI sprites loaded from PCX files for rendering yield icons (science, commerce, shield, food, culture, happiness) // Available in both regular and small sizes for different UI contexts in city interface Sprite district_science_icon; Sprite district_commerce_icon; @@ -1611,11 +2082,18 @@ struct injected_state { Sprite district_corruption_icon; Sprite district_food_icon; Sprite district_food_eaten_icon; + Sprite district_happiness_icon_small; Sprite district_shield_icon_small; Sprite district_commerce_icon_small; Sprite district_food_icon_small; Sprite district_science_icon_small; Sprite district_culture_icon_small; + Sprite district_unhappiness_icon_small; + Sprite district_negative_shield_icon_small; + Sprite district_negative_commerce_icon_small; + Sprite district_negative_food_icon_small; + Sprite district_negative_science_icon_small; + Sprite district_negative_culture_icon_small; // Guard to prevent recursive sharing when auto-adding buildings across cities bool sharing_buildings_by_districts_in_progress; @@ -1626,6 +2104,11 @@ struct injected_state { // Natural Wonder labels: table mapping natural wonder name strings to their IDs, count of defined natural wonders, struct table natural_wonder_name_to_id; + struct ai_candidate_bridge_or_canal_entry * ai_candidate_bridge_or_canals; + int ai_candidate_bridge_or_canals_count; + int ai_candidate_bridge_or_canals_capacity; + bool ai_candidate_bridge_or_canals_initialized; + // City work radius highlighting: flag to enable/disable, table mapping tile pointers to highlight_level for visual feedback bool highlight_city_radii; struct table highlighted_city_radius_tile_pointers; @@ -1637,12 +2120,24 @@ struct injected_state { // Stores the parameters to Unit::can_load while it's running, NULL otherwise. Unit * can_load_transport, * can_load_passenger; + // Current tile being rendered in Map_Renderer_m19_Draw_Tile_by_XY_and_Flags. For use within various Map_Renderer::impl_m*_Draw_* functions. Nulled out after the render function is complete + Tile * current_render_tile; + struct district_instance * current_render_tile_district; + int current_render_tile_x, current_render_tile_y; + + // Used in patch_Map_Renderer_m08_Draw_Tile_Forests_Jungle_Swamp and so on for flagging whether to draw forests over roads on the tile being rendered + bool draw_forests_over_roads_on_tile; + + // Set to true once the auto-build process for the Great Wall is complete to avoid running it again + enum great_wall_auto_build_state great_wall_auto_build; + Tile * focused_tile; + + // Stores the improve ID currently being evaluated inside patch_City_can_build_improvement. + int current_evaluating_improve_id; + // Stores the index in the list of target civs currently being drawn on the espionage form. Used to replace the civ name with its era-specific alias. int espionage_form_drawing_target_index; - // Used in patch_Map_Renderer_m08_Draw_Tile_Forests_Jungle_Swamp. Tracks the current tile coordinates being rendered, then for drawing forests over roads/railroad - int current_tile_x, current_tile_y; - // Tracks information about the last unit that was selected. These fields are updated when a new unit is selected or when the selected unit // moves or is destroyed. struct { diff --git a/Civ3Conquests.h b/Civ3Conquests.h index 63ab81e0..5d7e306a 100644 --- a/Civ3Conquests.h +++ b/Civ3Conquests.h @@ -772,6 +772,12 @@ enum Unit_Command_Values UCV_Build_WonderDistrict = -10000002, UCV_Build_DistributionHub = -10000003, UCV_Build_Aerodrome = -10000004, + UCV_Build_Port = -10000005, + UCV_Build_CentralRailHub = -10000006, + UCV_Build_EnergyGrid = -10000007, + UCV_Build_Bridge = -10000008, + UCV_Build_Canal = -10000009, + UCV_Build_GreatWall = -10000010, }; enum Unit_Mode_Actions diff --git a/DayNight/civ3_city_lights.py b/DayNight/civ3_city_lights.py index 1b1870dd..794f4637 100644 --- a/DayNight/civ3_city_lights.py +++ b/DayNight/civ3_city_lights.py @@ -7,7 +7,7 @@ - Multiple --light-key values (union mask) - **Per-light-key styles** via --light-style: per-key core/glow colors and optional overrides - Night (19..24,1..6): also replaces non-lights file -- Pins magenta #ff00ff to palette index 255; preserves #00ff00 +- Pins magenta #ff00ff to palette index 255; removes #00ff00 from palette """ import argparse, os, math, shutil @@ -98,7 +98,7 @@ def ensure_palette_has_colors(image: Image.Image, colors: List[Tuple[int,int,int hist = image.histogram() if image.mode=='P' else None candidates = sorted(range(256), key=(lambda i: hist[i])) if (hist and len(hist)==256) else list(range(255,-1,-1)) protected=set() - for rgb in colors+[MAGENTA,GREEN]: + for rgb in colors+[MAGENTA]: idx = find_color_index(pal, rgb) if idx!=-1: protected.add(idx) replace=[] @@ -283,16 +283,24 @@ def build_composite_with_styles(base_bg_rgba: Image.Image, def save_with_palette_and_magic(comp_rgba: Image.Image, out_path: str, base_for_magic_rgb: Image.Image) -> None: imP = quantize_to_p_256(comp_rgba) - pal_after = ensure_palette_has_colors(imP, [MAGENTA, GREEN]); put_palette(imP, pal_after) + pal_after = ensure_palette_has_colors(imP, [MAGENTA]); put_palette(imP, pal_after) replacement_idx = force_magenta_at_255(imP) - pal_after = get_palette(imP); idx_grn = find_color_index(pal_after, GREEN) dst_px = imP.load(); src_px = base_for_magic_rgb.load(); w,h = imP.size for y in range(h): for x in range(w): r,g,b = src_px[x,y]; cur = dst_px[x,y] - if (r,g,b)==MAGENTA: dst_px[x,y]=255 - elif (r,g,b)==GREEN and idx_grn!=-1: dst_px[x,y]=idx_grn - elif cur==255 and replacement_idx!=255: dst_px[x,y]=replacement_idx + if (r,g,b)==MAGENTA: + dst_px[x,y]=255 + elif (r,g,b)==GREEN: + dst_px[x,y]=255 + elif cur==255 and replacement_idx!=255: + dst_px[x,y]=replacement_idx + # Remove #00ff00 from palette entirely if present. + pal_after = get_palette(imP) + for i in range(256): + if (pal_after[3*i], pal_after[3*i+1], pal_after[3*i+2]) == GREEN: + pal_after[3*i:3*i+3] = [0, 0, 0] + put_palette(imP, pal_after) os.makedirs(os.path.dirname(out_path), exist_ok=True) imP.save(out_path, format='PCX') @@ -396,7 +404,7 @@ def main(): "Render glowing city lights from *_lights.pcx files.\n" "• Multiple light-keys supported (+ per-key styles)\n" "• Night hours (19..24,1..6) also overwrite the paired non-lights file\n" - "• MAGENTA pinned to index 255; GREEN written out as MAGENTA" + "• MAGENTA pinned to index 255; GREEN removed from palette if present" ), formatter_class=RawTextHelpFormatter, epilog=( diff --git a/DayNight/civ3_day_night.py b/DayNight/civ3_day_night.py index f1bc020d..b7b88bc3 100644 --- a/DayNight/civ3_day_night.py +++ b/DayNight/civ3_day_night.py @@ -1,14 +1,16 @@ #!/usr/bin/env python3 """ -civ3_daynight_pcx.py — v4.3 (stronger nighttime blue + stable noon-neutral zone) +civ3_daynight_pcx.py — v4.4 (remove #00FF00 from palette entirely) Highlights: - Half-hour (or any divisor of an hour) time slices via --step-minutes. -- Green→Magenta index remap (on by default). +- Green→Magenta pixel remap (on by default). +- NEW: After pixel remap, ANY palette entries exactly #00FF00 are replaced with black (#000000), + so #00FF00 no longer exists anywhere in the palette. - Palette-only tinting; indices preserved (except the optional index remap). - Noon-neutral zone keeps ~10:00–14:00 close to base 1200. -- NEW: Stronger nighttime blue response using the existing --blue knob, - with an additional night-only hue shift (blue up, red/green down). +- Stronger nighttime blue response using the existing --blue knob, + with an additional night-only hue shift (blue up, red/green down). Tested knobs (your current favorites work unchanged): --warmth 1.05 --blue 2.0 --darkness 1.1 --desat 0.85 --sat 1.2 --contrast 1.08 @@ -19,7 +21,7 @@ import argparse import shutil from pathlib import Path -from typing import Iterable, List, Sequence, Tuple, Set +from typing import Iterable, List, Sequence, Tuple, Set, Dict from PIL import Image from civ3_city_lights import is_night_hour @@ -29,8 +31,11 @@ PROTECTED_FALLBACK_NEIGHBOR_RADIUS = 1 # 1 => 3x3 window, 2 => 5x5, etc. PROTECTED_GAP_BRIDGE = 1 -from collections import defaultdict -from typing import List, Tuple, Sequence +from collections import defaultdict, Counter +from math import sqrt + + +# --------------------------- Helpers for protected ranges --------------------------- def _bridge_small_gaps( ranges: Sequence[Tuple[Tuple[int, int], Tuple[int, int]]], @@ -50,7 +55,6 @@ def _bridge_small_gaps( # Not horizontal; keep separate by_y[(y1, y2, 'nonh')].append((min(x1, x2), max(x1, x2), y1, y2)) continue - # horizontal by_y[y1].append((min(x1, x2), max(x1, x2))) out: List[Tuple[Tuple[int, int], Tuple[int, int]]] = [] @@ -64,12 +68,11 @@ def _bridge_small_gaps( # Bridge gaps on horizontal scanlines for y, spans in by_y.items(): - spans.sort() # sort by x1 then x2 + spans.sort() cur_x1, cur_x2 = spans[0] for nx1, nx2 in spans[1:]: gap = nx1 - cur_x2 - 1 # inclusive ranges if gap <= max_gap: - # Extend current span to cover the gap and next span cur_x2 = max(cur_x2, nx2) else: out.append(((cur_x1, y), (cur_x2, y))) @@ -79,30 +82,37 @@ def _bridge_small_gaps( return out -from math import sqrt -from collections import Counter -from typing import Set, Dict, Iterable, Tuple, List +def clamp_byte(x: float) -> int: + return int(max(0, min(255, round(x)))) + def _nudge_if_reserved(nrgb, reserved_set, orig_rgb): if nrgb in reserved_set: # Nudge 1 toward original to avoid exact collision - r0,g0,b0 = orig_rgb; r,g,b = nrgb - def nudge(c, c0): + r0, g0, b0 = orig_rgb + r, g, b = nrgb + + def nudge(c, c0): return clamp_byte(c - 1 if c > c0 else c + 1 if c < c0 else c) + return (nudge(r, r0), nudge(g, g0), nudge(b, b0)) return nrgb + def _rgb_of_index(pal: List[int], idx: int) -> Tuple[int, int, int]: - return pal[3*idx], pal[3*idx+1], pal[3*idx+2] + return pal[3 * idx], pal[3 * idx + 1], pal[3 * idx + 2] + + +def _color_dist2(a: Tuple[int, int, int], b: Tuple[int, int, int]) -> float: + dr, dg, db = a[0] - b[0], a[1] - b[1], a[2] - b[2] + return dr * dr + dg * dg + db * db -def _color_dist2(a: Tuple[int,int,int], b: Tuple[int,int,int]) -> float: - dr, dg, db = a[0]-b[0], a[1]-b[1], a[2]-b[2] - return dr*dr + dg*dg + db*db def _index_pixel_counts(im: Image.Image) -> Dict[int, int]: - colors = im.getcolors(maxcolors=256*256) or [] + colors = im.getcolors(maxcolors=256 * 256) or [] return {int(idx): int(cnt) for (cnt, idx) in colors} + def _indices_used_in_coords( im: Image.Image, ranges: Sequence[Tuple[Tuple[int, int], Tuple[int, int]]], @@ -110,14 +120,15 @@ def _indices_used_in_coords( w, h = im.size px = im.load() s: Set[int] = set() - for (x1,y1), (x2,y2) in ranges: + for (x1, y1), (x2, y2) in ranges: x_start, x_end = (x1, x2) if x1 <= x2 else (x2, x1) y_start, y_end = (y1, y2) if y1 <= y2 else (y2, y1) - for y in range(max(0,y_start), min(h, y_end+1)): - for x in range(max(0,x_start), min(w, x_end+1)): - s.add(int(px[x,y])) + for y in range(max(0, y_start), min(h, y_end + 1)): + for x in range(max(0, x_start), min(w, x_end + 1)): + s.add(int(px[x, y])) return s + def _find_nearest_index( pal: List[int], src_idx: int, @@ -135,13 +146,25 @@ def _find_nearest_index( best = int(j) return best + +def _used_palette_indices(im: Image.Image) -> Set[int]: + """ + Return the set of palette indices actually used by the image. + """ + colors = im.getcolors(maxcolors=256 * 256) or [] + used: Set[int] = set() + for _, idx in colors: + used.add(int(idx)) + return used + + def _free_palette_slots( im: Image.Image, pal: List[int], needed: int, *, - protected_source_indices: Set[int], # indices that appear inside protected coords - reserved_by_color_rgbs: Set[Tuple[int,int,int]], # magenta/green/light-keys as RGB + protected_source_indices: Set[int], # indices that appear inside protected coords + reserved_by_color_rgbs: Set[Tuple[int, int, int]], # magenta/green/light-keys as RGB ) -> int: """ Try to free up to `needed` palette slots by remapping the *least-used* indices @@ -160,7 +183,7 @@ def _free_palette_slots( reserved_by_color_indices: Set[int] = set() for i in range(256): if i in used_indices: - rgb = (pal[3*i], pal[3*i+1], pal[3*i+2]) + rgb = (pal[3 * i], pal[3 * i + 1], pal[3 * i + 2]) if rgb in reserved_by_color_rgbs: reserved_by_color_indices.add(i) @@ -169,7 +192,6 @@ def _free_palette_slots( # Candidates we are allowed to merge away candidates = [i for i in used_indices if i not in forbidden] - # If nothing to do, bail if not candidates: return 0 @@ -177,16 +199,14 @@ def _free_palette_slots( candidates.sort(key=lambda i: counts.get(i, 0)) freed = 0 - # Allowed targets for merging: any used index that is not the candidate itself - # and not forbidden. Build once and update as we go. allowed_targets = set(used_indices) - forbidden for src_idx in candidates: if freed >= needed: break if src_idx not in used_indices: - continue # may have been merged already - # Targets exclude this src + continue + options = allowed_targets - {src_idx} if not options: continue @@ -198,10 +218,9 @@ def _free_palette_slots( # Remap all pixels from src_idx -> tgt for y in range(h): for x in range(w): - if px[x,y] == src_idx: - px[x,y] = tgt + if px[x, y] == src_idx: + px[x, y] = tgt - # Update usage bookkeeping used_indices.discard(src_idx) allowed_targets.discard(src_idx) freed += 1 @@ -209,10 +228,6 @@ def _free_palette_slots( return freed -# --------------------------- Helpers for protected coords --------------------------- -from typing import Set, Dict -from collections import Counter - def _sample_neighbor_index(px, x: int, y: int, w: int, h: int, r: int) -> int | None: """ Return the most common palette index in the (2r+1)x(2r+1) neighborhood @@ -227,48 +242,15 @@ def _sample_neighbor_index(px, x: int, y: int, w: int, h: int, r: int) -> int | for xx in xs: if xx == x and yy == y: continue - counts[ int(px[xx, yy]) ] += 1 + counts[int(px[xx, yy])] += 1 return counts.most_common(1)[0][0] if counts else None -def _try_allocate_duplicate_index(pal: List[int], used: set[int]) -> int | None: - """ - Return an unused index if available, else None. - (Non-raising version to enable graceful fallback.) - """ - for i in range(256): - if i not in used: - used.add(i) - return i - return None - - -def _used_palette_indices(im: Image.Image) -> Set[int]: - """ - Return the set of palette indices actually used by the image. - """ - # getcolors may return None if there are too many colors; use a large maxcolors - colors = im.getcolors(maxcolors=256*256) or [] - used: Set[int] = set() - for count, idx in colors: - used.add(int(idx)) - return used - -def _allocate_duplicate_index(pal: List[int], used: Set[int]) -> int: - """ - Find an unused palette index (0..255). Raises RuntimeError if none available. - """ - for i in range(256): - if i not in used: - used.add(i) - return i - raise RuntimeError("No free palette indices available to duplicate protected pixels.") - def _protect_exact_pixels_by_index( im: Image.Image, pal: List[int], ranges: Sequence[Tuple[Tuple[int, int], Tuple[int, int]]], - reserved_by_color_rgbs: Set[Tuple[int,int,int]] = set(), + reserved_by_color_rgbs: Set[Tuple[int, int, int]] = set(), ) -> Set[int]: """ Ensure protected coords keep their original (noon) colors by duplicating indices. @@ -280,18 +262,17 @@ def _protect_exact_pixels_by_index( w, h = im.size px = im.load() - # 1) Determine which *source* indices appear inside protected coords protected_src_indices = _indices_used_in_coords(im, ranges) - # 2) See how many free slots we need (one per distinct source index) used_now = _used_palette_indices(im) free_now = [i for i in range(256) if i not in used_now] need = max(0, len(protected_src_indices) - len(free_now)) if need > 0: - # Try to free `need` slots by merging least-used, non-protected, non-reserved indices _ = _free_palette_slots( - im, pal, need, + im, + pal, + need, protected_source_indices=protected_src_indices, reserved_by_color_rgbs=reserved_by_color_rgbs, ) @@ -305,31 +286,31 @@ def _protect_exact_pixels_by_index( radius = int(PROTECTED_FALLBACK_NEIGHBOR_RADIUS) - # 3) Walk through protected coords; duplicate on first encounter of a src index + # Duplicate on first encounter of a src index for (x1, y1), (x2, y2) in ranges: x_start, x_end = (x1, x2) if x1 <= x2 else (x2, x1) y_start, y_end = (y1, y2) if y1 <= y2 else (y2, y1) for y in range(y_start, y_end + 1): - if not (0 <= y < h): continue + if not (0 <= y < h): + continue for x in range(x_start, x_end + 1): - if not (0 <= x < w): continue + if not (0 <= x < w): + continue orig_idx = int(px[x, y]) - # Reuse existing duplicate if we made one for this orig_idx dup = index_map.get(orig_idx) if dup is not None: px[x, y] = dup continue - # Allocate a fresh duplicate slot if available if free_pool: dup_idx = free_pool.pop(0) - r, g, b = pal[3*orig_idx:3*orig_idx+3] - pal[3*dup_idx+0] = r - pal[3*dup_idx+1] = g - pal[3*dup_idx+2] = b + r, g, b = pal[3 * orig_idx: 3 * orig_idx + 3] + pal[3 * dup_idx + 0] = r + pal[3 * dup_idx + 1] = g + pal[3 * dup_idx + 2] = b index_map[orig_idx] = dup_idx reserved_indices.add(dup_idx) px[x, y] = dup_idx @@ -345,8 +326,6 @@ def _protect_exact_pixels_by_index( return reserved_indices - - # --------------------------- CLI & helpers --------------------------- def parse_rgb(s: str) -> Tuple[int, int, int]: @@ -355,7 +334,7 @@ def parse_rgb(s: str) -> Tuple[int, int, int]: s = s[1:] if len(s) != 6: raise ValueError(f"Bad hex color: #{s}") - return tuple(int(s[i:i+2], 16) for i in (0, 2, 4)) # type: ignore + return tuple(int(s[i:i + 2], 16) for i in (0, 2, 4)) # type: ignore if "," in s: parts = [p.strip() for p in s.split(",")] if len(parts) != 3: @@ -384,10 +363,6 @@ def time_labels(step_minutes: int) -> List[str]: return labels -def clamp_byte(x: float) -> int: - return int(max(0, min(255, round(x)))) - - # --------------------------- TONEMAP MODEL --------------------------- def _gauss(x: float, mu: float, sigma: float) -> float: @@ -441,8 +416,6 @@ def hour_adjustments( gray_blend = min(max(gray_blend, 0.0), 0.85) # Night-only blue push factor (extra hue shift at night) - # - grows with (blue_scale - 1) and with 'night' - # - kept separate from b_mul so daytime remains unchanged blue_push = max(0.0, blue_scale - 1.0) * night # 0 at day, up to (blue-1) at night # Clamp gentle bounds @@ -487,13 +460,11 @@ def _apply_night_blue_push( - Slightly reduces R and G - Increases B 'blue_push' is ~ (blue_scale - 1) * night, so this is zero in daytime. - Coefficients tuned to be visible but not cartoonish; adjust if needed. """ if blue_push <= 0.0: return rgb r, g, b = rgb - c = blue_push # c in [0..(blue-1)] scaled by night - # Gentle but noticeable: at c=1 (e.g., --blue 2.0 at full night) + c = blue_push r *= (1.0 - 0.15 * c) g *= (1.0 - 0.10 * c) b *= (1.0 + 0.35 * c) @@ -549,22 +520,18 @@ def _interval_membership(x: float, a: float, b: float, soft: float) -> float: soft = max(0.0, soft) def segment_membership(x: float, s: float, e: float, soft: float) -> float: - # Non-wrapped segment s <= e in [0,24] if soft <= 0.0: return 1.0 if (s <= x <= e) else 0.0 - # Left ramp [s-soft, s] if s - soft <= x < s: - t = (x - (s - soft)) / soft # 0..1 + t = (x - (s - soft)) / soft return _smoothstep01(t) - # Core [s, e] if s <= x <= e: return 1.0 - # Right ramp [e, e+soft] if e < x <= e + soft: - t = (x - e) / soft # 0..1 + t = (x - e) / soft return 1.0 - _smoothstep01(t) return 0.0 @@ -572,7 +539,6 @@ def segment_membership(x: float, s: float, e: float, soft: float) -> float: if a <= b: return segment_membership(x, a, b, soft) else: - # Wrapped interval: union of [a,24) and [0,b] m1 = segment_membership(x, a, 24.0, soft) m2 = segment_membership(x, 0.0, b, soft) return max(m1, m2) @@ -585,7 +551,6 @@ def _noon_weight(hour_value: float, blend: float, sigma: float, - Gaussian around 12:00 with width 'sigma' (hours), scaled by 'blend'. - Smooth interval window [w_start, w_end] with soft edges 'w_soft', also scaled by 'blend'. - - The final weight is max of both components, clamped 0..1. """ try: h = hour_value % 24.0 @@ -600,14 +565,12 @@ def _noon_weight(hour_value: float, blend: float, sigma: float, if blend <= 0.0: return 0.0 - # Gaussian around noon from math import exp d = abs(h - 12.0) d = min(d, 24.0 - d) g = exp(-0.5 * (d / sigma) ** 2) if sigma > 0.0 else 0.0 g *= blend - # Smooth window window_m = _interval_membership(h, w_start, w_end, w_soft) * blend w = max(g, window_m) @@ -638,12 +601,55 @@ def set_palette(img: Image.Image, pal: Sequence[int]) -> None: def find_color_index(pal: Sequence[int], color: Tuple[int, int, int]) -> int: cr, cg, cb = color for i in range(256): - r, g, b = pal[3*i:3*i+3] + r, g, b = pal[3 * i: 3 * i + 3] if r == cr and g == cg and b == cb: return i return -1 +# --------------------------- Green removal (NEW) --------------------------- + +def remap_all_green_to_magenta_and_blacken_palette( + im: Image.Image, + pal: List[int], + *, + green: Tuple[int, int, int] = (0, 255, 0), + magenta: Tuple[int, int, int] = (255, 0, 255), + black: Tuple[int, int, int] = (0, 0, 0), +) -> Image.Image: + """ + 1) Remap pixels from ANY palette index that is exactly green -> magenta index. + 2) Replace ANY palette entry that is exactly green with black. + This removes #00FF00 from the palette entirely. + """ + magenta_idx = find_color_index(pal, magenta) + if magenta_idx < 0: + return im # can't remap without magenta entry + + green_indices: List[int] = [] + for i in range(256): + r, g, b = pal[3 * i: 3 * i + 3] + if (r, g, b) == green: + green_indices.append(i) + + if not green_indices: + return im + + # Remap pixels: any green index -> magenta index + lut = list(range(256)) + for gi in green_indices: + lut[gi] = magenta_idx + im = im.point(lut, mode="P") + + # Replace those palette entries with black + for gi in green_indices: + pal[3 * gi + 0] = black[0] + pal[3 * gi + 1] = black[1] + pal[3 * gi + 2] = black[2] + + return im + + # --------------------------- Core operations --------------------------- def adjust_palette_for_time( @@ -682,34 +688,31 @@ def adjust_palette_for_time( reserved_color_set = set(reserved_colors) reserved_index_set = set(int(i) for i in reserved_indices) - # Noon weight (0..1): stronger near 10:00–14:00, peak at 12:00 noon_w = _noon_weight( hour_value, noon_blend, noon_sigma, noon_window_start, noon_window_end, noon_window_soft ) - # Damp sat/contrast near noon to avoid “pop” sat_eff = 1.0 + (sat_boost - 1.0) * (1.0 - noon_w) contrast_eff = 1.0 + (contrast - 1.0) * (1.0 - noon_w) out = pal[:] # copy for i in range(256): - r, g, b = pal[3 * i:3 * i + 3] + r, g, b = pal[3 * i: 3 * i + 3] # Skip EXACT indices first (highest priority) if i in reserved_index_set: - out[3*i+0], out[3*i+1], out[3*i+2] = r, g, b + out[3 * i + 0], out[3 * i + 1], out[3 * i + 2] = r, g, b continue - # Then skip by color (magenta/green/light-keys) + # Then skip by color (magenta + user light-keys) if (r, g, b) in reserved_color_set: - out[3*i+0], out[3*i+1], out[3*i+2] = r, g, b + out[3 * i + 0], out[3 * i + 1], out[3 * i + 2] = r, g, b continue nr, ng, nb = tint_rgb((r, g, b), params, sat_boost=sat_eff, contrast=contrast_eff) if noon_w > 0.0: - # Blend back toward the original (noon) palette color at the SAME index nr = int(round((1.0 - noon_w) * nr + noon_w * r)) ng = int(round((1.0 - noon_w) * ng + noon_w * g)) nb = int(round((1.0 - noon_w) * nb + noon_w * b)) @@ -723,15 +726,6 @@ def adjust_palette_for_time( return out -def remap_green_to_magenta_indices(img: Image.Image, pal: Sequence[int]) -> Image.Image: - green_idx = find_color_index(pal, (0, 255, 0)) - magenta_idx = find_color_index(pal, (255, 0, 255)) - if green_idx < 0 or magenta_idx < 0 or green_idx == magenta_idx: - return img - lut = [magenta_idx if i == green_idx else i for i in range(256)] - return img.point(lut, mode="P") - - # --------------------------- File ops --------------------------- def copy_noon_to_label(noon_dir: Path, label_dir: Path) -> List[Path]: @@ -789,9 +783,9 @@ def process_time_label( im = im.convert("P") pal = get_palette(im) - # Optional green→magenta index remap first + # Optional green→magenta pixel remap AND remove green from palette (set to black) if do_index_remap: - im = remap_green_to_magenta_indices(im, pal) + im = remap_all_green_to_magenta_and_blacken_palette(im, pal) effective_reserved_colors: List[Tuple[int, int, int]] = list(reserved_colors) reserved_by_color_rgbs = set(effective_reserved_colors) @@ -809,7 +803,7 @@ def process_time_label( new_pal = adjust_palette_for_time( pal, hour_value, effective_reserved_colors, - reserved_indices=reserved_idx, # <-- NEW + reserved_indices=reserved_idx, warmth_scale=warmth_scale, blue_scale=blue_scale, darkness_scale=darkness_scale, @@ -830,7 +824,6 @@ def process_time_label( im.save(pcx_path, format="PCX") - # --------------------------- Main --------------------------- def main(): @@ -869,7 +862,7 @@ def main(): p.add_argument("--twilight-width", type=float, default=1.8, help="Sigma for sunrise/sunset warmth spread (higher = broader).") - # Noon-neutral zone controls (defaults keep ~10:00–14:00 close to noon) + # Noon-neutral zone controls p.add_argument("--noon-blend", type=float, default=0.85, help="0..1 strength to blend toward base palette near 12:00 (0=off).") p.add_argument("--noon-sigma", type=float, default=1.1, @@ -884,9 +877,9 @@ def main(): # Index remap control g = p.add_mutually_exclusive_group() g.add_argument("--keep-green-index", action="store_true", - help="Do NOT remap green index to magenta; leave pixel indices unchanged.") + help="Do NOT remap green pixels to magenta; also do NOT remove #00FF00 from the palette.") g.add_argument("--map-green-index", dest="map_green_index", action="store_true", - help="Force remap green index to magenta (default behavior).") + help="Force remap green pixels to magenta and replace any #00FF00 palette entries with black (default behavior).") args = p.parse_args() @@ -895,15 +888,16 @@ def main(): if not noon_dir.is_dir(): raise SystemExit(f"Noon folder not found: {noon_dir}") - # Reserved colors (Magenta + Green + user light-keys) - reserved: List[Tuple[int, int, int]] = [(255, 0, 255), (0, 255, 0)] + # Reserved colors: + # - Magenta is kept stable + # - Green is NOT reserved anymore because we remove it from the palette when remapping is enabled + reserved: List[Tuple[int, int, int]] = [(255, 0, 255)] for s in args.light_key: reserved.append(parse_rgb(s)) # Determine remap behavior (default ON) do_index_remap = not args.keep_green_index or args.map_green_index - # Build time labels for the given step, then exclude noon folder itself labels = [lbl for lbl in time_labels(args.step_minutes) if lbl != args.noon] # only-hour (accept 0000 -> 2400) @@ -919,7 +913,7 @@ def main(): print(f"Base: {base_dir}") print(f"Noon source: {noon_dir}") print(f"Time step: {args.step_minutes} minutes") - print(f"Index remap green→magenta: {'ON' if do_index_remap else 'OFF'}") + print(f"Green pixels→magenta + remove #00FF00 from palette: {'ON' if do_index_remap else 'OFF'}") print(f"Generating labels: {', '.join(labels)}") for lbl in labels: @@ -947,4 +941,4 @@ def main(): if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/DayNight/civ3_postprocess_pixels.py b/DayNight/civ3_postprocess_pixels.py index fd436799..a411f1f1 100644 --- a/DayNight/civ3_postprocess_pixels.py +++ b/DayNight/civ3_postprocess_pixels.py @@ -2,7 +2,7 @@ # -*- coding: utf-8 -*- """ -Civ3 post-process: remove *isolated* GREEN (#00ff00) pixels from PCX outputs. +Civ3 post-process: remove GREEN (#00ff00) pixels from PCX outputs. Rules ----- @@ -10,8 +10,9 @@ • Only touch files that have a corresponding "*_lights.pcx" in the same directory: - For "NAME.pcx" → process only if "NAME_lights.pcx" exists. - For "NAME_lights.pcx" → always process (it is the corresponding lights file). -• Only replace *isolated* green pixels: a green pixel whose neighbors within +• By default, only replace *isolated* green pixels: a green pixel whose neighbors within --isolation-radius (default 1, i.e. the 8-connected ring) contain no other green pixels. +• With --remove-all-green, replace *all* green pixels. • Replacement uses *neighbor palette indices* so the palette stays unchanged. If no non-green neighbors exist, fall back to nearest palette color to local RGB average. @@ -22,6 +23,7 @@ # --isolation-radius 1 (1 = 8-neighborhood) # --search-radius 3 # --max-radius 5 + # --remove-all-green """ import argparse, os, collections @@ -116,7 +118,8 @@ def local_average_rgb(rgb_img: Image.Image, x: int, y: int, radius: int, # ---------- core fixer ---------- -def fix_green_in_image(path: str, isolation_radius: int, search_radius: int, max_radius: int, verbose: bool=False) -> int: +def fix_green_in_image(path: str, isolation_radius: int, search_radius: int, max_radius: int, + remove_all_green: bool, verbose: bool=False) -> int: """ Returns number of pixels changed (only isolated greens are touched). """ @@ -135,14 +138,20 @@ def fix_green_in_image(path: str, isolation_radius: int, search_radius: int, max px = imRGB.load() w, h = imRGB.size - # Collect isolated green pixels - isolated_coords = [] - for y in range(h): - for x in range(w): - if px[x,y] == GREEN and not has_green_neighbor(imRGB, x, y, isolation_radius): - isolated_coords.append((x,y)) + # Collect target green pixels + coords = [] + if remove_all_green: + for y in range(h): + for x in range(w): + if px[x, y] == GREEN: + coords.append((x, y)) + else: + for y in range(h): + for x in range(w): + if px[x, y] == GREEN and not has_green_neighbor(imRGB, x, y, isolation_radius): + coords.append((x, y)) - if not isolated_coords: + if not coords: return 0 outP = imP.copy() @@ -152,7 +161,12 @@ def fix_green_in_image(path: str, isolation_radius: int, search_radius: int, max if mag_idx != -1: banned_indices.add(mag_idx) changed = 0 - for (x,y) in isolated_coords: + for (x,y) in coords: + if remove_all_green and mag_idx != -1: + out_px[x, y] = mag_idx + changed += 1 + continue + picked_index = None # 1) Most frequent neighbor index (expand search out to max_radius if needed) @@ -179,7 +193,10 @@ def fix_green_in_image(path: str, isolation_radius: int, search_radius: int, max outP.putpalette(pal) # keep palette exactly the same outP.save(path, format='PCX') if verbose: - print(f"[fixed] {path}: {changed} isolated green px") + if remove_all_green: + print(f"[fixed] {path}: {changed} green px") + else: + print(f"[fixed] {path}: {changed} isolated green px") else: if verbose: print(f"[ok] {path}: no isolated green px") @@ -206,7 +223,7 @@ def has_corresponding_lights(dirpath: str, fname: str) -> bool: def walk_and_fix(data_dir: str, noon_folder: str, isolation_radius: int, search_radius: int, max_radius: int, - only_hour: int=None, verbose: bool=False) -> None: + remove_all_green: bool, only_hour: int=None, verbose: bool=False) -> None: noon_abs = os.path.normpath(os.path.join(data_dir, noon_folder)) targets = [] @@ -233,22 +250,29 @@ def walk_and_fix(data_dir: str, noon_folder: str, if not has_corresponding_lights(dirpath, fname): continue path = os.path.join(dirpath, fname) - total_changed += fix_green_in_image(path, isolation_radius, search_radius, max_radius, verbose=verbose) + total_changed += fix_green_in_image( + path, isolation_radius, search_radius, max_radius, + remove_all_green, verbose=verbose + ) print(f"Done. Total isolated green pixels fixed: {total_changed}") def main(): - ap = argparse.ArgumentParser(description="Remove *isolated* #00ff00 pixels from PCX outputs (only files with a matching _lights.pcx).") + ap = argparse.ArgumentParser(description="Remove #00ff00 pixels from PCX outputs (only files with a matching _lights.pcx).") ap.add_argument("--data", required=True, help="Path to Data root (contains noon + hour folders).") ap.add_argument("--noon", default="1200", help="Name of noon folder to SKIP. Default: 1200") ap.add_argument("--only-hour", type=int, help="Restrict to a single hour folder (e.g., 2400)") ap.add_argument("--isolation-radius", type=int, default=1, help="Green is 'isolated' if no other green is found within this Chebyshev radius. Default: 1 (8-neighborhood)") ap.add_argument("--search-radius", type=int, default=3, help="Neighbor radius (pixels) for index voting. Default: 3") ap.add_argument("--max-radius", type=int, default=5, help="Max expansion radius if no neighbor found. Default: 5") + ap.add_argument("--remove-all-green", action="store_true", help="Replace all #00ff00 pixels (default: only isolated).") ap.add_argument("--verbose", action="store_true", help="Print per-file changes.") args = ap.parse_args() - walk_and_fix(args.data, args.noon, args.isolation_radius, args.search_radius, args.max_radius, args.only_hour, verbose=args.verbose) + walk_and_fix( + args.data, args.noon, args.isolation_radius, args.search_radius, args.max_radius, + args.remove_all_green, args.only_hour, verbose=args.verbose + ) if __name__ == "__main__": main() diff --git a/DayNight/generate.sh b/DayNight/generate.sh index e7df7f64..e3f037b3 100644 --- a/DayNight/generate.sh +++ b/DayNight/generate.sh @@ -171,6 +171,7 @@ process_art_set() { --data "$data_dir" --noon "$NOON_SUBFOLDER" --verbose + --remove-all-green ) if [[ -n "${ONLY_HOUR}" ]]; then diff --git a/Notes/district_todos.md b/Notes/district_todos.md new file mode 100644 index 00000000..ab315197 --- /dev/null +++ b/Notes/district_todos.md @@ -0,0 +1,99 @@ + - Districts: + - Municipal District (ZergMazter doing art) + - Central Rail Hub (ZergMazter doing art) + - Light annotations + - Municipal District + - Central Rail Hub + +## To ask Flintlock + - Naval units can't enter canals + - Unit movement from bridge->land (even when both have roads) still takes 1 turn + - Enabling pillage button for naval units + - Worker on coast and land units on bridge movement wake up issue + - Colony structure and loop for relation penalty + + +## Maritime Districts + - ~~Negative bonuses~~ + - ~~Add buildable_on_overlay~~ + - ~~Add commented instructions on fields in Wonder, Natural Wonder config files~~ + - ~~Firm up logic for river district rendering~~ + - ~~Validate and upfront bridge/canal algorithm~~ + - ~~Validate resource generation~~ + - ~~Fix scenario art not loading bug~~ + - ~~Add advance_prereqs~~ + - ~~Allow district OR logic for buildings~~ + - ~~Choose terrain type~~ + - ~~Buttons~~ + - ~~Fix scenario district art mod folder check~~ + - ~~Add buildable_by_war_allies, buildable_by_pact_allies~~ + - ~~Auto add road/railrood~~ + - ~~Named tiles~~ + - ~~AI great wall build strategy setting~~ + - ~~Allow workers over water only if in radius of city that can build water district/wonder~~ + - ~~Add ai_build_strategy option and AI worker handling~~ + - ~~AI navies target maritime districts~~ + - ~~Bump great wall bombard eval scores~~ + - ~~- Make sure AI workers remove great walls after they have metallury~~ + - ~~Prevent AI building districts on resources~~ + - ~~Add allow_irrigation_from flag~~ + - ~~Add logic for determining if canal connects bodies of water for AI port-building~~ + - ~~Add cap on count of distribution hubs, destroy extra if conquered~~ + - ~~Add districts flag for drawing resources under districts~~ + - ~~Add support for alternative render strategy (Municipal District use case)~~ + - ~~Add support for buildable_on_districts~~ + - ~~Show generated resource icon over districts~~ + - ~~Resources generated by districts appear on map~~ + - ~~Flexible config for extra district bonuses based on tile type and nearby buildings~~ + - ~~Put abandoned district art in proper separate field~~ + - ~~Put workable tile loop in macro~~ + - ~~Distro hub yield divisors can scale by connected city count~~ + - ~~Distro hub yields in city screen show even if outside workable ring~~ + - ~~District can add happiness~~ + - ~~Add happiness yield icons to city screen~~ + - ~~Limit district building by resources (multiple, and both on tile and connected?)~~ + - ~~Need to be able to specify buildable_on for specific wonders as well, carry over to wonder district construction and tile choosing for AI~~ + - ~~Add support for districts/wonders on forest, jungle, swamp, snow-mountain, snow-forest, snow-volcano (?)~~ + - ~~align_to_coast setting, parsable~~ + - ~~is_maritime & left/right direction img properties, parsable~~ + - ~~Make sure AI doesn't remove forests and jungle/swamp if district can be built on them~~ + - ~~Replacing ring tile loops with FOR_TILE_RINGS_AROUND ?~~ + - ~~Show district in terrain info modal~~ + - ~~Allow comments in config files~~ + - ~~images for colossus, great lighthouse~~ + - ~~Natural Wonders~~ + - ~~Art for ports~~ + - ~~AI builds non-special districts with no dependent buildings~~ + - ~~Add "auto" intelligent option for AI distro hub building strategy~~ + - ~~Add message if buildings lost due to destroyed district~~ + - ~~Add "display_name"~~ + - ~~Add option to not zoom city screen even if word radius over something~~ + - ~~Districts can provide resources (mills)~~ + - ~~Districts and Wonders can be aligned with rivers~~ + - ~~Add extended_height property for tall district art~~ + - ~~Make sure tech/resource/building parse errors show on load screen~~ + - ~~Auto-show city founding location if settler selected~~ + - ~~Make Tile Highlights green, not red~~ + - ~~Tune ai_defends_districts logic~~ + - ~~Move all units of same type, "build" natural wonder bug~~ + - ~~Fix mideast cathedral colors~~ + - ~~Make terrain highlights green~~ + - ~~Refine logic for air units using aerodrome~~ + - ~~Add buildable_by_civs, buildable_by_civ_traits, buildable_by_civ_govs, buildable_by_civ_cultures~~ + - ~~Support for buildable_on "irrigation", "mine"~~ + - ~~Hoover Dam (use alt dir, special positioning b/c on river)~~ + - ~~Other districts:~~ + - ~~Ski Resort~~ + - ~~Park~~ + - ~~Offshore Extraction Zone~~ + - ~~Data Center~~ + - ~~Energy Grid~~ + - ~~Bridge (2 opposite sides have land "strait" or bridges)~~ + + + + + + +## Seasons + - Splice nice terrain onto 1-2 pcx tiles, use for prompt for remainder diff --git a/Text/c3x-labels.txt b/Text/c3x-labels.txt index ac0d77f0..950f12d3 100644 --- a/Text/c3x-labels.txt +++ b/Text/c3x-labels.txt @@ -112,26 +112,41 @@ Bombarding Building ; Districts-related texts -requires a Neighborhood to grow +requires a +to grow destroyed by volcanic eruption! construction halted due to missing +lost population due to destroyed -; Shown if City A receives a building from City B via shared district, as in " recieved from shared with ". +; Shown if City A receives a building from City B via shared district, as in " received from shared with ". received from shared with +; Shown if a city loses one or more buildings due to destroyed district, as in "'s and other buildings have been lost due to destroyed " +'s +and +other buildings have been +lost due to destroyed + ; Districts config mismatch checked on game load District ID is in the save file but missing in the current configuration. in the save file in the current configuration is called The save file had -total district types but current configuration has only +total district types but current configuration has Warning! This save file was created with a different districts configuration. The game may not function correctly. Error: There may be other errors as well. Districts in save file Currently configured districts from +; Maritime district pillage button tooltip +(P)illage Improvement (Shift+P) + +; For naming tiles +Name Tile +Rename + ; This appears instead of the above actions on units that have been loaded into another (except an army). Transported diff --git a/Text/c3x-script.txt b/Text/c3x-script.txt index 8e16cadc..96e00120 100644 --- a/Text/c3x-script.txt +++ b/Text/c3x-script.txt @@ -104,7 +104,7 @@ Razing cities is prohibited by the rules of this scenario. #C3X_CANT_PILLAGE #caption Can't do that, chief! #text -Pillaging Districts is prohibited by the rules of this scenario. +Pillaging Wonder Districts is prohibited by the rules of this scenario. #C3X_LIMITED_UNIT_CHANGE #map_center 0 @@ -189,4 +189,37 @@ A $DISTRICT0 exists here, but either it has no dependent buildings or another $D Yes. There are better uses for this area. No, nevermind. +#C3X_BEGIN_GREAT_WALL_AUTO_BUILD +#advisor Military Happy +#text +Our $DISTRICT0 is ready! Let's review our borders to determine where to place it. + +#C3X_CONFIRM_BUILD_GREAT_WALL +#advisor Military +Shall we extend our $DISTRICT0 here? +#itemlist +Yes, we must protect our people. +No, don't build it here. + +#C3X_CONFIRM_BUILD_GREAT_WALL_OVER_IMPROVEMENT +#advisor Military +We've found an improvement already exists here. Should we replace it with a $DISTRICT0? +#itemlist +Yes, we must protect our people. +No, leave it alone. + +#C3X_CONFIRM_BUILD_GREAT_WALL_OVER_DISTRICT +#advisor Military +We've found a $DISTRICT1 already exists here with dependent buildings. Should we replace the $DISTRICT1 with a $DISTRICT0? Doing so will remove any dependent buildings. +#itemlist +Yes, we must protect our people. +No, leave it alone. + +#C3X_CONFIRM_BUILD_GREAT_WALL_OVER_DISTRICT_SAFE +#advisor Military +We've found a $DISTRICT1 already exists here but has no dependent buildings. Should we replace the $DISTRICT1 with a $DISTRICT0? +#itemlist +Yes, we must protect our people. +No, leave it alone. + # ; This line must remain at end of file \ No newline at end of file diff --git a/changelog.txt b/changelog.txt index 8e16f9fc..31d91832 100644 --- a/changelog.txt +++ b/changelog.txt @@ -12,6 +12,7 @@ - aggressively_penalize_bankruptcy now alternates between selling buildings or disbanding units first for bankrupt AI players and every third turn cuts their research spending - Fix crash caused by failure to find default option for popup asking player to select new build in city that completed its previous one - Except nuclear weapons from disallow_useless_bombard_vs_airfields +- Fix crash caused by modded empty armies with non-zero attack/defense being allowed to enter combat and dividing by their unit count * RELEASE 26 - Add some options to make land transports viable, controlled by land_transport_rules setting diff --git a/civ_prog_objects.csv b/civ_prog_objects.csv index 9f1ef8cd..5e7339f1 100644 --- a/civ_prog_objects.csv +++ b/civ_prog_objects.csv @@ -4,11 +4,13 @@ define, 0x9C3508, 0x9E5D08, 0x9C34C8, "p_bic_data", "BIC *" define, 0xA52E80, 0xA75680, 0xA52E40, "p_units", "Units *" define, 0xA52DD4, 0xA755D0, 0xA52D94, "p_tile_units", "TileUnits *" define, 0xA52E68, 0xA75668, 0xA52E28, "p_cities", "Cities *" +define, 0xA52E60, 0x0, 0x0, "p_colonies", "Colonies *" define, 0xA52E98, 0xA75698, 0xA52E58, "leaders", "Leader *" define, 0xB3CEA0, 0xB5F6A0, 0xB3CE60, "city_sprites", "Sprite *" define, 0xB3E7D0, 0xB60FD0, 0xB3E790, "destroyed_city_sprites", "Sprite *" define, 0x9F8700, 0xA1AF00, 0x9F86C0, "p_main_screen_form", "Main_Screen_Form *" define, 0xA52658, 0xA74E50, 0xA52618, "p_game", "Game *" +define, 0xCB8B38, 0x0, 0x0, "temp_ui_strs", "char[5][4096]" define, 0x665188, 0x68219C, 0x665188, "ADDR_ADDR_OUTPUTDEBUGSTRINGA", "void *" define, 0x665280, 0x6822E0, 0x665280, "ADDR_ADDR_GETASYNCKEYSTATE", "void *" define, 0x665168, 0x682130, 0x665168, "ADDR_ADDR_GETPROCADDRESS", "void *" @@ -23,6 +25,8 @@ define, 0x4CD0B1, 0x4D5151, 0x4CD171, "ADDR_PEDIA_TEXTURE_BUG_PATCH", "void define, 0x5640AC, 0x570385, 0x56405C, "ADDR_AUTORAZE_BYPASS", "void *" repl vptr, 0x66CB50, 0x689C3C, 0x66CB50, "Leader_impl_would_raze_city", "bool (__fastcall *) (Leader * this, int edx, City * city)" repl vptr, 0x66B44C, 0x68854C, 0x66B44C, "Main_Screen_Form_handle_left_click_on_map_1", "void (__fastcall *) (Main_Screen_Form * this, int edx, int param_1, int param_2)" +inlead, 0x4EA210, 0x0, 0x0, "Main_Screen_Form_handle_right_click_on_tile", "void (__fastcall *) (Main_Screen_Form * this, int edx, int tile_x, int tile_y, int mouse_x, int mouse_y)" +inlead, 0x4E8850, 0x0, 0x0, "Main_Screen_Form_open_right_click_menu", "void (__fastcall *) (Main_Screen_Form * this, int edx, int tile_x, int tile_y, int mouse_x, int mouse_y)" define, 0x499FE0, 0x49F9F0, 0x49A070, "is_online_game", "char (__stdcall *) (void)" define, 0x437A70, 0x439620, 0x437AF0, "tile_at", "Tile * (__cdecl *) (int x, int y)" define, 0x426C80, 0x4283C0, 0x426D00, "TileUnits_TileUnitID_to_UnitID", "int (__fastcall *) (TileUnits * this, int edx, int tile_unit_id, int * out_UnitItem_field_0)" @@ -30,6 +34,7 @@ inlead, 0x5C1410, 0x5CFFA0, 0x5C1120, "Unit_bombard_tile", "void (__fastca define, 0x5BE820, 0x5CD420, 0x5BE530, "Unit_get_defense_strength", "int (__fastcall *) (Unit * this)" inlead, 0x5BB650, 0x5CA190, 0x5BB360, "Unit_is_visible_to_civ", "char (__fastcall *) (Unit * this, int edx, int civ_id, int param_2)" define, 0x5EA6C0, 0x5F9F10, 0x5EA5F0, "Tile_has_city", "char (__fastcall *) (Tile * this)" +define, 0x5EA6E0, 0x0, 0x0, "Tile_has_colony", "bool (__fastcall *) (Tile * this)" define, 0x5BE5B0, 0x5CD180, 0x5BE2C0, "Unit_get_max_hp", "int (__fastcall *) (Unit * this)" define, 0x5E4EF0, 0x5F4750, 0x5E4E20, "UnitType_has_ability", "bool (__fastcall *) (UnitType * this, int edx, enum UnitTypeAbilities a)" define, 0x5CDDF0, 0x5DCEB0, 0x5CDD10, "get_max_move_points", "int (__cdecl *) (UnitType * unit_type, int civ_id)" @@ -76,6 +81,7 @@ define, 0x4DAA70, 0x4E3430, 0x4DAB30, "Main_Screen_Form_issue_command", "cha define, 0x609D60, 0x6233C0, 0x609C80, "clear_something_1", "void (__stdcall *) ()" define, 0x6207B0, 0x644F10, 0x6206E0, "Timer_clear", "void (__fastcall *) (Timer * this)" inlead, 0x55EFA0, 0x56B030, 0x55EF50, "Leader_can_do_worker_job", "char (__fastcall *) (Leader * this, int edx, enum Worker_Jobs job, int tile_x, int tile_y, int ask_if_replacing)" +define, 0x56A7C0, 0x0, 0x0, "Leader_can_build_unit", "bool (__fastcall *) (Leader * this, int edx, int unit_type_id, int param_2, bool allow_kings)" define, 0x455288, 0x457458, 0x455308, "ADDR_CHECK_ARTILLERY_IN_CITY", "void *" inlead, 0x455140, 0x457310, 0x4551C0, "Unit_ai_move_artillery", "void (__fastcall *) (Unit * this)" define, 0x5E6E50, 0x5F66A0, 0x5E6D80, "neighbor_index_to_diff", "void (__cdecl *) (int neighbor_index, int * out_x, int * out_y)" @@ -100,6 +106,7 @@ define, 0x5B2B20, 0x5C1440, 0x5B2830, "Unit_next_escorter_id", "int (__fast define, 0x5BC300, 0x5CAE50, 0x5BC010, "Unit_disband", "void (__fastcall *) (Unit * this)" inlead, 0x5C00A0, 0x5CEC20, 0x5BFDB0, "Unit_can_hurry_production", "bool (__fastcall *) (Unit * this, int edx, City * city, bool exclude_cheap_improvements)" inlead, 0x5B3AB0, 0x5C2400, 0x5B37C0, "Unit_can_pillage", "bool (__fastcall *) (Unit *this, int edx, int tile_x, int tile_y)" +inlead, 0x5CCBB0, 0x0, 0x0, "Unit_can_pass_between", "bool (__fastcall *) (Unit *this, int edx, int from_x, int from_y, int to_x, int to_y, byte param_5)" define, 0x5C0420, 0x5CEFC0, 0x5C0130, "Unit_hurry_production", "void (__fastcall *) (Unit * this)" define, 0x5C0300, 0x5CEE90, 0x5C0010, "Unit_ai_can_start_science_age", "bool (__fastcall *) (Unit * this)" define, 0x5C03B0, 0x5CEF50, 0x5C00C0, "Unit_start_science_age", "void (__fastcall *) (Unit * this)" @@ -136,12 +143,13 @@ inlead, 0x61A480, 0x63B9F0, 0x61A3B0, "Context_Menu_open", "int (__fastcal define, 0x4E993C, 0x4F273A, 0x4E99FC, "ADDR_OPEN_UNIT_MENU_RETURN", "int" define, 0x61B0C0, 0x63C940, 0x61AFF0, "Context_Menu_widen_for_text", "void (__fastcall *) (Context_Menu * this, int edx, char * text)" inlead, 0x61A110, 0x63B420, 0x61A040, "Context_Menu_add_item_and_set_color", "int (__fastcall *) (Context_Menu * this, int edx, int item_id, char * text, int red)" +define, 0x61A160, 0x0, 0x0, "Context_Menu_add_separator", "int (__fastcall *) (Context_Menu * this, int edx, int item_id)" inlead, 0x45C750, 0x45EA70, 0x45C7D0, "Unit_ai_move_terraformer", "void (__fastcall *) (Unit * this)" inlead, 0x4B1DC0, 0x4B8D50, 0x4B1E50, "City_requires_improvement_to_grow", "int (__fastcall *) (City * this)" inlead, 0x4DD530, 0x4E5F00, 0x4DD5F0, "maybe_show_improvement_needed_for_growth_dialog", "void (__fastcall *) (void * this, int edx, City * city, int * param_2)" repl call, 0x5213E3, 0x52B3F3, 0x521483, "City_requires_improvement_to_grow_besides_neighborhood", "" inlead, 0x5C1AD0, 0x5D0670, 0x5C17E0, "Unit_can_perform_command", "bool (__fastcall *) (Unit * this, int edx, int unit_command_value)" -define, 0x5B39D0, 0x5C2320, 0x5B36E0, "Unit_join_city", "void (__fastcall *) (Unit * this, int edx, City * city)" +inlead, 0x5B39D0, 0x5C2320, 0x5B36E0, "Unit_join_city", "void (__fastcall *) (Unit * this, int edx, City * city)" define, 0x5C0740, 0x5CF2E0, 0x5C0450, "Unit_upgrade", "Unit * (__fastcall *) (Unit * this, int edx, bool ignore_cost)" define, 0x5C04D0, 0x5CF070, 0x5C01E0, "Unit_get_upgrade_cost", "int (__fastcall *) (Unit * this)" define, 0xCADC18, 0xCD0510, 0xCADBD8, "script_dot_txt_file_path", "char *" @@ -180,6 +188,7 @@ define, 0x437540, 0x4390F0, 0x4375C0, "Improvement_has_wonder_flag", "bool define, 0x437710, 0x4392F0, 0x437790, "UnitType_has_ai_strategy", "bool (__fastcall *) (UnitType * this, int edx, byte n)" repl call, 0x431038, 0x432AE7, 0x4310B8, "UnitType_has_strat_0_for_ai_prod", "" define, 0x44CE50, 0x44EE40, 0x44CED0, "Unit_find_transport", "Unit * (__fastcall *) (Unit * this, int edx, int tile_x, int tile_y)" +inlead, 0x5C5F70, 0x0, 0x0, "Unit_select_transport", "Unit * (__fastcall *) (Unit * this, int edx, int tile_x, int tile_y, bool should_show_popup)" inlead, 0x5C5110, 0x5D3DF0, 0x5C4E20, "Unit_load", "void (__fastcall *) (Unit * this, int edx, Unit * transport)" define, 0xA526BC, 0xA74EB4, 0xA5267C, "p_human_player_bits", "int *" inlead, 0x5F8130, 0x6085D0, 0x5F8060, "Sprite_draw_on_map", "int (__fastcall *) (Sprite * this, int edx, Map_Renderer * map_renderer, int pixel_x, int pixel_y, int param_4, int param_5, int param_6, int param_7)" @@ -205,6 +214,7 @@ inlead, 0x4ACF40, 0x4B3F20, 0x4ACFD0, "City_add_or_remove_improvement", "voi define, 0x57E943, 0x58B68E, 0x57E6A3, "ADDR_RESOURCE_TILE_COUNT_MASK", "void *" define, 0x57E5EB, 0x58B328, 0x57E34B, "ADDR_RESOURCE_TILE_COUNT_ZERO_COMPARE", "void *" define, 0x5DA1A0, 0x5E9710, 0x5DA0D0, "Tile_get_resource_visible_to", "int (__fastcall *) (Tile * this, int edx, int civ_id)" +inlead, 0x5EA7f0, 0x0, 0x0, "Tile_m17_Check_Irrigation", "bool (__fastcall *) (Tile * this, int edx, int visible_to_civ_id)" define, 0x5D16A0, 0x5E0900, 0x5D15D0, "Map_get_tile", "Tile * (__fastcall *) (Map * this, int edx, int index)" repl call, 0x57E602, 0x58B345, 0x57E362, "Map_get_tile_when_recomputing_resources_1", "" repl call, 0x57E629, 0x58B36C, 0x57E389, "Map_get_tile_when_recomputing_resources_2", "" @@ -246,6 +256,7 @@ define, 0x53A960, 0x544C80, 0x53A9E0, "get_unit_support_info", "void (__std define, 0x55D2A0, 0x569330, 0x55D250, "Leader_get_free_unit_count", "int (__fastcall *) (Leader * this, int edx, int govt_id)" inlead, 0x55D030, 0x5690C0, 0x55CFE0, "Leader_count_maintenance_free_units", "int (__fastcall *) (Leader * this)" define, 0x56D7D0, 0x57A1B0, 0x56D740, "get_tile_occupier_id", "int (__cdecl *) (int x, int y, int pov_civ_id, bool respect_unit_invisibility)" +repl call, 0x4585F4, 0x0, 0x0, "get_tile_occupier_id_in_Unit_ai_move_naval_power_unit", "" inlead, 0x4ED220, 0x4F6140, 0x4ED2E0, "Main_Screen_Form_show_map_message", "void (__fastcall *) (Main_Screen_Form * this, int edx, int tile_x, int tile_y, char * text_key, bool pause)" repl call, 0x4BE664, 0x4C5C66, 0x4BE6F4, "Main_Screen_Form_show_wltk_message", "" repl call, 0x4BE71B, 0x4C5D1D, 0x4BE7AB, "Main_Screen_Form_show_wltk_ended_message", "" @@ -270,6 +281,8 @@ inlead, 0x561220, 0x56D350, 0x5611D0, "Leader_begin_unit_turns", "void (__f define, 0x4A1FA0, 0x4A8BB0, 0x4A2030, "Fighter_find_defender_against_bombardment", "Unit * (__fastcall *) (Fighter * this, int edx, Unit * bombarder, int tile_x, int tile_y, int bombarder_civ_id, bool land_lethal, bool sea_lethal)" repl call, 0x4A3ED9, 0x4AAB85, 0x4A3F69, "Fighter_find_actual_bombard_defender", "" repl call, 0x4A2BAA, 0x4A97F6, 0x4A2C3A, "Fighter_find_actual_bombard_defender", "" +define, 0x4A7280, 0x0, 0x0, "Fighter_eval_tile_vulnerability", "int (__fastcall *) (Fighter * this, int edx, Unit * unit, int tile_x, int tile_y)" +repl call, 0x45861F, 0x0, 0x0, "Fighter_eval_tile_vulnerability_in_Unit_ai_move_naval_power_unit", "" inlead, 0x5C6290, 0x5D50E0, 0x5C5FA0, "Unit_select", "void (__fastcall *) (Unit *this)" inlead, 0x5B6820, 0x5C5180, 0x5B6530, "Unit_select_stealth_attack_target", "bool (__fastcall *) (Unit * this, int edx, int target_civ_id, int x, int y, bool allow_popup, Unit ** out_selected_target)" repl call, 0x5C73FF, 0x5D629C, 0x5C710F, "Unit_try_flying_for_precision_strike", "" @@ -457,6 +470,7 @@ define, 0x55B1A0, 0x567100, 0x55B150, "Leader_reveal_tile", "void (__fastc define, 0x5FCB10, 0x610790, 0x5FC9F0, "PCX_Image_draw_onto", "int (__fastcall *) (PCX_Image * this, int edx, PCX_Image * canvas, int pixel_x, int pixel_y)" define, 0x5DBF60, 0x5EB4B0, 0x5DBE90, "Tile_get_terrain_move_cost", "int (__fastcall *) (Tile * this)" define, 0x56D340, 0x579CC0, 0x56D2B0, "get_combat_occupier", "int (__cdecl *) (int tile_x, int tile_y, int civ_id, byte ignore_visibility)" +repl call, 0x45871C, 0x0, 0x0, "get_combat_occupier_in_Unit_ai_move_naval_power_unit", "" define, 0x561480, 0x56D5E0, 0x561430, "Leader_has_tech_with_flag", "bool (__fastcall *) (Leader * this, int edx, enum AdvanceTypeFlags flag)" inlead, 0x57D980, 0x58A680, 0x57D6E0, "Trade_Net_recompute_city_connections", "void (__fastcall *) (Trade_Net * this, int edx, int civ_id, bool redo_road_network, byte param_3, int redo_roads_for_city_id)" inlead, 0x62B1A0, 0x64E220, 0x6274B0, "OpenGLRenderer_initialize", "int (__fastcall *) (OpenGLRenderer * this, int edx, PCX_Image * texture)" @@ -481,9 +495,9 @@ inlead, 0x55D310, 0x5693A0, 0x55D2C0, "Leader_sum_unit_maintenance", "int ( define, 0xA52684, 0xA74E7C, 0xA52644, "p_game_difficulty", "int *" define, 0x5CD960, 0x5DC940, 0x5CD880, "Unit_is_terrain_impassable", "bool (__fastcall *) (Unit * this, int edx, int terrain_type_index)" define, 0x41CD22, 0x41E04C, 0x41CDA2, "ADDR_LUXURY_BOX_ROW_HEIGHT", "byte *" -define, 0x30, 0x38, 0x30, "LUXURY_BOX_ROW_HEIGHT_STACK_OFFSET", "int" +define, 0x30, 0x38, 0x30, "LUXURY_BOX_ROW_HEIGHT_STACK_OFFSET", "int" define, 0x740A00, 0x75ADA0, 0x7409C0, "p_city_form", "City_Form *" -define, 0x5F82E0, 0x6087E0, 0x5F8210, "Sprite_draw", "int (__fastcall *) (Sprite * this, int edx, PCX_Image * canvas, int pixel_x, int pixel_y, PCX_Color_Table * color_table)" +inlead, 0x5F82E0, 0x6087E0, 0x5F8210, "Sprite_draw", "int (__fastcall *) (Sprite * this, int edx, PCX_Image * canvas, int pixel_x, int pixel_y, PCX_Color_Table * color_table)" repl call, 0x421295, 0x4227F5, 0x421315, "Sprite_draw_improv_img_on_city_form", "" inlead, 0x421240, 0x4227A0, 0x4212C0, "draw_improv_icons_on_city_screen", "void (__cdecl *) (Base_List_Control * control, int improv_id, int item_index, int offset_x, int offset_y)" define, 0x4B0710, 0x4B76C0, 0x4B07A0, "City_get_tourism_amount", "int (__fastcall *) (City * this, int edx, int improv_id)" @@ -584,8 +598,8 @@ define, 0x5FF0E0, 0x614190, 0x5FEFC0, "PCX_Image_do_draw_centered_text", "in repl call, 0x41AF59, 0x41C067, 0x41AFD9, "PCX_Image_do_draw_cntd_text_for_strat_res", "" repl call, 0x41AE9F, 0x41BFA9, 0x41AF1F, "Sprite_draw_strat_res_on_city_screen", "" define, 0x41AE1B, 0x41BF23, 0x41AE9B, "ADDR_MOST_STRAT_RES_ON_CITY_SCREEN", "void *" -define, 0x5BEB60, 0x5CD780, 0x5BE870, "Unit_can_heal_at", "bool (__fastcall *) (Unit * this, int edx, int tile_x, int tile_y)" -define, 0x5BECE0, 0x5CD900, 0x5BE9F0, "Unit_heal_at_start_of_turn", "void (__fastcall *) (Unit * this)" +inlead, 0x5BEB60, 0x5CD780, 0x5BE870, "Unit_can_heal_at", "bool (__fastcall *) (Unit * this, int edx, int tile_x, int tile_y)" +inlead, 0x5BECE0, 0x5CD900, 0x5BE9F0, "Unit_heal_at_start_of_turn", "void (__fastcall *) (Unit * this)" define, 0x469590, 0x46BD40, 0x469610, "check_online_skip_flag", "bool (__stdcall *) (int unit_id)" inlead, 0x448BF0, 0x44AB10, 0x448C70, "Leader_ai_eval_technology", "int (__fastcall *) (Leader * this, int edx, int id, bool param_2, bool param_3)" inlead, 0x4446C0, 0x4464C0, 0x444740, "Leader_ai_eval_government", "int (__fastcall *) (Leader * this, int edx, int id)" @@ -808,7 +822,10 @@ define, 0x50FDB3, 0x51A057, 0x50FE53, "ADDR_MAX_TRADABLE_CITY_ID", "byte *" define, 0x505B21, 0x50F8EB, 0x505BC1, "ADDR_TRADABLE_CITIES_SIZE_TO_CLEAR", "byte *" define, 0x51000C, 0x51A2AC, 0x5100AC, "ADDR_MAX_TRADABLE_UNIT_ID", "byte *" define, 0x505B67, 0x50F931, 0x505C07, "ADDR_TRADABLE_UNITS_SIZE_TO_CLEAR", "byte *" -repl vptr, 0x66A538, 0x687644, 0x66A538, "Map_Renderer_m12_Draw_Tile_Buildings", "void (__fastcall *) (Map_Renderer * this, int edx, int param_1, int tile_x, int tile_y, Map_Renderer * map_renderer, int pixel_x, int pixel_y)" +repl vptr, 0x66A52C, 0x0, 0x0, "Map_Renderer_m09_Draw_Tile_Resources", "void (__fastcall *) (Map_Renderer * this, int edx, int visible_to_civ_id, int tile_x, int tile_y, Map_Renderer * map_renderer, int pixel_x, int pixel_y)" +define, 0x66A530, 0x0, 0x0, "Map_Renderer_m10_Draw_Tile_Mountains_Hills_Volcano", "void (__fastcall *) (Map_Renderer * this, int edx, int tile_x, int tile_y, Map_Renderer * map_renderer, int pixel_x, int pixel_y, int flags)" +repl vptr, 0x66A538, 0x687644, 0x66A538, "Map_Renderer_m12_Draw_Tile_Buildings", "void (__fastcall *) (Map_Renderer * this, int edx, int visible_to_civ_id, int tile_x, int tile_y, Map_Renderer * map_renderer, int pixel_x, int pixel_y)" +inlead, 0x5F61A0, 0x0, 0x0, "Map_Renderer_m11_Draw_Tile_Irrigation", "void (__fastcall *) (Map_Renderer *this, int edx, int visible_to_civ, int tile_x, int tile_y, int param_4, int param_5, int param_6)" repl call, 0x5F5580, 0x6053D2, 0x5F54B0, "Tile_has_city_or_district", "" repl call, 0x5F5816, 0x605639, 0x5F5746, "Tile_has_city_or_district", "" repl call, 0x5F5ABB, 0x6058AF, 0x5F59EB, "Tile_has_city_or_district", "" @@ -873,11 +890,11 @@ ignore, 0x5FCC50, 0x0, 0x0, "PCX_Image_draw_region_to_location", "void (__ ignore, 0x600050, 0x0, 0x0, "PCX_Image_fill", "void (__fastcall *) (PCX_Image * this, int edx, int color)" ignore, 0x5FFF10, 0x0, 0x0, "PCX_Image_set_color_table", "int (__fastcall *) (PCX_Image * this, int edx, PCX_Color_Table * color_table)" ignore, 0x4507B0, 0x452860, 0x0, "Unit_ai_move_offensive_unit", "void (__fastcall *) (Unit * this)" -ignore, 0x44C340, 0x0, 0x0, "Unit_ai_eval_bombard_target", "int (__fastcall *) (Unit * this, int edx, int tile_x, int tile_y, int param_3)" +inlead, 0x44C340, 0x0, 0x0, "Unit_ai_eval_bombard_target", "int (__fastcall *) (Unit * this, int edx, int tile_x, int tile_y, int param_3)" +ignore, 0x558F70, 0x0, 0x0, "Leader_is_enemy_unit", "char (__fastcall *) (Leader * this, int edx, Unit * unit)" ignore, 0x4BFD40, 0x0, 0x0, "City_get_turns_to_build", "int (__fastcall *) (City * this, int edx, int order_type, int order_id, char param_3)" ignore, 0x4E3D90, 0x0, 0x0, "Main_Screen_Form_is_unit_hidden_from_player", "bool (__fastcall *) (Main_Screen_Form * this, int edx, Unit * unit)" ignore, 0x4E8850, 0x4F14E0, 0x4E8910, "Main_Screen_Form_open_right_click_unit_menu", "void (__fastcall *) (Main_Screen_Form * this, int edx, int tile_x, int tile_y, int mouse_x, int mouse_y)" -ignore, 0x4DBA70, 0x0, 0x0, "Main_Screen_Form_set_selected_unit", "void (__fastcall *) (Main_Screen_Form * this, int edx, Unit * unit, bool param_2)" ignore, 0x5CCBB0, 0x0, 0x0, "Unit_can_pass_between", "PassBetweenValidity (__fastcall *) (Unit * this, int edx, int from_x, int from_y, int to_x, int to_y, byte param_5)" ignore, 0x452510, 0x454600, 0x452590, "ai_move_defensive_unit", "void (__fastcall *) (Unit * this)" ignore, 0x5E78E0, 0x5F7130, 0x0, "General_load", "void (__fastcall *) (General * this, int edx, byte ** buffer)" @@ -890,5 +907,28 @@ define, 0xCC2BB0, 0xCE54BC, 0xCC2B70, "p_got_leader_gender", "int *" ignore, 0x49D070, 0x4A3AF0, 0x49D100, "Advisor_GUI_open", "void (__fastcall *) (Advisor_GUI * this, int edx, AdvisorKind kind)" ignore, 0x4BF660, 0x4C6C10, 0x4BF6F0, "City_draw_citizens", "void (__fastcall *) (City * this, int edx, PCX_Image * canvas, RECT * rect, char param_3)" ignore, 0x4B9F60, 0x4C15D0, 0x4B9FF0, "City_add_population", "void (__fastcall *) (City * this, int edx, int num, int race_id)" +define, 0x4BA230, 0x0, 0x0, "City_remove_population", "void (__fastcall *) (City * this, int edx, int num_pops, int race_id, char param_3)" +define, 0x4AE280, 0x0, 0x0, "City_get_largest_adjacent_sea", "int (__fastcall *) (City * this)" +define, 0x5F39E0, 0x0, 0x0, "Map_impl_has_fresh_water", "bool (__fastcall *) (Map * this, int edx, int tile_x, int tile_y)" +define, 0x5F37F0, 0x0, 0x0, "Map_impl_is_near_river", "bool (__fastcall *) (Map * this, int edx, int x, int y, int num_tiles)" +define, 0x5F38C0, 0x0, 0x0, "Map_impl_is_near_lake", "bool (__fastcall *) (Map * this, int edx, int x, int y, int num_tiles)" +repl call, 0x4C008E, 0x0, 0x0, "City_get_largest_adjacent_sea_within_work_area", "" +repl call, 0x4C010F, 0x0, 0x0, "Map_impl_has_fresh_water_within_work_area", "" +repl call, 0x4C0173, 0x0, 0x0, "Map_impl_is_near_river_within_work_area", "" +repl call, 0x4C01AF, 0x0, 0x0, "Map_impl_is_near_river_within_work_area", "" +repl call, 0x4C01CF, 0x0, 0x0, "Map_impl_is_near_lake_within_work_area", "" +inlead, 0x458120, 0x0, 0x0, "Unit_ai_move_naval_power_unit", "void (__fastcall *) (Unit * this)" +inlead, 0x45A170, 0x0, 0x0, "Unit_ai_move_naval_transport", "void (__fastcall *) (Unit * this)" +inlead, 0x460620, 0x0, 0x0, "Unit_ai_move_naval_missile_transport", "void (__fastcall *) (Unit * this)" +inlead, 0x44C130, 0x0, 0x0, "Unit_ai_eval_pillage_target", "int (__fastcall *) (Unit * this, int edx, int tile_x, int tile_y)" +inlead, 0x456840, 0x0, 0x0, "Unit_ai_move_air_bombard_unit", "void (__fastcall *) (Unit * this)" +inlead, 0x4579E0, 0x0, 0x0, "Unit_ai_move_air_defense_unit", "void (__fastcall *) (Unit * this)" +inlead, 0x459CE0, 0x0, 0x0, "Unit_ai_move_air_transport", "void (__fastcall *) (Unit * this)" +define, 0x5C1920, 0x0, 0x0, "Unit_airdrop", "void (__fastcall *) (Unit * this, int edx, int x, int y)" +repl call, 0x4C0D87, 0x0, 0x0, "Leader_count_wonders_with_flag_ignore_great_wall", "" +repl call, 0x5D3D67, 0x0, 0x0, "Tile_has_colony_ignore_extraterritorial", "" +inlead, 0x440100, 0x0, 0x0, "Leader_get_attitude_toward", "int (__fastcall *) (Leader * this, int edx, int civ_id, int param_2)" +inlead, 0x5D7080, 0x0, 0x0, "Map_check_colony_location", "int (__fastcall *) (Map * this, int edx, int tile_x, int tile_y, int civ_id)" ignore, 0x670234, 0x68D2E0, 0x670234, "Tile_m27_Check_Shield_Bonus", "bool (__fastcall *) (Tile * this)" ignore, 0x5f3448, 0x6032DF, 0x5F3378, "CHECK_SHIELD_BONUS_TO_CAN_SPAWN_RES_RETURN", "int" +inlead, 0x5BCE60, 0x0, 0x0, "Unit_select_army_member_for_combat", "Unit * (__fastcall *) (Unit * this, int edx, int param_1, char param_2)" diff --git a/common.c b/common.c index 32ceb8cf..8b1bf260 100644 --- a/common.c +++ b/common.c @@ -549,38 +549,47 @@ extract_slice (struct string_slice const * s) int read_int (struct string_slice const * s, int * out_val) { - struct string_slice trimmed = trim_string_slice (s, 1); - char * str = trimmed.str; - int len = trimmed.len; - - if ((len > 0) && (*str == '-') || ((*str >= '0') && (*str <= '9'))) { - char * end; - int base = 10; - if ((str[0] == '0') && ((str[1] == 'x') || (str[1] == 'X'))) { - base = 16; - str += 2; - len -= 2; - } - int res = strtol (str, &end, base); - if (end == str + len) { - *out_val = res; - return 1; - } else - return 0; - } else if ((len == 4) && - ((0 == strncmp (str, "true", 4)) || - (0 == strncmp (str, "True", 4)) || - (0 == strncmp (str, "TRUE", 4)))) { - *out_val = 1; - return 1; - } else if ((len == 5) && - ((0 == strncmp (str, "false", 5)) || - (0 == strncmp (str, "False", 5)) || - (0 == strncmp (str, "FALSE", 5)))) { - *out_val = 0; - return 1; - } else - return 0; + struct string_slice trimmed = trim_string_slice (s, 1); + char *str = trimmed.str; + int len = trimmed.len; + + if (len > 0 && (*str == '-' || (*str >= '0' && *str <= '9'))) { + char *end; + int base = 10; + + char *p = str; + if (*p == '-') { + p++; + } + + if (len >= 3 && p[0] == '0' && (p[1] == 'x' || p[1] == 'X')) { + base = 16; + } + + int res = strtol(str, &end, base); + + if (end == str + len) { + *out_val = res; + return 1; + } + return 0; + } + else if (len == 4 && + (!strncmp(str, "true", 4) || + !strncmp(str, "True", 4) || + !strncmp(str, "TRUE", 4))) { + *out_val = 1; + return 1; + } + else if (len == 5 && + (!strncmp(str, "false", 5) || + !strncmp(str, "False", 5) || + !strncmp(str, "FALSE", 5))) { + *out_val = 0; + return 1; + } + + return 0; } int diff --git a/default.c3x_config.ini b/default.c3x_config.ini index caca5109..e6ac9193 100644 --- a/default.c3x_config.ini +++ b/default.c3x_config.ini @@ -312,6 +312,8 @@ ai_worker_requirement_percent = 150 ; the search fails to find anything, there will be no default option, the dropdown selector will not be initialized, and the game will crash when it ; tries to open the popup. To fix this, the mod detects when the game has failed to find a new build option and fills in Wealth instead or, failing ; that, any unit or improvement that the city can build. +; PATCH EMPTY ARMY COMBAT CRASH -- Guards a crash in the army combat selection routine by returning the army unit itself if it has no contained +; units (prevents divide-by-zero in the vanilla selection loop). patch_submarine_bug = true patch_science_age_bug = true @@ -325,6 +327,7 @@ patch_barbarian_diagonal_bug = true patch_disease_stopping_tech_flag_bug = false patch_division_by_zero_in_ai_alliance_eval = true patch_empty_army_movement = true +patch_empty_army_combat_crash = true patch_premature_truncation_of_found_paths = true patch_zero_production_crash = true patch_ai_can_sacrifice_without_special_ability = true @@ -440,7 +443,9 @@ enable_ai_production_ranking = true ; Shades/highlights each tile to show how desirable the AI considers it as a city location. To activate, press L while in game. The scale goes from ; white (least desirable) to red (most desirable) with yellow in between. Also the exact number the AI gives any tile can be seen on its info box. +; If show_ai_city_location_desirability_if_settler is true, the desirability overlay will automatically appear when a settler is selected. enable_ai_city_location_desirability_display = true +show_ai_city_location_desirability_if_settler = false ; Setting a government's corruption level to "OFF" will remove all corruption, as expected, instead of maximizing it zero_corruption_when_off = true @@ -742,10 +747,12 @@ no_cross_shore_detection = false ; means cities would only be able to work tiles within their level 1 cultural borders, meaning the eight tiles neighboring the city's own ; tile. Setting to 2 gives you the game's standard rule that cities may work tiles within their level 2 cultural borders, which they attain after one ; expansion. Setting to 3 allows them to work within level 3 borders and so forth. -; Notes: (1) If the city can work tiles in the fourth ring or farther, the map view on the city screen will be zoomed out when you enter it. Otherwise -; it's not possible to show the entire work area. You can zoom back in by pressing Z. (2) The next option below can limit cities' workable areas to be -; smaller than the radius set here, based on their culture. (3) This option has a minimum value of 1 and a maximum of 7. +; Notes: (1) If the city can work tiles in the fourth ring or farther, the map view on the city screen will be zoomed out when you enter it if +; auto_zoom_city_screen_for_large_work_areas is true. Otherwise it's not possible to show the entire work area. You can zoom back in by pressing Z. +; (2) The next option below can limit cities' workable areas to be smaller than the radius set here, based on their culture. (3) This option has a +; minimum value of 1 and a maximum of 7. city_work_radius = 2 +auto_zoom_city_screen_for_large_work_areas = true ; This option can reduce the size of the area cities can work based on their cultural level. ; Possible values are: @@ -795,6 +802,10 @@ introduce_all_human_players_at_start_of_hotseat_game = false ; Allows units to be unloaded from an army. The army type must have the "unload" ability set in the editor. allow_unload_from_army = false +; Allow colonies to be built on tiles inside another civ's territory (for strategic/luxury resources); applies a relation penalty with the other civ, per each colony. +allow_extraterritorial_colonies = false +per_extraterritorial_colony_relation_penalty = 3 + ; A list of rule modifications to make land transports more practical. Works like special_defensive_bombard_rules. The possibilities are: ; load-onto-boat: Allows land transports to be loaded into naval units ; join-army: Allows empty land transports to join armies @@ -844,6 +855,9 @@ draw_forests_over_roads_and_railroads = true ; set to "none". aircraft_victory_animation = none +; Enables naming tiles via the right-click menu and displays those names on the map. +enable_named_tiles = true + [=======================] [=== DAY/NIGHT CYCLE ===] [=======================] @@ -877,6 +891,9 @@ pinned_hour_for_day_night_cycle = 0 ; Show or hide natural wonders on the map. When disabled, natural wonders will not appear on the map. enable_natural_wonders = false +; If a new scenario is loaded which has no natural wonders defined, add natural wonders. +add_natural_wonders_to_scenarios_if_none = false + ; Show the names of natural wonders on the map below their image. show_natural_wonder_name_on_map = true @@ -893,18 +910,30 @@ minimum_natural_wonder_separation = 10 ; their work radius. If multiple cities share a district, they can optionally share its buildings and wonders (see options below). Districts with ; pollution or enemy units yield no bonuses. Destroyed districts remove dependent buildings from affected cities unless they have another district of ; that type in range. The five district types below can be enabled independently: -; enable_districts: Enables standard configurable districts (by default: Encampment, Campus, Holy Site, Commercial Hub, Entertainment Complex, Industrial Zone). -; Configured is done in default.districts_config.txt, user.districts_config.txt, and scenario.districts_config.txt -; enable_neighborhood_districts: Enables population growth past a configurable limit (see maximum_pop_before_neighborhood_needed below) -; enable_wonder_districts: Wonders require a dedicated district tile, making them visible on the map and optionally destructible. -; Configured in default.districts_wonders_config.txt, user.districts_wonders_config.txt, and scenario.districts_wonders_config.txt +; enable_districts: Enables standard configurable districts (by default: Encampment, Campus, Holy Site, Commercial Hub, Entertainment Complex, Industrial Zone). +; Configured is done in default.districts_config.txt, user.districts_config.txt, and scenario.districts_config.txt +; enable_neighborhood_districts: Enables population growth past a configurable limit (see maximum_pop_before_neighborhood_needed below) +; enable_wonder_districts: Wonders require a dedicated district tile, making them visible on the map and optionally destructible. +; Configured in default.districts_wonders_config.txt, user.districts_wonders_config.txt, and scenario.districts_wonders_config.txt ; enable_distribution_hub_districts: Special districts that distribute food/shields from surrounding tiles to all connected cities -; enable_aerodrome_districts: Air units can only be built/based at Aerodrome districts instead of cities (if air_units_use_aerodrome_districts_not_cities is true) +; enable_aerodrome_districts: Air units can only be built/based at Aerodrome districts instead of cities (if air_units_use_aerodrome_districts_not_cities is true) +; enable_port_districts: Enables Port districts, which serve as coastal district tiles for naval production/basing when naval_units_use_port_districts_not_cities is on. +; enable_bridge_districts: Enables Bridge districts on coastal water; completed bridges let land units cross water tiles. +; enable_canal_districts: Enables Canal districts on land; completed canals let naval units traverse land tiles between water bodies. +; enable_central_rail_hub_districts: Enables Central Rail Hub districts used for rail-era infrastructure (e.g., Mass Transit). +; enable_energy_grid_districts: Enables Energy Grid districts associated with power plant infrastructure. +; enable_great_wall_districts: Enables Great Wall districts (wall-like tiles that can block others if great_wall_districts_impassible_by_others is on). enable_districts = false enable_neighborhood_districts = false enable_wonder_districts = false enable_distribution_hub_districts = false enable_aerodrome_districts = false +enable_port_districts = false +enable_bridge_districts = false +enable_canal_districts = false +enable_central_rail_hub_districts = false +enable_energy_grid_districts = false +enable_great_wall_districts = false ; When multiple cities share a district (i.e., the same district tile is within multiple cities' work radii), these options control whether those ; cities automatically share the benefits of buildings and wonders constructed in that district. For example, if Rome and Veii both have the same @@ -916,6 +945,7 @@ enable_aerodrome_districts = false cities_with_mutual_district_receive_buildings = true cities_with_mutual_district_receive_wonders = true show_message_when_building_received_by_mutual_district = true +show_message_when_building_lost_to_destroyed_district = true ; When enabled, air units can only be built in cities with an Aerodrome district in their work radius and must be based at Aerodrome districts instead ; of cities. Air units will spawn on Aerodromes and can only land on Aerodromes, vanilla airfields, or carriers. Airlifts and airdrops are similarly @@ -923,6 +953,11 @@ show_message_when_building_received_by_mutual_district = true ; Only applies when enable_aerodrome_districts is also set to true. air_units_use_aerodrome_districts_not_cities = true +; When enabled, naval units can only be built in cities with a Port district in their work radius and cannot enter city tiles directly. Naval unit +; healing must be done at Port districts, and travel between continents, lakes and so on can instead only be done via Canal districts (via enable_canal_districts), +; rather than cities on an isthmus. Additionally, a city cannot build naval units until it has a Port district in range. Only applies when enable_port_districts is set to true. +naval_units_use_port_districts_not_cities = true + ; Neighborhood district limit population growth. Once a city reaches maximum_pop_before_neighborhood_needed, it cannot ; grow further until it has at least one Neighborhood district in its work radius. Each Neighborhood then enables additional population growth equal to ; per_neighborhood_pop_growth_enabled. For example, with maximum set to 6 and per_neighborhood set to 2, a city can grow to pop 6 without @@ -930,9 +965,11 @@ air_units_use_aerodrome_districts_not_cities = true ; culture, by default) only apply if the city actually needs them based on its population. Periodic popups will remind you when a Neighborhood is needed. Only ; applies when enable_neighborhood_districts is set to true. neighborhood_needed_message_frequency sets how often (in turns) the reminder message appears; ; set to 0 to disable. +; If destroying_neighborhood_reduces_pop is true, losing a Neighborhood district reduces the city's population down to the new cap. maximum_pop_before_neighborhood_needed = 6 per_neighborhood_pop_growth_enabled = 3 neighborhood_needed_message_frequency = 4 +destroying_neighborhood_reduces_pop = true ; These options control the vulnerability and rebuildability of completed Wonders in Wonder districts. When completed_wonder_districts_can_be_destroyed ; is enabled, completed Wonders (both Great and Small) can be pillaged or destroyed by enemies, making them valuable strategic targets that must be @@ -942,15 +979,54 @@ completed_wonder_districts_can_be_destroyed = true destroyed_wonders_can_be_built_again = true ; Distribution Hubs work as "breadbaskets" and mining areas far from urban centers, minimizing local city potential but benefiting the entire civilization. -; Distribution Hubs make surrounding tiles unworkable and instead distribute their raw food and shield yields to ALL connected cities in your civilization. -; The divisor settings prevent this from being overpowered by dividing the raw yields before distribution (e.g., with food_divisor = 2, a hub with 10 -; raw food provides 5 food to each connected city). Divisor results are rounded down. -; Distribution Hub yields are subject to corruption as with regular shields. The ai_ideal_distribution_hub_count_per_100_cities setting controls how many -; Distribution Hubs the AI tries to maintain per 100 cities (e.g., 25 means the AI aims for 1 hub per 4 cities). -; Only applies when enable_distribution_hub_districts is set to true. -distribution_hub_food_yield_divisor = 3 -distribution_hub_shield_yield_divisor = 4 -ai_ideal_distribution_hub_count_per_100_cities = 25 +; Distribution Hubs make surrounding tiles unworkable and instead distribute their raw food and shield yields to ALL connected cities in your civilization. +; "Divisors" for food and shields multiply the sqrt-based divisor in scale-by-city-count mode, and are a straight divider in flat mode. Yields are subject +; to corruption as with regular shields. +; +; ai_ideal_distribution_hub_count_per_100_cities controls how many hubs the AI tries to maintain per 100 cities if distribution_hub_yield_division_mode +; is set to "flat" (e.g., 25 means the AI aims for 1 hub per 4 cities). +; +; distribution_hub_yield_division_mode controls how a hub splits its collected food/shields across connected cities: +; flat: Divide raw yields by the configured divisors (distribution_hub_food_yield_divisor & distribution_hub_shield_yield_divisor) +; scale-by-city-count: Bonuses gradually reduce as more cities plug into the hub. Formula: floor(raw_yield / (sqrt(connected_city_count) * divisor)) +; +; ai_distribution_hub_build_strategy controls how the AI decides to build distribution hubs: +; by-city-count: AI builds hubs based on its ideal hub count per 100 cities +; auto: AI dynamically assesses need based on city growth and food/shield deficits across civ +distribution_hub_yield_division_mode = scale-by-city-count +ai_distribution_hub_build_strategy = auto +distribution_hub_food_yield_divisor = 2 +distribution_hub_shield_yield_divisor = 2 +ai_ideal_distribution_hub_count_per_100_cities = 50 +max_distribution_hub_count_per_100_cities = 50 + +; Bonus percent applied to Distribution Hub food and shield yields for cities that have a Central Rail Hub district in their work radius. +central_rail_hub_distribution_food_bonus_percent = 25 +central_rail_hub_distribution_shield_bonus_percent = 25 + +; District placement and AI bridge/canal behavior: +; expand_water_tile_checks_to_city_work_area: Use the full city work radius (not just adjacent tiles) for river/lake/sea checks (e.g. to build a port if a +; city has coast within its work area, even if not adjacent to the sea). +; Note that this does not affect Aqueducts or Coastal Fortresses, which still require direct city adjacency. +; workers_can_enter_coast: Allow workers to move onto coast tiles without embarking. +; max_contiguous_bridge_districts: Maximum number of contiguous bridge district tiles allowed in a line (0 = no limit). +; max_contiguous_canal_districts: Maximum number of contiguous canal district tiles allowed in a line (0 = no limit). +; ai_canal_eval_min_bisected_land_tiles: Minimum land tiles required on each side for the AI to consider a canal candidate. +; ai_bridge_canal_eval_block_size: Map block size used when scanning for AI bridge/canal candidates. +; ai_bridge_eval_lake_tile_threshold: Water bodies at or below this size are treated as lakes for AI bridge evaluation. +; ai_can_replace_existing_districts_with_canals: Allow the AI to build canal/bridge districts over existing non-wonder districts. +; ai_builds_bridges: Allow the AI to construct bridge districts. +; ai_builds_canals: Allow the AI to construct canal districts. +expand_water_tile_checks_to_city_work_area = true +workers_can_enter_coast = true +max_contiguous_bridge_districts = 3 +max_contiguous_canal_districts = 5 +ai_canal_eval_min_bisected_land_tiles = 20 +ai_bridge_canal_eval_block_size = 10 +ai_bridge_eval_lake_tile_threshold = 5 +ai_can_replace_existing_districts_with_canals = true +ai_builds_bridges = true +ai_builds_canals = true ; When enabled, AI defensive units will actively seek out and defend districts within their territory, treating them as valuable assets like colonies. ; The AI prioritizes defending Wonder districts (if destructible wonders are enabled) over regular districts, searching within a 20-tile radius for @@ -958,6 +1034,22 @@ ai_ideal_distribution_hub_count_per_100_cities = 25 ; infrastructure. Only applies when enable_districts is set to true. ai_defends_districts = true +; As AI cities queue up districts needed, the max number of turns to wait for a worker to build it before the request is dropped +; until the AI city is triggered to build the district again (e.g., by trying to build a building that depends on it) +ai_city_district_max_build_wait_turns = 20 + +; Great Wall behavior: +; disable_great_wall_city_defense_bonus: Disables the Great Wall wonder's city defense doubling effect. +; great_wall_districts_impassible_by_others: Great Wall district tiles block movement by other civs (unless obsolete). +; auto_build_great_wall_around_territory: Auto-place Great Wall districts along your borders when the specified wonder is built. +; great_wall_auto_build_wonder_name: Name of the wonder that triggers auto-building (empty disables auto-build). +; ai_auto_build_great_wall_strategy: How AI uses auto-built Great Wall: all-borders (any border tiles) or other-civ-bordered-only (only borders touching another civ). +disable_great_wall_city_defense_bonus = false +great_wall_districts_impassible_by_others = true +auto_build_great_wall_around_territory = true +great_wall_auto_build_wonder_name = "The Great Wall" +ai_auto_build_great_wall_strategy = other-civ-bordered-only + ; When enabled, holding down the Control key while a worker is selected will highlight all tiles within the work radii of nearby cities. City centers ; are highlighted more brightly, while tiles that fall within multiple cities' work radii are highlighted more intensely. This visual aid helps you ; strategically place districts and improvements by showing which cities will be affected. The highlights update dynamically as you move the worker diff --git a/default.districts_config.txt b/default.districts_config.txt index e232cb6e..f0dc0484 100644 --- a/default.districts_config.txt +++ b/default.districts_config.txt @@ -1,152 +1,489 @@ +[======================================================================= NOTE =======================================================================] +[Instead of editing this file, changes to the settings should be placed in either the scenario or user config files. The scenario config file must ] +[be named scenario.districts_config.txt and must be located in your scenario search folder. Of course it only applies if you are using a scenario. ] +[The user config file must be named user.districts_config.txt and located in the C3X folder, which is the folder where this file is. When creating ] +[scenario or user configs, note that all districts defined here will be removed and only your scenario or user-defined districts will be used ] +[====================================================================================================================================================] + +[ + ; District config fields (each District block begins with "#District") + ; - name : Text (required). Internal district name; must be unique. + ; - display_name : Text. Name shown to user; defaults to name. + ; - tooltip : Text. Shown when hovering over build action button. + ; - img_paths : Comma-separated PCX filenames under Art/Districts/1200/. + 1 for single image, 5 for culture variants (AMER, EURO, ROMAN, MIDEAST, ASIAN). Order matters. + ; - img_column_count : Number. Overrides the number of sprite columns per image (derived from dependent_improvs if omitted). + ; - render_strategy : "by-count" | "by-building". Whether the PCX files show all buildings together, or one building per column. (default: "by-count") + ; - draw_over_resources : 0 or 1. If a resource is also on the tile, draw the district on top. (default: 0) + ; - vary_img_by_era : 0 or 1. If 1, each PCX image must have 4 rows, 1 for each era. + ; - vary_img_by_culture : 0 or 1. If 1, img_paths should list 5 files (AMER, EURO, ROMAN, MIDEAST, ASIAN). + ; - btn_tile_sheet_row : Number. Row index in the district button tile sheet (0-based). + ; - btn_tile_sheet_column : Number. Column index in the district button tile sheet (0-based). + ; - dependent_improvs : Comma-separated city improvement names. Cities can't build these improvements until they have this district. + ; - generated_resource : Resource name plus optional flags: local, yields, no-tech-req. District generates resource if tile connected by road. + ; - advance_prereqs : Comma-separated tech names. District cannot be built unless all techs are discovered. + ; - obsoleted_by : Tech name that obsoletes this district. District cannot built after discovery. + ; - resource_prereqs : Comma-separated resource names. District cannot be built unless a nearby city has all required resources. + ; - resource_prereq_on_tile : Resource name required on the district tile. District cannot be built unless resource is on same tile. + ; - wonder_prereqs : Comma-separated wonder names. District cannot be built unless all Wonders are built by same civ. + ; - natural_wonder_prereqs : Comma-separated natural wonder names. District cannot be built unless all Natural Wonders are within civ's territory. + ; - buildable_on : Comma-separated square types: desert, plains, grassland, tundra, floodplain, hills, mountains, volcano, + ; coast, sea, ocean, snow-forest, snow-mountains, snow-volcano, lake. (Tile *must* be one of these) + ; - buildable_on_overlays : Comma-separated overlays: jungle, forest, swamp. (Optional; Tile *may* be one of these, workers don't need to remove them). + ; - buildable_only_on_overlays : Comma-separated overlays: irrigation, mine, fortress, barricade, jungle, forest, swamp, river. (Optional; Tile *must* be one of these) + ; - buildable_adjacent_to : Same as buildable_on, plus "city". + ; - buildable_adjacent_to_overlays : Same as buildable_only_on_overlays. + ; - buildable_on_districts : Comma-separated district names (tile must already have a completed district of any of these types, which it would replace). + ; - buildable_adjacent_to_districts : Comma-separated district names (adjacent tile must have a completed district of any of these types). + ; - buildable_by_civs : Comma-separated civ names (e.g., Romans, Egyptians). + ; - buildable_by_civ_traits : Comma-separated trait names (e.g., Commercial, Militaristic). + ; - buildable_by_civ_govs : Comma-separated government names (e.g., Republic, Monarchy). + ; - buildable_by_civ_cultures : Comma-separated culture names (e.g., EURO, ASIAN). + ; - buildable_by_war_allies : 0 or 1. Can build if war allied with a civ that can build it. + ; - buildable_by_pact_allies : 0 or 1. Can build if mutual defense pact allied with a civ that can build it. + ; - ai_build_strategy : "district" | "tile-improvement". If "tile_improvement", the AI may build many. (default: "district") + ; - defense_bonus_percent : Number, with optional "Name: bonus" entries (Name = building or tile type, each added to base bonus if true. Negative values allowed). + ; - culture_bonus : Number, with optional "Name: bonus" entries (same pattern as defense_bonus_percent). + ; - science_bonus : Number, with optional "Name: bonus" entries. + ; - food_bonus : Number, with optional "Name: bonus" entries. + ; - gold_bonus : Number, with optional "Name: bonus" entries. + ; - shield_bonus : Number, with optional "Name: bonus" entries. + ; - happiness_bonus : Number, with optional "Name: bonus" entries. + ; - custom_width : Number (pixels). Override sprite width. (default: 128) + ; - custom_height : Number (pixels). Override sprite height. (default: 64) + ; - x_offset : Number (pixels). Push the sprite farther to the right (or left, if negative). (default: 0) + ; - y_offset : Number (pixels). Push the sprite farther down (or up, if negative). (default: 0) + ; - align_to_coast : 0 or 1. Aligns art to coastline, slightly adjusting x & y pixels. (default: 0) + ; - auto_add_road : 0 or 1. Auto-add road on completion. (default: 0) + ; - auto_add_railroad : 0 or 1. Auto-add railroad on completion. (default: 0) + ; - allow_irrigation_from : 0 or 1. District can act as an irrigation source. (default: 0) + ; - allow_multiple : 0 or 1. If 1, multiple copies can exist per city. (default: 0) + ; - heal_units_in_one_turn : 0 or 1. Friendly units fully heal when ending turn here. (default: 0) +] + +[=========================================================================] +[=========================STANDARD DISTRICTS==============================] +[=========================================================================] + +#District +name = Encampment +tooltip = Build Encampment +img_paths = Encampment.pcx +btn_tile_sheet_row = 1 +btn_tile_sheet_column = 0 +vary_img_by_era = 1 +vary_img_by_culture = 0 +advance_prereqs = Warrior Code +dependent_improvs = Barracks,"SAM Missile Battery" +buildable_on = desert,plains,grassland,tundra,floodplain,hills +heal_units_in_one_turn = 1 +defense_bonus_percent = 50, Barracks: 25, hills: 25 +allow_multiple = 1 +culture_bonus = 0 +science_bonus = 0 +food_bonus = 0 +gold_bonus = 0 +shield_bonus = 1, Barracks: 1 +happiness_bonus = 0 + +#District +name = Holy Site +tooltip = Build Holy Site +img_paths = HolySite_AMER.pcx, HolySite_EURO.pcx, HolySite_ROMAN.pcx, HolySite_MIDEAST.pcx, HolySite_ASIAN.pcx +btn_tile_sheet_row = 1 +btn_tile_sheet_column = 1 +vary_img_by_era = 0 +vary_img_by_culture = 1 +advance_prereqs = "Ceremonial Burial" +dependent_improvs = Temple,Cathedral +buildable_on = desert,plains,grassland,tundra,floodplain,hills +defense_bonus_percent = 0 +allow_multiple = 1 +culture_bonus = 2, Temple: 2, Cathedral: 2 +science_bonus = 0 +food_bonus = 0 +gold_bonus = 0 +shield_bonus = 0 +happiness_bonus = 0 + +#District +name = Campus +tooltip = Build Campus +img_paths = Campus.pcx +btn_tile_sheet_row = 1 +btn_tile_sheet_column = 2 +vary_img_by_era = 1 +vary_img_by_culture = 0 +advance_prereqs = Literature +dependent_improvs = Library, University +buildable_on = desert,plains,grassland,tundra,floodplain,hills +defense_bonus_percent = 0 +allow_multiple = 1 +culture_bonus = 1, Library: 2, University: 2 +science_bonus = 1, Library: 2, University: 2 +food_bonus = 0 +gold_bonus = 0 +shield_bonus = 0 +happiness_bonus = 0 + +#District +name = Entertainment Complex +tooltip = Build Entertainment Complex +img_paths = EntertainmentComplex_AMER.pcx, EntertainmentComplex_EURO.pcx, EntertainmentComplex_ROMAN.pcx, EntertainmentComplex_MIDEAST.pcx, EntertainmentComplex_ASIAN.pcx +btn_tile_sheet_row = 1 +btn_tile_sheet_column = 3 +vary_img_by_era = 1 +vary_img_by_culture = 1 +advance_prereqs = Construction +dependent_improvs = Colosseum +buildable_on = desert,plains,grassland,tundra,floodplain,hills +defense_bonus_percent = 0 +allow_multiple = 1 +culture_bonus = 1, Colosseum: 1 +science_bonus = 0 +food_bonus = 0 +gold_bonus = 2, Colosseum: 1 +shield_bonus = 0 +happiness_bonus = 0 + +#District +name = Commercial Hub +tooltip = Build Commercial Hub +img_paths = CommercialHub_AMER.pcx, CommercialHub_EURO.pcx, CommercialHub_ROMAN.pcx, CommercialHub_MIDEAST.pcx, CommercialHub_ASIAN.pcx +btn_tile_sheet_row = 1 +btn_tile_sheet_column = 4 +vary_img_by_era = 1 +vary_img_by_culture = 1 +advance_prereqs = Currency +dependent_improvs = Marketplace, Bank, "Stock Exchange" +buildable_on = desert,plains,grassland,tundra,floodplain,hills +defense_bonus_percent = 0 +allow_multiple = 1 +culture_bonus = 0, Marketplace: 1 +science_bonus = 0 +food_bonus = 0 +gold_bonus = 2, Marketplace: 1, Bank: 1, "Stock Exchange": 1, river: 2 +shield_bonus = 0 +happiness_bonus = 0 + +#District +name = Industrial Zone +tooltip = Build Industrial Zone +img_paths = IndustrialZone.pcx +btn_tile_sheet_row = 1 +btn_tile_sheet_column = 5 +vary_img_by_era = 1 +vary_img_by_culture = 0 +advance_prereqs = Industrialization +dependent_improvs = Factory, "Manufacturing Plant" +buildable_on = desert,plains,grassland,tundra,floodplain,hills +defense_bonus_percent = 0 +allow_multiple = 1 +culture_bonus = 0 +science_bonus = 0 +food_bonus = 0 +gold_bonus = 0 +shield_bonus = 2, Factory: 2, "Manufacturing Plant": 2, river: 2 +happiness_bonus = 0 + +#District +name = Data Center +tooltip = Build Data Center +img_paths = DataCenter.pcx +btn_tile_sheet_row = 1 +btn_tile_sheet_column = 6 +vary_img_by_era = 1 +vary_img_by_culture = 0 +advance_prereqs = Computers +dependent_improvs = "Research Lab" +buildable_on = desert,plains,grassland,tundra,floodplain,hills +defense_bonus_percent = 0 +allow_multiple = 1 +culture_bonus = 0 +science_bonus = 4, "Research Lab": 4 +food_bonus = 0 +gold_bonus = 0 +shield_bonus = 0 +happiness_bonus = -2 + +#District +name = Offshore Extraction Zone +tooltip = Build Offshore Extraction Zone +img_paths = OffshoreExtractionZone.pcx +btn_tile_sheet_row = 1 +btn_tile_sheet_column = 7 +vary_img_by_era = 0 +vary_img_by_culture = 0 +advance_prereqs = Miniaturization +dependent_improvs = Offshore Platform +buildable_on = coast +defense_bonus_percent = 0 +allow_multiple = 1 +culture_bonus = 0 +science_bonus = 0 +food_bonus = 0 +gold_bonus = 0 +shield_bonus = 2 +happiness_bonus = 0 + +#District +name = Park +tooltip = Build Park +img_paths = Park.pcx +btn_tile_sheet_row = 1 +btn_tile_sheet_column = 8 +vary_img_by_era = 1 +vary_img_by_culture = 0 +advance_prereqs = Engineering +dependent_improvs = +buildable_only_on_overlays = forest +defense_bonus_percent = 0 +allow_multiple = 1 +culture_bonus = 2 +science_bonus = 0 +food_bonus = 0 +gold_bonus = 0 +shield_bonus = 0 +happiness_bonus = 2 + +#District +name = Ski Resort +tooltip = Build Ski Resort +img_paths = SkiResort.pcx +btn_tile_sheet_row = 1 +btn_tile_sheet_column = 9 +vary_img_by_era = 0 +vary_img_by_culture = 0 +advance_prereqs = Electricity +dependent_improvs = +buildable_on = snow-mountains +custom_height = 88 +defense_bonus_percent = 0 +allow_multiple = 1 +culture_bonus = 2 +science_bonus = 0 +food_bonus = 0 +gold_bonus = 4 +shield_bonus = 0 +happiness_bonus = 2 + +#District +name = Water Park +tooltip = Build Water Park +img_paths = WaterPark.pcx +btn_tile_sheet_row = 1 +btn_tile_sheet_column = 10 +vary_img_by_era = 0 +vary_img_by_culture = 0 +advance_prereqs = Miniaturization +dependent_improvs = +buildable_on = coast +custom_height = 88 +defense_bonus_percent = 0 +allow_multiple = 0 +culture_bonus = 2 +science_bonus = 0 +food_bonus = 0 +gold_bonus = 4 +shield_bonus = -2 +happiness_bonus = 2 + +#District +name = Municipal District +tooltip = Build Municipal District +img_paths = MunicipalDistrict.pcx +render_strategy = by-building +btn_tile_sheet_row = 1 +btn_tile_sheet_column = 11 +vary_img_by_era = 0 +vary_img_by_culture = 0 +advance_prereqs = Code of Laws +dependent_improvs = Courthouse, Police Station, Hospital +buildable_on = desert,plains,grassland,tundra,floodplain,hills +defense_bonus_percent = 0 +allow_multiple = 1 +culture_bonus = 2 +science_bonus = 0 +food_bonus = 0 +gold_bonus = 0 +shield_bonus = 0 +happiness_bonus = 0 + +[========================================================================] +[=========================SPECIAL DISTRICTS==============================] +[========================================================================] + +#District +name = Neighborhood +tooltip = Build Neighborhood +buildable_on = desert,plains,grassland,tundra,floodplain,hills +advance_prereqs = +auto_add_road = 1 +defense_bonus_percent = 25 +allow_multiple = 1 +culture_bonus = 1 +science_bonus = 1 +food_bonus = 0 +gold_bonus = 1 +shield_bonus = 0 +happiness_bonus = 0 + +#District +name = Wonder District +tooltip = Build Wonder District +buildable_on = desert,plains,grassland,tundra,floodplain,hills,coast,mountains +advance_prereqs = +defense_bonus_percent = 0 +allow_multiple = 1 +culture_bonus = 0 +science_bonus = 0 +food_bonus = 0 +gold_bonus = 0 +shield_bonus = 0 +happiness_bonus = 0 + +#District +name = Distribution Hub +tooltip = Build Distribution Hub +buildable_on = desert,plains,grassland,tundra,floodplain,hills +advance_prereqs = Construction +defense_bonus_percent = 0 +allow_multiple = 1 +culture_bonus = 0 +science_bonus = 0 +food_bonus = 0 +gold_bonus = 0 +shield_bonus = 0 +happiness_bonus = 0 + +#District +name = Aerodrome +tooltip = Build Aerodrome +buildable_on = desert,plains,grassland,tundra,floodplain,hills +advance_prereqs = Flight +dependent_improvs = Airport +defense_bonus_percent = 0 +allow_multiple = 1 +culture_bonus = 0 +science_bonus = 0 +food_bonus = 0 +gold_bonus = 0 +shield_bonus = 0 +happiness_bonus = 0 + +#District +name = Port +tooltip = Build Port +buildable_on = coast +advance_prereqs = Map Making +dependent_improvs = Harbor, "Commercial Dock" +heal_units_in_one_turn = 1 +defense_bonus_percent = 0 +allow_multiple = 1 +culture_bonus = 0 +science_bonus = 0 +food_bonus = 0 +gold_bonus = 2 +shield_bonus = 0 +happiness_bonus = 0 + +#District +name = Central Rail Hub +tooltip = Build Central Rail Hub +btn_tile_sheet_row = 0 +btn_tile_sheet_column = 5 +vary_img_by_era = 1 +vary_img_by_culture = 1 +advance_prereqs = Steam Power +resource_prereqs = Iron, Coal +dependent_improvs = "Mass Transit System" +buildable_on = desert,plains,grassland,tundra,floodplain,hills +auto_add_railroad = 1 +defense_bonus_percent = 0 +allow_multiple = 1 +culture_bonus = 0 +science_bonus = 0 +food_bonus = 0 +gold_bonus = 0 +shield_bonus = 4 +happiness_bonus = 0 + +#District +name = Energy Grid +tooltip = Build Energy Grid +btn_tile_sheet_row = 0 +btn_tile_sheet_column = 6 +vary_img_by_era = 1 +vary_img_by_culture = 1 +custom_height = 84 +advance_prereqs = Industrialization +dependent_improvs = "Coal Plant", "Hydro Plant", "Nuclear Plant", "Solar Plant" +buildable_on = desert,plains,grassland,tundra,floodplain,hills +defense_bonus_percent = 0 +allow_multiple = 1 +culture_bonus = 0 +science_bonus = 0 +food_bonus = 0 +gold_bonus = 0 +shield_bonus = 2 +happiness_bonus = 0 + +#District +name = Bridge +tooltip = Build Bridge +btn_tile_sheet_row = 0 +btn_tile_sheet_column = 7 +custom_height = 112 +custom_width = 176 +y_offset = 24 +x_offset = 0 +advance_prereqs = Industrialization +buildable_on = coast +auto_add_road = 1 +defense_bonus_percent = 0 +allow_multiple = 1 +culture_bonus = 0 +science_bonus = 0 +food_bonus = 0 +gold_bonus = 0 +shield_bonus = 0 +happiness_bonus = 0 + +#District +name = Canal +tooltip = Build Canal +btn_tile_sheet_row = 0 +btn_tile_sheet_column = 8 +custom_height = 112 +custom_width = 176 +y_offset = 24 +x_offset = 0 +advance_prereqs = Industrialization +buildable_on = desert,plains,grassland,tundra,floodplain +defense_bonus_percent = 0 +allow_multiple = 1 +culture_bonus = 0 +science_bonus = 0 +food_bonus = 0 +gold_bonus = 0 +shield_bonus = 0 +happiness_bonus = 0 + #District -name = Encampment -tooltip = Build Encampment -img_paths = Encampment.pcx -btn_tile_sheet_row = 1 -btn_tile_sheet_column = 2 -vary_img_by_era = 1 -vary_img_by_culture = 0 -advance_prereq = Warrior Code -dependent_improvs = Barracks,"SAM Missile Battery" -defense_bonus_percent = 50 -allow_multiple = 1 -culture_bonus = 0 -science_bonus = 0 -food_bonus = 0 -gold_bonus = 0 -shield_bonus = 0 - -#District -name = Holy Site -tooltip = Build Holy Site -img_paths = HolySite_AMER.pcx, HolySite_EURO.pcx, HolySite_ROMAN.pcx, HolySite_MIDEAST.pcx, HolySite_ASIAN.pcx -btn_tile_sheet_row = 1 -btn_tile_sheet_column = 3 -vary_img_by_era = 0 -vary_img_by_culture = 1 -advance_prereq = "Ceremonial Burial" -dependent_improvs = Temple,Cathedral -defense_bonus_percent = 0 -allow_multiple = 1 -culture_bonus = 2 -science_bonus = 0 -food_bonus = 0 -gold_bonus = 0 -shield_bonus = 0 - -#District -name = Campus -tooltip = Build Campus -img_paths = Campus.pcx -btn_tile_sheet_row = 1 -btn_tile_sheet_column = 4 -vary_img_by_era = 1 -vary_img_by_culture = 0 -advance_prereq = Literature -dependent_improvs = Library, University, "Research Lab" -defense_bonus_percent = 0 -allow_multiple = 1 -culture_bonus = 1 -science_bonus = 2 -food_bonus = 0 -gold_bonus = 0 -shield_bonus = 0 - -#District -name = Entertainment Complex -tooltip = Build Entertainment Complex -img_paths = EntertainmentComplex_AMER.pcx, EntertainmentComplex_EURO.pcx, EntertainmentComplex_ROMAN.pcx, EntertainmentComplex_MIDEAST.pcx, EntertainmentComplex_ASIAN.pcx -btn_tile_sheet_row = 1 -btn_tile_sheet_column = 5 -vary_img_by_era = 1 -vary_img_by_culture = 1 -advance_prereq = Construction -dependent_improvs = Colosseum -defense_bonus_percent = 0 -allow_multiple = 1 -culture_bonus = 1 -science_bonus = 0 -food_bonus = 0 -gold_bonus = 2 -shield_bonus = 0 - -#District -name = Commercial Hub -tooltip = Build Commercial Hub -img_paths = CommercialHub_AMER.pcx, CommercialHub_EURO.pcx, CommercialHub_ROMAN.pcx, CommercialHub_MIDEAST.pcx, CommercialHub_ASIAN.pcx -btn_tile_sheet_row = 1 -btn_tile_sheet_column = 6 -vary_img_by_era = 1 -vary_img_by_culture = 1 -advance_prereq = Currency -dependent_improvs = Marketplace, Bank, "Stock Exchange" -defense_bonus_percent = 0 -allow_multiple = 1 -culture_bonus = 0 -science_bonus = 0 -food_bonus = 0 -gold_bonus = 2 -shield_bonus = 0 - -#District -name = Industrial Zone -tooltip = Build Industrial Zone -img_paths = IndustrialZone.pcx -btn_tile_sheet_row = 1 -btn_tile_sheet_column = 7 -vary_img_by_era = 1 -vary_img_by_culture = 0 -advance_prereq = Industrialization -dependent_improvs = Factory, "Manufacturing Plant" -defense_bonus_percent = 0 -allow_multiple = 1 -culture_bonus = 0 -science_bonus = 0 -food_bonus = 0 -gold_bonus = 0 -shield_bonus = 4 - -#District -name = Neighborhood -advance_prereq = -defense_bonus_percent = 25 -allow_multiple = 1 -culture_bonus = 1 -science_bonus = 1 -food_bonus = 0 -gold_bonus = 1 -shield_bonus = 0 - -#District -name = Wonder District -advance_prereq = -defense_bonus_percent = 0 -allow_multiple = 1 -culture_bonus = 0 -science_bonus = 0 -food_bonus = 0 -gold_bonus = 0 -shield_bonus = 0 - -#District -name = Distribution Hub -advance_prereq = Construction -defense_bonus_percent = 0 -allow_multiple = 1 -culture_bonus = 0 -science_bonus = 0 -food_bonus = 0 -gold_bonus = 0 -shield_bonus = 0 - -#District -name = Aerodrome -advance_prereq = Flight -dependent_improvs = Airport -defense_bonus_percent = 0 -allow_multiple = 1 -culture_bonus = 0 -science_bonus = 0 -food_bonus = 0 -gold_bonus = 0 -shield_bonus = 0 \ No newline at end of file +name = Great Wall +btn_tile_sheet_row = 0 +btn_tile_sheet_column = 9 +tooltip = Build Great Wall +obsoleted_by = Metallurgy +wonder_prereqs = "The Great Wall" +buildable_on = desert,plains,grassland,tundra,floodplain,mountains,hills,volcano +buildable_on_overlays = forest,swamp,jungle +draw_over_resources = 1 +defense_bonus_percent = 50 +allow_multiple = 1 +culture_bonus = 0 +science_bonus = 0 +food_bonus = 0 +gold_bonus = 0 +shield_bonus = 0 +happiness_bonus = 0 diff --git a/default.districts_natural_wonders_config.txt b/default.districts_natural_wonders_config.txt index caf8d322..1f0e1b47 100644 --- a/default.districts_natural_wonders_config.txt +++ b/default.districts_natural_wonders_config.txt @@ -1,141 +1,277 @@ +[======================================================================= NOTE =======================================================================] +[Instead of editing this file, changes to the settings should be placed in either the scenario or user config files. The scenario config file must ] +[be named scenario.districts_natural_wonders_config.txt and must be located in your scenario search folder. Of course it only applies if you are ] +[using a scenario. The user config file must be named user.districts_natural_wonders_config.txt and located in the C3X folder, which is the folder ] +[where this file is. When creating scenario or user configs, note that all natural wonders defined here will be removed and only your scenario or ] +[user-defined districts will be used. ] +[====================================================================================================================================================] + +[ + ; Natural Wonder config fields (each Wonder block begins with "#Wonder") + ; - name : Text (required). Internal natural wonder name; must be unique. + ; - terrain_type : Text (required). Base terrain: desert, plains, grassland, jungle, tundra, floodplain, swamp, hills, mountains, + forest, volcano, snow-forest, snow-mountains, snow-volcano, coast, sea, ocean. + ; - adjacent_to : Text. Optional adjacency requirement: same list as buildable square types plus river, any (no requirement). + ; - adjacency_dir : Text. Optional direction filter for adjacent_to (northeast, east, southeast, south, southwest, west, northwest, north). + ; - img_path : Text (required). PCX filename (under Art/Districts/1200/). + ; - img_row : Number (required). Row index in the PCX (0-based). + ; - img_column : Number (required). Column index in the PCX (0-based). + ; - culture_bonus : Number. Culture yield bonus when worked. + ; - science_bonus : Number. Science yield bonus when worked. + ; - food_bonus : Number. Food yield bonus when worked. + ; - gold_bonus : Number. Gold yield bonus when worked. + ; - shield_bonus : Number. Shield yield bonus when worked. + ; - happiness_bonus : Number. Happiness bonus when worked. +] + +#Wonder +name = Angel Falls +terrain_type = grassland +adjacent_to = river +adjacency_dir = southeast +img_path = NaturalWonders.pcx +img_row = 0 +img_column = 0 +culture_bonus = 2 +science_bonus = 1 +food_bonus = 0 +gold_bonus = 0 +shield_bonus = 0 +happiness_bonus = 1 + +#Wonder +name = Yosemite +terrain_type = grassland +adjacent_to = forest +img_path = NaturalWonders.pcx +img_row = 0 +img_column = 1 +culture_bonus = 2 +science_bonus = 1 +food_bonus = 0 +gold_bonus = 0 +shield_bonus = 0 +happiness_bonus = 1 + +#Wonder +name = Mount Fuji +terrain_type = grassland +img_path = NaturalWonders.pcx +img_row = 0 +img_column = 2 +culture_bonus = 2 +science_bonus = 1 +food_bonus = 0 +gold_bonus = 0 +shield_bonus = 0 +happiness_bonus = 1 + +#Wonder +name = Yellowstone +terrain_type = grassland +adjacent_to = forest +img_path = NaturalWonders.pcx +img_row = 0 +img_column = 3 +culture_bonus = 2 +science_bonus = 1 +food_bonus = 0 +gold_bonus = 0 +shield_bonus = 0 +happiness_bonus = 1 + +#Wonder +name = Mount Everest +terrain_type = grassland +adjacent_to = snow-mountains +img_path = NaturalWonders.pcx +img_row = 1 +img_column = 0 +culture_bonus = 2 +science_bonus = 1 +food_bonus = 0 +gold_bonus = 0 +shield_bonus = 0 +happiness_bonus = 1 + +#Wonder +name = Zhangjiajie Mountains +terrain_type = jungle +adjacent_to = jungle +img_path = NaturalWonders.pcx +img_row = 1 +img_column = 1 +culture_bonus = 2 +science_bonus = 1 +food_bonus = 0 +gold_bonus = 0 +shield_bonus = 0 +happiness_bonus = 1 + +#Wonder +name = Mount Kilimanjaro +terrain_type = grassland +img_path = NaturalWonders.pcx +img_row = 1 +img_column = 2 +culture_bonus = 2 +science_bonus = 1 +food_bonus = 0 +gold_bonus = 0 +shield_bonus = 0 +happiness_bonus = 1 + +#Wonder +name = Great Barrier Reef +terrain_type = sea +adjacent_to = coast +img_path = NaturalWonders.pcx +img_row = 1 +img_column = 3 +culture_bonus = 2 +science_bonus = 1 +food_bonus = 0 +gold_bonus = 0 +shield_bonus = 0 +happiness_bonus = 1 + +#Wonder +name = Matterhorn +terrain_type = grassland +adjacent_to = snow-mountains +img_path = NaturalWonders.pcx +img_row = 2 +img_column = 0 +culture_bonus = 2 +science_bonus = 1 +food_bonus = 0 +gold_bonus = 0 +shield_bonus = 0 +happiness_bonus = 1 + +#Wonder +name = Moraine Lake +terrain_type = grassland +adjacent_to = mountains +img_path = NaturalWonders.pcx +img_row = 2 +img_column = 1 +culture_bonus = 2 +science_bonus = 1 +food_bonus = 0 +gold_bonus = 0 +shield_bonus = 0 +happiness_bonus = 1 + +#Wonder +name = Tropical Rainforest +terrain_type = jungle +adjacent_to = jungle +img_path = NaturalWonders.pcx +img_row = 2 +img_column = 2 +culture_bonus = 2 +science_bonus = 1 +food_bonus = 0 +gold_bonus = 0 +shield_bonus = 0 +happiness_bonus = 1 + +#Wonder +name = Wadi Rum +terrain_type = desert +adjacent_to = desert +img_path = NaturalWonders.pcx +img_row = 2 +img_column = 3 +culture_bonus = 2 +science_bonus = 1 +food_bonus = 0 +gold_bonus = 0 +shield_bonus = 0 +happiness_bonus = 1 + +#Wonder +name = Eyjafjallajokull +terrain_type = tundra +adjacent_to = tundra +img_path = NaturalWonders.pcx +img_row = 3 +img_column = 0 +culture_bonus = 2 +science_bonus = 1 +food_bonus = 0 +gold_bonus = 0 +shield_bonus = 0 +happiness_bonus = 1 + +#Wonder +name = Ha Long Bay +terrain_type = coast +adjacent_to = sea +img_path = NaturalWonders.pcx +img_row = 3 +img_column = 1 +culture_bonus = 2 +science_bonus = 1 +food_bonus = 0 +gold_bonus = 0 +shield_bonus = 0 +happiness_bonus = 1 + +#Wonder +name = Lofoten Skerries +terrain_type = coast +adjacent_to = tundra +img_path = NaturalWonders.pcx +img_row = 3 +img_column = 2 +culture_bonus = 2 +science_bonus = 1 +food_bonus = 0 +gold_bonus = 0 +shield_bonus = 0 +happiness_bonus = 1 + +#Wonder +name = Geirangerfjord +terrain_type = tundra +adjacent_to = coast +adjacency_dir = south +img_path = NaturalWonders.pcx +img_row = 3 +img_column = 3 +culture_bonus = 2 +science_bonus = 1 +food_bonus = 0 +gold_bonus = 0 +shield_bonus = 0 +happiness_bonus = 1 + +#Wonder +name = Delicate Arch +terrain_type = desert +adjacent_to = desert +img_path = NaturalWonders.pcx +img_row = 4 +img_column = 0 +culture_bonus = 2 +science_bonus = 1 +food_bonus = 0 +gold_bonus = 0 +shield_bonus = 0 +happiness_bonus = 1 + #Wonder -name = Angel Falls -terrain_type = grassland -adjacent_to = river -adjacency_dir = southeast -img_path = NaturalWonders.pcx -img_row = 0 -img_column = 0 -culture_bonus = 2 -science_bonus = 1 -food_bonus = 0 -gold_bonus = 0 -shield_bonus = 0 - -#Wonder -name = Yosemite -terrain_type = grassland -adjacent_to = forest -img_path = NaturalWonders.pcx -img_row = 0 -img_column = 1 -culture_bonus = 2 -science_bonus = 1 -food_bonus = 0 -gold_bonus = 0 -shield_bonus = 0 - -#Wonder -name = Mount Fuji -terrain_type = grassland -img_path = NaturalWonders.pcx -img_row = 0 -img_column = 2 -culture_bonus = 2 -science_bonus = 1 -food_bonus = 0 -gold_bonus = 0 -shield_bonus = 0 - -#Wonder -name = Yellowstone -terrain_type = grassland -adjacent_to = forest -img_path = NaturalWonders.pcx -img_row = 0 -img_column = 3 -culture_bonus = 2 -science_bonus = 1 -food_bonus = 0 -gold_bonus = 0 -shield_bonus = 0 - -#Wonder -name = Mount Everest -terrain_type = grassland -adjacent_to = mountains -img_path = NaturalWonders.pcx -img_row = 1 -img_column = 0 -culture_bonus = 2 -science_bonus = 1 -food_bonus = 0 -gold_bonus = 0 -shield_bonus = 0 - -#Wonder -name = Zhangjiajie Mountains -terrain_type = jungle -adjacent_to = jungle -img_path = NaturalWonders.pcx -img_row = 1 -img_column = 1 -culture_bonus = 2 -science_bonus = 1 -food_bonus = 0 -gold_bonus = 0 -shield_bonus = 0 - -#Wonder -name = Mount Kilimanjaro -terrain_type = grassland -img_path = NaturalWonders.pcx -img_row = 1 -img_column = 2 -culture_bonus = 2 -science_bonus = 1 -food_bonus = 0 -gold_bonus = 0 -shield_bonus = 0 - -#Wonder -name = Great Barrier Reef -terrain_type = sea -adjacent_to = coast -img_path = NaturalWonders.pcx -img_row = 1 -img_column = 3 -culture_bonus = 2 -science_bonus = 1 -food_bonus = 0 -gold_bonus = 0 -shield_bonus = 0 - -#Wonder -name = Matterhorn -terrain_type = grassland -adjacent_to = mountains -img_path = NaturalWonders.pcx -img_row = 2 -img_column = 0 -culture_bonus = 2 -science_bonus = 1 -food_bonus = 0 -gold_bonus = 0 -shield_bonus = 0 - -#Wonder -name = Moraine Lake -terrain_type = grassland -adjacent_to = mountains -img_path = NaturalWonders.pcx -img_row = 2 -img_column = 1 -culture_bonus = 2 -science_bonus = 1 -food_bonus = 0 -gold_bonus = 0 -shield_bonus = 0 - -#Wonder -name = Tropical Rainforest -terrain_type = jungle -adjacent_to = jungle -img_path = NaturalWonders.pcx -img_row = 2 -img_column = 2 -culture_bonus = 2 -science_bonus = 1 -food_bonus = 0 -gold_bonus = 0 -shield_bonus = 0 \ No newline at end of file +name = Savanna +terrain_type = plains +adjacent_to = plains +img_path = NaturalWonders.pcx +img_row = 4 +img_column = 1 +culture_bonus = 2 +science_bonus = 1 +food_bonus = 0 +gold_bonus = 0 +shield_bonus = 0 +happiness_bonus = 1 \ No newline at end of file diff --git a/default.districts_wonders_config.txt b/default.districts_wonders_config.txt index 83609271..bf8a5fb6 100644 --- a/default.districts_wonders_config.txt +++ b/default.districts_wonders_config.txt @@ -1,5 +1,34 @@ +[======================================================================= NOTE =======================================================================] +[Instead of editing this file, changes to the settings should be placed in either the scenario or user config files. The scenario config file must ] +[be named scenario.districts_wonders_config.txt and must be located in your scenario search folder. Of course it only applies if you are using a ] +[scenario. The user config file must be named user.districts_wonders_config.txt and located in the C3X folder, which is the folder where this file ] +[is. When creating scenario or user configs, note that all wonders defined here will be removed and only your scenario or user-defined districts will] +[be used. ] +[====================================================================================================================================================] + +[ + ; Wonder config fields (each Wonder block begins with "#Wonder") + ; - name : Text (required). Must match the in-game Wonder improvement name. + ; - buildable_on : Comma-separated square types: desert, plains, grassland, tundra, floodplain, hills, mountains, volcano, + coast, sea, ocean, snow-forest, snow-mountains, snow-volcano, any. + ; - buildable_only_on_overlays : Overlays list for wonders only supports "river". Wonder can only be built if tile has river. + ; - buildable_adjacent_to : Same as buildable_on, plus "city". + ; - buildable_adjacent_to_overlays : Same as buildable_only_on_overlays. + ; - img_path : Text. PCX filename (under Art/Districts/1200/). Defaults to Wonders.pcx if omitted. + ; - img_construct_row : Number (required). Row index in the PCX for the "under construction" wonder art (0-based). + ; - img_construct_column : Number (required). Column index in the PCX for the "under construction" wonder art (0-based). + ; - img_row : Number (required). Row index in the PCX for the completed wonder art (0-based). + ; - img_column : Number (required). Column index in the PCX for the completed wonder art (0-based). + ; - enable_img_alt_dir : 0 or 1. If 1, use alternate art when river/city direction implies a flip (see img_alt_dir_* fields). + ; - img_alt_dir_construct_row : Number. Row index for alternate "under construction" art (0-based). + ; - img_alt_dir_construct_column : Number. Column index for alternate "under construction" art (0-based). + ; - img_alt_dir_row : Number. Row index for alternate completed art (0-based). + ; - img_alt_dir_column : Number. Column index for alternate completed art (0-based). +] + #Wonder name = The Pyramids +buildable_on = desert img_path = Wonders.pcx img_construct_row = 0 img_construct_column = 0 @@ -16,6 +45,7 @@ img_column = 3 #Wonder name = The Oracle +buildable_on = hills img_path = Wonders.pcx img_construct_row = 1 img_construct_column = 0 @@ -102,6 +132,44 @@ img_construct_column = 0 img_row = 2 img_column = 1 +#Wonder +name = The Great Lighthouse +buildable_on = coast +img_path = Wonders_2.pcx +img_construct_row = 2 +img_construct_column = 2 +img_row = 2 +img_column = 3 + +#Wonder +name = The Colossus +buildable_on = coast +enable_img_alt_dir = 1 +img_path = Wonders_2.pcx +img_construct_row = 3 +img_construct_column = 0 +img_row = 3 +img_column = 1 +img_alt_dir_construct_row = 3 +img_alt_dir_construct_column = 2 +img_alt_dir_row = 3 +img_alt_dir_column = 3 + +#Wonder +name = Hoover Dam +buildable_on = mountains +buildable_only_on_overlays = river +enable_img_alt_dir = 1 +img_path = Wonders_3.pcx +img_construct_row = 0 +img_construct_column = 0 +img_row = 0 +img_column = 1 +img_alt_dir_construct_row = 0 +img_alt_dir_construct_column = 2 +img_alt_dir_row = 0 +img_alt_dir_column = 3 + #Wonder name = Apollo Program img_path = SmallWonders.pcx @@ -156,4 +224,4 @@ img_path = SmallWonders.pcx img_construct_row = 3 img_construct_column = 0 img_row = 3 -img_column = 1 \ No newline at end of file +img_column = 1 diff --git a/injected_code.c b/injected_code.c index df523ac2..5c87618f 100644 --- a/injected_code.c +++ b/injected_code.c @@ -65,14 +65,32 @@ struct injected_state * is = ADDR_INJECTED_STATE; #define PEDIA_MULTIPAGE_EFFECTS_BUTTON_ID 0x222004 #define PEDIA_MULTIPAGE_PREV_BUTTON_ID 0x222005 +#define TILE_FLAG_ROAD 0x1 +#define TILE_FLAG_RAILROAD 0x2 #define TILE_FLAG_MINE 0x4 +#define TILE_FLAG_IRRIGATION 0x8 #define NEIGHBORHOOD_DISTRICT_ID 0 #define WONDER_DISTRICT_ID 1 #define DISTRIBUTION_HUB_DISTRICT_ID 2 #define AERODROME_DISTRICT_ID 3 #define NATURAL_WONDER_DISTRICT_ID 4 +#define PORT_DISTRICT_ID 5 +#define CENTRAL_RAIL_HUB_DISTRICT_ID 6 +#define ENERGY_GRID_DISTRICT_ID 7 +#define BRIDGE_DISTRICT_ID 8 +#define CANAL_DISTRICT_ID 9 +#define GREAT_WALL_DISTRICT_ID 10 +#define MAX_DISTRICT_VARIANT_COUNT 5 +#define MAX_DISTRICT_ERA_COUNT 4 +#define MAX_DISTRICT_COLUMN_COUNT 10 + +// Max grid of tiles that an AI will evaluate a candidate bridge or canal for, +// used to limit computational complexity +#define AI_BRIDGE_CANAL_CANDIDATE_MAX_EVAL_TILES 10 + +enum { NAMED_TILE_MENU_ID = 0x90 }; char const * const hotseat_replay_save_path = "Saves\\Auto\\ai-move-replay-before-interturn.SAV"; char const * const hotseat_resume_save_path = "Saves\\Auto\\ai-move-replay-resume.SAV"; @@ -198,8 +216,20 @@ get_city_ptr (int id) // Declare various functions needed for districts and hard to untangle and reorder here void __fastcall patch_City_recompute_yields_and_happiness (City * this); void __fastcall patch_Map_build_trade_network (Map * this); +bool __fastcall patch_Unit_can_perform_command (Unit * this, int edx, int unit_command_value); +bool __fastcall patch_Unit_can_pillage (Unit * this, int edx, int tile_x, int tile_y); +bool __fastcall patch_City_has_resource (City * this, int edx, int resource_id); +bool __fastcall patch_Leader_can_build_city_improvement (Leader * this, int edx, int i_improv, bool param_2); +char __fastcall patch_Leader_can_do_worker_job (Leader * this, int edx, enum Worker_Jobs job, int tile_x, int tile_y, int ask_if_replacing); +void __fastcall patch_Unit_despawn (Unit * this, int edx, int civ_id_responsible, byte param_2, byte param_3, byte param_4, byte param_5, byte param_6, byte param_7); +bool can_build_district_on_tile (Tile * tile, int district_id, int civ_id); +bool city_can_build_district (City * city, int district_id); +bool leader_can_build_district (Leader * leader, int district_id); +bool find_civ_trait_id_by_name (struct string_slice const * name, int * out_id); +bool find_civ_culture_id_by_name (struct string_slice const * name, int * out_id); Tile * find_tile_for_district (City * city, int district_id, int * out_x, int * out_y); struct district_instance * get_district_instance (Tile * tile); +struct named_tile_entry * get_named_tile_entry (Tile * tile); bool city_has_required_district (City * city, int district_id); bool district_is_complete (Tile * tile, int district_id); bool city_requires_district_for_improvement (City * city, int improv_id, int * out_district_id); @@ -209,6 +239,7 @@ bool city_radius_contains_tile (City * city, int tile_x, int tile_y); void on_distribution_hub_completed (Tile * tile, int tile_x, int tile_y); bool ai_move_district_worker (Unit * worker, struct district_worker_record * rec); bool has_active_building (City * city, int improv_id); +bool tile_coords_has_city_with_building_in_district_radius (int tile_x, int tile_y, int district_id, int i_improv); void recompute_distribution_hub_totals (); void get_neighbor_coords (Map * map, int x, int y, int neighbor_index, int * out_x, int * out_y); void wrap_tile_coords (Map * map, int * x, int * y); @@ -285,6 +316,7 @@ memoize (int val) is->memo[is->memo_len++] = val; } } + } void @@ -520,7 +552,7 @@ patch_City_controls_tile (City * this, int edx, int neighbor_index, bool conside if (is->current_config.enable_districts || is->current_config.enable_natural_wonders) { // Check if the tile itself is a completed district (includes natural wonders) struct district_instance * inst = get_district_instance (tile); - if (inst != NULL && district_is_complete (tile, inst->district_type)) + if (inst != NULL && district_is_complete (tile, inst->district_id)) return false; // Check if the tile is covered by a distribution hub @@ -599,7 +631,7 @@ patch_Main_Screen_Form_bring_cnter_view_city_focus (Main_Screen_Form * this, int int effective_radius = not_above (get_work_ring_limit_total (p_city_form->CurrentCity), is->current_config.city_work_radius); if (is->current_config.work_area_limit == WAL_CULTURAL_OR_ADJACENT) effective_radius = not_above (is->current_config.city_work_radius, effective_radius + 1); - if (effective_radius >= 4) + if (effective_radius >= 4 && is->current_config.auto_zoom_city_screen_for_large_work_areas) p_bic_data->is_zoomed_out = true; Main_Screen_Form_bring_tile_into_view (this, __, x, get_city_screen_center_y (p_city_form->CurrentCity), param_3, always_update_tile_bounds, param_5); @@ -1726,6 +1758,36 @@ read_day_night_cycle_mode (struct string_slice const * s, int * out_val) return false; } +bool +read_distribution_hub_yield_division_mode (struct string_slice const * s, int * out_val) +{ + struct string_slice trimmed = trim_string_slice (s, 1); + if (slice_matches_str (&trimmed, "flat" )) { *out_val = DHYDM_FLAT; return true; } + else if (slice_matches_str (&trimmed, "scale-by-city-count" )) { *out_val = DHYDM_SCALE_BY_CITY_COUNT; return true; } + else + return false; +} + +bool +read_ai_distribution_hub_build_strategy (struct string_slice const * s, int * out_val) +{ + struct string_slice trimmed = trim_string_slice (s, 1); + if (slice_matches_str (&trimmed, "auto" )) { *out_val = ADHBS_AUTO; return true; } + else if (slice_matches_str (&trimmed, "by-city-count" )) { *out_val = ADHBS_BY_CITY_COUNT; return true; } + else + return false; +} + +bool +read_ai_auto_build_great_wall_strategy (struct string_slice const * s, int * out_val) +{ + struct string_slice trimmed = trim_string_slice (s, 1); + if (slice_matches_str (&trimmed, "all-borders" )) { *out_val = AAGWS_ALL_BORDERS; return true; } + else if (slice_matches_str (&trimmed, "other-civ-bordered-only")) { *out_val = AAGWS_OTHER_CIV_BORDERED_ONLY; return true; } + else + return false; +} + bool read_square_type_value (struct string_slice const * s, enum SquareTypes * out_type) { @@ -1740,22 +1802,25 @@ read_square_type_value (struct string_slice const * s, enum SquareTypes * out_ty char const * name; int value; } const entries[] = { - {"desert", SQ_Desert}, - {"plains", SQ_Plains}, - {"grassland", SQ_Grassland}, - {"tundra", SQ_Tundra}, - {"floodplain", SQ_FloodPlain}, - {"hills", SQ_Hills}, - {"mountains", SQ_Mountains}, - {"forest", SQ_Forest}, - {"jungle", SQ_Jungle}, - {"swamp", SQ_Swamp}, - {"volcano", SQ_Volcano}, - {"coast", SQ_Coast}, - {"sea", SQ_Sea}, - {"ocean", SQ_Ocean}, - {"river", SQ_RIVER}, - {"any", SQ_INVALID} + {"desert", SQ_Desert}, + {"plains", SQ_Plains}, + {"grassland", SQ_Grassland}, + {"tundra", SQ_Tundra}, + {"floodplain", SQ_FloodPlain}, + {"hills", SQ_Hills}, + {"mountains", SQ_Mountains}, + {"forest", SQ_Forest}, + {"jungle", SQ_Jungle}, + {"swamp", SQ_Swamp}, + {"volcano", SQ_Volcano}, + {"coast", SQ_Coast}, + {"sea", SQ_Sea}, + {"ocean", SQ_Ocean}, + {"river", SQ_RIVER}, + {"snow-volcano", SQ_SNOW_VOLCANO}, + {"snow-forest", SQ_SNOW_FOREST}, + {"snow-mountains", SQ_SNOW_MOUNTAIN}, + {"any", SQ_INVALID} }; for (int i = 0; i < (int)ARRAY_LEN (entries); i++) { @@ -1768,6 +1833,181 @@ read_square_type_value (struct string_slice const * s, enum SquareTypes * out_ty return false; } +unsigned int +square_type_mask_bit (enum SquareTypes type) +{ + if ((int)type < 0 || type > SQ_SNOW_MOUNTAIN) + return 0; + return (unsigned int)(1u << type); +} + +unsigned int +district_buildable_mine_mask_bit (void) +{ + return (unsigned int)(1u << (SQ_SNOW_MOUNTAIN + 1)); +} + +unsigned int +district_buildable_irrigation_mask_bit (void) +{ + return (unsigned int)(1u << (SQ_SNOW_MOUNTAIN + 2)); +} + +unsigned int +district_buildable_lake_mask_bit (void) +{ + return (unsigned int)(1u << (SQ_SNOW_MOUNTAIN + 3)); +} + +unsigned int +all_square_types_mask (void) +{ + return (unsigned int)((1u << (SQ_SNOW_MOUNTAIN + 1)) - 1); +} + +unsigned int +district_default_buildable_mask (void) +{ + return (unsigned int)DEFAULT_DISTRICT_BUILDABLE_MASK; +} + +bool +tile_has_snow_mountain (Tile * tile) +{ + return (tile != NULL) && (tile != p_null_tile) && tile->vtable->m29_Check_Mountain_Snowcap (tile); +} + +bool +tile_is_lake (Tile * tile) +{ + if ((tile == NULL) || (tile == p_null_tile)) + return false; + + if (! tile->vtable->m35_Check_Is_Water (tile)) + return false; + + int continent_id = tile->vtable->m46_Get_ContinentID (tile); + if ((continent_id < 0) || (continent_id >= p_bic_data->Map.Continent_Count)) + return false; + + int lake_size_threshold = 21; + Continent * continent = &p_bic_data->Map.Continents[continent_id]; + return continent->Body.TileCount <= lake_size_threshold; +} + +bool +tile_has_snow_volcano (Tile * tile) +{ + if ((tile == NULL) || (tile == p_null_tile)) + return false; + + if (tile->vtable->m50_Get_Square_BaseType (tile) != SQ_Volcano) + return false; + + int overlays = tile->vtable->m43_Get_field_30 (tile); + return (overlays & 0x100000) != 0; +} + +bool +tile_has_snow_forest (Tile * tile) +{ + if ((tile == NULL) || (tile == p_null_tile)) + return false; + + if (tile->vtable->m50_Get_Square_BaseType (tile) != SQ_Forest) + return false; + + int overlays = tile->vtable->m43_Get_field_30 (tile); + return (overlays & 0x100000) != 0; +} + +bool +tile_matches_square_type (Tile * tile, enum SquareTypes type) +{ + if ((tile == NULL) || (tile == p_null_tile)) + return false; + + switch (type) { + case SQ_SNOW_MOUNTAIN: + return tile_has_snow_mountain (tile); + case SQ_SNOW_VOLCANO: + return tile_has_snow_volcano (tile); + case SQ_SNOW_FOREST: + return tile_has_snow_forest (tile); + case SQ_RIVER: + return tile->vtable->m37_Get_River_Code (tile) != 0; + default: + return tile->vtable->m50_Get_Square_BaseType (tile) == type; + } +} + +bool +tile_matches_square_type_mask (Tile * tile, unsigned int mask) +{ + if ((tile == NULL) || (tile == p_null_tile) || (mask == 0)) + return false; + + enum SquareTypes base_type = tile->vtable->m50_Get_Square_BaseType (tile); + unsigned int base_bit = square_type_mask_bit (base_type); + if ((base_bit != 0) && ((mask & base_bit) != 0)) + return true; + + enum SquareTypes const special_types[] = {SQ_RIVER, SQ_SNOW_MOUNTAIN, SQ_SNOW_VOLCANO, SQ_SNOW_FOREST}; + for (int i = 0; i < (int)ARRAY_LEN (special_types); i++) { + enum SquareTypes stype = special_types[i]; + unsigned int bit = square_type_mask_bit (stype); + if ((mask & bit) && tile_matches_square_type (tile, stype)) + return true; + } + + unsigned int mine_bit = district_buildable_mine_mask_bit (); + if ((mask & mine_bit) && tile->vtable->m18_Check_Mines (tile, __, 0)) + return true; + + unsigned int irrigation_bit = district_buildable_irrigation_mask_bit (); + if ((mask & irrigation_bit) && tile->vtable->m17_Check_Irrigation (tile, __, 0)) + return true; + + unsigned int lake_bit = district_buildable_lake_mask_bit (); + if ((mask & lake_bit) && tile_is_lake (tile)) + return true; + + return false; +} + +bool +tile_matches_overlay_mask (Tile * tile, unsigned int mask) +{ + if ((tile == NULL) || (tile == p_null_tile) || (mask == 0)) + return false; + + unsigned int overlays = tile->vtable->m42_Get_Overlays (tile, __, 0); + if ((mask & DOM_MINE) && ((overlays & TILE_FLAG_MINE) != 0)) + return true; + if ((mask & DOM_IRRIGATION) && ((overlays & 0x00000008) != 0)) + return true; + if ((mask & DOM_FORTRESS) && ((overlays & 0x00000010) != 0)) + return true; + if ((mask & DOM_BARRICADE) && ((overlays & 0x10000000) != 0)) + return true; + if ((mask & DOM_OUTPOST) && ((overlays & 0x80000000) != 0)) + return true; + if ((mask & DOM_RADAR_TOWER) && ((overlays & 0x40000000) != 0)) + return true; + if ((mask & DOM_AIRFIELD) && ((overlays & 0x20000000) != 0)) + return true; + if ((mask & DOM_JUNGLE) && tile_matches_square_type (tile, SQ_Jungle)) + return true; + if ((mask & DOM_FOREST) && tile_matches_square_type (tile, SQ_Forest)) + return true; + if ((mask & DOM_SWAMP) && tile_matches_square_type (tile, SQ_Swamp)) + return true; + if ((mask & DOM_RIVER) && tile_matches_square_type (tile, SQ_RIVER)) + return true; + + return false; +} + bool read_natural_wonder_terrain_type (struct string_slice const * s, enum SquareTypes * out_type) { @@ -1784,6 +2024,12 @@ read_natural_wonder_terrain_type (struct string_slice const * s, enum SquareType case SQ_FloodPlain: case SQ_Swamp: case SQ_Hills: + case SQ_Mountains: + case SQ_Forest: + case SQ_Volcano: + case SQ_SNOW_MOUNTAIN: + case SQ_SNOW_FOREST: + case SQ_SNOW_VOLCANO: case SQ_Coast: case SQ_Sea: case SQ_Ocean: @@ -2146,6 +2392,26 @@ load_config (char const * file_path, int path_is_relative_to_mod_dir) } else if (slice_matches_str (&p.key, "day_night_cycle_mode")) { if (! read_day_night_cycle_mode (&value, (int *)&cfg->day_night_cycle_mode)) handle_config_error (&p, CPE_BAD_VALUE); + } else if (slice_matches_str (&p.key, "distribution_hub_yield_division_mode")) { + if (! read_distribution_hub_yield_division_mode (&value, (int *)&cfg->distribution_hub_yield_division_mode)) + handle_config_error (&p, CPE_BAD_VALUE); + } else if (slice_matches_str (&p.key, "ai_distribution_hub_build_strategy")) { + if (! read_ai_distribution_hub_build_strategy (&value, (int *)&cfg->ai_distribution_hub_build_strategy)) + handle_config_error (&p, CPE_BAD_VALUE); + } else if (slice_matches_str (&p.key, "ai_auto_build_great_wall_strategy")) { + if (! read_ai_auto_build_great_wall_strategy (&value, (int *)&cfg->ai_auto_build_great_wall_strategy)) + handle_config_error (&p, CPE_BAD_VALUE); + } else if (slice_matches_str (&p.key, "great_wall_auto_build_wonder_name")) { + struct string_slice trimmed = trim_string_slice (&value, 1); + if (trimmed.len <= 0) { + cfg->great_wall_auto_build_wonder_improv_id = -1; + } else { + int improv_id; + if (find_improv_id_by_name (&trimmed, &improv_id)) + cfg->great_wall_auto_build_wonder_improv_id = improv_id; + else + handle_config_error (&p, CPE_BAD_VALUE); + } } else if (slice_matches_str (&p.key, "exclude_types_from_units_per_tile_limit")) { if (! read_unit_type_list (&value, &unrecognized_lines, &cfg->exclude_types_from_units_per_tile_limit)) handle_config_error (&p, CPE_BAD_VALUE); @@ -2359,9 +2625,6 @@ find_pending_district_request (City * city, int district_id) return NULL; int civ_id = city->Body.CivID; - if ((civ_id < 0) || (civ_id >= 32)) - return NULL; - int city_id = city->Body.ID; int key = get_pending_district_request_key (city_id, district_id); if (key < 0) @@ -2369,11 +2632,10 @@ find_pending_district_request (City * city, int district_id) int stored; if (itable_look_up (&is->city_pending_district_requests[civ_id], key, &stored)) { - struct pending_district_request * req = (struct pending_district_request *)(long)stored; - if ((req != NULL) && (req->city_id == city_id) && (req->district_id == district_id)) { + struct pending_district_request * req = (struct pending_district_request *)stored; + if ((req != NULL) && (req->civ_id == civ_id) && (req->city_id == city_id) && (req->district_id == district_id)) { if (req->city != city) req->city = city; - req->civ_id = civ_id; return req; } } @@ -2418,7 +2680,7 @@ create_pending_district_request (City * city, int district_id) city->Body.CityName, city->Body.ID, district_id, key); (*p_OutputDebugStringA) (ss); - itable_insert (&is->city_pending_district_requests[civ_id], key, (int)(long)req); + itable_insert (&is->city_pending_district_requests[civ_id], key, (int)req); return req; } @@ -2434,7 +2696,7 @@ find_pending_district_request_by_coords (City * city_or_null, int tile_x, int ti return NULL; FOR_TABLE_ENTRIES (tei, &is->city_pending_district_requests[civ_id]) { - struct pending_district_request * req = (struct pending_district_request *)(long)tei.value; + struct pending_district_request * req = (struct pending_district_request *)tei.value; if (req == NULL) continue; if (req->district_id != district_id) continue; if (city_or_null != NULL) { @@ -2458,11 +2720,9 @@ is_tile_earmarked_for_district (int tile_x, int tile_y) return false; int civ_id = tile->vtable->m38_Get_Territory_OwnerID (tile); - if ((civ_id < 0) || (civ_id >= 32)) - return false; FOR_TABLE_ENTRIES (tei, &is->city_pending_district_requests[civ_id]) { - struct pending_district_request * req = (struct pending_district_request *)(long)tei.value; + struct pending_district_request * req = (struct pending_district_request *)tei.value; if (req == NULL) continue; if ((req->target_x == tile_x) && (req->target_y == tile_y)) return true; @@ -2480,7 +2740,12 @@ get_district_instance (Tile * tile) if (! itable_look_up (&is->district_tile_map, (int)tile, &stored_ptr)) return NULL; - return (struct district_instance *)(long)stored_ptr; + struct district_instance * inst = (struct district_instance *)stored_ptr; + + if ((inst == NULL) || (inst->district_id < 0) || (inst->district_id >= is->district_count)) + return NULL; + + return inst; } struct wonder_district_info * @@ -2515,14 +2780,14 @@ district_instance_set_coords (struct district_instance * inst, int tile_x, int t if (inst == NULL) return; - // Normalize coordinates to map bounds for consistency. + // Normalize coordinates to map bounds for consistency wrap_tile_coords (&p_bic_data->Map, &tile_x, &tile_y); inst->tile_x = tile_x; inst->tile_y = tile_y; } struct district_instance * -ensure_district_instance (Tile * tile, int district_type, int tile_x, int tile_y) +ensure_district_instance (Tile * tile, int district_id, int tile_x, int tile_y) { if (tile == NULL || tile == p_null_tile) return NULL; @@ -2537,7 +2802,9 @@ ensure_district_instance (Tile * tile, int district_type, int tile_x, int tile_y return NULL; inst->state = DS_UNDER_CONSTRUCTION; - inst->district_type = district_type; + inst->district_id = district_id; + inst->built_by_civ_id = -1; + inst->completed_turn = -1; // Initialize wonder_info (only relevant for wonder districts) inst->wonder_info.state = WDS_UNUSED; @@ -2547,7 +2814,7 @@ ensure_district_instance (Tile * tile, int district_type, int tile_x, int tile_y inst->natural_wonder_info.natural_wonder_id = -1; district_instance_set_coords (inst, tile_x, tile_y); - itable_insert (&is->district_tile_map, (int)tile, (int)(long)inst); + itable_insert (&is->district_tile_map, (int)tile, (int)inst); return inst; } @@ -2605,12 +2872,42 @@ assign_natural_wonder_to_tile (Tile * tile, int tile_x, int tile_y, int natural_ if (inst == NULL) return; - inst->district_type = NATURAL_WONDER_DISTRICT_ID; + inst->district_id = NATURAL_WONDER_DISTRICT_ID; inst->state = DS_COMPLETED; inst->natural_wonder_info.natural_wonder_id = natural_wonder_id; set_tile_unworkable_for_all_cities (tile, tile_x, tile_y); } +int +apply_district_bonus_entries (struct district_instance * inst, + struct district_bonus_list const * extras, + int district_id) +{ + if ((inst == NULL) || (extras == NULL) || (extras->count <= 0)) + return 0; + + int tile_x = inst->tile_x; + int tile_y = inst->tile_y; + Tile * tile = tile_at (tile_x, tile_y); + if ((tile == NULL) || (tile == p_null_tile)) + return 0; + + int bonus = 0; + for (int i = 0; i < extras->count; i++) { + struct district_bonus_entry const * entry = &extras->entries[i]; + if (entry->type == DBET_TILE) { + if (tile_matches_square_type (tile, entry->tile_type)) + bonus += entry->bonus; + } else if (entry->type == DBET_BUILDING) { + if (entry->building_id >= 0 && + tile_coords_has_city_with_building_in_district_radius (tile_x, tile_y, district_id, entry->building_id)) + bonus += entry->bonus; + } + } + + return bonus; +} + void get_effective_district_yields (struct district_instance * inst, struct district_config const * cfg, @@ -2618,26 +2915,37 @@ get_effective_district_yields (struct district_instance * inst, int * out_shields, int * out_gold, int * out_science, - int * out_culture) + int * out_culture, + int * out_happiness) { - int food = 0, shields = 0, gold = 0, science = 0, culture = 0; + int food = 0, shields = 0, gold = 0, science = 0, culture = 0, happiness = 0; if (cfg != NULL && is->current_config.enable_districts) { - food = cfg->food_bonus; - shields = cfg->shield_bonus; - gold = cfg->gold_bonus; - science = cfg->science_bonus; - culture = cfg->culture_bonus; - } - - if (inst != NULL && is->current_config.enable_natural_wonders && inst->district_type == NATURAL_WONDER_DISTRICT_ID) { + food = cfg->food_bonus; + shields = cfg->shield_bonus; + gold = cfg->gold_bonus; + science = cfg->science_bonus; + culture = cfg->culture_bonus; + happiness = cfg->happiness_bonus; + + int district_id = (inst != NULL) ? inst->district_id : -1; + food += apply_district_bonus_entries (inst, &cfg->food_bonus_extras, district_id); + shields += apply_district_bonus_entries (inst, &cfg->shield_bonus_extras, district_id); + gold += apply_district_bonus_entries (inst, &cfg->gold_bonus_extras, district_id); + science += apply_district_bonus_entries (inst, &cfg->science_bonus_extras, district_id); + culture += apply_district_bonus_entries (inst, &cfg->culture_bonus_extras, district_id); + happiness += apply_district_bonus_entries (inst, &cfg->happiness_bonus_extras, district_id); + } + + if (inst != NULL && is->current_config.enable_natural_wonders && inst->district_id == NATURAL_WONDER_DISTRICT_ID) { struct natural_wonder_district_config const * nwcfg = get_natural_wonder_config_by_id (inst->natural_wonder_info.natural_wonder_id); if (nwcfg != NULL) { - food += nwcfg->food_bonus; - shields += nwcfg->shield_bonus; - gold += nwcfg->gold_bonus; - science += nwcfg->science_bonus; - culture += nwcfg->culture_bonus; + food += nwcfg->food_bonus; + shields += nwcfg->shield_bonus; + gold += nwcfg->gold_bonus; + science += nwcfg->science_bonus; + culture += nwcfg->culture_bonus; + happiness += nwcfg->happiness_bonus; } } @@ -2651,6 +2959,8 @@ get_effective_district_yields (struct district_instance * inst, *out_science = science; if (out_culture != NULL) *out_culture = culture; + if (out_happiness != NULL) + *out_happiness = happiness; } int @@ -2749,7 +3059,7 @@ natural_wonder_exists_within_distance (int tile_x, int tile_y, int min_distance) Tile * here = tile_at (tile_x, tile_y); if ((here != NULL) && (here != p_null_tile)) { struct district_instance * inst = get_district_instance (here); - if ((inst != NULL) && (inst->district_type == NATURAL_WONDER_DISTRICT_ID)) + if ((inst != NULL) && (inst->district_id == NATURAL_WONDER_DISTRICT_ID)) return true; } @@ -2778,7 +3088,7 @@ natural_wonder_exists_within_distance (int tile_x, int tile_y, int min_distance) if ((tile == NULL) || (tile == p_null_tile)) continue; struct district_instance * inst = get_district_instance (tile); - if ((inst != NULL) && (inst->district_type == NATURAL_WONDER_DISTRICT_ID)) + if ((inst != NULL) && (inst->district_id == NATURAL_WONDER_DISTRICT_ID)) return true; } } @@ -2797,7 +3107,7 @@ detach_workers_from_request (struct pending_district_request * req) if ((civ_id < 0) || (civ_id >= 32)) { for (int civ = 0; civ < 32; civ++) { FOR_TABLE_ENTRIES (tei, &is->district_worker_tables[civ]) { - struct district_worker_record * rec = (struct district_worker_record *)(long)tei.value; + struct district_worker_record * rec = (struct district_worker_record *)tei.value; if ((rec != NULL) && (rec->pending_req == req)) rec->pending_req = NULL; } @@ -2806,7 +3116,7 @@ detach_workers_from_request (struct pending_district_request * req) } FOR_TABLE_ENTRIES (tei, &is->district_worker_tables[civ_id]) { - struct district_worker_record * rec = (struct district_worker_record *)(long)tei.value; + struct district_worker_record * rec = (struct district_worker_record *)tei.value; if ((rec != NULL) && (rec->pending_req == req)) rec->pending_req = NULL; } @@ -2831,7 +3141,7 @@ remove_pending_district_request (struct pending_district_request * req) if ((tile != NULL) && (tile != p_null_tile)) { struct district_instance * inst = get_district_instance (tile); if ((inst != NULL) && - (inst->district_type == req->district_id) && + (inst->district_id == req->district_id) && (inst->state != DS_COMPLETED)) remove_district_instance (tile); } @@ -2977,6 +3287,29 @@ direction_to_neighbor_bit (enum direction dir) } } +bool +get_primary_river_direction (Tile * tile, enum direction * out_dir) +{ + if ((tile == NULL) || (tile == p_null_tile)) + return false; + + char river_bits = tile->vtable->m37_Get_River_Code (tile); + if (river_bits == 0) + return false; + + enum direction dirs[] = {DIR_E, DIR_W, DIR_SE, DIR_NE, DIR_SW, DIR_NW, DIR_S, DIR_N}; + for (int i = 0; i < (int)ARRAY_LEN (dirs); i++) { + int bit = direction_to_neighbor_bit (dirs[i]); + if ((bit >= 0) && ((river_bits & (1 << bit)) != 0)) { + if (out_dir != NULL) + *out_dir = dirs[i]; + return true; + } + } + + return false; +} + void wrap_tile_coords (Map * map, int * x, int * y) { @@ -3007,6 +3340,21 @@ tile_index_to_coords (Map * map, int index, int * out_x, int * out_y) *out_x = *out_y = -1; } +int +tile_coords_to_index (Map * map, int x, int y) +{ + if ((map == NULL) || (x < 0) || (y < 0) || (x >= map->Width) || (y >= map->Height)) + return -1; + + int width = map->Width; + int row = y / 2; + if ((y & 1) == 0) { + return row * width + (x / 2); + } else { + return row * width + (width / 2) + (x / 2); + } +} + Tile * tile_at_index (Map * map, int i) { @@ -3112,6 +3460,27 @@ uti_init (Tile * tile) #define FOR_UNITS_ON(uti_name, tile) for (struct unit_tile_iter uti_name = uti_init (tile); uti_name.id != -1; uti_next (&uti_name)) +bool +tile_has_enemy_unit (Tile * tile, int civ_id) +{ + if ((tile == NULL) || (tile == p_null_tile)) + return false; + if ((civ_id < 0) || (civ_id >= 32)) + return false; + + FOR_UNITS_ON (uti, tile) { + Unit * unit = uti.unit; + if ((unit == NULL) || (unit->Body.Container_Unit >= 0)) + continue; + if (unit->Body.CivID == civ_id) + continue; + if (unit->vtable->is_enemy_of_civ (unit, __, civ_id, 0)) + return true; + } + + return false; +} + struct citizen_iter { int index; Citizens * list; @@ -3187,6 +3556,7 @@ struct tiles_around_iter { int center_x, center_y; int n, num_tiles; Tile * tile; + int tile_x, tile_y; }; void @@ -3197,8 +3567,11 @@ tai_next (struct tiles_around_iter * tai) tai->n += 1; int tx, ty; get_neighbor_coords (&p_bic_data->Map, tai->center_x, tai->center_y, tai->n, &tx, &ty); - if ((tx >= 0) && (tx < p_bic_data->Map.Width) && (ty >= 0) && (ty < p_bic_data->Map.Height)) + if ((tx >= 0) && (tx < p_bic_data->Map.Width) && (ty >= 0) && (ty < p_bic_data->Map.Height)) { tai->tile = tile_at (tx, ty); + tai->tile_x = tx; + tai->tile_y = ty; + } } } @@ -3216,38 +3589,351 @@ tai_init (int num_tiles, int x, int y) #define FOR_TILES_AROUND(tai_name, _num_tiles, _x, _y) for (struct tiles_around_iter tai_name = tai_init (_num_tiles, _x, _y); (tai_name.n < tai_name.num_tiles); tai_next (&tai_name)) +enum work_area_iter_output_type { + WAIO_ANY, + WAIO_DISTRICTS, + WAIO_CITIES, +}; + +struct work_area_iter { + int center_x, center_y; + int n, num_tiles; + int civ_id; + int dx, dy; + int tile_x, tile_y; + Tile * tile; + City * city; + struct district_instance * district_inst; + enum work_area_iter_output_type output_type; + bool completed_districts_only; +}; + void -tai_get_coords (struct tiles_around_iter * tai, int * out_x, int * out_y) -{ - if (tai->tile != p_null_tile) - get_neighbor_coords (&p_bic_data->Map, tai->center_x, tai->center_y, tai->n, out_x, out_y); - else { - *out_x = -1; - *out_y = -1; +wai_next (struct work_area_iter * wai) +{ + wai->tile = p_null_tile; + wai->district_inst = NULL; + while ((wai->n + 1) < wai->num_tiles) { + wai->n += 1; + patch_ni_to_diff_for_work_area (wai->n, &wai->dx, &wai->dy); + wai->tile_x = wai->center_x + wai->dx; + wai->tile_y = wai->center_y + wai->dy; + wrap_tile_coords (&p_bic_data->Map, &wai->tile_x, &wai->tile_y); + Tile * candidate = tile_at (wai->tile_x, wai->tile_y); + if ((candidate == NULL) || (candidate == p_null_tile)) + continue; + if ((wai->civ_id < 0) || (candidate->vtable->m38_Get_Territory_OwnerID (candidate) != wai->civ_id)) + continue; + if (tile_has_enemy_unit (candidate, wai->civ_id)) + continue; + if (candidate->vtable->m20_Check_Pollution (candidate, __, 0)) + continue; + City * city; + if (wai->output_type == WAIO_CITIES) { + if (candidate->CityID < 0) + continue; + city = get_city_ptr (candidate->vtable->m45_Get_City_ID (candidate)); + if (city == NULL || city->Body.CivID != wai->civ_id) + continue; + } + struct district_instance * inst = get_district_instance (candidate); + if (wai->output_type == WAIO_DISTRICTS) { + if (inst == NULL) + continue; + int district_id = inst->district_id; + if (wai->completed_districts_only && (! district_is_complete (candidate, district_id))) + continue; + } + wai->city = city; + wai->district_inst = inst; + wai->tile = candidate; + break; } + if (wai->tile == p_null_tile) + wai->n = wai->num_tiles; } -bool -tile_square_type_is (Tile * tile, enum SquareTypes type) +struct work_area_iter +wai_init_common (int x, int y, enum work_area_iter_output_type output, bool completed_districts_only) { - if ((tile == NULL) || (tile == p_null_tile)) - return false; - return tile->vtable->m50_Get_Square_BaseType (tile) == type; + struct work_area_iter tr; + tr.center_x = x; + tr.center_y = y; + tr.n = -1; + tr.num_tiles = is->workable_tile_count; + Tile * center_tile = tile_at (x, y); + if ((center_tile == NULL) || (center_tile == p_null_tile)) + tr.civ_id = -1; + else + tr.civ_id = center_tile->vtable->m38_Get_Territory_OwnerID (center_tile); + tr.tile = p_null_tile; + tr.district_inst = NULL; + tr.dx = 0; + tr.dy = 0; + tr.tile_x = 0; + tr.tile_y = 0; + tr.output_type = output; + tr.completed_districts_only = completed_districts_only; + wai_next (&tr); + return tr; } -bool -natural_wonder_is_coastal_island (Tile * tile, int tile_x, int tile_y) +struct work_area_iter +wai_init (int x, int y) { - if ((tile == NULL) || (tile == p_null_tile)) - return false; + return wai_init_common (x, y, false, false); +} - if (tile->vtable->m35_Check_Is_Water (tile)) - return false; +struct work_area_iter +wai_init_districts (int x, int y, bool completed_only) +{ + return wai_init_common (x, y, WAIO_DISTRICTS, completed_only); +} - bool has_neighbor = false; - FOR_TILES_AROUND (tai, 9, tile_x, tile_y) { - if (tai.n == 0) - continue; +struct work_area_iter +wai_init_cities (int x, int y) +{ + struct work_area_iter tr = wai_init_common (x, y, WAIO_CITIES, false); + return tr; +} + +#define FOR_WORK_AREA_AROUND(wai_name, _x, _y) for (struct work_area_iter wai_name = wai_init (_x, _y); (wai_name.n < wai_name.num_tiles); wai_next (&wai_name)) +#define FOR_DISTRICTS_AROUND(wai_name, _x, _y, _completed_only) for (struct work_area_iter wai_name = wai_init_districts (_x, _y, _completed_only); (wai_name.n < wai_name.num_tiles); wai_next (&wai_name)) +#define FOR_CITIES_AROUND(wai_name, _x, _y) for (struct work_area_iter wai_name = wai_init_cities (_x, _y); (wai_name.n < wai_name.num_tiles); wai_next (&wai_name)) +#define FOR_AERODROMES_AROUND(unit_ptr) \ + for (struct table_entry_iter _tei = tei_init (&is->district_tile_map); \ + _tei.index < _tei.capacity; \ + tei_next (&_tei)) \ + for (Tile * aerodrome_tile = (Tile *)_tei.key; \ + (aerodrome_tile != NULL) && (aerodrome_tile != p_null_tile); \ + aerodrome_tile = NULL) \ + for (struct district_instance * aerodrome_inst = (struct district_instance *)_tei.value; \ + (aerodrome_inst != NULL) && \ + (aerodrome_inst->district_id == AERODROME_DISTRICT_ID) && \ + district_is_complete (aerodrome_tile, AERODROME_DISTRICT_ID); \ + aerodrome_inst = NULL) \ + for (int aerodrome_x = 0, aerodrome_y = 0; \ + district_instance_get_coords (aerodrome_inst, aerodrome_tile, &aerodrome_x, &aerodrome_y) && \ + (aerodrome_tile->vtable->m38_Get_Territory_OwnerID (aerodrome_tile) == (unit_ptr)->Body.CivID) && \ + patch_Unit_is_in_rebase_range ((unit_ptr), __, aerodrome_x, aerodrome_y); \ + aerodrome_x = 0, aerodrome_y = 0) + +struct tile_rings_iter { + int center_x, center_y; + int const * rings; + int ring_count; + int r_idx; + int ni; + int tile_x, tile_y; + int current_ring; + Tile * tile; +}; + +void +tri_next (struct tile_rings_iter * tri) +{ + tri->tile = p_null_tile; + while (tri->r_idx < tri->ring_count) { + int ring_no = tri->rings[tri->r_idx]; + if ((ring_no <= 0) || (ring_no >= 8)) { + tri->r_idx += 1; + tri->ni = -1; + continue; + } + int start_ni = workable_tile_counts[ring_no - 1]; + int end_ni = workable_tile_counts[ring_no]; + if ((tri->ni + 1) < end_ni) + tri->ni += 1; + else { + tri->r_idx += 1; + tri->ni = -1; + continue; + } + tri->current_ring = ring_no; + int dx, dy; + patch_ni_to_diff_for_work_area (tri->ni, &dx, &dy); + int tx = tri->center_x + dx; + int ty = tri->center_y + dy; + wrap_tile_coords (&p_bic_data->Map, &tx, &ty); + tri->tile_x = tx; + tri->tile_y = ty; + Tile * candidate = tile_at (tx, ty); + if ((candidate == NULL) || (candidate == p_null_tile)) + continue; + tri->tile = candidate; + break; + } + if (tri->tile == p_null_tile) { + tri->r_idx = tri->ring_count; + tri->ni = -1; + } +} + +struct tile_rings_iter +tri_init (int x, int y, int const * rings, int ring_count) +{ + struct tile_rings_iter tr; + tr.center_x = x; + tr.center_y = y; + tr.rings = rings; + tr.ring_count = ring_count; + tr.r_idx = 0; + tr.ni = -1; + tr.tile_x = 0; + tr.tile_y = 0; + tr.current_ring = 0; + tr.tile = p_null_tile; + tri_next (&tr); + return tr; +} + +#define FOR_TILE_RINGS_AROUND(tri_name, _x, _y, _rings, _ring_count) for (struct tile_rings_iter tri_name = tri_init (_x, _y, _rings, _ring_count); (tri_name.r_idx < tri_name.ring_count); tri_next (&tri_name)) + +void +tai_get_coords (struct tiles_around_iter * tai, int * out_x, int * out_y) +{ + if (tai->tile != p_null_tile) + get_neighbor_coords (&p_bic_data->Map, tai->center_x, tai->center_y, tai->n, out_x, out_y); + else { + *out_x = -1; + *out_y = -1; + } +} + +bool +tile_square_type_is (Tile * tile, enum SquareTypes type) +{ + return tile_matches_square_type (tile, type); +} + +bool +district_is_buildable_on_square_type (struct district_config const * cfg, Tile * tile) +{ + if ((cfg == NULL) || (tile == NULL) || (tile == p_null_tile)) + return false; + + unsigned int mask = cfg->buildable_square_types_mask; + if (mask == 0) + mask = district_default_buildable_mask (); + + bool square_matches = tile_matches_square_type_mask (tile, mask); + bool overlay_allowed = false; + bool overlay_required = false; + + if (cfg->has_buildable_on_overlays) + overlay_allowed = tile_matches_overlay_mask (tile, cfg->buildable_on_overlays_mask); + + if (cfg->has_buildable_only_on_overlays) { + overlay_required = tile_matches_overlay_mask (tile, cfg->buildable_only_on_overlays_mask); + if (! overlay_required) + return false; + } + + if (! square_matches && ! overlay_allowed && ! overlay_required) + return false; + + if (cfg->has_buildable_on_districts) { + struct district_instance * inst = get_district_instance (tile); + if (inst == NULL) + return false; + + int existing_district_id = inst->district_id; + if (! district_is_complete (tile, existing_district_id)) + return false; + + bool matches = false; + for (int i = 0; i < cfg->buildable_on_district_id_count; i++) { + if (cfg->buildable_on_district_ids[i] == existing_district_id) { + matches = true; + break; + } + } + if (! matches) + return false; + } + + if (cfg->has_buildable_adjacent_to || cfg->has_buildable_adjacent_to_districts || cfg->has_buildable_adjacent_to_overlays) { + int tile_x, tile_y; + if (! tile_coords_from_ptr (&p_bic_data->Map, tile, &tile_x, &tile_y)) + return false; + + if (cfg->has_buildable_adjacent_to) { + bool matches = false; + bool city_adjacent = false; + FOR_TILES_AROUND (tai, 9, tile_x, tile_y) { + if (tai.n == 0) + continue; + if (Tile_has_city (tai.tile)) { + city_adjacent = true; + if (cfg->buildable_adjacent_to_allows_city) + matches = true; + } + if (tile_matches_square_type_mask (tai.tile, cfg->buildable_adjacent_to_square_types_mask)) + matches = true; + if (matches && (cfg->buildable_adjacent_to_allows_city || ! city_adjacent)) + break; + } + if (city_adjacent && ! cfg->buildable_adjacent_to_allows_city) + return false; + if (! matches) + return false; + } + + if (cfg->has_buildable_adjacent_to_overlays) { + bool matches = false; + FOR_TILES_AROUND (tai, 9, tile_x, tile_y) { + if (tai.n == 0) + continue; + if (tile_matches_overlay_mask (tai.tile, cfg->buildable_adjacent_to_overlays_mask)) { + matches = true; + break; + } + } + if (! matches) + return false; + } + + if (cfg->has_buildable_adjacent_to_districts) { + bool matches = false; + FOR_TILES_AROUND (tai, 9, tile_x, tile_y) { + if (tai.n == 0) + continue; + struct district_instance * adj_inst = get_district_instance (tai.tile); + if (adj_inst == NULL) + continue; + int adj_district_id = adj_inst->district_id; + if (! district_is_complete (tai.tile, adj_district_id)) + continue; + for (int i = 0; i < cfg->buildable_adjacent_to_district_id_count; i++) { + if (cfg->buildable_adjacent_to_district_ids[i] == adj_district_id) { + matches = true; + break; + } + } + if (matches) + break; + } + if (! matches) + return false; + } + } + + return true; +} + +bool +natural_wonder_is_coastal_island (Tile * tile, int tile_x, int tile_y) +{ + if ((tile == NULL) || (tile == p_null_tile)) + return false; + + if (tile->vtable->m35_Check_Is_Water (tile)) + return false; + + bool has_neighbor = false; + FOR_TILES_AROUND (tai, 9, tile_x, tile_y) { + if (tai.n == 0) + continue; Tile * adj = tai.tile; if ((adj == NULL) || (adj == p_null_tile)) @@ -3350,15 +4036,25 @@ natural_wonder_terrain_matches (struct natural_wonder_district_config const * cf if ((cfg == NULL) || (tile == NULL) || (tile == p_null_tile)) return false; - enum SquareTypes base_type = tile->vtable->m50_Get_Square_BaseType (tile); - if (base_type != cfg->terrain_type) + if (! tile_matches_square_type (tile, cfg->terrain_type)) return false; + if (cfg->terrain_type == SQ_Coast) { + int continent_id = tile->vtable->m46_Get_ContinentID (tile); + if ((continent_id < 0) || (continent_id >= p_bic_data->Map.Continent_Count)) + return false; + Continent * continent = &p_bic_data->Map.Continents[continent_id]; + if ((continent == NULL) || (continent->Body.TileCount <= 5)) + return false; + } + if (natural_wonder_is_coastal_island (tile, tile_x, tile_y)) return false; - if ((cfg->adjacent_to != SQ_Coast) && - (count_adjacent_tiles_of_type (tile_x, tile_y, SQ_Coast) > 0)) + if ((cfg->adjacent_to != SQ_Coast) && (count_adjacent_tiles_of_type (tile_x, tile_y, SQ_Coast) > 0)) + return false; + + if ((cfg->adjacent_to == SQ_Coast) && (count_adjacent_tiles_of_type (tile_x, tile_y, SQ_Coast) > 4)) return false; if (! natural_wonder_adjacent_requirement_met (cfg, tile, tile_x, tile_y)) @@ -3376,7 +4072,7 @@ get_tracked_worker_record (Unit * worker) int value; if (itable_look_up (&is->district_worker_tables[civ_id], worker->Body.ID, &value)) - return (struct district_worker_record *)(long)value; + return (struct district_worker_record *)value; return NULL; } @@ -3399,7 +4095,7 @@ ensure_tracked_worker_record (Unit * worker) rec->continent_id = ((tile != NULL) && (tile != p_null_tile)) ? tile->vtable->m46_Get_ContinentID (tile) : -1; rec->pending_req = NULL; - itable_insert (&is->district_worker_tables[civ_id], worker->Body.ID, (int)(long)rec); + itable_insert (&is->district_worker_tables[civ_id], worker->Body.ID, (int)rec); return rec; } @@ -3413,7 +4109,7 @@ remove_tracked_worker_record (int civ_id, int unit_id) if (! itable_look_up (&is->district_worker_tables[civ_id], unit_id, &value)) return; - struct district_worker_record * rec = (struct district_worker_record *)(long)value; + struct district_worker_record * rec = (struct district_worker_record *)value; if (rec->pending_req != NULL) { rec->pending_req->assigned_worker_id = -1; rec->pending_req = NULL; @@ -3446,7 +4142,7 @@ clear_tracked_worker_assignment_by_id (int civ_id, int unit_id) if (! itable_look_up (&is->district_worker_tables[civ_id], unit_id, &value)) return; - struct district_worker_record * rec = (struct district_worker_record *)(long)value; + struct district_worker_record * rec = (struct district_worker_record *)value; if ((rec->pending_req != NULL) && (rec->pending_req->assigned_worker_id == unit_id)) rec->pending_req->assigned_worker_id = -1; rec->pending_req = NULL; @@ -3459,7 +4155,7 @@ clear_all_tracked_workers (void) { for (int civ = 0; civ < 32; civ++) { FOR_TABLE_ENTRIES (tei, &is->district_worker_tables[civ]) { - struct district_worker_record * rec = (struct district_worker_record *)(long)tei.value; + struct district_worker_record * rec = (struct district_worker_record *)tei.value; if (rec == NULL) continue; if ((rec->pending_req != NULL) && (rec->pending_req->assigned_worker_id == rec->unit_id)) @@ -3603,7 +4299,7 @@ find_best_worker_for_district (Leader * leader, City * city, int district_id, in (*p_OutputDebugStringA) (ss); FOR_TABLE_ENTRIES (tei, &is->district_worker_tables[civ_id]) { - struct district_worker_record * rec = (struct district_worker_record *)(long)tei.value; + struct district_worker_record * rec = (struct district_worker_record *)tei.value; if (rec == NULL) { continue; @@ -3681,9 +4377,9 @@ process_pending_district_request (Leader * leader, struct pending_district_reque if (req->assigned_worker_id >= 0) { Unit * assigned = get_unit_ptr (req->assigned_worker_id); if (assigned != NULL) { - // Check if more than 5 turns have elapsed since assignment and worker is not at target tile + // Check if more than allowed turns have elapsed since assignment and worker is not at target tile bool worker_at_target = (assigned->Body.X == req->target_x) && (assigned->Body.Y == req->target_y); - if ((*p_current_turn_no - req->worker_assigned_turn) > 5 && !worker_at_target) { + if ((*p_current_turn_no - req->worker_assigned_turn) > is->current_config.ai_city_district_max_build_wait_turns && !worker_at_target) { clear_tracked_worker_assignment_by_id (civ_id, req->assigned_worker_id); req->assigned_worker_id = -1; req->worker_assigned_turn = *p_current_turn_no; @@ -3740,7 +4436,7 @@ assign_workers_for_pending_districts (Leader * leader) return; FOR_TABLE_ENTRIES (tei, &is->city_pending_district_requests[civ_id]) { - struct pending_district_request * req = (struct pending_district_request *)(long)tei.value; + struct pending_district_request * req = (struct pending_district_request *)tei.value; if (req == NULL) continue; @@ -3760,3421 +4456,4997 @@ assign_workers_for_pending_districts (Leader * leader) } } -void -recompute_city_yields_with_districts (City * city) +City * +find_city_for_tile (int civ_id, int tile_x, int tile_y) { - if (city == NULL) - return; + City * best_city = NULL; + int best_dist = INT_MAX; - bool prev_flag = is->distribution_hub_refresh_in_progress; - is->distribution_hub_refresh_in_progress = true; - patch_City_recompute_yields_and_happiness (city); - is->distribution_hub_refresh_in_progress = prev_flag; -} + FOR_CITIES_OF (coi, civ_id) { + City * city = coi.city; + if (city == NULL) + continue; -enum UnitStateType __fastcall -patch_City_instruct_worker (City * this, int edx, int tile_x, int tile_y, bool param_3, Unit * worker) -{ - return City_instruct_worker (this, __, tile_x, tile_y, param_3, worker); + int dist = compute_wrapped_chebyshev_distance (city->Body.X, city->Body.Y, tile_x, tile_y); + if (dist < best_dist) { + best_dist = dist; + best_city = city; + } + } + + return best_city; } -int -find_wonder_config_index_by_improvement_id (int improv_id) +bool +ai_candidate_bridge_or_canal_is_buildable_for_civ (struct ai_candidate_bridge_or_canal_entry * entry, int civ_id, int * out_tile_index) { - if (improv_id < 0) - return -1; + if ((entry == NULL) || (entry->tile_count <= 0)) + return false; - char ss[200]; + int pending_index = -1; + for (int ti = 0; ti < entry->tile_count; ti++) { + int tx = entry->tile_x[ti]; + int ty = entry->tile_y[ti]; + wrap_tile_coords (&p_bic_data->Map, &tx, &ty); + Tile * tile = tile_at (tx, ty); + if ((tile == NULL) || (tile == p_null_tile)) + return false; + int owner = tile->vtable->m38_Get_Territory_OwnerID (tile); + if (owner != civ_id) + return false; + if (tile->CityID >= 0) + return false; - for (int wi = 0; wi < is->wonder_district_count; wi++) { - int bid = -1; - if (stable_look_up (&is->building_name_to_id, is->wonder_district_configs[wi].wonder_name, &bid) && - (bid == improv_id)) { - return wi; + struct district_instance * inst = get_district_instance (tile); + if (inst != NULL) { + if (inst->district_id == entry->district_id) + continue; + if (inst->district_id == WONDER_DISTRICT_ID) + return false; + if (! is->current_config.ai_can_replace_existing_districts_with_canals) { + return false; + } } + + if (pending_index < 0) + pending_index = ti; } - return -1; -} + if (pending_index < 0) { + entry->completed = true; + return false; + } -void set_wonder_built_flag (int improv_id, bool is_built); + if (out_tile_index != NULL) + *out_tile_index = pending_index; -int -get_wonder_improvement_id_from_index (int windex) + return true; +} + +void +release_ai_candidate_bridge_or_canal_worker (struct ai_candidate_bridge_or_canal_entry * entry) { - if ((windex < 0) || (windex >= is->wonder_district_count)) - return -1; + if ((entry == NULL) || (entry->assigned_worker_id < 0)) + return; - char const * wonder_name = is->wonder_district_configs[windex].wonder_name; - if ((wonder_name == NULL) || (wonder_name[0] == '\0')) - return -1; + int civ_id = entry->pending_req.civ_id; + if ((civ_id >= 0) && (civ_id < 32)) + clear_tracked_worker_assignment_by_id (civ_id, entry->assigned_worker_id); - int improv_id; - if (stable_look_up (&is->building_name_to_id, (char *)wonder_name, &improv_id)) - return improv_id; - else - return -1; + entry->assigned_worker_id = -1; + entry->assigned_tile_index = -1; + entry->pending_req.city = NULL; + entry->pending_req.city_id = -1; + entry->pending_req.civ_id = -1; + entry->pending_req.district_id = -1; + entry->pending_req.assigned_worker_id = -1; + entry->pending_req.target_x = -1; + entry->pending_req.target_y = -1; + entry->pending_req.worker_assigned_turn = 0; } void -remember_pending_building_order (City * city, int improvement_id) +reset_ai_candidate_bridge_or_canals (void) { - if (! is->current_config.enable_districts || - (city == NULL) || - (improvement_id < 0)) - return; + if (is->ai_candidate_bridge_or_canals != NULL) { + for (int i = 0; i < is->ai_candidate_bridge_or_canals_capacity; i++) { + struct ai_candidate_bridge_or_canal_entry * entry = &is->ai_candidate_bridge_or_canals[i]; + if (entry->tile_x != NULL) + free (entry->tile_x); + if (entry->tile_y != NULL) + free (entry->tile_y); + } + free (is->ai_candidate_bridge_or_canals); + is->ai_candidate_bridge_or_canals = NULL; + } + is->ai_candidate_bridge_or_canals_capacity = 0; + is->ai_candidate_bridge_or_canals_count = 0; + is->ai_candidate_bridge_or_canals_initialized = false; +} - if ((*p_human_player_bits & (1 << city->Body.CivID)) != 0) - return; +bool +tile_has_district_at (int tile_x, int tile_y, int district_id) +{ + wrap_tile_coords (&p_bic_data->Map, &tile_x, &tile_y); + Tile * tile = tile_at (tile_x, tile_y); + if ((tile == NULL) || (tile == p_null_tile)) + return false; - itable_insert (&is->city_pending_building_orders, (int)(long)city, improvement_id); + struct district_instance * inst = get_district_instance (tile); + return (inst != NULL) && (inst->district_id == district_id) && (district_is_complete (tile, district_id)); } bool -lookup_pending_building_order (City * city, int * out_improv_id) +tile_is_land (int civ_id, int tile_x, int tile_y, bool must_be_same_owner) { - if (! is->current_config.enable_districts || - (city == NULL) || - (out_improv_id == NULL)) + if (must_be_same_owner && (civ_id <= 0)) return false; - return itable_look_up (&is->city_pending_building_orders, (int)(long)city, out_improv_id); + wrap_tile_coords (&p_bic_data->Map, &tile_x, &tile_y); + Tile * tile = tile_at (tile_x, tile_y); + return (tile != NULL) && (tile != p_null_tile) && (! tile->vtable->m35_Check_Is_Water (tile)) && + ((! must_be_same_owner) || (tile->Territory_OwnerID == civ_id)); } -void -forget_pending_building_order (City * city) +bool +tile_is_water (int tile_x, int tile_y) { - if (! is->current_config.enable_districts || - (city == NULL)) - return; - - itable_remove (&is->city_pending_building_orders, (int)(long)city); + wrap_tile_coords (&p_bic_data->Map, &tile_x, &tile_y); + Tile * tile = tile_at (tile_x, tile_y); + return (tile != NULL) && (tile != p_null_tile) && (tile->vtable->m35_Check_Is_Water (tile)); } bool -is_wonder_or_small_wonder_already_being_built (City * city, int improv_id) +ensure_ai_candidate_bridge_or_canals_capacity (int required) { - if ((city == NULL) || (improv_id < 0) || (improv_id >= p_bic_data->ImprovementsCount)) + if (required <= 0) + return true; + if (required <= is->ai_candidate_bridge_or_canals_capacity) + return true; + int new_capacity = (is->ai_candidate_bridge_or_canals_capacity > 0) ? is->ai_candidate_bridge_or_canals_capacity * 2 : 4; + if (new_capacity < required) + new_capacity = required; + struct ai_candidate_bridge_or_canal_entry * larger = (struct ai_candidate_bridge_or_canal_entry *)realloc ( + is->ai_candidate_bridge_or_canals, new_capacity * sizeof *larger); + if (larger == NULL) return false; + for (int i = is->ai_candidate_bridge_or_canals_capacity; i < new_capacity; i++) { + struct ai_candidate_bridge_or_canal_entry * entry = &larger[i]; + entry->tile_x = NULL; + entry->tile_y = NULL; + entry->tile_capacity = 0; + entry->district_id = -1; + entry->owner_civ_id = -1; + entry->tile_count = 0; + entry->assigned_tile_index = -1; + entry->assigned_worker_id = -1; + entry->completed = false; + struct pending_district_request * req = &entry->pending_req; + req->city = NULL; + req->city_id = -1; + req->civ_id = -1; + req->district_id = -1; + req->assigned_worker_id = -1; + req->target_x = -1; + req->target_y = -1; + req->worker_assigned_turn = 0; + } + is->ai_candidate_bridge_or_canals = larger; + is->ai_candidate_bridge_or_canals_capacity = new_capacity; + return true; +} - Improvement * improv = &p_bic_data->Improvements[improv_id]; - if ((improv->Characteristics & (ITC_Wonder | ITC_Small_Wonder)) == 0) - return false; +bool +canal_has_different_adjacent_seas (int tile_x, int tile_y, int civ_id) +{ + struct water_pair { + int dx1, dy1; + int dx2, dy2; + }; - int civ_id = city->Body.CivID; - FOR_CITIES_OF (coi, civ_id) { - City * other_city = coi.city; - if ((other_city == NULL) || (other_city == city)) + const struct water_pair pairs[] = { + { 1, -1, -1, 1 }, // NE + SW + { 1, 1, -1, -1 }, // SE + NW + }; + + Map * map = &p_bic_data->Map; + bool require_owner = (civ_id >= 0); + + for (int i = 0; i < (int)(sizeof (pairs) / sizeof (pairs[0])); i++) { + int ax = tile_x + pairs[i].dx1; + int ay = tile_y + pairs[i].dy1; + wrap_tile_coords (map, &ax, &ay); + Tile * first = tile_at (ax, ay); + if ((first == NULL) || (first == p_null_tile)) + continue; + if (! first->vtable->m35_Check_Is_Water (first)) + continue; + if (require_owner && (first->vtable->m38_Get_Territory_OwnerID (first) != civ_id)) continue; - if ((other_city->Body.Order_Type == COT_Improvement) && - (other_city->Body.Order_ID == improv_id)) + int bx = tile_x + pairs[i].dx2; + int by = tile_y + pairs[i].dy2; + wrap_tile_coords (map, &bx, &by); + Tile * second = tile_at (bx, by); + if ((second == NULL) || (second == p_null_tile)) + continue; + if (! second->vtable->m35_Check_Is_Water (second)) + continue; + if (require_owner && (second->vtable->m38_Get_Territory_OwnerID (second) != civ_id)) + continue; + + int sea_a = first->vtable->m46_Get_ContinentID (first); + int sea_b = second->vtable->m46_Get_ContinentID (second); + if ((sea_a >= 0) && (sea_b >= 0) && (sea_a != sea_b)) return true; } return false; } - +// Check if two water tiles can reach each other via water within a radius, excluding a blocked tile bool -district_is_complete(Tile * tile, int district_id) -{ - if ((tile == NULL) || (tile == p_null_tile) || - (district_id < 0) || (district_id >= is->district_count)) - return false; +water_tiles_connected_within_radius (int start_x, int start_y, int target_x, int target_y, int block_x, int block_y, int radius) +{ + // Simple BFS using a fixed-size visited array for tiles within radius + // workable_tile_counts[6] = 137 tiles for radius 6 + int max_tiles = 137; + int visited_x[137]; + int visited_y[137]; + int visited_count = 0; + int queue_x[137]; + int queue_y[137]; + int queue_head = 0; + int queue_tail = 0; + + queue_x[queue_tail] = start_x; + queue_y[queue_tail] = start_y; + queue_tail++; + visited_x[visited_count] = start_x; + visited_y[visited_count] = start_y; + visited_count++; + + while (queue_head < queue_tail) { + int cx = queue_x[queue_head]; + int cy = queue_y[queue_head]; + queue_head++; + + // Check 8 adjacent tiles + FOR_TILES_AROUND (tai, 9, cx, cy) { + if (tai.n == 0) + continue; + int nx = tai.tile_x; + int ny = tai.tile_y; - bool is_natural_wonder = (district_id == NATURAL_WONDER_DISTRICT_ID); - bool districts_disabled = ! is->current_config.enable_districts; - bool natural_wonders_disabled = ! is->current_config.enable_natural_wonders; + // Found target + if (nx == target_x && ny == target_y) + return true; - if (districts_disabled && (!is_natural_wonder || natural_wonders_disabled)) - return false; + // Skip blocked tile (the isthmus) + if (nx == block_x && ny == block_y) + continue; - struct district_instance * inst = get_district_instance (tile); - if (inst == NULL || inst->district_type != district_id) - return false; + // Check if within radius of original start + int dx = nx - start_x; + int dy = ny - start_y; + if (dx < 0) dx = -dx; + if (dy < 0) dy = -dy; + // Use Chebyshev distance approximation for hex grid + int dist = (dx + dy + ((dx > dy) ? dx : dy)) / 2; + if (dist > radius) + continue; - // If already marked COMPLETED, just return true - if (inst->state == DS_COMPLETED) - return true; + Tile * adj = tai.tile; + if ((adj == NULL) || (adj == p_null_tile)) + continue; + if (! adj->vtable->m35_Check_Is_Water (adj)) + continue; - // State is UNDER_CONSTRUCTION - check if tile has mines now - bool has_mines = tile->vtable->m18_Check_Mines (tile, __, 0) != 0; + // Check if already visited + bool already_visited = false; + for (int i = 0; i < visited_count; i++) { + if (visited_x[i] == nx && visited_y[i] == ny) { + already_visited = true; + break; + } + } + if (already_visited) + continue; - if (! has_mines) { - // Still under construction - check if we should clean it up - bool worker_present = false; - FOR_UNITS_ON (uti, tile) { - Unit * unit = uti.unit; - if ((unit != NULL) && - (p_bic_data->UnitTypes[unit->Body.UnitTypeID].Worker_Actions != 0)) { - worker_present = true; - break; + // Add to queue and visited + if (visited_count < max_tiles && queue_tail < max_tiles) { + visited_x[visited_count] = nx; + visited_y[visited_count] = ny; + visited_count++; + queue_x[queue_tail] = nx; + queue_y[queue_tail] = ny; + queue_tail++; } } - if (! worker_present) { - remove_district_instance (tile); - } - return false; } + return false; +} - // Mark as completed and run one-time side effects - inst->state = DS_COMPLETED; - int tile_x, tile_y; - if (district_instance_get_coords (inst, tile, &tile_x, &tile_y)) { - set_tile_unworkable_for_all_cities (tile, tile_x, tile_y); +// Check if tile separates adjacent water tiles that are not connected within a small radius +bool +canal_has_same_sea_isthmus (int tile_x, int tile_y, int civ_id, int check_radius) +{ + (void) civ_id; + (void) check_radius; - // Activate distribution hub if applicable - if (is->current_config.enable_distribution_hub_districts && - (district_id == DISTRIBUTION_HUB_DISTRICT_ID)) { - on_distribution_hub_completed (tile, tile_x, tile_y); - } + // If another canal exists nearby, this isn't a unique isthmus target. + FOR_TILES_AROUND (tai, workable_tile_counts[2], tile_x, tile_y) { + if (tai.n == 0) + continue; + Tile * adj = tai.tile; + if ((adj == NULL) || (adj == p_null_tile)) + continue; + struct district_instance * adj_inst = get_district_instance (adj); + if ((adj_inst != NULL) && (adj_inst->district_id == CANAL_DISTRICT_ID)) + return false; + } - char ss[200]; - snprintf (ss, sizeof ss, "District %d completed at tile (%d,%d)\n", district_id, tile_x, tile_y); - (*p_OutputDebugStringA) (ss); + // Check opposite diagonal water tiles that are not connected within radius 2 + struct water_pair { + int dx1, dy1; + int dx2, dy2; + }; - // Check if this was an AI-requested district - struct pending_district_request * req = find_pending_district_request_by_coords (NULL, tile_x, tile_y, district_id); - if (req != NULL) { - City * requesting_city = get_city_ptr (req->city_id); - if (requesting_city != NULL) { - req->city = requesting_city; - snprintf (ss, sizeof ss, "District %d at tile (%d,%d) was ordered by city %s\n", - district_id, tile_x, tile_y, requesting_city->Body.CityName); - (*p_OutputDebugStringA) (ss); + const struct water_pair pairs[] = { + { 1, -1, -1, 1 }, // NE + SW + { 1, 1, -1, -1 }, // SE + NW + }; - // Check if city has pending building order that depends on this district - int pending_improv_id; - if (lookup_pending_building_order (requesting_city, &pending_improv_id)) { - int prereq_district_id; - if (itable_look_up (&is->district_building_prereqs, pending_improv_id, &prereq_district_id)) { - if (prereq_district_id == district_id) { - snprintf (ss, sizeof ss, "City %s setting production to improvement %d\n", - requesting_city->Body.CityName, pending_improv_id); - (*p_OutputDebugStringA) (ss); + for (int i = 0; i < (int)(sizeof (pairs) / sizeof (pairs[0])); i++) { + int ax = tile_x + pairs[i].dx1; + int ay = tile_y + pairs[i].dy1; + wrap_tile_coords (&p_bic_data->Map, &ax, &ay); + Tile * first = tile_at (ax, ay); + if ((first == NULL) || (first == p_null_tile)) + continue; + if (! first->vtable->m35_Check_Is_Water (first)) + continue; - // Check if another city is already building this wonder/small wonder - bool can_set_production = true; - if (is_wonder_or_small_wonder_already_being_built (requesting_city, pending_improv_id)) { - snprintf (ss, sizeof ss, "City %s cannot build improvement %d - already being built elsewhere\n", - requesting_city->Body.CityName, pending_improv_id); - (*p_OutputDebugStringA) (ss); - can_set_production = false; - } + int bx = tile_x + pairs[i].dx2; + int by = tile_y + pairs[i].dy2; + wrap_tile_coords (&p_bic_data->Map, &bx, &by); + Tile * second = tile_at (bx, by); + if ((second == NULL) || (second == p_null_tile)) + continue; + if (! second->vtable->m35_Check_Is_Water (second)) + continue; - // Set city production to the pending improvement - if (can_set_production && City_can_build_improvement (requesting_city, __, pending_improv_id, false)) { - snprintf (ss, sizeof ss, "City %s can now build improvement %d\n", - requesting_city->Body.CityName, pending_improv_id); - (*p_OutputDebugStringA) (ss); - City_set_production (requesting_city, __, COT_Improvement, pending_improv_id, false); - } + if (! water_tiles_connected_within_radius ( + ax, ay, + bx, by, + tile_x, tile_y, 2)) { + return true; + } + } + return false; +} - // Clear the pending building order - forget_pending_building_order (requesting_city); - } - } - } +bool +bridge_tile_connects_two_continents (int tile_x, int tile_y, int civ_id) +{ + struct bridge_pair { + int dx1, dy1; + int dx2, dy2; + }; - // Clear worker assignment so worker is freed up for other tasks - if (req->assigned_worker_id >= 0) { - snprintf (ss, sizeof ss, "Clearing worker assignment for unit %d\n", req->assigned_worker_id); - (*p_OutputDebugStringA) (ss); - int civ_id = req->civ_id; - clear_tracked_worker_assignment_by_id (civ_id, req->assigned_worker_id); - } - } + Map * map = &p_bic_data->Map; + const struct bridge_pair pairs[] = { + {0, -2, 0, 2}, + {-2, 0, 2, 0}, + {-1, -1, 1, 1}, + {-1, 1, 1, -1}, + }; - // Remove the pending district request - remove_pending_district_request (req); - } + bool require_owner = (civ_id >= 0); + + for (int i = 0; i < (int)(sizeof (pairs) / sizeof (pairs[0])); i++) { + int ax = tile_x + pairs[i].dx1; + int ay = tile_y + pairs[i].dy1; + wrap_tile_coords (map, &ax, &ay); + if (! tile_is_land (civ_id, ax, ay, require_owner)) + continue; + Tile * first = tile_at (ax, ay); + if ((first == NULL) || (first == p_null_tile)) + continue; + + int bx = tile_x + pairs[i].dx2; + int by = tile_y + pairs[i].dy2; + wrap_tile_coords (map, &bx, &by); + if (! tile_is_land (civ_id, bx, by, require_owner)) + continue; + Tile * second = tile_at (bx, by); + if ((second == NULL) || (second == p_null_tile)) + continue; + + int cont_a = first->vtable->m46_Get_ContinentID (first); + int cont_b = second->vtable->m46_Get_ContinentID (second); + if ((cont_a >= 0) && (cont_b >= 0) && (cont_a != cont_b)) + return true; } - return true; + return false; } -void -mark_city_needs_district (City * city, int district_id) +bool +tile_point_in_block (int tile_x, int tile_y, int block_x0, int block_y0, int block_x1, int block_y1) { - if (! is->current_config.enable_districts || - (city == NULL) || - (district_id < 0) || (district_id >= is->district_count)) - return; - - create_pending_district_request (city, district_id); + return (tile_x >= block_x0) && (tile_x < block_x1) && (tile_y >= block_y0) && (tile_y < block_y1); } -void -set_tile_unworkable_for_all_cities (Tile * tile, int tile_x, int tile_y) +bool +tile_is_coastal_water (int tile_x, int tile_y) { + Map * map = &p_bic_data->Map; + wrap_tile_coords (map, &tile_x, &tile_y); + Tile * tile = tile_at (tile_x, tile_y); if ((tile == NULL) || (tile == p_null_tile)) - return; + return false; + if (! tile->vtable->m35_Check_Is_Water (tile)) + return false; + enum SquareTypes base_type = tile->vtable->m50_Get_Square_BaseType (tile); + return base_type == SQ_Coast; +} +bool +tile_exists_at (int tile_x, int tile_y) +{ wrap_tile_coords (&p_bic_data->Map, &tile_x, &tile_y); - - City * assigned_city = NULL; - int assigned_city_id = tile->Body.CityAreaID; - if (assigned_city_id >= 0) - assigned_city = get_city_ptr (assigned_city_id); - - if (assigned_city != NULL) { - int neighbor_index = Map_compute_neighbor_index (&p_bic_data->Map, __, - assigned_city->Body.X, assigned_city->Body.Y, tile_x, tile_y, 1000); - bool removed_assignment = false; - if ((neighbor_index > 0) && (neighbor_index < ARRAY_LEN (is->ni_to_work_radius))) - removed_assignment = City_stop_working_tile (assigned_city, __, neighbor_index); - if (! removed_assignment) - tile->Body.CityAreaID = -1; - if (! removed_assignment) - recompute_city_yields_with_districts (assigned_city); - } else - tile->Body.CityAreaID = -1; - - if (p_cities->Cities != NULL) { - for (int city_index = 0; city_index <= p_cities->LastIndex; city_index++) { - City * city = get_city_ptr (city_index); - if ((city == NULL) || (city == assigned_city)) - continue; - int neighbor_index = Map_compute_neighbor_index (&p_bic_data->Map, __, - city->Body.X, city->Body.Y, tile_x, tile_y, 1000); - if ((neighbor_index <= 0) || (neighbor_index >= ARRAY_LEN (is->ni_to_work_radius))) - continue; - int work_radius = is->ni_to_work_radius[neighbor_index]; - if ((work_radius < 0) || (work_radius > is->current_config.city_work_radius)) - continue; - recompute_city_yields_with_districts (city); - } - } + Tile * tile = tile_at (tile_x, tile_y); + return (tile != NULL) && (tile != p_null_tile); } -struct distribution_hub_record * -get_distribution_hub_record (Tile * tile) +bool +tile_is_reserved_in_district_tile_map (int tile_x, int tile_y) { + wrap_tile_coords (&p_bic_data->Map, &tile_x, &tile_y); + Tile * tile = tile_at (tile_x, tile_y); if ((tile == NULL) || (tile == p_null_tile)) - return NULL; - - int stored; - if (itable_look_up (&is->distribution_hub_records, (int)tile, &stored)) - return (struct distribution_hub_record *)(long)stored; - else - return NULL; + return false; + int existing = 0; + return itable_look_up (&is->district_tile_map, (int)tile, &existing); } -City * -get_connected_city_for_distribution_hub (struct distribution_hub_record * rec) +bool +tile_has_diagonal_water (int tile_x, int tile_y) { - if (rec == NULL) - return NULL; - - Tile * tile = rec->tile; - if ((tile == NULL) || (tile == p_null_tile)) - tile = tile_at (rec->tile_x, rec->tile_y); - if ((tile == NULL) || (tile == p_null_tile)) - return NULL; + Map * map = &p_bic_data->Map; + int const adj_dx[4] = { 1, 1, -1, -1 }; + int const adj_dy[4] = { -1, 1, -1, 1 }; - int city_id = tile->Body.connected_city_ids[rec->civ_id]; - if (city_id < 0) - return NULL; + for (int i = 0; i < 4; i++) { + int nx = tile_x + adj_dx[i]; + int ny = tile_y + adj_dy[i]; + wrap_tile_coords (map, &nx, &ny); + Tile * tile = tile_at (nx, ny); + if ((tile == NULL) || (tile == p_null_tile)) + continue; + if (tile->vtable->m35_Check_Is_Water (tile)) + return true; + } + return false; +} - City * city = get_city_ptr (city_id); +bool +tile_part_of_existing_candidate (int tile_x, int tile_y) +{ + wrap_tile_coords (&p_bic_data->Map, &tile_x, &tile_y); + for (int ei = 0; ei < is->ai_candidate_bridge_or_canals_count; ei++) { + struct ai_candidate_bridge_or_canal_entry * entry = &is->ai_candidate_bridge_or_canals[ei]; + if (entry->tile_count <= 0) + continue; + for (int ti = 0; ti < entry->tile_count; ti++) { + if ((entry->tile_x[ti] == tile_x) && (entry->tile_y[ti] == tile_y)) + return true; + } + } + return false; +} - return city; +bool +tile_has_bridge_or_canal_nearby (int tile_x, int tile_y) +{ + FOR_TILES_AROUND (tai, workable_tile_counts[1], tile_x, tile_y) { + Tile * adj = tai.tile; + if ((adj == NULL) || (adj == p_null_tile)) + continue; + struct district_instance * inst = get_district_instance (adj); + if ((inst != NULL) && + ((inst->district_id == BRIDGE_DISTRICT_ID) || (inst->district_id == CANAL_DISTRICT_ID))) + return true; + int nx = (tai.n == 0) ? tile_x : tai.tile_x; + int ny = (tai.n == 0) ? tile_y : tai.tile_y; + if (tile_part_of_existing_candidate (nx, ny)) + return true; + } + return false; } bool -distribution_hub_accessible_to_city (struct distribution_hub_record * rec, City * city) +add_ai_candidate_entry (int district_id, short owner_civ_id, short * xs, short * ys, int count) { - if ((rec == NULL) || (city == NULL)) + if (count <= 0) return false; - - if (city->Body.CivID != rec->civ_id) + if (! ensure_ai_candidate_bridge_or_canals_capacity (is->ai_candidate_bridge_or_canals_count + 1)) return false; - City * anchor_city = get_connected_city_for_distribution_hub (rec); - if (anchor_city == NULL) + struct ai_candidate_bridge_or_canal_entry * entry = &is->ai_candidate_bridge_or_canals[is->ai_candidate_bridge_or_canals_count]; + entry->tile_x = (short *)malloc (sizeof *entry->tile_x * count); + entry->tile_y = (short *)malloc (sizeof *entry->tile_y * count); + if ((entry->tile_x == NULL) || (entry->tile_y == NULL)) { + if (entry->tile_x != NULL) + free (entry->tile_x); + if (entry->tile_y != NULL) + free (entry->tile_y); return false; + } - if (anchor_city == city) - return true; + entry->district_id = district_id; + entry->owner_civ_id = owner_civ_id; + entry->tile_count = (short)count; + entry->tile_capacity = count; + entry->assigned_tile_index = -1; + entry->assigned_worker_id = -1; + entry->completed = false; + for (int ti = 0; ti < count; ti++) { + entry->tile_x[ti] = xs[ti]; + entry->tile_y[ti] = ys[ti]; + } - return Trade_Net_have_trade_connection (is->trade_net, __, anchor_city, city, rec->civ_id); -} - -void -get_distribution_hub_yields_for_city (City * city, int * out_food, int * out_shields) -{ - int food = 0; - int shields = 0; - - if ((city != NULL) && - is->current_config.enable_districts && - is->current_config.enable_distribution_hub_districts) { - if (is->distribution_hub_totals_dirty && - ! is->distribution_hub_refresh_in_progress) - recompute_distribution_hub_totals (); - - FOR_TABLE_ENTRIES (tei, &is->distribution_hub_records) { - struct distribution_hub_record * rec = (struct distribution_hub_record *)(long)tei.value; - if (distribution_hub_accessible_to_city (rec, city)) { - food += rec->food_yield; - shields += rec->shield_yield; - } - } - } + struct pending_district_request * req = &entry->pending_req; + req->city = NULL; + req->city_id = -1; + req->civ_id = owner_civ_id; + req->district_id = district_id; + req->assigned_worker_id = -1; + req->target_x = -1; + req->target_y = -1; + req->worker_assigned_turn = 0; - if (out_food != NULL) - *out_food = food; - if (out_shields != NULL) - *out_shields = shields; + is->ai_candidate_bridge_or_canals_count++; + return true; } -void -adjust_distribution_hub_coverage (struct distribution_hub_record * rec) +int +find_bridge_candidate_in_block (Map * map, int block_x0, int block_y0, int block_x1, int block_y1, + int contiguous_limit, int candidate_capacity, + short * out_x, short * out_y, short * out_owner) { - if (rec == NULL) - return; + const int dirs[4][2] = { + { 0, -2 }, { -2, 0 }, { -1, -1 }, { -1, 1 } + }; - FOR_TILES_AROUND (tai, workable_tile_counts[1], rec->tile_x, rec->tile_y) { - Tile * area_tile = tai.tile; - if ((area_tile == NULL) || (area_tile == p_null_tile)) - continue; + int max_len = clamp (1, AI_BRIDGE_CANAL_CANDIDATE_MAX_EVAL_TILES, contiguous_limit); + if (candidate_capacity > 0 && max_len > candidate_capacity) + max_len = candidate_capacity; - int tx, ty; - tai_get_coords (&tai, &tx, &ty); + for (int length = 1; length <= max_len; length++) { + for (int ti = 0; ti < map->TileCount; ti++) { + int tx = -1; + int ty = -1; + tile_index_to_coords (map, ti, &tx, &ty); + if (! tile_point_in_block (tx, ty, block_x0, block_y0, block_x1, block_y1)) + continue; + Tile * tile = tile_at (tx, ty); + if ((tile == NULL) || (tile == p_null_tile)) + continue; + if (tile_has_resource (tile)) + continue; + if (! district_is_buildable_on_square_type (&is->district_configs[BRIDGE_DISTRICT_ID], tile)) + continue; + if (tile_is_reserved_in_district_tile_map (tx, ty)) + continue; + short owner = tile->vtable->m38_Get_Territory_OwnerID (tile); + + for (int di = 0; di < (int)(sizeof (dirs) / sizeof (dirs[0])); di++) { + int dx = dirs[di][0]; + int dy = dirs[di][1]; + int end_x = tx + dx * (length - 1); + int end_y = ty + dy * (length - 1); + wrap_tile_coords (map, &end_x, &end_y); + if (! tile_point_in_block (end_x, end_y, block_x0, block_y0, block_x1, block_y1)) + continue; - if (area_tile->vtable->m38_Get_Territory_OwnerID (area_tile) != rec->civ_id) - continue; - if (Tile_has_city (area_tile)) - continue; - if (get_district_instance (area_tile) != NULL) - continue; + bool ok = true; + for (int step = 0; step < length; step++) { + int cx = tx + dx * step; + int cy = ty + dy * step; + wrap_tile_coords (map, &cx, &cy); + if (! tile_point_in_block (cx, cy, block_x0, block_y0, block_x1, block_y1)) { + ok = false; + break; + } + if (! tile_exists_at (cx, cy)) { + ok = false; + break; + } + if (! tile_is_coastal_water (cx, cy)) { + ok = false; + break; + } + Tile * bridge_tile = tile_at (cx, cy); + if ((bridge_tile == NULL) || (bridge_tile == p_null_tile)) { + ok = false; + break; + } + if (tile_has_resource (bridge_tile)) { + ok = false; + break; + } + if (! district_is_buildable_on_square_type (&is->district_configs[BRIDGE_DISTRICT_ID], bridge_tile)) { + ok = false; + break; + } + if (tile_has_bridge_or_canal_nearby (cx, cy)) { + ok = false; + break; + } + if (tile_is_reserved_in_district_tile_map (cx, cy)) { + ok = false; + break; + } + if (tile_part_of_existing_candidate (cx, cy)) { + ok = false; + break; + } + } + if (! ok) + continue; - struct wonder_district_info * area_info = get_wonder_district_info (area_tile); - if ((area_info != NULL) && (area_info->state == WDS_COMPLETED)) - continue; + int land_ax = tx - dx; + int land_ay = ty - dy; + wrap_tile_coords (map, &land_ax, &land_ay); + if (! tile_point_in_block (land_ax, land_ay, block_x0, block_y0, block_x1, block_y1)) + continue; + if (! tile_exists_at (land_ax, land_ay)) + continue; + if (! tile_is_land (-1, land_ax, land_ay, false)) + continue; + Tile * land_a = tile_at (land_ax, land_ay); + if ((land_a == NULL) || (land_a == p_null_tile)) + continue; - int key = (int)area_tile; - int prev = itable_look_up_or (&is->distribution_hub_coverage_counts, key, 0); - itable_insert (&is->distribution_hub_coverage_counts, key, prev + 1); + int land_bx = tx + dx * length; + int land_by = ty + dy * length; + wrap_tile_coords (map, &land_bx, &land_by); + if (! tile_point_in_block (land_bx, land_by, block_x0, block_y0, block_x1, block_y1)) + continue; + if (! tile_exists_at (land_bx, land_by)) + continue; + if (! tile_is_land (-1, land_bx, land_by, false)) + continue; + Tile * land_b = tile_at (land_bx, land_by); + if ((land_b == NULL) || (land_b == p_null_tile)) + continue; - if (area_tile->Body.CityAreaID >= 0) { - set_tile_unworkable_for_all_cities (area_tile, tx, ty); - area_tile->Body.CityAreaID = -1; + int cont_a = land_a->vtable->m46_Get_ContinentID (land_a); + int cont_b = land_b->vtable->m46_Get_ContinentID (land_b); + if ((cont_a < 0) || (cont_b < 0) || (cont_a == cont_b)) + continue; + + for (int step = 0; step < length; step++) { + int cx = tx + dx * step; + int cy = ty + dy * step; + wrap_tile_coords (map, &cx, &cy); + out_x[step] = (short)cx; + out_y[step] = (short)cy; + } + *out_owner = owner; + return length; + } } } + return 0; } -void -release_distribution_hub_coverage (struct distribution_hub_record * rec) +int +gather_canal_line (int start_x, int start_y, int dx, int dy, int limit, + int block_x0, int block_y0, int block_x1, int block_y1, + short * out_x, short * out_y) { - if (rec == NULL) - return; + int effective_limit = clamp (1, AI_BRIDGE_CANAL_CANDIDATE_MAX_EVAL_TILES, limit); - FOR_TILES_AROUND (tai, workable_tile_counts[1], rec->tile_x, rec->tile_y) { - Tile * area_tile = tai.tile; - if ((area_tile == NULL) || (area_tile == p_null_tile)) - continue; + if (! tile_is_land (-1, start_x, start_y, false)) + return 0; + if (tile_part_of_existing_candidate (start_x, start_y)) + return 0; - int key = (int)area_tile; - int prev = itable_look_up_or (&is->distribution_hub_coverage_counts, key, 0); - if (prev <= 0) - continue; + short back_x[AI_BRIDGE_CANAL_CANDIDATE_MAX_EVAL_TILES]; + short back_y[AI_BRIDGE_CANAL_CANDIDATE_MAX_EVAL_TILES]; + short forward_x[AI_BRIDGE_CANAL_CANDIDATE_MAX_EVAL_TILES]; + short forward_y[AI_BRIDGE_CANAL_CANDIDATE_MAX_EVAL_TILES]; + int back_count = 0; + int forward_count = 0; + int remaining = effective_limit - 1; + Map * map = &p_bic_data->Map; - if (prev == 1) - itable_remove (&is->distribution_hub_coverage_counts, key); - else - itable_insert (&is->distribution_hub_coverage_counts, key, prev - 1); + int cx = start_x; + int cy = start_y; + while ((remaining > 0) && (back_count < effective_limit)) { + int nx = cx - dx; + int ny = cy - dy; + wrap_tile_coords (map, &nx, &ny); + if (! tile_point_in_block (nx, ny, block_x0, block_y0, block_x1, block_y1)) + break; + if (! tile_is_land (-1, nx, ny, false)) + break; + if (tile_part_of_existing_candidate (nx, ny)) + break; + back_x[back_count] = (short)nx; + back_y[back_count] = (short)ny; + back_count++; + remaining--; + cx = nx; + cy = ny; + } + + cx = start_x; + cy = start_y; + while ((remaining > 0) && (forward_count < effective_limit)) { + int nx = cx + dx; + int ny = cy + dy; + wrap_tile_coords (map, &nx, &ny); + if (! tile_point_in_block (nx, ny, block_x0, block_y0, block_x1, block_y1)) + break; + if (! tile_is_land (-1, nx, ny, false)) + break; + if (tile_part_of_existing_candidate (nx, ny)) + break; + forward_x[forward_count] = (short)nx; + forward_y[forward_count] = (short)ny; + forward_count++; + remaining--; + cx = nx; + cy = ny; } -} -void -clear_distribution_hub_tables (void) -{ - FOR_TABLE_ENTRIES (tei, &is->distribution_hub_records) { - struct distribution_hub_record * rec = (struct distribution_hub_record *)(long)tei.value; - free (rec); + int write = 0; + for (int bi = back_count - 1; bi >= 0; bi--) { + out_x[write] = back_x[bi]; + out_y[write] = back_y[bi]; + write++; + } + out_x[write] = (short)start_x; + out_y[write] = (short)start_y; + write++; + for (int fi = 0; fi < forward_count; fi++) { + out_x[write] = forward_x[fi]; + out_y[write] = forward_y[fi]; + write++; } - table_deinit (&is->distribution_hub_records); - table_deinit (&is->distribution_hub_coverage_counts); - is->distribution_hub_totals_dirty = true; -} - -bool -city_radius_contains_tile (City * city, int tile_x, int tile_y) -{ - if (city == NULL) - return false; - int ni = patch_Map_compute_ni_for_work_area (&p_bic_data->Map, __, city->Body.X, city->Body.Y, tile_x, tile_y, is->workable_tile_count); - return ni >= 0; + return write; } bool -tile_has_enemy_unit (Tile * tile, int civ_id) +cluster_connects_two_seas_or_isthmus (short * xs, short * ys, int count) { - if ((tile == NULL) || (tile == p_null_tile)) - return false; - if ((civ_id < 0) || (civ_id >= 32)) - return false; - - FOR_UNITS_ON (uti, tile) { - Unit * unit = uti.unit; - if ((unit == NULL) || (unit->Body.Container_Unit >= 0)) - continue; - if (unit->Body.CivID == civ_id) - continue; - if (unit->vtable->is_enemy_of_civ (unit, __, civ_id, 0)) + for (int i = 0; i < count; i++) { + if (canal_has_different_adjacent_seas (xs[i], ys[i], -1)) + return true; + if (canal_has_same_sea_isthmus (xs[i], ys[i], -1, 2)) return true; } - return false; } -void -recompute_distribution_hub_yields (struct distribution_hub_record * rec) +int +find_canal_candidate_in_block (Map * map, int block_x0, int block_y0, int block_x1, int block_y1, + int contiguous_limit, int candidate_capacity, + short * out_x, short * out_y, short * out_owner) { - if (rec == NULL) - return; + const int dir_dx[8] = { 0, 1, 2, 1, 0, -1, -2, -1 }; + const int dir_dy[8] = { -2, -1, 0, 1, 2, 1, 0, -1 }; - Tile * tile = tile_at (rec->tile_x, rec->tile_y); - rec->tile = tile; + int max_len = clamp (1, AI_BRIDGE_CANAL_CANDIDATE_MAX_EVAL_TILES, contiguous_limit); + if (candidate_capacity > 0 && max_len > candidate_capacity) + max_len = candidate_capacity; - if ((tile == NULL) || - (tile == p_null_tile) || - ! district_is_complete (tile, DISTRIBUTION_HUB_DISTRICT_ID) || - tile->vtable->m20_Check_Pollution (tile, __, 0) || - tile_has_enemy_unit (tile, rec->civ_id)) { - rec->food_yield = 0; - rec->shield_yield = 0; - rec->raw_food_yield = 0; - rec->raw_shield_yield = 0; - return; + int tile_count = map->TileCount; + int * visit = (int *)malloc (sizeof (*visit) * tile_count); + int * queue_x = (int *)malloc (sizeof (*queue_x) * tile_count); + int * queue_y = (int *)malloc (sizeof (*queue_y) * tile_count); + if ((visit == NULL) || (queue_x == NULL) || (queue_y == NULL)) { + if (visit != NULL) + free (visit); + if (queue_x != NULL) + free (queue_x); + if (queue_y != NULL) + free (queue_y); + return 0; } + for (int i = 0; i < tile_count; i++) + visit[i] = 0; - int food_sum = 0; - int shield_sum = 0; - City * anchor_city = get_connected_city_for_distribution_hub (rec); - FOR_TILES_AROUND (tai, workable_tile_counts[1], rec->tile_x, rec->tile_y) { - Tile * area_tile = tai.tile; - if (area_tile == p_null_tile) - continue; - - int tx, ty; - tai_get_coords (&tai, &tx, &ty); - - // Only include tiles that belong to the distribution hub owner - if (area_tile->vtable->m38_Get_Territory_OwnerID (area_tile) != rec->civ_id) - continue; + int visit_mark = 1; - // Skip city tiles - if (Tile_has_city (area_tile)) - continue; + for (int length = 1; length <= max_len; length++) { + for (int ti = 0; ti < map->TileCount; ti++) { + int start_x = -1; + int start_y = -1; + tile_index_to_coords (map, ti, &start_x, &start_y); + if (! tile_point_in_block (start_x, start_y, block_x0, block_y0, block_x1, block_y1)) + continue; + if (! tile_is_land (-1, start_x, start_y, false)) + continue; + if (tile_part_of_existing_candidate (start_x, start_y)) + continue; + if (tile_has_bridge_or_canal_nearby (start_x, start_y)) + continue; + if (tile_is_reserved_in_district_tile_map (start_x, start_y)) + continue; + Tile * start_tile = tile_at (start_x, start_y); + if ((start_tile == NULL) || (start_tile == p_null_tile)) + continue; + if (tile_has_resource (start_tile)) + continue; + if (! district_is_buildable_on_square_type (&is->district_configs[CANAL_DISTRICT_ID], start_tile)) + continue; + int continent_id = start_tile->vtable->m46_Get_ContinentID (start_tile); + if (continent_id < 0) + continue; + short owner = start_tile->vtable->m38_Get_Territory_OwnerID (start_tile); + + int stack_len = 1; + int dir_stack[AI_BRIDGE_CANAL_CANDIDATE_MAX_EVAL_TILES]; + int path_dir[AI_BRIDGE_CANAL_CANDIDATE_MAX_EVAL_TILES]; + out_x[0] = (short)start_x; + out_y[0] = (short)start_y; + dir_stack[0] = -1; + path_dir[0] = -1; + + while (stack_len > 0) { + int depth = stack_len - 1; + int cx = out_x[depth]; + int cy = out_y[depth]; + + if (depth + 1 == length) { + bool endpoints_ok = false; + if (length == 1) { + int ex = out_x[0]; + int ey = out_y[0]; + bool pair_ok = false; + if (tile_is_water (ex, ey - 2) && tile_is_water (ex, ey + 2)) pair_ok = true; + if (tile_is_water (ex - 2, ey) && tile_is_water (ex + 2, ey)) pair_ok = true; + if (tile_is_water (ex - 1, ey - 1) && tile_is_water (ex + 1, ey + 1)) pair_ok = true; + if (tile_is_water (ex - 1, ey + 1) && tile_is_water (ex + 1, ey - 1)) pair_ok = true; + if (pair_ok && tile_has_diagonal_water (ex, ey)) + endpoints_ok = true; + } else { + int first_dir = path_dir[1]; + int last_dir = path_dir[length - 1]; + int sx = out_x[0]; + int sy = out_y[0]; + int ex = out_x[length - 1]; + int ey = out_y[length - 1]; + bool start_water = tile_is_water (sx - dir_dx[first_dir], sy - dir_dy[first_dir]); + bool end_water = tile_is_water (ex + dir_dx[last_dir], ey + dir_dy[last_dir]); + if (start_water && end_water && + tile_has_diagonal_water (sx, sy) && + tile_has_diagonal_water (ex, ey)) + endpoints_ok = true; + } - // Skip tiles with enemy units - if (tile_has_enemy_unit (area_tile, rec->civ_id)) - continue; + bool buildable = true; + if (endpoints_ok) { + for (int pi = 0; pi < length; pi++) { + Tile * path_tile = tile_at (out_x[pi], out_y[pi]); + if ((path_tile == NULL) || (path_tile == p_null_tile) || + tile_has_resource (path_tile) || + (! district_is_buildable_on_square_type (&is->district_configs[CANAL_DISTRICT_ID], path_tile)) || + tile_is_reserved_in_district_tile_map (out_x[pi], out_y[pi])) { + buildable = false; + break; + } + if (tile_has_bridge_or_canal_nearby (out_x[pi], out_y[pi])) { + buildable = false; + break; + } + } + } else { + buildable = false; + } - // Skip tiles with pollution - if (area_tile->vtable->m20_Check_Pollution (area_tile, __, 0)) - continue; + if (buildable) { + // Collect adjacent land tiles for component checks + int adj_x[AI_BRIDGE_CANAL_CANDIDATE_MAX_EVAL_TILES * 8]; + int adj_y[AI_BRIDGE_CANAL_CANDIDATE_MAX_EVAL_TILES * 8]; + int adj_count = 0; + for (int pi = 0; pi < length; pi++) { + int px = out_x[pi]; + int py = out_y[pi]; + for (int di = 0; di < 8; di++) { + int nx = px + dir_dx[di]; + int ny = py + dir_dy[di]; + wrap_tile_coords (map, &nx, &ny); + if (! tile_is_land (-1, nx, ny, false)) + continue; + Tile * adj = tile_at (nx, ny); + if ((adj == NULL) || (adj == p_null_tile)) + continue; + if (adj->vtable->m46_Get_ContinentID (adj) != continent_id) + continue; + bool in_path = false; + for (int pj = 0; pj < length; pj++) { + if ((out_x[pj] == nx) && (out_y[pj] == ny)) { + in_path = true; + break; + } + } + if (in_path) + continue; + bool seen = false; + for (int aj = 0; aj < adj_count; aj++) { + if ((adj_x[aj] == nx) && (adj_y[aj] == ny)) { + seen = true; + break; + } + } + if (! seen && (adj_count < (int)(sizeof (adj_x) / sizeof (adj_x[0])))) { + adj_x[adj_count] = nx; + adj_y[adj_count] = ny; + adj_count++; + } + } + } - // Skip tiles that are other districts (but not this hub itself) - struct district_instance * area_district = get_district_instance (area_tile); - if ((area_district != NULL) && ((tx != rec->tile_x) || (ty != rec->tile_y))) - continue; + if (adj_count >= 2) { + int best1 = 0; + int best2 = 0; + int min_land = is->current_config.ai_canal_eval_min_bisected_land_tiles; + if (min_land < 1) + min_land = 1; + visit_mark++; + if (visit_mark == 0) { + visit_mark = 1; + for (int i = 0; i < tile_count; i++) + visit[i] = 0; + } - // Skip tiles with completed wonders - struct wonder_district_info * area_info = get_wonder_district_info (area_tile); - if ((area_info != NULL) && (area_info->state == WDS_COMPLETED)) - continue; + for (int ai = 0; ai < adj_count; ai++) { + int aidx = tile_coords_to_index (map, adj_x[ai], adj_y[ai]); + if ((aidx < 0) || (visit[aidx] == visit_mark)) + continue; + + int comp_size = 0; + int head = 0; + int tail = 0; + visit[aidx] = visit_mark; + queue_x[tail] = adj_x[ai]; + queue_y[tail] = adj_y[ai]; + tail++; + + while (head < tail) { + int qx = queue_x[head]; + int qy = queue_y[head]; + head++; + comp_size++; + for (int di = 0; di < 8; di++) { + int nx = qx + dir_dx[di]; + int ny = qy + dir_dy[di]; + wrap_tile_coords (map, &nx, &ny); + if (! tile_is_land (-1, nx, ny, false)) + continue; + Tile * adj = tile_at (nx, ny); + if ((adj == NULL) || (adj == p_null_tile)) + continue; + if (adj->vtable->m46_Get_ContinentID (adj) != continent_id) + continue; + bool blocked = false; + for (int pj = 0; pj < length; pj++) { + if ((out_x[pj] == nx) && (out_y[pj] == ny)) { + blocked = true; + break; + } + } + if (blocked) + continue; + int nidx = tile_coords_to_index (map, nx, ny); + if ((nidx < 0) || (visit[nidx] == visit_mark)) + continue; + visit[nidx] = visit_mark; + queue_x[tail] = nx; + queue_y[tail] = ny; + tail++; + } + } - // Check if another hub of the same civ is closer to this tile - int my_distance = compute_wrapped_manhattan_distance (rec->tile_x, rec->tile_y, tx, ty); - bool tile_belongs_to_me = true; + if (comp_size > best1) { + best2 = best1; + best1 = comp_size; + } else if (comp_size > best2) { + best2 = comp_size; + } + } - FOR_TABLE_ENTRIES (other_tei, &is->distribution_hub_records) { - struct distribution_hub_record * other_rec = (struct distribution_hub_record *)(long)other_tei.value; - if ((other_rec == NULL) || (other_rec == rec)) - continue; - if (other_rec->civ_id != rec->civ_id) - continue; + if ((best1 >= min_land) && (best2 >= min_land)) { + *out_owner = owner; + free (visit); + free (queue_x); + free (queue_y); + return length; + } + } + } + dir_stack[depth] = -1; + stack_len--; + continue; + } - int other_distance = compute_wrapped_manhattan_distance (other_rec->tile_x, other_rec->tile_y, tx, ty); - if (other_distance < my_distance) { - tile_belongs_to_me = false; - break; - } - if (other_distance == my_distance) { - // Tie-breaking: prefer hub with lower Y, then lower X - if (other_rec->tile_y < rec->tile_y) { - tile_belongs_to_me = false; - break; - } - if ((other_rec->tile_y == rec->tile_y) && (other_rec->tile_x < rec->tile_x)) { - tile_belongs_to_me = false; - break; - } - } - } - - if (! tile_belongs_to_me) - continue; + int next_dir = dir_stack[depth] + 1; + bool advanced = false; + while (next_dir < 8) { + int ndx = dir_dx[next_dir]; + int ndy = dir_dy[next_dir]; + int nx = cx + ndx; + int ny = cy + ndy; + wrap_tile_coords (map, &nx, &ny); + dir_stack[depth] = next_dir; + + if (! tile_point_in_block (nx, ny, block_x0, block_y0, block_x1, block_y1)) { + next_dir++; + continue; + } + if (! tile_is_land (-1, nx, ny, false)) { + next_dir++; + continue; + } + if (tile_part_of_existing_candidate (nx, ny)) { + next_dir++; + continue; + } + if (tile_is_reserved_in_district_tile_map (nx, ny)) { + next_dir++; + continue; + } + Tile * next_tile = tile_at (nx, ny); + if ((next_tile == NULL) || (next_tile == p_null_tile)) { + next_dir++; + continue; + } + if (tile_has_resource (next_tile)) { + next_dir++; + continue; + } + if (! district_is_buildable_on_square_type (&is->district_configs[CANAL_DISTRICT_ID], next_tile)) { + next_dir++; + continue; + } + if (tile_has_bridge_or_canal_nearby (nx, ny)) { + next_dir++; + continue; + } + if (next_tile->vtable->m46_Get_ContinentID (next_tile) != continent_id) { + next_dir++; + continue; + } + bool dup = false; + for (int pi = 0; pi < depth + 1; pi++) { + if ((out_x[pi] == nx) && (out_y[pi] == ny)) { + dup = true; + break; + } + } + if (dup) { + next_dir++; + continue; + } - food_sum += City_calc_tile_yield_at (anchor_city, __, 0, tx, ty); - shield_sum += City_calc_tile_yield_at (anchor_city, __, 1, tx, ty); - } + if (depth >= 1) { + int prev_dir = dir_stack[depth - 1]; + int diff = next_dir - prev_dir; + if (diff < 0) + diff += 8; + if (diff > 4) + diff = 8 - diff; + if (diff > 1) { + next_dir++; + continue; + } + } - rec->raw_food_yield = food_sum; - rec->raw_shield_yield = shield_sum; + out_x[depth + 1] = (short)nx; + out_y[depth + 1] = (short)ny; + dir_stack[depth + 1] = -1; + path_dir[depth + 1] = next_dir; + stack_len++; + advanced = true; + break; + } - int food_div = is->current_config.distribution_hub_food_yield_divisor; - int shield_div = is->current_config.distribution_hub_shield_yield_divisor; - if (food_div <= 0) - food_div = 1; - if (shield_div <= 0) - shield_div = 1; + if (! advanced) { + dir_stack[depth] = -1; + stack_len--; + } + } + } + } - rec->food_yield = food_sum / food_div; - rec->shield_yield = shield_sum / shield_div; + free (visit); + free (queue_x); + free (queue_y); + return 0; } void -remove_distribution_hub_record (Tile * tile) +generate_ai_bridge_candidates_by_block (Map * map, int block_size, int contiguous_limit) { - struct distribution_hub_record * rec = get_distribution_hub_record (tile); - if (rec == NULL) + if ((map == NULL) || (block_size <= 0)) return; - int affected_civ_id = rec->civ_id; - release_distribution_hub_coverage (rec); - itable_remove (&is->distribution_hub_records, (int)tile); - free (rec); - is->distribution_hub_totals_dirty = true; - recompute_distribution_hub_totals (); + int width = map->Width; + int height = map->Height; + if ((width <= 0) || (height <= 0)) + return; - // Recalculate yields for all cities of this civ - if ((affected_civ_id >= 0) && (p_cities->Cities != NULL)) { - for (int city_index = 0; city_index <= p_cities->LastIndex; city_index++) { - City * target_city = get_city_ptr (city_index); - if ((target_city != NULL) && (target_city->Body.CivID == affected_civ_id)) - recompute_city_yields_with_districts (target_city); + if (block_size < 1) + block_size = 1; + + int candidate_capacity = clamp (1, AI_BRIDGE_CANAL_CANDIDATE_MAX_EVAL_TILES, contiguous_limit); + + short * candidate_x = (short *)malloc (sizeof *candidate_x * candidate_capacity); + short * candidate_y = (short *)malloc (sizeof *candidate_y * candidate_capacity); + if ((candidate_x == NULL) || (candidate_y == NULL)) { + if (candidate_x != NULL) + free (candidate_x); + if (candidate_y != NULL) + free (candidate_y); + return; + } + + for (int base_y = 0; base_y < height; base_y += block_size) { + int block_y1 = base_y + block_size; + if (block_y1 > height) + block_y1 = height; + for (int base_x = 0; base_x < width; base_x += block_size) { + int block_x1 = base_x + block_size; + if (block_x1 > width) + block_x1 = width; + short owner = -1; + int count = find_bridge_candidate_in_block ( + map, base_x, base_y, block_x1, block_y1, + contiguous_limit, candidate_capacity, + candidate_x, candidate_y, &owner); + if (count <= 0) + continue; + if (! add_ai_candidate_entry (BRIDGE_DISTRICT_ID, owner, candidate_x, candidate_y, count)) { + free (candidate_x); + free (candidate_y); + return; } + } } + + free (candidate_x); + free (candidate_y); } void -recompute_distribution_hub_totals () +generate_ai_canal_candidates_by_block (Map * map, int block_size, int contiguous_limit) { - if (! is->current_config.enable_districts || - ! is->current_config.enable_distribution_hub_districts) { - is->distribution_hub_totals_dirty = false; + if ((map == NULL) || (block_size <= 0)) return; - } - struct table new_coverage_counts = {0}; - struct table newly_covered_tiles = {0}; + int width = map->Width; + int height = map->Height; + if ((width <= 0) || (height <= 0)) + return; - clear_memo (); - int civs_needing_recalc[32] = {0}; + if (block_size < 1) + block_size = 1; - FOR_TABLE_ENTRIES (tei, &is->distribution_hub_records) { - Tile * tile = (Tile *)tei.key; - struct distribution_hub_record * rec = (struct distribution_hub_record *)(long)tei.value; - if (rec == NULL) - continue; + int candidate_capacity = clamp (1, AI_BRIDGE_CANAL_CANDIDATE_MAX_EVAL_TILES, contiguous_limit); - Tile * current_tile = tile_at (rec->tile_x, rec->tile_y); - if ((current_tile == NULL) || - (current_tile == p_null_tile) || - ! district_is_complete (current_tile, DISTRIBUTION_HUB_DISTRICT_ID)) { - memoize (tei.key); - continue; + short * candidate_x = (short *)malloc (sizeof *candidate_x * candidate_capacity); + short * candidate_y = (short *)malloc (sizeof *candidate_y * candidate_capacity); + if ((candidate_x == NULL) || (candidate_y == NULL)) { + if (candidate_x != NULL) + free (candidate_x); + if (candidate_y != NULL) + free (candidate_y); + return; + } + + for (int base_y = 0; base_y < height; base_y += block_size) { + int block_y1 = base_y + block_size; + if (block_y1 > height) + block_y1 = height; + for (int base_x = 0; base_x < width; base_x += block_size) { + int block_x1 = base_x + block_size; + if (block_x1 > width) + block_x1 = width; + short owner = -1; + int count = find_canal_candidate_in_block ( + map, base_x, base_y, block_x1, block_y1, + contiguous_limit, candidate_capacity, + candidate_x, candidate_y, &owner); + if (count <= 0) + continue; + if (! add_ai_candidate_entry (CANAL_DISTRICT_ID, owner, candidate_x, candidate_y, count)) { + free (candidate_x); + free (candidate_y); + return; + } } + } - rec->tile = current_tile; - rec->food_yield = 0; - rec->shield_yield = 0; - rec->raw_food_yield = 0; - rec->raw_shield_yield = 0; + free (candidate_x); + free (candidate_y); +} - int old_civ_id = rec->civ_id; - rec->civ_id = current_tile->vtable->m38_Get_Territory_OwnerID (current_tile); +void +generate_ai_canal_and_bridge_targets () +{ + if (is->ai_candidate_bridge_or_canals_initialized) + return; + if ((! is->current_config.enable_canal_districts) && (! is->current_config.enable_bridge_districts)) + return; - if (old_civ_id != rec->civ_id) - civs_needing_recalc[old_civ_id] = 1; - civs_needing_recalc[rec->civ_id] = 1; + Map * map = &p_bic_data->Map; + int width = map->Width; + int height = map->Height; + if ((width <= 0) || (height <= 0)) + return; - City * anchor = get_connected_city_for_distribution_hub (rec); + int block_size = is->current_config.ai_bridge_canal_eval_block_size; + if (block_size <= 0) + block_size = 10; - if ((anchor == NULL) || - current_tile->vtable->m20_Check_Pollution (current_tile, __, 0) || - tile_has_enemy_unit (current_tile, rec->civ_id)) - continue; + if (is->current_config.enable_bridge_districts && is->current_config.ai_builds_bridges) { + generate_ai_bridge_candidates_by_block ( + map, + block_size, + is->current_config.max_contiguous_bridge_districts); + } - FOR_TILES_AROUND (tai, workable_tile_counts[1], rec->tile_x, rec->tile_y) { - Tile * area_tile = tai.tile; - if (area_tile == p_null_tile) - continue; + if (is->current_config.enable_canal_districts && is->current_config.ai_builds_canals) { + generate_ai_canal_candidates_by_block ( + map, + block_size, + is->current_config.max_contiguous_canal_districts); + } - int tx, ty; - tai_get_coords (&tai, &tx, &ty); + is->ai_candidate_bridge_or_canals_initialized = true; +} - if (area_tile->vtable->m38_Get_Territory_OwnerID (area_tile) != rec->civ_id) - continue; - if (Tile_has_city (area_tile)) - continue; - if (tile_has_enemy_unit (area_tile, rec->civ_id)) - continue; - if (area_tile->vtable->m20_Check_Pollution (area_tile, __, 0)) - continue; +void +assign_workers_for_ai_candidate_bridge_or_canals (Leader * leader) +{ + if ((leader == NULL) || (is->ai_candidate_bridge_or_canals_count <= 0)) + return; - struct district_instance * area_district = get_district_instance (area_tile); - if ((area_district != NULL) && ((tx != rec->tile_x) || (ty != rec->tile_y))) - continue; + int civ_id = leader->ID; + if ((*p_human_player_bits & (1 << civ_id)) != 0) + return; - struct wonder_district_info * area_info = get_wonder_district_info (area_tile); - if ((area_info != NULL) && (area_info->state == WDS_COMPLETED)) - continue; + if (! leader_can_build_district (leader, CANAL_DISTRICT_ID) && ! leader_can_build_district (leader, BRIDGE_DISTRICT_ID)) + return; - int key = (int)area_tile; - int prev_cover_pass = itable_look_up_or (&new_coverage_counts, key, 0); - int prev_cover_old = itable_look_up_or (&is->distribution_hub_coverage_counts, key, 0); - itable_insert (&new_coverage_counts, key, prev_cover_pass + 1); - if ((prev_cover_pass == 0) && (prev_cover_old <= 0)) - itable_insert (&newly_covered_tiles, key, 1); - } + if (! is->ai_candidate_bridge_or_canals_initialized) { + reset_ai_candidate_bridge_or_canals (); + generate_ai_canal_and_bridge_targets (); } - for (int i = 0; i < is->memo_len; i++) - remove_distribution_hub_record ((Tile *)is->memo[i]); - clear_memo (); - - FOR_TABLE_ENTRIES (tei, &is->distribution_hub_records) { - struct distribution_hub_record * rec = (struct distribution_hub_record *)(long)tei.value; - if (rec == NULL) + for (int ei = 0; ei < is->ai_candidate_bridge_or_canals_count; ei++) { + struct ai_candidate_bridge_or_canal_entry * entry = &is->ai_candidate_bridge_or_canals[ei]; + if (entry->completed) continue; - City * anchor = get_connected_city_for_distribution_hub (rec); - if (anchor == NULL) { - rec->food_yield = 0; - rec->shield_yield = 0; - rec->raw_food_yield = 0; - rec->raw_shield_yield = 0; + int district_id = entry->district_id; + if ((district_id == CANAL_DISTRICT_ID) && (! is->current_config.enable_canal_districts)) continue; + if ((district_id == BRIDGE_DISTRICT_ID) && (! is->current_config.enable_bridge_districts)) continue; + if (! leader_can_build_district (leader, district_id)) continue; + + if (entry->assigned_worker_id >= 0) { + Unit * worker = get_unit_ptr (entry->assigned_worker_id); + if (worker == NULL) { + release_ai_candidate_bridge_or_canal_worker (entry); + } else if ((entry->assigned_tile_index >= 0) && (entry->assigned_tile_index < entry->tile_count)) { + int tx = entry->tile_x[entry->assigned_tile_index]; + int ty = entry->tile_y[entry->assigned_tile_index]; + wrap_tile_coords (&p_bic_data->Map, &tx, &ty); + Tile * tile = tile_at (tx, ty); + if ((tile != NULL) && (tile != p_null_tile)) { + struct district_instance * inst = get_district_instance (tile); + if (inst != NULL && inst->district_id == district_id && district_is_complete (tile, district_id)) { + release_ai_candidate_bridge_or_canal_worker (entry); + } + } + } + if (entry->assigned_worker_id >= 0) + continue; + } + + int target_idx = -1; + if (! ai_candidate_bridge_or_canal_is_buildable_for_civ (entry, civ_id, &target_idx)) { + release_ai_candidate_bridge_or_canal_worker (entry); continue; } - recompute_distribution_hub_yields (rec); - } + if (target_idx < 0) + continue; - table_deinit (&is->distribution_hub_coverage_counts); - is->distribution_hub_coverage_counts = new_coverage_counts; - memset (&new_coverage_counts, 0, sizeof new_coverage_counts); + City * city = find_city_for_tile (civ_id, entry->tile_x[target_idx], entry->tile_y[target_idx]); + if (city == NULL) + continue; - FOR_TABLE_ENTRIES (tei, &newly_covered_tiles) { - Tile * covered_tile = (Tile *)(long)tei.key; - if ((covered_tile == NULL) || (covered_tile == p_null_tile)) + Unit * worker = find_best_worker_for_district (leader, city, district_id, entry->tile_x[target_idx], entry->tile_y[target_idx]); + if (worker == NULL) continue; - int tx, ty; - if (! tile_coords_from_ptr (&p_bic_data->Map, covered_tile, &tx, &ty)) + + memset (&entry->pending_req, 0, sizeof entry->pending_req); + entry->pending_req.district_id = district_id; + entry->pending_req.civ_id = civ_id; + if (! assign_worker_to_district (&entry->pending_req, worker, city, district_id, entry->tile_x[target_idx], entry->tile_y[target_idx])) continue; - set_tile_unworkable_for_all_cities (covered_tile, tx, ty); - covered_tile->Body.CityAreaID = -1; - } - table_deinit (&newly_covered_tiles); - // Recalculate yields for cities of civs whose distribution hub ownership changed - for (int civ_id = 0; civ_id < 32; civ_id++) { - if (civs_needing_recalc[civ_id] && (p_cities->Cities != NULL)) { - for (int city_index = 0; city_index <= p_cities->LastIndex; city_index++) { - City * city = get_city_ptr (city_index); - if ((city != NULL) && (city->Body.CivID == civ_id)) - recompute_city_yields_with_districts (city); - } - } + entry->assigned_worker_id = worker->Body.ID; + entry->assigned_tile_index = target_idx; } - - is->distribution_hub_totals_dirty = false; } void -on_distribution_hub_completed (Tile * tile, int tile_x, int tile_y) +recompute_city_yields_with_districts (City * city) { - if (! is->current_config.enable_districts || ! is->current_config.enable_distribution_hub_districts) + if (city == NULL) return; - int tile_owner = -1; - if ((tile != NULL) && (tile != p_null_tile)) - tile_owner = tile->vtable->m38_Get_Territory_OwnerID (tile); + bool prev_flag = is->distribution_hub_refresh_in_progress; + is->distribution_hub_refresh_in_progress = true; + patch_City_recompute_yields_and_happiness (city); + is->distribution_hub_refresh_in_progress = prev_flag; +} - struct distribution_hub_record * rec = get_distribution_hub_record (tile); - if (rec != NULL) { - int old_civ_id = rec->civ_id; - rec->tile = tile; - rec->tile_x = tile_x; - rec->tile_y = tile_y; +enum UnitStateType __fastcall +patch_City_instruct_worker (City * this, int edx, int tile_x, int tile_y, bool param_3, Unit * worker) +{ + return City_instruct_worker (this, __, tile_x, tile_y, param_3, worker); +} - release_distribution_hub_coverage (rec); - rec->civ_id = tile_owner; - is->distribution_hub_totals_dirty = true; - recompute_distribution_hub_totals (); +int +find_wonder_config_index_by_improvement_id (int improv_id) +{ + if (improv_id < 0) + return -1; - if (old_civ_id != tile_owner) { - // Recompute for old civ - for (int city_index = 0; city_index <= p_cities->LastIndex; city_index++) { - City * target_city = get_city_ptr (city_index); - if ((target_city != NULL) && (target_city->Body.CivID == old_civ_id)) - recompute_city_yields_with_districts (target_city); - } + char ss[200]; + + for (int wi = 0; wi < is->wonder_district_count; wi++) { + int bid = -1; + if (stable_look_up (&is->building_name_to_id, is->wonder_district_configs[wi].wonder_name, &bid) && + (bid == improv_id)) { + return wi; } } - rec = malloc (sizeof *rec); - if (rec == NULL) - return; - rec->tile = tile; - rec->tile_x = tile_x; - rec->tile_y = tile_y; - rec->civ_id = tile_owner; - rec->food_yield = 0; - rec->shield_yield = 0; - rec->raw_food_yield = 0; - rec->raw_shield_yield = 0; - itable_insert (&is->distribution_hub_records, (int)tile, (int)(long)rec); - adjust_distribution_hub_coverage (rec); + return -1; +} - is->distribution_hub_totals_dirty = true; - recompute_distribution_hub_totals (); +void set_wonder_built_flag (int improv_id, bool is_built); - // Recalculate yields for all cities of this civ - int affected_civ_id = rec->civ_id; - if ((affected_civ_id >= 0) && (p_cities->Cities != NULL)) { - for (int city_index = 0; city_index <= p_cities->LastIndex; city_index++) { - City * target_city = get_city_ptr (city_index); - if ((target_city != NULL) && (target_city->Body.CivID == affected_civ_id)) - recompute_city_yields_with_districts (target_city); - } - } +unsigned int +wonder_buildable_square_type_mask (struct wonder_district_config const * cfg) +{ + if (cfg == NULL) + return district_default_buildable_mask (); + + unsigned int mask = cfg->buildable_square_types_mask; + if (mask == 0) + mask = district_default_buildable_mask (); + return mask; } -void -refresh_distribution_hubs_for_city (City * city) +unsigned int +wonder_buildable_mask_for_improvement (int improv_id) { - if (! is->current_config.enable_districts || - ! is->current_config.enable_distribution_hub_districts || - (city == NULL)) - return; - - int city_x = city->Body.X; - int city_y = city->Body.Y; - for (int n = 0; n < is->workable_tile_count; n++) { - int dx, dy; - patch_ni_to_diff_for_work_area (n, &dx, &dy); - int tx = city_x + dx, ty = city_y + dy; - wrap_tile_coords (&p_bic_data->Map, &tx, &ty); - Tile * tile = tile_at (tx, ty); - if ((tile == NULL) || (tile == p_null_tile)) - continue; - struct district_instance * inst = get_district_instance (tile); - if (inst == NULL || inst->district_type != DISTRIBUTION_HUB_DISTRICT_ID) - continue; - on_distribution_hub_completed (tile, tx, ty); - } + int windex = find_wonder_config_index_by_improvement_id (improv_id); + if (windex < 0) + return district_default_buildable_mask (); + return wonder_buildable_square_type_mask (&is->wonder_district_configs[windex]); } bool -is_space_char (char c) +wonder_is_buildable_on_square_type (struct wonder_district_config const * cfg, Tile * tile) { - switch (c) { - case ' ': - case '\t': - case '\n': - case '\r': - case '\f': - case '\v': - return true; - default: + if ((cfg == NULL) || (tile == NULL) || (tile == p_null_tile)) + return false; + + if (! tile_matches_square_type_mask (tile, wonder_buildable_square_type_mask (cfg))) + return false; + + if (cfg->has_buildable_only_on_overlays) { + if (! tile_matches_overlay_mask (tile, cfg->buildable_only_on_overlays_mask)) return false; } -} -enum key_value_parse_status { - KVP_SUCCESS, - KVP_NO_EQUALS, - KVP_EMPTY_KEY -}; + if (cfg->has_buildable_adjacent_to) { + int tile_x, tile_y; + if (! tile_coords_from_ptr (&p_bic_data->Map, tile, &tile_x, &tile_y)) + return false; -enum key_value_parse_status -parse_trimmed_key_value (struct string_slice const * trimmed, - struct string_slice * out_key, - struct string_slice * out_value) -{ - if ((trimmed == NULL) || (trimmed->len <= 0)) - return KVP_NO_EQUALS; + bool matches = false; + bool city_adjacent = false; + FOR_TILES_AROUND (tai, 9, tile_x, tile_y) { + if (tai.n == 0) + continue; + if (Tile_has_city (tai.tile)) { + city_adjacent = true; + if (cfg->buildable_adjacent_to_allows_city) + matches = true; + } + if (tile_matches_square_type_mask (tai.tile, cfg->buildable_adjacent_to_square_types_mask)) + matches = true; + if (matches && (cfg->buildable_adjacent_to_allows_city || ! city_adjacent)) + break; + } + if (city_adjacent && ! cfg->buildable_adjacent_to_allows_city) + return false; + if (! matches) + return false; + } - char * equals = NULL; - for (int i = 0; i < trimmed->len; i++) { - if (trimmed->str[i] == '=') { - equals = trimmed->str + i; - break; + if (cfg->has_buildable_adjacent_to_overlays) { + int tile_x, tile_y; + if (! tile_coords_from_ptr (&p_bic_data->Map, tile, &tile_x, &tile_y)) + return false; + + bool matches = false; + FOR_TILES_AROUND (tai, 9, tile_x, tile_y) { + if (tai.n == 0) + continue; + if (tile_matches_overlay_mask (tai.tile, cfg->buildable_adjacent_to_overlays_mask)) { + matches = true; + break; + } } + if (! matches) + return false; } - if (equals == NULL) - return KVP_NO_EQUALS; - struct string_slice key = { .str = trimmed->str, .len = (int)(equals - trimmed->str) }; - key = trim_string_slice (&key, 0); - if (key.len == 0) - return KVP_EMPTY_KEY; + return true; +} - struct string_slice value = { .str = equals + 1, .len = (int)((trimmed->str + trimmed->len) - (equals + 1)) }; - *out_key = key; - *out_value = trim_string_slice (&value, 0); - return KVP_SUCCESS; +bool +wonder_is_buildable_on_tile (Tile * tile, int wonder_improv_id) +{ + if ((tile == NULL) || (tile == p_null_tile)) + return false; + + int windex = find_wonder_config_index_by_improvement_id (wonder_improv_id); + if (windex < 0) + return tile_matches_square_type_mask (tile, district_default_buildable_mask ()); + + return wonder_is_buildable_on_square_type (&is->wonder_district_configs[windex], tile); } -void -add_key_parse_error (struct error_line ** parse_errors, - int line_number, - struct string_slice const * key, - char const * message_suffix) +int +get_wonder_improvement_id_from_index (int windex) { - struct error_line * err = add_error_line (parse_errors); - char * key_str = extract_slice (key); - snprintf (err->text, sizeof err->text, "^ Line %d: %s %s", line_number, key_str, message_suffix); - err->text[(sizeof err->text) - 1] = '\0'; - free (key_str); + if ((windex < 0) || (windex >= is->wonder_district_count)) + return -1; + + char const * wonder_name = is->wonder_district_configs[windex].wonder_name; + if ((wonder_name == NULL) || (wonder_name[0] == '\0')) + return -1; + + int improv_id; + if (stable_look_up (&is->building_name_to_id, (char *)wonder_name, &improv_id)) + return improv_id; + else + return -1; } void -add_unrecognized_key_error (struct error_line ** unrecognized_keys, - int line_number, - struct string_slice const * key) +remember_pending_building_order (City * city, int improvement_id) { - struct error_line * err_line = add_error_line (unrecognized_keys); - char * key_str = extract_slice (key); - snprintf (err_line->text, sizeof err_line->text, "^ Line %d: %s", line_number, key_str); - err_line->text[(sizeof err_line->text) - 1] = '\0'; - free (key_str); + if (! is->current_config.enable_districts || + (city == NULL) || + (improvement_id < 0)) + return; + + if ((*p_human_player_bits & (1 << city->Body.CivID)) != 0) + return; + + itable_insert (&is->city_pending_building_orders, (int)city, improvement_id); } -char * -copy_trimmed_string_or_null (struct string_slice const * slice, int remove_quotes) +bool +lookup_pending_building_order (City * city, int * out_improv_id) { - struct string_slice trimmed = trim_string_slice (slice, remove_quotes); - if (trimmed.len == 0) - return NULL; - return extract_slice (&trimmed); + if (! is->current_config.enable_districts || + (city == NULL) || + (out_improv_id == NULL)) + return false; + + return itable_look_up (&is->city_pending_building_orders, (int)city, out_improv_id); } void -free_dynamic_district_config (struct district_config * cfg) +forget_pending_building_order (City * city) { - if (cfg == NULL) + if (! is->current_config.enable_districts || + (city == NULL)) return; - if (! cfg->is_dynamic) - return; + itable_remove (&is->city_pending_building_orders, (int)city); +} - if (cfg->name != NULL) { - free ((void *)cfg->name); - cfg->name = NULL; - } - if (cfg->tooltip != NULL) { - free ((void *)cfg->tooltip); - cfg->tooltip = NULL; - } - if (cfg->advance_prereq != NULL) { - free ((void *)cfg->advance_prereq); - cfg->advance_prereq = NULL; - } +bool +is_wonder_or_small_wonder_already_being_built (City * city, int improv_id) +{ + if ((city == NULL) || (improv_id < 0) || (improv_id >= p_bic_data->ImprovementsCount)) + return false; - for (int i = 0; i < 5; i++) { - if (cfg->dependent_improvements[i] != NULL) { - free ((void *)cfg->dependent_improvements[i]); - cfg->dependent_improvements[i] = NULL; - } - } + Improvement * improv = &p_bic_data->Improvements[improv_id]; + bool is_wonder = (improv->Characteristics & (ITC_Wonder | ITC_Small_Wonder)) != 0; + if ((improv->Characteristics & (ITC_Wonder | ITC_Small_Wonder)) == 0) + return false; - for (int i = 0; i < 10; i++) { - if (cfg->img_paths[i] != NULL) { - free ((void *)cfg->img_paths[i]); - cfg->img_paths[i] = NULL; - } + int civ_id = city->Body.CivID; + FOR_CITIES_OF (coi, civ_id) { + City * other_city = coi.city; + if ((other_city == NULL) || (other_city == city)) + continue; + + if ((other_city->Body.Order_Type == COT_Improvement) && + (other_city->Body.Order_ID == improv_id)) + return true; } - memset (cfg, 0, sizeof *cfg); + return false; } -void -free_dynamic_wonder_config (struct wonder_district_config * cfg) +struct district_building_prereq_list * +get_district_building_prereq_list (int improv_id) { - if (cfg == NULL) - return; - - if (! cfg->is_dynamic) - return; + if (improv_id < 0) + return NULL; - if (cfg->wonder_name != NULL) { - free ((void *)cfg->wonder_name); - cfg->wonder_name = NULL; - } + int stored = 0; + if (! itable_look_up (&is->district_building_prereqs, improv_id, &stored)) + return NULL; + return (struct district_building_prereq_list *)stored; +} - if (cfg->img_path != NULL) { - free ((void *)cfg->img_path); - cfg->img_path = NULL; +bool +district_building_prereq_list_contains (struct district_building_prereq_list * list, int district_id) +{ + if ((list == NULL) || (district_id < 0)) + return false; + for (int i = 0; i < list->count; i++) { + if (list->district_ids[i] == district_id) + return true; } - - memset (cfg, 0, sizeof *cfg); + return false; } void -free_dynamic_natural_wonder_config (struct natural_wonder_district_config * cfg) +add_district_building_prereq (int improv_id, int district_id) { - if (cfg == NULL) + if ((improv_id < 0) || (district_id < 0)) return; - if (! cfg->is_dynamic) - return; - - if (cfg->name != NULL) { - free ((void *)cfg->name); - cfg->name = NULL; + struct district_building_prereq_list * list = get_district_building_prereq_list (improv_id); + if (list == NULL) { + list = calloc (1, sizeof *list); + if (list == NULL) + return; + list->count = 0; + itable_insert (&is->district_building_prereqs, improv_id, (int)list); } - if (cfg->img_path != NULL) { - free ((void *)cfg->img_path); - cfg->img_path = NULL; - } + if (district_building_prereq_list_contains (list, district_id)) + return; - memset (cfg, 0, sizeof *cfg); - cfg->adjacent_to = (enum SquareTypes)SQ_INVALID; - cfg->adjacency_dir = DIR_ZERO; + if (list->count >= ARRAY_LEN (list->district_ids)) + return; + + list->district_ids[list->count++] = district_id; } -enum Unit_Command_Values -allocate_dynamic_district_command (char const * name) +bool +city_has_river_district (City * city, int district_id) { - int offset = is->next_custom_dynamic_command_index; - is->next_custom_dynamic_command_index += 1; - int value = C3X_DISTRICT_COMMAND_BASE - (offset + 1); - return (enum Unit_Command_Values)value; + if (city == NULL) + return false; + FOR_DISTRICTS_AROUND (wai, city->Body.X, city->Body.Y, true) { + if (wai.district_inst->district_id != district_id) + continue; + if (wai.tile->vtable->m37_Get_River_Code (wai.tile) != 0) + return true; + } + return false; } -void -free_special_district_override_strings (struct district_config * cfg, struct district_config const * defaults) +bool +city_has_any_prereq_district_for_improvement (City * city, + struct district_building_prereq_list * list, + bool requires_river, + bool allow_wonder_district) { - if (cfg == NULL || defaults == NULL) - return; - - if ((cfg->tooltip != NULL) && (cfg->tooltip != defaults->tooltip)) { - free ((void *)cfg->tooltip); - cfg->tooltip = NULL; - } - if ((cfg->advance_prereq != NULL) && (cfg->advance_prereq != defaults->advance_prereq)) { - free ((void *)cfg->advance_prereq); - cfg->advance_prereq = NULL; - } + if ((city == NULL) || (list == NULL)) + return false; - for (int i = 0; i < ARRAY_LEN (cfg->dependent_improvements); i++) { - char const * default_value = (i < defaults->dependent_improvement_count) ? defaults->dependent_improvements[i] : NULL; - if ((cfg->dependent_improvements[i] != NULL) && - (cfg->dependent_improvements[i] != default_value)) { - free ((void *)cfg->dependent_improvements[i]); + for (int i = 0; i < list->count; i++) { + int district_id = list->district_ids[i]; + if (district_id < 0) + continue; + if (! allow_wonder_district && district_id == WONDER_DISTRICT_ID) + continue; + if (requires_river) { + if (city_has_river_district (city, district_id)) + return true; + } else if (city_has_required_district (city, district_id)) { + return true; } - cfg->dependent_improvements[i] = NULL; } - cfg->dependent_improvement_count = defaults->dependent_improvement_count; + return false; +} - for (int i = 0; i < ARRAY_LEN (cfg->img_paths); i++) { - char const * default_value = (i < defaults->img_path_count) ? defaults->img_paths[i] : NULL; - if ((cfg->img_paths[i] != NULL) && - (cfg->img_paths[i] != default_value)) { - free ((void *)cfg->img_paths[i]); - } - cfg->img_paths[i] = NULL; +int +pick_missing_district_for_improvement (City * city, struct district_building_prereq_list * list) +{ + if ((list == NULL) || (list->count <= 0)) + return -1; + + int fallback = -1; + for (int i = 0; i < list->count; i++) { + int district_id = list->district_ids[i]; + if (district_id < 0) + continue; + if (fallback < 0) + fallback = district_id; + if ((city != NULL) && leader_can_build_district (&leaders[city->Body.CivID], district_id)) + return district_id; } - cfg->img_path_count = defaults->img_path_count; + return fallback; } -void -reset_regular_district_configs (void) + +bool +district_is_complete(Tile * tile, int district_id) { - for (int i = USED_SPECIAL_DISTRICT_TYPES; i < COUNT_DISTRICT_TYPES; i++) { - if (is->district_configs[i].is_dynamic) - free_dynamic_district_config (&is->district_configs[i]); - } + if ((tile == NULL) || (tile == p_null_tile) || + (district_id < 0) || (district_id >= is->district_count)) + return false; - for (int i = 0; i < USED_SPECIAL_DISTRICT_TYPES; i++) - free_special_district_override_strings (&is->district_configs[i], &special_district_defaults[i]); + bool is_natural_wonder = (district_id == NATURAL_WONDER_DISTRICT_ID); + bool districts_disabled = ! is->current_config.enable_districts; + bool natural_wonders_disabled = ! is->current_config.enable_natural_wonders; + struct district_config const * cfg = &is->district_configs[district_id]; - memset (is->district_configs, 0, sizeof is->district_configs); - for (int i = 0; i < USED_SPECIAL_DISTRICT_TYPES; i++) - is->district_configs[i] = special_district_defaults[i]; + if (districts_disabled && (!is_natural_wonder || natural_wonders_disabled)) + return false; - is->special_district_count = USED_SPECIAL_DISTRICT_TYPES; - is->dynamic_district_count = 0; - is->district_count = is->special_district_count; - is->next_custom_dynamic_command_index = 0; -} + struct district_instance * inst = get_district_instance (tile); + if (inst == NULL || inst->district_id != district_id) + return false; -void -reset_wonder_district_configs (void) -{ - for (int i = 0; i < MAX_WONDER_DISTRICT_TYPES; i++) { - if (is->wonder_district_configs[i].is_dynamic) - free_dynamic_wonder_config (&is->wonder_district_configs[i]); - } + // If already marked COMPLETED, just return true + if (inst->state == DS_COMPLETED) + return true; - memset (is->wonder_district_configs, 0, sizeof is->wonder_district_configs); - is->wonder_district_count = 0; -} + // State is UNDER_CONSTRUCTION - check if tile has mines now + bool has_mines = tile->vtable->m18_Check_Mines (tile, __, 0) != 0; -void -reset_natural_wonder_configs (void) -{ - for (int i = 0; i < MAX_NATURAL_WONDER_DISTRICT_TYPES; i++) { - if (is->natural_wonder_configs[i].is_dynamic) - free_dynamic_natural_wonder_config (&is->natural_wonder_configs[i]); + if (! has_mines) { + // Still under construction - check if we should clean it up + bool worker_present = false; + FOR_UNITS_ON (uti, tile) { + Unit * unit = uti.unit; + if ((unit != NULL) && + (p_bic_data->UnitTypes[unit->Body.UnitTypeID].Worker_Actions != 0)) { + worker_present = true; + break; + } + } + if (! worker_present) { + remove_district_instance (tile); + } + return false; } - memset (is->natural_wonder_configs, 0, sizeof is->natural_wonder_configs); - for (int i = 0; i < MAX_NATURAL_WONDER_DISTRICT_TYPES; i++) { - is->natural_wonder_configs[i].adjacent_to = (enum SquareTypes)SQ_INVALID; - is->natural_wonder_configs[i].adjacency_dir = DIR_ZERO; - } - for (int i = 0; i < MAX_NATURAL_WONDER_DISTRICT_TYPES; i++) - is->natural_wonder_img_sets[i].img.vtable = NULL; - stable_deinit (&is->natural_wonder_name_to_id); - is->natural_wonder_count = 0; -} + // Mark as completed and run one-time side effects + inst->state = DS_COMPLETED; + inst->completed_turn = *p_current_turn_no; -void -clear_dynamic_district_definitions (void) -{ - reset_regular_district_configs (); - reset_wonder_district_configs (); - reset_natural_wonder_configs (); -} + int tile_x, tile_y; + if (district_instance_get_coords (inst, tile, &tile_x, &tile_y)) { + set_tile_unworkable_for_all_cities (tile, tile_x, tile_y); + int territory_owner = tile->vtable->m38_Get_Territory_OwnerID (tile); -void -init_parsed_district_definition (struct parsed_district_definition * def) -{ - memset (def, 0, sizeof *def); - def->img_path_count = -1; - def->defense_bonus_percent = 100; -} + if (cfg->auto_add_road) { + bool has_road = tile->vtable->m25_Check_Roads (tile, __, 0) != 0; + if (! has_road) + tile->vtable->m56_Set_Tile_Flags (tile, __, 0, TILE_FLAG_ROAD, tile_x, tile_y); + } -void -free_parsed_district_definition (struct parsed_district_definition * def) -{ - if (def == NULL) - return; + if (cfg->auto_add_railroad) { + bool has_railroad = tile->vtable->m23_Check_Railroads (tile, __, 0) != 0; + if (! has_railroad) { + if ((territory_owner >= 0) && patch_Leader_can_do_worker_job (&leaders[territory_owner], __, WJ_Build_Railroad, tile_x, tile_y, 0)) { + tile->vtable->m56_Set_Tile_Flags (tile, __, 0, TILE_FLAG_RAILROAD, tile_x, tile_y); + } + } + } - if (def->name != NULL) { - free (def->name); - def->name = NULL; - } - if (def->tooltip != NULL) { - free (def->tooltip); - def->tooltip = NULL; - } - if (def->advance_prereq != NULL) { - free (def->advance_prereq); - def->advance_prereq = NULL; - } + // Activate distribution hub if applicable + if (is->current_config.enable_distribution_hub_districts && + (district_id == DISTRIBUTION_HUB_DISTRICT_ID)) { + on_distribution_hub_completed (tile, tile_x, tile_y); + } - for (int i = 0; i < def->dependent_improvement_count; i++) { - if (def->dependent_improvements[i] != NULL) { - free (def->dependent_improvements[i]); - def->dependent_improvements[i] = NULL; + // Remove forest/swamp if applicable + enum SquareTypes base_type = tile->vtable->m50_Get_Square_BaseType (tile); + if ((base_type == SQ_Forest) || (base_type == SQ_Swamp)) { + int new_terrain_type = tile->vtable->m71_Check_Worker_Job (tile); + if (new_terrain_type >= 0) + Map_change_tile_terrain (&p_bic_data->Map, __, (enum SquareTypes)new_terrain_type, tile_x, tile_y); } - } - def->dependent_improvement_count = 0; - for (int i = 0; i < def->img_path_count; i++) { - if (def->img_paths[i] != NULL) { - free (def->img_paths[i]); - def->img_paths[i] = NULL; + char ss[200]; + snprintf (ss, sizeof ss, "District %d completed at tile (%d,%d)\n", district_id, tile_x, tile_y); + (*p_OutputDebugStringA) (ss); + + // Check if this was an AI-requested district + struct pending_district_request * req = find_pending_district_request_by_coords (NULL, tile_x, tile_y, district_id); + if (req != NULL) { + City * requesting_city = get_city_ptr (req->city_id); + if (requesting_city != NULL) { + req->city = requesting_city; + snprintf (ss, sizeof ss, "District %d at tile (%d,%d) was ordered by city %s\n", + district_id, tile_x, tile_y, requesting_city->Body.CityName); + (*p_OutputDebugStringA) (ss); + + // Check if city has pending building order that depends on this district + int pending_improv_id; + if (lookup_pending_building_order (requesting_city, &pending_improv_id)) { + struct district_building_prereq_list * prereq_list = get_district_building_prereq_list (pending_improv_id); + if (district_building_prereq_list_contains (prereq_list, district_id)) { + snprintf (ss, sizeof ss, "City %s setting production to improvement %d\n", + requesting_city->Body.CityName, pending_improv_id); + (*p_OutputDebugStringA) (ss); + + // Check if another city is already building this wonder/small wonder + bool can_set_production = true; + if (is_wonder_or_small_wonder_already_being_built (requesting_city, pending_improv_id)) { + snprintf (ss, sizeof ss, "City %s cannot build improvement %d - already being built elsewhere\n", + requesting_city->Body.CityName, pending_improv_id); + (*p_OutputDebugStringA) (ss); + can_set_production = false; + } + + // Set city production to the pending improvement + if (can_set_production && City_can_build_improvement (requesting_city, __, pending_improv_id, false)) { + snprintf (ss, sizeof ss, "City %s can now build improvement %d\n", + requesting_city->Body.CityName, pending_improv_id); + (*p_OutputDebugStringA) (ss); + City_set_production (requesting_city, __, COT_Improvement, pending_improv_id, false); + } + + // Clear the pending building order + forget_pending_building_order (requesting_city); + } + } + + // Clear worker assignment so worker is freed up for other tasks + if (req->assigned_worker_id >= 0) { + snprintf (ss, sizeof ss, "Clearing worker assignment for unit %d\n", req->assigned_worker_id); + (*p_OutputDebugStringA) (ss); + int civ_id = req->civ_id; + clear_tracked_worker_assignment_by_id (civ_id, req->assigned_worker_id); + } + } + + // Remove the pending district request + remove_pending_district_request (req); } } - def->img_path_count = 0; - init_parsed_district_definition (def); + return true; } -int -find_special_district_index_by_name (char const * name) +void +mark_city_needs_district (City * city, int district_id) { - if (name == NULL) - return -1; + if (! is->current_config.enable_districts || + (city == NULL) || + (district_id < 0) || (district_id >= is->district_count)) + return; - for (int i = 0; i < is->special_district_count; i++) { - if ((is->district_configs[i].name != NULL) && - (strcmp (is->district_configs[i].name, name) == 0)) - return i; - } - return -1; + create_pending_district_request (city, district_id); } -bool -ensure_culture_variant_art (struct district_config * cfg, int section_start_line) +void +set_tile_unworkable_for_all_cities (Tile * tile, int tile_x, int tile_y) { - if ((cfg == NULL) || (! cfg->vary_img_by_culture)) - return true; + if ((tile == NULL) || (tile == p_null_tile)) + return; - const int required_variants = 5; - const int max_img_paths = ARRAY_LEN (is->district_configs[0].img_paths); - if (cfg->img_path_count <= 0) { - char ss[256]; - snprintf (ss, sizeof ss, "[C3X] load_dynamic_district_configs: district \"%s\" requires culture-specific art but none provided (line %d)\n", cfg->name, section_start_line); - (*p_OutputDebugStringA) (ss); - return false; - } + wrap_tile_coords (&p_bic_data->Map, &tile_x, &tile_y); - while ((cfg->img_path_count < required_variants) && - (cfg->img_path_count < max_img_paths)) { - cfg->img_paths[cfg->img_path_count] = strdup (cfg->img_paths[0]); - cfg->img_path_count += 1; - } + City * assigned_city = NULL; + int assigned_city_id = tile->Body.CityAreaID; + if (assigned_city_id >= 0) + assigned_city = get_city_ptr (assigned_city_id); - return true; -} + if (assigned_city != NULL) { + int neighbor_index = Map_compute_neighbor_index (&p_bic_data->Map, __, + assigned_city->Body.X, assigned_city->Body.Y, tile_x, tile_y, 1000); + bool removed_assignment = false; + if ((neighbor_index > 0) && (neighbor_index < ARRAY_LEN (is->ni_to_work_radius))) + removed_assignment = City_stop_working_tile (assigned_city, __, neighbor_index); + if (! removed_assignment) + tile->Body.CityAreaID = -1; + if (! removed_assignment) + recompute_city_yields_with_districts (assigned_city); + } else + tile->Body.CityAreaID = -1; -bool -parse_config_string_list (char * value_text, - char ** dest, - int capacity, - int * out_count, - struct error_line ** parse_errors, - int line_number, - char const * key) -{ - for (int i = 0; i < capacity; i++) { - if (dest[i] != NULL) { - free (dest[i]); - dest[i] = NULL; + if (p_cities->Cities != NULL) { + for (int city_index = 0; city_index <= p_cities->LastIndex; city_index++) { + City * city = get_city_ptr (city_index); + if ((city == NULL) || (city == assigned_city)) + continue; + int neighbor_index = Map_compute_neighbor_index (&p_bic_data->Map, __, + city->Body.X, city->Body.Y, tile_x, tile_y, 1000); + if ((neighbor_index <= 0) || (neighbor_index >= ARRAY_LEN (is->ni_to_work_radius))) + continue; + int work_radius = is->ni_to_work_radius[neighbor_index]; + if ((work_radius < 0) || (work_radius > is->current_config.city_work_radius)) + continue; + recompute_city_yields_with_districts (city); } } - *out_count = 0; - - if (value_text == NULL || *value_text == '\0') - return true; - - char * cursor = value_text; - while (1) { - while (is_space_char (*cursor)) - cursor++; +} - if (*cursor == '\0') - break; +struct distribution_hub_record * +get_distribution_hub_record (Tile * tile) +{ + if ((tile == NULL) || (tile == p_null_tile)) + return NULL; - char * item_start; - char * item_end; - if (*cursor == '"') { - cursor++; - item_start = cursor; - while ((*cursor != '\0') && (*cursor != '"')) - cursor++; - if (*cursor != '"') { - struct error_line * err = add_error_line (parse_errors); - snprintf (err->text, sizeof err->text, "^ Line %d: %s (missing closing quote)", line_number, key); - err->text[(sizeof err->text) - 1] = '\0'; - for (int j = 0; j < capacity; j++) { - if (dest[j] != NULL) { - free (dest[j]); - dest[j] = NULL; - } - } - *out_count = 0; - return false; - } - item_end = cursor; - cursor++; - } else { - item_start = cursor; - while ((*cursor != '\0') && (*cursor != ',')) - cursor++; - item_end = cursor; - } + int stored; + if (itable_look_up (&is->distribution_hub_records, (int)tile, &stored)) + return (struct distribution_hub_record *)stored; + else + return NULL; +} - while ((item_end > item_start) && is_space_char (item_end[-1])) - item_end--; +City * +get_connected_city_for_distribution_hub (struct distribution_hub_record * rec) +{ + if (rec == NULL) + return NULL; - int item_len = item_end - item_start; - if (item_len > 0) { - if (*out_count < capacity) { - char * copy = malloc (item_len + 1); - if (copy == NULL) { - struct error_line * err = add_error_line (parse_errors); - snprintf (err->text, sizeof err->text, "^ Line %d: %s (out of memory)", line_number, key); - err->text[(sizeof err->text) - 1] = '\0'; - for (int j = 0; j < capacity; j++) { - if (dest[j] != NULL) { - free (dest[j]); - dest[j] = NULL; - } - } - *out_count = 0; - return false; - } - memcpy (copy, item_start, item_len); - copy[item_len] = '\0'; - dest[*out_count] = copy; - *out_count += 1; - } - } + Tile * tile = rec->tile; + if ((tile == NULL) || (tile == p_null_tile)) + tile = tile_at (rec->tile_x, rec->tile_y); + if ((tile == NULL) || (tile == p_null_tile)) + return NULL; - while (is_space_char (*cursor)) - cursor++; + int city_id = tile->Body.connected_city_ids[rec->civ_id]; + if (city_id < 0) + return NULL; - if (*cursor == ',') { - cursor++; - continue; - } - if (*cursor == '\0') - break; - } + City * city = get_city_ptr (city_id); - return true; + return city; } bool -override_special_district_from_definition (struct parsed_district_definition * def, int section_start_line) +distribution_hub_accessible_to_city (struct distribution_hub_record * rec, City * city) { - if ((! def->has_name) || (def->name == NULL)) + if ((rec == NULL) || (city == NULL)) return false; - int index = find_special_district_index_by_name (def->name); - if (index < 0) + if (city->Body.CivID != rec->civ_id) return false; - struct district_config * cfg = &is->district_configs[index]; - struct district_config const * defaults = &special_district_defaults[index]; + City * anchor_city = get_connected_city_for_distribution_hub (rec); + if (anchor_city == NULL) + return false; - free (def->name); - def->name = NULL; - def->has_name = false; + if (anchor_city == city) + return true; - if (def->has_tooltip) { - if ((cfg->tooltip != NULL) && (cfg->tooltip != defaults->tooltip)) - free ((void *)cfg->tooltip); - cfg->tooltip = def->tooltip; - def->tooltip = NULL; - } + return Trade_Net_have_trade_connection (is->trade_net, __, anchor_city, city, rec->civ_id); +} - if (def->has_advance_prereq) { - if ((cfg->advance_prereq != NULL) && (cfg->advance_prereq != defaults->advance_prereq)) - free ((void *)cfg->advance_prereq); - cfg->advance_prereq = def->advance_prereq; - def->advance_prereq = NULL; - } +void +get_distribution_hub_yields_for_city (City * city, int * out_food, int * out_shields) +{ + int food = 0; + int shields = 0; - if (def->has_allow_multiple) - cfg->allow_multiple = def->allow_multiple; - if (def->has_vary_img_by_era) - cfg->vary_img_by_era = def->vary_img_by_era; - if (def->has_vary_img_by_culture) - cfg->vary_img_by_culture = def->vary_img_by_culture; - if (def->has_btn_tile_sheet_column) - cfg->btn_tile_sheet_column = def->btn_tile_sheet_column; - if (def->has_btn_tile_sheet_row) - cfg->btn_tile_sheet_row = def->btn_tile_sheet_row; - if (def->has_defense_bonus_percent) - cfg->defense_bonus_percent = def->defense_bonus_percent; - if (def->has_culture_bonus) - cfg->culture_bonus = def->culture_bonus; - if (def->has_science_bonus) - cfg->science_bonus = def->science_bonus; - if (def->has_food_bonus) - cfg->food_bonus = def->food_bonus; - if (def->has_gold_bonus) - cfg->gold_bonus = def->gold_bonus; - if (def->has_shield_bonus) - cfg->shield_bonus = def->shield_bonus; + if ((city != NULL) && + is->current_config.enable_districts && + is->current_config.enable_distribution_hub_districts) { + if (is->distribution_hub_totals_dirty && + ! is->distribution_hub_refresh_in_progress) + recompute_distribution_hub_totals (); - if (def->has_dependent_improvements) { - for (int i = 0; i < ARRAY_LEN (cfg->dependent_improvements); i++) { - char const * default_value = (i < defaults->dependent_improvement_count) ? defaults->dependent_improvements[i] : NULL; - if ((cfg->dependent_improvements[i] != NULL) && - (cfg->dependent_improvements[i] != default_value)) - free ((void *)cfg->dependent_improvements[i]); - cfg->dependent_improvements[i] = NULL; + FOR_TABLE_ENTRIES (tei, &is->distribution_hub_records) { + struct distribution_hub_record * rec = (struct distribution_hub_record *)tei.value; + if (distribution_hub_accessible_to_city (rec, city)) { + food += rec->food_yield; + shields += rec->shield_yield; + } } + } - cfg->dependent_improvement_count = def->dependent_improvement_count; - const int max_entries = ARRAY_LEN (cfg->dependent_improvements); - if (cfg->dependent_improvement_count > max_entries) - cfg->dependent_improvement_count = max_entries; - for (int i = 0; i < cfg->dependent_improvement_count; i++) { - cfg->dependent_improvements[i] = def->dependent_improvements[i]; - def->dependent_improvements[i] = NULL; - } - cfg->max_building_index = cfg->dependent_improvement_count; - if (cfg->max_building_index > 5) - cfg->max_building_index = 5; + if (city_has_required_district (city, CENTRAL_RAIL_HUB_DISTRICT_ID)) { + food += (food * is->current_config.central_rail_hub_distribution_food_bonus_percent) / 100; + shields += (shields * is->current_config.central_rail_hub_distribution_shield_bonus_percent) / 100; } - if (def->has_img_paths) { - for (int i = 0; i < ARRAY_LEN (cfg->img_paths); i++) { - char const * default_value = (i < defaults->img_path_count) ? defaults->img_paths[i] : NULL; - if ((cfg->img_paths[i] != NULL) && - (cfg->img_paths[i] != default_value)) - free ((void *)cfg->img_paths[i]); - cfg->img_paths[i] = NULL; - } + if (out_food != NULL) + *out_food = food; + if (out_shields != NULL) + *out_shields = shields; +} - cfg->img_path_count = def->img_path_count; - const int max_img_paths = ARRAY_LEN (cfg->img_paths); - if (cfg->img_path_count > max_img_paths) - cfg->img_path_count = max_img_paths; - for (int i = 0; i < cfg->img_path_count; i++) { - cfg->img_paths[i] = def->img_paths[i]; - def->img_paths[i] = NULL; +void +adjust_distribution_hub_coverage (struct distribution_hub_record * rec) +{ + if (rec == NULL) + return; + + FOR_TILES_AROUND (tai, workable_tile_counts[1], rec->tile_x, rec->tile_y) { + Tile * area_tile = tai.tile; + if ((area_tile == NULL) || (area_tile == p_null_tile)) + continue; + + int tx, ty; + tai_get_coords (&tai, &tx, &ty); + + if (area_tile->vtable->m38_Get_Territory_OwnerID (area_tile) != rec->civ_id) + continue; + if (Tile_has_city (area_tile)) + continue; + if (get_district_instance (area_tile) != NULL) + continue; + + struct wonder_district_info * area_info = get_wonder_district_info (area_tile); + if ((area_info != NULL) && (area_info->state == WDS_COMPLETED)) + continue; + + int key = (int)area_tile; + int prev = itable_look_up_or (&is->distribution_hub_coverage_counts, key, 0); + itable_insert (&is->distribution_hub_coverage_counts, key, prev + 1); + + if (area_tile->Body.CityAreaID >= 0) { + set_tile_unworkable_for_all_cities (area_tile, tx, ty); + area_tile->Body.CityAreaID = -1; } } +} - if (! ensure_culture_variant_art (cfg, section_start_line)) { - free_special_district_override_strings (cfg, defaults); - *cfg = *defaults; - return false; +void +release_distribution_hub_coverage (struct distribution_hub_record * rec) +{ + if (rec == NULL) + return; + + FOR_TILES_AROUND (tai, workable_tile_counts[1], rec->tile_x, rec->tile_y) { + Tile * area_tile = tai.tile; + if ((area_tile == NULL) || (area_tile == p_null_tile)) + continue; + + int key = (int)area_tile; + int prev = itable_look_up_or (&is->distribution_hub_coverage_counts, key, 0); + if (prev <= 0) + continue; + + if (prev == 1) + itable_remove (&is->distribution_hub_coverage_counts, key); + else + itable_insert (&is->distribution_hub_coverage_counts, key, prev - 1); } +} - return true; +void +clear_distribution_hub_tables (void) +{ + FOR_TABLE_ENTRIES (tei, &is->distribution_hub_records) { + struct distribution_hub_record * rec = (struct distribution_hub_record *)tei.value; + free (rec); + } + table_deinit (&is->distribution_hub_records); + table_deinit (&is->distribution_hub_coverage_counts); + is->distribution_hub_totals_dirty = true; } bool -add_dynamic_district_from_definition (struct parsed_district_definition * def, int section_start_line) +city_radius_contains_tile (City * city, int tile_x, int tile_y) { - if ((! def->has_name) || (def->name == NULL)) + if (city == NULL) return false; - if ((! def->has_img_paths) || (def->img_path_count <= 0)) - return false; + int ni = patch_Map_compute_ni_for_work_area (&p_bic_data->Map, __, city->Body.X, city->Body.Y, tile_x, tile_y, is->workable_tile_count); + return ni >= 0; +} - int existing_index = -1; - for (int i = is->special_district_count; i < is->district_count; i++) { - if ((is->district_configs[i].name != NULL) && - (strcmp (is->district_configs[i].name, def->name) == 0)) { - existing_index = i; - break; - } +void +recompute_distribution_hub_yields (struct distribution_hub_record * rec) +{ + if (rec == NULL) + return; + + Tile * tile = tile_at (rec->tile_x, rec->tile_y); + rec->tile = tile; + + if ((tile == NULL) || + (tile == p_null_tile) || + ! district_is_complete (tile, DISTRIBUTION_HUB_DISTRICT_ID) || + tile->vtable->m20_Check_Pollution (tile, __, 0) || + tile_has_enemy_unit (tile, rec->civ_id)) { + rec->food_yield = 0; + rec->shield_yield = 0; + rec->raw_food_yield = 0; + rec->raw_shield_yield = 0; + return; } - bool reusing_existing = existing_index >= 0; - int dest_index = reusing_existing ? existing_index : (is->special_district_count + is->dynamic_district_count); + int food_sum = 0; + int shield_sum = 0; + City * anchor_city = get_connected_city_for_distribution_hub (rec); + FOR_TILES_AROUND (tai, workable_tile_counts[1], rec->tile_x, rec->tile_y) { + Tile * area_tile = tai.tile; + if (area_tile == p_null_tile) + continue; - if ((! reusing_existing) && (dest_index >= COUNT_DISTRICT_TYPES)) - return false; + int tx, ty; + tai_get_coords (&tai, &tx, &ty); - enum Unit_Command_Values preserved_command = 0; - if (reusing_existing) - preserved_command = is->district_configs[dest_index].command; + // Only include tiles that belong to the distribution hub owner + if (area_tile->vtable->m38_Get_Territory_OwnerID (area_tile) != rec->civ_id) + continue; - struct district_config new_cfg; - memset (&new_cfg, 0, sizeof new_cfg); - new_cfg.is_dynamic = true; + // Skip city tiles + if (Tile_has_city (area_tile)) + continue; - new_cfg.name = def->name; - def->name = NULL; + // Skip tiles with enemy units + if (tile_has_enemy_unit (area_tile, rec->civ_id)) + continue; - if (def->has_tooltip) { - new_cfg.tooltip = def->tooltip; - def->tooltip = NULL; - } else if (new_cfg.name != NULL) { - char buffer[128]; - snprintf (buffer, sizeof buffer, "Build %s", new_cfg.name); - new_cfg.tooltip = strdup (buffer); - } + // Skip tiles with pollution + if (area_tile->vtable->m20_Check_Pollution (area_tile, __, 0)) + continue; - if (def->has_advance_prereq) { - new_cfg.advance_prereq = def->advance_prereq; - def->advance_prereq = NULL; - } + // Skip tiles that are other districts (but not this hub itself) + struct district_instance * area_district = get_district_instance (area_tile); + if ((area_district != NULL) && ((tx != rec->tile_x) || (ty != rec->tile_y))) + continue; - new_cfg.allow_multiple = def->has_allow_multiple ? def->allow_multiple : false; - new_cfg.vary_img_by_era = def->has_vary_img_by_era ? def->vary_img_by_era : false; - new_cfg.vary_img_by_culture = def->has_vary_img_by_culture ? def->vary_img_by_culture : false; - new_cfg.btn_tile_sheet_column = def->has_btn_tile_sheet_column ? def->btn_tile_sheet_column : 0; - new_cfg.btn_tile_sheet_row = def->has_btn_tile_sheet_row ? def->btn_tile_sheet_row : 0; - new_cfg.defense_bonus_percent = def->has_defense_bonus_percent ? def->defense_bonus_percent : 100; - new_cfg.culture_bonus = def->has_culture_bonus ? def->culture_bonus : 0; - new_cfg.science_bonus = def->has_science_bonus ? def->science_bonus : 0; - new_cfg.food_bonus = def->has_food_bonus ? def->food_bonus : 0; - new_cfg.gold_bonus = def->has_gold_bonus ? def->gold_bonus : 0; - new_cfg.shield_bonus = def->has_shield_bonus ? def->shield_bonus : 0; + // Skip tiles with completed wonders + struct wonder_district_info * area_info = get_wonder_district_info (area_tile); + if ((area_info != NULL) && (area_info->state == WDS_COMPLETED)) + continue; - new_cfg.dependent_improvement_count = def->has_dependent_improvements ? def->dependent_improvement_count : 0; - const int max_dependent_entries = ARRAY_LEN (is->district_configs[0].dependent_improvements); - if (new_cfg.dependent_improvement_count > max_dependent_entries) - new_cfg.dependent_improvement_count = max_dependent_entries; - for (int i = 0; i < new_cfg.dependent_improvement_count; i++) { - new_cfg.dependent_improvements[i] = def->dependent_improvements[i]; - def->dependent_improvements[i] = NULL; - } + // Check if another hub of the same civ is closer to this tile + int my_distance = compute_wrapped_manhattan_distance (rec->tile_x, rec->tile_y, tx, ty); + bool tile_belongs_to_me = true; - new_cfg.img_path_count = def->img_path_count; - const int max_img_paths = ARRAY_LEN (is->district_configs[0].img_paths); - if (new_cfg.img_path_count > max_img_paths) - new_cfg.img_path_count = max_img_paths; - for (int i = 0; i < new_cfg.img_path_count; i++) { - new_cfg.img_paths[i] = def->img_paths[i]; - def->img_paths[i] = NULL; - } + FOR_TABLE_ENTRIES (other_tei, &is->distribution_hub_records) { + struct distribution_hub_record * other_rec = (struct distribution_hub_record *)other_tei.value; + if ((other_rec == NULL) || (other_rec == rec)) + continue; + if (other_rec->civ_id != rec->civ_id) + continue; - if (! ensure_culture_variant_art (&new_cfg, section_start_line)) { - free_dynamic_district_config (&new_cfg); - return false; + int other_distance = compute_wrapped_manhattan_distance (other_rec->tile_x, other_rec->tile_y, tx, ty); + if (other_distance < my_distance) { + tile_belongs_to_me = false; + break; + } + if (other_distance == my_distance) { + // Tie-breaking: prefer hub with lower Y, then lower X + if (other_rec->tile_y < rec->tile_y) { + tile_belongs_to_me = false; + break; + } + if ((other_rec->tile_y == rec->tile_y) && (other_rec->tile_x < rec->tile_x)) { + tile_belongs_to_me = false; + break; + } + } + } + + if (! tile_belongs_to_me) + continue; + + food_sum += City_calc_tile_yield_at (anchor_city, __, 0, tx, ty); + shield_sum += City_calc_tile_yield_at (anchor_city, __, 1, tx, ty); } - new_cfg.max_building_index = new_cfg.dependent_improvement_count; - if (new_cfg.max_building_index > 5) - new_cfg.max_building_index = 5; + rec->raw_food_yield = food_sum; + rec->raw_shield_yield = shield_sum; - if (reusing_existing) - new_cfg.command = preserved_command; - else - new_cfg.command = allocate_dynamic_district_command (new_cfg.name); + int food_div = is->current_config.distribution_hub_food_yield_divisor; + int shield_div = is->current_config.distribution_hub_shield_yield_divisor; + if (food_div <= 0) + food_div = 1; + if (shield_div <= 0) + shield_div = 1; - struct district_config * dest_cfg = &is->district_configs[dest_index]; - if (reusing_existing) { - enum Unit_Command_Values saved_command = preserved_command; - free_dynamic_district_config (dest_cfg); - *dest_cfg = new_cfg; - dest_cfg->command = saved_command; + int connected_city_count = 0; + if (anchor_city != NULL) { + FOR_CITIES_OF (coi, rec->civ_id) { + City * other_city = coi.city; + if ((other_city != NULL) && distribution_hub_accessible_to_city (rec, other_city)) + connected_city_count++; + } + } + if (connected_city_count <= 0) + connected_city_count = 1; + + if (is->current_config.distribution_hub_yield_division_mode == DHYDM_SCALE_BY_CITY_COUNT) { + int city_root = 1; + while ((city_root + 1) * (city_root + 1) <= connected_city_count) + city_root++; + int city_food_divisor = city_root * food_div; + int city_shield_divisor = city_root * shield_div; + if (city_food_divisor < 1) city_food_divisor = 1; + if (city_shield_divisor < 1) city_shield_divisor = 1; + rec->food_yield = food_sum / city_food_divisor; + rec->shield_yield = shield_sum / city_shield_divisor; } else { - free_dynamic_district_config (dest_cfg); - *dest_cfg = new_cfg; - is->dynamic_district_count += 1; - is->district_count = is->special_district_count + is->dynamic_district_count; + rec->food_yield = food_sum / food_div; + rec->shield_yield = shield_sum / shield_div; } - - return true; } void -finalize_parsed_district_definition (struct parsed_district_definition * def, int section_start_line) +remove_distribution_hub_record (Tile * tile) { - if ((! def->has_name) || (def->name == NULL)) + struct distribution_hub_record * rec = get_distribution_hub_record (tile); + if (rec == NULL) return; - if (! override_special_district_from_definition (def, section_start_line)) - add_dynamic_district_from_definition (def, section_start_line); - - free_parsed_district_definition (def); -} - -void -handle_district_definition_key (struct parsed_district_definition * def, - struct string_slice const * key, - struct string_slice const * value, - int line_number, - struct error_line ** parse_errors, - struct error_line ** unrecognized_keys) -{ - if (slice_matches_str (key, "name")) { - if (def->name != NULL) { - free (def->name); - def->name = NULL; - } + int affected_civ_id = rec->civ_id; + release_distribution_hub_coverage (rec); + itable_remove (&is->distribution_hub_records, (int)tile); + free (rec); + is->distribution_hub_totals_dirty = true; + recompute_distribution_hub_totals (); - char * name_copy = copy_trimmed_string_or_null (value, 1); - if (name_copy == NULL) { - def->has_name = false; - add_key_parse_error (parse_errors, line_number, key, "(value is required)"); - } else { - def->name = name_copy; - def->has_name = true; - } + // Recalculate yields for all cities of this civ + if ((affected_civ_id >= 0) && (p_cities->Cities != NULL)) { + for (int city_index = 0; city_index <= p_cities->LastIndex; city_index++) { + City * target_city = get_city_ptr (city_index); + if ((target_city != NULL) && (target_city->Body.CivID == affected_civ_id)) + recompute_city_yields_with_districts (target_city); + } + } +} - } else if (slice_matches_str (key, "tooltip")) { - if (def->tooltip != NULL) { - free (def->tooltip); - def->tooltip = NULL; - } - def->tooltip = copy_trimmed_string_or_null (value, 1); - def->has_tooltip = true; +void +recompute_distribution_hub_totals () +{ + if (! is->current_config.enable_districts || + ! is->current_config.enable_distribution_hub_districts) { + is->distribution_hub_totals_dirty = false; + return; + } - } else if (slice_matches_str (key, "advance_prereq")) { - if (def->advance_prereq != NULL) { - free (def->advance_prereq); - def->advance_prereq = NULL; - } - def->advance_prereq = copy_trimmed_string_or_null (value, 1); - def->has_advance_prereq = true; + struct table new_coverage_counts = {0}; + struct table newly_covered_tiles = {0}; - } else if (slice_matches_str (key, "img_paths")) { - char * value_text = trim_and_extract_slice (value, 0); - int list_count = 0; - if (parse_config_string_list (value_text, - def->img_paths, - ARRAY_LEN (def->img_paths), - &list_count, - parse_errors, - line_number, - "img_paths")) { - def->img_path_count = list_count; - def->has_img_paths = true; - } else { - def->img_path_count = 0; - def->has_img_paths = false; - } - free (value_text); + clear_memo (); + int civs_needing_recalc[32] = {0}; - } else if (slice_matches_str (key, "dependent_improvs")) { - char * value_text = trim_and_extract_slice (value, 0); - int list_count = 0; - if (parse_config_string_list (value_text, - def->dependent_improvements, - ARRAY_LEN (def->dependent_improvements), - &list_count, - parse_errors, - line_number, - "dependent_improvs")) { - def->dependent_improvement_count = list_count; - def->has_dependent_improvements = true; - } else { - def->dependent_improvement_count = 0; - def->has_dependent_improvements = false; + FOR_TABLE_ENTRIES (tei, &is->distribution_hub_records) { + Tile * tile = (Tile *)tei.key; + struct distribution_hub_record * rec = (struct distribution_hub_record *)tei.value; + if (rec == NULL) + continue; + + Tile * current_tile = tile_at (rec->tile_x, rec->tile_y); + if ((current_tile == NULL) || + (current_tile == p_null_tile) || + ! district_is_complete (current_tile, DISTRIBUTION_HUB_DISTRICT_ID)) { + memoize (tei.key); + continue; } - free (value_text); - } else if (slice_matches_str (key, "allow_multiple")) { - struct string_slice val_slice = *value; - int ival; - if (read_int (&val_slice, &ival)) { - def->allow_multiple = (ival != 0); - def->has_allow_multiple = true; - } else - add_key_parse_error (parse_errors, line_number, key, "(expected integer)"); + rec->tile = current_tile; + rec->food_yield = 0; + rec->shield_yield = 0; + rec->raw_food_yield = 0; + rec->raw_shield_yield = 0; - } else if (slice_matches_str (key, "vary_img_by_era")) { - struct string_slice val_slice = *value; - int ival; - if (read_int (&val_slice, &ival)) { - def->vary_img_by_era = (ival != 0); - def->has_vary_img_by_era = true; - } else - add_key_parse_error (parse_errors, line_number, key, "(expected integer)"); + int old_civ_id = rec->civ_id; + rec->civ_id = current_tile->vtable->m38_Get_Territory_OwnerID (current_tile); - } else if (slice_matches_str (key, "vary_img_by_culture")) { - struct string_slice val_slice = *value; - int ival; - if (read_int (&val_slice, &ival)) { - def->vary_img_by_culture = (ival != 0); - def->has_vary_img_by_culture = true; - } else - add_key_parse_error (parse_errors, line_number, key, "(expected integer)"); + if (old_civ_id != rec->civ_id) + civs_needing_recalc[old_civ_id] = 1; + civs_needing_recalc[rec->civ_id] = 1; - } else if (slice_matches_str (key, "btn_tile_sheet_column")) { - struct string_slice val_slice = *value; - int ival; - if (read_int (&val_slice, &ival)) { - def->btn_tile_sheet_column = ival; - def->has_btn_tile_sheet_column = true; - } else - add_key_parse_error (parse_errors, line_number, key, "(expected integer)"); + City * anchor = get_connected_city_for_distribution_hub (rec); - } else if (slice_matches_str (key, "btn_tile_sheet_row")) { - struct string_slice val_slice = *value; - int ival; - if (read_int (&val_slice, &ival)) { - def->btn_tile_sheet_row = ival; - def->has_btn_tile_sheet_row = true; - } else - add_key_parse_error (parse_errors, line_number, key, "(expected integer)"); + if ((anchor == NULL) || + current_tile->vtable->m20_Check_Pollution (current_tile, __, 0) || + tile_has_enemy_unit (current_tile, rec->civ_id)) + continue; - } else if (slice_matches_str (key, "defense_bonus_percent")) { - struct string_slice val_slice = *value; - int ival; - if (read_int (&val_slice, &ival)) { - def->defense_bonus_percent = ival; - def->has_defense_bonus_percent = true; - } else - add_key_parse_error (parse_errors, line_number, key, "(expected integer)"); + FOR_TILES_AROUND (tai, workable_tile_counts[1], rec->tile_x, rec->tile_y) { + Tile * area_tile = tai.tile; + if (area_tile == p_null_tile) + continue; - } else if (slice_matches_str (key, "culture_bonus")) { - struct string_slice val_slice = *value; - int ival; - if (read_int (&val_slice, &ival)) { - def->culture_bonus = ival; - def->has_culture_bonus = true; - } else - add_key_parse_error (parse_errors, line_number, key, "(expected integer)"); + int tx, ty; + tai_get_coords (&tai, &tx, &ty); - } else if (slice_matches_str (key, "science_bonus")) { - struct string_slice val_slice = *value; - int ival; - if (read_int (&val_slice, &ival)) { - def->science_bonus = ival; - def->has_science_bonus = true; - } else - add_key_parse_error (parse_errors, line_number, key, "(expected integer)"); + if (area_tile->vtable->m38_Get_Territory_OwnerID (area_tile) != rec->civ_id) + continue; + if (Tile_has_city (area_tile)) + continue; + if (tile_has_enemy_unit (area_tile, rec->civ_id)) + continue; + if (area_tile->vtable->m20_Check_Pollution (area_tile, __, 0)) + continue; - } else if (slice_matches_str (key, "food_bonus")) { - struct string_slice val_slice = *value; - int ival; - if (read_int (&val_slice, &ival)) { - def->food_bonus = ival; - def->has_food_bonus = true; - } else - add_key_parse_error (parse_errors, line_number, key, "(expected integer)"); + struct district_instance * area_district = get_district_instance (area_tile); + if ((area_district != NULL) && ((tx != rec->tile_x) || (ty != rec->tile_y))) + continue; - } else if (slice_matches_str (key, "gold_bonus")) { - struct string_slice val_slice = *value; - int ival; - if (read_int (&val_slice, &ival)) { - def->gold_bonus = ival; - def->has_gold_bonus = true; - } else - add_key_parse_error (parse_errors, line_number, key, "(expected integer)"); + struct wonder_district_info * area_info = get_wonder_district_info (area_tile); + if ((area_info != NULL) && (area_info->state == WDS_COMPLETED)) + continue; - } else if (slice_matches_str (key, "shield_bonus")) { - struct string_slice val_slice = *value; - int ival; - if (read_int (&val_slice, &ival)) { - def->shield_bonus = ival; - def->has_shield_bonus = true; - } else - add_key_parse_error (parse_errors, line_number, key, "(expected integer)"); + int key = (int)area_tile; + int prev_cover_pass = itable_look_up_or (&new_coverage_counts, key, 0); + int prev_cover_old = itable_look_up_or (&is->distribution_hub_coverage_counts, key, 0); + itable_insert (&new_coverage_counts, key, prev_cover_pass + 1); + if ((prev_cover_pass == 0) && (prev_cover_old <= 0)) + itable_insert (&newly_covered_tiles, key, 1); + } + } - } else - add_unrecognized_key_error (unrecognized_keys, line_number, key); -} + for (int i = 0; i < is->memo_len; i++) + remove_distribution_hub_record ((Tile *)is->memo[i]); + clear_memo (); -void -load_dynamic_district_config_file (char const * file_path, - int path_is_relative_to_mod_dir, - int log_missing, - int drop_existing_configs) -{ - char path[MAX_PATH]; - if (path_is_relative_to_mod_dir) { - if (is->mod_rel_dir == NULL) - return; - snprintf (path, sizeof path, "%s\\%s", is->mod_rel_dir, file_path); - } else { - strncpy (path, file_path, sizeof path); - } - path[(sizeof path) - 1] = '\0'; + FOR_TABLE_ENTRIES (tei, &is->distribution_hub_records) { + struct distribution_hub_record * rec = (struct distribution_hub_record *)tei.value; + if (rec == NULL) + continue; - char * text = file_to_string (path); - if (text == NULL) { - if (log_missing) { - char ss[256]; - snprintf (ss, sizeof ss, "[C3X] Districts config file not found: %s", path); - (*p_OutputDebugStringA) (ss); + City * anchor = get_connected_city_for_distribution_hub (rec); + if (anchor == NULL) { + rec->food_yield = 0; + rec->shield_yield = 0; + rec->raw_food_yield = 0; + rec->raw_shield_yield = 0; + continue; } - return; + + recompute_distribution_hub_yields (rec); } - if (drop_existing_configs) - reset_regular_district_configs (); - - struct parsed_district_definition def; - init_parsed_district_definition (&def); - bool in_section = false; - int section_start_line = 0; - int line_number = 0; - struct error_line * unrecognized_keys = NULL; - struct error_line * parse_errors = NULL; - - char * cursor = text; - while (*cursor != '\0') { - line_number += 1; - - char * line_start = cursor; - char * line_end = cursor; - while ((*line_end != '\0') && (*line_end != '\n')) - line_end++; - - int line_len = line_end - line_start; - bool has_newline = (*line_end == '\n'); - if (has_newline) - *line_end = '\0'; + table_deinit (&is->distribution_hub_coverage_counts); + is->distribution_hub_coverage_counts = new_coverage_counts; + memset (&new_coverage_counts, 0, sizeof new_coverage_counts); - struct string_slice line_slice = { .str = line_start, .len = line_len }; - struct string_slice trimmed = trim_string_slice (&line_slice, 0); - if ((trimmed.len == 0) || (trimmed.str[0] == ';')) { - cursor = has_newline ? line_end + 1 : line_end; + FOR_TABLE_ENTRIES (tei, &newly_covered_tiles) { + Tile * covered_tile = (Tile *)tei.key; + if ((covered_tile == NULL) || (covered_tile == p_null_tile)) continue; - } + int tx, ty; + if (! tile_coords_from_ptr (&p_bic_data->Map, covered_tile, &tx, &ty)) + continue; + set_tile_unworkable_for_all_cities (covered_tile, tx, ty); + covered_tile->Body.CityAreaID = -1; + } + table_deinit (&newly_covered_tiles); - if (trimmed.str[0] == '#') { - struct string_slice directive = trimmed; - directive.str += 1; - directive.len -= 1; - directive = trim_string_slice (&directive, 0); - if ((directive.len > 0) && slice_matches_str (&directive, "District")) { - if (in_section) - finalize_parsed_district_definition (&def, section_start_line); - in_section = true; - section_start_line = line_number; + // Recalculate yields for cities of civs whose distribution hub ownership changed + for (int civ_id = 0; civ_id < 32; civ_id++) { + if (civs_needing_recalc[civ_id] && (p_cities->Cities != NULL)) { + for (int city_index = 0; city_index <= p_cities->LastIndex; city_index++) { + City * city = get_city_ptr (city_index); + if ((city != NULL) && (city->Body.CivID == civ_id)) + recompute_city_yields_with_districts (city); } - cursor = has_newline ? line_end + 1 : line_end; - continue; } + } - if (! in_section) { - cursor = has_newline ? line_end + 1 : line_end; - continue; - } + is->distribution_hub_totals_dirty = false; +} - struct string_slice key_slice = {0}; - struct string_slice value_slice = {0}; - enum key_value_parse_status status = parse_trimmed_key_value (&trimmed, &key_slice, &value_slice); - if (status == KVP_NO_EQUALS) { - char * line_text = extract_slice (&trimmed); - struct error_line * err = add_error_line (&parse_errors); - snprintf (err->text, sizeof err->text, "^ Line %d: %s (expected '=')", line_number, line_text); - err->text[(sizeof err->text) - 1] = '\0'; - free (line_text); - cursor = has_newline ? line_end + 1 : line_end; - continue; - } else if (status == KVP_EMPTY_KEY) { - struct error_line * err = add_error_line (&parse_errors); - snprintf (err->text, sizeof err->text, "^ Line %d: (missing key)", line_number); - err->text[(sizeof err->text) - 1] = '\0'; - cursor = has_newline ? line_end + 1 : line_end; - continue; - } +void +on_distribution_hub_completed (Tile * tile, int tile_x, int tile_y) +{ + if (! is->current_config.enable_districts || ! is->current_config.enable_distribution_hub_districts) + return; - handle_district_definition_key (&def, &key_slice, &value_slice, line_number, &parse_errors, &unrecognized_keys); - cursor = has_newline ? line_end + 1 : line_end; - } + int tile_owner = -1; + if ((tile != NULL) && (tile != p_null_tile)) + tile_owner = tile->vtable->m38_Get_Territory_OwnerID (tile); - if (in_section) - finalize_parsed_district_definition (&def, section_start_line); + struct distribution_hub_record * rec = get_distribution_hub_record (tile); + if (rec != NULL) { + int old_civ_id = rec->civ_id; + rec->tile = tile; + rec->tile_x = tile_x; + rec->tile_y = tile_y; - free_parsed_district_definition (&def); - free (text); + release_distribution_hub_coverage (rec); + rec->civ_id = tile_owner; + is->distribution_hub_totals_dirty = true; + recompute_distribution_hub_totals (); - // Append to loaded config names list - struct loaded_config_name * top_lcn = is->loaded_config_names; - while (top_lcn->next != NULL) - top_lcn = top_lcn->next; + if (old_civ_id != tile_owner) { + // Recompute for old civ + for (int city_index = 0; city_index <= p_cities->LastIndex; city_index++) { + City * target_city = get_city_ptr (city_index); + if ((target_city != NULL) && (target_city->Body.CivID == old_civ_id)) + recompute_city_yields_with_districts (target_city); + } + } + } - struct loaded_config_name * new_lcn = malloc (sizeof *new_lcn); - new_lcn->name = strdup (path); - new_lcn->next = NULL; + rec = malloc (sizeof *rec); + if (rec == NULL) + return; + rec->tile = tile; + rec->tile_x = tile_x; + rec->tile_y = tile_y; + rec->civ_id = tile_owner; + rec->food_yield = 0; + rec->shield_yield = 0; + rec->raw_food_yield = 0; + rec->raw_shield_yield = 0; + itable_insert (&is->distribution_hub_records, (int)tile, (int)rec); + adjust_distribution_hub_coverage (rec); - top_lcn->next = new_lcn; - snprintf (is->current_districts_config_path, sizeof is->current_districts_config_path, path); + is->distribution_hub_totals_dirty = true; + recompute_distribution_hub_totals (); - if (parse_errors != NULL || unrecognized_keys != NULL) { - PopupForm * popup = get_popup_form (); - popup->vtable->set_text_key_and_flags (popup, __, is->mod_script_path, "C3X_WARNING", -1, 0, 0, 0); - char s[200]; - snprintf (s, sizeof s, "District Config errors in %s:", path); - s[(sizeof s) - 1] = '\0'; - PopupForm_add_text (popup, __, s, false); - if (parse_errors != NULL) { - for (struct error_line * line = parse_errors; line != NULL; line = line->next) - PopupForm_add_text (popup, __, line->text, false); - } - if (unrecognized_keys != NULL) { - PopupForm_add_text (popup, __, "", false); - PopupForm_add_text (popup, __, "Unrecognized keys:", false); - for (struct error_line * line = unrecognized_keys; line != NULL; line = line->next) - PopupForm_add_text (popup, __, line->text, false); + // Recalculate yields for all cities of this civ + int affected_civ_id = rec->civ_id; + if ((affected_civ_id >= 0) && (p_cities->Cities != NULL)) { + for (int city_index = 0; city_index <= p_cities->LastIndex; city_index++) { + City * target_city = get_city_ptr (city_index); + if ((target_city != NULL) && (target_city->Body.CivID == affected_civ_id)) + recompute_city_yields_with_districts (target_city); } - patch_show_popup (popup, __, 0, 0); - free_error_lines (parse_errors); - free_error_lines (unrecognized_keys); } } void -load_dynamic_district_configs () +refresh_distribution_hubs_for_city (City * city) { - load_dynamic_district_config_file ("default.districts_config.txt", 1, 1, 1); - load_dynamic_district_config_file ("user.districts_config.txt", 1, 0, 1); - - - char * scenario_filename = "scenario.districts_config.txt"; - char * scenario_district_config_path = BIC_get_asset_path (p_bic_data, __, scenario_filename, false); - if ((scenario_district_config_path != NULL) && (0 != strcmp (scenario_filename, scenario_district_config_path))) - load_dynamic_district_config_file (scenario_district_config_path, 0, 0, 1); -} + if (! is->current_config.enable_districts || + ! is->current_config.enable_distribution_hub_districts || + (city == NULL)) + return; -void -init_parsed_wonder_definition (struct parsed_wonder_definition * def) -{ - memset (def, 0, sizeof *def); + FOR_DISTRICTS_AROUND (wai, city->Body.X, city->Body.Y, true) { + int tx = wai.tile_x, ty = wai.tile_y; + Tile * tile = wai.tile; + struct district_instance * inst = wai.district_inst; + if (inst->district_id != DISTRIBUTION_HUB_DISTRICT_ID) + continue; + on_distribution_hub_completed (tile, tx, ty); + } } -void -free_parsed_wonder_definition (struct parsed_wonder_definition * def) +bool +is_space_char (char c) { - if (def->name != NULL) { - free (def->name); - def->name = NULL; - } - - if (def->img_path != NULL) { - free (def->img_path); - def->img_path = NULL; + switch (c) { + case ' ': + case '\t': + case '\n': + case '\r': + case '\f': + case '\v': + return true; + default: + return false; } - - init_parsed_wonder_definition (def); } -bool -add_dynamic_wonder_from_definition (struct parsed_wonder_definition * def, int section_start_line) +enum key_value_parse_status { + KVP_SUCCESS, + KVP_NO_EQUALS, + KVP_EMPTY_KEY +}; + +enum key_value_parse_status +parse_trimmed_key_value (struct string_slice const * trimmed, + struct string_slice * out_key, + struct string_slice * out_value) { - int existing_index = -1; - for (int i = 0; i < is->wonder_district_count; i++) { - if ((is->wonder_district_configs[i].wonder_name != NULL) && - (strcmp (is->wonder_district_configs[i].wonder_name, def->name) == 0)) { - existing_index = i; + if ((trimmed == NULL) || (trimmed->len <= 0)) + return KVP_NO_EQUALS; + + char * equals = NULL; + for (int i = 0; i < trimmed->len; i++) { + if (trimmed->str[i] == '=') { + equals = trimmed->str + i; break; } } + if (equals == NULL) + return KVP_NO_EQUALS; - int dest = (existing_index >= 0) ? existing_index : is->wonder_district_count; - if ((dest < 0) || (dest >= MAX_WONDER_DISTRICT_TYPES)) - return false; - - struct wonder_district_config new_cfg; - memset (&new_cfg, 0, sizeof new_cfg); - new_cfg.index = dest; - new_cfg.is_dynamic = true; - new_cfg.wonder_name = strdup (def->name); - new_cfg.img_path = (def->img_path != NULL) ? strdup (def->img_path) : strdup ("Wonders.pcx"); - new_cfg.img_row = def->img_row; - new_cfg.img_column = def->img_column; - new_cfg.img_construct_row = def->img_construct_row; - new_cfg.img_construct_column = def->img_construct_column; + struct string_slice key = { .str = trimmed->str, .len = (int)(equals - trimmed->str) }; + key = trim_string_slice (&key, 0); + if (key.len == 0) + return KVP_EMPTY_KEY; - if (existing_index >= 0) { - struct wonder_district_config * cfg = &is->wonder_district_configs[existing_index]; - free_dynamic_wonder_config (cfg); - *cfg = new_cfg; - cfg->index = existing_index; - } else { - struct wonder_district_config * cfg = &is->wonder_district_configs[dest]; - free_dynamic_wonder_config (cfg); - *cfg = new_cfg; - is->wonder_district_count += 1; - } + struct string_slice value = { .str = equals + 1, .len = (int)((trimmed->str + trimmed->len) - (equals + 1)) }; + *out_key = key; + *out_value = trim_string_slice (&value, 0); + return KVP_SUCCESS; +} - return true; +void +add_key_parse_error (struct error_line ** parse_errors, + int line_number, + struct string_slice const * key, + struct string_slice const * value, + char const * message_suffix) +{ + struct error_line * err = add_error_line (parse_errors); + char * key_str = extract_slice (key); + char * value_str = (value != NULL) ? extract_slice (value) : NULL; + if (value_str != NULL) + snprintf (err->text, sizeof err->text, "^ Line %d: %s \"%s\" %s", line_number, key_str, value_str, message_suffix); + else + snprintf (err->text, sizeof err->text, "^ Line %d: %s %s", line_number, key_str, message_suffix); + err->text[(sizeof err->text) - 1] = '\0'; + if (value_str != NULL) + free (value_str); + free (key_str); } void -finalize_parsed_wonder_definition (struct parsed_wonder_definition * def, - int section_start_line, - struct error_line ** parse_errors) +add_unrecognized_key_error (struct error_line ** unrecognized_keys, + int line_number, + struct string_slice const * key) { - bool ok = true; + struct error_line * err_line = add_error_line (unrecognized_keys); + char * key_str = extract_slice (key); + snprintf (err_line->text, sizeof err_line->text, "^ Line %d: %s", line_number, key_str); + err_line->text[(sizeof err_line->text) - 1] = '\0'; + free (key_str); +} - if ((! def->has_name) || (def->name == NULL)) { - ok = false; - if (parse_errors != NULL) { - struct error_line * err = add_error_line (parse_errors); - snprintf (err->text, sizeof err->text, "^ Line %d: name (value is required)", section_start_line); - err->text[(sizeof err->text) - 1] = '\0'; - } - } - if (! def->has_img_row) { - ok = false; - if (parse_errors != NULL) { - struct error_line * err = add_error_line (parse_errors); - snprintf (err->text, sizeof err->text, "^ Line %d: img_row (value is required)", section_start_line); - err->text[(sizeof err->text) - 1] = '\0'; - } - } - if (! def->has_img_column) { - ok = false; - if (parse_errors != NULL) { - struct error_line * err = add_error_line (parse_errors); - snprintf (err->text, sizeof err->text, "^ Line %d: img_column (value is required)", section_start_line); - err->text[(sizeof err->text) - 1] = '\0'; - } - } - if (! def->has_img_construct_row) { - ok = false; - if (parse_errors != NULL) { - struct error_line * err = add_error_line (parse_errors); - snprintf (err->text, sizeof err->text, "^ Line %d: img_construct_row (value is required)", section_start_line); - err->text[(sizeof err->text) - 1] = '\0'; +char * +copy_trimmed_string_or_null (struct string_slice const * slice, int remove_quotes) +{ + struct string_slice trimmed = trim_string_slice (slice, remove_quotes); + if (trimmed.len == 0) + return NULL; + return extract_slice (&trimmed); +} + +void +free_bonus_entry_list (struct district_bonus_list * list) +{ + if (list == NULL) + return; + + for (int i = 0; i < list->count; i++) { + if (list->entries[i].type == DBET_BUILDING && + list->entries[i].building_name != NULL) { + free ((void *)list->entries[i].building_name); + list->entries[i].building_name = NULL; } } - if (! def->has_img_construct_column) { - ok = false; - if (parse_errors != NULL) { - struct error_line * err = add_error_line (parse_errors); - snprintf (err->text, sizeof err->text, "^ Line %d: img_construct_column (value is required)", section_start_line); - err->text[(sizeof err->text) - 1] = '\0'; + list->count = 0; +} + +void +free_bonus_entry_list_override (struct district_bonus_list * list, + struct district_bonus_list const * defaults) +{ + if (list == NULL) + return; + + for (int i = 0; i < list->count; i++) { + if (list->entries[i].type == DBET_BUILDING && + list->entries[i].building_name != NULL) { + char const * default_name = NULL; + if ((defaults != NULL) && (i < defaults->count)) + default_name = defaults->entries[i].building_name; + if (list->entries[i].building_name != default_name) + free ((void *)list->entries[i].building_name); + list->entries[i].building_name = NULL; } } + list->count = (defaults != NULL) ? defaults->count : 0; +} - if (ok) - add_dynamic_wonder_from_definition (def, section_start_line); +void +move_bonus_entry_list (struct district_bonus_list * dest, + struct district_bonus_list * src) +{ + if ((dest == NULL) || (src == NULL)) + return; - free_parsed_wonder_definition (def); + dest->count = src->count; + for (int i = 0; i < src->count; i++) { + dest->entries[i] = src->entries[i]; + src->entries[i].building_name = NULL; + } + src->count = 0; } void -handle_wonder_definition_key (struct parsed_wonder_definition * def, - struct string_slice const * key, - struct string_slice const * value, - int line_number, - struct error_line ** parse_errors, - struct error_line ** unrecognized_keys) +free_dynamic_district_config (struct district_config * cfg) { - if (slice_matches_str (key, "name")) { - if (def->name != NULL) { - free (def->name); - def->name = NULL; + if (cfg == NULL) + return; + + if (! cfg->is_dynamic) + return; + + char const * name_ptr = cfg->name; + if (cfg->name != NULL) { + free ((void *)cfg->name); + cfg->name = NULL; + } + if ((cfg->display_name != NULL) && + (cfg->display_name != name_ptr)) { + free ((void *)cfg->display_name); + cfg->display_name = NULL; + } + if (cfg->tooltip != NULL) { + free ((void *)cfg->tooltip); + cfg->tooltip = NULL; + } + for (int i = 0; i < ARRAY_LEN (cfg->advance_prereqs); i++) { + if (cfg->advance_prereqs[i] != NULL) { + free ((void *)cfg->advance_prereqs[i]); + cfg->advance_prereqs[i] = NULL; } + } + cfg->advance_prereq_count = 0; + if (cfg->obsoleted_by != NULL) { + free ((void *)cfg->obsoleted_by); + cfg->obsoleted_by = NULL; + } - struct string_slice unquoted = trim_string_slice (value, 1); - if (unquoted.len == 0) { - def->has_name = false; - add_key_parse_error (parse_errors, line_number, key, "(value is required)"); - } else { - char * name_copy = extract_slice (&unquoted); - if (name_copy == NULL) { - def->has_name = false; - add_key_parse_error (parse_errors, line_number, key, "(out of memory)"); - } else { - def->name = name_copy; - def->has_name = true; - } + for (int i = 0; i < 5; i++) { + if (cfg->resource_prereqs[i] != NULL) { + free ((void *)cfg->resource_prereqs[i]); + cfg->resource_prereqs[i] = NULL; } + } - } else if (slice_matches_str (key, "img_path")) { - if (def->img_path != NULL) { - free (def->img_path); - def->img_path = NULL; + if (cfg->resource_prereq_on_tile != NULL) { + free ((void *)cfg->resource_prereq_on_tile); + cfg->resource_prereq_on_tile = NULL; + } + + for (int i = 0; i < ARRAY_LEN (cfg->wonder_prereqs); i++) { + if (cfg->wonder_prereqs[i] != NULL) { + free ((void *)cfg->wonder_prereqs[i]); + cfg->wonder_prereqs[i] = NULL; } + } + cfg->wonder_prereq_count = 0; - struct string_slice unquoted = trim_string_slice (value, 1); - if (unquoted.len == 0) { - def->has_img_path = false; - } else { - char * path_copy = extract_slice (&unquoted); - if (path_copy == NULL) { - def->has_img_path = false; - } else { - def->img_path = path_copy; - def->has_img_path = true; - } + for (int i = 0; i < ARRAY_LEN (cfg->natural_wonder_prereqs); i++) { + if (cfg->natural_wonder_prereqs[i] != NULL) { + free ((void *)cfg->natural_wonder_prereqs[i]); + cfg->natural_wonder_prereqs[i] = NULL; } + } + cfg->natural_wonder_prereq_count = 0; - } else if (slice_matches_str (key, "img_row")) { - struct string_slice val_slice = *value; - int ival; - if (read_int (&val_slice, &ival)) { - def->img_row = ival; - def->has_img_row = true; - } else { - def->has_img_row = false; - add_key_parse_error (parse_errors, line_number, key, "(expected integer)"); + for (int i = 0; i < ARRAY_LEN (cfg->buildable_on_districts); i++) { + if (cfg->buildable_on_districts[i] != NULL) { + free ((void *)cfg->buildable_on_districts[i]); + cfg->buildable_on_districts[i] = NULL; + } + } + cfg->buildable_on_district_count = 0; + cfg->buildable_on_district_id_count = 0; + cfg->has_buildable_on_districts = false; + for (int i = 0; i < ARRAY_LEN (cfg->buildable_adjacent_to_districts); i++) { + if (cfg->buildable_adjacent_to_districts[i] != NULL) { + free ((void *)cfg->buildable_adjacent_to_districts[i]); + cfg->buildable_adjacent_to_districts[i] = NULL; } + } + cfg->buildable_adjacent_to_district_count = 0; + cfg->buildable_adjacent_to_district_id_count = 0; + cfg->has_buildable_adjacent_to = false; + cfg->has_buildable_adjacent_to_districts = false; - } else if (slice_matches_str (key, "img_column")) { - struct string_slice val_slice = *value; - int ival; - if (read_int (&val_slice, &ival)) { - def->img_column = ival; - def->has_img_column = true; - } else { - def->has_img_column = false; - add_key_parse_error (parse_errors, line_number, key, "(expected integer)"); + for (int i = 0; i < ARRAY_LEN (cfg->buildable_by_civs); i++) { + if (cfg->buildable_by_civs[i] != NULL) { + free ((void *)cfg->buildable_by_civs[i]); + cfg->buildable_by_civs[i] = NULL; } + } + cfg->buildable_by_civ_count = 0; + cfg->has_buildable_by_civs = false; + for (int i = 0; i < ARRAY_LEN (cfg->buildable_by_civ_traits_ids); i++) + cfg->buildable_by_civ_traits_ids[i] = -1; + cfg->buildable_by_civ_traits_id_count = 0; + cfg->has_buildable_by_civ_traits = false; + for (int i = 0; i < ARRAY_LEN (cfg->buildable_by_civ_govs_ids); i++) + cfg->buildable_by_civ_govs_ids[i] = -1; + cfg->buildable_by_civ_govs_id_count = 0; + cfg->has_buildable_by_civ_govs = false; + for (int i = 0; i < ARRAY_LEN (cfg->buildable_by_civ_cultures_ids); i++) + cfg->buildable_by_civ_cultures_ids[i] = -1; + cfg->buildable_by_civ_cultures_id_count = 0; + cfg->has_buildable_by_civ_cultures = false; - } else if (slice_matches_str (key, "img_construct_row")) { - struct string_slice val_slice = *value; - int ival; - if (read_int (&val_slice, &ival)) { - def->img_construct_row = ival; - def->has_img_construct_row = true; - } else { - def->has_img_construct_row = false; - add_key_parse_error (parse_errors, line_number, key, "(expected integer)"); + for (int i = 0; i < 5; i++) { + if (cfg->dependent_improvements[i] != NULL) { + free ((void *)cfg->dependent_improvements[i]); + cfg->dependent_improvements[i] = NULL; } + } - } else if (slice_matches_str (key, "img_construct_column")) { - struct string_slice val_slice = *value; - int ival; - if (read_int (&val_slice, &ival)) { - def->img_construct_column = ival; - def->has_img_construct_column = true; - } else { - def->has_img_construct_column = false; - add_key_parse_error (parse_errors, line_number, key, "(expected integer)"); + for (int i = 0; i < 10; i++) { + if (cfg->img_paths[i] != NULL) { + free ((void *)cfg->img_paths[i]); + cfg->img_paths[i] = NULL; } + } - } else - add_unrecognized_key_error (unrecognized_keys, line_number, key); + free_bonus_entry_list (&cfg->culture_bonus_extras); + free_bonus_entry_list (&cfg->science_bonus_extras); + free_bonus_entry_list (&cfg->food_bonus_extras); + free_bonus_entry_list (&cfg->gold_bonus_extras); + free_bonus_entry_list (&cfg->shield_bonus_extras); + free_bonus_entry_list (&cfg->happiness_bonus_extras); + free_bonus_entry_list (&cfg->defense_bonus_extras); + + memset (cfg, 0, sizeof *cfg); } void -load_dynamic_wonder_config_file (char const * file_path, - int path_is_relative_to_mod_dir, - int log_missing, - int drop_existing_configs) +free_dynamic_wonder_config (struct wonder_district_config * cfg) { - char path[MAX_PATH]; - if (path_is_relative_to_mod_dir) { - if (is->mod_rel_dir == NULL) - return; - snprintf (path, sizeof path, "%s\\%s", is->mod_rel_dir, file_path); - } else { - strncpy (path, file_path, sizeof path); - } - path[(sizeof path) - 1] = '\0'; + if (cfg == NULL) + return; - char * text = file_to_string (path); - if (text == NULL) { - if (log_missing) { - char ss[256]; - snprintf (ss, sizeof ss, "[C3X] Wonders config file not found: %s", path); - (*p_OutputDebugStringA) (ss); - } + if (! cfg->is_dynamic) return; + + if (cfg->wonder_name != NULL) { + free ((void *)cfg->wonder_name); + cfg->wonder_name = NULL; } - if (drop_existing_configs) - reset_wonder_district_configs (); + if (cfg->img_path != NULL) { + free ((void *)cfg->img_path); + cfg->img_path = NULL; + } - struct parsed_wonder_definition def; - init_parsed_wonder_definition (&def); - bool in_section = false; - int section_start_line = 0; - int line_number = 0; - struct error_line * unrecognized_keys = NULL; - struct error_line * parse_errors = NULL; + memset (cfg, 0, sizeof *cfg); +} - char * cursor = text; - while (*cursor != '\0') { - line_number += 1; +void +free_dynamic_natural_wonder_config (struct natural_wonder_district_config * cfg) +{ + if (cfg == NULL) + return; - char * line_start = cursor; - char * line_end = cursor; - while ((*line_end != '\0') && (*line_end != '\n')) - line_end++; + if (! cfg->is_dynamic) + return; - int line_len = line_end - line_start; - bool has_newline = (*line_end == '\n'); - if (has_newline) - *line_end = '\0'; + if (cfg->name != NULL) { + free ((void *)cfg->name); + cfg->name = NULL; + } - struct string_slice line_slice = { .str = line_start, .len = line_len }; - struct string_slice trimmed = trim_string_slice (&line_slice, 0); - if ((trimmed.len == 0) || (trimmed.str[0] == ';')) { - cursor = has_newline ? line_end + 1 : line_end; - continue; - } + if (cfg->img_path != NULL) { + free ((void *)cfg->img_path); + cfg->img_path = NULL; + } - if (trimmed.str[0] == '#') { - struct string_slice directive = trimmed; - directive.str += 1; - directive.len -= 1; - directive = trim_string_slice (&directive, 0); - if ((directive.len > 0) && slice_matches_str (&directive, "Wonder")) { - if (in_section) - finalize_parsed_wonder_definition (&def, section_start_line, &parse_errors); - in_section = true; - section_start_line = line_number; - } - cursor = has_newline ? line_end + 1 : line_end; - continue; - } + memset (cfg, 0, sizeof *cfg); + cfg->adjacent_to = (enum SquareTypes)SQ_INVALID; + cfg->adjacency_dir = DIR_ZERO; +} - if (! in_section) { - cursor = has_newline ? line_end + 1 : line_end; - continue; - } +enum Unit_Command_Values +allocate_dynamic_district_command (char const * name) +{ + int offset = is->next_custom_dynamic_command_index; + is->next_custom_dynamic_command_index += 1; + int value = C3X_DISTRICT_COMMAND_BASE - (offset + 1); + return (enum Unit_Command_Values)value; +} - struct string_slice key_slice = {0}; - struct string_slice value_slice = {0}; - enum key_value_parse_status status = parse_trimmed_key_value (&trimmed, &key_slice, &value_slice); - if (status == KVP_NO_EQUALS) { - char * line_text = extract_slice (&trimmed); - struct error_line * err = add_error_line (&parse_errors); - snprintf (err->text, sizeof err->text, "^ Line %d: %s (expected '=')", line_number, line_text); - err->text[(sizeof err->text) - 1] = '\0'; - free (line_text); - cursor = has_newline ? line_end + 1 : line_end; - continue; - } else if (status == KVP_EMPTY_KEY) { - struct error_line * err = add_error_line (&parse_errors); - snprintf (err->text, sizeof err->text, "^ Line %d: (missing key)", line_number); - err->text[(sizeof err->text) - 1] = '\0'; - cursor = has_newline ? line_end + 1 : line_end; - continue; +void +free_special_district_override_strings (struct district_config * cfg, struct district_config const * defaults) +{ + if (cfg == NULL || defaults == NULL) + return; + + if ((cfg->display_name != NULL) && + (cfg->display_name != cfg->name) && + (cfg->display_name != defaults->display_name)) { + free ((void *)cfg->display_name); + cfg->display_name = NULL; + } + if ((cfg->tooltip != NULL) && (cfg->tooltip != defaults->tooltip)) { + free ((void *)cfg->tooltip); + cfg->tooltip = NULL; + } + for (int i = 0; i < ARRAY_LEN (cfg->advance_prereqs); i++) { + char const * default_value = (defaults != NULL && i < defaults->advance_prereq_count) + ? defaults->advance_prereqs[i] + : NULL; + if ((cfg->advance_prereqs[i] != NULL) && + (cfg->advance_prereqs[i] != default_value)) { + free ((void *)cfg->advance_prereqs[i]); + } + cfg->advance_prereqs[i] = NULL; + } + cfg->advance_prereq_count = 0; + if ((cfg->obsoleted_by != NULL) && (cfg->obsoleted_by != defaults->obsoleted_by)) { + free ((void *)cfg->obsoleted_by); + cfg->obsoleted_by = NULL; + } + + for (int i = 0; i < ARRAY_LEN (cfg->resource_prereqs); i++) { + char const * default_value = (i < defaults->resource_prereq_count) ? defaults->resource_prereqs[i] : NULL; + if ((cfg->resource_prereqs[i] != NULL) && + (cfg->resource_prereqs[i] != default_value)) + free ((void *)cfg->resource_prereqs[i]); + cfg->resource_prereqs[i] = NULL; + } + cfg->resource_prereq_count = defaults->resource_prereq_count; + + if ((cfg->resource_prereq_on_tile != NULL) && (cfg->resource_prereq_on_tile != defaults->resource_prereq_on_tile)) { + free ((void *)cfg->resource_prereq_on_tile); + cfg->resource_prereq_on_tile = NULL; + } + + for (int i = 0; i < ARRAY_LEN (cfg->wonder_prereqs); i++) { + char const * default_value = (i < defaults->wonder_prereq_count) ? defaults->wonder_prereqs[i] : NULL; + if ((cfg->wonder_prereqs[i] != NULL) && + (cfg->wonder_prereqs[i] != default_value)) + free ((void *)cfg->wonder_prereqs[i]); + cfg->wonder_prereqs[i] = NULL; + } + cfg->wonder_prereq_count = defaults->wonder_prereq_count; + + for (int i = 0; i < ARRAY_LEN (cfg->natural_wonder_prereqs); i++) { + char const * default_value = (i < defaults->natural_wonder_prereq_count) ? defaults->natural_wonder_prereqs[i] : NULL; + if ((cfg->natural_wonder_prereqs[i] != NULL) && + (cfg->natural_wonder_prereqs[i] != default_value)) + free ((void *)cfg->natural_wonder_prereqs[i]); + cfg->natural_wonder_prereqs[i] = NULL; + } + cfg->natural_wonder_prereq_count = defaults->natural_wonder_prereq_count; + + for (int i = 0; i < ARRAY_LEN (cfg->buildable_on_districts); i++) { + char const * default_value = (i < defaults->buildable_on_district_count) ? defaults->buildable_on_districts[i] : NULL; + if ((cfg->buildable_on_districts[i] != NULL) && + (cfg->buildable_on_districts[i] != default_value)) { + free ((void *)cfg->buildable_on_districts[i]); + } + cfg->buildable_on_districts[i] = NULL; + } + cfg->buildable_on_district_count = defaults->buildable_on_district_count; + cfg->buildable_on_district_id_count = defaults->buildable_on_district_id_count; + cfg->has_buildable_on_districts = defaults->has_buildable_on_districts; + for (int i = 0; i < ARRAY_LEN (cfg->buildable_adjacent_to_districts); i++) { + char const * default_value = (i < defaults->buildable_adjacent_to_district_count) + ? defaults->buildable_adjacent_to_districts[i] + : NULL; + if ((cfg->buildable_adjacent_to_districts[i] != NULL) && + (cfg->buildable_adjacent_to_districts[i] != default_value)) { + free ((void *)cfg->buildable_adjacent_to_districts[i]); + } + cfg->buildable_adjacent_to_districts[i] = NULL; + } + cfg->buildable_adjacent_to_district_count = defaults->buildable_adjacent_to_district_count; + cfg->buildable_adjacent_to_district_id_count = defaults->buildable_adjacent_to_district_id_count; + cfg->has_buildable_adjacent_to = defaults->has_buildable_adjacent_to; + cfg->has_buildable_adjacent_to_districts = defaults->has_buildable_adjacent_to_districts; + + for (int i = 0; i < ARRAY_LEN (cfg->buildable_by_civs); i++) { + char const * default_value = (i < defaults->buildable_by_civ_count) ? defaults->buildable_by_civs[i] : NULL; + if ((cfg->buildable_by_civs[i] != NULL) && + (cfg->buildable_by_civs[i] != default_value)) { + free ((void *)cfg->buildable_by_civs[i]); + } + cfg->buildable_by_civs[i] = NULL; + } + cfg->buildable_by_civ_count = defaults->buildable_by_civ_count; + cfg->has_buildable_by_civs = defaults->has_buildable_by_civs; + for (int i = 0; i < ARRAY_LEN (cfg->buildable_by_civ_traits_ids); i++) + cfg->buildable_by_civ_traits_ids[i] = -1; + cfg->buildable_by_civ_traits_id_count = defaults->buildable_by_civ_traits_id_count; + cfg->has_buildable_by_civ_traits = defaults->has_buildable_by_civ_traits; + for (int i = 0; i < ARRAY_LEN (cfg->buildable_by_civ_govs_ids); i++) + cfg->buildable_by_civ_govs_ids[i] = -1; + cfg->buildable_by_civ_govs_id_count = defaults->buildable_by_civ_govs_id_count; + cfg->has_buildable_by_civ_govs = defaults->has_buildable_by_civ_govs; + for (int i = 0; i < ARRAY_LEN (cfg->buildable_by_civ_cultures_ids); i++) + cfg->buildable_by_civ_cultures_ids[i] = -1; + cfg->buildable_by_civ_cultures_id_count = defaults->buildable_by_civ_cultures_id_count; + cfg->has_buildable_by_civ_cultures = defaults->has_buildable_by_civ_cultures; + cfg->buildable_by_war_allies = defaults->buildable_by_war_allies; + cfg->buildable_by_pact_allies = defaults->buildable_by_pact_allies; + + for (int i = 0; i < ARRAY_LEN (cfg->dependent_improvements); i++) { + char const * default_value = (i < defaults->dependent_improvement_max_index) ? defaults->dependent_improvements[i] : NULL; + if ((cfg->dependent_improvements[i] != NULL) && + (cfg->dependent_improvements[i] != default_value)) { + free ((void *)cfg->dependent_improvements[i]); } + cfg->dependent_improvements[i] = NULL; + } + cfg->dependent_improvement_max_index = defaults->dependent_improvement_max_index; - handle_wonder_definition_key (&def, &key_slice, &value_slice, line_number, &parse_errors, &unrecognized_keys); - cursor = has_newline ? line_end + 1 : line_end; + for (int i = 0; i < ARRAY_LEN (cfg->img_paths); i++) { + char const * default_value = (i < defaults->img_path_count) ? defaults->img_paths[i] : NULL; + if ((cfg->img_paths[i] != NULL) && + (cfg->img_paths[i] != default_value)) { + free ((void *)cfg->img_paths[i]); + } + cfg->img_paths[i] = NULL; } + cfg->img_path_count = defaults->img_path_count; - if (in_section) - finalize_parsed_wonder_definition (&def, section_start_line, &parse_errors); + free_bonus_entry_list_override (&cfg->culture_bonus_extras, &defaults->culture_bonus_extras); + free_bonus_entry_list_override (&cfg->science_bonus_extras, &defaults->science_bonus_extras); + free_bonus_entry_list_override (&cfg->food_bonus_extras, &defaults->food_bonus_extras); + free_bonus_entry_list_override (&cfg->gold_bonus_extras, &defaults->gold_bonus_extras); + free_bonus_entry_list_override (&cfg->shield_bonus_extras, &defaults->shield_bonus_extras); + free_bonus_entry_list_override (&cfg->happiness_bonus_extras, &defaults->happiness_bonus_extras); + free_bonus_entry_list_override (&cfg->defense_bonus_extras, &defaults->defense_bonus_extras); +} - free_parsed_wonder_definition (&def); - free (text); +void +reset_regular_district_configs (void) +{ + for (int i = USED_SPECIAL_DISTRICT_TYPES; i < COUNT_DISTRICT_TYPES; i++) { + if (is->district_configs[i].is_dynamic) + free_dynamic_district_config (&is->district_configs[i]); + } - // Append to loaded config names list - struct loaded_config_name * top_lcn = is->loaded_config_names; - while (top_lcn->next != NULL) - top_lcn = top_lcn->next; + for (int i = 0; i < USED_SPECIAL_DISTRICT_TYPES; i++) + free_special_district_override_strings (&is->district_configs[i], &special_district_defaults[i]); - struct loaded_config_name * new_lcn = malloc (sizeof *new_lcn); - new_lcn->name = strdup (path); - new_lcn->next = NULL; + memset (is->district_configs, 0, sizeof is->district_configs); + for (int i = 0; i < USED_SPECIAL_DISTRICT_TYPES; i++) + is->district_configs[i] = special_district_defaults[i]; - top_lcn->next = new_lcn; + is->special_district_count = USED_SPECIAL_DISTRICT_TYPES; + is->dynamic_district_count = 0; + is->district_count = is->special_district_count; + is->next_custom_dynamic_command_index = 0; +} - if (parse_errors != NULL || unrecognized_keys != NULL) { - PopupForm * popup = get_popup_form (); - popup->vtable->set_text_key_and_flags (popup, __, is->mod_script_path, "C3X_WARNING", -1, 0, 0, 0); - char s[200]; - snprintf (s, sizeof s, "Wonder District Config errors in %s:", path); - s[(sizeof s) - 1] = '\0'; - PopupForm_add_text (popup, __, s, false); - if (parse_errors != NULL) { - for (struct error_line * line = parse_errors; line != NULL; line = line->next) - PopupForm_add_text (popup, __, line->text, false); - } - if (unrecognized_keys != NULL) { - PopupForm_add_text (popup, __, "", false); - PopupForm_add_text (popup, __, "Unrecognized keys:", false); - for (struct error_line * line = unrecognized_keys; line != NULL; line = line->next) - PopupForm_add_text (popup, __, line->text, false); - } - patch_show_popup (popup, __, 0, 0); - free_error_lines (parse_errors); - free_error_lines (unrecognized_keys); +void +reset_wonder_district_configs (void) +{ + for (int i = 0; i < MAX_WONDER_DISTRICT_TYPES; i++) { + if (is->wonder_district_configs[i].is_dynamic) + free_dynamic_wonder_config (&is->wonder_district_configs[i]); } + + memset (is->wonder_district_configs, 0, sizeof is->wonder_district_configs); + is->wonder_district_count = 0; } void -load_dynamic_wonder_configs () +reset_natural_wonder_configs (void) { - load_dynamic_wonder_config_file ("default.districts_wonders_config.txt", 1, 1, 1); - load_dynamic_wonder_config_file ("user.districts_wonders_config.txt", 1, 0, 1); + for (int i = 0; i < MAX_NATURAL_WONDER_DISTRICT_TYPES; i++) { + if (is->natural_wonder_configs[i].is_dynamic) + free_dynamic_natural_wonder_config (&is->natural_wonder_configs[i]); + } - char * scenario_filename = "scenario.districts_wonders_config.txt"; - char * scenario_wonder_config_path = BIC_get_asset_path (p_bic_data, __, scenario_filename, false); - if ((scenario_wonder_config_path != NULL) && (0 != strcmp (scenario_filename, scenario_wonder_config_path))) - load_dynamic_wonder_config_file (scenario_wonder_config_path, 0, 0, 1); + memset (is->natural_wonder_configs, 0, sizeof is->natural_wonder_configs); + for (int i = 0; i < MAX_NATURAL_WONDER_DISTRICT_TYPES; i++) { + is->natural_wonder_configs[i].adjacent_to = (enum SquareTypes)SQ_INVALID; + is->natural_wonder_configs[i].adjacency_dir = DIR_ZERO; + } + for (int i = 0; i < MAX_NATURAL_WONDER_DISTRICT_TYPES; i++) + is->natural_wonder_img_sets[i].img.vtable = NULL; + stable_deinit (&is->natural_wonder_name_to_id); + is->natural_wonder_count = 0; } void -init_parsed_natural_wonder_definition (struct parsed_natural_wonder_definition * def) +clear_dynamic_district_definitions (void) +{ + reset_regular_district_configs (); + reset_wonder_district_configs (); + reset_natural_wonder_configs (); + reset_ai_candidate_bridge_or_canals (); +} + +void +init_parsed_district_definition (struct parsed_district_definition * def) { memset (def, 0, sizeof *def); - def->terrain_type = SQ_Grassland; - def->adjacent_to = (enum SquareTypes)SQ_INVALID; - def->adjacency_dir = DIR_ZERO; + def->img_path_count = -1; + def->defense_bonus_percent = 0; + def->render_strategy = DRS_BY_COUNT; + def->ai_build_strategy = DABS_DISTRICT; + def->buildable_square_types_mask = district_default_buildable_mask (); + def->buildable_adjacent_to_square_types_mask = 0; + def->buildable_on_overlays_mask = 0; + def->buildable_only_on_overlays_mask = 0; + def->buildable_adjacent_to_overlays_mask = 0; + def->buildable_adjacent_to_allows_city = false; } void -free_parsed_natural_wonder_definition (struct parsed_natural_wonder_definition * def) +free_parsed_district_definition (struct parsed_district_definition * def) { + if (def == NULL) + return; + + if (def->display_name != NULL) { + free (def->display_name); + def->display_name = NULL; + } if (def->name != NULL) { free (def->name); def->name = NULL; } - if (def->img_path != NULL) { - free (def->img_path); - def->img_path = NULL; + if (def->tooltip != NULL) { + free (def->tooltip); + def->tooltip = NULL; + } + for (int i = 0; i < def->advance_prereq_count; i++) { + if (def->advance_prereqs[i] != NULL) { + free (def->advance_prereqs[i]); + def->advance_prereqs[i] = NULL; + } + } + def->advance_prereq_count = 0; + for (int i = 0; i < ARRAY_LEN (def->buildable_on_districts); i++) { + if (def->buildable_on_districts[i] != NULL) { + free (def->buildable_on_districts[i]); + def->buildable_on_districts[i] = NULL; + } + } + for (int i = 0; i < ARRAY_LEN (def->buildable_adjacent_to_districts); i++) { + if (def->buildable_adjacent_to_districts[i] != NULL) { + free (def->buildable_adjacent_to_districts[i]); + def->buildable_adjacent_to_districts[i] = NULL; + } + } + if (def->obsoleted_by != NULL) { + free (def->obsoleted_by); + def->obsoleted_by = NULL; } - init_parsed_natural_wonder_definition (def); -} - -bool -add_natural_wonder_from_definition (struct parsed_natural_wonder_definition * def, int section_start_line) -{ - if ((def == NULL) || (def->name == NULL)) - return false; - - int existing_index; - bool has_existing = stable_look_up (&is->natural_wonder_name_to_id, def->name, &existing_index); - int dest = has_existing ? existing_index : is->natural_wonder_count; - if ((dest < 0) || (dest >= MAX_NATURAL_WONDER_DISTRICT_TYPES)) - return false; + for (int i = 0; i < def->resource_prereq_count; i++) { + if (def->resource_prereqs[i] != NULL) { + free (def->resource_prereqs[i]); + def->resource_prereqs[i] = NULL; + } + } + def->resource_prereq_count = 0; - struct natural_wonder_district_config new_cfg; - memset (&new_cfg, 0, sizeof new_cfg); - new_cfg.index = dest; - new_cfg.is_dynamic = true; - new_cfg.adjacent_to = (enum SquareTypes)SQ_INVALID; - new_cfg.adjacency_dir = DIR_ZERO; + if (def->resource_prereq_on_tile != NULL) { + free (def->resource_prereq_on_tile); + def->resource_prereq_on_tile = NULL; + } - char * name_copy = strdup (def->name); - if (name_copy == NULL) - return false; - new_cfg.name = name_copy; + for (int i = 0; i < def->wonder_prereq_count; i++) { + if (def->wonder_prereqs[i] != NULL) { + free (def->wonder_prereqs[i]); + def->wonder_prereqs[i] = NULL; + } + } + def->wonder_prereq_count = 0; - char const * img_path_src = def->img_path; - char * img_copy = strdup (img_path_src); - if (img_copy == NULL) { - free (name_copy); - return false; + for (int i = 0; i < def->natural_wonder_prereq_count; i++) { + if (def->natural_wonder_prereqs[i] != NULL) { + free (def->natural_wonder_prereqs[i]); + def->natural_wonder_prereqs[i] = NULL; + } } - new_cfg.img_path = img_copy; - new_cfg.img_row = def->img_row; - new_cfg.img_column = def->img_column; - new_cfg.terrain_type = def->terrain_type; - new_cfg.adjacent_to = def->adjacent_to; - new_cfg.adjacency_dir = def->adjacency_dir; - new_cfg.culture_bonus = def->has_culture_bonus ? def->culture_bonus : 0; - new_cfg.science_bonus = def->has_science_bonus ? def->science_bonus : 0; - new_cfg.food_bonus = def->has_food_bonus ? def->food_bonus : 0; - new_cfg.gold_bonus = def->has_gold_bonus ? def->gold_bonus : 0; - new_cfg.shield_bonus = def->has_shield_bonus ? def->shield_bonus : 0; + def->natural_wonder_prereq_count = 0; + def->buildable_adjacent_to_district_count = 0; - if (has_existing) { - struct natural_wonder_district_config * cfg = &is->natural_wonder_configs[existing_index]; - free_dynamic_natural_wonder_config (cfg); - *cfg = new_cfg; - } else { - struct natural_wonder_district_config * cfg = &is->natural_wonder_configs[dest]; - free_dynamic_natural_wonder_config (cfg); - *cfg = new_cfg; - is->natural_wonder_count = dest + 1; - stable_insert (&is->natural_wonder_name_to_id, new_cfg.name, dest); + for (int i = 0; i < def->buildable_by_civ_count; i++) { + if (def->buildable_by_civs[i] != NULL) { + free (def->buildable_by_civs[i]); + def->buildable_by_civs[i] = NULL; + } } - - return true; -} - -void -finalize_parsed_natural_wonder_definition (struct parsed_natural_wonder_definition * def, - int section_start_line, - struct error_line ** parse_errors) -{ - bool ok = true; - - if ((! def->has_name) || (def->name == NULL)) { - ok = false; - if (parse_errors != NULL) { - struct error_line * err = add_error_line (parse_errors); - snprintf (err->text, sizeof err->text, "^ Line %d: name (value is required)", section_start_line); - err->text[(sizeof err->text) - 1] = '\0'; + def->buildable_by_civ_count = 0; + for (int i = 0; i < def->buildable_by_civ_traits_count; i++) { + if (def->buildable_by_civ_traits[i] != NULL) { + free (def->buildable_by_civ_traits[i]); + def->buildable_by_civ_traits[i] = NULL; } } - if (! def->has_img_row) { - ok = false; - if (parse_errors != NULL) { - struct error_line * err = add_error_line (parse_errors); - snprintf (err->text, sizeof err->text, "^ Line %d: img_row (value is required)", section_start_line); - err->text[(sizeof err->text) - 1] = '\0'; + def->buildable_by_civ_traits_count = 0; + def->buildable_by_civ_traits_id_count = 0; + for (int i = 0; i < def->buildable_by_civ_govs_count; i++) { + if (def->buildable_by_civ_govs[i] != NULL) { + free (def->buildable_by_civ_govs[i]); + def->buildable_by_civ_govs[i] = NULL; } } - if (! def->has_img_column) { - ok = false; - if (parse_errors != NULL) { - struct error_line * err = add_error_line (parse_errors); - snprintf (err->text, sizeof err->text, "^ Line %d: img_column (value is required)", section_start_line); - err->text[(sizeof err->text) - 1] = '\0'; + def->buildable_by_civ_govs_count = 0; + def->buildable_by_civ_govs_id_count = 0; + for (int i = 0; i < def->buildable_by_civ_cultures_count; i++) { + if (def->buildable_by_civ_cultures[i] != NULL) { + free (def->buildable_by_civ_cultures[i]); + def->buildable_by_civ_cultures[i] = NULL; } } - if (! def->has_terrain_type) { - ok = false; - if (parse_errors != NULL) { - struct error_line * err = add_error_line (parse_errors); - snprintf (err->text, sizeof err->text, "^ Line %d: terrain_type (value is required)", section_start_line); - err->text[(sizeof err->text) - 1] = '\0'; + def->buildable_by_civ_cultures_count = 0; + def->buildable_by_civ_cultures_id_count = 0; + + for (int i = 0; i < def->dependent_improvement_max_index; i++) { + if (def->dependent_improvements[i] != NULL) { + free (def->dependent_improvements[i]); + def->dependent_improvements[i] = NULL; } } + def->dependent_improvement_max_index = 0; - if (ok) - add_natural_wonder_from_definition (def, section_start_line); + for (int i = 0; i < def->img_path_count; i++) { + if (def->img_paths[i] != NULL) { + free (def->img_paths[i]); + def->img_paths[i] = NULL; + } + } + def->img_path_count = 0; - free_parsed_natural_wonder_definition (def); + if (def->generated_resource != NULL) { + free (def->generated_resource); + def->generated_resource = NULL; + } + + free_bonus_entry_list (&def->culture_bonus_extras); + free_bonus_entry_list (&def->science_bonus_extras); + free_bonus_entry_list (&def->food_bonus_extras); + free_bonus_entry_list (&def->gold_bonus_extras); + free_bonus_entry_list (&def->shield_bonus_extras); + free_bonus_entry_list (&def->happiness_bonus_extras); + free_bonus_entry_list (&def->defense_bonus_extras); + + init_parsed_district_definition (def); } -void -handle_natural_wonder_definition_key (struct parsed_natural_wonder_definition * def, - struct string_slice const * key, - struct string_slice const * value, - int line_number, - struct error_line ** parse_errors, - struct error_line ** unrecognized_keys) +int +find_special_district_index_by_name (char const * name) { - if (slice_matches_str (key, "name")) { - if (def->name != NULL) { - free (def->name); - def->name = NULL; - } + if (name == NULL) + return -1; - struct string_slice unquoted = trim_string_slice (value, 1); - if (unquoted.len == 0) { - def->has_name = false; - add_key_parse_error (parse_errors, line_number, key, "(value is required)"); - } else { - char * name_copy = extract_slice (&unquoted); - if (name_copy == NULL) { - def->has_name = false; - add_key_parse_error (parse_errors, line_number, key, "(out of memory)"); - } else { - def->name = name_copy; - def->has_name = true; - } - } + for (int i = 0; i < is->special_district_count; i++) { + if ((is->district_configs[i].name != NULL) && + (strcmp (is->district_configs[i].name, name) == 0)) + return i; + } + return -1; +} - } else if (slice_matches_str (key, "terrain_type")) { - enum SquareTypes terrain; - if (read_natural_wonder_terrain_type (value, &terrain)) { - def->terrain_type = terrain; - def->has_terrain_type = true; - } else { - def->has_terrain_type = false; - add_key_parse_error (parse_errors, line_number, key, "(unrecognized terrain type)"); - } +bool +ensure_culture_variant_art (struct district_config * cfg, int section_start_line) +{ + if ((cfg == NULL) || (! cfg->vary_img_by_culture)) + return true; - } else if (slice_matches_str (key, "adjacent_to")) { - enum SquareTypes adj; - if (read_square_type_value (value, &adj)) { - def->adjacent_to = adj; - def->has_adjacent_to = true; - } else { - def->adjacent_to = (enum SquareTypes)SQ_INVALID; - def->has_adjacent_to = false; - add_key_parse_error (parse_errors, line_number, key, "(unrecognized square type)"); - } + const int required_variants = 5; + const int max_img_paths = ARRAY_LEN (is->district_configs[0].img_paths); + if (cfg->img_path_count <= 0) { + char ss[256]; + snprintf (ss, sizeof ss, "[C3X] load_dynamic_district_configs: district \"%s\" requires culture-specific art but none provided (line %d)\n", cfg->name, section_start_line); + (*p_OutputDebugStringA) (ss); + return false; + } - } else if (slice_matches_str (key, "adjacency_dir")) { - enum direction dir; - if (read_direction_value (value, &dir)) { - def->adjacency_dir = dir; - def->has_adjacency_dir = true; - } else { - def->adjacency_dir = DIR_ZERO; - def->has_adjacency_dir = false; - add_key_parse_error (parse_errors, line_number, key, "(unrecognized direction)"); - } + while ((cfg->img_path_count < required_variants) && + (cfg->img_path_count < max_img_paths)) { + cfg->img_paths[cfg->img_path_count] = strdup (cfg->img_paths[0]); + cfg->img_path_count += 1; + } - } else if (slice_matches_str (key, "img_path")) { - if (def->img_path != NULL) { - free (def->img_path); - def->img_path = NULL; - } + return true; +} - struct string_slice unquoted = trim_string_slice (value, 1); - if (unquoted.len == 0) { - def->has_img_path = false; - } else { - char * path_copy = extract_slice (&unquoted); - if (path_copy == NULL) { - def->has_img_path = false; - } else { - def->img_path = path_copy; - def->has_img_path = true; - } +bool +parse_config_string_list (char * value_text, + char ** dest, + int capacity, + int * out_count, + struct error_line ** parse_errors, + int line_number, + char const * key) +{ + for (int i = 0; i < capacity; i++) { + if (dest[i] != NULL) { + free (dest[i]); + dest[i] = NULL; } + } + *out_count = 0; - } else if (slice_matches_str (key, "img_row")) { - struct string_slice val_slice = *value; - int ival; - if (read_int (&val_slice, &ival)) { - def->img_row = ival; - def->has_img_row = true; - } else { - def->has_img_row = false; - add_key_parse_error (parse_errors, line_number, key, "(expected integer)"); - } + if (value_text == NULL || *value_text == '\0') + return true; - } else if (slice_matches_str (key, "img_column")) { - struct string_slice val_slice = *value; - int ival; - if (read_int (&val_slice, &ival)) { - def->img_column = ival; - def->has_img_column = true; - } else { - def->has_img_column = false; - add_key_parse_error (parse_errors, line_number, key, "(expected integer)"); - } + char * cursor = value_text; + while (1) { + while (is_space_char (*cursor)) + cursor++; - } else if (slice_matches_str (key, "culture_bonus")) { - struct string_slice val_slice = *value; - int ival; - if (read_int (&val_slice, &ival)) { - def->culture_bonus = ival; - def->has_culture_bonus = true; - } else { - def->has_culture_bonus = false; - add_key_parse_error (parse_errors, line_number, key, "(expected integer)"); - } + if (*cursor == '\0') + break; - } else if (slice_matches_str (key, "science_bonus")) { - struct string_slice val_slice = *value; - int ival; - if (read_int (&val_slice, &ival)) { - def->science_bonus = ival; - def->has_science_bonus = true; + char * item_start; + char * item_end; + if (*cursor == '"') { + cursor++; + item_start = cursor; + while ((*cursor != '\0') && (*cursor != '"')) + cursor++; + if (*cursor != '"') { + struct error_line * err = add_error_line (parse_errors); + snprintf (err->text, sizeof err->text, "^ Line %d: %s (missing closing quote)", line_number, key); + err->text[(sizeof err->text) - 1] = '\0'; + for (int j = 0; j < capacity; j++) { + if (dest[j] != NULL) { + free (dest[j]); + dest[j] = NULL; + } + } + *out_count = 0; + return false; + } + item_end = cursor; + cursor++; } else { - def->has_science_bonus = false; - add_key_parse_error (parse_errors, line_number, key, "(expected integer)"); + item_start = cursor; + while ((*cursor != '\0') && (*cursor != ',')) + cursor++; + item_end = cursor; } - } else if (slice_matches_str (key, "food_bonus")) { - struct string_slice val_slice = *value; - int ival; - if (read_int (&val_slice, &ival)) { - def->food_bonus = ival; - def->has_food_bonus = true; - } else { - def->has_food_bonus = false; - add_key_parse_error (parse_errors, line_number, key, "(expected integer)"); - } - - } else if (slice_matches_str (key, "gold_bonus")) { - struct string_slice val_slice = *value; - int ival; - if (read_int (&val_slice, &ival)) { - def->gold_bonus = ival; - def->has_gold_bonus = true; - } else { - def->has_gold_bonus = false; - add_key_parse_error (parse_errors, line_number, key, "(expected integer)"); - } + while ((item_end > item_start) && is_space_char (item_end[-1])) + item_end--; - } else if (slice_matches_str (key, "shield_bonus")) { - struct string_slice val_slice = *value; - int ival; - if (read_int (&val_slice, &ival)) { - def->shield_bonus = ival; - def->has_shield_bonus = true; - } else { - def->has_shield_bonus = false; - add_key_parse_error (parse_errors, line_number, key, "(expected integer)"); + int item_len = item_end - item_start; + if (item_len > 0) { + if (*out_count < capacity) { + char * copy = malloc (item_len + 1); + if (copy == NULL) { + struct error_line * err = add_error_line (parse_errors); + snprintf (err->text, sizeof err->text, "^ Line %d: %s (out of memory)", line_number, key); + err->text[(sizeof err->text) - 1] = '\0'; + for (int j = 0; j < capacity; j++) { + if (dest[j] != NULL) { + free (dest[j]); + dest[j] = NULL; + } + } + *out_count = 0; + return false; + } + memcpy (copy, item_start, item_len); + copy[item_len] = '\0'; + dest[*out_count] = copy; + *out_count += 1; + } } - } else - add_unrecognized_key_error (unrecognized_keys, line_number, key); -} - -void -load_natural_wonder_config_file (char const * file_path, - int path_is_relative_to_mod_dir, - int log_missing, - int drop_existing_configs) -{ - char path[MAX_PATH]; - if (path_is_relative_to_mod_dir) { - if (is->mod_rel_dir == NULL) - return; - snprintf (path, sizeof path, "%s\\%s", is->mod_rel_dir, file_path); - } else { - strncpy (path, file_path, sizeof path); - } - path[(sizeof path) - 1] = '\0'; + while (is_space_char (*cursor)) + cursor++; - char * text = file_to_string (path); - if (text == NULL) { - if (log_missing) { - char ss[256]; - snprintf (ss, sizeof ss, "[C3X] Natural wonders config file not found: %s", path); - (*p_OutputDebugStringA) (ss); + if (*cursor == ',') { + cursor++; + continue; } - return; + if (*cursor == '\0') + break; } - if (drop_existing_configs) - reset_natural_wonder_configs (); - - struct parsed_natural_wonder_definition def; - init_parsed_natural_wonder_definition (&def); - bool in_section = false; - int section_start_line = 0; - int line_number = 0; - struct error_line * unrecognized_keys = NULL; - struct error_line * parse_errors = NULL; - - char * cursor = text; - while (*cursor != '\0') { - line_number += 1; - - char * line_start = cursor; - char * line_end = cursor; - while ((*line_end != '\0') && (*line_end != '\n')) - line_end++; + return true; +} - int line_len = line_end - line_start; - bool has_newline = (*line_end == '\n'); - if (has_newline) - *line_end = '\0'; +bool +parse_buildable_square_type_mask (struct string_slice const * value, + unsigned int * out_mask, + struct error_line ** parse_errors, + int line_number, + char const * key_name, + bool * out_allow_city) +{ + char * value_text = trim_and_extract_slice (value, 0); + unsigned int mask = 0; + int entry_count = 0; + bool allow_city = false; + bool allow_city_token = (key_name != NULL) && (strcmp (key_name, "buildable_adjacent_to") == 0); + if (key_name == NULL) + key_name = "buildable_on"; + + if (value_text != NULL) { + char * cursor = value_text; + while (1) { + while (is_space_char (*cursor)) + cursor++; - struct string_slice line_slice = { .str = line_start, .len = line_len }; - struct string_slice trimmed = trim_string_slice (&line_slice, 0); - if ((trimmed.len == 0) || (trimmed.str[0] == ';')) { - cursor = has_newline ? line_end + 1 : line_end; - continue; - } + char * item_start = cursor; + while ((*cursor != '\0') && (*cursor != ',')) + cursor++; - if (trimmed.str[0] == '#') { - struct string_slice directive = trimmed; - directive.str += 1; - directive.len -= 1; - directive = trim_string_slice (&directive, 0); - if ((directive.len > 0) && slice_matches_str (&directive, "Wonder")) { - if (in_section) - finalize_parsed_natural_wonder_definition (&def, section_start_line, &parse_errors); - in_section = true; - section_start_line = line_number; + char * item_end = cursor; + while ((item_end > item_start) && is_space_char (item_end[-1])) + item_end--; + + struct string_slice item_slice = { .str = item_start, .len = (int)(item_end - item_start) }; + if (item_slice.len > 0) { + if (slice_matches_str (&item_slice, "city")) { + if (! allow_city_token) { + struct error_line * err = add_error_line (parse_errors); + snprintf (err->text, sizeof err->text, "^ Line %d: %.*s (invalid %s entry)", line_number, item_slice.len, item_slice.str, key_name); + err->text[(sizeof err->text) - 1] = '\0'; + free (value_text); + return false; + } + allow_city = true; + entry_count += 1; + } else if (slice_matches_str (&item_slice, "lake")) { + mask |= district_buildable_lake_mask_bit (); + entry_count += 1; + } else { + enum SquareTypes parsed; + if (read_square_type_value (&item_slice, &parsed)) { + if ((parsed == SQ_RIVER) || + (parsed == SQ_Forest) || + (parsed == SQ_Jungle) || + (parsed == SQ_Swamp)) { + struct error_line * err = add_error_line (parse_errors); + snprintf (err->text, sizeof err->text, "^ Line %d: %.*s (invalid %s entry)", line_number, item_slice.len, item_slice.str, key_name); + err->text[(sizeof err->text) - 1] = '\0'; + free (value_text); + return false; + } + if (parsed == (enum SquareTypes)SQ_INVALID) { + mask = all_square_types_mask (); + mask &= ~(square_type_mask_bit (SQ_Forest) | + square_type_mask_bit (SQ_Jungle) | + square_type_mask_bit (SQ_Swamp)); + } else + mask |= square_type_mask_bit (parsed); + entry_count += 1; + } else { + struct error_line * err = add_error_line (parse_errors); + snprintf (err->text, sizeof err->text, "^ Line %d: %.*s (invalid %s entry)", line_number, item_slice.len, item_slice.str, key_name); + err->text[(sizeof err->text) - 1] = '\0'; + free (value_text); + return false; + } + } } - cursor = has_newline ? line_end + 1 : line_end; - continue; - } - if (! in_section) { - cursor = has_newline ? line_end + 1 : line_end; - continue; + if (*cursor == ',') { + cursor++; + continue; + } + break; } + } - struct string_slice key_slice = {0}; - struct string_slice value_slice = {0}; - enum key_value_parse_status status = parse_trimmed_key_value (&trimmed, &key_slice, &value_slice); - if (status == KVP_NO_EQUALS) { - char * line_text = extract_slice (&trimmed); - struct error_line * err = add_error_line (&parse_errors); - snprintf (err->text, sizeof err->text, "^ Line %d: %s (expected '=')", line_number, line_text); - err->text[(sizeof err->text) - 1] = '\0'; - free (line_text); - cursor = has_newline ? line_end + 1 : line_end; - continue; - } else if (status == KVP_EMPTY_KEY) { - struct error_line * err = add_error_line (&parse_errors); - snprintf (err->text, sizeof err->text, "^ Line %d: (missing key)", line_number); - err->text[(sizeof err->text) - 1] = '\0'; - cursor = has_newline ? line_end + 1 : line_end; - continue; - } + if (value_text != NULL) + free (value_text); - handle_natural_wonder_definition_key (&def, &key_slice, &value_slice, line_number, &parse_errors, &unrecognized_keys); - cursor = has_newline ? line_end + 1 : line_end; + if ((entry_count == 0) || ((mask == 0) && ! allow_city)) { + struct error_line * err = add_error_line (parse_errors); + snprintf (err->text, sizeof err->text, "^ Line %d: %s (expected at least one square type)", line_number, key_name); + err->text[(sizeof err->text) - 1] = '\0'; + return false; } - if (in_section) - finalize_parsed_natural_wonder_definition (&def, section_start_line, &parse_errors); + *out_mask = mask; + if (out_allow_city != NULL) + *out_allow_city = allow_city; + return true; +} - free_parsed_natural_wonder_definition (&def); - free (text); +bool +parse_buildable_overlay_mask (struct string_slice const * value, + unsigned int * out_mask, + struct error_line ** parse_errors, + int line_number, + char const * key_name) +{ + char * value_text = trim_and_extract_slice (value, 0); + unsigned int mask = 0; + unsigned int allowed_mask = (DOM_MINE | DOM_IRRIGATION | DOM_FORTRESS | DOM_BARRICADE | + DOM_OUTPOST | DOM_RADAR_TOWER | DOM_JUNGLE | DOM_FOREST | + DOM_SWAMP | DOM_RIVER | DOM_AIRFIELD); + int entry_count = 0; + + if (key_name == NULL) + key_name = "buildable_on_overlays"; + if (strcmp (key_name, "buildable_on_overlays") == 0) + allowed_mask = (DOM_JUNGLE | DOM_FOREST | DOM_SWAMP); + + if (value_text != NULL) { + char * cursor = value_text; + while (1) { + while (is_space_char (*cursor)) + cursor++; - // Append to loaded config names list - struct loaded_config_name * top_lcn = is->loaded_config_names; - while (top_lcn->next != NULL) - top_lcn = top_lcn->next; + char * item_start = cursor; + while ((*cursor != '\0') && (*cursor != ',')) + cursor++; - struct loaded_config_name * new_lcn = malloc (sizeof *new_lcn); - new_lcn->name = strdup (path); - new_lcn->next = NULL; + char * item_end = cursor; + while ((item_end > item_start) && is_space_char (item_end[-1])) + item_end--; + + struct string_slice item_slice = { .str = item_start, .len = (int)(item_end - item_start) }; + if (item_slice.len > 0) { + unsigned int bit = 0; + if (slice_matches_str (&item_slice, "mine")) { + bit = DOM_MINE; + } else if (slice_matches_str (&item_slice, "irrigation")) { + bit = DOM_IRRIGATION; + } else if (slice_matches_str (&item_slice, "fortress")) { + bit = DOM_FORTRESS; + } else if (slice_matches_str (&item_slice, "barricade")) { + bit = DOM_BARRICADE; + } else if (slice_matches_str (&item_slice, "outpost")) { + bit = DOM_OUTPOST; + } else if (slice_matches_str (&item_slice, "radar-tower")) { + bit = DOM_RADAR_TOWER; + } else if (slice_matches_str (&item_slice, "airfield")) { + bit = DOM_AIRFIELD; + } else if (slice_matches_str (&item_slice, "jungle")) { + bit = DOM_JUNGLE; + } else if (slice_matches_str (&item_slice, "forest")) { + bit = DOM_FOREST; + } else if (slice_matches_str (&item_slice, "swamp")) { + bit = DOM_SWAMP; + } else if (slice_matches_str (&item_slice, "river")) { + bit = DOM_RIVER; + } - top_lcn->next = new_lcn; + if (bit != 0) { + if ((allowed_mask & bit) == 0) { + struct error_line * err = add_error_line (parse_errors); + snprintf (err->text, sizeof err->text, "^ Line %d: %.*s (invalid %s entry)", line_number, item_slice.len, item_slice.str, key_name); + err->text[(sizeof err->text) - 1] = '\0'; + free (value_text); + return false; + } + mask |= bit; + entry_count += 1; + } else { + struct error_line * err = add_error_line (parse_errors); + snprintf (err->text, sizeof err->text, "^ Line %d: %.*s (invalid %s entry)", line_number, item_slice.len, item_slice.str, key_name); + err->text[(sizeof err->text) - 1] = '\0'; + free (value_text); + return false; + } + } - if (parse_errors != NULL || unrecognized_keys != NULL) { - PopupForm * popup = get_popup_form (); - popup->vtable->set_text_key_and_flags (popup, __, is->mod_script_path, "C3X_WARNING", -1, 0, 0, 0); - char s[200]; - snprintf (s, sizeof s, "Natural Wonder Config errors in %s:", path); - s[(sizeof s) - 1] = '\0'; - PopupForm_add_text (popup, __, s, false); - if (parse_errors != NULL) { - for (struct error_line * line = parse_errors; line != NULL; line = line->next) - PopupForm_add_text (popup, __, line->text, false); - } - if (unrecognized_keys != NULL) { - PopupForm_add_text (popup, __, "", false); - PopupForm_add_text (popup, __, "Unrecognized keys:", false); - for (struct error_line * line = unrecognized_keys; line != NULL; line = line->next) - PopupForm_add_text (popup, __, line->text, false); + if (*cursor == ',') { + cursor++; + continue; + } + break; } - patch_show_popup (popup, __, 0, 0); - free_error_lines (parse_errors); - free_error_lines (unrecognized_keys); } -} -void -load_natural_wonder_configs () -{ - load_natural_wonder_config_file ("default.districts_natural_wonders_config.txt", 1, 1, 1); - load_natural_wonder_config_file ("user.districts_natural_wonders_config.txt", 1, 0, 1); - - char * scenario_filename = "scenario.districts_natural_wonders_config.txt"; - char * scenario_natural_wonder_config_path = BIC_get_asset_path (p_bic_data, __, scenario_filename, false); - if ((scenario_natural_wonder_config_path != NULL) && (0 != strcmp (scenario_filename, scenario_natural_wonder_config_path))) - load_natural_wonder_config_file (scenario_natural_wonder_config_path, 0, 0, 1); + if (value_text != NULL) + free (value_text); + + if ((entry_count == 0) || (mask == 0)) { + struct error_line * err = add_error_line (parse_errors); + snprintf (err->text, sizeof err->text, "^ Line %d: %s (expected at least one overlay)", line_number, key_name); + err->text[(sizeof err->text) - 1] = '\0'; + return false; + } + + *out_mask = mask; + return true; } bool -district_config_has_dependent_improvement (struct district_config * cfg, char const * name) +parse_district_bonus_entries (struct string_slice const * value, + int * out_base_bonus, + struct district_bonus_list * out_extras, + struct error_line ** parse_errors, + int line_number, + struct string_slice const * key) { - if ((cfg == NULL) || (name == NULL) || (name[0] == '\0')) + if ((out_base_bonus == NULL) || (out_extras == NULL)) { + add_key_parse_error (parse_errors, line_number, key, value, "(invalid bonus target)"); return false; + } - for (int i = 0; i < cfg->dependent_improvement_count; i++) { - char const * existing = cfg->dependent_improvements[i]; - if ((existing != NULL) && (strcmp (existing, name) == 0)) - return true; + char * value_text = trim_and_extract_slice (value, 0); + free_bonus_entry_list (out_extras); + *out_base_bonus = 0; + + if ((value_text == NULL) || (*value_text == '\0')) { + add_key_parse_error (parse_errors, line_number, key, value, "(expected base bonus)"); + free (value_text); + return false; } - return false; -} -void -set_wonders_dependent_on_wonder_district (void) -{ - if (! is->current_config.enable_districts || - ! is->current_config.enable_wonder_districts) - return; + bool got_base = false; + int base_bonus = 0; - struct district_config * cfg = &is->district_configs[WONDER_DISTRICT_ID]; - for (int wi = 0; wi < is->wonder_district_count; wi++) { - char const * wonder_name = is->wonder_district_configs[wi].wonder_name; - if ((wonder_name == NULL) || (wonder_name[0] == '\0')) - continue; - if (district_config_has_dependent_improvement (cfg, wonder_name)) - continue; + char * cursor = value_text; + while (1) { + while (is_space_char (*cursor)) + cursor++; - int dest = cfg->dependent_improvement_count; - if (dest >= ARRAY_LEN (cfg->dependent_improvements)) { - continue; - } + if (*cursor == '\0') + break; - char * copy = strdup (wonder_name); - if (copy == NULL) { - continue; + char * item_start = cursor; + bool in_quotes = false; + while (*cursor != '\0') { + if (*cursor == '"') + in_quotes = ! in_quotes; + if ((! in_quotes) && (*cursor == ',')) + break; + cursor++; } - cfg->dependent_improvements[dest] = copy; - cfg->dependent_improvement_count = dest + 1; - } + char * item_end = cursor; + while ((item_end > item_start) && is_space_char (item_end[-1])) + item_end--; + while ((item_start < item_end) && is_space_char (*item_start)) + item_start++; + + if (item_end > item_start) { + struct string_slice item = { .str = item_start, .len = (int)(item_end - item_start) }; + + if (! got_base) { + struct string_slice base_slice = trim_string_slice (&item, 0); + if (! read_int (&base_slice, &base_bonus)) { + add_key_parse_error (parse_errors, line_number, key, value, "(expected base bonus)"); + free_bonus_entry_list (out_extras); + free (value_text); + return false; + } + got_base = true; + } else { + char * colon = NULL; + in_quotes = false; + for (char * p = item_start; p < item_end; p++) { + if (*p == '"') + in_quotes = ! in_quotes; + if ((! in_quotes) && (*p == ':')) { + colon = p; + break; + } + } - if (cfg->max_building_index < cfg->dependent_improvement_count) - cfg->max_building_index = cfg->dependent_improvement_count; -} + if (colon == NULL) { + add_key_parse_error (parse_errors, line_number, key, value, "(expected \"name: bonus\" entry)"); + free_bonus_entry_list (out_extras); + free (value_text); + return false; + } -void parse_building_and_tech_ids () -{ - struct c3x_config * cfg = &is->current_config; - char ss[200]; - for (int i = 0; i < is->district_count; i++) { - if (is->district_configs[i].command != 0) - itable_insert (&is->command_id_to_district_id, is->district_configs[i].command, i); - is->district_infos[i].advance_prereq_id = -1; + struct string_slice name_slice = { .str = item_start, .len = (int)(colon - item_start) }; + struct string_slice bonus_slice = { .str = colon + 1, .len = (int)(item_end - (colon + 1)) }; + struct string_slice trimmed_name = trim_string_slice (&name_slice, 1); + struct string_slice trimmed_bonus = trim_string_slice (&bonus_slice, 0); - // Map advance prereqs to districts - if (is->district_configs[i].advance_prereq != NULL && is->district_configs[i].advance_prereq != "") { - int tech_id; - struct string_slice tech_name = { .str = (char *)is->district_configs[i].advance_prereq, .len = (int)strlen (is->district_configs[i].advance_prereq) }; - if (find_game_object_id_by_name (GOK_TECHNOLOGY, &tech_name, 0, &tech_id)) { - snprintf (ss, sizeof ss, "Found tech prereq \"%.*s\" for district \"%s\", ID %d\n", tech_name.len, tech_name.str, is->district_configs[i].advance_prereq, tech_id); - (*p_OutputDebugStringA) (ss); - is->district_infos[i].advance_prereq_id = tech_id; - itable_insert (&is->district_tech_prereqs, tech_id, i); - } else { - is->district_infos[i].advance_prereq_id = -1; - } - } + if (trimmed_name.len <= 0) { + add_key_parse_error (parse_errors, line_number, key, value, "(expected bonus name)"); + free_bonus_entry_list (out_extras); + free (value_text); + return false; + } - // Map improvement prereqs to districts - int stored_count = 0; - for (int j = 0; j < is->district_configs[i].dependent_improvement_count; j++) { - int improv_id; - if (is->district_configs[i].dependent_improvements[j] == "" || is->district_configs[i].dependent_improvements[j] == NULL) - continue; + int bonus_value = 0; + if (! read_int (&trimmed_bonus, &bonus_value)) { + add_key_parse_error (parse_errors, line_number, key, value, "(expected bonus value)"); + free_bonus_entry_list (out_extras); + free (value_text); + return false; + } - // Gate wonder district prereqs behind enable_wonder_districts - if ((is->district_configs[i].command == UCV_Build_WonderDistrict) && (! cfg->enable_wonder_districts)) - continue; + if (out_extras->count >= MAX_DISTRICT_BONUS_ENTRIES) { + add_key_parse_error (parse_errors, line_number, key, value, "(too many bonus entries)"); + free_bonus_entry_list (out_extras); + free (value_text); + return false; + } - struct string_slice improv_name = { .str = (char *)is->district_configs[i].dependent_improvements[j], .len = (int)strlen (is->district_configs[i].dependent_improvements[j]) }; - if (find_game_object_id_by_name (GOK_BUILDING, &improv_name, 0, &improv_id)) { - snprintf (ss, sizeof ss, "Found improvement prereq \"%.*s\" for district \"%s\", ID %d\n", improv_name.len, improv_name.str, is->district_configs[i].dependent_improvements[j], improv_id); - (*p_OutputDebugStringA) (ss); - if (stored_count < ARRAY_LEN (is->district_infos[i].dependent_building_ids)) { - is->district_infos[i].dependent_building_ids[stored_count] = improv_id; - stored_count += 1; + struct district_bonus_entry * entry = &out_extras->entries[out_extras->count++]; + entry->bonus = bonus_value; + entry->building_id = -1; + entry->building_name = NULL; + + enum SquareTypes parsed_type; + if (read_square_type_value (&trimmed_name, &parsed_type)) { + entry->type = DBET_TILE; + entry->tile_type = parsed_type; + } else { + entry->type = DBET_BUILDING; + entry->tile_type = (enum SquareTypes)SQ_INVALID; + entry->building_name = extract_slice (&trimmed_name); } - itable_insert (&is->district_building_prereqs, improv_id, i); - stable_insert (&is->building_name_to_id, improv_name.str, improv_id); - } else { - is->district_infos[i].dependent_building_ids[j] = -1; } - is->district_infos[i].dependent_building_count = stored_count; } - } - // Map wonder names to their improvement IDs for rendering under-construction wonders - for (int wi = 0; wi < is->wonder_district_count; wi++) { - if (is->wonder_district_configs[wi].wonder_name == NULL || is->wonder_district_configs[wi].wonder_name[0] == '\0') + if (*cursor == ',') { + cursor++; continue; - - int improv_id; - struct string_slice wonder_name = { .str = (char *)is->wonder_district_configs[wi].wonder_name, .len = (int)strlen (is->wonder_district_configs[wi].wonder_name) }; - if (find_game_object_id_by_name (GOK_BUILDING, &wonder_name, 0, &improv_id)) { - snprintf (ss, sizeof ss, "Found improvement prereq \"%.*s\" for wonder district \"%s\", ID %d\n", wonder_name.len, wonder_name.str, is->wonder_district_configs[wi].wonder_name, improv_id); - (*p_OutputDebugStringA) (ss); - stable_insert (&is->building_name_to_id, wonder_name.str, improv_id); - } else { - snprintf (ss, sizeof ss, "Could not find improvement prereq \"%.*s\" for wonder district \"%s\"\n", wonder_name.len, wonder_name.str, is->wonder_district_configs[wi].wonder_name); - (*p_OutputDebugStringA) (ss); } + if (*cursor == '\0') + break; } -} -void -load_districts_config () -{ - clear_dynamic_district_definitions (); - load_dynamic_district_configs (); - load_dynamic_wonder_configs (); - load_natural_wonder_configs (); - is->district_count = is->special_district_count + is->dynamic_district_count; + free (value_text); - set_wonders_dependent_on_wonder_district (); - parse_building_and_tech_ids (); + if (! got_base) { + add_key_parse_error (parse_errors, line_number, key, value, "(expected base bonus)"); + free_bonus_entry_list (out_extras); + return false; + } + + *out_base_bonus = base_bonus; + return true; } -void -place_natural_wonders_on_map (void) +bool +override_special_district_from_definition (struct parsed_district_definition * def, int section_start_line) { - if (! is->current_config.enable_natural_wonders) - return; + if ((! def->has_name) || (def->name == NULL)) + return false; - int wonder_count = is->natural_wonder_count; - if (wonder_count <= 0) - return; + int index = find_special_district_index_by_name (def->name); + if (index < 0) + return false; - struct natural_wonder_candidate_list * candidate_lists = (struct natural_wonder_candidate_list *)calloc (wonder_count, sizeof *candidate_lists); - bool * already_placed = (bool *)calloc (wonder_count, sizeof *already_placed); + struct district_config * cfg = &is->district_configs[index]; + struct district_config const * defaults = &special_district_defaults[index]; - if ((candidate_lists == NULL) || (already_placed == NULL)) { - if (candidate_lists != NULL) free (candidate_lists); - if (already_placed != NULL) free (already_placed); - return; - } + free (def->name); + def->name = NULL; + def->has_name = false; - struct wonder_location * placements = NULL; - int placement_count = 0; - int placement_capacity = 0; - int existing_count = 0; + if (def->has_display_name) { + if ((cfg->display_name != NULL) && + (cfg->display_name != cfg->name) && + (cfg->display_name != defaults->display_name)) + free ((void *)cfg->display_name); + cfg->display_name = def->display_name; + def->display_name = NULL; + } - // Record existing natural wonders - FOR_TABLE_ENTRIES (tei, &is->district_tile_map) { - struct district_instance * inst = (struct district_instance *)(long)tei.value; - if ((inst == NULL) || (inst->district_type != NATURAL_WONDER_DISTRICT_ID)) - continue; + if (def->has_tooltip) { + if ((cfg->tooltip != NULL) && (cfg->tooltip != defaults->tooltip)) + free ((void *)cfg->tooltip); + cfg->tooltip = def->tooltip; + def->tooltip = NULL; + } - int wonder_id = inst->natural_wonder_info.natural_wonder_id; - if ((wonder_id < 0) || (wonder_id >= wonder_count)) - continue; + if (def->has_advance_prereqs) { + for (int i = 0; i < ARRAY_LEN (cfg->advance_prereqs); i++) { + char const * default_value = (i < defaults->advance_prereq_count) ? defaults->advance_prereqs[i] : NULL; + if ((cfg->advance_prereqs[i] != NULL) && + (cfg->advance_prereqs[i] != default_value)) + free ((void *)cfg->advance_prereqs[i]); + cfg->advance_prereqs[i] = NULL; + } - already_placed[wonder_id] = true; + cfg->advance_prereq_count = def->advance_prereq_count; + const int max_entries = ARRAY_LEN (cfg->advance_prereqs); + if (cfg->advance_prereq_count > max_entries) + cfg->advance_prereq_count = max_entries; + for (int i = 0; i < cfg->advance_prereq_count; i++) { + cfg->advance_prereqs[i] = def->advance_prereqs[i]; + def->advance_prereqs[i] = NULL; + } + def->advance_prereq_count = 0; + } - Tile * tile = (Tile *)tei.key; - int tile_x, tile_y; - if (! district_instance_get_coords (inst, tile, &tile_x, &tile_y)) - continue; + if (def->has_obsoleted_by) { + if ((cfg->obsoleted_by != NULL) && (cfg->obsoleted_by != defaults->obsoleted_by)) + free ((void *)cfg->obsoleted_by); + cfg->obsoleted_by = def->obsoleted_by; + def->obsoleted_by = NULL; + } - if (placement_count >= placement_capacity) { - int new_capacity = (placement_capacity > 0) ? placement_capacity * 2 : 8; - struct wonder_location * grown = - (struct wonder_location *)realloc (placements, new_capacity * sizeof *grown); - if (grown == NULL) - continue; - placements = grown; - placement_capacity = new_capacity; + if (def->has_resource_prereqs) { + for (int i = 0; i < ARRAY_LEN (cfg->resource_prereqs); i++) { + char const * default_value = (i < defaults->resource_prereq_count) ? defaults->resource_prereqs[i] : NULL; + if ((cfg->resource_prereqs[i] != NULL) && + (cfg->resource_prereqs[i] != default_value)) + free ((void *)cfg->resource_prereqs[i]); + cfg->resource_prereqs[i] = NULL; } - if (placements != NULL) { - placements[placement_count++] = (struct wonder_location){ - .x = (short)tile_x, - .y = (short)tile_y - }; - existing_count += 1; + cfg->resource_prereq_count = def->resource_prereq_count; + const int max_entries = ARRAY_LEN (cfg->resource_prereqs); + if (cfg->resource_prereq_count > max_entries) + cfg->resource_prereq_count = max_entries; + for (int i = 0; i < cfg->resource_prereq_count; i++) { + cfg->resource_prereqs[i] = def->resource_prereqs[i]; + def->resource_prereqs[i] = NULL; } } - // Build candidate lists - int map_width = p_bic_data->Map.Width; - int map_height = p_bic_data->Map.Height; - int minimum_separation = is->current_config.minimum_natural_wonder_separation; - - for (int y = 0; y < map_height; y++) { - for (int x = 0; x < map_width; x++) { - Tile * tile = tile_at (x, y); - if ((tile == NULL) || (tile == p_null_tile)) - continue; + if (def->has_resource_prereq_on_tile) { + if ((cfg->resource_prereq_on_tile != NULL) && (cfg->resource_prereq_on_tile != defaults->resource_prereq_on_tile)) + free ((void *)cfg->resource_prereq_on_tile); + cfg->resource_prereq_on_tile = def->resource_prereq_on_tile; + def->resource_prereq_on_tile = NULL; + } - if (! natural_wonder_tile_is_clear (tile, x, y)) continue; + if (def->has_wonder_prereqs) { + for (int i = 0; i < ARRAY_LEN (cfg->wonder_prereqs); i++) { + char const * default_value = (i < defaults->wonder_prereq_count) ? defaults->wonder_prereqs[i] : NULL; + if ((cfg->wonder_prereqs[i] != NULL) && + (cfg->wonder_prereqs[i] != default_value)) + free ((void *)cfg->wonder_prereqs[i]); + cfg->wonder_prereqs[i] = NULL; + } + + cfg->wonder_prereq_count = def->wonder_prereq_count; + const int max_entries = ARRAY_LEN (cfg->wonder_prereqs); + if (cfg->wonder_prereq_count > max_entries) + cfg->wonder_prereq_count = max_entries; + for (int i = 0; i < cfg->wonder_prereq_count; i++) { + cfg->wonder_prereqs[i] = def->wonder_prereqs[i]; + def->wonder_prereqs[i] = NULL; + } + } + + if (def->has_natural_wonder_prereqs) { + for (int i = 0; i < ARRAY_LEN (cfg->natural_wonder_prereqs); i++) { + char const * default_value = (i < defaults->natural_wonder_prereq_count) ? defaults->natural_wonder_prereqs[i] : NULL; + if ((cfg->natural_wonder_prereqs[i] != NULL) && + (cfg->natural_wonder_prereqs[i] != default_value)) + free ((void *)cfg->natural_wonder_prereqs[i]); + cfg->natural_wonder_prereqs[i] = NULL; + } + + cfg->natural_wonder_prereq_count = def->natural_wonder_prereq_count; + const int max_entries = ARRAY_LEN (cfg->natural_wonder_prereqs); + if (cfg->natural_wonder_prereq_count > max_entries) + cfg->natural_wonder_prereq_count = max_entries; + for (int i = 0; i < cfg->natural_wonder_prereq_count; i++) { + cfg->natural_wonder_prereqs[i] = def->natural_wonder_prereqs[i]; + def->natural_wonder_prereqs[i] = NULL; + } + } - if (natural_wonder_exists_within_distance (x, y, minimum_separation)) - continue; + if (def->has_buildable_on_districts) { + for (int i = 0; i < ARRAY_LEN (cfg->buildable_on_districts); i++) { + char const * default_value = (i < defaults->buildable_on_district_count) ? defaults->buildable_on_districts[i] : NULL; + if ((cfg->buildable_on_districts[i] != NULL) && + (cfg->buildable_on_districts[i] != default_value)) + free ((void *)cfg->buildable_on_districts[i]); + cfg->buildable_on_districts[i] = NULL; + } + + cfg->buildable_on_district_count = def->buildable_on_district_count; + const int max_entries = ARRAY_LEN (cfg->buildable_on_districts); + if (cfg->buildable_on_district_count > max_entries) + cfg->buildable_on_district_count = max_entries; + for (int i = 0; i < cfg->buildable_on_district_count; i++) { + cfg->buildable_on_districts[i] = def->buildable_on_districts[i]; + def->buildable_on_districts[i] = NULL; + } + cfg->buildable_on_district_id_count = 0; + cfg->has_buildable_on_districts = true; + } + + if (def->has_buildable_adjacent_to_districts) { + for (int i = 0; i < ARRAY_LEN (cfg->buildable_adjacent_to_districts); i++) { + char const * default_value = (i < defaults->buildable_adjacent_to_district_count) + ? defaults->buildable_adjacent_to_districts[i] + : NULL; + if ((cfg->buildable_adjacent_to_districts[i] != NULL) && + (cfg->buildable_adjacent_to_districts[i] != default_value)) + free ((void *)cfg->buildable_adjacent_to_districts[i]); + cfg->buildable_adjacent_to_districts[i] = NULL; + } + + cfg->buildable_adjacent_to_district_count = def->buildable_adjacent_to_district_count; + const int max_entries = ARRAY_LEN (cfg->buildable_adjacent_to_districts); + if (cfg->buildable_adjacent_to_district_count > max_entries) + cfg->buildable_adjacent_to_district_count = max_entries; + for (int i = 0; i < cfg->buildable_adjacent_to_district_count; i++) { + cfg->buildable_adjacent_to_districts[i] = def->buildable_adjacent_to_districts[i]; + def->buildable_adjacent_to_districts[i] = NULL; + } + cfg->buildable_adjacent_to_district_id_count = 0; + cfg->has_buildable_adjacent_to_districts = true; + } - for (int ni = 0; ni < wonder_count; ni++) { - if (already_placed[ni]) - continue; + if (def->has_buildable_by_civs) { + for (int i = 0; i < ARRAY_LEN (cfg->buildable_by_civs); i++) { + char const * default_value = (i < defaults->buildable_by_civ_count) ? defaults->buildable_by_civs[i] : NULL; + if ((cfg->buildable_by_civs[i] != NULL) && + (cfg->buildable_by_civs[i] != default_value)) + free ((void *)cfg->buildable_by_civs[i]); + cfg->buildable_by_civs[i] = NULL; + } + cfg->buildable_by_civ_count = def->buildable_by_civ_count; + const int max_civ_names = ARRAY_LEN (cfg->buildable_by_civs); + if (cfg->buildable_by_civ_count > max_civ_names) + cfg->buildable_by_civ_count = max_civ_names; + for (int i = 0; i < cfg->buildable_by_civ_count; i++) { + cfg->buildable_by_civs[i] = def->buildable_by_civs[i]; + def->buildable_by_civs[i] = NULL; + } + cfg->has_buildable_by_civs = true; + } + + if (def->has_buildable_by_civ_traits) { + cfg->buildable_by_civ_traits_id_count = def->buildable_by_civ_traits_id_count; + const int max_entries = ARRAY_LEN (cfg->buildable_by_civ_traits_ids); + if (cfg->buildable_by_civ_traits_id_count > max_entries) + cfg->buildable_by_civ_traits_id_count = max_entries; + for (int i = 0; i < cfg->buildable_by_civ_traits_id_count; i++) + cfg->buildable_by_civ_traits_ids[i] = def->buildable_by_civ_traits_ids[i]; + cfg->has_buildable_by_civ_traits = true; + } + + if (def->has_buildable_by_civ_govs) { + cfg->buildable_by_civ_govs_id_count = def->buildable_by_civ_govs_id_count; + const int max_entries = ARRAY_LEN (cfg->buildable_by_civ_govs_ids); + if (cfg->buildable_by_civ_govs_id_count > max_entries) + cfg->buildable_by_civ_govs_id_count = max_entries; + for (int i = 0; i < cfg->buildable_by_civ_govs_id_count; i++) + cfg->buildable_by_civ_govs_ids[i] = def->buildable_by_civ_govs_ids[i]; + cfg->has_buildable_by_civ_govs = true; + } + + if (def->has_buildable_by_civ_cultures) { + cfg->buildable_by_civ_cultures_id_count = def->buildable_by_civ_cultures_id_count; + const int max_entries = ARRAY_LEN (cfg->buildable_by_civ_cultures_ids); + if (cfg->buildable_by_civ_cultures_id_count > max_entries) + cfg->buildable_by_civ_cultures_id_count = max_entries; + for (int i = 0; i < cfg->buildable_by_civ_cultures_id_count; i++) + cfg->buildable_by_civ_cultures_ids[i] = def->buildable_by_civ_cultures_ids[i]; + cfg->has_buildable_by_civ_cultures = true; + } + + if (def->has_buildable_by_war_allies) + cfg->buildable_by_war_allies = def->buildable_by_war_allies; + if (def->has_buildable_by_pact_allies) + cfg->buildable_by_pact_allies = def->buildable_by_pact_allies; - struct natural_wonder_district_config const * cfg = &is->natural_wonder_configs[ni]; - if (cfg->name == NULL) - continue; + if (def->has_allow_multiple) + cfg->allow_multiple = def->allow_multiple; + if (def->has_vary_img_by_era) + cfg->vary_img_by_era = def->vary_img_by_era; + if (def->has_vary_img_by_culture) + cfg->vary_img_by_culture = def->vary_img_by_culture; + if (def->has_render_strategy) + cfg->render_strategy = def->render_strategy; + if (def->has_ai_build_strategy) + cfg->ai_build_strategy = def->ai_build_strategy; + if (def->has_align_to_coast) + cfg->align_to_coast = def->align_to_coast; + if (def->has_draw_over_resources) + cfg->draw_over_resources = def->draw_over_resources; + if (def->has_allow_irrigation_from) + cfg->allow_irrigation_from = def->allow_irrigation_from; + if (def->has_auto_add_road) + cfg->auto_add_road = def->auto_add_road; + if (def->has_auto_add_railroad) + cfg->auto_add_railroad = def->auto_add_railroad; + if (def->has_custom_width) + cfg->custom_width = def->custom_width; + if (def->has_custom_height) + cfg->custom_height = def->custom_height; + if (def->has_x_offset) + cfg->x_offset = def->x_offset; + if (def->has_y_offset) + cfg->y_offset = def->y_offset; + if (def->has_btn_tile_sheet_column) + cfg->btn_tile_sheet_column = def->btn_tile_sheet_column; + if (def->has_btn_tile_sheet_row) + cfg->btn_tile_sheet_row = def->btn_tile_sheet_row; + if (def->has_defense_bonus_percent) { + cfg->defense_bonus_percent = def->defense_bonus_percent; + free_bonus_entry_list_override (&cfg->defense_bonus_extras, &defaults->defense_bonus_extras); + move_bonus_entry_list (&cfg->defense_bonus_extras, &def->defense_bonus_extras); + } + if (def->has_heal_units_in_one_turn) + cfg->heal_units_in_one_turn = def->heal_units_in_one_turn; + if (def->has_culture_bonus) { + cfg->culture_bonus = def->culture_bonus; + free_bonus_entry_list_override (&cfg->culture_bonus_extras, &defaults->culture_bonus_extras); + move_bonus_entry_list (&cfg->culture_bonus_extras, &def->culture_bonus_extras); + } + if (def->has_science_bonus) { + cfg->science_bonus = def->science_bonus; + free_bonus_entry_list_override (&cfg->science_bonus_extras, &defaults->science_bonus_extras); + move_bonus_entry_list (&cfg->science_bonus_extras, &def->science_bonus_extras); + } + if (def->has_food_bonus) { + cfg->food_bonus = def->food_bonus; + free_bonus_entry_list_override (&cfg->food_bonus_extras, &defaults->food_bonus_extras); + move_bonus_entry_list (&cfg->food_bonus_extras, &def->food_bonus_extras); + } + if (def->has_gold_bonus) { + cfg->gold_bonus = def->gold_bonus; + free_bonus_entry_list_override (&cfg->gold_bonus_extras, &defaults->gold_bonus_extras); + move_bonus_entry_list (&cfg->gold_bonus_extras, &def->gold_bonus_extras); + } + if (def->has_shield_bonus) { + cfg->shield_bonus = def->shield_bonus; + free_bonus_entry_list_override (&cfg->shield_bonus_extras, &defaults->shield_bonus_extras); + move_bonus_entry_list (&cfg->shield_bonus_extras, &def->shield_bonus_extras); + } + if (def->has_happiness_bonus) { + cfg->happiness_bonus = def->happiness_bonus; + free_bonus_entry_list_override (&cfg->happiness_bonus_extras, &defaults->happiness_bonus_extras); + move_bonus_entry_list (&cfg->happiness_bonus_extras, &def->happiness_bonus_extras); + } + if (def->has_buildable_on) + cfg->buildable_square_types_mask = def->buildable_square_types_mask; + if (def->has_buildable_adjacent_to) { + cfg->buildable_adjacent_to_square_types_mask = def->buildable_adjacent_to_square_types_mask; + cfg->has_buildable_adjacent_to = true; + cfg->buildable_adjacent_to_allows_city = def->buildable_adjacent_to_allows_city; + } + if (def->has_buildable_on_overlays) { + cfg->buildable_on_overlays_mask = def->buildable_on_overlays_mask; + cfg->has_buildable_on_overlays = true; + } + if (def->has_buildable_only_on_overlays) { + cfg->buildable_only_on_overlays_mask = def->buildable_only_on_overlays_mask; + cfg->has_buildable_only_on_overlays = true; + } + if (def->has_buildable_adjacent_to_overlays) { + cfg->buildable_adjacent_to_overlays_mask = def->buildable_adjacent_to_overlays_mask; + cfg->has_buildable_adjacent_to_overlays = true; + } - if (! natural_wonder_terrain_matches (cfg, tile, x, y)) - continue; + if (def->has_generated_resource) { + if ((cfg->generated_resource != NULL) && (cfg->generated_resource != defaults->generated_resource)) + free ((void *)cfg->generated_resource); + cfg->generated_resource = def->generated_resource; + def->generated_resource = NULL; + cfg->generated_resource_flags = def->generated_resource_flags; + cfg->generated_resource_id = -1; + } - natural_wonder_candidate_list_push (&candidate_lists[ni], tile, x, y); - } + if (def->has_dependent_improvements) { + for (int i = 0; i < ARRAY_LEN (cfg->dependent_improvements); i++) { + char const * default_value = (i < defaults->dependent_improvement_max_index) ? defaults->dependent_improvements[i] : NULL; + if ((cfg->dependent_improvements[i] != NULL) && + (cfg->dependent_improvements[i] != default_value)) + free ((void *)cfg->dependent_improvements[i]); + cfg->dependent_improvements[i] = NULL; + } + + cfg->dependent_improvement_max_index = def->dependent_improvement_max_index; + const int max_entries = ARRAY_LEN (cfg->dependent_improvements); + if (cfg->dependent_improvement_max_index > max_entries) + cfg->dependent_improvement_max_index = max_entries; + for (int i = 0; i < cfg->dependent_improvement_max_index; i++) { + cfg->dependent_improvements[i] = def->dependent_improvements[i]; + def->dependent_improvements[i] = NULL; + } + if (! def->has_img_column_count) { + cfg->img_column_count = clamp (1, MAX_DISTRICT_COLUMN_COUNT, cfg->dependent_improvement_max_index + 1); + cfg->has_img_column_count_override = false; } } - bool wraps_horiz = (p_bic_data->Map.Flags & 1) != 0; - int newly_placed = 0; - int * wonder_order = (int *)malloc (wonder_count * sizeof *wonder_order); - if (wonder_order != NULL) { - for (int i = 0; i < wonder_count; i++) - wonder_order[i] = i; - for (int i = wonder_count - 1; i > 0; i--) { - int swap_index = rand_int (p_rand_object, __, i + 1); - int temp = wonder_order[i]; - wonder_order[i] = wonder_order[swap_index]; - wonder_order[swap_index] = temp; + if (def->has_img_paths) { + for (int i = 0; i < ARRAY_LEN (cfg->img_paths); i++) { + char const * default_value = (i < defaults->img_path_count) ? defaults->img_paths[i] : NULL; + if ((cfg->img_paths[i] != NULL) && + (cfg->img_paths[i] != default_value)) + free ((void *)cfg->img_paths[i]); + cfg->img_paths[i] = NULL; } - } - - for (int order_index = 0; order_index < wonder_count; order_index++) { - int ni = (wonder_order != NULL) ? wonder_order[order_index] : order_index; - if (already_placed[ni]) - continue; - struct natural_wonder_candidate_list * list = &candidate_lists[ni]; - if (list->count == 0) { - char msg[256]; - snprintf (msg, sizeof msg, "[C3X] No valid tiles to place natural wonder \"%s\".\n", - (is->natural_wonder_configs[ni].name != NULL) ? is->natural_wonder_configs[ni].name : "Natural Wonder"); - (*p_OutputDebugStringA) (msg); - continue; + cfg->img_path_count = def->img_path_count; + const int max_img_paths = ARRAY_LEN (cfg->img_paths); + if (cfg->img_path_count > max_img_paths) + cfg->img_path_count = max_img_paths; + for (int i = 0; i < cfg->img_path_count; i++) { + cfg->img_paths[i] = def->img_paths[i]; + def->img_paths[i] = NULL; } + } - int best_index = -1; - int best_dist = -1; - int best_adjacent_count = -1; - int best_target_diff = INT_MAX; - int best_rand = INT_MAX; - int best_same_type_count = -1; - int best_continent_priority = INT_MAX; - int target_x = (wonder_count > 0) - ? (int)(((long long)(2 * ni + 1) * map_width) / (2 * wonder_count)) - : (map_width >> 1); + if (def->has_img_column_count) { + cfg->img_column_count = clamp (1, MAX_DISTRICT_COLUMN_COUNT, cfg->img_column_count); + cfg->has_img_column_count_override = true; + } - for (int ci = 0; ci < list->count; ci++) { - struct natural_wonder_candidate * cand = &list->entries[ci]; - Tile * tile = cand->tile; - if ((tile == NULL) || (tile == p_null_tile)) continue; - if (get_district_instance (tile) != NULL) continue; - if (! natural_wonder_tile_is_clear (tile, cand->x, cand->y)) continue; - if (! natural_wonder_terrain_matches (&is->natural_wonder_configs[ni], tile, cand->x, cand->y)) continue; - if (natural_wonder_exists_within_distance (cand->x, cand->y, minimum_separation)) continue; + if (! ensure_culture_variant_art (cfg, section_start_line)) { + free_special_district_override_strings (cfg, defaults); + *cfg = *defaults; + return false; + } - int min_dist_sq = natural_wonder_min_distance_sq (cand->x, cand->y, placements, placement_count); - if (min_dist_sq == INT_MAX) { - int span = (map_width * map_width) + (map_height * map_height); - if (span <= 0) - span = INT_MAX; - min_dist_sq = span; - } + return true; +} - int dx_raw = int_abs (cand->x - target_x); - int dx_adjusted = compute_wrapped_component (dx_raw, map_width, wraps_horiz); - int rand_val = rand_int (p_rand_object, __, 0x7FFF); +bool +add_dynamic_district_from_definition (struct parsed_district_definition * def, int section_start_line) +{ + if ((! def->has_name) || (def->name == NULL)) + return false; - int continent_id = tile->vtable->m46_Get_ContinentID (tile); - int continent_priority = 1; - if ((continent_id >= 0) && ! continent_has_natural_wonder (continent_id, placements, placement_count)) - continent_priority = 0; + if ((! def->has_img_paths) || (def->img_path_count <= 0)) + return false; - bool adjacency_bonus_active = - (is->natural_wonder_configs[ni].adjacent_to != (enum SquareTypes)SQ_INVALID) && - (is->natural_wonder_configs[ni].adjacency_dir == DIR_ZERO); - int adjacency_count = -1; - if (adjacency_bonus_active) - adjacency_count = count_diagonal_adjacent_tiles_of_type (cand->x, cand->y, - is->natural_wonder_configs[ni].adjacent_to); + int existing_index = -1; + for (int i = is->special_district_count; i < is->district_count; i++) { + if ((is->district_configs[i].name != NULL) && + (strcmp (is->district_configs[i].name, def->name) == 0)) { + existing_index = i; + break; + } + } - int same_type_count = count_adjacent_tiles_of_type (cand->x, cand->y, - is->natural_wonder_configs[ni].terrain_type); + bool reusing_existing = existing_index >= 0; + int dest_index = reusing_existing ? existing_index : (is->special_district_count + is->dynamic_district_count); - bool better = false; - if (continent_priority < best_continent_priority) - better = true; - else if (continent_priority > best_continent_priority) - continue; + if ((! reusing_existing) && (dest_index >= COUNT_DISTRICT_TYPES)) + return false; - if (! better && adjacency_bonus_active) { - if (adjacency_count > best_adjacent_count) - better = true; - else if (adjacency_count < best_adjacent_count) - continue; - } + enum Unit_Command_Values preserved_command = 0; + if (reusing_existing) + preserved_command = is->district_configs[dest_index].command; - if (! better) { - if (same_type_count > best_same_type_count) - better = true; - else if (same_type_count < best_same_type_count) - continue; - } + struct district_config new_cfg; + memset (&new_cfg, 0, sizeof new_cfg); + new_cfg.is_dynamic = true; - if (! better) { - if ((min_dist_sq > best_dist) || - ((min_dist_sq == best_dist) && (dx_adjusted < best_target_diff)) || - ((min_dist_sq == best_dist) && (dx_adjusted == best_target_diff) && (rand_val < best_rand))) - better = true; - else - continue; - } + new_cfg.name = def->name; + def->name = NULL; + if (def->has_display_name) { + new_cfg.display_name = def->display_name; + if (new_cfg.display_name == NULL) + new_cfg.display_name = new_cfg.name; + def->display_name = NULL; + } else { + new_cfg.display_name = new_cfg.name; + } - best_dist = min_dist_sq; - best_target_diff = dx_adjusted; - best_rand = rand_val; - best_index = ci; - best_continent_priority = continent_priority; - if (adjacency_bonus_active) - best_adjacent_count = adjacency_count; - best_same_type_count = same_type_count; - } + if (def->has_tooltip) { + new_cfg.tooltip = def->tooltip; + def->tooltip = NULL; + } else if (new_cfg.name != NULL) { + char buffer[128]; + snprintf (buffer, sizeof buffer, "Build %s", new_cfg.name); + new_cfg.tooltip = strdup (buffer); + } - if (best_index < 0) { - char msg[256]; - snprintf (msg, sizeof msg, "[C3X] Could not find a suitable tile for natural wonder \"%s\" after filtering.\n", - (is->natural_wonder_configs[ni].name != NULL) ? is->natural_wonder_configs[ni].name : "Natural Wonder"); - (*p_OutputDebugStringA) (msg); - continue; - } + if (def->has_advance_prereqs) { + new_cfg.advance_prereq_count = def->advance_prereq_count; + const int max_entries = ARRAY_LEN (new_cfg.advance_prereqs); + if (new_cfg.advance_prereq_count > max_entries) + new_cfg.advance_prereq_count = max_entries; + for (int i = 0; i < new_cfg.advance_prereq_count; i++) { + new_cfg.advance_prereqs[i] = def->advance_prereqs[i]; + def->advance_prereqs[i] = NULL; + } + def->advance_prereq_count = 0; + } + + if (def->has_obsoleted_by) { + new_cfg.obsoleted_by = def->obsoleted_by; + def->obsoleted_by = NULL; + } + + new_cfg.resource_prereq_count = def->has_resource_prereqs ? def->resource_prereq_count : 0; + const int max_resource_entries = ARRAY_LEN (new_cfg.resource_prereqs); + if (new_cfg.resource_prereq_count > max_resource_entries) + new_cfg.resource_prereq_count = max_resource_entries; + for (int i = 0; i < new_cfg.resource_prereq_count; i++) { + new_cfg.resource_prereqs[i] = def->resource_prereqs[i]; + def->resource_prereqs[i] = NULL; + } + + if (def->has_resource_prereq_on_tile) { + new_cfg.resource_prereq_on_tile = def->resource_prereq_on_tile; + def->resource_prereq_on_tile = NULL; + } + + new_cfg.wonder_prereq_count = def->has_wonder_prereqs ? def->wonder_prereq_count : 0; + const int max_required_wonders = ARRAY_LEN (new_cfg.wonder_prereqs); + if (new_cfg.wonder_prereq_count > max_required_wonders) + new_cfg.wonder_prereq_count = max_required_wonders; + for (int i = 0; i < new_cfg.wonder_prereq_count; i++) { + new_cfg.wonder_prereqs[i] = def->wonder_prereqs[i]; + def->wonder_prereqs[i] = NULL; + } + + new_cfg.natural_wonder_prereq_count = def->has_natural_wonder_prereqs ? def->natural_wonder_prereq_count : 0; + const int max_required_natural_wonders = ARRAY_LEN (new_cfg.natural_wonder_prereqs); + if (new_cfg.natural_wonder_prereq_count > max_required_natural_wonders) + new_cfg.natural_wonder_prereq_count = max_required_natural_wonders; + for (int i = 0; i < new_cfg.natural_wonder_prereq_count; i++) { + new_cfg.natural_wonder_prereqs[i] = def->natural_wonder_prereqs[i]; + def->natural_wonder_prereqs[i] = NULL; + } + + new_cfg.buildable_on_district_count = def->has_buildable_on_districts ? def->buildable_on_district_count : 0; + const int max_buildable_on_districts = ARRAY_LEN (new_cfg.buildable_on_districts); + if (new_cfg.buildable_on_district_count > max_buildable_on_districts) + new_cfg.buildable_on_district_count = max_buildable_on_districts; + for (int i = 0; i < new_cfg.buildable_on_district_count; i++) { + new_cfg.buildable_on_districts[i] = def->buildable_on_districts[i]; + def->buildable_on_districts[i] = NULL; + } + new_cfg.buildable_on_district_id_count = 0; + new_cfg.has_buildable_on_districts = def->has_buildable_on_districts; + + new_cfg.buildable_adjacent_to_district_count = def->has_buildable_adjacent_to_districts ? def->buildable_adjacent_to_district_count : 0; + const int max_adjacent_to_districts = ARRAY_LEN (new_cfg.buildable_adjacent_to_districts); + if (new_cfg.buildable_adjacent_to_district_count > max_adjacent_to_districts) + new_cfg.buildable_adjacent_to_district_count = max_adjacent_to_districts; + for (int i = 0; i < new_cfg.buildable_adjacent_to_district_count; i++) { + new_cfg.buildable_adjacent_to_districts[i] = def->buildable_adjacent_to_districts[i]; + def->buildable_adjacent_to_districts[i] = NULL; + } + new_cfg.buildable_adjacent_to_district_id_count = 0; + new_cfg.has_buildable_adjacent_to_districts = def->has_buildable_adjacent_to_districts; + + new_cfg.buildable_by_civ_count = def->has_buildable_by_civs ? def->buildable_by_civ_count : 0; + const int max_civ_names = ARRAY_LEN (new_cfg.buildable_by_civs); + if (new_cfg.buildable_by_civ_count > max_civ_names) + new_cfg.buildable_by_civ_count = max_civ_names; + for (int i = 0; i < new_cfg.buildable_by_civ_count; i++) { + new_cfg.buildable_by_civs[i] = def->buildable_by_civs[i]; + def->buildable_by_civs[i] = NULL; + } + + new_cfg.has_buildable_by_civs = def->has_buildable_by_civs; + + new_cfg.buildable_by_civ_traits_id_count = def->has_buildable_by_civ_traits ? def->buildable_by_civ_traits_id_count : 0; + const int max_buildable_traits = ARRAY_LEN (new_cfg.buildable_by_civ_traits_ids); + if (new_cfg.buildable_by_civ_traits_id_count > max_buildable_traits) + new_cfg.buildable_by_civ_traits_id_count = max_buildable_traits; + for (int i = 0; i < new_cfg.buildable_by_civ_traits_id_count; i++) + new_cfg.buildable_by_civ_traits_ids[i] = def->buildable_by_civ_traits_ids[i]; + new_cfg.has_buildable_by_civ_traits = def->has_buildable_by_civ_traits; + + new_cfg.buildable_by_civ_govs_id_count = def->has_buildable_by_civ_govs ? def->buildable_by_civ_govs_id_count : 0; + const int max_buildable_govs = ARRAY_LEN (new_cfg.buildable_by_civ_govs_ids); + if (new_cfg.buildable_by_civ_govs_id_count > max_buildable_govs) + new_cfg.buildable_by_civ_govs_id_count = max_buildable_govs; + for (int i = 0; i < new_cfg.buildable_by_civ_govs_id_count; i++) + new_cfg.buildable_by_civ_govs_ids[i] = def->buildable_by_civ_govs_ids[i]; + new_cfg.has_buildable_by_civ_govs = def->has_buildable_by_civ_govs; + + new_cfg.buildable_by_civ_cultures_id_count = def->has_buildable_by_civ_cultures ? def->buildable_by_civ_cultures_id_count : 0; + const int max_buildable_cultures = ARRAY_LEN (new_cfg.buildable_by_civ_cultures_ids); + if (new_cfg.buildable_by_civ_cultures_id_count > max_buildable_cultures) + new_cfg.buildable_by_civ_cultures_id_count = max_buildable_cultures; + for (int i = 0; i < new_cfg.buildable_by_civ_cultures_id_count; i++) + new_cfg.buildable_by_civ_cultures_ids[i] = def->buildable_by_civ_cultures_ids[i]; + new_cfg.has_buildable_by_civ_cultures = def->has_buildable_by_civ_cultures; + + new_cfg.buildable_by_war_allies = def->has_buildable_by_war_allies ? def->buildable_by_war_allies : false; + new_cfg.buildable_by_pact_allies = def->has_buildable_by_pact_allies ? def->buildable_by_pact_allies : false; - struct natural_wonder_candidate * chosen = &list->entries[best_index]; - assign_natural_wonder_to_tile (chosen->tile, chosen->x, chosen->y, ni); + new_cfg.allow_multiple = def->has_allow_multiple ? def->allow_multiple : false; + new_cfg.vary_img_by_era = def->has_vary_img_by_era ? def->vary_img_by_era : false; + new_cfg.vary_img_by_culture = def->has_vary_img_by_culture ? def->vary_img_by_culture : false; + new_cfg.render_strategy = def->has_render_strategy ? def->render_strategy : DRS_BY_COUNT; + new_cfg.ai_build_strategy = def->has_ai_build_strategy ? def->ai_build_strategy : DABS_DISTRICT; + new_cfg.align_to_coast = def->has_align_to_coast ? def->align_to_coast : false; + new_cfg.draw_over_resources = def->has_draw_over_resources ? def->draw_over_resources : false; + new_cfg.allow_irrigation_from = def->has_allow_irrigation_from ? def->allow_irrigation_from : false; + new_cfg.auto_add_road = def->has_auto_add_road ? def->auto_add_road : false; + new_cfg.auto_add_railroad = def->has_auto_add_railroad ? def->auto_add_railroad : false; + new_cfg.custom_width = def->has_custom_width ? def->custom_width : 0; + new_cfg.custom_height = def->has_custom_height ? def->custom_height : 0; + new_cfg.x_offset = def->has_x_offset ? def->x_offset : 0; + new_cfg.y_offset = def->has_y_offset ? def->y_offset : 0; + new_cfg.btn_tile_sheet_column = def->has_btn_tile_sheet_column ? def->btn_tile_sheet_column : 0; + new_cfg.btn_tile_sheet_row = def->has_btn_tile_sheet_row ? def->btn_tile_sheet_row : 0; + new_cfg.defense_bonus_percent = def->has_defense_bonus_percent ? def->defense_bonus_percent : 0; + new_cfg.heal_units_in_one_turn = def->has_heal_units_in_one_turn ? def->heal_units_in_one_turn : false; + new_cfg.culture_bonus = def->has_culture_bonus ? def->culture_bonus : 0; + new_cfg.science_bonus = def->has_science_bonus ? def->science_bonus : 0; + new_cfg.food_bonus = def->has_food_bonus ? def->food_bonus : 0; + new_cfg.gold_bonus = def->has_gold_bonus ? def->gold_bonus : 0; + new_cfg.shield_bonus = def->has_shield_bonus ? def->shield_bonus : 0; + new_cfg.happiness_bonus = def->has_happiness_bonus ? def->happiness_bonus : 0; + new_cfg.buildable_square_types_mask = def->has_buildable_on ? def->buildable_square_types_mask : district_default_buildable_mask (); + new_cfg.buildable_adjacent_to_square_types_mask = def->has_buildable_adjacent_to ? def->buildable_adjacent_to_square_types_mask : 0; + new_cfg.has_buildable_adjacent_to = def->has_buildable_adjacent_to; + new_cfg.buildable_adjacent_to_allows_city = def->has_buildable_adjacent_to ? def->buildable_adjacent_to_allows_city : false; + new_cfg.buildable_on_overlays_mask = def->has_buildable_on_overlays ? def->buildable_on_overlays_mask : 0; + new_cfg.has_buildable_on_overlays = def->has_buildable_on_overlays; + new_cfg.buildable_only_on_overlays_mask = def->has_buildable_only_on_overlays ? def->buildable_only_on_overlays_mask : 0; + new_cfg.has_buildable_only_on_overlays = def->has_buildable_only_on_overlays; + new_cfg.buildable_adjacent_to_overlays_mask = def->has_buildable_adjacent_to_overlays ? def->buildable_adjacent_to_overlays_mask : 0; + new_cfg.has_buildable_adjacent_to_overlays = def->has_buildable_adjacent_to_overlays; - if (placement_count >= placement_capacity) { - int new_capacity = (placement_capacity > 0) ? placement_capacity * 2 : 8; - struct wonder_location * grown = - (struct wonder_location *)realloc (placements, new_capacity * sizeof *grown); - if (grown != NULL) { - placements = grown; - placement_capacity = new_capacity; - } - } + if (def->has_culture_bonus) + move_bonus_entry_list (&new_cfg.culture_bonus_extras, &def->culture_bonus_extras); + if (def->has_science_bonus) + move_bonus_entry_list (&new_cfg.science_bonus_extras, &def->science_bonus_extras); + if (def->has_food_bonus) + move_bonus_entry_list (&new_cfg.food_bonus_extras, &def->food_bonus_extras); + if (def->has_gold_bonus) + move_bonus_entry_list (&new_cfg.gold_bonus_extras, &def->gold_bonus_extras); + if (def->has_shield_bonus) + move_bonus_entry_list (&new_cfg.shield_bonus_extras, &def->shield_bonus_extras); + if (def->has_happiness_bonus) + move_bonus_entry_list (&new_cfg.happiness_bonus_extras, &def->happiness_bonus_extras); + if (def->has_defense_bonus_percent) + move_bonus_entry_list (&new_cfg.defense_bonus_extras, &def->defense_bonus_extras); - if ((placements != NULL) && (placement_count < placement_capacity)) { - placements[placement_count++] = (struct wonder_location){ - .x = chosen->x, - .y = chosen->y - }; - } + if (def->has_generated_resource) { + new_cfg.generated_resource = def->generated_resource; + def->generated_resource = NULL; + new_cfg.generated_resource_flags = def->generated_resource_flags; + new_cfg.generated_resource_id = -1; + } else { + new_cfg.generated_resource = NULL; + new_cfg.generated_resource_id = -1; + new_cfg.generated_resource_flags = 0; + } - newly_placed += 1; + new_cfg.dependent_improvement_max_index = def->has_dependent_improvements ? def->dependent_improvement_max_index : 0; + const int max_dependent_entries = ARRAY_LEN (is->district_configs[0].dependent_improvements); + if (new_cfg.dependent_improvement_max_index > max_dependent_entries) + new_cfg.dependent_improvement_max_index = max_dependent_entries; + for (int i = 0; i < new_cfg.dependent_improvement_max_index; i++) { + new_cfg.dependent_improvements[i] = def->dependent_improvements[i]; + def->dependent_improvements[i] = NULL; + } - char msg[256]; - snprintf (msg, sizeof msg, "[C3X] Placed natural wonder \"%s\" at (%d,%d).\n", - (is->natural_wonder_configs[ni].name != NULL) ? is->natural_wonder_configs[ni].name : "Natural Wonder", - chosen->x, chosen->y); - (*p_OutputDebugStringA) (msg); + new_cfg.img_path_count = def->img_path_count; + const int max_img_paths = ARRAY_LEN (is->district_configs[0].img_paths); + if (new_cfg.img_path_count > max_img_paths) + new_cfg.img_path_count = max_img_paths; + for (int i = 0; i < new_cfg.img_path_count; i++) { + new_cfg.img_paths[i] = def->img_paths[i]; + def->img_paths[i] = NULL; } - char summary[256]; - snprintf (summary, sizeof summary, "[C3X] Natural wonder placement complete. Newly placed: %d, already present: %d.\n", - newly_placed, existing_count); - (*p_OutputDebugStringA) (summary); + if (! ensure_culture_variant_art (&new_cfg, section_start_line)) { + free_dynamic_district_config (&new_cfg); + return false; + } - for (int ni = 0; ni < wonder_count; ni++) - free (candidate_lists[ni].entries); - free (wonder_order); - free (candidate_lists); - free (already_placed); - free (placements); -} + new_cfg.img_column_count = clamp (1, MAX_DISTRICT_COLUMN_COUNT, def->has_img_column_count ? def->img_column_count : new_cfg.dependent_improvement_max_index + 1); + new_cfg.has_img_column_count_override = def->has_img_column_count; -int -find_district_index_by_name (char const * name) -{ - if ((name == NULL) || (name[0] == '\0')) - return -1; + if (reusing_existing) + new_cfg.command = preserved_command; + else + new_cfg.command = allocate_dynamic_district_command (new_cfg.name); - for (int i = 0; i < is->district_count; i++) { - char const * existing = is->district_configs[i].name; - if ((existing != NULL) && (strcmp (existing, name) == 0)) - return i; + struct district_config * dest_cfg = &is->district_configs[dest_index]; + if (reusing_existing) { + enum Unit_Command_Values saved_command = preserved_command; + free_dynamic_district_config (dest_cfg); + *dest_cfg = new_cfg; + dest_cfg->command = saved_command; + } else { + free_dynamic_district_config (dest_cfg); + *dest_cfg = new_cfg; + is->dynamic_district_count += 1; + is->district_count = is->special_district_count + is->dynamic_district_count; } - return -1; + return true; } -int -find_wonder_district_index_by_name (char const * name) +void +finalize_parsed_district_definition (struct parsed_district_definition * def, int section_start_line) { - if ((name == NULL) || (name[0] == '\0')) - return -1; - - int improv_id; - if (! stable_look_up (&is->building_name_to_id, (char *)name, &improv_id)) - return -1; - - return find_wonder_config_index_by_improvement_id (improv_id); -} + if ((! def->has_name) || (def->name == NULL)) + return; -int -find_natural_wonder_index_by_name (char const * name) -{ - if ((name == NULL) || (name[0] == '\0') || (is == NULL)) - return -1; + if (! override_special_district_from_definition (def, section_start_line)) + add_dynamic_district_from_definition (def, section_start_line); - for (int i = 0; i < is->natural_wonder_count; i++) { - char const * existing = is->natural_wonder_configs[i].name; - if ((existing != NULL) && (strcmp (existing, name) == 0)) - return i; - } - return -1; + free_parsed_district_definition (def); } -City * -find_city_by_name (char const * name) +void +handle_district_definition_key (struct parsed_district_definition * def, + struct string_slice const * key, + struct string_slice const * value, + int line_number, + struct error_line ** parse_errors, + struct error_line ** unrecognized_keys) { - if ((name == NULL) || (name[0] == '\0') || (p_cities == NULL) || (p_cities->Cities == NULL)) - return NULL; + if (slice_matches_str (key, "name")) { + if (def->name != NULL) { + free (def->name); + def->name = NULL; + } - for (int city_index = 0; city_index <= p_cities->LastIndex; city_index++) { - City * city = get_city_ptr (city_index); - if ((city != NULL) && (city->Body.CityName != NULL)) { - (*p_OutputDebugStringA) (city->Body.CityName); + char * name_copy = copy_trimmed_string_or_null (value, 1); + if (name_copy == NULL) { + def->has_name = false; + add_key_parse_error (parse_errors, line_number, key, value, "(value is required)"); + } else { + def->name = name_copy; + def->has_name = true; } - if ((city != NULL) && (city->Body.CityName != NULL) && (strcmp (city->Body.CityName, name) == 0)) - return city; - } - return NULL; -} + } else if (slice_matches_str (key, "display_name")) { + if (def->display_name != NULL) { + free (def->display_name); + def->display_name = NULL; + } + def->display_name = copy_trimmed_string_or_null (value, 1); + def->has_display_name = true; -void -init_scenario_district_entry (struct scenario_district_entry * entry) -{ - if (entry == NULL) - return; + } else if (slice_matches_str (key, "tooltip")) { + if (def->tooltip != NULL) { + free (def->tooltip); + def->tooltip = NULL; + } + def->tooltip = copy_trimmed_string_or_null (value, 1); + def->has_tooltip = true; - memset (entry, 0, sizeof *entry); -} + } else if (slice_matches_str (key, "advance_prereqs") || slice_matches_str (key, "advance_prereq")) { + for (int i = 0; i < def->advance_prereq_count; i++) { + if (def->advance_prereqs[i] != NULL) { + free (def->advance_prereqs[i]); + def->advance_prereqs[i] = NULL; + } + } + def->advance_prereq_count = 0; + char * value_text = trim_and_extract_slice (value, 0); + int list_count = 0; + if (parse_config_string_list (value_text, + def->advance_prereqs, + ARRAY_LEN (def->advance_prereqs), + &list_count, + parse_errors, + line_number, + "advance_prereqs")) { + def->advance_prereq_count = list_count; + def->has_advance_prereqs = true; + } else { + def->advance_prereq_count = 0; + def->has_advance_prereqs = false; + } + free (value_text); -void -free_scenario_district_entry (struct scenario_district_entry * entry) -{ - if (entry == NULL) - return; + } else if (slice_matches_str (key, "obsoleted_by")) { + if (def->obsoleted_by != NULL) { + free (def->obsoleted_by); + def->obsoleted_by = NULL; + } + def->obsoleted_by = copy_trimmed_string_or_null (value, 1); + def->has_obsoleted_by = true; - if (entry->district_name != NULL) { - free (entry->district_name); - entry->district_name = NULL; - } - if (entry->wonder_city_name != NULL) { - free (entry->wonder_city_name); - entry->wonder_city_name = NULL; - } - if (entry->wonder_name != NULL) { - free (entry->wonder_name); - entry->wonder_name = NULL; - } - entry->has_coordinates = 0; - entry->has_district_name = 0; - entry->has_wonder_city = 0; - entry->has_wonder_name = 0; -} + } else if (slice_matches_str (key, "resource_prereqs")) { + char * value_text = trim_and_extract_slice (value, 0); + int list_count = 0; + if (parse_config_string_list (value_text, + def->resource_prereqs, + ARRAY_LEN (def->resource_prereqs), + &list_count, + parse_errors, + line_number, + "resource_prereqs")) { + def->resource_prereq_count = list_count; + def->has_resource_prereqs = true; + } else { + def->resource_prereq_count = 0; + def->has_resource_prereqs = false; + } + free (value_text); -void -add_scenario_district_error (struct error_line ** parse_errors, - int line_number, - char const * message) -{ - if (message == NULL) - return; + } else if (slice_matches_str (key, "resource_prereq_on_tile")) { + if (def->resource_prereq_on_tile != NULL) { + free (def->resource_prereq_on_tile); + def->resource_prereq_on_tile = NULL; + } + def->resource_prereq_on_tile = copy_trimmed_string_or_null (value, 1); + def->has_resource_prereq_on_tile = true; - struct error_line * err = add_error_line (parse_errors); - snprintf (err->text, sizeof err->text, "^ Line %d: %s", line_number, message); - err->text[(sizeof err->text) - 1] = '\0'; -} + } else if (slice_matches_str (key, "wonder_prereqs")) { + char * value_text = trim_and_extract_slice (value, 0); + int list_count = 0; + if (parse_config_string_list (value_text, + def->wonder_prereqs, + ARRAY_LEN (def->wonder_prereqs), + &list_count, + parse_errors, + line_number, + "wonder_prereqs")) { + def->wonder_prereq_count = list_count; + def->has_wonder_prereqs = true; + } else { + def->wonder_prereq_count = 0; + def->has_wonder_prereqs = false; + } + free (value_text); -int -parse_scenario_district_coordinates (struct string_slice const * value, int * out_x, int * out_y) -{ - if ((value == NULL) || (out_x == NULL) || (out_y == NULL)) - return 0; + } else if (slice_matches_str (key, "natural_wonder_prereqs")) { + char * value_text = trim_and_extract_slice (value, 0); + int list_count = 0; + if (parse_config_string_list (value_text, + def->natural_wonder_prereqs, + ARRAY_LEN (def->natural_wonder_prereqs), + &list_count, + parse_errors, + line_number, + "natural_wonder_prereqs")) { + def->natural_wonder_prereq_count = list_count; + def->has_natural_wonder_prereqs = true; + } else { + def->natural_wonder_prereq_count = 0; + def->has_natural_wonder_prereqs = false; + } + free (value_text); - char * text = trim_and_extract_slice (value, 0); - if (text == NULL) - return 0; + } else if (slice_matches_str (key, "buildable_on_districts")) { + char * value_text = trim_and_extract_slice (value, 0); + int list_count = 0; + if (parse_config_string_list (value_text, + def->buildable_on_districts, + ARRAY_LEN (def->buildable_on_districts), + &list_count, + parse_errors, + line_number, + "buildable_on_districts")) { + def->buildable_on_district_count = list_count; + def->has_buildable_on_districts = true; + } else { + def->buildable_on_district_count = 0; + def->has_buildable_on_districts = false; + } + free (value_text); - char * cursor = text; - int success = 0; - int x, y; - if (parse_int (&cursor, &x) && - skip_punctuation (&cursor, ',') && - parse_int (&cursor, &y)) { - skip_horiz_space (&cursor); - success = (*cursor == '\0'); - if (success) { - *out_x = x; - *out_y = y; + } else if (slice_matches_str (key, "buildable_adjacent_to_districts")) { + char * value_text = trim_and_extract_slice (value, 0); + int list_count = 0; + if (parse_config_string_list (value_text, + def->buildable_adjacent_to_districts, + ARRAY_LEN (def->buildable_adjacent_to_districts), + &list_count, + parse_errors, + line_number, + "buildable_adjacent_to_districts")) { + def->buildable_adjacent_to_district_count = list_count; + def->has_buildable_adjacent_to_districts = true; + } else { + def->buildable_adjacent_to_district_count = 0; + def->has_buildable_adjacent_to_districts = false; } - } + free (value_text); - free (text); - return success; -} + } else if (slice_matches_str (key, "buildable_by_civs")) { + char * value_text = trim_and_extract_slice (value, 0); + int list_count = 0; + if (parse_config_string_list (value_text, + def->buildable_by_civs, + ARRAY_LEN (def->buildable_by_civs), + &list_count, + parse_errors, + line_number, + "buildable_by_civs")) { + def->buildable_by_civ_count = list_count; + def->has_buildable_by_civs = true; + } else { + def->buildable_by_civ_count = 0; + def->has_buildable_by_civs = false; + } + free (value_text); -int -finalize_scenario_district_entry (struct scenario_district_entry * entry, - int section_start_line, - struct error_line ** parse_errors) -{ - int success = 1; - if ((entry == NULL) || (parse_errors == NULL)) - return 0; + } else if (slice_matches_str (key, "buildable_by_civ_traits")) { + char * value_text = trim_and_extract_slice (value, 0); + int list_count = 0; + if (parse_config_string_list (value_text, + def->buildable_by_civ_traits, + ARRAY_LEN (def->buildable_by_civ_traits), + &list_count, + parse_errors, + line_number, + "buildable_by_civ_traits")) { + def->buildable_by_civ_traits_count = list_count; + def->buildable_by_civ_traits_id_count = 0; + for (int i = 0; i < def->buildable_by_civ_traits_count; i++) { + char const * trait_name = def->buildable_by_civ_traits[i]; + if ((trait_name == NULL) || (trait_name[0] == '\0')) + continue; + int trait_id = -1; + struct string_slice trait_slice = { .str = (char *)trait_name, .len = (int)strlen (trait_name) }; + if (find_civ_trait_id_by_name (&trait_slice, &trait_id)) { + bool already_listed = false; + for (int k = 0; k < def->buildable_by_civ_traits_id_count; k++) { + if (def->buildable_by_civ_traits_ids[k] == trait_id) { + already_listed = true; + break; + } + } + if ((! already_listed) && (def->buildable_by_civ_traits_id_count < ARRAY_LEN (def->buildable_by_civ_traits_ids))) + def->buildable_by_civ_traits_ids[def->buildable_by_civ_traits_id_count++] = trait_id; + } else { + struct error_line * err = add_error_line (parse_errors); + snprintf (err->text, sizeof err->text, "^ Line %d: buildable_by_civ_traits entry \"%.*s\" not found", line_number, trait_slice.len, trait_slice.str); + err->text[(sizeof err->text) - 1] = '\0'; + } + } - if (! entry->has_coordinates) - add_scenario_district_error (parse_errors, section_start_line, "coordinates (value is required)"); - if ((! entry->has_district_name) || (entry->district_name == NULL) || (entry->district_name[0] == '\0')) - add_scenario_district_error (parse_errors, section_start_line, "district (value is required)"); + for (int i = 0; i < def->buildable_by_civ_traits_count; i++) { + if (def->buildable_by_civ_traits[i] != NULL) { + free (def->buildable_by_civ_traits[i]); + def->buildable_by_civ_traits[i] = NULL; + } + } + def->buildable_by_civ_traits_count = 0; + def->has_buildable_by_civ_traits = true; + } else { + def->buildable_by_civ_traits_count = 0; + def->buildable_by_civ_traits_id_count = 0; + def->has_buildable_by_civ_traits = false; + } + free (value_text); - if ((! entry->has_coordinates) || (! entry->has_district_name) || - (entry->district_name == NULL) || (entry->district_name[0] == '\0')) - success = 0; + } else if (slice_matches_str (key, "buildable_by_civ_govs")) { + char * value_text = trim_and_extract_slice (value, 0); + int list_count = 0; + if (parse_config_string_list (value_text, + def->buildable_by_civ_govs, + ARRAY_LEN (def->buildable_by_civ_govs), + &list_count, + parse_errors, + line_number, + "buildable_by_civ_govs")) { + def->buildable_by_civ_govs_count = list_count; + def->buildable_by_civ_govs_id_count = 0; + for (int i = 0; i < def->buildable_by_civ_govs_count; i++) { + char const * gov_name = def->buildable_by_civ_govs[i]; + if ((gov_name == NULL) || (gov_name[0] == '\0')) + continue; + int gov_id = -1; + struct string_slice gov_slice = { .str = (char *)gov_name, .len = (int)strlen (gov_name) }; + if (find_game_object_id_by_name (GOK_GOVERNMENT, &gov_slice, 0, &gov_id)) { + bool already_listed = false; + for (int k = 0; k < def->buildable_by_civ_govs_id_count; k++) { + if (def->buildable_by_civ_govs_ids[k] == gov_id) { + already_listed = true; + break; + } + } + if ((! already_listed) && (def->buildable_by_civ_govs_id_count < ARRAY_LEN (def->buildable_by_civ_govs_ids))) + def->buildable_by_civ_govs_ids[def->buildable_by_civ_govs_id_count++] = gov_id; + } else { + struct error_line * err = add_error_line (parse_errors); + snprintf (err->text, sizeof err->text, "^ Line %d: buildable_by_civ_govs entry \"%.*s\" not found", line_number, gov_slice.len, gov_slice.str); + err->text[(sizeof err->text) - 1] = '\0'; + } + } - int map_x = entry->tile_x; - int map_y = entry->tile_y; - if (success) { - wrap_tile_coords (&p_bic_data->Map, &map_x, &map_y); - Tile * tile = tile_at (map_x, map_y); - if ((tile == NULL) || (tile == p_null_tile)) { - add_scenario_district_error (parse_errors, section_start_line, "Invalid coordinates (tile not found)"); - success = 0; + for (int i = 0; i < def->buildable_by_civ_govs_count; i++) { + if (def->buildable_by_civ_govs[i] != NULL) { + free (def->buildable_by_civ_govs[i]); + def->buildable_by_civ_govs[i] = NULL; + } + } + def->buildable_by_civ_govs_count = 0; + def->has_buildable_by_civ_govs = true; } else { - int district_id = find_district_index_by_name (entry->district_name); - if ((district_id < 0) || (district_id >= is->district_count)) { - char msg[200]; - snprintf (msg, sizeof msg, "district (unrecognized name: \"%s\")", entry->district_name); - add_scenario_district_error (parse_errors, section_start_line, msg); - success = 0; - } else { - struct district_instance * inst = ensure_district_instance (tile, district_id, map_x, map_y); - if (inst == NULL) { - add_scenario_district_error (parse_errors, section_start_line, "Failed to create district instance"); - success = 0; - } else { - inst->district_type = district_id; - district_instance_set_coords (inst, map_x, map_y); - inst->state = DS_COMPLETED; - inst->wonder_info.state = WDS_UNUSED; - inst->wonder_info.city = NULL; - inst->wonder_info.city_id = -1; - inst->wonder_info.wonder_index = -1; - inst->natural_wonder_info.natural_wonder_id = -1; + def->buildable_by_civ_govs_count = 0; + def->buildable_by_civ_govs_id_count = 0; + def->has_buildable_by_civ_govs = false; + } + free (value_text); - if (district_id == WONDER_DISTRICT_ID) { - int has_city = entry->has_wonder_city && - (entry->wonder_city_name != NULL) && (entry->wonder_city_name[0] != '\0'); - int has_wonder = entry->has_wonder_name && - (entry->wonder_name != NULL) && (entry->wonder_name[0] != '\0'); - if (! has_city || ! has_wonder) { - add_scenario_district_error (parse_errors, section_start_line, "Wonder district requires both wonder_city and wonder_name"); - success = 0; - } else { - int wonder_index = find_wonder_district_index_by_name (entry->wonder_name); - if (wonder_index < 0) { - char msg[200]; - snprintf (msg, sizeof msg, "wonder_name (unrecognized wonder: \"%s\")", entry->wonder_name); - add_scenario_district_error (parse_errors, section_start_line, msg); - success = 0; - } else { - inst->wonder_info.city = NULL; - inst->wonder_info.city_id = -1; - inst->wonder_info.state = WDS_COMPLETED; - inst->wonder_info.wonder_index = wonder_index; - } - } - } else if (district_id == NATURAL_WONDER_DISTRICT_ID) { - int has_name = entry->has_wonder_name && - (entry->wonder_name != NULL) && (entry->wonder_name[0] != '\0'); - if (! has_name) { - add_scenario_district_error (parse_errors, section_start_line, "Natural Wonder district requires wonder_name"); - success = 0; - } else { - int natural_index = find_natural_wonder_index_by_name (entry->wonder_name); - if (natural_index < 0) { - char msg[200]; - snprintf (msg, sizeof msg, "wonder_name (unrecognized natural wonder: \"%s\")", entry->wonder_name); - add_scenario_district_error (parse_errors, section_start_line, msg); - success = 0; - } else - inst->natural_wonder_info.natural_wonder_id = natural_index; + } else if (slice_matches_str (key, "buildable_by_civ_cultures")) { + char * value_text = trim_and_extract_slice (value, 0); + int list_count = 0; + if (parse_config_string_list (value_text, + def->buildable_by_civ_cultures, + ARRAY_LEN (def->buildable_by_civ_cultures), + &list_count, + parse_errors, + line_number, + "buildable_by_civ_cultures")) { + def->buildable_by_civ_cultures_count = list_count; + def->buildable_by_civ_cultures_id_count = 0; + for (int i = 0; i < def->buildable_by_civ_cultures_count; i++) { + char const * culture_name = def->buildable_by_civ_cultures[i]; + if ((culture_name == NULL) || (culture_name[0] == '\0')) + continue; + int culture_id = -1; + struct string_slice culture_slice = { .str = (char *)culture_name, .len = (int)strlen (culture_name) }; + if (find_civ_culture_id_by_name (&culture_slice, &culture_id)) { + bool already_listed = false; + for (int k = 0; k < def->buildable_by_civ_cultures_id_count; k++) { + if (def->buildable_by_civ_cultures_ids[k] == culture_id) { + already_listed = true; + break; } - if (entry->has_wonder_city) - add_scenario_district_error (parse_errors, section_start_line, "wonder_city ignored for Natural Wonder district entries"); - } else if (entry->has_wonder_city || entry->has_wonder_name) { - add_scenario_district_error (parse_errors, section_start_line, "wonder_* fields only valid for Wonder or Natural Wonder district entries"); } + if ((! already_listed) && (def->buildable_by_civ_cultures_id_count < ARRAY_LEN (def->buildable_by_civ_cultures_ids))) + def->buildable_by_civ_cultures_ids[def->buildable_by_civ_cultures_id_count++] = culture_id; + } else { + struct error_line * err = add_error_line (parse_errors); + snprintf (err->text, sizeof err->text, "^ Line %d: buildable_by_civ_cultures entry \"%.*s\" not found", line_number, culture_slice.len, culture_slice.str); + err->text[(sizeof err->text) - 1] = '\0'; + } + } - if (success) { - if (district_id != NATURAL_WONDER_DISTRICT_ID && !tile->vtable->m18_Check_Mines (tile, __, 0)) - tile->vtable->m56_Set_Tile_Flags (tile, __, 0, TILE_FLAG_MINE, map_x, map_y); - set_tile_unworkable_for_all_cities (tile, map_x, map_y); - } + for (int i = 0; i < def->buildable_by_civ_cultures_count; i++) { + if (def->buildable_by_civ_cultures[i] != NULL) { + free (def->buildable_by_civ_cultures[i]); + def->buildable_by_civ_cultures[i] = NULL; } } + def->buildable_by_civ_cultures_count = 0; + def->has_buildable_by_civ_cultures = true; + } else { + def->buildable_by_civ_cultures_count = 0; + def->buildable_by_civ_cultures_id_count = 0; + def->has_buildable_by_civ_cultures = false; } - } - - free_scenario_district_entry (entry); - init_scenario_district_entry (entry); - return success; -} + free (value_text); -void -handle_scenario_district_key (struct scenario_district_entry * entry, - struct string_slice const * key, - struct string_slice const * value, - int line_number, - struct error_line ** parse_errors, - struct error_line ** unrecognized_keys) -{ - if ((entry == NULL) || (key == NULL) || (value == NULL)) - return; + } else if (slice_matches_str (key, "buildable_by_war_allies")) { + struct string_slice val_slice = *value; + int ival; + if (read_int (&val_slice, &ival)) { + def->buildable_by_war_allies = (ival != 0); + def->has_buildable_by_war_allies = true; + } else + add_key_parse_error (parse_errors, line_number, key, value, "(expected integer)"); - if (slice_matches_str (key, "coordinates")) { - int x, y; - if (parse_scenario_district_coordinates (value, &x, &y)) { - entry->tile_x = x; - entry->tile_y = y; - entry->has_coordinates = 1; + } else if (slice_matches_str (key, "buildable_by_pact_allies")) { + struct string_slice val_slice = *value; + int ival; + if (read_int (&val_slice, &ival)) { + def->buildable_by_pact_allies = (ival != 0); + def->has_buildable_by_pact_allies = true; } else - add_scenario_district_error (parse_errors, line_number, "coordinates (expected format: x,y)"); + add_key_parse_error (parse_errors, line_number, key, value, "(expected integer)"); - } else if (slice_matches_str (key, "district")) { - if (entry->district_name != NULL) { - free (entry->district_name); - entry->district_name = NULL; + } else if (slice_matches_str (key, "img_paths")) { + char * value_text = trim_and_extract_slice (value, 0); + int list_count = 0; + if (parse_config_string_list (value_text, + def->img_paths, + ARRAY_LEN (def->img_paths), + &list_count, + parse_errors, + line_number, + "img_paths")) { + def->img_path_count = list_count; + def->has_img_paths = true; + } else { + def->img_path_count = 0; + def->has_img_paths = false; } - entry->district_name = copy_trimmed_string_or_null (value, 1); - entry->has_district_name = (entry->district_name != NULL); - if (! entry->has_district_name) - add_scenario_district_error (parse_errors, line_number, "district (value is required)"); + free (value_text); - } else if (slice_matches_str (key, "wonder_city")) { - if (entry->wonder_city_name != NULL) { - free (entry->wonder_city_name); - entry->wonder_city_name = NULL; - } - entry->wonder_city_name = copy_trimmed_string_or_null (value, 1); - entry->has_wonder_city = (entry->wonder_city_name != NULL); + } else if (slice_matches_str (key, "img_column_count")) { + struct string_slice val_slice = *value; + int ival; + if (read_int (&val_slice, &ival)) { + def->img_column_count = ival; + def->has_img_column_count = true; + } else + add_key_parse_error (parse_errors, line_number, key, value, "(expected integer)"); - } else if (slice_matches_str (key, "wonder_name")) { - if (entry->wonder_name != NULL) { - free (entry->wonder_name); - entry->wonder_name = NULL; + } else if (slice_matches_str (key, "dependent_improvs")) { + char * value_text = trim_and_extract_slice (value, 0); + int list_count = 0; + if (parse_config_string_list (value_text, + def->dependent_improvements, + ARRAY_LEN (def->dependent_improvements), + &list_count, + parse_errors, + line_number, + "dependent_improvs")) { + def->dependent_improvement_max_index = list_count; + def->has_dependent_improvements = true; + } else { + def->dependent_improvement_max_index = 0; + def->has_dependent_improvements = false; } - entry->wonder_name = copy_trimmed_string_or_null (value, 1); - entry->has_wonder_name = (entry->wonder_name != NULL); - - } else - add_unrecognized_key_error (unrecognized_keys, line_number, key); -} + free (value_text); -// Parses a .c3x.txt file corresponding to the given scenario file path, loading district instances as specified. -// Attempts the scenario_path first, then scenario_config_path; if neither yields a readable file, no action is taken. -// -// The expected file format itself is very simple. Example: -// -// ``` -// DISTRICTS -// -// #District -// coordinates = 12,28 -// district = Entertainment Complex -// -// #District -// coordinates = 9,23 -// district = Wonder District -// wonder_city = Rome -// wonder_name = The Pyramids -// -// #District -// coordinates = 10,30 -// district = Natural Wonder -// wonder_name = Mount Everest -// ``` -// -// Details at https://github.com/instafluff0/Civ3_Editor_Fork_for_C3X_Districts -void -load_scenario_districts_from_file () -{ - char * scenario_filename = "scenario.districts.txt"; - char * scenario_districts_path = BIC_get_asset_path (p_bic_data, __, scenario_filename, false); + } else if (slice_matches_str (key, "buildable_on")) { + unsigned int mask; + if (parse_buildable_square_type_mask (value, &mask, parse_errors, line_number, "buildable_on", NULL)) { + def->buildable_square_types_mask = mask; + def->has_buildable_on = true; + } - // BIC_get_asset_path returns the file name when it can't find the file - if ((scenario_districts_path == NULL) || (0 == strcmp (scenario_filename, scenario_districts_path))) - return; + } else if (slice_matches_str (key, "buildable_on_overlays")) { + unsigned int mask; + if (parse_buildable_overlay_mask (value, &mask, parse_errors, line_number, "buildable_on_overlays")) { + def->buildable_on_overlays_mask = mask; + def->has_buildable_on_overlays = true; + } else { + def->has_buildable_on_overlays = false; + } - char * text = file_to_string (scenario_districts_path); - if (text == NULL) - return; + } else if (slice_matches_str (key, "buildable_only_on_overlays")) { + unsigned int mask; + if (parse_buildable_overlay_mask (value, &mask, parse_errors, line_number, "buildable_only_on_overlays")) { + def->buildable_only_on_overlays_mask = mask; + def->has_buildable_only_on_overlays = true; + } else { + def->has_buildable_only_on_overlays = false; + } - struct scenario_district_entry entry; - init_scenario_district_entry (&entry); - int in_section = 0; - int section_start_line = 0; - int line_number = 0; - int header_seen = 0; - struct error_line * unrecognized_keys = NULL; - struct error_line * parse_errors = NULL; + } else if (slice_matches_str (key, "buildable_adjacent_to")) { + unsigned int mask; + def->buildable_adjacent_to_allows_city = false; + if (parse_buildable_square_type_mask (value, &mask, parse_errors, line_number, "buildable_adjacent_to", &def->buildable_adjacent_to_allows_city)) { + def->buildable_adjacent_to_square_types_mask = mask; + def->has_buildable_adjacent_to = true; + } - char * cursor = text; - while (*cursor != '\0') { - line_number += 1; + } else if (slice_matches_str (key, "buildable_adjacent_to_overlays")) { + unsigned int mask; + if (parse_buildable_overlay_mask (value, &mask, parse_errors, line_number, "buildable_adjacent_to_overlays")) { + def->buildable_adjacent_to_overlays_mask = mask; + def->has_buildable_adjacent_to_overlays = true; + } else { + def->has_buildable_adjacent_to_overlays = false; + } - char * line_start = cursor; - char * line_end = cursor; - while ((*line_end != '\0') && (*line_end != '\n')) - line_end++; + } else if (slice_matches_str (key, "allow_multiple")) { + struct string_slice val_slice = *value; + int ival; + if (read_int (&val_slice, &ival)) { + def->allow_multiple = (ival != 0); + def->has_allow_multiple = true; + } else + add_key_parse_error (parse_errors, line_number, key, value, "(expected integer)"); - int line_len = line_end - line_start; - int has_newline = (*line_end == '\n'); - if (has_newline) - *line_end = '\0'; + } else if (slice_matches_str (key, "vary_img_by_era")) { + struct string_slice val_slice = *value; + int ival; + if (read_int (&val_slice, &ival)) { + def->vary_img_by_era = (ival != 0); + def->has_vary_img_by_era = true; + } else + add_key_parse_error (parse_errors, line_number, key, value, "(expected integer)"); - struct string_slice line_slice = { .str = line_start, .len = line_len }; - struct string_slice trimmed = trim_string_slice (&line_slice, 0); - if ((trimmed.len == 0) || (trimmed.str[0] == ';')) { - cursor = has_newline ? line_end + 1 : line_end; - continue; + } else if (slice_matches_str (key, "vary_img_by_culture")) { + struct string_slice val_slice = *value; + int ival; + if (read_int (&val_slice, &ival)) { + def->vary_img_by_culture = (ival != 0); + def->has_vary_img_by_culture = true; + } else + add_key_parse_error (parse_errors, line_number, key, value, "(expected integer)"); + + } else if (slice_matches_str (key, "render_strategy")) { + char * strategy = copy_trimmed_string_or_null (value, 1); + if (strategy == NULL) { + def->has_render_strategy = false; + add_key_parse_error (parse_errors, line_number, key, value, "(value is required)"); + } else if (strcmp (strategy, "by-count") == 0) { + def->render_strategy = DRS_BY_COUNT; + def->has_render_strategy = true; + } else if (strcmp (strategy, "by-building") == 0) { + def->render_strategy = DRS_BY_BUILDING; + def->has_render_strategy = true; + } else { + def->has_render_strategy = false; + add_key_parse_error (parse_errors, line_number, key, value, "(expected \"by-count\" or \"by-building\")"); + } + if (strategy != NULL) + free (strategy); + + } else if (slice_matches_str (key, "ai_build_strategy")) { + char * strategy = copy_trimmed_string_or_null (value, 1); + if (strategy == NULL) { + def->has_ai_build_strategy = false; + add_key_parse_error (parse_errors, line_number, key, value, "(value is required)"); + } else if (strcmp (strategy, "district") == 0) { + def->ai_build_strategy = DABS_DISTRICT; + def->has_ai_build_strategy = true; + } else if (strcmp (strategy, "tile-improvement") == 0) { + def->ai_build_strategy = DABS_TILE_IMPROVEMENT; + def->has_ai_build_strategy = true; + } else { + def->has_ai_build_strategy = false; + add_key_parse_error (parse_errors, line_number, key, value, "(expected \"district\" or \"tile-improvement\")"); } + if (strategy != NULL) + free (strategy); - if (! header_seen) { - if (slice_matches_str (&trimmed, "DISTRICTS")) { - header_seen = 1; - cursor = has_newline ? line_end + 1 : line_end; - continue; + } else if (slice_matches_str (key, "align_to_coast")) { + struct string_slice val_slice = *value; + int ival; + if (read_int (&val_slice, &ival)) { + def->align_to_coast = (ival != 0); + def->has_align_to_coast = true; + } else + add_key_parse_error (parse_errors, line_number, key, value, "(expected integer)"); + + } else if (slice_matches_str (key, "draw_over_resources")) { + struct string_slice val_slice = *value; + int ival; + if (read_int (&val_slice, &ival)) { + def->draw_over_resources = (ival != 0); + def->has_draw_over_resources = true; + } else + add_key_parse_error (parse_errors, line_number, key, value, "(expected integer)"); + + } else if (slice_matches_str (key, "allow_irrigation_from")) { + struct string_slice val_slice = *value; + int ival; + if (read_int (&val_slice, &ival)) { + def->allow_irrigation_from = (ival != 0); + def->has_allow_irrigation_from = true; + } else + add_key_parse_error (parse_errors, line_number, key, value, "(expected integer)"); + + } else if (slice_matches_str (key, "auto_add_road")) { + struct string_slice val_slice = *value; + int ival; + if (read_int (&val_slice, &ival)) { + def->auto_add_road = (ival != 0); + def->has_auto_add_road = true; + } else + add_key_parse_error (parse_errors, line_number, key, value, "(expected integer)"); + + } else if (slice_matches_str (key, "auto_add_railroad")) { + struct string_slice val_slice = *value; + int ival; + if (read_int (&val_slice, &ival)) { + def->auto_add_railroad = (ival != 0); + def->has_auto_add_railroad = true; + } else + add_key_parse_error (parse_errors, line_number, key, value, "(expected integer)"); + + } else if (slice_matches_str (key, "custom_width")) { + struct string_slice val_slice = *value; + int ival; + if (read_int (&val_slice, &ival)) { + def->custom_width = ival; + def->has_custom_width = true; + } else + add_key_parse_error (parse_errors, line_number, key, value, "(expected integer)"); + + } else if (slice_matches_str (key, "custom_height")) { + struct string_slice val_slice = *value; + int ival; + if (read_int (&val_slice, &ival)) { + def->custom_height = ival; + def->has_custom_height = true; + } else + add_key_parse_error (parse_errors, line_number, key, value, "(expected integer)"); + + } else if (slice_matches_str (key, "x_offset")) { + struct string_slice val_slice = *value; + int ival; + if (read_int (&val_slice, &ival)) { + def->x_offset = ival; + def->has_x_offset = true; + } else + add_key_parse_error (parse_errors, line_number, key, value, "(expected integer)"); + + } else if (slice_matches_str (key, "y_offset")) { + struct string_slice val_slice = *value; + int ival; + if (read_int (&val_slice, &ival)) { + def->y_offset = ival; + def->has_y_offset = true; + } else + add_key_parse_error (parse_errors, line_number, key, value, "(expected integer)"); + + } else if (slice_matches_str (key, "btn_tile_sheet_column")) { + struct string_slice val_slice = *value; + int ival; + if (read_int (&val_slice, &ival)) { + def->btn_tile_sheet_column = ival; + def->has_btn_tile_sheet_column = true; + } else + add_key_parse_error (parse_errors, line_number, key, value, "(expected integer)"); + + } else if (slice_matches_str (key, "btn_tile_sheet_row")) { + struct string_slice val_slice = *value; + int ival; + if (read_int (&val_slice, &ival)) { + def->btn_tile_sheet_row = ival; + def->has_btn_tile_sheet_row = true; + } else + add_key_parse_error (parse_errors, line_number, key, value, "(expected integer)"); + + } else if (slice_matches_str (key, "defense_bonus_percent")) { + if (parse_district_bonus_entries (value, &def->defense_bonus_percent, &def->defense_bonus_extras, parse_errors, line_number, key)) { + def->has_defense_bonus_percent = true; + } else { + def->has_defense_bonus_percent = false; + } + + } else if (slice_matches_str (key, "heal_units_in_one_turn")) { + struct string_slice val_slice = *value; + int ival; + if (read_int (&val_slice, &ival)) { + def->heal_units_in_one_turn = (ival != 0); + def->has_heal_units_in_one_turn = true; + } else + add_key_parse_error (parse_errors, line_number, key, value, "(expected integer)"); + + } else if (slice_matches_str (key, "culture_bonus")) { + if (parse_district_bonus_entries (value, &def->culture_bonus, &def->culture_bonus_extras, parse_errors, line_number, key)) { + def->has_culture_bonus = true; + } else { + def->has_culture_bonus = false; + } + + } else if (slice_matches_str (key, "science_bonus")) { + if (parse_district_bonus_entries (value, &def->science_bonus, &def->science_bonus_extras, parse_errors, line_number, key)) { + def->has_science_bonus = true; + } else { + def->has_science_bonus = false; + } + + } else if (slice_matches_str (key, "food_bonus")) { + if (parse_district_bonus_entries (value, &def->food_bonus, &def->food_bonus_extras, parse_errors, line_number, key)) { + def->has_food_bonus = true; + } else { + def->has_food_bonus = false; + } + + } else if (slice_matches_str (key, "gold_bonus")) { + if (parse_district_bonus_entries (value, &def->gold_bonus, &def->gold_bonus_extras, parse_errors, line_number, key)) { + def->has_gold_bonus = true; + } else { + def->has_gold_bonus = false; + } + + } else if (slice_matches_str (key, "shield_bonus")) { + if (parse_district_bonus_entries (value, &def->shield_bonus, &def->shield_bonus_extras, parse_errors, line_number, key)) { + def->has_shield_bonus = true; + } else { + def->has_shield_bonus = false; + } + + } else if (slice_matches_str (key, "happiness_bonus")) { + if (parse_district_bonus_entries (value, &def->happiness_bonus, &def->happiness_bonus_extras, parse_errors, line_number, key)) { + def->has_happiness_bonus = true; + } else { + def->has_happiness_bonus = false; + } + + } else if (slice_matches_str (key, "generated_resource")) { + if (def->generated_resource != NULL) { + free (def->generated_resource); + def->generated_resource = NULL; + } + def->generated_resource_flags = 0; + + char * value_text = trim_and_extract_slice (value, 0); + if ((value_text == NULL) || (*value_text == '\0')) { + def->generated_resource = NULL; + def->has_generated_resource = true; + } else { + char * cursor = value_text; + struct string_slice resource_name = {0}; + struct string_slice token; + bool ok = true; + while (skip_white_space (&cursor) && parse_string (&cursor, &token)) { + if (slice_matches_str (&token, "local")) + def->generated_resource_flags |= MF_LOCAL; + else if (slice_matches_str (&token, "no-tech-req")) + def->generated_resource_flags |= MF_NO_TECH_REQ; + else if (slice_matches_str (&token, "yields")) + def->generated_resource_flags |= MF_YIELDS; + else if (resource_name.str == NULL) + resource_name = token; + else { + ok = false; + break; + } + } + + if (! ok || (resource_name.str == NULL)) { + def->generated_resource = NULL; + def->has_generated_resource = false; + def->generated_resource_flags = 0; + add_key_parse_error (parse_errors, line_number, key, value, + "(expected resource name plus optional flags: local, yields, no-tech-req)"); } else { - add_scenario_district_error (&parse_errors, line_number, "Expected \"DISTRICTS\" header"); - header_seen = 1; - // Fall through to allow parsing of entries even if header missing. + def->generated_resource = extract_slice (&resource_name); + def->has_generated_resource = true; } } + free (value_text); + + } else + add_unrecognized_key_error (unrecognized_keys, line_number, key); +} + +bool +line_is_empty_or_comment (struct string_slice const * trimmed) +{ + return ((trimmed == NULL) || (trimmed->len == 0) || (trimmed->str[0] == ';') || (trimmed->str[0] == '[')); +} + +void +load_dynamic_district_config_file (char const * file_path, + int path_is_relative_to_mod_dir, + int log_missing, + int drop_existing_configs) +{ + char path[MAX_PATH]; + if (path_is_relative_to_mod_dir) { + if (is->mod_rel_dir == NULL) + return; + snprintf (path, sizeof path, "%s\\%s", is->mod_rel_dir, file_path); + } else { + strncpy (path, file_path, sizeof path); + } + path[(sizeof path) - 1] = '\0'; + + char * text = file_to_string (path); + if (text == NULL) { + if (log_missing) { + char ss[256]; + snprintf (ss, sizeof ss, "[C3X] Districts config file not found: %s", path); + (*p_OutputDebugStringA) (ss); + } + return; + } + + if (drop_existing_configs) + reset_regular_district_configs (); + + struct parsed_district_definition def; + init_parsed_district_definition (&def); + bool in_section = false; + int section_start_line = 0; + int line_number = 0; + struct error_line * unrecognized_keys = NULL; + struct error_line * parse_errors = NULL; + + char * cursor = text; + while (*cursor != '\0') { + line_number += 1; + + char * line_start = cursor; + char * line_end = cursor; + while ((*line_end != '\0') && (*line_end != '\n')) + line_end++; + + int line_len = line_end - line_start; + bool has_newline = (*line_end == '\n'); + if (has_newline) + *line_end = '\0'; + + struct string_slice line_slice = { .str = line_start, .len = line_len }; + struct string_slice trimmed = trim_string_slice (&line_slice, 0); + if (line_is_empty_or_comment (&trimmed)) { + cursor = has_newline ? line_end + 1 : line_end; + continue; + } if (trimmed.str[0] == '#') { struct string_slice directive = trimmed; directive.str += 1; directive.len -= 1; directive = trim_string_slice (&directive, 0); - if (slice_matches_str (&directive, "District")) { + if ((directive.len > 0) && slice_matches_str (&directive, "District")) { if (in_section) - finalize_scenario_district_entry (&entry, section_start_line, &parse_errors); - in_section = 1; + finalize_parsed_district_definition (&def, section_start_line); + in_section = true; section_start_line = line_number; - free_scenario_district_entry (&entry); - init_scenario_district_entry (&entry); } cursor = has_newline ? line_end + 1 : line_end; continue; @@ -7187,25 +9459,31 @@ load_scenario_districts_from_file () struct string_slice key_slice = {0}; struct string_slice value_slice = {0}; - switch (parse_trimmed_key_value (&trimmed, &key_slice, &value_slice)) { - case KVP_NO_EQUALS: - add_scenario_district_error (&parse_errors, line_number, "(expected '=')"); - break; - case KVP_EMPTY_KEY: - add_scenario_district_error (&parse_errors, line_number, "(missing key)"); - break; - case KVP_SUCCESS: - handle_scenario_district_key (&entry, &key_slice, &value_slice, line_number, &parse_errors, &unrecognized_keys); - break; + enum key_value_parse_status status = parse_trimmed_key_value (&trimmed, &key_slice, &value_slice); + if (status == KVP_NO_EQUALS) { + char * line_text = extract_slice (&trimmed); + struct error_line * err = add_error_line (&parse_errors); + snprintf (err->text, sizeof err->text, "^ Line %d: %s (expected '=')", line_number, line_text); + err->text[(sizeof err->text) - 1] = '\0'; + free (line_text); + cursor = has_newline ? line_end + 1 : line_end; + continue; + } else if (status == KVP_EMPTY_KEY) { + struct error_line * err = add_error_line (&parse_errors); + snprintf (err->text, sizeof err->text, "^ Line %d: (missing key)", line_number); + err->text[(sizeof err->text) - 1] = '\0'; + cursor = has_newline ? line_end + 1 : line_end; + continue; } + handle_district_definition_key (&def, &key_slice, &value_slice, line_number, &parse_errors, &unrecognized_keys); cursor = has_newline ? line_end + 1 : line_end; } if (in_section) - finalize_scenario_district_entry (&entry, section_start_line, &parse_errors); + finalize_parsed_district_definition (&def, section_start_line); - free_scenario_district_entry (&entry); + free_parsed_district_definition (&def); free (text); // Append to loaded config names list @@ -7214,7490 +9492,11531 @@ load_scenario_districts_from_file () top_lcn = top_lcn->next; struct loaded_config_name * new_lcn = malloc (sizeof *new_lcn); - new_lcn->name = strdup (scenario_districts_path); + new_lcn->name = strdup (path); new_lcn->next = NULL; top_lcn->next = new_lcn; + snprintf (is->current_districts_config_path, sizeof is->current_districts_config_path, path); - if ((parse_errors != NULL) || (unrecognized_keys != NULL)) { + if (parse_errors != NULL || unrecognized_keys != NULL) { PopupForm * popup = get_popup_form (); popup->vtable->set_text_key_and_flags (popup, __, is->mod_script_path, "C3X_WARNING", -1, 0, 0, 0); - char header[256]; - snprintf (header, sizeof header, "District scenario file issues in %s:", scenario_districts_path); - header[(sizeof header) - 1] = '\0'; - PopupForm_add_text (popup, __, header, 0); - if (parse_errors != NULL) + char s[200]; + snprintf (s, sizeof s, "District Config errors in %s:", path); + s[(sizeof s) - 1] = '\0'; + PopupForm_add_text (popup, __, s, false); + if (parse_errors != NULL) { for (struct error_line * line = parse_errors; line != NULL; line = line->next) - PopupForm_add_text (popup, __, line->text, 0); + PopupForm_add_text (popup, __, line->text, false); + } if (unrecognized_keys != NULL) { - PopupForm_add_text (popup, __, "", 0); - PopupForm_add_text (popup, __, "Unrecognized keys:", 0); + PopupForm_add_text (popup, __, "", false); + PopupForm_add_text (popup, __, "Unrecognized keys:", false); for (struct error_line * line = unrecognized_keys; line != NULL; line = line->next) - PopupForm_add_text (popup, __, line->text, 0); + PopupForm_add_text (popup, __, line->text, false); } patch_show_popup (popup, __, 0, 0); + free_error_lines (parse_errors); + free_error_lines (unrecognized_keys); } - - free_error_lines (parse_errors); - free_error_lines (unrecognized_keys); } void -deinit_district_images (void) +load_dynamic_district_configs () { - if (is->dc_img_state == IS_OK) { - for (int dc = 0; dc < COUNT_DISTRICT_TYPES; dc++) { - for (int variant = 0; variant < ARRAY_LEN (is->district_img_sets[dc].imgs); variant++) - for (int era = 0; era < 4; era++) - for (int col = 0; col < ARRAY_LEN (is->district_img_sets[dc].imgs[variant][era]); col++) { - Sprite * sprite = &is->district_img_sets[dc].imgs[variant][era][col]; - if (sprite->vtable != NULL) - sprite->vtable->destruct (sprite, __, 0); - } - } - - for (int wi = 0; wi < MAX_WONDER_DISTRICT_TYPES; wi++) { - struct wonder_district_image_set * set = &is->wonder_district_img_sets[wi]; - if (set->img.vtable != NULL) - set->img.vtable->destruct (&set->img, __, 0); - if (set->construct_img.vtable != NULL) - set->construct_img.vtable->destruct (&set->construct_img, __, 0); - } - - for (int ni = 0; ni < MAX_NATURAL_WONDER_DISTRICT_TYPES; ni++) { - Sprite * sprite = &is->natural_wonder_img_sets[ni].img; - if (sprite->vtable != NULL) - sprite->vtable->destruct (sprite, __, 0); - } - } + load_dynamic_district_config_file ("default.districts_config.txt", 1, 1, 1); + load_dynamic_district_config_file ("user.districts_config.txt", 1, 0, 1); - is->dc_img_state = IS_UNINITED; + + char * scenario_filename = "scenario.districts_config.txt"; + char * scenario_district_config_path = BIC_get_asset_path (p_bic_data, __, scenario_filename, false); + if ((scenario_district_config_path != NULL) && (0 != strcmp (scenario_filename, scenario_district_config_path))) + load_dynamic_district_config_file (scenario_district_config_path, 0, 0, 1); } void -clear_highlighted_worker_tiles_for_districts () +init_parsed_wonder_definition (struct parsed_wonder_definition * def) { - FOR_TABLE_ENTRIES (tei, &is->highlighted_city_radius_tile_pointers) { - struct highlighted_city_radius_tile_info * info = (struct highlighted_city_radius_tile_info *)(long)tei.value; - if (info != NULL) - free (info); - } - table_deinit (&is->highlighted_city_radius_tile_pointers); + memset (def, 0, sizeof *def); + def->buildable_square_types_mask = district_default_buildable_mask (); + def->buildable_only_on_overlays_mask = 0; + def->buildable_adjacent_to_square_types_mask = 0; + def->buildable_adjacent_to_overlays_mask = 0; + def->buildable_adjacent_to_allows_city = false; } - void -reset_district_state (bool reset_tile_map) +free_parsed_wonder_definition (struct parsed_wonder_definition * def) { - clear_all_tracked_workers (); - deinit_district_images (); - clear_highlighted_worker_tiles_for_districts (); - - table_deinit (&is->district_tech_prereqs); - table_deinit (&is->district_building_prereqs); - table_deinit (&is->command_id_to_district_id); - stable_deinit (&is->building_name_to_id); - if (reset_tile_map) { - FOR_TABLE_ENTRIES (tei, &is->district_tile_map) { - struct district_instance * inst = (struct district_instance *)(long)tei.value; - if (inst != NULL) - free (inst); - } - table_deinit (&is->district_tile_map); + if (def->name != NULL) { + free (def->name); + def->name = NULL; } - clear_distribution_hub_tables (); - - is->distribution_hub_totals_dirty = true; - - clear_dynamic_district_definitions (); - is->district_count = is->special_district_count; - - for (int i = 0; i < COUNT_DISTRICT_TYPES; i++) { - is->district_infos[i].advance_prereq_id = -1; - is->district_infos[i].dependent_building_count = 0; - for (int j = 0; j < ARRAY_LEN (is->district_infos[i].dependent_building_ids); j++) - is->district_infos[i].dependent_building_ids[j] = -1; + if (def->img_path != NULL) { + free (def->img_path); + def->img_path = NULL; } - for (int civ_id = 0; civ_id < 32; civ_id++) { - FOR_TABLE_ENTRIES (tei, &is->city_pending_district_requests[civ_id]) { - struct pending_district_request * req = (struct pending_district_request *)(long)tei.value; - if (req != NULL) - free (req); - } - table_deinit (&is->city_pending_district_requests[civ_id]); - } - table_deinit (&is->city_pending_building_orders); + init_parsed_wonder_definition (def); } -void -clear_city_district_request (City * city, int district_id) +bool +add_dynamic_wonder_from_definition (struct parsed_wonder_definition * def, int section_start_line) { - if (! is->current_config.enable_districts || - (city == NULL) || - (district_id < 0) || (district_id >= is->district_count)) - return; - - struct pending_district_request * req = find_pending_district_request (city, district_id); - if (req == NULL) - return; - - remove_pending_district_request (req); - - int pending_improv_id; - if (lookup_pending_building_order (city, &pending_improv_id)) { - int required_district_id; - if (city_requires_district_for_improvement (city, pending_improv_id, &required_district_id)) { - if (required_district_id == district_id) - forget_pending_building_order (city); + int existing_index = -1; + for (int i = 0; i < is->wonder_district_count; i++) { + if ((is->wonder_district_configs[i].wonder_name != NULL) && + (strcmp (is->wonder_district_configs[i].wonder_name, def->name) == 0)) { + existing_index = i; + break; } } -} - - -bool -tile_suitable_for_district (Tile * tile, int district_id, City * city, bool * out_has_resource) -{ - bool has_resource = false; - if ((tile != NULL) && (tile != p_null_tile)) - has_resource = tile_has_resource (tile); - if (out_has_resource != NULL) - *out_has_resource = has_resource; - if ((tile == NULL) || (tile == p_null_tile)) - return false; - if (tile->CityID >= 0) - return false; - if (tile->vtable->m38_Get_Territory_OwnerID (tile) != city->Body.CivID) - return false; - if (tile->vtable->m35_Check_Is_Water (tile)) - return false; - enum SquareTypes base_type = tile->vtable->m50_Get_Square_BaseType (tile); - if ((base_type == SQ_Mountains) || (base_type == SQ_Volcano)) + int dest = (existing_index >= 0) ? existing_index : is->wonder_district_count; + if ((dest < 0) || (dest >= MAX_WONDER_DISTRICT_TYPES)) return false; - struct district_instance * inst = get_district_instance (tile); - if (inst != NULL) { - if (inst->district_type != district_id) - return false; - if (district_id == WONDER_DISTRICT_ID && inst->state == DS_COMPLETED) { - struct wonder_district_info * info = &inst->wonder_info; - if (info->state == WDS_COMPLETED) - return false; - } + struct wonder_district_config new_cfg; + memset (&new_cfg, 0, sizeof new_cfg); + new_cfg.index = dest; + new_cfg.is_dynamic = true; + new_cfg.wonder_name = strdup (def->name); + new_cfg.img_path = (def->img_path != NULL) ? strdup (def->img_path) : strdup ("Wonders.pcx"); + new_cfg.img_row = def->img_row; + new_cfg.img_column = def->img_column; + new_cfg.img_construct_row = def->img_construct_row; + new_cfg.img_construct_column = def->img_construct_column; + new_cfg.img_alt_dir_construct_row = def->img_alt_dir_construct_row; + new_cfg.img_alt_dir_construct_column = def->img_alt_dir_construct_column; + new_cfg.img_alt_dir_row = def->img_alt_dir_row; + new_cfg.img_alt_dir_column = def->img_alt_dir_column; + new_cfg.enable_img_alt_dir = def->enable_img_alt_dir; + new_cfg.buildable_square_types_mask = def->has_buildable_on ? def->buildable_square_types_mask : district_default_buildable_mask (); + new_cfg.buildable_only_on_overlays_mask = def->has_buildable_only_on_overlays ? def->buildable_only_on_overlays_mask : 0; + new_cfg.buildable_adjacent_to_overlays_mask = def->has_buildable_adjacent_to_overlays ? def->buildable_adjacent_to_overlays_mask : 0; + new_cfg.has_buildable_only_on_overlays = def->has_buildable_only_on_overlays; + new_cfg.buildable_adjacent_to_square_types_mask = def->has_buildable_adjacent_to ? def->buildable_adjacent_to_square_types_mask : 0; + new_cfg.buildable_adjacent_to_allows_city = def->has_buildable_adjacent_to ? def->buildable_adjacent_to_allows_city : false; + new_cfg.has_buildable_adjacent_to = def->has_buildable_adjacent_to; + new_cfg.has_buildable_adjacent_to_overlays = def->has_buildable_adjacent_to_overlays; + + if (existing_index >= 0) { + struct wonder_district_config * cfg = &is->wonder_district_configs[existing_index]; + free_dynamic_wonder_config (cfg); + *cfg = new_cfg; + cfg->index = existing_index; + } else { + struct wonder_district_config * cfg = &is->wonder_district_configs[dest]; + free_dynamic_wonder_config (cfg); + *cfg = new_cfg; + is->wonder_district_count += 1; } return true; } void -calculate_city_center_district_bonus (City * city, int * out_food, int * out_shields, int * out_gold) +finalize_parsed_wonder_definition (struct parsed_wonder_definition * def, + int section_start_line, + struct error_line ** parse_errors) { - if (out_food != NULL) - *out_food = 0; - if (out_shields != NULL) - *out_shields = 0; - if (out_gold != NULL) - *out_gold = 0; - - if ((! is->current_config.enable_districts && ! is->current_config.enable_natural_wonders) || (city == NULL)) - return; - - int bonus_food = 0; - int bonus_shields = 0; - int bonus_gold = 0; - - int city_civ_id = city->Body.CivID; - int city_x = city->Body.X; - int city_y = city->Body.Y; - int utilized_neighborhoods = count_utilized_neighborhoods_in_city_radius (city); - int neighborhoods_counted = 0; - - for (int n = 0; n < is->workable_tile_count; n++) { - if (n == 0) - continue; - int dx, dy; - patch_ni_to_diff_for_work_area (n, &dx, &dy); - int tile_x = city_x + dx; - int tile_y = city_y + dy; - wrap_tile_coords (&p_bic_data->Map, &tile_x, &tile_y); - Tile * tile = tile_at (tile_x, tile_y); - if ((tile == NULL) || (tile == p_null_tile)) - continue; - if (tile_has_enemy_unit (tile, city_civ_id)) - continue; - if (tile->vtable->m20_Check_Pollution (tile, __, 0)) - continue; - if (tile->Territory_OwnerID != city_civ_id) - continue; - - struct district_instance * inst = get_district_instance (tile); - if (inst == NULL) - continue; - int district_id = inst->district_type; - if ((district_id < 0) || (district_id >= is->district_count)) - continue; - if (! district_is_complete (tile, district_id)) - continue; + bool ok = true; - if (is->current_config.enable_neighborhood_districts && - (district_id == NEIGHBORHOOD_DISTRICT_ID)) { - if (neighborhoods_counted >= utilized_neighborhoods) - continue; - neighborhoods_counted++; + if ((! def->has_name) || (def->name == NULL)) { + ok = false; + if (parse_errors != NULL) { + struct error_line * err = add_error_line (parse_errors); + snprintf (err->text, sizeof err->text, "^ Line %d: name (value is required)", section_start_line); + err->text[(sizeof err->text) - 1] = '\0'; } - - struct district_config const * cfg = &is->district_configs[district_id]; - int food_bonus = 0, shield_bonus = 0, gold_bonus = 0; - get_effective_district_yields (inst, cfg, &food_bonus, &shield_bonus, &gold_bonus, NULL, NULL); - bonus_food += food_bonus; - bonus_shields += shield_bonus; - bonus_gold += gold_bonus; } - - if (is->current_config.enable_districts && is->current_config.enable_distribution_hub_districts) { - int hub_food = 0; - int hub_shields = 0; - get_distribution_hub_yields_for_city (city, &hub_food, &hub_shields); - bonus_food += hub_food; - bonus_shields += hub_shields; + if (! def->has_img_row) { + ok = false; + if (parse_errors != NULL) { + struct error_line * err = add_error_line (parse_errors); + snprintf (err->text, sizeof err->text, "^ Line %d: img_row (value is required)", section_start_line); + err->text[(sizeof err->text) - 1] = '\0'; + } } - - if (out_food != NULL) - *out_food = bonus_food; - if (out_shields != NULL) - *out_shields = bonus_shields; - if (out_gold != NULL) - *out_gold = bonus_gold; -} - -int __fastcall -patch_Map_calc_food_yield_at (Map * this, int edx, int tile_x, int tile_y, int tile_base_type, int civ_id, int imagine_fully_improved, City * city) -{ - if (! is->current_config.enable_districts) - return Map_calc_food_yield_at (this, __, tile_x, tile_y, tile_base_type, civ_id, imagine_fully_improved, city); - - Tile * tile = tile_at (tile_x, tile_y); - if ((tile != NULL) && (tile != p_null_tile)) { - struct district_instance * inst = get_district_instance (tile); - if (inst != NULL && district_is_complete (tile, inst->district_type)) { - return 0; + if (! def->has_img_column) { + ok = false; + if (parse_errors != NULL) { + struct error_line * err = add_error_line (parse_errors); + snprintf (err->text, sizeof err->text, "^ Line %d: img_column (value is required)", section_start_line); + err->text[(sizeof err->text) - 1] = '\0'; } } - - return Map_calc_food_yield_at (this, __, tile_x, tile_y, tile_base_type, civ_id, imagine_fully_improved, city); -} - -int __fastcall -patch_Map_calc_shield_yield_at (Map * this, int edx, int tile_x, int tile_y, int civ_id, City * city, int param_5, int param_6) -{ - if (! is->current_config.enable_districts) - return Map_calc_shield_yield_at (this, __, tile_x, tile_y, civ_id, city, param_5, param_6); - - Tile * tile = tile_at (tile_x, tile_y); - if ((tile != NULL) && (tile != p_null_tile)) { - struct district_instance * inst = get_district_instance (tile); - if (inst != NULL && district_is_complete (tile, inst->district_type)) { - return 0; + if (! def->has_img_construct_row) { + ok = false; + if (parse_errors != NULL) { + struct error_line * err = add_error_line (parse_errors); + snprintf (err->text, sizeof err->text, "^ Line %d: img_construct_row (value is required)", section_start_line); + err->text[(sizeof err->text) - 1] = '\0'; + } + } + if (! def->has_img_construct_column) { + ok = false; + if (parse_errors != NULL) { + struct error_line * err = add_error_line (parse_errors); + snprintf (err->text, sizeof err->text, "^ Line %d: img_construct_column (value is required)", section_start_line); + err->text[(sizeof err->text) - 1] = '\0'; + } + } + if (def->enable_img_alt_dir) { + if (! def->has_img_alt_dir_row) { + ok = false; + if (parse_errors != NULL) { + struct error_line * err = add_error_line (parse_errors); + snprintf (err->text, sizeof err->text, "^ Line %d: img_alt_dir_row (value is required when enable_img_alt_dir is set)", section_start_line); + err->text[(sizeof err->text) - 1] = '\0'; + } + } + if (! def->has_img_alt_dir_column) { + ok = false; + if (parse_errors != NULL) { + struct error_line * err = add_error_line (parse_errors); + snprintf (err->text, sizeof err->text, "^ Line %d: img_alt_dir_column (value is required when enable_img_alt_dir is set)", section_start_line); + err->text[(sizeof err->text) - 1] = '\0'; + } + } + if (! def->has_img_alt_dir_construct_row) { + ok = false; + if (parse_errors != NULL) { + struct error_line * err = add_error_line (parse_errors); + snprintf (err->text, sizeof err->text, "^ Line %d: img_alt_dir_construct_row (value is required when enable_img_alt_dir is set)", section_start_line); + err->text[(sizeof err->text) - 1] = '\0'; + } + } + if (! def->has_img_alt_dir_construct_column) { + ok = false; + if (parse_errors != NULL) { + struct error_line * err = add_error_line (parse_errors); + snprintf (err->text, sizeof err->text, "^ Line %d: img_alt_dir_construct_column (value is required when enable_img_alt_dir is set)", section_start_line); + err->text[(sizeof err->text) - 1] = '\0'; + } } } - return Map_calc_shield_yield_at (this, __, tile_x, tile_y, civ_id, city, param_5, param_6); -} + if (ok) + add_dynamic_wonder_from_definition (def, section_start_line); -int -compute_city_tile_yield_sum (City * city, int tile_x, int tile_y) -{ - if (city == NULL) - return 0; - int food = City_calc_tile_yield_at (city, __, YK_FOOD, tile_x, tile_y); - int shields = City_calc_tile_yield_at (city, __, YK_SHIELDS, tile_x, tile_y); - int commerce = City_calc_tile_yield_at (city, __, YK_COMMERCE, tile_x, tile_y); - return food + shields + commerce; + free_parsed_wonder_definition (def); } -Tile * -find_tile_for_neighborhood_district (City * city, int * out_x, int * out_y) +void +handle_wonder_definition_key (struct parsed_wonder_definition * def, + struct string_slice const * key, + struct string_slice const * value, + int line_number, + struct error_line ** parse_errors, + struct error_line ** unrecognized_keys) { - if (city == NULL) - return NULL; - - int city_x = city->Body.X; - int city_y = city->Body.Y; - int city_work_radius = is->current_config.city_work_radius; - - // Search in order: ring 1, then rings 2..N - // Ring order array: 1, 2, 3, ..., city_work_radius - int ring_order[8]; - int ring_count = 0; - for (int r = 1; r <= city_work_radius; r++) - ring_order[ring_count++] = r; - - for (int r_idx = 0; r_idx < ring_count; r_idx++) { - int ring = ring_order[r_idx]; - int start_ni = workable_tile_counts[ring - 1]; - int end_ni = workable_tile_counts[ring]; + if (slice_matches_str (key, "name")) { + if (def->name != NULL) { + free (def->name); + def->name = NULL; + } - Tile * best_tile = NULL; - int best_yield = INT_MAX; + struct string_slice unquoted = trim_string_slice (value, 1); + if (unquoted.len == 0) { + def->has_name = false; + add_key_parse_error (parse_errors, line_number, key, value, "(value is required)"); + } else { + char * name_copy = extract_slice (&unquoted); + if (name_copy == NULL) { + def->has_name = false; + add_key_parse_error (parse_errors, line_number, key, value, "(out of memory)"); + } else { + def->name = name_copy; + def->has_name = true; + } + } - for (int ni = start_ni; ni < end_ni; ni++) { - int dx, dy; - patch_ni_to_diff_for_work_area (ni, &dx, &dy); - int tx = city_x + dx; - int ty = city_y + dy; - wrap_tile_coords (&p_bic_data->Map, &tx, &ty); - Tile * tile = tile_at (tx, ty); + } else if (slice_matches_str (key, "img_path")) { + if (def->img_path != NULL) { + free (def->img_path); + def->img_path = NULL; + } - if ((tile == NULL) || (tile == p_null_tile)) - continue; - if (is->current_config.enable_distribution_hub_districts) { - int covered = itable_look_up_or (&is->distribution_hub_coverage_counts, (int)tile, 0); - if (covered > 0) - continue; + struct string_slice unquoted = trim_string_slice (value, 1); + if (unquoted.len == 0) { + def->has_img_path = false; + } else { + char * path_copy = extract_slice (&unquoted); + if (path_copy == NULL) { + def->has_img_path = false; + } else { + def->img_path = path_copy; + def->has_img_path = true; } + } - if (! tile_suitable_for_district (tile, NEIGHBORHOOD_DISTRICT_ID, city, NULL)) - continue; - if (get_district_instance (tile) != NULL) - continue; + } else if (slice_matches_str (key, "img_row")) { + struct string_slice val_slice = *value; + int ival; + if (read_int (&val_slice, &ival)) { + def->img_row = ival; + def->has_img_row = true; + } else { + def->has_img_row = false; + add_key_parse_error (parse_errors, line_number, key, value, "(expected integer)"); + } - int yield = compute_city_tile_yield_sum (city, tx, ty); - if (yield < best_yield) { - best_yield = yield; - best_tile = tile; - *out_x = tx; - *out_y = ty; - } + } else if (slice_matches_str (key, "img_column")) { + struct string_slice val_slice = *value; + int ival; + if (read_int (&val_slice, &ival)) { + def->img_column = ival; + def->has_img_column = true; + } else { + def->has_img_column = false; + add_key_parse_error (parse_errors, line_number, key, value, "(expected integer)"); } - if (best_tile != NULL) - return best_tile; - } + } else if (slice_matches_str (key, "img_construct_row")) { + struct string_slice val_slice = *value; + int ival; + if (read_int (&val_slice, &ival)) { + def->img_construct_row = ival; + def->has_img_construct_row = true; + } else { + def->has_img_construct_row = false; + add_key_parse_error (parse_errors, line_number, key, value, "(expected integer)"); + } - return NULL; -} + } else if (slice_matches_str (key, "img_construct_column")) { + struct string_slice val_slice = *value; + int ival; + if (read_int (&val_slice, &ival)) { + def->img_construct_column = ival; + def->has_img_construct_column = true; + } else { + def->has_img_construct_column = false; + add_key_parse_error (parse_errors, line_number, key, value, "(expected integer)"); + } -Tile * -find_tile_for_distribution_hub_district (City * city, int * out_x, int * out_y) -{ - if (city == NULL) - return NULL; + } else if (slice_matches_str (key, "img_alt_dir_construct_row")) { + struct string_slice val_slice = *value; + int ival; + if (read_int (&val_slice, &ival)) { + def->img_alt_dir_construct_row = ival; + def->has_img_alt_dir_construct_row = true; + } else { + def->has_img_alt_dir_construct_row = false; + add_key_parse_error (parse_errors, line_number, key, value, "(expected integer)"); + } - const int resource_penalty = 100; - const int yield_weight = 40; - const int city_distance_weight = 8; - const int capital_distance_weight = 45; - const int desired_min_capital_distance = 8; - const int proximity_penalty_scale = 300; - const int different_continent_bonus = 500; + } else if (slice_matches_str (key, "img_alt_dir_construct_column")) { + struct string_slice val_slice = *value; + int ival; + if (read_int (&val_slice, &ival)) { + def->img_alt_dir_construct_column = ival; + def->has_img_alt_dir_construct_column = true; + } else { + def->has_img_alt_dir_construct_column = false; + add_key_parse_error (parse_errors, line_number, key, value, "(expected integer)"); + } - Tile * best_tile = NULL; - int best_score = INT_MIN; - int best_adjusted_yield = INT_MIN; - int best_distance = -1; - int best_distance_to_capital = -1; - bool best_has_resource = true; - int city_x = city->Body.X; - int city_y = city->Body.Y; + } else if (slice_matches_str (key, "img_alt_dir_row")) { + struct string_slice val_slice = *value; + int ival; + if (read_int (&val_slice, &ival)) { + def->img_alt_dir_row = ival; + def->has_img_alt_dir_row = true; + } else { + def->has_img_alt_dir_row = false; + add_key_parse_error (parse_errors, line_number, key, value, "(expected integer)"); + } - int civ_id = city->Body.CivID; - bool has_capital = false; - int capital_x = 0; - int capital_y = 0; - int capital_continent_id = -1; - - City * capital = get_city_ptr (leaders[civ_id].CapitalID); - if (capital != NULL) { - has_capital = true; - capital_x = capital->Body.X; - capital_y = capital->Body.Y; - Tile * capital_tile = tile_at (capital_x, capital_y); - if ((capital_tile != NULL) && (capital_tile != p_null_tile)) - capital_continent_id = capital_tile->vtable->m46_Get_ContinentID (capital_tile); - } + } else if (slice_matches_str (key, "img_alt_dir_column")) { + struct string_slice val_slice = *value; + int ival; + if (read_int (&val_slice, &ival)) { + def->img_alt_dir_column = ival; + def->has_img_alt_dir_column = true; + } else { + def->has_img_alt_dir_column = false; + add_key_parse_error (parse_errors, line_number, key, value, "(expected integer)"); + } - for (int n = 0; n < is->workable_tile_count; n++) { - int dx, dy; - patch_ni_to_diff_for_work_area (n, &dx, &dy); - int tx = city_x + dx, ty = city_y + dy; - wrap_tile_coords (&p_bic_data->Map, &tx, &ty); - Tile * tile = tile_at (tx, ty); - bool has_resource; - if (! tile_suitable_for_district (tile, DISTRIBUTION_HUB_DISTRICT_ID, city, &has_resource)) - continue; + } else if (slice_matches_str (key, "enable_img_alt_dir")) { + struct string_slice val_slice = *value; + int ival; + if (read_int (&val_slice, &ival)) { + def->enable_img_alt_dir = (ival != 0); + def->has_enable_img_alt_dir = true; + } else { + def->has_enable_img_alt_dir = false; + add_key_parse_error (parse_errors, line_number, key, value, "(expected integer)"); + } - int chebyshev = compute_wrapped_chebyshev_distance (tx, ty, city_x, city_y); - if (chebyshev <= 1) - continue; + } else if (slice_matches_str (key, "buildable_on")) { + unsigned int mask; + if (parse_buildable_square_type_mask (value, &mask, parse_errors, line_number, "buildable_on", NULL)) { + def->buildable_square_types_mask = mask; + def->has_buildable_on = true; + } else { + def->has_buildable_on = false; + } - bool too_close_to_existing_hub_or_city = false; - for (int m = 0; m < workable_tile_counts[2]; m++) { - int ndx, ndy; - patch_ni_to_diff_for_work_area (m, &ndx, &ndy); - int nx = tx + ndx, ny = ty + ndy; - wrap_tile_coords (&p_bic_data->Map, &nx, &ny); - Tile * nearby_tile = tile_at (nx, ny); - if ((nearby_tile != NULL) && (nearby_tile != p_null_tile)) { - struct district_instance * nearby_inst = get_district_instance (nearby_tile); - if ((nearby_inst != NULL) && - (nearby_inst->district_type == DISTRIBUTION_HUB_DISTRICT_ID) && - district_is_complete (nearby_tile, DISTRIBUTION_HUB_DISTRICT_ID)) { - too_close_to_existing_hub_or_city = true; - break; - } - else if (nearby_tile->CityID >= 0) { - too_close_to_existing_hub_or_city = true; - break; - } + } else if (slice_matches_str (key, "buildable_only_on_overlays")) { + unsigned int mask; + if (parse_buildable_overlay_mask (value, &mask, parse_errors, line_number, "buildable_only_on_overlays")) { + if (mask != DOM_RIVER) { + add_key_parse_error (parse_errors, line_number, key, value, "(only \"river\" is allowed for wonders)"); + def->has_buildable_only_on_overlays = false; + } else { + def->buildable_only_on_overlays_mask = mask; + def->has_buildable_only_on_overlays = true; } + } else { + def->has_buildable_only_on_overlays = false; } - if (too_close_to_existing_hub_or_city) - continue; - - int raw_yield = compute_city_tile_yield_sum (city, tx, ty); - int adjusted_yield = raw_yield - (has_resource ? resource_penalty : 0); - int distance = compute_wrapped_manhattan_distance (tx, ty, city_x, city_y); - int distance_to_capital = has_capital - ? compute_wrapped_manhattan_distance (tx, ty, capital_x, capital_y) - : 0; - int proximity_penalty = 0; - if (has_capital && (distance_to_capital < desired_min_capital_distance)) - proximity_penalty = (desired_min_capital_distance - distance_to_capital) * proximity_penalty_scale; - - int continent_bonus = 0; - if ((capital_continent_id >= 0) && (tile != NULL) && (tile != p_null_tile)) { - int tile_continent_id = tile->vtable->m46_Get_ContinentID (tile); - if ((tile_continent_id >= 0) && (tile_continent_id != capital_continent_id)) - continent_bonus = different_continent_bonus; + } else if (slice_matches_str (key, "buildable_adjacent_to")) { + unsigned int mask; + def->buildable_adjacent_to_allows_city = false; + if (parse_buildable_square_type_mask (value, &mask, parse_errors, line_number, "buildable_adjacent_to", &def->buildable_adjacent_to_allows_city)) { + def->buildable_adjacent_to_square_types_mask = mask; + def->has_buildable_adjacent_to = true; + } else { + def->has_buildable_adjacent_to = false; } - int score = - adjusted_yield * yield_weight + - distance * city_distance_weight + - distance_to_capital * capital_distance_weight - - proximity_penalty + - continent_bonus; - - if ((score > best_score) || - ((score == best_score) && (distance_to_capital > best_distance_to_capital)) || - ((score == best_score) && (distance_to_capital == best_distance_to_capital) && (adjusted_yield > best_adjusted_yield)) || - ((score == best_score) && (distance_to_capital == best_distance_to_capital) && (adjusted_yield == best_adjusted_yield) && (distance > best_distance)) || - ((score == best_score) && (distance_to_capital == best_distance_to_capital) && (adjusted_yield == best_adjusted_yield) && (distance == best_distance) && (! has_resource && best_has_resource))) { - best_tile = tile; - best_score = score; - best_adjusted_yield = adjusted_yield; - best_distance = distance; - best_distance_to_capital = distance_to_capital; - best_has_resource = has_resource; - *out_x = tx; - *out_y = ty; + } else if (slice_matches_str (key, "buildable_adjacent_to_overlays")) { + unsigned int mask; + if (parse_buildable_overlay_mask (value, &mask, parse_errors, line_number, "buildable_adjacent_to_overlays")) { + def->buildable_adjacent_to_overlays_mask = mask; + def->has_buildable_adjacent_to_overlays = true; + } else { + def->has_buildable_adjacent_to_overlays = false; } - } - return best_tile; + } else + add_unrecognized_key_error (unrecognized_keys, line_number, key); } -int -count_cities_in_work_radius_of_tile (int tile_x, int tile_y, int civ_id) +void +load_dynamic_wonder_config_file (char const * file_path, + int path_is_relative_to_mod_dir, + int log_missing, + int drop_existing_configs) { - int count = 0; + char path[MAX_PATH]; + if (path_is_relative_to_mod_dir) { + if (is->mod_rel_dir == NULL) + return; + snprintf (path, sizeof path, "%s\\%s", is->mod_rel_dir, file_path); + } else { + strncpy (path, file_path, sizeof path); + } + path[(sizeof path) - 1] = '\0'; - for (int n = 0; n < is->workable_tile_count; n++) { - int dx, dy; - patch_ni_to_diff_for_work_area (n, &dx, &dy); - int x = tile_x + dx, y = tile_y + dy; - wrap_tile_coords (&p_bic_data->Map, &x, &y); - Tile * tile = tile_at (x, y); - if ((tile != NULL) && (tile != p_null_tile) && (tile->CityID >= 0)) { - City * city = get_city_ptr (tile->vtable->m45_Get_City_ID (tile)); - if ((city != NULL) && (city->Body.CivID == civ_id)) - count += 1; + char * text = file_to_string (path); + if (text == NULL) { + if (log_missing) { + char ss[256]; + snprintf (ss, sizeof ss, "[C3X] Wonders config file not found: %s", path); + (*p_OutputDebugStringA) (ss); } + return; } - return count; -} - -Tile * -find_tile_for_district (City * city, int district_id, int * out_x, int * out_y) -{ - if ((city == NULL) || (out_x == NULL) || (out_y == NULL)) - return NULL; - if ((district_id < 0) || (district_id >= is->district_count)) - return NULL; - - if (district_id == NEIGHBORHOOD_DISTRICT_ID) - return find_tile_for_neighborhood_district (city, out_x, out_y); - if (district_id == DISTRIBUTION_HUB_DISTRICT_ID) - return find_tile_for_distribution_hub_district (city, out_x, out_y); + if (drop_existing_configs) + reset_wonder_district_configs (); - int city_x = city->Body.X; - int city_y = city->Body.Y; - int city_work_radius = is->current_config.city_work_radius; + struct parsed_wonder_definition def; + init_parsed_wonder_definition (&def); + bool in_section = false; + int section_start_line = 0; + int line_number = 0; + struct error_line * unrecognized_keys = NULL; + struct error_line * parse_errors = NULL; - // Search in order: ring 2, then rings 3..N, then ring 1 as last resort - // Ring order array: 2, 3, 4, ..., city_work_radius, 1 - int ring_order[8]; - int ring_count = 0; - ring_order[ring_count++] = 2; - for (int r = 3; r <= city_work_radius; r++) - ring_order[ring_count++] = r; - ring_order[ring_count++] = 1; + char * cursor = text; + while (*cursor != '\0') { + line_number += 1; - for (int r_idx = 0; r_idx < ring_count; r_idx++) { - int ring = ring_order[r_idx]; - int start_ni = workable_tile_counts[ring - 1]; - int end_ni = workable_tile_counts[ring]; + char * line_start = cursor; + char * line_end = cursor; + while ((*line_end != '\0') && (*line_end != '\n')) + line_end++; - Tile * best_tile = NULL; - int best_yield = INT_MAX; + int line_len = line_end - line_start; + bool has_newline = (*line_end == '\n'); + if (has_newline) + *line_end = '\0'; - for (int ni = start_ni; ni < end_ni; ni++) { - int dx, dy; - patch_ni_to_diff_for_work_area (ni, &dx, &dy); - int tx = (city_x + dx) & 0xFF; - int ty = (city_y + dy) & 0xFF; - Tile * tile = tile_at (tx, ty); + struct string_slice line_slice = { .str = line_start, .len = line_len }; + struct string_slice trimmed = trim_string_slice (&line_slice, 0); + if (line_is_empty_or_comment (&trimmed)) { + cursor = has_newline ? line_end + 1 : line_end; + continue; + } - if ((tile == NULL) || (tile == p_null_tile)) - continue; - if (is->current_config.enable_distribution_hub_districts) { - int covered = itable_look_up_or (&is->distribution_hub_coverage_counts, (int)tile, 0); - if (covered > 0) - continue; + if (trimmed.str[0] == '#') { + struct string_slice directive = trimmed; + directive.str += 1; + directive.len -= 1; + directive = trim_string_slice (&directive, 0); + if ((directive.len > 0) && slice_matches_str (&directive, "Wonder")) { + if (in_section) + finalize_parsed_wonder_definition (&def, section_start_line, &parse_errors); + in_section = true; + section_start_line = line_number; } + cursor = has_newline ? line_end + 1 : line_end; + continue; + } - if (! tile_suitable_for_district (tile, district_id, city, NULL)) - continue; - if (get_district_instance (tile) != NULL) - continue; + if (! in_section) { + cursor = has_newline ? line_end + 1 : line_end; + continue; + } - int yield = compute_city_tile_yield_sum (city, tx, ty); - if (yield < best_yield && ! is_tile_earmarked_for_district (tx, ty)) { - best_yield = yield; - best_tile = tile; - *out_x = tx; - *out_y = ty; - } + struct string_slice key_slice = {0}; + struct string_slice value_slice = {0}; + enum key_value_parse_status status = parse_trimmed_key_value (&trimmed, &key_slice, &value_slice); + if (status == KVP_NO_EQUALS) { + char * line_text = extract_slice (&trimmed); + struct error_line * err = add_error_line (&parse_errors); + snprintf (err->text, sizeof err->text, "^ Line %d: %s (expected '=')", line_number, line_text); + err->text[(sizeof err->text) - 1] = '\0'; + free (line_text); + cursor = has_newline ? line_end + 1 : line_end; + continue; + } else if (status == KVP_EMPTY_KEY) { + struct error_line * err = add_error_line (&parse_errors); + snprintf (err->text, sizeof err->text, "^ Line %d: (missing key)", line_number); + err->text[(sizeof err->text) - 1] = '\0'; + cursor = has_newline ? line_end + 1 : line_end; + continue; } - if (best_tile != NULL) - return best_tile; + handle_wonder_definition_key (&def, &key_slice, &value_slice, line_number, &parse_errors, &unrecognized_keys); + cursor = has_newline ? line_end + 1 : line_end; } - return NULL; -} + if (in_section) + finalize_parsed_wonder_definition (&def, section_start_line, &parse_errors); -Tile * -get_completed_district_tile_for_city (City * city, int district_id, int * out_x, int * out_y) -{ - if ((city == NULL) || (district_id < 0) || (district_id >= is->district_count)) - return NULL; + free_parsed_wonder_definition (&def); + free (text); - int civ_id = city->Body.CivID; - int city_x = city->Body.X; - int city_y = city->Body.Y; - for (int n = 0; n < is->workable_tile_count; n++) { - int dx, dy; - patch_ni_to_diff_for_work_area (n, &dx, &dy); - int x = city_x + dx, y = city_y + dy; - wrap_tile_coords (&p_bic_data->Map, &x, &y); - Tile * candidate = tile_at (x, y); - if ((candidate == NULL) || (candidate == p_null_tile)) - continue; - if (candidate->vtable->m38_Get_Territory_OwnerID (candidate) != civ_id) - continue; + // Append to loaded config names list + struct loaded_config_name * top_lcn = is->loaded_config_names; + while (top_lcn->next != NULL) + top_lcn = top_lcn->next; - struct district_instance * inst = get_district_instance (candidate); - if (inst != NULL && - inst->district_type == district_id && - district_is_complete (candidate, district_id)) { + struct loaded_config_name * new_lcn = malloc (sizeof *new_lcn); + new_lcn->name = strdup (path); + new_lcn->next = NULL; - // For wonder districts, filter based on wonder_district_state - if (district_id == WONDER_DISTRICT_ID) { - struct wonder_district_info * info = &inst->wonder_info; - // Must be either unused or under construction by this city - if (info->state == WDS_COMPLETED) - continue; - if (info->state == WDS_UNDER_CONSTRUCTION && info->city_id != city->Body.ID) - continue; - if (info->state == WDS_UNDER_CONSTRUCTION) { - info->city = city; - info->city_id = city->Body.ID; - } - } + top_lcn->next = new_lcn; - if (out_x != NULL) - *out_x = x; - if (out_y != NULL) - *out_y = y; - return candidate; - } + if (parse_errors != NULL || unrecognized_keys != NULL) { + PopupForm * popup = get_popup_form (); + popup->vtable->set_text_key_and_flags (popup, __, is->mod_script_path, "C3X_WARNING", -1, 0, 0, 0); + char s[200]; + snprintf (s, sizeof s, "Wonder District Config errors in %s:", path); + s[(sizeof s) - 1] = '\0'; + PopupForm_add_text (popup, __, s, false); + if (parse_errors != NULL) { + for (struct error_line * line = parse_errors; line != NULL; line = line->next) + PopupForm_add_text (popup, __, line->text, false); } - - return NULL; + if (unrecognized_keys != NULL) { + PopupForm_add_text (popup, __, "", false); + PopupForm_add_text (popup, __, "Unrecognized keys:", false); + for (struct error_line * line = unrecognized_keys; line != NULL; line = line->next) + PopupForm_add_text (popup, __, line->text, false); + } + patch_show_popup (popup, __, 0, 0); + free_error_lines (parse_errors); + free_error_lines (unrecognized_keys); + } } -bool -tile_has_friendly_aerodrome_district (Tile * tile, int civ_id, bool require_available) +void +load_dynamic_wonder_configs () { - if (! is->current_config.enable_districts || - ! is->current_config.enable_aerodrome_districts || - (tile == NULL) || - (tile == p_null_tile) || - (civ_id < 0) || (civ_id >= 32)) - return false; - - int aerodrome_id = AERODROME_DISTRICT_ID; - if (aerodrome_id < 0) - return false; - - struct district_instance * inst = get_district_instance (tile); - if (inst == NULL || inst->district_type != aerodrome_id) - return false; - - if (! district_is_complete (tile, aerodrome_id)) - return false; - - int territory_owner = tile->vtable->m38_Get_Territory_OwnerID (tile); - if (territory_owner != civ_id) - return false; - - if (require_available) { - int usage_mask; - if (itable_look_up (&is->aerodrome_airlift_usage, (int)tile, &usage_mask) && - (usage_mask & (1 << civ_id))) - return false; - } + load_dynamic_wonder_config_file ("default.districts_wonders_config.txt", 1, 1, 1); + load_dynamic_wonder_config_file ("user.districts_wonders_config.txt", 1, 0, 1); - return true; + char * scenario_filename = "scenario.districts_wonders_config.txt"; + char * scenario_wonder_config_path = BIC_get_asset_path (p_bic_data, __, scenario_filename, false); + if ((scenario_wonder_config_path != NULL) && (0 != strcmp (scenario_filename, scenario_wonder_config_path))) + load_dynamic_wonder_config_file (scenario_wonder_config_path, 0, 0, 1); } -bool -city_has_required_district (City * city, int district_id) +void +init_parsed_natural_wonder_definition (struct parsed_natural_wonder_definition * def) { - if ((city == NULL) || (district_id < 0) || (district_id >= is->district_count)) - return false; - - if (get_completed_district_tile_for_city (city, district_id, NULL, NULL) != NULL) { - clear_city_district_request (city, district_id); - return true; + memset (def, 0, sizeof *def); + def->terrain_type = SQ_Grassland; + def->adjacent_to = (enum SquareTypes)SQ_INVALID; + def->adjacency_dir = DIR_ZERO; +} + +void +free_parsed_natural_wonder_definition (struct parsed_natural_wonder_definition * def) +{ + if (def->name != NULL) { + free (def->name); + def->name = NULL; } - return false; + if (def->img_path != NULL) { + free (def->img_path); + def->img_path = NULL; + } + init_parsed_natural_wonder_definition (def); } bool -city_has_wonder_district_with_no_completed_wonder (City * city) +add_natural_wonder_from_definition (struct parsed_natural_wonder_definition * def, int section_start_line) { - if (! is->current_config.enable_wonder_districts || (city == NULL)) + if ((def == NULL) || (def->name == NULL)) return false; - int civ_id = city->Body.CivID; - int city_x = city->Body.X; - int city_y = city->Body.Y; - for (int n = 0; n < is->workable_tile_count; n++) { - int dx, dy; - patch_ni_to_diff_for_work_area (n, &dx, &dy); - int x = city_x + dx, y = city_y + dy; - wrap_tile_coords (&p_bic_data->Map, &x, &y); - Tile * candidate = tile_at (x, y); - if (candidate == NULL || candidate == p_null_tile) continue; - if (candidate->vtable->m38_Get_Territory_OwnerID (candidate) != civ_id) continue; - struct district_instance * inst = get_district_instance (candidate); - if (inst == NULL || inst->district_type != WONDER_DISTRICT_ID) continue; - if (! district_is_complete (candidate, WONDER_DISTRICT_ID)) continue; + int existing_index; + bool has_existing = stable_look_up (&is->natural_wonder_name_to_id, def->name, &existing_index); - // Get wonder district info - struct wonder_district_info * info = get_wonder_district_info (candidate); - if (info == NULL) return true; - if (info->state == WDS_COMPLETED) continue; - if (info->state == WDS_UNUSED) return true; // Unreserved and available - if ((info->state == WDS_UNDER_CONSTRUCTION) && (info->city_id == city->Body.ID)) { - info->city = city; - info->city_id = city->Body.ID; - return true; // Reserved by this city - } - } + int dest = has_existing ? existing_index : is->natural_wonder_count; + if ((dest < 0) || (dest >= MAX_NATURAL_WONDER_DISTRICT_TYPES)) + return false; - return false; -} + struct natural_wonder_district_config new_cfg; + memset (&new_cfg, 0, sizeof new_cfg); + new_cfg.index = dest; + new_cfg.is_dynamic = true; + new_cfg.adjacent_to = (enum SquareTypes)SQ_INVALID; + new_cfg.adjacency_dir = DIR_ZERO; -int __fastcall patch_Leader_count_any_shared_wonders_with_flag (Leader * this, int edx, enum ImprovementTypeWonderFeatures flag, City * only_in_city); -int __fastcall patch_Leader_count_wonders_with_small_flag (Leader * this, int edx, enum ImprovementTypeSmallWonderFeatures flag, City * city_or_null); -void __fastcall patch_City_add_or_remove_improvement (City * this, int edx, int improv_id, int add, bool param_3); + char * name_copy = strdup (def->name); + if (name_copy == NULL) + return false; + new_cfg.name = name_copy; + char const * img_path_src = def->img_path; + char * img_copy = strdup (img_path_src); + if (img_copy == NULL) { + free (name_copy); + return false; + } + new_cfg.img_path = img_copy; + new_cfg.img_row = def->img_row; + new_cfg.img_column = def->img_column; + new_cfg.terrain_type = def->terrain_type; + new_cfg.adjacent_to = def->adjacent_to; + new_cfg.adjacency_dir = def->adjacency_dir; + new_cfg.culture_bonus = def->has_culture_bonus ? def->culture_bonus : 0; + new_cfg.science_bonus = def->has_science_bonus ? def->science_bonus : 0; + new_cfg.food_bonus = def->has_food_bonus ? def->food_bonus : 0; + new_cfg.gold_bonus = def->has_gold_bonus ? def->gold_bonus : 0; + new_cfg.shield_bonus = def->has_shield_bonus ? def->shield_bonus : 0; + new_cfg.happiness_bonus = def->has_happiness_bonus ? def->happiness_bonus : 0; -void __fastcall -patch_Main_Screen_Form_show_map_message (Main_Screen_Form * this, int edx, int tile_x, int tile_y, char * text_key, bool pause) -{ - WITH_PAUSE_FOR_POPUP { - Main_Screen_Form_show_map_message (this, __, tile_x, tile_y, text_key, pause); + if (has_existing) { + struct natural_wonder_district_config * cfg = &is->natural_wonder_configs[existing_index]; + free_dynamic_natural_wonder_config (cfg); + *cfg = new_cfg; + } else { + struct natural_wonder_district_config * cfg = &is->natural_wonder_configs[dest]; + free_dynamic_natural_wonder_config (cfg); + *cfg = new_cfg; + is->natural_wonder_count = dest + 1; + stable_insert (&is->natural_wonder_name_to_id, new_cfg.name, dest); } -} -int __cdecl -patch_process_text_for_map_message (char * in, char * out) -{ - if (is->map_message_text_override != NULL) { - strcpy (out, is->map_message_text_override); - is->map_message_text_override = NULL; - return 0; - } else - return process_text_snippet (in, out); + return true; } -// Works like show_map_message but takes a bit of text to display instead of a key for script.txt void -show_map_specific_text (int tile_x, int tile_y, char const * text, bool pause) -{ - is->map_message_text_override = text; - patch_Main_Screen_Form_show_map_message (p_main_screen_form, __, tile_x, tile_y, "LANDCONQUER", pause); // Use any key here. It will be overridden. -} - -bool __fastcall -patch_City_has_improvement (City * this, int edx, int improv_id, bool include_auto_improvements) +finalize_parsed_natural_wonder_definition (struct parsed_natural_wonder_definition * def, + int section_start_line, + struct error_line ** parse_errors) { - bool tr = City_has_improvement (this, __, improv_id, include_auto_improvements); - - // Check if the improvement is provided for free by another human player's wonder if we're in a hotseat game and the config option is on - if ((! tr) && - include_auto_improvements && - is->current_config.share_wonders_in_hotseat && - ((1 << this->Body.CivID) & *p_human_player_bits) && - (*p_is_offline_mp_game && ! *p_is_pbem_game)) { // if we're in a hotseat game + bool ok = true; - // Loop over every other human player in the game and check if the city would have the improv if they were its owner - int actual_owner_id = this->Body.CivID; - unsigned player_bits = *(unsigned *)p_human_player_bits >> 1; - int n_player = 1; - while (player_bits != 0) { - if ((player_bits & 1) && (n_player != actual_owner_id)) { - this->Body.CivID = n_player; - if (City_has_improvement (this, __, improv_id, include_auto_improvements)) { - tr = true; - break; - } - } - player_bits >>= 1; - n_player++; + if ((! def->has_name) || (def->name == NULL)) { + ok = false; + if (parse_errors != NULL) { + struct error_line * err = add_error_line (parse_errors); + snprintf (err->text, sizeof err->text, "^ Line %d: name (value is required)", section_start_line); + err->text[(sizeof err->text) - 1] = '\0'; + } + } + if (! def->has_img_row) { + ok = false; + if (parse_errors != NULL) { + struct error_line * err = add_error_line (parse_errors); + snprintf (err->text, sizeof err->text, "^ Line %d: img_row (value is required)", section_start_line); + err->text[(sizeof err->text) - 1] = '\0'; + } + } + if (! def->has_img_column) { + ok = false; + if (parse_errors != NULL) { + struct error_line * err = add_error_line (parse_errors); + snprintf (err->text, sizeof err->text, "^ Line %d: img_column (value is required)", section_start_line); + err->text[(sizeof err->text) - 1] = '\0'; + } + } + if (! def->has_terrain_type) { + ok = false; + if (parse_errors != NULL) { + struct error_line * err = add_error_line (parse_errors); + snprintf (err->text, sizeof err->text, "^ Line %d: terrain_type (value is required)", section_start_line); + err->text[(sizeof err->text) - 1] = '\0'; } - this->Body.CivID = actual_owner_id; - } - return tr; -} - -int -count_neighborhoods_in_city_radius (City * city) -{ - if (! is->current_config.enable_districts || - ! is->current_config.enable_neighborhood_districts || - (city == NULL)) - return 0; - - int count = 0; - int city_x = city->Body.X; - int city_y = city->Body.Y; - for (int n = 0; n < is->workable_tile_count; n++) { - int dx, dy; - patch_ni_to_diff_for_work_area (n, &dx, &dy); - int x = city_x + dx, y = city_y + dy; - wrap_tile_coords (&p_bic_data->Map, &x, &y); - Tile * tile = tile_at (x, y); - if ((tile == NULL) || (tile == p_null_tile)) - continue; - if (! (tile->vtable->m38_Get_Territory_OwnerID (tile) == city->Body.CivID)) - continue; + if (ok) + add_natural_wonder_from_definition (def, section_start_line); - struct district_instance * inst = get_district_instance (tile); - if (inst != NULL && - inst->district_type >= 0 && inst->district_type < is->district_count && - inst->district_type == NEIGHBORHOOD_DISTRICT_ID && - district_is_complete (tile, inst->district_type)) - count++; - } - return count; + free_parsed_natural_wonder_definition (def); } -int -count_utilized_neighborhoods_in_city_radius (City * city) +void +handle_natural_wonder_definition_key (struct parsed_natural_wonder_definition * def, + struct string_slice const * key, + struct string_slice const * value, + int line_number, + struct error_line ** parse_errors, + struct error_line ** unrecognized_keys) { - if (! is->current_config.enable_districts || - ! is->current_config.enable_neighborhood_districts || - (city == NULL)) - return 0; - - int base_cap = is->current_config.maximum_pop_before_neighborhood_needed; - if (base_cap <= 0) - return 0; - - int per_neighborhood = is->current_config.per_neighborhood_pop_growth_enabled; - if (per_neighborhood <= 0) - return 0; + if (slice_matches_str (key, "name")) { + if (def->name != NULL) { + free (def->name); + def->name = NULL; + } - int pop = city->Body.Population.Size; - if (pop <= base_cap) - return 0; + struct string_slice unquoted = trim_string_slice (value, 1); + if (unquoted.len == 0) { + def->has_name = false; + add_key_parse_error (parse_errors, line_number, key, value, "(value is required)"); + } else { + char * name_copy = extract_slice (&unquoted); + if (name_copy == NULL) { + def->has_name = false; + add_key_parse_error (parse_errors, line_number, key, value, "(out of memory)"); + } else { + def->name = name_copy; + def->has_name = true; + } + } - int excess_pop = pop - base_cap; - int utilized = (excess_pop + per_neighborhood - 1) / per_neighborhood; - int total_neighborhoods = count_neighborhoods_in_city_radius (city); + } else if (slice_matches_str (key, "terrain_type")) { + enum SquareTypes terrain; + if (read_natural_wonder_terrain_type (value, &terrain)) { + def->terrain_type = terrain; + def->has_terrain_type = true; + } else { + def->has_terrain_type = false; + add_key_parse_error (parse_errors, line_number, key, value, "(unrecognized terrain type)"); + } - if (utilized > total_neighborhoods) - utilized = total_neighborhoods; + } else if (slice_matches_str (key, "adjacent_to")) { + enum SquareTypes adj; + if (read_square_type_value (value, &adj)) { + def->adjacent_to = adj; + def->has_adjacent_to = true; + } else { + def->adjacent_to = (enum SquareTypes)SQ_INVALID; + def->has_adjacent_to = false; + add_key_parse_error (parse_errors, line_number, key, value, "(unrecognized square type)"); + } - return utilized; -} + } else if (slice_matches_str (key, "adjacency_dir")) { + enum direction dir; + if (read_direction_value (value, &dir)) { + def->adjacency_dir = dir; + def->has_adjacency_dir = true; + } else { + def->adjacency_dir = DIR_ZERO; + def->has_adjacency_dir = false; + add_key_parse_error (parse_errors, line_number, key, value, "(unrecognized direction)"); + } -int -get_neighborhood_pop_cap (City * city) -{ - if (! is->current_config.enable_districts || - ! is->current_config.enable_neighborhood_districts || - (city == NULL)) - return -1; + } else if (slice_matches_str (key, "img_path")) { + if (def->img_path != NULL) { + free (def->img_path); + def->img_path = NULL; + } - int base_cap = is->current_config.maximum_pop_before_neighborhood_needed; - if (base_cap <= 0) - return -1; + struct string_slice unquoted = trim_string_slice (value, 1); + if (unquoted.len == 0) { + def->has_img_path = false; + } else { + char * path_copy = extract_slice (&unquoted); + if (path_copy == NULL) { + def->has_img_path = false; + } else { + def->img_path = path_copy; + def->has_img_path = true; + } + } - int per_neighborhood = is->current_config.per_neighborhood_pop_growth_enabled; - if (per_neighborhood < 0) - per_neighborhood = 0; + } else if (slice_matches_str (key, "img_row")) { + struct string_slice val_slice = *value; + int ival; + if (read_int (&val_slice, &ival)) { + def->img_row = ival; + def->has_img_row = true; + } else { + def->has_img_row = false; + add_key_parse_error (parse_errors, line_number, key, value, "(expected integer)"); + } - int neighborhoods = count_neighborhoods_in_city_radius (city); - long long cap = (long long)base_cap + (long long)per_neighborhood * neighborhoods; - if (cap < base_cap) - cap = base_cap; + } else if (slice_matches_str (key, "img_column")) { + struct string_slice val_slice = *value; + int ival; + if (read_int (&val_slice, &ival)) { + def->img_column = ival; + def->has_img_column = true; + } else { + def->has_img_column = false; + add_key_parse_error (parse_errors, line_number, key, value, "(expected integer)"); + } - return (int)cap; -} + } else if (slice_matches_str (key, "culture_bonus")) { + struct string_slice val_slice = *value; + int ival; + if (read_int (&val_slice, &ival)) { + def->culture_bonus = ival; + def->has_culture_bonus = true; + } else { + def->has_culture_bonus = false; + add_key_parse_error (parse_errors, line_number, key, value, "(expected integer)"); + } -bool -city_is_at_neighborhood_cap (City * city) -{ - if (! is->current_config.enable_districts || - ! is->current_config.enable_neighborhood_districts || - (city == NULL)) - return false; + } else if (slice_matches_str (key, "science_bonus")) { + struct string_slice val_slice = *value; + int ival; + if (read_int (&val_slice, &ival)) { + def->science_bonus = ival; + def->has_science_bonus = true; + } else { + def->has_science_bonus = false; + add_key_parse_error (parse_errors, line_number, key, value, "(expected integer)"); + } - int cap = get_neighborhood_pop_cap (city); - if (cap <= 0) - return false; + } else if (slice_matches_str (key, "food_bonus")) { + struct string_slice val_slice = *value; + int ival; + if (read_int (&val_slice, &ival)) { + def->food_bonus = ival; + def->has_food_bonus = true; + } else { + def->has_food_bonus = false; + add_key_parse_error (parse_errors, line_number, key, value, "(expected integer)"); + } - return city->Body.Population.Size >= cap; -} + } else if (slice_matches_str (key, "gold_bonus")) { + struct string_slice val_slice = *value; + int ival; + if (read_int (&val_slice, &ival)) { + def->gold_bonus = ival; + def->has_gold_bonus = true; + } else { + def->has_gold_bonus = false; + add_key_parse_error (parse_errors, line_number, key, value, "(expected integer)"); + } -void -ensure_neighborhood_request_for_city (City * city) -{ - if (! is->current_config.enable_districts || - ! is->current_config.enable_neighborhood_districts || - (city == NULL)) - return; + } else if (slice_matches_str (key, "shield_bonus")) { + struct string_slice val_slice = *value; + int ival; + if (read_int (&val_slice, &ival)) { + def->shield_bonus = ival; + def->has_shield_bonus = true; + } else { + def->has_shield_bonus = false; + add_key_parse_error (parse_errors, line_number, key, value, "(expected integer)"); + } - int prereq = is->district_infos[NEIGHBORHOOD_DISTRICT_ID].advance_prereq_id; - if ((prereq >= 0) && ! Leader_has_tech (&leaders[city->Body.CivID], __, prereq)) return; + } else if (slice_matches_str (key, "happiness_bonus")) { + struct string_slice val_slice = *value; + int ival; + if (read_int (&val_slice, &ival)) { + def->happiness_bonus = ival; + def->has_happiness_bonus = true; + } else { + def->has_happiness_bonus = false; + add_key_parse_error (parse_errors, line_number, key, value, "(expected integer)"); + } - mark_city_needs_district (city, NEIGHBORHOOD_DISTRICT_ID); + } else + add_unrecognized_key_error (unrecognized_keys, line_number, key); } void -calculate_district_culture_science_bonuses (City * city, int * culture_bonus, int * science_bonus) +load_natural_wonder_config_file (char const * file_path, + int path_is_relative_to_mod_dir, + int log_missing, + int drop_existing_configs) { - if (culture_bonus != NULL) - *culture_bonus = 0; - if (science_bonus != NULL) - *science_bonus = 0; + char path[MAX_PATH]; + if (path_is_relative_to_mod_dir) { + if (is->mod_rel_dir == NULL) + return; + snprintf (path, sizeof path, "%s\\%s", is->mod_rel_dir, file_path); + } else { + strncpy (path, file_path, sizeof path); + } + path[(sizeof path) - 1] = '\0'; - if (! (is->current_config.enable_districts || is->current_config.enable_natural_wonders) || (city == NULL)) + char * text = file_to_string (path); + if (text == NULL) { + if (log_missing) { + char ss[256]; + snprintf (ss, sizeof ss, "[C3X] Natural wonders config file not found: %s", path); + (*p_OutputDebugStringA) (ss); + } return; + } - int total_culture = 0; - int total_science = 0; - int city_civ_id = city->Body.CivID; - int utilized_neighborhoods = count_utilized_neighborhoods_in_city_radius (city); + if (drop_existing_configs) + reset_natural_wonder_configs (); - int city_x = city->Body.X; - int city_y = city->Body.Y; + struct parsed_natural_wonder_definition def; + init_parsed_natural_wonder_definition (&def); + bool in_section = false; + int section_start_line = 0; + int line_number = 0; + struct error_line * unrecognized_keys = NULL; + struct error_line * parse_errors = NULL; - for (int n = 0; n < is->workable_tile_count; n++) { - if (n == 0) - continue; - int dx, dy; - patch_ni_to_diff_for_work_area (n, &dx, &dy); - int x = city_x + dx, y = city_y + dy; - wrap_tile_coords (&p_bic_data->Map, &x, &y); - Tile * tile = tile_at (x, y); - if ((tile == NULL) || (tile == p_null_tile)) continue; - if (tile_has_enemy_unit (tile, city_civ_id)) continue; - if (tile->vtable->m20_Check_Pollution (tile, __, 0)) continue; - if (tile->vtable->m38_Get_Territory_OwnerID (tile) != city_civ_id) continue; - struct district_instance * inst = get_district_instance (tile); - if (inst == NULL) continue; - int district_id = inst->district_type; - if ((district_id < 0) || (district_id >= is->district_count)) continue; - if (! district_is_complete (tile, district_id)) continue; + char * cursor = text; + while (*cursor != '\0') { + line_number += 1; - struct district_config const * cfg = &is->district_configs[district_id]; - int district_culture_bonus = 0; - int district_science_bonus = 0; - get_effective_district_yields (inst, cfg, NULL, NULL, NULL, &district_science_bonus, &district_culture_bonus); + char * line_start = cursor; + char * line_end = cursor; + while ((*line_end != '\0') && (*line_end != '\n')) + line_end++; - bool is_neighborhood = (cfg->command == UCV_Build_Neighborhood); - if (is_neighborhood) { - if (utilized_neighborhoods > 0) { - total_culture += district_culture_bonus; - total_science += district_science_bonus; - utilized_neighborhoods--; - } - } else { - total_culture += district_culture_bonus; - total_science += district_science_bonus; - } - } + int line_len = line_end - line_start; + bool has_newline = (*line_end == '\n'); + if (has_newline) + *line_end = '\0'; - if (culture_bonus != NULL) - *culture_bonus = total_culture; - if (science_bonus != NULL) - *science_bonus = total_science; -} + struct string_slice line_slice = { .str = line_start, .len = line_len }; + struct string_slice trimmed = trim_string_slice (&line_slice, 0); + if (line_is_empty_or_comment (&trimmed)) { + cursor = has_newline ? line_end + 1 : line_end; + continue; + } -int __fastcall -patch_City_requires_improvement_to_grow (City * this) -{ - int required_improv = City_requires_improvement_to_grow (this); - if ((required_improv == -1) && - (this != NULL) && - is->current_config.enable_districts && - is->current_config.enable_neighborhood_districts && - city_is_at_neighborhood_cap (this)) { - return 0; // Neighborhood sentinel - } + if (trimmed.str[0] == '#') { + struct string_slice directive = trimmed; + directive.str += 1; + directive.len -= 1; + directive = trim_string_slice (&directive, 0); + if ((directive.len > 0) && slice_matches_str (&directive, "Wonder")) { + if (in_section) + finalize_parsed_natural_wonder_definition (&def, section_start_line, &parse_errors); + in_section = true; + section_start_line = line_number; + } + cursor = has_newline ? line_end + 1 : line_end; + continue; + } - return required_improv; -} + if (! in_section) { + cursor = has_newline ? line_end + 1 : line_end; + continue; + } -// Replacement check specifically for stalled growth check function, -// where we can't pass through the neighborhood sentinel. That function itself -// doesn't block growth, it just triggers the dialog, so safe to skip it if -// neighborhoods are needed. -int __fastcall -patch_City_requires_improvement_to_grow_besides_neighborhood (City * this) -{ - return City_requires_improvement_to_grow (this); -} + struct string_slice key_slice = {0}; + struct string_slice value_slice = {0}; + enum key_value_parse_status status = parse_trimmed_key_value (&trimmed, &key_slice, &value_slice); + if (status == KVP_NO_EQUALS) { + char * line_text = extract_slice (&trimmed); + struct error_line * err = add_error_line (&parse_errors); + snprintf (err->text, sizeof err->text, "^ Line %d: %s (expected '=')", line_number, line_text); + err->text[(sizeof err->text) - 1] = '\0'; + free (line_text); + cursor = has_newline ? line_end + 1 : line_end; + continue; + } else if (status == KVP_EMPTY_KEY) { + struct error_line * err = add_error_line (&parse_errors); + snprintf (err->text, sizeof err->text, "^ Line %d: (missing key)", line_number); + err->text[(sizeof err->text) - 1] = '\0'; + cursor = has_newline ? line_end + 1 : line_end; + continue; + } -void __fastcall -patch_maybe_show_improvement_needed_for_growth_dialog (void * this, int edx, City * city, int * param_2) -{ - int required_improv_id = (int)param_2; - if (is->current_config.enable_districts && - is->current_config.enable_neighborhood_districts && - (required_improv_id == 0)) // Neighborhood sentinel - return; + handle_natural_wonder_definition_key (&def, &key_slice, &value_slice, line_number, &parse_errors, &unrecognized_keys); + cursor = has_newline ? line_end + 1 : line_end; + } - maybe_show_improvement_needed_for_growth_dialog (this, __, city, param_2); -} + if (in_section) + finalize_parsed_natural_wonder_definition (&def, section_start_line, &parse_errors); -void -maybe_show_neighborhood_growth_warning (City * city) -{ - if ((city == NULL) || ! (is->current_config.enable_districts && is->current_config.enable_neighborhood_districts)) return; - int requirement = patch_City_requires_improvement_to_grow (city); - if (requirement != 0) return; // Neighborhood sentinel not present - if (city->Body.FoodIncome <= 0) return; // Not growing - if (is_online_game ()) return; - int civ_id = city->Body.CivID; - if (civ_id != p_main_screen_form->Player_CivID) return; - if ((*p_human_player_bits & (1u << civ_id)) == 0) return; + free_parsed_natural_wonder_definition (&def); + free (text); - unsigned int turn_no = (unsigned int)*p_current_turn_no; - unsigned int throttle = ((unsigned int)city->Body.X << 5) ^ (unsigned int)city->Body.Y; - int frequency = is->current_config.neighborhood_needed_message_frequency; - if ((frequency <= 0) || (((turn_no + throttle) % frequency) != 0)) - return; + // Append to loaded config names list + struct loaded_config_name * top_lcn = is->loaded_config_names; + while (top_lcn->next != NULL) + top_lcn = top_lcn->next; - char msg[160]; - char const * city_name = city->Body.CityName; - snprintf (msg, sizeof msg, "%s %s", city_name, is->c3x_labels[CL_REQUIRES_NEIGHBORHOOD_TO_GROW]); - msg[(sizeof msg) - 1] = '\0'; - show_map_specific_text (city->Body.X, city->Body.Y, msg, true); -} + struct loaded_config_name * new_lcn = malloc (sizeof *new_lcn); + new_lcn->name = strdup (path); + new_lcn->next = NULL; -bool __stdcall -patch_is_not_pop_capped_or_starving (City * city) -{ - bool tr = is_not_pop_capped_or_starving (city); - if (! tr) return false; + top_lcn->next = new_lcn; - if (is->current_config.enable_districts) { - if (city_is_at_neighborhood_cap (city)) - return false; + if (parse_errors != NULL || unrecognized_keys != NULL) { + PopupForm * popup = get_popup_form (); + popup->vtable->set_text_key_and_flags (popup, __, is->mod_script_path, "C3X_WARNING", -1, 0, 0, 0); + char s[200]; + snprintf (s, sizeof s, "Natural Wonder Config errors in %s:", path); + s[(sizeof s) - 1] = '\0'; + PopupForm_add_text (popup, __, s, false); + if (parse_errors != NULL) { + for (struct error_line * line = parse_errors; line != NULL; line = line->next) + PopupForm_add_text (popup, __, line->text, false); + } + if (unrecognized_keys != NULL) { + PopupForm_add_text (popup, __, "", false); + PopupForm_add_text (popup, __, "Unrecognized keys:", false); + for (struct error_line * line = unrecognized_keys; line != NULL; line = line->next) + PopupForm_add_text (popup, __, line->text, false); + } + patch_show_popup (popup, __, 0, 0); + free_error_lines (parse_errors); + free_error_lines (unrecognized_keys); } - - return true; } void -remove_building_if_no_district (City * city, int district_id, int building_id) +load_natural_wonder_configs () { - if ((city == NULL) || (building_id < 0)) return; - if (! patch_City_has_improvement (city, __, building_id, false)) return; - if (city_has_required_district (city, district_id)) return; - - patch_City_add_or_remove_improvement (city, __, building_id, 0, false); + load_natural_wonder_config_file ("default.districts_natural_wonders_config.txt", 1, 1, 1); + load_natural_wonder_config_file ("user.districts_natural_wonders_config.txt", 1, 0, 1); + + char * scenario_filename = "scenario.districts_natural_wonders_config.txt"; + char * scenario_natural_wonder_config_path = BIC_get_asset_path (p_bic_data, __, scenario_filename, false); + if ((scenario_natural_wonder_config_path != NULL) && (0 != strcmp (scenario_filename, scenario_natural_wonder_config_path))) + load_natural_wonder_config_file (scenario_natural_wonder_config_path, 0, 0, 1); } bool -city_has_other_completed_district (City * city, int district_id, int removed_x, int removed_y) +district_config_has_dependent_improvement (struct district_config * cfg, char const * name) { - if (! is->current_config.enable_districts || - (city == NULL) || (district_id < 0) || (district_id >= is->district_count)) + if ((cfg == NULL) || (name == NULL) || (name[0] == '\0')) return false; - int civ_id = city->Body.CivID; - int city_x = city->Body.X; - int city_y = city->Body.Y; - for (int n = 0; n < is->workable_tile_count; n++) { - int dx, dy; - patch_ni_to_diff_for_work_area (n, &dx, &dy); - int x = city_x + dx, y = city_y + dy; - wrap_tile_coords (&p_bic_data->Map, &x, &y); - Tile * candidate = tile_at (x, y); - if ((candidate == NULL) || (candidate == p_null_tile)) - continue; - if ((x == removed_x) && (y == removed_y)) - continue; - - if (candidate->vtable->m38_Get_Territory_OwnerID (candidate) != civ_id) - continue; - - struct district_instance * inst = get_district_instance (candidate); - if (inst == NULL || inst->district_type != district_id) - continue; - - if (! district_is_complete (candidate, district_id)) - continue; - - return true; + for (int i = 0; i < cfg->dependent_improvement_max_index; i++) { + char const * existing = cfg->dependent_improvements[i]; + if ((existing != NULL) && (strcmp (existing, name) == 0)) + return true; } - return false; } bool -district_instance_is_redundant (struct district_instance * inst, Tile * tile) +find_civ_trait_id_by_name (struct string_slice const * name, int * out_id) { - if (! is->current_config.enable_districts || - (inst == NULL) || - (tile == NULL) || (tile == p_null_tile)) + if ((name == NULL) || (name->len <= 0) || (out_id == NULL)) return false; - int district_id = inst->district_type; - if ((district_id < 0) || (district_id >= is->district_count)) - return false; + struct trait_entry { char const * name; int id; } traits[] = { + {"Agricultural", 6}, + {"Commercial", 1}, + {"Expansionist", 2}, + {"Industrious", 5}, + {"Militaristic", 0}, + {"Religious", 4}, + {"Scientific", 3}, + {"Seafaring", 7} + }; - if (! district_is_complete (tile, district_id)) - return false; + for (int i = 0; i < ARRAY_LEN (traits); i++) { + if (slice_matches_str (name, traits[i].name)) { + *out_id = traits[i].id; + return true; + } + } - if (district_id == NEIGHBORHOOD_DISTRICT_ID || district_id == DISTRIBUTION_HUB_DISTRICT_ID) - return false; + return false; +} - int tile_x = inst->tile_x; - int tile_y = inst->tile_y; - if (! district_instance_get_coords (inst, tile, &tile_x, &tile_y)) +bool +find_civ_culture_id_by_name (struct string_slice const * name, int * out_id) +{ + if ((name == NULL) || (name->len <= 0) || (out_id == NULL)) return false; - int civ_id = tile->vtable->m38_Get_Territory_OwnerID (tile); + struct culture_entry { char const * name; int id; } cultures[] = { + {"American", 0}, + {"AMERICAN", 0}, + {"AMER", 0}, + {"European", 1}, + {"EUROPEAN", 1}, + {"EURO", 1}, + {"Roman", 2}, + {"ROMAN", 2}, + {"Mid East", 3}, + {"Mideast", 3}, + {"MIDEAST", 3}, + {"Asian", 4}, + {"ASIAN", 4} + }; - if (district_id == WONDER_DISTRICT_ID) - return inst->wonder_info.state == WDS_UNUSED; + for (int i = 0; i < ARRAY_LEN (cultures); i++) { + if (slice_matches_str (name, cultures[i].name)) { + *out_id = cultures[i].id; + return true; + } + } - bool found_city = false; - for (int n = 0; n < is->workable_tile_count; n++) { - int dx, dy; - patch_ni_to_diff_for_work_area (n, &dx, &dy); - int x = tile_x + dx, y = tile_y + dy; - wrap_tile_coords (&p_bic_data->Map, &x, &y); - Tile * candidate = tile_at (x, y); - if ((candidate == NULL) || (candidate == p_null_tile)) - continue; + return false; +} - City * city = get_city_ptr (candidate->vtable->m45_Get_City_ID (candidate)); - if ((city == NULL) || (city->Body.CivID != civ_id)) - continue; +int +find_district_index_by_name (char const * name) +{ + if ((name == NULL) || (name[0] == '\0')) + return -1; - found_city = true; - if (! city_has_other_completed_district (city, district_id, tile_x, tile_y)) - return false; + for (int i = 0; i < is->district_count; i++) { + char const * existing = is->district_configs[i].name; + if ((existing != NULL) && (strcmp (existing, name) == 0)) + return i; } - return true; + return -1; } -bool -any_nearby_city_would_lose_district_benefits (int district_id, int civ_id, int removed_x, int removed_y) +int +find_wonder_district_index_by_name (char const * name) { - if (! is->current_config.enable_districts) - return false; - - if (district_id < 0 || district_id >= is->district_count) - return false; + if ((name == NULL) || (name[0] == '\0')) + return -1; - struct district_infos * info = &is->district_infos[district_id]; + int improv_id; + if (! stable_look_up (&is->building_name_to_id, (char *)name, &improv_id)) + return -1; - // If there are no dependent buildings, no city can lose benefits - if (info->dependent_building_count == 0) - return false; + return find_wonder_config_index_by_improvement_id (improv_id); +} - // Check all cities within work radius of the removed district - for (int n = 0; n < is->workable_tile_count; n++) { - int dx, dy; - patch_ni_to_diff_for_work_area (n, &dx, &dy); - int x = removed_x + dx, y = removed_y + dy; - wrap_tile_coords (&p_bic_data->Map, &x, &y); - Tile * tile = tile_at (x, y); - if ((tile == NULL) || (tile == p_null_tile)) - continue; +int +find_natural_wonder_index_by_name (char const * name) +{ + if ((name == NULL) || (name[0] == '\0') || (is == NULL)) + return -1; - City * city = get_city_ptr (tile->vtable->m45_Get_City_ID (tile)); - if (city == NULL || city->Body.CivID != civ_id) - continue; + for (int i = 0; i < is->natural_wonder_count; i++) { + char const * existing = is->natural_wonder_configs[i].name; + if ((existing != NULL) && (strcmp (existing, name) == 0)) + return i; + } + return -1; +} - // Check if this city has another completed district of the same type nearby (excluding the one being removed) - if (city_has_other_completed_district (city, district_id, removed_x, removed_y)) - continue; +City * +find_city_by_name (char const * name) +{ + if ((name == NULL) || (name[0] == '\0') || (p_cities == NULL) || (p_cities->Cities == NULL)) + return NULL; - // This city doesn't have another district, check if it has any dependent buildings - for (int i = 0; i < info->dependent_building_count; i++) { - int building_id = info->dependent_building_ids[i]; - if (building_id >= 0 && patch_City_has_improvement (city, __, building_id, false)) - return true; + for (int city_index = 0; city_index <= p_cities->LastIndex; city_index++) { + City * city = get_city_ptr (city_index); + if ((city != NULL) && (city->Body.CityName != NULL)) { + (*p_OutputDebugStringA) (city->Body.CityName); } + if ((city != NULL) && (city->Body.CityName != NULL) && (strcmp (city->Body.CityName, name) == 0)) + return city; } - return false; + return NULL; } void -remove_dependent_buildings_for_district (int district_id, int center_x, int center_y) +set_wonders_dependent_on_wonder_district (void) { - if (! is->current_config.enable_districts || (district_id < 0) || (district_id >= is->district_count)) + if (! is->current_config.enable_districts || + ! is->current_config.enable_wonder_districts) return; - struct district_infos * info = &is->district_infos[district_id]; - - if ((center_x < 0) || (center_y < 0) || - (center_x >= p_bic_data->Map.Width) || (center_y >= p_bic_data->Map.Height)) - return; + struct district_config * cfg = &is->district_configs[WONDER_DISTRICT_ID]; + for (int wi = 0; wi < is->wonder_district_count; wi++) { + char const * wonder_name = is->wonder_district_configs[wi].wonder_name; + if ((wonder_name == NULL) || (wonder_name[0] == '\0')) + continue; + if (district_config_has_dependent_improvement (cfg, wonder_name)) + continue; - for (int n = 0; n < is->workable_tile_count; n++) { - int dx, dy; - patch_ni_to_diff_for_work_area (n, &dx, &dy); - int x = center_x + dx, y = center_y + dy; - wrap_tile_coords (&p_bic_data->Map, &x, &y); - Tile * tile = tile_at (x, y); - if ((tile == NULL) || (tile == p_null_tile)) - continue; - City * city = get_city_ptr (tile->vtable->m45_Get_City_ID (tile)); - if (city == NULL) + int dest = cfg->dependent_improvement_max_index; + if (dest >= ARRAY_LEN (cfg->dependent_improvements)) { continue; + } - if (city_has_other_completed_district (city, district_id, center_x, center_y)) + char * copy = strdup (wonder_name); + if (copy == NULL) { continue; - - for (int i = 0; i < info->dependent_building_count; i++) { - int building_id = info->dependent_building_ids[i]; - if (building_id >= 0) { - // This also loops through tiles around the city but is not redundant, as the city - // may have multiple districts of the same type in its radius (eg outside radius of this particular district) - remove_building_if_no_district (city, district_id, building_id); - } } + + cfg->dependent_improvements[dest] = copy; + cfg->dependent_improvement_max_index = dest + 1; } + + if (! cfg->has_img_column_count_override && + (cfg->img_column_count < cfg->dependent_improvement_max_index + 1)) + cfg->img_column_count = cfg->dependent_improvement_max_index + 1; } void -remove_wonder_improvement_for_destroyed_district (int wonder_improv_id) +resolve_district_bonus_building_entries (struct district_bonus_list * list, + char const * district_name, + char const * bonus_name, + struct error_line ** parse_errors) { - if ((wonder_improv_id < 0) || (wonder_improv_id >= p_bic_data->ImprovementsCount)) - return; - - if ((p_cities == NULL) || (p_cities->Cities == NULL)) + if (list == NULL) return; - for (int idx = 0; idx <= p_cities->LastIndex; idx++) { - City * city = get_city_ptr (idx); - if (city == NULL) + for (int i = 0; i < list->count; i++) { + struct district_bonus_entry * entry = &list->entries[i]; + if (entry->type != DBET_BUILDING) continue; - if (! patch_City_has_improvement (city, __, wonder_improv_id, false)) + if ((entry->building_name == NULL) || (entry->building_name[0] == '\0')) { + entry->building_id = -1; continue; + } - patch_City_add_or_remove_improvement (city, __, wonder_improv_id, 0, false); + int improv_id; + struct string_slice improv_name = { .str = (char *)entry->building_name, .len = (int)strlen (entry->building_name) }; + if (find_game_object_id_by_name (GOK_BUILDING, &improv_name, 0, &improv_id)) { + entry->building_id = improv_id; + stable_insert (&is->building_name_to_id, improv_name.str, improv_id); + } else { + entry->building_id = -1; + struct error_line * err = add_error_line (parse_errors); + snprintf (err->text, sizeof err->text, "^ District \"%s\": %s entry \"%.*s\" not found", + district_name, bonus_name, improv_name.len, improv_name.str); + err->text[(sizeof err->text) - 1] = '\0'; + } } } -void -handle_district_removed (Tile * tile, int district_id, int center_x, int center_y, bool leave_ruins) +void parse_building_and_tech_ids () { - if ((tile == NULL) || (tile == p_null_tile) || ! is->current_config.enable_districts) - return; + struct c3x_config * cfg = &is->current_config; + char ss[200]; + struct error_line * district_parse_errors = NULL; + struct error_line * wonder_parse_errors = NULL; + for (int i = 0; i < is->district_count; i++) { + char const * district_name = (is->district_configs[i].name != NULL) ? is->district_configs[i].name : "District"; + if (is->district_configs[i].command != 0) + itable_insert (&is->command_id_to_district_id, is->district_configs[i].command, i); + is->district_infos[i].advance_prereq_count = 0; + for (int j = 0; j < ARRAY_LEN (is->district_infos[i].advance_prereq_ids); j++) + is->district_infos[i].advance_prereq_ids[j] = -1; + is->district_infos[i].obsoleted_by_id = -1; + is->district_infos[i].resource_prereq_count = 0; + for (int j = 0; j < MAX_DISTRICT_DEPENDENTS; j++) + is->district_infos[i].resource_prereq_ids[j] = -1; + is->district_infos[i].resource_prereq_on_tile_id = -1; + is->district_infos[i].wonder_prereq_count = 0; + for (int j = 0; j < ARRAY_LEN (is->district_infos[i].wonder_prereq_ids); j++) + is->district_infos[i].wonder_prereq_ids[j] = -1; + is->district_infos[i].natural_wonder_prereq_count = 0; + for (int j = 0; j < ARRAY_LEN (is->district_infos[i].natural_wonder_prereq_ids); j++) + is->district_infos[i].natural_wonder_prereq_ids[j] = -1; - int wonder_windex = -1; - int wonder_improv_id = -1; + // Map advance prereqs to districts + int stored_tech_count = 0; + for (int j = 0; j < is->district_configs[i].advance_prereq_count; j++) { + char const * prereq = is->district_configs[i].advance_prereqs[j]; + if (prereq == NULL || prereq[0] == '\0') + continue; + int tech_id; + struct string_slice tech_name = { .str = (char *)prereq, .len = (int)strlen (prereq) }; + if (find_game_object_id_by_name (GOK_TECHNOLOGY, &tech_name, 0, &tech_id)) { + snprintf (ss, sizeof ss, "Found tech prereq \"%.*s\" for district \"%s\", ID %d\n", tech_name.len, tech_name.str, district_name, tech_id); + (*p_OutputDebugStringA) (ss); + if (stored_tech_count < ARRAY_LEN (is->district_infos[i].advance_prereq_ids)) { + is->district_infos[i].advance_prereq_ids[stored_tech_count] = tech_id; + stored_tech_count++; + } + itable_insert (&is->district_tech_prereqs, tech_id, i); + } else { + struct error_line * err = add_error_line (&district_parse_errors); + snprintf (err->text, sizeof err->text, "^ District \"%s\": advance_prereqs entry \"%.*s\" not found", district_name, tech_name.len, tech_name.str); + err->text[(sizeof err->text) - 1] = '\0'; + } + } + is->district_infos[i].advance_prereq_count = stored_tech_count; - // Get wonder district info before removing - struct wonder_district_info * info = get_wonder_district_info (tile); - if (info != NULL && info->state == WDS_COMPLETED) - wonder_windex = info->wonder_index; + // Map obsoleted_by to tech ID + if (is->district_configs[i].obsoleted_by != NULL && is->district_configs[i].obsoleted_by != "") { + int tech_id; + struct string_slice tech_name = { .str = (char *)is->district_configs[i].obsoleted_by, .len = (int)strlen (is->district_configs[i].obsoleted_by) }; + if (find_game_object_id_by_name (GOK_TECHNOLOGY, &tech_name, 0, &tech_id)) { + snprintf (ss, sizeof ss, "Found tech obsoleted_by \"%.*s\" for district \"%s\", ID %d\n", tech_name.len, tech_name.str, district_name, tech_id); + (*p_OutputDebugStringA) (ss); + is->district_infos[i].obsoleted_by_id = tech_id; + } else { + is->district_infos[i].obsoleted_by_id = -1; + struct error_line * err = add_error_line (&district_parse_errors); + snprintf (err->text, sizeof err->text, "^ District \"%s\": obsoleted_by \"%.*s\" not found", district_name, tech_name.len, tech_name.str); + err->text[(sizeof err->text) - 1] = '\0'; + } + } - int actual_district_id = district_id; - if (actual_district_id < 0) { - struct district_instance * inst = get_district_instance (tile); - if (inst != NULL) - actual_district_id = inst->district_type; - } + // Map resource prereqs to districts (multiple resources now supported) + int stored_res_count = 0; + for (int j = 0; j < is->district_configs[i].resource_prereq_count; j++) { + if (is->district_configs[i].resource_prereqs[j] == "" || is->district_configs[i].resource_prereqs[j] == NULL) + continue; + int res_id; + struct string_slice res_name = { .str = (char *)is->district_configs[i].resource_prereqs[j], .len = (int)strlen (is->district_configs[i].resource_prereqs[j]) }; + if (find_game_object_id_by_name (GOK_RESOURCE, &res_name, 0, &res_id)) { + snprintf (ss, sizeof ss, "Found resource prereq \"%.*s\" for district \"%s\", ID %d\n", res_name.len, res_name.str, district_name, res_id); + (*p_OutputDebugStringA) (ss); + if (stored_res_count < ARRAY_LEN (is->district_infos[i].resource_prereq_ids)) { + is->district_infos[i].resource_prereq_ids[stored_res_count] = res_id; + stored_res_count++; + } + } else { + struct error_line * err = add_error_line (&district_parse_errors); + snprintf (err->text, sizeof err->text, "^ District \"%s\": resource_prereq \"%.*s\" not found", district_name, res_name.len, res_name.str); + err->text[(sizeof err->text) - 1] = '\0'; + } + } + is->district_infos[i].resource_prereq_count = stored_res_count; + if (is->district_configs[i].resource_prereq_on_tile != NULL && is->district_configs[i].resource_prereq_on_tile != "") { + int res_id; + struct string_slice res_name = { .str = (char *)is->district_configs[i].resource_prereq_on_tile, .len = (int)strlen (is->district_configs[i].resource_prereq_on_tile) }; + if (find_game_object_id_by_name (GOK_RESOURCE, &res_name, 0, &res_id)) { + snprintf (ss, sizeof ss, "Found on-tile resource prereq \"%.*s\" for district \"%s\", ID %d\n", res_name.len, res_name.str, district_name, res_id); + (*p_OutputDebugStringA) (ss); + is->district_infos[i].resource_prereq_on_tile_id = res_id; + } else { + is->district_infos[i].resource_prereq_on_tile_id = -1; + struct error_line * err = add_error_line (&district_parse_errors); + snprintf (err->text, sizeof err->text, "^ District \"%s\": resource_prereq_on_tile \"%.*s\" not found", district_name, res_name.len, res_name.str); + err->text[(sizeof err->text) - 1] = '\0'; + } + } - remove_district_instance (tile); + int stored_wonder_count = 0; + for (int j = 0; j < is->district_configs[i].wonder_prereq_count; j++) { + if (is->district_configs[i].wonder_prereqs[j] == "" || is->district_configs[i].wonder_prereqs[j] == NULL) + continue; + int improv_id; + struct string_slice wonder_name = { .str = (char *)is->district_configs[i].wonder_prereqs[j], .len = (int)strlen (is->district_configs[i].wonder_prereqs[j]) }; + if (find_game_object_id_by_name (GOK_BUILDING, &wonder_name, 0, &improv_id)) { + if (stored_wonder_count < ARRAY_LEN (is->district_infos[i].wonder_prereq_ids)) { + is->district_infos[i].wonder_prereq_ids[stored_wonder_count] = improv_id; + stored_wonder_count += 1; + } + stable_insert (&is->building_name_to_id, wonder_name.str, improv_id); + } else { + struct error_line * err = add_error_line (&district_parse_errors); + snprintf (err->text, sizeof err->text, "^ District \"%s\": wonder_prereqs entry \"%.*s\" not found", district_name, wonder_name.len, wonder_name.str); + err->text[(sizeof err->text) - 1] = '\0'; + } + } + is->district_infos[i].wonder_prereq_count = stored_wonder_count; - if (is->current_config.enable_wonder_districts && - (actual_district_id == WONDER_DISTRICT_ID) && - (wonder_windex >= 0)) - wonder_improv_id = get_wonder_improvement_id_from_index (wonder_windex); + int stored_natural_wonder_count = 0; + for (int j = 0; j < is->district_configs[i].natural_wonder_prereq_count; j++) { + if (is->district_configs[i].natural_wonder_prereqs[j] == "" || is->district_configs[i].natural_wonder_prereqs[j] == NULL) + continue; + int natural_wonder_id = -1; + char const * name = is->district_configs[i].natural_wonder_prereqs[j]; + if (stable_look_up (&is->natural_wonder_name_to_id, (char *)name, &natural_wonder_id)) { + if (stored_natural_wonder_count < ARRAY_LEN (is->district_infos[i].natural_wonder_prereq_ids)) { + is->district_infos[i].natural_wonder_prereq_ids[stored_natural_wonder_count] = natural_wonder_id; + stored_natural_wonder_count += 1; + } + } else { + struct error_line * err = add_error_line (&district_parse_errors); + snprintf (err->text, sizeof err->text, "^ District \"%s\": natural_wonder_prereqs entry \"%s\" not found", district_name, name); + err->text[(sizeof err->text) - 1] = '\0'; + } + } + is->district_infos[i].natural_wonder_prereq_count = stored_natural_wonder_count; - if (wonder_improv_id >= 0 && is->current_config.completed_wonder_districts_can_be_destroyed) - remove_wonder_improvement_for_destroyed_district (wonder_improv_id); + for (int j = 0; j < ARRAY_LEN (is->district_configs[i].buildable_on_district_ids); j++) + is->district_configs[i].buildable_on_district_ids[j] = -1; + is->district_configs[i].buildable_on_district_id_count = 0; - if (is->current_config.enable_distribution_hub_districts && - (actual_district_id == DISTRIBUTION_HUB_DISTRICT_ID)) - remove_distribution_hub_record (tile); + if (is->district_configs[i].has_buildable_on_districts) { + int stored_buildable_on_count = 0; + for (int j = 0; j < is->district_configs[i].buildable_on_district_count; j++) { + char const * name = is->district_configs[i].buildable_on_districts[j]; + if (name == NULL || name[0] == '\0') + continue; + int other_district_id = find_district_index_by_name (name); + if (other_district_id >= 0) { + if (stored_buildable_on_count < ARRAY_LEN (is->district_configs[i].buildable_on_district_ids)) { + is->district_configs[i].buildable_on_district_ids[stored_buildable_on_count] = other_district_id; + stored_buildable_on_count += 1; + } + } else { + struct error_line * err = add_error_line (&district_parse_errors); + snprintf (err->text, sizeof err->text, "^ District \"%s\": buildable_on_districts entry \"%s\" not found", district_name, name); + err->text[(sizeof err->text) - 1] = '\0'; + } + } + is->district_configs[i].buildable_on_district_id_count = stored_buildable_on_count; + } - if (district_id >= 0) - remove_dependent_buildings_for_district (district_id, center_x, center_y); + for (int j = 0; j < ARRAY_LEN (is->district_configs[i].buildable_adjacent_to_district_ids); j++) + is->district_configs[i].buildable_adjacent_to_district_ids[j] = -1; + is->district_configs[i].buildable_adjacent_to_district_id_count = 0; - // Make the tile workable again by resetting CityAreaID and recomputing yields for nearby cities - tile->Body.CityAreaID = -1; + if (is->district_configs[i].has_buildable_adjacent_to_districts) { + int stored_adjacent_count = 0; + for (int j = 0; j < is->district_configs[i].buildable_adjacent_to_district_count; j++) { + char const * name = is->district_configs[i].buildable_adjacent_to_districts[j]; + if (name == NULL || name[0] == '\0') + continue; + int other_district_id = find_district_index_by_name (name); + if (other_district_id >= 0) { + if (stored_adjacent_count < ARRAY_LEN (is->district_configs[i].buildable_adjacent_to_district_ids)) { + is->district_configs[i].buildable_adjacent_to_district_ids[stored_adjacent_count] = other_district_id; + stored_adjacent_count += 1; + } + } else { + struct error_line * err = add_error_line (&district_parse_errors); + snprintf (err->text, sizeof err->text, "^ District \"%s\": buildable_adjacent_to_districts entry \"%s\" not found", district_name, name); + err->text[(sizeof err->text) - 1] = '\0'; + } + } + is->district_configs[i].buildable_adjacent_to_district_id_count = stored_adjacent_count; + } - int tile_owner = tile->vtable->m38_Get_Territory_OwnerID (tile); + // Resolve generated resource name to ID + if (is->district_configs[i].generated_resource != NULL && is->district_configs[i].generated_resource != "") { + int res_id; + struct string_slice res_name = { .str = (char *)is->district_configs[i].generated_resource, .len = (int)strlen (is->district_configs[i].generated_resource) }; + if (find_game_object_id_by_name (GOK_RESOURCE, &res_name, 0, &res_id)) { + snprintf (ss, sizeof ss, "Found generated resource \"%.*s\" for district \"%s\", ID %d\n", res_name.len, res_name.str, district_name, res_id); + (*p_OutputDebugStringA) (ss); + is->district_configs[i].generated_resource_id = res_id; + } else { + is->district_configs[i].generated_resource_id = -1; + struct error_line * err = add_error_line (&district_parse_errors); + snprintf (err->text, sizeof err->text, "^ District \"%s\": generated_resource \"%.*s\" not found", district_name, res_name.len, res_name.str); + err->text[(sizeof err->text) - 1] = '\0'; + } + } - // Check all tiles within city work radius to find cities that might now be able to work this tile - for (int n = 0; n < is->workable_tile_count; n++) { - int dx, dy; - patch_ni_to_diff_for_work_area (n, &dx, &dy); - int x = center_x + dx, y = center_y + dy; - wrap_tile_coords (&p_bic_data->Map, &x, &y); - Tile * nearby_tile = tile_at (x, y); - if ((nearby_tile == NULL) || (nearby_tile == p_null_tile)) - continue; + // Map improvement prereqs to districts + int stored_count = 0; + for (int j = 0; j < is->district_configs[i].dependent_improvement_max_index; j++) { + int improv_id; + if (is->district_configs[i].dependent_improvements[j] == "" || is->district_configs[i].dependent_improvements[j] == NULL) + continue; - int city_id = nearby_tile->vtable->m45_Get_City_ID (nearby_tile); - if (city_id < 0) - continue; + // Gate wonder district prereqs behind enable_wonder_districts + if ((is->district_configs[i].command == UCV_Build_WonderDistrict) && (! cfg->enable_wonder_districts)) + continue; - City * city = get_city_ptr (city_id); - if (city == NULL) - continue; + struct string_slice improv_name = { .str = (char *)is->district_configs[i].dependent_improvements[j], .len = (int)strlen (is->district_configs[i].dependent_improvements[j]) }; + if (find_game_object_id_by_name (GOK_BUILDING, &improv_name, 0, &improv_id)) { + snprintf (ss, sizeof ss, "Found improvement prereq \"%.*s\" for district \"%s\", ID %d\n", improv_name.len, improv_name.str, district_name, improv_id); + (*p_OutputDebugStringA) (ss); + if (stored_count < ARRAY_LEN (is->district_infos[i].dependent_building_ids)) { + is->district_infos[i].dependent_building_ids[stored_count] = improv_id; + stored_count += 1; + } + add_district_building_prereq (improv_id, i); + stable_insert (&is->building_name_to_id, improv_name.str, improv_id); + } else { + is->district_infos[i].dependent_building_ids[j] = -1; + struct error_line * err = add_error_line (&district_parse_errors); + snprintf (err->text, sizeof err->text, "^ District \"%s\": dependent_improvs entry \"%.*s\" not found", district_name, improv_name.len, improv_name.str); + err->text[(sizeof err->text) - 1] = '\0'; + } + is->district_infos[i].dependent_building_count = stored_count; + } + + resolve_district_bonus_building_entries (&is->district_configs[i].culture_bonus_extras, district_name, "culture_bonus", &district_parse_errors); + resolve_district_bonus_building_entries (&is->district_configs[i].science_bonus_extras, district_name, "science_bonus", &district_parse_errors); + resolve_district_bonus_building_entries (&is->district_configs[i].food_bonus_extras, district_name, "food_bonus", &district_parse_errors); + resolve_district_bonus_building_entries (&is->district_configs[i].gold_bonus_extras, district_name, "gold_bonus", &district_parse_errors); + resolve_district_bonus_building_entries (&is->district_configs[i].shield_bonus_extras, district_name, "shield_bonus", &district_parse_errors); + resolve_district_bonus_building_entries (&is->district_configs[i].happiness_bonus_extras, district_name, "happiness_bonus", &district_parse_errors); + resolve_district_bonus_building_entries (&is->district_configs[i].defense_bonus_extras, district_name, "defense_bonus_percent", &district_parse_errors); + } - // Only recompute for cities of the same civ that owns this tile - if (city->Body.CivID != tile_owner) + // Map wonder names to their improvement IDs for rendering under-construction wonders + for (int wi = 0; wi < is->wonder_district_count; wi++) { + if (is->wonder_district_configs[wi].wonder_name == NULL || is->wonder_district_configs[wi].wonder_name[0] == '\0') continue; - recompute_city_yields_with_districts (city); + int improv_id; + struct string_slice wonder_name = { .str = (char *)is->wonder_district_configs[wi].wonder_name, .len = (int)strlen (is->wonder_district_configs[wi].wonder_name) }; + if (find_game_object_id_by_name (GOK_BUILDING, &wonder_name, 0, &improv_id)) { + snprintf (ss, sizeof ss, "Found improvement prereq \"%.*s\" for wonder district \"%s\", ID %d\n", wonder_name.len, wonder_name.str, is->wonder_district_configs[wi].wonder_name, improv_id); + (*p_OutputDebugStringA) (ss); + stable_insert (&is->building_name_to_id, wonder_name.str, improv_id); + } else { + snprintf (ss, sizeof ss, "Could not find improvement prereq \"%.*s\" for wonder district \"%s\"\n", wonder_name.len, wonder_name.str, is->wonder_district_configs[wi].wonder_name); + (*p_OutputDebugStringA) (ss); + struct error_line * err = add_error_line (&wonder_parse_errors); + snprintf (err->text, sizeof err->text, "^ Wonder district \"%s\": improvement \"%.*s\" not found", (is->wonder_district_configs[wi].wonder_name != NULL) ? is->wonder_district_configs[wi].wonder_name : "Wonder District", wonder_name.len, wonder_name.str); + err->text[(sizeof err->text) - 1] = '\0'; + } } - if (leave_ruins && (tile->vtable->m60_Set_Ruins != NULL)) { - tile->vtable->m51_Unset_Tile_Flags (tile, __, 0, TILE_FLAG_MINE, center_x, center_y); - tile->vtable->m60_Set_Ruins (tile, __, 1); - p_main_screen_form->vtable->m73_call_m22_Draw ((Base_Form *)p_main_screen_form); - } -} + if ((district_parse_errors != NULL) || (wonder_parse_errors != NULL)) { + PopupForm * popup = get_popup_form (); + popup->vtable->set_text_key_and_flags (popup, __, is->mod_script_path, "C3X_WARNING", -1, 0, 0, 0); -bool -city_has_active_wonder_for_district (City * city) -{ - struct district_infos * info = &is->district_infos[WONDER_DISTRICT_ID]; - for (int n = 0; n < ARRAY_LEN (info->dependent_building_ids); n++) { - int building_id = info->dependent_building_ids[n]; - if ((building_id >= 0) && has_active_building (city, building_id)) - return true; - } - return false; -} + if (district_parse_errors != NULL) { + char header[256]; + if (is->current_districts_config_path[0] != '\0') + snprintf (header, sizeof header, "District Config errors in %s:", is->current_districts_config_path); + else + snprintf (header, sizeof header, "District Config lookup errors:"); + header[(sizeof header) - 1] = '\0'; + PopupForm_add_text (popup, __, header, false); + for (struct error_line * line = district_parse_errors; line != NULL; line = line->next) + PopupForm_add_text (popup, __, line->text, false); + } -bool -city_requires_district_for_improvement (City * city, int improv_id, int * out_district_id) -{ - int district_id; - if (! itable_look_up (&is->district_building_prereqs, improv_id, &district_id)) - return false; - if (is->current_config.enable_wonder_districts) { - if (district_id == WONDER_DISTRICT_ID) { - if (city_has_wonder_district_with_no_completed_wonder (city)) - return false; - if (out_district_id != NULL) - *out_district_id = district_id; - return true; + if ((district_parse_errors != NULL) && (wonder_parse_errors != NULL)) + PopupForm_add_text (popup, __, "", false); + + if (wonder_parse_errors != NULL) { + char header[256]; + snprintf (header, sizeof header, "Wonder District Config lookup errors:"); + header[(sizeof header) - 1] = '\0'; + PopupForm_add_text (popup, __, header, false); + for (struct error_line * line = wonder_parse_errors; line != NULL; line = line->next) + PopupForm_add_text (popup, __, line->text, false); } + + patch_show_popup (popup, __, 0, 0); + free_error_lines (district_parse_errors); + free_error_lines (wonder_parse_errors); } - if (city_has_required_district (city, district_id)) - return false; - if (out_district_id != NULL) - *out_district_id = district_id; - return true; } void -clear_best_feasible_order (City * city) +load_districts_config () { - int key = (int)(long)city; - int stored_int; - if (itable_look_up (&is->ai_best_feasible_orders, key, &stored_int)) { - struct ai_best_feasible_order * stored = (struct ai_best_feasible_order *)(long)stored_int; - free (stored); - itable_remove (&is->ai_best_feasible_orders, key); - } + clear_dynamic_district_definitions (); + load_dynamic_district_configs (); + load_dynamic_wonder_configs (); + load_natural_wonder_configs (); + is->district_count = is->special_district_count + is->dynamic_district_count; + + set_wonders_dependent_on_wonder_district (); + parse_building_and_tech_ids (); } void -record_best_feasible_order (City * city, City_Order const * order, int value) +place_natural_wonders_on_map (void) { - int key = (int)(long)city; - int stored_int; - struct ai_best_feasible_order * stored; - if (itable_look_up (&is->ai_best_feasible_orders, key, &stored_int)) - stored = (struct ai_best_feasible_order *)(long)stored_int; - else { - stored = malloc (sizeof *stored); - if (stored == NULL) - return; - stored->order = *order; - stored->value = value; - itable_insert (&is->ai_best_feasible_orders, key, (int)(long)stored); + if (! is->current_config.enable_natural_wonders) return; - } - if (value > stored->value) { - stored->order = *order; - stored->value = value; - } -} + int wonder_count = is->natural_wonder_count; + if (wonder_count <= 0) + return; -struct ai_best_feasible_order * -get_best_feasible_order (City * city) -{ - int stored_int; - if (itable_look_up (&is->ai_best_feasible_orders, (int)(long)city, &stored_int)) - return (struct ai_best_feasible_order *)(long)stored_int; - else - return NULL; -} + struct natural_wonder_candidate_list * candidate_lists = (struct natural_wonder_candidate_list *)calloc (wonder_count, sizeof *candidate_lists); + bool * already_placed = (bool *)calloc (wonder_count, sizeof *already_placed); -bool -city_is_currently_building_wonder (City * city) -{ - if ((city == NULL) || (city->Body.Order_Type != COT_Improvement)) - return false; - int order_id = city->Body.Order_ID; - if ((order_id < 0) || (order_id >= p_bic_data->ImprovementsCount)) - return false; - return (p_bic_data->Improvements[order_id].Characteristics & (ITC_Wonder | ITC_Small_Wonder)) != 0; -} + if ((candidate_lists == NULL) || (already_placed == NULL)) { + if (candidate_lists != NULL) free (candidate_lists); + if (already_placed != NULL) free (already_placed); + return; + } -bool -wonder_district_tile_under_construction (Tile * tile, int tile_x, int tile_y, int * out_windex) -{ - if (! is->current_config.enable_wonder_districts || - (tile == NULL) || (tile == p_null_tile)) - return false; + struct wonder_location * placements = NULL; + int placement_count = 0; + int placement_capacity = 0; + int existing_count = 0; - struct district_instance * inst = get_district_instance (tile); - if (inst == NULL || inst->district_type != WONDER_DISTRICT_ID) - return false; + // Record existing natural wonders + FOR_TABLE_ENTRIES (tei, &is->district_tile_map) { + struct district_instance * inst = (struct district_instance *)tei.value; + if ((inst == NULL) || (inst->district_id != NATURAL_WONDER_DISTRICT_ID)) + continue; - struct wonder_district_info * info = get_wonder_district_info (tile); - if (info == NULL || info->state != WDS_UNDER_CONSTRUCTION) - return false; + int wonder_id = inst->natural_wonder_info.natural_wonder_id; + if ((wonder_id < 0) || (wonder_id >= wonder_count)) + continue; - City * reserved_city = get_city_ptr (info->city_id); - if (reserved_city == NULL) - return false; - info->city = reserved_city; - info->city_id = reserved_city->Body.ID; + already_placed[wonder_id] = true; - // Verify the reserved city is still building a wonder - if (! city_is_currently_building_wonder (reserved_city)) - return false; + Tile * tile = (Tile *)tei.key; + int tile_x, tile_y; + if (! district_instance_get_coords (inst, tile, &tile_x, &tile_y)) + continue; - // Verify this tile is within the reserved city's radius - if (! city_radius_contains_tile (reserved_city, tile_x, tile_y)) - return false; + if (placement_count >= placement_capacity) { + int new_capacity = (placement_capacity > 0) ? placement_capacity * 2 : 8; + struct wonder_location * grown = + (struct wonder_location *)realloc (placements, new_capacity * sizeof *grown); + if (grown == NULL) + continue; + placements = grown; + placement_capacity = new_capacity; + } - // Get the wonder index for the wonder being built - if (out_windex != NULL) { - int order_id = reserved_city->Body.Order_ID; - int windex = find_wonder_config_index_by_improvement_id (order_id); - *out_windex = windex; + if (placements != NULL) { + placements[placement_count++] = (struct wonder_location){ + .x = (short)tile_x, + .y = (short)tile_y + }; + existing_count += 1; + } } - return true; -} + // Build candidate lists + int map_width = p_bic_data->Map.Width; + int map_height = p_bic_data->Map.Height; + int minimum_separation = is->current_config.minimum_natural_wonder_separation; + + for (int y = 0; y < map_height; y++) { + for (int x = 0; x < map_width; x++) { + Tile * tile = tile_at (x, y); + if ((tile == NULL) || (tile == p_null_tile)) + continue; -bool -city_needs_wonder_district (City * city) -{ - if (! is->current_config.enable_wonder_districts || (city == NULL)) - return false; + if (! natural_wonder_tile_is_clear (tile, x, y)) continue; - int pending_improv_id; - if (lookup_pending_building_order (city, &pending_improv_id)) { - // Check if it's actually a wonder - if ((pending_improv_id >= 0) && (pending_improv_id < p_bic_data->ImprovementsCount)) { - Improvement * improv = &p_bic_data->Improvements[pending_improv_id]; - if (improv->Characteristics & (ITC_Wonder | ITC_Small_Wonder)) - return true; - } - } - if (city_is_currently_building_wonder (city)) - return true; - if (city_has_active_wonder_for_district (city)) - return true; - return false; -} + if (natural_wonder_exists_within_distance (x, y, minimum_separation)) + continue; -bool -city_has_assigned_wonder_district (City * city, Tile * ignore_tile) -{ - if (! is->current_config.enable_wonder_districts || (city == NULL)) - return false; + for (int ni = 0; ni < wonder_count; ni++) { + if (already_placed[ni]) + continue; - int civ_id = city->Body.CivID; - int city_x = city->Body.X; - int city_y = city->Body.Y; - for (int n = 0; n < is->workable_tile_count; n++) { - int dx, dy; - patch_ni_to_diff_for_work_area (n, &dx, &dy); - int x = city_x + dx, y = city_y + dy; - wrap_tile_coords (&p_bic_data->Map, &x, &y); - Tile * candidate = tile_at (x, y); - if ((candidate == NULL) || (candidate == p_null_tile) || (candidate == ignore_tile)) - continue; - if (candidate->vtable->m38_Get_Territory_OwnerID (candidate) != civ_id) - continue; + struct natural_wonder_district_config const * cfg = &is->natural_wonder_configs[ni]; + if (cfg->name == NULL) + continue; - struct district_instance * inst = get_district_instance (candidate); - if ((inst == NULL) || (inst->district_type != WONDER_DISTRICT_ID)) - continue; + if (! natural_wonder_terrain_matches (cfg, tile, x, y)) + continue; - struct wonder_district_info * info = inst ? &inst->wonder_info : NULL; - if ((info != NULL) && - (info->state == WDS_UNDER_CONSTRUCTION) && - (info->city_id == city->Body.ID)) { - info->city = city; - return true; + natural_wonder_candidate_list_push (&candidate_lists[ni], tile, x, y); + } } } - return false; -} - -bool -free_wonder_district_for_city (City * city) -{ - if (! is->current_config.enable_wonder_districts || (city == NULL)) - return false; - if ((*p_human_player_bits & (1 << city->Body.CivID)) != 0) - return false; - if (city_needs_wonder_district (city)) - return false; + bool wraps_horiz = (p_bic_data->Map.Flags & 1) != 0; + int newly_placed = 0; + int * wonder_order = (int *)malloc (wonder_count * sizeof *wonder_order); + if (wonder_order != NULL) { + for (int i = 0; i < wonder_count; i++) + wonder_order[i] = i; + for (int i = wonder_count - 1; i > 0; i--) { + int swap_index = rand_int (p_rand_object, __, i + 1); + int temp = wonder_order[i]; + wonder_order[i] = wonder_order[swap_index]; + wonder_order[swap_index] = temp; + } + } - int city_x = city->Body.X; - int city_y = city->Body.Y; - for (int n = 0; n < is->workable_tile_count; n++) { - int dx, dy; - patch_ni_to_diff_for_work_area (n, &dx, &dy); - int x = city_x + dx, y = city_y + dy; - wrap_tile_coords (&p_bic_data->Map, &x, &y); - Tile * tile = tile_at (x, y); - if (tile == p_null_tile) - continue; - struct district_instance * inst = get_district_instance (tile); - if (inst == NULL || inst->district_type != WONDER_DISTRICT_ID) + for (int order_index = 0; order_index < wonder_count; order_index++) { + int ni = (wonder_order != NULL) ? wonder_order[order_index] : order_index; + if (already_placed[ni]) continue; - struct wonder_district_info * info = get_wonder_district_info (tile); - if (info != NULL && info->state == WDS_COMPLETED) - continue; // Don't remove completed wonder districts - - int tile_x, tile_y; - if (! district_instance_get_coords (inst, tile, &tile_x, &tile_y)) + struct natural_wonder_candidate_list * list = &candidate_lists[ni]; + if (list->count == 0) { + char msg[256]; + snprintf (msg, sizeof msg, "[C3X] No valid tiles to place natural wonder \"%s\".\n", + (is->natural_wonder_configs[ni].name != NULL) ? is->natural_wonder_configs[ni].name : "Natural Wonder"); + (*p_OutputDebugStringA) (msg); continue; - tile->vtable->m51_Unset_Tile_Flags (tile, __, 0, TILE_FLAG_MINE, tile_x, tile_y); + } - handle_district_removed (tile, WONDER_DISTRICT_ID, city->Body.X, city->Body.Y, false); - return true; - } + int best_index = -1; + int best_dist = -1; + int best_adjacent_count = -1; + int best_target_diff = INT_MAX; + int best_rand = INT_MAX; + int best_same_type_count = -1; + int best_continent_priority = INT_MAX; + int target_x = (wonder_count > 0) + ? (int)(((long long)(2 * ni + 1) * map_width) / (2 * wonder_count)) + : (map_width >> 1); - return false; -} + for (int ci = 0; ci < list->count; ci++) { + struct natural_wonder_candidate * cand = &list->entries[ci]; + Tile * tile = cand->tile; + if ((tile == NULL) || (tile == p_null_tile)) continue; + if (get_district_instance (tile) != NULL) continue; + if (! natural_wonder_tile_is_clear (tile, cand->x, cand->y)) continue; + if (! natural_wonder_terrain_matches (&is->natural_wonder_configs[ni], tile, cand->x, cand->y)) continue; + if (natural_wonder_exists_within_distance (cand->x, cand->y, minimum_separation)) continue; -bool -reserve_wonder_district_for_city (City * city) -{ - if (! is->current_config.enable_wonder_districts || (city == NULL)) - return false; + int min_dist_sq = natural_wonder_min_distance_sq (cand->x, cand->y, placements, placement_count); + if (min_dist_sq == INT_MAX) { + int span = (map_width * map_width) + (map_height * map_height); + if (span <= 0) + span = INT_MAX; + min_dist_sq = span; + } - if (city_has_assigned_wonder_district (city, NULL)) - return true; + int dx_raw = int_abs (cand->x - target_x); + int dx_adjusted = compute_wrapped_component (dx_raw, map_width, wraps_horiz); + int rand_val = rand_int (p_rand_object, __, 0x7FFF); - int civ_id = city->Body.CivID; - int city_x = city->Body.X; - int city_y = city->Body.Y; - for (int n = 0; n < is->workable_tile_count; n++) { - int dx, dy; - patch_ni_to_diff_for_work_area (n, &dx, &dy); - int x = city_x + dx, y = city_y + dy; - wrap_tile_coords (&p_bic_data->Map, &x, &y); - Tile * candidate = tile_at (x, y); - if (candidate == NULL || candidate == p_null_tile) continue; - if (candidate->vtable->m38_Get_Territory_OwnerID (candidate) != civ_id) continue; + int continent_id = tile->vtable->m46_Get_ContinentID (tile); + int continent_priority = 1; + if ((continent_id >= 0) && ! continent_has_natural_wonder (continent_id, placements, placement_count)) + continent_priority = 0; - struct district_instance * inst = get_district_instance (candidate); - if (inst == NULL || inst->district_type != WONDER_DISTRICT_ID) continue; - if (! district_is_complete (candidate, WONDER_DISTRICT_ID)) continue; + bool adjacency_bonus_active = + (is->natural_wonder_configs[ni].adjacent_to != (enum SquareTypes)SQ_INVALID) && + (is->natural_wonder_configs[ni].adjacency_dir == DIR_ZERO); + int adjacency_count = -1; + if (adjacency_bonus_active) + adjacency_count = count_adjacent_tiles_of_type (cand->x, cand->y, + is->natural_wonder_configs[ni].adjacent_to); - struct wonder_district_info * info = &inst->wonder_info; - if (info->state == WDS_COMPLETED) continue; - if (info->state == WDS_UNDER_CONSTRUCTION) { - if (info->city_id == city->Body.ID) { - info->city = city; - info->city_id = city->Body.ID; - return true; - } - continue; - } - if (city_has_assigned_wonder_district (city, candidate)) - return true; + int same_type_count = count_adjacent_tiles_of_type (cand->x, cand->y, + is->natural_wonder_configs[ni].terrain_type); - // Reserve this Wonder district for this city - info->state = WDS_UNDER_CONSTRUCTION; - info->city = city; - info->city_id = city->Body.ID; - info->wonder_index = -1; - return true; - } + bool better = false; + if (continent_priority < best_continent_priority) + better = true; + else if (continent_priority > best_continent_priority) + continue; - return false; -} + if (! better && adjacency_bonus_active) { + if (adjacency_count > best_adjacent_count) + better = true; + else if (adjacency_count < best_adjacent_count) + continue; + } -void -release_wonder_district_reservation (City * city) -{ - if (! is->current_config.enable_wonder_districts || (city == NULL)) - return; + if (! better) { + if (same_type_count > best_same_type_count) + better = true; + else if (same_type_count < best_same_type_count) + continue; + } - // Find and remove any reservations for this city - int civ_id = city->Body.CivID; - int city_x = city->Body.X; - int city_y = city->Body.Y; - for (int n = 0; n < is->workable_tile_count; n++) { - int dx, dy; - patch_ni_to_diff_for_work_area (n, &dx, &dy); - int x = city_x + dx, y = city_y + dy; - wrap_tile_coords (&p_bic_data->Map, &x, &y); - Tile * candidate = tile_at (x, y); - if (candidate == p_null_tile) - continue; - if (candidate->vtable->m38_Get_Territory_OwnerID (candidate) != civ_id) - continue; + if (! better) { + if ((min_dist_sq > best_dist) || + ((min_dist_sq == best_dist) && (dx_adjusted < best_target_diff)) || + ((min_dist_sq == best_dist) && (dx_adjusted == best_target_diff) && (rand_val < best_rand))) + better = true; + else + continue; + } - struct wonder_district_info * info = get_wonder_district_info (candidate); - if ((info != NULL) && - (info->state == WDS_UNDER_CONSTRUCTION) && - (info->city_id == city->Body.ID)) { - info->city = city; - info->state = WDS_UNUSED; - info->city = NULL; - info->city_id = -1; - info->wonder_index = -1; + best_dist = min_dist_sq; + best_target_diff = dx_adjusted; + best_rand = rand_val; + best_index = ci; + best_continent_priority = continent_priority; + if (adjacency_bonus_active) + best_adjacent_count = adjacency_count; + best_same_type_count = same_type_count; } - } -} -void -handle_district_destroyed_by_attack (Tile * tile, int tile_x, int tile_y, bool leave_ruins) -{ - if (! is->current_config.enable_districts || tile == NULL || tile == p_null_tile) - return; + if (best_index < 0) { + char msg[256]; + snprintf (msg, sizeof msg, "[C3X] Could not find a suitable tile for natural wonder \"%s\" after filtering.\n", + (is->natural_wonder_configs[ni].name != NULL) ? is->natural_wonder_configs[ni].name : "Natural Wonder"); + (*p_OutputDebugStringA) (msg); + continue; + } - struct district_instance * inst = get_district_instance (tile); - if (inst != NULL) { - int district_id = inst->district_type; + struct natural_wonder_candidate * chosen = &list->entries[best_index]; + assign_natural_wonder_to_tile (chosen->tile, chosen->x, chosen->y, ni); - // If this is a Wonder District with a completed wonder and wonders can't be destroyed, restore overlay and keep district - if (is->current_config.enable_wonder_districts) { - struct wonder_district_info * info = get_wonder_district_info (tile); - if ((district_id == WONDER_DISTRICT_ID) && - (info != NULL && info->state == WDS_COMPLETED) && - (! is->current_config.completed_wonder_districts_can_be_destroyed)) { - unsigned int overlays = tile->vtable->m42_Get_Overlays (tile, __, 0); - if ((overlays & TILE_FLAG_MINE) == 0) - tile->vtable->m56_Set_Tile_Flags (tile, __, 0, TILE_FLAG_MINE, tile_x, tile_y); - return; + if (placement_count >= placement_capacity) { + int new_capacity = (placement_capacity > 0) ? placement_capacity * 2 : 8; + struct wonder_location * grown = + (struct wonder_location *)realloc (placements, new_capacity * sizeof *grown); + if (grown != NULL) { + placements = grown; + placement_capacity = new_capacity; } } - handle_district_removed (tile, district_id, tile_x, tile_y, leave_ruins); + + if ((placements != NULL) && (placement_count < placement_capacity)) { + placements[placement_count++] = (struct wonder_location){ + .x = chosen->x, + .y = chosen->y + }; + } + + newly_placed += 1; + + char msg[256]; + snprintf (msg, sizeof msg, "[C3X] Placed natural wonder \"%s\" at (%d,%d).\n", + (is->natural_wonder_configs[ni].name != NULL) ? is->natural_wonder_configs[ni].name : "Natural Wonder", + chosen->x, chosen->y); + (*p_OutputDebugStringA) (msg); } -} -bool -has_active_building (City * city, int improv_id) -{ - Leader * owner = &leaders[city->Body.CivID]; - Improvement * improv = &p_bic_data->Improvements[improv_id]; - return patch_City_has_improvement (city, __, improv_id, 1) && // building is physically present in city AND - ((improv->ObsoleteID < 0) || (! Leader_has_tech (owner, __, improv->ObsoleteID))) && // building is not obsolete AND - ((improv->GovernmentID < 0) || (improv->GovernmentID == owner->GovernmentType)); // building is not restricted to a different govt -} + char summary[256]; + snprintf (summary, sizeof summary, "[C3X] Natural wonder placement complete. Newly placed: %d, already present: %d.\n", + newly_placed, existing_count); + (*p_OutputDebugStringA) (summary); -bool -can_generate_resource (int for_civ_id, struct mill * mill) -{ - int req_tech_id = (mill->flags & MF_NO_TECH_REQ) ? -1 : p_bic_data->ResourceTypes[mill->resource_id].RequireID; - return (req_tech_id < 0) || Leader_has_tech (&leaders[for_civ_id], __, req_tech_id); + for (int ni = 0; ni < wonder_count; ni++) + free (candidate_lists[ni].entries); + free (wonder_order); + free (candidate_lists); + free (already_placed); + free (placements); } void -init_unit_type_count (Leader * leader) +init_scenario_district_entry (struct scenario_district_entry * entry) { - int id = leader->ID; - struct table * counts = &is->unit_type_counts[id]; - - if (counts->len > 0) - table_deinit (counts); - - if (p_units->Units != NULL) - for (int n = 0; n <= p_units->LastIndex; n++) { - Unit_Body * body = p_units->Units[n].Unit; - if ((body != NULL) && ((int)body != offsetof (Unit, Body)) && (body->CivID == id)) { - int prev_count = itable_look_up_or (counts, body->UnitTypeID, 0); - itable_insert (counts, body->UnitTypeID, prev_count + 1); - } - } + if (entry == NULL) + return; - is->unit_type_count_init_bits |= 1 << id; + memset (entry, 0, sizeof *entry); } -int -get_unit_type_count (Leader * leader, int unit_type_id) +void +init_scenario_named_tile_entry (struct scenario_named_tile_entry * entry) { - int id = leader->ID; - struct table * counts = &is->unit_type_counts[id]; - - if ((is->unit_type_count_init_bits & 1<ID; - struct table * counts = &is->unit_type_counts[id]; - if ((is->unit_type_count_init_bits & (1 << id)) == 0) - init_unit_type_count (leader); + if (entry == NULL) + return; - int prev_amount = itable_look_up_or (counts, unit_type_id, 0); - itable_insert (counts, unit_type_id, prev_amount + amount); + if (entry->district_name != NULL) { + free (entry->district_name); + entry->district_name = NULL; + } + if (entry->wonder_city_name != NULL) { + free (entry->wonder_city_name); + entry->wonder_city_name = NULL; + } + if (entry->wonder_name != NULL) { + free (entry->wonder_name); + entry->wonder_name = NULL; + } + entry->has_coordinates = 0; + entry->has_district_name = 0; + entry->has_wonder_city = 0; + entry->has_wonder_name = 0; } -// If this unit type is limited, returns true and writes how many units of the type the given player is allowed to "out_limit". If the type is not -// limited, returns false. -bool -get_unit_limit (Leader * leader, int unit_type_id, int * out_limit) +void +free_scenario_named_tile_entry (struct scenario_named_tile_entry * entry) { - UnitType * type = &p_bic_data->UnitTypes[unit_type_id]; - struct unit_type_limit * lim; - if ((unit_type_id >= 0) && (unit_type_id < p_bic_data->UnitTypeCount) && - stable_look_up (&is->current_config.unit_limits, type->Name, (int *)&lim)) { - int city_count = leader->Cities_Count; - int tr = lim->per_civ + lim->per_city * city_count; - if (lim->cities_per != 0) - tr += city_count / lim->cities_per; - *out_limit = tr; - return true; - } else - return false; + if (entry == NULL) + return; + + if (entry->name != NULL) { + free (entry->name); + entry->name = NULL; + } + entry->has_coordinates = 0; + entry->has_name = 0; } -// This this unit type is limited, returns true and writes to "out_available" how many units the given player can add before reaching the limit. If -// the type is not limited, returns false. -bool -get_available_unit_count (Leader * leader, int unit_type_id, int * out_available) +void +add_scenario_district_error (struct error_line ** parse_errors, + int line_number, + char const * message) { - int limit; - if (get_unit_limit (leader, unit_type_id, &limit)) { - int count = get_unit_type_count (leader, unit_type_id); - int dups[30]; - int dups_count = list_unit_type_duplicates (unit_type_id, dups, ARRAY_LEN (dups)); - for (int n = 0; n < dups_count; n++) - count += get_unit_type_count (leader, dups[n]); + if (message == NULL) + return; - *out_available = limit - count; - return true; - } else - return false; + struct error_line * err = add_error_line (parse_errors); + snprintf (err->text, sizeof err->text, "^ Line %d: %s", line_number, message); + err->text[(sizeof err->text) - 1] = '\0'; } int -add_i31b_to_int (int base, i31b addition) +parse_scenario_district_coordinates (struct string_slice const * value, int * out_x, int * out_y) { - int amount; - bool percent; - i31b_unpack (addition, &amount, &percent); - if (! percent) - return base + amount; - else { - int fraction = (base * int_abs (amount) + 50) / 100; - return (amount >= 0) ? base + fraction : base - fraction; - } -} - -int -apply_perfume (enum perfume_kind kind, char const * name, int base_amount) -{ - i31b perfume_value; - if (stable_look_up (&is->current_config.perfume_specs[kind], name, &perfume_value)) - return add_i31b_to_int (base_amount, perfume_value); - else - return base_amount; + if ((value == NULL) || (out_x == NULL) || (out_y == NULL)) + return 0; + + char * text = trim_and_extract_slice (value, 0); + if (text == NULL) + return 0; + + char * cursor = text; + int success = 0; + int x, y; + if (parse_int (&cursor, &x) && + skip_punctuation (&cursor, ',') && + parse_int (&cursor, &y)) { + skip_horiz_space (&cursor); + success = (*cursor == '\0'); + if (success) { + *out_x = x; + *out_y = y; + } + } + + free (text); + return success; } -int __stdcall -intercept_consideration (int valuation) +int +finalize_scenario_district_entry (struct scenario_district_entry * entry, + int section_start_line, + struct error_line ** parse_errors) { - City * city = is->ai_considering_production_for_city; - City_Order * order = &is->ai_considering_order; + int success = 1; + if ((entry == NULL) || (parse_errors == NULL)) + return 0; - // Apply perfume - char * order_name; { - if (order->OrderType == COT_Improvement) - order_name = p_bic_data->Improvements[order->OrderID].Name.S; - else - order_name = p_bic_data->UnitTypes[order->OrderID].Name; + if (! is->current_config.enable_districts && ! is->current_config.enable_natural_wonders) { + free_scenario_district_entry (entry); + init_scenario_district_entry (entry); + return 1; } - valuation = apply_perfume (PK_PRODUCTION, order_name, valuation); - - // Apply temp AI settler perfume - if ((order->OrderType == COT_Unit) && - (p_bic_data->UnitTypes[order->OrderID].AI_Strategy & UTAI_Settle) && - (is->current_config.ai_settler_perfume_on_founding != 0) && - (is->current_config.ai_settler_perfume_on_founding_duration != 0) && - (is->turn_no_of_last_founding_for_settler_perfume[city->Body.CivID] >= 0)) { - int turns_since_founding = *p_current_turn_no - is->turn_no_of_last_founding_for_settler_perfume[city->Body.CivID]; - int duration = is->current_config.ai_settler_perfume_on_founding_duration; - if (turns_since_founding < duration) { - i31b perfume = is->current_config.ai_settler_perfume_on_founding; - // Scale amount by turns remaining - { - int amount; - bool percent; - i31b_unpack (perfume, &amount, &percent); + if (! entry->has_coordinates) + add_scenario_district_error (parse_errors, section_start_line, "coordinates (value is required)"); + if ((! entry->has_district_name) || (entry->district_name == NULL) || (entry->district_name[0] == '\0')) + add_scenario_district_error (parse_errors, section_start_line, "district (value is required)"); - int percent_remaining = (100 * (duration - turns_since_founding)) / duration; - amount = (amount * percent_remaining + 50) / 100; + if ((! entry->has_coordinates) || (! entry->has_district_name) || + (entry->district_name == NULL) || (entry->district_name[0] == '\0')) + success = 0; - perfume = i31b_pack (amount, percent); - } + int map_x = entry->tile_x; + int map_y = entry->tile_y; + if (success) { + wrap_tile_coords (&p_bic_data->Map, &map_x, &map_y); + Tile * tile = tile_at (map_x, map_y); + if ((tile == NULL) || (tile == p_null_tile)) { + add_scenario_district_error (parse_errors, section_start_line, "Invalid coordinates (tile not found)"); + success = 0; + } else { + int district_id = find_district_index_by_name (entry->district_name); + if ((district_id < 0) || (district_id >= is->district_count)) { + char msg[200]; + snprintf (msg, sizeof msg, "district (unrecognized name: \"%s\")", entry->district_name); + add_scenario_district_error (parse_errors, section_start_line, msg); + success = 0; + } else { + if ((district_id == NATURAL_WONDER_DISTRICT_ID) && ! is->current_config.enable_natural_wonders) { + free_scenario_district_entry (entry); + init_scenario_district_entry (entry); + return 1; + } + if ((district_id != NATURAL_WONDER_DISTRICT_ID) && ! is->current_config.enable_districts) { + free_scenario_district_entry (entry); + init_scenario_district_entry (entry); + return 1; + } + struct district_instance * inst = ensure_district_instance (tile, district_id, map_x, map_y); + if (inst == NULL) { + add_scenario_district_error (parse_errors, section_start_line, "Failed to create district instance"); + success = 0; + } else { + inst->district_id = district_id; + district_instance_set_coords (inst, map_x, map_y); + inst->state = DS_COMPLETED; + inst->wonder_info.state = WDS_UNUSED; + inst->wonder_info.city = NULL; + inst->wonder_info.city_id = -1; + inst->wonder_info.wonder_index = -1; + inst->natural_wonder_info.natural_wonder_id = -1; - valuation = add_i31b_to_int (valuation, perfume); - } - } + if (district_id == WONDER_DISTRICT_ID) { + int has_city = entry->has_wonder_city && + (entry->wonder_city_name != NULL) && (entry->wonder_city_name[0] != '\0'); + int has_wonder = entry->has_wonder_name && + (entry->wonder_name != NULL) && (entry->wonder_name[0] != '\0'); + if (! has_city || ! has_wonder) { + add_scenario_district_error (parse_errors, section_start_line, "Wonder district requires both wonder_city and wonder_name"); + success = 0; + } else { + int wonder_index = find_wonder_district_index_by_name (entry->wonder_name); + if (wonder_index < 0) { + char msg[200]; + snprintf (msg, sizeof msg, "wonder_name (unrecognized wonder: \"%s\")", entry->wonder_name); + add_scenario_district_error (parse_errors, section_start_line, msg); + success = 0; + } else { + inst->wonder_info.city = NULL; + inst->wonder_info.city_id = -1; + inst->wonder_info.state = WDS_COMPLETED; + inst->wonder_info.wonder_index = wonder_index; + } + } + } else if (district_id == NATURAL_WONDER_DISTRICT_ID) { + int has_name = entry->has_wonder_name && + (entry->wonder_name != NULL) && (entry->wonder_name[0] != '\0'); + if (! has_name) { + add_scenario_district_error (parse_errors, section_start_line, "Natural Wonder district requires wonder_name"); + success = 0; + } else { + int natural_index = find_natural_wonder_index_by_name (entry->wonder_name); + if (natural_index < 0) { + char msg[200]; + snprintf (msg, sizeof msg, "wonder_name (unrecognized natural wonder: \"%s\")", entry->wonder_name); + add_scenario_district_error (parse_errors, section_start_line, msg); + success = 0; + } else + inst->natural_wonder_info.natural_wonder_id = natural_index; + } + if (entry->has_wonder_city) + add_scenario_district_error (parse_errors, section_start_line, "wonder_city ignored for Natural Wonder district entries"); + } else if (entry->has_wonder_city || entry->has_wonder_name) { + add_scenario_district_error (parse_errors, section_start_line, "wonder_* fields only valid for Wonder or Natural Wonder district entries"); + } - if (is->current_config.enable_districts && - (city != NULL) && - ((*p_human_player_bits & (1 << city->Body.CivID)) == 0)) { - bool feasible = false; - switch (order->OrderType) { - case COT_Improvement: - if ((order->OrderID >= 0) && (order->OrderID < p_bic_data->ImprovementsCount) && - (! city_requires_district_for_improvement (city, order->OrderID, NULL))) - feasible = true; - break; - case COT_Unit: - if ((order->OrderID >= 0) && (order->OrderID < p_bic_data->UnitTypeCount)) - feasible = true; - break; - default: - break; + if (success) { + if (district_id != NATURAL_WONDER_DISTRICT_ID && !tile->vtable->m18_Check_Mines (tile, __, 0)) + tile->vtable->m56_Set_Tile_Flags (tile, __, 0, TILE_FLAG_MINE, map_x, map_y); + set_tile_unworkable_for_all_cities (tile, map_x, map_y); + } + } + } } - if (feasible) - record_best_feasible_order (city, order, valuation); } - // Expand the list of valuations if necessary - reserve (sizeof is->ai_prod_valuations[0], (void **)&is->ai_prod_valuations, &is->ai_prod_valuations_capacity, is->count_ai_prod_valuations); - - // Record this valuation - int n = is->count_ai_prod_valuations++; - is->ai_prod_valuations[n] = (struct ai_prod_valuation) { - .order_type = order->OrderType, - .order_id = order->OrderID, - .point_value = valuation - }; - - return valuation; + free_scenario_district_entry (entry); + init_scenario_district_entry (entry); + return success; } -// Returns a pointer to a bitfield that can be used to record resource access for resource IDs >= 32. This procedure can work with any city ID since -// it allocates and zero-initializes these bit fields as necessary. The given resource ID must be at least 32. The index of the bit for that resource -// within the field is resource_id%32. -unsigned * -get_extra_resource_bits (int city_id, int resource_id) +bool +tile_can_be_named (Tile * tile, int tile_x, int tile_y) { - int extra_resource_count = not_below (0, p_bic_data->ResourceTypeCount - 32); - int ints_per_city = 1 + extra_resource_count/32; - if (city_id >= is->extra_available_resources_capacity) { - int new_capacity = city_id + 100; - unsigned * new_array = calloc (new_capacity * ints_per_city, sizeof new_array[0]); - if (is->extra_available_resources != NULL) { - memcpy (new_array, is->extra_available_resources, is->extra_available_resources_capacity * ints_per_city * sizeof (unsigned)); - free (is->extra_available_resources); - } - is->extra_available_resources = new_array; - is->extra_available_resources_capacity = new_capacity; - } - return &is->extra_available_resources[city_id * ints_per_city + (resource_id-32)/32]; + if ((tile == NULL) || (tile == p_null_tile)) + return false; + struct district_instance * inst = get_district_instance (tile); + if ((inst != NULL) && (inst->district_id == NATURAL_WONDER_DISTRICT_ID)) + return false; + return true; } -void __stdcall -intercept_set_resource_bit (City * city, int resource_id) +void +remove_named_tile_entry (Tile * tile) { - if (resource_id < 32) - city->Body.Available_Resources |= 1 << resource_id; - else - *get_extra_resource_bits (city->Body.ID, resource_id) |= 1 << (resource_id&31); + struct named_tile_entry * entry = get_named_tile_entry (tile); + if (entry == NULL) + return; + itable_remove (&is->named_tile_map, (int)tile); + free (entry); } -// Must forward declare this function since there's a circular dependency between it and patch_City_has_resource -bool has_resources_required_by_building_r (City * city, int improv_id, int max_req_resource_id); +void +set_named_tile_entry (Tile * tile, int tile_x, int tile_y, char const * name) +{ + if ((tile == NULL) || (tile == p_null_tile)) + return; + if ((name == NULL) || (name[0] == '\0')) { + remove_named_tile_entry (tile); + return; + } -bool __fastcall -patch_City_has_resource (City * this, int edx, int resource_id) + struct named_tile_entry * entry = get_named_tile_entry (tile); + if (entry == NULL) { + entry = calloc (1, sizeof *entry); + if (entry == NULL) + return; + itable_insert (&is->named_tile_map, (int)tile, (int)entry); + } + entry->tile_x = tile_x; + entry->tile_y = tile_y; + strncpy (entry->name, name, sizeof entry->name); + entry->name[(sizeof entry->name) - 1] = '\0'; +} + +int +finalize_scenario_named_tile_entry (struct scenario_named_tile_entry * entry, + int section_start_line, + struct error_line ** parse_errors) { - bool tr; - if (is->current_config.patch_phantom_resource_bug && - (resource_id >= 32) && (resource_id < p_bic_data->ResourceTypeCount) && - (! City_has_trade_connection_to_capital (this))) { - unsigned bits = (this->Body.ID < is->extra_available_resources_capacity) ? *get_extra_resource_bits (this->Body.ID, resource_id) : 0; - tr = (bits >> (resource_id&31)) & 1; - } else - tr = City_has_resource (this, __, resource_id); + int success = 1; + if ((entry == NULL) || (parse_errors == NULL)) + return 0; - // Check if access to this resource is provided by a building in the city - if (! tr) - for (int n = 0; n < is->current_config.count_mills; n++) { - struct mill * mill = &is->current_config.mills[n]; - if ((mill->resource_id == resource_id) && - (mill->flags & MF_LOCAL) && - can_generate_resource (this->Body.CivID, mill) && - has_active_building (this, mill->improv_id) && - has_resources_required_by_building_r (this, mill->improv_id, mill->resource_id - 1)) { - tr = true; - break; - } + if (! is->current_config.enable_named_tiles) { + free_scenario_named_tile_entry (entry); + init_scenario_named_tile_entry (entry); + return 1; + } + + if (! entry->has_coordinates) + add_scenario_district_error (parse_errors, section_start_line, "coordinates (value is required)"); + if ((! entry->has_name) || (entry->name == NULL) || (entry->name[0] == '\0')) + add_scenario_district_error (parse_errors, section_start_line, "name (value is required)"); + + if ((! entry->has_coordinates) || (! entry->has_name) || + (entry->name == NULL) || (entry->name[0] == '\0')) + success = 0; + + int map_x = entry->tile_x; + int map_y = entry->tile_y; + if (success) { + wrap_tile_coords (&p_bic_data->Map, &map_x, &map_y); + Tile * tile = tile_at (map_x, map_y); + if ((tile == NULL) || (tile == p_null_tile)) { + add_scenario_district_error (parse_errors, section_start_line, "Invalid coordinates (tile not found)"); + success = 0; + } else if (! tile_can_be_named (tile, map_x, map_y)) { + add_scenario_district_error (parse_errors, section_start_line, "Invalid coordinates (tile cannot be named)"); + success = 0; + } else { + set_named_tile_entry (tile, map_x, map_y, entry->name); } + } - return tr; + free_scenario_named_tile_entry (entry); + init_scenario_named_tile_entry (entry); + return success; } -// Checks if the resource requirements for an improvement are satisfied in a given city. The "max_req_resource_id" parameter is to guard against -// infinite loops in case of circular resource dependencies due to mills. The way it works is that resource requirements can only be satisfied if -// their ID is not greater than that limit. This function is called recursively by patch_City_has_resource and in the recursive calls, the limit is -// set to be below the resource ID provided by the mill being considered. So, when considering resource production chains, the limit approaches zero -// with each recursive call, hence infinite loops are not possible. -bool -has_resources_required_by_building_r (City * city, int improv_id, int max_req_resource_id) +void +handle_scenario_district_key (struct scenario_district_entry * entry, + struct string_slice const * key, + struct string_slice const * value, + int line_number, + struct error_line ** parse_errors, + struct error_line ** unrecognized_keys) { - Improvement * improv = &p_bic_data->Improvements[improv_id]; - if (! (improv->ImprovementFlags & ITF_Required_Goods_Must_Be_In_City_Radius)) { - for (int n = 0; n < 2; n++) { - int res_id = (&improv->Resource1ID)[n]; - if ((res_id >= 0) && - ((res_id > max_req_resource_id) || (! patch_City_has_resource (city, __, res_id)))) - return false; + if ((entry == NULL) || (key == NULL) || (value == NULL)) + return; + + if (slice_matches_str (key, "coordinates")) { + int x, y; + if (parse_scenario_district_coordinates (value, &x, &y)) { + entry->tile_x = x; + entry->tile_y = y; + entry->has_coordinates = 1; + } else + add_scenario_district_error (parse_errors, line_number, "coordinates (expected format: x,y)"); + + } else if (slice_matches_str (key, "district")) { + if (entry->district_name != NULL) { + free (entry->district_name); + entry->district_name = NULL; } - return true; - } else { - int * targets = &improv->Resource1ID; - if ((targets[0] < 0) && (targets[1] < 0)) - return true; - int finds[2] = {0, 0}; + entry->district_name = copy_trimmed_string_or_null (value, 1); + entry->has_district_name = (entry->district_name != NULL); + if (! entry->has_district_name) + add_scenario_district_error (parse_errors, line_number, "district (value is required)"); - int civ_id = city->Body.CivID; - for (int n = 0; n < is->workable_tile_count; n++) { - int dx, dy; - patch_ni_to_diff_for_work_area (n, &dx, &dy); - int x = city->Body.X + dx, y = city->Body.Y + dy; - wrap_tile_coords (&p_bic_data->Map, &x, &y); - Tile * tile = tile_at (x, y); - if (tile->vtable->m38_Get_Territory_OwnerID (tile) == civ_id) { - int res_here = Tile_get_resource_visible_to (tile, __, civ_id); - if (res_here >= 0) { - finds[0] |= targets[0] == res_here; - finds[1] |= targets[1] == res_here; - } - } + } else if (slice_matches_str (key, "wonder_city")) { + if (entry->wonder_city_name != NULL) { + free (entry->wonder_city_name); + entry->wonder_city_name = NULL; } + entry->wonder_city_name = copy_trimmed_string_or_null (value, 1); + entry->has_wonder_city = (entry->wonder_city_name != NULL); - return ((targets[0] < 0) || finds[0]) && ((targets[1] < 0) || finds[1]); - } -} + } else if (slice_matches_str (key, "wonder_name")) { + if (entry->wonder_name != NULL) { + free (entry->wonder_name); + entry->wonder_name = NULL; + } + entry->wonder_name = copy_trimmed_string_or_null (value, 1); + entry->has_wonder_name = (entry->wonder_name != NULL); -bool -has_resources_required_by_building (City * city, int improv_id) -{ - return has_resources_required_by_building_r (city, improv_id, INT_MAX); + } else + add_unrecognized_key_error (unrecognized_keys, line_number, key); } -void __fastcall -patch_City_recompute_commerce (City * this) +void +handle_scenario_named_tile_key (struct scenario_named_tile_entry * entry, + struct string_slice const * key, + struct string_slice const * value, + int line_number, + struct error_line ** parse_errors, + struct error_line ** unrecognized_keys) { - City_recompute_commerce (this); - - if (! (is->current_config.enable_districts || is->current_config.enable_natural_wonders)) + if ((entry == NULL) || (key == NULL) || (value == NULL)) return; - int science_bonus = 0; - calculate_district_culture_science_bonuses (this, NULL, &science_bonus); - - if (science_bonus != 0) { - this->Body.Science += science_bonus; - if (this->Body.Science < 0) - this->Body.Science = 0; - } -} + if (slice_matches_str (key, "coordinates")) { + int x, y; + if (parse_scenario_district_coordinates (value, &x, &y)) { + entry->tile_x = x; + entry->tile_y = y; + entry->has_coordinates = 1; + } else + add_scenario_district_error (parse_errors, line_number, "coordinates (expected format: x,y)"); -void __fastcall -patch_City_recompute_yields_and_happiness (City * this) -{ - if (is->current_config.enable_districts && - is->current_config.enable_distribution_hub_districts && - ! is->distribution_hub_refresh_in_progress && - is->distribution_hub_totals_dirty) - recompute_distribution_hub_totals (); + } else if (slice_matches_str (key, "name")) { + if (entry->name != NULL) { + free (entry->name); + entry->name = NULL; + } + entry->name = copy_trimmed_string_or_null (value, 1); + entry->has_name = (entry->name != NULL); + if (! entry->has_name) + add_scenario_district_error (parse_errors, line_number, "name (value is required)"); - City_recompute_yields_and_happiness (this); + } else + add_unrecognized_key_error (unrecognized_keys, line_number, key); } -void __fastcall -patch_City_update_culture (City * this) +// Parses a .c3x.txt file corresponding to the given scenario file path, loading district instances as specified. +// Attempts the scenario_path first, then scenario_config_path; if neither yields a readable file, no action is taken. +// +// The expected file format itself is very simple. Example: +// +// ``` +// DISTRICTS +// +// #District +// coordinates = 12,28 +// district = Entertainment Complex +// +// #District +// coordinates = 9,23 +// district = Wonder District +// wonder_city = Rome +// wonder_name = The Pyramids +// +// #District +// coordinates = 10,30 +// district = Natural Wonder +// wonder_name = Mount Everest +// +// #NamedTile +// coordinates = 41,23 +// name = Tiber River +// ``` +// +// Details at https://github.com/instafluff0/Civ3_Editor_Fork_for_C3X_Districts +void +load_scenario_districts_from_file () { - City_update_culture (this); + char * scenario_filename = "scenario.districts.txt"; + char * scenario_districts_path = BIC_get_asset_path (p_bic_data, __, scenario_filename, false); - if ((this == NULL) || ! (is->current_config.enable_districts || is->current_config.enable_natural_wonders)) + // BIC_get_asset_path returns the file name when it can't find the file + if ((scenario_districts_path == NULL) || (0 == strcmp (scenario_filename, scenario_districts_path))) return; - int culture_bonus = 0; - calculate_district_culture_science_bonuses (this, &culture_bonus, NULL); - - if (culture_bonus == 0) + char * text = file_to_string (scenario_districts_path); + if (text == NULL) return; - int culture_income = this->Body.CultureIncome + culture_bonus; - if (culture_income < 0) - culture_income = 0; - this->Body.CultureIncome = culture_income; - - int civ_id = this->Body.CivID; - int total_culture = this->Body.Total_Cultures[civ_id] + culture_bonus; - if (total_culture < 0) - total_culture = 0; - this->Body.Total_Cultures[civ_id] = total_culture; + struct scenario_district_entry entry; + struct scenario_named_tile_entry named_entry; + init_scenario_district_entry (&entry); + init_scenario_named_tile_entry (&named_entry); + int in_section = 0; + int section_start_line = 0; + int section_type = 0; + int line_number = 0; + int header_seen = 0; + struct error_line * unrecognized_keys = NULL; + struct error_line * parse_errors = NULL; - City_recompute_cultural_level (this, __, '\0', '\0', '\0'); -} + char * cursor = text; + while (*cursor != '\0') { + line_number += 1; -void __fastcall -patch_City_recompute_culture_income (City * this) -{ - City_recompute_culture_income (this); + char * line_start = cursor; + char * line_end = cursor; + while ((*line_end != '\0') && (*line_end != '\n')) + line_end++; - if (! (is->current_config.enable_districts || is->current_config.enable_natural_wonders)) - return; + int line_len = line_end - line_start; + int has_newline = (*line_end == '\n'); + if (has_newline) + *line_end = '\0'; - int culture_bonus = 0; - calculate_district_culture_science_bonuses (this, &culture_bonus, NULL); + struct string_slice line_slice = { .str = line_start, .len = line_len }; + struct string_slice trimmed = trim_string_slice (&line_slice, 0); + if (line_is_empty_or_comment (&trimmed)) { + cursor = has_newline ? line_end + 1 : line_end; + continue; + } - if (culture_bonus != 0) { - this->Body.CultureIncome += culture_bonus; - if (this->Body.CultureIncome < 0) - this->Body.CultureIncome = 0; - } -} + // Keep support for legacy header, technically not needed + if (! header_seen) { + if (slice_matches_str (&trimmed, "DISTRICTS")) { + header_seen = 1; + cursor = has_newline ? line_end + 1 : line_end; + continue; + } + } -// Recomputes yields in cities with active mills that depend on input resources. Intended to be called when an input resource has been potentially -// gained or lost. Recomputes only for the cities of a given leader or, if NULL, for all cities on the map. -void -recompute_mill_yields_after_resource_change (Leader * leader_or_null) -{ - if (p_cities->Cities != NULL) - for (int city_index = 0; city_index <= p_cities->LastIndex; city_index++) { - City * city = get_city_ptr (city_index); - if ((city != NULL) && - ((leader_or_null == NULL) || (city->Body.CivID == leader_or_null->ID))) { - bool any_relevant_mills = false; - for (int n = 0; n < is->current_config.count_mills; n++) { - struct mill * mill = &is->current_config.mills[n]; - Improvement * mill_improv = &p_bic_data->Improvements[mill->improv_id]; - if ((mill->flags & MF_YIELDS) && - ((mill_improv->Resource1ID >= 0) || (mill_improv->Resource2ID >= 0)) && - has_active_building (city, mill->improv_id)) { - any_relevant_mills = true; - break; - } + if (trimmed.str[0] == '#') { + struct string_slice directive = trimmed; + directive.str += 1; + directive.len -= 1; + directive = trim_string_slice (&directive, 0); + if (slice_matches_str (&directive, "District")) { + if (in_section) { + if (section_type == 1) + finalize_scenario_district_entry (&entry, section_start_line, &parse_errors); + else if (section_type == 2) + finalize_scenario_named_tile_entry (&named_entry, section_start_line, &parse_errors); } - if (any_relevant_mills) - patch_City_recompute_yields_and_happiness (city); + in_section = 1; + section_type = 1; + section_start_line = line_number; + free_scenario_district_entry (&entry); + init_scenario_district_entry (&entry); + } else if (slice_matches_str (&directive, "NamedTile")) { + if (in_section) { + if (section_type == 1) + finalize_scenario_district_entry (&entry, section_start_line, &parse_errors); + else if (section_type == 2) + finalize_scenario_named_tile_entry (&named_entry, section_start_line, &parse_errors); + } + in_section = 1; + section_type = 2; + section_start_line = line_number; + free_scenario_named_tile_entry (&named_entry); + init_scenario_named_tile_entry (&named_entry); } + cursor = has_newline ? line_end + 1 : line_end; + continue; } -} - - -int -compare_mill_tiles (void const * vp_a, void const * vp_b) -{ - struct mill_tile const * a = vp_a, * b = vp_b; - return a->mill->resource_id - b->mill->resource_id; -} -void __fastcall -patch_Trade_Net_recompute_resources (Trade_Net * this, int edx, bool skip_popups) -{ - int extra_resource_count = not_below (0, p_bic_data->ResourceTypeCount - 32); - int ints_per_city = 1 + extra_resource_count/32; - memset (is->extra_available_resources, 0, is->extra_available_resources_capacity * ints_per_city * sizeof (unsigned)); + if (! in_section) { + cursor = has_newline ? line_end + 1 : line_end; + continue; + } - // Assemble list of mill tiles - is->count_mill_tiles = 0; - if (p_cities->Cities != NULL) - for (int city_index = 0; city_index <= p_cities->LastIndex; city_index++) { - City * city = get_city_ptr (city_index); - if (city != NULL) - for (int n = 0; n < is->current_config.count_mills; n++) { - struct mill * mill = &is->current_config.mills[n]; - if (((mill->flags & MF_LOCAL) == 0) && - has_active_building (city, mill->improv_id) && - can_generate_resource (city->Body.CivID, mill)) { - reserve (sizeof is->mill_tiles[0], - (void **)&is->mill_tiles, - &is->mill_tiles_capacity, - is->count_mill_tiles); - is->mill_tiles[is->count_mill_tiles++] = (struct mill_tile) { - .tile = tile_at (city->Body.X, city->Body.Y), - .city = city, - .mill = mill - }; - } - } + struct string_slice key_slice = {0}; + struct string_slice value_slice = {0}; + switch (parse_trimmed_key_value (&trimmed, &key_slice, &value_slice)) { + case KVP_NO_EQUALS: + add_scenario_district_error (&parse_errors, line_number, "(expected '=')"); + break; + case KVP_EMPTY_KEY: + add_scenario_district_error (&parse_errors, line_number, "(missing key)"); + break; + case KVP_SUCCESS: + if (section_type == 1) + handle_scenario_district_key (&entry, &key_slice, &value_slice, line_number, &parse_errors, &unrecognized_keys); + else if (section_type == 2) + handle_scenario_named_tile_key (&named_entry, &key_slice, &value_slice, line_number, &parse_errors, &unrecognized_keys); + break; } - qsort (is->mill_tiles, is->count_mill_tiles, sizeof is->mill_tiles[0], compare_mill_tiles); - is->got_mill_tile = NULL; - is->saved_tile_count = p_bic_data->Map.TileCount; - p_bic_data->Map.TileCount += is->count_mill_tiles; - Trade_Net_recompute_resources (this, __, skip_popups); + cursor = has_newline ? line_end + 1 : line_end; + } - // Restore the tile count if necessary. It may have already been restored by patch_Map_Renderer_m71_Draw_Tiles. This happens when the call to - // recompute_resources above opens a popup message about connecting a resource for the first time, which triggers redraw of the map. - if (is->saved_tile_count >= 0) { - p_bic_data->Map.TileCount = is->saved_tile_count; - is->saved_tile_count = -1; + if (in_section) { + if (section_type == 1) + finalize_scenario_district_entry (&entry, section_start_line, &parse_errors); + else if (section_type == 2) + finalize_scenario_named_tile_entry (&named_entry, section_start_line, &parse_errors); } - recompute_mill_yields_after_resource_change (NULL); + free_scenario_district_entry (&entry); + free_scenario_named_tile_entry (&named_entry); + free (text); - is->must_recompute_resources_for_mill_inputs = false; -} + // Append to loaded config names list + struct loaded_config_name * top_lcn = is->loaded_config_names; + while (top_lcn->next != NULL) + top_lcn = top_lcn->next; -Tile * -get_mill_tile (int index) -{ - struct mill_tile * mt = &is->mill_tiles[index]; - is->got_mill_tile = mt; - return mt->tile; -} + struct loaded_config_name * new_lcn = malloc (sizeof *new_lcn); + new_lcn->name = strdup (scenario_districts_path); + new_lcn->next = NULL; -Tile * __fastcall patch_Map_get_tile_when_recomputing_resources_1 (Map * map, int edx, int index) { return (index < is->saved_tile_count) ? Map_get_tile (map, __, index) : get_mill_tile (index - is->saved_tile_count); } -Tile * __fastcall patch_Map_get_tile_when_recomputing_resources_2 (Map * map, int edx, int index) { return (index < is->saved_tile_count) ? Map_get_tile (map, __, index) : get_mill_tile (index - is->saved_tile_count); } -Tile * __fastcall patch_Map_get_tile_when_recomputing_resources_3 (Map * map, int edx, int index) { return (index < is->saved_tile_count) ? Map_get_tile (map, __, index) : get_mill_tile (index - is->saved_tile_count); } -Tile * __fastcall patch_Map_get_tile_when_recomputing_resources_4 (Map * map, int edx, int index) { return (index < is->saved_tile_count) ? Map_get_tile (map, __, index) : get_mill_tile (index - is->saved_tile_count); } -Tile * __fastcall patch_Map_get_tile_when_recomputing_resources_5 (Map * map, int edx, int index) { return (index < is->saved_tile_count) ? Map_get_tile (map, __, index) : get_mill_tile (index - is->saved_tile_count); } + top_lcn->next = new_lcn; -int __fastcall -patch_Tile_get_visible_resource_when_recomputing (Tile * tile, int edx, int civ_id) -{ - if (is->got_mill_tile == NULL) - return Tile_get_resource_visible_to (tile, __, civ_id); - else { - struct mill_tile * mt = is->got_mill_tile; - is->got_mill_tile = NULL; - if (has_resources_required_by_building (mt->city, mt->mill->improv_id)) - return mt->mill->resource_id; - else - return -1; + if ((parse_errors != NULL) || (unrecognized_keys != NULL)) { + PopupForm * popup = get_popup_form (); + popup->vtable->set_text_key_and_flags (popup, __, is->mod_script_path, "C3X_WARNING", -1, 0, 0, 0); + char header[256]; + snprintf (header, sizeof header, "District scenario file issues in %s:", scenario_districts_path); + header[(sizeof header) - 1] = '\0'; + PopupForm_add_text (popup, __, header, 0); + if (parse_errors != NULL) + for (struct error_line * line = parse_errors; line != NULL; line = line->next) + PopupForm_add_text (popup, __, line->text, 0); + if (unrecognized_keys != NULL) { + PopupForm_add_text (popup, __, "", 0); + PopupForm_add_text (popup, __, "Unrecognized keys:", 0); + for (struct error_line * line = unrecognized_keys; line != NULL; line = line->next) + PopupForm_add_text (popup, __, line->text, 0); + } + patch_show_popup (popup, __, 0, 0); } + + free_error_lines (parse_errors); + free_error_lines (unrecognized_keys); } -int WINAPI -patch_MessageBoxA (HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType) +void +deinit_district_images (void) { - if (is->current_config.suppress_hypertext_links_exceeded_popup && - (strcmp (lpText, "Maximum hypertext links exceeded!") == 0)) - return IDOK; - else - return MessageBoxA (hWnd, lpText, lpCaption, uType); + if (is->dc_img_state == IS_OK) { + for (int dc = 0; dc < COUNT_DISTRICT_TYPES; dc++) { + for (int variant = 0; variant < ARRAY_LEN (is->district_img_sets[dc].imgs); variant++) + for (int era = 0; era < 4; era++) + for (int col = 0; col < ARRAY_LEN (is->district_img_sets[dc].imgs[variant][era]); col++) { + Sprite * sprite = &is->district_img_sets[dc].imgs[variant][era][col]; + if (sprite->vtable != NULL) + sprite->vtable->destruct (sprite, __, 0); + } + } + + for (int wi = 0; wi < MAX_WONDER_DISTRICT_TYPES; wi++) { + struct wonder_district_image_set * set = &is->wonder_district_img_sets[wi]; + if (set->img.vtable != NULL) + set->img.vtable->destruct (&set->img, __, 0); + if (set->construct_img.vtable != NULL) + set->construct_img.vtable->destruct (&set->construct_img, __, 0); + if (set->alt_dir_img.vtable != NULL) + set->alt_dir_img.vtable->destruct (&set->alt_dir_img, __, 0); + if (set->alt_dir_construct_img.vtable != NULL) + set->alt_dir_construct_img.vtable->destruct (&set->alt_dir_construct_img, __, 0); + } + + for (int ni = 0; ni < MAX_NATURAL_WONDER_DISTRICT_TYPES; ni++) { + Sprite * sprite = &is->natural_wonder_img_sets[ni].img; + if (sprite->vtable != NULL) + sprite->vtable->destruct (sprite, __, 0); + } + + if (is->abandoned_district_img.vtable != NULL) + is->abandoned_district_img.vtable->destruct (&is->abandoned_district_img, __, 0); + if (is->abandoned_maritime_district_img.vtable != NULL) + is->abandoned_maritime_district_img.vtable->destruct (&is->abandoned_maritime_district_img, __, 0); + } + + is->dc_img_state = IS_UNINITED; } -char * __fastcall -do_capture_modified_gold_trade (TradeOffer * trade_offer, int edx, int val, char * str, unsigned base) +void +clear_highlighted_worker_tiles_for_districts () { - is->modifying_gold_trade = trade_offer; - return print_int (val, str, base); + FOR_TABLE_ENTRIES (tei, &is->highlighted_city_radius_tile_pointers) { + struct highlighted_city_radius_tile_info * info = (struct highlighted_city_radius_tile_info *)tei.value; + if (info != NULL) + free (info); + } + table_deinit (&is->highlighted_city_radius_tile_pointers); } -// Here the order of the registers matches the order that they're pushed by the pusha instruction -struct register_set { - int edi, esi, ebp, esp, ebx, edx, ecx, eax; -}; -// Return 1 to allow the candidate unit to exert ZoC, 0 to exclude it. A pointer to the candidate is in esi. -int __stdcall -filter_zoc_candidate (struct register_set * reg) +void +reset_district_state (bool reset_tile_map) { - Unit * candidate = (Unit *)reg->esi, - * defender = is->zoc_defender; - - UnitType * candidate_type = &p_bic_data->UnitTypes[candidate->Body.UnitTypeID], - * defender_type = &p_bic_data->UnitTypes[defender ->Body.UnitTypeID]; - - enum UnitTypeClasses candidate_class = candidate_type->Unit_Class, - defender_class = defender_type ->Unit_Class; + clear_all_tracked_workers (); + deinit_district_images (); + clear_highlighted_worker_tiles_for_districts (); - bool lethal = ((is->current_config.special_zone_of_control_rules & SZOCR_LETHAL) != 0) && (! is->temporarily_disallow_lethal_zoc); - bool aerial = (is->current_config.special_zone_of_control_rules & SZOCR_AERIAL ) != 0, - amphibious = (is->current_config.special_zone_of_control_rules & SZOCR_AMPHIBIOUS) != 0; + table_deinit (&is->district_tech_prereqs); + FOR_TABLE_ENTRIES (tei, &is->district_building_prereqs) { + struct district_building_prereq_list * list = (struct district_building_prereq_list *)tei.value; + if (list != NULL) + free (list); + } + table_deinit (&is->district_building_prereqs); + table_deinit (&is->command_id_to_district_id); + stable_deinit (&is->building_name_to_id); + if (reset_tile_map) { + FOR_TABLE_ENTRIES (tei, &is->district_tile_map) { + struct district_instance * inst = (struct district_instance *)tei.value; + if (inst != NULL) + free (inst); + } + table_deinit (&is->district_tile_map); + FOR_TABLE_ENTRIES (tei, &is->named_tile_map) { + struct named_tile_entry * entry = (struct named_tile_entry *)tei.value; + if (entry != NULL) + free (entry); + } + table_deinit (&is->named_tile_map); + } - // Exclude air units if aerial ZoC is not enabled and exclude land-to-sea & sea-to-land ZoC if amphibious is not enabled - if ((! aerial) && (candidate_class == UTC_Air)) - return 0; - if ((! amphibious) && - (((candidate_class == UTC_Land) && (defender_class == UTC_Sea )) || - ((candidate_class == UTC_Sea ) && (defender_class == UTC_Land)))) - return 0; + clear_distribution_hub_tables (); - // In case of cross-domain ZoC, filter out units with zero bombard strength or range. They can't use their attack strength in this case, so - // without bombard they can be ruled out. Don't forget units may have non-zero bombard strength and zero range for defensive bombard. - int range = (candidate_class != UTC_Air) ? candidate_type->Bombard_Range : candidate_type->OperationalRange; - if ((candidate_class != defender_class) && ((candidate_type->Bombard_Strength <= 0) || (range <= 0))) - return 0; + is->distribution_hub_totals_dirty = true; - // Require lethal config option & lethal bombard against one HP defender - if ((Unit_get_max_hp (defender) - defender->Body.Damage <= 1) && - ((! lethal) || - ((defender_class == UTC_Sea) && ! UnitType_has_ability (candidate_type, __, UTA_Lethal_Sea_Bombardment)) || - ((defender_class != UTC_Sea) && ! UnitType_has_ability (candidate_type, __, UTA_Lethal_Land_Bombardment)))) - return 0; + clear_dynamic_district_definitions (); + is->district_count = is->special_district_count; - // Air units require the bombing action to perform ZoC - if ((candidate_class == UTC_Air) && ! (candidate_type->Air_Missions & UCV_Bombing)) - return 0; + for (int i = 0; i < COUNT_DISTRICT_TYPES; i++) { + is->district_infos[i].advance_prereq_count = 0; + for (int j = 0; j < ARRAY_LEN (is->district_infos[i].advance_prereq_ids); j++) + is->district_infos[i].advance_prereq_ids[j] = -1; + is->district_infos[i].obsoleted_by_id = -1; + is->district_infos[i].resource_prereq_count = 0; + for (int j = 0; j < ARRAY_LEN (is->district_infos[i].resource_prereq_ids); j++) + is->district_infos[i].resource_prereq_ids[j] = -1; + is->district_infos[i].resource_prereq_on_tile_id = -1; + is->district_infos[i].wonder_prereq_count = 0; + for (int j = 0; j < ARRAY_LEN (is->district_infos[i].wonder_prereq_ids); j++) + is->district_infos[i].wonder_prereq_ids[j] = -1; + is->district_infos[i].natural_wonder_prereq_count = 0; + for (int j = 0; j < ARRAY_LEN (is->district_infos[i].natural_wonder_prereq_ids); j++) + is->district_infos[i].natural_wonder_prereq_ids[j] = -1; + is->district_infos[i].dependent_building_count = 0; + for (int j = 0; j < ARRAY_LEN (is->district_infos[i].dependent_building_ids); j++) + is->district_infos[i].dependent_building_ids[j] = -1; + } - // Exclude land units in transports if configured - if ((is->current_config.special_zone_of_control_rules & SZOCR_NOT_FROM_INSIDE) && candidate_class == UTC_Land) { - Unit * container = get_unit_ptr (candidate->Body.Container_Unit); - if ((container != NULL) && ! UnitType_has_ability (&p_bic_data->UnitTypes[container->Body.UnitTypeID], __, UTA_Army)) - return 0; + for (int civ_id = 0; civ_id < 32; civ_id++) { + FOR_TABLE_ENTRIES (tei, &is->city_pending_district_requests[civ_id]) { + struct pending_district_request * req = (struct pending_district_request *)tei.value; + if (req != NULL) + free (req); + } + table_deinit (&is->city_pending_district_requests[civ_id]); } + table_deinit (&is->city_pending_building_orders); - return 1; + is->great_wall_auto_build = GWABS_NOT_STARTED; } -#define TRADE_NET_REF_COUNT 315 -#define TRADE_NET_INSTR_COUNT_GOG 22 -#define TRADE_NET_INSTR_COUNT_STEAM 23 -#define TRADE_NET_INSTR_COUNT_PCG 22 -#define TRADE_NET_ADDR_TOTAL_COUNT ((TRADE_NET_REF_COUNT * 3) + TRADE_NET_INSTR_COUNT_GOG + TRADE_NET_INSTR_COUNT_STEAM + TRADE_NET_INSTR_COUNT_PCG) - -int * -load_trade_net_addrs () +void +clear_city_district_request (City * city, int district_id) { - if (is->trade_net_addrs_load_state == IS_OK) - return is->trade_net_addrs; - else if (is->trade_net_addrs_load_state == IS_INIT_FAILED) - return NULL; + if (! is->current_config.enable_districts || + (city == NULL) || + (district_id < 0) || (district_id >= is->district_count)) + return; - bool success = false; - char err_msg[300] = {0}; + struct pending_district_request * req = find_pending_district_request (city, district_id); + if (req == NULL) + return; - is->trade_net_addrs = calloc (3 * TRADE_NET_ADDR_TOTAL_COUNT, sizeof is->trade_net_addrs[0]); - if (! is->trade_net_addrs) { - snprintf (err_msg, (sizeof err_msg) - 1, "Bad alloc"); - goto done; + remove_pending_district_request (req); + + int pending_improv_id; + if (lookup_pending_building_order (city, &pending_improv_id)) { + int required_district_id; + if (city_requires_district_for_improvement (city, pending_improv_id, &required_district_id)) { + if (required_district_id == district_id) + forget_pending_building_order (city); + } } +} - char file_path[MAX_PATH] = {0}; - snprintf (file_path, (sizeof file_path) - 1, "%s\\trade_net_addresses.txt", is->mod_rel_dir); - char * refs_file = file_to_string (file_path); - if (! refs_file) { - snprintf (err_msg, (sizeof err_msg) - 1, "Couldn't load %s", file_path); - goto done; +bool +district_resource_prereqs_met (Tile * tile, int tile_x, int tile_y, int district_id, City * city) +{ + if ((tile == NULL) || (tile == p_null_tile) || + (district_id < 0) || (district_id >= is->district_count)) + return false; + + struct district_infos * info = &is->district_infos[district_id]; + int on_tile_req = info->resource_prereq_on_tile_id; + if (on_tile_req >= 0) { + int res_here = tile->vtable->m39_Get_Resource_Type (tile); + if (res_here != on_tile_req) + return false; } - char * cursor = refs_file; - int loaded_count = 0; - while (true) { - if (*cursor == '#') { // comment line - skip_line (&cursor); - continue; - } + // If no resource prereqs, then the check passes + if (info->resource_prereq_count <= 0) + return true; - skip_horiz_space (&cursor); - if (*cursor == '\n') { // empty line - cursor++; + int owner = tile->vtable->m38_Get_Territory_OwnerID (tile); + if (owner < 0) + return false; + + // Check all resource prereqs - ALL must be present + for (int i = 0; i < info->resource_prereq_count; i++) { + int resource_req = info->resource_prereq_ids[i]; + if (resource_req < 0) continue; - } else if (*cursor == '\0') // end of file - break; - // otherwise we must be on a line with some addresses - int ref; - bool got_any_addresses = false; - while (parse_int (&cursor, &ref)) { - if (loaded_count >= TRADE_NET_ADDR_TOTAL_COUNT) { - snprintf (err_msg, (sizeof err_msg) - 1, "Too many values in file (expected %d exactly)", TRADE_NET_ADDR_TOTAL_COUNT); - goto done; - } - is->trade_net_addrs[loaded_count] = ref; - loaded_count++; - got_any_addresses = true; + bool has_resource = false; + + // Check if the specified city has this resource + if ((city != NULL) && + (city->Body.CivID == owner) && + city_radius_contains_tile (city, tile_x, tile_y) && + patch_City_has_resource (city, __, resource_req)) { + has_resource = true; } - if (! got_any_addresses) { - snprintf (err_msg, (sizeof err_msg) - 1, "Parse error"); - goto done; + // Check cities around this tile for the resource + if (!has_resource) { + FOR_CITIES_AROUND (wai, tile_x, tile_y) { + if (patch_City_has_resource (wai.city, __, resource_req)) { + has_resource = true; + break; + } + } } - } - if (loaded_count < TRADE_NET_ADDR_TOTAL_COUNT) { - snprintf (err_msg, (sizeof err_msg) - 1, "Too few values in file (expected %d exactly)", TRADE_NET_ADDR_TOTAL_COUNT); - goto done; + // If this required resource is not available, the check fails + if (!has_resource) + return false; } - success = true; - -done: - free (refs_file); - if (! success) { - char full_err_msg[300] = {0}; - snprintf (full_err_msg, (sizeof full_err_msg) - 1, "Failed to load trade net refs: %s", err_msg); - MessageBox (NULL, full_err_msg, NULL, MB_ICONERROR); - is->trade_net_addrs_load_state = IS_INIT_FAILED; - return NULL; - } else { - is->trade_net_addrs_load_state = IS_OK; - return is->trade_net_addrs; - } + return true; } -unsigned short * __fastcall -patch_get_pixel_to_draw_city_dot (JGL_Image * this, int edx, int x, int y) +int +count_contiguous_bridge_districts (int tile_x, int tile_y, int dx, int dy) { - unsigned short * tr = this->vtable->m07_m05_Get_Pixel (this, __, x, y); + Map * map = &p_bic_data->Map; + int nx = tile_x + dx; + int ny = tile_y + dy; + int count = 0; + int max_steps = map->Width + map->Height; - if ((x + 1 < p_main_screen_form->GUI.Navigator_Data.Mini_Map_Width2) && (y + 1 < p_main_screen_form->GUI.Navigator_Data.Mini_Map_Height2)) { - unsigned short * below = this->vtable->m07_m05_Get_Pixel (this, __, x + 1, y + 1); - if (below != NULL) - *below = 0; + for (int step = 0; step < max_steps; step++) { + wrap_tile_coords (map, &nx, &ny); + if (! tile_has_district_at (nx, ny, BRIDGE_DISTRICT_ID)) + break; + count++; + nx += dx; + ny += dy; } - return tr; + return count; } -enum branch_kind { BK_CALL, BK_JUMP }; - -byte * -emit_branch (enum branch_kind kind, byte * cursor, void const * target) +int +count_contiguous_canal_districts (int tile_x, int tile_y, int max_count) { - int offset = (int)target - ((int)cursor + 5); - *cursor++ = (kind == BK_CALL) ? 0xE8 : 0xE9; - return int_to_bytes (cursor, offset); -} + if (max_count <= 0) + return 0; -// Just calls VirtualProtect and displays an error message if it fails. Made for use by the WITH_MEM_PROTECTION macro. -bool -check_virtual_protect (LPVOID addr, SIZE_T size, DWORD flags, PDWORD old_protect) -{ - if (VirtualProtect (addr, size, flags, old_protect)) - return true; - else { - char err_msg[1000]; - snprintf (err_msg, sizeof err_msg, "VirtualProtect failed! Args:\n Address: 0x%p\n Size: %d\n Flags: 0x%x", addr, size, flags); - err_msg[(sizeof err_msg) - 1] = '\0'; - MessageBoxA (NULL, err_msg, NULL, MB_ICONWARNING); - return false; - } -} + Map * map = &p_bic_data->Map; + int limit = max_count + 1; + int capacity = limit; + if (capacity < 1) + capacity = 1; + + int * xs = malloc (sizeof (*xs) * capacity); + int * ys = malloc (sizeof (*ys) * capacity); + if ((xs == NULL) || (ys == NULL)) { + if (xs != NULL) + free (xs); + if (ys != NULL) + free (ys); + return limit; + } + + int const adj_dx[8] = { 0, 0, -2, 2, 1, 1, -1, -1 }; + int const adj_dy[8] = { -2, 2, 0, 0, -1, 1, -1, 1 }; + int head = 0; + int tail = 0; + int count = 0; -#define WITH_MEM_PROTECTION(addr, size, flags) \ - for (DWORD old_protect, unused, iter_count = 0; \ - (iter_count == 0) && check_virtual_protect (addr, size, flags, &old_protect); \ - VirtualProtect (addr, size, old_protect, &unused), iter_count++) + xs[tail] = tile_x; + ys[tail] = tile_y; + tail++; -void __fastcall adjust_sliders_preproduction (Leader * this); + while (head < tail) { + int cx = xs[head]; + int cy = ys[head]; + head++; + count++; + if (count >= limit) + break; -struct saved_code_area { - int size; - byte original_contents[]; -}; + for (int i = 0; i < 8; i++) { + int nx = cx + adj_dx[i]; + int ny = cy + adj_dy[i]; + wrap_tile_coords (map, &nx, &ny); + if (! tile_has_district_at (nx, ny, CANAL_DISTRICT_ID)) + continue; -// Saves an area of base game code and optionally replaces it with no-ops. The area can be restored with restore_code_area. This method assumes that -// the necessary memory protection has already been set on the area, specifically that it can be written to. The method will do nothing if the area -// has already been saved with the same size. It's an error to re-save an address with a different size or overlap two saved areas. -void -save_code_area (byte * addr, int size, bool nopify) -{ - struct saved_code_area * sca; - if (itable_look_up (&is->saved_code_areas, (int)addr, (int *)&sca)) { - if (sca->size != 0) { - if (sca->size != size) { - char s[200]; - snprintf (s, sizeof s, "Save code area conflict: address %p was already saved with size %d, conflicting with new size %d.", addr, sca->size, size); - s[(sizeof s) - 1] = '\0'; - pop_up_in_game_error (s); + bool seen = false; + for (int j = 0; j < tail; j++) { + if ((xs[j] == nx) && (ys[j] == ny)) { + seen = true; + break; + } + } + if (seen) + continue; + + if (tail < capacity) { + xs[tail] = nx; + ys[tail] = ny; + tail++; + } else { + count = limit; + head = tail; + break; } - return; } - } else { - sca = malloc (size + sizeof *sca); - itable_insert (&is->saved_code_areas, (int)addr, (int)sca); } - sca->size = size; - memcpy (&sca->original_contents, addr, size); - if (nopify) - memset (addr, 0x90, size); + + free (xs); + free (ys); + + return count; } -// Restores a saved chunk of code to its original contents. Does nothing if the area hasn't been saved. Assumes the appropriate memory protection has -// already been set. -void -restore_code_area (byte * addr) +bool +district_line_is_straight (int tile_x, int tile_y, int district_id) { - struct saved_code_area * sca; - if (itable_look_up (&is->saved_code_areas, (int)addr, (int *)&sca)) { - memcpy (addr, &sca->original_contents, sca->size); - sca->size = 0; + Map * map = &p_bic_data->Map; + int const adj_dx[8] = { 0, 0, -2, 2, -1, 1, -1, 1 }; + int const adj_dy[8] = { -2, 2, 0, 0, 1, -1, -1, 1 }; + + for (int i = 0; i < 8; i++) { + int nx = tile_x + adj_dx[i]; + int ny = tile_y + adj_dy[i]; + wrap_tile_coords (map, &nx, &ny); + if (! tile_has_district_at (nx, ny, district_id)) + continue; + + int cand_dx = -adj_dx[i]; + int cand_dy = -adj_dy[i]; + + for (int j = 0; j < 8; j++) { + int ox = nx + adj_dx[j]; + int oy = ny + adj_dy[j]; + wrap_tile_coords (map, &ox, &oy); + if ((ox == tile_x) && (oy == tile_y)) + continue; + if (! tile_has_district_at (ox, oy, district_id)) + continue; + + if (! ((adj_dx[j] == cand_dx) && (adj_dy[j] == cand_dy)) && + ! ((adj_dx[j] == -cand_dx) && (adj_dy[j] == -cand_dy))) + return false; + } } + + return true; } bool -is_code_area_saved (byte * addr) +bridge_district_tile_is_valid (int tile_x, int tile_y) { - struct saved_code_area * sca; - return itable_look_up (&is->saved_code_areas, (int)addr, (int *)&sca) && (sca->size > 0); -} + if (! tile_is_coastal_water (tile_x, tile_y)) + return false; -// Nopifies or restores an area depending on if yes_or_no is 1 or 0. Sets the necessary memory protections. -void -set_nopification (int yes_or_no, byte * addr, int size) -{ - WITH_MEM_PROTECTION (addr, size, PAGE_EXECUTE_READWRITE) { - if (yes_or_no) - save_code_area (addr, size, true); - else - restore_code_area (addr); + bool has_adjacent_land_or_bridge = false; + Map * map = &p_bic_data->Map; + int const adj_dx[8] = { 0, 0, -2, 2, -1, 1, -1, 1 }; + int const adj_dy[8] = { -2, 2, 0, 0, 1, -1, -1, 1 }; + + for (int i = 0; i < 8; i++) { + int nx = tile_x + adj_dx[i]; + int ny = tile_y + adj_dy[i]; + wrap_tile_coords (map, &nx, &ny); + if (tile_is_land (-1, nx, ny, false) || tile_has_district_at (nx, ny, BRIDGE_DISTRICT_ID)) { + has_adjacent_land_or_bridge = true; + break; + } + } + if (! has_adjacent_land_or_bridge) + return false; + + if (is->current_config.max_contiguous_bridge_districts > 0) { + int max_bridges = is->current_config.max_contiguous_bridge_districts; + + int ns_count = count_contiguous_bridge_districts (tile_x, tile_y, 0, -2) + + count_contiguous_bridge_districts (tile_x, tile_y, 0, 2); + if (ns_count > max_bridges) + return false; + + int we_count = count_contiguous_bridge_districts (tile_x, tile_y, -2, 0) + + count_contiguous_bridge_districts (tile_x, tile_y, 2, 0); + if (we_count > max_bridges) + return false; + + int swne_count = count_contiguous_bridge_districts (tile_x, tile_y, -1, 1) + + count_contiguous_bridge_districts (tile_x, tile_y, 1, -1); + if (swne_count > max_bridges) + return false; + + int nwse_count = count_contiguous_bridge_districts (tile_x, tile_y, -1, -1) + + count_contiguous_bridge_districts (tile_x, tile_y, 1, 1); + if (nwse_count > max_bridges) + return false; } + + return district_line_is_straight (tile_x, tile_y, BRIDGE_DISTRICT_ID); } -void -apply_machine_code_edits (struct c3x_config const * cfg, bool at_program_start) +bool +canal_district_tile_is_valid (int tile_x, int tile_y) { - DWORD old_protect, unused; + if (tile_is_water (tile_x, tile_y)) + return false; - // Allow stealth attack against single unit - WITH_MEM_PROTECTION (ADDR_STEALTH_ATTACK_TARGET_COUNT_CHECK, 1, PAGE_EXECUTE_READWRITE) - *(byte *)ADDR_STEALTH_ATTACK_TARGET_COUNT_CHECK = cfg->allow_stealth_attack_against_single_unit ? 0 : 1; + if (is->current_config.max_contiguous_canal_districts > 0) { + int count = count_contiguous_canal_districts (tile_x, tile_y, is->current_config.max_contiguous_canal_districts); + if (count > is->current_config.max_contiguous_canal_districts) + return false; + } - // Enable small wonders providing free buildings - WITH_MEM_PROTECTION (ADDR_RECOMPUTE_AUTO_IMPROVS_FILTER, 10, PAGE_EXECUTE_READWRITE) { - byte normal[8] = {0x83, 0xE1, 0x04, 0x80, 0xF9, 0x04, 0x0F, 0x85}; // and ecx, 4; cmp ecx, 4; jnz [offset] - byte modded[8] = {0x83, 0xE1, 0x0C, 0x80, 0xF9, 0x00, 0x0F, 0x84}; // and ecx, 12; cmp ecx, 0; jz [offset] - for (int n = 0; n < 8; n++) - ((byte *)ADDR_RECOMPUTE_AUTO_IMPROVS_FILTER)[n] = cfg->enable_free_buildings_from_small_wonders ? modded[n] : normal[n]; + Map * map = &p_bic_data->Map; + int const adj_dx[8] = { 0, 0, -2, 2, 1, 1, -1, -1 }; + int const adj_dy[8] = { -2, 2, 0, 0, -1, 1, -1, 1 }; + + for (int i = 0; i < 8; i++) { + int nx = tile_x + adj_dx[i]; + int ny = tile_y + adj_dy[i]; + wrap_tile_coords (map, &nx, &ny); + Tile * tile = tile_at (nx, ny); + if ((tile == NULL) || (tile == p_null_tile)) + continue; + if (tile->vtable->m35_Check_Is_Water (tile)) + return true; + + if (tile_has_district_at (nx, ny, CANAL_DISTRICT_ID)) { + struct district_instance * inst = get_district_instance (tile); + if ((inst != NULL) && (inst->district_id == CANAL_DISTRICT_ID) && + district_is_complete (tile, CANAL_DISTRICT_ID)) + return true; + } } - // Bypass artillery in city check - // replacing 0x74 (= jump if [city ptr is] zero) with 0xEB (= uncond. jump) - WITH_MEM_PROTECTION (ADDR_CHECK_ARTILLERY_IN_CITY, 1, PAGE_EXECUTE_READWRITE) - *(byte *)ADDR_CHECK_ARTILLERY_IN_CITY = cfg->use_offensive_artillery_ai ? 0xEB : 0x74; - - // Remove unit limit - if (! at_program_start) { - // Replace 0x7C (= jump if less than [unit limit]) with 0xEB (= uncond. jump) in Leader::spawn_unit - WITH_MEM_PROTECTION (ADDR_UNIT_COUNT_CHECK, 1, PAGE_EXECUTE_READWRITE) - *(byte *)ADDR_UNIT_COUNT_CHECK = cfg->remove_unit_limit ? 0xEB : 0x7C; - - // Increase max ID to search for tradable units by 10x if limit removed - WITH_MEM_PROTECTION (ADDR_MAX_TRADABLE_UNIT_ID, 4, PAGE_EXECUTE_READWRITE) - int_to_bytes (ADDR_MAX_TRADABLE_UNIT_ID, cfg->remove_unit_limit ? 81920 : 8192); - - // Reallocate diplo_form->tradable_units array so it's 10x long if limit removed - civ_prog_free (p_diplo_form->tradable_units); - int tradable_units_len = cfg->remove_unit_limit ? 81920 : 8192, - tradable_units_size = tradable_units_len * sizeof (TradableItem); - p_diplo_form->tradable_units = new (tradable_units_size); - for (int n = 0; n < tradable_units_len; n++) - p_diplo_form->tradable_units[n] = (TradableItem) {.label = (char *)-1, 0}; // clear label to -1 and other fields to 0 + return false; +} - // Patch the size limit on some code that clears the tradable units array when starting a new game - WITH_MEM_PROTECTION (ADDR_TRADABLE_UNITS_SIZE_TO_CLEAR, 4, PAGE_EXECUTE_READWRITE) - int_to_bytes (ADDR_TRADABLE_UNITS_SIZE_TO_CLEAR, tradable_units_size); - } +bool +can_build_district_on_tile (Tile * tile, int district_id, int civ_id) +{ + if ((! is->current_config.enable_districts) || + (tile == NULL) || (tile == p_null_tile) || + (tile->CityID >= 0) || + tile->vtable->m21_Check_Crates (tile, __, 0) || + tile->vtable->m20_Check_Pollution (tile, __, 0) || + (district_id < 0) || (district_id >= is->district_count)) + return false; - // Remove era limit - // replacing 0x74 (= jump if zero [after cmp'ing era count with 4]) with 0xEB - WITH_MEM_PROTECTION (ADDR_ERA_COUNT_CHECK, 1, PAGE_EXECUTE_READWRITE) - *(byte *)ADDR_ERA_COUNT_CHECK = cfg->remove_era_limit ? 0xEB : 0x74; + struct district_config const * cfg = &is->district_configs[district_id]; + if (cfg->command == -1) + return false; - // Fix science age bug - // Similar in nature to the sub bug, the function that measures a city's research output accepts a flag that determines whether or not it - // takes science ages into account. It's mistakenly not set by the code that gathers all research points to increment tech progress (but it - // is set elsewhere in code for the interface). The patch simply sets this flag. - WITH_MEM_PROTECTION (ADDR_SCIENCE_AGE_BUG_PATCH, 1, PAGE_EXECUTE_READWRITE) - *(byte *)ADDR_SCIENCE_AGE_BUG_PATCH = cfg->patch_science_age_bug ? 1 : 0; + if ((cfg->command == UCV_Build_Neighborhood) && !is->current_config.enable_neighborhood_districts) return false; + if ((cfg->command == UCV_Build_WonderDistrict) && !is->current_config.enable_wonder_districts) return false; + if ((cfg->command == UCV_Build_DistributionHub) && !is->current_config.enable_distribution_hub_districts) return false; + if ((cfg->command == UCV_Build_Aerodrome) && !is->current_config.enable_aerodrome_districts) return false; + if ((cfg->command == UCV_Build_Port) && !is->current_config.enable_port_districts) return false; + if ((cfg->command == UCV_Build_Bridge) && !is->current_config.enable_bridge_districts) return false; + if ((cfg->command == UCV_Build_CentralRailHub) && !is->current_config.enable_central_rail_hub_districts) return false; + if ((cfg->command == UCV_Build_EnergyGrid) && !is->current_config.enable_energy_grid_districts) return false; + if ((cfg->command == UCV_Build_GreatWall) && !is->current_config.enable_great_wall_districts) return false; + + if (! district_is_buildable_on_square_type (cfg, tile)) + return false; - // Pedia pink line bug fix - // The size of the pedia background texture is hard-coded into the EXE and in the base game it's one pixel too small. This shows up in game as - // a one pixel wide pink line along the right edge of the civilopedia. This patch simply increases the texture width by one. - WITH_MEM_PROTECTION (ADDR_PEDIA_TEXTURE_BUG_PATCH, 1, PAGE_EXECUTE_READWRITE) - *(byte *)ADDR_PEDIA_TEXTURE_BUG_PATCH = cfg->patch_pedia_texture_bug ? 0xA6 : 0xA5; + int tile_x = 0, tile_y = 0; + tile_coords_from_ptr (&p_bic_data->Map, tile, &tile_x, &tile_y); - // Fix for houseboat bug - // See my posts on CFC for an explanation of the bug and its fix: - // https://forums.civfanatics.com/threads/sub-bug-fix-and-other-adventures-in-exe-modding.666881/page-10#post-16084386 - // https://forums.civfanatics.com/threads/sub-bug-fix-and-other-adventures-in-exe-modding.666881/page-10#post-16085242 - WITH_MEM_PROTECTION (ADDR_HOUSEBOAT_BUG_PATCH, ADDR_HOUSEBOAT_BUG_PATCH_END - ADDR_HOUSEBOAT_BUG_PATCH, PAGE_EXECUTE_READWRITE) { - if (cfg->patch_houseboat_bug) { - save_code_area (ADDR_HOUSEBOAT_BUG_PATCH, ADDR_HOUSEBOAT_BUG_PATCH_END - ADDR_HOUSEBOAT_BUG_PATCH, true); - byte * cursor = ADDR_HOUSEBOAT_BUG_PATCH; - *cursor++ = 0x50; // push eax - int call_offset = (int)&tile_at_city_or_null - ((int)cursor + 5); - *cursor++ = 0xE8; // call - cursor = int_to_bytes (cursor, call_offset); - } else - restore_code_area (ADDR_HOUSEBOAT_BUG_PATCH); - } + if (! leader_can_build_district (&leaders[civ_id], district_id)) + return false; - // NoRaze - WITH_MEM_PROTECTION (ADDR_AUTORAZE_BYPASS, 2, PAGE_EXECUTE_READWRITE) { - byte normal[2] = {0x0F, 0x85}; // jnz - byte bypass[2] = {0x90, 0xE9}; // nop, jmp - for (int n = 0; n < 2; n++) - ((byte *)ADDR_AUTORAZE_BYPASS)[n] = cfg->prevent_autorazing ? bypass[n] : normal[n]; - } + if (! district_resource_prereqs_met (tile, tile_x, tile_y, district_id, NULL)) + return false; - // Overwrite the instruction(s) where the AI's production choosing code compares the value of what it's currently considering to the best - // option so far. This is done twice since improvements and units are handled in separate loops. The instr(s) are overwritten with a jump to - // an "airlock", which is a bit of code that wraps the call to intercept_consideration. The contents of the airlocks are prepared by the - // patcher in init_consideration_airlocks. - // TODO: This instruction replacement could be done in the patcher too and that might be a better place for it. Think about this. - for (int n = 0; n < 2; n++) { - void * addr_intercept = (n == 0) ? ADDR_INTERCEPT_AI_IMPROV_VALUE : ADDR_INTERCEPT_AI_UNIT_VALUE; - void * addr_airlock = (n == 0) ? ADDR_IMPROV_CONSIDERATION_AIRLOCK : ADDR_UNIT_CONSIDERATION_AIRLOCK; + if ((district_id == CANAL_DISTRICT_ID) && (! canal_district_tile_is_valid (tile_x, tile_y))) + return false; - WITH_MEM_PROTECTION (addr_intercept, AI_CONSIDERATION_INTERCEPT_LEN, PAGE_EXECUTE_READWRITE) { - byte * cursor = addr_intercept; + if ((district_id == BRIDGE_DISTRICT_ID) && (! bridge_district_tile_is_valid (tile_x, tile_y))) + return false; - // write jump to airlock - *cursor++ = 0xE9; - int offset = (int)addr_airlock - ((int)addr_intercept + 5); - cursor = int_to_bytes (cursor, offset); + struct district_instance * existing_inst = get_district_instance (tile); + struct district_infos const * info = &is->district_infos[district_id]; + int existing_district_id = (existing_inst != NULL) ? existing_inst->district_id : -1; + bool district_completed = district_is_complete (tile, existing_district_id); - // fill the rest of the space with NOPs - while (cursor < (byte *)addr_intercept + AI_CONSIDERATION_INTERCEPT_LEN) - *cursor++ = 0x90; // nop + if ((existing_district_id == district_id) && district_completed) + return false; + if ((existing_district_id >= 0) && (existing_district_id != district_id) && (! district_completed)) + return false; + + if (! cfg->allow_multiple) { + FOR_DISTRICTS_AROUND (wai, tile_x, tile_y, false) { + if (wai.district_inst->district_id == district_id) + return false; } } - // Overwrite instruction that sets bits in City.Body.Available_Resources with a jump to the airlock - WITH_MEM_PROTECTION (ADDR_INTERCEPT_SET_RESOURCE_BIT, 6, PAGE_EXECUTE_READWRITE) { - byte * cursor = ADDR_INTERCEPT_SET_RESOURCE_BIT; - if (cfg->patch_phantom_resource_bug) { - *cursor++ = 0xE9; - int offset = (int)ADDR_SET_RESOURCE_BIT_AIRLOCK - ((int)ADDR_INTERCEPT_SET_RESOURCE_BIT + 5); - cursor = int_to_bytes (cursor, offset); - *cursor++ = 0x90; // nop - } else { - byte original[6] = {0x09, 0xB0, 0x9C, 0x00, 0x00, 0x00}; // or dword ptr [eax+0x9C], esi - for (int n = 0; n < 6; n++) - cursor[n] = original[n]; - } - } + return true; +} - // Enlarge the mask that's applied to p_bic_data->Map.TileCount in the loop over lines in Trade_Net::recompute_resources. This lets us loop - // over more than 0xFFFF tiles. - WITH_MEM_PROTECTION (ADDR_RESOURCE_TILE_COUNT_MASK, 1, PAGE_EXECUTE_READWRITE) { - *(byte *)ADDR_RESOURCE_TILE_COUNT_MASK = (cfg->count_mills > 0) ? 0xFF : 0x00; - } - // Similarly, enlarge the cmp instruction that jumps over the entire loop when the tile count is zero. Do this by simply removing the operand - // override prefix byte (0x66), overwriting it with a nop (0x90). This converts the instruction "cmp word ptr [bic_data.Map.TileCount], di" - // into "nop; cmp dword ptr [bic_data.Map.TileCount], edi" - WITH_MEM_PROTECTION (ADDR_RESOURCE_TILE_COUNT_ZERO_COMPARE, 1, PAGE_EXECUTE_READWRITE) { - *(byte *)ADDR_RESOURCE_TILE_COUNT_ZERO_COMPARE = 0x90; - } +bool +district_is_obsolete_for_civ (int district_id, int civ_id) +{ + if ((district_id < 0) || (district_id >= is->district_count)) + return false; - byte * addr_turn_metalimits[] = {ADDR_TURN_METALIMIT_1, ADDR_TURN_METALIMIT_2, ADDR_TURN_METALIMIT_3, ADDR_TURN_METALIMIT_4, - ADDR_TURN_METALIMIT_5, ADDR_TURN_METALIMIT_6, ADDR_TURN_METALIMIT_7}; - for (int n = 0; n < ARRAY_LEN (addr_turn_metalimits); n++) { - byte * addr = addr_turn_metalimits[n]; - WITH_MEM_PROTECTION (addr, 4, PAGE_EXECUTE_READWRITE) { - int_to_bytes (addr, cfg->remove_cap_on_turn_limit ? 1000000 : 1000); - } - } + int obsolete_id = is->district_infos[district_id].obsoleted_by_id; + if (obsolete_id < 0) + return false; - // Overwrite the human-ness test and call to Leader::ai_adjust_sliders that happens during the preproduction player update method. The new - // code calls adjust_sliders_preproduction for all players. - WITH_MEM_PROTECTION (ADDR_AI_PREPRODUCTION_SLIDER_ADJUSTMENT, 9, PAGE_EXECUTE_READWRITE) { - byte * cursor = ADDR_AI_PREPRODUCTION_SLIDER_ADJUSTMENT; - *cursor++ = 0x8B; *cursor++ = 0xCE; // mov ecx, esi - cursor = emit_branch (BK_CALL, cursor, adjust_sliders_preproduction); - for (; cursor < ADDR_AI_PREPRODUCTION_SLIDER_ADJUSTMENT + 9; cursor++) - *cursor = 0x90; // nop - } + return Leader_has_tech (&leaders[civ_id], __, obsolete_id); +} - // Set up a special intercept of the base game's calls to print_int in order to grab a pointer to a gold TradeOffer object being modified. The - // calls to print_int happen in the context of creating the default text to be placed in the set-gold-amount popup. Conveniently, a pointer to - // the TradeOffer object is always stored in register ecx when this call happens. It's not easily accessible since print_int uses the cdecl - // convention so we must use an airlock-like thing to effectively convert the calling convention to fastcall. The first part of this code - // simply replaces the call to print_int with a call to the airlock-like thing, and the second part initializes its contents. - byte * addr_print_gold_amounts[] = {ADDR_PRINT_GOLD_AMOUNT_1, ADDR_PRINT_GOLD_AMOUNT_2}; - for (int n = 0; n < ARRAY_LEN (addr_print_gold_amounts); n++) { - byte * addr = addr_print_gold_amounts[n]; - WITH_MEM_PROTECTION (addr, 5, PAGE_EXECUTE_READWRITE) - emit_branch (BK_CALL, addr, ADDR_CAPTURE_MODIFIED_GOLD_TRADE); - } - WITH_MEM_PROTECTION (ADDR_CAPTURE_MODIFIED_GOLD_TRADE, 32, PAGE_EXECUTE_READWRITE) { - byte * cursor = ADDR_CAPTURE_MODIFIED_GOLD_TRADE; +bool +tile_has_obsolete_district_for_civ (Tile * tile, int civ_id) +{ + if ((tile == NULL) || (tile == p_null_tile)) + return false; - // Repush all of the arguments to print_int onto the stack, they will be consumed by do_capture_modified_gold_trade since it's - // fastcall. The original args don't need to be removed b/c we're replacing a cdecl function so that's the caller's - // responsibility. The TradeOffer pointer is already in ECX, so that's fine as long as we don't touch that register. - byte repush[] = {0xFF, 0x74, 0x24, 0x0C}; // push [esp+0xC] - for (int n = 0; n < 3; n++) - for (int k = 0; k < ARRAY_LEN (repush); k++) - *cursor++ = repush[k]; + struct district_instance * inst = get_district_instance (tile); + if (inst == NULL) + return false; - cursor = emit_branch (BK_CALL, cursor, do_capture_modified_gold_trade); // call do_capture_modified_gold_trade - *cursor++ = 0xC3; // ret - } + int district_id = inst->district_id; + if ((district_id < 0) || (district_id >= is->district_count)) + return false; - // Edit branch in capture_city to never run code for barbs, this allows barbs to capture cities - WITH_MEM_PROTECTION (ADDR_CAPTURE_CITY_BARB_BRANCH, 2, PAGE_EXECUTE_READWRITE) { - byte normal[2] = {0x0F, 0x85}; // jnz - byte bypass[2] = {0x90, 0xE9}; // nop, jmp - for (int n = 0; n < 2; n++) - ((byte *)ADDR_CAPTURE_CITY_BARB_BRANCH)[n] = cfg->enable_city_capture_by_barbarians ? bypass[n] : normal[n]; - } + if (! district_is_complete (tile, district_id)) + return false; - // After the production phase is done for the barb player, there are two jump instructions skipping the production code for civs. Replacing - // those jumps lets us run the civ production code for the barbs as well. - WITH_MEM_PROTECTION (ADDR_PROD_PHASE_BARB_DONE_NO_SPAWN_JUMP, 6, PAGE_EXECUTE_READWRITE) { - if (cfg->enable_city_capture_by_barbarians) { - save_code_area (ADDR_PROD_PHASE_BARB_DONE_NO_SPAWN_JUMP, 6, true); - byte jump_to_civ[6] = {0x0F, 0x8D, 0x0C, 0x00, 0x00, 0x00}; // jge +0x12 - for (int n = 0; n < 6; n++) - ADDR_PROD_PHASE_BARB_DONE_NO_SPAWN_JUMP[n] = jump_to_civ[n]; - } else - restore_code_area (ADDR_PROD_PHASE_BARB_DONE_NO_SPAWN_JUMP); - } - set_nopification (cfg->enable_city_capture_by_barbarians, ADDR_PROD_PHASE_BARB_DONE_JUMP, 5); + return district_is_obsolete_for_civ (district_id, civ_id); +} - for (int domain = 0; domain < 2; domain++) { - byte * addr_skip = (domain == 0) ? ADDR_SKIP_LAND_UNITS_FOR_SEA_ZOC : ADDR_SKIP_SEA_UNITS_FOR_LAND_ZOC, - * addr_airlock = (domain == 0) ? ADDR_SEA_ZOC_FILTER_AIRLOCK : ADDR_LAND_ZOC_FILTER_AIRLOCK; +bool +tile_suitable_for_district (Tile * tile, int district_id, City * city, bool * out_has_resource) +{ + bool has_resource = false; + if ((tile != NULL) && (tile != p_null_tile)) + has_resource = tile_has_resource (tile); + if (out_has_resource != NULL) + *out_has_resource = has_resource; - WITH_MEM_PROTECTION (addr_skip, 6, PAGE_EXECUTE_READWRITE) { - if ((cfg->special_zone_of_control_rules != 0) && ! is_code_area_saved (addr_skip)) { - byte * original_target = addr_skip + 6 + int_from_bytes (addr_skip + 2); // target addr of jump instr we're replacing - save_code_area (addr_skip, 6, true); + if ((tile == NULL) || (tile == p_null_tile)) return false; + if (tile->CityID >= 0) return false; + if (tile->vtable->m38_Get_Territory_OwnerID (tile) != city->Body.CivID) return false; + if (! can_build_district_on_tile (tile, district_id, city->Body.CivID)) return false; - // Initialize airlock. The airlock preserves all registers and calls filter_zoc_candidate then either follows or skips the - // original jump depending on what it returns. If zero is returned, follows the jump, skipping a bunch of code and filtering - // out the unit as a candidate for ZoC. - WITH_MEM_PROTECTION (addr_airlock, INLEAD_SIZE, PAGE_READWRITE) { - byte * cursor = addr_airlock; - *cursor++ = 0x60; // pusha - *cursor++ = 0x54; // push esp - cursor = emit_branch (BK_CALL, cursor, filter_zoc_candidate); - *cursor++ = 0x83; *cursor++ = 0xF8; *cursor++ = 0x01; // cmp eax, 1 - *cursor++ = 0x75; *cursor++ = 0x06; // jne 6 - *cursor++ = 0x61; // popa - cursor = emit_branch (BK_JUMP, cursor, addr_skip + 6); - *cursor++ = 0x61; // popa - cursor = emit_branch (BK_JUMP, cursor, original_target); - } + int tile_x = 0, tile_y = 0; + tile_coords_from_ptr (&p_bic_data->Map, tile, &tile_x, &tile_y); + if (! district_resource_prereqs_met (tile, tile_x, tile_y, district_id, city)) + return false; - // Write jump to airlock - emit_branch (BK_JUMP, addr_skip, addr_airlock); - } else if (cfg->special_zone_of_control_rules == 0) - restore_code_area (addr_skip); + struct district_instance * inst = get_district_instance (tile); + if (inst != NULL) { + if (tile_has_obsolete_district_for_civ (tile, city->Body.CivID)) + return true; + + // Unused wonder districts can be repurposed, completed cannot + if (district_id == WONDER_DISTRICT_ID && inst->state == DS_COMPLETED) { + struct wonder_district_info * winfo = &inst->wonder_info; + if (winfo->state == WDS_COMPLETED) + return false; } + + return false; } - set_nopification ( cfg->special_zone_of_control_rules != 0, ADDR_ZOC_CHECK_ATTACKER_ANIM_FIELD_111, 6); - set_nopification ((cfg->special_zone_of_control_rules & SZOCR_LETHAL) != 0, ADDR_SKIP_ZOC_FOR_ONE_HP_LAND_UNIT , 6); - set_nopification ((cfg->special_zone_of_control_rules & SZOCR_LETHAL) != 0, ADDR_SKIP_ZOC_FOR_ONE_HP_SEA_UNIT , 6); + return true; +} - WITH_MEM_PROTECTION (ADDR_LUXURY_BOX_ROW_HEIGHT, 4, PAGE_EXECUTE_READWRITE) { - byte normal [4] = {0x8B, 0x44, 0x24, LUXURY_BOX_ROW_HEIGHT_STACK_OFFSET}; // mov eax, dword ptr [esp + LUXURY_BOX_ROW_HEIGHT_STACK_OFFSET] - byte compact[4] = {0x31, 0xC0, 0xB0, 0x10}; // xor eax, eax; mov al, 0x10 - for (int n = 0; n < 4; n++) - ADDR_LUXURY_BOX_ROW_HEIGHT[n] = cfg->compact_luxury_display_on_city_screen ? compact[n] : normal[n]; - } +bool +can_generate_resource (int for_civ_id, struct mill * mill) +{ + int req_tech_id = (mill->flags & MF_NO_TECH_REQ) ? -1 : p_bic_data->ResourceTypes[mill->resource_id].RequireID; + return (req_tech_id < 0) || Leader_has_tech (&leaders[for_civ_id], __, req_tech_id); +} - WITH_MEM_PROTECTION (ADDR_MOST_STRAT_RES_ON_CITY_SCREEN, 1, PAGE_EXECUTE_READWRITE) { - *(byte *)ADDR_MOST_STRAT_RES_ON_CITY_SCREEN = cfg->compact_strategic_resource_display_on_city_screen ? 13 : 8; - } +bool +district_can_generate_resource (int for_civ_id, struct district_config * dc) +{ + if (dc->generated_resource_id < 0) + return false; + int req_tech_id = (dc->generated_resource_flags & MF_NO_TECH_REQ) ? -1 : p_bic_data->ResourceTypes[dc->generated_resource_id].RequireID; + return (req_tech_id < 0) || Leader_has_tech (&leaders[for_civ_id], __, req_tech_id); +} - // Remove a check that a returned neighbor index is within the 21 tile work area in code related to hoving the mouse over the work area on the - // city screen. This check is redundant and could be removed always, but only do so if work area is expanded. - set_nopification (cfg->city_work_radius > 2, ADDR_REDUNDANT_CHECK_ON_WORK_AREA_HOVER, 6); +void +calculate_city_center_district_bonus (City * city, int * out_food, int * out_shields, int * out_gold) +{ + if (out_food != NULL) + *out_food = 0; + if (out_shields != NULL) + *out_shields = 0; + if (out_gold != NULL) + *out_gold = 0; - // Skip redundant check of clicked tile's neighbor index in City_Form::handle_left_click. - WITH_MEM_PROTECTION (ADDR_CITY_FORM_LEFT_CLICK_JUMP, 1, PAGE_EXECUTE_READWRITE) { - *ADDR_CITY_FORM_LEFT_CLICK_JUMP = 0xEB; // 0x7C (jl) -> 0xEB (jmp) - } + if ((! is->current_config.enable_districts && ! is->current_config.enable_natural_wonders) || (city == NULL)) + return; - // Skip check that neighbor index passed to City::controls_tile is within work radius. This check is now implemented in the patch func. - WITH_MEM_PROTECTION (ADDR_CONTROLS_TILE_JUMP, 1, PAGE_EXECUTE_READWRITE) { - *ADDR_CONTROLS_TILE_JUMP = 0xEB; // 0x7C (jl) -> 0xEB (jmp) - } + int bonus_food = 0; + int bonus_shields = 0; + int bonus_gold = 0; - // When searching for a tile on which to spawn pollution, the game wraps its search around by using remainder after division. Here, we replace - // the dividend to match a potentially expanded city work area. - WITH_MEM_PROTECTION (ADDR_SPAWN_POLLUTION_MOD, 4, PAGE_EXECUTE_READWRITE) { - int_to_bytes (ADDR_SPAWN_POLLUTION_MOD, is->workable_tile_count - 1); - } + int utilized_neighborhoods = count_utilized_neighborhoods_in_city_radius (city); + int neighborhoods_counted = 0; - WITH_MEM_PROTECTION (ADDR_PATHFINDER_RECONSTRUCTION_MAX_LEN, 4, PAGE_EXECUTE_READWRITE) { - int_to_bytes (ADDR_PATHFINDER_RECONSTRUCTION_MAX_LEN, cfg->patch_premature_truncation_of_found_paths ? 2560 : 256); - } + FOR_DISTRICTS_AROUND (wai, city->Body.X, city->Body.Y, true) { + if ((wai.dx == 0) && (wai.dy == 0)) + continue; + Tile * tile = wai.tile; - int * trade_net_addrs; - bool already_moved_trade_net = is->trade_net != p_original_trade_net, - want_moved_trade_net = cfg->city_limit > 512; - int lifted_city_limit_exp = 11; - int lifted_city_limit = 1 << lifted_city_limit_exp; - if ((! at_program_start) && - ((trade_net_addrs = load_trade_net_addrs ()) != NULL) && - ((already_moved_trade_net && ! want_moved_trade_net) || (want_moved_trade_net && ! already_moved_trade_net))) { - // Allocate a new trade net object if necessary. To construct it, all we have to do is zero a few fields and set the vptr. Otherwise, - // set the allocated object aside for deletion later. Also set new & old addresses to the locations we're moving to & from. - Trade_Net * to_free = NULL; - int p_old, p_new; - if (want_moved_trade_net) { - is->trade_net = calloc (1, (sizeof (Trade_Net)) - (4 * 512 * 512) + (4 * lifted_city_limit * lifted_city_limit)); - is->city_limit = lifted_city_limit; - is->trade_net->vtable = p_original_trade_net->vtable; - p_old = (int)p_original_trade_net; - p_new = (int)is->trade_net; - } else { - to_free = is->trade_net; - is->city_limit = 512; - p_old = (int)is->trade_net; - p_new = (int)p_original_trade_net; - is->trade_net = p_original_trade_net; - } - already_moved_trade_net = is->trade_net != p_original_trade_net; // Keep this variable up to date + struct district_instance * inst = wai.district_inst; + int district_id = inst->district_id; - // Patch all references from the "old" object to the "new" one - int offset; - bool popped_up_error = false; - char err_msg[200] = {0}; - int * refs; { - if (exe_version_index == 0) - refs = trade_net_addrs; - else if (exe_version_index == 1) // Steam version, skip refs and instructions for GOG - refs = &trade_net_addrs[TRADE_NET_REF_COUNT + TRADE_NET_INSTR_COUNT_GOG]; - else // PCGames.de version, skip two sets of refs and instrs for GOG & Steam - refs = &trade_net_addrs[2 * TRADE_NET_REF_COUNT + TRADE_NET_INSTR_COUNT_GOG + TRADE_NET_INSTR_COUNT_STEAM]; - } - for (int n_ref = 0; n_ref < TRADE_NET_REF_COUNT; n_ref++) { - int addr = refs[n_ref]; - WITH_MEM_PROTECTION ((void *)(addr - 10), 20, PAGE_EXECUTE_READWRITE) { - byte * instr = (byte *)addr; - if ((instr[0] == 0xB9) && (int_from_bytes (&instr[1]) == p_old)) // move trade net ptr to ecx - int_to_bytes (&instr[1], p_new); - else if ((instr[0] == 0xC7) && (instr[1] == 0x05) && (int_from_bytes (&instr[2]) == p_old)) // write trade net vtable ptr - int_to_bytes (&instr[2], p_new); - else if ((instr[0] == 0x81) && (instr[1] == 0xFE) && (int_from_bytes (&instr[2]) == (int)p_original_trade_net)) // cmp esi, trade net location - ; // Do not patch this location because it's the upper limit for a memcpy - else if ((instr[0] == 0x81) && (instr[1] == 0xFF) && (int_from_bytes (&instr[2]) == (int)p_original_trade_net)) // cmp edi, trade net location - ; // Same - else if ((instr[0] == 0x81) && (instr[1] == 0xFA) && (int_from_bytes (&instr[2]) == (int)&p_original_trade_net->Data2)) // cmp edx, trade net data2 location - ; // Same - else if (((instr[0] == 0xA3) || (instr[0] == 0xA1)) && // move eax to field or vice-versa - (offset = int_from_bytes (&instr[1]) - p_old, (offset >= 0) && (offset < 100))) - int_to_bytes (&instr[1], p_new + offset); - else if ((instr[0] == 0x89) && ((instr[1] >= 0x0D) && (instr[1] <= 0x3D)) && // move other regs to field - (offset = int_from_bytes (&instr[2]) - p_old, (offset >= 0) && (offset < 100))) - int_to_bytes (&instr[2], p_new + offset); - else if ((instr[0] == 0x8B) && ((instr[1] == 0x35) || (instr[1] == 0x3D) || (instr[1] == 0x0D)) && // mov field to esi, edi or ecx - (offset = int_from_bytes (&instr[2]) - p_old, (offset >= 0) && (offset < 100))) - int_to_bytes (&instr[2], p_new + offset); - else if (! popped_up_error) { - snprintf (err_msg, (sizeof err_msg) - 1, "Can't move trade net object from address 0x%x. Pattern doesn't match.", addr); - MessageBox (NULL, err_msg, NULL, MB_ICONERROR); - popped_up_error = true; - } - } + if (is->current_config.enable_neighborhood_districts && + (district_id == NEIGHBORHOOD_DISTRICT_ID)) { + if (neighborhoods_counted >= utilized_neighborhoods) + continue; + neighborhoods_counted++; } - // Patch all instructions that involve the stride of Trade_Net.Matrix - int * addrs, addr_count; { - if (exe_version_index == 0) { - addrs = &trade_net_addrs[TRADE_NET_REF_COUNT]; - addr_count = TRADE_NET_INSTR_COUNT_GOG; - } else if (exe_version_index == 1) { - addrs = &trade_net_addrs[2 * TRADE_NET_REF_COUNT + TRADE_NET_INSTR_COUNT_GOG]; - addr_count = TRADE_NET_INSTR_COUNT_STEAM; - } else { - addrs = &trade_net_addrs[3 * TRADE_NET_REF_COUNT + TRADE_NET_INSTR_COUNT_GOG + TRADE_NET_INSTR_COUNT_STEAM]; - addr_count = TRADE_NET_INSTR_COUNT_GOG; - } - for (int n = 0; n < addr_count; n++) { - byte * instr = (byte *)addrs[n]; - WITH_MEM_PROTECTION (instr, 10, PAGE_EXECUTE_READWRITE) { - if (! want_moved_trade_net) - restore_code_area (instr); - - else { - if ((instr[0] == 0xC1) && (instr[1] >= 0xE0) && (instr[1] <= 0xE7)) { // shl - save_code_area (instr, 3, false); - // shift amount is either 9 (1<<9 == 512) or 11 (1<<11 == 4*512, stride in bytes) - // in the second case, replace with lifted_exp + 2 to convert to bytes - instr[2] = lifted_city_limit_exp + ((instr[2] == 11) ? 2 : 0); - - } else if ((instr[0] == 0x81) && (instr[1] >= 0xC0) && (instr[1] <= 0xC7)) { // add - save_code_area (instr, 6, false); - int amount = int_from_bytes (&instr[2]); - // amount is either 512 or 4*512, replace with lifted_lim or 4*lifted_lim - int_to_bytes (&instr[2], (amount == 512) ? lifted_city_limit : 4*lifted_city_limit); - - } else if ((instr[0] == 0x8D) && (instr[1] == 0x9C) && (instr[2] == 0x90)) { // lea - save_code_area (instr, 7, false); - int offset = 4 * lifted_city_limit + 0x38; // stride in bytes plus 0x38 offset to Matrix in Trade_Net object - int_to_bytes (&instr[3], offset); - - } else if (instr[0] == 0xB9) { // mov - save_code_area (instr, 5, false); - int_to_bytes (&instr[1], lifted_city_limit * lifted_city_limit); + struct district_config * cfg = &is->district_configs[district_id]; + int food_bonus = 0, shield_bonus = 0, gold_bonus = 0; + get_effective_district_yields (inst, cfg, &food_bonus, &shield_bonus, &gold_bonus, NULL, NULL, NULL); + bonus_food += food_bonus; + bonus_shields += shield_bonus; + bonus_gold += gold_bonus; - } else if (! popped_up_error) { - snprintf (err_msg, (sizeof err_msg) - 1, "Can't patch matrix stride at address 0x%x. Pattern doesn't match.", (int)instr); - MessageBox (NULL, err_msg, NULL, MB_ICONERROR); - popped_up_error = true; - } - } - } + if ((cfg->generated_resource_id >= 0) && (cfg->generated_resource_flags & MF_YIELDS)) { + int res_id = cfg->generated_resource_id; + if (district_can_generate_resource (city->Body.CivID, cfg)) { + Resource_Type * res = &p_bic_data->ResourceTypes[res_id]; + bonus_food += res->Food; + bonus_shields += res->Shield; + bonus_gold += res->Commerce; } } + } - // Reallocate diplo_form->tradable_cities array so it matches the city limit - civ_prog_free (p_diplo_form->tradable_cities); - int tradable_cities_len = want_moved_trade_net ? lifted_city_limit : 512, - tradable_cities_size = tradable_cities_len * sizeof (TradableItem); - p_diplo_form->tradable_cities = new (tradable_cities_size); - for (int n = 0; n < tradable_cities_len; n++) - p_diplo_form->tradable_cities[n] = (TradableItem) {.label = (char *)-1, 0}; // clear label to -1 and other fields to 0 + if (is->current_config.enable_districts && is->current_config.enable_distribution_hub_districts) { + int hub_food = 0; + int hub_shields = 0; + get_distribution_hub_yields_for_city (city, &hub_food, &hub_shields); + bonus_food += hub_food; + bonus_shields += hub_shields; + } - // Patch the size limit on some code that clears the tradable cities array when starting a new game - WITH_MEM_PROTECTION (ADDR_TRADABLE_CITIES_SIZE_TO_CLEAR, 4, PAGE_EXECUTE_READWRITE) { - int_to_bytes (ADDR_TRADABLE_CITIES_SIZE_TO_CLEAR, tradable_cities_size); - } + if (out_food != NULL) + *out_food = bonus_food; + if (out_shields != NULL) + *out_shields = bonus_shields; + if (out_gold != NULL) + *out_gold = bonus_gold; +} - if (to_free) { - to_free->vtable->destruct (to_free, __, 0); - free (to_free); +int __fastcall +patch_Map_calc_food_yield_at (Map * this, int edx, int tile_x, int tile_y, int tile_base_type, int civ_id, int imagine_fully_improved, City * city) +{ + if (! is->current_config.enable_districts) + return Map_calc_food_yield_at (this, __, tile_x, tile_y, tile_base_type, civ_id, imagine_fully_improved, city); + + Tile * tile = tile_at (tile_x, tile_y); + if ((tile != NULL) && (tile != p_null_tile)) { + struct district_instance * inst = get_district_instance (tile); + if (inst != NULL && district_is_complete (tile, inst->district_id)) { + return 0; } } - // Set is->city_limit and patch two instructions that contain the limit - is->city_limit = clamp (0, already_moved_trade_net ? lifted_city_limit : 512, cfg->city_limit); - WITH_MEM_PROTECTION (ADDR_CITY_LIM_CMP_IN_CONT_BEGIN_TURN, 6, PAGE_EXECUTE_READWRITE) { - int_to_bytes (&ADDR_CITY_LIM_CMP_IN_CONT_BEGIN_TURN[2], is->city_limit); - } - WITH_MEM_PROTECTION (ADDR_CITY_LIM_CMP_IN_CREATE_CITY, 5, PAGE_EXECUTE_READWRITE) { - int_to_bytes (&ADDR_CITY_LIM_CMP_IN_CREATE_CITY[1], is->city_limit); - } - WITH_MEM_PROTECTION (ADDR_MAX_TRADABLE_CITY_ID, 4, PAGE_EXECUTE_READWRITE) { - int_to_bytes (ADDR_MAX_TRADABLE_CITY_ID, already_moved_trade_net ? lifted_city_limit : 512); - } + return Map_calc_food_yield_at (this, __, tile_x, tile_y, tile_base_type, civ_id, imagine_fully_improved, city); +} - WITH_MEM_PROTECTION (ADDR_CULTURE_DOUBLING_TIME_CMP_INSTR, 6, PAGE_EXECUTE_READWRITE) { - byte * instr = ADDR_CULTURE_DOUBLING_TIME_CMP_INSTR; - if (cfg->years_to_double_building_culture == 1000) - restore_code_area (instr); - else { - if (instr[0] == 0x3D) { // in GOG and PCG EXEs, instr is cmp eax, 0x3E8 - save_code_area (instr, 5, false); - int_to_bytes (&instr[1], cfg->years_to_double_building_culture); - } else if (instr[0] == 0x3B) { // in Steam EXE, instr is cmp eax, dword ptr 0x688C9C - save_code_area (instr, 6, true); - instr[0] = 0x3D; - int_to_bytes (&instr[1], cfg->years_to_double_building_culture); - } +int __fastcall +patch_Map_calc_shield_yield_at (Map * this, int edx, int tile_x, int tile_y, int civ_id, City * city, int param_5, int param_6) +{ + if (! is->current_config.enable_districts) + return Map_calc_shield_yield_at (this, __, tile_x, tile_y, civ_id, city, param_5, param_6); + + Tile * tile = tile_at (tile_x, tile_y); + if ((tile != NULL) && (tile != p_null_tile)) { + struct district_instance * inst = get_district_instance (tile); + if (inst != NULL && district_is_complete (tile, inst->district_id)) { + return 0; } } - WITH_MEM_PROTECTION (ADDR_GET_PIXEL_FOR_DRAW_CITY_DOT, 5, PAGE_EXECUTE_READWRITE) { - if (cfg->accentuate_cities_on_minimap) { - save_code_area (ADDR_GET_PIXEL_FOR_DRAW_CITY_DOT, 5, false); - emit_branch (BK_JUMP, ADDR_GET_PIXEL_FOR_DRAW_CITY_DOT, ADDR_INLEAD_FOR_CITY_DOT_DRAW_PIXEL_REPL); - } else - restore_code_area (ADDR_GET_PIXEL_FOR_DRAW_CITY_DOT); + return Map_calc_shield_yield_at (this, __, tile_x, tile_y, civ_id, city, param_5, param_6); +} + +int +compute_city_tile_yield_sum (City * city, int tile_x, int tile_y) +{ + if (city == NULL) + return 0; + int food = City_calc_tile_yield_at (city, __, YK_FOOD, tile_x, tile_y); + int shields = City_calc_tile_yield_at (city, __, YK_SHIELDS, tile_x, tile_y); + int commerce = City_calc_tile_yield_at (city, __, YK_COMMERCE, tile_x, tile_y); + return food + shields + commerce; +} + +int * +get_water_continent_ids_connected_via_canal (Tile * tile, int * out_count) +{ + if (out_count != NULL) + *out_count = 0; + if (! is->current_config.enable_canal_districts) + return NULL; + if ((tile == NULL) || (tile == p_null_tile)) + return NULL; + if (! tile->vtable->m35_Check_Is_Water (tile)) + return NULL; + + int origin_continent_id = tile->vtable->m46_Get_ContinentID (tile); + int continent_count = p_bic_data->Map.Continent_Count; + if ((origin_continent_id < 0) || (origin_continent_id >= continent_count)) + return NULL; + + Map * map = &p_bic_data->Map; + int tile_count = map->TileCount; + if (tile_count <= 0) + return NULL; + + // Use a BFS over completed canal tiles, starting from those touching the origin water body. + int * queue_xs = malloc (sizeof (*queue_xs) * tile_count); + int * queue_ys = malloc (sizeof (*queue_ys) * tile_count); + int * ids = malloc (sizeof (*ids) * continent_count); + bool * seen = calloc (continent_count, sizeof (*seen)); + if ((queue_xs == NULL) || (queue_ys == NULL) || (ids == NULL) || (seen == NULL)) { + if (queue_xs != NULL) + free (queue_xs); + if (queue_ys != NULL) + free (queue_ys); + if (ids != NULL) + free (ids); + if (seen != NULL) + free (seen); + return NULL; } - WITH_MEM_PROTECTION (ADDR_INLEAD_FOR_CITY_DOT_DRAW_PIXEL_REPL, INLEAD_SIZE, PAGE_EXECUTE_READWRITE) { - byte * code = ADDR_INLEAD_FOR_CITY_DOT_DRAW_PIXEL_REPL; - code[0] = 0x55; code[1] = 0x53; // write push ebp and push ebx, two overwritten instrs before the call - emit_branch (BK_CALL, &code[2], &patch_get_pixel_to_draw_city_dot); // call patch func - emit_branch (BK_JUMP, &code[7], ADDR_GET_PIXEL_FOR_DRAW_CITY_DOT + 5); // jump back to original code + + seen[origin_continent_id] = true; + + int const adj_dx[8] = { 0, 0, -2, 2, 1, 1, -1, -1 }; + int const adj_dy[8] = { -2, 2, 0, 0, -1, 1, -1, 1 }; + int head = 0; + int tail = 0; + + // Seed queue with completed canal districts adjacent to the origin water body. + for (int index = 0; index < tile_count; index++) { + Tile * cand = Map_get_tile (map, __, index); + if ((cand == NULL) || (cand == p_null_tile)) + continue; + + int cx = 0, cy = 0; + tile_index_to_coords (map, index, &cx, &cy); + if (! tile_has_district_at (cx, cy, CANAL_DISTRICT_ID)) + continue; + + struct district_instance * inst = get_district_instance (cand); + if ((inst == NULL) || (inst->district_id != CANAL_DISTRICT_ID) || + (! district_is_complete (cand, CANAL_DISTRICT_ID))) + continue; + + bool adjacent_to_origin = false; + for (int i = 0; i < 8; i++) { + int nx = cx + adj_dx[i]; + int ny = cy + adj_dy[i]; + wrap_tile_coords (map, &nx, &ny); + Tile * adj = tile_at (nx, ny); + if ((adj == NULL) || (adj == p_null_tile)) + continue; + if (! adj->vtable->m35_Check_Is_Water (adj)) + continue; + int adj_continent_id = adj->vtable->m46_Get_ContinentID (adj); + if (adj_continent_id == origin_continent_id) { + adjacent_to_origin = true; + break; + } + } + if (! adjacent_to_origin) + continue; + + bool seen_canal = false; + for (int j = 0; j < tail; j++) { + if ((queue_xs[j] == cx) && (queue_ys[j] == cy)) { + seen_canal = true; + break; + } + } + if (! seen_canal && (tail < tile_count)) { + queue_xs[tail] = cx; + queue_ys[tail] = cy; + tail++; + } } - // Bypass adjacent resource of different type check - // replacing 0x7D (= jge) with 0xEB (= uncond. jump) - WITH_MEM_PROTECTION (ADDR_RES_CHECK_JUMP_TILE_INDEX_AT_LEAST_9, 1, PAGE_EXECUTE_READWRITE) - *(byte *)ADDR_RES_CHECK_JUMP_TILE_INDEX_AT_LEAST_9 = cfg->allow_adjacent_resources_of_different_types ? 0xEB : 0x7D; + int id_count = 0; - WITH_MEM_PROTECTION (ADDR_RESOURCE_GEN_TILE_COUNT_DIV, 7, PAGE_EXECUTE_READWRITE) { - if (cfg->tiles_per_non_luxury_resource == 32) - restore_code_area (ADDR_RESOURCE_GEN_TILE_COUNT_DIV); - else { - save_code_area (ADDR_RESOURCE_GEN_TILE_COUNT_DIV, 7, true); + while (head < tail) { + int cx = queue_xs[head]; + int cy = queue_ys[head]; + head++; - // Jump to replacement division logic, kept in an inlead because it doesn't fit in 7 bytes - emit_branch (BK_JUMP, ADDR_RESOURCE_GEN_TILE_COUNT_DIV, ADDR_RESOURCE_GEN_TILE_COUNT_DIV_REPL); + // Record all water bodies adjacent to this canal tile. + for (int i = 0; i < 8; i++) { + int nx = cx + adj_dx[i]; + int ny = cy + adj_dy[i]; + wrap_tile_coords (map, &nx, &ny); + Tile * adj = tile_at (nx, ny); + if ((adj == NULL) || (adj == p_null_tile)) + continue; + if (! adj->vtable->m35_Check_Is_Water (adj)) + continue; - // Fill in the replacement division logic. Instead of computing tile_count>>5, compute tile_count/cfg->tiles_per_non_luxury_resource. - WITH_MEM_PROTECTION (ADDR_RESOURCE_GEN_TILE_COUNT_DIV_REPL, INLEAD_SIZE, PAGE_EXECUTE_READWRITE) { - int d = not_below (1, cfg->tiles_per_non_luxury_resource); - byte d0 = d & 0xFF, - d1 = (d >> 8) & 0xFF, - d2 = (d >> 16) & 0xFF, - d3 = d >> 24; + int adj_continent_id = adj->vtable->m46_Get_ContinentID (adj); + if ((adj_continent_id < 0) || (adj_continent_id >= continent_count)) + continue; + if (! seen[adj_continent_id]) { + seen[adj_continent_id] = true; + ids[id_count] = adj_continent_id; + id_count++; + } + } - byte * cursor = ADDR_RESOURCE_GEN_TILE_COUNT_DIV_REPL; + // Traverse to neighboring completed canal districts. + for (int i = 0; i < 8; i++) { + int nx = cx + adj_dx[i]; + int ny = cy + adj_dy[i]; + wrap_tile_coords (map, &nx, &ny); + if (! tile_has_district_at (nx, ny, CANAL_DISTRICT_ID)) + continue; - // In the Steam EXE, the limit (tile_count/32 by default) must be left in ecx. For the other EXEs, edx. - if (exe_version_index == 1) { - byte new_div[] = { - 0x50, // push eax - 0x52, // push edx - 0x66, 0x8B, 0x56, 0x40, // mov dx, word ptr [esi+0x40] # Loads tile count - 0x0F, 0xB7, 0xD2, // movzx edx, dx - 0x89, 0xD0, // mov eax, edx - 0x31, 0xD2, // xor edx, edx - 0xB9, d0, d1, d2, d3, // mov ecx, {{divisor}} - 0xF7, 0xF1, // div ecx - 0x89, 0xC1, // mov ecx, eax - 0x5A, // pop edx - 0x58 // pop eax - }; - for (int n = 0; n < ARRAY_LEN (new_div); n++) - *cursor++ = new_div[n]; + Tile * adj = tile_at (nx, ny); + if ((adj == NULL) || (adj == p_null_tile)) + continue; - } else { - byte new_div[] = { - 0x50, // push eax - 0x51, // push ecx - 0x66, 0x8B, 0x56, 0x40, // mov dx, word ptr [esi+0x40] # Loads tile count - 0x0F, 0xB7, 0xD2, // movzx edx, dx - 0x89, 0xD0, // mov eax, edx - 0x31, 0xD2, // xor edx, edx - 0xB9, d0, d1, d2, d3, // mov ecx, {{divisor}} - 0xF7, 0xF1, // div ecx - 0x89, 0xC2, // mov edx, eax - 0x59, // pop ecx - 0x58 // pop eax - }; - for (int n = 0; n < ARRAY_LEN (new_div); n++) - *cursor++ = new_div[n]; - } + struct district_instance * inst = get_district_instance (adj); + if ((inst == NULL) || (inst->district_id != CANAL_DISTRICT_ID) || (! district_is_complete (adj, CANAL_DISTRICT_ID))) + continue; - cursor = emit_branch (BK_JUMP, cursor, ADDR_RESOURCE_GEN_TILE_COUNT_DIV + 7); + bool seen_canal = false; + for (int j = 0; j < tail; j++) { + if ((queue_xs[j] == nx) && (queue_ys[j] == ny)) { + seen_canal = true; + break; + } + } + if (! seen_canal && (tail < tile_count)) { + queue_xs[tail] = nx; + queue_ys[tail] = ny; + tail++; } } } - // Disable a jump that skips playing victory animations for air units if they've been configured to have a victory anim - set_nopification (cfg->aircraft_victory_animation != NULL, ADDR_SKIP_VICTORY_ANIM_IF_AIR, 6); -} + free (queue_xs); + free (queue_ys); + free (seen); -Sprite* -SpriteList_at(SpriteList *list, int i) { - return &list->field_0[i]; -} + if (out_count != NULL) + *out_count = id_count; + if (id_count <= 0) { + free (ids); + return NULL; + } -void -set_path(String260 *dst, const char *p) { - snprintf(dst->S, sizeof(dst->S), "%s", p); + return ids; } -void -slice_grid(Sprite *out, PCX_Image *img, - int tile_w, int tile_h, int full_w, int full_h) +Tile * +find_tile_for_neighborhood_district (City * city, int * out_x, int * out_y) { - for (int y = 0; y < full_h; y += tile_h) - for (int x = 0; x < full_w; x += tile_w) - Sprite_slice_pcx(out++, __, img, x, y, tile_w, tile_h, 1, 1); -} + if (city == NULL) + return NULL; -void -slice_grid_into_list(SpriteList *bucket, PCX_Image *img, - int tile_w, int tile_h, int full_w, int full_h) -{ - int k = 0; - for (int y = 0; y < full_h; y += tile_h) - for (int x = 0; x < full_w; x += tile_w) - Sprite_slice_pcx(SpriteList_at(bucket, k++), __, img, x, y, tile_w, tile_h, 1, 1); -} + int city_x = city->Body.X; + int city_y = city->Body.Y; + int city_work_radius = is->current_config.city_work_radius; -void -join_path(char *out, size_t out_sz, const char *dir, const char *file) -{ - size_t n = strlen(dir); - int need_sep = (n > 0 && dir[n-1] != '/' && dir[n-1] != '\\'); - snprintf(out, out_sz, "%s%s%s", dir, need_sep ? "\\" : "", file); -} + // Search in order: ring 1, then rings 2..N + // Ring order array: 1, 2, 3, ..., city_work_radius + int ring_order[8]; + int ring_count = 0; + for (int r = 1; r <= city_work_radius; r++) + ring_order[ring_count++] = r; -void -read_in_dir(PCX_Image *img, - const char *art_dir, - const char *filename, - String260 *store) { - char pbuf[512]; - join_path(pbuf, sizeof pbuf, art_dir, filename); - if (store) { - // assumes: typedef struct { char S[260]; } String260; - snprintf(store->S, sizeof store->S, "%s", pbuf); - } + Tile * best_tile = NULL; + int best_yield = INT_MAX; + int best_x = -1, best_y = -1; + int current_ring = -1; - char temp_path[2*MAX_PATH]; + FOR_TILE_RINGS_AROUND (tri, city_x, city_y, ring_order, ring_count) { + if (tri.current_ring != current_ring) { + if (best_tile != NULL) { + *out_x = best_x; + *out_y = best_y; + return best_tile; + } + current_ring = tri.current_ring; + best_tile = NULL; + best_yield = INT_MAX; + } - snprintf(temp_path, sizeof temp_path, "%s\\%s", art_dir, filename); - PCX_Image_read_file(img, __, temp_path, NULL, 0, 0x100, 2); + Tile * tile = tri.tile; + if (is->current_config.enable_distribution_hub_districts) { + int covered = itable_look_up_or (&is->distribution_hub_coverage_counts, (int)tile, 0); + if (covered > 0) + continue; + } + + if (! tile_suitable_for_district (tile, NEIGHBORHOOD_DISTRICT_ID, city, NULL)) + continue; + if (tile_has_resource (tile)) + continue; + if (get_district_instance (tile) != NULL && + ! tile_has_obsolete_district_for_civ (tile, city->Body.CivID)) + continue; + + int yield = compute_city_tile_yield_sum (city, tri.tile_x, tri.tile_y); + if (yield < best_yield) { + best_yield = yield; + best_tile = tile; + best_x = tri.tile_x; + best_y = tri.tile_y; + } + } + + if (best_tile != NULL) { + *out_x = best_x; + *out_y = best_y; + return best_tile; + } + + return NULL; } -bool load_day_night_hour_images(struct day_night_cycle_img_set *this, const char *art_dir, const char *hour) +Tile * +find_tile_for_port_district (City * city, int * out_x, int * out_y) { - char ss[200]; - PCX_Image img; - PCX_Image_construct(&img); + if (city == NULL) + return NULL; - // Std terrain (9 sheets): 6x9 of 128x64 over 0x480x0x240 - const char *STD_SHEETS[9] = { - "xtgc.pcx", "xpgc.pcx", "xdgc.pcx", "xdpc.pcx", "xdgp.pcx", "xggc.pcx", - "wCSO.pcx", "wSSS.pcx", "wOOO.pcx" - }; - for (int i = 0; i < 9; ++i) { - read_in_dir(&img, art_dir, STD_SHEETS[i], NULL); - if (img.JGL.Image == NULL) return false; - slice_grid_into_list(&this->Std_Terrain_Images[i], &img, 0x80, 0x40, 0x480, 0x240); - } - - // LM terrain (9): same slicing - const char *LMT_SHEETS[9] = { - "lxtgc.pcx", "lxpgc.pcx", "lxdgc.pcx", "lxdpc.pcx", "lxdgp.pcx", "lxggc.pcx", - "lwCSO.pcx", "lwSSS.pcx", "lwOOO.pcx" - }; - for (int i = 0; i < 9; ++i) { - read_in_dir(&img, art_dir, LMT_SHEETS[i], NULL); - if (img.JGL.Image == NULL) return false; - slice_grid_into_list(&this->LM_Terrain_Images[i], &img, 0x80, 0x40, 0x480, 0x240); - } + int city_x = city->Body.X; + int city_y = city->Body.Y; + int city_work_radius = is->current_config.city_work_radius; + int threshold_for_body_of_water = 21; // Mimic vanilla behavior so AI doesn't build ports in small lakes - // Polar icecaps: 8x4 of 128x64 - read_in_dir(&img, art_dir, "polarICEcaps-final.pcx", NULL); - if (img.JGL.Image == NULL) return false; - slice_grid(this->Polar_Icecaps_Images, &img, 0x80, 0x40, 0x400, 0x100); + // Search in order: ring 1, then rings 2..N + int ring_order[8]; + int ring_count = 0; + for (int r = 1; r <= city_work_radius; r++) + ring_order[ring_count++] = r; - // Hills / LM Hills: 4x3 of 128x72 - read_in_dir(&img, art_dir, "xhills.pcx", NULL); - if (img.JGL.Image == NULL) return false; - slice_grid(this->Hills_Images, &img, 0x80, 0x48, 0x200, 0x120); - read_in_dir(&img, art_dir, "hill forests.pcx", NULL); - if (img.JGL.Image == NULL) return false; - slice_grid(this->Hills_Forests_Images, &img, 0x80, 0x48, 0x200, 0x120); - read_in_dir(&img, art_dir, "hill jungle.pcx", NULL); - if (img.JGL.Image == NULL) return false; - slice_grid(this->Hills_Jungle_Images, &img, 0x80, 0x48, 0x200, 0x120); - read_in_dir(&img, art_dir, "LMHills.pcx", NULL); - if (img.JGL.Image == NULL) return false; - slice_grid(this->LM_Hills_Images, &img, 0x80, 0x48, 0x200, 0x120); + Tile * best_tile = NULL; + int best_yield = INT_MAX; + int best_x = -1, best_y = -1; + int current_ring = -1; - // Flood plains: 4x4 of 128x64 - read_in_dir(&img, art_dir, "floodplains.pcx", NULL); - if (img.JGL.Image == NULL) return false; - slice_grid(this->Flood_Plains_Images, &img, 0x80, 0x40, 0x200, 0x100); + FOR_TILE_RINGS_AROUND (tri, city_x, city_y, ring_order, ring_count) { + if (tri.current_ring != current_ring) { + if (best_tile != NULL) { + *out_x = best_x; + *out_y = best_y; + return best_tile; + } + current_ring = tri.current_ring; + best_tile = NULL; + best_yield = INT_MAX; + } - // Delta + Mountain rivers: 4x4 each, interleaved across one contiguous block - { - const char *RIV_SHEETS[2] = { "deltaRivers.pcx", "mtnRivers.pcx" }; - Sprite *contig = this->Delta_Rivers_Images; // Mountain_Rivers_Images follows - for (int s = 0; s < 2; ++s) { - read_in_dir(&img, art_dir, RIV_SHEETS[s], NULL); - if (img.JGL.Image == NULL) return false; - Sprite *p = contig + s; // even=delta, odd=mountain - for (int y = 0; y < 0x100; y += 0x40) - for (int x = 0; x < 0x200; x += 0x80) { - Sprite_slice_pcx(p, __, &img, x, y, 0x80, 0x40, 1, 1); - p += 2; - } - } - } + Tile * tile = tri.tile; + if (is->current_config.enable_distribution_hub_districts) { + int covered = itable_look_up_or (&is->distribution_hub_coverage_counts, (int)tile, 0); + if (covered > 0) + continue; + } - // Waterfalls: 4x1 of 128x64 - read_in_dir(&img, art_dir, "waterfalls.pcx", NULL); - if (img.JGL.Image == NULL) return false; - slice_grid(this->Waterfalls_Images, &img, 0x80, 0x40, 0x200, 0x40); + if (! tile_suitable_for_district (tile, PORT_DISTRICT_ID, city, NULL)) + continue; + if (tile_has_resource (tile)) + continue; + if (get_district_instance (tile) != NULL && + ! tile_has_obsolete_district_for_civ (tile, city->Body.CivID)) + continue; + if (! tile->vtable->m35_Check_Is_Water (tile)) + continue; - // Irrigation (desert/plains/normal/tundra): each 4x4 of 128x64 - read_in_dir(&img, art_dir, "irrigation DESETT.pcx", NULL); - if (img.JGL.Image == NULL) return false; - slice_grid(this->Irrigation_Desert_Images, &img, 0x80, 0x40, 0x200, 0x100); - read_in_dir(&img, art_dir, "irrigation PLAINS.pcx", NULL); - if (img.JGL.Image == NULL) return false; - slice_grid(this->Irrigation_Plains_Images, &img, 0x80, 0x40, 0x200, 0x100); - read_in_dir(&img, art_dir, "irrigation.pcx", NULL); - if (img.JGL.Image == NULL) return false; - slice_grid(this->Irrigation_Images, &img, 0x80, 0x40, 0x200, 0x100); - read_in_dir(&img, art_dir, "irrigation TUNDRA.pcx", NULL); - if (img.JGL.Image == NULL) return false; - slice_grid(this->Irrigation_Tundra_Images, &img, 0x80, 0x40, 0x200, 0x100); + int continent_id = tile->vtable->m46_Get_ContinentID (tile); + if ((continent_id < 0) || (continent_id >= p_bic_data->Map.Continent_Count)) + continue; + Continent * continent = &p_bic_data->Map.Continents[continent_id]; + bool large_enough_body = (continent->Body.TileCount >= threshold_for_body_of_water); + if (! large_enough_body) { + int connected_count = 0; + int * connected_ids = get_water_continent_ids_connected_via_canal (tile, &connected_count); + for (int i = 0; i < connected_count; i++) { + int connected_id = connected_ids[i]; + if ((connected_id < 0) || (connected_id >= p_bic_data->Map.Continent_Count)) + continue; + Continent * connected_continent = &p_bic_data->Map.Continents[connected_id]; + if (connected_continent->Body.TileCount >= threshold_for_body_of_water) { + large_enough_body = true; + break; + } + } + if (connected_ids != NULL) + free (connected_ids); + } + if (! large_enough_body) + continue; - // Volcanos (plain/forests/jungles/snow): 4x4 of 128x88 - read_in_dir(&img, art_dir, "Volcanos.pcx", NULL); - if (img.JGL.Image == NULL) return false; - slice_grid(this->Volcanos_Images, &img, 0x80, 0x58, 0x200, 0x160); - read_in_dir(&img, art_dir, "Volcanos forests.pcx", NULL); - if (img.JGL.Image == NULL) return false; - slice_grid(this->Volcanos_Forests_Images, &img, 0x80, 0x58, 0x200, 0x160); - read_in_dir(&img, art_dir, "Volcanos jungles.pcx", NULL); - if (img.JGL.Image == NULL) return false; - slice_grid(this->Volcanos_Jungles_Images, &img, 0x80, 0x58, 0x200, 0x160); - read_in_dir(&img, art_dir, "Volcanos-snow.pcx", NULL); - if (img.JGL.Image == NULL) return false; - slice_grid(this->Volcanos_Snow_Images, &img, 0x80, 0x58, 0x200, 0x160); + int yield = compute_city_tile_yield_sum (city, tri.tile_x, tri.tile_y); + if (yield < best_yield) { + best_yield = yield; + best_tile = tile; + best_x = tri.tile_x; + best_y = tri.tile_y; + } + } - // Marsh: Large band then Small band (tiles 128x88) - read_in_dir(&img, art_dir, "marsh.pcx", NULL); - if (img.JGL.Image == NULL) return false; - // Large (2 rows, 4 cols) - { int k=0; for (int y=0; y<0xb0; y+=0x58) for (int x=0; x<0x200; x+=0x80) - Sprite_slice_pcx(&this->Marsh_Large[k++], __, &img, x, y, 0x80, 0x58, 1, 1); } - // Small (2 rows, 5 cols) - { int k=0; for (int y=0xb0; y<0x160; y+=0x58) for (int x=0; x<0x280; x+=0x80) - Sprite_slice_pcx(&this->Marsh_Small[k++], __, &img, x, y, 0x80, 0x58, 1, 1); } + if (best_tile != NULL) { + *out_x = best_x; + *out_y = best_y; + return best_tile; + } - // LM mountains + standard mountains (plain/forests/jungles/snow): 4x4 of 128x88 - read_in_dir(&img, art_dir, "LMMountains.pcx", NULL); - if (img.JGL.Image == NULL) return false; - slice_grid(this->LM_Mountains_Images, &img, 0x80, 0x58, 0x200, 0x160); - read_in_dir(&img, art_dir, "Mountains.pcx", NULL); - if (img.JGL.Image == NULL) return false; - slice_grid(this->Mountains_Images, &img, 0x80, 0x58, 0x200, 0x160); - read_in_dir(&img, art_dir, "mountain forests.pcx", NULL); - if (img.JGL.Image == NULL) return false; - slice_grid(this->Mountains_Forests_Images, &img, 0x80, 0x58, 0x200, 0x160); - read_in_dir(&img, art_dir, "mountain jungles.pcx", NULL); - if (img.JGL.Image == NULL) return false; - slice_grid(this->Mountains_Jungles_Images, &img, 0x80, 0x58, 0x200, 0x160); - read_in_dir(&img, art_dir, "Mountains-snow.pcx", NULL); - if (img.JGL.Image == NULL) return false; - slice_grid(this->Mountains_Snow_Images, &img, 0x80, 0x58, 0x200, 0x160); + return NULL; +} - // Roads (16x16) and Railroads (16x17), tiles 128x64 - read_in_dir(&img, art_dir, "roads.pcx", NULL); - if (img.JGL.Image == NULL) return false; - slice_grid(this->Roads_Images, &img, 0x80, 0x40, 0x800, 0x400); - read_in_dir(&img, art_dir, "railroads.pcx", NULL); - if (img.JGL.Image == NULL) return false; - slice_grid(this->Railroads_Images, &img, 0x80, 0x40, 0x800, 0x440); +Tile * +find_tile_for_wonder_district (City * city, int * out_x, int * out_y) +{ + if (city == NULL) + return NULL; - // LM Forests (Large 2x4, Small 2x6, Pines 2x6), tiles 128x88 - read_in_dir(&img, art_dir, "LMForests.pcx", NULL); - if (img.JGL.Image == NULL) return false; - { int k=0; for (int y=0; y<0xb0; y+=0x58) for (int x=0; x<0x200; x+=0x80) - Sprite_slice_pcx(&this->LM_Forests_Large_Images[k++], __, &img, x, y, 0x80, 0x58, 1, 1); } - { int k=0; for (int y=0xb0; y<0x160; y+=0x58) for (int x=0; x<0x300; x+=0x80) - Sprite_slice_pcx(&this->LM_Forests_Small_Images[k++], __, &img, x, y, 0x80, 0x58, 1, 1); } - { int k=0; for (int y=0x160; y<0x210; y+=0x58) for (int x=0; x<0x300; x+=0x80) - Sprite_slice_pcx(&this->LM_Forests_Pines_Images[k++], __, &img, x, y, 0x80, 0x58, 1, 1); } + int target_improv_id = -1; + if (lookup_pending_building_order (city, &target_improv_id)) { + if ((target_improv_id < 0) || + (target_improv_id >= p_bic_data->ImprovementsCount) || + ((p_bic_data->Improvements[target_improv_id].Characteristics & (ITC_Wonder | ITC_Small_Wonder)) == 0)) + target_improv_id = -1; + } + if (target_improv_id < 0) { + int order_id = city->Body.Order_ID; + if ((city->Body.Order_Type == COT_Improvement) && + (order_id >= 0) && + (order_id < p_bic_data->ImprovementsCount)) { + Improvement * order = &p_bic_data->Improvements[order_id]; + if (order->Characteristics & (ITC_Wonder | ITC_Small_Wonder)) + target_improv_id = order_id; + } + } - // Grassland/Plains/Tundra forests & jungles (bands; tiles 128x88) — order is important - read_in_dir(&img, art_dir, "grassland forests.pcx", NULL); - if (img.JGL.Image == NULL) return false; - // Jungles Large, Small - { int k=0; for (int y=0; y<0xb0; y+=0x58) for (int x=0; x<0x200; x+=0x80) - Sprite_slice_pcx(&this->Grassland_Jungles_Large[k++], __, &img, x, y, 0x80, 0x58, 1, 1); } - { int k=0; for (int y=0xb0; y<0x160; y+=0x58) for (int x=0; x<0x300; x+=0x80) - Sprite_slice_pcx(&this->Grassland_Jungles_Small[k++], __, &img, x, y, 0x80, 0x58, 1, 1); } - // Forests Large, Small, Pines - { int k=0; for (int y=0x160; y<0x210; y+=0x58) for (int x=0; x<0x200; x+=0x80) - Sprite_slice_pcx(&this->Grassland_Forests_Large[k++], __, &img, x, y, 0x80, 0x58, 1, 1); } - { int k=0; for (int y=0x210; y<0x2c0; y+=0x58) for (int x=0; x<0x280; x+=0x80) - Sprite_slice_pcx(&this->Grassland_Forests_Small[k++], __, &img, x, y, 0x80, 0x58, 1, 1); } - { int k=0; for (int y=0x2c0; y<0x370; y+=0x58) for (int x=0; x<0x300; x+=0x80) - Sprite_slice_pcx(&this->Grassland_Forests_Pines[k++], __, &img, x, y, 0x80, 0x58, 1, 1); } + int city_x = city->Body.X; + int city_y = city->Body.Y; + int city_work_radius = is->current_config.city_work_radius; - read_in_dir(&img, art_dir, "plains forests.pcx", NULL); - if (img.JGL.Image == NULL) return false; - { int k=0; for (int y=0x160; y<0x210; y+=0x58) for (int x=0; x<0x200; x+=0x80) - Sprite_slice_pcx(&this->Plains_Forests_Large[k++], __, &img, x, y, 0x80, 0x58, 1, 1); } - { int k=0; for (int y=0x210; y<0x2c0; y+=0x58) for (int x=0; x<0x280; x+=0x80) - Sprite_slice_pcx(&this->Plains_Forests_Small[k++], __, &img, x, y, 0x80, 0x58, 1, 1); } - { int k=0; for (int y=0x2c0; y<0x370; y+=0x58) for (int x=0; x<0x300; x+=0x80) - Sprite_slice_pcx(&this->Plains_Forests_Pines[k++], __, &img, x, y, 0x80, 0x58, 1, 1); } + // Search in order: ring 2, then rings 3..N, then ring 1 as last resort + int ring_order[8]; + int ring_count = 0; + ring_order[ring_count++] = 2; + for (int r = 3; r <= city_work_radius; r++) + ring_order[ring_count++] = r; + ring_order[ring_count++] = 1; - read_in_dir(&img, art_dir, "tundra forests.pcx", NULL); - if (img.JGL.Image == NULL) return false; - { int k=0; for (int y=0x160; y<0x210; y+=0x58) for (int x=0; x<0x200; x+=0x80) - Sprite_slice_pcx(&this->Tundra_Forests_Large[k++], __, &img, x, y, 0x80, 0x58, 1, 1); } - { int k=0; for (int y=0x210; y<0x2c0; y+=0x58) for (int x=0; x<0x280; x+=0x80) - Sprite_slice_pcx(&this->Tundra_Forests_Small[k++], __, &img, x, y, 0x80, 0x58, 1, 1); } - { int k=0; for (int y=0x2c0; y<0x370; y+=0x58) for (int x=0; x<0x300; x+=0x80) - Sprite_slice_pcx(&this->Tundra_Forests_Pines[k++], __, &img, x, y, 0x80, 0x58, 1, 1); } + Tile * best_tile = NULL; + int best_yield = INT_MAX; + int best_x = -1, best_y = -1; + int current_ring = -1; - // LM Terrain (7 single 128x64, vertical strip) - read_in_dir(&img, art_dir, "landmark_terrain.pcx", NULL); - if (img.JGL.Image == NULL) return false; - for (int i = 0, y = 0; i < 7; ++i, y += 0x40) - Sprite_slice_pcx(&this->LM_Terrain[i], __, &img, 0, y, 0x80, 0x40, 1, 1); + FOR_TILE_RINGS_AROUND (tri, city_x, city_y, ring_order, ring_count) { + if (tri.current_ring != current_ring) { + if (best_tile != NULL) { + *out_x = best_x; + *out_y = best_y; + return best_tile; + } + current_ring = tri.current_ring; + best_tile = NULL; + best_yield = INT_MAX; + } - // TNT (same odd ordering as original) - read_in_dir(&img, art_dir, "tnt.pcx", NULL); - if (img.JGL.Image == NULL) return false; - for (int i=0, x=0; i<3; ++i, x+=0x80) Sprite_slice_pcx(&this->Tnt_Images[6+i], __, &img, x, 0x00, 0x80, 0x40, 1, 1); - for (int i=0, x=0; i<3; ++i, x+=0x80) Sprite_slice_pcx(&this->Tnt_Images[9+i], __, &img, x, 0x40, 0x80, 0x40, 1, 1); - for (int i=0, x=0; i<3; ++i, x+=0x80) Sprite_slice_pcx(&this->Tnt_Images[12+i], __, &img, x, 0x80, 0x80, 0x40, 1, 1); - for (int i=0, x=0; i<3; ++i, x+=0x80) Sprite_slice_pcx(&this->Tnt_Images[0+i], __, &img, x, 0xC0, 0x80, 0x40, 1, 1); - for (int i=0, x=0; i<3; ++i, x+=0x80) Sprite_slice_pcx(&this->Tnt_Images[15+i], __, &img, x, 0x100, 0x80, 0x40, 1, 1); - for (int i=0, x=0; i<3; ++i, x+=0x80) Sprite_slice_pcx(&this->Tnt_Images[3+i], __, &img, x, 0x140, 0x80, 0x40, 1, 1); + Tile * tile = tri.tile; + if (! tile_suitable_for_district (tile, WONDER_DISTRICT_ID, city, NULL)) + continue; + if (tile_has_resource (tile)) + continue; + if (! wonder_is_buildable_on_tile (tile, target_improv_id)) + continue; - // Goody huts: 8 tiles, x=(i%3)*0x80, y=(i/3)*0x40 - read_in_dir(&img, art_dir, "goodyhuts.pcx", NULL); - if (img.JGL.Image == NULL) return false; - for (int i = 0; i < 8; ++i) { - int x = (i % 3) << 7; - int y = (i / 3) << 6; - Sprite_slice_pcx(&this->Goody_Huts_Images[i], __, &img, x, y, 0x80, 0x40, 1, 1); - } + int yield = compute_city_tile_yield_sum (city, tri.tile_x, tri.tile_y); + if (yield < best_yield && (! is_tile_earmarked_for_district (tri.tile_x, tri.tile_y))) { + best_yield = yield; + best_tile = tile; + best_x = tri.tile_x; + best_y = tri.tile_y; + } + } - // Terrain buildings (fortress/camp/barbarian camp/mines/barricade) - read_in_dir(&img, art_dir, "TerrainBuildings.pcx", NULL); - if (img.JGL.Image == NULL) return false; - for (int i=0, y=0; i<4; ++i, y+=0x40) Sprite_slice_pcx(&this->Terrain_Buldings_Fortress[i], __, &img, 0x00, y, 0x80, 0x40, 1, 1); - for (int i=0, y=0; i<4; ++i, y+=0x40) Sprite_slice_pcx(&this->Terrain_Buldings_Camp[i], __, &img, 0x80, y, 0x80, 0x40, 1, 1); - Sprite_slice_pcx(&this->Terrain_Buldings_Barbarian_Camp, __, &img, 0x100, 0x00, 0x80, 0x40, 1, 1); - Sprite_slice_pcx(&this->Terrain_Buldings_Mines, __, &img, 0x100, 0x40, 0x80, 0x40, 1, 1); - for (int i=0, y=0; i<4; ++i, y+=0x40) Sprite_slice_pcx(&this->Terrain_Buldings_Barricade[i], __, &img, 0x180, y, 0x80, 0x40, 1, 1); + if (best_tile != NULL) { + *out_x = best_x; + *out_y = best_y; + return best_tile; + } - // Pollution & Craters (5x5 of 128x64) - read_in_dir(&img, art_dir, "pollution.pcx", NULL); - if (img.JGL.Image == NULL) return false; - slice_grid(this->Pollution, &img, 0x80, 0x40, 0x280, 0x140); - read_in_dir(&img, art_dir, "craters.pcx", NULL); - if (img.JGL.Image == NULL) return false; - slice_grid(this->Craters, &img, 0x80, 0x40, 0x280, 0x140); + return NULL; +} - // Airfields / Outposts / Radar - read_in_dir(&img, art_dir, "x_airfields and detect.pcx", NULL); - if (img.JGL.Image == NULL) return false; - for (int i=0, x=0; i<2; ++i, x+=0x80) Sprite_slice_pcx(&this->Terrain_Buldings_Airfields[i], __, &img, x, 0x00, 0x80, 0x40, 1, 1); - for (int i=0, x=0; i<3; ++i, x+=0x80) Sprite_slice_pcx(&this->Terrain_Buldings_Outposts[i], __, &img, x, 0x40, 0x80, 0x80, 1, 1); - Sprite_slice_pcx(&this->Terrain_Buldings_Radar, __, &img, 0x00, 0xC0, 0x80, 0x80, 1, 1); +Tile * +find_tile_for_distribution_hub_district (City * city, int * out_x, int * out_y) +{ + if (city == NULL) + return NULL; - // Victory (single 128x64) - read_in_dir(&img, art_dir, "x_victory.pcx", NULL); - if (img.JGL.Image == NULL) return false; - Sprite_slice_pcx(&this->Victory_Image, __, &img, 0, 0, 0x80, 0x40, 1, 1); + const int resource_penalty = 100; + const int yield_weight = 40; + const int city_distance_weight = 8; + const int capital_distance_weight = 45; + const int desired_min_capital_distance = 8; + const int proximity_penalty_scale = 300; + const int different_continent_bonus = 500; - // Resources - read_in_dir(&img, art_dir, "resources.pcx", NULL); - if (img.JGL.Image == NULL) return false; - size_t k = 0; - for (int r = 0, y = 1; r < 6; ++r, y += 50) { - for (int c = 0, x = 1; c < 6; ++c, x += 50) { - Sprite_slice_pcx(&this->Resources[k++], __, &img, x, y, 49, 49, 1, 1); - } - } + Tile * best_tile = NULL; + int best_score = INT_MIN; + int best_adjusted_yield = INT_MIN; + int best_distance = -1; + int best_distance_to_capital = -1; + bool best_has_resource = true; - // Base cities - static const char *CITY_BASE[5] = { - "rAMER.pcx", "rEURO.pcx", "rROMAN.pcx", "rMIDEAST.pcx", "rASIAN.pcx" - }; - for (int culture = 0; culture < 5; culture++) { - read_in_dir(&img, art_dir, CITY_BASE[culture], NULL); - if (img.JGL.Image == NULL) return false; - int y = 0; - for (int era = 0; era < 4; ++era, y += 95) { - int x = 0; - for (int size = 0; size < 3; ++size, x += 167) { - const int idx = culture + 5*era + 20*size; - Sprite_slice_pcx(&this->City_Images[idx], __, &img, x, y, 167, 95, 1, 1); - } - } - } - - // Walled cities - static const char *CITY_WALL[5] = { - "AMERWALL.pcx", "EUROWALL.pcx", "ROMANWALL.pcx", "MIDEASTWALL.pcx", "ASIANWALL.pcx" - }; - for (int culture = 0; culture < 5; ++culture) { - read_in_dir(&img, art_dir, CITY_WALL[culture], NULL); - if (img.JGL.Image == NULL) return false; - int y = 0; - for (int era = 0; era < 4; ++era, y += 95) { - const int size = 3; // walled towns are a special category - const int idx = culture + 5*era + 20*size; - Sprite_slice_pcx(&this->City_Images[idx], __, &img, 0, y, 167, 95, 1, 1); - } + int civ_id = city->Body.CivID; + bool has_capital = false; + int capital_x = 0; + int capital_y = 0; + int capital_continent_id = -1; + + City * capital = get_city_ptr (leaders[civ_id].CapitalID); + if (capital != NULL) { + has_capital = true; + capital_x = capital->Body.X; + capital_y = capital->Body.Y; + Tile * capital_tile = tile_at (capital_x, capital_y); + if ((capital_tile != NULL) && (capital_tile != p_null_tile)) + capital_continent_id = capital_tile->vtable->m46_Get_ContinentID (capital_tile); } - // Destroyed cities - read_in_dir(&img, art_dir, "DESTROY.pcx", NULL); - if (img.JGL.Image == NULL) return false; - int x = 0; - for (int i = 0; i < 3; ++i, x += 167) { - Sprite_slice_pcx(&this->Destroyed_City_Images[i], __, &img, x, 0, 167, 95, 1, 1); - } - - // Districts (if enabled) - if (is->current_config.enable_districts) { - char districts_hour_dir[200]; - snprintf (districts_hour_dir, sizeof districts_hour_dir, "%s\\Art\\Districts\\%s", is->mod_rel_dir, hour); - for (int dc = 0; dc < is->district_count; dc++) { - struct district_config const * cfg = &is->district_configs[dc]; - int variant_capacity = ARRAY_LEN (this->District_Images[dc]); - int variant_count = cfg->img_path_count; - if (variant_count <= 0) - continue; - if (variant_count > variant_capacity) - variant_count = variant_capacity; - - int era_count = cfg->vary_img_by_era ? 4 : 1; - int column_count = cfg->max_building_index + 1; - - for (int variant_i = 0; variant_i < variant_count; variant_i++) { - const char * img_path = cfg->img_paths[variant_i]; - if ((img_path == NULL) || (img_path[0] == '\0')) - continue; + FOR_WORK_AREA_AROUND (wai, city->Body.X, city->Body.Y) { + int tx = wai.tile_x, ty = wai.tile_y; + Tile * tile = wai.tile; + bool has_resource; + if (! tile_suitable_for_district (tile, DISTRIBUTION_HUB_DISTRICT_ID, city, &has_resource)) + continue; + if (has_resource) + continue; - read_in_dir (&img, districts_hour_dir, img_path, NULL); - if (img.JGL.Image == NULL) - return false; + int chebyshev = compute_wrapped_chebyshev_distance (tx, ty, city->Body.X, city->Body.Y); + if (chebyshev <= 1) + continue; - for (int era = 0; era < era_count; era++) { - for (int col = 0; col < column_count; col++) { - int tile_x = 128 * col; - int tile_y = 64 * era; - Sprite_slice_pcx (&this->District_Images[dc][variant_i][era][col], __, &img, tile_x, tile_y, 128, 64, 1, 1); - } + bool too_close_to_existing_hub_or_city = false; + for (int m = 0; m < workable_tile_counts[2]; m++) { + int ndx, ndy; + patch_ni_to_diff_for_work_area (m, &ndx, &ndy); + int nx = tx + ndx, ny = ty + ndy; + wrap_tile_coords (&p_bic_data->Map, &nx, &ny); + Tile * nearby_tile = tile_at (nx, ny); + if ((nearby_tile != NULL) && (nearby_tile != p_null_tile)) { + struct district_instance * nearby_inst = get_district_instance (nearby_tile); + if ((nearby_inst != NULL) && + (nearby_inst->district_id == DISTRIBUTION_HUB_DISTRICT_ID) && + district_is_complete (nearby_tile, DISTRIBUTION_HUB_DISTRICT_ID)) { + too_close_to_existing_hub_or_city = true; + break; + } + else if (nearby_tile->CityID >= 0) { + too_close_to_existing_hub_or_city = true; + break; } } } + if (too_close_to_existing_hub_or_city) + continue; - // Load wonder district images (dynamically per wonder) for this hour - if (is->current_config.enable_wonder_districts) { - char const * last_img_path = NULL; - PCX_Image wpcx; - PCX_Image_construct (&wpcx); - bool pcx_loaded = false; - - for (int wi = 0; wi < is->wonder_district_count; wi++) { - char const * img_path = is->wonder_district_configs[wi].img_path; - if (img_path == NULL) - img_path = "Wonders.pcx"; + int raw_yield = compute_city_tile_yield_sum (city, tx, ty); + int adjusted_yield = raw_yield - (has_resource ? resource_penalty : 0); + int distance = compute_wrapped_manhattan_distance (tx, ty, city->Body.X, city->Body.Y); + int distance_to_capital = has_capital + ? compute_wrapped_manhattan_distance (tx, ty, capital_x, capital_y) + : 0; - // Load new image file if different from previous - if ((last_img_path == NULL) || (strcmp (img_path, last_img_path) != 0)) { - if (pcx_loaded) - wpcx.vtable->clear_JGL (&wpcx); + int proximity_penalty = 0; + if (has_capital && (distance_to_capital < desired_min_capital_distance)) + proximity_penalty = (desired_min_capital_distance - distance_to_capital) * proximity_penalty_scale; - read_in_dir (&wpcx, districts_hour_dir, img_path, NULL); + int continent_bonus = 0; + if ((capital_continent_id >= 0) && (tile != NULL) && (tile != p_null_tile)) { + int tile_continent_id = tile->vtable->m46_Get_ContinentID (tile); + if ((tile_continent_id >= 0) && (tile_continent_id != capital_continent_id)) + continent_bonus = different_continent_bonus; + } - if (wpcx.JGL.Image == NULL) { - pcx_loaded = false; - continue; - } + int score = + adjusted_yield * yield_weight + + distance * city_distance_weight + + distance_to_capital * capital_distance_weight - + proximity_penalty + + continent_bonus; - pcx_loaded = true; - last_img_path = img_path; - } + if ((score > best_score) || + ((score == best_score) && (distance_to_capital > best_distance_to_capital)) || + ((score == best_score) && (distance_to_capital == best_distance_to_capital) && (adjusted_yield > best_adjusted_yield)) || + ((score == best_score) && (distance_to_capital == best_distance_to_capital) && (adjusted_yield == best_adjusted_yield) && (distance > best_distance)) || + ((score == best_score) && (distance_to_capital == best_distance_to_capital) && (adjusted_yield == best_adjusted_yield) && (distance == best_distance) && (! has_resource && best_has_resource))) { + best_tile = tile; + best_score = score; + best_adjusted_yield = adjusted_yield; + best_distance = distance; + best_distance_to_capital = distance_to_capital; + best_has_resource = has_resource; + *out_x = tx; + *out_y = ty; + } + } - if (! pcx_loaded) - continue; + return best_tile; +} - Sprite_construct (&this->Wonder_District_Images[wi].img); - int x = 128 * is->wonder_district_configs[wi].img_column; - int y = 64 * is->wonder_district_configs[wi].img_row; - Sprite_slice_pcx (&this->Wonder_District_Images[wi].img, __, &wpcx, x, y, 128, 64, 1, 1); +bool +city_can_build_district (City * city, int district_id) +{ + if ((city == NULL) || (district_id < 0) || (district_id >= is->district_count)) + return false; - Sprite_construct (&this->Wonder_District_Images[wi].construct_img); - int cx = 128 * is->wonder_district_configs[wi].img_construct_column; - int cy = 64 * is->wonder_district_configs[wi].img_construct_row; - Sprite_slice_pcx (&this->Wonder_District_Images[wi].construct_img, __, &wpcx, cx, cy, 128, 64, 1, 1); - } + struct district_config const * cfg = &is->district_configs[district_id]; + struct district_infos const * info = &is->district_infos[district_id]; - if (pcx_loaded) - wpcx.vtable->clear_JGL (&wpcx); - wpcx.vtable->destruct (&wpcx, __, 0); - } + if (! leader_can_build_district (&leaders[city->Body.CivID], district_id)) + return false; + // Check resource prerequisites (city connection) - ALL must be present + for (int i = 0; i < info->resource_prereq_count; i++) { + int resource_req = info->resource_prereq_ids[i]; + if ((resource_req >= 0) && ! patch_City_has_resource (city, __, resource_req)) + return false; } - if (is->current_config.enable_natural_wonders && (is->natural_wonder_count > 0)) { - char natural_dir[200]; - snprintf (natural_dir, sizeof natural_dir, "%s\\Art\\Districts\\%s", is->mod_rel_dir, hour); + // Check if district does not allow multiple and city already has one + if (! cfg->allow_multiple && city_has_required_district (city, district_id)) + return false; - char const * last_img_path = NULL; - PCX_Image nwpcx; - PCX_Image_construct (&nwpcx); - bool pcx_loaded = false; + return true; +} - for (int ni = 0; ni < is->natural_wonder_count; ni++) { - struct natural_wonder_district_config const * cfg = &is->natural_wonder_configs[ni]; - if ((cfg->img_path == NULL) || (cfg->img_path[0] == '\0')) - continue; +bool +leader_has_wonder_prereq (Leader * leader, struct district_infos const * info) +{ + if (info == NULL) + return false; + if (info->wonder_prereq_count <= 0) + return true; + if ((leader == NULL) || (leader->Improvement_Counts == NULL)) + return false; - char const * img_path = cfg->img_path; - if ((last_img_path == NULL) || (strcmp (img_path, last_img_path) != 0)) { - if (pcx_loaded) - nwpcx.vtable->clear_JGL (&nwpcx); + for (int i = 0; i < info->wonder_prereq_count; i++) { + int improv_id = info->wonder_prereq_ids[i]; + if (improv_id >= 0 && leader->Improvement_Counts[improv_id] > 0) + return true; + } - read_in_dir (&nwpcx, natural_dir, img_path, NULL); + return false; +} - if (nwpcx.JGL.Image == NULL) { - pcx_loaded = false; - continue; - } +bool +leader_has_natural_wonder_prereq_in_territory (int civ_id, struct district_infos const * info) +{ + if (info == NULL) + return false; + if (info->natural_wonder_prereq_count <= 0) + return true; + if (! is->current_config.enable_natural_wonders) + return false; - pcx_loaded = true; - last_img_path = img_path; - } + FOR_TABLE_ENTRIES (tei, &is->district_tile_map) { + struct district_instance * inst = (struct district_instance *)tei.value; + if ((inst == NULL) || (inst->district_id != NATURAL_WONDER_DISTRICT_ID)) + continue; - if (! pcx_loaded) - continue; + int wonder_id = inst->natural_wonder_info.natural_wonder_id; + if (wonder_id < 0) + continue; - Sprite_construct (&this->Natural_Wonder_Images[ni].img); - int x = 128 * cfg->img_column; - int y = 88 * cfg->img_row; - Sprite_slice_pcx (&this->Natural_Wonder_Images[ni].img, __, &nwpcx, x, y, 128, 88, 1, 1); + bool matches = false; + for (int i = 0; i < info->natural_wonder_prereq_count; i++) { + if (info->natural_wonder_prereq_ids[i] == wonder_id) { + matches = true; + break; + } } + if (! matches) + continue; - if (pcx_loaded) - nwpcx.vtable->clear_JGL (&nwpcx); - nwpcx.vtable->destruct (&nwpcx, __, 0); - } - - img.vtable->destruct (&img, __, 0); + Tile * tile = (Tile *)tei.key; + if ((tile == NULL) || (tile == p_null_tile)) + continue; - return true; -} + return tile->vtable->m38_Get_Territory_OwnerID (tile) == civ_id; + } -Sprite * -get_sprite_proxy_for_current_hour(Sprite *s) { - int v; - int hour = is->current_day_night_cycle; // 0..23 - if (itable_look_up(&is->day_night_sprite_proxy_by_hour[hour], (int)s, &v)) - return (Sprite *)v; - return NULL; // not proxied, fall back to s + return false; } -void -insert_spritelist_proxies(SpriteList *ss, SpriteList *ps, int hour, int len1, int len2) { - for (int i = 0; i < len1; i++) { - for (int j = 0; j < len2; j++) { - Sprite *s = &ss[i].field_0[j]; - Sprite *p = &ps[i].field_0[j]; - if (s && p) { - itable_insert(&is->day_night_sprite_proxy_by_hour[hour], (int)s, (int)p); - } - } +int +count_distribution_hubs_for_civ (int civ_id) +{ + int current = 0; + FOR_TABLE_ENTRIES (tei, &is->distribution_hub_records) { + struct distribution_hub_record * rec = (struct distribution_hub_record *)tei.value; + if ((rec != NULL) && (rec->civ_id == civ_id)) + current++; } + return current; } -void -insert_sprite_proxies(Sprite *ss, Sprite *ps, int hour, int len) { - for (int i = 0; i < len; i++) { - Sprite *s = &ss[i]; - Sprite *p = &ps[i]; - if (s && p) { - itable_insert(&is->day_night_sprite_proxy_by_hour[hour], (int)s, (int)p); - } - } -} +bool +leader_can_natively_build_district (Leader * leader, int district_id) +{ + if ((leader == NULL) || (district_id < 0) || (district_id >= is->district_count)) + return false; -void -insert_sprite_proxy(Sprite *s, Sprite *p, int hour) { - if (s && p) { - itable_insert(&is->day_night_sprite_proxy_by_hour[hour], (int)s, (int)p); + struct district_config const * cfg = &is->district_configs[district_id]; + struct district_infos const * info = &is->district_infos[district_id]; + + for (int i = 0; i < info->advance_prereq_count; i++) { + int prereq_id = info->advance_prereq_ids[i]; + if ((prereq_id >= 0) && ! Leader_has_tech (leader, __, prereq_id)) + return false; } -} + int obsolete_id = info->obsoleted_by_id; + if ((obsolete_id >= 0) && Leader_has_tech (leader, __, obsolete_id)) + return false; -void -build_sprite_proxies_24(Map_Renderer *mr) { - for (int h = 0; h < 24; ++h) { - insert_sprite_proxies(city_sprites, is->day_night_cycle_imgs[h].City_Images, h, 80); - insert_sprite_proxies(destroyed_city_sprites, is->day_night_cycle_imgs[h].Destroyed_City_Images, h, 3); - insert_sprite_proxies(mr->Resources, is->day_night_cycle_imgs[h].Resources, h, 36); - insert_spritelist_proxies(mr->Std_Terrain_Images, is->day_night_cycle_imgs[h].Std_Terrain_Images, h, 9, 81); - insert_spritelist_proxies(mr->LM_Terrain_Images, is->day_night_cycle_imgs[h].LM_Terrain_Images, h, 9, 81); - insert_sprite_proxy(&mr->Terrain_Buldings_Barbarian_Camp, &is->day_night_cycle_imgs[h].Terrain_Buldings_Barbarian_Camp, h); - insert_sprite_proxy(&mr->Terrain_Buldings_Mines, &is->day_night_cycle_imgs[h].Terrain_Buldings_Mines, h); - insert_sprite_proxy(&mr->Victory_Image, &is->day_night_cycle_imgs[h].Victory_Image, h); - insert_sprite_proxy(&mr->Terrain_Buldings_Radar, &is->day_night_cycle_imgs[h].Terrain_Buldings_Radar, h); - insert_sprite_proxies(mr->Flood_Plains_Images, is->day_night_cycle_imgs[h].Flood_Plains_Images, h, 16); - insert_sprite_proxies(mr->Polar_Icecaps_Images, is->day_night_cycle_imgs[h].Polar_Icecaps_Images, h, 32); - insert_sprite_proxies(mr->Roads_Images, is->day_night_cycle_imgs[h].Roads_Images, h, 256); - insert_sprite_proxies(mr->Railroads_Images, is->day_night_cycle_imgs[h].Railroads_Images, h, 272); - insert_sprite_proxies(mr->Terrain_Buldings_Airfields, is->day_night_cycle_imgs[h].Terrain_Buldings_Airfields, h, 2); - insert_sprite_proxies(mr->Terrain_Buldings_Camp, is->day_night_cycle_imgs[h].Terrain_Buldings_Camp, h, 4); - insert_sprite_proxies(mr->Terrain_Buldings_Fortress, is->day_night_cycle_imgs[h].Terrain_Buldings_Fortress, h, 4); - insert_sprite_proxies(mr->Terrain_Buldings_Barricade, is->day_night_cycle_imgs[h].Terrain_Buldings_Barricade, h, 4); - insert_sprite_proxies(mr->Goody_Huts_Images, is->day_night_cycle_imgs[h].Goody_Huts_Images, h, 8); - insert_sprite_proxies(mr->Terrain_Buldings_Outposts, is->day_night_cycle_imgs[h].Terrain_Buldings_Outposts, h, 3); - insert_sprite_proxies(mr->Pollution, is->day_night_cycle_imgs[h].Pollution, h, 25); - insert_sprite_proxies(mr->Craters, is->day_night_cycle_imgs[h].Craters, h, 25); - insert_sprite_proxies(mr->Tnt_Images, is->day_night_cycle_imgs[h].Tnt_Images, h, 18); - insert_sprite_proxies(mr->Waterfalls_Images, is->day_night_cycle_imgs[h].Waterfalls_Images, h, 4); - insert_sprite_proxies(mr->LM_Terrain, is->day_night_cycle_imgs[h].LM_Terrain, h, 7); - insert_sprite_proxies(mr->Marsh_Large, is->day_night_cycle_imgs[h].Marsh_Large, h, 8); - insert_sprite_proxies(mr->Marsh_Small, is->day_night_cycle_imgs[h].Marsh_Small, h, 10); - insert_sprite_proxies(mr->Volcanos_Images, is->day_night_cycle_imgs[h].Volcanos_Images, h, 16); - insert_sprite_proxies(mr->Volcanos_Forests_Images, is->day_night_cycle_imgs[h].Volcanos_Forests_Images, h, 16); - insert_sprite_proxies(mr->Volcanos_Jungles_Images, is->day_night_cycle_imgs[h].Volcanos_Jungles_Images, h, 16); - insert_sprite_proxies(mr->Volcanos_Snow_Images, is->day_night_cycle_imgs[h].Volcanos_Snow_Images, h, 16); - insert_sprite_proxies(mr->Grassland_Forests_Large, is->day_night_cycle_imgs[h].Grassland_Forests_Large, h, 8); - insert_sprite_proxies(mr->Plains_Forests_Large, is->day_night_cycle_imgs[h].Plains_Forests_Large, h, 8); - insert_sprite_proxies(mr->Tundra_Forests_Large, is->day_night_cycle_imgs[h].Tundra_Forests_Large, h, 8); - insert_sprite_proxies(mr->Grassland_Forests_Small, is->day_night_cycle_imgs[h].Grassland_Forests_Small, h, 10); - insert_sprite_proxies(mr->Plains_Forests_Small, is->day_night_cycle_imgs[h].Plains_Forests_Small, h, 10); - insert_sprite_proxies(mr->Tundra_Forests_Small, is->day_night_cycle_imgs[h].Tundra_Forests_Small, h, 10); - insert_sprite_proxies(mr->Grassland_Forests_Pines, is->day_night_cycle_imgs[h].Grassland_Forests_Pines, h, 12); - insert_sprite_proxies(mr->Plains_Forests_Pines, is->day_night_cycle_imgs[h].Plains_Forests_Pines, h, 12); - insert_sprite_proxies(mr->Tundra_Forests_Pines, is->day_night_cycle_imgs[h].Tundra_Forests_Pines, h, 12); - insert_sprite_proxies(mr->Irrigation_Desert_Images, is->day_night_cycle_imgs[h].Irrigation_Desert_Images, h, 16); - insert_sprite_proxies(mr->Irrigation_Plains_Images, is->day_night_cycle_imgs[h].Irrigation_Plains_Images, h, 16); - insert_sprite_proxies(mr->Irrigation_Images, is->day_night_cycle_imgs[h].Irrigation_Images, h, 16); - insert_sprite_proxies(mr->Irrigation_Tundra_Images, is->day_night_cycle_imgs[h].Irrigation_Tundra_Images, h, 16); - insert_sprite_proxies(mr->Grassland_Jungles_Large, is->day_night_cycle_imgs[h].Grassland_Jungles_Large, h, 8); - insert_sprite_proxies(mr->Grassland_Jungles_Small, is->day_night_cycle_imgs[h].Grassland_Jungles_Small, h, 12); - insert_sprite_proxies(mr->Mountains_Images, is->day_night_cycle_imgs[h].Mountains_Images, h, 16); - insert_sprite_proxies(mr->Mountains_Forests_Images, is->day_night_cycle_imgs[h].Mountains_Forests_Images, h, 16); - insert_sprite_proxies(mr->Mountains_Jungles_Images, is->day_night_cycle_imgs[h].Mountains_Jungles_Images, h, 16); - insert_sprite_proxies(mr->Mountains_Snow_Images, is->day_night_cycle_imgs[h].Mountains_Snow_Images, h, 16); - insert_sprite_proxies(mr->Hills_Images, is->day_night_cycle_imgs[h].Hills_Images, h, 16); - insert_sprite_proxies(mr->Hills_Forests_Images, is->day_night_cycle_imgs[h].Hills_Forests_Images, h, 16); - insert_sprite_proxies(mr->Hills_Jungle_Images, is->day_night_cycle_imgs[h].Hills_Jungle_Images, h, 16); - insert_sprite_proxies(mr->Delta_Rivers_Images, is->day_night_cycle_imgs[h].Delta_Rivers_Images, h, 16); - insert_sprite_proxies(mr->Mountain_Rivers_Images, is->day_night_cycle_imgs[h].Mountain_Rivers_Images, h, 16); - insert_sprite_proxies(mr->LM_Mountains_Images, is->day_night_cycle_imgs[h].LM_Mountains_Images, h, 16); - insert_sprite_proxies(mr->LM_Forests_Large_Images, is->day_night_cycle_imgs[h].LM_Forests_Large_Images, h, 8); - insert_sprite_proxies(mr->LM_Forests_Small_Images, is->day_night_cycle_imgs[h].LM_Forests_Small_Images, h, 10); - insert_sprite_proxies(mr->LM_Forests_Pines_Images, is->day_night_cycle_imgs[h].LM_Forests_Pines_Images, h, 12); - insert_sprite_proxies(mr->LM_Hills_Images, is->day_night_cycle_imgs[h].LM_Hills_Images, h, 16); - - if (is->current_config.enable_districts) { - for (int dc = 0; dc < is->district_count; dc++) { - struct district_config const * cfg = &is->district_configs[dc]; - int variant_capacity = ARRAY_LEN (is->district_img_sets[dc].imgs); - int variant_count = cfg->img_path_count; - if (variant_count <= 0) - continue; - if (variant_count > variant_capacity) - variant_count = variant_capacity; + if (! leader_has_wonder_prereq (leader, info)) + return false; - int era_count = cfg->vary_img_by_era ? 4 : 1; - int column_count = cfg->max_building_index + 1; + if (! leader_has_natural_wonder_prereq_in_territory (leader->ID, info)) + return false; - for (int variant_i = 0; variant_i < variant_count; variant_i++) { - if ((cfg->img_paths[variant_i] == NULL) || (cfg->img_paths[variant_i][0] == '\0')) - continue; - for (int era = 0; era < era_count; era++) { - for (int col = 0; col < column_count; col++) { - Sprite * base = &is->district_img_sets[dc].imgs[variant_i][era][col]; - Sprite * proxy = &is->day_night_cycle_imgs[h].District_Images[dc][variant_i][era][col]; - insert_sprite_proxy (base, proxy, h); - } - } - } + if (district_id == DISTRIBUTION_HUB_DISTRICT_ID) { + int max_per_100 = is->current_config.max_distribution_hub_count_per_100_cities; + if (max_per_100 > 0) { + int city_count = leader->Cities_Count; + int max_allowed = (city_count * max_per_100 + 99) / 100; + if (max_allowed <= 0) + return false; + + int current = count_distribution_hubs_for_civ (leader->ID); + if (current >= max_allowed) + return false; + } + } + + if (cfg->has_buildable_by_civs) { + bool civ_match = false; + if (cfg->buildable_by_civ_count > 0) { + Race * race = (leader->RaceID >= 0 && leader->RaceID < p_bic_data->RacesCount) + ? &p_bic_data->Races[leader->RaceID] + : NULL; + if (race == NULL) + return false; + for (int i = 0; i < cfg->buildable_by_civ_count; i++) { + char const * civ_name = cfg->buildable_by_civs[i]; + if ((civ_name == NULL) || (civ_name[0] == '\0')) continue; + if ((race->CountryName != NULL) && (strcmp (civ_name, race->CountryName) == 0)) civ_match = true; break; + if ((race->SingularName != NULL) && (strcmp (civ_name, race->SingularName) == 0)) civ_match = true; break; + if ((race->AdjectiveName != NULL) && (strcmp (civ_name, race->AdjectiveName) == 0)) civ_match = true; break; + if ((race->LeaderName != NULL) && (strcmp (civ_name, race->LeaderName) == 0)) civ_match = true; break; } + } + if (! civ_match) + return false; + } - // Wonder districts - if (is->current_config.enable_wonder_districts) { - for (int wi = 0; wi < is->wonder_district_count; wi++) { - Sprite * base_img = &is->wonder_district_img_sets[wi].img; - Sprite * proxy_img = &is->day_night_cycle_imgs[h].Wonder_District_Images[wi].img; - insert_sprite_proxy (base_img, proxy_img, h); + if (cfg->has_buildable_by_civ_traits) { + if (cfg->buildable_by_civ_traits_id_count <= 0) + return false; + Race * race = (leader->RaceID >= 0 && leader->RaceID < p_bic_data->RacesCount) + ? &p_bic_data->Races[leader->RaceID] + : NULL; + if (race == NULL || race->vtable == NULL) + return false; + bool trait_match = false; + for (int i = 0; i < cfg->buildable_by_civ_traits_id_count; i++) { + int trait_id = cfg->buildable_by_civ_traits_ids[i]; + if (trait_id >= 0 && race->vtable->CheckBonus (race, __, trait_id)) { + trait_match = true; + break; + } + } + if (! trait_match) + return false; + } - Sprite * base_construct = &is->wonder_district_img_sets[wi].construct_img; - Sprite * proxy_construct = &is->day_night_cycle_imgs[h].Wonder_District_Images[wi].construct_img; - insert_sprite_proxy (base_construct, proxy_construct, h); - } + if (cfg->has_buildable_by_civ_govs) { + if (cfg->buildable_by_civ_govs_id_count <= 0) + return false; + int gov_id = leader->GovernmentType; + bool gov_match = false; + for (int i = 0; i < cfg->buildable_by_civ_govs_id_count; i++) { + if (cfg->buildable_by_civ_govs_ids[i] == gov_id) { + gov_match = true; + break; } } + if (! gov_match) + return false; + } - // Natural wonders - if (is->current_config.enable_natural_wonders && (is->natural_wonder_count > 0)) { - for (int ni = 0; ni < is->natural_wonder_count; ni++) { - Sprite * base_nw = &is->natural_wonder_img_sets[ni].img; - Sprite * proxy_nw = &is->day_night_cycle_imgs[h].Natural_Wonder_Images[ni].img; - insert_sprite_proxy (base_nw, proxy_nw, h); + if (cfg->has_buildable_by_civ_cultures) { + if (cfg->buildable_by_civ_cultures_id_count <= 0) + return false; + Race * race = (leader->RaceID >= 0 && leader->RaceID < p_bic_data->RacesCount) + ? &p_bic_data->Races[leader->RaceID] + : NULL; + if (race == NULL) + return false; + int culture_id = race->CultureGroupID; + bool culture_match = false; + for (int i = 0; i < cfg->buildable_by_civ_cultures_id_count; i++) { + if (cfg->buildable_by_civ_cultures_ids[i] == culture_id) { + culture_match = true; + break; } } + if (! culture_match) + return false; } - is->day_night_cycle_img_proxies_indexed = true; + + return true; } -void -init_day_night_images() +bool +leader_has_war_ally_district_access (Leader * leader, int district_id) { - if (is->day_night_cycle_img_state != IS_UNINITED) - return; - - const char *hour_strs[24] = { - "2400", "0100", "0200", "0300", "0400", "0500", "0600", "0700", - "0800", "0900", "1000", "1100", "1200", "1300", "1400", "1500", - "1600", "1700", "1800", "1900", "2000", "2100", "2200", "2300" - }; + if (leader == NULL) + return false; - for (int i = 0; i < 24; i++) { + int self_id = leader->ID; + for (int civ_id = 0; civ_id < 32; civ_id++) { + if (civ_id == self_id) + continue; + Leader * other = &leaders[civ_id]; + if (other->Military_Allies[self_id] != 0 && leader_can_natively_build_district (other, district_id)) + return true; + } - char art_dir[200]; - snprintf(art_dir, sizeof art_dir, "%s\\Art\\DayNight\\%s", is->mod_rel_dir, hour_strs[i]); - bool success = load_day_night_hour_images(&is->day_night_cycle_imgs[i], art_dir, hour_strs[i]); + return false; +} - if (!success) { - char ss[200]; - snprintf(ss, sizeof ss, "Failed to load day/night cycle images for hour %s, reverting to base game art.", hour_strs[i]); - pop_up_in_game_error (ss); +bool +leader_has_pact_ally_district_access (Leader * leader, int district_id) +{ + if (leader == NULL) + return false; - is->day_night_cycle_img_state = IS_INIT_FAILED; - return; + int self_id = leader->ID; + for (int civ_id = 0; civ_id < 32; civ_id++) { + if (civ_id == self_id) + continue; + Leader * other = &leaders[civ_id]; + if (((leader->Relation_Treaties[civ_id] & 1) != 0 || // 1 = peace treaty + (leader->Relation_Treaties[civ_id] & 4) != 0) && // 4 = mutual protection pact + leader_can_natively_build_district (other, district_id)) { + return true; } } - Map_Renderer * mr = &p_bic_data->Map.Renderer; - build_sprite_proxies_24(mr); - - is->day_night_cycle_img_state = IS_OK; + return false; } -void -deindex_day_night_image_proxies() +bool +leader_can_build_district (Leader * leader, int district_id) { - if (!is->day_night_cycle_img_proxies_indexed) - return; + bool can_natively_build = leader_can_natively_build_district (leader, district_id); - for (int i = 0; i < 24; i++) { - table_deinit (&is->day_night_sprite_proxy_by_hour[i]); - } - is->day_night_cycle_img_proxies_indexed = false; + if (can_natively_build) + return true; + + struct district_config const * cfg = &is->district_configs[district_id]; + if (cfg->buildable_by_war_allies && leader_has_war_ally_district_access (leader, district_id)) + return true; + if (cfg->buildable_by_pact_allies && leader_has_pact_ally_district_access (leader, district_id)) + return true; + + return can_natively_build; } -int -calculate_current_day_night_cycle_hour () +Tile * +find_tile_for_district (City * city, int district_id, int * out_x, int * out_y) { - int output = 12; // Default to noon - int increment = is->current_config.fixed_hours_per_turn_for_day_night_cycle; - - switch (is->current_config.day_night_cycle_mode) { + if ((city == NULL) || (out_x == NULL) || (out_y == NULL)) + return NULL; + if ((district_id < 0) || (district_id >= is->district_count)) + return NULL; - // Disabled. This shouldn't be possible, but default to noon to be safe - case DNCM_OFF: - return output; + // Check if city can build this district at all + if (! city_can_build_district (city, district_id)) + return NULL; - // Time elapsed since last update - case DNCM_TIMER: { - LARGE_INTEGER perf_freq; - QueryPerformanceFrequency(&perf_freq); + if (district_id == NEIGHBORHOOD_DISTRICT_ID) + return find_tile_for_neighborhood_district (city, out_x, out_y); + if (district_id == DISTRIBUTION_HUB_DISTRICT_ID) + return find_tile_for_distribution_hub_district (city, out_x, out_y); + if (district_id == PORT_DISTRICT_ID) + return find_tile_for_port_district (city, out_x, out_y); + if (district_id == WONDER_DISTRICT_ID) + return find_tile_for_wonder_district (city, out_x, out_y); - if (is->day_night_cycle_unstarted) { - is->current_day_night_cycle = output; - QueryPerformanceCounter(&is->last_day_night_cycle_update_time); - } + int city_x = city->Body.X; + int city_y = city->Body.Y; + int city_work_radius = is->current_config.city_work_radius; - LARGE_INTEGER time_now; - QueryPerformanceCounter(&time_now); + // Search in order: ring 2, then rings 3..N, then ring 1 as last resort + // Ring order array: 2, 3, 4, ..., city_work_radius, 1 + int ring_order[8]; + int ring_count = 0; + ring_order[ring_count++] = 2; + for (int r = 3; r <= city_work_radius; r++) + ring_order[ring_count++] = r; + ring_order[ring_count++] = 1; - double elapsed_seconds = - (double)(time_now.QuadPart - is->last_day_night_cycle_update_time.QuadPart) / - (double)perf_freq.QuadPart; + Tile * best_tile = NULL; + int best_yield = INT_MAX; + int best_x = -1, best_y = -1; + int current_ring = -1; - if (elapsed_seconds > (double)is->current_config.elapsed_minutes_per_day_night_hour_transition * 60.0) { - output = is->current_day_night_cycle + increment; - is->last_day_night_cycle_update_time = time_now; - } else { - output = is->current_day_night_cycle; + FOR_TILE_RINGS_AROUND (tri, city_x, city_y, ring_order, ring_count) { + if (tri.current_ring != current_ring) { + if (best_tile != NULL) { + *out_x = best_x; + *out_y = best_y; + return best_tile; } - break; + current_ring = tri.current_ring; + best_tile = NULL; + best_yield = INT_MAX; } - // Match user's current time - case DNCM_USER_TIME: { - LPSYSTEMTIME lpSystemTime = (LPSYSTEMTIME)malloc(sizeof(SYSTEMTIME)); - GetLocalTime (lpSystemTime); - output = lpSystemTime->wHour; - free (lpSystemTime); - break; + Tile * tile = tri.tile; + if (is->current_config.enable_distribution_hub_districts) { + int covered = itable_look_up_or (&is->distribution_hub_coverage_counts, (int)tile, 0); + if (covered > 0) + continue; } - // Increment fixed amount each interturn - case DNCM_EVERY_TURN: { - if (is->day_night_cycle_unstarted) { - increment = 0; - is->current_day_night_cycle = output; - } - output = is->current_day_night_cycle + increment; - break; - } + if (! tile_suitable_for_district (tile, district_id, city, NULL)) + continue; + if (get_district_instance (tile) != NULL && + ! tile_has_obsolete_district_for_civ (tile, city->Body.CivID)) + continue; - // Pin the hour to a specific value - case DNCM_SPECIFIED: { - output = is->current_config.pinned_hour_for_day_night_cycle; - break; + int yield = compute_city_tile_yield_sum (city, tri.tile_x, tri.tile_y); + if (yield < best_yield && (! is_tile_earmarked_for_district (tri.tile_x, tri.tile_y))) { + best_yield = yield; + best_tile = tile; + best_x = tri.tile_x; + best_y = tri.tile_y; } } - // If midnight or over, restart at 0 or later - if (output > 23) output = output - 24; - - // Clamp to valid range of 0-23 in case of weird config values - output = clamp (0, 23, output); - is->day_night_cycle_unstarted = false; + if (best_tile != NULL) { + *out_x = best_x; + *out_y = best_y; + return best_tile; + } - return output; + return NULL; } -void __fastcall -patch_Map_Renderer_load_images (Map_Renderer *this, int edx) +Tile * +get_completed_district_tile_for_city (City * city, int district_id, int * out_x, int * out_y) { - Map_Renderer_load_images(this, __); + if ((city == NULL) || (district_id < 0) || (district_id >= is->district_count)) + return NULL; - // Initialize day/night cycle and re-calculate hour, if applicable - if (is->current_config.day_night_cycle_mode != DNCM_OFF) { - is->current_day_night_cycle = calculate_current_day_night_cycle_hour (); + FOR_DISTRICTS_AROUND (wai, city->Body.X, city->Body.Y, true) { + int x = wai.tile_x, y = wai.tile_y; + Tile * candidate = wai.tile; + struct district_instance * inst = wai.district_inst; - if (is->day_night_cycle_img_state == IS_UNINITED) { - init_day_night_images (); - } + if (inst->district_id != district_id) + continue; - if (is->day_night_cycle_img_state == IS_OK) { - - // Sprite proxies are deindexed during each load event as sprite instances (really only Resources, which are reloaded) may change. - if (!is->day_night_cycle_img_proxies_indexed) { - build_sprite_proxies_24(this); + // For wonder districts, filter based on wonder_district_state + if (district_id == WONDER_DISTRICT_ID) { + struct wonder_district_info * info = &inst->wonder_info; + // Must be either unused or under construction by this city + if (info->state == WDS_COMPLETED) + continue; + if (info->state == WDS_UNDER_CONSTRUCTION && info->city_id != city->Body.ID) + continue; + if (info->state == WDS_UNDER_CONSTRUCTION) { + info->city = city; + info->city_id = city->Body.ID; } } + + if (out_x != NULL) + *out_x = x; + if (out_y != NULL) + *out_y = y; + return candidate; } + + return NULL; } -void -patch_init_floating_point () +bool +tile_has_friendly_aerodrome_district (Tile * tile, int civ_id, bool require_available) { - init_floating_point (); + if (! is->current_config.enable_districts || + ! is->current_config.enable_aerodrome_districts || + (tile == NULL) || + (tile == p_null_tile)) + return false; - // NOTE: At this point the program is done with the CRT initialization stuff and will start calling constructors for global - // objects as soon as this function returns. This is a good place to inject code that will run at program start. + struct district_instance * inst = get_district_instance (tile); + if (inst == NULL || inst->district_id != AERODROME_DISTRICT_ID) + return false; - // Specify metadata about all boolean options on the mod config. We'll use this info to set up the table of offsets (for easy parsing) and to - // fill out the base config. - struct boolean_config_option { - char * name; - bool base_val; - int offset; - } boolean_config_options[] = { - {"enable_stack_bombard" , true , offsetof (struct c3x_config, enable_stack_bombard)}, - {"enable_disorder_warning" , true , offsetof (struct c3x_config, enable_disorder_warning)}, - {"allow_stealth_attack_against_single_unit" , false, offsetof (struct c3x_config, allow_stealth_attack_against_single_unit)}, - {"show_detailed_city_production_info" , true , offsetof (struct c3x_config, show_detailed_city_production_info)}, - {"limited_railroads_work_like_fast_roads" , false, offsetof (struct c3x_config, limited_railroads_work_like_fast_roads)}, - {"exclude_cities_from_units_per_tile_limit" , false, offsetof (struct c3x_config, exclude_cities_from_units_per_tile_limit)}, - {"enable_free_buildings_from_small_wonders" , true , offsetof (struct c3x_config, enable_free_buildings_from_small_wonders)}, - {"enable_stack_unit_commands" , true , offsetof (struct c3x_config, enable_stack_unit_commands)}, - {"skip_repeated_tile_improv_replacement_asks" , true , offsetof (struct c3x_config, skip_repeated_tile_improv_replacement_asks)}, - {"autofill_best_gold_amount_when_trading" , true , offsetof (struct c3x_config, autofill_best_gold_amount_when_trading)}, - {"disallow_founding_next_to_foreign_city" , true , offsetof (struct c3x_config, disallow_founding_next_to_foreign_city)}, - {"enable_trade_screen_scroll" , true , offsetof (struct c3x_config, enable_trade_screen_scroll)}, - {"group_units_on_right_click_menu" , true , offsetof (struct c3x_config, group_units_on_right_click_menu)}, - {"gray_out_units_on_menu_with_no_remaining_moves" , true , offsetof (struct c3x_config, gray_out_units_on_menu_with_no_remaining_moves)}, - {"put_movement_icons_on_units_on_menu" , true , offsetof (struct c3x_config, put_movement_icons_on_units_on_menu)}, - {"describe_states_of_units_on_menu" , true , offsetof (struct c3x_config, describe_states_of_units_on_menu)}, - {"show_golden_age_turns_remaining" , true , offsetof (struct c3x_config, show_golden_age_turns_remaining)}, - {"show_zoc_attacks_from_mid_stack" , true , offsetof (struct c3x_config, show_zoc_attacks_from_mid_stack)}, - {"show_armies_performing_defensive_bombard" , true , offsetof (struct c3x_config, show_armies_performing_defensive_bombard)}, - {"cut_research_spending_to_avoid_bankruptcy" , true , offsetof (struct c3x_config, cut_research_spending_to_avoid_bankruptcy)}, - {"dont_pause_for_love_the_king_messages" , true , offsetof (struct c3x_config, dont_pause_for_love_the_king_messages)}, - {"reverse_specialist_order_with_shift" , true , offsetof (struct c3x_config, reverse_specialist_order_with_shift)}, - {"toggle_zoom_with_z_on_city_screen" , true , offsetof (struct c3x_config, toggle_zoom_with_z_on_city_screen)}, - {"dont_give_king_names_in_non_regicide_games" , true , offsetof (struct c3x_config, dont_give_king_names_in_non_regicide_games)}, - {"no_elvis_easter_egg" , false, offsetof (struct c3x_config, no_elvis_easter_egg)}, - {"disable_worker_automation" , false, offsetof (struct c3x_config, disable_worker_automation)}, - {"enable_land_sea_intersections" , false, offsetof (struct c3x_config, enable_land_sea_intersections)}, - {"disallow_trespassing" , false, offsetof (struct c3x_config, disallow_trespassing)}, - {"show_detailed_tile_info" , true , offsetof (struct c3x_config, show_detailed_tile_info)}, - {"warn_about_unrecognized_names" , true , offsetof (struct c3x_config, warn_about_unrecognized_names)}, - {"enable_ai_production_ranking" , true , offsetof (struct c3x_config, enable_ai_production_ranking)}, - {"enable_ai_city_location_desirability_display" , true , offsetof (struct c3x_config, enable_ai_city_location_desirability_display)}, - {"zero_corruption_when_off" , true , offsetof (struct c3x_config, zero_corruption_when_off)}, - {"disallow_land_units_from_affecting_water_tiles" , true , offsetof (struct c3x_config, disallow_land_units_from_affecting_water_tiles)}, - {"dont_end_units_turn_after_airdrop" , false, offsetof (struct c3x_config, dont_end_units_turn_after_airdrop)}, - {"allow_airdrop_without_airport" , false, offsetof (struct c3x_config, allow_airdrop_without_airport)}, - {"enable_negative_pop_pollution" , true , offsetof (struct c3x_config, enable_negative_pop_pollution)}, - {"allow_defensive_retreat_on_water" , false, offsetof (struct c3x_config, allow_defensive_retreat_on_water)}, - {"promote_wonder_decorruption_effect" , false, offsetof (struct c3x_config, promote_wonder_decorruption_effect)}, - {"allow_military_leaders_to_hurry_wonders" , false, offsetof (struct c3x_config, allow_military_leaders_to_hurry_wonders)}, - {"aggressively_penalize_bankruptcy" , false, offsetof (struct c3x_config, aggressively_penalize_bankruptcy)}, - {"no_penalty_exception_for_agri_fresh_water_city_tiles" , false, offsetof (struct c3x_config, no_penalty_exception_for_agri_fresh_water_city_tiles)}, - {"use_offensive_artillery_ai" , true , offsetof (struct c3x_config, use_offensive_artillery_ai)}, - {"dont_escort_unflagged_units" , false, offsetof (struct c3x_config, dont_escort_unflagged_units)}, - {"replace_leader_unit_ai" , true , offsetof (struct c3x_config, replace_leader_unit_ai)}, - {"fix_ai_army_composition" , true , offsetof (struct c3x_config, fix_ai_army_composition)}, - {"enable_pop_unit_ai" , true , offsetof (struct c3x_config, enable_pop_unit_ai)}, - {"enable_caravan_unit_ai" , true , offsetof (struct c3x_config, enable_caravan_unit_ai)}, - {"remove_unit_limit" , true , offsetof (struct c3x_config, remove_unit_limit)}, - {"remove_city_improvement_limit" , true , offsetof (struct c3x_config, remove_city_improvement_limit)}, - {"remove_cap_on_turn_limit" , true , offsetof (struct c3x_config, remove_cap_on_turn_limit)}, - {"remove_era_limit" , false, offsetof (struct c3x_config, remove_era_limit)}, - {"patch_submarine_bug" , true , offsetof (struct c3x_config, patch_submarine_bug)}, - {"patch_science_age_bug" , true , offsetof (struct c3x_config, patch_science_age_bug)}, - {"patch_pedia_texture_bug" , true , offsetof (struct c3x_config, patch_pedia_texture_bug)}, - {"patch_blocked_disembark_freeze" , true , offsetof (struct c3x_config, patch_blocked_disembark_freeze)}, - {"patch_houseboat_bug" , true , offsetof (struct c3x_config, patch_houseboat_bug)}, - {"patch_intercept_lost_turn_bug" , true , offsetof (struct c3x_config, patch_intercept_lost_turn_bug)}, - {"patch_phantom_resource_bug" , true , offsetof (struct c3x_config, patch_phantom_resource_bug)}, - {"patch_maintenance_persisting_for_obsolete_buildings" , true , offsetof (struct c3x_config, patch_maintenance_persisting_for_obsolete_buildings)}, - {"patch_barbarian_diagonal_bug" , true , offsetof (struct c3x_config, patch_barbarian_diagonal_bug)}, - {"patch_disease_stopping_tech_flag_bug" , false, offsetof (struct c3x_config, patch_disease_stopping_tech_flag_bug)}, - {"patch_division_by_zero_in_ai_alliance_eval" , true , offsetof (struct c3x_config, patch_division_by_zero_in_ai_alliance_eval)}, - {"patch_empty_army_movement" , true , offsetof (struct c3x_config, patch_empty_army_movement)}, - {"patch_premature_truncation_of_found_paths" , true , offsetof (struct c3x_config, patch_premature_truncation_of_found_paths)}, - {"patch_zero_production_crash" , true , offsetof (struct c3x_config, patch_zero_production_crash)}, - {"patch_ai_can_form_army_without_special_ability" , true , offsetof (struct c3x_config, patch_ai_can_form_army_without_special_ability)}, - {"patch_ai_can_sacrifice_without_special_ability" , true , offsetof (struct c3x_config, patch_ai_can_sacrifice_without_special_ability)}, - {"patch_crash_in_leader_unit_ai" , true , offsetof (struct c3x_config, patch_crash_in_leader_unit_ai)}, - {"patch_failure_to_find_new_city_build" , true , offsetof (struct c3x_config, patch_failure_to_find_new_city_build)}, - {"delete_off_map_ai_units" , true , offsetof (struct c3x_config, delete_off_map_ai_units)}, - {"fix_overlapping_specialist_yield_icons" , true , offsetof (struct c3x_config, fix_overlapping_specialist_yield_icons)}, - {"prevent_autorazing" , false, offsetof (struct c3x_config, prevent_autorazing)}, - {"prevent_razing_by_players" , false, offsetof (struct c3x_config, prevent_razing_by_players)}, - {"suppress_hypertext_links_exceeded_popup" , true , offsetof (struct c3x_config, suppress_hypertext_links_exceeded_popup)}, - {"indicate_non_upgradability_in_pedia" , true , offsetof (struct c3x_config, indicate_non_upgradability_in_pedia)}, - {"show_message_after_dodging_sam" , true , offsetof (struct c3x_config, show_message_after_dodging_sam)}, - {"include_stealth_attack_cancel_option" , false, offsetof (struct c3x_config, include_stealth_attack_cancel_option)}, - {"intercept_recon_missions" , false, offsetof (struct c3x_config, intercept_recon_missions)}, - {"charge_one_move_for_recon_and_interception" , false, offsetof (struct c3x_config, charge_one_move_for_recon_and_interception)}, - {"polish_precision_striking" , true , offsetof (struct c3x_config, polish_precision_striking)}, - {"enable_stealth_attack_via_bombardment" , false, offsetof (struct c3x_config, enable_stealth_attack_via_bombardment)}, - {"immunize_aircraft_against_bombardment" , false, offsetof (struct c3x_config, immunize_aircraft_against_bombardment)}, - {"replay_ai_moves_in_hotseat_games" , false, offsetof (struct c3x_config, replay_ai_moves_in_hotseat_games)}, - {"restore_unit_directions_on_game_load" , true , offsetof (struct c3x_config, restore_unit_directions_on_game_load)}, - {"apply_grid_ini_setting_on_game_load" , true , offsetof (struct c3x_config, apply_grid_ini_setting_on_game_load)}, - {"charm_flag_triggers_ptw_like_targeting" , false, offsetof (struct c3x_config, charm_flag_triggers_ptw_like_targeting)}, - {"city_icons_show_unit_effects_not_trade" , true , offsetof (struct c3x_config, city_icons_show_unit_effects_not_trade)}, - {"ignore_king_ability_for_defense_priority" , false, offsetof (struct c3x_config, ignore_king_ability_for_defense_priority)}, - {"show_untradable_techs_on_trade_screen" , false, offsetof (struct c3x_config, show_untradable_techs_on_trade_screen)}, - {"disallow_useless_bombard_vs_airfields" , true , offsetof (struct c3x_config, disallow_useless_bombard_vs_airfields)}, - {"compact_luxury_display_on_city_screen" , false, offsetof (struct c3x_config, compact_luxury_display_on_city_screen)}, - {"compact_strategic_resource_display_on_city_screen" , false, offsetof (struct c3x_config, compact_strategic_resource_display_on_city_screen)}, - {"warn_when_chosen_building_would_replace_another" , false, offsetof (struct c3x_config, warn_when_chosen_building_would_replace_another)}, - {"do_not_unassign_workers_from_polluted_tiles" , false, offsetof (struct c3x_config, do_not_unassign_workers_from_polluted_tiles)}, - {"do_not_make_capital_cities_appear_larger" , false, offsetof (struct c3x_config, do_not_make_capital_cities_appear_larger)}, - {"show_territory_colors_on_water_tiles_in_minimap" , false, offsetof (struct c3x_config, show_territory_colors_on_water_tiles_in_minimap)}, - {"convert_some_popups_into_online_mp_messages" , false, offsetof (struct c3x_config, convert_some_popups_into_online_mp_messages)}, - {"enable_debug_mode_switch" , false, offsetof (struct c3x_config, enable_debug_mode_switch)}, - {"accentuate_cities_on_minimap" , false, offsetof (struct c3x_config, accentuate_cities_on_minimap)}, - {"allow_multipage_civilopedia_descriptions" , true , offsetof (struct c3x_config, allow_multipage_civilopedia_descriptions)}, - {"enable_trade_net_x" , true , offsetof (struct c3x_config, enable_trade_net_x)}, - {"optimize_improvement_loops" , true , offsetof (struct c3x_config, optimize_improvement_loops)}, - {"measure_turn_times" , false, offsetof (struct c3x_config, measure_turn_times)}, - {"enable_city_capture_by_barbarians" , false, offsetof (struct c3x_config, enable_city_capture_by_barbarians)}, - {"share_visibility_in_hotseat" , false, offsetof (struct c3x_config, share_visibility_in_hotseat)}, - {"share_wonders_in_hotseat" , false, offsetof (struct c3x_config, share_wonders_in_hotseat)}, - {"allow_precision_strikes_against_tile_improvements" , false, offsetof (struct c3x_config, allow_precision_strikes_against_tile_improvements)}, - {"dont_end_units_turn_after_bombarding_barricade" , false, offsetof (struct c3x_config, dont_end_units_turn_after_bombarding_barricade)}, - {"remove_land_artillery_target_restrictions" , false, offsetof (struct c3x_config, remove_land_artillery_target_restrictions)}, - {"allow_bombard_of_other_improvs_on_occupied_airfield" , false, offsetof (struct c3x_config, allow_bombard_of_other_improvs_on_occupied_airfield)}, - {"show_total_city_count" , false, offsetof (struct c3x_config, show_total_city_count)}, - {"strengthen_forbidden_palace_ocn_effect" , false, offsetof (struct c3x_config, strengthen_forbidden_palace_ocn_effect)}, - {"allow_upgrades_in_any_city" , false, offsetof (struct c3x_config, allow_upgrades_in_any_city)}, - {"do_not_generate_volcanos" , false, offsetof (struct c3x_config, do_not_generate_volcanos)}, - {"do_not_pollute_impassable_tiles" , false, offsetof (struct c3x_config, do_not_pollute_impassable_tiles)}, - {"show_hp_of_stealth_attack_options" , false, offsetof (struct c3x_config, show_hp_of_stealth_attack_options)}, - {"exclude_invisible_units_from_stealth_attack" , false, offsetof (struct c3x_config, exclude_invisible_units_from_stealth_attack)}, - {"convert_to_landmark_after_planting_forest" , false, offsetof (struct c3x_config, convert_to_landmark_after_planting_forest)}, - {"allow_sale_of_aqueducts_and_hospitals" , false, offsetof (struct c3x_config, allow_sale_of_aqueducts_and_hospitals)}, - {"no_cross_shore_detection" , false, offsetof (struct c3x_config, no_cross_shore_detection)}, - {"limit_unit_loading_to_one_transport_per_turn" , false, offsetof (struct c3x_config, limit_unit_loading_to_one_transport_per_turn)}, - {"prevent_old_units_from_upgrading_past_ability_block" , false, offsetof (struct c3x_config, prevent_old_units_from_upgrading_past_ability_block)}, - {"draw_forests_over_roads_and_railroads" , false, offsetof (struct c3x_config, draw_forests_over_roads_and_railroads)}, - {"enable_districts" , false, offsetof (struct c3x_config, enable_districts)}, - {"enable_neighborhood_districts" , false, offsetof (struct c3x_config, enable_neighborhood_districts)}, - {"enable_wonder_districts" , false, offsetof (struct c3x_config, enable_wonder_districts)}, - {"enable_natural_wonders" , false, offsetof (struct c3x_config, enable_natural_wonders)}, - {"enable_distribution_hub_districts" , false, offsetof (struct c3x_config, enable_distribution_hub_districts)}, - {"enable_aerodrome_districts" , false, offsetof (struct c3x_config, enable_aerodrome_districts)}, - {"completed_wonder_districts_can_be_destroyed" , false, offsetof (struct c3x_config, completed_wonder_districts_can_be_destroyed)}, - {"destroyed_wonders_can_be_built_again" , false, offsetof (struct c3x_config, destroyed_wonders_can_be_built_again)}, - {"cities_with_mutual_district_receive_buildings" , false, offsetof (struct c3x_config, cities_with_mutual_district_receive_buildings)}, - {"cities_with_mutual_district_receive_wonders" , false, offsetof (struct c3x_config, cities_with_mutual_district_receive_wonders)}, - {"show_message_when_building_received_by_mutual_district", false, offsetof (struct c3x_config, show_message_when_building_received_by_mutual_district)}, - {"air_units_use_aerodrome_districts_not_cities" , false, offsetof (struct c3x_config, air_units_use_aerodrome_districts_not_cities)}, - {"show_natural_wonder_name_on_map" , false, offsetof (struct c3x_config, show_natural_wonder_name_on_map)}, - {"ai_defends_districts" , false, offsetof (struct c3x_config, ai_defends_districts)}, - {"enable_city_work_radii_highlights" , false, offsetof (struct c3x_config, enable_city_work_radii_highlights)}, - {"introduce_all_human_players_at_start_of_hotseat_game" , false, offsetof (struct c3x_config, introduce_all_human_players_at_start_of_hotseat_game)}, - {"allow_unload_from_army" , false, offsetof (struct c3x_config, allow_unload_from_army)}, - {"no_land_anti_air_from_inside_naval_transport" , false, offsetof (struct c3x_config, no_land_anti_air_from_inside_naval_transport)}, - {"prevent_enslaving_by_bombardment" , false, offsetof (struct c3x_config, prevent_enslaving_by_bombardment)}, - {"allow_adjacent_resources_of_different_types" , false, offsetof (struct c3x_config, allow_adjacent_resources_of_different_types)}, - {"allow_sale_of_small_wonders" , false, offsetof (struct c3x_config, allow_sale_of_small_wonders)}, - }; + if (! district_is_complete (tile, AERODROME_DISTRICT_ID)) + return false; - struct integer_config_option { - char * name; - int base_val; - int offset; - } integer_config_options[] = { - {"limit_railroad_movement" , 0, offsetof (struct c3x_config, limit_railroad_movement)}, - {"minimum_city_separation" , 1, offsetof (struct c3x_config, minimum_city_separation)}, - {"anarchy_length_percent" , 100, offsetof (struct c3x_config, anarchy_length_percent)}, - {"ai_multi_city_start" , 0, offsetof (struct c3x_config, ai_multi_city_start)}, - {"max_tries_to_place_fp_city" , 10000, offsetof (struct c3x_config, max_tries_to_place_fp_city)}, - {"ai_research_multiplier" , 100, offsetof (struct c3x_config, ai_research_multiplier)}, - {"ai_settler_perfume_on_founding_duration" , 0, offsetof (struct c3x_config, ai_settler_perfume_on_founding_duration)}, - {"extra_unit_maintenance_per_shields" , 0, offsetof (struct c3x_config, extra_unit_maintenance_per_shields)}, - {"ai_build_artillery_ratio" , 16, offsetof (struct c3x_config, ai_build_artillery_ratio)}, - {"ai_artillery_value_damage_percent" , 50, offsetof (struct c3x_config, ai_artillery_value_damage_percent)}, - {"ai_build_bomber_ratio" , 70, offsetof (struct c3x_config, ai_build_bomber_ratio)}, - {"max_ai_naval_escorts" , 3, offsetof (struct c3x_config, max_ai_naval_escorts)}, - {"ai_worker_requirement_percent" , 150, offsetof (struct c3x_config, ai_worker_requirement_percent)}, - {"chance_for_nukes_to_destroy_max_one_hp_units" , 100, offsetof (struct c3x_config, chance_for_nukes_to_destroy_max_one_hp_units)}, - {"rebase_range_multiplier" , 6, offsetof (struct c3x_config, rebase_range_multiplier)}, - {"elapsed_minutes_per_day_night_hour_transition" , 3, offsetof (struct c3x_config, elapsed_minutes_per_day_night_hour_transition)}, - {"fixed_hours_per_turn_for_day_night_cycle" , 1, offsetof (struct c3x_config, fixed_hours_per_turn_for_day_night_cycle)}, - {"pinned_hour_for_day_night_cycle" , 0, offsetof (struct c3x_config, pinned_hour_for_day_night_cycle)}, - {"years_to_double_building_culture" , 1000, offsetof (struct c3x_config, years_to_double_building_culture)}, - {"tourism_time_scale_percent" , 100, offsetof (struct c3x_config, tourism_time_scale_percent)}, - {"luxury_randomized_appearance_rate_percent" , 100, offsetof (struct c3x_config, luxury_randomized_appearance_rate_percent)}, - {"tiles_per_non_luxury_resource" , 32, offsetof (struct c3x_config, tiles_per_non_luxury_resource)}, - {"city_limit" , 2048, offsetof (struct c3x_config, city_limit)}, - {"maximum_pop_before_neighborhood_needed" , 8, offsetof (struct c3x_config, maximum_pop_before_neighborhood_needed)}, - {"per_neighborhood_pop_growth_enabled" , 2, offsetof (struct c3x_config, per_neighborhood_pop_growth_enabled)}, - {"minimum_natural_wonder_separation" , 10, offsetof (struct c3x_config, minimum_natural_wonder_separation)}, - {"distribution_hub_food_yield_divisor" , 1, offsetof (struct c3x_config, distribution_hub_food_yield_divisor)}, - {"distribution_hub_shield_yield_divisor" , 1, offsetof (struct c3x_config, distribution_hub_shield_yield_divisor)}, - {"ai_ideal_distribution_hub_count_per_100_cities", 1, offsetof (struct c3x_config, ai_ideal_distribution_hub_count_per_100_cities)}, - {"neighborhood_needed_message_frequency" , 4, offsetof (struct c3x_config, neighborhood_needed_message_frequency)}, - }; + int territory_owner = tile->vtable->m38_Get_Territory_OwnerID (tile); + if (territory_owner != civ_id) + return false; - is->kernel32 = (*p_GetModuleHandleA) ("kernel32.dll"); - is->user32 = (*p_GetModuleHandleA) ("user32.dll"); - is->msvcrt = (*p_GetModuleHandleA) ("msvcrt.dll"); + if (require_available) { + int usage_mask; + if (itable_look_up (&is->aerodrome_airlift_usage, (int)tile, &usage_mask) && + (usage_mask & (1 << civ_id))) + return false; + } - // Remember the function names here are macros that expand to is->... - VirtualProtect = (void *)(*p_GetProcAddress) (is->kernel32, "VirtualProtect"); - CloseHandle = (void *)(*p_GetProcAddress) (is->kernel32, "CloseHandle"); - CreateFileA = (void *)(*p_GetProcAddress) (is->kernel32, "CreateFileA"); - GetFileSize = (void *)(*p_GetProcAddress) (is->kernel32, "GetFileSize"); - ReadFile = (void *)(*p_GetProcAddress) (is->kernel32, "ReadFile"); - LoadLibraryA = (void *)(*p_GetProcAddress) (is->kernel32, "LoadLibraryA"); - FreeLibrary = (void *)(*p_GetProcAddress) (is->kernel32, "FreeLibrary"); - MultiByteToWideChar = (void *)(*p_GetProcAddress) (is->kernel32, "MultiByteToWideChar"); - WideCharToMultiByte = (void *)(*p_GetProcAddress) (is->kernel32, "WideCharToMultiByte"); - GetLastError = (void *)(*p_GetProcAddress) (is->kernel32, "GetLastError"); - QueryPerformanceCounter = (void *)(*p_GetProcAddress) (is->kernel32, "QueryPerformanceCounter"); - QueryPerformanceFrequency = (void *)(*p_GetProcAddress) (is->kernel32, "QueryPerformanceFrequency"); - GetLocalTime = (void *)(*p_GetProcAddress) (is->kernel32, "GetLocalTime"); - MessageBoxA = (void *)(*p_GetProcAddress) (is->user32, "MessageBoxA"); - is->msimg32 = LoadLibraryA ("Msimg32.dll"); - TransparentBlt = (void *)(*p_GetProcAddress) (is->msimg32, "TransparentBlt"); - snprintf = (void *)(*p_GetProcAddress) (is->msvcrt, "_snprintf"); - malloc = (void *)(*p_GetProcAddress) (is->msvcrt, "malloc"); - calloc = (void *)(*p_GetProcAddress) (is->msvcrt, "calloc"); - realloc = (void *)(*p_GetProcAddress) (is->msvcrt, "realloc"); - free = (void *)(*p_GetProcAddress) (is->msvcrt, "free"); - strtol = (void *)(*p_GetProcAddress) (is->msvcrt, "strtol"); - strcmp = (void *)(*p_GetProcAddress) (is->msvcrt, "strcmp"); - strncmp = (void *)(*p_GetProcAddress) (is->msvcrt, "strncmp"); - strlen = (void *)(*p_GetProcAddress) (is->msvcrt, "strlen"); - strncpy = (void *)(*p_GetProcAddress) (is->msvcrt, "strncpy"); - strcpy = (void *)(*p_GetProcAddress) (is->msvcrt, "strcpy"); - strdup = (void *)(*p_GetProcAddress) (is->msvcrt, "_strdup"); - strstr = (void *)(*p_GetProcAddress) (is->msvcrt, "strstr"); - qsort = (void *)(*p_GetProcAddress) (is->msvcrt, "qsort"); - memcmp = (void *)(*p_GetProcAddress) (is->msvcrt, "memcmp"); - memcpy = (void *)(*p_GetProcAddress) (is->msvcrt, "memcpy"); - tolower = (void *)(*p_GetProcAddress) (is->msvcrt, "tolower"); + return true; +} - // Intercept the game's calls to MessageBoxA. We can't do this through the patcher since that would interfere with the runtime loader. - WITH_MEM_PROTECTION (p_MessageBoxA, 4, PAGE_READWRITE) - *p_MessageBoxA = patch_MessageBoxA; +bool +tile_has_friendly_port_district (Tile * tile, int civ_id) +{ + if (! is->current_config.enable_districts || + ! is->current_config.enable_port_districts || + (tile == NULL) || (tile == p_null_tile)) + return false; - // Set file path to mod's script.txt - snprintf (is->mod_script_path, sizeof is->mod_script_path, "%s\\Text\\c3x-script.txt", is->mod_rel_dir); - is->mod_script_path[(sizeof is->mod_script_path) - 1] = '\0'; + struct district_instance * inst = get_district_instance (tile); + if ((inst == NULL) || (inst->district_id != PORT_DISTRICT_ID)) + return false; - // Fill in base config - struct c3x_config base_config = {0}; - base_config.land_retreat_rules = RR_STANDARD; - base_config.sea_retreat_rules = RR_STANDARD; - base_config.ai_settler_perfume_on_founding = 0; - base_config.work_area_limit = WAL_NONE; - base_config.draw_lines_using_gdi_plus = LDO_WINE; - base_config.double_minimap_size = MDM_HIGH_DEF; - base_config.unit_cycle_search_criteria = UCSC_STANDARD; - base_config.city_work_radius = 2; - base_config.day_night_cycle_mode = DNCM_OFF; - for (int n = 0; n < ARRAY_LEN (boolean_config_options); n++) - *((char *)&base_config + boolean_config_options[n].offset) = boolean_config_options[n].base_val; - for (int n = 0; n < ARRAY_LEN (integer_config_options); n++) - *(int *)((byte *)&base_config + integer_config_options[n].offset) = integer_config_options[n].base_val; - memcpy (&is->base_config, &base_config, sizeof base_config); + if (! district_is_complete (tile, PORT_DISTRICT_ID)) + return false; - // Load labels - { - for (int n = 0; n < COUNT_C3X_LABELS; n++) - is->c3x_labels[n] = ""; + return tile->vtable->m38_Get_Territory_OwnerID (tile) == civ_id; +} - char labels_path[MAX_PATH]; - snprintf (labels_path, sizeof labels_path, "%s\\Text\\c3x-labels.txt", is->mod_rel_dir); - labels_path[(sizeof labels_path) - 1] = '\0'; - char * labels_file_contents = file_to_string (labels_path); +bool +city_has_required_district (City * city, int district_id) +{ + if ((city == NULL) || (district_id < 0) || (district_id >= is->district_count)) + return false; - if (labels_file_contents != NULL) { - char * cursor = labels_file_contents; - int n = 0; - while ((n < COUNT_C3X_LABELS) && (*cursor != '\0')) { - if (*cursor == '\n') - cursor++; - else if ((cursor[0] == '\r') && (cursor[1] == '\n')) - cursor += 2; - else if (*cursor == ';') { - while ((*cursor != '\0') && (*cursor != '\n')) - cursor++; - } else { - char * line_start = cursor; - while ((*cursor != '\0') && (*cursor != '\r') && (*cursor != '\n')) - cursor++; - int line_len = cursor - line_start; - if (NULL != (is->c3x_labels[n] = malloc (line_len + 1))) { - strncpy (is->c3x_labels[n], line_start, line_len); - is->c3x_labels[n][line_len] = '\0'; - } - n++; - } - } - free (labels_file_contents); + if (get_completed_district_tile_for_city (city, district_id, NULL, NULL) != NULL) { + clear_city_district_request (city, district_id); + return true; + } + return false; +} - } else { - char err_msg[500]; - snprintf (err_msg, sizeof err_msg, "Couldn't read labels from %s\nPlease make sure the file exists. If you moved the modded EXE you must move the mod folder after it.", labels_path); - err_msg[(sizeof err_msg) - 1] = '\0'; - MessageBoxA (NULL, err_msg, NULL, MB_ICONWARNING); +bool +city_has_wonder_district_with_no_completed_wonder (City * city, int wonder_improv_id) +{ + if (! is->current_config.enable_wonder_districts || (city == NULL)) + return false; + + FOR_DISTRICTS_AROUND (wai, city->Body.X, city->Body.Y, true) { + Tile * candidate = wai.tile; + struct district_instance * inst = wai.district_inst; + if (inst->district_id != WONDER_DISTRICT_ID) continue; + + struct wonder_district_info * info = get_wonder_district_info (candidate); + bool buildable = wonder_is_buildable_on_tile (candidate, wonder_improv_id); + if (info == NULL) return true; + if (info->state == WDS_COMPLETED) continue; + if (info->state == WDS_UNUSED && buildable) return true; + if (info->state == WDS_UNDER_CONSTRUCTION && buildable && info->city_id == city->Body.ID) { + info->city = city; + return true; // Reserved by this city } } - is->sb_next_up = NULL; - is->trade_net = p_original_trade_net; - is->city_limit = 512; - is->trade_net_addrs_load_state = IS_UNINITED; - is->trade_net_addrs = NULL; - is->tnx_cache = NULL; - is->is_computing_city_connections = false; - is->keep_tnx_cache = false; - is->must_recompute_resources_for_mill_inputs = false; - is->is_placing_scenario_things = false; - is->paused_for_popup = false; - is->time_spent_paused_during_popup = 0; - is->time_spent_computing_city_connections = 0; - is->count_calls_to_recompute_city_connections = 0; + return false; +} - is->have_job_and_loc_to_skip = 0; +int __fastcall patch_Leader_count_any_shared_wonders_with_flag (Leader * this, int edx, enum ImprovementTypeWonderFeatures flag, City * only_in_city); +int __fastcall patch_Leader_count_wonders_with_small_flag (Leader * this, int edx, enum ImprovementTypeSmallWonderFeatures flag, City * city_or_null); +void __fastcall patch_City_add_or_remove_improvement (City * this, int edx, int improv_id, int add, bool param_3); - // Initialize trade screen scroll vars - is->open_diplo_form_straight_to_trade = 0; - is->trade_screen_scroll_to_id = -1; - is->trade_scroll_button_left = is->trade_scroll_button_right = NULL; - is->trade_scroll_button_images = NULL; - is->trade_scroll_button_state = IS_UNINITED; - is->eligible_for_trade_scroll = 0; - memset (&is->saved_code_areas, 0, sizeof is->saved_code_areas); +void __fastcall +patch_Main_Screen_Form_show_map_message (Main_Screen_Form * this, int edx, int tile_x, int tile_y, char * text_key, bool pause) +{ + WITH_PAUSE_FOR_POPUP { + Main_Screen_Form_show_map_message (this, __, tile_x, tile_y, text_key, pause); + } +} - is->unit_menu_duplicates = NULL; +int __cdecl +patch_process_text_for_map_message (char * in, char * out) +{ + if (is->map_message_text_override != NULL) { + strcpy (out, is->map_message_text_override); + is->map_message_text_override = NULL; + return 0; + } else + return process_text_snippet (in, out); +} - is->memo = NULL; - is->memo_len = 0; - is->memo_capacity = 0; +// Works like show_map_message but takes a bit of text to display instead of a key for script.txt +void +show_map_specific_text (int tile_x, int tile_y, char const * text, bool pause) +{ + is->map_message_text_override = text; + patch_Main_Screen_Form_show_map_message (p_main_screen_form, __, tile_x, tile_y, "LANDCONQUER", pause); // Use any key here. It will be overridden. +} - // Fill in array mapping cultural NIs to standard ones. - is->cultural_ni_to_standard = malloc (MAX_CULTURAL_NI + 1); - for (int n = 0; n <= MAX_CULTURAL_NI; n++) { - char const * p = &cultural_ni_to_diffs[n << 1]; - is->cultural_ni_to_standard[n] = diff_to_neighbor_index (p[0], p[1], 1000); - } +bool __fastcall +patch_City_has_improvement (City * this, int edx, int improv_id, bool include_auto_improvements) +{ + bool tr = City_has_improvement (this, __, improv_id, include_auto_improvements); - // Fill in array mapping standard NIs to work radii AKA work ring numbers - for (int n = 0; n < ARRAY_LEN (is->ni_to_work_radius); n++) { - int work_radius = -1; - int dx, dy; - neighbor_index_to_diff (n, &dx, &dy); - for (int ring_no = 0; ring_no <= 7; ring_no++) { - for (int k = workable_tile_counts[ring_no-1]; k < workable_tile_counts[ring_no]; k++) { - char const * p = &cultural_ni_to_diffs[k<<1]; - if ((p[0] == dx) && (p[1] == dy)) { - work_radius = ring_no; + // Check if the improvement is provided for free by another human player's wonder if we're in a hotseat game and the config option is on + if ((! tr) && + include_auto_improvements && + is->current_config.share_wonders_in_hotseat && + ((1 << this->Body.CivID) & *p_human_player_bits) && + (*p_is_offline_mp_game && ! *p_is_pbem_game)) { // if we're in a hotseat game + + // Loop over every other human player in the game and check if the city would have the improv if they were its owner + int actual_owner_id = this->Body.CivID; + unsigned player_bits = *(unsigned *)p_human_player_bits >> 1; + int n_player = 1; + while (player_bits != 0) { + if ((player_bits & 1) && (n_player != actual_owner_id)) { + this->Body.CivID = n_player; + if (City_has_improvement (this, __, improv_id, include_auto_improvements)) { + tr = true; break; } } - if (work_radius != -1) - break; + player_bits >>= 1; + n_player++; } - is->ni_to_work_radius[n] = work_radius; + this->Body.CivID = actual_owner_id; + } - is->city_loc_display_perspective = -1; + return tr; +} - is->aliased_civ_noun_bits = is->aliased_civ_adjective_bits = is->aliased_civ_formal_name_bits = is->aliased_leader_name_bits = is->aliased_leader_title_bits = 0; +int +count_neighborhoods_in_city_radius (City * city) +{ + if (! is->current_config.enable_districts || + ! is->current_config.enable_neighborhood_districts || + (city == NULL)) + return 0; - is->extra_available_resources = NULL; - is->extra_available_resources_capacity = 0; + int count = 0; + FOR_DISTRICTS_AROUND (wai, city->Body.X, city->Body.Y, true) { + struct district_instance * inst = wai.district_inst; + if (inst != NULL && + inst->district_id >= 0 && inst->district_id < is->district_count && + inst->district_id == NEIGHBORHOOD_DISTRICT_ID) + count++; + } + return count; +} - memset (is->interceptor_reset_lists, 0, sizeof is->interceptor_reset_lists); +int +count_utilized_neighborhoods_in_city_radius (City * city) +{ + if (! is->current_config.enable_districts || + ! is->current_config.enable_neighborhood_districts || + (city == NULL)) + return 0; - is->ai_prod_valuations = NULL; - is->count_ai_prod_valuations = 0; - is->ai_prod_valuations_capacity = 0; + int base_cap = is->current_config.maximum_pop_before_neighborhood_needed; + if (base_cap <= 0) + return 0; - is->mill_tiles = NULL; - is->count_mill_tiles = 0; - is->mill_tiles_capacity = 0; - is->saved_tile_count = -1; - is->mill_input_resource_bits = NULL; + int per_neighborhood = is->current_config.per_neighborhood_pop_growth_enabled; + if (per_neighborhood <= 0) + return 0; - is->drawing_icons_for_improv_id = -1; + int pop = city->Body.Population.Size; + if (pop <= base_cap) + return 0; - is->resources_sheet = NULL; + int excess_pop = pop - base_cap; + int utilized = (excess_pop + per_neighborhood - 1) / per_neighborhood; + int total_neighborhoods = count_neighborhoods_in_city_radius (city); - is->modifying_gold_trade = NULL; + if (utilized > total_neighborhoods) + utilized = total_neighborhoods; - is->bombard_stealth_target = NULL; - is->selecting_stealth_target_for_bombard = 0; + return utilized; +} - is->map_message_text_override = NULL; - - is->load_file_path_override = NULL; - - is->replay_for_players = 0; +int +get_neighborhood_pop_cap (City * city) +{ + if (! is->current_config.enable_districts || + ! is->current_config.enable_neighborhood_districts || + (city == NULL)) + return -1; - is->suppress_intro_after_load_popup = 0; + int base_cap = is->current_config.maximum_pop_before_neighborhood_needed; + if (base_cap <= 0) + return -1; - is->force_barb_activity_for_cities = 0; + int per_neighborhood = is->current_config.per_neighborhood_pop_growth_enabled; + if (per_neighborhood < 0) + per_neighborhood = 0; - is->dummy_tile = calloc (1, sizeof *is->dummy_tile); + int neighborhoods = count_neighborhoods_in_city_radius (city); + long long cap = (long long)base_cap + (long long)per_neighborhood * neighborhoods; + if (cap < base_cap) + cap = base_cap; - is->bombarding_unit = NULL; + return (int)cap; +} - is->unit_bombard_attacking_tile = NULL; - is->attacking_tile_x = is->attacking_tile_y = -1; +bool +city_is_at_neighborhood_cap (City * city) +{ + if (! is->current_config.enable_districts || + ! is->current_config.enable_neighborhood_districts || + (city == NULL)) + return false; - is->temporarily_disallow_lethal_zoc = false; - is->moving_unit_to_adjacent_tile = false; - is->showing_hotseat_replay = false; - is->getting_tile_occupier_for_ai_pathfinding = false; + int cap = get_neighborhood_pop_cap (city); + if (cap <= 0) + return false; - is->running_on_wine = false; { - HMODULE ntdll = (*p_GetModuleHandleA) ("ntdll.dll"); - is->running_on_wine = (ntdll != NULL) && ((*p_GetProcAddress) (ntdll, "wine_get_version") != NULL); - } + return city->Body.Population.Size >= cap; +} - is->gdi_plus.init_state = IS_UNINITED; +void +ensure_neighborhood_request_for_city (City * city) +{ + if (! is->current_config.enable_districts || + ! is->current_config.enable_neighborhood_districts || + (city == NULL)) + return; - is->water_trade_improvs = (struct improv_id_list) {0}; - is->air_trade_improvs = (struct improv_id_list) {0}; - is->combat_defense_improvs = (struct improv_id_list) {0}; + if (! leader_can_build_district (&leaders[city->Body.CivID], NEIGHBORHOOD_DISTRICT_ID)) + return; - is->unit_display_override = (struct unit_display_override) {-1, -1, -1}; + mark_city_needs_district (city, NEIGHBORHOOD_DISTRICT_ID); +} - is->dbe = (struct defensive_bombard_event) {0}; +void +calculate_district_culture_science_bonuses (City * city, int * culture_bonus, int * science_bonus) +{ + if (culture_bonus != NULL) + *culture_bonus = 0; + if (science_bonus != NULL) + *science_bonus = 0; - memset (&is->boolean_config_offsets, 0, sizeof is->boolean_config_offsets); - for (int n = 0; n < ARRAY_LEN (boolean_config_options); n++) - stable_insert (&is->boolean_config_offsets, boolean_config_options[n].name, boolean_config_options[n].offset); - memset (&is->integer_config_offsets, 0, sizeof is->integer_config_offsets); - for (int n = 0; n < ARRAY_LEN (integer_config_options); n++) - stable_insert (&is->integer_config_offsets, integer_config_options[n].name, integer_config_options[n].offset); + if (! (is->current_config.enable_districts || is->current_config.enable_natural_wonders) || (city == NULL)) + return; - memset (&is->unit_type_alt_strategies, 0, sizeof is->unit_type_alt_strategies); - memset (&is->unit_type_duplicates , 0, sizeof is->unit_type_duplicates); - memset (&is->extra_defensive_bombards, 0, sizeof is->extra_defensive_bombards); - memset (&is->airdrops_this_turn , 0, sizeof is->airdrops_this_turn); - memset (&is->unit_transport_ties , 0, sizeof is->unit_transport_ties); - memset (&is->extra_city_improvs , 0, sizeof is->extra_city_improvs); + int total_culture = 0; + int total_science = 0; + int utilized_neighborhoods = count_utilized_neighborhoods_in_city_radius (city); - is->unit_type_count_init_bits = 0; - for (int n = 0; n < 32; n++) - memset (&is->unit_type_counts[n], 0, sizeof is->unit_type_counts[n]); + FOR_DISTRICTS_AROUND (wai, city->Body.X, city->Body.Y, true) { + if ((wai.dx == 0) && (wai.dy == 0)) + continue; + Tile * tile = wai.tile; + struct district_instance * inst = wai.district_inst; + int district_id = inst->district_id; - is->penciled_in_upgrades = NULL; - is->penciled_in_upgrade_count = is->penciled_in_upgrade_capacity = 0; + struct district_config const * cfg = &is->district_configs[district_id]; + int district_culture_bonus = 0; + int district_science_bonus = 0; + get_effective_district_yields (inst, cfg, NULL, NULL, NULL, &district_science_bonus, &district_culture_bonus, NULL); - is->currently_capturing_city = NULL; - is->accessing_save_file = NULL; + bool is_neighborhood = (cfg->command == UCV_Build_Neighborhood); + if (is_neighborhood) { + if (utilized_neighborhoods > 0) { + total_culture += district_culture_bonus; + total_science += district_science_bonus; + utilized_neighborhoods--; + } + } else { + total_culture += district_culture_bonus; + total_science += district_science_bonus; + } + } - is->drawn_strat_resource_count = 0; + if (culture_bonus != NULL) + *culture_bonus = total_culture; + if (science_bonus != NULL) + *science_bonus = total_science; +} - is->charmed_types_converted_to_ptw_arty = NULL; - is->count_charmed_types_converted_to_ptw_arty = 0; - is->charmed_types_converted_to_ptw_arty_capacity = 0; +void +calculate_district_happiness_bonus (City * city, int * happiness_bonus) +{ + if (happiness_bonus != NULL) + *happiness_bonus = 0; - is->checking_visibility_for_unit = NULL; + if (! is->current_config.enable_districts || (city == NULL)) + return; - is->do_not_bounce_invisible_units = false; - is->always_despawn_passengers = false; - is->do_not_enslave_units = false; + int total_happy = 0; + int utilized_neighborhoods = count_utilized_neighborhoods_in_city_radius (city); - is->saved_improv_counts = NULL; - is->saved_improv_counts_capacity = 0; + FOR_DISTRICTS_AROUND (wai, city->Body.X, city->Body.Y, true) { + if ((wai.dx == 0) && (wai.dy == 0)) + continue; - memset (is->last_main_screen_key_up_events, 0, sizeof is->last_main_screen_key_up_events); + struct district_instance * inst = wai.district_inst; + int district_id = inst->district_id; + struct district_config const * cfg = &is->district_configs[district_id]; - reset_district_state (true); + bool is_neighborhood = district_id == NEIGHBORHOOD_DISTRICT_ID; + if (is_neighborhood && (utilized_neighborhoods <= 0)) + continue; - is->natural_wonder_count = 0; + if (is_neighborhood) + utilized_neighborhoods--; - is->sharing_buildings_by_districts_in_progress = false; - is->can_load_transport = is->can_load_passenger = NULL; + int district_happy = 0; + get_effective_district_yields (inst, cfg, NULL, NULL, NULL, NULL, NULL, &district_happy); + if (district_happy != 0) + total_happy += district_happy; + } - is->last_selected_unit.initial_x = is->last_selected_unit.initial_y = -1; - is->last_selected_unit.last_x = is->last_selected_unit.last_y = is->last_selected_unit.type_id = -1; - is->last_selected_unit.ptr = NULL; + if (happiness_bonus != NULL) + *happiness_bonus = total_happy; +} - is->waiting_units = (struct table) {0}; - is->have_loaded_waiting_units = false; +int __fastcall +patch_City_requires_improvement_to_grow (City * this) +{ + int required_improv = City_requires_improvement_to_grow (this); + if ((required_improv == -1) && + (this != NULL) && + is->current_config.enable_districts && + is->current_config.enable_neighborhood_districts && + city_is_at_neighborhood_cap (this)) { + return 0; // Neighborhood sentinel + } - is->extra_capture_despawns = NULL; - is->count_extra_capture_despawns = 0; - is->extra_capture_despawns_capacity = 0; + return required_improv; +} - is->loaded_config_names = NULL; - reset_to_base_config (); - apply_machine_code_edits (&is->current_config, true); +// Replacement check specifically for stalled growth check function, +// where we can't pass through the neighborhood sentinel. That function itself +// doesn't block growth, it just triggers the dialog, so safe to skip it if +// neighborhoods are needed. +int __fastcall +patch_City_requires_improvement_to_grow_besides_neighborhood (City * this) +{ + return City_requires_improvement_to_grow (this); } -void -get_mod_art_path (char const * file_name, char * out_path, int path_buf_size) +void __fastcall +patch_maybe_show_improvement_needed_for_growth_dialog (void * this, int edx, City * city, int * param_2) { - char s[1000]; - snprintf (s, sizeof s, "Art\\%s", file_name); - s[(sizeof s) - 1] = '\0'; + int required_improv_id = (int)param_2; + if (is->current_config.enable_districts && + is->current_config.enable_neighborhood_districts && + (required_improv_id == 0)) // Neighborhood sentinel + return; - char * scenario_path = BIC_get_asset_path (p_bic_data, __, s, false); - if (0 != strcmp (scenario_path, s)) // get_asset_path returns its input when the file is not found - snprintf (out_path, path_buf_size, "%s", scenario_path); - else - snprintf (out_path, path_buf_size, "%s\\Art\\%s", is->mod_rel_dir, file_name); - out_path[path_buf_size - 1] = '\0'; + maybe_show_improvement_needed_for_growth_dialog (this, __, city, param_2); } void -init_stackable_command_buttons () +maybe_show_neighborhood_growth_warning (City * city) { - if (is->sc_img_state != IS_UNINITED) - return; - - PCX_Image pcx; - PCX_Image_construct (&pcx); - for (int sc = 0; sc < COUNT_STACKABLE_COMMANDS; sc++) - for (int n = 0; n < 4; n++) - Sprite_construct (&is->sc_button_image_sets[sc].imgs[n]); + if ((city == NULL) || ! (is->current_config.enable_districts && is->current_config.enable_neighborhood_districts)) return; + int requirement = patch_City_requires_improvement_to_grow (city); + if (requirement != 0) return; // Neighborhood sentinel not present + if (city->Body.FoodIncome <= 0) return; // Not growing + if (is_online_game ()) return; + int civ_id = city->Body.CivID; + if (civ_id != p_main_screen_form->Player_CivID) return; + if ((*p_human_player_bits & (1u << civ_id)) == 0) return; - char temp_path[2*MAX_PATH]; + unsigned int turn_no = (unsigned int)*p_current_turn_no; + unsigned int throttle = ((unsigned int)city->Body.X << 5) ^ (unsigned int)city->Body.Y; + int frequency = is->current_config.neighborhood_needed_message_frequency; + if ((frequency <= 0) || (((turn_no + throttle) % frequency) != 0)) + return; - is->sb_activated_by_button = 0; - is->sc_img_state = IS_INIT_FAILED; + char msg[160]; + char const * city_name = city->Body.CityName; + snprintf (msg, sizeof msg, "%s %s %s %s", + city_name, + is->c3x_labels[CL_REQUIRES], + is->district_configs[NEIGHBORHOOD_DISTRICT_ID].display_name, + is->c3x_labels[CL_TO_GROW] + ); + msg[(sizeof msg) - 1] = '\0'; + show_map_specific_text (city->Body.X, city->Body.Y, msg, true); +} - char const * filenames[4] = {"StackedNormButtons.pcx", "StackedRolloverButtons.pcx", "StackedHighlightedButtons.pcx", "StackedButtonsAlpha.pcx"}; - for (int n = 0; n < 4; n++) { - get_mod_art_path (filenames[n], temp_path, sizeof temp_path); - PCX_Image_read_file (&pcx, __, temp_path, NULL, 0, 0x100, 2); - if (pcx.JGL.Image == NULL) { - (*p_OutputDebugStringA) ("[C3X] Failed to load stacked command buttons sprite sheet.\n"); - for (int sc = 0; sc < COUNT_STACKABLE_COMMANDS; sc++) - for (int k = 0; k < 4; k++) { - Sprite * sprite = &is->sc_button_image_sets[sc].imgs[k]; - sprite->vtable->destruct (sprite, __, 0); - } - pcx.vtable->destruct (&pcx, __, 0); - return; - } - - for (int sc = 0; sc < COUNT_STACKABLE_COMMANDS; sc++) { - int x = 32 * sc_button_infos[sc].tile_sheet_column, - y = 32 * sc_button_infos[sc].tile_sheet_row; - Sprite_slice_pcx (&is->sc_button_image_sets[sc].imgs[n], __, &pcx, x, y, 32, 32, 1, 0); - } +bool __stdcall +patch_is_not_pop_capped_or_starving (City * city) +{ + bool tr = is_not_pop_capped_or_starving (city); + if (! tr) return false; - pcx.vtable->clear_JGL (&pcx); + if (is->current_config.enable_districts) { + if (city_is_at_neighborhood_cap (city)) + return false; } - is->sc_img_state = IS_OK; - pcx.vtable->destruct (&pcx, __, 0); + return true; +} + +bool +remove_building_if_no_district (City * city, int district_id, int building_id) +{ + if ((city == NULL) || (building_id < 0)) return false; + if (! patch_City_has_improvement (city, __, building_id, false)) return false; + if (city_has_required_district (city, district_id)) return false; + + patch_City_add_or_remove_improvement (city, __, building_id, 0, false); + return true; } void -init_disabled_command_buttons () +reduce_city_population_due_to_lost_neighborhood (City * city) { - if (is->disabled_command_img_state != IS_UNINITED) + if (city == NULL) return; - is->disabled_command_img_state = IS_INIT_FAILED; + if (! (is->current_config.enable_districts && + is->current_config.enable_neighborhood_districts && + is->current_config.destroying_neighborhood_reduces_pop)) + return; - PCX_Image pcx; - PCX_Image_construct (&pcx); + int base_cap = is->current_config.maximum_pop_before_neighborhood_needed; + int per_neighborhood = is->current_config.per_neighborhood_pop_growth_enabled; + if ((base_cap <= 0) || (per_neighborhood <= 0)) + return; - char temp_path[2*MAX_PATH]; - get_mod_art_path ("DisabledButtons.pcx", temp_path, sizeof temp_path); - PCX_Image_read_file (&pcx, __, temp_path, NULL, 0, 0x100, 2); - if (pcx.JGL.Image == NULL) { - (*p_OutputDebugStringA) ("[C3X] Failed to load disabled command buttons sprite sheet.\n"); - pcx.vtable->destruct (&pcx, __, 0); + int neighborhoods = count_neighborhoods_in_city_radius (city); + int cap = base_cap + (per_neighborhood * neighborhoods); + if (cap < base_cap) + cap = base_cap; + + int pop = city->Body.Population.Size; + if (pop <= cap) return; - } - Sprite_construct (&is->disabled_build_city_button_img); - Sprite_slice_pcx (&is->disabled_build_city_button_img, __, &pcx, 32*5, 32*2, 32, 32, 1, 0); + int to_remove = pop - cap; + int removed = 0; + while (to_remove-- > 0) { + City_remove_population (city, __, 1, -1, '\0'); + removed++; + } - is->disabled_command_img_state = IS_OK; - pcx.vtable->destruct (&pcx, __, 0); + if ((removed > 0) && ((*p_human_player_bits & (1 << city->Body.CivID)) != 0)) { + char msg[160]; + snprintf (msg, sizeof msg, "%s %s %s", + city->Body.CityName, + is->c3x_labels[CL_LOST_POPULATION_DUE_TO_DESTROYED_NEIGHBORHOOD], + is->district_configs[NEIGHBORHOOD_DISTRICT_ID].display_name + ); + msg[(sizeof msg) - 1] = '\0'; + show_map_specific_text (city->Body.X, city->Body.Y, msg, true); + } } -void -deinit_stackable_command_buttons () +bool +city_has_other_completed_district (City * city, int district_id, int removed_x, int removed_y) { - if (is->sc_img_state == IS_OK) - for (int sc = 0; sc < COUNT_STACKABLE_COMMANDS; sc++) - for (int n = 0; n < 4; n++) { - Sprite * sprite = &is->sc_button_image_sets[sc].imgs[n]; - sprite->vtable->destruct (sprite, __, 0); - } - is->sc_img_state = IS_UNINITED; -} + if (! is->current_config.enable_districts || + (city == NULL) || (district_id < 0) || (district_id >= is->district_count)) + return false; -void -deinit_disabled_command_buttons () -{ - Sprite * sprite = &is->disabled_build_city_button_img; - if (is->disabled_command_img_state == IS_OK) - sprite->vtable->destruct (sprite, __, 0); - memset (sprite, 0, sizeof *sprite); - is->disabled_command_img_state = IS_UNINITED; + FOR_DISTRICTS_AROUND (wai, city->Body.X, city->Body.Y, true) { + int x = wai.tile_x, y = wai.tile_y; + Tile * candidate = wai.tile; + if ((x == removed_x) && (y == removed_y)) + continue; + + struct district_instance * inst = wai.district_inst; + if (inst->district_id != district_id) + continue; + + return true; + } + + return false; } -void -init_tile_highlights () +bool +district_instance_is_redundant (struct district_instance * inst, Tile * tile) { - if (is->tile_highlight_state != IS_UNINITED) - return; + if (! is->current_config.enable_districts || + (inst == NULL) || + (tile == NULL) || (tile == p_null_tile)) + return false; - PCX_Image pcx; - PCX_Image_construct (&pcx); + int district_id = inst->district_id; + if ((district_id < 0) || (district_id >= is->district_count)) + return false; - char temp_path[2*MAX_PATH]; + if (! district_is_complete (tile, district_id)) + return false; - is->tile_highlight_state = IS_INIT_FAILED; + if (district_id == NEIGHBORHOOD_DISTRICT_ID || district_id == DISTRIBUTION_HUB_DISTRICT_ID) + return false; - snprintf (temp_path, sizeof temp_path, "%s\\Art\\TileHighlights.pcx", is->mod_rel_dir); - temp_path[(sizeof temp_path) - 1] = '\0'; - PCX_Image_read_file (&pcx, __, temp_path, NULL, 0, 0x100, 2); - if (pcx.JGL.Image == NULL) { - (*p_OutputDebugStringA) ("[C3X] Failed to load stacked command buttons sprite sheet.\n"); - goto cleanup; - } + int tile_x = inst->tile_x; + int tile_y = inst->tile_y; + if (! district_instance_get_coords (inst, tile, &tile_x, &tile_y)) + return false; - for (int n = 0; n < COUNT_TILE_HIGHLIGHTS; n++) - Sprite_slice_pcx (&is->tile_highlights[n], __, &pcx, 128*n, 0, 128, 64, 1, 0); + int civ_id = tile->vtable->m38_Get_Territory_OwnerID (tile); - is->tile_highlight_state = IS_OK; -cleanup: - pcx.vtable->destruct (&pcx, __, 0); + if (district_id == WONDER_DISTRICT_ID) + return inst->wonder_info.state == WDS_UNUSED; + + FOR_CITIES_AROUND (wai, tile_x, tile_y) { + if (! city_has_other_completed_district (wai.city, district_id, tile_x, tile_y)) + return false; + } + + return true; } -void -init_unit_rcm_icons () +bool +any_nearby_city_would_lose_district_benefits (int district_id, int civ_id, int removed_x, int removed_y) { - if (is->unit_rcm_icon_state != IS_UNINITED) - return; + if (! is->current_config.enable_districts) + return false; - PCX_Image pcx; - PCX_Image_construct (&pcx); + if (district_id < 0 || district_id >= is->district_count) + return false; - char temp_path[2*MAX_PATH]; - get_mod_art_path ("UnitRCMIcons.pcx", temp_path, sizeof temp_path); + struct district_infos * info = &is->district_infos[district_id]; - PCX_Image_read_file (&pcx, __, temp_path, NULL, 0, 0x100, 2); - if ((pcx.JGL.Image == NULL) || - (pcx.JGL.Image->vtable->m54_Get_Width (pcx.JGL.Image) < 57) || - (pcx.JGL.Image->vtable->m55_Get_Height (pcx.JGL.Image) < 64)) { - (*p_OutputDebugStringA) ("[C3X] PCX file for unit RCM icons failed to load or is too small.\n"); - goto cleanup; - } + // If there are no dependent buildings, no city can lose benefits + if (info->dependent_building_count == 0) + return false; - for (int set = 0; set < COUNT_UNIT_RCM_ICON_SETS; set++) { - Sprite * icons = &is->unit_rcm_icons[set * COUNT_UNIT_RCM_ICONS]; - for (int n = 0; n < COUNT_UNIT_RCM_ICONS; n++) { - Sprite_construct (&icons[n]); - Sprite_slice_pcx (&icons[n], __, &pcx, 1 + 14*set, 1 + 16*n, 14, 15, 1, 0); + // Check all cities within work radius of the removed district + FOR_CITIES_AROUND (wai, removed_x, removed_y) { + City * city = wai.city; + + // Check if this city has another completed district of the same type nearby (excluding the one being removed) + if (city_has_other_completed_district (city, district_id, removed_x, removed_y)) + continue; + + // This city doesn't have another district, check if it has any dependent buildings + for (int i = 0; i < info->dependent_building_count; i++) { + int building_id = info->dependent_building_ids[i]; + if (building_id >= 0 && patch_City_has_improvement (city, __, building_id, false)) + return true; } } - is->unit_rcm_icon_state = IS_OK; -cleanup: - pcx.vtable->destruct (&pcx, __, 0); + return false; } void -deinit_unit_rcm_icons () +remove_dependent_buildings_for_district (int district_id, int center_x, int center_y) { - if (is->unit_rcm_icon_state == IS_OK) { - int total_icon_count = COUNT_UNIT_RCM_ICONS * COUNT_UNIT_RCM_ICON_SETS; - for (int n = 0; n < total_icon_count; n++) { - Sprite * sprite = &is->unit_rcm_icons[n]; - sprite->vtable->destruct (sprite, __, 0); + if (! is->current_config.enable_districts || (district_id < 0) || (district_id >= is->district_count)) + return; + + struct district_infos * info = &is->district_infos[district_id]; + + if ((center_x < 0) || (center_y < 0) || + (center_x >= p_bic_data->Map.Width) || (center_y >= p_bic_data->Map.Height)) + return; + + FOR_CITIES_AROUND (wai, center_x, center_y) { + City * city = wai.city; + + if (city_has_other_completed_district (city, district_id, center_x, center_y)) + continue; + + int removed_count = 0; + int first_building_id = -1; + + for (int i = 0; i < info->dependent_building_count; i++) { + int building_id = info->dependent_building_ids[i]; + if (building_id >= 0) { + // This also loops through tiles around the city but is not redundant, as the city + // may have multiple districts of the same type in its radius (eg outside radius of this particular district) + if (remove_building_if_no_district (city, district_id, building_id)) { + if (removed_count == 0) + first_building_id = building_id; + removed_count++; + } + } + } + + if ((removed_count > 0) && + is->current_config.show_message_when_building_lost_to_destroyed_district && + ((*p_human_player_bits & (1 << city->Body.CivID)) != 0)) { + char msg[200]; + char const * district_name = is->district_configs[district_id].name; + char const * building_name = (first_building_id >= 0) ? p_bic_data->Improvements[first_building_id].Name.S : ""; + + if (removed_count == 1) + snprintf (msg, sizeof msg, "%s%s %s %s %s", + city->Body.CityName, + is->c3x_labels[CL_APOSTROPHE_S], + building_name, + is->c3x_labels[CL_LOST_DUE_TO_DESTROYED], + district_name); + else + snprintf (msg, sizeof msg, "%s%s %s %s %d %s %s %s", + city->Body.CityName, + is->c3x_labels[CL_APOSTROPHE_S], + building_name, + is->c3x_labels[CL_AND], + removed_count - 1, + is->c3x_labels[CL_OTHER_BUILDINGS_HAVE_BEEN], + is->c3x_labels[CL_LOST_DUE_TO_DESTROYED], + district_name); + + msg[(sizeof msg) - 1] = '\0'; + show_map_specific_text (city->Body.X, city->Body.Y, msg, true); } - is->unit_rcm_icon_state = IS_UNINITED; } } void -init_red_food_icon () +remove_wonder_improvement_for_destroyed_district (int wonder_improv_id) { - if (is->red_food_icon_state != IS_UNINITED) + if ((wonder_improv_id < 0) || (wonder_improv_id >= p_bic_data->ImprovementsCount)) return; - PCX_Image pcx; - PCX_Image_construct (&pcx); + if ((p_cities == NULL) || (p_cities->Cities == NULL)) + return; - char temp_path[2*MAX_PATH]; - get_mod_art_path ("MoreCityIcons.pcx", temp_path, sizeof temp_path); + for (int idx = 0; idx <= p_cities->LastIndex; idx++) { + City * city = get_city_ptr (idx); + if (city == NULL) + continue; + if (! patch_City_has_improvement (city, __, wonder_improv_id, false)) + continue; - PCX_Image_read_file (&pcx, __, temp_path, NULL, 0, 0x100, 2); - if ((pcx.JGL.Image != NULL) && - (pcx.JGL.Image->vtable->m54_Get_Width (pcx.JGL.Image) >= 32) && - (pcx.JGL.Image->vtable->m55_Get_Height (pcx.JGL.Image) == 32)) { - Sprite_construct (&is->red_food_icon); - Sprite_slice_pcx (&is->red_food_icon, __, &pcx, 1, 1, 30, 30, 1, 1); - is->red_food_icon_state = IS_OK; - } else { - (*p_OutputDebugStringA) ("[C3X] PCX file for red food icon failed to load or is not the correct size.\n"); - is->red_food_icon_state = IS_INIT_FAILED; + patch_City_add_or_remove_improvement (city, __, wonder_improv_id, 0, false); } - - pcx.vtable->destruct (&pcx, __, 0); } void -deinit_red_food_icon () +handle_district_removed (Tile * tile, int district_id, int center_x, int center_y, bool leave_ruins) { - if (is->red_food_icon_state == IS_OK) - is->red_food_icon.vtable->destruct (&is->red_food_icon, __, 0); - is->red_food_icon_state = IS_UNINITED; -} + if ((tile == NULL) || (tile == p_null_tile) || ! is->current_config.enable_districts) + return; -enum init_state -init_large_minimap_frame () -{ - if (is->large_minimap_frame_img_state != IS_UNINITED) - return is->large_minimap_frame_img_state; + int wonder_windex = -1; + int wonder_improv_id = -1; - PCX_Image pcx; - PCX_Image_construct (&pcx); + // Get wonder district info before removing + struct wonder_district_info * info = get_wonder_district_info (tile); + if (info != NULL && info->state == WDS_COMPLETED) + wonder_windex = info->wonder_index; - char temp_path[2*MAX_PATH]; + int actual_district_id = district_id; + if (actual_district_id < 0) { + struct district_instance * inst = get_district_instance (tile); + if (inst != NULL) + actual_district_id = inst->district_id; + } - get_mod_art_path ("interface\\DoubleSizeBoxLeftColor.pcx", temp_path, sizeof temp_path); - PCX_Image_read_file (&pcx, __, temp_path, NULL, 0, 0x100, 2); - if (pcx.JGL.Image != NULL) { - Sprite_construct (&is->double_size_box_left_color_pcx); - int width = pcx.JGL.Image->vtable->m54_Get_Width (pcx.JGL.Image), - height = pcx.JGL.Image->vtable->m55_Get_Height (pcx.JGL.Image); - Sprite_slice_pcx (&is->double_size_box_left_color_pcx, __, &pcx, 0, 0, width, height, 1, 1); - } else { - pcx.vtable->destruct (&pcx, __, 0); - return is->large_minimap_frame_img_state = IS_INIT_FAILED; - } + remove_district_instance (tile); - get_mod_art_path ("interface\\DoubleSizeBoxLeftAlpha.pcx", temp_path, sizeof temp_path); - PCX_Image_read_file (&pcx, __, temp_path, NULL, 0, 0x100, 2); - if (pcx.JGL.Image != NULL) { - Sprite_construct (&is->double_size_box_left_alpha_pcx); - int width = pcx.JGL.Image->vtable->m54_Get_Width (pcx.JGL.Image), - height = pcx.JGL.Image->vtable->m55_Get_Height (pcx.JGL.Image); - Sprite_slice_pcx (&is->double_size_box_left_alpha_pcx, __, &pcx, 0, 0, width, height, 1, 1); - } else { - is->double_size_box_left_color_pcx.vtable->destruct (&is->double_size_box_left_color_pcx, __, 0); - pcx.vtable->destruct (&pcx, __, 0); - return is->large_minimap_frame_img_state = IS_INIT_FAILED;; - } + bool removed_neighborhood = actual_district_id == NEIGHBORHOOD_DISTRICT_ID; - pcx.vtable->destruct (&pcx, __, 0); - return is->large_minimap_frame_img_state = IS_OK; -} + if (is->current_config.enable_wonder_districts && + (actual_district_id == WONDER_DISTRICT_ID) && + (wonder_windex >= 0)) + wonder_improv_id = get_wonder_improvement_id_from_index (wonder_windex); -void -deinit_large_minimap_frame () -{ - if (is->large_minimap_frame_img_state == IS_OK) { - is->double_size_box_left_color_pcx.vtable->destruct (&is->double_size_box_left_color_pcx, __, 0); - is->double_size_box_left_alpha_pcx.vtable->destruct (&is->double_size_box_left_alpha_pcx, __, 0); - } - is->large_minimap_frame_img_state = IS_UNINITED; -} + if (wonder_improv_id >= 0 && is->current_config.completed_wonder_districts_can_be_destroyed) + remove_wonder_improvement_for_destroyed_district (wonder_improv_id); -int __cdecl -patch_get_tile_occupier_for_ai_path (int x, int y, int pov_civ_id, bool respect_unit_invisibility) -{ - is->getting_tile_occupier_for_ai_pathfinding = true; - return get_tile_occupier_id (x, y, pov_civ_id, respect_unit_invisibility); - is->getting_tile_occupier_for_ai_pathfinding = false; -} + if (is->current_config.enable_distribution_hub_districts && + (actual_district_id == DISTRIBUTION_HUB_DISTRICT_ID)) + remove_distribution_hub_record (tile); -char __fastcall patch_Unit_is_visible_to_civ (Unit * this, int edx, int civ_id, int param_2); + if (district_id >= 0) + remove_dependent_buildings_for_district (district_id, center_x, center_y); -char __fastcall -patch_Unit_is_tile_occupier_visible (Unit * this, int edx, int civ_id, int param_2) -{ - // Here's the fix for the submarine bug: If we're constructing a path for an AI unit, ignore unit invisibility so the pathfinder will path - // around instead of over other civ's units. We must carve out an exception if the AI is at war with the unit in question. In that case if it - // "accidentally" paths over the unit, it should get stuck in combat like the human player would. - if (is->current_config.patch_submarine_bug && - is->getting_tile_occupier_for_ai_pathfinding && - ! this->vtable->is_enemy_of_civ (this, __, civ_id, 0)) - return 1; - else - return patch_Unit_is_visible_to_civ (this, __, civ_id, param_2); -} + // Make the tile workable again by resetting CityAreaID and recomputing yields for nearby cities + tile->Body.CityAreaID = -1; -void do_trade_scroll (DiploForm * diplo, int forward); + int tile_owner = tile->vtable->m38_Get_Territory_OwnerID (tile); -void __cdecl -activate_trade_scroll_button (int control_id) -{ - do_trade_scroll (p_diplo_form, control_id == TRADE_SCROLL_BUTTON_ID_RIGHT); + FOR_CITIES_AROUND (wai, center_x, center_y) { + if (removed_neighborhood) + reduce_city_population_due_to_lost_neighborhood (wai.city); + recompute_city_yields_with_districts (wai.city); + } + + if (leave_ruins && (tile->vtable->m60_Set_Ruins != NULL)) { + tile->vtable->m51_Unset_Tile_Flags (tile, __, 0, TILE_FLAG_MINE, center_x, center_y); + tile->vtable->m60_Set_Ruins (tile, __, 1); + } + p_main_screen_form->vtable->m73_call_m22_Draw ((Base_Form *)p_main_screen_form); } -void -init_trade_scroll_buttons (DiploForm * diplo_form) +bool +city_has_active_wonder_for_district (City * city) { - if (is->trade_scroll_button_state != IS_UNINITED) - return; - - char temp_path[2*MAX_PATH]; - PCX_Image pcx; - PCX_Image_construct (&pcx); - get_mod_art_path ("TradeScrollButtons.pcx", temp_path, sizeof temp_path); - PCX_Image_read_file (&pcx, __, temp_path, NULL, 0, 0x100, 2); - if (pcx.JGL.Image == NULL) { - is->trade_scroll_button_state = IS_INIT_FAILED; - (*p_OutputDebugStringA) ("[C3X] Failed to load TradeScrollButtons.pcx\n"); - goto cleanup; + struct district_infos * info = &is->district_infos[WONDER_DISTRICT_ID]; + for (int n = 0; n < ARRAY_LEN (info->dependent_building_ids); n++) { + int building_id = info->dependent_building_ids[n]; + if ((building_id >= 0) && has_active_building (city, building_id)) + return true; } + return false; +} - // Stores normal, rollover, and highlight images, in that order, first for the left button then for the right - is->trade_scroll_button_images = calloc (6, sizeof is->trade_scroll_button_images[0]); - for (int n = 0; n < 6; n++) - Sprite_construct (&is->trade_scroll_button_images[n]); - Sprite * imgs = is->trade_scroll_button_images; - - for (int right = 0; right < 2; right++) - for (int n = 0; n < 3; n++) - Sprite_slice_pcx (&imgs[n + 3*right], __, &pcx, right ? 44 : 0, 1 + 48*n, 43, 47, 1, 1); +bool +city_requires_district_for_improvement (City * city, int improv_id, int * out_district_id) +{ + struct district_building_prereq_list * prereq_list = get_district_building_prereq_list (improv_id); + if ((prereq_list == NULL) || (prereq_list->count <= 0)) + return false; + Improvement * improv = &p_bic_data->Improvements[improv_id]; + bool is_wonder = false; - for (int right = 0; right < 2; right++) { - Button * b = new (sizeof *b); + // Special logic for handling rivers + bool requires_river = (improv->ImprovementFlags & ITF_Must_Be_Near_River) != 0; + bool has_prereq = false; - Button_construct (b); - int id = right ? TRADE_SCROLL_BUTTON_ID_RIGHT : TRADE_SCROLL_BUTTON_ID_LEFT; - Button_initialize (b, __, NULL, id, right ? 622 : 358, 50, 43, 47, (Base_Form *)diplo_form, 0); - for (int n = 0; n < 3; n++) - b->Images[n] = &imgs[n + 3*right]; + if (is->current_config.enable_wonder_districts && + district_building_prereq_list_contains (prereq_list, WONDER_DISTRICT_ID)) { + is_wonder = true; + if (requires_river) { + bool has_river_wonder_district = false; + FOR_DISTRICTS_AROUND (wai, city->Body.X, city->Body.Y, true) { + if (wai.district_inst->district_id != WONDER_DISTRICT_ID) + continue; + if (wai.tile->vtable->m37_Get_River_Code (wai.tile) == 0) + continue; + if (! wonder_is_buildable_on_tile (wai.tile, improv_id)) + continue; + struct wonder_district_info * info = get_wonder_district_info (wai.tile); + if (info == NULL) { + has_river_wonder_district = true; + break; + } + if (info->state == WDS_COMPLETED) + continue; + if (info->state == WDS_UNDER_CONSTRUCTION && info->city_id != city->Body.ID) + continue; + has_river_wonder_district = true; + break; + } + if (has_river_wonder_district) + has_prereq = true; + } else if (city_has_wonder_district_with_no_completed_wonder (city, improv_id)) { + has_prereq = true; + } + } - b->activation_handler = &activate_trade_scroll_button; - b->field_630[0] = 0; // TODO: Is this necessary? It's done by the base game code when creating the city screen scroll buttons + if (! has_prereq && city_has_any_prereq_district_for_improvement (city, prereq_list, requires_river, !is_wonder)) + has_prereq = true; - if (! right) - is->trade_scroll_button_left = b; - else - is->trade_scroll_button_right = b; - } + if (has_prereq) + return false; - is->trade_scroll_button_state = IS_OK; -cleanup: - pcx.vtable->destruct (&pcx, __, 0); + if (out_district_id != NULL) + *out_district_id = pick_missing_district_for_improvement (city, prereq_list); + return true; } void -deinit_trade_scroll_buttons () +clear_best_feasible_order (City * city) { - if (is->trade_scroll_button_state == IS_OK) { - is->trade_scroll_button_left ->vtable->destruct ((Base_Form *)is->trade_scroll_button_left , __, 0); - is->trade_scroll_button_right->vtable->destruct ((Base_Form *)is->trade_scroll_button_right, __, 0); - is->trade_scroll_button_left = is->trade_scroll_button_right = NULL; - for (int n = 0; n < 6; n++) { - Sprite * sprite = &is->trade_scroll_button_images[n]; - sprite->vtable->destruct (sprite, __, 0); - } - free (is->trade_scroll_button_images); - is->trade_scroll_button_images = NULL; + int key = (int)city; + int stored_int; + if (itable_look_up (&is->ai_best_feasible_orders, key, &stored_int)) { + struct ai_best_feasible_order * stored = (struct ai_best_feasible_order *)stored_int; + free (stored); + itable_remove (&is->ai_best_feasible_orders, key); } - is->trade_scroll_button_state = IS_UNINITED; } void -init_mod_info_button_images () +record_best_feasible_order (City * city, City_Order const * order, int value) { - if (is->mod_info_button_images_state != IS_UNINITED) - return; - - is->mod_info_button_images_state = IS_INIT_FAILED; - - PCX_Image * descbox_pcx = new (sizeof *descbox_pcx); - PCX_Image_construct (descbox_pcx); - char * descbox_path = BIC_get_asset_path (p_bic_data, __, "art\\civilopedia\\descbox.pcx", true); - PCX_Image_read_file (descbox_pcx, __, descbox_path, NULL, 0, 0x100, 1); - if (descbox_pcx->JGL.Image == NULL) { - (*p_OutputDebugStringA) ("[C3X] Failed to load descbox.pcx\n"); + int key = (int)city; + int stored_int; + struct ai_best_feasible_order * stored; + if (itable_look_up (&is->ai_best_feasible_orders, key, &stored_int)) + stored = (struct ai_best_feasible_order *)stored_int; + else { + stored = malloc (sizeof *stored); + if (stored == NULL) + return; + stored->order = *order; + stored->value = value; + itable_insert (&is->ai_best_feasible_orders, key, (int)stored); return; } - for (int n = 0; n < 3; n++) { - Sprite_construct (&is->mod_info_button_images[n]); - Sprite_slice_pcx_with_color_table (&is->mod_info_button_images[n], __, descbox_pcx, 1 + n * 103, 1, MOD_INFO_BUTTON_WIDTH, - MOD_INFO_BUTTON_HEIGHT, 1, 1); + if (value > stored->value) { + stored->order = *order; + stored->value = value; } - - is->mod_info_button_images_state = IS_OK; -} - -int -count_escorters (Unit * unit) -{ - IDLS * idls = &unit->Body.IDLS; - if (idls->escorters.contents != NULL) { - int tr = 0; - for (int * p_escorter_id = idls->escorters.contents; p_escorter_id < idls->escorters.contents_end; p_escorter_id++) - tr += NULL != get_unit_ptr (*p_escorter_id); - return tr; - } else - return 0; } -// If "unit" belongs to the AI, records that all players that have visibility on (x, y) have seen an AI unit -void -record_ai_unit_seen (Unit * unit, int x, int y) +struct ai_best_feasible_order * +get_best_feasible_order (City * city) { - if (0 == (*p_human_player_bits & 1<Body.CivID)) { - Tile * tile = tile_at (x, y); - if ((tile != NULL) && (tile != p_null_tile)) { - Tile_Body * body = &tile->Body; - is->players_saw_ai_unit |= body->FOWStatus | body->V3 | body->Visibility | body->field_D0_Visibility; - } - } + int stored_int; + if (itable_look_up (&is->ai_best_feasible_orders, (int)city, &stored_int)) + return (struct ai_best_feasible_order *)stored_int; + else + return NULL; } -void -recompute_resources_if_necessary () +bool +city_is_currently_building_wonder (City * city) { - if (is->must_recompute_resources_for_mill_inputs) - patch_Trade_Net_recompute_resources (is->trade_net, __, false); + if ((city == NULL) || (city->Body.Order_Type != COT_Improvement)) + return false; + int order_id = city->Body.Order_ID; + if ((order_id < 0) || (order_id >= p_bic_data->ImprovementsCount)) + return false; + return (p_bic_data->Improvements[order_id].Characteristics & (ITC_Wonder | ITC_Small_Wonder)) != 0; } -void __fastcall -patch_Unit_bombard_tile (Unit * this, int edx, int x, int y) +bool +wonder_district_tile_under_construction (Tile * tile, int tile_x, int tile_y, int * out_windex) { - Tile * target_tile = NULL; - bool had_district_before = false; - int tile_x = x; - int tile_y = y; - struct district_instance * inst; + if (! is->current_config.enable_wonder_districts || + (tile == NULL) || (tile == p_null_tile)) + return false; - if (is->current_config.enable_districts) { - wrap_tile_coords (&p_bic_data->Map, &tile_x, &tile_y); - target_tile = tile_at (tile_x, tile_y); - if ((target_tile != NULL) && (target_tile != p_null_tile)) { - inst = get_district_instance (target_tile); - had_district_before = (inst != NULL); - } - } + struct district_instance * inst = get_district_instance (tile); + if (inst == NULL || inst->district_id != WONDER_DISTRICT_ID) + return false; - is->bombarding_unit = this; - record_ai_unit_seen (this, x, y); - Unit_bombard_tile (this, __, x, y); - is->bombard_stealth_target = NULL; - is->bombarding_unit = NULL; + struct wonder_district_info * info = get_wonder_district_info (tile); + if (info == NULL || info->state != WDS_UNDER_CONSTRUCTION) + return false; - if (had_district_before && target_tile != NULL && target_tile != p_null_tile && inst->district_type != NATURAL_WONDER_DISTRICT_ID) { - unsigned int overlays = target_tile->vtable->m42_Get_Overlays (target_tile, __, 0); - if ((overlays & TILE_FLAG_MINE) == 0) - handle_district_destroyed_by_attack (target_tile, tile_x, tile_y, false); - } -} + City * reserved_city = get_city_ptr (info->city_id); + if (reserved_city == NULL) + return false; + info->city = reserved_city; + info->city_id = reserved_city->Body.ID; -void __fastcall -patch_Unit_move (Unit * this, int edx, int tile_x, int tile_y) -{ - record_ai_unit_seen (this, tile_x, tile_y); + // Verify the reserved city is still building a wonder + if (! city_is_currently_building_wonder (reserved_city)) + return false; - Unit_move (this, __, tile_x, tile_y); + // Verify this tile is within the reserved city's radius + if (! city_radius_contains_tile (reserved_city, tile_x, tile_y)) + return false; - if (this == is->last_selected_unit.ptr) { - is->last_selected_unit.last_x = this->Body.X; - is->last_selected_unit.last_y = this->Body.Y; + // Get the wonder index for the wonder being built + if (out_windex != NULL) { + int order_id = reserved_city->Body.Order_ID; + int windex = find_wonder_config_index_by_improvement_id (order_id); + *out_windex = windex; } -} -// Returns true if the unit has attacked & does not have blitz or if it's run out of movement points for the turn -bool -has_exhausted_attack (Unit * unit) -{ - return (unit->Body.Moves >= patch_Unit_get_max_move_points (unit)) || - ((unit->Body.Status & USF_USED_ATTACK) && ! UnitType_has_ability (&p_bic_data->UnitTypes[unit->Body.UnitTypeID], __, UTA_Blitz)); + return true; } -// Returns whether or not the unit type is a combat type and can do damage on offense (as opposed to only being able to defend). This includes units -// that can only do damage by bombarding and nuclear weapons. bool -is_offensive_combat_type (UnitType * unit_type) +city_needs_wonder_district (City * city) { - return (unit_type->Attack > 0) || - ((((unit_type->Special_Actions & UCV_Bombard) | (unit_type->Air_Missions & UCV_Bombing)) & 0x0FFFFFFF) && // (type can perform bombard or bombing AND - ((unit_type->Bombard_Strength > 0) || UnitType_has_ability (unit_type, __, UTA_Nuclear_Weapon))); // (unit has bombard strength OR is a nuclear weapon)) + if (! is->current_config.enable_wonder_districts || (city == NULL)) + return false; + + int pending_improv_id; + if (lookup_pending_building_order (city, &pending_improv_id)) { + // Check if it's actually a wonder + if ((pending_improv_id >= 0) && (pending_improv_id < p_bic_data->ImprovementsCount)) { + Improvement * improv = &p_bic_data->Improvements[pending_improv_id]; + if (improv->Characteristics & (ITC_Wonder | ITC_Small_Wonder)) + return true; + } + } + if (city_is_currently_building_wonder (city)) + return true; + if (city_has_active_wonder_for_district (city)) + return true; + return false; } bool -can_damage_bombarding (UnitType * attacker_type, Unit * defender, Tile * defender_tile) +city_has_assigned_wonder_district (City * city, Tile * ignore_tile, int wonder_improv_id) { - Unit * container; - if ((is->current_config.special_helicopter_rules & SHR_NO_DEFENSE_FROM_INSIDE) && - (container = get_unit_ptr (defender->Body.Container_Unit)) != NULL && - p_bic_data->UnitTypes[container->Body.UnitTypeID].Unit_Class == UTC_Air) - return false; - - UnitType * defender_type = &p_bic_data->UnitTypes[defender->Body.UnitTypeID]; - if (defender_type->Unit_Class == UTC_Land) { - int has_lethal_land_bombard = UnitType_has_ability (attacker_type, __, UTA_Lethal_Land_Bombardment); - return defender->Body.Damage + (! has_lethal_land_bombard) < Unit_get_max_hp (defender); - } else if (defender_type->Unit_Class == UTC_Sea) { - // Land artillery can't normally damage ships in port - if ((attacker_type->Unit_Class == UTC_Land) && (! is->current_config.remove_land_artillery_target_restrictions) && Tile_has_city (defender_tile)) - return false; - int has_lethal_sea_bombard = UnitType_has_ability (attacker_type, __, UTA_Lethal_Sea_Bombardment); - return defender->Body.Damage + (! has_lethal_sea_bombard) < Unit_get_max_hp (defender); - } else if (defender_type->Unit_Class == UTC_Air) { - if (is->current_config.immunize_aircraft_against_bombardment) - return false; - // Can't damage aircraft in an airfield by bombarding, the attack doesn't even go off - if ((defender_tile->vtable->m42_Get_Overlays (defender_tile, __, 0) & 0x20000000) != 0) - return false; - // Land artillery can't normally damage aircraft but naval artillery and other aircraft can. Lethal bombard doesn't apply; anything - // that can damage can kill. - return (attacker_type->Unit_Class != UTC_Land) || is->current_config.remove_land_artillery_target_restrictions; - } else // UTC_Space? UTC_Alternate_Dimension??? + if (! is->current_config.enable_wonder_districts || (city == NULL)) return false; -} -char __fastcall -patch_Unit_is_visible_to_civ (Unit * this, int edx, int civ_id, int param_2) -{ - // Save the previous value here b/c this function gets called recursively - Unit * prev_checking = is->checking_visibility_for_unit; - is->checking_visibility_for_unit = this; + FOR_DISTRICTS_AROUND (wai, city->Body.X, city->Body.Y, false) { + int x = wai.tile_x, y = wai.tile_y; + Tile * candidate = wai.tile; + if (candidate == ignore_tile) + continue; - char base_vis = Unit_is_visible_to_civ (this, __, civ_id, param_2); - if ((! base_vis) && // if unit is not visible to civ_id AND - is->current_config.share_visibility_in_hotseat && // shared hotseat vis is enabled AND - ((1 << civ_id) & *p_human_player_bits) && // civ_id is a human player AND - (*p_is_offline_mp_game && ! *p_is_pbem_game)) { // we're in a hotseat game + struct district_instance * inst = wai.district_inst; + if (inst->district_id != WONDER_DISTRICT_ID) + continue; - // Check if the unit is visible to any other human player in the game - unsigned player_bits = *(unsigned *)p_human_player_bits >> 1; - int n_player = 1; - while (player_bits != 0) { - if ((player_bits & 1) && - (n_player != civ_id) && - Unit_is_visible_to_civ (this, __, n_player, param_2)) - return 1; - player_bits >>= 1; - n_player++; + struct wonder_district_info * info = inst ? &inst->wonder_info : NULL; + if ((info != NULL) && + (info->state == WDS_UNDER_CONSTRUCTION) && + (info->city_id == city->Body.ID)) { + info->city = city; + if ((wonder_improv_id >= 0) && (! wonder_is_buildable_on_tile (candidate, wonder_improv_id))) { + info->state = WDS_UNUSED; + info->city = NULL; + info->city_id = -1; + info->wonder_index = -1; + continue; + } + return true; } } - is->checking_visibility_for_unit = prev_checking; - return base_vis; + return false; } bool -has_any_destructible_improvements (City * city) +free_wonder_district_for_city (City * city) { - for (int n = 0; n < p_bic_data->ImprovementsCount; n++) { - Improvement * improv = &p_bic_data->Improvements[n]; - if (((improv->Characteristics & (ITC_Wonder | ITC_Small_Wonder)) == 0) && // if improv is not a wonder AND - ((improv->ImprovementFlags & ITF_Center_of_Empire) == 0) && // it's not the palace AND - (improv->SpaceshipPart < 0) && // it's not a spaceship part AND - patch_City_has_improvement (city, __, n, 0)) // it's present in the city ignoring free improvements - return true; + if (! is->current_config.enable_wonder_districts || (city == NULL)) + return false; + if ((*p_human_player_bits & (1 << city->Body.CivID)) != 0) + return false; + if (city_needs_wonder_district (city)) + return false; + + FOR_DISTRICTS_AROUND (wai, city->Body.X, city->Body.Y, false) { + int x = wai.tile_x, y = wai.tile_y; + Tile * tile = wai.tile; + struct district_instance * inst = wai.district_inst; + if (inst->district_id != WONDER_DISTRICT_ID) + continue; + + struct wonder_district_info * info = get_wonder_district_info (tile); + if (info != NULL && info->state == WDS_COMPLETED) + continue; // Don't remove completed wonder districts + + int tile_x, tile_y; + if (! district_instance_get_coords (inst, tile, &tile_x, &tile_y)) + continue; + tile->vtable->m51_Unset_Tile_Flags (tile, __, 0, TILE_FLAG_MINE, tile_x, tile_y); + + handle_district_removed (tile, WONDER_DISTRICT_ID, city->Body.X, city->Body.Y, false); + return true; } + return false; } -int const destructible_overlays = - 0x00000003 | // road, railroad - 0x00000004 | // mine - 0x00000008 | // irrigation - 0x00000010 | // fortress - 0x10000000 | // barricade - 0x20000000 | // airfield - 0x40000000 | // radar - 0x80000000; // outpost - bool -has_any_destructible_overlays (Tile * tile, bool precision_strike) +reserve_wonder_district_for_city (City * city, int wonder_improv_id) { - int overlays = tile->vtable->m42_Get_Overlays (tile, __, 0); - if ((overlays & destructible_overlays) == 0) + if (! is->current_config.enable_wonder_districts || (city == NULL)) return false; - else { - if (! precision_strike) - return true; - else { - if (overlays == 0x20000000) { // if tile ONLY has an airfield - int any_aircraft_on_tile = 0; { - FOR_UNITS_ON (uti, tile) - if (p_bic_data->UnitTypes[uti.unit->Body.UnitTypeID].Unit_Class == UTC_Air) { - any_aircraft_on_tile = 1; - break; - } - } - return ! any_aircraft_on_tile; - } else + + if (city_has_assigned_wonder_district (city, NULL, wonder_improv_id)) + return true; + + FOR_DISTRICTS_AROUND (wai, city->Body.X, city->Body.Y, true) { + int x = wai.tile_x, y = wai.tile_y; + Tile * candidate = wai.tile; + struct district_instance * inst = wai.district_inst; + if (inst->district_id != WONDER_DISTRICT_ID) continue; + if ((wonder_improv_id >= 0) && (! wonder_is_buildable_on_tile (candidate, wonder_improv_id))) + continue; + + struct wonder_district_info * info = &inst->wonder_info; + if (info->state == WDS_COMPLETED) continue; + if (info->state == WDS_UNDER_CONSTRUCTION) { + if (info->city_id == city->Body.ID) { + info->city = city; return true; + } + continue; } + if (city_has_assigned_wonder_district (city, candidate, wonder_improv_id)) + return true; + + // Reserve this Wonder district for this city + info->state = WDS_UNDER_CONSTRUCTION; + info->city = city; + info->city_id = city->Body.ID; + info->wonder_index = -1; + return true; } + + return false; } -void __fastcall -patch_Main_Screen_Form_perform_action_on_tile (Main_Screen_Form * this, int edx, enum Unit_Mode_Actions action, int x, int y) +void +release_wonder_district_reservation (City * city) { - if ((! is->current_config.enable_stack_bombard) || // if stack bombard is disabled OR - (! ((action == UMA_Bombard) || (action == UMA_Air_Bombard))) || // action is not bombard OR - ((((*p_GetAsyncKeyState) (VK_CONTROL)) >> 8 == 0) && // (control key is not down AND - ((is->sc_img_state != IS_UNINITED) && (is->sb_activated_by_button == 0))) || // (button flag is valid AND not set)) OR - is_online_game ()) { // is online game - Main_Screen_Form_perform_action_on_tile (this, __, action, x, y); + if (! is->current_config.enable_wonder_districts || (city == NULL)) return; - } - // Save preferences so we can restore them at the end of the stack bombard operation. We might change them to turn off combat animations. - unsigned init_prefs = *p_preferences; + // Find and remove any reservations for this city + FOR_DISTRICTS_AROUND (wai, city->Body.X, city->Body.Y, false) { + struct district_instance * inst = wai.district_inst; + if (inst->district_id != WONDER_DISTRICT_ID) + continue; - clear_memo (); + struct wonder_district_info * info = &inst->wonder_info; + if ((info != NULL) && + (info->state == WDS_UNDER_CONSTRUCTION) && + (info->city_id == city->Body.ID)) { + info->city = city; + info->state = WDS_UNUSED; + info->city = NULL; + info->city_id = -1; + info->wonder_index = -1; + } + } +} - wrap_tile_coords (&p_bic_data->Map, &x, &y); - Tile * base_tile = tile_at (this->Current_Unit->Body.X, this->Current_Unit->Body.Y); - Tile * target_tile = tile_at (x, y); - int attacker_type_id = this->Current_Unit->Body.UnitTypeID; - UnitType * attacker_type = &p_bic_data->UnitTypes[attacker_type_id]; - int civ_id = this->Current_Unit->Body.CivID; +void +handle_district_destroyed_by_attack (Tile * tile, int tile_x, int tile_y, bool leave_ruins) +{ + if (! is->current_config.enable_districts || tile == NULL || tile == p_null_tile) + return; - // Count & memoize attackers - int selected_unit_id = this->Current_Unit->Body.ID; - FOR_UNITS_ON (uti, base_tile) - if ((uti.id != selected_unit_id) && - (uti.unit->Body.CivID == civ_id) && - (uti.unit->Body.UnitTypeID == attacker_type_id) && - ((uti.unit->Body.Container_Unit < 0) || (attacker_type->Unit_Class == UTC_Air)) && - (uti.unit->Body.UnitState == 0) && - ! has_exhausted_attack (uti.unit)) - memoize (uti.id); - int count_attackers = is->memo_len; - - // Count & memoize targets (also count air units while we're at it) - int num_air_units_on_target_tile = 0; - FOR_UNITS_ON (uti, target_tile) { - num_air_units_on_target_tile += UTC_Air == p_bic_data->UnitTypes[uti.unit->Body.UnitTypeID].Unit_Class; - if ((uti.unit->Body.CivID != civ_id) && - (Unit_get_defense_strength (uti.unit) > 0) && - (uti.unit->Body.Container_Unit < 0) && - patch_Unit_is_visible_to_civ (uti.unit, __, civ_id, 0) && - can_damage_bombarding (attacker_type, uti.unit, target_tile)) - memoize (uti.id); - } - int count_targets = is->memo_len - count_attackers; - - // Now our attackers and targets arrays will just be pointers into the memo - int * attackers = &is->memo[0], * targets = &is->memo[count_attackers]; - - int attacking_units = 0, attacking_tile = 0; - City * target_city = NULL; - if (count_targets > 0) - attacking_units = 1; - else if (Tile_has_city (target_tile)) - target_city = get_city_ptr (target_tile->vtable->m45_Get_City_ID (target_tile)); - else { - // Make sure not to set attacking_tile when the tile has an airfield and air units (but no land units) on - // it. In that case we can't damage the airfield and if we try to anyway, the units will waste their moves - // without attacking. - int has_airfield = (target_tile->vtable->m42_Get_Overlays (target_tile, __, 0) & 0x20000000) != 0; - attacking_tile = (! has_airfield) || (num_air_units_on_target_tile == 0); - } - - is->sb_next_up = this->Current_Unit; - int i_next_attacker = 0; - int anything_left_to_attack; - int last_attack_didnt_happen; - do { - // If combat animations were enabled when the stack bombard operation started, reset the preference according to the state of the - // shift key (down => skip animations, up => show them). - if (init_prefs & P_ANIMATE_BATTLES) { - if ((*p_GetAsyncKeyState) (VK_SHIFT) >> 8) - *p_preferences &= ~P_ANIMATE_BATTLES; - else - *p_preferences |= P_ANIMATE_BATTLES; - } - - int moves_before_bombard = is->sb_next_up->Body.Moves; - - patch_Unit_bombard_tile (is->sb_next_up, __, x, y); - // At this point sb_next_up may have become NULL if the unit was a bomber that got shot down. - - // Check if the last unit sent into battle actually did anything. If it didn't we should at least skip over - // it to avoid an infinite loop, but actually the only time this should happen is if the player is not at - // war with the targeted civ and chose not to declare when prompted. In this case it's better to just stop - // trying to attack so as to not spam the player with prompts. - last_attack_didnt_happen = (is->sb_next_up == NULL) || (is->sb_next_up->Body.Moves == moves_before_bombard); + struct district_instance * inst = get_district_instance (tile); + if (inst != NULL) { + int district_id = inst->district_id; - if ((is->sb_next_up == NULL) || has_exhausted_attack (is->sb_next_up)) { - is->sb_next_up = NULL; - while ((i_next_attacker < count_attackers) && (is->sb_next_up == NULL)) - is->sb_next_up = get_unit_ptr (attackers[i_next_attacker++]); + // If this is a Wonder District with a completed wonder and wonders can't be destroyed, restore overlay and keep district + if (is->current_config.enable_wonder_districts) { + struct wonder_district_info * info = get_wonder_district_info (tile); + if ((district_id == WONDER_DISTRICT_ID) && + (info != NULL && info->state == WDS_COMPLETED) && + (! is->current_config.completed_wonder_districts_can_be_destroyed)) { + unsigned int overlays = tile->vtable->m42_Get_Overlays (tile, __, 0); + if ((overlays & TILE_FLAG_MINE) == 0) + tile->vtable->m56_Set_Tile_Flags (tile, __, 0, TILE_FLAG_MINE, tile_x, tile_y); + return; + } } - - if (attacking_units) { - anything_left_to_attack = 0; - for (int n = 0; n < count_targets; n++) { - Unit * unit = get_unit_ptr (targets[n]); - - // Make sure this unit is still a valid target. Keep in mind it's possible for new units to be created during the - // stack bombard operation if the attackers have lethal bombard and the enslave ability. - if ((unit != NULL) && (unit->Body.X == x) && (unit->Body.Y == y) && (unit->Body.CivID != civ_id)) { - - if (can_damage_bombarding (attacker_type, unit, target_tile)) { - anything_left_to_attack = 1; - break; + if (district_id == BRIDGE_DISTRICT_ID || district_id == CANAL_DISTRICT_ID) { + enum UnitTypeClasses target_class = (district_id == BRIDGE_DISTRICT_ID) ? UTC_Land : UTC_Sea; + clear_memo (); + FOR_UNITS_ON (uti, tile) { + Unit * unit = uti.unit; + if (unit == NULL) + continue; + int unit_type_id = unit->Body.UnitTypeID; + if ((unit_type_id < 0) || (unit_type_id >= p_bic_data->UnitTypeCount)) + continue; + UnitType * type = &p_bic_data->UnitTypes[unit_type_id]; + bool matches = (type->Unit_Class == target_class); + if (! matches && unit->Body.Container_Unit >= 0) { + Unit * container = get_unit_ptr (unit->Body.Container_Unit); + if (container != NULL) { + int container_type_id = container->Body.UnitTypeID; + if ((container_type_id >= 0) && (container_type_id < p_bic_data->UnitTypeCount)) { + UnitType * container_type = &p_bic_data->UnitTypes[container_type_id]; + matches = (container_type->Unit_Class == target_class); + } } } + if (matches) { + bool already = false; + for (int n = 0; n < is->memo_len; n++) + if (is->memo[n] == unit->Body.ID) { + already = true; + break; + } + if (! already) + memoize (unit->Body.ID); + } } - } else if (target_city != NULL) - anything_left_to_attack = (target_city->Body.Population.Size > 1) || has_any_destructible_improvements (target_city); - else if (attacking_tile) - anything_left_to_attack = has_any_destructible_overlays (target_tile, false); - else - anything_left_to_attack = 0; - } while ((is->sb_next_up != NULL) && anything_left_to_attack && (! last_attack_didnt_happen)); - - is->sb_activated_by_button = 0; - is->sb_next_up = NULL; - *p_preferences = init_prefs; - this->GUI.Base.vtable->m73_call_m22_Draw ((Base_Form *)&this->GUI); -} - -void -set_up_stack_bombard_buttons (Main_GUI * this) -{ - if (is_online_game () || (! is->current_config.enable_stack_bombard)) - return; - - init_stackable_command_buttons (); - if (is->sc_img_state != IS_OK) - return; - - // Find button that the original method set to (air) bombard, then find the next unused button after that. - Command_Button * bombard_button = NULL, * free_button = NULL; { - int i_bombard_button; - for (int n = 0; n < 42; n++) - if (((this->Unit_Command_Buttons[n].Button.Base_Data.Status2 & 1) != 0) && - (this->Unit_Command_Buttons[n].Command == UCV_Bombard || this->Unit_Command_Buttons[n].Command == UCV_Bombing)) { - bombard_button = &this->Unit_Command_Buttons[n]; - i_bombard_button = n; - break; + for (int n = 0; n < is->memo_len; n++) { + Unit * to_despawn = get_unit_ptr (is->memo[n]); + if (to_despawn != NULL) + patch_Unit_despawn (to_despawn, __, 0, 1, 0, 0, 0, 0, 0); } - if (bombard_button != NULL) - for (int n = i_bombard_button + 1; n < 42; n++) - if ((this->Unit_Command_Buttons[n].Button.Base_Data.Status2 & 1) == 0) { - free_button = &this->Unit_Command_Buttons[n]; - break; - } + clear_memo (); + } + handle_district_removed (tile, district_id, tile_x, tile_y, leave_ruins); } - - if ((bombard_button == NULL) || (free_button == NULL)) - return; - - // Set up free button for stack bombard - free_button->Command = bombard_button->Command; - free_button->field_6D8 = bombard_button->field_6D8; - struct sc_button_image_set * img_set = - (bombard_button->Command == UCV_Bombing) ? &is->sc_button_image_sets[SC_BOMB] : &is->sc_button_image_sets[SC_BOMBARD]; - for (int n = 0; n < 4; n++) - free_button->Button.Images[n] = &img_set->imgs[n]; - free_button->Button.field_664 = bombard_button->Button.field_664; - // FUN_005559E0 is also called in the original code. I don't know what it actually does but I'm pretty sure it doesn't - // matter for our purposes. - Button_set_tooltip (&free_button->Button, __, is->c3x_labels[CL_SB_TOOLTIP]); - free_button->Button.field_5FC[13] = bombard_button->Button.field_5FC[13]; - free_button->Button.vtable->m01_Show_Enabled ((Base_Form *)&free_button->Button, __, 0); } -bool __fastcall patch_Unit_can_perform_command (Unit * this, int edx, int unit_command_value); -bool __fastcall patch_Unit_can_pillage (Unit * this, int edx, int tile_x, int tile_y); +bool +has_active_building (City * city, int improv_id) +{ + Leader * owner = &leaders[city->Body.CivID]; + Improvement * improv = &p_bic_data->Improvements[improv_id]; + return patch_City_has_improvement (city, __, improv_id, 1) && // building is physically present in city AND + ((improv->ObsoleteID < 0) || (! Leader_has_tech (owner, __, improv->ObsoleteID))) && // building is not obsolete AND + ((improv->GovernmentID < 0) || (improv->GovernmentID == owner->GovernmentType)); // building is not restricted to a different govt +} void -init_district_command_buttons () +init_unit_type_count (Leader * leader) { - if (is_online_game () || is->dc_btn_img_state != IS_UNINITED) - return; - - PCX_Image pcx; - PCX_Image_construct (&pcx); - for (int dc = 0; dc < COUNT_DISTRICT_TYPES; dc++) - for (int n = 0; n < 4; n++) - Sprite_construct (&is->district_btn_img_sets[dc].imgs[n]); - - char temp_path[2*MAX_PATH]; - - is->dc_btn_img_state = IS_INIT_FAILED; - - // For each button sprite type (normal, rollover, highlighted, alpha) - char const * filenames[4] = { - "Districts\\WorkerDistrictButtonsNorm.pcx", "Districts\\WorkerDistrictButtonsRollover.pcx", - "Districts\\WorkerDistrictButtonsHighlighted.pcx", "Districts\\WorkerDistrictButtonsAlpha.pcx" - }; - for (int n = 0; n < 4; n++) { - get_mod_art_path (filenames[n], temp_path, sizeof temp_path); - PCX_Image_read_file (&pcx, __, temp_path, NULL, 0, 0x100, 2); - if (pcx.JGL.Image == NULL) { - (*p_OutputDebugStringA) ("[C3X] Failed to load work district command buttons sprite sheet.\n"); - for (int dc = 0; dc < COUNT_DISTRICT_TYPES; dc++) - for (int k = 0; k < 4; k++) { - Sprite * sprite = &is->district_btn_img_sets[dc].imgs[k]; - sprite->vtable->destruct (sprite, __, 0); - } - pcx.vtable->destruct (&pcx, __, 0); - - char ss[200]; - snprintf (ss, sizeof ss, "[C3X] Failed to load district command button images from %s", temp_path); - pop_up_in_game_error (ss); + int id = leader->ID; + struct table * counts = &is->unit_type_counts[id]; - return; - } + if (counts->len > 0) + table_deinit (counts); - // For each district type - for (int dc = 0; dc < is->district_count; dc++) { - int x = 32 * is->district_configs[dc].btn_tile_sheet_column, - y = 32 * is->district_configs[dc].btn_tile_sheet_row; - Sprite_slice_pcx (&is->district_btn_img_sets[dc].imgs[n], __, &pcx, x, y, 32, 32, 1, 0); + if (p_units->Units != NULL) + for (int n = 0; n <= p_units->LastIndex; n++) { + Unit_Body * body = p_units->Units[n].Unit; + if ((body != NULL) && ((int)body != offsetof (Unit, Body)) && (body->CivID == id)) { + int prev_count = itable_look_up_or (counts, body->UnitTypeID, 0); + itable_insert (counts, body->UnitTypeID, prev_count + 1); + } } - pcx.vtable->clear_JGL (&pcx); - } - - is->dc_btn_img_state = IS_OK; - pcx.vtable->destruct (&pcx, __, 0); + is->unit_type_count_init_bits |= 1 << id; } int -parse_turns_from_tooltip (char const * tooltip) +get_unit_type_count (Leader * leader, int unit_type_id) { - if ((tooltip == NULL) || (*tooltip == '\0')) - return -1; - - char const * last_paren = NULL; - for (char const * cursor = tooltip; *cursor != '\0'; cursor++) - if (*cursor == '(') - last_paren = cursor; - if (last_paren == NULL) - return -1; + int id = leader->ID; + struct table * counts = &is->unit_type_counts[id]; - char const * digit_ptr = last_paren + 1; - while (*digit_ptr == ' ') - digit_ptr++; + if ((is->unit_type_count_init_bits & 1<= '0') && (*digit_ptr <= '9')) { - have_digit = true; - turns = (turns * 10) + (*digit_ptr - '0'); - digit_ptr++; - } - return have_digit ? turns : -1; + return itable_look_up_or (counts, unit_type_id, 0); } void -compute_highlighted_worker_tiles_for_districts () +change_unit_type_count (Leader * leader, int unit_type_id, int amount) { - if (is_online_game () - || ! is->current_config.enable_districts - || ! is->current_config.enable_city_work_radii_highlights) - return; - - Unit * selected_unit = p_main_screen_form->Current_Unit; - if (selected_unit == NULL) - return; - - int unit_type_id = selected_unit->Body.UnitTypeID; - if (p_bic_data->UnitTypes[unit_type_id].Worker_Actions == 0) - return; - - if (is->tile_highlight_state == IS_UNINITED) - init_tile_highlights (); - if (is->tile_highlight_state != IS_OK) - return; - - int worker_civ_id = selected_unit->Body.CivID; - if ((p_cities == NULL) || (p_cities->Cities == NULL)) - return; + int id = leader->ID; + struct table * counts = &is->unit_type_counts[id]; + if ((is->unit_type_count_init_bits & (1 << id)) == 0) + init_unit_type_count (leader); - // Loop over all cities owned by this civ and tally their workable tiles - FOR_CITIES_OF (coi, worker_civ_id) { - City * city = coi.city; - if (city == NULL) - continue; + int prev_amount = itable_look_up_or (counts, unit_type_id, 0); + itable_insert (counts, unit_type_id, prev_amount + amount); +} - // Highlight city center so players can easily see which cities contribute - Tile * city_center_tile = tile_at (city->Body.X, city->Body.Y); - if ((city_center_tile != NULL) && (city_center_tile != p_null_tile)) { - int stored_ptr; - if (! itable_look_up (&is->highlighted_city_radius_tile_pointers, (int)city_center_tile, &stored_ptr)) { - struct highlighted_city_radius_tile_info * info = malloc (sizeof (struct highlighted_city_radius_tile_info)); - info->highlight_level = 0; - itable_insert (&is->highlighted_city_radius_tile_pointers, (int)city_center_tile, (int)info); - } - } +// If this unit type is limited, returns true and writes how many units of the type the given player is allowed to "out_limit". If the type is not +// limited, returns false. +bool +get_unit_limit (Leader * leader, int unit_type_id, int * out_limit) +{ + UnitType * type = &p_bic_data->UnitTypes[unit_type_id]; + struct unit_type_limit * lim; + if ((unit_type_id >= 0) && (unit_type_id < p_bic_data->UnitTypeCount) && + stable_look_up (&is->current_config.unit_limits, type->Name, (int *)&lim)) { + int city_count = leader->Cities_Count; + int tr = lim->per_civ + lim->per_city * city_count; + if (lim->cities_per != 0) + tr += city_count / lim->cities_per; + *out_limit = tr; + return true; + } else + return false; +} - // Add all workable tiles around the city (excluding city center) - for (int n = 1; n < is->workable_tile_count; n++) { - int dx, dy; - patch_ni_to_diff_for_work_area (n, &dx, &dy); - int tile_x = city->Body.X + dx; - int tile_y = city->Body.Y + dy; - wrap_tile_coords (&p_bic_data->Map, &tile_x, &tile_y); +// This this unit type is limited, returns true and writes to "out_available" how many units the given player can add before reaching the limit. If +// the type is not limited, returns false. +bool +get_available_unit_count (Leader * leader, int unit_type_id, int * out_available) +{ + int limit; + if (get_unit_limit (leader, unit_type_id, &limit)) { + int count = get_unit_type_count (leader, unit_type_id); + int dups[30]; + int dups_count = list_unit_type_duplicates (unit_type_id, dups, ARRAY_LEN (dups)); + for (int n = 0; n < dups_count; n++) + count += get_unit_type_count (leader, dups[n]); - Tile * workable_tile = tile_at (tile_x, tile_y); - if ((workable_tile == NULL) || (workable_tile == p_null_tile)) continue; - if (workable_tile->vtable->m38_Get_Territory_OwnerID (workable_tile) != worker_civ_id) continue; + *out_available = limit - count; + return true; + } else + return false; +} - // Upsert into highlighted_city_radius_tile_pointers - int stored_ptr; - struct highlighted_city_radius_tile_info * info; - if (! itable_look_up (&is->highlighted_city_radius_tile_pointers, (int)workable_tile, &stored_ptr)) { - info = malloc (sizeof (struct highlighted_city_radius_tile_info)); - info->highlight_level = 0; - itable_insert (&is->highlighted_city_radius_tile_pointers, (int)workable_tile, (int)info); - } else { - info = (struct highlighted_city_radius_tile_info *)stored_ptr; - info->highlight_level += 3; - } - } +int +add_i31b_to_int (int base, i31b addition) +{ + int amount; + bool percent; + i31b_unpack (addition, &amount, &percent); + if (! percent) + return base + amount; + else { + int fraction = (base * int_abs (amount) + 50) / 100; + return (amount >= 0) ? base + fraction : base - fraction; } } -void -set_up_district_buttons (Main_GUI * this) +int +apply_perfume (enum perfume_kind kind, char const * name, int base_amount) { - if (is_online_game () || ! is->current_config.enable_districts) return; - if (is->dc_btn_img_state == IS_UNINITED) init_district_command_buttons (); - if (is->dc_btn_img_state != IS_OK) return; - - Unit * selected_unit = p_main_screen_form->Current_Unit; - if (selected_unit == NULL || ! is_worker(selected_unit)) return; - - Tile * tile = tile_at (selected_unit->Body.X, selected_unit->Body.Y); - if ((tile == NULL) || (tile == p_null_tile) || (tile->CityID >= 0)) return; + i31b perfume_value; + if (stable_look_up (&is->current_config.perfume_specs[kind], name, &perfume_value)) + return add_i31b_to_int (base_amount, perfume_value); + else + return base_amount; +} - int base_type = tile->vtable->m50_Get_Square_BaseType (tile); - if (base_type == SQ_Mountains || base_type == SQ_Forest || base_type == SQ_Jungle || base_type == SQ_Swamp || base_type == SQ_Volcano) return; - if (tile->vtable->m21_Check_Crates (tile, __, 0)) return; - if (tile->vtable->m20_Check_Pollution (tile, __, 0)) return; +int __stdcall +intercept_consideration (int valuation) +{ + City * city = is->ai_considering_production_for_city; + City_Order * order = &is->ai_considering_order; - Command_Button * fortify_button = NULL; - int i_starting_button; - int mine_turns = -1; - for (int n = 0; n < 42; n++) { - if (((this->Unit_Command_Buttons[n].Button.Base_Data.Status2 & 1) != 0) && - (this->Unit_Command_Buttons[n].Command == UCV_Fortify)) { - fortify_button = &this->Unit_Command_Buttons[n]; - i_starting_button = n; - } - if (this->Unit_Command_Buttons[n].Command == UCV_Build_Mine) { - mine_turns = parse_turns_from_tooltip (this->Unit_Command_Buttons[n].Button.ToolTip); - } - if (fortify_button != NULL && mine_turns >= 0) - break; + // Apply perfume + char * order_name; { + if (order->OrderType == COT_Improvement) + order_name = p_bic_data->Improvements[order->OrderID].Name.S; + else + order_name = p_bic_data->UnitTypes[order->OrderID].Name; } + valuation = apply_perfume (PK_PRODUCTION, order_name, valuation); - if (fortify_button == NULL) - return; + // Apply temp AI settler perfume + if ((order->OrderType == COT_Unit) && + (p_bic_data->UnitTypes[order->OrderID].AI_Strategy & UTAI_Settle) && + (is->current_config.ai_settler_perfume_on_founding != 0) && + (is->current_config.ai_settler_perfume_on_founding_duration != 0) && + (is->turn_no_of_last_founding_for_settler_perfume[city->Body.CivID] >= 0)) { + int turns_since_founding = *p_current_turn_no - is->turn_no_of_last_founding_for_settler_perfume[city->Body.CivID]; + int duration = is->current_config.ai_settler_perfume_on_founding_duration; + if (turns_since_founding < duration) { + i31b perfume = is->current_config.ai_settler_perfume_on_founding; - i_starting_button = -1; + // Scale amount by turns remaining + { + int amount; + bool percent; + i31b_unpack (perfume, &amount, &percent); - // Check if there's already a district on this tile. If so, and the unit can build mines, - // ensure the mine button is enabled so the worker can continue construction. - int existing_district_id = -1; - struct district_instance * existing_inst = get_district_instance (tile); - if (existing_inst != NULL) { - existing_district_id = existing_inst->district_type; - if (is->current_config.enable_natural_wonders && existing_inst->district_type == NATURAL_WONDER_DISTRICT_ID) { - return; - } - if (patch_Unit_can_perform_command(selected_unit, __, UCV_Build_Mine)) { - for (int n = 0; n < 42; n++) { - if (this->Unit_Command_Buttons[n].Command == UCV_Build_Mine) { - Command_Button * mine_button = &this->Unit_Command_Buttons[n]; - if ((mine_button->Button.Base_Data.Status2 & 1) == 0) { - mine_button->Button.field_5FC[13] = 0; - mine_button->Button.vtable->m01_Show_Enabled ((Base_Form *)&mine_button->Button, __, 0); - } - break; - } + int percent_remaining = (100 * (duration - turns_since_founding)) / duration; + amount = (amount * percent_remaining + 50) / 100; + + perfume = i31b_pack (amount, percent); } + + valuation = add_i31b_to_int (valuation, perfume); } } - bool district_completed = district_is_complete (tile, existing_district_id); - - // First pass: collect which district types should be shown - int active_districts[COUNT_DISTRICT_TYPES]; - int active_count = 0; - - for (int dc = 0; dc < is->district_count; dc++) { - - if (is->district_configs[dc].command == -1) - continue; - - if ((is->district_configs[dc].command == UCV_Build_Neighborhood) && !is->current_config.enable_neighborhood_districts) continue; - if ((is->district_configs[dc].command == UCV_Build_WonderDistrict) && !is->current_config.enable_wonder_districts) continue; - if ((is->district_configs[dc].command == UCV_Build_DistributionHub) && !is->current_config.enable_distribution_hub_districts) continue; - if ((is->district_configs[dc].command == UCV_Build_Aerodrome) && !is->current_config.enable_aerodrome_districts) continue; - - if (existing_district_id == dc && district_completed) continue; - if ((existing_district_id >= 0) && (existing_district_id != dc) && (! district_completed)) continue; - - if (!is->district_configs[dc].allow_multiple) { - bool found_same_district_nearby = false; - FOR_TILES_AROUND(tai, is->workable_tile_count, selected_unit->Body.X, selected_unit->Body.Y) { - struct district_instance * nearby_inst = get_district_instance (tai.tile); - if (nearby_inst != NULL && nearby_inst->district_type == dc) { - found_same_district_nearby = true; - break; - } - } - if (found_same_district_nearby) - continue; + if (is->current_config.enable_districts && + (city != NULL) && + ((*p_human_player_bits & (1 << city->Body.CivID)) == 0)) { + bool feasible = false; + switch (order->OrderType) { + case COT_Improvement: + if ((order->OrderID >= 0) && (order->OrderID < p_bic_data->ImprovementsCount) && + (! city_requires_district_for_improvement (city, order->OrderID, NULL))) + feasible = true; + break; + case COT_Unit: + if ((order->OrderID >= 0) && (order->OrderID < p_bic_data->UnitTypeCount)) + feasible = true; + break; + default: + break; } + if (feasible) + record_best_feasible_order (city, order, valuation); + } - int prereq_id = is->district_infos[dc].advance_prereq_id; - if ((prereq_id >= 0) && !Leader_has_tech(&leaders[selected_unit->Body.CivID], __, prereq_id)) continue; + // Expand the list of valuations if necessary + reserve (sizeof is->ai_prod_valuations[0], (void **)&is->ai_prod_valuations, &is->ai_prod_valuations_capacity, is->count_ai_prod_valuations); - // This district should be shown - active_districts[active_count++] = dc; - } + // Record this valuation + int n = is->count_ai_prod_valuations++; + is->ai_prod_valuations[n] = (struct ai_prod_valuation) { + .order_type = order->OrderType, + .order_id = order->OrderID, + .point_value = valuation + }; - if (active_count == 0) - return; + return valuation; +} - // Calculate centered starting position - // For odd counts, center perfectly; for even counts, favor left of center - int center_pos = 6; - i_starting_button = center_pos - (active_count / 2); - if (i_starting_button < 0) - i_starting_button = 0; - - // Second pass: render the buttons - for (int idx = 0; idx < active_count; idx++) { - int dc = active_districts[idx]; - - Command_Button * free_button = NULL; - for (int n = i_starting_button; n < 42; n++) { - if ((this->Unit_Command_Buttons[n].Button.Base_Data.Status2 & 1) == 0) { - free_button = &this->Unit_Command_Buttons[n]; - i_starting_button = n + 1; - break; - } +// Returns a pointer to a bitfield that can be used to record resource access for resource IDs >= 32. This procedure can work with any city ID since +// it allocates and zero-initializes these bit fields as necessary. The given resource ID must be at least 32. The index of the bit for that resource +// within the field is resource_id%32. +unsigned * +get_extra_resource_bits (int city_id, int resource_id) +{ + int extra_resource_count = not_below (0, p_bic_data->ResourceTypeCount - 32); + int ints_per_city = 1 + extra_resource_count/32; + if (city_id >= is->extra_available_resources_capacity) { + int new_capacity = city_id + 100; + unsigned * new_array = calloc (new_capacity * ints_per_city, sizeof new_array[0]); + if (is->extra_available_resources != NULL) { + memcpy (new_array, is->extra_available_resources, is->extra_available_resources_capacity * ints_per_city * sizeof (unsigned)); + free (is->extra_available_resources); } - - if (free_button == NULL) - return; - - // Set up free button for creating district - free_button->Command = is->district_configs[dc].command; - - // Replace the button's image with the district image. Disabling & re-enabling and - // clearing field_5FC[13] are all necessary to trigger a redraw. - free_button->Button.vtable->m02_Show_Disabled ((Base_Form *)&free_button->Button); - free_button->field_6D8 = fortify_button->field_6D8; - for (int k = 0; k < 4; k++) - free_button->Button.Images[k] = &is->district_btn_img_sets[dc].imgs[k]; - free_button->Button.field_664 = fortify_button->Button.field_664; - if (mine_turns >= 0) { - char tooltip[256]; - char const * turn_word = (mine_turns == 1) ? "turn" : "turns"; - snprintf (tooltip, sizeof tooltip, "%s (%d %s)", is->district_configs[dc].tooltip, mine_turns, turn_word); - tooltip[(sizeof tooltip) - 1] = '\0'; - Button_set_tooltip (&free_button->Button, __, tooltip); - } else - Button_set_tooltip (&free_button->Button, __, (char *)is->district_configs[dc].tooltip); - free_button->Button.field_5FC[13] = 0; - free_button->Button.vtable->m01_Show_Enabled ((Base_Form *)&free_button->Button, __, 0); + is->extra_available_resources = new_array; + is->extra_available_resources_capacity = new_capacity; } + return &is->extra_available_resources[city_id * ints_per_city + (resource_id-32)/32]; } -void -set_up_stack_worker_buttons (Main_GUI * this) +void __stdcall +intercept_set_resource_bit (City * city, int resource_id) { - if ((((*p_GetAsyncKeyState) (VK_CONTROL)) >> 8 == 0) || // (control key is not down OR - (! is->current_config.enable_stack_unit_commands) || // stack worker commands not enabled OR - is_online_game ()) // is online game - return; + if (resource_id < 32) + city->Body.Available_Resources |= 1 << resource_id; + else + *get_extra_resource_bits (city->Body.ID, resource_id) |= 1 << (resource_id&31); +} - init_stackable_command_buttons (); - if (is->sc_img_state != IS_OK) - return; +// Must forward declare this function since there's a circular dependency between it and patch_City_has_resource +bool has_resources_required_by_building_r (City * city, int improv_id, int max_req_resource_id); - // For each unit command button - for (int n = 0; n < 42; n++) { - Command_Button * cb = &this->Unit_Command_Buttons[n]; +bool __fastcall +patch_City_has_resource (City * this, int edx, int resource_id) +{ + bool tr; + if (is->current_config.patch_phantom_resource_bug && + (resource_id >= 32) && (resource_id < p_bic_data->ResourceTypeCount) && + (! City_has_trade_connection_to_capital (this))) { + unsigned bits = (this->Body.ID < is->extra_available_resources_capacity) ? *get_extra_resource_bits (this->Body.ID, resource_id) : 0; + tr = (bits >> (resource_id&31)) & 1; + } else + tr = City_has_resource (this, __, resource_id); - // If it's enabled and not a bombard button (those are handled in the function above) - if (((cb->Button.Base_Data.Status2 & 1) != 0) && - (cb->Command != UCV_Bombard) && (cb->Command != UCV_Bombing)) { + // Check if access to this resource is provided by a building in the city + if (! tr) + for (int n = 0; n < is->current_config.count_mills; n++) { + struct mill * mill = &is->current_config.mills[n]; + if ((mill->resource_id == resource_id) && + (mill->flags & MF_LOCAL) && + can_generate_resource (this->Body.CivID, mill) && + has_active_building (this, mill->improv_id) && + has_resources_required_by_building_r (this, mill->improv_id, mill->resource_id - 1)) { + tr = true; + break; + } + } - // Find the stackable worker command that this button controls, if there is one, and check that - // the button isn't already showing the stack image for that command. Note: this check is important - // b/c this function gets called repeatedly while the CTRL key is held down. - for (int sc = 0; sc < COUNT_STACKABLE_COMMANDS; sc++) - if ((cb->Command == sc_button_infos[sc].command) && - (cb->Button.Images[0] != &is->sc_button_image_sets[sc].imgs[0])) { + // Check if access to this resource is provided by a district in the city's work radius + if ((! tr) && is->current_config.enable_districts) { + FOR_DISTRICTS_AROUND (wai, this->Body.X, this->Body.Y, true) { + struct district_instance * di = wai.district_inst; + int district_id = di->district_id; + if ((district_id < 0) || (district_id >= is->district_count)) + continue; - // Replace the button's image with the stack image. Disabling & re-enabling and - // clearing field_5FC[13] are all necessary to trigger a redraw. - cb->Button.vtable->m02_Show_Disabled ((Base_Form *)&cb->Button); - for (int k = 0; k < 4; k++) - cb->Button.Images[k] = &is->sc_button_image_sets[sc].imgs[k]; - cb->Button.field_5FC[13] = 0; - cb->Button.vtable->m01_Show_Enabled ((Base_Form *)&cb->Button, __, 0); + struct district_config * dc = &is->district_configs[district_id]; + if ((dc->generated_resource_id == resource_id) && + (dc->generated_resource_flags & MF_LOCAL) && + district_can_generate_resource (this->Body.CivID, dc)) { + tr = true; + break; + } + } - break; + // A district may alternatively require a bonus resource, which would be missed in above checks. + // If that's the case, check if one is in the work radius + if (! tr) { + int res_class = p_bic_data->ResourceTypes[resource_id].Class; + if (res_class == RC_Bonus) { + int civ_id = this->Body.CivID; + FOR_TILES_AROUND (tai, is->workable_tile_count, this->Body.X, this->Body.Y) { + Tile * tile = tai.tile; + if ((tile == NULL) || (tile == p_null_tile)) + continue; + if (tile->vtable->m38_Get_Territory_OwnerID (tile) != civ_id) + continue; + if (Tile_get_resource_visible_to (tile, __, civ_id) == resource_id) { + tr = true; + break; + } } + } } } -} -CityLocValidity __fastcall patch_Map_check_city_location (Map * this, int edx, int tile_x, int tile_y, int civ_id, bool check_for_city_on_tile); + return tr; +} -void __fastcall -patch_Main_GUI_set_up_unit_command_buttons (Main_GUI * this) +// Checks if the resource requirements for an improvement are satisfied in a given city. The "max_req_resource_id" parameter is to guard against +// infinite loops in case of circular resource dependencies due to mills. The way it works is that resource requirements can only be satisfied if +// their ID is not greater than that limit. This function is called recursively by patch_City_has_resource and in the recursive calls, the limit is +// set to be below the resource ID provided by the mill being considered. So, when considering resource production chains, the limit approaches zero +// with each recursive call, hence infinite loops are not possible. +bool +has_resources_required_by_building_r (City * city, int improv_id, int max_req_resource_id) { - // Recompute resources now if needed because of a trade deal involving mill inputs. In rare cases the change in deals might affect a mill that - // produces a resource that's used for a worker job. - recompute_resources_if_necessary (); + Improvement * improv = &p_bic_data->Improvements[improv_id]; + if (! (improv->ImprovementFlags & ITF_Required_Goods_Must_Be_In_City_Radius)) { + for (int n = 0; n < 2; n++) { + int res_id = (&improv->Resource1ID)[n]; + if ((res_id >= 0) && + ((res_id > max_req_resource_id) || (! patch_City_has_resource (city, __, res_id)))) + return false; + } + return true; + } else { + int * targets = &improv->Resource1ID; + if ((targets[0] < 0) && (targets[1] < 0)) + return true; + int finds[2] = {0, 0}; - Main_GUI_set_up_unit_command_buttons (this); - set_up_stack_bombard_buttons (this); - set_up_stack_worker_buttons (this); + int civ_id = city->Body.CivID; + for (int n = 0; n < is->workable_tile_count; n++) { + int dx, dy; + patch_ni_to_diff_for_work_area (n, &dx, &dy); + int x = city->Body.X + dx, y = city->Body.Y + dy; + wrap_tile_coords (&p_bic_data->Map, &x, &y); + Tile * tile = tile_at (x, y); + if (tile->vtable->m38_Get_Territory_OwnerID (tile) == civ_id) { + int res_here = Tile_get_resource_visible_to (tile, __, civ_id); + if (res_here >= 0) { + finds[0] |= targets[0] == res_here; + finds[1] |= targets[1] == res_here; + } + } + } - if (is->current_config.enable_districts) { - set_up_district_buttons (this); + return ((targets[0] < 0) || finds[0]) && ((targets[1] < 0) || finds[1]); } +} - // If the minimum city separation is increased, then gray out the found city button if we're too close to another city. - if ((is->current_config.minimum_city_separation > 1) && (p_main_screen_form->Current_Unit != NULL) && (is->disabled_command_img_state == IS_OK)) { - Unit_Body * selected_unit = &p_main_screen_form->Current_Unit->Body; - - // For each unit command button - for (int n = 0; n < 42; n++) { - Command_Button * cb = &this->Unit_Command_Buttons[n]; - - // If it's enabled, set to city founding, and the current city location is too close to another city - if (((cb->Button.Base_Data.Status2 & 1) != 0) && - (cb->Command == UCV_Build_City) && - (patch_Map_check_city_location (&p_bic_data->Map, __, selected_unit->X, selected_unit->Y, selected_unit->CivID, false) == CLV_CITY_TOO_CLOSE)) { +bool +has_resources_required_by_building (City * city, int improv_id) +{ + return has_resources_required_by_building_r (city, improv_id, INT_MAX); +} - // Replace the button's image with the disabled image, as in set_up_stack_worker_buttons. - cb->Button.vtable->m02_Show_Disabled ((Base_Form *)&cb->Button); - for (int k = 0; k < 3; k++) - cb->Button.Images[k] = &is->disabled_build_city_button_img; - cb->Button.field_5FC[13] = 0; +void __fastcall +patch_City_recompute_commerce (City * this) +{ + City_recompute_commerce (this); - char tooltip[200]; { - memset (tooltip, 0, sizeof tooltip); - char * label = is->c3x_labels[CL_CITY_TOO_CLOSE_BUTTON_TOOLTIP], - * to_replace = "$NUM0", - * replace_location = strstr (label, to_replace); - if (replace_location != NULL) - snprintf (tooltip, sizeof tooltip, "%.*s%d%s", replace_location - label, label, is->current_config.minimum_city_separation, replace_location + strlen (to_replace)); - else - snprintf (tooltip, sizeof tooltip, "%s", label); - tooltip[(sizeof tooltip) - 1] = '\0'; - } - Button_set_tooltip (&cb->Button, __, tooltip); + if (! (is->current_config.enable_districts || is->current_config.enable_natural_wonders)) + return; - cb->Button.vtable->m01_Show_Enabled ((Base_Form *)&cb->Button, __, 0); + int science_bonus = 0; + calculate_district_culture_science_bonuses (this, NULL, &science_bonus); - } - } + if (science_bonus != 0) { + this->Body.Science += science_bonus; + if (this->Body.Science < 0) + this->Body.Science = 0; } } -void -clear_highlighted_worker_tiles_and_redraw () +void __fastcall +patch_City_recompute_yields_and_happiness (City * this) { - clear_highlighted_worker_tiles_for_districts (); - p_main_screen_form->vtable->m73_call_m22_Draw ((Base_Form *)p_main_screen_form); + if (is->current_config.enable_districts && + is->current_config.enable_distribution_hub_districts && + ! is->distribution_hub_refresh_in_progress && + is->distribution_hub_totals_dirty) + recompute_distribution_hub_totals (); + + City_recompute_yields_and_happiness (this); } -void -check_happiness_at_end_of_turn () +void __fastcall +patch_City_update_culture (City * this) { - int num_unhappy_cities = 0; - City * first_unhappy_city = NULL; - FOR_CITIES_OF (coi, p_main_screen_form->Player_CivID) { - City_recompute_happiness (coi.city); - int num_happy = 0, num_unhappy = 0; - FOR_CITIZENS_IN (ci, coi.city) { - num_happy += ci.ctzn->Body.Mood == CMT_Happy; - num_unhappy += ci.ctzn->Body.Mood == CMT_Unhappy; - } - if (num_unhappy > num_happy) { - num_unhappy_cities++; - if (first_unhappy_city == NULL) - first_unhappy_city = coi.city; - } - } + City_update_culture (this); - if (first_unhappy_city != NULL) { - PopupForm * popup = get_popup_form (); - set_popup_str_param (0, first_unhappy_city->Body.CityName, -1, -1); - if (num_unhappy_cities > 1) - set_popup_int_param (1, num_unhappy_cities - 1); - char * key = (num_unhappy_cities > 1) ? "C3X_DISORDER_WARNING_MULTIPLE" : "C3X_DISORDER_WARNING_ONE"; - popup->vtable->set_text_key_and_flags (popup, __, is->mod_script_path, key, -1, 0, 0, 0); - int response = patch_show_popup (popup, __, 0, 0); + if ((this == NULL) || ! (is->current_config.enable_districts || is->current_config.enable_natural_wonders)) + return; - if (response == 2) { // zoom to city - p_main_screen_form->turn_end_flag = 1; - City_zoom_to (first_unhappy_city, __, 0); - } else if (response == 1) // just cancel turn end - p_main_screen_form->turn_end_flag = 1; - // else do nothing, let turn end - - } -} + int culture_bonus = 0; + calculate_district_culture_science_bonuses (this, &culture_bonus, NULL); -void -do_trade_scroll (DiploForm * diplo, int forward) -{ - int increment = forward ? 1 : -1; - int id = -1; - for (int n = (diplo->other_party_civ_id + increment) & 31; n != diplo->other_party_civ_id; n = (n + increment) & 31) - if ((n != 0) && // if N is not barbs AND - (n != p_main_screen_form->Player_CivID) && // N is not the player's AND - (*p_player_bits & (1U << n)) && // N belongs to an active player AND - (leaders[p_main_screen_form->Player_CivID].Contacts[n] & 1) && // N has contact with the player AND - Leader_ai_would_meet_with (&leaders[n], __, p_main_screen_form->Player_CivID)) { // AI is willing to meet - id = n; - break; - } + if (culture_bonus == 0) + return; - if (id >= 0) { - is->trade_screen_scroll_to_id = id; - DiploForm_close (diplo); - // Note extra code needs to get run here if the other player is not an AI - } -} + int culture_income = this->Body.CultureIncome + culture_bonus; + if (culture_income < 0) + culture_income = 0; + this->Body.CultureIncome = culture_income; -void __fastcall -patch_DiploForm_m82_handle_key_event (DiploForm * this, int edx, int virtual_key_code, int is_down) -{ - if (is->eligible_for_trade_scroll && - (this->mode == 2) && - ((virtual_key_code == VK_LEFT) || (virtual_key_code == VK_RIGHT)) && - (! is_down)) - do_trade_scroll (this, virtual_key_code == VK_RIGHT); - else - DiploForm_m82_handle_key_event (this, __, virtual_key_code, is_down); + int civ_id = this->Body.CivID; + int total_culture = this->Body.Total_Cultures[civ_id] + culture_bonus; + if (total_culture < 0) + total_culture = 0; + this->Body.Total_Cultures[civ_id] = total_culture; + + City_recompute_cultural_level (this, __, '\0', '\0', '\0'); } -int __fastcall -patch_DiploForm_m68_Show_Dialog (DiploForm * this, int edx, int param_1, void * param_2, void * param_3) +void __fastcall +patch_City_recompute_culture_income (City * this) { - if (is->open_diplo_form_straight_to_trade) { - is->open_diplo_form_straight_to_trade = 0; + City_recompute_culture_income (this); - // Done by the base game but not necessary as far as I can tell - // void (__cdecl * FUN_00537700) (int) = 0x537700; - // FUN_00537700 (0x15); - // this->field_E9C[0] = this->field_E9C[0] + 1; + if (! (is->current_config.enable_districts || is->current_config.enable_natural_wonders)) + return; - // Set diplo screen mode to two-way trade negotation - this->mode = 2; + int culture_bonus = 0; + calculate_district_culture_science_bonuses (this, &culture_bonus, NULL); - // Set AI's diplo message to something like "what did you have in mind" - int iVar35 = DiploForm_set_their_message (this, __, DM_AI_PROPOSAL_RESPONSE, 0, 0x1A8); - this->field_1390[0] = DM_AI_PROPOSAL_RESPONSE; - this->field_1390[1] = 0; - this->field_1390[2] = iVar35; + if (culture_bonus != 0) { + this->Body.CultureIncome += culture_bonus; + if (this->Body.CultureIncome < 0) + this->Body.CultureIncome = 0; + } +} - // Reset player message options. This will replace the propose deal, declare war, and leave options with the usual "nevermind" option - // that appears for negotiations. - DiploForm_reset_our_message_choices (this); +// Recomputes yields in cities with active mills that depend on input resources. Intended to be called when an input resource has been potentially +// gained or lost. Recomputes only for the cities of a given leader or, if NULL, for all cities on the map. +void +recompute_mill_yields_after_resource_change (Leader * leader_or_null) +{ + if (p_cities->Cities != NULL) + for (int city_index = 0; city_index <= p_cities->LastIndex; city_index++) { + City * city = get_city_ptr (city_index); + if ((city != NULL) && + ((leader_or_null == NULL) || (city->Body.CivID == leader_or_null->ID))) { + bool any_relevant_mills = false; + for (int n = 0; n < is->current_config.count_mills; n++) { + struct mill * mill = &is->current_config.mills[n]; + Improvement * mill_improv = &p_bic_data->Improvements[mill->improv_id]; + if ((mill->flags & MF_YIELDS) && + ((mill_improv->Resource1ID >= 0) || (mill_improv->Resource2ID >= 0)) && + has_active_building (city, mill->improv_id)) { + any_relevant_mills = true; + break; + } + } + if (any_relevant_mills) + patch_City_recompute_yields_and_happiness (city); + } + } +} - // Done by the base game but not necessary as far as I can tell - // void (__fastcall * FUN_004C89A0) (void *) = 0x4C89A0; - // FUN_004C89A0 (&this->field_1BF4[0]); +int +resource_tile_resource_id (struct extra_resource_tile const * rt) +{ + if (rt == NULL) + return -1; + if (rt->type == ERT_MILL_RESOURCE) { + return (rt->mill_info.mill != NULL) ? rt->mill_info.mill->resource_id : -1; + } else if (rt->district_info.cfg != NULL) { + return rt->district_info.cfg->generated_resource_id; } + return -1; +} - return DiploForm_m68_Show_Dialog (this, __, param_1, param_2, param_3); +int +compare_resource_tiles (void const * vp_a, void const * vp_b) +{ + struct extra_resource_tile const * a = vp_a, * b = vp_b; + return resource_tile_resource_id (a) - resource_tile_resource_id (b); } void __fastcall -patch_DiploForm_do_diplomacy (DiploForm * this, int edx, int diplo_message, int param_2, int civ_id, int do_not_request_audience, int war_negotiation, int disallow_proposal, TradeOfferList * our_offers, TradeOfferList * their_offers) +patch_Trade_Net_recompute_resources (Trade_Net * this, int edx, bool skip_popups) { - is->open_diplo_form_straight_to_trade = 0; - is->trade_screen_scroll_to_id = -1; + int extra_resource_count = not_below (0, p_bic_data->ResourceTypeCount - 32); + int ints_per_city = 1 + extra_resource_count/32; + memset (is->extra_available_resources, 0, is->extra_available_resources_capacity * ints_per_city * sizeof (unsigned)); - // Trade screen scroll is disabled in online games b/c there's extra synchronization we'd need to do to open or close the diplo screen with - // a human player. - is->eligible_for_trade_scroll = is->current_config.enable_trade_screen_scroll && (! is_online_game ()); + // Assemble list of mill tiles + is->count_resource_tiles = 0; + if (p_cities->Cities != NULL) + for (int city_index = 0; city_index <= p_cities->LastIndex; city_index++) { + City * city = get_city_ptr (city_index); + if (city != NULL) + for (int n = 0; n < is->current_config.count_mills; n++) { + struct mill * mill = &is->current_config.mills[n]; + if (((mill->flags & MF_LOCAL) == 0) && + has_active_building (city, mill->improv_id) && + can_generate_resource (city->Body.CivID, mill)) { + Tile * resource_tile = tile_at (city->Body.X, city->Body.Y); + if ((resource_tile == NULL) || (resource_tile == p_null_tile)) + continue; + reserve (sizeof is->resource_tiles[0], + (void **)&is->resource_tiles, + &is->resource_tiles_capacity, + is->count_resource_tiles); + is->resource_tiles[is->count_resource_tiles++] = (struct extra_resource_tile) { + .tile = resource_tile, + .type = ERT_MILL_RESOURCE, + .mill_info = { + .city = city, + .mill = mill + } + }; + } + } + } + if (is->current_config.enable_districts) { + FOR_TABLE_ENTRIES (tei, &is->district_tile_map) { + Tile * district_tile = (Tile *)tei.key; + struct district_instance * inst = (struct district_instance *)tei.value; + if ((district_tile == NULL) || (district_tile == p_null_tile) || (inst == NULL) || (inst->state != DS_COMPLETED)) + continue; - if (is->eligible_for_trade_scroll && (is->trade_scroll_button_state == IS_UNINITED)) - init_trade_scroll_buttons (this); + int district_id = inst->district_id; + if ((district_id < 0) || (district_id >= is->district_count)) + continue; - WITH_PAUSE_FOR_POPUP { - DiploForm_do_diplomacy (this, __, diplo_message, param_2, civ_id, do_not_request_audience, war_negotiation, disallow_proposal, our_offers, their_offers); + struct district_config * cfg = &is->district_configs[district_id]; + if ((cfg == NULL) || (cfg->generated_resource_id < 0)) + continue; - while (is->trade_screen_scroll_to_id >= 0) { - int scroll_to_id = is->trade_screen_scroll_to_id; - is->trade_screen_scroll_to_id = -1; - is->open_diplo_form_straight_to_trade = 1; - DiploForm_do_diplomacy (this, __, DM_AI_COUNTER, 0, scroll_to_id, 1, 0, 0, NULL, NULL); + int owner_id = district_tile->vtable->m38_Get_Territory_OwnerID (district_tile); + if (owner_id < 0) + continue; + if (! district_can_generate_resource (owner_id, cfg)) + continue; + if ((cfg->generated_resource_flags & MF_LOCAL) != 0) + continue; + + reserve (sizeof is->resource_tiles[0], + (void **)&is->resource_tiles, + &is->resource_tiles_capacity, + is->count_resource_tiles); + is->resource_tiles[is->count_resource_tiles++] = (struct extra_resource_tile) { + .tile = district_tile, + .type = ERT_DISTRICT_RESOURCE, + .district_info = { + .inst = inst, + .cfg = cfg + } + }; } } + qsort (is->resource_tiles, is->count_resource_tiles, sizeof is->resource_tiles[0], compare_resource_tiles); - is->open_diplo_form_straight_to_trade = 0; - is->eligible_for_trade_scroll = 0; -} + is->got_resource_tile = NULL; + is->saved_tile_count = p_bic_data->Map.TileCount; + p_bic_data->Map.TileCount += is->count_resource_tiles; + Trade_Net_recompute_resources (this, __, skip_popups); -void __fastcall -patch_DiploForm_m22_Draw (DiploForm * this) -{ - if (is->trade_scroll_button_state == IS_OK) { - Button * left = is->trade_scroll_button_left, - * right = is->trade_scroll_button_right; - if (is->eligible_for_trade_scroll && (this->mode == 2)) { - left ->vtable->m01_Show_Enabled ((Base_Form *)left , __, 0); - right->vtable->m01_Show_Enabled ((Base_Form *)right, __, 0); - left ->vtable->m73_call_m22_Draw ((Base_Form *)left); - right->vtable->m73_call_m22_Draw ((Base_Form *)right); - } else { - left ->vtable->m02_Show_Disabled ((Base_Form *)left); - right->vtable->m02_Show_Disabled ((Base_Form *)right); - } + // Restore the tile count if necessary. It may have already been restored by patch_Map_Renderer_m71_Draw_Tiles. This happens when the call to + // recompute_resources above opens a popup message about connecting a resource for the first time, which triggers redraw of the map. + if (is->saved_tile_count >= 0) { + p_bic_data->Map.TileCount = is->saved_tile_count; + is->saved_tile_count = -1; } - DiploForm_m22_Draw (this); + recompute_mill_yields_after_resource_change (NULL); + + is->must_recompute_resources_for_mill_inputs = false; } -void -intercept_end_of_turn () +Tile * +get_resource_tile (int index) { - if (is->current_config.enable_disorder_warning) { - check_happiness_at_end_of_turn (); - if (p_main_screen_form->turn_end_flag == 1) // Check if player cancelled turn ending in the disorder warning popup - return; - } + struct extra_resource_tile * rt = &is->resource_tiles[index]; + is->got_resource_tile = rt; + return rt->tile; +} - // Sometimes clearing of highlighted tiles doesn't trigger when CTRL lifted, so double-check here - if (is->current_config.enable_city_work_radii_highlights && is->highlight_city_radii) { - is->highlight_city_radii = false; - clear_highlighted_worker_tiles_and_redraw (); +Tile * __fastcall patch_Map_get_tile_when_recomputing_resources_1 (Map * map, int edx, int index) { return (index < is->saved_tile_count) ? Map_get_tile (map, __, index) : get_resource_tile (index - is->saved_tile_count); } +Tile * __fastcall patch_Map_get_tile_when_recomputing_resources_2 (Map * map, int edx, int index) { return (index < is->saved_tile_count) ? Map_get_tile (map, __, index) : get_resource_tile (index - is->saved_tile_count); } +Tile * __fastcall patch_Map_get_tile_when_recomputing_resources_3 (Map * map, int edx, int index) { return (index < is->saved_tile_count) ? Map_get_tile (map, __, index) : get_resource_tile (index - is->saved_tile_count); } +Tile * __fastcall patch_Map_get_tile_when_recomputing_resources_4 (Map * map, int edx, int index) { return (index < is->saved_tile_count) ? Map_get_tile (map, __, index) : get_resource_tile (index - is->saved_tile_count); } +Tile * __fastcall patch_Map_get_tile_when_recomputing_resources_5 (Map * map, int edx, int index) { return (index < is->saved_tile_count) ? Map_get_tile (map, __, index) : get_resource_tile (index - is->saved_tile_count); } + +int __fastcall +patch_Tile_get_visible_resource_when_recomputing (Tile * tile, int edx, int civ_id) +{ + if (is->got_resource_tile != NULL) { + struct extra_resource_tile * rt = is->got_resource_tile; + is->got_resource_tile = NULL; + if (rt->type == ERT_MILL_RESOURCE) { + if ((rt->mill_info.city != NULL) && (rt->mill_info.mill != NULL) && + has_resources_required_by_building (rt->mill_info.city, rt->mill_info.mill->improv_id)) + return rt->mill_info.mill->resource_id; + else + return -1; + } else { + Tile * district_tile = rt->tile; + struct district_instance * inst = rt->district_info.inst; + struct district_config * cfg = rt->district_info.cfg; + if ((district_tile != NULL) && (district_tile != p_null_tile) && (inst != NULL) && + (cfg != NULL) && (inst->state == DS_COMPLETED)) { + int owner_id = district_tile->vtable->m38_Get_Territory_OwnerID (district_tile); + if ((owner_id == civ_id) && district_can_generate_resource (owner_id, cfg)) + return cfg->generated_resource_id; + } + return -1; + } } - // Clear things that don't apply across turns - is->have_job_and_loc_to_skip = 0; + int base_resource = Tile_get_resource_visible_to (tile, __, civ_id); + return base_resource; } -bool -is_worker_or_settler_command (int unit_command_value) +int WINAPI +patch_MessageBoxA (HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType) { - return (unit_command_value & 0x20000000) || - ((unit_command_value >= UCV_Build_Remote_Colony) && (unit_command_value <= UCV_Auto_Save_Tiles)); + if (is->current_config.suppress_hypertext_links_exceeded_popup && + (strcmp (lpText, "Maximum hypertext links exceeded!") == 0)) + return IDOK; + else + return MessageBoxA (hWnd, lpText, lpCaption, uType); } -bool -command_would_replace_district (int unit_command_value) +char * __fastcall +do_capture_modified_gold_trade (TradeOffer * trade_offer, int edx, int val, char * str, unsigned base) { - // Note: Roads & railroads, etc. can coexist with the district - return (unit_command_value == UCV_Build_Mine) || - (unit_command_value == UCV_Irrigate) || - (unit_command_value == UCV_Plant_Forest) || - (unit_command_value == UCV_Build_Outpost) || - (unit_command_value == UCV_Build_Fortress) || - (unit_command_value == UCV_Build_Barricade) || - (unit_command_value == UCV_Build_Airfield) || - (unit_command_value == UCV_Build_Radar_Tower) || - (unit_command_value == UCV_Build_Colony); + is->modifying_gold_trade = trade_offer; + return print_int (val, str, base); } -bool -handle_worker_command_that_may_replace_district (Unit * unit, int unit_command_value, bool * removed_existing) +// Here the order of the registers matches the order that they're pushed by the pusha instruction +struct register_set { + int edi, esi, ebp, esp, ebx, edx, ecx, eax; +}; + +// Return 1 to allow the candidate unit to exert ZoC, 0 to exclude it. A pointer to the candidate is in esi. +int __stdcall +filter_zoc_candidate (struct register_set * reg) { - if (removed_existing != NULL) - *removed_existing = false; + Unit * candidate = (Unit *)reg->esi, + * defender = is->zoc_defender; - if ((! is->current_config.enable_districts) || (unit == NULL)) return true; - if (! is_worker_or_settler_command (unit_command_value)) return true; - if (! command_would_replace_district (unit_command_value)) return true; - if (! patch_Unit_can_perform_command (unit, __, unit_command_value)) return true; + UnitType * candidate_type = &p_bic_data->UnitTypes[candidate->Body.UnitTypeID], + * defender_type = &p_bic_data->UnitTypes[defender ->Body.UnitTypeID]; - Tile * tile = tile_at (unit->Body.X, unit->Body.Y); - if ((tile == NULL) || (tile == p_null_tile)) - return true; + enum UnitTypeClasses candidate_class = candidate_type->Unit_Class, + defender_class = defender_type ->Unit_Class; - struct district_instance * inst = get_district_instance (tile); - if ((inst == NULL) || (! district_is_complete (tile, inst->district_type))) - return true; + bool lethal = ((is->current_config.special_zone_of_control_rules & SZOCR_LETHAL) != 0) && (! is->temporarily_disallow_lethal_zoc); + bool aerial = (is->current_config.special_zone_of_control_rules & SZOCR_AERIAL ) != 0, + amphibious = (is->current_config.special_zone_of_control_rules & SZOCR_AMPHIBIOUS) != 0; - int tile_x, tile_y; - if (! district_instance_get_coords (inst, tile, &tile_x, &tile_y)) - return false; + // Exclude air units if aerial ZoC is not enabled and exclude land-to-sea & sea-to-land ZoC if amphibious is not enabled + if ((! aerial) && (candidate_class == UTC_Air)) + return 0; + if ((! amphibious) && + (((candidate_class == UTC_Land) && (defender_class == UTC_Sea )) || + ((candidate_class == UTC_Sea ) && (defender_class == UTC_Land)))) + return 0; - int district_id = inst->district_type; - int civ_id = unit->Body.CivID; - bool redundant_district = district_instance_is_redundant (inst, tile); - bool would_lose_buildings = any_nearby_city_would_lose_district_benefits (district_id, civ_id, tile_x, tile_y); - if (redundant_district) - would_lose_buildings = false; + // In case of cross-domain ZoC, filter out units with zero bombard strength or range. They can't use their attack strength in this case, so + // without bombard they can be ruled out. Don't forget units may have non-zero bombard strength and zero range for defensive bombard. + int range = (candidate_class != UTC_Air) ? candidate_type->Bombard_Range : candidate_type->OperationalRange; + if ((candidate_class != defender_class) && ((candidate_type->Bombard_Strength <= 0) || (range <= 0))) + return 0; - bool remove_existing = redundant_district; - if (inst != NULL && district_id >= 0 && district_id < is->district_count) { - PopupForm * popup = get_popup_form (); - set_popup_str_param (0, (char *)is->district_configs[district_id].name, -1, -1); - set_popup_str_param (1, (char *)is->district_configs[district_id].name, -1, -1); - popup->vtable->set_text_key_and_flags ( - popup, __, is->mod_script_path, - would_lose_buildings - ? "C3X_CONFIRM_BUILD_IMPROVEMENT_OVER_DISTRICT" - : "C3X_CONFIRM_BUILD_IMPROVEMENT_OVER_DISTRICT_SAFE", - -1, 0, 0, 0); - int sel = patch_show_popup (popup, __, 0, 0); - if (sel != 0) - return false; - remove_existing = true; - } + // Require lethal config option & lethal bombard against one HP defender + if ((Unit_get_max_hp (defender) - defender->Body.Damage <= 1) && + ((! lethal) || + ((defender_class == UTC_Sea) && ! UnitType_has_ability (candidate_type, __, UTA_Lethal_Sea_Bombardment)) || + ((defender_class != UTC_Sea) && ! UnitType_has_ability (candidate_type, __, UTA_Lethal_Land_Bombardment)))) + return 0; - if (remove_existing) { - remove_district_instance (tile); - tile->vtable->m62_Set_Tile_BuildingID (tile, __, -1); - tile->vtable->m51_Unset_Tile_Flags (tile, __, 0, TILE_FLAG_MINE, tile_x, tile_y); - handle_district_removed (tile, district_id, tile_x, tile_y, false); - if (removed_existing != NULL) - *removed_existing = true; + // Air units require the bombing action to perform ZoC + if ((candidate_class == UTC_Air) && ! (candidate_type->Air_Missions & UCV_Bombing)) + return 0; + + // Exclude land units in transports if configured + if ((is->current_config.special_zone_of_control_rules & SZOCR_NOT_FROM_INSIDE) && candidate_class == UTC_Land) { + Unit * container = get_unit_ptr (candidate->Body.Container_Unit); + if ((container != NULL) && ! UnitType_has_ability (&p_bic_data->UnitTypes[container->Body.UnitTypeID], __, UTA_Army)) + return 0; } - return true; + return 1; } -bool __fastcall - patch_Unit_can_upgrade (Unit * this) -{ - bool base = Unit_can_upgrade (this); - int available; - City * city = city_at (this->Body.X, this->Body.Y); - if (base && - (city != NULL) && - get_available_unit_count (&leaders[this->Body.CivID], City_get_upgraded_type_id (city, __, this->Body.UnitTypeID), &available) && - (available <= 0)) - return false; - else - return base; -} +#define TRADE_NET_REF_COUNT 315 +#define TRADE_NET_INSTR_COUNT_GOG 22 +#define TRADE_NET_INSTR_COUNT_STEAM 23 +#define TRADE_NET_INSTR_COUNT_PCG 22 +#define TRADE_NET_ADDR_TOTAL_COUNT ((TRADE_NET_REF_COUNT * 3) + TRADE_NET_INSTR_COUNT_GOG + TRADE_NET_INSTR_COUNT_STEAM + TRADE_NET_INSTR_COUNT_PCG) -bool -is_district_command (int unit_command_value) +int * +load_trade_net_addrs () { - int dummy; - return itable_look_up (&is->command_id_to_district_id, unit_command_value, &dummy); -} + if (is->trade_net_addrs_load_state == IS_OK) + return is->trade_net_addrs; + else if (is->trade_net_addrs_load_state == IS_INIT_FAILED) + return NULL; -bool __fastcall -patch_Unit_can_perform_command (Unit * this, int edx, int unit_command_value) -{ - if (is->current_config.enable_districts || is->current_config.enable_natural_wonders) { - Tile * tile = tile_at (this->Body.X, this->Body.Y); - if ((tile != NULL) && (tile != p_null_tile) && is->current_config.enable_natural_wonders) { - struct district_instance * inst = get_district_instance (tile); - if ((inst != NULL) && - (inst->district_type == NATURAL_WONDER_DISTRICT_ID) && - (inst->natural_wonder_info.natural_wonder_id >= 0)) { - if (is_worker_or_settler_command (unit_command_value) || is_district_command (unit_command_value)) - return false; - } - } - enum SquareTypes base_type = tile->vtable->m50_Get_Square_BaseType (tile); + bool success = false; + char err_msg[300] = {0}; - if (is_district_command (unit_command_value)) { - return (base_type != SQ_Mountains && base_type != SQ_Forest && base_type != SQ_Jungle && base_type != SQ_Swamp && base_type != SQ_Volcano); + is->trade_net_addrs = calloc (3 * TRADE_NET_ADDR_TOTAL_COUNT, sizeof is->trade_net_addrs[0]); + if (! is->trade_net_addrs) { + snprintf (err_msg, (sizeof err_msg) - 1, "Bad alloc"); + goto done; + } + + char file_path[MAX_PATH] = {0}; + snprintf (file_path, (sizeof file_path) - 1, "%s\\trade_net_addresses.txt", is->mod_rel_dir); + char * refs_file = file_to_string (file_path); + if (! refs_file) { + snprintf (err_msg, (sizeof err_msg) - 1, "Couldn't load %s", file_path); + goto done; + } + + char * cursor = refs_file; + int loaded_count = 0; + while (true) { + if (*cursor == '#') { // comment line + skip_line (&cursor); + continue; } - else if (unit_command_value == UCV_Build_Mine) { - bool has_district = (get_district_instance (tile) != NULL); - if (has_district) { - return (base_type != SQ_FloodPlain && base_type != SQ_Forest && base_type != SQ_Jungle && base_type != SQ_Volcano); + skip_horiz_space (&cursor); + if (*cursor == '\n') { // empty line + cursor++; + continue; + } else if (*cursor == '\0') // end of file + break; + + // otherwise we must be on a line with some addresses + int ref; + bool got_any_addresses = false; + while (parse_int (&cursor, &ref)) { + if (loaded_count >= TRADE_NET_ADDR_TOTAL_COUNT) { + snprintf (err_msg, (sizeof err_msg) - 1, "Too many values in file (expected %d exactly)", TRADE_NET_ADDR_TOTAL_COUNT); + goto done; } + is->trade_net_addrs[loaded_count] = ref; + loaded_count++; + got_any_addresses = true; } - else if (unit_command_value == UCV_Join_City) { - bool is_human = (*p_human_player_bits & (1 << this->Body.CivID)) != 0; - if (!is_human) { - int type_id = this->Body.UnitTypeID; - if ((type_id >= 0) && (type_id < p_bic_data->UnitTypeCount)) { - int worker_actions = p_bic_data->UnitTypes[type_id].Worker_Actions; - if (worker_actions != 0 && (worker_actions & (UCV_Automate))) { - int civ_id = this->Body.CivID; - if (civ_id >= 0 && civ_id < 32) { - return is->city_pending_district_requests[civ_id].len == 0; - } - return true; - } - } - } + + if (! got_any_addresses) { + snprintf (err_msg, (sizeof err_msg) - 1, "Parse error"); + goto done; } - } - if (is->current_config.disable_worker_automation && - (this->Body.CivID == p_main_screen_form->Player_CivID) && - (unit_command_value == UCV_Automate)) - return false; - else if (is->current_config.disallow_land_units_from_affecting_water_tiles && - is_worker_or_settler_command (unit_command_value)) { - Tile * tile = tile_at (this->Body.X, this->Body.Y); - enum UnitTypeClasses class = p_bic_data->UnitTypes[this->Body.UnitTypeID].Unit_Class; - return ((class != UTC_Land) || (! tile->vtable->m35_Check_Is_Water (tile))) && - Unit_can_perform_command (this, __, unit_command_value); } - if ((unit_command_value == UCV_Pillage) && - ! patch_Unit_can_pillage (this, __, this->Body.X, this->Body.Y)) { - return false; + if (loaded_count < TRADE_NET_ADDR_TOTAL_COUNT) { + snprintf (err_msg, (sizeof err_msg) - 1, "Too few values in file (expected %d exactly)", TRADE_NET_ADDR_TOTAL_COUNT); + goto done; } - - return Unit_can_perform_command (this, __, unit_command_value); -} - -bool __fastcall -patch_Unit_can_pillage (Unit * this, int edx, int tile_x, int tile_y) -{ - bool base = Unit_can_pillage (this, __, tile_x, tile_y); - if (! base) - return false; - if (! is->current_config.enable_districts || - ! is->current_config.enable_wonder_districts || - is->current_config.completed_wonder_districts_can_be_destroyed) - return true; + success = true; - Tile * tile = tile_at (tile_x, tile_y); - if ((tile == NULL) || (tile == p_null_tile)) - return true; +done: + free (refs_file); + if (! success) { + char full_err_msg[300] = {0}; + snprintf (full_err_msg, (sizeof full_err_msg) - 1, "Failed to load trade net refs: %s", err_msg); + MessageBox (NULL, full_err_msg, NULL, MB_ICONERROR); + is->trade_net_addrs_load_state = IS_INIT_FAILED; + return NULL; + } else { + is->trade_net_addrs_load_state = IS_OK; + return is->trade_net_addrs; + } +} - struct district_instance * inst = get_district_instance (tile); - if (inst == NULL) - return true; +unsigned short * __fastcall +patch_get_pixel_to_draw_city_dot (JGL_Image * this, int edx, int x, int y) +{ + unsigned short * tr = this->vtable->m07_m05_Get_Pixel (this, __, x, y); - if (is->current_config.enable_natural_wonders && - (inst->district_type == NATURAL_WONDER_DISTRICT_ID) && - (inst->natural_wonder_info.natural_wonder_id >= 0)) - return false; + if ((x + 1 < p_main_screen_form->GUI.Navigator_Data.Mini_Map_Width2) && (y + 1 < p_main_screen_form->GUI.Navigator_Data.Mini_Map_Height2)) { + unsigned short * below = this->vtable->m07_m05_Get_Pixel (this, __, x + 1, y + 1); + if (below != NULL) + *below = 0; + } - int district_id = inst->district_type; - if (district_id != WONDER_DISTRICT_ID) - return true; + return tr; +} - if (! district_is_complete (tile, district_id)) - return true; +enum branch_kind { BK_CALL, BK_JUMP }; - // Check if this wonder district has a completed wonder on it - struct wonder_district_info * info = get_wonder_district_info (tile); - if (info == NULL || info->state != WDS_COMPLETED) - return true; - - return false; +byte * +emit_branch (enum branch_kind kind, byte * cursor, void const * target) +{ + int offset = (int)target - ((int)cursor + 5); + *cursor++ = (kind == BK_CALL) ? 0xE8 : 0xE9; + return int_to_bytes (cursor, offset); } -bool __fastcall -patch_Unit_can_do_worker_command_for_button_setup (Unit * this, int edx, int unit_command_value) +// Just calls VirtualProtect and displays an error message if it fails. Made for use by the WITH_MEM_PROTECTION macro. +bool +check_virtual_protect (LPVOID addr, SIZE_T size, DWORD flags, PDWORD old_protect) { - bool base = patch_Unit_can_perform_command (this, __, unit_command_value); - - // If the command is to build a city and it can't be done because another city is already too close, and the minimum separation was changed - // from its standard value, then return true here so that the build city button will be added anyway. We'll gray it out later. Check that the - // grayed out button image is initialized now so we don't activate the build city button then find out later we can't gray it out. - if ((! base) && - (unit_command_value == UCV_Build_City) && - (is->current_config.minimum_city_separation > 1) && - (patch_Map_check_city_location (&p_bic_data->Map, __, this->Body.X, this->Body.Y, this->Body.CivID, false) == CLV_CITY_TOO_CLOSE) && - (init_disabled_command_buttons (), is->disabled_command_img_state == IS_OK)) + if (VirtualProtect (addr, size, flags, old_protect)) return true; - - else - return base; + else { + char err_msg[1000]; + snprintf (err_msg, sizeof err_msg, "VirtualProtect failed! Args:\n Address: 0x%p\n Size: %d\n Flags: 0x%x", addr, size, flags); + err_msg[(sizeof err_msg) - 1] = '\0'; + MessageBoxA (NULL, err_msg, NULL, MB_ICONWARNING); + return false; + } } -int -compare_helpers (void const * vp_a, void const * vp_b) -{ - Unit * a = get_unit_ptr (*(int *)vp_a), - * b = get_unit_ptr (*(int *)vp_b); - if ((a != NULL) && (b != NULL)) { - // Compute how many movement points each has left (ML = moves left) - int ml_a = patch_Unit_get_max_move_points (a) - a->Body.Moves, - ml_b = patch_Unit_get_max_move_points (b) - b->Body.Moves; +#define WITH_MEM_PROTECTION(addr, size, flags) \ + for (DWORD old_protect, unused, iter_count = 0; \ + (iter_count == 0) && check_virtual_protect (addr, size, flags, &old_protect); \ + VirtualProtect (addr, size, old_protect, &unused), iter_count++) - // Whichever one has more MP left comes first in the array - if (ml_a > ml_b) return 1; - else if (ml_b > ml_a) return -1; - else return 0; - } else - // If at least one of the unit ids is invalid, might as well sort it later in the array - return (a != NULL) ? -1 : ((b != NULL) ? 1 : 0); -} +void __fastcall adjust_sliders_preproduction (Leader * this); + +struct saved_code_area { + int size; + byte original_contents[]; +}; +// Saves an area of base game code and optionally replaces it with no-ops. The area can be restored with restore_code_area. This method assumes that +// the necessary memory protection has already been set on the area, specifically that it can be written to. The method will do nothing if the area +// has already been saved with the same size. It's an error to re-save an address with a different size or overlap two saved areas. void -issue_stack_worker_command (Unit * unit, int command) +save_code_area (byte * addr, int size, bool nopify) { - int tile_x = unit->Body.X; - int tile_y = unit->Body.Y; - Tile * tile = tile_at (tile_x, tile_y); - int unit_type_id = unit->Body.UnitTypeID; - int unit_id = unit->Body.ID; - - // Put together a list of helpers and store it on the memo. Helpers are just other workers on the same tile that can be issued the same command. - clear_memo (); - FOR_UNITS_ON (uti, tile) - if ((uti.id != unit_id) && - (uti.unit->Body.Container_Unit < 0) && - (uti.unit->Body.UnitState == 0) && - (uti.unit->Body.Moves < patch_Unit_get_max_move_points (uti.unit))) { - // check if the clicked command is among worker actions that this unit type can perform - int actions = p_bic_data->UnitTypes[uti.unit->Body.UnitTypeID].Worker_Actions; - int command_without_category = command & 0x0FFFFFFF; - if ((actions & command_without_category) == command_without_category) - memoize (uti.id); + struct saved_code_area * sca; + if (itable_look_up (&is->saved_code_areas, (int)addr, (int *)&sca)) { + if (sca->size != 0) { + if (sca->size != size) { + char s[200]; + snprintf (s, sizeof s, "Save code area conflict: address %p was already saved with size %d, conflicting with new size %d.", addr, sca->size, size); + s[(sizeof s) - 1] = '\0'; + pop_up_in_game_error (s); + } + return; } - - // Sort the list of helpers so that the ones with the fewest remaining movement points are listed first. - qsort (is->memo, is->memo_len, sizeof is->memo[0], compare_helpers); - - Unit * next_up = unit; - int i_next_helper = 0; - int last_action_didnt_happen; - do { - int state_before_action = next_up->Body.UnitState; - Main_Screen_Form_issue_command (p_main_screen_form, __, command, next_up); - last_action_didnt_happen = (next_up->Body.UnitState == state_before_action); + } else { + sca = malloc (size + sizeof *sca); + itable_insert (&is->saved_code_areas, (int)addr, (int)sca); + } + sca->size = size; + memcpy (&sca->original_contents, addr, size); + if (nopify) + memset (addr, 0x90, size); +} - // Call this update function to cause the worker to actually perform the action. Otherwise - // it only gets queued, the worker keeps is movement points, and the action doesn't get done - // until the interturn. - if (! last_action_didnt_happen) - next_up->vtable->update_while_active (next_up); +// Restores a saved chunk of code to its original contents. Does nothing if the area hasn't been saved. Assumes the appropriate memory protection has +// already been set. +void +restore_code_area (byte * addr) +{ + struct saved_code_area * sca; + if (itable_look_up (&is->saved_code_areas, (int)addr, (int *)&sca)) { + memcpy (addr, &sca->original_contents, sca->size); + sca->size = 0; + } +} - next_up = NULL; - while ((i_next_helper < is->memo_len) && (next_up == NULL)) - next_up = get_unit_ptr (is->memo[i_next_helper++]); - } while ((next_up != NULL) && (! last_action_didnt_happen)); +bool +is_code_area_saved (byte * addr) +{ + struct saved_code_area * sca; + return itable_look_up (&is->saved_code_areas, (int)addr, (int *)&sca) && (sca->size > 0); } +// Nopifies or restores an area depending on if yes_or_no is 1 or 0. Sets the necessary memory protections. void -issue_district_worker_command (Unit * unit, int command) +set_nopification (int yes_or_no, byte * addr, int size) { - if (! is->current_config.enable_districts) - return; + WITH_MEM_PROTECTION (addr, size, PAGE_EXECUTE_READWRITE) { + if (yes_or_no) + save_code_area (addr, size, true); + else + restore_code_area (addr); + } +} - int tile_x = unit->Body.X; - int tile_y = unit->Body.Y; - Tile * tile = tile_at (tile_x, tile_y); - int unit_type_id = unit->Body.UnitTypeID; - int unit_id = unit->Body.ID; +void +apply_machine_code_edits (struct c3x_config const * cfg, bool at_program_start) +{ + DWORD old_protect, unused; - if (! is_worker(unit)) - return; + // Allow stealth attack against single unit + WITH_MEM_PROTECTION (ADDR_STEALTH_ATTACK_TARGET_COUNT_CHECK, 1, PAGE_EXECUTE_READWRITE) + *(byte *)ADDR_STEALTH_ATTACK_TARGET_COUNT_CHECK = cfg->allow_stealth_attack_against_single_unit ? 0 : 1; - // Check tech prerequisite for the selected district, if any - int district_id; - if (itable_look_up (&is->command_id_to_district_id, command, &district_id)) { - if (district_id < 0 || district_id >= is->district_count) - return; - int prereq_id = is->district_infos[district_id].advance_prereq_id; - // Only enforce if a prereq is configured - if (prereq_id >= 0 && !Leader_has_tech (&leaders[unit->Body.CivID], __, prereq_id)) { - return; // Civ lacks required tech; do not issue command - } + // Enable small wonders providing free buildings + WITH_MEM_PROTECTION (ADDR_RECOMPUTE_AUTO_IMPROVS_FILTER, 10, PAGE_EXECUTE_READWRITE) { + byte normal[8] = {0x83, 0xE1, 0x04, 0x80, 0xF9, 0x04, 0x0F, 0x85}; // and ecx, 4; cmp ecx, 4; jnz [offset] + byte modded[8] = {0x83, 0xE1, 0x0C, 0x80, 0xF9, 0x00, 0x0F, 0x84}; // and ecx, 12; cmp ecx, 0; jz [offset] + for (int n = 0; n < 8; n++) + ((byte *)ADDR_RECOMPUTE_AUTO_IMPROVS_FILTER)[n] = cfg->enable_free_buildings_from_small_wonders ? modded[n] : normal[n]; } - // Disallow placing districts on invalid terrain, pollution, or cratered tiles - if (tile != NULL && tile != p_null_tile) { - if (tile->vtable->m21_Check_Crates (tile, __, 0)) - return; - if (tile->vtable->m20_Check_Pollution (tile, __, 0)) - return; - enum SquareTypes base_type = tile->vtable->m50_Get_Square_BaseType(tile); - if (base_type == SQ_Mountains || base_type == SQ_Forest || base_type == SQ_Jungle || base_type == SQ_Swamp || base_type == SQ_Volcano) { - return; - } - } - if (tile != NULL && tile != p_null_tile) { + // Bypass artillery in city check + // replacing 0x74 (= jump if [city ptr is] zero) with 0xEB (= uncond. jump) + WITH_MEM_PROTECTION (ADDR_CHECK_ARTILLERY_IN_CITY, 1, PAGE_EXECUTE_READWRITE) + *(byte *)ADDR_CHECK_ARTILLERY_IN_CITY = cfg->use_offensive_artillery_ai ? 0xEB : 0x74; + + // Remove unit limit + if (! at_program_start) { + // Replace 0x7C (= jump if less than [unit limit]) with 0xEB (= uncond. jump) in Leader::spawn_unit + WITH_MEM_PROTECTION (ADDR_UNIT_COUNT_CHECK, 1, PAGE_EXECUTE_READWRITE) + *(byte *)ADDR_UNIT_COUNT_CHECK = cfg->remove_unit_limit ? 0xEB : 0x7C; - // If District will be replaced by another District - struct district_instance * inst = get_district_instance (tile); - if (inst != NULL && district_is_complete(tile, inst->district_type)) { - int district_id = inst->district_type; - int inst_x, inst_y; - if (! district_instance_get_coords (inst, tile, &inst_x, &inst_y)) - return; + // Increase max ID to search for tradable units by 10x if limit removed + WITH_MEM_PROTECTION (ADDR_MAX_TRADABLE_UNIT_ID, 4, PAGE_EXECUTE_READWRITE) + int_to_bytes (ADDR_MAX_TRADABLE_UNIT_ID, cfg->remove_unit_limit ? 81920 : 8192); - int civ_id = unit->Body.CivID; - bool redundant_district = district_instance_is_redundant (inst, tile); - bool would_lose_buildings = any_nearby_city_would_lose_district_benefits (district_id, civ_id, inst_x, inst_y); - if (redundant_district) - would_lose_buildings = false; + // Reallocate diplo_form->tradable_units array so it's 10x long if limit removed + civ_prog_free (p_diplo_form->tradable_units); + int tradable_units_len = cfg->remove_unit_limit ? 81920 : 8192, + tradable_units_size = tradable_units_len * sizeof (TradableItem); + p_diplo_form->tradable_units = new (tradable_units_size); + for (int n = 0; n < tradable_units_len; n++) + p_diplo_form->tradable_units[n] = (TradableItem) {.label = (char *)-1, 0}; // clear label to -1 and other fields to 0 - bool remove_existing = false; - - PopupForm * popup = get_popup_form (); - set_popup_str_param (0, (char*)is->district_configs[district_id].name, -1, -1); - set_popup_str_param (1, (char*)is->district_configs[district_id].name, -1, -1); - popup->vtable->set_text_key_and_flags ( - popup, __, is->mod_script_path, - would_lose_buildings - ? "C3X_CONFIRM_REPLACE_DISTRICT_WITH_DIFFERENT_DISTRICT" - : "C3X_CONFIRM_REPLACE_DISTRICT_WITH_DIFFERENT_DISTRICT_SAFE", - -1, 0, 0, 0 - ); + // Patch the size limit on some code that clears the tradable units array when starting a new game + WITH_MEM_PROTECTION (ADDR_TRADABLE_UNITS_SIZE_TO_CLEAR, 4, PAGE_EXECUTE_READWRITE) + int_to_bytes (ADDR_TRADABLE_UNITS_SIZE_TO_CLEAR, tradable_units_size); + } - int sel = patch_show_popup (popup, __, 0, 0); - if (sel == 0) - remove_existing = true; - else - return; + // Remove era limit + // replacing 0x74 (= jump if zero [after cmp'ing era count with 4]) with 0xEB + WITH_MEM_PROTECTION (ADDR_ERA_COUNT_CHECK, 1, PAGE_EXECUTE_READWRITE) + *(byte *)ADDR_ERA_COUNT_CHECK = cfg->remove_era_limit ? 0xEB : 0x74; - if (remove_existing) { - remove_district_instance (tile); - tile->vtable->m62_Set_Tile_BuildingID (tile, __, -1); - tile->vtable->m51_Unset_Tile_Flags (tile, __, 0, TILE_FLAG_MINE, inst_x, inst_y); - handle_district_removed (tile, district_id, inst_x, inst_y, false); - } - } + // Fix science age bug + // Similar in nature to the sub bug, the function that measures a city's research output accepts a flag that determines whether or not it + // takes science ages into account. It's mistakenly not set by the code that gathers all research points to increment tech progress (but it + // is set elsewhere in code for the interface). The patch simply sets this flag. + WITH_MEM_PROTECTION (ADDR_SCIENCE_AGE_BUG_PATCH, 1, PAGE_EXECUTE_READWRITE) + *(byte *)ADDR_SCIENCE_AGE_BUG_PATCH = cfg->patch_science_age_bug ? 1 : 0; - // If District will replace an improvement - if (itable_look_up (&is->command_id_to_district_id, command, &district_id)) { - unsigned int overlay_flags = tile->vtable->m42_Get_Overlays (tile, __, 0); - unsigned int removable_flags = overlay_flags & 0xfc; + // Pedia pink line bug fix + // The size of the pedia background texture is hard-coded into the EXE and in the base game it's one pixel too small. This shows up in game as + // a one pixel wide pink line along the right edge of the civilopedia. This patch simply increases the texture width by one. + WITH_MEM_PROTECTION (ADDR_PEDIA_TEXTURE_BUG_PATCH, 1, PAGE_EXECUTE_READWRITE) + *(byte *)ADDR_PEDIA_TEXTURE_BUG_PATCH = cfg->patch_pedia_texture_bug ? 0xA6 : 0xA5; - if (removable_flags != 0) { - PopupForm * popup = get_popup_form (); - set_popup_str_param (0, (char*)is->district_configs[district_id].name, -1, -1); - popup->vtable->set_text_key_and_flags (popup, __, is->mod_script_path, "C3X_CONFIRM_BUILD_DISTRICT_OVER_IMPROVEMENT", -1, 0, 0, 0); - int sel = patch_show_popup (popup, __, 0, 0); - if (sel != 0) - return; - } + // Fix for houseboat bug + // See my posts on CFC for an explanation of the bug and its fix: + // https://forums.civfanatics.com/threads/sub-bug-fix-and-other-adventures-in-exe-modding.666881/page-10#post-16084386 + // https://forums.civfanatics.com/threads/sub-bug-fix-and-other-adventures-in-exe-modding.666881/page-10#post-16085242 + WITH_MEM_PROTECTION (ADDR_HOUSEBOAT_BUG_PATCH, ADDR_HOUSEBOAT_BUG_PATCH_END - ADDR_HOUSEBOAT_BUG_PATCH, PAGE_EXECUTE_READWRITE) { + if (cfg->patch_houseboat_bug) { + save_code_area (ADDR_HOUSEBOAT_BUG_PATCH, ADDR_HOUSEBOAT_BUG_PATCH_END - ADDR_HOUSEBOAT_BUG_PATCH, true); + byte * cursor = ADDR_HOUSEBOAT_BUG_PATCH; + *cursor++ = 0x50; // push eax + int call_offset = (int)&tile_at_city_or_null - ((int)cursor + 5); + *cursor++ = 0xE8; // call + cursor = int_to_bytes (cursor, call_offset); + } else + restore_code_area (ADDR_HOUSEBOAT_BUG_PATCH); + } - if (removable_flags != 0) - tile->vtable->m51_Unset_Tile_Flags (tile, __, 0, removable_flags, tile_x, tile_y); - tile->vtable->m51_Unset_Tile_Flags (tile, __, 0, TILE_FLAG_MINE, tile_x, tile_y); - } - - inst = ensure_district_instance (tile, district_id, tile_x, tile_y); - if (inst != NULL) - inst->state = DS_UNDER_CONSTRUCTION; - - Unit_set_state(unit, __, UnitState_Build_Mines); - unit->Body.Job_ID = WJ_Build_Mines; + // NoRaze + WITH_MEM_PROTECTION (ADDR_AUTORAZE_BYPASS, 2, PAGE_EXECUTE_READWRITE) { + byte normal[2] = {0x0F, 0x85}; // jnz + byte bypass[2] = {0x90, 0xE9}; // nop, jmp + for (int n = 0; n < 2; n++) + ((byte *)ADDR_AUTORAZE_BYPASS)[n] = cfg->prevent_autorazing ? bypass[n] : normal[n]; } -} - -void -issue_stack_unit_mgmt_command (Unit * unit, int command) -{ - Tile * tile = tile_at (unit->Body.X, unit->Body.Y); - int unit_type_id = unit->Body.UnitTypeID; - int unit_id = unit->Body.ID; - - PopupForm * popup = get_popup_form (); - clear_memo (); + // Overwrite the instruction(s) where the AI's production choosing code compares the value of what it's currently considering to the best + // option so far. This is done twice since improvements and units are handled in separate loops. The instr(s) are overwritten with a jump to + // an "airlock", which is a bit of code that wraps the call to intercept_consideration. The contents of the airlocks are prepared by the + // patcher in init_consideration_airlocks. + // TODO: This instruction replacement could be done in the patcher too and that might be a better place for it. Think about this. + for (int n = 0; n < 2; n++) { + void * addr_intercept = (n == 0) ? ADDR_INTERCEPT_AI_IMPROV_VALUE : ADDR_INTERCEPT_AI_UNIT_VALUE; + void * addr_airlock = (n == 0) ? ADDR_IMPROV_CONSIDERATION_AIRLOCK : ADDR_UNIT_CONSIDERATION_AIRLOCK; - if (command == UCV_Fortify) { - // This probably won't work for online games since "fortify all" does additional work in that case. See Main_Screen_Form::fortify_all. - // I don't like how this method doesn't place units in the fortified pose. One workaround is so use - // Main_Screen_Form::issue_fortify_command, but that plays the entire fortify animation for each unit which is a major annoyance for - // large stacks. The base game's "fortify all" function also doesn't set the pose so I don't see any easy way to fix this. - FOR_UNITS_ON (uti, tile) - if ((uti.unit->Body.UnitTypeID == unit_type_id) && - (uti.unit->Body.Container_Unit < 0) && - (uti.unit->Body.UnitState == 0) && - (uti.unit->Body.CivID == unit->Body.CivID) && - (uti.unit->Body.Moves < patch_Unit_get_max_move_points (uti.unit))) - Unit_set_state (uti.unit, __, UnitState_Fortifying); + WITH_MEM_PROTECTION (addr_intercept, AI_CONSIDERATION_INTERCEPT_LEN, PAGE_EXECUTE_READWRITE) { + byte * cursor = addr_intercept; - } else if (command == UCV_Upgrade_Unit) { - int our_treasury = leaders[unit->Body.CivID].Gold_Encoded + leaders[unit->Body.CivID].Gold_Decrement; + // write jump to airlock + *cursor++ = 0xE9; + int offset = (int)addr_airlock - ((int)addr_intercept + 5); + cursor = int_to_bytes (cursor, offset); - // If the unit type we're upgrading to is limited, find out how many we can add. Keep that in "available". If the type is not limited, - // leave available set to INT_MAX. - int available = INT_MAX; { - City * city; - int upgrade_id; - if ((is->current_config.unit_limits.len > 0) && - patch_Unit_can_perform_command (unit, __, UCV_Upgrade_Unit) && - (NULL != (city = city_at (unit->Body.X, unit->Body.Y))) && - (0 < (upgrade_id = City_get_upgraded_type_id (city, __, unit_type_id)))) - get_available_unit_count (&leaders[unit->Body.CivID], upgrade_id, &available); + // fill the rest of the space with NOPs + while (cursor < (byte *)addr_intercept + AI_CONSIDERATION_INTERCEPT_LEN) + *cursor++ = 0x90; // nop } + } - int cost = 0; - FOR_UNITS_ON (uti, tile) - if ((available > 0) && - (uti.unit->Body.UnitTypeID == unit_type_id) && - (uti.unit->Body.Container_Unit < 0) && - (uti.unit->Body.UnitState == 0) && - patch_Unit_can_perform_command (uti.unit, __, UCV_Upgrade_Unit)) { - cost += Unit_get_upgrade_cost (uti.unit); - available--; - memoize (uti.id); - } - - if (cost <= our_treasury) { - set_popup_str_param (0, p_bic_data->UnitTypes[unit_type_id].Name, -1, -1); - set_popup_int_param (0, is->memo_len); - set_popup_int_param (1, cost); - popup->vtable->set_text_key_and_flags (popup, __, script_dot_txt_file_path, "UPGRADE_ALL", -1, 0, 0, 0); - if (patch_show_popup (popup, __, 0, 0) == 0) - for (int n = 0; n < is->memo_len; n++) { - Unit * to_upgrade = get_unit_ptr (is->memo[n]); - if (to_upgrade != NULL) - Unit_upgrade (to_upgrade, __, false); - } - + // Overwrite instruction that sets bits in City.Body.Available_Resources with a jump to the airlock + WITH_MEM_PROTECTION (ADDR_INTERCEPT_SET_RESOURCE_BIT, 6, PAGE_EXECUTE_READWRITE) { + byte * cursor = ADDR_INTERCEPT_SET_RESOURCE_BIT; + if (cfg->patch_phantom_resource_bug) { + *cursor++ = 0xE9; + int offset = (int)ADDR_SET_RESOURCE_BIT_AIRLOCK - ((int)ADDR_INTERCEPT_SET_RESOURCE_BIT + 5); + cursor = int_to_bytes (cursor, offset); + *cursor++ = 0x90; // nop } else { - set_popup_int_param (0, cost); - int param_5 = is_online_game () ? 0x4000 : 0; // As in base code - popup->vtable->set_text_key_and_flags (popup, __, script_dot_txt_file_path, "NO_GOLD_TO_UPGRADE_ALL", -1, 0, param_5, 0); - patch_show_popup (popup, __, 0, 0); + byte original[6] = {0x09, 0xB0, 0x9C, 0x00, 0x00, 0x00}; // or dword ptr [eax+0x9C], esi + for (int n = 0; n < 6; n++) + cursor[n] = original[n]; } + } - } else if (command == UCV_Disband) { - FOR_UNITS_ON (uti, tile) - if ((uti.unit->Body.UnitTypeID == unit_type_id) && - (uti.unit->Body.Container_Unit < 0) && - (uti.unit->Body.UnitState == 0) && - (uti.unit->Body.Moves < patch_Unit_get_max_move_points (uti.unit))) - memoize (uti.id); + // Enlarge the mask that's applied to p_bic_data->Map.TileCount in the loop over lines in Trade_Net::recompute_resources. This lets us loop + // over more than 0xFFFF tiles. + WITH_MEM_PROTECTION (ADDR_RESOURCE_TILE_COUNT_MASK, 1, PAGE_EXECUTE_READWRITE) { + *(byte *)ADDR_RESOURCE_TILE_COUNT_MASK = (cfg->count_mills > 0) ? 0xFF : 0x00; + } + // Similarly, enlarge the cmp instruction that jumps over the entire loop when the tile count is zero. Do this by simply removing the operand + // override prefix byte (0x66), overwriting it with a nop (0x90). This converts the instruction "cmp word ptr [bic_data.Map.TileCount], di" + // into "nop; cmp dword ptr [bic_data.Map.TileCount], edi" + WITH_MEM_PROTECTION (ADDR_RESOURCE_TILE_COUNT_ZERO_COMPARE, 1, PAGE_EXECUTE_READWRITE) { + *(byte *)ADDR_RESOURCE_TILE_COUNT_ZERO_COMPARE = 0x90; + } - if (is->memo_len > 0) { - set_popup_int_param (0, is->memo_len); - popup->vtable->set_text_key_and_flags (popup, __, is->mod_script_path, "C3X_CONFIRM_STACK_DISBAND", -1, 0, 0, 0); - if (patch_show_popup (popup, __, 0, 0) == 0) { - for (int n = 0; n < is->memo_len; n++) { - Unit * to_disband = get_unit_ptr (is->memo[n]); - if (to_disband) - Unit_disband (to_disband); - } - } + byte * addr_turn_metalimits[] = {ADDR_TURN_METALIMIT_1, ADDR_TURN_METALIMIT_2, ADDR_TURN_METALIMIT_3, ADDR_TURN_METALIMIT_4, + ADDR_TURN_METALIMIT_5, ADDR_TURN_METALIMIT_6, ADDR_TURN_METALIMIT_7}; + for (int n = 0; n < ARRAY_LEN (addr_turn_metalimits); n++) { + byte * addr = addr_turn_metalimits[n]; + WITH_MEM_PROTECTION (addr, 4, PAGE_EXECUTE_READWRITE) { + int_to_bytes (addr, cfg->remove_cap_on_turn_limit ? 1000000 : 1000); } } -} -void __fastcall -patch_Main_GUI_handle_button_press (Main_GUI * this, int edx, int button_id) -{ - // Set SB flag according to case (2) - if (button_id < 42) { - if ((is->sc_img_state == IS_OK) && - ((this->Unit_Command_Buttons[button_id].Button.Images[0] == &is->sc_button_image_sets[SC_BOMBARD].imgs[0]) || - (this->Unit_Command_Buttons[button_id].Button.Images[0] == &is->sc_button_image_sets[SC_BOMB ].imgs[0]))) - is->sb_activated_by_button = 1; - else - is->sb_activated_by_button = 0; + // Overwrite the human-ness test and call to Leader::ai_adjust_sliders that happens during the preproduction player update method. The new + // code calls adjust_sliders_preproduction for all players. + WITH_MEM_PROTECTION (ADDR_AI_PREPRODUCTION_SLIDER_ADJUSTMENT, 9, PAGE_EXECUTE_READWRITE) { + byte * cursor = ADDR_AI_PREPRODUCTION_SLIDER_ADJUSTMENT; + *cursor++ = 0x8B; *cursor++ = 0xCE; // mov ecx, esi + cursor = emit_branch (BK_CALL, cursor, adjust_sliders_preproduction); + for (; cursor < ADDR_AI_PREPRODUCTION_SLIDER_ADJUSTMENT + 9; cursor++) + *cursor = 0x90; // nop } - int command = this->Unit_Command_Buttons[button_id].Command; - - // Clear any highlighted tiles - if (is->current_config.enable_city_work_radii_highlights && is->highlight_city_radii) { - is->highlight_city_radii = false; - clear_highlighted_worker_tiles_and_redraw (); + // Set up a special intercept of the base game's calls to print_int in order to grab a pointer to a gold TradeOffer object being modified. The + // calls to print_int happen in the context of creating the default text to be placed in the set-gold-amount popup. Conveniently, a pointer to + // the TradeOffer object is always stored in register ecx when this call happens. It's not easily accessible since print_int uses the cdecl + // convention so we must use an airlock-like thing to effectively convert the calling convention to fastcall. The first part of this code + // simply replaces the call to print_int with a call to the airlock-like thing, and the second part initializes its contents. + byte * addr_print_gold_amounts[] = {ADDR_PRINT_GOLD_AMOUNT_1, ADDR_PRINT_GOLD_AMOUNT_2}; + for (int n = 0; n < ARRAY_LEN (addr_print_gold_amounts); n++) { + byte * addr = addr_print_gold_amounts[n]; + WITH_MEM_PROTECTION (addr, 5, PAGE_EXECUTE_READWRITE) + emit_branch (BK_CALL, addr, ADDR_CAPTURE_MODIFIED_GOLD_TRADE); } + WITH_MEM_PROTECTION (ADDR_CAPTURE_MODIFIED_GOLD_TRADE, 32, PAGE_EXECUTE_READWRITE) { + byte * cursor = ADDR_CAPTURE_MODIFIED_GOLD_TRADE; - // If a district, run district build logic - if (is->current_config.enable_districts && is_district_command (command)) { - clear_something_1 (); - Timer_clear (&this->timer_1); - issue_district_worker_command (p_main_screen_form->Current_Unit, command); - return; - } + // Repush all of the arguments to print_int onto the stack, they will be consumed by do_capture_modified_gold_trade since it's + // fastcall. The original args don't need to be removed b/c we're replacing a cdecl function so that's the caller's + // responsibility. The TradeOffer pointer is already in ECX, so that's fine as long as we don't touch that register. + byte repush[] = {0xFF, 0x74, 0x24, 0x0C}; // push [esp+0xC] + for (int n = 0; n < 3; n++) + for (int k = 0; k < ARRAY_LEN (repush); k++) + *cursor++ = repush[k]; - // Check if command is a worker build command (not a district) and a district exists on the tile - if (is->current_config.enable_districts && p_main_screen_form->Current_Unit != NULL) { - bool removed_existing = false; - if (! handle_worker_command_that_may_replace_district (p_main_screen_form->Current_Unit, command, &removed_existing)) - return; - if (removed_existing) { - clear_something_1 (); - Timer_clear (&this->timer_1); - Main_GUI_handle_button_press (this, __, button_id); - return; - } + cursor = emit_branch (BK_CALL, cursor, do_capture_modified_gold_trade); // call do_capture_modified_gold_trade + *cursor++ = 0xC3; // ret } - struct sc_button_info const * stack_button_info; { - stack_button_info = NULL; - if (button_id < 42) // If button pressed was a unit command button - for (int n = 0; n < COUNT_STACKABLE_COMMANDS; n++) - if (command == sc_button_infos[n].command) { - stack_button_info = &sc_button_infos[n]; - break; - } + // Edit branch in capture_city to never run code for barbs, this allows barbs to capture cities + WITH_MEM_PROTECTION (ADDR_CAPTURE_CITY_BARB_BRANCH, 2, PAGE_EXECUTE_READWRITE) { + byte normal[2] = {0x0F, 0x85}; // jnz + byte bypass[2] = {0x90, 0xE9}; // nop, jmp + for (int n = 0; n < 2; n++) + ((byte *)ADDR_CAPTURE_CITY_BARB_BRANCH)[n] = cfg->enable_city_capture_by_barbarians ? bypass[n] : normal[n]; } - if ((stack_button_info == NULL) || // If there's no stack command for the pressed button OR - (! is->current_config.enable_stack_unit_commands) || // stack unit commands are not enabled OR - (((*p_GetAsyncKeyState) (VK_CONTROL)) >> 8 == 0) || // CTRL key is not down OR - (p_main_screen_form->Current_Unit == NULL) || // no unit is selected OR - is_online_game ()) { // is online game - Main_GUI_handle_button_press (this, __, button_id); - return; + // After the production phase is done for the barb player, there are two jump instructions skipping the production code for civs. Replacing + // those jumps lets us run the civ production code for the barbs as well. + WITH_MEM_PROTECTION (ADDR_PROD_PHASE_BARB_DONE_NO_SPAWN_JUMP, 6, PAGE_EXECUTE_READWRITE) { + if (cfg->enable_city_capture_by_barbarians) { + save_code_area (ADDR_PROD_PHASE_BARB_DONE_NO_SPAWN_JUMP, 6, true); + byte jump_to_civ[6] = {0x0F, 0x8D, 0x0C, 0x00, 0x00, 0x00}; // jge +0x12 + for (int n = 0; n < 6; n++) + ADDR_PROD_PHASE_BARB_DONE_NO_SPAWN_JUMP[n] = jump_to_civ[n]; + } else + restore_code_area (ADDR_PROD_PHASE_BARB_DONE_NO_SPAWN_JUMP); } + set_nopification (cfg->enable_city_capture_by_barbarians, ADDR_PROD_PHASE_BARB_DONE_JUMP, 5); - enum stackable_command_kind kind = stack_button_info->kind; - if ((kind == SCK_TERRAFORM) || (kind == SCK_UNIT_MGMT)) { - // Replicate behavior of function we're replacing - clear_something_1 (); - Timer_clear (&this->timer_1); + for (int domain = 0; domain < 2; domain++) { + byte * addr_skip = (domain == 0) ? ADDR_SKIP_LAND_UNITS_FOR_SEA_ZOC : ADDR_SKIP_SEA_UNITS_FOR_LAND_ZOC, + * addr_airlock = (domain == 0) ? ADDR_SEA_ZOC_FILTER_AIRLOCK : ADDR_LAND_ZOC_FILTER_AIRLOCK; - if (kind == SCK_TERRAFORM) - issue_stack_worker_command (p_main_screen_form->Current_Unit, stack_button_info->command); - else if (kind == SCK_UNIT_MGMT) - issue_stack_unit_mgmt_command (p_main_screen_form->Current_Unit, stack_button_info->command); - } else - Main_GUI_handle_button_press (this, __, button_id); -} + WITH_MEM_PROTECTION (addr_skip, 6, PAGE_EXECUTE_READWRITE) { + if ((cfg->special_zone_of_control_rules != 0) && ! is_code_area_saved (addr_skip)) { + byte * original_target = addr_skip + 6 + int_from_bytes (addr_skip + 2); // target addr of jump instr we're replacing + save_code_area (addr_skip, 6, true); -bool __fastcall -patch_Main_Screen_Form_issue_command (Main_Screen_Form * this, int edx, int command, Unit * unit) -{ - Unit * target_unit = unit; - if (target_unit == NULL) - target_unit = this->Current_Unit; + // Initialize airlock. The airlock preserves all registers and calls filter_zoc_candidate then either follows or skips the + // original jump depending on what it returns. If zero is returned, follows the jump, skipping a bunch of code and filtering + // out the unit as a candidate for ZoC. + WITH_MEM_PROTECTION (addr_airlock, INLEAD_SIZE, PAGE_READWRITE) { + byte * cursor = addr_airlock; + *cursor++ = 0x60; // pusha + *cursor++ = 0x54; // push esp + cursor = emit_branch (BK_CALL, cursor, filter_zoc_candidate); + *cursor++ = 0x83; *cursor++ = 0xF8; *cursor++ = 0x01; // cmp eax, 1 + *cursor++ = 0x75; *cursor++ = 0x06; // jne 6 + *cursor++ = 0x61; // popa + cursor = emit_branch (BK_JUMP, cursor, addr_skip + 6); + *cursor++ = 0x61; // popa + cursor = emit_branch (BK_JUMP, cursor, original_target); + } - if (is->current_config.enable_districts) { - bool removed_existing = false; - if (! handle_worker_command_that_may_replace_district (target_unit, command, &removed_existing)) - return false; + // Write jump to airlock + emit_branch (BK_JUMP, addr_skip, addr_airlock); + } else if (cfg->special_zone_of_control_rules == 0) + restore_code_area (addr_skip); + } } - return Main_Screen_Form_issue_command (this, __, command, unit); -} - -bool -is_command_button_active (Main_GUI * main_gui, enum Unit_Command_Values command) -{ - Command_Button * buttons = main_gui->Unit_Command_Buttons; - for (int n = 0; n < 42; n++) - if (((buttons[n].Button.Base_Data.Status2 & 1) != 0) && (buttons[n].Command == command)) - return true; - return false; -} - -int __fastcall -patch_Main_Screen_Form_handle_key_down (Main_Screen_Form * this, int edx, int char_code, int virtual_key_code) -{ - // Set SB flag according to case (4) - int precision_strike_is_available = is_command_button_active (&this->GUI, UCV_Precision_Bombing); - if ((virtual_key_code == VK_B) || (precision_strike_is_available && (virtual_key_code == VK_P))) - is->sb_activated_by_button = 0; + set_nopification ( cfg->special_zone_of_control_rules != 0, ADDR_ZOC_CHECK_ATTACKER_ANIM_FIELD_111, 6); + set_nopification ((cfg->special_zone_of_control_rules & SZOCR_LETHAL) != 0, ADDR_SKIP_ZOC_FOR_ONE_HP_LAND_UNIT , 6); + set_nopification ((cfg->special_zone_of_control_rules & SZOCR_LETHAL) != 0, ADDR_SKIP_ZOC_FOR_ONE_HP_SEA_UNIT , 6); - if ((virtual_key_code & 0xFF) == VK_CONTROL) { - set_up_stack_worker_buttons (&this->GUI); + WITH_MEM_PROTECTION (ADDR_LUXURY_BOX_ROW_HEIGHT, 4, PAGE_EXECUTE_READWRITE) { + byte normal [4] = {0x8B, 0x44, 0x24, LUXURY_BOX_ROW_HEIGHT_STACK_OFFSET}; // mov eax, dword ptr [esp + LUXURY_BOX_ROW_HEIGHT_STACK_OFFSET] + byte compact[4] = {0x31, 0xC0, 0xB0, 0x10}; // xor eax, eax; mov al, 0x10 + for (int n = 0; n < 4; n++) + ADDR_LUXURY_BOX_ROW_HEIGHT[n] = cfg->compact_luxury_display_on_city_screen ? compact[n] : normal[n]; + } - if (is->current_config.enable_city_work_radii_highlights && - ! is->highlight_city_radii) { - Unit * unit = p_main_screen_form->Current_Unit; - if (unit != NULL) { - is->highlight_city_radii = true; - compute_highlighted_worker_tiles_for_districts (); - this->vtable->m73_call_m22_Draw ((Base_Form *)this); - } - } - } else { - if (is->highlight_city_radii) { - is->highlight_city_radii = false; - clear_highlighted_worker_tiles_and_redraw (); - } + WITH_MEM_PROTECTION (ADDR_MOST_STRAT_RES_ON_CITY_SCREEN, 1, PAGE_EXECUTE_READWRITE) { + *(byte *)ADDR_MOST_STRAT_RES_ON_CITY_SCREEN = cfg->compact_strategic_resource_display_on_city_screen ? 13 : 8; } - char original_turn_end_flag = this->turn_end_flag; - int tr = Main_Screen_Form_handle_key_down (this, __, char_code, virtual_key_code); - if ((original_turn_end_flag == 1) && (this->turn_end_flag == 0)) - intercept_end_of_turn (); + // Remove a check that a returned neighbor index is within the 21 tile work area in code related to hoving the mouse over the work area on the + // city screen. This check is redundant and could be removed always, but only do so if work area is expanded. + set_nopification (cfg->city_work_radius > 2, ADDR_REDUNDANT_CHECK_ON_WORK_AREA_HOVER, 6); - return tr; -} + // Skip redundant check of clicked tile's neighbor index in City_Form::handle_left_click. + WITH_MEM_PROTECTION (ADDR_CITY_FORM_LEFT_CLICK_JUMP, 1, PAGE_EXECUTE_READWRITE) { + *ADDR_CITY_FORM_LEFT_CLICK_JUMP = 0xEB; // 0x7C (jl) -> 0xEB (jmp) + } -int -patch_handle_cursor_change_in_jgl () -{ - // Set SB flag according to case (3) and the annoying state - if ((is->sb_activated_by_button != 2) && - (p_main_screen_form->Mode_Action != UMA_Bombard) && - (p_main_screen_form->Mode_Action != UMA_Air_Bombard)) - is->sb_activated_by_button = 0; + // Skip check that neighbor index passed to City::controls_tile is within work radius. This check is now implemented in the patch func. + WITH_MEM_PROTECTION (ADDR_CONTROLS_TILE_JUMP, 1, PAGE_EXECUTE_READWRITE) { + *ADDR_CONTROLS_TILE_JUMP = 0xEB; // 0x7C (jl) -> 0xEB (jmp) + } - return handle_cursor_change_in_jgl (); -} + // When searching for a tile on which to spawn pollution, the game wraps its search around by using remainder after division. Here, we replace + // the dividend to match a potentially expanded city work area. + WITH_MEM_PROTECTION (ADDR_SPAWN_POLLUTION_MOD, 4, PAGE_EXECUTE_READWRITE) { + int_to_bytes (ADDR_SPAWN_POLLUTION_MOD, is->workable_tile_count - 1); + } -void __fastcall -patch_Main_Screen_Form_handle_left_click_on_map_1 (Main_Screen_Form * this, int edx, int param_1, int param_2) -{ - if (is->sb_activated_by_button == 1) - is->sb_activated_by_button = 2; - Main_Screen_Form_handle_left_click_on_map_1 (this, __, param_1, param_2); - is->sb_activated_by_button = 0; -} + WITH_MEM_PROTECTION (ADDR_PATHFINDER_RECONSTRUCTION_MAX_LEN, 4, PAGE_EXECUTE_READWRITE) { + int_to_bytes (ADDR_PATHFINDER_RECONSTRUCTION_MAX_LEN, cfg->patch_premature_truncation_of_found_paths ? 2560 : 256); + } + int * trade_net_addrs; + bool already_moved_trade_net = is->trade_net != p_original_trade_net, + want_moved_trade_net = cfg->city_limit > 512; + int lifted_city_limit_exp = 11; + int lifted_city_limit = 1 << lifted_city_limit_exp; + if ((! at_program_start) && + ((trade_net_addrs = load_trade_net_addrs ()) != NULL) && + ((already_moved_trade_net && ! want_moved_trade_net) || (want_moved_trade_net && ! already_moved_trade_net))) { + // Allocate a new trade net object if necessary. To construct it, all we have to do is zero a few fields and set the vptr. Otherwise, + // set the allocated object aside for deletion later. Also set new & old addresses to the locations we're moving to & from. + Trade_Net * to_free = NULL; + int p_old, p_new; + if (want_moved_trade_net) { + is->trade_net = calloc (1, (sizeof (Trade_Net)) - (4 * 512 * 512) + (4 * lifted_city_limit * lifted_city_limit)); + is->city_limit = lifted_city_limit; + is->trade_net->vtable = p_original_trade_net->vtable; + p_old = (int)p_original_trade_net; + p_new = (int)is->trade_net; + } else { + to_free = is->trade_net; + is->city_limit = 512; + p_old = (int)is->trade_net; + p_new = (int)p_original_trade_net; + is->trade_net = p_original_trade_net; + } + already_moved_trade_net = is->trade_net != p_original_trade_net; // Keep this variable up to date -void __fastcall -patch_Main_GUI_handle_click_in_status_panel (Main_GUI * this, int edx, int mouse_x, int mouse_y) -{ - char original_turn_end_flag = p_main_screen_form->turn_end_flag; - Main_GUI_handle_click_in_status_panel (this, __, mouse_x, mouse_y); - if ((original_turn_end_flag == 1) && (p_main_screen_form->turn_end_flag == 0)) - intercept_end_of_turn (); -} + // Patch all references from the "old" object to the "new" one + int offset; + bool popped_up_error = false; + char err_msg[200] = {0}; + int * refs; { + if (exe_version_index == 0) + refs = trade_net_addrs; + else if (exe_version_index == 1) // Steam version, skip refs and instructions for GOG + refs = &trade_net_addrs[TRADE_NET_REF_COUNT + TRADE_NET_INSTR_COUNT_GOG]; + else // PCGames.de version, skip two sets of refs and instrs for GOG & Steam + refs = &trade_net_addrs[2 * TRADE_NET_REF_COUNT + TRADE_NET_INSTR_COUNT_GOG + TRADE_NET_INSTR_COUNT_STEAM]; + } + for (int n_ref = 0; n_ref < TRADE_NET_REF_COUNT; n_ref++) { + int addr = refs[n_ref]; + WITH_MEM_PROTECTION ((void *)(addr - 10), 20, PAGE_EXECUTE_READWRITE) { + byte * instr = (byte *)addr; + if ((instr[0] == 0xB9) && (int_from_bytes (&instr[1]) == p_old)) // move trade net ptr to ecx + int_to_bytes (&instr[1], p_new); + else if ((instr[0] == 0xC7) && (instr[1] == 0x05) && (int_from_bytes (&instr[2]) == p_old)) // write trade net vtable ptr + int_to_bytes (&instr[2], p_new); + else if ((instr[0] == 0x81) && (instr[1] == 0xFE) && (int_from_bytes (&instr[2]) == (int)p_original_trade_net)) // cmp esi, trade net location + ; // Do not patch this location because it's the upper limit for a memcpy + else if ((instr[0] == 0x81) && (instr[1] == 0xFF) && (int_from_bytes (&instr[2]) == (int)p_original_trade_net)) // cmp edi, trade net location + ; // Same + else if ((instr[0] == 0x81) && (instr[1] == 0xFA) && (int_from_bytes (&instr[2]) == (int)&p_original_trade_net->Data2)) // cmp edx, trade net data2 location + ; // Same + else if (((instr[0] == 0xA3) || (instr[0] == 0xA1)) && // move eax to field or vice-versa + (offset = int_from_bytes (&instr[1]) - p_old, (offset >= 0) && (offset < 100))) + int_to_bytes (&instr[1], p_new + offset); + else if ((instr[0] == 0x89) && ((instr[1] >= 0x0D) && (instr[1] <= 0x3D)) && // move other regs to field + (offset = int_from_bytes (&instr[2]) - p_old, (offset >= 0) && (offset < 100))) + int_to_bytes (&instr[2], p_new + offset); + else if ((instr[0] == 0x8B) && ((instr[1] == 0x35) || (instr[1] == 0x3D) || (instr[1] == 0x0D)) && // mov field to esi, edi or ecx + (offset = int_from_bytes (&instr[2]) - p_old, (offset >= 0) && (offset < 100))) + int_to_bytes (&instr[2], p_new + offset); + else if (! popped_up_error) { + snprintf (err_msg, (sizeof err_msg) - 1, "Can't move trade net object from address 0x%x. Pattern doesn't match.", addr); + MessageBox (NULL, err_msg, NULL, MB_ICONERROR); + popped_up_error = true; + } + } + } -// Gets effective shields generated per turn, including civil engineers, disorder, and anarchy. -int -get_city_production_rate (City * city, enum City_Order_Types order_type, int order_id) -{ - int in_disorder = city->Body.Status & CSF_Civil_Disorder, - in_anarchy = p_bic_data->Governments[leaders[city->Body.CivID].GovernmentType].b_Transition_Type, - getting_tile_shields = (! in_disorder) && (! in_anarchy); + // Patch all instructions that involve the stride of Trade_Net.Matrix + int * addrs, addr_count; { + if (exe_version_index == 0) { + addrs = &trade_net_addrs[TRADE_NET_REF_COUNT]; + addr_count = TRADE_NET_INSTR_COUNT_GOG; + } else if (exe_version_index == 1) { + addrs = &trade_net_addrs[2 * TRADE_NET_REF_COUNT + TRADE_NET_INSTR_COUNT_GOG]; + addr_count = TRADE_NET_INSTR_COUNT_STEAM; + } else { + addrs = &trade_net_addrs[3 * TRADE_NET_REF_COUNT + TRADE_NET_INSTR_COUNT_GOG + TRADE_NET_INSTR_COUNT_STEAM]; + addr_count = TRADE_NET_INSTR_COUNT_GOG; + } + for (int n = 0; n < addr_count; n++) { + byte * instr = (byte *)addrs[n]; + WITH_MEM_PROTECTION (instr, 10, PAGE_EXECUTE_READWRITE) { + if (! want_moved_trade_net) + restore_code_area (instr); - if (order_type == COT_Improvement) { - int building_wealth = (p_bic_data->Improvements[order_id].ImprovementFlags & ITF_Capitalization) != 0; - int specialist_shields = 0; - FOR_CITIZENS_IN (ci, city) - if ((ci.ctzn->Body.field_20[0] & 0xFF) == 0) // I don't know what is check is for but it's done in the base code - specialist_shields += p_bic_data->CitizenTypes[ci.ctzn->Body.WorkerType].Construction; - return (getting_tile_shields ? city->Body.ProductionIncome : 0) + ((! building_wealth) ? specialist_shields : 0); - } else if ((order_type == COT_Unit) && getting_tile_shields) - return city->Body.ProductionIncome; - else - return 0; -} + else { + if ((instr[0] == 0xC1) && (instr[1] >= 0xE0) && (instr[1] <= 0xE7)) { // shl + save_code_area (instr, 3, false); + // shift amount is either 9 (1<<9 == 512) or 11 (1<<11 == 4*512, stride in bytes) + // in the second case, replace with lifted_exp + 2 to convert to bytes + instr[2] = lifted_city_limit_exp + ((instr[2] == 11) ? 2 : 0); -void __fastcall -patch_City_Form_open (City_Form * this, int edx, City * city, int param_2) -{ - recompute_resources_if_necessary (); + } else if ((instr[0] == 0x81) && (instr[1] >= 0xC0) && (instr[1] <= 0xC7)) { // add + save_code_area (instr, 6, false); + int amount = int_from_bytes (&instr[2]); + // amount is either 512 or 4*512, replace with lifted_lim or 4*lifted_lim + int_to_bytes (&instr[2], (amount == 512) ? lifted_city_limit : 4*lifted_city_limit); - WITH_PAUSE_FOR_POPUP { - City_Form_open (this, __, city, param_2); - } -} + } else if ((instr[0] == 0x8D) && (instr[1] == 0x9C) && (instr[2] == 0x90)) { // lea + save_code_area (instr, 7, false); + int offset = 4 * lifted_city_limit + 0x38; // stride in bytes plus 0x38 offset to Matrix in Trade_Net object + int_to_bytes (&instr[3], offset); -void init_district_icons (); + } else if (instr[0] == 0xB9) { // mov + save_code_area (instr, 5, false); + int_to_bytes (&instr[1], lifted_city_limit * lifted_city_limit); -void __fastcall -patch_City_Form_draw (City_Form * this) -{ - // Recompute city form production rect location every time because the config might have changed. Doing it here is also easier than - // patching the constructor. - int form_top = (p_bic_data->ScreenHeight - this->Background_Image.Height) / 2; - this->Production_Storage_Indicator.top = form_top + 621 + (is->current_config.show_detailed_city_production_info ? 34 : 0); - - is->drawn_strat_resource_count = 0; - - // Make sure culture income (including from districts) is up to date before the draw event - if (is->current_config.enable_districts) - patch_City_recompute_culture_income(this->CurrentCity); - - City_Form_draw (this); - - if (is->current_config.show_detailed_city_production_info) { - City * city = this->CurrentCity; - int order_type = city->Body.Order_Type, - order_id = city->Body.Order_ID, - order_progress = City_get_order_progress (city), - order_cost = City_get_order_cost (city), - prod_rate = get_city_production_rate (city, order_type, order_id), - building_wealth = (order_type == COT_Improvement) && ((p_bic_data->Improvements[order_id].ImprovementFlags & ITF_Capitalization) != 0); - - int turns_left, surplus; { - if (prod_rate > 0) { - turns_left = (order_cost - order_progress) / prod_rate; - if ((order_cost - order_progress) % prod_rate != 0) - turns_left++; - if (turns_left < 1) - turns_left = 1; - surplus = (turns_left * prod_rate) - (order_cost - order_progress); - } else { - turns_left = 9999; - surplus = 0; + } else if (! popped_up_error) { + snprintf (err_msg, (sizeof err_msg) - 1, "Can't patch matrix stride at address 0x%x. Pattern doesn't match.", (int)instr); + MessageBox (NULL, err_msg, NULL, MB_ICONERROR); + popped_up_error = true; + } + } + } } } - char line1[100]; { - if (prod_rate > 0) { - if (! building_wealth) - snprintf (line1, sizeof line1, "%s %d %s", this->Labels.To_Build, turns_left, (turns_left == 1) ? this->Labels.Single_Turn : this->Labels.Multiple_Turns); - else - snprintf (line1, sizeof line1, "%s", is->c3x_labels[CL_NEVER_COMPLETES]); - } else - snprintf (line1, sizeof line1, "%s", is->c3x_labels[CL_HALTED]); - line1[(sizeof line1) - 1] = '\0'; - } + // Reallocate diplo_form->tradable_cities array so it matches the city limit + civ_prog_free (p_diplo_form->tradable_cities); + int tradable_cities_len = want_moved_trade_net ? lifted_city_limit : 512, + tradable_cities_size = tradable_cities_len * sizeof (TradableItem); + p_diplo_form->tradable_cities = new (tradable_cities_size); + for (int n = 0; n < tradable_cities_len; n++) + p_diplo_form->tradable_cities[n] = (TradableItem) {.label = (char *)-1, 0}; // clear label to -1 and other fields to 0 - char line2[100]; { - if (! building_wealth) { - int percent_complete = order_cost > 0 ? ((10000 * order_progress) / order_cost + 50) / 100 : 100; - snprintf (line2, sizeof line2, "%d / %d (%d%s)", order_progress, order_cost, percent_complete, this->Labels.Percent); - } else - snprintf (line2, sizeof line2, "---"); - line2[(sizeof line2) - 1] = '\0'; + // Patch the size limit on some code that clears the tradable cities array when starting a new game + WITH_MEM_PROTECTION (ADDR_TRADABLE_CITIES_SIZE_TO_CLEAR, 4, PAGE_EXECUTE_READWRITE) { + int_to_bytes (ADDR_TRADABLE_CITIES_SIZE_TO_CLEAR, tradable_cities_size); } - char line3[100]; { - if ((! building_wealth) && (prod_rate > 0)) { - int s_per, s_rem; { - if (turns_left > 1) { - s_per = surplus / turns_left; - s_rem = surplus % turns_left; - } else { - s_per = surplus; - s_rem = 0; - } - } - char * s_lab = is->c3x_labels[CL_SURPLUS]; - if ((s_per != 0) && (s_rem != 0)) snprintf (line3, sizeof line3, "%s: %d + %d %s", s_lab, s_rem, s_per, this->Labels.Per_Turn); - else if ((s_per == 0) && (s_rem != 0)) snprintf (line3, sizeof line3, "%s: %d", s_lab, s_rem); - else if ((s_per != 0) && (s_rem == 0)) snprintf (line3, sizeof line3, "%s: %d %s", s_lab, s_per, this->Labels.Per_Turn); - else snprintf (line3, sizeof line3, "%s", is->c3x_labels[CL_SURPLUS_NONE]); - } else - snprintf (line3, sizeof line3, "%s", is->c3x_labels[CL_SURPLUS_NA]); - line3[(sizeof line3) - 1] = '\0'; + if (to_free) { + to_free->vtable->destruct (to_free, __, 0); + free (to_free); } + } - Object_66C3FC * font = get_font (10, FSF_NONE); - int left = this->Production_Storage_Indicator.left, - top = this->Production_Storage_Indicator.top, - width = this->Production_Storage_Indicator.right - left; - PCX_Image_draw_centered_text (&this->Base.Data.Canvas, __, font, line1, left, top - 42, width, strlen (line1)); - PCX_Image_draw_centered_text (&this->Base.Data.Canvas, __, font, line2, left, top - 28, width, strlen (line2)); - PCX_Image_draw_centered_text (&this->Base.Data.Canvas, __, font, line3, left, top - 14, width, strlen (line3)); + // Set is->city_limit and patch two instructions that contain the limit + is->city_limit = clamp (0, already_moved_trade_net ? lifted_city_limit : 512, cfg->city_limit); + WITH_MEM_PROTECTION (ADDR_CITY_LIM_CMP_IN_CONT_BEGIN_TURN, 6, PAGE_EXECUTE_READWRITE) { + int_to_bytes (&ADDR_CITY_LIM_CMP_IN_CONT_BEGIN_TURN[2], is->city_limit); + } + WITH_MEM_PROTECTION (ADDR_CITY_LIM_CMP_IN_CREATE_CITY, 5, PAGE_EXECUTE_READWRITE) { + int_to_bytes (&ADDR_CITY_LIM_CMP_IN_CREATE_CITY[1], is->city_limit); + } + WITH_MEM_PROTECTION (ADDR_MAX_TRADABLE_CITY_ID, 4, PAGE_EXECUTE_READWRITE) { + int_to_bytes (ADDR_MAX_TRADABLE_CITY_ID, already_moved_trade_net ? lifted_city_limit : 512); } - // Draw district commerce bonuses (gold and science) - if (! (is->current_config.enable_districts || is->current_config.enable_natural_wonders)) - return; + WITH_MEM_PROTECTION (ADDR_CULTURE_DOUBLING_TIME_CMP_INSTR, 6, PAGE_EXECUTE_READWRITE) { + byte * instr = ADDR_CULTURE_DOUBLING_TIME_CMP_INSTR; + if (cfg->years_to_double_building_culture == 1000) + restore_code_area (instr); + else { + if (instr[0] == 0x3D) { // in GOG and PCG EXEs, instr is cmp eax, 0x3E8 + save_code_area (instr, 5, false); + int_to_bytes (&instr[1], cfg->years_to_double_building_culture); + } else if (instr[0] == 0x3B) { // in Steam EXE, instr is cmp eax, dword ptr 0x688C9C + save_code_area (instr, 6, true); + instr[0] = 0x3D; + int_to_bytes (&instr[1], cfg->years_to_double_building_culture); + } + } + } - // Lazy load district icons - if (is->dc_icons_img_state == IS_UNINITED) - init_district_icons (); - if (is->dc_icons_img_state != IS_OK) - return; + WITH_MEM_PROTECTION (ADDR_GET_PIXEL_FOR_DRAW_CITY_DOT, 5, PAGE_EXECUTE_READWRITE) { + if (cfg->accentuate_cities_on_minimap) { + save_code_area (ADDR_GET_PIXEL_FOR_DRAW_CITY_DOT, 5, false); + emit_branch (BK_JUMP, ADDR_GET_PIXEL_FOR_DRAW_CITY_DOT, ADDR_INLEAD_FOR_CITY_DOT_DRAW_PIXEL_REPL); + } else + restore_code_area (ADDR_GET_PIXEL_FOR_DRAW_CITY_DOT); + } + WITH_MEM_PROTECTION (ADDR_INLEAD_FOR_CITY_DOT_DRAW_PIXEL_REPL, INLEAD_SIZE, PAGE_EXECUTE_READWRITE) { + byte * code = ADDR_INLEAD_FOR_CITY_DOT_DRAW_PIXEL_REPL; + code[0] = 0x55; code[1] = 0x53; // write push ebp and push ebx, two overwritten instrs before the call + emit_branch (BK_CALL, &code[2], &patch_get_pixel_to_draw_city_dot); // call patch func + emit_branch (BK_JUMP, &code[7], ADDR_GET_PIXEL_FOR_DRAW_CITY_DOT + 5); // jump back to original code + } - City * city = this->CurrentCity; - if (city == NULL) - return; + // Bypass adjacent resource of different type check + // replacing 0x7D (= jge) with 0xEB (= uncond. jump) + WITH_MEM_PROTECTION (ADDR_RES_CHECK_JUMP_TILE_INDEX_AT_LEAST_9, 1, PAGE_EXECUTE_READWRITE) + *(byte *)ADDR_RES_CHECK_JUMP_TILE_INDEX_AT_LEAST_9 = cfg->allow_adjacent_resources_of_different_types ? 0xEB : 0x7D; - // Calculate district gold and science bonuses by iterating workable tiles - int district_gold = 0; - int city_x = city->Body.X; - int city_y = city->Body.Y; - int city_civ_id = city->Body.CivID; + WITH_MEM_PROTECTION (ADDR_RESOURCE_GEN_TILE_COUNT_DIV, 7, PAGE_EXECUTE_READWRITE) { + if (cfg->tiles_per_non_luxury_resource == 32) + restore_code_area (ADDR_RESOURCE_GEN_TILE_COUNT_DIV); + else { + save_code_area (ADDR_RESOURCE_GEN_TILE_COUNT_DIV, 7, true); - for (int n = 0; n < is->workable_tile_count; n++) { - if (n == 0) continue; - int dx, dy; - patch_ni_to_diff_for_work_area (n, &dx, &dy); - int x = city_x + dx, y = city_y + dy; - wrap_tile_coords (&p_bic_data->Map, &x, &y); - Tile * tile = tile_at (x, y); - if ((tile == NULL) || (tile == p_null_tile)) continue; - if (tile->Territory_OwnerID != city_civ_id) continue; - if (tile_has_enemy_unit (tile, city_civ_id)) continue; - if (tile->vtable->m20_Check_Pollution (tile, __, 0)) continue; - struct district_instance * inst = get_district_instance (tile); - if (inst == NULL) continue; - int district_id = inst->district_type; - if (district_id < 0 || district_id >= is->district_count) continue; - if (! district_is_complete (tile, district_id)) continue; + // Jump to replacement division logic, kept in an inlead because it doesn't fit in 7 bytes + emit_branch (BK_JUMP, ADDR_RESOURCE_GEN_TILE_COUNT_DIV, ADDR_RESOURCE_GEN_TILE_COUNT_DIV_REPL); - struct district_config const * cfg = &is->district_configs[district_id]; - int gold_bonus = 0; - get_effective_district_yields (inst, cfg, NULL, NULL, &gold_bonus, NULL, NULL); - district_gold += gold_bonus; - } + // Fill in the replacement division logic. Instead of computing tile_count>>5, compute tile_count/cfg->tiles_per_non_luxury_resource. + WITH_MEM_PROTECTION (ADDR_RESOURCE_GEN_TILE_COUNT_DIV_REPL, INLEAD_SIZE, PAGE_EXECUTE_READWRITE) { + int d = not_below (1, cfg->tiles_per_non_luxury_resource); + byte d0 = d & 0xFF, + d1 = (d >> 8) & 0xFF, + d2 = (d >> 16) & 0xFF, + d3 = d >> 24; - Leader * leader = &leaders[city_civ_id]; - int gold_proportion = (district_gold * leader->gold_slider) / 10; - int science_proportion = (district_gold * leader->science_slider) / 10; + byte * cursor = ADDR_RESOURCE_GEN_TILE_COUNT_DIV_REPL; - // Draw district gold icons - if (gold_proportion > 0) { - Sprite * gold_sprite = &is->district_commerce_icon; - int sprite_width = gold_sprite->Width; - int sprite_height = gold_sprite->Height; + // In the Steam EXE, the limit (tile_count/32 by default) must be left in ecx. For the other EXEs, edx. + if (exe_version_index == 1) { + byte new_div[] = { + 0x50, // push eax + 0x52, // push edx + 0x66, 0x8B, 0x56, 0x40, // mov dx, word ptr [esi+0x40] # Loads tile count + 0x0F, 0xB7, 0xD2, // movzx edx, dx + 0x89, 0xD0, // mov eax, edx + 0x31, 0xD2, // xor edx, edx + 0xB9, d0, d1, d2, d3, // mov ecx, {{divisor}} + 0xF7, 0xF1, // div ecx + 0x89, 0xC1, // mov ecx, eax + 0x5A, // pop edx + 0x58 // pop eax + }; + for (int n = 0; n < ARRAY_LEN (new_div); n++) + *cursor++ = new_div[n]; - struct tagRECT * gold_rect = &this->Gold_Income_Rect; - int total_gold = City_get_net_commerce (city, __, 2, true); + } else { + byte new_div[] = { + 0x50, // push eax + 0x51, // push ecx + 0x66, 0x8B, 0x56, 0x40, // mov dx, word ptr [esi+0x40] # Loads tile count + 0x0F, 0xB7, 0xD2, // movzx edx, dx + 0x89, 0xD0, // mov eax, edx + 0x31, 0xD2, // xor edx, edx + 0xB9, d0, d1, d2, d3, // mov ecx, {{divisor}} + 0xF7, 0xF1, // div ecx + 0x89, 0xC2, // mov edx, eax + 0x59, // pop ecx + 0x58 // pop eax + }; + for (int n = 0; n < ARRAY_LEN (new_div); n++) + *cursor++ = new_div[n]; + } - // Calculate spacing - int spacing = sprite_width; - if ((total_gold > 1) && (total_gold * sprite_width != (gold_rect->right - gold_rect->left))) { - int rect_width = gold_rect->right - gold_rect->left; - if (rect_width <= total_gold * sprite_width) { - spacing = (rect_width - sprite_width) / (total_gold - 1); - if (spacing < 1) - spacing = 1; - else if (spacing > sprite_width) - spacing = sprite_width; + cursor = emit_branch (BK_JUMP, cursor, ADDR_RESOURCE_GEN_TILE_COUNT_DIV + 7); } } - - // Draw from right to left - int x_offset = 0; - int y_offset = 5; - for (int i = 0; i < gold_proportion && i < total_gold; i++) { - int x = gold_rect->right - x_offset - sprite_width; - int y = gold_rect->top + ((gold_rect->bottom - gold_rect->top >> 1) - - (sprite_height >> 1)) + y_offset; - - Sprite_draw (gold_sprite, __, &(this->Base).Data.Canvas, x, y, NULL); - x_offset += spacing; - } } - // Draw district science icons - if (science_proportion > 0) { - Sprite * science_sprite = &is->district_commerce_icon; - int sprite_width = science_sprite->Width; - int sprite_height = science_sprite->Height; + // Disable a jump that skips playing victory animations for air units if they've been configured to have a victory anim + set_nopification (cfg->aircraft_victory_animation != NULL, ADDR_SKIP_VICTORY_ANIM_IF_AIR, 6); +} - struct tagRECT * science_rect = &this->Science_Income_Rect; - int total_science = City_get_net_commerce (city, __, 1, true); +void +get_mod_art_path (char const * file_name, char * out_path, int path_buf_size) +{ + char s[1000]; + snprintf (s, sizeof s, "Art\\%s", file_name); + s[(sizeof s) - 1] = '\0'; - // Calculate spacing - int spacing = sprite_width; - if ((total_science > 1) && (total_science * sprite_width != (science_rect->right - science_rect->left))) { - int rect_width = science_rect->right - science_rect->left; - if (rect_width <= total_science * sprite_width) { - spacing = (rect_width - sprite_width) / (total_science - 1); - if (spacing < 1) - spacing = 1; - else if (spacing > sprite_width) - spacing = sprite_width; - } - } + char * scenario_path = BIC_get_asset_path (p_bic_data, __, s, false); + if (0 != strcmp (scenario_path, s)) // get_asset_path returns its input when the file is not found + snprintf (out_path, path_buf_size, "%s", scenario_path); + else + snprintf (out_path, path_buf_size, "%s\\Art\\%s", is->mod_rel_dir, file_name); + out_path[path_buf_size - 1] = '\0'; +} - // Draw from right to left - int x_offset = 0; - int y_offset = 5; - for (int i = 0; i < science_proportion && i < total_science; i++) { - int x = science_rect->right - x_offset - sprite_width; - int y = science_rect->top + ((science_rect->bottom - science_rect->top >> 1) - - (sprite_height >> 1)) + y_offset; - Sprite_draw (science_sprite, __, &(this->Base).Data.Canvas, x, y, NULL); - x_offset += spacing; - } - } +Sprite* +SpriteList_at(SpriteList *list, int i) { + return &list->field_0[i]; } -void __fastcall -patch_City_Form_print_production_info (City_Form *this, int edx, String256 * out_strs, int str_capacity) -{ - City_Form_print_production_info (this, __, out_strs, str_capacity); - if (is->current_config.show_detailed_city_production_info) - out_strs[1].S[0] = '\0'; +void +set_path(String260 *dst, const char *p) { + snprintf(dst->S, sizeof(dst->S), "%s", p); } -int __fastcall -patch_Sprite_draw_strat_res_on_city_screen (Sprite * this, int edx, PCX_Image * canvas, int pixel_x, int pixel_y, PCX_Color_Table * color_table) +void +slice_grid(Sprite *out, PCX_Image *img, + int tile_w, int tile_h, int full_w, int full_h) { - if (is->current_config.compact_strategic_resource_display_on_city_screen) - pixel_x -= 13 * is->drawn_strat_resource_count + 17; - return Sprite_draw (this, __, canvas, pixel_x, pixel_y, color_table); + for (int y = 0; y < full_h; y += tile_h) + for (int x = 0; x < full_w; x += tile_w) + Sprite_slice_pcx(out++, __, img, x, y, tile_w, tile_h, 1, 1); } -int __fastcall -patch_PCX_Image_do_draw_cntd_text_for_strat_res (PCX_Image * this, int edx, char * str, int x, int y, int width, unsigned str_len) +void +slice_grid_into_list(SpriteList *bucket, PCX_Image *img, + int tile_w, int tile_h, int full_w, int full_h) { - if (is->current_config.compact_strategic_resource_display_on_city_screen) - x -= 13 * is->drawn_strat_resource_count + 17; - int tr = PCX_Image_do_draw_centered_text (this, __, str, x, y, width, str_len); - is->drawn_strat_resource_count++; - return tr; + int k = 0; + for (int y = 0; y < full_h; y += tile_h) + for (int x = 0; x < full_w; x += tile_w) + Sprite_slice_pcx(SpriteList_at(bucket, k++), __, img, x, y, tile_w, tile_h, 1, 1); } -int __fastcall -patch_City_get_turns_to_build (City * this, int edx, enum City_Order_Types order_type, int order_id, bool param_3) +void +join_path(char *out, size_t out_sz, const char *dir, const char *file) { - // To fix the zero production crash, return 9999 when the city's total production rate is zero, avoiding a division by zero. That's only - // possible when producing an improvement due to negative shields from specialists. The original logic attempts to return 9999 in case of zero - // production but checks for that before including shields from specialists. - if (is->current_config.patch_zero_production_crash && (order_type == COT_Improvement)) { + size_t n = strlen(dir); + int need_sep = (n > 0 && dir[n-1] != '/' && dir[n-1] != '\\'); + snprintf(out, out_sz, "%s%s%s", dir, need_sep ? "\\" : "", file); +} - int specialist_shields = 0; - FOR_CITIZENS_IN (ci, this) - if ((ci.ctzn->Body.field_20[0] & 0xFF) == 0) - specialist_shields += p_bic_data->CitizenTypes[ci.ctzn->Body.WorkerType].Construction; +void +read_in_dir(PCX_Image *img, + const char *art_dir, + const char *filename, + String260 *store) { + char pbuf[512]; + join_path(pbuf, sizeof pbuf, art_dir, filename); + if (store) { + // assumes: typedef struct { char S[260]; } String260; + snprintf(store->S, sizeof store->S, "%s", pbuf); + } - // Return 9999 if the denominator in the base function's calculation would be zero. Note the base calc is incorrect in that it - // considers specialist shields to count toward Wealth, however we're not going to address that issue here, just stop the crash. - if (this->Body.ProductionIncome + specialist_shields <= 0) - return 9999; - } + char temp_path[2*MAX_PATH]; - return City_get_turns_to_build (this, __, order_type, order_id, param_3); + snprintf(temp_path, sizeof temp_path, "%s\\%s", art_dir, filename); + PCX_Image_read_file(img, __, temp_path, NULL, 0, 0x100, 2); } -bool -is_below_stack_limit (Tile * tile, int civ_id, int type_id) +bool load_day_night_hour_images(struct day_night_cycle_img_set *this, const char *art_dir, const char *hour) { - enum UnitTypeClasses class = p_bic_data->UnitTypes[type_id].Unit_Class; - - int stack_limit = is->current_config.limit_units_per_tile[class]; - if (stack_limit <= 0) - return true; - - if (is->current_config.exclude_cities_from_units_per_tile_limit && - get_city_ptr (tile->CityID) != NULL) - return true; + char ss[200]; + PCX_Image img; + PCX_Image_construct(&img); - if (itable_look_up_or (&is->current_config.exclude_types_from_units_per_tile_limit, type_id, 0)) - return true; + // Std terrain (9 sheets): 6x9 of 128x64 over 0x480x0x240 + const char *STD_SHEETS[9] = { + "xtgc.pcx", "xpgc.pcx", "xdgc.pcx", "xdpc.pcx", "xdgp.pcx", "xggc.pcx", + "wCSO.pcx", "wSSS.pcx", "wOOO.pcx" + }; + for (int i = 0; i < 9; ++i) { + read_in_dir(&img, art_dir, STD_SHEETS[i], NULL); + if (img.JGL.Image == NULL) return false; + slice_grid_into_list(&this->Std_Terrain_Images[i], &img, 0x80, 0x40, 0x480, 0x240); + } + + // LM terrain (9): same slicing + const char *LMT_SHEETS[9] = { + "lxtgc.pcx", "lxpgc.pcx", "lxdgc.pcx", "lxdpc.pcx", "lxdgp.pcx", "lxggc.pcx", + "lwCSO.pcx", "lwSSS.pcx", "lwOOO.pcx" + }; + for (int i = 0; i < 9; ++i) { + read_in_dir(&img, art_dir, LMT_SHEETS[i], NULL); + if (img.JGL.Image == NULL) return false; + slice_grid_into_list(&this->LM_Terrain_Images[i], &img, 0x80, 0x40, 0x480, 0x240); + } - FOR_UNITS_ON (uti, tile) { - // If there is a foreign unit on the tile then consider it as being below the stack limit. This ensures that the stack limit doesn't - // block combat between players. - if (uti.unit->Body.CivID != civ_id) - return true; + // Polar icecaps: 8x4 of 128x64 + read_in_dir(&img, art_dir, "polarICEcaps-final.pcx", NULL); + if (img.JGL.Image == NULL) return false; + slice_grid(this->Polar_Icecaps_Images, &img, 0x80, 0x40, 0x400, 0x100); - int uti_type_id = uti.unit->Body.UnitTypeID; - if ((uti.unit->Body.Container_Unit < 0) && - (class == p_bic_data->UnitTypes[uti_type_id].Unit_Class) && - ! itable_look_up_or (&is->current_config.exclude_types_from_units_per_tile_limit, uti_type_id, 0)) { - stack_limit -= 1; - if (stack_limit <= 0) - return false; - } - } - return true; -} + // Hills / LM Hills: 4x3 of 128x72 + read_in_dir(&img, art_dir, "xhills.pcx", NULL); + if (img.JGL.Image == NULL) return false; + slice_grid(this->Hills_Images, &img, 0x80, 0x48, 0x200, 0x120); + read_in_dir(&img, art_dir, "hill forests.pcx", NULL); + if (img.JGL.Image == NULL) return false; + slice_grid(this->Hills_Forests_Images, &img, 0x80, 0x48, 0x200, 0x120); + read_in_dir(&img, art_dir, "hill jungle.pcx", NULL); + if (img.JGL.Image == NULL) return false; + slice_grid(this->Hills_Jungle_Images, &img, 0x80, 0x48, 0x200, 0x120); + read_in_dir(&img, art_dir, "LMHills.pcx", NULL); + if (img.JGL.Image == NULL) return false; + slice_grid(this->LM_Hills_Images, &img, 0x80, 0x48, 0x200, 0x120); -// Returns the ID of the civ this move is trespassing against, or 0 if it's not trespassing. -int -check_trespassing (int civ_id, Tile * from, Tile * to) -{ - int from_territory_id = from->vtable->m38_Get_Territory_OwnerID (from), - to_territory_id = to ->vtable->m38_Get_Territory_OwnerID (to); - if ((civ_id > 0) && - (to_territory_id != civ_id) && - (to_territory_id > 0) && - (to_territory_id != from_territory_id) && - (! leaders[civ_id].At_War[to_territory_id]) && - ((leaders[civ_id].Relation_Treaties[to_territory_id] & 2) == 0)) // Check right of passage - return to_territory_id; - else - return 0; -} + // Flood plains: 4x4 of 128x64 + read_in_dir(&img, art_dir, "floodplains.pcx", NULL); + if (img.JGL.Image == NULL) return false; + slice_grid(this->Flood_Plains_Images, &img, 0x80, 0x40, 0x200, 0x100); -bool -is_allowed_to_trespass (Unit * unit) -{ - int type_id = unit->Body.UnitTypeID; - if ((type_id >= 0) && (type_id < p_bic_data->UnitTypeCount)) { - UnitType * type = &p_bic_data->UnitTypes[type_id]; - return UnitType_has_ability (type, __, UTA_Hidden_Nationality) || UnitType_has_ability (type, __, UTA_Invisible); - } else - return false; -} + // Delta + Mountain rivers: 4x4 each, interleaved across one contiguous block + { + const char *RIV_SHEETS[2] = { "deltaRivers.pcx", "mtnRivers.pcx" }; + Sprite *contig = this->Delta_Rivers_Images; // Mountain_Rivers_Images follows + for (int s = 0; s < 2; ++s) { + read_in_dir(&img, art_dir, RIV_SHEETS[s], NULL); + if (img.JGL.Image == NULL) return false; + Sprite *p = contig + s; // even=delta, odd=mountain + for (int y = 0; y < 0x100; y += 0x40) + for (int x = 0; x < 0x200; x += 0x80) { + Sprite_slice_pcx(p, __, &img, x, y, 0x80, 0x40, 1, 1); + p += 2; + } + } + } -AdjacentMoveValidity __fastcall -patch_Unit_can_move_to_adjacent_tile (Unit * this, int edx, int neighbor_index, int param_2) -{ - AdjacentMoveValidity base_validity = Unit_can_move_to_adjacent_tile (this, __, neighbor_index, param_2); + // Waterfalls: 4x1 of 128x64 + read_in_dir(&img, art_dir, "waterfalls.pcx", NULL); + if (img.JGL.Image == NULL) return false; + slice_grid(this->Waterfalls_Images, &img, 0x80, 0x40, 0x200, 0x40); - // Apply unit count per tile limit - int type_id = this->Body.UnitTypeID; - if ((base_validity == AMV_OK) && (is->current_config.limit_units_per_tile[p_bic_data->UnitTypes[type_id].Unit_Class] > 0)) { - int nx, ny; - get_neighbor_coords (&p_bic_data->Map, this->Body.X, this->Body.Y, neighbor_index, &nx, &ny); - if (! is_below_stack_limit (tile_at (nx, ny), this->Body.CivID, type_id)) - return AMV_CANNOT_PASS_BETWEEN; - } + // Irrigation (desert/plains/normal/tundra): each 4x4 of 128x64 + read_in_dir(&img, art_dir, "irrigation DESETT.pcx", NULL); + if (img.JGL.Image == NULL) return false; + slice_grid(this->Irrigation_Desert_Images, &img, 0x80, 0x40, 0x200, 0x100); + read_in_dir(&img, art_dir, "irrigation PLAINS.pcx", NULL); + if (img.JGL.Image == NULL) return false; + slice_grid(this->Irrigation_Plains_Images, &img, 0x80, 0x40, 0x200, 0x100); + read_in_dir(&img, art_dir, "irrigation.pcx", NULL); + if (img.JGL.Image == NULL) return false; + slice_grid(this->Irrigation_Images, &img, 0x80, 0x40, 0x200, 0x100); + read_in_dir(&img, art_dir, "irrigation TUNDRA.pcx", NULL); + if (img.JGL.Image == NULL) return false; + slice_grid(this->Irrigation_Tundra_Images, &img, 0x80, 0x40, 0x200, 0x100); - // Apply trespassing restriction - if (is->current_config.disallow_trespassing && (base_validity == AMV_OK)) { - Tile * from = tile_at (this->Body.X, this->Body.Y); - int nx, ny; - get_neighbor_coords (&p_bic_data->Map, this->Body.X, this->Body.Y, neighbor_index, &nx, &ny); - int trespasses_against_civ_id = check_trespassing (this->Body.CivID, from, tile_at (nx, ny)); - if ((trespasses_against_civ_id > 0) && (! is_allowed_to_trespass (this))) - // The tile might be occupied by a unit belonging to a civ other than the one that owns the territory (against whom we'd be - // trespassing). In this case we must forbid the move entirely since TRIGGERS_WAR will not stop it if we're at war with the - // occupying civ. This fixes a bug where units could trespass by attacking an enemy unit across a border. They would then get - // stuck halfway between tiles if they won. - return (trespasses_against_civ_id == get_tile_occupier_id (nx, ny, this->Body.CivID, false)) ? AMV_TRIGGERS_WAR : AMV_CANNOT_PASS_BETWEEN; - } + // Volcanos (plain/forests/jungles/snow): 4x4 of 128x88 + read_in_dir(&img, art_dir, "Volcanos.pcx", NULL); + if (img.JGL.Image == NULL) return false; + slice_grid(this->Volcanos_Images, &img, 0x80, 0x58, 0x200, 0x160); + read_in_dir(&img, art_dir, "Volcanos forests.pcx", NULL); + if (img.JGL.Image == NULL) return false; + slice_grid(this->Volcanos_Forests_Images, &img, 0x80, 0x58, 0x200, 0x160); + read_in_dir(&img, art_dir, "Volcanos jungles.pcx", NULL); + if (img.JGL.Image == NULL) return false; + slice_grid(this->Volcanos_Jungles_Images, &img, 0x80, 0x58, 0x200, 0x160); + read_in_dir(&img, art_dir, "Volcanos-snow.pcx", NULL); + if (img.JGL.Image == NULL) return false; + slice_grid(this->Volcanos_Snow_Images, &img, 0x80, 0x58, 0x200, 0x160); - return base_validity; -} + // Marsh: Large band then Small band (tiles 128x88) + read_in_dir(&img, art_dir, "marsh.pcx", NULL); + if (img.JGL.Image == NULL) return false; + // Large (2 rows, 4 cols) + { int k=0; for (int y=0; y<0xb0; y+=0x58) for (int x=0; x<0x200; x+=0x80) + Sprite_slice_pcx(&this->Marsh_Large[k++], __, &img, x, y, 0x80, 0x58, 1, 1); } + // Small (2 rows, 5 cols) + { int k=0; for (int y=0xb0; y<0x160; y+=0x58) for (int x=0; x<0x280; x+=0x80) + Sprite_slice_pcx(&this->Marsh_Small[k++], __, &img, x, y, 0x80, 0x58, 1, 1); } -int __fastcall -patch_Trade_Net_get_movement_cost (Trade_Net * this, int edx, int from_x, int from_y, int to_x, int to_y, Unit * unit, int civ_id, unsigned flags, int neighbor_index, Trade_Net_Distance_Info * dist_info) -{ - if (is->is_computing_city_connections && // if this call came while rebuilding the trade network AND - (is->tnx_init_state == IS_OK) && (is->tnx_cache != NULL) && // Trade Net X is set up AND - (unit == NULL) && ((flags == 0x1009) || (flags == 0x9))) // this call can be accelerated by TNX - return is->get_move_cost_for_sea_trade (this, is->tnx_cache, from_x, from_y, to_x, to_y, civ_id, flags, neighbor_index, dist_info); + // LM mountains + standard mountains (plain/forests/jungles/snow): 4x4 of 128x88 + read_in_dir(&img, art_dir, "LMMountains.pcx", NULL); + if (img.JGL.Image == NULL) return false; + slice_grid(this->LM_Mountains_Images, &img, 0x80, 0x58, 0x200, 0x160); + read_in_dir(&img, art_dir, "Mountains.pcx", NULL); + if (img.JGL.Image == NULL) return false; + slice_grid(this->Mountains_Images, &img, 0x80, 0x58, 0x200, 0x160); + read_in_dir(&img, art_dir, "mountain forests.pcx", NULL); + if (img.JGL.Image == NULL) return false; + slice_grid(this->Mountains_Forests_Images, &img, 0x80, 0x58, 0x200, 0x160); + read_in_dir(&img, art_dir, "mountain jungles.pcx", NULL); + if (img.JGL.Image == NULL) return false; + slice_grid(this->Mountains_Jungles_Images, &img, 0x80, 0x58, 0x200, 0x160); + read_in_dir(&img, art_dir, "Mountains-snow.pcx", NULL); + if (img.JGL.Image == NULL) return false; + slice_grid(this->Mountains_Snow_Images, &img, 0x80, 0x58, 0x200, 0x160); - int const base_cost = Trade_Net_get_movement_cost (this, __, from_x, from_y, to_x, to_y, unit, civ_id, flags, neighbor_index, dist_info); + // Roads (16x16) and Railroads (16x17), tiles 128x64 + read_in_dir(&img, art_dir, "roads.pcx", NULL); + if (img.JGL.Image == NULL) return false; + slice_grid(this->Roads_Images, &img, 0x80, 0x40, 0x800, 0x400); + read_in_dir(&img, art_dir, "railroads.pcx", NULL); + if (img.JGL.Image == NULL) return false; + slice_grid(this->Railroads_Images, &img, 0x80, 0x40, 0x800, 0x440); - // Apply unit count per tile limit - if ((unit != NULL) && ! is_below_stack_limit (tile_at (to_x, to_y), unit->Body.CivID, unit->Body.UnitTypeID)) - return -1; + // LM Forests (Large 2x4, Small 2x6, Pines 2x6), tiles 128x88 + read_in_dir(&img, art_dir, "LMForests.pcx", NULL); + if (img.JGL.Image == NULL) return false; + { int k=0; for (int y=0; y<0xb0; y+=0x58) for (int x=0; x<0x200; x+=0x80) + Sprite_slice_pcx(&this->LM_Forests_Large_Images[k++], __, &img, x, y, 0x80, 0x58, 1, 1); } + { int k=0; for (int y=0xb0; y<0x160; y+=0x58) for (int x=0; x<0x300; x+=0x80) + Sprite_slice_pcx(&this->LM_Forests_Small_Images[k++], __, &img, x, y, 0x80, 0x58, 1, 1); } + { int k=0; for (int y=0x160; y<0x210; y+=0x58) for (int x=0; x<0x300; x+=0x80) + Sprite_slice_pcx(&this->LM_Forests_Pines_Images[k++], __, &img, x, y, 0x80, 0x58, 1, 1); } - // Apply trespassing restriction - if (is->current_config.disallow_trespassing && - check_trespassing (civ_id, tile_at (from_x, from_y), tile_at (to_x, to_y)) && - ((unit == NULL) || (! is_allowed_to_trespass (unit)))) - return -1; + // Grassland/Plains/Tundra forests & jungles (bands; tiles 128x88) — order is important + read_in_dir(&img, art_dir, "grassland forests.pcx", NULL); + if (img.JGL.Image == NULL) return false; + // Jungles Large, Small + { int k=0; for (int y=0; y<0xb0; y+=0x58) for (int x=0; x<0x200; x+=0x80) + Sprite_slice_pcx(&this->Grassland_Jungles_Large[k++], __, &img, x, y, 0x80, 0x58, 1, 1); } + { int k=0; for (int y=0xb0; y<0x160; y+=0x58) for (int x=0; x<0x300; x+=0x80) + Sprite_slice_pcx(&this->Grassland_Jungles_Small[k++], __, &img, x, y, 0x80, 0x58, 1, 1); } + // Forests Large, Small, Pines + { int k=0; for (int y=0x160; y<0x210; y+=0x58) for (int x=0; x<0x200; x+=0x80) + Sprite_slice_pcx(&this->Grassland_Forests_Large[k++], __, &img, x, y, 0x80, 0x58, 1, 1); } + { int k=0; for (int y=0x210; y<0x2c0; y+=0x58) for (int x=0; x<0x280; x+=0x80) + Sprite_slice_pcx(&this->Grassland_Forests_Small[k++], __, &img, x, y, 0x80, 0x58, 1, 1); } + { int k=0; for (int y=0x2c0; y<0x370; y+=0x58) for (int x=0; x<0x300; x+=0x80) + Sprite_slice_pcx(&this->Grassland_Forests_Pines[k++], __, &img, x, y, 0x80, 0x58, 1, 1); } - // Adjust movement cost to enforce limited railroad movement - if ((is->current_config.limit_railroad_movement > 0) && (is->saved_road_movement_rate > 0)) { - if ((unit != NULL) && (base_cost == 0)) { // Railroad move - if (! is->current_config.limited_railroads_work_like_fast_roads) { // If Civ 4 style RR, scale cost by type's moves - int type_moves_available = patch_Unit_get_max_move_points (unit) / p_bic_data->General.RoadsMovementRate; - return type_moves_available * is->railroad_mp_cost_per_move; - } else - return is->railroad_mp_cost_per_move; - } else if (base_cost == 1) // Road move - return is->road_mp_cost; - } + read_in_dir(&img, art_dir, "plains forests.pcx", NULL); + if (img.JGL.Image == NULL) return false; + { int k=0; for (int y=0x160; y<0x210; y+=0x58) for (int x=0; x<0x200; x+=0x80) + Sprite_slice_pcx(&this->Plains_Forests_Large[k++], __, &img, x, y, 0x80, 0x58, 1, 1); } + { int k=0; for (int y=0x210; y<0x2c0; y+=0x58) for (int x=0; x<0x280; x+=0x80) + Sprite_slice_pcx(&this->Plains_Forests_Small[k++], __, &img, x, y, 0x80, 0x58, 1, 1); } + { int k=0; for (int y=0x2c0; y<0x370; y+=0x58) for (int x=0; x<0x300; x+=0x80) + Sprite_slice_pcx(&this->Plains_Forests_Pines[k++], __, &img, x, y, 0x80, 0x58, 1, 1); } - return base_cost; -} + read_in_dir(&img, art_dir, "tundra forests.pcx", NULL); + if (img.JGL.Image == NULL) return false; + { int k=0; for (int y=0x160; y<0x210; y+=0x58) for (int x=0; x<0x200; x+=0x80) + Sprite_slice_pcx(&this->Tundra_Forests_Large[k++], __, &img, x, y, 0x80, 0x58, 1, 1); } + { int k=0; for (int y=0x210; y<0x2c0; y+=0x58) for (int x=0; x<0x280; x+=0x80) + Sprite_slice_pcx(&this->Tundra_Forests_Small[k++], __, &img, x, y, 0x80, 0x58, 1, 1); } + { int k=0; for (int y=0x2c0; y<0x370; y+=0x58) for (int x=0; x<0x300; x+=0x80) + Sprite_slice_pcx(&this->Tundra_Forests_Pines[k++], __, &img, x, y, 0x80, 0x58, 1, 1); } -int __fastcall -patch_Trade_Net_set_unit_path (Trade_Net * this, int edx, int from_x, int from_y, int to_x, int to_y, Unit * unit, int civ_id, int flags, int * out_path_length_in_mp) -{ - int tr = Trade_Net_set_unit_path (this, __, from_x, from_y, to_x, to_y, unit, civ_id, flags, out_path_length_in_mp); + // LM Terrain (7 single 128x64, vertical strip) + read_in_dir(&img, art_dir, "landmark_terrain.pcx", NULL); + if (img.JGL.Image == NULL) return false; + for (int i = 0, y = 0; i < 7; ++i, y += 0x40) + Sprite_slice_pcx(&this->LM_Terrain[i], __, &img, 0, y, 0x80, 0x40, 1, 1); - bool may_require_length_fix = (is->current_config.limit_railroad_movement > 0) && // if railroad movement is limited AND - (tr > 0) && (out_path_length_in_mp != NULL) && // path was found AND caller wants to know its length AND - (unit != NULL); // the path is for an actual unit + // TNT (same odd ordering as original) + read_in_dir(&img, art_dir, "tnt.pcx", NULL); + if (img.JGL.Image == NULL) return false; + for (int i=0, x=0; i<3; ++i, x+=0x80) Sprite_slice_pcx(&this->Tnt_Images[6+i], __, &img, x, 0x00, 0x80, 0x40, 1, 1); + for (int i=0, x=0; i<3; ++i, x+=0x80) Sprite_slice_pcx(&this->Tnt_Images[9+i], __, &img, x, 0x40, 0x80, 0x40, 1, 1); + for (int i=0, x=0; i<3; ++i, x+=0x80) Sprite_slice_pcx(&this->Tnt_Images[12+i], __, &img, x, 0x80, 0x80, 0x40, 1, 1); + for (int i=0, x=0; i<3; ++i, x+=0x80) Sprite_slice_pcx(&this->Tnt_Images[0+i], __, &img, x, 0xC0, 0x80, 0x40, 1, 1); + for (int i=0, x=0; i<3; ++i, x+=0x80) Sprite_slice_pcx(&this->Tnt_Images[15+i], __, &img, x, 0x100, 0x80, 0x40, 1, 1); + for (int i=0, x=0; i<3; ++i, x+=0x80) Sprite_slice_pcx(&this->Tnt_Images[3+i], __, &img, x, 0x140, 0x80, 0x40, 1, 1); - // We might have to correct the path length returned by the base game's pathfinder. This occurs when railroad movement is limited and the - // unit's total MP exceeds what can be stored in a one-byte integer. The cause of the incorrect length is that the pathfinder internally - // stores the remaining moves at each node of the search in a single byte. This correction fixes the bug (reported several times) that ETAs - // shown in the interface are wrong. - if (may_require_length_fix && (patch_Unit_get_max_move_points (unit) > 255)) { // Need to recompute path length if unit's total MP can overflow a uint8 + // Goody huts: 8 tiles, x=(i%3)*0x80, y=(i/3)*0x40 + read_in_dir(&img, art_dir, "goodyhuts.pcx", NULL); + if (img.JGL.Image == NULL) return false; + for (int i = 0; i < 8; ++i) { + int x = (i % 3) << 7; + int y = (i / 3) << 6; + Sprite_slice_pcx(&this->Goody_Huts_Images[i], __, &img, x, y, 0x80, 0x40, 1, 1); + } - // First memoize the cost of taking each step along the path. This must be done separately because the pathfinder's internal data only - // lets us traverse the path backwards. - { - // Must pass cached "distance info" to Trade_Net::get_movement_cost. Trade_Net stores two sets of cached data, which one was - // used depends on the flags. I believe one is intended to be used for city trade connections and the other for unit movement. - Trade_Net_Distance_Info * dist_info = (flags & 1) ? this->Data2 : this->Data4; + // Terrain buildings (fortress/camp/barbarian camp/mines/barricade) + read_in_dir(&img, art_dir, "TerrainBuildings.pcx", NULL); + if (img.JGL.Image == NULL) return false; + for (int i=0, y=0; i<4; ++i, y+=0x40) Sprite_slice_pcx(&this->Terrain_Buldings_Fortress[i], __, &img, 0x00, y, 0x80, 0x40, 1, 1); + for (int i=0, y=0; i<4; ++i, y+=0x40) Sprite_slice_pcx(&this->Terrain_Buldings_Camp[i], __, &img, 0x80, y, 0x80, 0x40, 1, 1); + Sprite_slice_pcx(&this->Terrain_Buldings_Barbarian_Camp, __, &img, 0x100, 0x00, 0x80, 0x40, 1, 1); + Sprite_slice_pcx(&this->Terrain_Buldings_Mines, __, &img, 0x100, 0x40, 0x80, 0x40, 1, 1); + for (int i=0, y=0; i<4; ++i, y+=0x40) Sprite_slice_pcx(&this->Terrain_Buldings_Barricade[i], __, &img, 0x180, y, 0x80, 0x40, 1, 1); - clear_memo (); - int x = to_x, y = to_y; - do { - // "flags & 1" again determines whether Data2 or Data4 was used. - enum direction dir = Trade_Net_get_direction_from_internal_map (this, __, x, y, flags & 1); - if (dir == DIR_ZERO) - break; + // Pollution & Craters (5x5 of 128x64) + read_in_dir(&img, art_dir, "pollution.pcx", NULL); + if (img.JGL.Image == NULL) return false; + slice_grid(this->Pollution, &img, 0x80, 0x40, 0x280, 0x140); + read_in_dir(&img, art_dir, "craters.pcx", NULL); + if (img.JGL.Image == NULL) return false; + slice_grid(this->Craters, &img, 0x80, 0x40, 0x280, 0x140); - int prev_x, prev_y; { - int dx, dy; - neighbor_index_to_diff (dir, &dx, &dy); - prev_x = x + dx; prev_y = y + dy; - wrap_tile_coords (&p_bic_data->Map, &prev_x, &prev_y); - } + // Airfields / Outposts / Radar + read_in_dir(&img, art_dir, "x_airfields and detect.pcx", NULL); + if (img.JGL.Image == NULL) return false; + for (int i=0, x=0; i<2; ++i, x+=0x80) Sprite_slice_pcx(&this->Terrain_Buldings_Airfields[i], __, &img, x, 0x00, 0x80, 0x40, 1, 1); + for (int i=0, x=0; i<3; ++i, x+=0x80) Sprite_slice_pcx(&this->Terrain_Buldings_Outposts[i], __, &img, x, 0x40, 0x80, 0x80, 1, 1); + Sprite_slice_pcx(&this->Terrain_Buldings_Radar, __, &img, 0x00, 0xC0, 0x80, 0x80, 1, 1); - memoize (patch_Trade_Net_get_movement_cost (this, __, prev_x, prev_y, x, y, unit, civ_id, flags, reverse_dir (dir), dist_info)); - x = prev_x; y = prev_y; - } while (! ((x == from_x) && (y == from_y))); - } + // Victory (single 128x64) + read_in_dir(&img, art_dir, "x_victory.pcx", NULL); + if (img.JGL.Image == NULL) return false; + Sprite_slice_pcx(&this->Victory_Image, __, &img, 0, 0, 0x80, 0x40, 1, 1); - // Now walk the path forwards tracking how much MP the unit would spend. We must be aware that the unit can't spend more MP than it - // has. For example, if a unit with 1 move walks onto an unimproved mountain, that effectively costs only 1 move, not 3. - int mp_remaining = patch_Unit_get_max_move_points (unit) - unit->Body.Moves, - mp_spent = 0; - for (int n = is->memo_len - 1; n >= 0; n--) { - int cost = is->memo[n]; - if (cost < mp_remaining) { - mp_spent += cost; - mp_remaining -= cost; - } else { - mp_spent += mp_remaining; - mp_remaining = patch_Unit_get_max_move_points (unit); + // Resources + read_in_dir(&img, art_dir, "resources.pcx", NULL); + if (img.JGL.Image == NULL) return false; + size_t k = 0; + for (int r = 0, y = 1; r < 6; ++r, y += 50) { + for (int c = 0, x = 1; c < 6; ++c, x += 50) { + Sprite_slice_pcx(&this->Resources[k++], __, &img, x, y, 49, 49, 1, 1); + } + } + + // Base cities + static const char *CITY_BASE[5] = { + "rAMER.pcx", "rEURO.pcx", "rROMAN.pcx", "rMIDEAST.pcx", "rASIAN.pcx" + }; + for (int culture = 0; culture < 5; culture++) { + read_in_dir(&img, art_dir, CITY_BASE[culture], NULL); + if (img.JGL.Image == NULL) return false; + int y = 0; + for (int era = 0; era < 4; ++era, y += 95) { + int x = 0; + for (int size = 0; size < 3; ++size, x += 167) { + const int idx = culture + 5*era + 20*size; + Sprite_slice_pcx(&this->City_Images[idx], __, &img, x, y, 167, 95, 1, 1); } } - *out_path_length_in_mp = mp_spent; - - // Also, if this is a move between adjacent tiles, make sure the path length doesn't exceed the unit's remaining MP. Otherwise, the game may - // erroneously show an ETA of >1 turn. - } else if (may_require_length_fix && are_tiles_adjacent (from_x, from_y, to_x, to_y)) - *out_path_length_in_mp = not_above (patch_Unit_get_max_move_points (unit) - unit->Body.Moves, *out_path_length_in_mp); - - return tr; -} + } -int __fastcall -patch_Trade_Net_set_unit_path_to_find_sea_route (Trade_Net * this, int edx, int from_x, int from_y, int to_x, int to_y, Unit * unit, int civ_id, int flags, int * out_path_length_in_mp) -{ - // Accelerate this call with TNX if possible - if ((is->tnx_init_state == IS_OK) && (is->tnx_cache != NULL)) { - bool route_exists = is->try_drawing_sea_trade_route (this, is->tnx_cache, from_x, from_y, to_x, to_y, civ_id, flags); - return route_exists ? 1 : 0; - } else - return Trade_Net_set_unit_path (this, __, from_x, from_y, to_x, to_y, unit, civ_id, flags, out_path_length_in_mp); -} + // Walled cities + static const char *CITY_WALL[5] = { + "AMERWALL.pcx", "EUROWALL.pcx", "ROMANWALL.pcx", "MIDEASTWALL.pcx", "ASIANWALL.pcx" + }; + for (int culture = 0; culture < 5; ++culture) { + read_in_dir(&img, art_dir, CITY_WALL[culture], NULL); + if (img.JGL.Image == NULL) return false; + int y = 0; + for (int era = 0; era < 4; ++era, y += 95) { + const int size = 3; // walled towns are a special category + const int idx = culture + 5*era + 20*size; + Sprite_slice_pcx(&this->City_Images[idx], __, &img, 0, y, 167, 95, 1, 1); + } + } -// Renames this leader and their civ based on their era. Era-specific names come from the config file. If this leader has a custom name set by the -// human player, this method does nothing. -void -apply_era_specific_names (Leader * leader) -{ - int leader_bit = 1 << leader->ID; - Race * race = &p_bic_data->Races[leader->RaceID]; + // Destroyed cities + read_in_dir(&img, art_dir, "DESTROY.pcx", NULL); + if (img.JGL.Image == NULL) return false; + int x = 0; + for (int i = 0; i < 3; ++i, x += 167) { + Sprite_slice_pcx(&this->Destroyed_City_Images[i], __, &img, x, 0, 167, 95, 1, 1); + } - struct replaceable_name { - char * base_name; - int * tracking_bits; - char * buf; - int buf_size; - } replaceable_names[] = { - {race->vtable->GetAdjectiveName (race), &is->aliased_civ_adjective_bits , leader->tribe_customization.civ_adjective , sizeof leader->tribe_customization.civ_adjective}, - {race->vtable->GetSingularName (race) , &is->aliased_civ_noun_bits , leader->tribe_customization.civ_noun , sizeof leader->tribe_customization.civ_noun}, - {race->vtable->GetCountryName (race) , &is->aliased_civ_formal_name_bits, leader->tribe_customization.civ_formal_name, sizeof leader->tribe_customization.civ_formal_name} - }; + // Districts (if enabled) + if (is->current_config.enable_districts) { + char art_dir[200]; + char temp_path[2*MAX_PATH]; + snprintf (art_dir, sizeof art_dir, "Districts/%s", hour); + get_mod_art_path (art_dir, temp_path, sizeof temp_path); + for (int dc = 0; dc < is->district_count; dc++) { + struct district_config const * cfg = &is->district_configs[dc]; + int variant_capacity = ARRAY_LEN (this->District_Images[dc]); + int variant_count = cfg->img_path_count; + if (variant_count <= 0) + continue; + if (variant_count > variant_capacity) + variant_count = variant_capacity; - // Apply replacements to civ noun, adjective, and formal name - for (int n = 0; n < ARRAY_LEN (replaceable_names); n++) { - struct replaceable_name * repl = &replaceable_names[n]; - if ((*repl->tracking_bits & leader_bit) || (repl->buf[0] == '\0')) { - char * replacement = NULL; - if (leader->Era < ERA_ALIAS_LIST_CAPACITY) - // Search through the list of aliases in reverse order so that, if the same key was listed multiple times, the last - // appearance overrides the earlier ones. This is important b/c when configs are loaded, their aliases get appended to - // the list. - for (int k = is->current_config.count_civ_era_alias_lists - 1; k >= 0; k--) { - struct civ_era_alias_list * list = &is->current_config.civ_era_alias_lists[k]; - if (strcmp (list->key, repl->base_name) == 0) { - replacement = list->aliases[leader->Era]; - break; + int era_count = cfg->vary_img_by_era ? 4 : 1; + int column_count = cfg->img_column_count; + int sprite_width = (cfg->custom_width > 0) ? cfg->custom_width : 128; + int sprite_height = (cfg->custom_height > 0) ? cfg->custom_height : 64; + + for (int variant_i = 0; variant_i < variant_count; variant_i++) { + const char * img_path = cfg->img_paths[variant_i]; + if ((img_path == NULL) || (img_path[0] == '\0')) + continue; + + read_in_dir (&img, temp_path, img_path, NULL); + if (img.JGL.Image == NULL) + return false; + + for (int era = 0; era < era_count; era++) { + for (int col = 0; col < column_count; col++) { + int tile_x = sprite_width * col; + int tile_y = sprite_height * era; + Sprite_slice_pcx (&this->District_Images[dc][variant_i][era][col], __, &img, tile_x, tile_y, sprite_width, sprite_height, 1, 1); } } - if (replacement != NULL) { - strncpy (repl->buf, replacement, repl->buf_size); - repl->buf[repl->buf_size - 1] = '\0'; - *repl->tracking_bits |= leader_bit; - } else { - repl->buf[0] = '\0'; - *repl->tracking_bits &= ~leader_bit; } } - } - // Apply replacement to leader name, gender, and title - if ((is->aliased_leader_name_bits & leader_bit) || (leader->tribe_customization.leader_name[0] == '\0')) { - char * base_name = race->vtable->GetLeaderName (race); - char * replacement_name = NULL; - char * replacement_title = NULL; - int replacement_gender; // Only used if replacement_name is - if (leader->Era < ERA_ALIAS_LIST_CAPACITY) - for (int k = is->current_config.count_leader_era_alias_lists - 1; k >= 0; k--) { - struct leader_era_alias_list * list = &is->current_config.leader_era_alias_lists[k]; - if (strcmp (list->key, base_name) == 0) { - replacement_name = list->aliases[leader->Era]; - replacement_title = list->titles[leader->Era]; - replacement_gender = (list->gender_bits >> leader->Era) & 1; - break; - } - } - if (replacement_name != NULL) { - TribeCustomization * tc = &leader->tribe_customization; - strncpy (tc->leader_name, replacement_name, sizeof tc->leader_name); - tc->leader_name[(sizeof tc->leader_name) - 1] = '\0'; - tc->leader_gender = replacement_gender; - is->aliased_leader_name_bits |= leader_bit; + // Abandoned district art (land + maritime) for this hour + read_in_dir (&img, temp_path, "Abandoned.pcx", NULL); + if (img.JGL.Image == NULL) + return false; + Sprite_slice_pcx (&this->Abandoned_District_Image, __, &img, 0, 0, 128, 64, 1, 1); + Sprite_slice_pcx (&this->Abandoned_Maritime_District_Image, __, &img, 128, 0, 128, 64, 1, 1); - // If this replacement name has a special title and this player does not have a custom title set, replace the title. - if ((replacement_title != NULL) && ((is->aliased_leader_title_bits & leader_bit) || (tc->leader_title[0] == '\0'))) { - strncpy (tc->leader_title, replacement_title, sizeof tc->leader_title); - tc->leader_title[(sizeof tc->leader_title) - 1] = '\0'; - is->aliased_leader_title_bits |= leader_bit; + // Load wonder district images (dynamically per wonder) for this hour + if (is->current_config.enable_wonder_districts) { + char const * last_img_path = NULL; + PCX_Image wpcx; + PCX_Image_construct (&wpcx); + bool pcx_loaded = false; - // If the current name has no title and the player's title was previously replaced, undo the replacement. - } else if ((replacement_title == NULL) && (is->aliased_leader_title_bits & leader_bit)) { - leader->tribe_customization.leader_title[0] = '\0'; - is->aliased_leader_title_bits &= ~leader_bit; - } - } else { - leader->tribe_customization.leader_name[0] = '\0'; - // Don't need to clear custom leader gender since it's not used unless a custom name was set - is->aliased_leader_name_bits &= ~leader_bit; + for (int wi = 0; wi < is->wonder_district_count; wi++) { + struct wonder_district_config * cfg = &is->wonder_district_configs[wi]; + char const * img_path = cfg->img_path; + if (img_path == NULL) + img_path = "Wonders.pcx"; - // Remove title replacement if present - if (is->aliased_leader_title_bits & leader_bit) { - leader->tribe_customization.leader_title[0] = '\0'; - is->aliased_leader_title_bits &= ~leader_bit; - } - } - } -} + // Load new image file if different from previous + if ((last_img_path == NULL) || (strcmp (img_path, last_img_path) != 0)) { + if (pcx_loaded) + wpcx.vtable->clear_JGL (&wpcx); -int __cdecl -patch_do_save_game (char const * file_path, char param_2, GUID * guid) -{ - // Do not save the modified road movement rate, if it was modified to limit railroad movement - int restore_rmr = (is->current_config.limit_railroad_movement > 0) && (is->saved_road_movement_rate > 0); - int rmr; - if (restore_rmr) { - rmr = p_bic_data->General.RoadsMovementRate; - p_bic_data->General.RoadsMovementRate = is->saved_road_movement_rate; - } + read_in_dir (&wpcx, temp_path, img_path, NULL); - // Do not save the modified barb culture group ID - int restore_barb_culture_group = is->current_config.enable_city_capture_by_barbarians && (is->saved_barb_culture_group >= 0); - int barb_culture; - if (restore_barb_culture_group) { - barb_culture = p_bic_data->Races[leaders[0].RaceID].CultureGroupID; - p_bic_data->Races[leaders[0].RaceID].CultureGroupID = is->saved_barb_culture_group; - } + if (wpcx.JGL.Image == NULL) { + pcx_loaded = false; + continue; + } - // Do not save names that were replaced with era-specific versions - for (int n = 0; n < 32; n++) { - Leader * leader = &leaders[n]; - int leader_bit = 1 << leader->ID; - if (is->aliased_civ_noun_bits & leader_bit) leader->tribe_customization.civ_noun [0] = '\0'; - if (is->aliased_civ_adjective_bits & leader_bit) leader->tribe_customization.civ_adjective [0] = '\0'; - if (is->aliased_civ_formal_name_bits & leader_bit) leader->tribe_customization.civ_formal_name[0] = '\0'; - if (is->aliased_leader_name_bits & leader_bit) leader->tribe_customization.leader_name [0] = '\0'; - if (is->aliased_leader_title_bits & leader_bit) leader->tribe_customization.leader_title [0] = '\0'; - } - is->aliased_civ_noun_bits = is->aliased_civ_adjective_bits = is->aliased_civ_formal_name_bits = is->aliased_leader_name_bits = is->aliased_leader_title_bits = 0; + pcx_loaded = true; + last_img_path = img_path; + } - // Do not save unit types with the charm bits cleared if they were cleared by convertion to PTW targeting. The special actions field must not - // include the top category bits that are part of the UCV_* enum - for (int n = 0; n < is->count_charmed_types_converted_to_ptw_arty; n++) { - UnitType * converted_type = &p_bic_data->UnitTypes[is->charmed_types_converted_to_ptw_arty[n]]; - converted_type->Special_Actions |= (0x00FFFFFF & UCV_Charm_Bombard); - } + if (! pcx_loaded) + continue; - int tr = do_save_game (file_path, param_2, guid); + struct wonder_district_image_set * set = &this->Wonder_District_Images[wi]; - if (restore_rmr) - p_bic_data->General.RoadsMovementRate = rmr; - if (restore_barb_culture_group) - p_bic_data->Races[leaders[0].RaceID].CultureGroupID = barb_culture; + Sprite_construct (&set->img); + int x = 128 * cfg->img_column; + int y = 64 * cfg->img_row; + Sprite_slice_pcx (&set->img, __, &wpcx, x, y, 128, 64, 1, 1); - // Reapply era aliases - for (int n = 0; n < 32; n++) - if (*p_player_bits & (1 << n)) - apply_era_specific_names (&leaders[n]); + Sprite_construct (&set->construct_img); + int cx = 128 * cfg->img_construct_column; + int cy = 64 * cfg->img_construct_row; + Sprite_slice_pcx (&set->construct_img, __, &wpcx, cx, cy, 128, 64, 1, 1); - // Reclear charm bits on converted types - for (int n = 0; n < is->count_charmed_types_converted_to_ptw_arty; n++) { - UnitType * converted_type = &p_bic_data->UnitTypes[is->charmed_types_converted_to_ptw_arty[n]]; - converted_type->Special_Actions &= ~(0x00FFFFFF & UCV_Charm_Bombard); - } + if (cfg->enable_img_alt_dir) { + Sprite_construct (&set->alt_dir_img); + int ax = 128 * cfg->img_alt_dir_column; + int ay = 64 * cfg->img_alt_dir_row; + Sprite_slice_pcx (&set->alt_dir_img, __, &wpcx, ax, ay, 128, 64, 1, 1); - return tr; -} + Sprite_construct (&set->alt_dir_construct_img); + int acx = 128 * cfg->img_alt_dir_construct_column; + int acy = 64 * cfg->img_alt_dir_construct_row; + Sprite_slice_pcx (&set->alt_dir_construct_img, __, &wpcx, acx, acy, 128, 64, 1, 1); + } + } -void -record_unit_type_alt_strategy (int type_id) -{ - int ai_strat_index; { - int ai_strat_bits = p_bic_data->UnitTypes[type_id].AI_Strategy; - if ((ai_strat_bits & ai_strat_bits - 1) != 0) // Sanity check: must only have one strat (one bit) set - return; - ai_strat_index = 0; - while ((ai_strat_bits & 1) == 0) { - ai_strat_index++; - ai_strat_bits >>= 1; + if (pcx_loaded) + wpcx.vtable->clear_JGL (&wpcx); + wpcx.vtable->destruct (&wpcx, __, 0); } + } - itable_insert (&is->unit_type_alt_strategies, type_id, ai_strat_index); -} + if (is->current_config.enable_natural_wonders && (is->natural_wonder_count > 0)) { + char art_dir[200]; + char temp_path[2*MAX_PATH]; + snprintf (art_dir, sizeof art_dir, "Districts/%s", hour); + get_mod_art_path (art_dir, temp_path, sizeof temp_path); -void -append_improv_id_to_list (struct improv_id_list * list, int id) -{ - reserve (sizeof list->items[0], (void **)&list->items, &list->capacity, list->count); - list->items[list->count] = id; - list->count += 1; -} + char const * last_img_path = NULL; + PCX_Image nwpcx; + PCX_Image_construct (&nwpcx); + bool pcx_loaded = false; -unsigned __fastcall -patch_load_scenario (void * this, int edx, char * param_1, unsigned * param_2) -{ - int ret_addr = ((int *)¶m_1)[-1]; + for (int ni = 0; ni < is->natural_wonder_count; ni++) { + struct natural_wonder_district_config const * cfg = &is->natural_wonder_configs[ni]; + if ((cfg->img_path == NULL) || (cfg->img_path[0] == '\0')) + continue; - // Destroy TNX cache from previous map. A new one will be created when needed. - if ((is->tnx_init_state == IS_OK) && (is->tnx_cache != NULL)) { - is->destroy_tnx_cache (is->tnx_cache); - is->tnx_cache = NULL; - } + char const * img_path = cfg->img_path; + if ((last_img_path == NULL) || (strcmp (img_path, last_img_path) != 0)) { + if (pcx_loaded) + nwpcx.vtable->clear_JGL (&nwpcx); - unsigned tr = load_scenario (this, __, param_1, param_2); - char * scenario_path = param_1; + read_in_dir (&nwpcx, temp_path, img_path, NULL); - // There are two cases when load_scenario is called that we don't want to run our own code. These are (1) when the scenario is loaded to - // generate a preview (map, etc.) for the "Civ Content" or "Conquests" menu and (2) when the function is called recursively. The recursive - // call is done, I believe, only when loading saves using the default rules. The game first tries to load custom rules then falls back on - // loading the default rules. In any case, we want to ignore that call so we don't run the same code twice. - if ((ret_addr == ADDR_LOAD_SCENARIO_PREVIEW_RETURN) || (ret_addr == ADDR_LOAD_SCENARIO_RESUME_SAVE_2_RETURN)) - return tr; + if (nwpcx.JGL.Image == NULL) { + pcx_loaded = false; + continue; + } - reset_to_base_config (); - load_config ("default.c3x_config.ini", 1); - char * scenario_config_file_name = "scenario.c3x_config.ini"; - char * scenario_config_path = BIC_get_asset_path (p_bic_data, __, scenario_config_file_name, false); - - // BIC_get_asset_path returns the file name when it can't find the file - if (0 != strcmp (scenario_config_file_name, scenario_config_path)) { - load_config (scenario_config_path, 0); - } - load_config ("custom.c3x_config.ini", 1); - apply_machine_code_edits (&is->current_config, false); + pcx_loaded = true; + last_img_path = img_path; + } - if (is->current_config.enable_districts || is->current_config.enable_natural_wonders) { - reset_district_state (true); - load_districts_config (); + if (! pcx_loaded) + continue; + + Sprite_construct (&this->Natural_Wonder_Images[ni].img); + int x = 128 * cfg->img_column; + int y = 88 * cfg->img_row; + Sprite_slice_pcx (&this->Natural_Wonder_Images[ni].img, __, &nwpcx, x, y, 128, 88, 1, 1); + } + + if (pcx_loaded) + nwpcx.vtable->clear_JGL (&nwpcx); + nwpcx.vtable->destruct (&nwpcx, __, 0); } - // Initialize Trade Net X - if (is->current_config.enable_trade_net_x && (is->tnx_init_state == IS_UNINITED)) { - char path[MAX_PATH]; - snprintf (path, sizeof path, "%s\\Trade Net X\\TradeNetX.dll", is->mod_rel_dir); - path[(sizeof path) - 1] = '\0'; - is->trade_net_x = LoadLibraryA (path); - if (is->trade_net_x != NULL) { - is->set_exe_version = (void *)(*p_GetProcAddress) (is->trade_net_x, "set_exe_version"); - is->create_tnx_cache = (void *)(*p_GetProcAddress) (is->trade_net_x, "create_tnx_cache"); - is->destroy_tnx_cache = (void *)(*p_GetProcAddress) (is->trade_net_x, "destroy_tnx_cache"); - is->set_up_before_building_network = (void *)(*p_GetProcAddress) (is->trade_net_x, "set_up_before_building_network"); - is->get_move_cost_for_sea_trade = (void *)(*p_GetProcAddress) (is->trade_net_x, "get_move_cost_for_sea_trade"); - is->flood_fill_road_network = (void *)(*p_GetProcAddress) (is->trade_net_x, "flood_fill_road_network"); - is->try_drawing_sea_trade_route = (void *)(*p_GetProcAddress) (is->trade_net_x, "try_drawing_sea_trade_route"); + img.vtable->destruct (&img, __, 0); - is->set_exe_version (exe_version_index); + return true; +} - // Run tests - if (0) { - int (__stdcall * test) () = (void *)(*p_GetProcAddress) (is->trade_net_x, "test"); - int failed_test_count = test (); - if (failed_test_count > 0) - MessageBoxA (NULL, "Failed some tests in Trade Net X!", NULL, MB_ICONWARNING); - else - MessageBoxA (NULL, "All tests in Trade Net X passed.", "Success", MB_ICONINFORMATION); - } +Sprite * +get_sprite_proxy_for_current_hour(Sprite *s) { + int v; + int hour = is->current_day_night_cycle; // 0..23 + if (itable_look_up(&is->day_night_sprite_proxy_by_hour[hour], (int)s, &v)) + return (Sprite *)v; + return NULL; // not proxied, fall back to s +} - is->tnx_init_state = IS_OK; - } else { - MessageBoxA (NULL, "Failed to load Trade Net X!", NULL, MB_ICONERROR); - is->tnx_init_state = IS_INIT_FAILED; +void +insert_spritelist_proxies(SpriteList *ss, SpriteList *ps, int hour, int len1, int len2) { + for (int i = 0; i < len1; i++) { + for (int j = 0; j < len2; j++) { + Sprite *s = &ss[i].field_0[j]; + Sprite *p = &ps[i].field_0[j]; + if (s && p) { + itable_insert(&is->day_night_sprite_proxy_by_hour[hour], (int)s, (int)p); + } } - - // Deinitialize Trade Net X - } else if ((! is->current_config.enable_trade_net_x) && (is->tnx_init_state == IS_OK)) { - FreeLibrary (is->trade_net_x); - is->trade_net_x = NULL; - is->tnx_init_state = IS_UNINITED; } +} - // This scenario might use different mod art assets than the old one - deinit_stackable_command_buttons (); - deinit_disabled_command_buttons (); - deinit_trade_scroll_buttons (); - deinit_unit_rcm_icons (); - deinit_red_food_icon (); - deinit_large_minimap_frame (); - if (is->tile_already_worked_zoomed_out_sprite_init_state != IS_UNINITED) { - enum init_state * state = &is->tile_already_worked_zoomed_out_sprite_init_state; - if (*state == IS_OK) { - Sprite * sprite = &is->tile_already_worked_zoomed_out_sprite; - sprite->vtable->destruct (sprite, __, 0); +void +insert_sprite_proxies(Sprite *ss, Sprite *ps, int hour, int len) { + for (int i = 0; i < len; i++) { + Sprite *s = &ss[i]; + Sprite *p = &ps[i]; + if (s && p) { + itable_insert(&is->day_night_sprite_proxy_by_hour[hour], (int)s, (int)p); } - *state = IS_UNINITED; } +} - // Need to clear this since the resource count might have changed - if (is->extra_available_resources != NULL) { - free (is->extra_available_resources); - is->extra_available_resources = NULL; - is->extra_available_resources_capacity = 0; +void +insert_sprite_proxy(Sprite *s, Sprite *p, int hour) { + if (s && p) { + itable_insert(&is->day_night_sprite_proxy_by_hour[hour], (int)s, (int)p); } +} - // Similarly, these don't carry over between games - for (int n = 0; n < 32; n++) - is->interceptor_reset_lists[n].count = 0; - is->replay_for_players = 0; - table_deinit (&is->extra_defensive_bombards); - table_deinit (&is->airdrops_this_turn); - table_deinit (&is->unit_transport_ties); - is->last_selected_unit.initial_x = is->last_selected_unit.initial_y = -1; - is->last_selected_unit.last_x = is->last_selected_unit.last_y = is->last_selected_unit.type_id = -1; - is->last_selected_unit.ptr = NULL; - table_deinit (&is->waiting_units); - is->have_loaded_waiting_units = false; +void +build_sprite_proxies_24(Map_Renderer *mr) { + for (int h = 0; h < 24; ++h) { + insert_sprite_proxies(city_sprites, is->day_night_cycle_imgs[h].City_Images, h, 80); + insert_sprite_proxies(destroyed_city_sprites, is->day_night_cycle_imgs[h].Destroyed_City_Images, h, 3); + insert_sprite_proxies(mr->Resources, is->day_night_cycle_imgs[h].Resources, h, 36); + insert_spritelist_proxies(mr->Std_Terrain_Images, is->day_night_cycle_imgs[h].Std_Terrain_Images, h, 9, 81); + insert_spritelist_proxies(mr->LM_Terrain_Images, is->day_night_cycle_imgs[h].LM_Terrain_Images, h, 9, 81); + insert_sprite_proxy(&mr->Terrain_Buldings_Barbarian_Camp, &is->day_night_cycle_imgs[h].Terrain_Buldings_Barbarian_Camp, h); + insert_sprite_proxy(&mr->Terrain_Buldings_Mines, &is->day_night_cycle_imgs[h].Terrain_Buldings_Mines, h); + insert_sprite_proxy(&mr->Victory_Image, &is->day_night_cycle_imgs[h].Victory_Image, h); + insert_sprite_proxy(&mr->Terrain_Buldings_Radar, &is->day_night_cycle_imgs[h].Terrain_Buldings_Radar, h); + insert_sprite_proxies(mr->Flood_Plains_Images, is->day_night_cycle_imgs[h].Flood_Plains_Images, h, 16); + insert_sprite_proxies(mr->Polar_Icecaps_Images, is->day_night_cycle_imgs[h].Polar_Icecaps_Images, h, 32); + insert_sprite_proxies(mr->Roads_Images, is->day_night_cycle_imgs[h].Roads_Images, h, 256); + insert_sprite_proxies(mr->Railroads_Images, is->day_night_cycle_imgs[h].Railroads_Images, h, 272); + insert_sprite_proxies(mr->Terrain_Buldings_Airfields, is->day_night_cycle_imgs[h].Terrain_Buldings_Airfields, h, 2); + insert_sprite_proxies(mr->Terrain_Buldings_Camp, is->day_night_cycle_imgs[h].Terrain_Buldings_Camp, h, 4); + insert_sprite_proxies(mr->Terrain_Buldings_Fortress, is->day_night_cycle_imgs[h].Terrain_Buldings_Fortress, h, 4); + insert_sprite_proxies(mr->Terrain_Buldings_Barricade, is->day_night_cycle_imgs[h].Terrain_Buldings_Barricade, h, 4); + insert_sprite_proxies(mr->Goody_Huts_Images, is->day_night_cycle_imgs[h].Goody_Huts_Images, h, 8); + insert_sprite_proxies(mr->Terrain_Buldings_Outposts, is->day_night_cycle_imgs[h].Terrain_Buldings_Outposts, h, 3); + insert_sprite_proxies(mr->Pollution, is->day_night_cycle_imgs[h].Pollution, h, 25); + insert_sprite_proxies(mr->Craters, is->day_night_cycle_imgs[h].Craters, h, 25); + insert_sprite_proxies(mr->Tnt_Images, is->day_night_cycle_imgs[h].Tnt_Images, h, 18); + insert_sprite_proxies(mr->Waterfalls_Images, is->day_night_cycle_imgs[h].Waterfalls_Images, h, 4); + insert_sprite_proxies(mr->LM_Terrain, is->day_night_cycle_imgs[h].LM_Terrain, h, 7); + insert_sprite_proxies(mr->Marsh_Large, is->day_night_cycle_imgs[h].Marsh_Large, h, 8); + insert_sprite_proxies(mr->Marsh_Small, is->day_night_cycle_imgs[h].Marsh_Small, h, 10); + insert_sprite_proxies(mr->Volcanos_Images, is->day_night_cycle_imgs[h].Volcanos_Images, h, 16); + insert_sprite_proxies(mr->Volcanos_Forests_Images, is->day_night_cycle_imgs[h].Volcanos_Forests_Images, h, 16); + insert_sprite_proxies(mr->Volcanos_Jungles_Images, is->day_night_cycle_imgs[h].Volcanos_Jungles_Images, h, 16); + insert_sprite_proxies(mr->Volcanos_Snow_Images, is->day_night_cycle_imgs[h].Volcanos_Snow_Images, h, 16); + insert_sprite_proxies(mr->Grassland_Forests_Large, is->day_night_cycle_imgs[h].Grassland_Forests_Large, h, 8); + insert_sprite_proxies(mr->Plains_Forests_Large, is->day_night_cycle_imgs[h].Plains_Forests_Large, h, 8); + insert_sprite_proxies(mr->Tundra_Forests_Large, is->day_night_cycle_imgs[h].Tundra_Forests_Large, h, 8); + insert_sprite_proxies(mr->Grassland_Forests_Small, is->day_night_cycle_imgs[h].Grassland_Forests_Small, h, 10); + insert_sprite_proxies(mr->Plains_Forests_Small, is->day_night_cycle_imgs[h].Plains_Forests_Small, h, 10); + insert_sprite_proxies(mr->Tundra_Forests_Small, is->day_night_cycle_imgs[h].Tundra_Forests_Small, h, 10); + insert_sprite_proxies(mr->Grassland_Forests_Pines, is->day_night_cycle_imgs[h].Grassland_Forests_Pines, h, 12); + insert_sprite_proxies(mr->Plains_Forests_Pines, is->day_night_cycle_imgs[h].Plains_Forests_Pines, h, 12); + insert_sprite_proxies(mr->Tundra_Forests_Pines, is->day_night_cycle_imgs[h].Tundra_Forests_Pines, h, 12); + insert_sprite_proxies(mr->Irrigation_Desert_Images, is->day_night_cycle_imgs[h].Irrigation_Desert_Images, h, 16); + insert_sprite_proxies(mr->Irrigation_Plains_Images, is->day_night_cycle_imgs[h].Irrigation_Plains_Images, h, 16); + insert_sprite_proxies(mr->Irrigation_Images, is->day_night_cycle_imgs[h].Irrigation_Images, h, 16); + insert_sprite_proxies(mr->Irrigation_Tundra_Images, is->day_night_cycle_imgs[h].Irrigation_Tundra_Images, h, 16); + insert_sprite_proxies(mr->Grassland_Jungles_Large, is->day_night_cycle_imgs[h].Grassland_Jungles_Large, h, 8); + insert_sprite_proxies(mr->Grassland_Jungles_Small, is->day_night_cycle_imgs[h].Grassland_Jungles_Small, h, 12); + insert_sprite_proxies(mr->Mountains_Images, is->day_night_cycle_imgs[h].Mountains_Images, h, 16); + insert_sprite_proxies(mr->Mountains_Forests_Images, is->day_night_cycle_imgs[h].Mountains_Forests_Images, h, 16); + insert_sprite_proxies(mr->Mountains_Jungles_Images, is->day_night_cycle_imgs[h].Mountains_Jungles_Images, h, 16); + insert_sprite_proxies(mr->Mountains_Snow_Images, is->day_night_cycle_imgs[h].Mountains_Snow_Images, h, 16); + insert_sprite_proxies(mr->Hills_Images, is->day_night_cycle_imgs[h].Hills_Images, h, 16); + insert_sprite_proxies(mr->Hills_Forests_Images, is->day_night_cycle_imgs[h].Hills_Forests_Images, h, 16); + insert_sprite_proxies(mr->Hills_Jungle_Images, is->day_night_cycle_imgs[h].Hills_Jungle_Images, h, 16); + insert_sprite_proxies(mr->Delta_Rivers_Images, is->day_night_cycle_imgs[h].Delta_Rivers_Images, h, 16); + insert_sprite_proxies(mr->Mountain_Rivers_Images, is->day_night_cycle_imgs[h].Mountain_Rivers_Images, h, 16); + insert_sprite_proxies(mr->LM_Mountains_Images, is->day_night_cycle_imgs[h].LM_Mountains_Images, h, 16); + insert_sprite_proxies(mr->LM_Forests_Large_Images, is->day_night_cycle_imgs[h].LM_Forests_Large_Images, h, 8); + insert_sprite_proxies(mr->LM_Forests_Small_Images, is->day_night_cycle_imgs[h].LM_Forests_Small_Images, h, 10); + insert_sprite_proxies(mr->LM_Forests_Pines_Images, is->day_night_cycle_imgs[h].LM_Forests_Pines_Images, h, 12); + insert_sprite_proxies(mr->LM_Hills_Images, is->day_night_cycle_imgs[h].LM_Hills_Images, h, 16); + + if (is->current_config.enable_districts) { + for (int dc = 0; dc < is->district_count; dc++) { + struct district_config const * cfg = &is->district_configs[dc]; + int variant_capacity = ARRAY_LEN (is->district_img_sets[dc].imgs); + int variant_count = cfg->img_path_count; + if (variant_count <= 0) + continue; + if (variant_count > variant_capacity) + variant_count = variant_capacity; - // Clear extra city improvement bits - FOR_TABLE_ENTRIES (tei, &is->extra_city_improvs) - free ((void *)tei.value); - table_deinit (&is->extra_city_improvs); + int era_count = cfg->vary_img_by_era ? 4 : 1; + int column_count = cfg->img_column_count; - // Clear unit type counts - for (int n = 0; n < 32; n++) - table_deinit (&is->unit_type_counts[n]); - is->unit_type_count_init_bits = 0; + for (int variant_i = 0; variant_i < variant_count; variant_i++) { + if ((cfg->img_paths[variant_i] == NULL) || (cfg->img_paths[variant_i][0] == '\0')) + continue; + for (int era = 0; era < era_count; era++) { + for (int col = 0; col < column_count; col++) { + Sprite * base = &is->district_img_sets[dc].imgs[variant_i][era][col]; + Sprite * proxy = &is->day_night_cycle_imgs[h].District_Images[dc][variant_i][era][col]; + insert_sprite_proxy (base, proxy, h); + } + } + } + } - // Clear last city founding turn numbers - for (int n = 0; n < 32; n++) - is->turn_no_of_last_founding_for_settler_perfume[n] = -1; + insert_sprite_proxy (&is->abandoned_district_img, &is->day_night_cycle_imgs[h].Abandoned_District_Image, h); + insert_sprite_proxy (&is->abandoned_maritime_district_img, &is->day_night_cycle_imgs[h].Abandoned_Maritime_District_Image, h); - // Load resources.pcx - { - PCX_Image * rs = is->resources_sheet; - if (rs != NULL) - rs->vtable->destruct (rs, __, 0); - else - rs = malloc (sizeof *rs); - memset (rs, 0, sizeof *rs); - PCX_Image_construct (rs); + // Wonder districts + if (is->current_config.enable_wonder_districts) { + for (int wi = 0; wi < is->wonder_district_count; wi++) { + Sprite * base_img = &is->wonder_district_img_sets[wi].img; + Sprite * proxy_img = &is->day_night_cycle_imgs[h].Wonder_District_Images[wi].img; + insert_sprite_proxy (base_img, proxy_img, h); - char * resources_pcx_path = BIC_get_asset_path (p_bic_data, __, "Art\\resources.pcx", true); - PCX_Image_read_file (rs, __, resources_pcx_path, NULL, 0, 0x100, 2); - is->resources_sheet = rs; - } + Sprite * base_construct = &is->wonder_district_img_sets[wi].construct_img; + Sprite * proxy_construct = &is->day_night_cycle_imgs[h].Wonder_District_Images[wi].construct_img; + insert_sprite_proxy (base_construct, proxy_construct, h); - // Recreate table of alt strategies mapping duplicates to their strategies - table_deinit (&is->unit_type_alt_strategies); - for (int n = 0; n < p_bic_data->UnitTypeCount; n++) { - int alt_for_id = p_bic_data->UnitTypes[n].alternate_strategy_for_id; - if (alt_for_id >= 0) { - record_unit_type_alt_strategy (n); - record_unit_type_alt_strategy (alt_for_id); // Record the original too so we know it has alternatives - } - } - - // Recreate table of duplicates mapping unit types to the next duplicate - table_deinit (&is->unit_type_duplicates); - for (int n = 0; n < p_bic_data->UnitTypeCount; n++) { - int alt_for_id = p_bic_data->UnitTypes[n].alternate_strategy_for_id; - if (alt_for_id >= 0) { + if (is->wonder_district_img_sets[wi].alt_dir_img.vtable != NULL) { + Sprite * base_alt = &is->wonder_district_img_sets[wi].alt_dir_img; + Sprite * proxy_alt = &is->day_night_cycle_imgs[h].Wonder_District_Images[wi].alt_dir_img; + insert_sprite_proxy (base_alt, proxy_alt, h); + } - // Find the type ID of the last duplicate in the list. alt_for_id refers to the original unit that was duplicated possibly - // multiple times. Begin searching there and, each time we find another duplicate, use its ID to continue the search. When - // we're done last_dup_id will be set to whichever ID in the list of duplicates is not already associated with another. - int last_dup_id = alt_for_id; { - int next; - while (itable_look_up (&is->unit_type_duplicates, last_dup_id, &next)) - last_dup_id = next; + if (is->wonder_district_img_sets[wi].alt_dir_construct_img.vtable != NULL) { + Sprite * base_alt_construct = &is->wonder_district_img_sets[wi].alt_dir_construct_img; + Sprite * proxy_alt_construct = &is->day_night_cycle_imgs[h].Wonder_District_Images[wi].alt_dir_construct_img; + insert_sprite_proxy (base_alt_construct, proxy_alt_construct, h); + } + } } - - // Add this unit type to the end of the list of duplicates - itable_insert (&is->unit_type_duplicates, last_dup_id, n); } - } - - // Convert charm-flagged units to using PTW targeting if necessary - if (is->current_config.charm_flag_triggers_ptw_like_targeting) { - for (int n = 0; n < p_bic_data->UnitTypeCount; n++) { - UnitType * type = &p_bic_data->UnitTypes[n]; - if (type->Special_Actions & UCV_Charm_Bombard) { - // Also add it to the list of converted types - reserve (sizeof is->charmed_types_converted_to_ptw_arty[0], // item size - (void **)&is->charmed_types_converted_to_ptw_arty, // ptr to items - &is->charmed_types_converted_to_ptw_arty_capacity, // ptr to capacity - is->count_charmed_types_converted_to_ptw_arty); // count - is->charmed_types_converted_to_ptw_arty[is->count_charmed_types_converted_to_ptw_arty] = n; - is->count_charmed_types_converted_to_ptw_arty += 1; - - // Add this type ID to the table - itable_insert (&is->current_config.ptw_arty_types, n, 1); - // Clear the charm flag, taking care not to clear the category bit. This is necessary for the PTW targeting to work - // since the logic to implement it only applies to the non-charm code path. It wouldn't make sense to have both charm - // attack and PTW targeting anyway, since charm attack already works that way vs cities. - type->Special_Actions &= ~(0x00FFFFFF & UCV_Charm_Bombard); + // Natural wonders + if (is->current_config.enable_natural_wonders && (is->natural_wonder_count > 0)) { + for (int ni = 0; ni < is->natural_wonder_count; ni++) { + Sprite * base_nw = &is->natural_wonder_img_sets[ni].img; + Sprite * proxy_nw = &is->day_night_cycle_imgs[h].Natural_Wonder_Images[ni].img; + insert_sprite_proxy (base_nw, proxy_nw, h); } } } + is->day_night_cycle_img_proxies_indexed = true; +} - // Pick out which resources are used as mill inputs - if (is->mill_input_resource_bits) - free (is->mill_input_resource_bits); - is->mill_input_resource_bits = calloc (1, 1 + p_bic_data->ResourceTypeCount / 8); - for (int n = 0; n < is->current_config.count_mills; n++) { - struct mill * mill = &is->current_config.mills[n]; - for (int k = 0; k < 2; k++) { - int resource_id = (&p_bic_data->Improvements[mill->improv_id].Resource1ID)[k]; - if ((resource_id >= 0) && (resource_id < p_bic_data->ResourceTypeCount)) { - byte bit = 1 << (resource_id & 7); - is->mill_input_resource_bits[resource_id>>3] |= bit; - } - } - } +void +init_day_night_images() +{ + if (is->day_night_cycle_img_state != IS_UNINITED) + return; - // Recreate lists of water & air trade improvements - is->water_trade_improvs .count = 0; - is->air_trade_improvs .count = 0; - is->combat_defense_improvs.count = 0; - for (int n = 0; n < p_bic_data->ImprovementsCount; n++) { - enum ImprovementTypeFlags flags = p_bic_data->Improvements[n].ImprovementFlags; - if (flags & ITF_Allows_Water_Trade) append_improv_id_to_list (&is->water_trade_improvs , n); - if (flags & ITF_Allows_Air_Trade) append_improv_id_to_list (&is->air_trade_improvs , n); - if (p_bic_data->Improvements[n].Combat_Defence != 0) append_improv_id_to_list (&is->combat_defense_improvs, n); - } + const char *hour_strs[24] = { + "2400", "0100", "0200", "0300", "0400", "0500", "0600", "0700", + "0800", "0900", "1000", "1100", "1200", "1300", "1400", "1500", + "1600", "1700", "1800", "1900", "2000", "2100", "2200", "2300" + }; - // Set up for limiting railroad movement, if enabled - if (is->current_config.limit_railroad_movement > 0) { - int base_rmr = p_bic_data->General.RoadsMovementRate; - int g = gcd (base_rmr, is->current_config.limit_railroad_movement); // Scale down all MP costs by this common divisor to help against - // overflow of 8-bit integers inside the pathfinder. - is->saved_road_movement_rate = base_rmr; - p_bic_data->General.RoadsMovementRate = base_rmr * is->current_config.limit_railroad_movement / g; // Full move in MP - is->road_mp_cost = is->current_config.limit_railroad_movement / g; - is->railroad_mp_cost_per_move = base_rmr / g; - } else { - is->saved_road_movement_rate = -1; - is->road_mp_cost = 1; - is->railroad_mp_cost_per_move = 0; - } + for (int i = 0; i < 24; i++) { - // If barb city capturing is enabled and the barbs have the non-existent "none" culture group (index -1), switch them to the first real - // culture group. The "none" group produces corrupt graphics and crashes. - int * barb_culture_group = &p_bic_data->Races[leaders[0].RaceID].CultureGroupID; - if (is->current_config.enable_city_capture_by_barbarians && (*barb_culture_group < 0)) { - is->saved_barb_culture_group = *barb_culture_group; - *barb_culture_group = 0; - } else - is->saved_barb_culture_group = -1; + char art_dir[200]; + char temp_path[2*MAX_PATH]; + snprintf (art_dir, sizeof art_dir, "DayNight/%s", hour_strs[i]); + get_mod_art_path (art_dir, temp_path, sizeof temp_path); + bool success = load_day_night_hour_images (&is->day_night_cycle_imgs[i], temp_path, hour_strs[i]); - // Clear old alias bits - is->aliased_civ_noun_bits = is->aliased_civ_adjective_bits = is->aliased_civ_formal_name_bits = is->aliased_leader_name_bits = is->aliased_leader_title_bits = 0; + if (!success) { + char ss[200]; + snprintf(ss, sizeof ss, "Failed to load day/night cycle images for hour %s, reverting to base game art.", hour_strs[i]); + pop_up_in_game_error (ss); - // Clear day/night cycle vars and deindex sprite proxies, if necessary. - if (is->current_config.day_night_cycle_mode != DNCM_OFF) { - is->day_night_cycle_unstarted = true; - is->current_day_night_cycle = 12; - if (is->day_night_cycle_img_proxies_indexed) { - deindex_day_night_image_proxies (); + is->day_night_cycle_img_state = IS_INIT_FAILED; + return; } } - return tr; -} + Map_Renderer * mr = &p_bic_data->Map.Renderer; + build_sprite_proxies_24(mr); -void __fastcall -patch_Leader_recompute_auto_improvements (Leader * this) -{ - is->leader_param_for_patch_get_wonder_city_id = this; - Leader_recompute_auto_improvements (this); + is->day_night_cycle_img_state = IS_OK; } -int __fastcall -patch_Game_get_wonder_city_id (Game * this, int edx, int wonder_improvement_id) +void +deindex_day_night_image_proxies() { - int ret_addr = ((int *)&wonder_improvement_id)[-1]; - if ((is->current_config.enable_free_buildings_from_small_wonders) && (ret_addr == ADDR_SMALL_WONDER_FREE_IMPROVS_RETURN)) { - Leader * leader = is->leader_param_for_patch_get_wonder_city_id; - Improvement * improv = &p_bic_data->Improvements[wonder_improvement_id]; - if ((improv->Characteristics & ITC_Small_Wonder) != 0) { - // Need to check if Small_Wonders array is NULL b/c recompute_auto_improvements gets called with leaders that are absent/dead. - return (leader->Small_Wonders != NULL) ? leader->Small_Wonders[wonder_improvement_id] : -1; - } + if (!is->day_night_cycle_img_proxies_indexed) + return; + + for (int i = 0; i < 24; i++) { + table_deinit (&is->day_night_sprite_proxy_by_hour[i]); } - return Game_get_wonder_city_id (this, __, wonder_improvement_id); + is->day_night_cycle_img_proxies_indexed = false; } -int __fastcall -patch_Main_Screen_Form_handle_key_up (Main_Screen_Form * this, int edx, int virtual_key_code) +int +calculate_current_day_night_cycle_hour () { - if ((virtual_key_code & 0xFF) == VK_CONTROL) { - patch_Main_GUI_set_up_unit_command_buttons (&this->GUI); + int output = 12; // Default to noon + int increment = is->current_config.fixed_hours_per_turn_for_day_night_cycle; - if (is->current_config.enable_city_work_radii_highlights && is->highlight_city_radii) { - is->highlight_city_radii = false; - clear_highlighted_worker_tiles_and_redraw (); - } - } + switch (is->current_config.day_night_cycle_mode) { - return Main_Screen_Form_handle_key_up (this, __, virtual_key_code); -} + // Disabled. This shouldn't be possible, but default to noon to be safe + case DNCM_OFF: + return output; -char __fastcall -patch_Leader_can_do_worker_job (Leader * this, int edx, enum Worker_Jobs job, int tile_x, int tile_y, int ask_if_replacing) -{ - char tr; - bool skip_replacement_logic = - (p_main_screen_form->Player_CivID == this->ID) && is->current_config.skip_repeated_tile_improv_replacement_asks; + // Time elapsed since last update + case DNCM_TIMER: { + LARGE_INTEGER perf_freq; + QueryPerformanceFrequency(&perf_freq); - // Check if AI is trying to change a district tile (before calling vanilla logic) - if ((is->current_config.enable_districts || is->current_config.enable_natural_wonders) && - ((*p_human_player_bits & (1 << this->ID)) == 0)) { - Tile * tile = tile_at (tile_x, tile_y); - if ((tile != NULL) && (tile != p_null_tile)) { - struct district_instance * inst = get_district_instance (tile); - if (inst != NULL && inst->district_type >= 0 && inst->district_type < is->district_count) { - int district_id = inst->district_type; + if (is->day_night_cycle_unstarted) { + is->current_day_night_cycle = output; + QueryPerformanceCounter(&is->last_day_night_cycle_update_time); + } - // For Wonder Districts: check if unused (can be replaced) - if (is->current_config.enable_wonder_districts && (district_id == WONDER_DISTRICT_ID)) { - struct wonder_district_info * info = get_wonder_district_info (tile); + LARGE_INTEGER time_now; + QueryPerformanceCounter(&time_now); - // If there's a reservation (wonder being built) or completed wonder, block replacement - if (info != NULL && info->state != WDS_UNUSED) - return 0; + double elapsed_seconds = + (double)(time_now.QuadPart - is->last_day_night_cycle_update_time.QuadPart) / + (double)perf_freq.QuadPart; - // Wonder district is unused - fall through to normal tech checks - } - else if (is->current_config.enable_natural_wonders && (district_id == NATURAL_WONDER_DISTRICT_ID)) { - return 0; - } - else { - // For all other district types: AI should not change them - return 0; - } + if (elapsed_seconds > (double)is->current_config.elapsed_minutes_per_day_night_hour_transition * 60.0) { + output = is->current_day_night_cycle + increment; + is->last_day_night_cycle_update_time = time_now; + } else { + output = is->current_day_night_cycle; } + break; } - } - if (! skip_replacement_logic) - tr = Leader_can_do_worker_job (this, __, job, tile_x, tile_y, ask_if_replacing); - else if (is->have_job_and_loc_to_skip && - (is->to_skip.job == job) && (is->to_skip.tile_x == tile_x) && (is->to_skip.tile_y == tile_y)) - tr = Leader_can_do_worker_job (this, __, job, tile_x, tile_y, 0); - else { - is->show_popup_was_called = 0; - tr = Leader_can_do_worker_job (this, __, job, tile_x, tile_y, ask_if_replacing); - if (is->show_popup_was_called && tr) { // Check that the popup was shown and the player chose to replace - is->to_skip = (struct worker_job_and_location) { .job = job, .tile_x = tile_x, .tile_y = tile_y }; - is->have_job_and_loc_to_skip = 1; + // Match user's current time + case DNCM_USER_TIME: { + LPSYSTEMTIME lpSystemTime = (LPSYSTEMTIME)malloc(sizeof(SYSTEMTIME)); + GetLocalTime (lpSystemTime); + output = lpSystemTime->wHour; + free (lpSystemTime); + break; + } + + // Increment fixed amount each interturn + case DNCM_EVERY_TURN: { + if (is->day_night_cycle_unstarted) { + increment = 0; + is->current_day_night_cycle = output; + } + output = is->current_day_night_cycle + increment; + break; + } + + // Pin the hour to a specific value + case DNCM_SPECIFIED: { + output = is->current_config.pinned_hour_for_day_night_cycle; + break; } } - if (! tr && is->current_config.enable_districts && (job == WJ_Build_Mines)) { - Tile * tile = tile_at (tile_x, tile_y); - if ((tile != NULL) && (tile != p_null_tile)) { - struct district_instance * inst = get_district_instance (tile); - if (inst != NULL && - inst->district_type >= 0 && inst->district_type < is->district_count && - ! tile->vtable->m35_Check_Is_Water (tile) && - (tile->CityID < 0) && - ! tile->vtable->m18_Check_Mines (tile, __, 0)) { - int owner_civ = tile->vtable->m38_Get_Territory_OwnerID (tile); - if ((owner_civ == this->ID) || (owner_civ == 0)) { - // Check if the leader has the tech prereq for this district - int prereq_id = is->district_infos[inst->district_type].advance_prereq_id; - if (prereq_id < 0 || Leader_has_tech (this, __, prereq_id)) - tr = 1; - } - } - } - } - - return tr; -} - -bool __fastcall -patch_Unit_can_hurry_production (Unit * this, int edx, City * city, bool exclude_cheap_improvements) -{ - if (is->current_config.allow_military_leaders_to_hurry_wonders && Unit_has_ability (this, __, UTA_Leader)) { - LeaderKind actual_kind = this->Body.leader_kind; - this->Body.leader_kind = LK_Scientific; - bool tr = Unit_can_hurry_production (this, __, city, exclude_cheap_improvements); - this->Body.leader_kind = actual_kind; - return tr; - } else - return Unit_can_hurry_production (this, __, city, exclude_cheap_improvements); -} - -bool __fastcall -patch_Unit_can_load (Unit * this, int edx, Unit * passenger) -{ - is->can_load_transport = this; - is->can_load_passenger = passenger; - bool tr; - - // If this potential passenger is tied to a different transport, do not allow it to load into this one - int tied_transport_id = -1; - if (is->current_config.limit_unit_loading_to_one_transport_per_turn && - (! Unit_has_ability (this, __, UTA_Army)) && - itable_look_up (&is->unit_transport_ties, passenger->Body.ID, &tied_transport_id) && - (this->Body.ID != tied_transport_id)) - tr = false; - - else - tr = Unit_can_load (this, __, passenger); - - is->can_load_transport = is->can_load_passenger = NULL; - return tr; -} - -void __fastcall -patch_Unit_load (Unit * this, int edx, Unit * transport) -{ - Unit_load (this, __, transport); - - // Tie the unit to the transport if configured to do so - if (is->current_config.limit_unit_loading_to_one_transport_per_turn && ! Unit_has_ability (transport, __, UTA_Army)) - itable_insert (&is->unit_transport_ties, this->Body.ID, transport->Body.ID); -} + // If midnight or over, restart at 0 or later + if (output > 23) output = output - 24; -bool -any_enemies_near (Leader const * me, int tile_x, int tile_y, enum UnitTypeClasses class, int num_tiles) -{ - bool tr = false; - FOR_TILES_AROUND (tai, num_tiles, tile_x, tile_y) { - int enemy_on_this_tile = 0; - FOR_UNITS_ON (uti, tai.tile) { - UnitType const * unit_type = &p_bic_data->UnitTypes[uti.unit->Body.UnitTypeID]; - if (patch_Unit_is_visible_to_civ (uti.unit, __, me->ID, 0) && - (((int)class < 0) || (unit_type->Unit_Class == class))) { - if (me->At_War[uti.unit->Body.CivID]) { - if ((unit_type->Defence > 0) || (unit_type->Attack > 0)) { - enemy_on_this_tile = 1; - break; - } - } else - break; - } - } - if (enemy_on_this_tile) { - tr = true; - break; - } - } - return tr; -} + // Clamp to valid range of 0-23 in case of weird config values + output = clamp (0, 23, output); + is->day_night_cycle_unstarted = false; -bool -any_enemies_near_unit (Unit * unit, int num_tiles) -{ - UnitType * unit_type = &p_bic_data->UnitTypes[unit->Body.UnitTypeID]; - return any_enemies_near (&leaders[unit->Body.CivID], unit->Body.X, unit->Body.Y, unit_type->Unit_Class, num_tiles); + return output; } void __fastcall -patch_Unit_ai_move_artillery (Unit * this) +patch_Map_Renderer_load_images (Map_Renderer *this, int edx) { - if ((! is->current_config.use_offensive_artillery_ai) || - ((this->Body.UnitTypeID < 0) || (this->Body.UnitTypeID >= p_bic_data->UnitTypeCount))) // Check for invalid unit type id which appears sometimes, IDK why - goto base_impl; - - Tile * on_tile = tile_at (this->Body.X, this->Body.Y); - City * in_city = get_city_ptr (on_tile->vtable->m45_Get_City_ID (on_tile)); - UnitType const * this_type = &p_bic_data->UnitTypes[this->Body.UnitTypeID]; - int num_escorters_req = this->vtable->eval_escort_requirement (this); - - if ((in_city == NULL) || (count_escorters (this) >= num_escorters_req)) - goto base_impl; - - // Don't assign escort if there are any enemies around because in that case it might be a serious mistake to take a defender out of the city - if (any_enemies_near_unit (this, 37)) - goto base_impl; - - // Find the strongest healthy defender the city has and assign that unit as an escort but make sure doing so doesn't leave the city - // defenseless. I think picking the strongest defender is the right choice here because the artillery pair is more likely to come under - // attack than a city and also leaving obsolete units in the city gives them a chance to upgrade. - int num_defenders = 0; - Unit * best_defender = NULL; - int best_defender_strength = -1; - FOR_UNITS_ON (uti, on_tile) { - Unit_Body * body = &uti.unit->Body; - UnitType * type = &p_bic_data->UnitTypes[body->UnitTypeID]; - if ((type->AI_Strategy & UTAI_Defence) && - (! UnitType_has_ability (type, __, UTA_Immobile)) && - (body->Damage == 0) && - ((body->UnitState == 0) || (body->UnitState == UnitState_Fortifying)) && - (body->escortee < 0)) { - num_defenders++; - int str = type->Defence * Unit_get_max_hp (uti.unit); - if (str > best_defender_strength) { - best_defender = uti.unit; - best_defender_strength = str; - } - } - } - if ((num_defenders >= 2) && (best_defender != NULL)) { - Unit_set_state (best_defender, __, 0); - Unit_set_escortee (best_defender, __, this->Body.ID); - } - -base_impl: - Unit_ai_move_artillery (this); - - // Recompute these since the unit might have moved - on_tile = tile_at (this->Body.X, this->Body.Y); - in_city = get_city_ptr (on_tile->vtable->m45_Get_City_ID (on_tile)); - - // Load the unit into a transport for a naval invasion if it's just sitting in a city with nothing else to do - if (is->current_config.use_offensive_artillery_ai && - (in_city != NULL) && - (this->Body.Moves == 0) && - (this->Body.UnitState == UnitState_Fortifying) && - (this->Body.Container_Unit < 0)) { - Unit * transport = Unit_find_transport (this, __, this->Body.X, this->Body.Y); - if (transport != NULL) { + Map_Renderer_load_images(this, __); - int transport_capacity = p_bic_data->UnitTypes[transport->Body.UnitTypeID].Transport_Capacity; - int units_in_transport, arty_in_transport; { - units_in_transport = arty_in_transport = 0; - FOR_UNITS_ON (uti, on_tile) - if (uti.unit->Body.Container_Unit == transport->Body.ID) { - units_in_transport++; - arty_in_transport += (p_bic_data->UnitTypes[uti.unit->Body.UnitTypeID].AI_Strategy & UTAI_Artillery) != 0; - } - } + // Initialize day/night cycle and re-calculate hour, if applicable + if (is->current_config.day_night_cycle_mode != DNCM_OFF) { + is->current_day_night_cycle = calculate_current_day_night_cycle_hour (); - // Check that there's space for the arty unit and an escort, and that the transport does not have more than one arty per three - // spaces so we're less likely to assemble invasion forces composed of nothing but artillery. - if ((units_in_transport + 2 <= transport_capacity) && - (arty_in_transport < not_below (1, transport_capacity / 3))) { - Unit_set_escortee (this, __, -1); - patch_Unit_load (this, __, transport); - } + if (is->day_night_cycle_img_state == IS_UNINITED) { + init_day_night_images (); } - } -} - -// Estimates the number of turns necessary to move the given unit to the given tile and writes it to *out_num_turns. Returns 0 if there is no path -// from the unit's current position to the given tile, 1 otherwise. -int -estimate_travel_time (Unit * unit, int to_tile_x, int to_tile_y, int * out_num_turns) -{ - int dist_in_mp; - patch_Trade_Net_set_unit_path (is->trade_net, __, unit->Body.X, unit->Body.Y, to_tile_x, to_tile_y, unit, unit->Body.CivID, 1, &dist_in_mp); - dist_in_mp += unit->Body.Moves; // Add MP already spent this turn to the distance - int max_mp = patch_Unit_get_max_move_points (unit); - if ((dist_in_mp >= 0) && (max_mp > 0)) { - *out_num_turns = dist_in_mp / max_mp; - return 1; - } else - return 0; // No path or unit cannot move -} -City * -find_nearest_established_city (Unit * unit, int continent_id) -{ - int lowest_unattractiveness = INT_MAX; - City * least_unattractive_city = NULL; - FOR_CITIES_OF (coi,unit->Body.CivID) { - Tile * city_tile = tile_at (coi.city->Body.X, coi.city->Body.Y); - if (continent_id == city_tile->vtable->m46_Get_ContinentID (city_tile)) { - int dist_in_turns; - if (! estimate_travel_time (unit, coi.city->Body.X, coi.city->Body.Y, &dist_in_turns)) - continue; - int unattractiveness = 10 * dist_in_turns; - unattractiveness = (10 + unattractiveness) / (10 + coi.city->Body.Population.Size); - if (coi.city->Body.CultureIncome > 0) - unattractiveness /= 5; - if (unattractiveness < lowest_unattractiveness) { - lowest_unattractiveness = unattractiveness; - least_unattractive_city = coi.city; + if (is->day_night_cycle_img_state == IS_OK) { + + // Sprite proxies are deindexed during each load event as sprite instances (really only Resources, which are reloaded) may change. + if (!is->day_night_cycle_img_proxies_indexed) { + build_sprite_proxies_24(this); } } } - return least_unattractive_city; -} - -bool __fastcall -patch_Unit_ai_can_form_army (Unit * this) -{ - int build_army_action = UCV_Build_Army & 0x0FFFFFFF; // Mask out top four category bits - UnitType * type = &p_bic_data->UnitTypes[this->Body.UnitTypeID]; - if (is->current_config.patch_ai_can_form_army_without_special_ability && ((type->Special_Actions & build_army_action) == 0)) - return false; - else - return Unit_ai_can_form_army (this); } -void __fastcall -patch_Unit_ai_move_leader (Unit * this) +void +patch_init_floating_point () { - if (! is->current_config.replace_leader_unit_ai) { - Unit_ai_move_leader (this); - return; - } - - Tile * tile = tile_at (this->Body.X, this->Body.Y); - int continent_id = tile->vtable->m46_Get_ContinentID (tile); + init_floating_point (); - // Disband the unit if it's on a continent with no cities of its civ. This is what the original logic does. - if (leaders[this->Body.CivID].city_count_per_cont[continent_id] == 0) { - Unit_disband (this); - return; - } + // NOTE: At this point the program is done with the CRT initialization stuff and will start calling constructors for global + // objects as soon as this function returns. This is a good place to inject code that will run at program start. - // Flee if the unit is near an enemy without adequate escort - int has_adequate_escort; { - int escorter_count = 0; + // Specify metadata about all boolean options on the mod config. We'll use this info to set up the table of offsets (for easy parsing) and to + // fill out the base config. + struct boolean_config_option { + char * name; + bool base_val; + int offset; + } boolean_config_options[] = { + {"enable_stack_bombard" , true , offsetof (struct c3x_config, enable_stack_bombard)}, + {"enable_disorder_warning" , true , offsetof (struct c3x_config, enable_disorder_warning)}, + {"allow_stealth_attack_against_single_unit" , false, offsetof (struct c3x_config, allow_stealth_attack_against_single_unit)}, + {"show_detailed_city_production_info" , true , offsetof (struct c3x_config, show_detailed_city_production_info)}, + {"limited_railroads_work_like_fast_roads" , false, offsetof (struct c3x_config, limited_railroads_work_like_fast_roads)}, + {"exclude_cities_from_units_per_tile_limit" , false, offsetof (struct c3x_config, exclude_cities_from_units_per_tile_limit)}, + {"enable_free_buildings_from_small_wonders" , true , offsetof (struct c3x_config, enable_free_buildings_from_small_wonders)}, + {"enable_stack_unit_commands" , true , offsetof (struct c3x_config, enable_stack_unit_commands)}, + {"skip_repeated_tile_improv_replacement_asks" , true , offsetof (struct c3x_config, skip_repeated_tile_improv_replacement_asks)}, + {"autofill_best_gold_amount_when_trading" , true , offsetof (struct c3x_config, autofill_best_gold_amount_when_trading)}, + {"disallow_founding_next_to_foreign_city" , true , offsetof (struct c3x_config, disallow_founding_next_to_foreign_city)}, + {"enable_trade_screen_scroll" , true , offsetof (struct c3x_config, enable_trade_screen_scroll)}, + {"group_units_on_right_click_menu" , true , offsetof (struct c3x_config, group_units_on_right_click_menu)}, + {"gray_out_units_on_menu_with_no_remaining_moves" , true , offsetof (struct c3x_config, gray_out_units_on_menu_with_no_remaining_moves)}, + {"put_movement_icons_on_units_on_menu" , true , offsetof (struct c3x_config, put_movement_icons_on_units_on_menu)}, + {"describe_states_of_units_on_menu" , true , offsetof (struct c3x_config, describe_states_of_units_on_menu)}, + {"show_golden_age_turns_remaining" , true , offsetof (struct c3x_config, show_golden_age_turns_remaining)}, + {"show_zoc_attacks_from_mid_stack" , true , offsetof (struct c3x_config, show_zoc_attacks_from_mid_stack)}, + {"show_armies_performing_defensive_bombard" , true , offsetof (struct c3x_config, show_armies_performing_defensive_bombard)}, + {"cut_research_spending_to_avoid_bankruptcy" , true , offsetof (struct c3x_config, cut_research_spending_to_avoid_bankruptcy)}, + {"dont_pause_for_love_the_king_messages" , true , offsetof (struct c3x_config, dont_pause_for_love_the_king_messages)}, + {"reverse_specialist_order_with_shift" , true , offsetof (struct c3x_config, reverse_specialist_order_with_shift)}, + {"toggle_zoom_with_z_on_city_screen" , true , offsetof (struct c3x_config, toggle_zoom_with_z_on_city_screen)}, + {"dont_give_king_names_in_non_regicide_games" , true , offsetof (struct c3x_config, dont_give_king_names_in_non_regicide_games)}, + {"no_elvis_easter_egg" , false, offsetof (struct c3x_config, no_elvis_easter_egg)}, + {"disable_worker_automation" , false, offsetof (struct c3x_config, disable_worker_automation)}, + {"enable_land_sea_intersections" , false, offsetof (struct c3x_config, enable_land_sea_intersections)}, + {"disallow_trespassing" , false, offsetof (struct c3x_config, disallow_trespassing)}, + {"show_detailed_tile_info" , true , offsetof (struct c3x_config, show_detailed_tile_info)}, + {"warn_about_unrecognized_names" , true , offsetof (struct c3x_config, warn_about_unrecognized_names)}, + {"enable_ai_production_ranking" , true , offsetof (struct c3x_config, enable_ai_production_ranking)}, + {"enable_ai_city_location_desirability_display" , true, offsetof (struct c3x_config, enable_ai_city_location_desirability_display)}, + {"show_ai_city_location_desirability_if_settler" , false, offsetof (struct c3x_config, show_ai_city_location_desirability_if_settler)}, + {"auto_zoom_city_screen_for_large_work_areas" , true, offsetof (struct c3x_config, auto_zoom_city_screen_for_large_work_areas)}, + {"zero_corruption_when_off" , true , offsetof (struct c3x_config, zero_corruption_when_off)}, + {"disallow_land_units_from_affecting_water_tiles" , true , offsetof (struct c3x_config, disallow_land_units_from_affecting_water_tiles)}, + {"dont_end_units_turn_after_airdrop" , false, offsetof (struct c3x_config, dont_end_units_turn_after_airdrop)}, + {"allow_airdrop_without_airport" , false, offsetof (struct c3x_config, allow_airdrop_without_airport)}, + {"enable_negative_pop_pollution" , true , offsetof (struct c3x_config, enable_negative_pop_pollution)}, + {"allow_defensive_retreat_on_water" , false, offsetof (struct c3x_config, allow_defensive_retreat_on_water)}, + {"promote_wonder_decorruption_effect" , false, offsetof (struct c3x_config, promote_wonder_decorruption_effect)}, + {"allow_military_leaders_to_hurry_wonders" , false, offsetof (struct c3x_config, allow_military_leaders_to_hurry_wonders)}, + {"aggressively_penalize_bankruptcy" , false, offsetof (struct c3x_config, aggressively_penalize_bankruptcy)}, + {"no_penalty_exception_for_agri_fresh_water_city_tiles" , false, offsetof (struct c3x_config, no_penalty_exception_for_agri_fresh_water_city_tiles)}, + {"use_offensive_artillery_ai" , true , offsetof (struct c3x_config, use_offensive_artillery_ai)}, + {"dont_escort_unflagged_units" , false, offsetof (struct c3x_config, dont_escort_unflagged_units)}, + {"replace_leader_unit_ai" , true , offsetof (struct c3x_config, replace_leader_unit_ai)}, + {"fix_ai_army_composition" , true , offsetof (struct c3x_config, fix_ai_army_composition)}, + {"enable_pop_unit_ai" , true , offsetof (struct c3x_config, enable_pop_unit_ai)}, + {"enable_caravan_unit_ai" , true , offsetof (struct c3x_config, enable_caravan_unit_ai)}, + {"remove_unit_limit" , true , offsetof (struct c3x_config, remove_unit_limit)}, + {"remove_city_improvement_limit" , true , offsetof (struct c3x_config, remove_city_improvement_limit)}, + {"remove_cap_on_turn_limit" , true , offsetof (struct c3x_config, remove_cap_on_turn_limit)}, + {"remove_era_limit" , false, offsetof (struct c3x_config, remove_era_limit)}, + {"patch_submarine_bug" , true , offsetof (struct c3x_config, patch_submarine_bug)}, + {"patch_science_age_bug" , true , offsetof (struct c3x_config, patch_science_age_bug)}, + {"patch_pedia_texture_bug" , true , offsetof (struct c3x_config, patch_pedia_texture_bug)}, + {"patch_blocked_disembark_freeze" , true , offsetof (struct c3x_config, patch_blocked_disembark_freeze)}, + {"patch_houseboat_bug" , true , offsetof (struct c3x_config, patch_houseboat_bug)}, + {"patch_intercept_lost_turn_bug" , true , offsetof (struct c3x_config, patch_intercept_lost_turn_bug)}, + {"patch_phantom_resource_bug" , true , offsetof (struct c3x_config, patch_phantom_resource_bug)}, + {"patch_maintenance_persisting_for_obsolete_buildings" , true , offsetof (struct c3x_config, patch_maintenance_persisting_for_obsolete_buildings)}, + {"patch_barbarian_diagonal_bug" , true , offsetof (struct c3x_config, patch_barbarian_diagonal_bug)}, + {"patch_disease_stopping_tech_flag_bug" , false, offsetof (struct c3x_config, patch_disease_stopping_tech_flag_bug)}, + {"patch_division_by_zero_in_ai_alliance_eval" , true , offsetof (struct c3x_config, patch_division_by_zero_in_ai_alliance_eval)}, + {"patch_empty_army_movement" , true , offsetof (struct c3x_config, patch_empty_army_movement)}, + {"patch_empty_army_combat_crash" , true , offsetof (struct c3x_config, patch_empty_army_combat_crash)}, + {"patch_premature_truncation_of_found_paths" , true , offsetof (struct c3x_config, patch_premature_truncation_of_found_paths)}, + {"patch_zero_production_crash" , true , offsetof (struct c3x_config, patch_zero_production_crash)}, + {"patch_ai_can_form_army_without_special_ability" , true , offsetof (struct c3x_config, patch_ai_can_form_army_without_special_ability)}, + {"patch_ai_can_sacrifice_without_special_ability" , true , offsetof (struct c3x_config, patch_ai_can_sacrifice_without_special_ability)}, + {"patch_crash_in_leader_unit_ai" , true , offsetof (struct c3x_config, patch_crash_in_leader_unit_ai)}, + {"patch_failure_to_find_new_city_build" , true , offsetof (struct c3x_config, patch_failure_to_find_new_city_build)}, + {"delete_off_map_ai_units" , true , offsetof (struct c3x_config, delete_off_map_ai_units)}, + {"fix_overlapping_specialist_yield_icons" , true , offsetof (struct c3x_config, fix_overlapping_specialist_yield_icons)}, + {"prevent_autorazing" , false, offsetof (struct c3x_config, prevent_autorazing)}, + {"prevent_razing_by_players" , false, offsetof (struct c3x_config, prevent_razing_by_players)}, + {"suppress_hypertext_links_exceeded_popup" , true , offsetof (struct c3x_config, suppress_hypertext_links_exceeded_popup)}, + {"indicate_non_upgradability_in_pedia" , true , offsetof (struct c3x_config, indicate_non_upgradability_in_pedia)}, + {"show_message_after_dodging_sam" , true , offsetof (struct c3x_config, show_message_after_dodging_sam)}, + {"include_stealth_attack_cancel_option" , false, offsetof (struct c3x_config, include_stealth_attack_cancel_option)}, + {"intercept_recon_missions" , false, offsetof (struct c3x_config, intercept_recon_missions)}, + {"charge_one_move_for_recon_and_interception" , false, offsetof (struct c3x_config, charge_one_move_for_recon_and_interception)}, + {"polish_precision_striking" , true , offsetof (struct c3x_config, polish_precision_striking)}, + {"enable_stealth_attack_via_bombardment" , false, offsetof (struct c3x_config, enable_stealth_attack_via_bombardment)}, + {"immunize_aircraft_against_bombardment" , false, offsetof (struct c3x_config, immunize_aircraft_against_bombardment)}, + {"replay_ai_moves_in_hotseat_games" , false, offsetof (struct c3x_config, replay_ai_moves_in_hotseat_games)}, + {"restore_unit_directions_on_game_load" , true , offsetof (struct c3x_config, restore_unit_directions_on_game_load)}, + {"apply_grid_ini_setting_on_game_load" , true , offsetof (struct c3x_config, apply_grid_ini_setting_on_game_load)}, + {"charm_flag_triggers_ptw_like_targeting" , false, offsetof (struct c3x_config, charm_flag_triggers_ptw_like_targeting)}, + {"city_icons_show_unit_effects_not_trade" , true , offsetof (struct c3x_config, city_icons_show_unit_effects_not_trade)}, + {"ignore_king_ability_for_defense_priority" , false, offsetof (struct c3x_config, ignore_king_ability_for_defense_priority)}, + {"show_untradable_techs_on_trade_screen" , false, offsetof (struct c3x_config, show_untradable_techs_on_trade_screen)}, + {"disallow_useless_bombard_vs_airfields" , true , offsetof (struct c3x_config, disallow_useless_bombard_vs_airfields)}, + {"compact_luxury_display_on_city_screen" , false, offsetof (struct c3x_config, compact_luxury_display_on_city_screen)}, + {"compact_strategic_resource_display_on_city_screen" , false, offsetof (struct c3x_config, compact_strategic_resource_display_on_city_screen)}, + {"warn_when_chosen_building_would_replace_another" , false, offsetof (struct c3x_config, warn_when_chosen_building_would_replace_another)}, + {"do_not_unassign_workers_from_polluted_tiles" , false, offsetof (struct c3x_config, do_not_unassign_workers_from_polluted_tiles)}, + {"do_not_make_capital_cities_appear_larger" , false, offsetof (struct c3x_config, do_not_make_capital_cities_appear_larger)}, + {"show_territory_colors_on_water_tiles_in_minimap" , false, offsetof (struct c3x_config, show_territory_colors_on_water_tiles_in_minimap)}, + {"convert_some_popups_into_online_mp_messages" , false, offsetof (struct c3x_config, convert_some_popups_into_online_mp_messages)}, + {"enable_debug_mode_switch" , false, offsetof (struct c3x_config, enable_debug_mode_switch)}, + {"accentuate_cities_on_minimap" , false, offsetof (struct c3x_config, accentuate_cities_on_minimap)}, + {"allow_multipage_civilopedia_descriptions" , true , offsetof (struct c3x_config, allow_multipage_civilopedia_descriptions)}, + {"enable_trade_net_x" , true , offsetof (struct c3x_config, enable_trade_net_x)}, + {"optimize_improvement_loops" , true , offsetof (struct c3x_config, optimize_improvement_loops)}, + {"measure_turn_times" , false, offsetof (struct c3x_config, measure_turn_times)}, + {"enable_city_capture_by_barbarians" , false, offsetof (struct c3x_config, enable_city_capture_by_barbarians)}, + {"share_visibility_in_hotseat" , false, offsetof (struct c3x_config, share_visibility_in_hotseat)}, + {"share_wonders_in_hotseat" , false, offsetof (struct c3x_config, share_wonders_in_hotseat)}, + {"allow_precision_strikes_against_tile_improvements" , false, offsetof (struct c3x_config, allow_precision_strikes_against_tile_improvements)}, + {"dont_end_units_turn_after_bombarding_barricade" , false, offsetof (struct c3x_config, dont_end_units_turn_after_bombarding_barricade)}, + {"remove_land_artillery_target_restrictions" , false, offsetof (struct c3x_config, remove_land_artillery_target_restrictions)}, + {"allow_bombard_of_other_improvs_on_occupied_airfield" , false, offsetof (struct c3x_config, allow_bombard_of_other_improvs_on_occupied_airfield)}, + {"show_total_city_count" , false, offsetof (struct c3x_config, show_total_city_count)}, + {"strengthen_forbidden_palace_ocn_effect" , false, offsetof (struct c3x_config, strengthen_forbidden_palace_ocn_effect)}, + {"allow_upgrades_in_any_city" , false, offsetof (struct c3x_config, allow_upgrades_in_any_city)}, + {"do_not_generate_volcanos" , false, offsetof (struct c3x_config, do_not_generate_volcanos)}, + {"do_not_pollute_impassable_tiles" , false, offsetof (struct c3x_config, do_not_pollute_impassable_tiles)}, + {"show_hp_of_stealth_attack_options" , false, offsetof (struct c3x_config, show_hp_of_stealth_attack_options)}, + {"exclude_invisible_units_from_stealth_attack" , false, offsetof (struct c3x_config, exclude_invisible_units_from_stealth_attack)}, + {"convert_to_landmark_after_planting_forest" , false, offsetof (struct c3x_config, convert_to_landmark_after_planting_forest)}, + {"allow_sale_of_aqueducts_and_hospitals" , false, offsetof (struct c3x_config, allow_sale_of_aqueducts_and_hospitals)}, + {"no_cross_shore_detection" , false, offsetof (struct c3x_config, no_cross_shore_detection)}, + {"auto_zoom_city_screen_for_large_work_areas" , true, offsetof (struct c3x_config, auto_zoom_city_screen_for_large_work_areas)}, + {"limit_unit_loading_to_one_transport_per_turn" , false, offsetof (struct c3x_config, limit_unit_loading_to_one_transport_per_turn)}, + {"prevent_old_units_from_upgrading_past_ability_block" , false, offsetof (struct c3x_config, prevent_old_units_from_upgrading_past_ability_block)}, + {"allow_extraterritorial_colonies" , false, offsetof (struct c3x_config, allow_extraterritorial_colonies)}, + {"draw_forests_over_roads_and_railroads" , false, offsetof (struct c3x_config, draw_forests_over_roads_and_railroads)}, + {"enable_districts" , false, offsetof (struct c3x_config, enable_districts)}, + {"enable_neighborhood_districts" , false, offsetof (struct c3x_config, enable_neighborhood_districts)}, + {"enable_wonder_districts" , false, offsetof (struct c3x_config, enable_wonder_districts)}, + {"enable_natural_wonders" , false, offsetof (struct c3x_config, enable_natural_wonders)}, + {"add_natural_wonders_to_scenarios_if_none" , false, offsetof (struct c3x_config, add_natural_wonders_to_scenarios_if_none)}, + {"enable_named_tiles" , false, offsetof (struct c3x_config, enable_named_tiles)}, + {"enable_distribution_hub_districts" , false, offsetof (struct c3x_config, enable_distribution_hub_districts)}, + {"enable_aerodrome_districts" , false, offsetof (struct c3x_config, enable_aerodrome_districts)}, + {"enable_port_districts" , false, offsetof (struct c3x_config, enable_port_districts)}, + {"enable_bridge_districts" , false, offsetof (struct c3x_config, enable_bridge_districts)}, + {"enable_canal_districts" , false, offsetof (struct c3x_config, enable_canal_districts)}, + {"enable_central_rail_hub_districts" , false, offsetof (struct c3x_config, enable_central_rail_hub_districts)}, + {"enable_energy_grid_districts" , false, offsetof (struct c3x_config, enable_energy_grid_districts)}, + {"enable_great_wall_districts" , false, offsetof (struct c3x_config, enable_great_wall_districts)}, + {"completed_wonder_districts_can_be_destroyed" , false, offsetof (struct c3x_config, completed_wonder_districts_can_be_destroyed)}, + {"destroyed_wonders_can_be_built_again" , false, offsetof (struct c3x_config, destroyed_wonders_can_be_built_again)}, + {"cities_with_mutual_district_receive_buildings" , false, offsetof (struct c3x_config, cities_with_mutual_district_receive_buildings)}, + {"cities_with_mutual_district_receive_wonders" , false, offsetof (struct c3x_config, cities_with_mutual_district_receive_wonders)}, + {"show_message_when_building_received_by_mutual_district", false, offsetof (struct c3x_config, show_message_when_building_received_by_mutual_district)}, + {"show_message_when_building_lost_to_destroyed_district" , false, offsetof (struct c3x_config, show_message_when_building_lost_to_destroyed_district)}, + {"destroying_neighborhood_reduces_pop" , false, offsetof (struct c3x_config, destroying_neighborhood_reduces_pop)}, + {"air_units_use_aerodrome_districts_not_cities" , false, offsetof (struct c3x_config, air_units_use_aerodrome_districts_not_cities)}, + {"naval_units_use_port_districts_not_cities" , false, offsetof (struct c3x_config, naval_units_use_port_districts_not_cities)}, + {"show_natural_wonder_name_on_map" , false, offsetof (struct c3x_config, show_natural_wonder_name_on_map)}, + {"ai_defends_districts" , false, offsetof (struct c3x_config, ai_defends_districts)}, + {"great_wall_districts_impassible_by_others" , false, offsetof (struct c3x_config, great_wall_districts_impassible_by_others)}, + {"auto_build_great_wall_around_territory" , false, offsetof (struct c3x_config, auto_build_great_wall_around_territory)}, + {"disable_great_wall_city_defense_bonus" , false, offsetof (struct c3x_config, disable_great_wall_city_defense_bonus)}, + {"expand_water_tile_checks_to_city_work_area" , false, offsetof (struct c3x_config, expand_water_tile_checks_to_city_work_area)}, + {"ai_can_replace_existing_districts_with_canals" , false, offsetof (struct c3x_config, ai_can_replace_existing_districts_with_canals)}, + {"ai_builds_bridges" , false, offsetof (struct c3x_config, ai_builds_bridges)}, + {"ai_builds_canals" , false, offsetof (struct c3x_config, ai_builds_canals)}, + {"workers_can_enter_coast" , false, offsetof (struct c3x_config, workers_can_enter_coast)}, + {"enable_city_work_radii_highlights" , false, offsetof (struct c3x_config, enable_city_work_radii_highlights)}, + {"introduce_all_human_players_at_start_of_hotseat_game" , false, offsetof (struct c3x_config, introduce_all_human_players_at_start_of_hotseat_game)}, + {"allow_unload_from_army" , false, offsetof (struct c3x_config, allow_unload_from_army)}, + {"no_land_anti_air_from_inside_naval_transport" , false, offsetof (struct c3x_config, no_land_anti_air_from_inside_naval_transport)}, + {"prevent_enslaving_by_bombardment" , false, offsetof (struct c3x_config, prevent_enslaving_by_bombardment)}, + {"allow_adjacent_resources_of_different_types" , false, offsetof (struct c3x_config, allow_adjacent_resources_of_different_types)}, + {"allow_sale_of_small_wonders" , false, offsetof (struct c3x_config, allow_sale_of_small_wonders)}, + }; + + struct integer_config_option { + char * name; + int base_val; + int offset; + } integer_config_options[] = { + {"limit_railroad_movement" , 0, offsetof (struct c3x_config, limit_railroad_movement)}, + {"minimum_city_separation" , 1, offsetof (struct c3x_config, minimum_city_separation)}, + {"anarchy_length_percent" , 100, offsetof (struct c3x_config, anarchy_length_percent)}, + {"ai_multi_city_start" , 0, offsetof (struct c3x_config, ai_multi_city_start)}, + {"max_tries_to_place_fp_city" , 10000, offsetof (struct c3x_config, max_tries_to_place_fp_city)}, + {"ai_research_multiplier" , 100, offsetof (struct c3x_config, ai_research_multiplier)}, + {"ai_settler_perfume_on_founding_duration" , 0, offsetof (struct c3x_config, ai_settler_perfume_on_founding_duration)}, + {"extra_unit_maintenance_per_shields" , 0, offsetof (struct c3x_config, extra_unit_maintenance_per_shields)}, + {"ai_build_artillery_ratio" , 16, offsetof (struct c3x_config, ai_build_artillery_ratio)}, + {"ai_artillery_value_damage_percent" , 50, offsetof (struct c3x_config, ai_artillery_value_damage_percent)}, + {"ai_build_bomber_ratio" , 70, offsetof (struct c3x_config, ai_build_bomber_ratio)}, + {"max_ai_naval_escorts" , 3, offsetof (struct c3x_config, max_ai_naval_escorts)}, + {"ai_worker_requirement_percent" , 150, offsetof (struct c3x_config, ai_worker_requirement_percent)}, + {"chance_for_nukes_to_destroy_max_one_hp_units" , 100, offsetof (struct c3x_config, chance_for_nukes_to_destroy_max_one_hp_units)}, + {"rebase_range_multiplier" , 6, offsetof (struct c3x_config, rebase_range_multiplier)}, + {"elapsed_minutes_per_day_night_hour_transition" , 3, offsetof (struct c3x_config, elapsed_minutes_per_day_night_hour_transition)}, + {"fixed_hours_per_turn_for_day_night_cycle" , 1, offsetof (struct c3x_config, fixed_hours_per_turn_for_day_night_cycle)}, + {"pinned_hour_for_day_night_cycle" , 0, offsetof (struct c3x_config, pinned_hour_for_day_night_cycle)}, + {"years_to_double_building_culture" , 1000, offsetof (struct c3x_config, years_to_double_building_culture)}, + {"tourism_time_scale_percent" , 100, offsetof (struct c3x_config, tourism_time_scale_percent)}, + {"luxury_randomized_appearance_rate_percent" , 100, offsetof (struct c3x_config, luxury_randomized_appearance_rate_percent)}, + {"tiles_per_non_luxury_resource" , 32, offsetof (struct c3x_config, tiles_per_non_luxury_resource)}, + {"city_limit" , 2048, offsetof (struct c3x_config, city_limit)}, + {"maximum_pop_before_neighborhood_needed" , 8, offsetof (struct c3x_config, maximum_pop_before_neighborhood_needed)}, + {"per_neighborhood_pop_growth_enabled" , 2, offsetof (struct c3x_config, per_neighborhood_pop_growth_enabled)}, + {"minimum_natural_wonder_separation" , 10, offsetof (struct c3x_config, minimum_natural_wonder_separation)}, + {"distribution_hub_food_yield_divisor" , 1, offsetof (struct c3x_config, distribution_hub_food_yield_divisor)}, + {"distribution_hub_shield_yield_divisor" , 1, offsetof (struct c3x_config, distribution_hub_shield_yield_divisor)}, + {"ai_ideal_distribution_hub_count_per_100_cities" , 1, offsetof (struct c3x_config, ai_ideal_distribution_hub_count_per_100_cities)}, + {"max_distribution_hub_count_per_100_cities" , 50, offsetof (struct c3x_config, max_distribution_hub_count_per_100_cities)}, + {"central_rail_hub_distribution_food_bonus_percent" , 25, offsetof (struct c3x_config, central_rail_hub_distribution_food_bonus_percent)}, + {"central_rail_hub_distribution_shield_bonus_percent", 25, offsetof (struct c3x_config, central_rail_hub_distribution_shield_bonus_percent)}, + {"neighborhood_needed_message_frequency" , 4, offsetof (struct c3x_config, neighborhood_needed_message_frequency)}, + {"max_contiguous_bridge_districts" , 3, offsetof (struct c3x_config, max_contiguous_bridge_districts)}, + {"max_contiguous_canal_districts" , 5, offsetof (struct c3x_config, max_contiguous_canal_districts)}, + {"ai_canal_eval_min_bisected_land_tiles" , 10, offsetof (struct c3x_config, ai_canal_eval_min_bisected_land_tiles)}, + {"ai_bridge_canal_eval_block_size" , 20, offsetof (struct c3x_config, ai_bridge_canal_eval_block_size)}, + {"ai_bridge_eval_lake_tile_threshold" , 6, offsetof (struct c3x_config, ai_bridge_eval_lake_tile_threshold)}, + {"ai_city_district_max_build_wait_turns" , 20, offsetof (struct c3x_config, ai_city_district_max_build_wait_turns)}, + {"per_extraterritorial_colony_relation_penalty" , 0, offsetof (struct c3x_config, per_extraterritorial_colony_relation_penalty)}, + }; + + is->kernel32 = (*p_GetModuleHandleA) ("kernel32.dll"); + is->user32 = (*p_GetModuleHandleA) ("user32.dll"); + is->msvcrt = (*p_GetModuleHandleA) ("msvcrt.dll"); + + // Remember the function names here are macros that expand to is->... + VirtualProtect = (void *)(*p_GetProcAddress) (is->kernel32, "VirtualProtect"); + CloseHandle = (void *)(*p_GetProcAddress) (is->kernel32, "CloseHandle"); + CreateFileA = (void *)(*p_GetProcAddress) (is->kernel32, "CreateFileA"); + GetFileSize = (void *)(*p_GetProcAddress) (is->kernel32, "GetFileSize"); + ReadFile = (void *)(*p_GetProcAddress) (is->kernel32, "ReadFile"); + LoadLibraryA = (void *)(*p_GetProcAddress) (is->kernel32, "LoadLibraryA"); + FreeLibrary = (void *)(*p_GetProcAddress) (is->kernel32, "FreeLibrary"); + MultiByteToWideChar = (void *)(*p_GetProcAddress) (is->kernel32, "MultiByteToWideChar"); + WideCharToMultiByte = (void *)(*p_GetProcAddress) (is->kernel32, "WideCharToMultiByte"); + GetLastError = (void *)(*p_GetProcAddress) (is->kernel32, "GetLastError"); + QueryPerformanceCounter = (void *)(*p_GetProcAddress) (is->kernel32, "QueryPerformanceCounter"); + QueryPerformanceFrequency = (void *)(*p_GetProcAddress) (is->kernel32, "QueryPerformanceFrequency"); + GetLocalTime = (void *)(*p_GetProcAddress) (is->kernel32, "GetLocalTime"); + MessageBoxA = (void *)(*p_GetProcAddress) (is->user32, "MessageBoxA"); + is->msimg32 = LoadLibraryA ("Msimg32.dll"); + TransparentBlt = (void *)(*p_GetProcAddress) (is->msimg32, "TransparentBlt"); + snprintf = (void *)(*p_GetProcAddress) (is->msvcrt, "_snprintf"); + malloc = (void *)(*p_GetProcAddress) (is->msvcrt, "malloc"); + calloc = (void *)(*p_GetProcAddress) (is->msvcrt, "calloc"); + realloc = (void *)(*p_GetProcAddress) (is->msvcrt, "realloc"); + free = (void *)(*p_GetProcAddress) (is->msvcrt, "free"); + strtol = (void *)(*p_GetProcAddress) (is->msvcrt, "strtol"); + strcmp = (void *)(*p_GetProcAddress) (is->msvcrt, "strcmp"); + strncmp = (void *)(*p_GetProcAddress) (is->msvcrt, "strncmp"); + strlen = (void *)(*p_GetProcAddress) (is->msvcrt, "strlen"); + strncpy = (void *)(*p_GetProcAddress) (is->msvcrt, "strncpy"); + strcpy = (void *)(*p_GetProcAddress) (is->msvcrt, "strcpy"); + strdup = (void *)(*p_GetProcAddress) (is->msvcrt, "_strdup"); + strstr = (void *)(*p_GetProcAddress) (is->msvcrt, "strstr"); + qsort = (void *)(*p_GetProcAddress) (is->msvcrt, "qsort"); + memcmp = (void *)(*p_GetProcAddress) (is->msvcrt, "memcmp"); + memcpy = (void *)(*p_GetProcAddress) (is->msvcrt, "memcpy"); + tolower = (void *)(*p_GetProcAddress) (is->msvcrt, "tolower"); + + // Intercept the game's calls to MessageBoxA. We can't do this through the patcher since that would interfere with the runtime loader. + WITH_MEM_PROTECTION (p_MessageBoxA, 4, PAGE_READWRITE) + *p_MessageBoxA = patch_MessageBoxA; + + // Set file path to mod's script.txt + snprintf (is->mod_script_path, sizeof is->mod_script_path, "%s\\Text\\c3x-script.txt", is->mod_rel_dir); + is->mod_script_path[(sizeof is->mod_script_path) - 1] = '\0'; + + // Fill in base config + struct c3x_config base_config = {0}; + base_config.land_retreat_rules = RR_STANDARD; + base_config.sea_retreat_rules = RR_STANDARD; + base_config.ai_settler_perfume_on_founding = 0; + base_config.work_area_limit = WAL_NONE; + base_config.draw_lines_using_gdi_plus = LDO_WINE; + base_config.double_minimap_size = MDM_HIGH_DEF; + base_config.unit_cycle_search_criteria = UCSC_STANDARD; + base_config.city_work_radius = 2; + base_config.day_night_cycle_mode = DNCM_OFF; + base_config.distribution_hub_yield_division_mode = DHYDM_FLAT; + base_config.ai_distribution_hub_build_strategy = ADHBS_BY_CITY_COUNT; + base_config.ai_auto_build_great_wall_strategy = AAGWS_ALL_BORDERS; + base_config.great_wall_auto_build_wonder_improv_id = -1; + for (int n = 0; n < ARRAY_LEN (boolean_config_options); n++) + *((char *)&base_config + boolean_config_options[n].offset) = boolean_config_options[n].base_val; + for (int n = 0; n < ARRAY_LEN (integer_config_options); n++) + *(int *)((byte *)&base_config + integer_config_options[n].offset) = integer_config_options[n].base_val; + memcpy (&is->base_config, &base_config, sizeof base_config); + + // Load labels + { + for (int n = 0; n < COUNT_C3X_LABELS; n++) + is->c3x_labels[n] = ""; + + char labels_path[MAX_PATH]; + snprintf (labels_path, sizeof labels_path, "%s\\Text\\c3x-labels.txt", is->mod_rel_dir); + labels_path[(sizeof labels_path) - 1] = '\0'; + char * labels_file_contents = file_to_string (labels_path); + + if (labels_file_contents != NULL) { + char * cursor = labels_file_contents; + int n = 0; + while ((n < COUNT_C3X_LABELS) && (*cursor != '\0')) { + if (*cursor == '\n') + cursor++; + else if ((cursor[0] == '\r') && (cursor[1] == '\n')) + cursor += 2; + else if (*cursor == ';') { + while ((*cursor != '\0') && (*cursor != '\n')) + cursor++; + } else { + char * line_start = cursor; + while ((*cursor != '\0') && (*cursor != '\r') && (*cursor != '\n')) + cursor++; + int line_len = cursor - line_start; + if (NULL != (is->c3x_labels[n] = malloc (line_len + 1))) { + strncpy (is->c3x_labels[n], line_start, line_len); + is->c3x_labels[n][line_len] = '\0'; + } + n++; + } + } + free (labels_file_contents); + + } else { + char err_msg[500]; + snprintf (err_msg, sizeof err_msg, "Couldn't read labels from %s\nPlease make sure the file exists. If you moved the modded EXE you must move the mod folder after it.", labels_path); + err_msg[(sizeof err_msg) - 1] = '\0'; + MessageBoxA (NULL, err_msg, NULL, MB_ICONWARNING); + } + } + + is->sb_next_up = NULL; + is->trade_net = p_original_trade_net; + is->city_limit = 512; + is->trade_net_addrs_load_state = IS_UNINITED; + is->trade_net_addrs = NULL; + is->tnx_cache = NULL; + is->is_computing_city_connections = false; + is->keep_tnx_cache = false; + is->must_recompute_resources_for_mill_inputs = false; + is->is_placing_scenario_things = false; + is->paused_for_popup = false; + is->time_spent_paused_during_popup = 0; + is->time_spent_computing_city_connections = 0; + is->count_calls_to_recompute_city_connections = 0; + + is->have_job_and_loc_to_skip = 0; + + // Initialize trade screen scroll vars + is->open_diplo_form_straight_to_trade = 0; + is->trade_screen_scroll_to_id = -1; + is->trade_scroll_button_left = is->trade_scroll_button_right = NULL; + is->trade_scroll_button_images = NULL; + is->trade_scroll_button_state = IS_UNINITED; + is->eligible_for_trade_scroll = 0; + + memset (&is->saved_code_areas, 0, sizeof is->saved_code_areas); + + is->unit_menu_duplicates = NULL; + + is->memo = NULL; + is->memo_len = 0; + is->memo_capacity = 0; + + // Fill in array mapping cultural NIs to standard ones. + is->cultural_ni_to_standard = malloc (MAX_CULTURAL_NI + 1); + for (int n = 0; n <= MAX_CULTURAL_NI; n++) { + char const * p = &cultural_ni_to_diffs[n << 1]; + is->cultural_ni_to_standard[n] = diff_to_neighbor_index (p[0], p[1], 1000); + } + + // Fill in array mapping standard NIs to work radii AKA work ring numbers + for (int n = 0; n < ARRAY_LEN (is->ni_to_work_radius); n++) { + int work_radius = -1; + int dx, dy; + neighbor_index_to_diff (n, &dx, &dy); + for (int ring_no = 0; ring_no <= 7; ring_no++) { + for (int k = workable_tile_counts[ring_no-1]; k < workable_tile_counts[ring_no]; k++) { + char const * p = &cultural_ni_to_diffs[k<<1]; + if ((p[0] == dx) && (p[1] == dy)) { + work_radius = ring_no; + break; + } + } + if (work_radius != -1) + break; + } + is->ni_to_work_radius[n] = work_radius; + } + + is->city_loc_display_perspective = -1; + + is->aliased_civ_noun_bits = is->aliased_civ_adjective_bits = is->aliased_civ_formal_name_bits = is->aliased_leader_name_bits = is->aliased_leader_title_bits = 0; + + is->extra_available_resources = NULL; + is->extra_available_resources_capacity = 0; + + memset (is->interceptor_reset_lists, 0, sizeof is->interceptor_reset_lists); + + is->ai_prod_valuations = NULL; + is->count_ai_prod_valuations = 0; + is->ai_prod_valuations_capacity = 0; + + is->resource_tiles = NULL; + is->count_resource_tiles = 0; + is->resource_tiles_capacity = 0; + is->got_resource_tile = NULL; + is->saved_tile_count = -1; + is->mill_input_resource_bits = NULL; + + is->drawing_icons_for_improv_id = -1; + + is->resources_sheet = NULL; + + is->modifying_gold_trade = NULL; + + is->bombard_stealth_target = NULL; + is->selecting_stealth_target_for_bombard = 0; + + is->map_message_text_override = NULL; + + is->load_file_path_override = NULL; + + is->replay_for_players = 0; + + is->suppress_intro_after_load_popup = 0; + + is->force_barb_activity_for_cities = 0; + + is->dummy_tile = calloc (1, sizeof *is->dummy_tile); + + is->bombarding_unit = NULL; + + is->unit_bombard_attacking_tile = NULL; + is->attacking_tile_x = is->attacking_tile_y = -1; + + is->temporarily_disallow_lethal_zoc = false; + is->moving_unit_to_adjacent_tile = false; + is->showing_hotseat_replay = false; + is->getting_tile_occupier_for_ai_pathfinding = false; + + is->running_on_wine = false; { + HMODULE ntdll = (*p_GetModuleHandleA) ("ntdll.dll"); + is->running_on_wine = (ntdll != NULL) && ((*p_GetProcAddress) (ntdll, "wine_get_version") != NULL); + } + + is->gdi_plus.init_state = IS_UNINITED; + + is->water_trade_improvs = (struct improv_id_list) {0}; + is->air_trade_improvs = (struct improv_id_list) {0}; + is->combat_defense_improvs = (struct improv_id_list) {0}; + + is->unit_display_override = (struct unit_display_override) {-1, -1, -1}; + + is->dbe = (struct defensive_bombard_event) {0}; + + memset (&is->boolean_config_offsets, 0, sizeof is->boolean_config_offsets); + for (int n = 0; n < ARRAY_LEN (boolean_config_options); n++) + stable_insert (&is->boolean_config_offsets, boolean_config_options[n].name, boolean_config_options[n].offset); + memset (&is->integer_config_offsets, 0, sizeof is->integer_config_offsets); + for (int n = 0; n < ARRAY_LEN (integer_config_options); n++) + stable_insert (&is->integer_config_offsets, integer_config_options[n].name, integer_config_options[n].offset); + + memset (&is->unit_type_alt_strategies, 0, sizeof is->unit_type_alt_strategies); + memset (&is->unit_type_duplicates , 0, sizeof is->unit_type_duplicates); + memset (&is->extra_defensive_bombards, 0, sizeof is->extra_defensive_bombards); + memset (&is->airdrops_this_turn , 0, sizeof is->airdrops_this_turn); + memset (&is->unit_transport_ties , 0, sizeof is->unit_transport_ties); + memset (&is->extra_city_improvs , 0, sizeof is->extra_city_improvs); + + is->unit_type_count_init_bits = 0; + for (int n = 0; n < 32; n++) + memset (&is->unit_type_counts[n], 0, sizeof is->unit_type_counts[n]); + + is->penciled_in_upgrades = NULL; + is->penciled_in_upgrade_count = is->penciled_in_upgrade_capacity = 0; + + is->currently_capturing_city = NULL; + is->accessing_save_file = NULL; + + is->drawn_strat_resource_count = 0; + + is->charmed_types_converted_to_ptw_arty = NULL; + is->count_charmed_types_converted_to_ptw_arty = 0; + is->charmed_types_converted_to_ptw_arty_capacity = 0; + + is->checking_visibility_for_unit = NULL; + + is->do_not_bounce_invisible_units = false; + is->always_despawn_passengers = false; + is->do_not_enslave_units = false; + + is->saved_improv_counts = NULL; + is->saved_improv_counts_capacity = 0; + + memset (is->last_main_screen_key_up_events, 0, sizeof is->last_main_screen_key_up_events); + + reset_district_state (true); + + is->natural_wonder_count = 0; + + is->sharing_buildings_by_districts_in_progress = false; + is->can_load_transport = is->can_load_passenger = NULL; + + is->last_selected_unit.initial_x = is->last_selected_unit.initial_y = -1; + is->last_selected_unit.last_x = is->last_selected_unit.last_y = is->last_selected_unit.type_id = -1; + is->last_selected_unit.ptr = NULL; + + is->waiting_units = (struct table) {0}; + is->have_loaded_waiting_units = false; + + is->extra_capture_despawns = NULL; + is->count_extra_capture_despawns = 0; + is->extra_capture_despawns_capacity = 0; + + is->loaded_config_names = NULL; + reset_to_base_config (); + apply_machine_code_edits (&is->current_config, true); +} + +void +init_stackable_command_buttons () +{ + if (is->sc_img_state != IS_UNINITED) + return; + + PCX_Image pcx; + PCX_Image_construct (&pcx); + for (int sc = 0; sc < COUNT_STACKABLE_COMMANDS; sc++) + for (int n = 0; n < 4; n++) + Sprite_construct (&is->sc_button_image_sets[sc].imgs[n]); + + char temp_path[2*MAX_PATH]; + + is->sb_activated_by_button = 0; + is->sc_img_state = IS_INIT_FAILED; + + char const * filenames[4] = {"StackedNormButtons.pcx", "StackedRolloverButtons.pcx", "StackedHighlightedButtons.pcx", "StackedButtonsAlpha.pcx"}; + for (int n = 0; n < 4; n++) { + get_mod_art_path (filenames[n], temp_path, sizeof temp_path); + PCX_Image_read_file (&pcx, __, temp_path, NULL, 0, 0x100, 2); + if (pcx.JGL.Image == NULL) { + (*p_OutputDebugStringA) ("[C3X] Failed to load stacked command buttons sprite sheet.\n"); + for (int sc = 0; sc < COUNT_STACKABLE_COMMANDS; sc++) + for (int k = 0; k < 4; k++) { + Sprite * sprite = &is->sc_button_image_sets[sc].imgs[k]; + sprite->vtable->destruct (sprite, __, 0); + } + pcx.vtable->destruct (&pcx, __, 0); + return; + } + + for (int sc = 0; sc < COUNT_STACKABLE_COMMANDS; sc++) { + int x = 32 * sc_button_infos[sc].tile_sheet_column, + y = 32 * sc_button_infos[sc].tile_sheet_row; + Sprite_slice_pcx (&is->sc_button_image_sets[sc].imgs[n], __, &pcx, x, y, 32, 32, 1, 0); + } + + pcx.vtable->clear_JGL (&pcx); + } + + is->sc_img_state = IS_OK; + pcx.vtable->destruct (&pcx, __, 0); +} + +void +init_disabled_command_buttons () +{ + if (is->disabled_command_img_state != IS_UNINITED) + return; + + is->disabled_command_img_state = IS_INIT_FAILED; + + PCX_Image pcx; + PCX_Image_construct (&pcx); + + char temp_path[2*MAX_PATH]; + get_mod_art_path ("DisabledButtons.pcx", temp_path, sizeof temp_path); + PCX_Image_read_file (&pcx, __, temp_path, NULL, 0, 0x100, 2); + if (pcx.JGL.Image == NULL) { + (*p_OutputDebugStringA) ("[C3X] Failed to load disabled command buttons sprite sheet.\n"); + pcx.vtable->destruct (&pcx, __, 0); + return; + } + + Sprite_construct (&is->disabled_build_city_button_img); + Sprite_slice_pcx (&is->disabled_build_city_button_img, __, &pcx, 32*5, 32*2, 32, 32, 1, 0); + + is->disabled_command_img_state = IS_OK; + pcx.vtable->destruct (&pcx, __, 0); +} + +void +deinit_stackable_command_buttons () +{ + if (is->sc_img_state == IS_OK) + for (int sc = 0; sc < COUNT_STACKABLE_COMMANDS; sc++) + for (int n = 0; n < 4; n++) { + Sprite * sprite = &is->sc_button_image_sets[sc].imgs[n]; + sprite->vtable->destruct (sprite, __, 0); + } + is->sc_img_state = IS_UNINITED; +} + +void +deinit_disabled_command_buttons () +{ + Sprite * sprite = &is->disabled_build_city_button_img; + if (is->disabled_command_img_state == IS_OK) + sprite->vtable->destruct (sprite, __, 0); + memset (sprite, 0, sizeof *sprite); + is->disabled_command_img_state = IS_UNINITED; +} + +void +init_tile_highlights () +{ + if (is->tile_highlight_state != IS_UNINITED) + return; + + PCX_Image pcx; + PCX_Image_construct (&pcx); + + char temp_path[2*MAX_PATH]; + + is->tile_highlight_state = IS_INIT_FAILED; + + snprintf (temp_path, sizeof temp_path, "%s\\Art\\TileHighlights.pcx", is->mod_rel_dir); + temp_path[(sizeof temp_path) - 1] = '\0'; + PCX_Image_read_file (&pcx, __, temp_path, NULL, 0, 0x100, 2); + if (pcx.JGL.Image == NULL) { + (*p_OutputDebugStringA) ("[C3X] Failed to load stacked command buttons sprite sheet.\n"); + goto cleanup; + } + + for (int n = 0; n < COUNT_TILE_HIGHLIGHTS; n++) + Sprite_slice_pcx (&is->tile_highlights[n], __, &pcx, 128*n, 0, 128, 64, 1, 0); + + is->tile_highlight_state = IS_OK; +cleanup: + pcx.vtable->destruct (&pcx, __, 0); +} + +void +init_unit_rcm_icons () +{ + if (is->unit_rcm_icon_state != IS_UNINITED) + return; + + PCX_Image pcx; + PCX_Image_construct (&pcx); + + char temp_path[2*MAX_PATH]; + get_mod_art_path ("UnitRCMIcons.pcx", temp_path, sizeof temp_path); + + PCX_Image_read_file (&pcx, __, temp_path, NULL, 0, 0x100, 2); + if ((pcx.JGL.Image == NULL) || + (pcx.JGL.Image->vtable->m54_Get_Width (pcx.JGL.Image) < 57) || + (pcx.JGL.Image->vtable->m55_Get_Height (pcx.JGL.Image) < 64)) { + (*p_OutputDebugStringA) ("[C3X] PCX file for unit RCM icons failed to load or is too small.\n"); + goto cleanup; + } + + for (int set = 0; set < COUNT_UNIT_RCM_ICON_SETS; set++) { + Sprite * icons = &is->unit_rcm_icons[set * COUNT_UNIT_RCM_ICONS]; + for (int n = 0; n < COUNT_UNIT_RCM_ICONS; n++) { + Sprite_construct (&icons[n]); + Sprite_slice_pcx (&icons[n], __, &pcx, 1 + 14*set, 1 + 16*n, 14, 15, 1, 0); + } + } + + is->unit_rcm_icon_state = IS_OK; +cleanup: + pcx.vtable->destruct (&pcx, __, 0); +} + +void +deinit_unit_rcm_icons () +{ + if (is->unit_rcm_icon_state == IS_OK) { + int total_icon_count = COUNT_UNIT_RCM_ICONS * COUNT_UNIT_RCM_ICON_SETS; + for (int n = 0; n < total_icon_count; n++) { + Sprite * sprite = &is->unit_rcm_icons[n]; + sprite->vtable->destruct (sprite, __, 0); + } + is->unit_rcm_icon_state = IS_UNINITED; + } +} + +void +init_red_food_icon () +{ + if (is->red_food_icon_state != IS_UNINITED) + return; + + PCX_Image pcx; + PCX_Image_construct (&pcx); + + char temp_path[2*MAX_PATH]; + get_mod_art_path ("MoreCityIcons.pcx", temp_path, sizeof temp_path); + + PCX_Image_read_file (&pcx, __, temp_path, NULL, 0, 0x100, 2); + if ((pcx.JGL.Image != NULL) && + (pcx.JGL.Image->vtable->m54_Get_Width (pcx.JGL.Image) >= 32) && + (pcx.JGL.Image->vtable->m55_Get_Height (pcx.JGL.Image) == 32)) { + Sprite_construct (&is->red_food_icon); + Sprite_slice_pcx (&is->red_food_icon, __, &pcx, 1, 1, 30, 30, 1, 1); + is->red_food_icon_state = IS_OK; + } else { + (*p_OutputDebugStringA) ("[C3X] PCX file for red food icon failed to load or is not the correct size.\n"); + is->red_food_icon_state = IS_INIT_FAILED; + } + + pcx.vtable->destruct (&pcx, __, 0); +} + +void +deinit_red_food_icon () +{ + if (is->red_food_icon_state == IS_OK) + is->red_food_icon.vtable->destruct (&is->red_food_icon, __, 0); + is->red_food_icon_state = IS_UNINITED; +} + +enum init_state +init_large_minimap_frame () +{ + if (is->large_minimap_frame_img_state != IS_UNINITED) + return is->large_minimap_frame_img_state; + + PCX_Image pcx; + PCX_Image_construct (&pcx); + + char temp_path[2*MAX_PATH]; + + get_mod_art_path ("interface\\DoubleSizeBoxLeftColor.pcx", temp_path, sizeof temp_path); + PCX_Image_read_file (&pcx, __, temp_path, NULL, 0, 0x100, 2); + if (pcx.JGL.Image != NULL) { + Sprite_construct (&is->double_size_box_left_color_pcx); + int width = pcx.JGL.Image->vtable->m54_Get_Width (pcx.JGL.Image), + height = pcx.JGL.Image->vtable->m55_Get_Height (pcx.JGL.Image); + Sprite_slice_pcx (&is->double_size_box_left_color_pcx, __, &pcx, 0, 0, width, height, 1, 1); + } else { + pcx.vtable->destruct (&pcx, __, 0); + return is->large_minimap_frame_img_state = IS_INIT_FAILED; + } + + get_mod_art_path ("interface\\DoubleSizeBoxLeftAlpha.pcx", temp_path, sizeof temp_path); + PCX_Image_read_file (&pcx, __, temp_path, NULL, 0, 0x100, 2); + if (pcx.JGL.Image != NULL) { + Sprite_construct (&is->double_size_box_left_alpha_pcx); + int width = pcx.JGL.Image->vtable->m54_Get_Width (pcx.JGL.Image), + height = pcx.JGL.Image->vtable->m55_Get_Height (pcx.JGL.Image); + Sprite_slice_pcx (&is->double_size_box_left_alpha_pcx, __, &pcx, 0, 0, width, height, 1, 1); + } else { + is->double_size_box_left_color_pcx.vtable->destruct (&is->double_size_box_left_color_pcx, __, 0); + pcx.vtable->destruct (&pcx, __, 0); + return is->large_minimap_frame_img_state = IS_INIT_FAILED;; + } + + pcx.vtable->destruct (&pcx, __, 0); + return is->large_minimap_frame_img_state = IS_OK; +} + +void +deinit_large_minimap_frame () +{ + if (is->large_minimap_frame_img_state == IS_OK) { + is->double_size_box_left_color_pcx.vtable->destruct (&is->double_size_box_left_color_pcx, __, 0); + is->double_size_box_left_alpha_pcx.vtable->destruct (&is->double_size_box_left_alpha_pcx, __, 0); + } + is->large_minimap_frame_img_state = IS_UNINITED; +} + +int __cdecl +patch_get_tile_occupier_for_ai_path (int x, int y, int pov_civ_id, bool respect_unit_invisibility) +{ + is->getting_tile_occupier_for_ai_pathfinding = true; + return get_tile_occupier_id (x, y, pov_civ_id, respect_unit_invisibility); + is->getting_tile_occupier_for_ai_pathfinding = false; +} + +char __fastcall patch_Unit_is_visible_to_civ (Unit * this, int edx, int civ_id, int param_2); + +char __fastcall +patch_Unit_is_tile_occupier_visible (Unit * this, int edx, int civ_id, int param_2) +{ + // Here's the fix for the submarine bug: If we're constructing a path for an AI unit, ignore unit invisibility so the pathfinder will path + // around instead of over other civ's units. We must carve out an exception if the AI is at war with the unit in question. In that case if it + // "accidentally" paths over the unit, it should get stuck in combat like the human player would. + if (is->current_config.patch_submarine_bug && + is->getting_tile_occupier_for_ai_pathfinding && + ! this->vtable->is_enemy_of_civ (this, __, civ_id, 0)) + return 1; + else + return patch_Unit_is_visible_to_civ (this, __, civ_id, param_2); +} + +void do_trade_scroll (DiploForm * diplo, int forward); + +void __cdecl +activate_trade_scroll_button (int control_id) +{ + do_trade_scroll (p_diplo_form, control_id == TRADE_SCROLL_BUTTON_ID_RIGHT); +} + +void +init_trade_scroll_buttons (DiploForm * diplo_form) +{ + if (is->trade_scroll_button_state != IS_UNINITED) + return; + + char temp_path[2*MAX_PATH]; + PCX_Image pcx; + PCX_Image_construct (&pcx); + get_mod_art_path ("TradeScrollButtons.pcx", temp_path, sizeof temp_path); + PCX_Image_read_file (&pcx, __, temp_path, NULL, 0, 0x100, 2); + if (pcx.JGL.Image == NULL) { + is->trade_scroll_button_state = IS_INIT_FAILED; + (*p_OutputDebugStringA) ("[C3X] Failed to load TradeScrollButtons.pcx\n"); + goto cleanup; + } + + // Stores normal, rollover, and highlight images, in that order, first for the left button then for the right + is->trade_scroll_button_images = calloc (6, sizeof is->trade_scroll_button_images[0]); + for (int n = 0; n < 6; n++) + Sprite_construct (&is->trade_scroll_button_images[n]); + Sprite * imgs = is->trade_scroll_button_images; + + for (int right = 0; right < 2; right++) + for (int n = 0; n < 3; n++) + Sprite_slice_pcx (&imgs[n + 3*right], __, &pcx, right ? 44 : 0, 1 + 48*n, 43, 47, 1, 1); + + for (int right = 0; right < 2; right++) { + Button * b = new (sizeof *b); + + Button_construct (b); + int id = right ? TRADE_SCROLL_BUTTON_ID_RIGHT : TRADE_SCROLL_BUTTON_ID_LEFT; + Button_initialize (b, __, NULL, id, right ? 622 : 358, 50, 43, 47, (Base_Form *)diplo_form, 0); + for (int n = 0; n < 3; n++) + b->Images[n] = &imgs[n + 3*right]; + + b->activation_handler = &activate_trade_scroll_button; + b->field_630[0] = 0; // TODO: Is this necessary? It's done by the base game code when creating the city screen scroll buttons + + if (! right) + is->trade_scroll_button_left = b; + else + is->trade_scroll_button_right = b; + } + + is->trade_scroll_button_state = IS_OK; +cleanup: + pcx.vtable->destruct (&pcx, __, 0); +} + +void +deinit_trade_scroll_buttons () +{ + if (is->trade_scroll_button_state == IS_OK) { + is->trade_scroll_button_left ->vtable->destruct ((Base_Form *)is->trade_scroll_button_left , __, 0); + is->trade_scroll_button_right->vtable->destruct ((Base_Form *)is->trade_scroll_button_right, __, 0); + is->trade_scroll_button_left = is->trade_scroll_button_right = NULL; + for (int n = 0; n < 6; n++) { + Sprite * sprite = &is->trade_scroll_button_images[n]; + sprite->vtable->destruct (sprite, __, 0); + } + free (is->trade_scroll_button_images); + is->trade_scroll_button_images = NULL; + } + is->trade_scroll_button_state = IS_UNINITED; +} + +void +init_mod_info_button_images () +{ + if (is->mod_info_button_images_state != IS_UNINITED) + return; + + is->mod_info_button_images_state = IS_INIT_FAILED; + + PCX_Image * descbox_pcx = new (sizeof *descbox_pcx); + PCX_Image_construct (descbox_pcx); + char * descbox_path = BIC_get_asset_path (p_bic_data, __, "art\\civilopedia\\descbox.pcx", true); + PCX_Image_read_file (descbox_pcx, __, descbox_path, NULL, 0, 0x100, 1); + if (descbox_pcx->JGL.Image == NULL) { + (*p_OutputDebugStringA) ("[C3X] Failed to load descbox.pcx\n"); + return; + } + + for (int n = 0; n < 3; n++) { + Sprite_construct (&is->mod_info_button_images[n]); + Sprite_slice_pcx_with_color_table (&is->mod_info_button_images[n], __, descbox_pcx, 1 + n * 103, 1, MOD_INFO_BUTTON_WIDTH, + MOD_INFO_BUTTON_HEIGHT, 1, 1); + } + + is->mod_info_button_images_state = IS_OK; +} + +int +count_escorters (Unit * unit) +{ + IDLS * idls = &unit->Body.IDLS; + if (idls->escorters.contents != NULL) { + int tr = 0; + for (int * p_escorter_id = idls->escorters.contents; p_escorter_id < idls->escorters.contents_end; p_escorter_id++) + tr += NULL != get_unit_ptr (*p_escorter_id); + return tr; + } else + return 0; +} + +// If "unit" belongs to the AI, records that all players that have visibility on (x, y) have seen an AI unit +void +record_ai_unit_seen (Unit * unit, int x, int y) +{ + if (0 == (*p_human_player_bits & 1<Body.CivID)) { + Tile * tile = tile_at (x, y); + if ((tile != NULL) && (tile != p_null_tile)) { + Tile_Body * body = &tile->Body; + is->players_saw_ai_unit |= body->FOWStatus | body->V3 | body->Visibility | body->field_D0_Visibility; + } + } +} + +void +recompute_resources_if_necessary () +{ + if (is->must_recompute_resources_for_mill_inputs) + patch_Trade_Net_recompute_resources (is->trade_net, __, false); +} + +void __fastcall +patch_Unit_bombard_tile (Unit * this, int edx, int x, int y) +{ + Tile * target_tile = NULL; + bool had_district_before = false; + int tile_x = x; + int tile_y = y; + struct district_instance * inst; + + if (is->current_config.enable_districts) { + wrap_tile_coords (&p_bic_data->Map, &tile_x, &tile_y); + target_tile = tile_at (tile_x, tile_y); + if ((target_tile != NULL) && (target_tile != p_null_tile)) { + inst = get_district_instance (target_tile); + had_district_before = (inst != NULL); + } + } + + is->bombarding_unit = this; + record_ai_unit_seen (this, x, y); + Unit_bombard_tile (this, __, x, y); + is->bombard_stealth_target = NULL; + is->bombarding_unit = NULL; + + if (had_district_before && target_tile != NULL && target_tile != p_null_tile && inst->district_id != NATURAL_WONDER_DISTRICT_ID) { + unsigned int overlays = target_tile->vtable->m42_Get_Overlays (target_tile, __, 0); + if ((overlays & TILE_FLAG_MINE) == 0) + handle_district_destroyed_by_attack (target_tile, tile_x, tile_y, false); + } +} + +void __fastcall +patch_Unit_move (Unit * this, int edx, int tile_x, int tile_y) +{ + record_ai_unit_seen (this, tile_x, tile_y); + + Unit_move (this, __, tile_x, tile_y); + + if (this == is->last_selected_unit.ptr) { + is->last_selected_unit.last_x = this->Body.X; + is->last_selected_unit.last_y = this->Body.Y; + } +} + +// Returns true if the unit has attacked & does not have blitz or if it's run out of movement points for the turn +bool +has_exhausted_attack (Unit * unit) +{ + return (unit->Body.Moves >= patch_Unit_get_max_move_points (unit)) || + ((unit->Body.Status & USF_USED_ATTACK) && ! UnitType_has_ability (&p_bic_data->UnitTypes[unit->Body.UnitTypeID], __, UTA_Blitz)); +} + +// Returns whether or not the unit type is a combat type and can do damage on offense (as opposed to only being able to defend). This includes units +// that can only do damage by bombarding and nuclear weapons. +bool +is_offensive_combat_type (UnitType * unit_type) +{ + return (unit_type->Attack > 0) || + ((((unit_type->Special_Actions & UCV_Bombard) | (unit_type->Air_Missions & UCV_Bombing)) & 0x0FFFFFFF) && // (type can perform bombard or bombing AND + ((unit_type->Bombard_Strength > 0) || UnitType_has_ability (unit_type, __, UTA_Nuclear_Weapon))); // (unit has bombard strength OR is a nuclear weapon)) +} + +bool +can_damage_bombarding (UnitType * attacker_type, Unit * defender, Tile * defender_tile) +{ + Unit * container; + if ((is->current_config.special_helicopter_rules & SHR_NO_DEFENSE_FROM_INSIDE) && + (container = get_unit_ptr (defender->Body.Container_Unit)) != NULL && + p_bic_data->UnitTypes[container->Body.UnitTypeID].Unit_Class == UTC_Air) + return false; + + UnitType * defender_type = &p_bic_data->UnitTypes[defender->Body.UnitTypeID]; + if (defender_type->Unit_Class == UTC_Land) { + int has_lethal_land_bombard = UnitType_has_ability (attacker_type, __, UTA_Lethal_Land_Bombardment); + return defender->Body.Damage + (! has_lethal_land_bombard) < Unit_get_max_hp (defender); + } else if (defender_type->Unit_Class == UTC_Sea) { + // Land artillery can't normally damage ships in port + if ((attacker_type->Unit_Class == UTC_Land) && (! is->current_config.remove_land_artillery_target_restrictions) && Tile_has_city (defender_tile)) + return false; + int has_lethal_sea_bombard = UnitType_has_ability (attacker_type, __, UTA_Lethal_Sea_Bombardment); + return defender->Body.Damage + (! has_lethal_sea_bombard) < Unit_get_max_hp (defender); + } else if (defender_type->Unit_Class == UTC_Air) { + if (is->current_config.immunize_aircraft_against_bombardment) + return false; + // Can't damage aircraft in an airfield by bombarding, the attack doesn't even go off + if ((defender_tile->vtable->m42_Get_Overlays (defender_tile, __, 0) & 0x20000000) != 0) + return false; + // Land artillery can't normally damage aircraft but naval artillery and other aircraft can. Lethal bombard doesn't apply; anything + // that can damage can kill. + return (attacker_type->Unit_Class != UTC_Land) || is->current_config.remove_land_artillery_target_restrictions; + } else // UTC_Space? UTC_Alternate_Dimension??? + return false; +} + +char __fastcall +patch_Unit_is_visible_to_civ (Unit * this, int edx, int civ_id, int param_2) +{ + // Save the previous value here b/c this function gets called recursively + Unit * prev_checking = is->checking_visibility_for_unit; + is->checking_visibility_for_unit = this; + + char base_vis = Unit_is_visible_to_civ (this, __, civ_id, param_2); + if ((! base_vis) && // if unit is not visible to civ_id AND + is->current_config.share_visibility_in_hotseat && // shared hotseat vis is enabled AND + ((1 << civ_id) & *p_human_player_bits) && // civ_id is a human player AND + (*p_is_offline_mp_game && ! *p_is_pbem_game)) { // we're in a hotseat game + + // Check if the unit is visible to any other human player in the game + unsigned player_bits = *(unsigned *)p_human_player_bits >> 1; + int n_player = 1; + while (player_bits != 0) { + if ((player_bits & 1) && + (n_player != civ_id) && + Unit_is_visible_to_civ (this, __, n_player, param_2)) + return 1; + player_bits >>= 1; + n_player++; + } + } + + is->checking_visibility_for_unit = prev_checking; + return base_vis; +} + +bool +has_any_destructible_improvements (City * city) +{ + for (int n = 0; n < p_bic_data->ImprovementsCount; n++) { + Improvement * improv = &p_bic_data->Improvements[n]; + if (((improv->Characteristics & (ITC_Wonder | ITC_Small_Wonder)) == 0) && // if improv is not a wonder AND + ((improv->ImprovementFlags & ITF_Center_of_Empire) == 0) && // it's not the palace AND + (improv->SpaceshipPart < 0) && // it's not a spaceship part AND + patch_City_has_improvement (city, __, n, 0)) // it's present in the city ignoring free improvements + return true; + } + return false; +} + +int const destructible_overlays = + 0x00000003 | // road, railroad + 0x00000004 | // mine + 0x00000008 | // irrigation + 0x00000010 | // fortress + 0x10000000 | // barricade + 0x20000000 | // airfield + 0x40000000 | // radar + 0x80000000; // outpost + +bool +has_any_destructible_overlays (Tile * tile, bool precision_strike) +{ + int overlays = tile->vtable->m42_Get_Overlays (tile, __, 0); + if ((overlays & destructible_overlays) == 0) + return false; + else { + if (! precision_strike) + return true; + else { + if (overlays == 0x20000000) { // if tile ONLY has an airfield + int any_aircraft_on_tile = 0; { + FOR_UNITS_ON (uti, tile) + if (p_bic_data->UnitTypes[uti.unit->Body.UnitTypeID].Unit_Class == UTC_Air) { + any_aircraft_on_tile = 1; + break; + } + } + return ! any_aircraft_on_tile; + } else + return true; + } + } +} + +void __fastcall +patch_Main_Screen_Form_perform_action_on_tile (Main_Screen_Form * this, int edx, enum Unit_Mode_Actions action, int x, int y) +{ + if ((! is->current_config.enable_stack_bombard) || // if stack bombard is disabled OR + (! ((action == UMA_Bombard) || (action == UMA_Air_Bombard))) || // action is not bombard OR + ((((*p_GetAsyncKeyState) (VK_CONTROL)) >> 8 == 0) && // (control key is not down AND + ((is->sc_img_state != IS_UNINITED) && (is->sb_activated_by_button == 0))) || // (button flag is valid AND not set)) OR + is_online_game ()) { // is online game + Main_Screen_Form_perform_action_on_tile (this, __, action, x, y); + return; + } + + // Save preferences so we can restore them at the end of the stack bombard operation. We might change them to turn off combat animations. + unsigned init_prefs = *p_preferences; + + clear_memo (); + + wrap_tile_coords (&p_bic_data->Map, &x, &y); + Tile * base_tile = tile_at (this->Current_Unit->Body.X, this->Current_Unit->Body.Y); + Tile * target_tile = tile_at (x, y); + int attacker_type_id = this->Current_Unit->Body.UnitTypeID; + UnitType * attacker_type = &p_bic_data->UnitTypes[attacker_type_id]; + int civ_id = this->Current_Unit->Body.CivID; + + // Count & memoize attackers + int selected_unit_id = this->Current_Unit->Body.ID; + FOR_UNITS_ON (uti, base_tile) + if ((uti.id != selected_unit_id) && + (uti.unit->Body.CivID == civ_id) && + (uti.unit->Body.UnitTypeID == attacker_type_id) && + ((uti.unit->Body.Container_Unit < 0) || (attacker_type->Unit_Class == UTC_Air)) && + (uti.unit->Body.UnitState == 0) && + ! has_exhausted_attack (uti.unit)) + memoize (uti.id); + int count_attackers = is->memo_len; + + // Count & memoize targets (also count air units while we're at it) + int num_air_units_on_target_tile = 0; + FOR_UNITS_ON (uti, target_tile) { + num_air_units_on_target_tile += UTC_Air == p_bic_data->UnitTypes[uti.unit->Body.UnitTypeID].Unit_Class; + if ((uti.unit->Body.CivID != civ_id) && + (Unit_get_defense_strength (uti.unit) > 0) && + (uti.unit->Body.Container_Unit < 0) && + patch_Unit_is_visible_to_civ (uti.unit, __, civ_id, 0) && + can_damage_bombarding (attacker_type, uti.unit, target_tile)) + memoize (uti.id); + } + int count_targets = is->memo_len - count_attackers; + + // Now our attackers and targets arrays will just be pointers into the memo + int * attackers = &is->memo[0], * targets = &is->memo[count_attackers]; + + int attacking_units = 0, attacking_tile = 0; + City * target_city = NULL; + if (count_targets > 0) + attacking_units = 1; + else if (Tile_has_city (target_tile)) + target_city = get_city_ptr (target_tile->vtable->m45_Get_City_ID (target_tile)); + else { + // Make sure not to set attacking_tile when the tile has an airfield and air units (but no land units) on + // it. In that case we can't damage the airfield and if we try to anyway, the units will waste their moves + // without attacking. + int has_airfield = (target_tile->vtable->m42_Get_Overlays (target_tile, __, 0) & 0x20000000) != 0; + attacking_tile = (! has_airfield) || (num_air_units_on_target_tile == 0); + } + + is->sb_next_up = this->Current_Unit; + int i_next_attacker = 0; + int anything_left_to_attack; + int last_attack_didnt_happen; + do { + // If combat animations were enabled when the stack bombard operation started, reset the preference according to the state of the + // shift key (down => skip animations, up => show them). + if (init_prefs & P_ANIMATE_BATTLES) { + if ((*p_GetAsyncKeyState) (VK_SHIFT) >> 8) + *p_preferences &= ~P_ANIMATE_BATTLES; + else + *p_preferences |= P_ANIMATE_BATTLES; + } + + int moves_before_bombard = is->sb_next_up->Body.Moves; + + patch_Unit_bombard_tile (is->sb_next_up, __, x, y); + // At this point sb_next_up may have become NULL if the unit was a bomber that got shot down. + + // Check if the last unit sent into battle actually did anything. If it didn't we should at least skip over + // it to avoid an infinite loop, but actually the only time this should happen is if the player is not at + // war with the targeted civ and chose not to declare when prompted. In this case it's better to just stop + // trying to attack so as to not spam the player with prompts. + last_attack_didnt_happen = (is->sb_next_up == NULL) || (is->sb_next_up->Body.Moves == moves_before_bombard); + + if ((is->sb_next_up == NULL) || has_exhausted_attack (is->sb_next_up)) { + is->sb_next_up = NULL; + while ((i_next_attacker < count_attackers) && (is->sb_next_up == NULL)) + is->sb_next_up = get_unit_ptr (attackers[i_next_attacker++]); + } + + if (attacking_units) { + anything_left_to_attack = 0; + for (int n = 0; n < count_targets; n++) { + Unit * unit = get_unit_ptr (targets[n]); + + // Make sure this unit is still a valid target. Keep in mind it's possible for new units to be created during the + // stack bombard operation if the attackers have lethal bombard and the enslave ability. + if ((unit != NULL) && (unit->Body.X == x) && (unit->Body.Y == y) && (unit->Body.CivID != civ_id)) { + + if (can_damage_bombarding (attacker_type, unit, target_tile)) { + anything_left_to_attack = 1; + break; + } + } + } + } else if (target_city != NULL) + anything_left_to_attack = (target_city->Body.Population.Size > 1) || has_any_destructible_improvements (target_city); + else if (attacking_tile) + anything_left_to_attack = has_any_destructible_overlays (target_tile, false); + else + anything_left_to_attack = 0; + } while ((is->sb_next_up != NULL) && anything_left_to_attack && (! last_attack_didnt_happen)); + + is->sb_activated_by_button = 0; + is->sb_next_up = NULL; + *p_preferences = init_prefs; + this->GUI.Base.vtable->m73_call_m22_Draw ((Base_Form *)&this->GUI); +} + +void +set_up_stack_bombard_buttons (Main_GUI * this) +{ + if (is_online_game () || (! is->current_config.enable_stack_bombard)) + return; + + init_stackable_command_buttons (); + if (is->sc_img_state != IS_OK) + return; + + // Find button that the original method set to (air) bombard, then find the next unused button after that. + Command_Button * bombard_button = NULL, * free_button = NULL; { + int i_bombard_button; + for (int n = 0; n < 42; n++) + if (((this->Unit_Command_Buttons[n].Button.Base_Data.Status2 & 1) != 0) && + (this->Unit_Command_Buttons[n].Command == UCV_Bombard || this->Unit_Command_Buttons[n].Command == UCV_Bombing)) { + bombard_button = &this->Unit_Command_Buttons[n]; + i_bombard_button = n; + break; + } + if (bombard_button != NULL) + for (int n = i_bombard_button + 1; n < 42; n++) + if ((this->Unit_Command_Buttons[n].Button.Base_Data.Status2 & 1) == 0) { + free_button = &this->Unit_Command_Buttons[n]; + break; + } + } + + if ((bombard_button == NULL) || (free_button == NULL)) + return; + + // Set up free button for stack bombard + free_button->Command = bombard_button->Command; + free_button->field_6D8 = bombard_button->field_6D8; + struct sc_button_image_set * img_set = + (bombard_button->Command == UCV_Bombing) ? &is->sc_button_image_sets[SC_BOMB] : &is->sc_button_image_sets[SC_BOMBARD]; + for (int n = 0; n < 4; n++) + free_button->Button.Images[n] = &img_set->imgs[n]; + free_button->Button.field_664 = bombard_button->Button.field_664; + // FUN_005559E0 is also called in the original code. I don't know what it actually does but I'm pretty sure it doesn't + // matter for our purposes. + Button_set_tooltip (&free_button->Button, __, is->c3x_labels[CL_SB_TOOLTIP]); + free_button->Button.field_5FC[13] = bombard_button->Button.field_5FC[13]; + free_button->Button.vtable->m01_Show_Enabled ((Base_Form *)&free_button->Button, __, 0); +} + +void +init_district_command_buttons () +{ + if (is_online_game () || is->dc_btn_img_state != IS_UNINITED) + return; + + PCX_Image pcx; + PCX_Image_construct (&pcx); + for (int dc = 0; dc < COUNT_DISTRICT_TYPES; dc++) + for (int n = 0; n < 4; n++) + Sprite_construct (&is->district_btn_img_sets[dc].imgs[n]); + + char temp_path[2*MAX_PATH]; + + is->dc_btn_img_state = IS_INIT_FAILED; + + // For each button sprite type (normal, rollover, highlighted, alpha) + char const * filenames[4] = { + "Districts\\WorkerDistrictButtonsNorm.pcx", "Districts\\WorkerDistrictButtonsRollover.pcx", + "Districts\\WorkerDistrictButtonsHighlighted.pcx", "Districts\\WorkerDistrictButtonsAlpha.pcx" + }; + for (int n = 0; n < 4; n++) { + get_mod_art_path (filenames[n], temp_path, sizeof temp_path); + PCX_Image_read_file (&pcx, __, temp_path, NULL, 0, 0x100, 2); + if (pcx.JGL.Image == NULL) { + (*p_OutputDebugStringA) ("[C3X] Failed to load work district command buttons sprite sheet.\n"); + for (int dc = 0; dc < COUNT_DISTRICT_TYPES; dc++) + for (int k = 0; k < 4; k++) { + Sprite * sprite = &is->district_btn_img_sets[dc].imgs[k]; + sprite->vtable->destruct (sprite, __, 0); + } + pcx.vtable->destruct (&pcx, __, 0); + + char ss[200]; + snprintf (ss, sizeof ss, "[C3X] Failed to load district command button images from %s", temp_path); + pop_up_in_game_error (ss); + + return; + } + + // For each district type + for (int dc = 0; dc < is->district_count; dc++) { + int x = 32 * is->district_configs[dc].btn_tile_sheet_column, + y = 32 * is->district_configs[dc].btn_tile_sheet_row; + Sprite_slice_pcx (&is->district_btn_img_sets[dc].imgs[n], __, &pcx, x, y, 32, 32, 1, 0); + } + + pcx.vtable->clear_JGL (&pcx); + } + + is->dc_btn_img_state = IS_OK; + pcx.vtable->destruct (&pcx, __, 0); +} + +int +parse_turns_from_tooltip (char const * tooltip) +{ + if ((tooltip == NULL) || (*tooltip == '\0')) + return -1; + + char const * last_paren = NULL; + for (char const * cursor = tooltip; *cursor != '\0'; cursor++) + if (*cursor == '(') + last_paren = cursor; + if (last_paren == NULL) + return -1; + + char const * digit_ptr = last_paren + 1; + while (*digit_ptr == ' ') + digit_ptr++; + + int turns = 0; + bool have_digit = false; + while ((*digit_ptr >= '0') && (*digit_ptr <= '9')) { + have_digit = true; + turns = (turns * 10) + (*digit_ptr - '0'); + digit_ptr++; + } + return have_digit ? turns : -1; +} + +void +compute_highlighted_worker_tiles_for_districts () +{ + if (is_online_game () + || ! is->current_config.enable_districts + || ! is->current_config.enable_city_work_radii_highlights) + return; + + Unit * selected_unit = p_main_screen_form->Current_Unit; + if (selected_unit == NULL) + return; + + int unit_type_id = selected_unit->Body.UnitTypeID; + if (p_bic_data->UnitTypes[unit_type_id].Worker_Actions == 0) + return; + + if (is->tile_highlight_state == IS_UNINITED) + init_tile_highlights (); + if (is->tile_highlight_state != IS_OK) + return; + + int worker_civ_id = selected_unit->Body.CivID; + if ((p_cities == NULL) || (p_cities->Cities == NULL)) + return; + + // Loop over all cities owned by this civ and tally their workable tiles + FOR_CITIES_OF (coi, worker_civ_id) { + City * city = coi.city; + if (city == NULL) + continue; + + // Highlight city center so players can easily see which cities contribute + Tile * city_center_tile = tile_at (city->Body.X, city->Body.Y); + if ((city_center_tile != NULL) && (city_center_tile != p_null_tile)) { + int stored_ptr; + if (! itable_look_up (&is->highlighted_city_radius_tile_pointers, (int)city_center_tile, &stored_ptr)) { + struct highlighted_city_radius_tile_info * info = malloc (sizeof (struct highlighted_city_radius_tile_info)); + info->highlight_level = 0; + itable_insert (&is->highlighted_city_radius_tile_pointers, (int)city_center_tile, (int)info); + } + } + + // Add all workable tiles around the city (excluding city center) + for (int n = 1; n < is->workable_tile_count; n++) { + int dx, dy; + patch_ni_to_diff_for_work_area (n, &dx, &dy); + int tile_x = city->Body.X + dx; + int tile_y = city->Body.Y + dy; + wrap_tile_coords (&p_bic_data->Map, &tile_x, &tile_y); + + Tile * workable_tile = tile_at (tile_x, tile_y); + if ((workable_tile == NULL) || (workable_tile == p_null_tile)) continue; + if (workable_tile->vtable->m38_Get_Territory_OwnerID (workable_tile) != worker_civ_id) continue; + + // Upsert into highlighted_city_radius_tile_pointers + int stored_ptr; + struct highlighted_city_radius_tile_info * info; + if (! itable_look_up (&is->highlighted_city_radius_tile_pointers, (int)workable_tile, &stored_ptr)) { + info = malloc (sizeof (struct highlighted_city_radius_tile_info)); + info->highlight_level = 0; + itable_insert (&is->highlighted_city_radius_tile_pointers, (int)workable_tile, (int)info); + } else { + info = (struct highlighted_city_radius_tile_info *)stored_ptr; + info->highlight_level += 3; + } + } + } +} + +void +set_up_district_buttons (Main_GUI * this) +{ + if (is_online_game () || ! is->current_config.enable_districts) return; + if (is->dc_btn_img_state == IS_UNINITED) init_district_command_buttons (); + if (is->dc_btn_img_state != IS_OK) return; + + Unit * selected_unit = p_main_screen_form->Current_Unit; + if (selected_unit == NULL || ! is_worker(selected_unit)) return; + + Tile * tile = tile_at (selected_unit->Body.X, selected_unit->Body.Y); + if ((tile == NULL) || (tile == p_null_tile) || (tile->CityID >= 0)) return; + + enum SquareTypes base_type = tile->vtable->m50_Get_Square_BaseType (tile); + if (tile->vtable->m21_Check_Crates (tile, __, 0)) return; + if (tile->vtable->m20_Check_Pollution (tile, __, 0)) return; + + Command_Button * fortify_button = NULL; + int i_starting_button; + int mine_turns = -1; + for (int n = 0; n < 42; n++) { + if (((this->Unit_Command_Buttons[n].Button.Base_Data.Status2 & 1) != 0) && + (this->Unit_Command_Buttons[n].Command == UCV_Fortify)) { + fortify_button = &this->Unit_Command_Buttons[n]; + i_starting_button = n; + } + if (this->Unit_Command_Buttons[n].Command == UCV_Build_Mine) { + mine_turns = parse_turns_from_tooltip (this->Unit_Command_Buttons[n].Button.ToolTip); + } + if (fortify_button != NULL && mine_turns >= 0) + break; + } + + if (fortify_button == NULL) + return; + + i_starting_button = -1; + + // Check if there's already a district on this tile. If so, and the unit can build mines, + // ensure the mine button is enabled so the worker can continue construction. + int existing_district_id = -1; + struct district_instance * existing_inst = get_district_instance (tile); + if (existing_inst != NULL) { + existing_district_id = existing_inst->district_id; + if (is->current_config.enable_natural_wonders && existing_inst->district_id == NATURAL_WONDER_DISTRICT_ID) { + return; + } + if (patch_Unit_can_perform_command(selected_unit, __, UCV_Build_Mine)) { + for (int n = 0; n < 42; n++) { + if (this->Unit_Command_Buttons[n].Command == UCV_Build_Mine) { + Command_Button * mine_button = &this->Unit_Command_Buttons[n]; + if (base_type == SQ_Coast) { + mine_button->Button.vtable->m02_Show_Disabled ((Base_Form *)&mine_button->Button); + break; + } + if ((mine_button->Button.Base_Data.Status2 & 1) == 0) { + mine_button->Button.field_5FC[13] = 0; + mine_button->Button.vtable->m01_Show_Enabled ((Base_Form *)&mine_button->Button, __, 0); + } + break; + } + } + } + } + + bool district_completed = district_is_complete (tile, existing_district_id); + + // First pass: collect which district types should be shown + int active_districts[COUNT_DISTRICT_TYPES]; + int active_count = 0; + + for (int dc = 0; dc < is->district_count; dc++) { + + if (is->district_configs[dc].command == -1) + continue; + if ((dc == BRIDGE_DISTRICT_ID) && ! is->current_config.enable_bridge_districts) + continue; + + if (existing_district_id == dc && district_completed) continue; + if ((existing_district_id >= 0) && (existing_district_id != dc) && (! district_completed)) continue; + + if (! can_build_district_on_tile (tile, dc, selected_unit->Body.CivID)) + continue; + + // This district should be shown + active_districts[active_count++] = dc; + } + + if (active_count == 0) + return; + + // Calculate centered starting position + // For odd counts, center perfectly; for even counts, favor left of center + int center_pos = 6; + i_starting_button = center_pos - (active_count / 2); + if (i_starting_button < 0) + i_starting_button = 0; + + // Second pass: render the buttons + for (int idx = 0; idx < active_count; idx++) { + int dc = active_districts[idx]; + + Command_Button * free_button = NULL; + for (int n = i_starting_button; n < 42; n++) { + if ((this->Unit_Command_Buttons[n].Button.Base_Data.Status2 & 1) == 0) { + free_button = &this->Unit_Command_Buttons[n]; + i_starting_button = n + 1; + break; + } + } + + if (free_button == NULL) + return; + + // Set up free button for creating district + free_button->Command = is->district_configs[dc].command; + + // Replace the button's image with the district image. Disabling & re-enabling and + // clearing field_5FC[13] are all necessary to trigger a redraw. + free_button->Button.vtable->m02_Show_Disabled ((Base_Form *)&free_button->Button); + free_button->field_6D8 = fortify_button->field_6D8; + for (int k = 0; k < 4; k++) + free_button->Button.Images[k] = &is->district_btn_img_sets[dc].imgs[k]; + free_button->Button.field_664 = fortify_button->Button.field_664; + if (mine_turns >= 0) { + char tooltip[256]; + char const * turn_word = (mine_turns == 1) ? "turn" : "turns"; + snprintf (tooltip, sizeof tooltip, "%s (%d %s)", is->district_configs[dc].tooltip, mine_turns, turn_word); + tooltip[(sizeof tooltip) - 1] = '\0'; + Button_set_tooltip (&free_button->Button, __, tooltip); + } else + Button_set_tooltip (&free_button->Button, __, (char *)is->district_configs[dc].tooltip); + free_button->Button.field_5FC[13] = 0; + free_button->Button.vtable->m01_Show_Enabled ((Base_Form *)&free_button->Button, __, 0); + } +} + +void +set_up_stack_worker_buttons (Main_GUI * this) +{ + if ((((*p_GetAsyncKeyState) (VK_CONTROL)) >> 8 == 0) || // (control key is not down OR + (! is->current_config.enable_stack_unit_commands) || // stack worker commands not enabled OR + is_online_game ()) // is online game + return; + + init_stackable_command_buttons (); + if (is->sc_img_state != IS_OK) + return; + + // For each unit command button + for (int n = 0; n < 42; n++) { + Command_Button * cb = &this->Unit_Command_Buttons[n]; + + // If it's enabled and not a bombard button (those are handled in the function above) + if (((cb->Button.Base_Data.Status2 & 1) != 0) && + (cb->Command != UCV_Bombard) && (cb->Command != UCV_Bombing)) { + + // Find the stackable worker command that this button controls, if there is one, and check that + // the button isn't already showing the stack image for that command. Note: this check is important + // b/c this function gets called repeatedly while the CTRL key is held down. + for (int sc = 0; sc < COUNT_STACKABLE_COMMANDS; sc++) + if ((cb->Command == sc_button_infos[sc].command) && + (cb->Button.Images[0] != &is->sc_button_image_sets[sc].imgs[0])) { + + // Replace the button's image with the stack image. Disabling & re-enabling and + // clearing field_5FC[13] are all necessary to trigger a redraw. + cb->Button.vtable->m02_Show_Disabled ((Base_Form *)&cb->Button); + for (int k = 0; k < 4; k++) + cb->Button.Images[k] = &is->sc_button_image_sets[sc].imgs[k]; + cb->Button.field_5FC[13] = 0; + cb->Button.vtable->m01_Show_Enabled ((Base_Form *)&cb->Button, __, 0); + + break; + } + } + } +} + +CityLocValidity __fastcall patch_Map_check_city_location (Map * this, int edx, int tile_x, int tile_y, int civ_id, bool check_for_city_on_tile); + +void __fastcall +patch_Main_GUI_set_up_unit_command_buttons (Main_GUI * this) +{ + // Recompute resources now if needed because of a trade deal involving mill inputs. In rare cases the change in deals might affect a mill that + // produces a resource that's used for a worker job. + recompute_resources_if_necessary (); + + Main_GUI_set_up_unit_command_buttons (this); + set_up_stack_bombard_buttons (this); + set_up_stack_worker_buttons (this); + + if (is->current_config.enable_districts) { + set_up_district_buttons (this); + + if (p_main_screen_form->Current_Unit != NULL) { + Unit_Body * selected_unit = &p_main_screen_form->Current_Unit->Body; + + enum UnitTypeClasses class = p_bic_data->UnitTypes[selected_unit->UnitTypeID].Unit_Class; + Tile * tile = tile_at (selected_unit->X, selected_unit->Y); + struct district_instance * existing_inst = get_district_instance (tile); + if (class == UTC_Sea && tile->vtable->m35_Check_Is_Water (tile) && existing_inst != NULL && + patch_Unit_can_perform_command (p_main_screen_form->Current_Unit, __, UCV_Pillage)) { + + // Really hate this but can't figure out a cleaner way to do it. + // If looping through command buttons, the command ID is always zero if disallowed + Command_Button * pillage_button = &this->Unit_Command_Buttons[3]; + // Special actions sprites: UCV_Pillage is index 3 (0x10000008). + Sprite * base_img = &this->Images[0x80 + 3]; + pillage_button->Command = UCV_Pillage; + pillage_button->field_6D8 = 0x10; + pillage_button->Button.vtable->m02_Show_Disabled ((Base_Form *)&pillage_button->Button); + pillage_button->Button.Images[0] = base_img - 0x3b; + pillage_button->Button.Images[1] = base_img; + pillage_button->Button.Images[2] = base_img + 0x3b; + pillage_button->Button.Images[3] = &this->Images[2]; + pillage_button->Button.field_664 = 0; + pillage_button->Button.field_5FC[13] = 0; + pillage_button->Button.vtable->m01_Show_Enabled ((Base_Form *)&pillage_button->Button, __, 0); + Button_set_tooltip (&pillage_button->Button, __, is->c3x_labels[CL_PILLAGE]); + } + } + } + + // If the minimum city separation is increased, then gray out the found city button if we're too close to another city. + if ((is->current_config.minimum_city_separation > 1) && (p_main_screen_form->Current_Unit != NULL) && (is->disabled_command_img_state == IS_OK)) { + Unit_Body * selected_unit = &p_main_screen_form->Current_Unit->Body; + + // For each unit command button + for (int n = 0; n < 42; n++) { + Command_Button * cb = &this->Unit_Command_Buttons[n]; + + // If it's enabled, set to city founding, and the current city location is too close to another city + if (((cb->Button.Base_Data.Status2 & 1) != 0) && + (cb->Command == UCV_Build_City) && + (patch_Map_check_city_location (&p_bic_data->Map, __, selected_unit->X, selected_unit->Y, selected_unit->CivID, false) == CLV_CITY_TOO_CLOSE)) { + + // Replace the button's image with the disabled image, as in set_up_stack_worker_buttons. + cb->Button.vtable->m02_Show_Disabled ((Base_Form *)&cb->Button); + for (int k = 0; k < 3; k++) + cb->Button.Images[k] = &is->disabled_build_city_button_img; + cb->Button.field_5FC[13] = 0; + + char tooltip[200]; { + memset (tooltip, 0, sizeof tooltip); + char * label = is->c3x_labels[CL_CITY_TOO_CLOSE_BUTTON_TOOLTIP], + * to_replace = "$NUM0", + * replace_location = strstr (label, to_replace); + if (replace_location != NULL) + snprintf (tooltip, sizeof tooltip, "%.*s%d%s", replace_location - label, label, is->current_config.minimum_city_separation, replace_location + strlen (to_replace)); + else + snprintf (tooltip, sizeof tooltip, "%s", label); + tooltip[(sizeof tooltip) - 1] = '\0'; + } + Button_set_tooltip (&cb->Button, __, tooltip); + + cb->Button.vtable->m01_Show_Enabled ((Base_Form *)&cb->Button, __, 0); + + } + } + } +} + +void +clear_highlighted_worker_tiles_and_redraw () +{ + clear_highlighted_worker_tiles_for_districts (); + p_main_screen_form->vtable->m73_call_m22_Draw ((Base_Form *)p_main_screen_form); +} + +void +check_happiness_at_end_of_turn () +{ + int num_unhappy_cities = 0; + City * first_unhappy_city = NULL; + FOR_CITIES_OF (coi, p_main_screen_form->Player_CivID) { + City_recompute_happiness (coi.city); + int num_happy = 0, num_unhappy = 0; + FOR_CITIZENS_IN (ci, coi.city) { + num_happy += ci.ctzn->Body.Mood == CMT_Happy; + num_unhappy += ci.ctzn->Body.Mood == CMT_Unhappy; + } + if (num_unhappy > num_happy) { + num_unhappy_cities++; + if (first_unhappy_city == NULL) + first_unhappy_city = coi.city; + } + } + + if (first_unhappy_city != NULL) { + PopupForm * popup = get_popup_form (); + set_popup_str_param (0, first_unhappy_city->Body.CityName, -1, -1); + if (num_unhappy_cities > 1) + set_popup_int_param (1, num_unhappy_cities - 1); + char * key = (num_unhappy_cities > 1) ? "C3X_DISORDER_WARNING_MULTIPLE" : "C3X_DISORDER_WARNING_ONE"; + popup->vtable->set_text_key_and_flags (popup, __, is->mod_script_path, key, -1, 0, 0, 0); + int response = patch_show_popup (popup, __, 0, 0); + + if (response == 2) { // zoom to city + p_main_screen_form->turn_end_flag = 1; + City_zoom_to (first_unhappy_city, __, 0); + } else if (response == 1) // just cancel turn end + p_main_screen_form->turn_end_flag = 1; + // else do nothing, let turn end + + } +} + +void +do_trade_scroll (DiploForm * diplo, int forward) +{ + int increment = forward ? 1 : -1; + int id = -1; + for (int n = (diplo->other_party_civ_id + increment) & 31; n != diplo->other_party_civ_id; n = (n + increment) & 31) + if ((n != 0) && // if N is not barbs AND + (n != p_main_screen_form->Player_CivID) && // N is not the player's AND + (*p_player_bits & (1U << n)) && // N belongs to an active player AND + (leaders[p_main_screen_form->Player_CivID].Contacts[n] & 1) && // N has contact with the player AND + Leader_ai_would_meet_with (&leaders[n], __, p_main_screen_form->Player_CivID)) { // AI is willing to meet + id = n; + break; + } + + if (id >= 0) { + is->trade_screen_scroll_to_id = id; + DiploForm_close (diplo); + // Note extra code needs to get run here if the other player is not an AI + } +} + +void __fastcall +patch_DiploForm_m82_handle_key_event (DiploForm * this, int edx, int virtual_key_code, int is_down) +{ + if (is->eligible_for_trade_scroll && + (this->mode == 2) && + ((virtual_key_code == VK_LEFT) || (virtual_key_code == VK_RIGHT)) && + (! is_down)) + do_trade_scroll (this, virtual_key_code == VK_RIGHT); + else + DiploForm_m82_handle_key_event (this, __, virtual_key_code, is_down); +} + +int __fastcall +patch_DiploForm_m68_Show_Dialog (DiploForm * this, int edx, int param_1, void * param_2, void * param_3) +{ + if (is->open_diplo_form_straight_to_trade) { + is->open_diplo_form_straight_to_trade = 0; + + // Done by the base game but not necessary as far as I can tell + // void (__cdecl * FUN_00537700) (int) = 0x537700; + // FUN_00537700 (0x15); + // this->field_E9C[0] = this->field_E9C[0] + 1; + + // Set diplo screen mode to two-way trade negotation + this->mode = 2; + + // Set AI's diplo message to something like "what did you have in mind" + int iVar35 = DiploForm_set_their_message (this, __, DM_AI_PROPOSAL_RESPONSE, 0, 0x1A8); + this->field_1390[0] = DM_AI_PROPOSAL_RESPONSE; + this->field_1390[1] = 0; + this->field_1390[2] = iVar35; + + // Reset player message options. This will replace the propose deal, declare war, and leave options with the usual "nevermind" option + // that appears for negotiations. + DiploForm_reset_our_message_choices (this); + + // Done by the base game but not necessary as far as I can tell + // void (__fastcall * FUN_004C89A0) (void *) = 0x4C89A0; + // FUN_004C89A0 (&this->field_1BF4[0]); + + } + + return DiploForm_m68_Show_Dialog (this, __, param_1, param_2, param_3); +} + +void __fastcall +patch_DiploForm_do_diplomacy (DiploForm * this, int edx, int diplo_message, int param_2, int civ_id, int do_not_request_audience, int war_negotiation, int disallow_proposal, TradeOfferList * our_offers, TradeOfferList * their_offers) +{ + is->open_diplo_form_straight_to_trade = 0; + is->trade_screen_scroll_to_id = -1; + + // Trade screen scroll is disabled in online games b/c there's extra synchronization we'd need to do to open or close the diplo screen with + // a human player. + is->eligible_for_trade_scroll = is->current_config.enable_trade_screen_scroll && (! is_online_game ()); + + if (is->eligible_for_trade_scroll && (is->trade_scroll_button_state == IS_UNINITED)) + init_trade_scroll_buttons (this); + + WITH_PAUSE_FOR_POPUP { + DiploForm_do_diplomacy (this, __, diplo_message, param_2, civ_id, do_not_request_audience, war_negotiation, disallow_proposal, our_offers, their_offers); + + while (is->trade_screen_scroll_to_id >= 0) { + int scroll_to_id = is->trade_screen_scroll_to_id; + is->trade_screen_scroll_to_id = -1; + is->open_diplo_form_straight_to_trade = 1; + DiploForm_do_diplomacy (this, __, DM_AI_COUNTER, 0, scroll_to_id, 1, 0, 0, NULL, NULL); + } + } + + is->open_diplo_form_straight_to_trade = 0; + is->eligible_for_trade_scroll = 0; +} + +void __fastcall +patch_DiploForm_m22_Draw (DiploForm * this) +{ + if (is->trade_scroll_button_state == IS_OK) { + Button * left = is->trade_scroll_button_left, + * right = is->trade_scroll_button_right; + if (is->eligible_for_trade_scroll && (this->mode == 2)) { + left ->vtable->m01_Show_Enabled ((Base_Form *)left , __, 0); + right->vtable->m01_Show_Enabled ((Base_Form *)right, __, 0); + left ->vtable->m73_call_m22_Draw ((Base_Form *)left); + right->vtable->m73_call_m22_Draw ((Base_Form *)right); + } else { + left ->vtable->m02_Show_Disabled ((Base_Form *)left); + right->vtable->m02_Show_Disabled ((Base_Form *)right); + } + } + + DiploForm_m22_Draw (this); +} + +void +intercept_end_of_turn () +{ + if (is->current_config.enable_disorder_warning) { + check_happiness_at_end_of_turn (); + if (p_main_screen_form->turn_end_flag == 1) // Check if player cancelled turn ending in the disorder warning popup + return; + } + + // Sometimes clearing of highlighted tiles doesn't trigger when CTRL lifted, so double-check here + if (is->current_config.enable_city_work_radii_highlights && is->highlight_city_radii) { + is->highlight_city_radii = false; + clear_highlighted_worker_tiles_and_redraw (); + } + + if (is->current_config.show_ai_city_location_desirability_if_settler && is->city_loc_display_perspective >= 0) { + is->city_loc_display_perspective = -1; + p_main_screen_form->vtable->m73_call_m22_Draw ((Base_Form *)p_main_screen_form); // Trigger map redraw + } + + // Clear things that don't apply across turns + is->have_job_and_loc_to_skip = 0; +} + +bool +is_worker_or_settler_command (int unit_command_value) +{ + return (unit_command_value & 0x20000000) || + ((unit_command_value >= UCV_Build_Remote_Colony) && (unit_command_value <= UCV_Auto_Save_Tiles)); +} + +bool +command_would_replace_district (int unit_command_value) +{ + // Note: Roads & railroads, etc. can coexist with the district + return (unit_command_value == UCV_Build_Mine) || + (unit_command_value == UCV_Irrigate) || + (unit_command_value == UCV_Plant_Forest) || + (unit_command_value == UCV_Build_Outpost) || + (unit_command_value == UCV_Build_Fortress) || + (unit_command_value == UCV_Build_Barricade) || + (unit_command_value == UCV_Build_Airfield) || + (unit_command_value == UCV_Build_Radar_Tower) || + (unit_command_value == UCV_Build_Colony); +} + +bool +handle_worker_command_that_may_replace_district (Unit * unit, int unit_command_value, bool * removed_existing) +{ + if (removed_existing != NULL) + *removed_existing = false; + + if ((! is->current_config.enable_districts) || (unit == NULL)) return true; + if (! is_worker_or_settler_command (unit_command_value)) return true; + if (! command_would_replace_district (unit_command_value)) return true; + if (! patch_Unit_can_perform_command (unit, __, unit_command_value)) return true; + + Tile * tile = tile_at (unit->Body.X, unit->Body.Y); + if ((tile == NULL) || (tile == p_null_tile)) + return true; + + struct district_instance * inst = get_district_instance (tile); + if ((inst == NULL) || (! district_is_complete (tile, inst->district_id))) + return true; + + int tile_x, tile_y; + if (! district_instance_get_coords (inst, tile, &tile_x, &tile_y)) + return false; + + int district_id = inst->district_id; + int civ_id = unit->Body.CivID; + bool redundant_district = district_instance_is_redundant (inst, tile); + bool would_lose_buildings = any_nearby_city_would_lose_district_benefits (district_id, civ_id, tile_x, tile_y); + if (redundant_district) + would_lose_buildings = false; + + bool remove_existing = redundant_district; + if (inst != NULL && district_id >= 0 && district_id < is->district_count) { + PopupForm * popup = get_popup_form (); + set_popup_str_param (0, (char *)is->district_configs[district_id].display_name, -1, -1); + set_popup_str_param (1, (char *)is->district_configs[district_id].display_name, -1, -1); + popup->vtable->set_text_key_and_flags ( + popup, __, is->mod_script_path, + would_lose_buildings + ? "C3X_CONFIRM_BUILD_IMPROVEMENT_OVER_DISTRICT" + : "C3X_CONFIRM_BUILD_IMPROVEMENT_OVER_DISTRICT_SAFE", + -1, 0, 0, 0); + int sel = patch_show_popup (popup, __, 0, 0); + if (sel != 0) + return false; + remove_existing = true; + } + + if (remove_existing) { + remove_district_instance (tile); + tile->vtable->m62_Set_Tile_BuildingID (tile, __, -1); + tile->vtable->m51_Unset_Tile_Flags (tile, __, 0, TILE_FLAG_MINE, tile_x, tile_y); + handle_district_removed (tile, district_id, tile_x, tile_y, false); + if (removed_existing != NULL) + *removed_existing = true; + } + + return true; +} + +bool __fastcall + patch_Unit_can_upgrade (Unit * this) +{ + bool base = Unit_can_upgrade (this); + int available; + City * city = city_at (this->Body.X, this->Body.Y); + if (base && + (city != NULL) && + get_available_unit_count (&leaders[this->Body.CivID], City_get_upgraded_type_id (city, __, this->Body.UnitTypeID), &available) && + (available <= 0)) + return false; + else + return base; +} + +bool +is_district_command (int unit_command_value) +{ + int district_id; + bool maybe_district_command = itable_look_up (&is->command_id_to_district_id, unit_command_value, &district_id); + + // Natural wonder command IDs are -1 (unbuildable), which can be a false positive + if (district_id == NATURAL_WONDER_DISTRICT_ID) + return false; + + return maybe_district_command; +} + +int __fastcall +patch_Map_check_colony_location (Map * this, int edx, int tile_x, int tile_y, int civ_id) +{ + int base = Map_check_colony_location (this, __, tile_x, tile_y, civ_id); + Tile * tile = tile_at (tile_x, tile_y); + + if ((tile == NULL) || (tile == p_null_tile) || ! is->current_config.allow_extraterritorial_colonies) + return base; + + if (tile->vtable->m35_Check_Is_Water (tile)) return base; + if (Tile_has_city (tile) || Tile_has_colony (tile)) return base; + + int owner_id = tile->vtable->m38_Get_Territory_OwnerID (tile); + if ((owner_id < 0) || (owner_id == civ_id)) return base; + + int resource_type = Tile_get_resource_visible_to (tile, __, civ_id); + if ((resource_type < 0) || (resource_type >= p_bic_data->ResourceTypeCount)) return base; + + int req_tech = p_bic_data->ResourceTypes[resource_type].RequireID; + if ((req_tech >= 0) && (! Leader_has_tech (&leaders[civ_id], __, req_tech))) return base; + + int res_class = p_bic_data->ResourceTypes[resource_type].Class; + if ((res_class != RC_Strategic) && (res_class != RC_Luxury)) return base; + if (tile->vtable->m26_Check_Tile_Building (tile)) + return 6; + + return 0; +} + +bool __fastcall +patch_Unit_can_perform_command (Unit * this, int edx, int unit_command_value) +{ + if (is->current_config.enable_districts || is->current_config.enable_natural_wonders) { + Tile * tile = tile_at (this->Body.X, this->Body.Y); + enum SquareTypes base_type = (tile != NULL && tile != p_null_tile) ? tile->vtable->m50_Get_Square_BaseType (tile) : SQ_INVALID; + + // No worker or settler commands allowed on natural wonders + if ((tile != NULL) && (tile != p_null_tile) && is->current_config.enable_natural_wonders) { + struct district_instance * inst = get_district_instance (tile); + if ((inst != NULL) && + (inst->district_id == NATURAL_WONDER_DISTRICT_ID) && + (inst->natural_wonder_info.natural_wonder_id >= 0)) { + if (is_worker_or_settler_command (unit_command_value) || is_district_command (unit_command_value)) + return false; + } + } + + if (is_district_command (unit_command_value)) { + int district_id; + if (! itable_look_up (&is->command_id_to_district_id, unit_command_value, &district_id)) + return false; + + return is_worker (this) && can_build_district_on_tile (tile, district_id, this->Body.CivID); + } + // Extra check for colony founding if extraterritorial colonies allowed + else if (unit_command_value == UCV_Build_Colony || unit_command_value == UCV_Build_Remote_Colony) { + if (is->current_config.allow_extraterritorial_colonies && is_worker (this)) { + return patch_Map_check_colony_location (&p_bic_data->Map, __, this->Body.X, this->Body.Y, this->Body.CivID) == 0; + } + } + // Extra check for road building if bridge districts allowed + else if (unit_command_value == UCV_Build_Road) { + struct district_instance * inst = get_district_instance (tile); + bool has_road = tile->vtable->m25_Check_Roads (tile, __, 0) != 0; + if (!has_road && (inst != NULL) && tile_has_district_at (this->Body.X, this->Body.Y, BRIDGE_DISTRICT_ID)) + return true; + } + // Extra check for railroad building if bridge districts allowed + else if (unit_command_value == UCV_Build_Railroad) { + struct district_instance * inst = get_district_instance (tile); + bool has_road = tile->vtable->m25_Check_Roads (tile, __, 0) != 0; + bool has_rail = tile->vtable->m23_Check_Railroads (tile, __, 0) != 0; + if ((inst != NULL) && is_worker (this) && has_road && + ! has_rail && tile_has_district_at (this->Body.X, this->Body.Y, BRIDGE_DISTRICT_ID)) { + int req_tech = p_bic_data->WorkerJobs[WJ_Build_Railroad].RequireID; + if ((req_tech < 0) || + ((req_tech != p_bic_data->AdvanceCount) && + Leader_has_tech (&leaders[this->Body.CivID], __, req_tech))) + return true; + } + } + else if (unit_command_value == UCV_Build_Mine) { + bool has_district = (tile != NULL) && (tile != p_null_tile) && (get_district_instance (tile) != NULL); + + if (has_district) { + return base_type == SQ_Hills || + base_type == SQ_Mountains || + base_type == SQ_Desert || + base_type == SQ_Plains || + base_type == SQ_Grassland || + base_type == SQ_Tundra; + } + } else if (unit_command_value == UCV_Pillage) { + return patch_Unit_can_pillage (this, __, this->Body.X, this->Body.Y); + } + } + if (is->current_config.disable_worker_automation && + (this->Body.CivID == p_main_screen_form->Player_CivID) && + (unit_command_value == UCV_Automate)) + return false; + else if (is->current_config.disallow_land_units_from_affecting_water_tiles && + is_worker_or_settler_command (unit_command_value)) { + Tile * tile = tile_at (this->Body.X, this->Body.Y); + enum UnitTypeClasses class = p_bic_data->UnitTypes[this->Body.UnitTypeID].Unit_Class; + return ((class != UTC_Land) || (! tile->vtable->m35_Check_Is_Water (tile))) && + Unit_can_perform_command (this, __, unit_command_value); + } + + return Unit_can_perform_command (this, __, unit_command_value); +} + +void __fastcall +patch_Unit_join_city (Unit * this, int edx, City * city) +{ + if (is->current_config.enable_districts && is_worker (this)) { + int civ_id = this->Body.CivID; + bool is_human = (*p_human_player_bits & (1 << civ_id)) != 0; + if (! is_human) { + struct district_worker_record * rec = get_tracked_worker_record (this); + if ((rec != NULL) && (rec->pending_req != NULL)) { + ai_move_district_worker (this, rec); + return; + } + + Tile * worker_tile = tile_at (this->Body.X, this->Body.Y); + if ((worker_tile != NULL) && (worker_tile != p_null_tile)) { + int worker_continent_id = worker_tile->vtable->m46_Get_ContinentID (worker_tile); + + if ((civ_id >= 0) && (civ_id < 32)) { + struct pending_district_request * same_city_req = NULL; + struct pending_district_request * same_continent_req = NULL; + + FOR_TABLE_ENTRIES (tei, &is->city_pending_district_requests[civ_id]) { + struct pending_district_request * req = (struct pending_district_request *)tei.value; + if ((req == NULL) || (req->assigned_worker_id >= 0)) + continue; + if ((req->civ_id != civ_id) || (req->city_id < 0)) + continue; + + City * req_city = get_city_ptr (req->city_id); + if (req_city == NULL) + continue; + + if (city != NULL && req_city->Body.ID == city->Body.ID) { + same_city_req = req; + break; + } + + Tile * req_city_tile = tile_at (req_city->Body.X, req_city->Body.Y); + if ((req_city_tile != NULL) && (req_city_tile != p_null_tile)) { + int req_continent_id = req_city_tile->vtable->m46_Get_ContinentID (req_city_tile); + if (req_continent_id == worker_continent_id) { + same_continent_req = req; + } + } + } + + struct pending_district_request * chosen_req = (same_city_req != NULL) ? same_city_req : same_continent_req; + if (chosen_req != NULL) { + City * req_city = get_city_ptr (chosen_req->city_id); + if (req_city != NULL) { + int target_x = 0; + int target_y = 0; + Tile * target_tile = find_tile_for_district (req_city, chosen_req->district_id, &target_x, &target_y); + if ((target_tile != NULL) && (target_tile != p_null_tile)) { + wrap_tile_coords (&p_bic_data->Map, &target_x, &target_y); + assign_worker_to_district (chosen_req, this, req_city, chosen_req->district_id, target_x, target_y); + return; + } + } + } + } + } + } + } + + Unit_join_city (this, __, city); +} + +bool __fastcall +patch_Unit_can_pillage (Unit * this, int edx, int tile_x, int tile_y) +{ + bool base = Unit_can_pillage (this, __, tile_x, tile_y); + + if (! is->current_config.enable_districts || ! is->current_config.enable_wonder_districts) + return base; + + Tile * tile = tile_at (tile_x, tile_y); + if ((tile == NULL) || (tile == p_null_tile)) + return base; + + struct district_instance * inst = get_district_instance (tile); + if (inst == NULL) + return base; + + int district_id = inst->district_id; + if (is->current_config.enable_natural_wonders && + (district_id == NATURAL_WONDER_DISTRICT_ID) && + (inst->natural_wonder_info.natural_wonder_id >= 0)) + return false; + + if (! district_is_complete (tile, district_id)) + return true; + + if (district_id == WONDER_DISTRICT_ID) { + struct wonder_district_info * info = get_wonder_district_info (tile); + if (info == NULL || info->state != WDS_COMPLETED) + return true; + return is->current_config.completed_wonder_districts_can_be_destroyed; + } + + return true; +} + +bool __fastcall +patch_Unit_can_do_worker_command_for_button_setup (Unit * this, int edx, int unit_command_value) +{ + bool base = patch_Unit_can_perform_command (this, __, unit_command_value); + + // If the command is to build a city and it can't be done because another city is already too close, and the minimum separation was changed + // from its standard value, then return true here so that the build city button will be added anyway. We'll gray it out later. Check that the + // grayed out button image is initialized now so we don't activate the build city button then find out later we can't gray it out. + if ((! base) && + (unit_command_value == UCV_Build_City) && + (is->current_config.minimum_city_separation > 1) && + (patch_Map_check_city_location (&p_bic_data->Map, __, this->Body.X, this->Body.Y, this->Body.CivID, false) == CLV_CITY_TOO_CLOSE) && + (init_disabled_command_buttons (), is->disabled_command_img_state == IS_OK)) + return true; + + else + return base; +} + +int +compare_helpers (void const * vp_a, void const * vp_b) +{ + Unit * a = get_unit_ptr (*(int *)vp_a), + * b = get_unit_ptr (*(int *)vp_b); + if ((a != NULL) && (b != NULL)) { + // Compute how many movement points each has left (ML = moves left) + int ml_a = patch_Unit_get_max_move_points (a) - a->Body.Moves, + ml_b = patch_Unit_get_max_move_points (b) - b->Body.Moves; + + // Whichever one has more MP left comes first in the array + if (ml_a > ml_b) return 1; + else if (ml_b > ml_a) return -1; + else return 0; + } else + // If at least one of the unit ids is invalid, might as well sort it later in the array + return (a != NULL) ? -1 : ((b != NULL) ? 1 : 0); +} + +void +issue_stack_worker_command (Unit * unit, int command) +{ + int tile_x = unit->Body.X; + int tile_y = unit->Body.Y; + Tile * tile = tile_at (tile_x, tile_y); + int unit_type_id = unit->Body.UnitTypeID; + int unit_id = unit->Body.ID; + + // Put together a list of helpers and store it on the memo. Helpers are just other workers on the same tile that can be issued the same command. + clear_memo (); + FOR_UNITS_ON (uti, tile) + if ((uti.id != unit_id) && + (uti.unit->Body.Container_Unit < 0) && + (uti.unit->Body.UnitState == 0) && + (uti.unit->Body.Moves < patch_Unit_get_max_move_points (uti.unit))) { + // check if the clicked command is among worker actions that this unit type can perform + int actions = p_bic_data->UnitTypes[uti.unit->Body.UnitTypeID].Worker_Actions; + int command_without_category = command & 0x0FFFFFFF; + if ((actions & command_without_category) == command_without_category) + memoize (uti.id); + } + + // Sort the list of helpers so that the ones with the fewest remaining movement points are listed first. + qsort (is->memo, is->memo_len, sizeof is->memo[0], compare_helpers); + + Unit * next_up = unit; + int i_next_helper = 0; + int last_action_didnt_happen; + do { + int state_before_action = next_up->Body.UnitState; + Main_Screen_Form_issue_command (p_main_screen_form, __, command, next_up); + last_action_didnt_happen = (next_up->Body.UnitState == state_before_action); + + // Call this update function to cause the worker to actually perform the action. Otherwise + // it only gets queued, the worker keeps is movement points, and the action doesn't get done + // until the interturn. + if (! last_action_didnt_happen) + next_up->vtable->update_while_active (next_up); + + next_up = NULL; + while ((i_next_helper < is->memo_len) && (next_up == NULL)) + next_up = get_unit_ptr (is->memo[i_next_helper++]); + } while ((next_up != NULL) && (! last_action_didnt_happen)); +} + +void +issue_district_worker_command (Unit * unit, int command) +{ + if (! is->current_config.enable_districts) + return; + + if (unit == NULL) + return; + + int tile_x = unit->Body.X; + int tile_y = unit->Body.Y; + Tile * tile = tile_at (tile_x, tile_y); + if ((tile == NULL) || (tile == p_null_tile)) + return; + + if (! is_worker(unit)) + return; + + int district_id = -1; + if (! itable_look_up (&is->command_id_to_district_id, command, &district_id)) + return; + if ((district_id < 0) || (district_id >= is->district_count)) + return; + + if (! leader_can_build_district (&leaders[unit->Body.CivID], district_id)) + return; + + // Disallow placing districts on invalid terrain, pollution, or cratered tiles + if (tile->vtable->m21_Check_Crates (tile, __, 0)) + return; + if (tile->vtable->m20_Check_Pollution (tile, __, 0)) + return; + + if (! district_is_buildable_on_square_type (&is->district_configs[district_id], tile)) + return; + + // If District will be replaced by another District + struct district_instance * inst = get_district_instance (tile); + if (inst != NULL && district_is_complete(tile, inst->district_id)) { + int existing_district_id = inst->district_id; + int inst_x, inst_y; + if (! district_instance_get_coords (inst, tile, &inst_x, &inst_y)) + return; + + int civ_id = unit->Body.CivID; + bool redundant_district = district_instance_is_redundant (inst, tile); + bool would_lose_buildings = any_nearby_city_would_lose_district_benefits (existing_district_id, civ_id, inst_x, inst_y); + if (redundant_district) + would_lose_buildings = false; + + bool remove_existing = false; + + PopupForm * popup = get_popup_form (); + set_popup_str_param (0, (char*)is->district_configs[existing_district_id].display_name, -1, -1); + set_popup_str_param (1, (char*)is->district_configs[existing_district_id].display_name, -1, -1); + popup->vtable->set_text_key_and_flags ( + popup, __, is->mod_script_path, + would_lose_buildings + ? "C3X_CONFIRM_REPLACE_DISTRICT_WITH_DIFFERENT_DISTRICT" + : "C3X_CONFIRM_REPLACE_DISTRICT_WITH_DIFFERENT_DISTRICT_SAFE", + -1, 0, 0, 0 + ); + + int sel = patch_show_popup (popup, __, 0, 0); + if (sel == 0) + remove_existing = true; + else + return; + + if (remove_existing) { + remove_district_instance (tile); + tile->vtable->m62_Set_Tile_BuildingID (tile, __, -1); + tile->vtable->m51_Unset_Tile_Flags (tile, __, 0, TILE_FLAG_MINE, inst_x, inst_y); + handle_district_removed (tile, existing_district_id, inst_x, inst_y, false); + } + } + + // If District will replace an improvement + unsigned int overlay_flags = tile->vtable->m42_Get_Overlays (tile, __, 0); + unsigned int removable_flags = overlay_flags & (destructible_overlays & ~(TILE_FLAG_ROAD | TILE_FLAG_RAILROAD)); + + if (removable_flags != 0) { + PopupForm * popup = get_popup_form (); + set_popup_str_param (0, (char*)is->district_configs[district_id].display_name, -1, -1); + popup->vtable->set_text_key_and_flags (popup, __, is->mod_script_path, "C3X_CONFIRM_BUILD_DISTRICT_OVER_IMPROVEMENT", -1, 0, 0, 0); + int sel = patch_show_popup (popup, __, 0, 0); + if (sel != 0) + return; + } + + if (removable_flags != 0) + tile->vtable->m51_Unset_Tile_Flags (tile, __, 0, removable_flags, tile_x, tile_y); + tile->vtable->m51_Unset_Tile_Flags (tile, __, 0, TILE_FLAG_MINE, tile_x, tile_y); + + inst = ensure_district_instance (tile, district_id, tile_x, tile_y); + inst->built_by_civ_id = unit->Body.CivID; + if (inst != NULL) + inst->state = DS_UNDER_CONSTRUCTION; + + Unit_set_state (unit, __, UnitState_Build_Mines); + unit->Body.Job_ID = WJ_Build_Mines; +} + +void +issue_stack_unit_mgmt_command (Unit * unit, int command) +{ + Tile * tile = tile_at (unit->Body.X, unit->Body.Y); + int unit_type_id = unit->Body.UnitTypeID; + int unit_id = unit->Body.ID; + + PopupForm * popup = get_popup_form (); + + clear_memo (); + + if (command == UCV_Fortify) { + // This probably won't work for online games since "fortify all" does additional work in that case. See Main_Screen_Form::fortify_all. + // I don't like how this method doesn't place units in the fortified pose. One workaround is so use + // Main_Screen_Form::issue_fortify_command, but that plays the entire fortify animation for each unit which is a major annoyance for + // large stacks. The base game's "fortify all" function also doesn't set the pose so I don't see any easy way to fix this. + FOR_UNITS_ON (uti, tile) + if ((uti.unit->Body.UnitTypeID == unit_type_id) && + (uti.unit->Body.Container_Unit < 0) && + (uti.unit->Body.UnitState == 0) && + (uti.unit->Body.CivID == unit->Body.CivID) && + (uti.unit->Body.Moves < patch_Unit_get_max_move_points (uti.unit))) + Unit_set_state (uti.unit, __, UnitState_Fortifying); + + } else if (command == UCV_Upgrade_Unit) { + int our_treasury = leaders[unit->Body.CivID].Gold_Encoded + leaders[unit->Body.CivID].Gold_Decrement; + + // If the unit type we're upgrading to is limited, find out how many we can add. Keep that in "available". If the type is not limited, + // leave available set to INT_MAX. + int available = INT_MAX; { + City * city; + int upgrade_id; + if ((is->current_config.unit_limits.len > 0) && + patch_Unit_can_perform_command (unit, __, UCV_Upgrade_Unit) && + (NULL != (city = city_at (unit->Body.X, unit->Body.Y))) && + (0 < (upgrade_id = City_get_upgraded_type_id (city, __, unit_type_id)))) + get_available_unit_count (&leaders[unit->Body.CivID], upgrade_id, &available); + } + + int cost = 0; + FOR_UNITS_ON (uti, tile) + if ((available > 0) && + (uti.unit->Body.UnitTypeID == unit_type_id) && + (uti.unit->Body.Container_Unit < 0) && + (uti.unit->Body.UnitState == 0) && + patch_Unit_can_perform_command (uti.unit, __, UCV_Upgrade_Unit)) { + cost += Unit_get_upgrade_cost (uti.unit); + available--; + memoize (uti.id); + } + + if (cost <= our_treasury) { + set_popup_str_param (0, p_bic_data->UnitTypes[unit_type_id].Name, -1, -1); + set_popup_int_param (0, is->memo_len); + set_popup_int_param (1, cost); + popup->vtable->set_text_key_and_flags (popup, __, script_dot_txt_file_path, "UPGRADE_ALL", -1, 0, 0, 0); + if (patch_show_popup (popup, __, 0, 0) == 0) + for (int n = 0; n < is->memo_len; n++) { + Unit * to_upgrade = get_unit_ptr (is->memo[n]); + if (to_upgrade != NULL) + Unit_upgrade (to_upgrade, __, false); + } + + } else { + set_popup_int_param (0, cost); + int param_5 = is_online_game () ? 0x4000 : 0; // As in base code + popup->vtable->set_text_key_and_flags (popup, __, script_dot_txt_file_path, "NO_GOLD_TO_UPGRADE_ALL", -1, 0, param_5, 0); + patch_show_popup (popup, __, 0, 0); + } + + } else if (command == UCV_Disband) { + FOR_UNITS_ON (uti, tile) + if ((uti.unit->Body.UnitTypeID == unit_type_id) && + (uti.unit->Body.Container_Unit < 0) && + (uti.unit->Body.UnitState == 0) && + (uti.unit->Body.Moves < patch_Unit_get_max_move_points (uti.unit))) + memoize (uti.id); + + if (is->memo_len > 0) { + set_popup_int_param (0, is->memo_len); + popup->vtable->set_text_key_and_flags (popup, __, is->mod_script_path, "C3X_CONFIRM_STACK_DISBAND", -1, 0, 0, 0); + if (patch_show_popup (popup, __, 0, 0) == 0) { + for (int n = 0; n < is->memo_len; n++) { + Unit * to_disband = get_unit_ptr (is->memo[n]); + if (to_disband) + Unit_disband (to_disband); + } + } + } + } +} + +void __fastcall +patch_Main_GUI_handle_button_press (Main_GUI * this, int edx, int button_id) +{ + // Set SB flag according to case (2) + if (button_id < 42) { + if ((is->sc_img_state == IS_OK) && + ((this->Unit_Command_Buttons[button_id].Button.Images[0] == &is->sc_button_image_sets[SC_BOMBARD].imgs[0]) || + (this->Unit_Command_Buttons[button_id].Button.Images[0] == &is->sc_button_image_sets[SC_BOMB ].imgs[0]))) + is->sb_activated_by_button = 1; + else + is->sb_activated_by_button = 0; + } + + int command = this->Unit_Command_Buttons[button_id].Command; + + // Clear any highlighted tiles + if (is->current_config.enable_city_work_radii_highlights && is->highlight_city_radii) { + is->highlight_city_radii = false; + clear_highlighted_worker_tiles_and_redraw (); + } + + if (is->current_config.show_ai_city_location_desirability_if_settler && is->city_loc_display_perspective >= 0) { + is->city_loc_display_perspective = -1; + p_main_screen_form->vtable->m73_call_m22_Draw ((Base_Form *)p_main_screen_form); // Trigger map redraw + } + + // If a district, run district build logic + if (is->current_config.enable_districts && is_district_command (command)) { + clear_something_1 (); + Timer_clear (&this->timer_1); + issue_district_worker_command (p_main_screen_form->Current_Unit, command); + return; + } + + // Check if command is a worker build command (not a district) and a district exists on the tile + if (is->current_config.enable_districts && p_main_screen_form->Current_Unit != NULL) { + + bool removed_existing = false; + if (! handle_worker_command_that_may_replace_district (p_main_screen_form->Current_Unit, command, &removed_existing)) + return; + if (removed_existing) { + clear_something_1 (); + Timer_clear (&this->timer_1); + Main_GUI_handle_button_press (this, __, button_id); + return; + } + } + + struct sc_button_info const * stack_button_info; { + stack_button_info = NULL; + if (button_id < 42) // If button pressed was a unit command button + for (int n = 0; n < COUNT_STACKABLE_COMMANDS; n++) + if (command == sc_button_infos[n].command) { + stack_button_info = &sc_button_infos[n]; + break; + } + } + + if ((stack_button_info == NULL) || // If there's no stack command for the pressed button OR + (! is->current_config.enable_stack_unit_commands) || // stack unit commands are not enabled OR + (((*p_GetAsyncKeyState) (VK_CONTROL)) >> 8 == 0) || // CTRL key is not down OR + (p_main_screen_form->Current_Unit == NULL) || // no unit is selected OR + is_online_game ()) { // is online game + Main_GUI_handle_button_press (this, __, button_id); + return; + } + + enum stackable_command_kind kind = stack_button_info->kind; + if ((kind == SCK_TERRAFORM) || (kind == SCK_UNIT_MGMT)) { + // Replicate behavior of function we're replacing + clear_something_1 (); + Timer_clear (&this->timer_1); + + if (kind == SCK_TERRAFORM) + issue_stack_worker_command (p_main_screen_form->Current_Unit, stack_button_info->command); + else if (kind == SCK_UNIT_MGMT) + issue_stack_unit_mgmt_command (p_main_screen_form->Current_Unit, stack_button_info->command); + } else + Main_GUI_handle_button_press (this, __, button_id); +} + +bool __fastcall +patch_Main_Screen_Form_issue_command (Main_Screen_Form * this, int edx, int command, Unit * unit) +{ + Unit * target_unit = unit; + if (target_unit == NULL) + target_unit = this->Current_Unit; + + if (is->current_config.enable_districts) { + bool removed_existing = false; + if (! handle_worker_command_that_may_replace_district (target_unit, command, &removed_existing)) + return false; + } + + return Main_Screen_Form_issue_command (this, __, command, unit); +} + +bool +is_command_button_active (Main_GUI * main_gui, enum Unit_Command_Values command) +{ + Command_Button * buttons = main_gui->Unit_Command_Buttons; + for (int n = 0; n < 42; n++) + if (((buttons[n].Button.Base_Data.Status2 & 1) != 0) && (buttons[n].Command == command)) + return true; + return false; +} + +int __fastcall +patch_Main_Screen_Form_handle_key_down (Main_Screen_Form * this, int edx, int char_code, int virtual_key_code) +{ + // Set SB flag according to case (4) + int precision_strike_is_available = is_command_button_active (&this->GUI, UCV_Precision_Bombing); + if ((virtual_key_code == VK_B) || (precision_strike_is_available && (virtual_key_code == VK_P))) + is->sb_activated_by_button = 0; + + if ((virtual_key_code & 0xFF) == VK_CONTROL) { + set_up_stack_worker_buttons (&this->GUI); + + if (is->current_config.enable_city_work_radii_highlights && + ! is->highlight_city_radii) { + Unit * unit = p_main_screen_form->Current_Unit; + if (unit != NULL) { + is->highlight_city_radii = true; + compute_highlighted_worker_tiles_for_districts (); + this->vtable->m73_call_m22_Draw ((Base_Form *)this); + } + } + } else { + if (is->highlight_city_radii) { + is->highlight_city_radii = false; + clear_highlighted_worker_tiles_and_redraw (); + } + } + + char original_turn_end_flag = this->turn_end_flag; + int tr = Main_Screen_Form_handle_key_down (this, __, char_code, virtual_key_code); + if ((original_turn_end_flag == 1) && (this->turn_end_flag == 0)) + intercept_end_of_turn (); + + return tr; +} + +int +patch_handle_cursor_change_in_jgl () +{ + // Set SB flag according to case (3) and the annoying state + if ((is->sb_activated_by_button != 2) && + (p_main_screen_form->Mode_Action != UMA_Bombard) && + (p_main_screen_form->Mode_Action != UMA_Air_Bombard)) + is->sb_activated_by_button = 0; + + return handle_cursor_change_in_jgl (); +} + +void __fastcall +patch_Main_Screen_Form_handle_left_click_on_map_1 (Main_Screen_Form * this, int edx, int param_1, int param_2) +{ + if (is->sb_activated_by_button == 1) + is->sb_activated_by_button = 2; + Main_Screen_Form_handle_left_click_on_map_1 (this, __, param_1, param_2); + is->sb_activated_by_button = 0; +} + + +void __fastcall +patch_Main_GUI_handle_click_in_status_panel (Main_GUI * this, int edx, int mouse_x, int mouse_y) +{ + char original_turn_end_flag = p_main_screen_form->turn_end_flag; + Main_GUI_handle_click_in_status_panel (this, __, mouse_x, mouse_y); + if ((original_turn_end_flag == 1) && (p_main_screen_form->turn_end_flag == 0)) + intercept_end_of_turn (); +} + +// Gets effective shields generated per turn, including civil engineers, disorder, and anarchy. +int +get_city_production_rate (City * city, enum City_Order_Types order_type, int order_id) +{ + int in_disorder = city->Body.Status & CSF_Civil_Disorder, + in_anarchy = p_bic_data->Governments[leaders[city->Body.CivID].GovernmentType].b_Transition_Type, + getting_tile_shields = (! in_disorder) && (! in_anarchy); + + if (order_type == COT_Improvement) { + int building_wealth = (p_bic_data->Improvements[order_id].ImprovementFlags & ITF_Capitalization) != 0; + int specialist_shields = 0; + FOR_CITIZENS_IN (ci, city) + if ((ci.ctzn->Body.field_20[0] & 0xFF) == 0) // I don't know what is check is for but it's done in the base code + specialist_shields += p_bic_data->CitizenTypes[ci.ctzn->Body.WorkerType].Construction; + return (getting_tile_shields ? city->Body.ProductionIncome : 0) + ((! building_wealth) ? specialist_shields : 0); + } else if ((order_type == COT_Unit) && getting_tile_shields) + return city->Body.ProductionIncome; + else + return 0; +} + +void __fastcall +patch_City_Form_open (City_Form * this, int edx, City * city, int param_2) +{ + recompute_resources_if_necessary (); + + WITH_PAUSE_FOR_POPUP { + City_Form_open (this, __, city, param_2); + } +} + +void +init_district_icons () +{ + if (is->dc_icons_img_state != IS_UNINITED) + return; + + char ss[200]; + snprintf (ss, sizeof ss, "[C3X] init_district_icons: state=%d\n", is->dc_icons_img_state); + (*p_OutputDebugStringA) (ss); + + PCX_Image pcx; + PCX_Image_construct (&pcx); + + char temp_path[2*MAX_PATH]; + get_mod_art_path ("Districts/DistrictIncomeIcons.pcx", temp_path, sizeof temp_path); + + PCX_Image_read_file (&pcx, __, temp_path, NULL, 0, 0x100, 2); + if ((pcx.JGL.Image == NULL) || + (pcx.JGL.Image->vtable->m54_Get_Width (pcx.JGL.Image) < 776) || + (pcx.JGL.Image->vtable->m55_Get_Height (pcx.JGL.Image) < 32)) { + (*p_OutputDebugStringA) ("[C3X] PCX file for district icons failed to load or is too small.\n"); + is->dc_icons_img_state = IS_INIT_FAILED; + goto cleanup; + } + + // Extract science icon (index 1) + Sprite_construct (&is->district_science_icon); + Sprite_slice_pcx (&is->district_science_icon, __, &pcx, 1 + 1*31, 1, 30, 30, 1, 1); + + // Extract commerce icon (index 2) + Sprite_construct (&is->district_commerce_icon); + Sprite_slice_pcx (&is->district_commerce_icon, __, &pcx, 1 + 2*31, 1, 30, 30, 1, 1); + + // Extract shield icon (index 4) + Sprite_construct (&is->district_shield_icon); + Sprite_slice_pcx (&is->district_shield_icon, __, &pcx, 1 + 4*31, 1, 30, 30, 1, 1); + + // Extract corruption icon (index 5) + Sprite_construct (&is->district_corruption_icon); + Sprite_slice_pcx (&is->district_corruption_icon, __, &pcx, 1 + 5*31, 1, 30, 30, 1, 1); + + // Extract food icon (index 6) + Sprite_construct (&is->district_food_icon); + Sprite_slice_pcx (&is->district_food_icon, __, &pcx, 1 + 6*31, 1, 30, 30, 1, 1); + + // Extract food eaten icon (index 7) + Sprite_construct (&is->district_food_eaten_icon); + Sprite_slice_pcx (&is->district_food_eaten_icon, __, &pcx, 1 + 7*31, 1, 30, 30, 1, 1); + + // Extract small happiness icon (index 12) + Sprite_construct (&is->district_happiness_icon_small); + Sprite_slice_pcx (&is->district_happiness_icon_small, __, &pcx, 1 + 12*31, 1, 30, 30, 1, 1); + + // Extract small shield icon (index 13) + Sprite_construct (&is->district_shield_icon_small); + Sprite_slice_pcx (&is->district_shield_icon_small, __, &pcx, 1 + 13*31, 1, 30, 30, 1, 1); + + // Extract small commerce icon (index 14) + Sprite_construct (&is->district_commerce_icon_small); + Sprite_slice_pcx (&is->district_commerce_icon_small, __, &pcx, 1 + 14*31, 1, 30, 30, 1, 1); + + // Extract small food icon (index 15: x = 1 + 15*31 = 466, width 30) + Sprite_construct (&is->district_food_icon_small); + Sprite_slice_pcx (&is->district_food_icon_small, __, &pcx, 1 + 15*31, 1, 30, 30, 1, 1); + + // Extract small science icon (index 16) + Sprite_construct (&is->district_science_icon_small); + Sprite_slice_pcx (&is->district_science_icon_small, __, &pcx, 1 + 16*31, 1, 30, 30, 1, 1); + + // Extract small culture icon (index 18) + Sprite_construct (&is->district_culture_icon_small); + Sprite_slice_pcx (&is->district_culture_icon_small, __, &pcx, 1 + 18*31, 1, 30, 30, 1, 1); + + // Load Negatives (mostly red) from here + + // Extract negative small commerce icon (index 17) + Sprite_construct (&is->district_negative_commerce_icon_small); + Sprite_slice_pcx (&is->district_negative_commerce_icon_small, __, &pcx, 1 + 17*31, 1, 30, 30, 1, 1); + + // Extract small unhappiness icon (index 19) + Sprite_construct (&is->district_unhappiness_icon_small); + Sprite_slice_pcx (&is->district_unhappiness_icon_small, __, &pcx, 1 + 19*31, 1, 30, 30, 1, 1); + + // Extract negative small shield icon (index 20) + Sprite_construct (&is->district_negative_shield_icon_small); + Sprite_slice_pcx (&is->district_negative_shield_icon_small, __, &pcx, 1 + 20*31, 1, 30, 30, 1, 1); + + // Extract negative small culture icon (index 21) + Sprite_construct (&is->district_negative_culture_icon_small); + Sprite_slice_pcx (&is->district_negative_culture_icon_small, __, &pcx, 1 + 21*31, 1, 30, 30, 1, 1); + + // Extract negative small culture icon (index 22) + Sprite_construct (&is->district_negative_food_icon_small); + Sprite_slice_pcx (&is->district_negative_food_icon_small, __, &pcx, 1 + 22*31, 1, 30, 30, 1, 1); + + // Extract negative small science icon (index 23) + Sprite_construct (&is->district_negative_science_icon_small); + Sprite_slice_pcx (&is->district_negative_science_icon_small, __, &pcx, 1 + 23*31, 1, 30, 30, 1, 1); + + is->dc_icons_img_state = IS_OK; +cleanup: + pcx.vtable->destruct (&pcx, __, 0); +} + +void __fastcall +patch_City_Form_draw (City_Form * this) +{ + // Recompute city form production rect location every time because the config might have changed. Doing it here is also easier than + // patching the constructor. + int form_top = (p_bic_data->ScreenHeight - this->Background_Image.Height) / 2; + this->Production_Storage_Indicator.top = form_top + 621 + (is->current_config.show_detailed_city_production_info ? 34 : 0); + + is->drawn_strat_resource_count = 0; + + // Make sure culture income (including from districts) is up to date before the draw event + if (is->current_config.enable_districts) + patch_City_recompute_culture_income(this->CurrentCity); + + City_Form_draw (this); + + if (is->current_config.show_detailed_city_production_info) { + City * city = this->CurrentCity; + int order_type = city->Body.Order_Type, + order_id = city->Body.Order_ID, + order_progress = City_get_order_progress (city), + order_cost = City_get_order_cost (city), + prod_rate = get_city_production_rate (city, order_type, order_id), + building_wealth = (order_type == COT_Improvement) && ((p_bic_data->Improvements[order_id].ImprovementFlags & ITF_Capitalization) != 0); + + int turns_left, surplus; { + if (prod_rate > 0) { + turns_left = (order_cost - order_progress) / prod_rate; + if ((order_cost - order_progress) % prod_rate != 0) + turns_left++; + if (turns_left < 1) + turns_left = 1; + surplus = (turns_left * prod_rate) - (order_cost - order_progress); + } else { + turns_left = 9999; + surplus = 0; + } + } + + char line1[100]; { + if (prod_rate > 0) { + if (! building_wealth) + snprintf (line1, sizeof line1, "%s %d %s", this->Labels.To_Build, turns_left, (turns_left == 1) ? this->Labels.Single_Turn : this->Labels.Multiple_Turns); + else + snprintf (line1, sizeof line1, "%s", is->c3x_labels[CL_NEVER_COMPLETES]); + } else + snprintf (line1, sizeof line1, "%s", is->c3x_labels[CL_HALTED]); + line1[(sizeof line1) - 1] = '\0'; + } + + char line2[100]; { + if (! building_wealth) { + int percent_complete = order_cost > 0 ? ((10000 * order_progress) / order_cost + 50) / 100 : 100; + snprintf (line2, sizeof line2, "%d / %d (%d%s)", order_progress, order_cost, percent_complete, this->Labels.Percent); + } else + snprintf (line2, sizeof line2, "---"); + line2[(sizeof line2) - 1] = '\0'; + } + + char line3[100]; { + if ((! building_wealth) && (prod_rate > 0)) { + int s_per, s_rem; { + if (turns_left > 1) { + s_per = surplus / turns_left; + s_rem = surplus % turns_left; + } else { + s_per = surplus; + s_rem = 0; + } + } + char * s_lab = is->c3x_labels[CL_SURPLUS]; + if ((s_per != 0) && (s_rem != 0)) snprintf (line3, sizeof line3, "%s: %d + %d %s", s_lab, s_rem, s_per, this->Labels.Per_Turn); + else if ((s_per == 0) && (s_rem != 0)) snprintf (line3, sizeof line3, "%s: %d", s_lab, s_rem); + else if ((s_per != 0) && (s_rem == 0)) snprintf (line3, sizeof line3, "%s: %d %s", s_lab, s_per, this->Labels.Per_Turn); + else snprintf (line3, sizeof line3, "%s", is->c3x_labels[CL_SURPLUS_NONE]); + } else + snprintf (line3, sizeof line3, "%s", is->c3x_labels[CL_SURPLUS_NA]); + line3[(sizeof line3) - 1] = '\0'; + } + + Object_66C3FC * font = get_font (10, FSF_NONE); + int left = this->Production_Storage_Indicator.left, + top = this->Production_Storage_Indicator.top, + width = this->Production_Storage_Indicator.right - left; + PCX_Image_draw_centered_text (&this->Base.Data.Canvas, __, font, line1, left, top - 42, width, strlen (line1)); + PCX_Image_draw_centered_text (&this->Base.Data.Canvas, __, font, line2, left, top - 28, width, strlen (line2)); + PCX_Image_draw_centered_text (&this->Base.Data.Canvas, __, font, line3, left, top - 14, width, strlen (line3)); + } + + // Draw district commerce bonuses (gold and science) + if (! (is->current_config.enable_districts || is->current_config.enable_natural_wonders)) + return; + + // Lazy load district icons + if (is->dc_icons_img_state == IS_UNINITED) + init_district_icons (); + if (is->dc_icons_img_state != IS_OK) + return; + + City * city = this->CurrentCity; + if (city == NULL) + return; + + // Calculate district gold and science bonuses by iterating workable tiles + int district_gold = 0; + int city_civ_id = city->Body.CivID; + + FOR_DISTRICTS_AROUND (wai, city->Body.X, city->Body.Y, true) { + if ((wai.dx == 0) && (wai.dy == 0)) continue; + Tile * tile = wai.tile; + struct district_instance * inst = wai.district_inst; + int district_id = inst->district_id; + + struct district_config const * cfg = &is->district_configs[district_id]; + int gold_bonus = 0; + get_effective_district_yields (inst, cfg, NULL, NULL, &gold_bonus, NULL, NULL, NULL); + district_gold += gold_bonus; + } + + Leader * leader = &leaders[city_civ_id]; + int gold_proportion = (district_gold * leader->gold_slider) / 10; + int science_proportion = (district_gold * leader->science_slider) / 10; + + // Draw district gold icons + if (gold_proportion > 0) { + Sprite * gold_sprite = &is->district_commerce_icon; + int sprite_width = gold_sprite->Width; + int sprite_height = gold_sprite->Height; + + struct tagRECT * gold_rect = &this->Gold_Income_Rect; + int total_gold = City_get_net_commerce (city, __, 2, true); + + // Calculate spacing + int spacing = sprite_width; + if ((total_gold > 1) && (total_gold * sprite_width != (gold_rect->right - gold_rect->left))) { + int rect_width = gold_rect->right - gold_rect->left; + if (rect_width <= total_gold * sprite_width) { + spacing = (rect_width - sprite_width) / (total_gold - 1); + if (spacing < 1) + spacing = 1; + else if (spacing > sprite_width) + spacing = sprite_width; + } + } + + // Draw from right to left + int x_offset = 0; + int y_offset = 5; + for (int i = 0; i < gold_proportion && i < total_gold; i++) { + int x = gold_rect->right - x_offset - sprite_width; + int y = gold_rect->top + ((gold_rect->bottom - gold_rect->top >> 1) - + (sprite_height >> 1)) + y_offset; + + Sprite_draw (gold_sprite, __, &(this->Base).Data.Canvas, x, y, NULL); + x_offset += spacing; + } + } + + // Draw district science icons + if (science_proportion > 0) { + Sprite * science_sprite = &is->district_commerce_icon; + int sprite_width = science_sprite->Width; + int sprite_height = science_sprite->Height; + + struct tagRECT * science_rect = &this->Science_Income_Rect; + int total_science = City_get_net_commerce (city, __, 1, true); + + // Calculate spacing + int spacing = sprite_width; + if ((total_science > 1) && (total_science * sprite_width != (science_rect->right - science_rect->left))) { + int rect_width = science_rect->right - science_rect->left; + if (rect_width <= total_science * sprite_width) { + spacing = (rect_width - sprite_width) / (total_science - 1); + if (spacing < 1) + spacing = 1; + else if (spacing > sprite_width) + spacing = sprite_width; + } + } + + // Draw from right to left + int x_offset = 0; + int y_offset = 5; + for (int i = 0; i < science_proportion && i < total_science; i++) { + int x = science_rect->right - x_offset - sprite_width; + int y = science_rect->top + ((science_rect->bottom - science_rect->top >> 1) - + (sprite_height >> 1)) + y_offset; + + Sprite_draw (science_sprite, __, &(this->Base).Data.Canvas, x, y, NULL); + x_offset += spacing; + } + } +} + +void __fastcall +patch_City_Form_print_production_info (City_Form *this, int edx, String256 * out_strs, int str_capacity) +{ + City_Form_print_production_info (this, __, out_strs, str_capacity); + if (is->current_config.show_detailed_city_production_info) + out_strs[1].S[0] = '\0'; +} + +int __fastcall +patch_Sprite_draw_strat_res_on_city_screen (Sprite * this, int edx, PCX_Image * canvas, int pixel_x, int pixel_y, PCX_Color_Table * color_table) +{ + if (is->current_config.compact_strategic_resource_display_on_city_screen) + pixel_x -= 13 * is->drawn_strat_resource_count + 17; + return Sprite_draw (this, __, canvas, pixel_x, pixel_y, color_table); +} + +int __fastcall +patch_PCX_Image_do_draw_cntd_text_for_strat_res (PCX_Image * this, int edx, char * str, int x, int y, int width, unsigned str_len) +{ + if (is->current_config.compact_strategic_resource_display_on_city_screen) + x -= 13 * is->drawn_strat_resource_count + 17; + int tr = PCX_Image_do_draw_centered_text (this, __, str, x, y, width, str_len); + is->drawn_strat_resource_count++; + return tr; +} + +int __fastcall +patch_City_get_turns_to_build (City * this, int edx, enum City_Order_Types order_type, int order_id, bool param_3) +{ + // To fix the zero production crash, return 9999 when the city's total production rate is zero, avoiding a division by zero. That's only + // possible when producing an improvement due to negative shields from specialists. The original logic attempts to return 9999 in case of zero + // production but checks for that before including shields from specialists. + if (is->current_config.patch_zero_production_crash && (order_type == COT_Improvement)) { + + int specialist_shields = 0; + FOR_CITIZENS_IN (ci, this) + if ((ci.ctzn->Body.field_20[0] & 0xFF) == 0) + specialist_shields += p_bic_data->CitizenTypes[ci.ctzn->Body.WorkerType].Construction; + + // Return 9999 if the denominator in the base function's calculation would be zero. Note the base calc is incorrect in that it + // considers specialist shields to count toward Wealth, however we're not going to address that issue here, just stop the crash. + if (this->Body.ProductionIncome + specialist_shields <= 0) + return 9999; + } + + return City_get_turns_to_build (this, __, order_type, order_id, param_3); +} + +bool +is_below_stack_limit (Tile * tile, int civ_id, int type_id) +{ + enum UnitTypeClasses class = p_bic_data->UnitTypes[type_id].Unit_Class; + + int stack_limit = is->current_config.limit_units_per_tile[class]; + if (stack_limit <= 0) + return true; + + if (is->current_config.exclude_cities_from_units_per_tile_limit && + get_city_ptr (tile->CityID) != NULL) + return true; + + if (itable_look_up_or (&is->current_config.exclude_types_from_units_per_tile_limit, type_id, 0)) + return true; + + FOR_UNITS_ON (uti, tile) { + // If there is a foreign unit on the tile then consider it as being below the stack limit. This ensures that the stack limit doesn't + // block combat between players. + if (uti.unit->Body.CivID != civ_id) + return true; + + int uti_type_id = uti.unit->Body.UnitTypeID; + if ((uti.unit->Body.Container_Unit < 0) && + (class == p_bic_data->UnitTypes[uti_type_id].Unit_Class) && + ! itable_look_up_or (&is->current_config.exclude_types_from_units_per_tile_limit, uti_type_id, 0)) { + stack_limit -= 1; + if (stack_limit <= 0) + return false; + } + } + return true; +} + +// Returns the ID of the civ this move is trespassing against, or 0 if it's not trespassing. +int +check_trespassing (int civ_id, Tile * from, Tile * to) +{ + int from_territory_id = from->vtable->m38_Get_Territory_OwnerID (from), + to_territory_id = to ->vtable->m38_Get_Territory_OwnerID (to); + if ((civ_id > 0) && + (to_territory_id != civ_id) && + (to_territory_id > 0) && + (to_territory_id != from_territory_id) && + (! leaders[civ_id].At_War[to_territory_id]) && + ((leaders[civ_id].Relation_Treaties[to_territory_id] & 2) == 0)) // Check right of passage + return to_territory_id; + else + return 0; +} + +bool +is_allowed_to_trespass (Unit * unit) +{ + int type_id = unit->Body.UnitTypeID; + if ((type_id >= 0) && (type_id < p_bic_data->UnitTypeCount)) { + UnitType * type = &p_bic_data->UnitTypes[type_id]; + return UnitType_has_ability (type, __, UTA_Hidden_Nationality) || UnitType_has_ability (type, __, UTA_Invisible); + } else + return false; +} + +AdjacentMoveValidity __fastcall +patch_Unit_can_move_to_adjacent_tile (Unit * this, int edx, int neighbor_index, int param_2) +{ + AdjacentMoveValidity base_validity = Unit_can_move_to_adjacent_tile (this, __, neighbor_index, param_2); + + if (is->current_config.enable_districts) { + int nx, ny; + get_neighbor_coords (&p_bic_data->Map, this->Body.X, this->Body.Y, neighbor_index, &nx, &ny); + Tile * dest = tile_at (nx, ny); + + // Let workers step onto coast tiles when the config flag is enabled (base logic treats this as an invalid sea move) + if (is->current_config.workers_can_enter_coast && is_worker (this) && + ((base_validity == AMV_INVALID_SEA_MOVE) || (base_validity == AMV_CANNOT_EMBARK))) { + if ((dest != NULL) && + dest->vtable->m35_Check_Is_Water (dest) && + (dest->vtable->m50_Get_Square_BaseType (dest) == SQ_Coast)) + base_validity = AMV_OK; + } + + // Allow land units to enter bridge tiles + if (is->current_config.enable_bridge_districts && + (p_bic_data->UnitTypes[this->Body.UnitTypeID].Unit_Class == UTC_Land) && + ((base_validity == AMV_INVALID_SEA_MOVE) || (base_validity == AMV_CANNOT_EMBARK))) { + if ((dest != NULL) && (dest != p_null_tile)) { + struct district_instance * inst = get_district_instance (dest); + if ((inst != NULL) && (inst->district_id == BRIDGE_DISTRICT_ID) && district_is_complete (dest, inst->district_id)) { + base_validity = AMV_OK; + } + } + } + + // Allow naval units to enter completed canal tiles + if (is->current_config.enable_canal_districts && + (p_bic_data->UnitTypes[this->Body.UnitTypeID].Unit_Class == UTC_Sea) && + ((base_validity == AMV_INVALID_SEA_MOVE) || (base_validity == AMV_CANNOT_EMBARK))) { + if ((dest != NULL) && (dest != p_null_tile)) { + struct district_instance * inst = get_district_instance (dest); + if ((inst != NULL) && (inst->district_id == CANAL_DISTRICT_ID) && district_is_complete (dest, inst->district_id)) { + base_validity = AMV_OK; + } + } + } + + if ((base_validity == AMV_OK) && + is->current_config.enable_port_districts && + is->current_config.naval_units_use_port_districts_not_cities && + (p_bic_data->UnitTypes[this->Body.UnitTypeID].Unit_Class == UTC_Sea)) { + if ((dest != NULL) && (dest != p_null_tile) && Tile_has_city (dest)) + return AMV_INVALID_SEA_MOVE; + } + } + + // Apply unit count per tile limit + int type_id = this->Body.UnitTypeID; + if ((base_validity == AMV_OK) && (is->current_config.limit_units_per_tile[p_bic_data->UnitTypes[type_id].Unit_Class] > 0)) { + int nx, ny; + get_neighbor_coords (&p_bic_data->Map, this->Body.X, this->Body.Y, neighbor_index, &nx, &ny); + if (! is_below_stack_limit (tile_at (nx, ny), this->Body.CivID, type_id)) + return AMV_CANNOT_PASS_BETWEEN; + } + + // Apply trespassing restriction + if (is->current_config.disallow_trespassing && (base_validity == AMV_OK)) { + Tile * from = tile_at (this->Body.X, this->Body.Y); + int nx, ny; + get_neighbor_coords (&p_bic_data->Map, this->Body.X, this->Body.Y, neighbor_index, &nx, &ny); + int trespasses_against_civ_id = check_trespassing (this->Body.CivID, from, tile_at (nx, ny)); + if ((trespasses_against_civ_id > 0) && (! is_allowed_to_trespass (this))) + // The tile might be occupied by a unit belonging to a civ other than the one that owns the territory (against whom we'd be + // trespassing). In this case we must forbid the move entirely since TRIGGERS_WAR will not stop it if we're at war with the + // occupying civ. This fixes a bug where units could trespass by attacking an enemy unit across a border. They would then get + // stuck halfway between tiles if they won. + return (trespasses_against_civ_id == get_tile_occupier_id (nx, ny, this->Body.CivID, false)) ? AMV_TRIGGERS_WAR : AMV_CANNOT_PASS_BETWEEN; + } + + return base_validity; +} + +bool +great_wall_blocks_civ (Tile * tile, int civ_id) +{ + if (! is->current_config.enable_districts || + ! is->current_config.enable_great_wall_districts || + ! is->current_config.great_wall_districts_impassible_by_others) + return false; + + if ((tile == NULL) || (tile == p_null_tile)) + return false; + + int owner_id = tile->vtable->m38_Get_Territory_OwnerID (tile); + if (owner_id <= 0) + return false; + if (owner_id == civ_id) + return false; + + struct district_instance * inst = get_district_instance (tile); + if ((inst == NULL) || (inst->district_id != GREAT_WALL_DISTRICT_ID)) + return false; + + if (! district_is_complete (tile, GREAT_WALL_DISTRICT_ID)) + return false; + + if (district_is_obsolete_for_civ (GREAT_WALL_DISTRICT_ID, civ_id)) + return false; + + return true; +} + +int __fastcall +patch_Trade_Net_get_movement_cost (Trade_Net * this, int edx, int from_x, int from_y, int to_x, int to_y, Unit * unit, int civ_id, unsigned flags, int neighbor_index, Trade_Net_Distance_Info * dist_info) +{ + if (is->is_computing_city_connections && // if this call came while rebuilding the trade network AND + (is->tnx_init_state == IS_OK) && (is->tnx_cache != NULL) && // Trade Net X is set up AND + (unit == NULL) && ((flags == 0x1009) || (flags == 0x9))) // this call can be accelerated by TNX + return is->get_move_cost_for_sea_trade (this, is->tnx_cache, from_x, from_y, to_x, to_y, civ_id, flags, neighbor_index, dist_info); + + int base_cost = Trade_Net_get_movement_cost (this, __, from_x, from_y, to_x, to_y, unit, civ_id, flags, neighbor_index, dist_info); + + if ((unit != NULL) && great_wall_blocks_civ (tile_at (to_x, to_y), unit->Body.CivID)) + return -1; + + // Let the pathfinder consider coastal tiles reachable for workers when the config flag is on + if (is->current_config.enable_districts && is->current_config.workers_can_enter_coast && + (base_cost < 0) && (unit != NULL) && is_worker (unit)) { + Tile * dest = tile_at (to_x, to_y); + if ((dest != NULL) && + dest->vtable->m35_Check_Is_Water (dest) && + (dest->vtable->m50_Get_Square_BaseType (dest) == SQ_Coast)) + base_cost = Unit_get_max_move_points (unit); + } + + // Let the pathfinder consider bridge tiles reachable for land units + if (is->current_config.enable_districts && + is->current_config.enable_bridge_districts && + (base_cost < 0) && (unit != NULL) && + (p_bic_data->UnitTypes[unit->Body.UnitTypeID].Unit_Class == UTC_Land)) { + Tile * dest = tile_at (to_x, to_y); + if ((dest != NULL) && (dest != p_null_tile)) { + struct district_instance * inst = get_district_instance (dest); + if ((inst != NULL) && + (inst->district_id == BRIDGE_DISTRICT_ID) && + district_is_complete (dest, inst->district_id)) { + base_cost = Unit_get_max_move_points (unit); + } + } + } + + // Let the pathfinder consider canal tiles reachable for naval units + if (is->current_config.enable_districts && + is->current_config.enable_canal_districts && + (base_cost < 0) && (unit != NULL) && + (p_bic_data->UnitTypes[unit->Body.UnitTypeID].Unit_Class == UTC_Sea)) { + Tile * dest = tile_at (to_x, to_y); + if ((dest != NULL) && (dest != p_null_tile)) { + struct district_instance * inst = get_district_instance (dest); + if ((inst != NULL) && + (inst->district_id == CANAL_DISTRICT_ID) && + district_is_complete (dest, inst->district_id)) { + base_cost = Unit_get_max_move_points (unit); + } + } + } + + // Treat roads/rails on bridge districts like land roads/rails for movement cost. + if ((unit != NULL) && + is->current_config.enable_districts && + is->current_config.enable_bridge_districts && + (p_bic_data->UnitTypes[unit->Body.UnitTypeID].Unit_Class == UTC_Land)) { + Tile * from = tile_at (from_x, from_y); + Tile * to = tile_at (to_x, to_y); + if ((from != NULL) && (from != p_null_tile) && (to != NULL) && (to != p_null_tile)) { + struct district_instance * from_inst = get_district_instance (from); + struct district_instance * to_inst = get_district_instance (to); + bool from_bridge = (from_inst != NULL) && + (from_inst->district_id == BRIDGE_DISTRICT_ID) && + district_is_complete (from, from_inst->district_id); + bool to_bridge = (to_inst != NULL) && + (to_inst->district_id == BRIDGE_DISTRICT_ID) && + district_is_complete (to, to_inst->district_id); + if (from_bridge || to_bridge) { + bool from_rail = from->vtable->m23_Check_Railroads (from, __, 0) != 0; + bool to_rail = to->vtable->m23_Check_Railroads (to, __, 0) != 0; + bool from_road = from->vtable->m25_Check_Roads (from, __, 0) != 0; + bool to_road = to->vtable->m25_Check_Roads (to, __, 0) != 0; + if (from_rail && to_rail) + base_cost = 0; + else if (from_road && to_road) + base_cost = 1; + } + } + } + + if ((unit != NULL) && + (base_cost >= 0) && + is->current_config.enable_districts && + is->current_config.enable_port_districts && + is->current_config.naval_units_use_port_districts_not_cities && + (p_bic_data->UnitTypes[unit->Body.UnitTypeID].Unit_Class == UTC_Sea)) { + Tile * dest = tile_at (to_x, to_y); + if ((dest != NULL) && (dest != p_null_tile) && Tile_has_city (dest)) + return -1; + } + + // Apply unit count per tile limit + if ((unit != NULL) && ! is_below_stack_limit (tile_at (to_x, to_y), unit->Body.CivID, unit->Body.UnitTypeID)) + return -1; + + // Apply trespassing restriction + if (is->current_config.disallow_trespassing && + check_trespassing (civ_id, tile_at (from_x, from_y), tile_at (to_x, to_y)) && + ((unit == NULL) || (! is_allowed_to_trespass (unit)))) + return -1; + + // Adjust movement cost to enforce limited railroad movement + if ((is->current_config.limit_railroad_movement > 0) && (is->saved_road_movement_rate > 0)) { + if ((unit != NULL) && (base_cost == 0)) { // Railroad move + if (! is->current_config.limited_railroads_work_like_fast_roads) { // If Civ 4 style RR, scale cost by type's moves + int type_moves_available = patch_Unit_get_max_move_points (unit) / p_bic_data->General.RoadsMovementRate; + return type_moves_available * is->railroad_mp_cost_per_move; + } else + return is->railroad_mp_cost_per_move; + } else if (base_cost == 1) // Road move + return is->road_mp_cost; + } + + return base_cost; +} + +int __fastcall +patch_Trade_Net_set_unit_path (Trade_Net * this, int edx, int from_x, int from_y, int to_x, int to_y, Unit * unit, int civ_id, int flags, int * out_path_length_in_mp) +{ + int tr = Trade_Net_set_unit_path (this, __, from_x, from_y, to_x, to_y, unit, civ_id, flags, out_path_length_in_mp); + + bool may_require_length_fix = (is->current_config.limit_railroad_movement > 0) && // if railroad movement is limited AND + (tr > 0) && (out_path_length_in_mp != NULL) && // path was found AND caller wants to know its length AND + (unit != NULL); // the path is for an actual unit + + // We might have to correct the path length returned by the base game's pathfinder. This occurs when railroad movement is limited and the + // unit's total MP exceeds what can be stored in a one-byte integer. The cause of the incorrect length is that the pathfinder internally + // stores the remaining moves at each node of the search in a single byte. This correction fixes the bug (reported several times) that ETAs + // shown in the interface are wrong. + if (may_require_length_fix && (patch_Unit_get_max_move_points (unit) > 255)) { // Need to recompute path length if unit's total MP can overflow a uint8 + + // First memoize the cost of taking each step along the path. This must be done separately because the pathfinder's internal data only + // lets us traverse the path backwards. + { + // Must pass cached "distance info" to Trade_Net::get_movement_cost. Trade_Net stores two sets of cached data, which one was + // used depends on the flags. I believe one is intended to be used for city trade connections and the other for unit movement. + Trade_Net_Distance_Info * dist_info = (flags & 1) ? this->Data2 : this->Data4; + + clear_memo (); + int x = to_x, y = to_y; + do { + // "flags & 1" again determines whether Data2 or Data4 was used. + enum direction dir = Trade_Net_get_direction_from_internal_map (this, __, x, y, flags & 1); + if (dir == DIR_ZERO) + break; + + int prev_x, prev_y; { + int dx, dy; + neighbor_index_to_diff (dir, &dx, &dy); + prev_x = x + dx; prev_y = y + dy; + wrap_tile_coords (&p_bic_data->Map, &prev_x, &prev_y); + } + + memoize (patch_Trade_Net_get_movement_cost (this, __, prev_x, prev_y, x, y, unit, civ_id, flags, reverse_dir (dir), dist_info)); + x = prev_x; y = prev_y; + } while (! ((x == from_x) && (y == from_y))); + } + + // Now walk the path forwards tracking how much MP the unit would spend. We must be aware that the unit can't spend more MP than it + // has. For example, if a unit with 1 move walks onto an unimproved mountain, that effectively costs only 1 move, not 3. + int mp_remaining = patch_Unit_get_max_move_points (unit) - unit->Body.Moves, + mp_spent = 0; + for (int n = is->memo_len - 1; n >= 0; n--) { + int cost = is->memo[n]; + if (cost < mp_remaining) { + mp_spent += cost; + mp_remaining -= cost; + } else { + mp_spent += mp_remaining; + mp_remaining = patch_Unit_get_max_move_points (unit); + } + } + *out_path_length_in_mp = mp_spent; + + // Also, if this is a move between adjacent tiles, make sure the path length doesn't exceed the unit's remaining MP. Otherwise, the game may + // erroneously show an ETA of >1 turn. + } else if (may_require_length_fix && are_tiles_adjacent (from_x, from_y, to_x, to_y)) + *out_path_length_in_mp = not_above (patch_Unit_get_max_move_points (unit) - unit->Body.Moves, *out_path_length_in_mp); + + return tr; +} + +int __fastcall +patch_Trade_Net_set_unit_path_to_find_sea_route (Trade_Net * this, int edx, int from_x, int from_y, int to_x, int to_y, Unit * unit, int civ_id, int flags, int * out_path_length_in_mp) +{ + // Accelerate this call with TNX if possible + if ((is->tnx_init_state == IS_OK) && (is->tnx_cache != NULL)) { + bool route_exists = is->try_drawing_sea_trade_route (this, is->tnx_cache, from_x, from_y, to_x, to_y, civ_id, flags); + return route_exists ? 1 : 0; + } else + return Trade_Net_set_unit_path (this, __, from_x, from_y, to_x, to_y, unit, civ_id, flags, out_path_length_in_mp); +} + +// Renames this leader and their civ based on their era. Era-specific names come from the config file. If this leader has a custom name set by the +// human player, this method does nothing. +void +apply_era_specific_names (Leader * leader) +{ + int leader_bit = 1 << leader->ID; + Race * race = &p_bic_data->Races[leader->RaceID]; + + struct replaceable_name { + char * base_name; + int * tracking_bits; + char * buf; + int buf_size; + } replaceable_names[] = { + {race->vtable->GetAdjectiveName (race), &is->aliased_civ_adjective_bits , leader->tribe_customization.civ_adjective , sizeof leader->tribe_customization.civ_adjective}, + {race->vtable->GetSingularName (race) , &is->aliased_civ_noun_bits , leader->tribe_customization.civ_noun , sizeof leader->tribe_customization.civ_noun}, + {race->vtable->GetCountryName (race) , &is->aliased_civ_formal_name_bits, leader->tribe_customization.civ_formal_name, sizeof leader->tribe_customization.civ_formal_name} + }; + + // Apply replacements to civ noun, adjective, and formal name + for (int n = 0; n < ARRAY_LEN (replaceable_names); n++) { + struct replaceable_name * repl = &replaceable_names[n]; + if ((*repl->tracking_bits & leader_bit) || (repl->buf[0] == '\0')) { + char * replacement = NULL; + if (leader->Era < ERA_ALIAS_LIST_CAPACITY) + // Search through the list of aliases in reverse order so that, if the same key was listed multiple times, the last + // appearance overrides the earlier ones. This is important b/c when configs are loaded, their aliases get appended to + // the list. + for (int k = is->current_config.count_civ_era_alias_lists - 1; k >= 0; k--) { + struct civ_era_alias_list * list = &is->current_config.civ_era_alias_lists[k]; + if (strcmp (list->key, repl->base_name) == 0) { + replacement = list->aliases[leader->Era]; + break; + } + } + if (replacement != NULL) { + strncpy (repl->buf, replacement, repl->buf_size); + repl->buf[repl->buf_size - 1] = '\0'; + *repl->tracking_bits |= leader_bit; + } else { + repl->buf[0] = '\0'; + *repl->tracking_bits &= ~leader_bit; + } + } + } + + // Apply replacement to leader name, gender, and title + if ((is->aliased_leader_name_bits & leader_bit) || (leader->tribe_customization.leader_name[0] == '\0')) { + char * base_name = race->vtable->GetLeaderName (race); + char * replacement_name = NULL; + char * replacement_title = NULL; + int replacement_gender; // Only used if replacement_name is + if (leader->Era < ERA_ALIAS_LIST_CAPACITY) + for (int k = is->current_config.count_leader_era_alias_lists - 1; k >= 0; k--) { + struct leader_era_alias_list * list = &is->current_config.leader_era_alias_lists[k]; + if (strcmp (list->key, base_name) == 0) { + replacement_name = list->aliases[leader->Era]; + replacement_title = list->titles[leader->Era]; + replacement_gender = (list->gender_bits >> leader->Era) & 1; + break; + } + } + if (replacement_name != NULL) { + TribeCustomization * tc = &leader->tribe_customization; + strncpy (tc->leader_name, replacement_name, sizeof tc->leader_name); + tc->leader_name[(sizeof tc->leader_name) - 1] = '\0'; + tc->leader_gender = replacement_gender; + is->aliased_leader_name_bits |= leader_bit; + + // If this replacement name has a special title and this player does not have a custom title set, replace the title. + if ((replacement_title != NULL) && ((is->aliased_leader_title_bits & leader_bit) || (tc->leader_title[0] == '\0'))) { + strncpy (tc->leader_title, replacement_title, sizeof tc->leader_title); + tc->leader_title[(sizeof tc->leader_title) - 1] = '\0'; + is->aliased_leader_title_bits |= leader_bit; + + // If the current name has no title and the player's title was previously replaced, undo the replacement. + } else if ((replacement_title == NULL) && (is->aliased_leader_title_bits & leader_bit)) { + leader->tribe_customization.leader_title[0] = '\0'; + is->aliased_leader_title_bits &= ~leader_bit; + } + } else { + leader->tribe_customization.leader_name[0] = '\0'; + // Don't need to clear custom leader gender since it's not used unless a custom name was set + is->aliased_leader_name_bits &= ~leader_bit; + + // Remove title replacement if present + if (is->aliased_leader_title_bits & leader_bit) { + leader->tribe_customization.leader_title[0] = '\0'; + is->aliased_leader_title_bits &= ~leader_bit; + } + } + } +} + +int __cdecl +patch_do_save_game (char const * file_path, char param_2, GUID * guid) +{ + // Do not save the modified road movement rate, if it was modified to limit railroad movement + int restore_rmr = (is->current_config.limit_railroad_movement > 0) && (is->saved_road_movement_rate > 0); + int rmr; + if (restore_rmr) { + rmr = p_bic_data->General.RoadsMovementRate; + p_bic_data->General.RoadsMovementRate = is->saved_road_movement_rate; + } + + // Do not save the modified barb culture group ID + int restore_barb_culture_group = is->current_config.enable_city_capture_by_barbarians && (is->saved_barb_culture_group >= 0); + int barb_culture; + if (restore_barb_culture_group) { + barb_culture = p_bic_data->Races[leaders[0].RaceID].CultureGroupID; + p_bic_data->Races[leaders[0].RaceID].CultureGroupID = is->saved_barb_culture_group; + } + + // Do not save names that were replaced with era-specific versions + for (int n = 0; n < 32; n++) { + Leader * leader = &leaders[n]; + int leader_bit = 1 << leader->ID; + if (is->aliased_civ_noun_bits & leader_bit) leader->tribe_customization.civ_noun [0] = '\0'; + if (is->aliased_civ_adjective_bits & leader_bit) leader->tribe_customization.civ_adjective [0] = '\0'; + if (is->aliased_civ_formal_name_bits & leader_bit) leader->tribe_customization.civ_formal_name[0] = '\0'; + if (is->aliased_leader_name_bits & leader_bit) leader->tribe_customization.leader_name [0] = '\0'; + if (is->aliased_leader_title_bits & leader_bit) leader->tribe_customization.leader_title [0] = '\0'; + } + is->aliased_civ_noun_bits = is->aliased_civ_adjective_bits = is->aliased_civ_formal_name_bits = is->aliased_leader_name_bits = is->aliased_leader_title_bits = 0; + + // Do not save unit types with the charm bits cleared if they were cleared by convertion to PTW targeting. The special actions field must not + // include the top category bits that are part of the UCV_* enum + for (int n = 0; n < is->count_charmed_types_converted_to_ptw_arty; n++) { + UnitType * converted_type = &p_bic_data->UnitTypes[is->charmed_types_converted_to_ptw_arty[n]]; + converted_type->Special_Actions |= (0x00FFFFFF & UCV_Charm_Bombard); + } + + int tr = do_save_game (file_path, param_2, guid); + + if (restore_rmr) + p_bic_data->General.RoadsMovementRate = rmr; + if (restore_barb_culture_group) + p_bic_data->Races[leaders[0].RaceID].CultureGroupID = barb_culture; + + // Reapply era aliases + for (int n = 0; n < 32; n++) + if (*p_player_bits & (1 << n)) + apply_era_specific_names (&leaders[n]); + + // Reclear charm bits on converted types + for (int n = 0; n < is->count_charmed_types_converted_to_ptw_arty; n++) { + UnitType * converted_type = &p_bic_data->UnitTypes[is->charmed_types_converted_to_ptw_arty[n]]; + converted_type->Special_Actions &= ~(0x00FFFFFF & UCV_Charm_Bombard); + } + + return tr; +} + +void +record_unit_type_alt_strategy (int type_id) +{ + int ai_strat_index; { + int ai_strat_bits = p_bic_data->UnitTypes[type_id].AI_Strategy; + if ((ai_strat_bits & ai_strat_bits - 1) != 0) // Sanity check: must only have one strat (one bit) set + return; + ai_strat_index = 0; + while ((ai_strat_bits & 1) == 0) { + ai_strat_index++; + ai_strat_bits >>= 1; + } + } + + itable_insert (&is->unit_type_alt_strategies, type_id, ai_strat_index); +} + +void +append_improv_id_to_list (struct improv_id_list * list, int id) +{ + reserve (sizeof list->items[0], (void **)&list->items, &list->capacity, list->count); + list->items[list->count] = id; + list->count += 1; +} + +unsigned __fastcall +patch_load_scenario (void * this, int edx, char * param_1, unsigned * param_2) +{ + int ret_addr = ((int *)¶m_1)[-1]; + + // Destroy TNX cache from previous map. A new one will be created when needed. + if ((is->tnx_init_state == IS_OK) && (is->tnx_cache != NULL)) { + is->destroy_tnx_cache (is->tnx_cache); + is->tnx_cache = NULL; + } + + unsigned tr = load_scenario (this, __, param_1, param_2); + char * scenario_path = param_1; + + // There are two cases when load_scenario is called that we don't want to run our own code. These are (1) when the scenario is loaded to + // generate a preview (map, etc.) for the "Civ Content" or "Conquests" menu and (2) when the function is called recursively. The recursive + // call is done, I believe, only when loading saves using the default rules. The game first tries to load custom rules then falls back on + // loading the default rules. In any case, we want to ignore that call so we don't run the same code twice. + if ((ret_addr == ADDR_LOAD_SCENARIO_PREVIEW_RETURN) || (ret_addr == ADDR_LOAD_SCENARIO_RESUME_SAVE_2_RETURN)) + return tr; + + reset_to_base_config (); + load_config ("default.c3x_config.ini", 1); + char * scenario_config_file_name = "scenario.c3x_config.ini"; + char * scenario_config_path = BIC_get_asset_path (p_bic_data, __, scenario_config_file_name, false); + + // BIC_get_asset_path returns the file name when it can't find the file + if (0 != strcmp (scenario_config_file_name, scenario_config_path)) { + load_config (scenario_config_path, 0); + } + load_config ("custom.c3x_config.ini", 1); + apply_machine_code_edits (&is->current_config, false); + + if (is->current_config.enable_districts || is->current_config.enable_natural_wonders) { + reset_district_state (true); + load_districts_config (); + } + + // Initialize Trade Net X + if (is->current_config.enable_trade_net_x && (is->tnx_init_state == IS_UNINITED)) { + char path[MAX_PATH]; + snprintf (path, sizeof path, "%s\\Trade Net X\\TradeNetX.dll", is->mod_rel_dir); + path[(sizeof path) - 1] = '\0'; + is->trade_net_x = LoadLibraryA (path); + if (is->trade_net_x != NULL) { + is->set_exe_version = (void *)(*p_GetProcAddress) (is->trade_net_x, "set_exe_version"); + is->create_tnx_cache = (void *)(*p_GetProcAddress) (is->trade_net_x, "create_tnx_cache"); + is->destroy_tnx_cache = (void *)(*p_GetProcAddress) (is->trade_net_x, "destroy_tnx_cache"); + is->set_up_before_building_network = (void *)(*p_GetProcAddress) (is->trade_net_x, "set_up_before_building_network"); + is->get_move_cost_for_sea_trade = (void *)(*p_GetProcAddress) (is->trade_net_x, "get_move_cost_for_sea_trade"); + is->flood_fill_road_network = (void *)(*p_GetProcAddress) (is->trade_net_x, "flood_fill_road_network"); + is->try_drawing_sea_trade_route = (void *)(*p_GetProcAddress) (is->trade_net_x, "try_drawing_sea_trade_route"); + + is->set_exe_version (exe_version_index); + + // Run tests + if (0) { + int (__stdcall * test) () = (void *)(*p_GetProcAddress) (is->trade_net_x, "test"); + int failed_test_count = test (); + if (failed_test_count > 0) + MessageBoxA (NULL, "Failed some tests in Trade Net X!", NULL, MB_ICONWARNING); + else + MessageBoxA (NULL, "All tests in Trade Net X passed.", "Success", MB_ICONINFORMATION); + } + + is->tnx_init_state = IS_OK; + } else { + MessageBoxA (NULL, "Failed to load Trade Net X!", NULL, MB_ICONERROR); + is->tnx_init_state = IS_INIT_FAILED; + } + + // Deinitialize Trade Net X + } else if ((! is->current_config.enable_trade_net_x) && (is->tnx_init_state == IS_OK)) { + FreeLibrary (is->trade_net_x); + is->trade_net_x = NULL; + is->tnx_init_state = IS_UNINITED; + } + + // This scenario might use different mod art assets than the old one + deinit_stackable_command_buttons (); + deinit_disabled_command_buttons (); + deinit_trade_scroll_buttons (); + deinit_unit_rcm_icons (); + deinit_red_food_icon (); + deinit_large_minimap_frame (); + if (is->tile_already_worked_zoomed_out_sprite_init_state != IS_UNINITED) { + enum init_state * state = &is->tile_already_worked_zoomed_out_sprite_init_state; + if (*state == IS_OK) { + Sprite * sprite = &is->tile_already_worked_zoomed_out_sprite; + sprite->vtable->destruct (sprite, __, 0); + } + *state = IS_UNINITED; + } + + // Need to clear this since the resource count might have changed + if (is->extra_available_resources != NULL) { + free (is->extra_available_resources); + is->extra_available_resources = NULL; + is->extra_available_resources_capacity = 0; + } + + // Similarly, these don't carry over between games + for (int n = 0; n < 32; n++) + is->interceptor_reset_lists[n].count = 0; + is->replay_for_players = 0; + table_deinit (&is->extra_defensive_bombards); + table_deinit (&is->airdrops_this_turn); + table_deinit (&is->unit_transport_ties); + is->last_selected_unit.initial_x = is->last_selected_unit.initial_y = -1; + is->last_selected_unit.last_x = is->last_selected_unit.last_y = is->last_selected_unit.type_id = -1; + is->last_selected_unit.ptr = NULL; + table_deinit (&is->waiting_units); + is->have_loaded_waiting_units = false; + + // Clear extra city improvement bits + FOR_TABLE_ENTRIES (tei, &is->extra_city_improvs) + free ((void *)tei.value); + table_deinit (&is->extra_city_improvs); + + // Clear unit type counts + for (int n = 0; n < 32; n++) + table_deinit (&is->unit_type_counts[n]); + is->unit_type_count_init_bits = 0; + + // Clear last city founding turn numbers + for (int n = 0; n < 32; n++) + is->turn_no_of_last_founding_for_settler_perfume[n] = -1; + + // Load resources.pcx + { + PCX_Image * rs = is->resources_sheet; + if (rs != NULL) + rs->vtable->destruct (rs, __, 0); + else + rs = malloc (sizeof *rs); + memset (rs, 0, sizeof *rs); + PCX_Image_construct (rs); + + char * resources_pcx_path = BIC_get_asset_path (p_bic_data, __, "Art\\resources.pcx", true); + PCX_Image_read_file (rs, __, resources_pcx_path, NULL, 0, 0x100, 2); + is->resources_sheet = rs; + } + + // Recreate table of alt strategies mapping duplicates to their strategies + table_deinit (&is->unit_type_alt_strategies); + for (int n = 0; n < p_bic_data->UnitTypeCount; n++) { + int alt_for_id = p_bic_data->UnitTypes[n].alternate_strategy_for_id; + if (alt_for_id >= 0) { + record_unit_type_alt_strategy (n); + record_unit_type_alt_strategy (alt_for_id); // Record the original too so we know it has alternatives + } + } + + // Recreate table of duplicates mapping unit types to the next duplicate + table_deinit (&is->unit_type_duplicates); + for (int n = 0; n < p_bic_data->UnitTypeCount; n++) { + int alt_for_id = p_bic_data->UnitTypes[n].alternate_strategy_for_id; + if (alt_for_id >= 0) { + + // Find the type ID of the last duplicate in the list. alt_for_id refers to the original unit that was duplicated possibly + // multiple times. Begin searching there and, each time we find another duplicate, use its ID to continue the search. When + // we're done last_dup_id will be set to whichever ID in the list of duplicates is not already associated with another. + int last_dup_id = alt_for_id; { + int next; + while (itable_look_up (&is->unit_type_duplicates, last_dup_id, &next)) + last_dup_id = next; + } + + // Add this unit type to the end of the list of duplicates + itable_insert (&is->unit_type_duplicates, last_dup_id, n); + } + } + + // Convert charm-flagged units to using PTW targeting if necessary + if (is->current_config.charm_flag_triggers_ptw_like_targeting) { + for (int n = 0; n < p_bic_data->UnitTypeCount; n++) { + UnitType * type = &p_bic_data->UnitTypes[n]; + if (type->Special_Actions & UCV_Charm_Bombard) { + // Also add it to the list of converted types + reserve (sizeof is->charmed_types_converted_to_ptw_arty[0], // item size + (void **)&is->charmed_types_converted_to_ptw_arty, // ptr to items + &is->charmed_types_converted_to_ptw_arty_capacity, // ptr to capacity + is->count_charmed_types_converted_to_ptw_arty); // count + is->charmed_types_converted_to_ptw_arty[is->count_charmed_types_converted_to_ptw_arty] = n; + is->count_charmed_types_converted_to_ptw_arty += 1; + + // Add this type ID to the table + itable_insert (&is->current_config.ptw_arty_types, n, 1); + + // Clear the charm flag, taking care not to clear the category bit. This is necessary for the PTW targeting to work + // since the logic to implement it only applies to the non-charm code path. It wouldn't make sense to have both charm + // attack and PTW targeting anyway, since charm attack already works that way vs cities. + type->Special_Actions &= ~(0x00FFFFFF & UCV_Charm_Bombard); + } + } + } + + // Pick out which resources are used as mill inputs + if (is->mill_input_resource_bits) + free (is->mill_input_resource_bits); + is->mill_input_resource_bits = calloc (1, 1 + p_bic_data->ResourceTypeCount / 8); + for (int n = 0; n < is->current_config.count_mills; n++) { + struct mill * mill = &is->current_config.mills[n]; + for (int k = 0; k < 2; k++) { + int resource_id = (&p_bic_data->Improvements[mill->improv_id].Resource1ID)[k]; + if ((resource_id >= 0) && (resource_id < p_bic_data->ResourceTypeCount)) { + byte bit = 1 << (resource_id & 7); + is->mill_input_resource_bits[resource_id>>3] |= bit; + } + } + } + + // Recreate lists of water & air trade improvements + is->water_trade_improvs .count = 0; + is->air_trade_improvs .count = 0; + is->combat_defense_improvs.count = 0; + for (int n = 0; n < p_bic_data->ImprovementsCount; n++) { + enum ImprovementTypeFlags flags = p_bic_data->Improvements[n].ImprovementFlags; + if (flags & ITF_Allows_Water_Trade) append_improv_id_to_list (&is->water_trade_improvs , n); + if (flags & ITF_Allows_Air_Trade) append_improv_id_to_list (&is->air_trade_improvs , n); + if (p_bic_data->Improvements[n].Combat_Defence != 0) append_improv_id_to_list (&is->combat_defense_improvs, n); + } + + // Set up for limiting railroad movement, if enabled + if (is->current_config.limit_railroad_movement > 0) { + int base_rmr = p_bic_data->General.RoadsMovementRate; + int g = gcd (base_rmr, is->current_config.limit_railroad_movement); // Scale down all MP costs by this common divisor to help against + // overflow of 8-bit integers inside the pathfinder. + is->saved_road_movement_rate = base_rmr; + p_bic_data->General.RoadsMovementRate = base_rmr * is->current_config.limit_railroad_movement / g; // Full move in MP + is->road_mp_cost = is->current_config.limit_railroad_movement / g; + is->railroad_mp_cost_per_move = base_rmr / g; + } else { + is->saved_road_movement_rate = -1; + is->road_mp_cost = 1; + is->railroad_mp_cost_per_move = 0; + } + + // If barb city capturing is enabled and the barbs have the non-existent "none" culture group (index -1), switch them to the first real + // culture group. The "none" group produces corrupt graphics and crashes. + int * barb_culture_group = &p_bic_data->Races[leaders[0].RaceID].CultureGroupID; + if (is->current_config.enable_city_capture_by_barbarians && (*barb_culture_group < 0)) { + is->saved_barb_culture_group = *barb_culture_group; + *barb_culture_group = 0; + } else + is->saved_barb_culture_group = -1; + + // Clear old alias bits + is->aliased_civ_noun_bits = is->aliased_civ_adjective_bits = is->aliased_civ_formal_name_bits = is->aliased_leader_name_bits = is->aliased_leader_title_bits = 0; + + // Clear day/night cycle vars and deindex sprite proxies, if necessary. + if (is->current_config.day_night_cycle_mode != DNCM_OFF) { + is->day_night_cycle_unstarted = true; + is->current_day_night_cycle = 12; + if (is->day_night_cycle_img_proxies_indexed) { + deindex_day_night_image_proxies (); + } + } + + return tr; +} + +void __fastcall +patch_Leader_recompute_auto_improvements (Leader * this) +{ + is->leader_param_for_patch_get_wonder_city_id = this; + Leader_recompute_auto_improvements (this); +} + +int __fastcall +patch_Game_get_wonder_city_id (Game * this, int edx, int wonder_improvement_id) +{ + int ret_addr = ((int *)&wonder_improvement_id)[-1]; + if ((is->current_config.enable_free_buildings_from_small_wonders) && (ret_addr == ADDR_SMALL_WONDER_FREE_IMPROVS_RETURN)) { + Leader * leader = is->leader_param_for_patch_get_wonder_city_id; + Improvement * improv = &p_bic_data->Improvements[wonder_improvement_id]; + if ((improv->Characteristics & ITC_Small_Wonder) != 0) { + // Need to check if Small_Wonders array is NULL b/c recompute_auto_improvements gets called with leaders that are absent/dead. + return (leader->Small_Wonders != NULL) ? leader->Small_Wonders[wonder_improvement_id] : -1; + } + } + return Game_get_wonder_city_id (this, __, wonder_improvement_id); +} + +int __fastcall +patch_Main_Screen_Form_handle_key_up (Main_Screen_Form * this, int edx, int virtual_key_code) +{ + if ((virtual_key_code & 0xFF) == VK_CONTROL) { + patch_Main_GUI_set_up_unit_command_buttons (&this->GUI); + + if (is->current_config.enable_city_work_radii_highlights && is->highlight_city_radii) { + is->highlight_city_radii = false; + clear_highlighted_worker_tiles_and_redraw (); + } + } + + return Main_Screen_Form_handle_key_up (this, __, virtual_key_code); +} + +char __fastcall +patch_Leader_can_do_worker_job (Leader * this, int edx, enum Worker_Jobs job, int tile_x, int tile_y, int ask_if_replacing) +{ + char tr; + bool skip_replacement_logic = + (p_main_screen_form->Player_CivID == this->ID) && is->current_config.skip_repeated_tile_improv_replacement_asks; + + Tile * tile = tile_at (tile_x, tile_y); + + // Check if AI is trying to change a district tile (before calling vanilla logic) + if ((is->current_config.enable_districts || is->current_config.enable_natural_wonders) && + ((*p_human_player_bits & (1 << this->ID)) == 0)) { + if ((tile != NULL) && (tile != p_null_tile)) { + struct district_instance * inst = get_district_instance (tile); + if (inst != NULL && inst->district_id >= 0 && inst->district_id < is->district_count) { + int district_id = inst->district_id; + bool allow_ai_change = false; + + // Allow AI to modify obsolete districts + if (district_is_obsolete_for_civ (district_id, this->ID)) + allow_ai_change = true; + + // Allow if district could be used as a prerequisite for other buildable districts + if (! allow_ai_change) { + for (int other_id = 0; other_id < is->district_count; other_id++) { + if (other_id == district_id) + continue; + struct district_config const * other_cfg = &is->district_configs[other_id]; + if (! other_cfg->has_buildable_on_districts) + continue; + for (int i = 0; i < other_cfg->buildable_on_district_id_count; i++) { + if (other_cfg->buildable_on_district_ids[i] == district_id) { + if (leader_can_build_district (this, other_id)) + allow_ai_change = true; + break; + } + } + if (allow_ai_change) + break; + } + } + + // Allow AI to build roads/rails on bridge districts + if (! allow_ai_change && (district_id == BRIDGE_DISTRICT_ID) && + (job == WJ_Build_Road || job == WJ_Build_Railroad)) + allow_ai_change = true; + + // For Wonder Districts: check if unused (can be replaced) + if (! allow_ai_change && is->current_config.enable_wonder_districts && (district_id == WONDER_DISTRICT_ID)) { + struct wonder_district_info * info = get_wonder_district_info (tile); + + // If there's a reservation (wonder being built) or completed wonder, block replacement + if (info != NULL && info->state != WDS_UNUSED) + return 0; + + // Wonder district is unused - fall through to normal tech checks + } + else if (! allow_ai_change && is->current_config.enable_natural_wonders && (district_id == NATURAL_WONDER_DISTRICT_ID)) { + return 0; + } + else if (! allow_ai_change) { + // For all other district types: AI should not change them + return 0; + } + } + } + } + + if (! skip_replacement_logic) + tr = Leader_can_do_worker_job (this, __, job, tile_x, tile_y, ask_if_replacing); + else if (is->have_job_and_loc_to_skip && + (is->to_skip.job == job) && (is->to_skip.tile_x == tile_x) && (is->to_skip.tile_y == tile_y)) + tr = Leader_can_do_worker_job (this, __, job, tile_x, tile_y, 0); + else { + is->show_popup_was_called = 0; + tr = Leader_can_do_worker_job (this, __, job, tile_x, tile_y, ask_if_replacing); + if (is->show_popup_was_called && tr) { // Check that the popup was shown and the player chose to replace + is->to_skip = (struct worker_job_and_location) { .job = job, .tile_x = tile_x, .tile_y = tile_y }; + is->have_job_and_loc_to_skip = 1; + } + } + + if (! tr && is->current_config.enable_districts && (job == WJ_Build_Mines)) { + if ((tile != NULL) && (tile != p_null_tile)) { + struct district_instance * inst = get_district_instance (tile); + if (inst != NULL && + inst->district_id >= 0 && inst->district_id < is->district_count && + ! tile->vtable->m35_Check_Is_Water (tile) && + (tile->CityID < 0) && + ! tile->vtable->m18_Check_Mines (tile, __, 0)) { + int tile_x = 0, tile_y = 0; + tile_coords_from_ptr (&p_bic_data->Map, tile, &tile_x, &tile_y); + int owner_civ = tile->vtable->m38_Get_Territory_OwnerID (tile); + if ((owner_civ == this->ID) || (owner_civ == 0)) { + if (leader_can_build_district (this, inst->district_id) && + district_resource_prereqs_met (tile, tile_x, tile_y, inst->district_id, NULL)) + tr = 1; + } + } + } + } + + if (! tr && is->current_config.enable_districts && is->current_config.enable_bridge_districts && + (job == WJ_Build_Road || job == WJ_Build_Railroad)) { + if ((tile != NULL) && (tile != p_null_tile)) { + struct district_instance * inst = get_district_instance (tile); + if ((inst != NULL) && (inst->district_id == BRIDGE_DISTRICT_ID)) { + bool has_road = tile->vtable->m25_Check_Roads (tile, __, 0) != 0; + if (job == WJ_Build_Road) { + if (! has_road) + tr = 1; + } else { + bool has_rail = tile->vtable->m23_Check_Railroads (tile, __, 0) != 0; + if (has_road && ! has_rail) { + int req_tech = p_bic_data->WorkerJobs[job].RequireID; + if ((req_tech < 0) || + ((req_tech != p_bic_data->AdvanceCount) && + Leader_has_tech (&leaders[this->ID], __, req_tech))) + tr = 1; + } + } + } + } + } + + return tr; +} + +bool __fastcall +patch_Unit_can_hurry_production (Unit * this, int edx, City * city, bool exclude_cheap_improvements) +{ + if (is->current_config.allow_military_leaders_to_hurry_wonders && Unit_has_ability (this, __, UTA_Leader)) { + LeaderKind actual_kind = this->Body.leader_kind; + this->Body.leader_kind = LK_Scientific; + bool tr = Unit_can_hurry_production (this, __, city, exclude_cheap_improvements); + this->Body.leader_kind = actual_kind; + return tr; + } else + return Unit_can_hurry_production (this, __, city, exclude_cheap_improvements); +} + +bool __fastcall +patch_Unit_can_load (Unit * this, int edx, Unit * passenger) +{ + is->can_load_transport = this; + is->can_load_passenger = passenger; + bool tr; + + // If this potential passenger is tied to a different transport, do not allow it to load into this one + int tied_transport_id = -1; + if (is->current_config.limit_unit_loading_to_one_transport_per_turn && + (! Unit_has_ability (this, __, UTA_Army)) && + itable_look_up (&is->unit_transport_ties, passenger->Body.ID, &tied_transport_id) && + (this->Body.ID != tied_transport_id)) + tr = false; + + else + tr = Unit_can_load (this, __, passenger); + + is->can_load_transport = is->can_load_passenger = NULL; + return tr; +} + +void __fastcall +patch_Unit_load (Unit * this, int edx, Unit * transport) +{ + Unit_load (this, __, transport); + + // Tie the unit to the transport if configured to do so + if (is->current_config.limit_unit_loading_to_one_transport_per_turn && ! Unit_has_ability (transport, __, UTA_Army)) + itable_insert (&is->unit_transport_ties, this->Body.ID, transport->Body.ID); +} + +bool +any_enemies_near (Leader const * me, int tile_x, int tile_y, enum UnitTypeClasses class, int num_tiles) +{ + bool tr = false; + FOR_TILES_AROUND (tai, num_tiles, tile_x, tile_y) { + int enemy_on_this_tile = 0; + FOR_UNITS_ON (uti, tai.tile) { + UnitType const * unit_type = &p_bic_data->UnitTypes[uti.unit->Body.UnitTypeID]; + if (patch_Unit_is_visible_to_civ (uti.unit, __, me->ID, 0) && + (((int)class < 0) || (unit_type->Unit_Class == class))) { + if (me->At_War[uti.unit->Body.CivID]) { + if ((unit_type->Defence > 0) || (unit_type->Attack > 0)) { + enemy_on_this_tile = 1; + break; + } + } else + break; + } + } + if (enemy_on_this_tile) { + tr = true; + break; + } + } + return tr; +} + +bool +any_enemies_near_unit (Unit * unit, int num_tiles) +{ + UnitType * unit_type = &p_bic_data->UnitTypes[unit->Body.UnitTypeID]; + return any_enemies_near (&leaders[unit->Body.CivID], unit->Body.X, unit->Body.Y, unit_type->Unit_Class, num_tiles); +} + +void __fastcall +patch_Unit_ai_move_artillery (Unit * this) +{ + if ((! is->current_config.use_offensive_artillery_ai) || + ((this->Body.UnitTypeID < 0) || (this->Body.UnitTypeID >= p_bic_data->UnitTypeCount))) // Check for invalid unit type id which appears sometimes, IDK why + goto base_impl; + + Tile * on_tile = tile_at (this->Body.X, this->Body.Y); + City * in_city = get_city_ptr (on_tile->vtable->m45_Get_City_ID (on_tile)); + UnitType const * this_type = &p_bic_data->UnitTypes[this->Body.UnitTypeID]; + int num_escorters_req = this->vtable->eval_escort_requirement (this); + + if ((in_city == NULL) || (count_escorters (this) >= num_escorters_req)) + goto base_impl; + + // Don't assign escort if there are any enemies around because in that case it might be a serious mistake to take a defender out of the city + if (any_enemies_near_unit (this, 37)) + goto base_impl; + + // Find the strongest healthy defender the city has and assign that unit as an escort but make sure doing so doesn't leave the city + // defenseless. I think picking the strongest defender is the right choice here because the artillery pair is more likely to come under + // attack than a city and also leaving obsolete units in the city gives them a chance to upgrade. + int num_defenders = 0; + Unit * best_defender = NULL; + int best_defender_strength = -1; + FOR_UNITS_ON (uti, on_tile) { + Unit_Body * body = &uti.unit->Body; + UnitType * type = &p_bic_data->UnitTypes[body->UnitTypeID]; + if ((type->AI_Strategy & UTAI_Defence) && + (! UnitType_has_ability (type, __, UTA_Immobile)) && + (body->Damage == 0) && + ((body->UnitState == 0) || (body->UnitState == UnitState_Fortifying)) && + (body->escortee < 0)) { + num_defenders++; + int str = type->Defence * Unit_get_max_hp (uti.unit); + if (str > best_defender_strength) { + best_defender = uti.unit; + best_defender_strength = str; + } + } + } + if ((num_defenders >= 2) && (best_defender != NULL)) { + Unit_set_state (best_defender, __, 0); + Unit_set_escortee (best_defender, __, this->Body.ID); + } + +base_impl: + Unit_ai_move_artillery (this); + + // Recompute these since the unit might have moved + on_tile = tile_at (this->Body.X, this->Body.Y); + in_city = get_city_ptr (on_tile->vtable->m45_Get_City_ID (on_tile)); + + // Load the unit into a transport for a naval invasion if it's just sitting in a city with nothing else to do + if (is->current_config.use_offensive_artillery_ai && + (in_city != NULL) && + (this->Body.Moves == 0) && + (this->Body.UnitState == UnitState_Fortifying) && + (this->Body.Container_Unit < 0)) { + Unit * transport = Unit_find_transport (this, __, this->Body.X, this->Body.Y); + if (transport != NULL) { + + int transport_capacity = p_bic_data->UnitTypes[transport->Body.UnitTypeID].Transport_Capacity; + int units_in_transport, arty_in_transport; { + units_in_transport = arty_in_transport = 0; + FOR_UNITS_ON (uti, on_tile) + if (uti.unit->Body.Container_Unit == transport->Body.ID) { + units_in_transport++; + arty_in_transport += (p_bic_data->UnitTypes[uti.unit->Body.UnitTypeID].AI_Strategy & UTAI_Artillery) != 0; + } + } + + // Check that there's space for the arty unit and an escort, and that the transport does not have more than one arty per three + // spaces so we're less likely to assemble invasion forces composed of nothing but artillery. + if ((units_in_transport + 2 <= transport_capacity) && + (arty_in_transport < not_below (1, transport_capacity / 3))) { + Unit_set_escortee (this, __, -1); + patch_Unit_load (this, __, transport); + } + } + } +} + +// Estimates the number of turns necessary to move the given unit to the given tile and writes it to *out_num_turns. Returns 0 if there is no path +// from the unit's current position to the given tile, 1 otherwise. +int +estimate_travel_time (Unit * unit, int to_tile_x, int to_tile_y, int * out_num_turns) +{ + int dist_in_mp; + patch_Trade_Net_set_unit_path (is->trade_net, __, unit->Body.X, unit->Body.Y, to_tile_x, to_tile_y, unit, unit->Body.CivID, 1, &dist_in_mp); + dist_in_mp += unit->Body.Moves; // Add MP already spent this turn to the distance + int max_mp = patch_Unit_get_max_move_points (unit); + if ((dist_in_mp >= 0) && (max_mp > 0)) { + *out_num_turns = dist_in_mp / max_mp; + return 1; + } else + return 0; // No path or unit cannot move +} + +City * +find_nearest_established_city (Unit * unit, int continent_id) +{ + int lowest_unattractiveness = INT_MAX; + City * least_unattractive_city = NULL; + FOR_CITIES_OF (coi,unit->Body.CivID) { + Tile * city_tile = tile_at (coi.city->Body.X, coi.city->Body.Y); + if (continent_id == city_tile->vtable->m46_Get_ContinentID (city_tile)) { + int dist_in_turns; + if (! estimate_travel_time (unit, coi.city->Body.X, coi.city->Body.Y, &dist_in_turns)) + continue; + int unattractiveness = 10 * dist_in_turns; + unattractiveness = (10 + unattractiveness) / (10 + coi.city->Body.Population.Size); + if (coi.city->Body.CultureIncome > 0) + unattractiveness /= 5; + if (unattractiveness < lowest_unattractiveness) { + lowest_unattractiveness = unattractiveness; + least_unattractive_city = coi.city; + } + } + } + return least_unattractive_city; +} + +bool __fastcall +patch_Unit_ai_can_form_army (Unit * this) +{ + int build_army_action = UCV_Build_Army & 0x0FFFFFFF; // Mask out top four category bits + UnitType * type = &p_bic_data->UnitTypes[this->Body.UnitTypeID]; + if (is->current_config.patch_ai_can_form_army_without_special_ability && ((type->Special_Actions & build_army_action) == 0)) + return false; + else + return Unit_ai_can_form_army (this); +} + +void __fastcall +patch_Unit_ai_move_leader (Unit * this) +{ + if (! is->current_config.replace_leader_unit_ai) { + Unit_ai_move_leader (this); + return; + } + + Tile * tile = tile_at (this->Body.X, this->Body.Y); + int continent_id = tile->vtable->m46_Get_ContinentID (tile); + + // Disband the unit if it's on a continent with no cities of its civ. This is what the original logic does. + if (leaders[this->Body.CivID].city_count_per_cont[continent_id] == 0) { + Unit_disband (this); + return; + } + + // Flee if the unit is near an enemy without adequate escort + int has_adequate_escort; { + int escorter_count = 0; int any_healthy_escorters = 0; int index; for (int escorter_id = Unit_next_escorter_id (this, __, &index, true); escorter_id >= 0; escorter_id = Unit_next_escorter_id (this, __, &index, false)) { @@ -14708,111 +21027,1383 @@ patch_Unit_ai_move_leader (Unit * this) any_healthy_escorters |= (remaining_health >= 3) || (escorter->Body.Damage == 0); } } - has_adequate_escort = (escorter_count > 0) && any_healthy_escorters; + has_adequate_escort = (escorter_count > 0) && any_healthy_escorters; + } + if ((! has_adequate_escort) && any_enemies_near_unit (this, 49)) { + Unit_set_state (this, __, UnitState_Fleeing); + bool done = this->vtable->work (this); + if (done || (this->Body.UnitState != 0)) + return; + } + + // Move along path if the unit already has one set + byte next_move = Unit_pop_next_move_from_path (this); + if (next_move > 0) { + this->vtable->Move (this, __, next_move, 0); + return; + } + + // Start a science age if we can + // It would be nice to compute a value for this and compare it to the option of rushing production but it's hard to come up with a valuation + if (is->current_config.patch_science_age_bug && Unit_ai_can_start_science_age (this)) { + Unit_start_science_age (this); + return; + } + + // Estimate the value of creating an army. First test if that's even a possiblity and if not leave the value at -1 so rushing production is + // always preferable (note we can't use Unit_ai_can_form_army for this b/c it returns false for units not in a city). The value of forming + // an army is "infinite" if we don't already have one and otherwise it depends on the army unit's shield cost +/- 25% for each point of + // aggression divided by the number of armies already in the field. + int num_armies = leaders[this->Body.CivID].Armies_Count; + int form_army_value = -1; + int build_army_action = UCV_Build_Army & 0x0FFFFFFF; // Mask out top four category bits + if ((this->Body.leader_kind & LK_Military) && + (p_bic_data->UnitTypes[this->Body.UnitTypeID].Special_Actions & build_army_action) && + ((num_armies + 1) * p_bic_data->General.ArmySupportCities <= leaders[this->Body.CivID].Cities_Count) && + (p_bic_data->General.BuildArmyUnitID >= 0) && + (p_bic_data->General.BuildArmyUnitID < p_bic_data->UnitTypeCount)) { + if (num_armies < 1) + form_army_value = INT_MAX; + else { + form_army_value = p_bic_data->UnitTypes[p_bic_data->General.BuildArmyUnitID].Cost; + int aggression_level = p_bic_data->Races[leaders[this->Body.CivID].RaceID].AggressionLevel; // aggression level varies between -2 and +2 + form_army_value = (form_army_value * (4 + aggression_level)) / 4; + if (num_armies > 1) + form_army_value /= num_armies; + } + } + + // Estimate the value of rushing production in every city on this continent and remember the highest one + City * best_rush_loc = NULL; + int best_rush_value = -1; + FOR_CITIES_OF (coi, this->Body.CivID) { + City * city = coi.city; + Tile * city_tile = tile_at (city->Body.X, city->Body.Y); + if ((continent_id == city_tile->vtable->m46_Get_ContinentID (city_tile)) && + patch_Unit_can_hurry_production (this, __, city, false)) { + // Base value is equal to the number of shields rushing would save + int value = City_get_order_cost (city) - City_get_order_progress (city) - city->Body.ProductionIncome; + if (value <= 0) + continue; + + // Consider distance: Reduce the value by the number of shields produced while the leader is en route to rush. + int dist_in_turns; + if (! estimate_travel_time (this, city->Body.X, city->Body.Y, &dist_in_turns)) + continue; // no path or unit cannot move + value -= dist_in_turns * city->Body.ProductionIncome; + if (value <= 0) + continue; + + // Consider corruption: Scale down the value of rushing an improvement by the corruption rate of the city. + // This is to reflect the fact that infrastructure is more valuable in less corrupt cities. But do not apply + // this to wonders since their benefit is in most cases not lessened by local corruption. + Improvement * improv = (city->Body.Order_Type == COT_Improvement) ? &p_bic_data->Improvements[city->Body.Order_ID] : NULL; + int is_wonder = (improv != NULL) && (0 != (improv->Characteristics & (ITC_Small_Wonder | ITC_Wonder))); + if ((improv != NULL) && (! is_wonder)) { + int good_shields = city->Body.ProductionIncome; + int corrupt_shields = city->Body.ProductionLoss; + if (good_shields + corrupt_shields > 0) + value = (value * good_shields) / (good_shields + corrupt_shields); + else + continue; + } + + if ((value > 0) && (value > best_rush_value)) { + best_rush_loc = city; + best_rush_value = value; + } + } + } + + // Hurry production or form an army depending on the estimated values of doing so. We might have to move to a (different) city if that's where + // we want to rush production or if we want to form an army but aren't already in a city. + City * in_city = get_city_ptr (tile->vtable->m45_Get_City_ID (tile)); + City * moving_to_city = NULL; + if ((best_rush_loc != NULL) && (best_rush_value > form_army_value)) { + if (best_rush_loc == in_city) { + Unit_hurry_production (this); + return; + } else + moving_to_city = best_rush_loc; + } else if ((form_army_value > -1) && patch_Unit_ai_can_form_army (this)) { + Unit_form_army (this); + return; + } else if (in_city == NULL) { + // Nothing to do. Try to find a close, established city to move to & wait in. + moving_to_city = find_nearest_established_city (this, continent_id); + } + if (moving_to_city) { + int first_move = patch_Trade_Net_set_unit_path (is->trade_net, __, this->Body.X, this->Body.Y, moving_to_city->Body.X, moving_to_city->Body.Y, this, this->Body.CivID, 0x101, NULL); + if (first_move > 0) { + Unit_set_escortee (this, __, -1); + this->vtable->Move (this, __, first_move, 0); + return; + } + } + + // Nothing to do, nowhere to go, just fortify in place. + Unit_set_escortee (this, __, -1); + Unit_set_state (this, __, UnitState_Fortifying); +} + +int +measure_strength_in_army (UnitType * type) +{ + return ((type->Attack + type->Defence) * (4 + type->Hit_Point_Bonus)) / 4; +} + +bool __fastcall +patch_impl_ai_is_good_army_addition (Unit * this, int edx, Unit * candidate) +{ + if (! is->current_config.fix_ai_army_composition) + return impl_ai_is_good_army_addition (this, __, candidate); + + UnitType * candidate_type = &p_bic_data->UnitTypes[candidate->Body.UnitTypeID]; + Tile * tile = tile_at (this->Body.X, this->Body.Y); + + if (((candidate_type->AI_Strategy & UTAI_Offence) == 0) || + UnitType_has_ability (candidate_type, __, UTA_Hidden_Nationality)) + return false; + + int num_units_in_army = 0, + army_min_speed = INT_MAX, + army_min_strength = INT_MAX; + FOR_UNITS_ON (uti, tile) { + if (uti.unit->Body.Container_Unit == this->Body.ID) { + num_units_in_army++; + int movement = p_bic_data->UnitTypes[uti.unit->Body.UnitTypeID].Movement; + if (movement < army_min_speed) + army_min_speed = movement; + int member_strength = measure_strength_in_army (&p_bic_data->UnitTypes[uti.unit->Body.UnitTypeID]); + if (member_strength < army_min_strength) + army_min_strength = member_strength; + } + } + + return (num_units_in_army == 0) || + ((candidate_type->Movement >= army_min_speed) && + (measure_strength_in_army (candidate_type) >= army_min_strength)); +} + +int +rate_artillery (UnitType * type) +{ + int tr = type->Attack + type->Defence + 2 * type->Bombard_Strength * type->FireRate; + + // include movement + int moves = type->Movement; + if (moves >= 2) + tr = tr * (moves + 1) / 2; + + // include range + int range = type->Bombard_Range; + if (range >= 2) + tr = tr * (range + 1) / 2; + + // include extra hp + if (type->Hit_Point_Bonus > 0) + tr = tr * (4 + type->Hit_Point_Bonus) / 4; + + return tr; +} + +int +rate_bomber (UnitType * type) +{ + int tr = type->Bombard_Strength * type->FireRate + type->Defence; + + // include range + tr = tr * (10 + type->OperationalRange) / 10; + + // include cost + tr = (tr * 100) / (100 + type->Cost); + + // include abilities + if (UnitType_has_ability (type, __, UTA_Blitz)) + tr = tr * (type->Movement + 1) / 2; + if (UnitType_has_ability (type, __, UTA_Lethal_Land_Bombardment)) + tr = tr * 3 / 2; + if (UnitType_has_ability (type, __, UTA_Lethal_Sea_Bombardment)) + tr = tr * 5 / 4; + if (UnitType_has_ability (type, __, UTA_Stealth)) + tr = tr * 3 / 2; + + // include extra hp + if (type->Hit_Point_Bonus > 0) + tr = tr * (4 + type->Hit_Point_Bonus) / 4; + + return tr; +} + +bool __fastcall +patch_City_can_build_unit (City * this, int edx, int unit_type_id, bool exclude_upgradable, int param_3, bool allow_kings) +{ + bool base = City_can_build_unit (this, __, unit_type_id, exclude_upgradable, param_3, allow_kings); + + if (base) { + // Apply building prereqs + int building_prereq; + if (itable_look_up (&is->current_config.building_unit_prereqs, unit_type_id, &building_prereq)) { + // If the prereq is an encoded building ID + if (building_prereq & 1) { + if (! has_active_building (this, building_prereq >> 1)) + return false; + + // Else it's a pointer to a list of building IDs + } else { + int * list = (int *)building_prereq; + for (int n = 0; n < MAX_BUILDING_PREREQS_FOR_UNIT; n++) + if ((list[n] >= 0) && ! has_active_building (this, list[n])) + return false; + } + } + + // Apply unit type limit + int available; + if (get_available_unit_count (&leaders[this->Body.CivID], unit_type_id, &available) && (available <= 0)) + return false; + } + + if (is->current_config.enable_districts) { + bool is_human = (*p_human_player_bits & (1 << this->Body.CivID)) != 0; + UnitType * type = &p_bic_data->UnitTypes[unit_type_id]; + + // Bail if tech reqs are not met + int prereq_id = type->AdvReq; + if (prereq_id >= 0 && ! Leader_has_tech (&leaders[this->Body.CivID], __, prereq_id)) + return false; + + if (! Leader_can_build_unit (&leaders[this->Body.CivID], __, unit_type_id, 1, false)) + return false; + + // Superficially allow the AI to choose the unit for scoring and production. + // If a disallowed air/naval unit is chosen in ai_choose_production, we'll swap it out for a feasible fallback later + // after prioritizing the aerodrome/port to be built + if (! is_human && ( + (type->Unit_Class == UTC_Air || city_can_build_district (this, AERODROME_DISTRICT_ID)) || + (type->Unit_Class == UTC_Sea || city_can_build_district (this, PORT_DISTRICT_ID))) + ) + return base; + + // Air units + if (type->Unit_Class == UTC_Air) { + if (is->current_config.enable_aerodrome_districts && is->current_config.air_units_use_aerodrome_districts_not_cities) { + if (find_pending_district_request (this, AERODROME_DISTRICT_ID) != NULL) + return false; + if (! city_can_build_district (this, AERODROME_DISTRICT_ID)) + return false; + return city_has_required_district (this, AERODROME_DISTRICT_ID); + } + // Naval units + } else if (type->Unit_Class == UTC_Sea) { + if (is->current_config.enable_port_districts && is->current_config.naval_units_use_port_districts_not_cities) { + if (find_pending_district_request (this, PORT_DISTRICT_ID) != NULL) + return false; + if (! city_can_build_district (this, PORT_DISTRICT_ID)) + return false; + return city_has_required_district (this, PORT_DISTRICT_ID); + } + } + } + + return base; +} + +int __fastcall +patch_City_get_largest_adjacent_sea_within_work_area (City * this) +{ + if (is->current_config.enable_districts && is->current_config.expand_water_tile_checks_to_city_work_area) { + int improv_id = is->current_evaluating_improve_id; + if ((improv_id >= 0) && (improv_id < p_bic_data->ImprovementsCount)) { + + // If Coastal Fortress, default to original logic (city must be next to coast) + if (p_bic_data->Improvements[improv_id].Naval_Bombard_Defence > 0) + return City_get_largest_adjacent_sea (this); + } + int lake_size_threshold = 21; + int largest_size = 0; + int largest_continent_id = -1; + FOR_WORK_AREA_AROUND (wai, this->Body.X, this->Body.Y) { + if (wai.tile->vtable->m35_Check_Is_Water (wai.tile)) { + int continent_id = wai.tile->vtable->m46_Get_ContinentID (wai.tile); + if ((continent_id < 0) || (continent_id >= p_bic_data->Map.Continent_Count)) + continue; + Continent * continent = &p_bic_data->Map.Continents[continent_id]; + if (continent->Body.TileCount <= lake_size_threshold && ! is->current_config.enable_canal_districts) + continue; + if (p_bic_data->Map.Continents[continent_id].Body.TileCount > largest_size) { + largest_size = p_bic_data->Map.Continents[continent_id].Body.TileCount; + largest_continent_id = continent_id; + } + } + } + return largest_continent_id; + } + return City_get_largest_adjacent_sea (this); +} + +bool __fastcall +patch_Map_impl_is_near_river_within_work_area (Map * this, int edx, int x, int y, int num_tiles) +{ + if (is->current_config.enable_districts && is->current_config.expand_water_tile_checks_to_city_work_area) { + FOR_WORK_AREA_AROUND (wai, x, y) { + if (wai.tile->vtable->m37_Get_River_Code (wai.tile) != 0) { + return true; + } + } + return false; + } + + return Map_impl_is_near_river (this, __, x, y, num_tiles); +} + +bool __fastcall +patch_Map_impl_is_near_lake_within_work_area (Map * this, int edx, int x, int y, int num_tiles) +{ + if (is->current_config.enable_districts && is->current_config.expand_water_tile_checks_to_city_work_area) { + int lake_size_threshold = 21; + FOR_WORK_AREA_AROUND (wai, x, y) { + if (wai.tile->vtable->m35_Check_Is_Water (wai.tile)) { + int continent_id = wai.tile->vtable->m46_Get_ContinentID (wai.tile); + if ((continent_id < 0) || (continent_id >= p_bic_data->Map.Continent_Count)) + continue; + Continent * continent = &p_bic_data->Map.Continents[continent_id]; + if (continent->Body.TileCount <= lake_size_threshold) + return true; + } + } + return false; + } + + return Map_impl_is_near_lake (this, __, x, y, num_tiles); +} + +bool __fastcall +patch_Map_impl_has_fresh_water_within_work_area (Map * this, int edx, int tile_x, int tile_y) +{ + if (is->current_config.enable_districts && is->current_config.expand_water_tile_checks_to_city_work_area) { + int improv_id = is->current_evaluating_improve_id; + if ((improv_id >= 0) && (improv_id < p_bic_data->ImprovementsCount)) { + + // If an Aqueduct, default to original logic (city must be next to coast) + if ((p_bic_data->Improvements[improv_id].ImprovementFlags & ITF_Allows_City_Level_2) != 0) + return Map_impl_has_fresh_water(this, __, tile_x, tile_y); + } + if (patch_Map_impl_is_near_river_within_work_area (this, __, tile_x, tile_y, 1)) + return true; + if (patch_Map_impl_is_near_lake_within_work_area (this, __, tile_x, tile_y, 1)) + return true; + return false; + } + + return Map_impl_has_fresh_water (this, __, tile_x, tile_y); +} + +bool +city_meets_district_prereqs_to_build_improvement (City * city, int i_improv, bool apply_strict_rules) +{ + // Different logic for human vs AI players + bool is_human = (*p_human_player_bits & (1 << city->Body.CivID)) != 0; + + Improvement * improv = &p_bic_data->Improvements[i_improv]; + bool requires_river = (improv->ImprovementFlags & ITF_Must_Be_Near_River) != 0; + + // Check if the improvement requires a district + bool needs_district = city_requires_district_for_improvement (city, i_improv, NULL); + struct district_building_prereq_list * prereq_list = get_district_building_prereq_list (i_improv); + + // District is either not needed or already built + if (! needs_district) + return true; + + if (prereq_list == NULL) + return false; + + bool has_buildable_candidate = false; + bool has_wonder_candidate = false; + bool has_non_wonder_candidate = false; + for (int i = 0; i < prereq_list->count; i++) { + int district_id = prereq_list->district_ids[i]; + if (district_id < 0) + continue; + if (! leader_can_build_district (&leaders[city->Body.CivID], district_id)) + continue; + has_buildable_candidate = true; + if (district_id == WONDER_DISTRICT_ID) + has_wonder_candidate = true; + else + has_non_wonder_candidate = true; + } + if (! has_buildable_candidate) + return false; + + bool is_wonder = is->current_config.enable_wonder_districts && improv->Characteristics & (ITC_Wonder | ITC_Small_Wonder); + + // Check that we have the necessary terrain + bool has_terrain_for_district = false; + bool has_terrain_for_wonder = false; + bool has_river_terrain_for_district = false; + FOR_WORK_AREA_AROUND (wai, city->Body.X, city->Body.Y) { + Tile * tile = wai.tile; + for (int i = 0; i < prereq_list->count; i++) { + int district_id = prereq_list->district_ids[i]; + if (district_id < 0) + continue; + if (! leader_can_build_district (&leaders[city->Body.CivID], district_id)) + continue; + if (! tile_suitable_for_district (tile, district_id, city, NULL)) + continue; + + if (is_wonder && district_id == WONDER_DISTRICT_ID) { + if (! wonder_is_buildable_on_tile (tile, i_improv)) + continue; + } + + has_terrain_for_district = true; + if (requires_river && tile->vtable->m37_Get_River_Code (tile) != 0) + has_river_terrain_for_district = true; + + if (is_wonder && district_id == WONDER_DISTRICT_ID) { + if (! requires_river || tile->vtable->m37_Get_River_Code (tile) != 0) { + has_terrain_for_wonder = true; + } + } + } + } + + bool requires_wonder_terrain = is_wonder && has_wonder_candidate && ! has_non_wonder_candidate; + if (! has_terrain_for_district || + (requires_river && ! has_river_terrain_for_district) || + (requires_wonder_terrain && ! has_terrain_for_wonder)) { + return false; + } + + // If human has everything needed except a built district (which is buildable), allow relaxed checks so UI can gray entry out + if (is_human) { + return ! apply_strict_rules; + } + + // If AI already has a pending district request for this required district, return false + // to prevent wasting a turn trying to choose this improvement + for (int i = 0; i < prereq_list->count; i++) { + int district_id = prereq_list->district_ids[i]; + if (district_id < 0) + continue; + if (find_pending_district_request (city, district_id) != NULL) + return false; + } + + // Superficially allow the AI to choose the improvement for scoring and production. + // If a disallowed improvement is chosen in ai_choose_production, we'll swap it out for a feasible fallback later + // after prioritizing the district to be built + return true; +} + +bool __fastcall +patch_City_can_build_improvement (City * this, int edx, int i_improv, bool apply_strict_rules) +{ + is->current_evaluating_improve_id = i_improv; + + char ss[200]; + snprintf (ss, sizeof ss, "patch_City_can_build_improvement:evaluating improvement %s (%d)\n", + p_bic_data->Improvements[i_improv].Name.S, i_improv); + (*p_OutputDebugStringA) (ss); + + // First defer to the base game's logic + bool base = City_can_build_improvement (this, __, i_improv, apply_strict_rules); + if (! base) return false; + if (! is->current_config.enable_districts) return base; + + bool can_build = city_meets_district_prereqs_to_build_improvement (this, i_improv, apply_strict_rules); + is->current_evaluating_improve_id = -1; + + return can_build; +} + +bool +ai_handle_district_production_requirements (City * city, City_Order * out) +{ + clear_best_feasible_order (city); + bool swapped_to_fallback = false; + City_Order fallback_order = {0}; + int required_district_id = -1; + bool should_mark_district = false; + + char ss[200]; + + if (is->current_config.enable_districts && + (out->OrderID >= 0)) { + bool needs_wonder_district = false; + bool requires_district = false; + bool needs_district = false; + + if ((out->OrderType == COT_Unit) && + (out->OrderID < p_bic_data->UnitTypeCount)) { + UnitType * type = &p_bic_data->UnitTypes[out->OrderID]; + if ((type->Unit_Class == UTC_Air) && + is->current_config.enable_aerodrome_districts && + is->current_config.air_units_use_aerodrome_districts_not_cities && + (! city_has_required_district (city, AERODROME_DISTRICT_ID))) { + required_district_id = AERODROME_DISTRICT_ID; + needs_district = true; + should_mark_district = true; + } else if ((type->Unit_Class == UTC_Sea) && + is->current_config.enable_port_districts && + is->current_config.naval_units_use_port_districts_not_cities && + (! city_has_required_district (city, PORT_DISTRICT_ID))) { + required_district_id = PORT_DISTRICT_ID; + needs_district = true; + should_mark_district = true; + } + } else if ((out->OrderType == COT_Improvement) && + (out->OrderID < p_bic_data->ImprovementsCount)) { + // Check if AI is trying to build a wonder without an incomplete wonder district + requires_district = city_requires_district_for_improvement (city, out->OrderID, &required_district_id); + if (is->current_config.enable_wonder_districts) { + Improvement * improv = &p_bic_data->Improvements[out->OrderID]; + if ((improv->Characteristics & (ITC_Wonder | ITC_Small_Wonder)) && + (! city_has_wonder_district_with_no_completed_wonder (city, out->OrderID))) { + snprintf (ss, sizeof ss, "ai_handle_district_production_requirements: City %d (%s) needs wonder district to build wonder improvement %d\n", + city->Body.ID, city->Body.CityName, out->OrderID); + (*p_OutputDebugStringA) (ss); + needs_wonder_district = true; + if (required_district_id < 0) { + required_district_id = WONDER_DISTRICT_ID; + } + } + } + needs_district = needs_wonder_district || requires_district; + } + + if (needs_district) { + struct ai_best_feasible_order * stored = get_best_feasible_order (city); + if (stored != NULL) { + bool fallback_is_feasible = true; + if (stored->order.OrderType == COT_Improvement) { + if ((stored->order.OrderID < 0) || + (stored->order.OrderID >= p_bic_data->ImprovementsCount)) + fallback_is_feasible = false; + + // Check if fallback requires a district the city doesn't have + if (fallback_is_feasible && + city_requires_district_for_improvement (city, stored->order.OrderID, NULL)) + fallback_is_feasible = false; + + // If original order was a wonder, ensure fallback is not also a wonder + if (fallback_is_feasible && needs_wonder_district) { + Improvement * fallback_improv = &p_bic_data->Improvements[stored->order.OrderID]; + if (fallback_improv->Characteristics & (ITC_Wonder | ITC_Small_Wonder)) + fallback_is_feasible = false; + } + + // If fallback is a wonder, check if it has an incomplete wonder district + if (fallback_is_feasible && is->current_config.enable_wonder_districts) { + Improvement * fallback_improv = &p_bic_data->Improvements[stored->order.OrderID]; + if ((fallback_improv->Characteristics & (ITC_Wonder | ITC_Small_Wonder)) && + (! city_has_wonder_district_with_no_completed_wonder (city, stored->order.OrderID))) + fallback_is_feasible = false; + } + } else if (stored->order.OrderType == COT_Unit) { + if ((stored->order.OrderID < 0) || + (stored->order.OrderID >= p_bic_data->UnitTypeCount)) + fallback_is_feasible = false; + if (fallback_is_feasible) { + UnitType * type = &p_bic_data->UnitTypes[stored->order.OrderID]; + if (type->Unit_Class != UTC_Land) + fallback_is_feasible = false; + if ((type->Unit_Class == UTC_Air) && + is->current_config.enable_aerodrome_districts && + is->current_config.air_units_use_aerodrome_districts_not_cities && + (! city_has_required_district (city, AERODROME_DISTRICT_ID))) + fallback_is_feasible = false; + if ((type->Unit_Class == UTC_Sea) && + is->current_config.enable_port_districts && + is->current_config.naval_units_use_port_districts_not_cities && + (! city_has_required_district (city, PORT_DISTRICT_ID))) + fallback_is_feasible = false; + } + } else + fallback_is_feasible = false; + + if (fallback_is_feasible) { + if (out->OrderType == COT_Improvement) { + // Remember pending building order for any improvement that requires a district + snprintf (ss, sizeof ss, "ai_handle_district_production_requirements: Remembering fallback pending building order for city %d (%s): order type %d id %d\n", + city->Body.ID, city->Body.CityName, out->OrderType, out->OrderID); + (*p_OutputDebugStringA) (ss); + remember_pending_building_order (city, out->OrderID); + } + + fallback_order = stored->order; + swapped_to_fallback = true; + } + } + } + } + + if (swapped_to_fallback) { + *out = fallback_order; + } + if ((swapped_to_fallback || should_mark_district) && (required_district_id >= 0)) + mark_city_needs_district (city, required_district_id); + + clear_best_feasible_order (city); + return swapped_to_fallback; +} + +void __fastcall +patch_City_ai_choose_production (City * this, int edx, City_Order * out) +{ + is->ai_considering_production_for_city = this; + City_ai_choose_production (this, __, out); + + char ss[200]; + snprintf (ss, sizeof ss, "patch_City_ai_choose_production: City %d (%s) chose order type %d id %d\n", + this->Body.ID, this->Body.CityName, out->OrderType, out->OrderID); + (*p_OutputDebugStringA) (ss); + + if (is->current_config.enable_districts) { + if (ai_handle_district_production_requirements (this, out)) { + return; + } + } + + Leader * me = &leaders[this->Body.CivID]; + int arty_ratio = is->current_config.ai_build_artillery_ratio; + int bomber_ratio = is->current_config.ai_build_bomber_ratio; + + // Check if AI-build-more-artillery mod option is activated and this city is building something that we might want to switch to artillery + if ((arty_ratio > 0) && + (out->OrderType == COT_Unit) && + (out->OrderID >= 0) && (out->OrderID < p_bic_data->UnitTypeCount) && + (p_bic_data->UnitTypes[out->OrderID].AI_Strategy & (UTAI_Offence | UTAI_Defence))) { + + // Check how many offense/defense/artillery units this AI has already built. We'll only force it to build arty if it has a minimum + // of one defender per city, one attacker per two cities, and of course if it's below the ratio limit. + int num_attackers = me->AI_Strategy_Unit_Counts[0], + num_defenders = me->AI_Strategy_Unit_Counts[1], + num_artillery = me->AI_Strategy_Unit_Counts[2]; + if ((num_attackers > me->Cities_Count / 2) && + (num_defenders > me->Cities_Count) && + (100*num_artillery < arty_ratio*(num_attackers + num_defenders))) { + + // Loop over all build options to determine the best artillery unit available. Record its attack power and also find & record + // the highest attack power available from any offensive unit so we can compare them. + int best_arty_type_id = -1, + best_arty_rating = -1, + best_arty_strength = -1, + best_attacker_strength = -1; + for (int n = 0; n < p_bic_data->UnitTypeCount; n++) { + UnitType * type = &p_bic_data->UnitTypes[n]; + if ((type->AI_Strategy & (UTAI_Artillery | UTAI_Offence)) && + patch_City_can_build_unit (this, __, n, 1, 0, 0)) { + if (type->AI_Strategy & UTAI_Artillery) { + int this_rating = rate_artillery (&p_bic_data->UnitTypes[n]); + if (this_rating > best_arty_rating) { + best_arty_type_id = n; + best_arty_rating = this_rating; + best_arty_strength = type->Bombard_Strength * type->FireRate; + } + } else { // attacker + int this_strength = (type->Attack * (4 + type->Hit_Point_Bonus)) / 4; + if (this_strength > best_attacker_strength) + best_attacker_strength = this_strength; + } + } + } + + // Randomly switch city production to the artillery unit if we found one + if (best_arty_type_id >= 0) { + int chance = 12 * arty_ratio / 10; + + // Scale the chance of switching by the ratio of the attack power the artillery would provide to the attack power + // of the strongest offensive unit. This way the AI will rapidly build artillery up to the ratio limit only when + // artillery are its best way of dealing damage. + // Some example numbers: + // | Best attacker (str) | Best arty (str) | Chance w/ ratio = 20, damage% = 50 + // | Swordsman (3) | Catapult (4) | 16 + // | Knight (4) | Trebuchet (6) | 18 + // | Cavalry (6) | Cannon (8) | 16 + // | Cavalry (6) | Artillery (24) | 48 + // | Tank (16) | Artillery (24) | 18 + if (best_attacker_strength > 0) + chance = (chance * best_arty_strength * is->current_config.ai_artillery_value_damage_percent) / (best_attacker_strength * 100); + + if (rand_int (p_rand_object, __, 100) < chance) + out->OrderID = best_arty_type_id; + } + } + + } else if ((bomber_ratio > 0) && + (out->OrderType == COT_Unit) && + (out->OrderID >= 0) && (out->OrderID < p_bic_data->UnitTypeCount) && + (p_bic_data->UnitTypes[out->OrderID].AI_Strategy & UTAI_Air_Defence)) { + int num_fighters = me->AI_Strategy_Unit_Counts[7], + num_bombers = me->AI_Strategy_Unit_Counts[6]; + if (100 * num_bombers < bomber_ratio * num_fighters) { + int best_bomber_type_id = -1, + best_bomber_rating = -1; + for (int n = 0; n < p_bic_data->UnitTypeCount; n++) { + UnitType * type = &p_bic_data->UnitTypes[n]; + if ((type->AI_Strategy & UTAI_Air_Bombard) && + patch_City_can_build_unit (this, __, n, 1, 0, 0)) { + int this_rating = rate_bomber (&p_bic_data->UnitTypes[n]); + if (this_rating > best_bomber_rating) { + best_bomber_type_id = n; + best_bomber_rating = this_rating; + } + } + } + + if (best_bomber_type_id >= 0) { + int chance = 12 * bomber_ratio / 10; + if (rand_int (p_rand_object, __, 100) < chance) + out->OrderID = best_bomber_type_id; + } + } + } + + is->ai_considering_production_for_city = NULL; +} + +int __fastcall +patch_Unit_disembark_passengers (Unit * this, int edx, int tile_x, int tile_y) +{ + // Break any escort relationships if the escorting unit can't move onto the target tile. This prevents a freeze where the game continues + // trying to disembark units because an escortee looks like it could be disembarked except it can't because it won't move if its escorter + // can't be moved first. + Tile * tile = tile_at (this->Body.X, this->Body.Y), + * target = tile_at (tile_x , tile_y); + if (is->current_config.patch_blocked_disembark_freeze && (tile != NULL) && (target != NULL)) { + enum SquareTypes target_terrain = target->vtable->m50_Get_Square_BaseType (target); + FOR_UNITS_ON (uti, tile) { + Unit * escortee = get_unit_ptr (uti.unit->Body.escortee); + if ( (escortee != NULL) + && (uti.unit->Body.Container_Unit == this->Body.ID) + && (escortee->Body.Container_Unit == this->Body.ID) + && ( UnitType_has_ability (&p_bic_data->UnitTypes[uti.unit->Body.UnitTypeID], __, UTA_Immobile) + || ( is->current_config.disallow_trespassing + && check_trespassing (uti.unit->Body.CivID, tile, target) + && ! is_allowed_to_trespass (uti.unit)) + || Unit_is_terrain_impassable (uti.unit, __, target_terrain))) + Unit_set_escortee (uti.unit, __, -1); + } + } + + return Unit_disembark_passengers (this, __, tile_x, tile_y); +} + +// Returns whether or not the current deal being offered to the AI on the trade screen would be accepted. How advantageous the AI thinks the +// trade is for itself is stored in out_their_advantage if it's not NULL. This advantage is measured in gold, if it's positive it means the +// AI thinks it's gaining that much value from the trade and if it's negative it thinks it would be losing that much. I don't know what would +// happen if this function were called while the trade screen is not active. +bool +is_current_offer_acceptable (int * out_their_advantage) +{ + int their_id = p_diplo_form->other_party_civ_id; + + DiploMessage consideration = Leader_consider_trade ( + &leaders[their_id], + __, + &p_diplo_form->our_offer_lists[their_id], + &p_diplo_form->their_offer_lists[their_id], + p_main_screen_form->Player_CivID, + 0, true, 0, 0, + out_their_advantage, + NULL, NULL); + + return consideration == DM_AI_ACCEPT; +} + +// Adds an offer of gold to the list and returns it. If one already exists in the list, returns a pointer to it. +TradeOffer * +offer_gold (TradeOfferList * list, int is_lump_sum, int * is_new_offer) +{ + if (list->length > 0) + for (TradeOffer * offer = list->first; offer != NULL; offer = offer->next) + if ((offer->kind == 7) && (offer->param_1 == is_lump_sum)) { + *is_new_offer = 0; + return offer; + } + + TradeOffer * tr = new (sizeof *tr); + *tr = (struct TradeOffer) { + .vtable = p_trade_offer_vtable, + .kind = 7, // TODO: Replace with enum + .param_1 = is_lump_sum, + .param_2 = 0, + .next = NULL, + .prev = NULL + }; + + if (list->length > 0) { + tr->prev = list->last; + list->last->next = tr; + list->last = tr; + list->length += 1; + } else { + list->last = list->first = tr; + list->length = 1; + } + + *is_new_offer = 1; + return tr; +} + +// Removes offer from list of offers but does not free it. Assumes offer is in the list. +void +remove_offer (TradeOfferList * list, TradeOffer * offer) +{ + if (list->length == 1) { + list->first = list->last = NULL; + list->length = 0; + } else if (list->length > 1) { + TradeOffer * prev = offer->prev, * next = offer->next; + if (prev) + prev->next = next; + if (next) + next->prev = prev; + if (list->first == offer) + list->first = next; + if (list->last == offer) + list->last = prev; + list->length -= 1; + } + offer->prev = offer->next = NULL; +} + +void __fastcall +patch_PopupForm_set_text_key_and_flags (PopupForm * this, int edx, char * script_path, char * text_key, int param_3, int param_4, int param_5, int param_6) +{ + int * p_stack = (int *)&script_path; + int ret_addr = p_stack[-1]; + + int is_initial_gold_trade = (ret_addr == ADDR_SETUP_INITIAL_GOLD_ASK_RETURN) || (ret_addr == ADDR_SETUP_INITIAL_GOLD_OFFER_RETURN), + is_modifying_gold_trade = (ret_addr == ADDR_SETUP_MODIFY_GOLD_ASK_RETURN ) || (ret_addr == ADDR_SETUP_MODIFY_GOLD_OFFER_RETURN); + + // This function gets called from all over the place, check that it's being called to setup the set gold amount popup in the trade screen + if (is->current_config.autofill_best_gold_amount_when_trading && (is_initial_gold_trade || is_modifying_gold_trade)) { + int asking = (ret_addr == ADDR_SETUP_INITIAL_GOLD_ASK_RETURN) || (ret_addr == ADDR_SETUP_MODIFY_GOLD_ASK_RETURN); + int is_lump_sum = is_initial_gold_trade ? + p_stack[TRADE_GOLD_SETTER_IS_LUMP_SUM_OFFSET] : // Read this variable from the caller's frame + is->modifying_gold_trade->param_1; + + int their_id = p_diplo_form->other_party_civ_id, + our_id = p_main_screen_form->Player_CivID; + + // This variable will store the result of the optimization process and it's what will be used as the final amount presented to the + // player in the popup and left in the TradeOffer object (if applicable). Default to zero for new offers and the previous amount when + // modifying an offer. When modifying, we also zero the amount on the table b/c the optimization process only works properly from that + // starting point. + int best_amount = 0; + if (is_modifying_gold_trade) { + best_amount = is->modifying_gold_trade->param_2; + is->modifying_gold_trade->param_2 = 0; + } + + int their_advantage; + bool is_original_acceptable = is_current_offer_acceptable (&their_advantage); + + // if (we're asking for money on an acceptable trade and are offering something) OR (we're offering money on an unacceptable trade and + // are asking for something) + if (( asking && is_original_acceptable && (p_diplo_form->our_offer_lists[their_id] .length > 0)) || + ((! asking) && (! is_original_acceptable) && (p_diplo_form->their_offer_lists[their_id].length > 0))) { + + TradeOfferList * offers = asking ? &p_diplo_form->their_offer_lists[their_id] : &p_diplo_form->our_offer_lists[their_id]; + int test_offer_is_new; + TradeOffer * test_offer = offer_gold (offers, is_lump_sum, &test_offer_is_new); + + // When asking for gold, start at zero and work upwards. When offering gold it's more complicated. For lump sum + // offers, start with our entire treasury and work downward. For GPT offers, start with an upper bound and work + // downward. The upper bound depends on how much the AI thinks it's losing on the trade (in gold) divided by 20 + // (b/c 20 turn deal) with a lot of extra headroom just to make sure. + int starting_amount; { + if (asking) + starting_amount = 0; + else { + if (is_lump_sum) + starting_amount = leaders[our_id].Gold_Encoded + leaders[our_id].Gold_Decrement; + else { + int guess = not_below (0, 0 - their_advantage) / 20; + starting_amount = 10 + guess * 2; + } + } + } + + // Check if optimization is still possible. It's not if we're offering gold and our maximum offer is unacceptable + test_offer->param_2 = starting_amount; + if (asking || is_current_offer_acceptable (NULL)) { + + best_amount = starting_amount; + for (int step_size = asking ? 1000 : -1000; step_size != 0; step_size /= 10) { + test_offer->param_2 = best_amount; + while (1) { + test_offer->param_2 += step_size; + if (test_offer->param_2 < 0) + break; + else if (is_current_offer_acceptable (NULL)) + best_amount = test_offer->param_2; + else + break; + } + } + } + + // Annoying little edge case: The limitation on AIs not to trade more than their entire treasury is handled in the + // interface not the trade evaluation logic so we have to limit our gold request here to their treasury (otherwise + // the amount will default to how much they would pay if they had infinite money). + int their_treasury = leaders[their_id].Gold_Encoded + leaders[their_id].Gold_Decrement; + if (asking && is_lump_sum && (best_amount > their_treasury)) + best_amount = their_treasury; + + // Restore the trade table to its original state. Remove & free test_offer if it was newly created, otherwise put back its + // original amount. + if (test_offer_is_new) { + remove_offer (offers, test_offer); + test_offer->vtable->destruct (test_offer, __, 1); + } + + // If we're offering gold on a trade that's already acceptable, the optimal amount is zero. We must handle this explicitly in case + // we're modifying an amount already on the table. If we didn't, the above condition wouldn't optimize the amount and we'd default to + // the original amount. + } else if ((! asking) && is_original_acceptable) + best_amount = 0; + + if (is_modifying_gold_trade) + is->modifying_gold_trade->param_2 = best_amount; + snprintf (is->ask_gold_default, sizeof is->ask_gold_default, "%d", best_amount); + is->ask_gold_default[(sizeof is->ask_gold_default) - 1] = '\0'; + PopupForm_set_text_key_and_flags (this, __, script_path, text_key, param_3, (int)is->ask_gold_default, param_5, param_6); + } else + PopupForm_set_text_key_and_flags (this, __, script_path, text_key, param_3, param_4, param_5, param_6); +} + +CityLocValidity __fastcall +patch_Map_check_city_location (Map *this, int edx, int tile_x, int tile_y, int civ_id, bool check_for_city_on_tile) +{ + if (is->current_config.enable_natural_wonders) { + Tile * tile = tile_at (tile_x, tile_y); + if ((tile != NULL) && (tile != p_null_tile)) { + struct district_instance * inst = get_district_instance (tile); + if (inst != NULL && (inst->district_id == NATURAL_WONDER_DISTRICT_ID)) { + return CLV_BLOCKED; + } + } + } + + int min_sep = is->current_config.minimum_city_separation; + CityLocValidity base_result = Map_check_city_location (this, __, tile_x, tile_y, civ_id, check_for_city_on_tile); + + // If minimum separation is one, make no change + if (min_sep == 1) + return base_result; + + // If minimum separation is <= 0, ignore the CITY_TOO_CLOSE objection to city placement unless the location is next to a city belonging to + // another civ and the settings forbid founding there. + else if ((min_sep <= 0) && (base_result == CLV_CITY_TOO_CLOSE)) { + if (is->current_config.disallow_founding_next_to_foreign_city) + for (int n = 1; n <= 8; n++) { + int x, y; + get_neighbor_coords (&p_bic_data->Map, tile_x, tile_y, n, &x, &y); + City * city = city_at (x, y); + if ((city != NULL) && (city->Body.CivID != civ_id)) + return CLV_CITY_TOO_CLOSE; + } + return CLV_OK; + + // If we have an increased separation we might have to exclude some locations the base code allows. + } else if ((min_sep > 1) && (base_result == CLV_OK)) { + // Check tiles around (x, y) for a city. Because the base result is CLV_OK, we don't have to check neighboring tiles, just those at + // distance 2, 3, ... up to (an including) the minimum separation + for (int dist = 2; dist <= min_sep; dist++) { + + // vertices stores the unwrapped coords of the tiles at the vertices of the square of tiles at distance "dist" around + // (tile_x, tile_y). The order of the vertices is north, east, south, west. + struct vertex { + int x, y; + } vertices[4] = { + {tile_x , tile_y - 2*dist}, + {tile_x + 2*dist, tile_y }, + {tile_x , tile_y + 2*dist}, + {tile_x - 2*dist, tile_y } + }; + + // neighbor index for direction of tiles along edge starting from each vertex + // values correspond to directions: southeast, southwest, northwest, northeast + int edge_dirs[4] = {3, 5, 7, 1}; + + // Loop over verts and check tiles along their associated edges. The N vert is associated with the NE edge, the E vert with + // the SE edge, etc. + for (int vert = 0; vert < 4; vert++) { + wrap_tile_coords (&p_bic_data->Map, &vertices[vert].x, &vertices[vert].y); + int dx, dy; + neighbor_index_to_diff (edge_dirs[vert], &dx, &dy); + for (int j = 0; j < 2*dist; j++) { // loop over tiles along this edge + int cx = vertices[vert].x + j * dx, + cy = vertices[vert].y + j * dy; + wrap_tile_coords (&p_bic_data->Map, &cx, &cy); + if (city_at (cx, cy)) + return CLV_CITY_TOO_CLOSE; + } + } + + } + return base_result; + + } else + return base_result; +} + +bool +is_zero_strength (UnitType * ut) +{ + return (ut->Attack == 0) && (ut->Defence == 0); +} + +bool +is_captured (Unit_Body * u) +{ + return (u->RaceID >= 0) && (u->CivID >= 0) && (u->CivID < 32) && leaders[u->CivID].RaceID != u->RaceID; +} + +// Decides whether or not units "a" and "b" are duplicates, i.e., whether they are interchangeable. +// If surface_only is set, the function checks only surface-level characteristics, meaning those that are visible to other players. +bool +are_units_duplicate (Unit_Body * a, Unit_Body * b, bool surface_only) +{ + UnitType * a_type = &p_bic_data->UnitTypes[a->UnitTypeID], + * b_type = &p_bic_data->UnitTypes[b->UnitTypeID]; + + // If only doing a surface comparison, look through the alternate strategy types to the base types. The difference b/w the alt types is only + // their AI strategies, and that isn't considered a surface feature. + if (surface_only) { + while (a_type->alternate_strategy_for_id >= 0) + a_type = &p_bic_data->UnitTypes[a_type->alternate_strategy_for_id]; + while (b_type->alternate_strategy_for_id >= 0) + b_type = &p_bic_data->UnitTypes[b_type->alternate_strategy_for_id]; + } + + // a and b are duplicates "on the surface" if... + bool are_surface_duplicates = + // ... they belong to the same player ... + (a->CivID == b->CivID) && + + // ... they have the same type that is not [a leader OR army] AND not a transport AND ... + (a_type == b_type) && + (! (UnitType_has_ability (a_type, __, UTA_Leader) || UnitType_has_ability (a_type, __, UTA_Army))) && + (a_type->Transport_Capacity == 0) && + + // ... they've taken the same amount of damage AND have the same charm status AND ... + (a->Damage == b->Damage) && (a->charmed == b->charmed) && + + // ... they're either both fortified or both not AND ... + (! (a->UnitState == UnitState_Fortifying) ^ (b->UnitState == UnitState_Fortifying)) && + + // ... [they have the same experience level OR are zero strength units] AND ... + ((a->Combat_Experience == b->Combat_Experience) || is_zero_strength (a_type)) && + + // ... [they are both not contained in any unit OR they are both contained in the same unit] AND ... + (((a->Container_Unit < 0) && (b->Container_Unit < 0)) || (a->Container_Unit == b->Container_Unit)) && + + // ... neither one is carrying a princess AND ... + ((a->carrying_princess_of_race < 0) && (b->carrying_princess_of_race < 0)) && + + // ... [they are both captured units OR are both native units] AND ... + (! (is_captured (a) ^ is_captured (b))) && + + // ... their custom names are identical. + (0 == strncmp (a->Custom_Name.S, b->Custom_Name.S, sizeof (a->Custom_Name))); + + if ((! are_surface_duplicates) || surface_only) + return are_surface_duplicates; + + // a and b are additionally "deep", i.e. in all ways, duplicates if... + bool are_deep_duplicates = + // ... they've used up the same number of moves AND ... + (a->Moves == b->Moves) && + + // ... they have matching statuses (has attacked this turn, etc.) AND states (is fortified, is doing worker action, etc.) AND automation status AND ... + (a->Status == b->Status) && (a->UnitState == b->UnitState) && (a->automated == b->automated) && + + // ... they have both done the same number of airdrops this turn. + (itable_look_up_or (&is->airdrops_this_turn, a->ID, 0) == itable_look_up_or (&is->airdrops_this_turn, b->ID, 0)); + + return are_deep_duplicates; +} + +bool +is_busy (Unit * unit) +{ + int state = unit->Body.UnitState; + return ((state >= UnitState_Build_Mines) && (state <= UnitState_Explore)) || + (state == UnitState_Auto_Bombard) || (state == UnitState_Auto_Air_Bombard) || + unit->Body.automated; +} + +int __fastcall +patch_Context_Menu_add_item_and_set_color (Context_Menu * this, int edx, int item_id, char * text, int red) +{ + // Initialize the array of duplicate counts. The array is 0x100 items long because that's the maximum number of items a menu can have. + if (is->current_config.group_units_on_right_click_menu && + (is->unit_menu_duplicates == NULL)) { + unsigned dups_size = 0x100 * sizeof is->unit_menu_duplicates[0]; + is->unit_menu_duplicates = malloc (dups_size); + memset (is->unit_menu_duplicates, 0, dups_size); + } + + // Check if this menu item is a valid unit and grab pointers to its info + int unit_id; + Unit_Body * unit_body; + bool disable = false, put_icon = false; + int icon_index = 0; + if ((0 <= (unit_id = item_id - (0x13 + p_bic_data->UnitTypeCount))) && + (NULL != (unit_body = p_units->Units[unit_id].Unit)) && + (unit_body->UnitTypeID >= 0) && (unit_body->UnitTypeID < p_bic_data->UnitTypeCount)) { + + if (is->current_config.group_units_on_right_click_menu) { + // Loop over all existing menu items and check if any of them is a unit that's a duplicate of the one being added + for (int n = 0; n < this->Item_Count; n++) { + Context_Menu_Item * item = &this->Items[n]; + int dup_unit_id = this->Items[n].Menu_Item_ID - (0x13 + p_bic_data->UnitTypeCount); + Unit_Body * dup_body; + if ((dup_unit_id >= 0) && + (NULL != (dup_body = p_units->Units[dup_unit_id].Unit)) && + are_units_duplicate (unit_body, dup_body, unit_body->CivID != p_main_screen_form->Player_CivID)) { + // The new item is a duplicate of the nth. Mark that in the duplicate counts array and return without actually + // adding the item. It doesn't matter what value we return because the caller doesn't use it. + is->unit_menu_duplicates[n] += 1; + return 0; + } + } + } + + if (unit_body->CivID == p_main_screen_form->Player_CivID) { + Unit * unit = (Unit *)((int)unit_body - offsetof (Unit, Body)); + UnitType * unit_type = &p_bic_data->UnitTypes[unit_body->UnitTypeID]; + bool no_moves_left = unit_body->Moves >= patch_Unit_get_max_move_points (unit); + if (no_moves_left && is->current_config.gray_out_units_on_menu_with_no_remaining_moves) + disable = true; + + // Put an icon next to this unit if we're configured to do so and it's not in an army + Unit * container; + if (is->current_config.put_movement_icons_on_units_on_menu && + ((NULL == (container = get_unit_ptr (unit_body->Container_Unit))) || + (! UnitType_has_ability (&p_bic_data->UnitTypes[container->Body.UnitTypeID], __, UTA_Army)))) { + put_icon = true; + + bool attacker = is_offensive_combat_type (unit_type) || (Unit_has_ability (unit, __, UTA_Army) && (Unit_count_contained_units (unit) >= 1)), + busy = is_busy (unit); + + int icon_set_index = ((int)busy << 1) + (int)(! attacker); + + int icon_row; { + if (no_moves_left) + icon_row = URCMI_CANT_MOVE; + else if (unit_body->Moves == 0) + icon_row = URCMI_UNMOVED; + else if (! attacker) + icon_row = URCMI_MOVED_NO_ATTACK; + else + icon_row = (! has_exhausted_attack (unit)) ? URCMI_MOVED_CAN_ATTACK : URCMI_MOVED_NO_ATTACK; + } + + icon_index = icon_set_index * COUNT_UNIT_RCM_ICONS + icon_row; + } + } } - if ((! has_adequate_escort) && any_enemies_near_unit (this, 49)) { + + int tr = Context_Menu_add_item_and_set_color (this, __, item_id, text, red); + + if (disable) + Context_Menu_disable_item (this, __, item_id); + + if (put_icon) { + init_unit_rcm_icons (); + if (is->unit_rcm_icon_state == IS_OK) + Context_Menu_put_image_on_item (this, __, item_id, &is->unit_rcm_icons[icon_index]); + } + + return tr; +} + +int __fastcall +patch_Context_Menu_open (Context_Menu * this, int edx, int x, int y, int param_3) +{ + int * p_stack = (int *)&x; + int ret_addr = p_stack[-1]; + + if (is->current_config.enable_named_tiles && + is->named_tile_menu_active) { + int tile_x = is->named_tile_menu_tile_x; + int tile_y = is->named_tile_menu_tile_y; + Tile * tile = tile_at (tile_x, tile_y); + if (tile_can_be_named (tile, tile_x, tile_y)) { + struct named_tile_entry * entry = get_named_tile_entry (tile); + char menu_text[64]; + if ((entry != NULL) && (entry->name[0] != '\0')) + snprintf (menu_text, sizeof menu_text, "%s %s", is->c3x_labels[CL_RENAME_TILE], entry->name); + else + strcpy (menu_text,is->c3x_labels[CL_NAME_TILE]); + if (this->Item_Count > 0) + Context_Menu_add_separator (this, __, 0); + Context_Menu_add_item (this, __, NAMED_TILE_MENU_ID, menu_text, false, (Sprite *)0x0); + } + } + + if (is->current_config.group_units_on_right_click_menu && + (ret_addr == ADDR_OPEN_UNIT_MENU_RETURN) && + (is->unit_menu_duplicates != NULL)) { + + // Change the menu text to include the duplicate counts. This is pretty straight forward except for a couple little issues: we must + // use the game's internal malloc & free for compatibility with the base code and must call a function that widens the menu form, + // as necessary, to accommodate the longer strings. + for (int n = 0; n < this->Item_Count; n++) + if (is->unit_menu_duplicates[n] > 0) { + Context_Menu_Item * item = &this->Items[n]; + unsigned new_text_len = strlen (item->Text) + 20; + char * new_text = civ_prog_malloc (new_text_len); + + // Print entry text including dup count to new_text. Biggest complication here is that we want to print the dup count + // after any leading spaces to preserve indentation. + { + int num_spaces = 0; + while (item->Text[num_spaces] == ' ') + num_spaces++; + snprintf (new_text, new_text_len, "%.*s%dx %s", num_spaces, item->Text, is->unit_menu_duplicates[n] + 1, &item->Text[num_spaces]); + new_text[new_text_len - 1] = '\0'; + } + + civ_prog_free (item->Text); + item->Text = new_text; + Context_Menu_widen_for_text (this, __, new_text); + } + + // Clear the duplicate counts + memset (is->unit_menu_duplicates, 0, 0x100 * sizeof is->unit_menu_duplicates[0]); + } + + return Context_Menu_open (this, __, x, y, param_3); +} + +bool +is_material_unit (UnitType const * type, bool * out_is_pop_else_caravan) +{ + int join_city_action = UCV_Join_City & 0x0FFFFFFF; // To get the join city action code, use the command value and mask out the top 4 category bits + int disband_action = UCV_Disband & 0x0FFFFFFF; + if ((type->Attack | type->Defence | type->Bombard_Strength) == 0) { // if non-combat unit + if ((type->PopulationCost > 0) && (type->Worker_Actions == join_city_action)) { + *out_is_pop_else_caravan = true; + return true; + } else if ((type->Standard_Actions & disband_action) && (type->Worker_Actions == 0)) { + *out_is_pop_else_caravan = false; + return true; + } else + return false; + } else + return false; +} + +void +ai_move_material_unit (Unit * this) +{ + int type_id = this->Body.UnitTypeID; + Tile * tile = tile_at (this->Body.X, this->Body.Y); + UnitType * type = &p_bic_data->UnitTypes[type_id]; + + // Determine whether this is a pop unit (will search for a city to join) or a caravan (will search for a city to disband) + int join_city_action = UCV_Join_City & 0x0FFFFFFF; + bool pop_else_caravan = type->Worker_Actions == join_city_action; + + int continent_id = tile->vtable->m46_Get_ContinentID (tile); + + // This part of the code follows how the replacement leader AI works. Basically it disbands the unit if it's on a continent with no + // friendly cities, then flees if it's in danger, then moves it along a set path if it has one. + if (leaders[this->Body.CivID].city_count_per_cont[continent_id] == 0) { + Unit_disband (this); + return; + } + if (any_enemies_near_unit (this, 49)) { Unit_set_state (this, __, UnitState_Fleeing); bool done = this->vtable->work (this); if (done || (this->Body.UnitState != 0)) return; } - - // Move along path if the unit already has one set byte next_move = Unit_pop_next_move_from_path (this); if (next_move > 0) { this->vtable->Move (this, __, next_move, 0); return; } - // Start a science age if we can - // It would be nice to compute a value for this and compare it to the option of rushing production but it's hard to come up with a valuation - if (is->current_config.patch_science_age_bug && Unit_ai_can_start_science_age (this)) { - Unit_start_science_age (this); - return; - } - - // Estimate the value of creating an army. First test if that's even a possiblity and if not leave the value at -1 so rushing production is - // always preferable (note we can't use Unit_ai_can_form_army for this b/c it returns false for units not in a city). The value of forming - // an army is "infinite" if we don't already have one and otherwise it depends on the army unit's shield cost +/- 25% for each point of - // aggression divided by the number of armies already in the field. - int num_armies = leaders[this->Body.CivID].Armies_Count; - int form_army_value = -1; - int build_army_action = UCV_Build_Army & 0x0FFFFFFF; // Mask out top four category bits - if ((this->Body.leader_kind & LK_Military) && - (p_bic_data->UnitTypes[this->Body.UnitTypeID].Special_Actions & build_army_action) && - ((num_armies + 1) * p_bic_data->General.ArmySupportCities <= leaders[this->Body.CivID].Cities_Count) && - (p_bic_data->General.BuildArmyUnitID >= 0) && - (p_bic_data->General.BuildArmyUnitID < p_bic_data->UnitTypeCount)) { - if (num_armies < 1) - form_army_value = INT_MAX; - else { - form_army_value = p_bic_data->UnitTypes[p_bic_data->General.BuildArmyUnitID].Cost; - int aggression_level = p_bic_data->Races[leaders[this->Body.CivID].RaceID].AggressionLevel; // aggression level varies between -2 and +2 - form_army_value = (form_army_value * (4 + aggression_level)) / 4; - if (num_armies > 1) - form_army_value /= num_armies; - } - } - - // Estimate the value of rushing production in every city on this continent and remember the highest one - City * best_rush_loc = NULL; - int best_rush_value = -1; + // Find the best city to act on + City * best_city = NULL; + int best_city_value = pop_else_caravan ? -1 : 0; // min value to act is 0 for pop units, 1 for caravans FOR_CITIES_OF (coi, this->Body.CivID) { City * city = coi.city; Tile * city_tile = tile_at (city->Body.X, city->Body.Y); - if ((continent_id == city_tile->vtable->m46_Get_ContinentID (city_tile)) && - patch_Unit_can_hurry_production (this, __, city, false)) { - // Base value is equal to the number of shields rushing would save - int value = City_get_order_cost (city) - City_get_order_progress (city) - city->Body.ProductionIncome; - if (value <= 0) - continue; + if (continent_id == city_tile->vtable->m46_Get_ContinentID (city_tile)) { - // Consider distance: Reduce the value by the number of shields produced while the leader is en route to rush. + if (pop_else_caravan) { + // Skip this city if it can't support another citizen + if ((city->Body.FoodIncome <= 0) || + (City_requires_improvement_to_grow (city) > -1)) + continue; + } else { + // Skip this city if its current build can't be rushed + if (! City_can_take_outside_shields (city, __, 0)) + continue; + } + + // Consider distance. int dist_in_turns; if (! estimate_travel_time (this, city->Body.X, city->Body.Y, &dist_in_turns)) - continue; // no path or unit cannot move - value -= dist_in_turns * city->Body.ProductionIncome; - if (value <= 0) - continue; + continue; // No path or unit cannot move - // Consider corruption: Scale down the value of rushing an improvement by the corruption rate of the city. - // This is to reflect the fact that infrastructure is more valuable in less corrupt cities. But do not apply - // this to wonders since their benefit is in most cases not lessened by local corruption. - Improvement * improv = (city->Body.Order_Type == COT_Improvement) ? &p_bic_data->Improvements[city->Body.Order_ID] : NULL; - int is_wonder = (improv != NULL) && (0 != (improv->Characteristics & (ITC_Small_Wonder | ITC_Wonder))); - if ((improv != NULL) && (! is_wonder)) { - int good_shields = city->Body.ProductionIncome; - int corrupt_shields = city->Body.ProductionLoss; + int value; + if (pop_else_caravan) + value = 1000 / (10 + dist_in_turns); // Base value of 100 * 10 / (10 + dist_in_turn) + else { + // value is number of useful shields we'd get by moving to this city and disbanding there + int shields_per_turn = get_city_production_rate (city, city->Body.Order_Type, city->Body.Order_ID), + shields_to_complete_build = City_get_order_cost (city) - City_get_order_progress (city) - shields_per_turn, + disband_value = Leader_get_unit_cost (&leaders[city->Body.CivID], __, type_id, false) >> 2; + value = not_above (shields_to_complete_build, disband_value) - shields_per_turn * dist_in_turns; + } + + // Scale value by city corruption rate for pop units or caravan units targeting cities building improvs + if (pop_else_caravan || (city->Body.Order_Type == COT_Improvement)) { + int good_shields = city->Body.ProductionIncome, + corrupt_shields = city->Body.ProductionLoss; if (good_shields + corrupt_shields > 0) value = (value * good_shields) / (good_shields + corrupt_shields); else - continue; + value = -1; } - if ((value > 0) && (value > best_rush_value)) { - best_rush_loc = city; - best_rush_value = value; + if (value > best_city_value) { + best_city = city; + best_city_value = value; } } } - // Hurry production or form an army depending on the estimated values of doing so. We might have to move to a (different) city if that's where - // we want to rush production or if we want to form an army but aren't already in a city. + // Join city if we're already in the city we want to join, otherwise move to that city. If we couldn't find a city to join, go to the + // nearest established city and wait. City * in_city = get_city_ptr (tile->vtable->m45_Get_City_ID (tile)); City * moving_to_city = NULL; - if ((best_rush_loc != NULL) && (best_rush_value > form_army_value)) { - if (best_rush_loc == in_city) { - Unit_hurry_production (this); - return; + if (best_city != NULL) { + if (best_city == in_city) { + if (pop_else_caravan && patch_Unit_can_perform_command (this, __, UCV_Join_City)) { + Unit_join_city (this, __, in_city); + return; + } else if ((! pop_else_caravan) && patch_Unit_can_perform_command (this, __, UCV_Disband)) { + Unit_disband (this); + return; + } } else - moving_to_city = best_rush_loc; - } else if ((form_army_value > -1) && patch_Unit_ai_can_form_army (this)) { - Unit_form_army (this); - return; - } else if (in_city == NULL) { - // Nothing to do. Try to find a close, established city to move to & wait in. + moving_to_city = best_city; + } else if (in_city == NULL) moving_to_city = find_nearest_established_city (this, continent_id); - } + if (moving_to_city) { int first_move = patch_Trade_Net_set_unit_path (is->trade_net, __, this->Body.X, this->Body.Y, moving_to_city->Body.X, moving_to_city->Body.Y, this, this->Body.CivID, 0x101, NULL); if (first_move > 0) { @@ -14827,10481 +22418,12569 @@ patch_Unit_ai_move_leader (Unit * this) Unit_set_state (this, __, UnitState_Fortifying); } -int -measure_strength_in_army (UnitType * type) +int __stdcall +patch_get_anarchy_length (int leader_id) { - return ((type->Attack + type->Defence) * (4 + type->Hit_Point_Bonus)) / 4; + int base = get_anarchy_length (leader_id); + int multiplier = is->current_config.anarchy_length_percent; + if (multiplier != 100) { + // Anarchy cannot be less than 2 turns or you'll get an instant government switch. Only let that happen when the percent multiplier is + // set below zero, indicating a desire for no anarchy. Otherwise we can't reduce anarchy below 2 turns. + if (multiplier < 0) + return 1; + else if (base <= 2) + return base; + else + return not_below (2, rand_div (base * multiplier, 100)); + } else + return base; } bool __fastcall -patch_impl_ai_is_good_army_addition (Unit * this, int edx, Unit * candidate) +patch_Unit_check_king_ability_while_spawning (Unit * this, int edx, enum UnitTypeAbilities a) { - if (! is->current_config.fix_ai_army_composition) - return impl_ai_is_good_army_addition (this, __, candidate); - - UnitType * candidate_type = &p_bic_data->UnitTypes[candidate->Body.UnitTypeID]; - Tile * tile = tile_at (this->Body.X, this->Body.Y); - - if (((candidate_type->AI_Strategy & UTAI_Offence) == 0) || - UnitType_has_ability (candidate_type, __, UTA_Hidden_Nationality)) + if (is->current_config.dont_give_king_names_in_non_regicide_games && + ((*p_toggleable_rules & (TR_REGICIDE | TR_MASS_REGICIDE)) == 0)) return false; - - int num_units_in_army = 0, - army_min_speed = INT_MAX, - army_min_strength = INT_MAX; - FOR_UNITS_ON (uti, tile) { - if (uti.unit->Body.Container_Unit == this->Body.ID) { - num_units_in_army++; - int movement = p_bic_data->UnitTypes[uti.unit->Body.UnitTypeID].Movement; - if (movement < army_min_speed) - army_min_speed = movement; - int member_strength = measure_strength_in_army (&p_bic_data->UnitTypes[uti.unit->Body.UnitTypeID]); - if (member_strength < army_min_strength) - army_min_strength = member_strength; - } - } - - return (num_units_in_army == 0) || - ((candidate_type->Movement >= army_min_speed) && - (measure_strength_in_army (candidate_type) >= army_min_strength)); + else + return Unit_has_ability (this, __, a); } -int -rate_artillery (UnitType * type) +int __fastcall +patch_Map_compute_neighbor_index_for_pass_between (Map * this, int edx, int x_home, int y_home, int x_neigh, int y_neigh, int lim) { - int tr = type->Attack + type->Defence + 2 * type->Bombard_Strength * type->FireRate; + if (is->current_config.enable_land_sea_intersections) + return 0; + else + return Map_compute_neighbor_index (this, __, x_home, y_home, x_neigh, y_neigh, lim); +} - // include movement - int moves = type->Movement; - if (moves >= 2) - tr = tr * (moves + 1) / 2; +// This call replacement used to be part of improvement perfuming but now its only purpose is to set ai_considering_order when +// ai_choose_production is looping over improvements. +bool __fastcall +patch_Improvement_has_wonder_com_bonus_for_ai_prod (Improvement * this, int edx, enum ImprovementTypeWonderFeatures flag) +{ + is->ai_considering_order.OrderID = this - p_bic_data->Improvements; + is->ai_considering_order.OrderType = COT_Improvement; + return Improvement_has_wonder_flag (this, __, flag); +} - // include range - int range = type->Bombard_Range; - if (range >= 2) - tr = tr * (range + 1) / 2; +// Similarly, this one sets the var when looping over unit types. +bool __fastcall +patch_UnitType_has_strat_0_for_ai_prod (UnitType * this, int edx, byte n) +{ + is->ai_considering_order.OrderID = this - p_bic_data->UnitTypes; + is->ai_considering_order.OrderType = COT_Unit; + return UnitType_has_ai_strategy (this, __, n); +} - // include extra hp - if (type->Hit_Point_Bonus > 0) - tr = tr * (4 + type->Hit_Point_Bonus) / 4; +int +compare_ai_prod_valuations (void const * vp_a, void const * vp_b) +{ + struct ai_prod_valuation const * a = vp_a, + * b = vp_b; + if (a->point_value > b->point_value) return -1; + else if (b->point_value > a->point_value) return 1; + else return 0; +} - return tr; +void +rank_ai_production_options (City * city) +{ + is->count_ai_prod_valuations = 0; + City_Order unused; + patch_City_ai_choose_production (city, __, &unused); // records valuations in is->ai_prod_valuations + qsort (is->ai_prod_valuations, is->count_ai_prod_valuations, sizeof is->ai_prod_valuations[0], compare_ai_prod_valuations); } -int -rate_bomber (UnitType * type) +void __fastcall +patch_City_Form_m82_handle_key_event (City_Form * this, int edx, int virtual_key_code, int is_down) { - int tr = type->Bombard_Strength * type->FireRate + type->Defence; + if (is->current_config.enable_ai_production_ranking && + (~*p_human_player_bits & (1 << this->CurrentCity->Body.CivID)) && + (virtual_key_code == VK_P) && is_down) { + rank_ai_production_options (this->CurrentCity); + PopupForm * popup = get_popup_form (); + popup->vtable->set_text_key_and_flags (popup, __, is->mod_script_path, "C3X_AI_PROD_RANKING", -1, 0, 0, 0); + char s[200]; + for (int n = 0; n < is->count_ai_prod_valuations; n++) { + struct ai_prod_valuation const * val = &is->ai_prod_valuations[n]; + char * name = (val->order_type == COT_Improvement) ? p_bic_data->Improvements[val->order_id].Name.S : p_bic_data->UnitTypes[val->order_id].Name; - // include range - tr = tr * (10 + type->OperationalRange) / 10; + int show_strategy = -1; + if (val->order_type == COT_Unit) + itable_look_up (&is->unit_type_alt_strategies, val->order_id, &show_strategy); - // include cost - tr = (tr * 100) / (100 + type->Cost); + if ((show_strategy >= 0) && (show_strategy <= CL_LAST_UNIT_STRAT - CL_FIRST_UNIT_STRAT)) + snprintf (s, sizeof s, "^%d %s (%s)", val->point_value, name, is->c3x_labels[CL_FIRST_UNIT_STRAT + show_strategy]); + else + snprintf (s, sizeof s, "^%d %s", val->point_value, name); + s[(sizeof s) - 1] = '\0'; + PopupForm_add_text (popup, __, s, 0); + } + patch_show_popup (popup, __, 0, 0); - // include abilities - if (UnitType_has_ability (type, __, UTA_Blitz)) - tr = tr * (type->Movement + 1) / 2; - if (UnitType_has_ability (type, __, UTA_Lethal_Land_Bombardment)) - tr = tr * 3 / 2; - if (UnitType_has_ability (type, __, UTA_Lethal_Sea_Bombardment)) - tr = tr * 5 / 4; - if (UnitType_has_ability (type, __, UTA_Stealth)) - tr = tr * 3 / 2; + } else if (is->current_config.toggle_zoom_with_z_on_city_screen && + (virtual_key_code == VK_Z) && is_down) { + p_bic_data->is_zoomed_out = ! p_bic_data->is_zoomed_out; + Main_Screen_Form_bring_tile_into_view (p_main_screen_form, __, this->CurrentCity->Body.X, get_city_screen_center_y (this->CurrentCity), 0, true, false); // also redraws map + this->Base.vtable->m73_call_m22_Draw ((Base_Form *)this); + } - // include extra hp - if (type->Hit_Point_Bonus > 0) - tr = tr * (4 + type->Hit_Point_Bonus) / 4; + City_Form_m82_handle_key_event (this, __, virtual_key_code, is_down); +} - return tr; +bool +can_harvest_shields_from_forest (Tile * tile) +{ + int flags = tile->vtable->m43_Get_field_30 (tile); + return (flags & 0x10000000) == 0; } -bool __fastcall -patch_City_can_build_unit (City * this, int edx, int unit_type_id, bool exclude_upgradable, int param_3, bool allow_kings) +void __fastcall +patch_open_tile_info (void * this, int edx, int mouse_x, int mouse_y, int civ_id) { - bool base = City_can_build_unit (this, __, unit_type_id, exclude_upgradable, param_3, allow_kings); + int tx, ty; + if (is->current_config.show_detailed_tile_info && + (! Main_Screen_Form_get_tile_coords_under_mouse (p_main_screen_form, __, mouse_x, mouse_y, &tx, &ty))) { + is->viewing_tile_info_x = tx; + is->viewing_tile_info_y = ty; + is->tile_info_open = true; + } else + is->viewing_tile_info_x = is->viewing_tile_info_y = -1; - if (base) { - // Apply building prereqs - int building_prereq; - if (itable_look_up (&is->current_config.building_unit_prereqs, unit_type_id, &building_prereq)) { - // If the prereq is an encoded building ID - if (building_prereq & 1) { - if (! has_active_building (this, building_prereq >> 1)) - return false; + open_tile_info (this, __, mouse_x, mouse_y, civ_id); - // Else it's a pointer to a list of building IDs - } else { - int * list = (int *)building_prereq; - for (int n = 0; n < MAX_BUILDING_PREREQS_FOR_UNIT; n++) - if ((list[n] >= 0) && ! has_active_building (this, list[n])) - return false; - } - } + is->tile_info_open = false; +} - // Apply unit type limit - int available; - if (get_available_unit_count (&leaders[this->Body.CivID], unit_type_id, &available) && (available <= 0)) - return false; +int __fastcall +patch_Match_ai_eval_city_location (void * this, int edx, int x, int y, int civ_id, bool param_4, int * out_breakdown) +{ + is->ai_evaling_city_loc_x = x; + is->ai_evaling_city_loc_y = y; + is->ai_evaling_city_field_30_get_counter = 0; - if (is->current_config.enable_districts && - is->current_config.enable_aerodrome_districts && - is->current_config.air_units_use_aerodrome_districts_not_cities) { - UnitType * type = &p_bic_data->UnitTypes[unit_type_id]; - int aerodrome_id = AERODROME_DISTRICT_ID; - if ((type->Unit_Class == UTC_Air) && (aerodrome_id >= 0) && - ! city_has_required_district (this, aerodrome_id)) - return false; - } - } + return Match_ai_eval_city_location (this, __, x, y, civ_id, param_4, out_breakdown); +} + +bool +is_explored (Tile * tile, int civ_id) +{ + unsigned explored_bits = tile->Body.Fog_Of_War; + int in_debug_mode = (*p_debug_mode_bits & 8) != 0; // checking bit 3 here b/c that's how resource visibility is checked in open_tile_info + if (in_debug_mode || (explored_bits & (1 << civ_id))) + return true; + else if (is->current_config.share_visibility_in_hotseat && // if shared hotseat vis is enabled AND + (*p_is_offline_mp_game && ! *p_is_pbem_game) && // is hotseat game AND + ((1 << civ_id) & *p_human_player_bits) && // "civ_id" is a human player AND + (explored_bits & *p_human_player_bits)) // any human player has visibility on the tile + return true; + else + return false; +} - return base; +// On the GOG executable, this function intercepts the call to draw the "No information available" text on a unexplored (black) tile. In that case we +// replace that text with the coords. But on the Steam EXE this func also intercepts the call that draws the resource name on the visible tile info +// box b/c the call site is reused via a jump. So we must check that the tile is actually unexplored before replacing the text so that the resource +// name doesn't get overwritten. +int __fastcall +patch_PCX_Image_draw_no_tile_info (PCX_Image * this, int edx, char * str, int x, int y, int str_len) +{ + Tile * tile = tile_at (is->viewing_tile_info_x, is->viewing_tile_info_y); + if ((tile != p_null_tile) && ! is_explored (tile, p_main_screen_form->Player_CivID)) { + char s[100]; + snprintf (s, sizeof s, "(%d, %d)", is->viewing_tile_info_x, is->viewing_tile_info_y); + s[(sizeof s) - 1] = '\0'; + return PCX_Image_draw_text (this, __, s, x, y, strlen (s)); + } else + return PCX_Image_draw_text (this, __, str, x, y, str_len); } -bool __fastcall -patch_City_can_build_improvement (City * this, int edx, int i_improv, bool apply_strict_rules) +int __fastcall +patch_PCX_Image_draw_tile_info_terrain (PCX_Image * this, int edx, char * str, int x, int y, int str_len) { - // First defer to the base game's logic - bool base = City_can_build_improvement (this, __, i_improv, apply_strict_rules); - if (! base) return false; - if (! is->current_config.enable_districts) return base; + Tile * tile = tile_at (is->viewing_tile_info_x, is->viewing_tile_info_y); + if (tile != p_null_tile) { + bool show_district_name = false; + + char s[200]; + if (is->current_config.enable_districts || is->current_config.enable_natural_wonders) { + // Draw district name to the right of terrain name if tile has one + struct district_instance * dist = get_district_instance (tile); - // Different logic for human vs AI players - bool is_human = (*p_human_player_bits & (1 << this->Body.CivID)) != 0; + if (dist != NULL) { + show_district_name = true; + char const * display_name = is->district_configs[dist->district_id].display_name; + if ((display_name == NULL) || (display_name[0] == '\0')) + display_name = is->district_configs[dist->district_id].name; - // Check if this is a wonder and if wonder districts are enabled - if (is_human && is->current_config.enable_wonder_districts && - (i_improv >= 0) && (i_improv < p_bic_data->ImprovementsCount)) { - Improvement * improv = &p_bic_data->Improvements[i_improv]; - if (improv->Characteristics & (ITC_Wonder | ITC_Small_Wonder)) { - bool wonder_requires_district = false; - int required_id; - if (itable_look_up (&is->district_building_prereqs, i_improv, &required_id) && (required_id == WONDER_DISTRICT_ID)) - wonder_requires_district = true; + // If it's a wonder district with a completed wonder, show the wonder name instead + if ((dist->district_id == WONDER_DISTRICT_ID) && + (dist->wonder_info.state == WDS_COMPLETED) && + (dist->wonder_info.wonder_index >= 0) && + (dist->wonder_info.wonder_index < is->wonder_district_count)) { + char const * wonder_name = is->wonder_district_configs[dist->wonder_info.wonder_index].wonder_name; + if ((wonder_name != NULL) && (wonder_name[0] != '\0')) { + display_name = wonder_name; + } + } else if ((dist->district_id == NATURAL_WONDER_DISTRICT_ID) && + (dist->natural_wonder_info.natural_wonder_id >= 0) && + (dist->natural_wonder_info.natural_wonder_id < is->natural_wonder_count)) { + int natural_id = dist->natural_wonder_info.natural_wonder_id; + char const * natural_name = is->natural_wonder_configs[natural_id].name; + if ((natural_name != NULL) && (natural_name[0] != '\0')) { + display_name = natural_name; + } + } - // Can only build wonders that need districts if an incomplete wonder district exists - if (wonder_requires_district && - ! city_has_wonder_district_with_no_completed_wonder (this)) - return !apply_strict_rules; + snprintf (s, sizeof s, "%s", display_name); + PCX_Image_draw_text (this, __, s, x + 68, y, strlen (s)); + } } - } - // Check if the improvement requires a district and output the required district id when it does - int required_district_id; - bool needs_district = city_requires_district_for_improvement (this, i_improv, &required_district_id); - if (! needs_district) - return true; + // Show sprites & sheet indexes if in debug mode + int is_debug_mode = (*p_debug_mode_bits & 4) != 0; + if (is_debug_mode) { + int sheet_index = (tile->SquareParts >> 8) & 0xFF; + int sprite_index = tile->SquareParts & 0xFF; + snprintf (s, sizeof s, "%d, %d", sheet_index, sprite_index); + PCX_Image_draw_text (this, __, s, x, y - 18, strlen (s)); + } - // Ensure prereq tech for the district - int prereq_id = is->district_infos[required_district_id].advance_prereq_id; - if ((prereq_id >= 0) && ! Leader_has_tech (&leaders[this->Body.CivID], __, prereq_id)) - return false; + // Draw tile coords on line below terrain name + snprintf (s, sizeof s, "(%d, %d)", is->viewing_tile_info_x, is->viewing_tile_info_y); + PCX_Image_draw_text (this, __, s, x, y + 14, strlen (s)); - // Human doesn't have appropriate district but needs one; allow relaxed checks so UI can gray entry out - if (needs_district && is_human) { - return !apply_strict_rules; - } + if ((is->city_loc_display_perspective >= 0) && + ((1 << is->city_loc_display_perspective) & *p_player_bits)) { + int eval = patch_Match_ai_eval_city_location (p_match, __, is->viewing_tile_info_x, is->viewing_tile_info_y, is->city_loc_display_perspective, false, NULL); + if (eval > 0) { + snprintf (s, sizeof s, "%d", eval - 1000000); + PCX_Image_draw_text (this, __, s, x + 95, y, strlen (s)); + } + } - // If AI already has a pending district request for this required district, return false - // to prevent wasting a turn trying to choose this improvement - if (find_pending_district_request (this, required_district_id) != NULL) { - return false; + // If tile has been chopped and district name not shown (uses same space), indicate that to the right of the terrain name + if (! can_harvest_shields_from_forest (tile) && !show_district_name) + PCX_Image_draw_text (this, __, is->c3x_labels[CL_CHOPPED], x + 145, y, strlen (is->c3x_labels[CL_CHOPPED])); } - - // Superficially allow the AI to choose the improvement for scoring and production. - // If a disallowed improvement is chosen in ai_choose_production, we'll swap it out for a feasible fallback later - // after prioritizing the district to be built - return true; + return PCX_Image_draw_text (this, __, str, x, y, str_len); } -bool -ai_handle_district_production_requirements (City * city, City_Order * out) +int __fastcall +patch_City_compute_corrupted_yield (City * this, int edx, int gross_yield, bool is_production) { - clear_best_feasible_order (city); - bool swapped_to_fallback = false; - City_Order fallback_order = {0}; - int required_district_id = -1; + Leader * leader = &leaders[this->Body.CivID]; - char ss[200]; + if (is->current_config.zero_corruption_when_off && + (p_bic_data->Governments[leader->GovernmentType].CorruptionAndWaste == CWT_Off)) + return 0; - if (is->current_config.enable_districts && - (out->OrderType == COT_Improvement) && - (out->OrderID >= 0) && (out->OrderID < p_bic_data->ImprovementsCount)) { + int tr = City_compute_corrupted_yield (this, __, gross_yield, is_production); - // Check if AI is trying to build a wonder without an incomplete wonder district - bool needs_wonder_district = false; - bool requires_district = city_requires_district_for_improvement (city, out->OrderID, &required_district_id); - if (is->current_config.enable_wonder_districts) { - Improvement * improv = &p_bic_data->Improvements[out->OrderID]; - if ((improv->Characteristics & (ITC_Wonder | ITC_Small_Wonder)) && - (! city_has_wonder_district_with_no_completed_wonder (city))) { - needs_wonder_district = true; - if (required_district_id < 0) { - required_district_id = WONDER_DISTRICT_ID; + if (is->current_config.promote_wonder_decorruption_effect) { + int actual_capital_id = leader->CapitalID; + for (int improv_id = 0; improv_id < p_bic_data->ImprovementsCount; improv_id++) { + Improvement * improv = &p_bic_data->Improvements[improv_id]; + City * pseudo_capital = NULL; + if (improv->SmallWonderFlags & ITSW_Reduces_Corruption) { + if (improv->Characteristics & ITC_Small_Wonder) { + pseudo_capital = get_city_ptr (leader->Small_Wonders[improv_id]); + } else if (improv->Characteristics & ITC_Wonder) { + pseudo_capital = get_city_ptr (Game_get_wonder_city_id(p_game, __, improv_id)); } - } - } - - if (needs_wonder_district || requires_district) { - struct ai_best_feasible_order * stored = get_best_feasible_order (city); - if (stored != NULL) { - bool fallback_is_feasible = true; - if (stored->order.OrderType == COT_Improvement) { - if ((stored->order.OrderID < 0) || - (stored->order.OrderID >= p_bic_data->ImprovementsCount)) - fallback_is_feasible = false; - - // Check if fallback requires a district the city doesn't have - if (fallback_is_feasible && - city_requires_district_for_improvement (city, stored->order.OrderID, NULL)) - fallback_is_feasible = false; - // If original order was a wonder, ensure fallback is not also a wonder - if (fallback_is_feasible && needs_wonder_district) { - Improvement * fallback_improv = &p_bic_data->Improvements[stored->order.OrderID]; - if (fallback_improv->Characteristics & (ITC_Wonder | ITC_Small_Wonder)) - fallback_is_feasible = false; - } - - // If fallback is a wonder, check if it has an incomplete wonder district - if (fallback_is_feasible && is->current_config.enable_wonder_districts) { - Improvement * fallback_improv = &p_bic_data->Improvements[stored->order.OrderID]; - if ((fallback_improv->Characteristics & (ITC_Wonder | ITC_Small_Wonder)) && - (! city_has_wonder_district_with_no_completed_wonder (city))) - fallback_is_feasible = false; - } - } else if (stored->order.OrderType == COT_Unit) { - if ((stored->order.OrderID < 0) || - (stored->order.OrderID >= p_bic_data->UnitTypeCount)) - fallback_is_feasible = false; - } else - fallback_is_feasible = false; - - if (fallback_is_feasible) { - // Remember pending building order for any improvement that requires a district - snprintf (ss, sizeof ss, "ai_handle_district_production_requirements: Remembering fallback pending building order for city %d (%s): order type %d id %d\n", - city->Body.ID, city->Body.CityName, out->OrderType, out->OrderID); - (*p_OutputDebugStringA) (ss); - remember_pending_building_order (city, out->OrderID); - - fallback_order = stored->order; - swapped_to_fallback = true; + if (pseudo_capital != NULL && pseudo_capital->Body.CivID == this->Body.CivID && pseudo_capital->Body.ID != actual_capital_id) { + leader->CapitalID = pseudo_capital->Body.ID; + int fp_corrupted_yield = City_compute_corrupted_yield (this, __, gross_yield, is_production); + if (fp_corrupted_yield < tr) + tr = fp_corrupted_yield; } } } + leader->CapitalID = actual_capital_id; } - if (swapped_to_fallback) { - *out = fallback_order; - mark_city_needs_district (city, required_district_id); - } + return tr; +} - clear_best_feasible_order (city); - return swapped_to_fallback; +int __fastcall +patch_Sprite_draw (Sprite * this, int edx, PCX_Image * canvas, int pixel_x, int pixel_y, PCX_Color_Table * color_table) +{ + Sprite * to_draw = get_sprite_proxy_for_current_hour(this); + return Sprite_draw(to_draw ? to_draw : this, __, canvas, pixel_x, pixel_y, color_table); +} + +int __fastcall +patch_Sprite_draw_on_map (Sprite * this, int edx, Map_Renderer * map_renderer, int pixel_x, int pixel_y, int param_4, int param_5, int param_6, int param_7) +{ + Sprite * to_draw = get_sprite_proxy_for_current_hour(this); + return Sprite_draw_on_map(to_draw ? to_draw : this, __, map_renderer, pixel_x, pixel_y, param_4, param_5, param_6, param_7); +} + +bool +is_or_could_become_grassland (Tile * tile) +{ + enum SquareTypes sq_type = tile->vtable->m50_Get_Square_BaseType (tile), + underlying_type = tile->vtable->m49_Get_Square_RealType (tile); + int worker_job_id = p_bic_data->TileTypes[sq_type].WorkerJobID; + return sq_type == SQ_Grassland || + (underlying_type == SQ_Grassland && (worker_job_id == WJ_Clean_Forest || worker_job_id == WJ_Clear_Swamp)) || + tile->vtable->m72_Get_Pollution_Effect (tile) == SQ_Grassland; } void __fastcall -patch_City_ai_choose_production (City * this, int edx, City_Order * out) +patch_Map_Renderer_m19_Draw_Tile_by_XY_and_Flags (Map_Renderer * this, int edx, int param_1, int pixel_x, int pixel_y, Map_Renderer * map_renderer, int param_5, int tile_x, int tile_y, int param_8) { - is->ai_considering_production_for_city = this; - City_ai_choose_production (this, __, out); + Map * map = &p_bic_data->Map; + Tile * tile = tile_at (tile_x, tile_y); + is->current_render_tile = tile; + is->current_render_tile_x = tile_x; + is->current_render_tile_y = tile_y; + is->current_render_tile_district = get_district_instance (tile); - if (is->current_config.enable_districts) { - if (ai_handle_district_production_requirements (this, out)) { - return; + Map_Renderer_m19_Draw_Tile_by_XY_and_Flags (this, __, param_1, pixel_x, pixel_y, map_renderer, param_5, tile_x, tile_y, param_8); + + is->current_render_tile = NULL; + is->current_render_tile_x = -1; + is->current_render_tile_y = -1; + is->current_render_tile_district = NULL; + + if ((is->city_loc_display_perspective >= 0) && + (! map->vtable->m10_Get_Map_Zoom (map)) && // Turn off display when zoomed out. Need another set of highlight images for that. + ((1 << is->city_loc_display_perspective) & *p_player_bits) && + (((tile_x + tile_y) % 2) == 0)) { // Replicate a check from the base game code. Without this we'd be drawing additional tiles half-way off the grid. + + init_tile_highlights (); + if (is->tile_highlight_state == IS_OK) { + int eval = patch_Match_ai_eval_city_location (p_match, __, tile_x, tile_y, is->city_loc_display_perspective, false, NULL); + if (eval > 0) { + int step_size = 10; + int midpoint = (COUNT_TILE_HIGHLIGHTS % 2 == 0) ? 1000000 : (1000000 - step_size/2); + int grade = (eval >= midpoint) ? (eval - midpoint) / step_size : (eval - midpoint) / step_size - 1; + int i_highlight = clamp (0, COUNT_TILE_HIGHLIGHTS - 1, COUNT_TILE_HIGHLIGHTS/2 + grade); + Sprite_draw_on_map (&is->tile_highlights[i_highlight], __, this, pixel_x, pixel_y, 1, 1, 1, 0); + } } } - Leader * me = &leaders[this->Body.CivID]; - int arty_ratio = is->current_config.ai_build_artillery_ratio; - int bomber_ratio = is->current_config.ai_build_bomber_ratio; - - // Check if AI-build-more-artillery mod option is activated and this city is building something that we might want to switch to artillery - if ((arty_ratio > 0) && - (out->OrderType == COT_Unit) && - (out->OrderID >= 0) && (out->OrderID < p_bic_data->UnitTypeCount) && - (p_bic_data->UnitTypes[out->OrderID].AI_Strategy & (UTAI_Offence | UTAI_Defence))) { + // Districts-related highlights + if (is->current_config.enable_districts && is->tile_highlight_state == IS_OK && + (((tile_x + tile_y) % 2) == 0)) { + init_tile_highlights (); - // Check how many offense/defense/artillery units this AI has already built. We'll only force it to build arty if it has a minimum - // of one defender per city, one attacker per two cities, and of course if it's below the ratio limit. - int num_attackers = me->AI_Strategy_Unit_Counts[0], - num_defenders = me->AI_Strategy_Unit_Counts[1], - num_artillery = me->AI_Strategy_Unit_Counts[2]; - if ((num_attackers > me->Cities_Count / 2) && - (num_defenders > me->Cities_Count) && - (100*num_artillery < arty_ratio*(num_attackers + num_defenders))) { + // Draw city work radius highlights for selected worker + if (is->current_config.enable_city_work_radii_highlights && + is->highlight_city_radii) { // Replicate a check from the base game code. Without this we'd be drawing additional tiles half-way off the grid. - // Loop over all build options to determine the best artillery unit available. Record its attack power and also find & record - // the highest attack power available from any offensive unit so we can compare them. - int best_arty_type_id = -1, - best_arty_rating = -1, - best_arty_strength = -1, - best_attacker_strength = -1; - for (int n = 0; n < p_bic_data->UnitTypeCount; n++) { - UnitType * type = &p_bic_data->UnitTypes[n]; - if ((type->AI_Strategy & (UTAI_Artillery | UTAI_Offence)) && - patch_City_can_build_unit (this, __, n, 1, 0, 0)) { - if (type->AI_Strategy & UTAI_Artillery) { - int this_rating = rate_artillery (&p_bic_data->UnitTypes[n]); - if (this_rating > best_arty_rating) { - best_arty_type_id = n; - best_arty_rating = this_rating; - best_arty_strength = type->Bombard_Strength * type->FireRate; - } - } else { // attacker - int this_strength = (type->Attack * (4 + type->Hit_Point_Bonus)) / 4; - if (this_strength > best_attacker_strength) - best_attacker_strength = this_strength; - } + if ((tile != NULL) && (tile != p_null_tile)) { + int stored_ptr; + if (itable_look_up (&is->highlighted_city_radius_tile_pointers, (int)tile, &stored_ptr)) { + struct highlighted_city_radius_tile_info * info = (struct highlighted_city_radius_tile_info *)stored_ptr; + Sprite_draw_on_map (&is->tile_highlights[clamp(0, COUNT_TILE_HIGHLIGHTS - 1, info->highlight_level)], __, this, pixel_x, pixel_y, 1, 1, 1, 0); } } + } - // Randomly switch city production to the artillery unit if we found one - if (best_arty_type_id >= 0) { - int chance = 12 * arty_ratio / 10; + // If focusing on a tile after Great Wall completed, highlight the tile while getting user confirmation + if (is->focused_tile != NULL && is->focused_tile == tile) { + Sprite_draw_on_map (&is->tile_highlights[10], __, this, pixel_x, pixel_y, 1, 1, 1, 0); + } + } +} - // Scale the chance of switching by the ratio of the attack power the artillery would provide to the attack power - // of the strongest offensive unit. This way the AI will rapidly build artillery up to the ratio limit only when - // artillery are its best way of dealing damage. - // Some example numbers: - // | Best attacker (str) | Best arty (str) | Chance w/ ratio = 20, damage% = 50 - // | Swordsman (3) | Catapult (4) | 16 - // | Knight (4) | Trebuchet (6) | 18 - // | Cavalry (6) | Cannon (8) | 16 - // | Cavalry (6) | Artillery (24) | 48 - // | Tank (16) | Artillery (24) | 18 - if (best_attacker_strength > 0) - chance = (chance * best_arty_strength * is->current_config.ai_artillery_value_damage_percent) / (best_attacker_strength * 100); +// We determine at the start of the game where *any* AI player might want to build canals and bridges. +// This is done up front in a single pass, as re-assessing every single turn per AI is wasteful and the +// geography of the map doesn't change. This function adds all the candidates as districts, effectively +// drawing them directly on the map. We don't use it in a normal game, but this is extremely useful for +// debugging so good to keep it around. For debugging, just call it immediately after +// generate_ai_canal_and_bridge_targets () +void +insert_ai_candidate_bridge_or_canals_into_district_tile_map () +{ + if ((is->ai_candidate_bridge_or_canals_count <= 0) || (! is->ai_candidate_bridge_or_canals_initialized)) + return; - if (rand_int (p_rand_object, __, 100) < chance) - out->OrderID = best_arty_type_id; - } + for (int ei = 0; ei < is->ai_candidate_bridge_or_canals_count; ei++) { + struct ai_candidate_bridge_or_canal_entry * entry = &is->ai_candidate_bridge_or_canals[ei]; + if (entry == NULL) + continue; + + char ss[256]; + snprintf (ss, sizeof ss, "Entry %d: district %d owner %d tiles %d completed %d\n", + ei, entry->district_id, entry->owner_civ_id, entry->tile_count, entry->completed ? 1 : 0); + (*p_OutputDebugStringA)(ss); + + for (int ti = 0; ti < entry->tile_count; ti++) { + int tx = entry->tile_x[ti]; + int ty = entry->tile_y[ti]; + wrap_tile_coords (&p_bic_data->Map, &tx, &ty); + snprintf (ss, sizeof ss, " (%d,%d)%s\n", tx, ty, (ti == entry->assigned_tile_index) ? " [assigned]" : ""); + (*p_OutputDebugStringA)(ss); } + } - } else if ((bomber_ratio > 0) && - (out->OrderType == COT_Unit) && - (out->OrderID >= 0) && (out->OrderID < p_bic_data->UnitTypeCount) && - (p_bic_data->UnitTypes[out->OrderID].AI_Strategy & UTAI_Air_Defence)) { - int num_fighters = me->AI_Strategy_Unit_Counts[7], - num_bombers = me->AI_Strategy_Unit_Counts[6]; - if (100 * num_bombers < bomber_ratio * num_fighters) { - int best_bomber_type_id = -1, - best_bomber_rating = -1; - for (int n = 0; n < p_bic_data->UnitTypeCount; n++) { - UnitType * type = &p_bic_data->UnitTypes[n]; - if ((type->AI_Strategy & UTAI_Air_Bombard) && - patch_City_can_build_unit (this, __, n, 1, 0, 0)) { - int this_rating = rate_bomber (&p_bic_data->UnitTypes[n]); - if (this_rating > best_bomber_rating) { - best_bomber_type_id = n; - best_bomber_rating = this_rating; - } - } - } + for (int ei = 0; ei < is->ai_candidate_bridge_or_canals_count; ei++) { + struct ai_candidate_bridge_or_canal_entry * entry = &is->ai_candidate_bridge_or_canals[ei]; + if ((entry == NULL) || (entry->completed)) + continue; - if (best_bomber_type_id >= 0) { - int chance = 12 * bomber_ratio / 10; - if (rand_int (p_rand_object, __, 100) < chance) - out->OrderID = best_bomber_type_id; - } + for (int ti = 0; ti < entry->tile_count; ti++) { + int tx = entry->tile_x[ti]; + int ty = entry->tile_y[ti]; + wrap_tile_coords (&p_bic_data->Map, &tx, &ty); + Tile * tile = tile_at (tx, ty); + if ((tile == NULL) || (tile == p_null_tile)) + continue; + + int key = (int)tile; + int existing; + if (itable_look_up (&is->district_tile_map, key, &existing)) + continue; + + struct district_instance * inst = (struct district_instance *)calloc (1, sizeof *inst); + if (inst == NULL) + continue; + inst->state = DS_COMPLETED; + inst->district_id = entry->district_id; + inst->tile_x = tx; + inst->tile_y = ty; + + itable_insert (&is->district_tile_map, key, (int)inst); } } +} - is->ai_considering_production_for_city = NULL; +void __fastcall +patch_Map_Renderer_m08_Draw_Tile_Forests_Jungle_Swamp (Map_Renderer * this, int edx, int tile_x, int tile_y, Map_Renderer * map_renderer, int pixel_x, int pixel_y) +{ + if (! is->current_config.draw_forests_over_roads_and_railroads) { + Map_Renderer_m08_Draw_Tile_Forests_Jungle_Swamp (this, __, tile_x, tile_y, map_renderer, pixel_x, pixel_y); + return; + } + + Tile * tile = is->current_render_tile; + if ((tile == NULL) || (tile == p_null_tile)) + return; + + if ((tile->vtable->m50_Get_Square_BaseType (tile) == SQ_Forest) && + (*tile->vtable->m25_Check_Roads)(tile, __, 0)) { + is->draw_forests_over_roads_on_tile = true; + return; + } + + Map_Renderer_m08_Draw_Tile_Forests_Jungle_Swamp (this, __, tile_x, tile_y, map_renderer, pixel_x, pixel_y); } -int __fastcall -patch_Unit_disembark_passengers (Unit * this, int edx, int tile_x, int tile_y) +void __fastcall +patch_Map_Renderer_m52_Draw_Roads (Map_Renderer * this, int edx, int image_index, Map_Renderer * map_renderer, int pixel_x, int pixel_y) { - // Break any escort relationships if the escorting unit can't move onto the target tile. This prevents a freeze where the game continues - // trying to disembark units because an escortee looks like it could be disembarked except it can't because it won't move if its escorter - // can't be moved first. - Tile * tile = tile_at (this->Body.X, this->Body.Y), - * target = tile_at (tile_x , tile_y); - if (is->current_config.patch_blocked_disembark_freeze && (tile != NULL) && (target != NULL)) { - enum SquareTypes target_terrain = target->vtable->m50_Get_Square_BaseType (target); - FOR_UNITS_ON (uti, tile) { - Unit * escortee = get_unit_ptr (uti.unit->Body.escortee); - if ( (escortee != NULL) - && (uti.unit->Body.Container_Unit == this->Body.ID) - && (escortee->Body.Container_Unit == this->Body.ID) - && ( UnitType_has_ability (&p_bic_data->UnitTypes[uti.unit->Body.UnitTypeID], __, UTA_Immobile) - || ( is->current_config.disallow_trespassing - && check_trespassing (uti.unit->Body.CivID, tile, target) - && ! is_allowed_to_trespass (uti.unit)) - || Unit_is_terrain_impassable (uti.unit, __, target_terrain))) - Unit_set_escortee (uti.unit, __, -1); + // Don't draw roads if a bridge is here + if (is->current_config.enable_districts && is->current_config.enable_bridge_districts) { + Tile * tile = is->current_render_tile; + if ((tile != NULL) && (tile != p_null_tile)) { + struct district_instance * dist = get_district_instance (tile); + if ((dist != NULL) && (dist->district_id == BRIDGE_DISTRICT_ID)) { + return; + } } } - return Unit_disembark_passengers (this, __, tile_x, tile_y); + Map_Renderer_m52_Draw_Roads (this, __, image_index, map_renderer, pixel_x, pixel_y); + + if (! is->current_config.draw_forests_over_roads_and_railroads || ! is->draw_forests_over_roads_on_tile) + return; + + Map_Renderer_m08_Draw_Tile_Forests_Jungle_Swamp (this, __, is->current_render_tile_x, is->current_render_tile_y, map_renderer, pixel_x, pixel_y); } -// Returns whether or not the current deal being offered to the AI on the trade screen would be accepted. How advantageous the AI thinks the -// trade is for itself is stored in out_their_advantage if it's not NULL. This advantage is measured in gold, if it's positive it means the -// AI thinks it's gaining that much value from the trade and if it's negative it thinks it would be losing that much. I don't know what would -// happen if this function were called while the trade screen is not active. -bool -is_current_offer_acceptable (int * out_their_advantage) +void __fastcall +patch_Map_Renderer_m52_Draw_Railroads (Map_Renderer * this, int edx, int image_index, Map_Renderer * map_renderer, int pixel_x, int pixel_y) { - int their_id = p_diplo_form->other_party_civ_id; + // Don't draw railroads if a bridge is here + if (is->current_config.enable_districts && is->current_config.enable_bridge_districts) { + Tile * tile = is->current_render_tile; + if ((tile != NULL) && (tile != p_null_tile)) { + struct district_instance * dist = get_district_instance (tile); + if ((dist != NULL) && (dist->district_id == BRIDGE_DISTRICT_ID)) { + return; + } + } + } - DiploMessage consideration = Leader_consider_trade ( - &leaders[their_id], - __, - &p_diplo_form->our_offer_lists[their_id], - &p_diplo_form->their_offer_lists[their_id], - p_main_screen_form->Player_CivID, - 0, true, 0, 0, - out_their_advantage, - NULL, NULL); + Map_Renderer_m52_Draw_Railroads (this, __, image_index, map_renderer, pixel_x, pixel_y); - return consideration == DM_AI_ACCEPT; + if (! is->current_config.draw_forests_over_roads_and_railroads || ! is->draw_forests_over_roads_on_tile) + return; + + Map_Renderer_m08_Draw_Tile_Forests_Jungle_Swamp (this, __, is->current_render_tile_x, is->current_render_tile_y, map_renderer, pixel_x, pixel_y); } -// Adds an offer of gold to the list and returns it. If one already exists in the list, returns a pointer to it. -TradeOffer * -offer_gold (TradeOfferList * list, int is_lump_sum, int * is_new_offer) +void __fastcall +patch_Main_Screen_Form_m82_handle_key_event (Main_Screen_Form * this, int edx, int virtual_key_code, int is_down) { - if (list->length > 0) - for (TradeOffer * offer = list->first; offer != NULL; offer = offer->next) - if ((offer->kind == 7) && (offer->param_1 == is_lump_sum)) { - *is_new_offer = 0; - return offer; + char s[200]; + int * last_events = is->last_main_screen_key_up_events; + bool in_game = *p_player_bits != 0; // Player bits all zero indicates we aren't currently in a game. Need to check for this because UI events + // on the main menu also pass through this function. + + if (! is_down) { + for (int n = ARRAY_LEN (is->last_main_screen_key_up_events) - 1; n > 0; n--) + last_events[n] = last_events[n - 1]; + last_events[0] = virtual_key_code; + } + + if (is->current_config.enable_ai_city_location_desirability_display && + (virtual_key_code == VK_L) && is_down && + (! (is_command_button_active (&this->GUI, UCV_Load) || is_command_button_active (&this->GUI, UCV_Unload))) && + in_game) { + int is_debug_mode = (*p_debug_mode_bits & 4) != 0; // This is how the check is done in open_tile_info. Actually there are two debug + // mode bits (4 and 8) and I don't know what the difference is. + PopupForm * popup = get_popup_form (); + popup->vtable->set_text_key_and_flags (popup, __, is->mod_script_path, "C3X_CITY_LOC_HIGHLIGHTS", -1, 0, 0x40, 0); + snprintf (s, sizeof s, " %s", is->c3x_labels[CL_OFF]); + s[(sizeof s) - 1] = '\0'; + PopupSelection_add_item (&popup->selection, __, s, 0); + for (int n = 1; n < 32; n++) + if ((*p_player_bits & (1 << n)) && + (is_debug_mode || (n == p_main_screen_form->Player_CivID))) { + Race * race = &p_bic_data->Races[leaders[n].RaceID]; + snprintf (s, sizeof s, " (%d) %s", n, race->vtable->GetLeaderName (race)); + s[(sizeof s) - 1] = '\0'; + PopupSelection_add_item (&popup->selection, __, s, n); } + int sel = patch_show_popup (popup, __, 0, 0); + if (sel >= 0) { // -1 indicates popup was closed without making a selection + is->city_loc_display_perspective = (sel >= 1) ? sel : -1; + this->vtable->m73_call_m22_Draw ((Base_Form *)this); // Trigger map redraw + } - TradeOffer * tr = new (sizeof *tr); - *tr = (struct TradeOffer) { - .vtable = p_trade_offer_vtable, - .kind = 7, // TODO: Replace with enum - .param_1 = is_lump_sum, - .param_2 = 0, - .next = NULL, - .prev = NULL - }; + } else if (is->current_config.enable_debug_mode_switch && + (in_game && ! is_down) && + (last_events[4] == VK_D) && (last_events[3] == VK_E) && (last_events[2] == VK_B) && (last_events[1] == VK_U) && (last_events[0] == VK_G)) { + PopupForm * popup = get_popup_form (); + if ((*p_debug_mode_bits & 0xC) != 0) { // Consider debug mode on if either bit is set + *p_debug_mode_bits &= ~0xC; + popup->vtable->set_text_key_and_flags (popup, __, is->mod_script_path, "C3X_INFO", -1, 0, 0, 0); + PopupForm_add_text (popup, __, "Debug mode deactivated.", 0); + patch_show_popup (popup, __, 0, 0); + } else { + popup->vtable->set_text_key_and_flags (popup, __, is->mod_script_path, "C3X_CONFIRM_DEBUG_ACTIVATION", -1, 0, 0, 0); + int sel = patch_show_popup (popup, __, 0, 0); + if (sel == 0) { + *p_debug_mode_bits |= 0xC; + *(bool *)((int)p_human_player_bits + 37) = true; // Set MegaTrainerXL flag indicating edited save + } + } + this->vtable->m73_call_m22_Draw ((Base_Form *)this); // Trigger map redraw - if (list->length > 0) { - tr->prev = list->last; - list->last->next = tr; - list->last = tr; - list->length += 1; - } else { - list->last = list->first = tr; - list->length = 1; + // Worker keyboard shortcuts that would normally go to patch_Main_Screen_Form_issue_command + // unfortunately get short-circuited if a district is on a tile and the default popup to replace a "mine" is shown. + // The only way to catch these beforehand I've found it is to intercept the key event here. + } else if (is->current_config.enable_districts && + p_main_screen_form->Current_Unit != NULL && + is_down && + is_worker (p_main_screen_form->Current_Unit)) { + int command = -1; + bool removed_existing = false; + if (virtual_key_code == VK_M && is_command_button_active (&this->GUI, UCV_Build_Mine)) command = UCV_Build_Mine; + else if (virtual_key_code == VK_I && is_command_button_active (&this->GUI, UCV_Irrigate)) command = UCV_Irrigate; + else if (virtual_key_code == VK_N && is_command_button_active (&this->GUI, UCV_Plant_Forest)) command = UCV_Plant_Forest; + + if (handle_worker_command_that_may_replace_district (p_main_screen_form->Current_Unit, command, &removed_existing)) { + Main_Screen_Form_issue_command (this, __, command, p_main_screen_form->Current_Unit); + } } - *is_new_offer = 1; - return tr; +after_district_key_handling: + Main_Screen_Form_m82_handle_key_event (this, __, virtual_key_code, is_down); } -// Removes offer from list of offers but does not free it. Assumes offer is in the list. -void -remove_offer (TradeOfferList * list, TradeOffer * offer) +int __fastcall +patch_Unit_get_move_points_after_airdrop (Unit * this) { - if (list->length == 1) { - list->first = list->last = NULL; - list->length = 0; - } else if (list->length > 1) { - TradeOffer * prev = offer->prev, * next = offer->next; - if (prev) - prev->next = next; - if (next) - next->prev = prev; - if (list->first == offer) - list->first = next; - if (list->last == offer) - list->last = prev; - list->length -= 1; - } - offer->prev = offer->next = NULL; + int prev_airdrop_count = itable_look_up_or (&is->airdrops_this_turn, this->Body.ID, 0); + itable_insert (&is->airdrops_this_turn, this->Body.ID, prev_airdrop_count + 1); + + return is->current_config.dont_end_units_turn_after_airdrop ? this->Body.Moves : patch_Unit_get_max_move_points (this); } -void __fastcall -patch_PopupForm_set_text_key_and_flags (PopupForm * this, int edx, char * script_path, char * text_key, int param_3, int param_4, int param_5, int param_6) +int __fastcall +patch_Unit_get_move_points_after_set_to_intercept (Unit * this) { - int * p_stack = (int *)&script_path; - int ret_addr = p_stack[-1]; - - int is_initial_gold_trade = (ret_addr == ADDR_SETUP_INITIAL_GOLD_ASK_RETURN) || (ret_addr == ADDR_SETUP_INITIAL_GOLD_OFFER_RETURN), - is_modifying_gold_trade = (ret_addr == ADDR_SETUP_MODIFY_GOLD_ASK_RETURN ) || (ret_addr == ADDR_SETUP_MODIFY_GOLD_OFFER_RETURN); + return is->current_config.patch_intercept_lost_turn_bug ? this->Body.Moves : patch_Unit_get_max_move_points (this); +} - // This function gets called from all over the place, check that it's being called to setup the set gold amount popup in the trade screen - if (is->current_config.autofill_best_gold_amount_when_trading && (is_initial_gold_trade || is_modifying_gold_trade)) { - int asking = (ret_addr == ADDR_SETUP_INITIAL_GOLD_ASK_RETURN) || (ret_addr == ADDR_SETUP_MODIFY_GOLD_ASK_RETURN); - int is_lump_sum = is_initial_gold_trade ? - p_stack[TRADE_GOLD_SETTER_IS_LUMP_SUM_OFFSET] : // Read this variable from the caller's frame - is->modifying_gold_trade->param_1; +void __cdecl +activate_mod_info_button (int control_id) +{ + PopupForm * popup = get_popup_form (); + popup->vtable->set_text_key_and_flags (popup, __, is->mod_script_path, "C3X_INFO", -1, 0, 0, 0); + char s[500]; + char version_letter = 'A' + MOD_VERSION%100; - int their_id = p_diplo_form->other_party_civ_id, - our_id = p_main_screen_form->Player_CivID; + if (MOD_PREVIEW_VERSION == 0) + snprintf (s, sizeof s, "%s: %d%c", is->c3x_labels[CL_VERSION], MOD_VERSION/100, MOD_VERSION%100 != 0 ? version_letter : ' '); + else + snprintf (s, sizeof s, "%s: %d%c%s %d", is->c3x_labels[CL_VERSION], MOD_VERSION/100, MOD_VERSION%100 != 0 ? version_letter : ' ', is->c3x_labels[CL_PREVIEW], MOD_PREVIEW_VERSION); + s[(sizeof s) - 1] = '\0'; + PopupForm_add_text (popup, __, s, 0); - // This variable will store the result of the optimization process and it's what will be used as the final amount presented to the - // player in the popup and left in the TradeOffer object (if applicable). Default to zero for new offers and the previous amount when - // modifying an offer. When modifying, we also zero the amount on the table b/c the optimization process only works properly from that - // starting point. - int best_amount = 0; - if (is_modifying_gold_trade) { - best_amount = is->modifying_gold_trade->param_2; - is->modifying_gold_trade->param_2 = 0; - } + snprintf (s, sizeof s, "^%s:", is->c3x_labels[CL_CONFIG_FILES_LOADED]); + s[(sizeof s) - 1] = '\0'; + PopupForm_add_text (popup, __, s, 0); - int their_advantage; - bool is_original_acceptable = is_current_offer_acceptable (&their_advantage); + int n = 1; + for (struct loaded_config_name * lcn = is->loaded_config_names; lcn != NULL; lcn = lcn->next) { + snprintf (s, sizeof s, "^ %d. %s", n, lcn->name); + s[(sizeof s) - 1] = '\0'; + n++; + PopupForm_add_text (popup, __, s, 0); + } - // if (we're asking for money on an acceptable trade and are offering something) OR (we're offering money on an unacceptable trade and - // are asking for something) - if (( asking && is_original_acceptable && (p_diplo_form->our_offer_lists[their_id] .length > 0)) || - ((! asking) && (! is_original_acceptable) && (p_diplo_form->their_offer_lists[their_id].length > 0))) { + patch_show_popup (popup, __, 0, 0); +} - TradeOfferList * offers = asking ? &p_diplo_form->their_offer_lists[their_id] : &p_diplo_form->our_offer_lists[their_id]; - int test_offer_is_new; - TradeOffer * test_offer = offer_gold (offers, is_lump_sum, &test_offer_is_new); +int __fastcall +patch_Parameters_Form_m68_Show_Dialog (Parameters_Form * this, int edx, int param_1, void * param_2, void * param_3) +{ + init_mod_info_button_images (); - // When asking for gold, start at zero and work upwards. When offering gold it's more complicated. For lump sum - // offers, start with our entire treasury and work downward. For GPT offers, start with an upper bound and work - // downward. The upper bound depends on how much the AI thinks it's losing on the trade (in gold) divided by 20 - // (b/c 20 turn deal) with a lot of extra headroom just to make sure. - int starting_amount; { - if (asking) - starting_amount = 0; - else { - if (is_lump_sum) - starting_amount = leaders[our_id].Gold_Encoded + leaders[our_id].Gold_Decrement; - else { - int guess = not_below (0, 0 - their_advantage) / 20; - starting_amount = 10 + guess * 2; - } - } - } + // "b" is the mod info button. It's created each time the preferences form (or "parameters" form as Antal called it) is opened and destroyed + // every time the form is closed. This is the easiest way since it matches how the base game works. At first I tried creating the button once + // but then it will only appear once since its attachment to the prefs form is lost when the form is destroyed on closure. I tried reattaching + // the button to each newly created form but wasn't able to make that work. + Button * b = NULL; + if (is->mod_info_button_images_state == IS_OK) { + b = malloc (sizeof *b); + Button_construct (b); - // Check if optimization is still possible. It's not if we're offering gold and our maximum offer is unacceptable - test_offer->param_2 = starting_amount; - if (asking || is_current_offer_acceptable (NULL)) { + Button_initialize (b, __, + is->c3x_labels[CL_MOD_INFO_BUTTON_TEXT], // text + MOD_INFO_BUTTON_ID, // control ID + (p_bic_data->ScreenWidth - 1024) / 2 + 891, // location x + (p_bic_data->ScreenHeight - 768) / 2 + 31, // location y + MOD_INFO_BUTTON_WIDTH, MOD_INFO_BUTTON_HEIGHT, // width, height + (Base_Form *)this, // parent + 0); // ? - best_amount = starting_amount; - for (int step_size = asking ? 1000 : -1000; step_size != 0; step_size /= 10) { - test_offer->param_2 = best_amount; - while (1) { - test_offer->param_2 += step_size; - if (test_offer->param_2 < 0) - break; - else if (is_current_offer_acceptable (NULL)) - best_amount = test_offer->param_2; - else - break; - } - } - } + for (int n = 0; n < 3; n++) + b->Images[n] = &is->mod_info_button_images[n]; + PCX_Image_set_font (&b->Base_Data.Canvas, __, get_font (15, FSF_NONE), 0, 0, 0); + b->activation_handler = &activate_mod_info_button; - // Annoying little edge case: The limitation on AIs not to trade more than their entire treasury is handled in the - // interface not the trade evaluation logic so we have to limit our gold request here to their treasury (otherwise - // the amount will default to how much they would pay if they had infinite money). - int their_treasury = leaders[their_id].Gold_Encoded + leaders[their_id].Gold_Decrement; - if (asking && is_lump_sum && (best_amount > their_treasury)) - best_amount = their_treasury; + // Need to draw once manually or the button won't look right + b->vtable->m73_call_m22_Draw ((Base_Form *)b); + } - // Restore the trade table to its original state. Remove & free test_offer if it was newly created, otherwise put back its - // original amount. - if (test_offer_is_new) { - remove_offer (offers, test_offer); - test_offer->vtable->destruct (test_offer, __, 1); - } + int tr = Parameters_Form_m68_Show_Dialog (this, __, param_1, param_2, param_3); - // If we're offering gold on a trade that's already acceptable, the optimal amount is zero. We must handle this explicitly in case - // we're modifying an amount already on the table. If we didn't, the above condition wouldn't optimize the amount and we'd default to - // the original amount. - } else if ((! asking) && is_original_acceptable) - best_amount = 0; + if (b != NULL) { + b->vtable->destruct ((Base_Form *)b, __, 0); + free (b); + } - if (is_modifying_gold_trade) - is->modifying_gold_trade->param_2 = best_amount; - snprintf (is->ask_gold_default, sizeof is->ask_gold_default, "%d", best_amount); - is->ask_gold_default[(sizeof is->ask_gold_default) - 1] = '\0'; - PopupForm_set_text_key_and_flags (this, __, script_path, text_key, param_3, (int)is->ask_gold_default, param_5, param_6); - } else - PopupForm_set_text_key_and_flags (this, __, script_path, text_key, param_3, param_4, param_5, param_6); + return tr; } -CityLocValidity __fastcall -patch_Map_check_city_location (Map *this, int edx, int tile_x, int tile_y, int civ_id, bool check_for_city_on_tile) +bool __fastcall +patch_City_cycle_specialist_type (City * this, int edx, int mouse_x, int mouse_y, Citizen * citizen, City_Form * city_form) { - if (is->current_config.enable_natural_wonders) { - Tile * tile = tile_at (tile_x, tile_y); - if ((tile != NULL) && (tile != p_null_tile)) { - struct district_instance * inst = get_district_instance (tile); - if (inst != NULL && (inst->district_type == NATURAL_WONDER_DISTRICT_ID)) { - return CLV_BLOCKED; - } - } + int specialist_count = 0; { + Leader * city_owner = &leaders[this->Body.CivID]; + for (int n = 0; n < p_bic_data->CitizenTypeCount; n++) + specialist_count += (n != p_bic_data->default_citizen_type) && + Leader_has_tech (city_owner, __, p_bic_data->CitizenTypes[n].RequireID); } + int shift_down = (*p_GetAsyncKeyState) (VK_SHIFT) >> 8; + int original_worker_type = citizen->WorkerType; - int min_sep = is->current_config.minimum_city_separation; - CityLocValidity base_result = Map_check_city_location (this, __, tile_x, tile_y, civ_id, check_for_city_on_tile); - - // If minimum separation is one, make no change - if (min_sep == 1) - return base_result; - - // If minimum separation is <= 0, ignore the CITY_TOO_CLOSE objection to city placement unless the location is next to a city belonging to - // another civ and the settings forbid founding there. - else if ((min_sep <= 0) && (base_result == CLV_CITY_TOO_CLOSE)) { - if (is->current_config.disallow_founding_next_to_foreign_city) - for (int n = 1; n <= 8; n++) { - int x, y; - get_neighbor_coords (&p_bic_data->Map, tile_x, tile_y, n, &x, &y); - City * city = city_at (x, y); - if ((city != NULL) && (city->Body.CivID != civ_id)) - return CLV_CITY_TOO_CLOSE; - } - return CLV_OK; + // The return value of this function is not actually used by either of the two original callers. + bool tr = City_cycle_specialist_type (this, __, mouse_x, mouse_y, citizen, city_form); - // If we have an increased separation we might have to exclude some locations the base code allows. - } else if ((min_sep > 1) && (base_result == CLV_OK)) { - // Check tiles around (x, y) for a city. Because the base result is CLV_OK, we don't have to check neighboring tiles, just those at - // distance 2, 3, ... up to (an including) the minimum separation - for (int dist = 2; dist <= min_sep; dist++) { + // Cycle all the way around back to the previous specialist type, if appropriate. + // If the worker type was not changed after the first call to cycle_specialist_type, that indicates that the player was asked to disable + // governor management and chose not to. Do not try to cycle backwards in that case or else we'll spam the player with more popups. + if (is->current_config.reverse_specialist_order_with_shift && + shift_down && (specialist_count > 2) && (citizen->WorkerType != original_worker_type)) { + for (int n = 0; n < specialist_count - 2; n++) + City_cycle_specialist_type (this, __, mouse_x, mouse_y, citizen, city_form); + } - // vertices stores the unwrapped coords of the tiles at the vertices of the square of tiles at distance "dist" around - // (tile_x, tile_y). The order of the vertices is north, east, south, west. - struct vertex { - int x, y; - } vertices[4] = { - {tile_x , tile_y - 2*dist}, - {tile_x + 2*dist, tile_y }, - {tile_x , tile_y + 2*dist}, - {tile_x - 2*dist, tile_y } - }; + return tr; +} - // neighbor index for direction of tiles along edge starting from each vertex - // values correspond to directions: southeast, southwest, northwest, northeast - int edge_dirs[4] = {3, 5, 7, 1}; +int __fastcall +patch_City_get_pollution_from_pop (City * this) +{ + if (! is->current_config.enable_negative_pop_pollution) + return City_get_pollution_from_pop (this); - // Loop over verts and check tiles along their associated edges. The N vert is associated with the NE edge, the E vert with - // the SE edge, etc. - for (int vert = 0; vert < 4; vert++) { - wrap_tile_coords (&p_bic_data->Map, &vertices[vert].x, &vertices[vert].y); - int dx, dy; - neighbor_index_to_diff (edge_dirs[vert], &dx, &dy); - for (int j = 0; j < 2*dist; j++) { // loop over tiles along this edge - int cx = vertices[vert].x + j * dx, - cy = vertices[vert].y + j * dy; - wrap_tile_coords (&p_bic_data->Map, &cx, &cy); - if (city_at (cx, cy)) - return CLV_CITY_TOO_CLOSE; - } - } + int base_pollution = this->Body.Population.Size - p_bic_data->General.MaximumSize_City; + if (base_pollution <= 0) + return 0; + // Consider improvements + int net_pollution = base_pollution; + int any_cleaning_improvs = 0; + for (int n = 0; n < p_bic_data->ImprovementsCount; n++) { + Improvement * improv = &p_bic_data->Improvements[n]; + if ((improv->ImprovementFlags & ITF_Removes_Population_Pollution) && has_active_building (this, n)) { + any_cleaning_improvs = 1; + if (improv->Pollution < 0) + net_pollution += improv->Pollution; } - return base_result; - - } else - return base_result; + } + + if (net_pollution <= 0) + return 0; + else if (any_cleaning_improvs) + return 1; + else + return net_pollution; } -bool -is_zero_strength (UnitType * ut) +// The original version of this function in the base game contains a duplicate of the logic that computes the total pollution from buildings and +// pop. By re-implementing it to use the get_pollution_from_* functions, we ensure that our changes to get_pollution_from_pop will be accounted for. +// Note: This function is called from two places, one is the city screen and the other I'm not sure about. The pollution spawning logic works by +// calling the get_pollution_from_* funcs directly. +int __fastcall +patch_City_get_total_pollution (City * this) { - return (ut->Attack == 0) && (ut->Defence == 0); + return City_get_pollution_from_buildings (this) + patch_City_get_pollution_from_pop (this); } -bool -is_captured (Unit_Body * u) +void remove_extra_palaces (City * city, City * excluded_destination); + +void +set_wonder_built_flag (int improv_id, bool is_built) { - return (u->RaceID >= 0) && (u->CivID >= 0) && (u->CivID < 32) && leaders[u->CivID].RaceID != u->RaceID; + if ((p_match == NULL) || (improv_id < 0) || (improv_id >= p_bic_data->ImprovementsCount)) + return; + + byte * built_flags = *(byte **)((byte *)p_match + 0x4fc); + if (built_flags == NULL) + return; + + built_flags[improv_id] = is_built; } -// Decides whether or not units "a" and "b" are duplicates, i.e., whether they are interchangeable. -// If surface_only is set, the function checks only surface-level characteristics, meaning those that are visible to other players. bool -are_units_duplicate (Unit_Body * a, Unit_Body * b, bool surface_only) +choose_defensive_unit_order (City * city, City_Order * out_order) { - UnitType * a_type = &p_bic_data->UnitTypes[a->UnitTypeID], - * b_type = &p_bic_data->UnitTypes[b->UnitTypeID]; + if ((city == NULL) || (out_order == NULL)) + return false; - // If only doing a surface comparison, look through the alternate strategy types to the base types. The difference b/w the alt types is only - // their AI strategies, and that isn't considered a surface feature. - if (surface_only) { - while (a_type->alternate_strategy_for_id >= 0) - a_type = &p_bic_data->UnitTypes[a_type->alternate_strategy_for_id]; - while (b_type->alternate_strategy_for_id >= 0) - b_type = &p_bic_data->UnitTypes[b_type->alternate_strategy_for_id]; - } + for (int pass = 0; pass < 3; pass++) { + int best_unit_id = -1; + int best_defence = -1; - // a and b are duplicates "on the surface" if... - bool are_surface_duplicates = - // ... they belong to the same player ... - (a->CivID == b->CivID) && + for (int n = 0; n < p_bic_data->UnitTypeCount; n++) { + UnitType * type = &p_bic_data->UnitTypes[n]; + if (! patch_City_can_build_unit (city, __, n, 1, 0, 0)) + continue; - // ... they have the same type that is not [a leader OR army] AND not a transport AND ... - (a_type == b_type) && - (! (UnitType_has_ability (a_type, __, UTA_Leader) || UnitType_has_ability (a_type, __, UTA_Army))) && - (a_type->Transport_Capacity == 0) && + int defence = type->Defence; + if ((pass < 2) && (defence <= 0)) + continue; + if ((pass <= 1) && (type->Unit_Class != UTC_Land)) + continue; + if ((pass == 0) && ((type->AI_Strategy & UTAI_Defence) == 0)) + continue; - // ... they've taken the same amount of damage AND have the same charm status AND ... - (a->Damage == b->Damage) && (a->charmed == b->charmed) && + if (defence > best_defence) { + best_defence = defence; + best_unit_id = n; + } + } - // ... they're either both fortified or both not AND ... - (! (a->UnitState == UnitState_Fortifying) ^ (b->UnitState == UnitState_Fortifying)) && + if (best_unit_id >= 0) { + out_order->OrderType = COT_Unit; + out_order->OrderID = best_unit_id; + return true; + } + } - // ... [they have the same experience level OR are zero strength units] AND ... - ((a->Combat_Experience == b->Combat_Experience) || is_zero_strength (a_type)) && + return false; +} - // ... [they are both not contained in any unit OR they are both contained in the same unit] AND ... - (((a->Container_Unit < 0) && (b->Container_Unit < 0)) || (a->Container_Unit == b->Container_Unit)) && +// When a city adds a building that depends on a district, optionally mirror that +// building to all other same-civ cities that can also work the district tile. +void +copy_building_with_cities_in_radius (City * source, int improv_id, int required_district_id, int tile_x, int tile_y) +{ + if (! is->current_config.enable_districts) return; + if (source == NULL) return; - // ... neither one is carrying a princess AND ... - ((a->carrying_princess_of_race < 0) && (b->carrying_princess_of_race < 0)) && + Improvement * improv = &p_bic_data->Improvements[improv_id]; + bool is_wonder = (improv->Characteristics & (ITC_Wonder | ITC_Small_Wonder)) != 0; - // ... [they are both captured units OR are both native units] AND ... - (! (is_captured (a) ^ is_captured (b))) && + if (is_wonder) { + if (! is->current_config.cities_with_mutual_district_receive_wonders) + return; + } else if (! is->current_config.cities_with_mutual_district_receive_buildings) + return; - // ... their custom names are identical. - (0 == strncmp (a->Custom_Name.S, b->Custom_Name.S, sizeof (a->Custom_Name))); + // If a Wonder, we know the specific tile it is at, so determine which other cities have that in work radius. + if (is_wonder) { + Tile * tile = tile_at (tile_x, tile_y); + if (tile == p_null_tile) return; + if (tile->vtable->m38_Get_Territory_OwnerID (tile) != source->Body.CivID) return; - if ((! are_surface_duplicates) || surface_only) - return are_surface_duplicates; + struct district_instance * inst = get_district_instance (tile); + if (inst == NULL || inst->district_id != required_district_id) return; + if (! district_is_complete (tile, required_district_id)) return; - // a and b are additionally "deep", i.e. in all ways, duplicates if... - bool are_deep_duplicates = - // ... they've used up the same number of moves AND ... - (a->Moves == b->Moves) && + FOR_CITIES_OF (coi, source->Body.CivID) { + City * city = coi.city; + if (city == NULL) continue; + if (city == source) continue; - // ... they have matching statuses (has attacked this turn, etc.) AND states (is fortified, is doing worker action, etc.) AND automation status AND ... - (a->Status == b->Status) && (a->UnitState == b->UnitState) && (a->automated == b->automated) && + if (! city_radius_contains_tile (city, tile_x, tile_y)) + continue; - // ... they have both done the same number of airdrops this turn. - (itable_look_up_or (&is->airdrops_this_turn, a->ID, 0) == itable_look_up_or (&is->airdrops_this_turn, b->ID, 0)); + if (tile->vtable->m38_Get_Territory_OwnerID (tile) != city->Body.CivID) + continue; - return are_deep_duplicates; -} + if (! patch_City_has_improvement (city, __, improv_id, false)) { + City_add_or_remove_improvement (city, __, improv_id, 1, false); + } + } + } + // Else there may be multiple district instances of this type, so check each tile around the city + else { + FOR_DISTRICTS_AROUND (wai, source->Body.X, source->Body.Y, true) { + int x = wai.tile_x, y = wai.tile_y; + Tile * tile = wai.tile; + struct district_instance * inst = wai.district_inst; + if (inst->district_id != required_district_id) continue; -bool -is_busy (Unit * unit) -{ - int state = unit->Body.UnitState; - return ((state >= UnitState_Build_Mines) && (state <= UnitState_Explore)) || - (state == UnitState_Auto_Bombard) || (state == UnitState_Auto_Air_Bombard) || - unit->Body.automated; -} + FOR_CITIES_OF (coi, source->Body.CivID) { + City * city = coi.city; + if (city == NULL) continue; + if (city == source) continue; -int __fastcall -patch_Context_Menu_add_item_and_set_color (Context_Menu * this, int edx, int item_id, char * text, int red) -{ - // Initialize the array of duplicate counts. The array is 0x100 items long because that's the maximum number of items a menu can have. - if (is->current_config.group_units_on_right_click_menu && - (is->unit_menu_duplicates == NULL)) { - unsigned dups_size = 0x100 * sizeof is->unit_menu_duplicates[0]; - is->unit_menu_duplicates = malloc (dups_size); - memset (is->unit_menu_duplicates, 0, dups_size); - } + if (! city_radius_contains_tile (city, x, y)) + continue; - // Check if this menu item is a valid unit and grab pointers to its info - int unit_id; - Unit_Body * unit_body; - bool disable = false, put_icon = false; - int icon_index = 0; - if ((0 <= (unit_id = item_id - (0x13 + p_bic_data->UnitTypeCount))) && - (NULL != (unit_body = p_units->Units[unit_id].Unit)) && - (unit_body->UnitTypeID >= 0) && (unit_body->UnitTypeID < p_bic_data->UnitTypeCount)) { + if (! patch_City_has_improvement (city, __, improv_id, false)) { + City_add_or_remove_improvement (city, __, improv_id, 1, false); - if (is->current_config.group_units_on_right_click_menu) { - // Loop over all existing menu items and check if any of them is a unit that's a duplicate of the one being added - for (int n = 0; n < this->Item_Count; n++) { - Context_Menu_Item * item = &this->Items[n]; - int dup_unit_id = this->Items[n].Menu_Item_ID - (0x13 + p_bic_data->UnitTypeCount); - Unit_Body * dup_body; - if ((dup_unit_id >= 0) && - (NULL != (dup_body = p_units->Units[dup_unit_id].Unit)) && - are_units_duplicate (unit_body, dup_body, unit_body->CivID != p_main_screen_form->Player_CivID)) { - // The new item is a duplicate of the nth. Mark that in the duplicate counts array and return without actually - // adding the item. It doesn't matter what value we return because the caller doesn't use it. - is->unit_menu_duplicates[n] += 1; - return 0; + // If city already building it, switch to a defensive unit instead + int current_improv_id = city->Body.Order_ID; + if (current_improv_id == improv_id) { + City_Order defensive_order = { .OrderID = -1, .OrderType = 0 }; + if (choose_defensive_unit_order (city, &defensive_order)) { + UnitType * def_type = &p_bic_data->UnitTypes[defensive_order.OrderID]; + if (def_type->Unit_Class == UTC_Land) + City_set_production (city, __, defensive_order.OrderType, defensive_order.OrderID, false); + } + } + + // Show message to user + if (is->current_config.show_message_when_building_received_by_mutual_district && + city->Body.CivID == p_main_screen_form->Player_CivID) { + char msg[300]; + snprintf (msg, sizeof msg, "%s %s %s %s %s %s %s", + city->Body.CityName, + is->c3x_labels[CL_RECEIVED], + p_bic_data->Improvements[improv_id].Name.S, + is->c3x_labels[CL_FROM_SHARED], + is->district_configs[required_district_id].name, + is->c3x_labels[CL_WITH], + source->Body.CityName); + msg[(sizeof msg) - 1] = '\0'; + show_map_specific_text (city->Body.X, city->Body.Y, msg, true); + } } } } + } +} - if (unit_body->CivID == p_main_screen_form->Player_CivID) { - Unit * unit = (Unit *)((int)unit_body - offsetof (Unit, Body)); - UnitType * unit_type = &p_bic_data->UnitTypes[unit_body->UnitTypeID]; - bool no_moves_left = unit_body->Moves >= patch_Unit_get_max_move_points (unit); - if (no_moves_left && is->current_config.gray_out_units_on_menu_with_no_remaining_moves) - disable = true; +void +grant_existing_district_buildings_to_city (City * city) +{ + if (! is->current_config.enable_districts || + (! is->current_config.cities_with_mutual_district_receive_buildings && + ! is->current_config.cities_with_mutual_district_receive_wonders) || + (city == NULL)) + return; - // Put an icon next to this unit if we're configured to do so and it's not in an army - Unit * container; - if (is->current_config.put_movement_icons_on_units_on_menu && - ((NULL == (container = get_unit_ptr (unit_body->Container_Unit))) || - (! UnitType_has_ability (&p_bic_data->UnitTypes[container->Body.UnitTypeID], __, UTA_Army)))) { - put_icon = true; + int civ_id = city->Body.CivID; + int current_improv_id = city->Body.Order_ID; + bool prev_flag = is->sharing_buildings_by_districts_in_progress; + is->sharing_buildings_by_districts_in_progress = true; - bool attacker = is_offensive_combat_type (unit_type) || (Unit_has_ability (unit, __, UTA_Army) && (Unit_count_contained_units (unit) >= 1)), - busy = is_busy (unit); + FOR_DISTRICTS_AROUND (wai, city->Body.X, city->Body.Y, true) { + Tile * tile = wai.tile; - int icon_set_index = ((int)busy << 1) + (int)(! attacker); + struct district_instance * inst = wai.district_inst; + int district_id = inst->district_id; - int icon_row; { - if (no_moves_left) - icon_row = URCMI_CANT_MOVE; - else if (unit_body->Moves == 0) - icon_row = URCMI_UNMOVED; - else if (! attacker) - icon_row = URCMI_MOVED_NO_ATTACK; - else - icon_row = (! has_exhausted_attack (unit)) ? URCMI_MOVED_CAN_ATTACK : URCMI_MOVED_NO_ATTACK; - } + struct district_infos * info = &is->district_infos[district_id]; + if (info->dependent_building_count <= 0) + continue; - icon_index = icon_set_index * COUNT_UNIT_RCM_ICONS + icon_row; - } - } - } + FOR_CITIES_OF (coi, civ_id) { + City * other = coi.city; + if ((other == NULL) || (other == city)) + continue; - int tr = Context_Menu_add_item_and_set_color (this, __, item_id, text, red); + if (! city_radius_contains_tile (other, wai.tile_x, wai.tile_y)) + continue; - if (disable) - Context_Menu_disable_item (this, __, item_id); + if (tile->vtable->m38_Get_Territory_OwnerID (tile) != other->Body.CivID) + continue; - if (put_icon) { - init_unit_rcm_icons (); - if (is->unit_rcm_icon_state == IS_OK) - Context_Menu_put_image_on_item (this, __, item_id, &is->unit_rcm_icons[icon_index]); - } + for (int i = 0; i < info->dependent_building_count; i++) { + int building_id = info->dependent_building_ids[i]; + if (building_id < 0) + continue; - return tr; -} + Improvement * building = &p_bic_data->Improvements[building_id]; + bool is_wonder = (building->Characteristics & (ITC_Wonder | ITC_Small_Wonder)) != 0; -int __fastcall -patch_Context_Menu_open (Context_Menu * this, int edx, int x, int y, int param_3) -{ - int * p_stack = (int *)&x; - int ret_addr = p_stack[-1]; + if (is_wonder) + continue; - if (is->current_config.group_units_on_right_click_menu && - (ret_addr == ADDR_OPEN_UNIT_MENU_RETURN) && - (is->unit_menu_duplicates != NULL)) { + if (! patch_City_has_improvement (other, __, building_id, false)) + continue; - // Change the menu text to include the duplicate counts. This is pretty straight forward except for a couple little issues: we must - // use the game's internal malloc & free for compatibility with the base code and must call a function that widens the menu form, - // as necessary, to accommodate the longer strings. - for (int n = 0; n < this->Item_Count; n++) - if (is->unit_menu_duplicates[n] > 0) { - Context_Menu_Item * item = &this->Items[n]; - unsigned new_text_len = strlen (item->Text) + 20; - char * new_text = civ_prog_malloc (new_text_len); + if (patch_City_has_improvement (city, __, building_id, false)) + continue; - // Print entry text including dup count to new_text. Biggest complication here is that we want to print the dup count - // after any leading spaces to preserve indentation. - { - int num_spaces = 0; - while (item->Text[num_spaces] == ' ') - num_spaces++; - snprintf (new_text, new_text_len, "%.*s%dx %s", num_spaces, item->Text, is->unit_menu_duplicates[n] + 1, &item->Text[num_spaces]); - new_text[new_text_len - 1] = '\0'; + City_add_or_remove_improvement (city, __, building_id, 1, false); + + // If city already building it, switch to a defensive unit instead + if (current_improv_id == building_id) { + City_Order defensive_order = { .OrderID = -1, .OrderType = 0 }; + if (choose_defensive_unit_order (city, &defensive_order)) { + UnitType * def_type = &p_bic_data->UnitTypes[defensive_order.OrderID]; + if (def_type->Unit_Class == UTC_Land) + City_set_production (city, __, defensive_order.OrderType, defensive_order.OrderID, false); + } } - civ_prog_free (item->Text); - item->Text = new_text; - Context_Menu_widen_for_text (this, __, new_text); + if (is->current_config.show_message_when_building_received_by_mutual_district && + city->Body.CivID == p_main_screen_form->Player_CivID) { + char msg[300]; + snprintf (msg, sizeof msg, "%s %s %s %s %s %s %s", + city->Body.CityName, + is->c3x_labels[CL_RECEIVED], + p_bic_data->Improvements[building_id].Name.S, + is->c3x_labels[CL_FROM_SHARED], + is->district_configs[district_id].name, + is->c3x_labels[CL_WITH], + other->Body.CityName); + msg[(sizeof msg) - 1] = '\0'; + show_map_specific_text (city->Body.X, city->Body.Y, msg, true); + } } - - // Clear the duplicate counts - memset (is->unit_menu_duplicates, 0, 0x100 * sizeof is->unit_menu_duplicates[0]); + } } - return Context_Menu_open (this, __, x, y, param_3); -} - -bool -is_material_unit (UnitType const * type, bool * out_is_pop_else_caravan) -{ - int join_city_action = UCV_Join_City & 0x0FFFFFFF; // To get the join city action code, use the command value and mask out the top 4 category bits - int disband_action = UCV_Disband & 0x0FFFFFFF; - if ((type->Attack | type->Defence | type->Bombard_Strength) == 0) { // if non-combat unit - if ((type->PopulationCost > 0) && (type->Worker_Actions == join_city_action)) { - *out_is_pop_else_caravan = true; - return true; - } else if ((type->Standard_Actions & disband_action) && (type->Worker_Actions == 0)) { - *out_is_pop_else_caravan = false; - return true; - } else - return false; - } else - return false; + is->sharing_buildings_by_districts_in_progress = prev_flag; } void -ai_move_material_unit (Unit * this) +auto_build_great_wall_districts_for_civ (int civ_id) { - int type_id = this->Body.UnitTypeID; - Tile * tile = tile_at (this->Body.X, this->Body.Y); - UnitType * type = &p_bic_data->UnitTypes[type_id]; - - // Determine whether this is a pop unit (will search for a city to join) or a caravan (will search for a city to disband) - int join_city_action = UCV_Join_City & 0x0FFFFFFF; - bool pop_else_caravan = type->Worker_Actions == join_city_action; + if ((! is->current_config.enable_districts) || + (! is->current_config.enable_great_wall_districts) || + (! is->current_config.auto_build_great_wall_around_territory) || + (is->great_wall_auto_build == GWABS_DONE) || + (civ_id < 0) || + is->is_placing_scenario_things) + return; - int continent_id = tile->vtable->m46_Get_ContinentID (tile); + bool is_human = (*p_human_player_bits & (1 << civ_id)) != 0; - // This part of the code follows how the replacement leader AI works. Basically it disbands the unit if it's on a continent with no - // friendly cities, then flees if it's in danger, then moves it along a set path if it has one. - if (leaders[this->Body.CivID].city_count_per_cont[continent_id] == 0) { - Unit_disband (this); - return; - } - if (any_enemies_near_unit (this, 49)) { - Unit_set_state (this, __, UnitState_Fleeing); - bool done = this->vtable->work (this); - if (done || (this->Body.UnitState != 0)) - return; - } - byte next_move = Unit_pop_next_move_from_path (this); - if (next_move > 0) { - this->vtable->Move (this, __, next_move, 0); + if ((GREAT_WALL_DISTRICT_ID < 0) || (GREAT_WALL_DISTRICT_ID >= is->district_count)) { + is->great_wall_auto_build = GWABS_DONE; return; } - // Find the best city to act on - City * best_city = NULL; - int best_city_value = pop_else_caravan ? -1 : 0; // min value to act is 0 for pop units, 1 for caravans - FOR_CITIES_OF (coi, this->Body.CivID) { - City * city = coi.city; - Tile * city_tile = tile_at (city->Body.X, city->Body.Y); - if (continent_id == city_tile->vtable->m46_Get_ContinentID (city_tile)) { + init_tile_highlights (); - if (pop_else_caravan) { - // Skip this city if it can't support another citizen - if ((city->Body.FoodIncome <= 0) || - (City_requires_improvement_to_grow (city) > -1)) - continue; - } else { - // Skip this city if its current build can't be rushed - if (! City_can_take_outside_shields (city, __, 0)) - continue; - } + struct district_config const * cfg = &is->district_configs[GREAT_WALL_DISTRICT_ID]; + if ((cfg->command == -1) || (! leader_can_build_district (&leaders[civ_id], GREAT_WALL_DISTRICT_ID))) { + is->great_wall_auto_build = GWABS_DONE; + return; + } - // Consider distance. - int dist_in_turns; - if (! estimate_travel_time (this, city->Body.X, city->Body.Y, &dist_in_turns)) - continue; // No path or unit cannot move + PopupForm * popup = get_popup_form (); + set_popup_str_param (0, (char *)is->district_configs[GREAT_WALL_DISTRICT_ID].display_name, -1, -1); + popup->vtable->set_text_key_and_flags (popup, __, is->mod_script_path, "C3X_BEGIN_GREAT_WALL_AUTO_BUILD", -1, 0, 0, 0); + if (civ_id == p_main_screen_form->Player_CivID) + patch_show_popup (popup, __, 0, 0); - int value; - if (pop_else_caravan) - value = 1000 / (10 + dist_in_turns); // Base value of 100 * 10 / (10 + dist_in_turn) - else { - // value is number of useful shields we'd get by moving to this city and disbanding there - int shields_per_turn = get_city_production_rate (city, city->Body.Order_Type, city->Body.Order_ID), - shields_to_complete_build = City_get_order_cost (city) - City_get_order_progress (city) - shields_per_turn, - disband_value = Leader_get_unit_cost (&leaders[city->Body.CivID], __, type_id, false) >> 2; - value = not_above (shields_to_complete_build, disband_value) - shields_per_turn * dist_in_turns; - } + is->great_wall_auto_build = GWABS_RUNNING; - // Scale value by city corruption rate for pop units or caravan units targeting cities building improvs - if (pop_else_caravan || (city->Body.Order_Type == COT_Improvement)) { - int good_shields = city->Body.ProductionIncome, - corrupt_shields = city->Body.ProductionLoss; - if (good_shields + corrupt_shields > 0) - value = (value * good_shields) / (good_shields + corrupt_shields); - else - value = -1; - } + unsigned int const replaceable_flags = TILE_FLAG_MINE | TILE_FLAG_IRRIGATION; + bool require_other_civ_border = (! is_human) && is->current_config.ai_auto_build_great_wall_strategy == AAGWS_OTHER_CIV_BORDERED_ONLY; - if (value > best_city_value) { - best_city = city; - best_city_value = value; - } - } - } + for (int index = 0; index < p_bic_data->Map.TileCount; index++) { + int x, y; + tile_index_to_coords (&p_bic_data->Map, index, &x, &y); + Tile * tile = tile_at (x, y); + if ((tile == NULL) || (tile == p_null_tile)) + continue; + if (tile->CityID >= 0) + continue; + if (tile->vtable->m35_Check_Is_Water (tile)) + continue; + if (tile->vtable->m38_Get_Territory_OwnerID (tile) != civ_id) + continue; - // Join city if we're already in the city we want to join, otherwise move to that city. If we couldn't find a city to join, go to the - // nearest established city and wait. - City * in_city = get_city_ptr (tile->vtable->m45_Get_City_ID (tile)); - City * moving_to_city = NULL; - if (best_city != NULL) { - if (best_city == in_city) { - if (pop_else_caravan && patch_Unit_can_perform_command (this, __, UCV_Join_City)) { - Unit_join_city (this, __, in_city); - return; - } else if ((! pop_else_caravan) && patch_Unit_can_perform_command (this, __, UCV_Disband)) { - Unit_disband (this); - return; + bool has_border = false; + bool has_other_civ_border = false; + FOR_TILES_AROUND (tai, 9, x, y) { + if (tai.n == 0) + continue; + Tile * neighbor = tai.tile; + if (neighbor->vtable->m35_Check_Is_Water (neighbor)) + continue; + int owner_id = neighbor->vtable->m38_Get_Territory_OwnerID (neighbor); + if (owner_id != civ_id) { + has_border = true; + if (owner_id > 0) { + has_other_civ_border = true; + break; + } + } } - } else - moving_to_city = best_city; - } else if (in_city == NULL) - moving_to_city = find_nearest_established_city (this, continent_id); - - if (moving_to_city) { - int first_move = patch_Trade_Net_set_unit_path (is->trade_net, __, this->Body.X, this->Body.Y, moving_to_city->Body.X, moving_to_city->Body.Y, this, this->Body.CivID, 0x101, NULL); - if (first_move > 0) { - Unit_set_escortee (this, __, -1); - this->vtable->Move (this, __, first_move, 0); - return; - } - } - - // Nothing to do, nowhere to go, just fortify in place. - Unit_set_escortee (this, __, -1); - Unit_set_state (this, __, UnitState_Fortifying); -} - -int __stdcall -patch_get_anarchy_length (int leader_id) -{ - int base = get_anarchy_length (leader_id); - int multiplier = is->current_config.anarchy_length_percent; - if (multiplier != 100) { - // Anarchy cannot be less than 2 turns or you'll get an instant government switch. Only let that happen when the percent multiplier is - // set below zero, indicating a desire for no anarchy. Otherwise we can't reduce anarchy below 2 turns. - if (multiplier < 0) - return 1; - else if (base <= 2) - return base; - else - return not_below (2, rand_div (base * multiplier, 100)); - } else - return base; -} + if (! has_border) + continue; + if (require_other_civ_border && (! has_other_civ_border)) + continue; -bool __fastcall -patch_Unit_check_king_ability_while_spawning (Unit * this, int edx, enum UnitTypeAbilities a) -{ - if (is->current_config.dont_give_king_names_in_non_regicide_games && - ((*p_toggleable_rules & (TR_REGICIDE | TR_MASS_REGICIDE)) == 0)) - return false; - else - return Unit_has_ability (this, __, a); -} + if (! district_is_buildable_on_square_type (cfg, tile)) + continue; + if (! district_resource_prereqs_met (tile, x, y, GREAT_WALL_DISTRICT_ID, NULL)) + continue; -int __fastcall -patch_Map_compute_neighbor_index_for_pass_between (Map * this, int edx, int x_home, int y_home, int x_neigh, int y_neigh, int lim) -{ - if (is->current_config.enable_land_sea_intersections) - return 0; - else - return Map_compute_neighbor_index (this, __, x_home, y_home, x_neigh, y_neigh, lim); -} + struct district_instance * inst = get_district_instance (tile); + if ((inst != NULL) && (inst->district_id == GREAT_WALL_DISTRICT_ID)) { + if (! district_is_complete (tile, GREAT_WALL_DISTRICT_ID)) { + inst->state = DS_COMPLETED; + if (! tile->vtable->m18_Check_Mines (tile, __, 0)) + tile->vtable->m56_Set_Tile_Flags (tile, __, 0, TILE_FLAG_MINE, x, y); + set_tile_unworkable_for_all_cities (tile, x, y); + } + continue; + } -// This call replacement used to be part of improvement perfuming but now its only purpose is to set ai_considering_order when -// ai_choose_production is looping over improvements. -bool __fastcall -patch_Improvement_has_wonder_com_bonus_for_ai_prod (Improvement * this, int edx, enum ImprovementTypeWonderFeatures flag) -{ - is->ai_considering_order.OrderID = this - p_bic_data->Improvements; - is->ai_considering_order.OrderType = COT_Improvement; - return Improvement_has_wonder_flag (this, __, flag); -} + unsigned int overlay_flags = tile->vtable->m42_Get_Overlays (tile, __, 0); + unsigned int replace_flags = overlay_flags & replaceable_flags; + bool has_district = (inst != NULL); + int existing_district_id = -1; + if (has_district) + existing_district_id = inst->district_id; + + if (is_human && civ_id == p_main_screen_form->Player_CivID) { + is->focused_tile = tile; + Main_Screen_Form_bring_tile_into_view (p_main_screen_form, __, x, y, 0, true, false); + + char * popup_key = "C3X_CONFIRM_BUILD_GREAT_WALL"; + set_popup_str_param (0, (char *)is->district_configs[GREAT_WALL_DISTRICT_ID].display_name, -1, -1); + + if (has_district) { + bool redundant_district = district_instance_is_redundant (inst, tile); + bool would_lose_buildings; + would_lose_buildings = any_nearby_city_would_lose_district_benefits (existing_district_id, civ_id, x, y); + if (redundant_district) + would_lose_buildings = false; + if ((existing_district_id >= 0) && (existing_district_id < is->district_count)) { + set_popup_str_param (1, (char *)is->district_configs[existing_district_id].display_name, -1, -1); + } + popup_key = would_lose_buildings + ? "C3X_CONFIRM_BUILD_GREAT_WALL_OVER_DISTRICT" + : "C3X_CONFIRM_BUILD_GREAT_WALL_OVER_DISTRICT_SAFE"; + } else if (replace_flags != 0) { + popup_key = "C3X_CONFIRM_BUILD_GREAT_WALL_OVER_IMPROVEMENT"; + } -// Similarly, this one sets the var when looping over unit types. -bool __fastcall -patch_UnitType_has_strat_0_for_ai_prod (UnitType * this, int edx, byte n) -{ - is->ai_considering_order.OrderID = this - p_bic_data->UnitTypes; - is->ai_considering_order.OrderType = COT_Unit; - return UnitType_has_ai_strategy (this, __, n); -} + popup->vtable->set_text_key_and_flags ( + popup, __, is->mod_script_path, + popup_key, + -1, 0, 0, 0); -int -compare_ai_prod_valuations (void const * vp_a, void const * vp_b) -{ - struct ai_prod_valuation const * a = vp_a, - * b = vp_b; - if (a->point_value > b->point_value) return -1; - else if (b->point_value > a->point_value) return 1; - else return 0; -} + int sel = patch_show_popup (popup, __, 0, 0); + if (sel != 0) + continue; + } -void -rank_ai_production_options (City * city) -{ - is->count_ai_prod_valuations = 0; - City_Order unused; - patch_City_ai_choose_production (city, __, &unused); // records valuations in is->ai_prod_valuations - qsort (is->ai_prod_valuations, is->count_ai_prod_valuations, sizeof is->ai_prod_valuations[0], compare_ai_prod_valuations); -} + if (has_district || (replace_flags != 0)) { + if (has_district) { + int inst_x = x, inst_y = y; + if (! district_instance_get_coords (inst, tile, &inst_x, &inst_y)) + continue; + remove_district_instance (tile); + tile->vtable->m62_Set_Tile_BuildingID (tile, __, -1); + tile->vtable->m51_Unset_Tile_Flags (tile, __, 0, TILE_FLAG_MINE, inst_x, inst_y); + handle_district_removed (tile, existing_district_id, inst_x, inst_y, false); + } -void __fastcall -patch_City_Form_m82_handle_key_event (City_Form * this, int edx, int virtual_key_code, int is_down) -{ - if (is->current_config.enable_ai_production_ranking && - (~*p_human_player_bits & (1 << this->CurrentCity->Body.CivID)) && - (virtual_key_code == VK_P) && is_down) { - rank_ai_production_options (this->CurrentCity); - PopupForm * popup = get_popup_form (); - popup->vtable->set_text_key_and_flags (popup, __, is->mod_script_path, "C3X_AI_PROD_RANKING", -1, 0, 0, 0); - char s[200]; - for (int n = 0; n < is->count_ai_prod_valuations; n++) { - struct ai_prod_valuation const * val = &is->ai_prod_valuations[n]; - char * name = (val->order_type == COT_Improvement) ? p_bic_data->Improvements[val->order_id].Name.S : p_bic_data->UnitTypes[val->order_id].Name; + if (replace_flags != 0) + tile->vtable->m51_Unset_Tile_Flags (tile, __, 0, replace_flags, x, y); + } - int show_strategy = -1; - if (val->order_type == COT_Unit) - itable_look_up (&is->unit_type_alt_strategies, val->order_id, &show_strategy); + if (get_district_instance (tile) != NULL) + continue; - if ((show_strategy >= 0) && (show_strategy <= CL_LAST_UNIT_STRAT - CL_FIRST_UNIT_STRAT)) - snprintf (s, sizeof s, "^%d %s (%s)", val->point_value, name, is->c3x_labels[CL_FIRST_UNIT_STRAT + show_strategy]); - else - snprintf (s, sizeof s, "^%d %s", val->point_value, name); - s[(sizeof s) - 1] = '\0'; - PopupForm_add_text (popup, __, s, 0); - } - patch_show_popup (popup, __, 0, 0); + inst = ensure_district_instance (tile, GREAT_WALL_DISTRICT_ID, x, y); + if (inst == NULL) + continue; - } else if (is->current_config.toggle_zoom_with_z_on_city_screen && - (virtual_key_code == VK_Z) && is_down) { - p_bic_data->is_zoomed_out = ! p_bic_data->is_zoomed_out; - Main_Screen_Form_bring_tile_into_view (p_main_screen_form, __, this->CurrentCity->Body.X, get_city_screen_center_y (this->CurrentCity), 0, true, false); // also redraws map - this->Base.vtable->m73_call_m22_Draw ((Base_Form *)this); + inst->district_id = GREAT_WALL_DISTRICT_ID; + district_instance_set_coords (inst, x, y); + inst->state = DS_COMPLETED; + if (! tile->vtable->m18_Check_Mines (tile, __, 0)) + tile->vtable->m56_Set_Tile_Flags (tile, __, 0, TILE_FLAG_MINE, x, y); + set_tile_unworkable_for_all_cities (tile, x, y); } - City_Form_m82_handle_key_event (this, __, virtual_key_code, is_down); + is->great_wall_auto_build = GWABS_DONE; + is->focused_tile = NULL; } -bool -can_harvest_shields_from_forest (Tile * tile) -{ - int flags = tile->vtable->m43_Get_field_30 (tile); - return (flags & 0x10000000) == 0; -} +//We need to forwards-declare this +void __fastcall +patch_City_manage_by_governor (City * this, int edx, bool manage_professions); void __fastcall -patch_open_tile_info (void * this, int edx, int mouse_x, int mouse_y, int civ_id) +patch_City_add_or_remove_improvement (City * this, int edx, int improv_id, int add, bool param_3) { - int tx, ty; - if (is->current_config.show_detailed_tile_info && - (! Main_Screen_Form_get_tile_coords_under_mouse (p_main_screen_form, __, mouse_x, mouse_y, &tx, &ty))) { - is->viewing_tile_info_x = tx; - is->viewing_tile_info_y = ty; - } else - is->viewing_tile_info_x = is->viewing_tile_info_y = -1; + int init_maintenance = this->Body.Improvements_Maintenance; + Improvement * improv = &p_bic_data->Improvements[improv_id]; + bool is_wonder_removal = (! add) && + ((improv->Characteristics & (ITC_Wonder | ITC_Small_Wonder)) != 0) && + (! is->is_placing_scenario_things); + int init_work_area_radius = get_work_ring_limit_total(this); - return open_tile_info (this, __, mouse_x, mouse_y, civ_id); -} + // The enable_negative_pop_pollution feature changes the rules so that improvements flagged as removing pop pollution and having a negative + // pollution amount contribute to the city's pop pollution instead of building pollution. Here we make sure that such improvements do not + // contribute to building pollution by temporarily zeroing out their pollution stat when they're added to or removed from a city. + if (is->current_config.enable_negative_pop_pollution && + (improv->ImprovementFlags & ITF_Removes_Population_Pollution) && + (improv->Pollution < 0)) { + int saved_pollution_amount = improv->Pollution; + improv->Pollution = 0; + City_add_or_remove_improvement (this, __, improv_id, add, param_3); + improv->Pollution = saved_pollution_amount; + } else + City_add_or_remove_improvement (this, __, improv_id, add, param_3); -int __fastcall -patch_Match_ai_eval_city_location (void * this, int edx, int x, int y, int civ_id, bool param_4, int * out_breakdown) -{ - is->ai_evaling_city_loc_x = x; - is->ai_evaling_city_loc_y = y; - is->ai_evaling_city_field_30_get_counter = 0; + if (is->current_config.enable_districts && is_wonder_removal && ((improv->Characteristics & ITC_Wonder) != 0)) { + if (is->current_config.destroyed_wonders_can_be_built_again) + set_wonder_built_flag (improv_id, false); - return Match_ai_eval_city_location (this, __, x, y, civ_id, param_4, out_breakdown); -} + PopupForm * popup = get_popup_form (); + set_popup_str_param (0, improv->Name.S, -1, -1); + popup->vtable->set_text_key_and_flags ( + popup, __, is->mod_script_path, + is->current_config.destroyed_wonders_can_be_built_again ? "C3X_DISTRICT_WONDER_DESTROYED_REBUILD" : "C3X_DISTRICT_WONDER_DESTROYED", + -1, 0, 0, 0 + ); + patch_show_popup (popup, __, 0, 0); + } -bool -is_explored (Tile * tile, int civ_id) -{ - unsigned explored_bits = tile->Body.Fog_Of_War; - int in_debug_mode = (*p_debug_mode_bits & 8) != 0; // checking bit 3 here b/c that's how resource visibility is checked in open_tile_info - if (in_debug_mode || (explored_bits & (1 << civ_id))) - return true; - else if (is->current_config.share_visibility_in_hotseat && // if shared hotseat vis is enabled AND - (*p_is_offline_mp_game && ! *p_is_pbem_game) && // is hotseat game AND - ((1 << civ_id) & *p_human_player_bits) && // "civ_id" is a human player AND - (explored_bits & *p_human_player_bits)) // any human player has visibility on the tile - return true; - else - return false; -} + // If the city just finished a wonder and was using a wonder district for that, set the wonder + // as completed (which switches the art over and prevents the tile from being used again) + int x, y; + if (add && is->current_config.enable_districts && is->current_config.enable_wonder_districts) { + if (improv->Characteristics & (ITC_Wonder | ITC_Small_Wonder)) { + int matched_windex = find_wonder_config_index_by_improvement_id (improv_id); -// On the GOG executable, this function intercepts the call to draw the "No information available" text on a unexplored (black) tile. In that case we -// replace that text with the coords. But on the Steam EXE this func also intercepts the call that draws the resource name on the visible tile info -// box b/c the call site is reused via a jump. So we must check that the tile is actually unexplored before replacing the text so that the resource -// name doesn't get overwritten. -int __fastcall -patch_PCX_Image_draw_no_tile_info (PCX_Image * this, int edx, char * str, int x, int y, int str_len) -{ - Tile * tile = tile_at (is->viewing_tile_info_x, is->viewing_tile_info_y); - if ((tile != p_null_tile) && ! is_explored (tile, p_main_screen_form->Player_CivID)) { - char s[100]; - snprintf (s, sizeof s, "(%d, %d)", is->viewing_tile_info_x, is->viewing_tile_info_y); - s[(sizeof s) - 1] = '\0'; - return PCX_Image_draw_text (this, __, s, x, y, strlen (s)); - } else - return PCX_Image_draw_text (this, __, str, x, y, str_len); -} + if (matched_windex >= 0) { + FOR_DISTRICTS_AROUND (wai, this->Body.X, this->Body.Y, true) { + x = wai.tile_x; + y = wai.tile_y; + Tile * t = wai.tile; + struct district_instance * inst = wai.district_inst; + if (inst->district_id != WONDER_DISTRICT_ID) continue; + if (! wonder_is_buildable_on_tile (t, improv_id)) + continue; -int __fastcall -patch_PCX_Image_draw_tile_info_terrain (PCX_Image * this, int edx, char * str, int x, int y, int str_len) -{ - Tile * tile = tile_at (is->viewing_tile_info_x, is->viewing_tile_info_y); - if (tile != p_null_tile) { - bool show_district_name = false; - - char s[200]; - if (is->current_config.enable_districts || is->current_config.enable_natural_wonders) { - // Draw district name to the right of terrain name if tile has one - struct district_instance * dist = get_district_instance (tile); - if (dist != NULL) { - show_district_name = true; - char const * display_name = is->district_configs[dist->district_type].name; + struct wonder_district_info * info = &inst->wonder_info; + if (info->state != WDS_UNDER_CONSTRUCTION) continue; + if (info->city_id != this->Body.ID) continue; - // If it's a wonder district with a completed wonder, show the wonder name instead - if ((dist->district_type == WONDER_DISTRICT_ID) && - (dist->wonder_info.state == WDS_COMPLETED) && - (dist->wonder_info.wonder_index >= 0) && - (dist->wonder_info.wonder_index < is->wonder_district_count)) { - char const * wonder_name = is->wonder_district_configs[dist->wonder_info.wonder_index].wonder_name; - if ((wonder_name != NULL) && (wonder_name[0] != '\0')) { - display_name = wonder_name; - } - } else if ((dist->district_type == NATURAL_WONDER_DISTRICT_ID) && - (dist->natural_wonder_info.natural_wonder_id >= 0) && - (dist->natural_wonder_info.natural_wonder_id < is->natural_wonder_count)) { - int natural_id = dist->natural_wonder_info.natural_wonder_id; - char const * natural_name = is->natural_wonder_configs[natural_id].name; - if ((natural_name != NULL) && (natural_name[0] != '\0')) { - display_name = natural_name; - } + // Mark this wonder district as completed with the wonder + info->city = this; + info->city_id = this->Body.ID; + info->state = WDS_COMPLETED; + info->wonder_index = matched_windex; + break; } - - snprintf (s, sizeof s, "%s", display_name); - PCX_Image_draw_text (this, __, s, x + 68, y, strlen (s)); } } - // Draw tile coords on line below terrain name - snprintf (s, sizeof s, "(%d, %d)", is->viewing_tile_info_x, is->viewing_tile_info_y); - PCX_Image_draw_text (this, __, s, x, y + 14, strlen (s)); + } - if ((is->city_loc_display_perspective >= 0) && - ((1 << is->city_loc_display_perspective) & *p_player_bits)) { - int eval = patch_Match_ai_eval_city_location (p_match, __, is->viewing_tile_info_x, is->viewing_tile_info_y, is->city_loc_display_perspective, false, NULL); - if (eval > 0) { - snprintf (s, sizeof s, "%d", eval - 1000000); - PCX_Image_draw_text (this, __, s, x + 95, y, strlen (s)); + int gw_auto_build_improv_id = is->current_config.great_wall_auto_build_wonder_improv_id; + if (add && is->current_config.enable_districts && + is->current_config.auto_build_great_wall_around_territory && + (gw_auto_build_improv_id >= 0) && (improv_id == gw_auto_build_improv_id)) + auto_build_great_wall_districts_for_civ (this->Body.CivID); + + //Calculate if work_area has shrunk, and if so, redistribute citizens. + int post_work_area_radius = get_work_ring_limit_total(this); + if (post_work_area_radius < init_work_area_radius) { + patch_City_manage_by_governor(this, __, false); + } + + // Update things in case we've added or removed a mill. This is only necessary while in-game. If the game is still loading the scenario, it + // will recompute resources after it's done and we'll recompute yields and happiness ourselves. + if (! is->is_placing_scenario_things) { + // Collect info about this mill, if in fact the added or removed improvement is a mill. If it's not, all these vars will be left + // false. "generates_input" tracks whether or not the mill generates a resource that's an input for another mill. + bool is_non_local_mill, is_yielding_mill, generates_input; { + is_non_local_mill = is_yielding_mill = generates_input = false; + for (int n = 0; n < is->current_config.count_mills; n++) { + struct mill * mill = &is->current_config.mills[n]; + if (mill->improv_id == improv_id) { + is_non_local_mill |= (mill->flags & MF_LOCAL) == 0; + is_yielding_mill |= (mill->flags & MF_YIELDS) != 0; + generates_input |= (is->mill_input_resource_bits[mill->resource_id >> 3] & (1 << (mill->resource_id & 7))) != 0; + } } } - // If tile has been chopped and district name not shown (uses same space), indicate that to the right of the terrain name - if (! can_harvest_shields_from_forest (tile) && !show_district_name) - PCX_Image_draw_text (this, __, is->c3x_labels[CL_CHOPPED], x + 145, y, strlen (is->c3x_labels[CL_CHOPPED])); + // If the mill generates a resource that's added to the trade network or it's generating an input (potentially used locally to + // generate a traded resource) then rebuild the resource network. This is not necessary if the improvement also affects trade routes + // since the base method will have already done this recomputation. + if ((is_non_local_mill || generates_input) && + ((improv->ImprovementFlags & ITF_Allows_Water_Trade) == 0) && + ((improv->ImprovementFlags & ITF_Allows_Air_Trade) == 0) && + ((improv->WonderFlags & ITW_Safe_Sea_Travel) == 0)) + patch_Trade_Net_recompute_resources (is->trade_net, __, 0); + + // If the mill adds yields or might be a link in a resource production chain that does, recompute yields in the city. + if (is_yielding_mill || generates_input) + patch_City_recompute_yields_and_happiness (this); } - return PCX_Image_draw_text (this, __, str, x, y, str_len); -} -int __fastcall -patch_City_compute_corrupted_yield (City * this, int edx, int gross_yield, bool is_production) -{ - Leader * leader = &leaders[this->Body.CivID]; + // Adding or removing an obsolete improvement should not change the total maintenance since obsolete improvs shouldn't cost maintenance. In + // the base game, they usually do since the game doesn't update maintenance costs when buildings are obsoleted, but with that bug patched we + // can enforce the correct behavior. + if (is->current_config.patch_maintenance_persisting_for_obsolete_buildings && + (improv->ObsoleteID >= 0) && Leader_has_tech (&leaders[this->Body.CivID], __, improv->ObsoleteID)) + this->Body.Improvements_Maintenance = init_maintenance; - if (is->current_config.zero_corruption_when_off && - (p_bic_data->Governments[leader->GovernmentType].CorruptionAndWaste == CWT_Off)) - return 0; + // If AI MCS is enabled and we've added the Palace to a city, then remove any extra palaces from that city. If we're in the process of + // capturing a city, it's necessary to exclude that city as a possible destination for removed extra palaces. + if ((is->current_config.ai_multi_city_start > 1) && + (! is->is_placing_scenario_things) && + add && (improv->ImprovementFlags & ITF_Center_of_Empire) && + ((*p_human_player_bits & (1 << this->Body.CivID)) == 0)) + remove_extra_palaces (this, is->currently_capturing_city); - int tr = City_compute_corrupted_yield (this, __, gross_yield, is_production); + // If sharing wonders in hotseat mode, we must recompute improvement maintenance for all human players when any one of them gains or loses a + // wonder that grants free improvements. + if ((! is->is_placing_scenario_things) && + is->current_config.share_wonders_in_hotseat && + (*p_is_offline_mp_game && ! *p_is_pbem_game) && // is hotseat game + ((1 << this->Body.CivID) & *p_human_player_bits)) { // is this city owned by a human player + unsigned player_bits = *(unsigned *)p_human_player_bits >> 1; + int n_player = 1; + while (player_bits != 0) { + if ((player_bits & 1) && (n_player != this->Body.CivID)) + Leader_recompute_buildings_maintenance (&leaders[n_player]); + player_bits >>= 1; + n_player++; + } + } - if (is->current_config.promote_wonder_decorruption_effect) { - int actual_capital_id = leader->CapitalID; - for (int improv_id = 0; improv_id < p_bic_data->ImprovementsCount; improv_id++) { - Improvement * improv = &p_bic_data->Improvements[improv_id]; - City * pseudo_capital = NULL; - if (improv->SmallWonderFlags & ITSW_Reduces_Corruption) { - if (improv->Characteristics & ITC_Small_Wonder) { - pseudo_capital = get_city_ptr (leader->Small_Wonders[improv_id]); - } else if (improv->Characteristics & ITC_Wonder) { - pseudo_capital = get_city_ptr (Game_get_wonder_city_id(p_game, __, improv_id)); - } + // Optionally share district-dependent buildings or wonders to other cities in range of the same district + bool allow_wonder_sharing = is->current_config.cities_with_mutual_district_receive_wonders; + bool allow_building_sharing = is->current_config.cities_with_mutual_district_receive_buildings; - if (pseudo_capital != NULL && pseudo_capital->Body.CivID == this->Body.CivID && pseudo_capital->Body.ID != actual_capital_id) { - leader->CapitalID = pseudo_capital->Body.ID; - int fp_corrupted_yield = City_compute_corrupted_yield (this, __, gross_yield, is_production); - if (fp_corrupted_yield < tr) - tr = fp_corrupted_yield; + if ((! is->is_placing_scenario_things) && add && + is->current_config.enable_districts && (allow_building_sharing || allow_wonder_sharing) && + (! is->sharing_buildings_by_districts_in_progress)) { + struct district_building_prereq_list * prereq_list = get_district_building_prereq_list (improv_id); + if (prereq_list != NULL) { + bool is_wonder = (improv->Characteristics & (ITC_Wonder | ITC_Small_Wonder)) != 0; + if ((! is_wonder && allow_building_sharing) || (is_wonder && allow_wonder_sharing)) { + is->sharing_buildings_by_districts_in_progress = true; + for (int i = 0; i < prereq_list->count; i++) { + int district_id = prereq_list->district_ids[i]; + if (district_id < 0) + continue; + copy_building_with_cities_in_radius (this, improv_id, district_id, x, y); } + is->sharing_buildings_by_districts_in_progress = false; } } - leader->CapitalID = actual_capital_id; } - - return tr; } -int __fastcall -patch_Sprite_draw_on_map (Sprite * this, int edx, Map_Renderer * map_renderer, int pixel_x, int pixel_y, int param_4, int param_5, int param_6, int param_7) +void __fastcall +patch_Fighter_begin (Fighter * this, int edx, Unit * attacker, int attack_direction, Unit * defender) { - Sprite *to_draw = get_sprite_proxy_for_current_hour(this); - return Sprite_draw_on_map(to_draw ? to_draw : this, __, map_renderer, pixel_x, pixel_y, param_4, param_5, param_6, param_7); -} + Fighter_begin (this, __, attacker, attack_direction, defender); -bool -is_or_could_become_grassland (Tile * tile) -{ - enum SquareTypes sq_type = tile->vtable->m50_Get_Square_BaseType (tile), - underlying_type = tile->vtable->m49_Get_Square_RealType (tile); - int worker_job_id = p_bic_data->TileTypes[sq_type].WorkerJobID; - return sq_type == SQ_Grassland || - (underlying_type == SQ_Grassland && (worker_job_id == WJ_Clean_Forest || worker_job_id == WJ_Clear_Swamp)) || - tile->vtable->m72_Get_Pollution_Effect (tile) == SQ_Grassland; + // Apply override of retreat eligibility + // Must use this->defender instead of the defender argument since the argument is often NULL, in which case Fighter_begin finds a defender on + // the target tile and stores it in this->defender. Also must check that against NULL since Fighter_begin might fail to find a defender. + enum UnitTypeClasses class = p_bic_data->UnitTypes[this->attacker->Body.UnitTypeID].Unit_Class; + if ((this->defender != NULL) && ((class == UTC_Land) || (class == UTC_Sea))) { + enum retreat_rules retreat_rules = (class == UTC_Land) ? is->current_config.land_retreat_rules : is->current_config.sea_retreat_rules; + if (retreat_rules != RR_STANDARD) { + if (retreat_rules == RR_NONE) + this->attacker_eligible_to_retreat = this->defender_eligible_to_retreat = 0; + else if (retreat_rules == RR_ALL_UNITS) { + if (! UnitType_has_ability (&p_bic_data->UnitTypes[this->attacker->Body.UnitTypeID], __, UTA_Immobile)) + this->attacker_eligible_to_retreat = 1; + if (! UnitType_has_ability (&p_bic_data->UnitTypes[this->defender->Body.UnitTypeID], __, UTA_Immobile)) + this->defender_eligible_to_retreat = 1; + } else if (retreat_rules == RR_IF_FASTER) { + int diff = patch_Unit_get_max_move_points (this->attacker) - patch_Unit_get_max_move_points (this->defender); + this->attacker_eligible_to_retreat = diff > 0; + this->defender_eligible_to_retreat = diff < 0; + } + this->defender_eligible_to_retreat &= city_at (this->defender_location_x, this->defender_location_y) == NULL; + } + } } void __fastcall -patch_Map_Renderer_m19_Draw_Tile_by_XY_and_Flags (Map_Renderer * this, int edx, int param_1, int pixel_x, int pixel_y, Map_Renderer * map_renderer, int param_5, int tile_x, int tile_y, int param_8) +patch_Unit_despawn (Unit * this, int edx, int civ_id_responsible, byte param_2, byte param_3, byte param_4, byte param_5, byte param_6, byte param_7) { - Map_Renderer_m19_Draw_Tile_by_XY_and_Flags (this, __, param_1, pixel_x, pixel_y, map_renderer, param_5, tile_x, tile_y, param_8); + // Determine whether this despawn happened involuntarily, i.e. whether it's because of the actions of a player other than its owner. This + // includes cases where the unit was destroyed in combat, captured, kicked from territory with nowhere to go, etc. + int ret_addr = ((int *)&civ_id_responsible)[-1]; + bool involuntary = + ret_addr == DESPAWN_TO_FIGHT_1_RETURN || ret_addr == DESPAWN_TO_FIGHT_2_RETURN + || ret_addr == DESPAWN_TO_DO_BOMBARD_TILE_RETURN || ret_addr == DESPAWN_TO_CRUISE_MISSILE_DEFENDER_RETURN + || ret_addr == DESPAWN_TO_BOUNCE_TRESPASSING_UNITS_RETURN || ret_addr == DESPAWN_TO_NUKE_DAMAGE_RETURN + || ret_addr == DESPAWN_RECURSIVE_RETURN || ret_addr == DESPAWN_TO_DO_CAPTURE_UNITS_RETURN + || ret_addr == DESPAWN_TO_TRY_FLYING_OVER_TILE_RETURN; - Map * map = &p_bic_data->Map; - if ((is->city_loc_display_perspective >= 0) && - (! map->vtable->m10_Get_Map_Zoom (map)) && // Turn off display when zoomed out. Need another set of highlight images for that. - ((1 << is->city_loc_display_perspective) & *p_player_bits) && - (((tile_x + tile_y) % 2) == 0)) { // Replicate a check from the base game code. Without this we'd be drawing additional tiles half-way off the grid. + int owner_id = this->Body.CivID; + int type_id = this->Body.UnitTypeID; + UnitType * type = &p_bic_data->UnitTypes[type_id]; + Tile * tile = tile_at (this->Body.X, this->Body.Y); - init_tile_highlights (); - if (is->tile_highlight_state == IS_OK) { - int eval = patch_Match_ai_eval_city_location (p_match, __, tile_x, tile_y, is->city_loc_display_perspective, false, NULL); - if (eval > 0) { - int step_size = 10; - int midpoint = (COUNT_TILE_HIGHLIGHTS % 2 == 0) ? 1000000 : (1000000 - step_size/2); - int grade = (eval >= midpoint) ? (eval - midpoint) / step_size : (eval - midpoint) / step_size - 1; - int i_highlight = clamp (0, COUNT_TILE_HIGHLIGHTS - 1, COUNT_TILE_HIGHLIGHTS/2 + grade); - Sprite_draw_on_map (&is->tile_highlights[i_highlight], __, this, pixel_x, pixel_y, 1, 1, 1, 0); - } + // Clear extra DBs, airdrops, wait records, and transport ties used by this unit + itable_remove (&is->extra_defensive_bombards, this->Body.ID); + itable_remove (&is->airdrops_this_turn, this->Body.ID); + itable_remove (&is->waiting_units, this->Body.ID); + itable_remove (&is->unit_transport_ties, this->Body.ID); + + // If we're despawning the stored ZoC defender, clear that variable so we don't despawn it again in check_life_after_zoc + if (this == is->zoc_defender) + is->zoc_defender = NULL; + + if (this == is->sb_next_up) + is->sb_next_up = NULL; + + if (this == is->last_selected_unit.ptr) + is->last_selected_unit.ptr = NULL; + + // Remove this unit from the list of extra units to despawn after capturing + int original_count = is->count_extra_capture_despawns; + for (int n_src = 0, n_dest = 0; n_src < original_count; n_src++) { + if (is->extra_capture_despawns[n_src] != this) { + is->extra_capture_despawns[n_dest] = is->extra_capture_despawns[n_src]; + n_dest++; + } else + is->count_extra_capture_despawns -= 1; + } + + // If the unit being despawned is a land transport or helicopter, we may have to make sure to despawn its passengers as well because the base + // game won't. In general, the passengers are lost if the transport was despawned involuntarily, i.e. because of another player's attack or + // something like that, or if the passengers couldn't survive outside the transport, specifically because it's a helicopter at sea. + bool must_despawn_passengers = false; { + if (is_land_transport (this)) + must_despawn_passengers = involuntary && (is->current_config.land_transport_rules & LTR_NO_ESCAPE); + + else if (type->Unit_Class == UTC_Air && type->Transport_Capacity > 0) { // if "this" is a helicopter + Unit * container = get_unit_ptr (this->Body.Container_Unit); + bool on_carrier = container != NULL && p_bic_data->UnitTypes[container->Body.UnitTypeID].Unit_Class == UTC_Sea && + Unit_has_ability (container, __, UTA_Transports_Only_Aircraft); + bool passengers_could_survive = Tile_has_city (tile) || ! tile->vtable->m35_Check_Is_Water (tile); + + // If no-defense-from-inside is off, passengers in helicopters will fight to defend their tile, so they wouldn't be left + // inside the helicopter when their tile is taken. If it's on, an occupied heli can be destroyed or captured by a land unit, + // and we must make sure to despawn the passengers in that case because they can't remain on the tile. Hence, + // no-defense-from-inside implies no-escape in almost all circumstances (nukes are the exception). + enum special_helicopter_rules special_rules = is->current_config.special_helicopter_rules; + if (((special_rules & SHR_ALLOW_ON_CARRIERS) && on_carrier) || (special_rules & (SHR_NO_DEFENSE_FROM_INSIDE | SHR_NO_ESCAPE))) + must_despawn_passengers = involuntary || ! passengers_could_survive; } } - // Draw city work radius highlights for selected worker - if (is->current_config.enable_districts && - is->current_config.enable_city_work_radii_highlights && - is->highlight_city_radii && - (((tile_x + tile_y) % 2) == 0)) { // Replicate a check from the base game code. Without this we'd be drawing additional tiles half-way off the grid. + bool prev_always_despawn_passengers = is->always_despawn_passengers; - if (is->tile_highlight_state == IS_OK) { - Tile * tile = tile_at (tile_x, tile_y); - if ((tile != NULL) && (tile != p_null_tile)) { - int stored_ptr; - if (itable_look_up (&is->highlighted_city_radius_tile_pointers, (int)tile, &stored_ptr)) { - struct highlighted_city_radius_tile_info * info = (struct highlighted_city_radius_tile_info *)stored_ptr; - Sprite_draw_on_map (&is->tile_highlights[clamp(0, COUNT_TILE_HIGHLIGHTS - 1, info->highlight_level)], __, this, pixel_x, pixel_y, 1, 1, 1, 0); + if (must_despawn_passengers) { + // If we've been called from do_capture_units, record the passengers in the list of units to be despawned after do_capture_units + // returns. We can't simply despawn the units now even by setting always_despawn_passengers because the despawning messes up an + // iterator over tile units inside do_capture_units. + if (ret_addr == DESPAWN_TO_DO_CAPTURE_UNITS_RETURN) { + FOR_UNITS_ON (uti, tile) + if (uti.unit->Body.Container_Unit == this->Body.ID) { + reserve (sizeof is->extra_capture_despawns[0], // item size + (void **)&is->extra_capture_despawns, // ptr to items + &is->extra_capture_despawns_capacity, // ptr to capacity + is->count_extra_capture_despawns); // count + is->extra_capture_despawns[is->count_extra_capture_despawns] = uti.unit; + is->count_extra_capture_despawns += 1; } - } - } + + } else + is->always_despawn_passengers = true; } + + Unit_despawn (this, __, civ_id_responsible, param_2, param_3, param_4, param_5, param_6, param_7); + + is->always_despawn_passengers = prev_always_despawn_passengers; + + change_unit_type_count (&leaders[owner_id], type_id, -1); } -void __fastcall -patch_Map_Renderer_m08_Draw_Tile_Forests_Jungle_Swamp (Map_Renderer * this, int edx, int tile_x, int tile_y, Map_Renderer * map_renderer, int pixel_x, int pixel_y) +bool __fastcall +patch_Unit_do_capture_units (Unit * this, int edx, int tile_x, int tile_y, int owner_civ_id) { - if (! is->current_config.draw_forests_over_roads_and_railroads) { - Map_Renderer_m08_Draw_Tile_Forests_Jungle_Swamp (this, __, tile_x, tile_y, map_renderer, pixel_x, pixel_y); - return; - } - - Tile * tile = tile_at (tile_x, tile_y); - if ((tile == NULL) || (tile == p_null_tile)) - return; + is->count_extra_capture_despawns = 0; - is->current_tile_x = -1; - is->current_tile_y = -1; - if ((tile->vtable->m50_Get_Square_BaseType (tile) == SQ_Forest) && - (*tile->vtable->m25_Check_Roads)(tile, __, 0)) { - is->current_tile_x = tile_x; - is->current_tile_y = tile_y; - return; - } + bool tr = Unit_do_capture_units (this, __, tile_x, tile_y, owner_civ_id); - Map_Renderer_m08_Draw_Tile_Forests_Jungle_Swamp (this, __, tile_x, tile_y, map_renderer, pixel_x, pixel_y); + // Here we rely on patch_Unit_despawn to remove despawned units from the list + while (is->count_extra_capture_despawns > 0) + patch_Unit_despawn (is->extra_capture_despawns[0], __, this->Body.ID, 0, 0, 0, 0, 0, 0); + + return tr; } void __fastcall -patch_Map_Renderer_m52_Draw_Roads (Map_Renderer * this, int edx, int image_index, Map_Renderer * map_renderer, int pixel_x, int pixel_y) +patch_Map_Renderer_m71_Draw_Tiles (Map_Renderer * this, int edx, int param_1, int param_2, int param_3) { - Map_Renderer_m52_Draw_Roads (this, __, image_index, map_renderer, pixel_x, pixel_y); + // Restore the tile count if it was saved by recompute_resources. This is necessary because the Draw_Tiles method loops over all tiles. + if (is->saved_tile_count >= 0) { + p_bic_data->Map.TileCount = is->saved_tile_count; + is->saved_tile_count = -1; + } - if (! is->current_config.draw_forests_over_roads_and_railroads || - is->current_tile_x == -1 || is->current_tile_y == -1) - return; + Map_Renderer_m71_Draw_Tiles (this, __, param_1, param_2, param_3); +} - // Current tile x & y will only have coordinates if we have a forest (per check in patch_Map_Renderer_m08_Draw_Tile_Forests_Jungle_Swamp), - // so go ahead and render the forest on top of the road here. - Map_Renderer_m08_Draw_Tile_Forests_Jungle_Swamp (this, __, is->current_tile_x, is->current_tile_y, map_renderer, pixel_x, pixel_y); +struct named_tile_entry * +get_named_tile_entry (Tile * tile) +{ + if ((tile == NULL) || (tile == p_null_tile)) + return NULL; + int stored_ptr = 0; + if (! itable_look_up (&is->named_tile_map, (int)tile, &stored_ptr)) + return NULL; + return (struct named_tile_entry *)stored_ptr; } -void __fastcall -patch_Map_Renderer_m52_Draw_Railroads (Map_Renderer * this, int edx, int image_index, Map_Renderer * map_renderer, int pixel_x, int pixel_y) +bool +prompt_for_named_tile (char const * seed_name, char * out_name, int out_len) { - Map_Renderer_m52_Draw_Railroads (this, __, image_index, map_renderer, pixel_x, pixel_y); + if ((out_name == NULL) || (out_len <= 0)) + return false; + out_name[0] = '\0'; - if (! is->current_config.draw_forests_over_roads_and_railroads - || is->current_tile_x == -1 || is->current_tile_y == -1) - return; + PopupForm * popup = get_popup_form (); + if (popup == NULL) + return false; + + char seed_buf[101]; + if (seed_name == NULL) + seed_name = ""; + strncpy (seed_buf, seed_name, sizeof seed_buf); + seed_buf[(sizeof seed_buf) - 1] = '\0'; + if (seed_buf[0] == '\0') + strncpy (seed_buf, "Tile", sizeof seed_buf); + + set_popup_str_param (0, seed_buf, -1, -1); + popup->vtable->set_text_key_and_flags (popup, __, script_dot_txt_file_path, "RENAME_CITY", 0x64, (int)seed_buf, 0x44, 0); + int sel = patch_show_popup (popup, __, 0, 0); + is->focused_tile = NULL; + if (sel != 0) + return false; + + if (temp_ui_strs[0][0] == '\0') + return true; - // patch_Map_Renderer_m08_Draw_Tile_Forests_Jungle_Swamp sets x & y only if we have a forest, so render on top of railroad - Map_Renderer_m08_Draw_Tile_Forests_Jungle_Swamp (this, __, is->current_tile_x, is->current_tile_y, map_renderer, pixel_x, pixel_y); + strncpy (out_name, temp_ui_strs[0], out_len); + out_name[out_len - 1] = '\0'; + return true; } -void __fastcall -patch_Main_Screen_Form_m82_handle_key_event (Main_Screen_Form * this, int edx, int virtual_key_code, int is_down) +void +handle_named_tile_menu_selection (void) { - char s[200]; - int * last_events = is->last_main_screen_key_up_events; - bool in_game = *p_player_bits != 0; // Player bits all zero indicates we aren't currently in a game. Need to check for this because UI events - // on the main menu also pass through this function. - - if (! is_down) { - for (int n = ARRAY_LEN (is->last_main_screen_key_up_events) - 1; n > 0; n--) - last_events[n] = last_events[n - 1]; - last_events[0] = virtual_key_code; - } + int tile_x = is->named_tile_menu_tile_x; + int tile_y = is->named_tile_menu_tile_y; + Tile * tile = tile_at (tile_x, tile_y); + if (! tile_can_be_named (tile, tile_x, tile_y)) + return; - if (is->current_config.enable_ai_city_location_desirability_display && - (virtual_key_code == VK_L) && is_down && - (! (is_command_button_active (&this->GUI, UCV_Load) || is_command_button_active (&this->GUI, UCV_Unload))) && - in_game) { - int is_debug_mode = (*p_debug_mode_bits & 4) != 0; // This is how the check is done in open_tile_info. Actually there are two debug - // mode bits (4 and 8) and I don't know what the difference is. - PopupForm * popup = get_popup_form (); - popup->vtable->set_text_key_and_flags (popup, __, is->mod_script_path, "C3X_CITY_LOC_HIGHLIGHTS", -1, 0, 0x40, 0); - snprintf (s, sizeof s, " %s", is->c3x_labels[CL_OFF]); - s[(sizeof s) - 1] = '\0'; - PopupSelection_add_item (&popup->selection, __, s, 0); - for (int n = 1; n < 32; n++) - if ((*p_player_bits & (1 << n)) && - (is_debug_mode || (n == p_main_screen_form->Player_CivID))) { - Race * race = &p_bic_data->Races[leaders[n].RaceID]; - snprintf (s, sizeof s, " (%d) %s", n, race->vtable->GetLeaderName (race)); - s[(sizeof s) - 1] = '\0'; - PopupSelection_add_item (&popup->selection, __, s, n); - } - int sel = patch_show_popup (popup, __, 0, 0); - if (sel >= 0) { // -1 indicates popup was closed without making a selection - is->city_loc_display_perspective = (sel >= 1) ? sel : -1; - this->vtable->m73_call_m22_Draw ((Base_Form *)this); // Trigger map redraw - } + init_tile_highlights (); - } else if (is->current_config.enable_debug_mode_switch && - (in_game && ! is_down) && - (last_events[4] == VK_D) && (last_events[3] == VK_E) && (last_events[2] == VK_B) && (last_events[1] == VK_U) && (last_events[0] == VK_G)) { - PopupForm * popup = get_popup_form (); - if ((*p_debug_mode_bits & 0xC) != 0) { // Consider debug mode on if either bit is set - *p_debug_mode_bits &= ~0xC; - popup->vtable->set_text_key_and_flags (popup, __, is->mod_script_path, "C3X_INFO", -1, 0, 0, 0); - PopupForm_add_text (popup, __, "Debug mode deactivated.", 0); - patch_show_popup (popup, __, 0, 0); - } else { - popup->vtable->set_text_key_and_flags (popup, __, is->mod_script_path, "C3X_CONFIRM_DEBUG_ACTIVATION", -1, 0, 0, 0); - int sel = patch_show_popup (popup, __, 0, 0); - if (sel == 0) { - *p_debug_mode_bits |= 0xC; - *(bool *)((int)p_human_player_bits + 37) = true; // Set MegaTrainerXL flag indicating edited save - } - } - this->vtable->m73_call_m22_Draw ((Base_Form *)this); // Trigger map redraw - } + struct named_tile_entry * entry = get_named_tile_entry (tile); + char const * current_name = (entry != NULL) ? entry->name : ""; + char new_name[100]; + is->focused_tile = tile; + p_main_screen_form->vtable->m73_call_m22_Draw ((Base_Form *)p_main_screen_form); + bool got_name = prompt_for_named_tile (current_name, new_name, sizeof new_name); + is->focused_tile = NULL; + p_main_screen_form->vtable->m73_call_m22_Draw ((Base_Form *)p_main_screen_form); + if (! got_name) + return; - Main_Screen_Form_m82_handle_key_event (this, __, virtual_key_code, is_down); + set_named_tile_entry (tile, tile_x, tile_y, new_name); + p_main_screen_form->vtable->m73_call_m22_Draw ((Base_Form *)p_main_screen_form); } -int __fastcall -patch_Unit_get_move_points_after_airdrop (Unit * this) +void __fastcall +patch_Main_Screen_Form_handle_right_click_on_tile (Main_Screen_Form * this, int edx, int tile_x, int tile_y, int mouse_x, int mouse_y) { - int prev_airdrop_count = itable_look_up_or (&is->airdrops_this_turn, this->Body.ID, 0); - itable_insert (&is->airdrops_this_turn, this->Body.ID, prev_airdrop_count + 1); + if (is->current_config.enable_named_tiles) { + Tile * tile = tile_at (tile_x, tile_y); + if (tile_can_be_named (tile, tile_x, tile_y)) { + is->named_tile_menu_active = true; + is->named_tile_menu_tile_x = tile_x; + is->named_tile_menu_tile_y = tile_y; + Main_Screen_Form_open_right_click_menu (this, __, tile_x, tile_y, mouse_x, mouse_y); + is->named_tile_menu_active = false; + return; + } + } - return is->current_config.dont_end_units_turn_after_airdrop ? this->Body.Moves : patch_Unit_get_max_move_points (this); + Main_Screen_Form_handle_right_click_on_tile (this, __, tile_x, tile_y, mouse_x, mouse_y); } -int __fastcall -patch_Unit_get_move_points_after_set_to_intercept (Unit * this) +void __fastcall +patch_Main_Screen_Form_open_right_click_menu (Main_Screen_Form * this, int edx, int tile_x, int tile_y, int mouse_x, int mouse_y) { - return is->current_config.patch_intercept_lost_turn_bug ? this->Body.Moves : patch_Unit_get_max_move_points (this); + bool set_active = false; + if (!is->named_tile_menu_active && is->current_config.enable_named_tiles) { + Tile * tile = tile_at (tile_x, tile_y); + if (tile_can_be_named (tile, tile_x, tile_y)) { + is->named_tile_menu_active = true; + is->named_tile_menu_tile_x = tile_x; + is->named_tile_menu_tile_y = tile_y; + set_active = true; + } + } + Main_Screen_Form_open_right_click_menu (this, __, tile_x, tile_y, mouse_x, mouse_y); + if (set_active) + is->named_tile_menu_active = false; } -void __cdecl -activate_mod_info_button (int control_id) +void +draw_map_tile_text (Main_Screen_Form * this, PCX_Image * canvas, char * text, int screen_x, int screen_y, int base_screen_height, int y_offset) { - PopupForm * popup = get_popup_form (); - popup->vtable->set_text_key_and_flags (popup, __, is->mod_script_path, "C3X_INFO", -1, 0, 0, 0); - char s[500]; - char version_letter = 'A' + MOD_VERSION%100; - - if (MOD_PREVIEW_VERSION == 0) - snprintf (s, sizeof s, "%s: %d%c", is->c3x_labels[CL_VERSION], MOD_VERSION/100, MOD_VERSION%100 != 0 ? version_letter : ' '); - else - snprintf (s, sizeof s, "%s: %d%c%s %d", is->c3x_labels[CL_VERSION], MOD_VERSION/100, MOD_VERSION%100 != 0 ? version_letter : ' ', is->c3x_labels[CL_PREVIEW], MOD_PREVIEW_VERSION); - s[(sizeof s) - 1] = '\0'; - PopupForm_add_text (popup, __, s, 0); + int is_zoomed_out = (p_bic_data->is_zoomed_out != false); + int scale = is_zoomed_out ? 2 : 1; + int screen_width = 128 / scale; + int screen_height = base_screen_height / scale; + int text_width = screen_width - (is_zoomed_out ? 4 : 8); + if (text_width < 12) + text_width = screen_width; - snprintf (s, sizeof s, "^%s:", is->c3x_labels[CL_CONFIG_FILES_LOADED]); - s[(sizeof s) - 1] = '\0'; - PopupForm_add_text (popup, __, s, 0); + int text_left = screen_x + (screen_width - text_width) / 2; + int draw_y = screen_y - y_offset; + int text_top = draw_y + screen_height + (is_zoomed_out ? 2 : 4); - int n = 1; - for (struct loaded_config_name * lcn = is->loaded_config_names; lcn != NULL; lcn = lcn->next) { - snprintf (s, sizeof s, "^ %d. %s", n, lcn->name); - s[(sizeof s) - 1] = '\0'; - n++; - PopupForm_add_text (popup, __, s, 0); + Object_66C3FC * font = get_font (10, FSF_NONE); + if (font != NULL) { + PCX_Image_set_text_effects (canvas, __, 0x80FFFFFF, 0x80000000, 1, 1); + PCX_Image_draw_centered_text (canvas, __, font, text, text_left, text_top - 10, text_width, strlen (text)); } - - patch_show_popup (popup, __, 0, 0); } -int __fastcall -patch_Parameters_Form_m68_Show_Dialog (Parameters_Form * this, int edx, int param_1, void * param_2, void * param_3) +void __fastcall +patch_Main_Screen_Form_draw_city_hud (Main_Screen_Form * this, int edx, PCX_Image * canvas) { - init_mod_info_button_images (); + Main_Screen_Form_draw_city_hud (this, __, canvas); - // "b" is the mod info button. It's created each time the preferences form (or "parameters" form as Antal called it) is opened and destroyed - // every time the form is closed. This is the easiest way since it matches how the base game works. At first I tried creating the button once - // but then it will only appear once since its attachment to the prefs form is lost when the form is destroyed on closure. I tried reattaching - // the button to each newly created form but wasn't able to make that work. - Button * b = NULL; - if (is->mod_info_button_images_state == IS_OK) { - b = malloc (sizeof *b); - Button_construct (b); + bool draw_natural_wonders = is->current_config.enable_natural_wonders && + is->current_config.show_natural_wonder_name_on_map; + bool draw_named_tiles = is->current_config.enable_named_tiles; + if (!draw_natural_wonders && !draw_named_tiles) + return; - Button_initialize (b, __, - is->c3x_labels[CL_MOD_INFO_BUTTON_TEXT], // text - MOD_INFO_BUTTON_ID, // control ID - (p_bic_data->ScreenWidth - 1024) / 2 + 891, // location x - (p_bic_data->ScreenHeight - 768) / 2 + 31, // location y - MOD_INFO_BUTTON_WIDTH, MOD_INFO_BUTTON_HEIGHT, // width, height - (Base_Form *)this, // parent - 0); // ? + if (canvas == NULL) + canvas = &this->Base_Data.Canvas; - for (int n = 0; n < 3; n++) - b->Images[n] = &is->mod_info_button_images[n]; - PCX_Image_set_font (&b->Base_Data.Canvas, __, get_font (15, FSF_NONE), 0, 0, 0); - b->activation_handler = &activate_mod_info_button; + if ((canvas == NULL) || (canvas->JGL.Image == NULL)) + return; - // Need to draw once manually or the button won't look right - b->vtable->m73_call_m22_Draw ((Base_Form *)b); - } + if (draw_natural_wonders) { + FOR_TABLE_ENTRIES (tei, &is->district_tile_map) { + struct district_instance * inst = (struct district_instance *)tei.value; + if ((inst == NULL) || (inst->district_id != NATURAL_WONDER_DISTRICT_ID)) + continue; - int tr = Parameters_Form_m68_Show_Dialog (this, __, param_1, param_2, param_3); + int natural_id = inst->natural_wonder_info.natural_wonder_id; + if ((natural_id < 0) || (natural_id >= is->natural_wonder_count)) + continue; - if (b != NULL) { - b->vtable->destruct ((Base_Form *)b, __, 0); - free (b); - } + struct natural_wonder_district_config const * nw_cfg = &is->natural_wonder_configs[natural_id]; + if ((nw_cfg == NULL) || (nw_cfg->name == NULL) || (nw_cfg->name[0] == '\0')) + continue; - return tr; -} + Tile * tile = (Tile *)tei.key; + if ((tile == NULL) || (tile == p_null_tile)) + continue; -bool __fastcall -patch_City_cycle_specialist_type (City * this, int edx, int mouse_x, int mouse_y, Citizen * citizen, City_Form * city_form) -{ - int specialist_count = 0; { - Leader * city_owner = &leaders[this->Body.CivID]; - for (int n = 0; n < p_bic_data->CitizenTypeCount; n++) - specialist_count += (n != p_bic_data->default_citizen_type) && - Leader_has_tech (city_owner, __, p_bic_data->CitizenTypes[n].RequireID); - } - int shift_down = (*p_GetAsyncKeyState) (VK_SHIFT) >> 8; - int original_worker_type = citizen->WorkerType; + int tile_x, tile_y; + if (!tile_coords_from_ptr (&p_bic_data->Map, tile, &tile_x, &tile_y)) + continue; - // The return value of this function is not actually used by either of the two original callers. - bool tr = City_cycle_specialist_type (this, __, mouse_x, mouse_y, citizen, city_form); + int screen_x, screen_y; + Main_Screen_Form_tile_to_screen_coords (this, __, tile_x, tile_y, &screen_x, &screen_y); - // Cycle all the way around back to the previous specialist type, if appropriate. - // If the worker type was not changed after the first call to cycle_specialist_type, that indicates that the player was asked to disable - // governor management and chose not to. Do not try to cycle backwards in that case or else we'll spam the player with more popups. - if (is->current_config.reverse_specialist_order_with_shift && - shift_down && (specialist_count > 2) && (citizen->WorkerType != original_worker_type)) { - for (int n = 0; n < specialist_count - 2; n++) - City_cycle_specialist_type (this, __, mouse_x, mouse_y, citizen, city_form); + draw_map_tile_text (this, canvas, (char *)nw_cfg->name, screen_x, screen_y, 88, 88 - 64); + } } - return tr; -} + if (draw_named_tiles) { + FOR_TABLE_ENTRIES (tei, &is->named_tile_map) { + struct named_tile_entry * entry = (struct named_tile_entry *)tei.value; + if ((entry == NULL) || (entry->name[0] == '\0')) + continue; -int __fastcall -patch_City_get_pollution_from_pop (City * this) -{ - if (! is->current_config.enable_negative_pop_pollution) - return City_get_pollution_from_pop (this); + Tile * tile = (Tile *)tei.key; + if ((tile == NULL) || (tile == p_null_tile)) + continue; - int base_pollution = this->Body.Population.Size - p_bic_data->General.MaximumSize_City; - if (base_pollution <= 0) - return 0; + int tile_x, tile_y; + if (!tile_coords_from_ptr (&p_bic_data->Map, tile, &tile_x, &tile_y)) { + tile_x = entry->tile_x; + tile_y = entry->tile_y; + tile = tile_at (tile_x, tile_y); + if ((tile == NULL) || (tile == p_null_tile)) + continue; + } - // Consider improvements - int net_pollution = base_pollution; - int any_cleaning_improvs = 0; - for (int n = 0; n < p_bic_data->ImprovementsCount; n++) { - Improvement * improv = &p_bic_data->Improvements[n]; - if ((improv->ImprovementFlags & ITF_Removes_Population_Pollution) && has_active_building (this, n)) { - any_cleaning_improvs = 1; - if (improv->Pollution < 0) - net_pollution += improv->Pollution; + struct district_instance * inst = get_district_instance (tile); + if ((inst != NULL) && (inst->district_id == NATURAL_WONDER_DISTRICT_ID)) + continue; + + int screen_x, screen_y; + Main_Screen_Form_tile_to_screen_coords (this, __, tile_x, tile_y, &screen_x, &screen_y); + + draw_map_tile_text (this, canvas, entry->name, screen_x, screen_y, 64, 3); } } - - if (net_pollution <= 0) - return 0; - else if (any_cleaning_improvs) - return 1; - else - return net_pollution; } -// The original version of this function in the base game contains a duplicate of the logic that computes the total pollution from buildings and -// pop. By re-implementing it to use the get_pollution_from_* functions, we ensure that our changes to get_pollution_from_pop will be accounted for. -// Note: This function is called from two places, one is the city screen and the other I'm not sure about. The pollution spawning logic works by -// calling the get_pollution_from_* funcs directly. -int __fastcall -patch_City_get_total_pollution (City * this) +// Returns whether or not city has an "extra palace", a concept used by the AI multi-city start. Extra palaces are small wonders that reduce +// corruption (e.g. forbidden palace) that are listed under the ai_multi_start_extra_palaces config option. +bool +has_extra_palace (City * city) { - return City_get_pollution_from_buildings (this) + patch_City_get_pollution_from_pop (this); + for (int n = 0; n < is->current_config.count_ai_multi_start_extra_palaces; n++) { + int improv_id = is->current_config.ai_multi_start_extra_palaces[n]; + Improvement * improv = &p_bic_data->Improvements[improv_id]; + if ((improv->Characteristics & ITC_Small_Wonder) && + (improv->SmallWonderFlags & ITSW_Reduces_Corruption) && + (leaders[city->Body.CivID].Small_Wonders[improv_id] == city->Body.ID)) { + return true; + } + } + return false; } -void remove_extra_palaces (City * city, City * excluded_destination); - +// Removes any extra palaces from this city to other cities owned by the same player (if possible). Does not check that the city belongs to an AI or +// that AI MCS is enabled. void -set_wonder_built_flag (int improv_id, bool is_built) +remove_extra_palaces (City * city, City * excluded_destination) { - if ((p_match == NULL) || (improv_id < 0) || (improv_id >= p_bic_data->ImprovementsCount)) - return; + Leader * leader = &leaders[city->Body.CivID]; + int extra_palace_lost; + do { + // Search for an extra palace in the city and delete it. Remember its ID so we can put it elsewhere. + extra_palace_lost = -1; + for (int n = 0; n < is->current_config.count_ai_multi_start_extra_palaces; n++) { + int improv_id = is->current_config.ai_multi_start_extra_palaces[n]; + Improvement * improv = &p_bic_data->Improvements[improv_id]; + if ((improv->Characteristics & ITC_Small_Wonder) && + (improv->SmallWonderFlags & ITSW_Reduces_Corruption) && + (leader->Small_Wonders[improv_id] == city->Body.ID)) { + patch_City_add_or_remove_improvement (city, __, improv_id, 0, false); + extra_palace_lost = improv_id; + break; + } + } - byte * built_flags = *(byte **)((byte *)p_match + 0x4fc); - if (built_flags == NULL) - return; + // Replace the lost extra palace like what happens to the real palace + if (extra_palace_lost >= 0) { + int best_rating = -1; + City * best_location = NULL; + FOR_CITIES_OF (coi, leader->ID) { + City * candidate = coi.city; + if ((candidate != city) && + (candidate->Body.ID != leader->CapitalID) && + (candidate != excluded_destination) && + ! has_extra_palace (candidate)) { - built_flags[improv_id] = is_built; + // Rate this candidate as a possible (extra) palace location. The criteria we use to rate it are identical to + // what the base game uses to find a new location for the palace. + int rating = 0; + rating += candidate->Body.Population.Size; + rating += count_units_at (candidate->Body.X, candidate->Body.Y, UF_4, -1, 0, -1); + rating += 2 * City_count_citizens_of_race (candidate, __, leader->RaceID); + FOR_TILES_AROUND (tai, 0x121, candidate->Body.X, candidate->Body.Y) { + if (tai.n == 0) + continue; + City * neighbor = get_city_ptr (tai.tile->CityID); + if ((neighbor != NULL) && (neighbor != city) && (neighbor->Body.CivID == leader->ID)) { + int size = neighbor->Body.Population.Size; + if (size > p_bic_data->General.MaximumSize_City) rating += 3; + else if (size > p_bic_data->General.MaximumSize_Town) rating += 2; + else rating += 1; + } + } + + if (rating > best_rating) { + best_rating = rating; + best_location = candidate; + } + } + } + + if (best_location != NULL) + City_add_or_remove_improvement (best_location, __, extra_palace_lost, 1, false); + } + } while (extra_palace_lost >= 0); } -bool -choose_defensive_unit_order (City * city, City_Order * out_order) +void +handle_possible_duplicate_small_wonders(City * city, Leader * leader) { - if ((city == NULL) || (out_order == NULL)) - return false; + if ((city == NULL) || (leader == NULL)) + return; - for (int pass = 0; pass < 3; pass++) { - int best_unit_id = -1; - int best_defence = -1; + FOR_DISTRICTS_AROUND (wai, city->Body.X, city->Body.Y, true) { + int x = wai.tile_x, y = wai.tile_y; + Tile * tile = wai.tile; - for (int n = 0; n < p_bic_data->UnitTypeCount; n++) { - UnitType * type = &p_bic_data->UnitTypes[n]; - if (! patch_City_can_build_unit (city, __, n, 1, 0, 0)) - continue; + // Make sure it's a completed wonder district + struct district_instance * inst = wai.district_inst; + if (inst->district_id != WONDER_DISTRICT_ID) continue; + struct wonder_district_info * info = &inst->wonder_info; + if ((info == NULL) || (info->state != WDS_COMPLETED)) continue; - int defence = type->Defence; - if ((pass < 2) && (defence <= 0)) - continue; - if ((pass <= 1) && (type->Unit_Class != UTC_Land)) - continue; - if ((pass == 0) && ((type->AI_Strategy & UTAI_Defence) == 0)) - continue; + int improv_id = get_wonder_improvement_id_from_index (info->wonder_index); + if (improv_id < 0) continue; - if (defence > best_defence) { - best_defence = defence; - best_unit_id = n; - } - } + Improvement * improv = &p_bic_data->Improvements[improv_id]; - if (best_unit_id >= 0) { - out_order->OrderType = COT_Unit; - out_order->OrderID = best_unit_id; - return true; + // Only check Small Wonders, which shouldn't be duplicated in a civ + if ((improv->Characteristics & ITC_Small_Wonder) != 0) { + City * owning_city = get_city_ptr (leader->Small_Wonders[improv_id]); + if (owning_city != NULL) { + // Another city of the conquering civ has this Small Wonder, so remove it from captured city and destroy the district. + // We run the district removal manually here rather than through "handle_district_removed", as that function removes Wonders + // using leader->Small_Wonders, which would unset the Small Wonder from the original owning city, which we don't want, + // and would only remove the Wonder if completed_wonder_districts_can_be_destroyed is true, which may not be the case + patch_City_add_or_remove_improvement (city, __, improv_id, 0, false); + remove_district_instance (tile); + tile->vtable->m51_Unset_Tile_Flags (tile, __, 0, TILE_FLAG_MINE, x, y); + tile->vtable->m60_Set_Ruins (tile, __, 1); + } } } - - return false; } -// When a city adds a building that depends on a district, optionally mirror that -// building to all other same-civ cities that can also work the district tile. void -copy_building_with_cities_in_radius (City * source, int improv_id, int required_district_id, int tile_x, int tile_y) +grant_nearby_wonders_to_city (City * city) { - if (! is->current_config.enable_districts) return; - if (source == NULL) return; - - Improvement * improv = &p_bic_data->Improvements[improv_id]; - bool is_wonder = (improv->Characteristics & (ITC_Wonder | ITC_Small_Wonder)) != 0; - - if (is_wonder) { - if (! is->current_config.cities_with_mutual_district_receive_wonders) - return; - } else if (! is->current_config.cities_with_mutual_district_receive_buildings) + // Give a city any completed wonder districts in work radius, if cities_with_mutual_district_receive_wonders is true. + // This is essentially for cases where the original Wonder-constructing city is destroyed and a new city is built + // that can work the same wonder district tile. + if (! is->current_config.enable_districts || + ! is->current_config.enable_wonder_districts || + ! is->current_config.cities_with_mutual_district_receive_wonders || + (city == NULL)) return; - // If a Wonder, we know the specific tile it is at, so determine which other cities have that in work radius. - if (is_wonder) { - Tile * tile = tile_at (tile_x, tile_y); - if (tile == p_null_tile) return; - if (tile->vtable->m38_Get_Territory_OwnerID (tile) != source->Body.CivID) return; - - struct district_instance * inst = get_district_instance (tile); - if (inst == NULL || inst->district_type != required_district_id) return; - if (! district_is_complete (tile, required_district_id)) return; + FOR_DISTRICTS_AROUND (wai, city->Body.X, city->Body.Y, true) { + int x = wai.tile_x, y = wai.tile_y; + Tile * tile = wai.tile; - FOR_CITIES_OF (coi, source->Body.CivID) { - City * city = coi.city; - if (city == NULL) continue; - if (city == source) continue; + // Make sure Wonder is completed + struct district_instance * inst = wai.district_inst; + if (inst->district_id != WONDER_DISTRICT_ID) continue; + struct wonder_district_info * info = &inst->wonder_info; + if ((info == NULL) || (info->state != WDS_COMPLETED)) continue; - if (! city_radius_contains_tile (city, tile_x, tile_y)) - continue; + // Check that city doesn't already have the Wonder + int improv_id = get_wonder_improvement_id_from_index (info->wonder_index); + if (improv_id < 0) continue; - if (tile->vtable->m38_Get_Territory_OwnerID (tile) != city->Body.CivID) - continue; + if (patch_City_has_improvement (city, __, improv_id, false)) continue; - if (! patch_City_has_improvement (city, __, improv_id, false)) { - City_add_or_remove_improvement (city, __, improv_id, 1, false); - } - } - } - // Else there may be multiple district instances of this type, so check each tile around the city - else { - for (int n = 0; n < is->workable_tile_count; n++) { - int dx, dy; - patch_ni_to_diff_for_work_area (n, &dx, &dy); - int x = source->Body.X + dx, y = source->Body.Y + dy; - wrap_tile_coords (&p_bic_data->Map, &x, &y); - Tile * tile = tile_at (x, y); - if (tile == p_null_tile) continue; - if (tile->vtable->m38_Get_Territory_OwnerID (tile) != source->Body.CivID) continue; + // Add the Wonder to the city + patch_City_add_or_remove_improvement (city, __, improv_id, 1, false); + } +} - struct district_instance * inst = get_district_instance (tile); - if (inst == NULL || inst->district_type != required_district_id) continue; - if (! district_is_complete (tile, required_district_id)) continue; +void +on_gain_city (Leader * leader, City * city, enum city_gain_reason reason) +{ + if (reason == CGR_FOUNDED) + is->turn_no_of_last_founding_for_settler_perfume[leader->ID] = *p_current_turn_no; - FOR_CITIES_OF (coi, source->Body.CivID) { - City * city = coi.city; - if (city == NULL) continue; - if (city == source) continue; + // Handle extra palaces for AI multi-city start + if (((*p_human_player_bits & (1<ID)) == 0) && // If leader is an AI AND + (is->current_config.ai_multi_city_start > 1) && // AI multi-city start is enabled AND + (leader->Cities_Count > 1) && // city is not the only one the AI has (i.e. it's not the capital) AND + (reason != CGR_PLACED_FOR_SCENARIO) && (reason != CGR_PLACED_FOR_AI_MULTI_CITY_START)) { // city was not placed before the game started - if (! city_radius_contains_tile (city, x, y)) - continue; + // Find an extra palace that this player does not already have + int free_extra_palace = -1; + for (int n = 0; n < is->current_config.count_ai_multi_start_extra_palaces; n++) { + int improv_id = is->current_config.ai_multi_start_extra_palaces[n]; + Improvement * improv = &p_bic_data->Improvements[improv_id]; + if ((improv->Characteristics & ITC_Small_Wonder) && + (improv->SmallWonderFlags & ITSW_Reduces_Corruption) && + (NULL == get_city_ptr (leader->Small_Wonders[improv_id]))) { + free_extra_palace = improv_id; + break; + } + } - if (! patch_City_has_improvement (city, __, improv_id, false)) { - City_add_or_remove_improvement (city, __, improv_id, 1, false); + // Place that extra palace here + if (free_extra_palace >= 0) + City_add_or_remove_improvement (city, __, free_extra_palace, 1, false); + } - // If city already building it, switch to a defensive unit instead - int current_improv_id = city->Body.Order_ID; - if (current_improv_id == improv_id) { - City_Order defensive_order = { .OrderID = -1, .OrderType = 0 }; - if (choose_defensive_unit_order (city, &defensive_order)) { - City_set_production (city, __, defensive_order.OrderType, defensive_order.OrderID, false); - } - } + if (is->current_config.enable_districts || is->current_config.enable_natural_wonders) { + patch_City_recompute_yields_and_happiness (city); + patch_City_recompute_culture_income (city); + } - // Show message to user - if (is->current_config.show_message_when_building_received_by_mutual_district && - city->Body.CivID == p_main_screen_form->Player_CivID) { - char msg[300]; - snprintf (msg, sizeof msg, "%s %s %s %s %s %s %s", - city->Body.CityName, - is->c3x_labels[CL_RECEIVED], - p_bic_data->Improvements[improv_id].Name.S, - is->c3x_labels[CL_FROM_SHARED], - is->district_configs[required_district_id].name, - is->c3x_labels[CL_WITH], - source->Body.CityName); - msg[(sizeof msg) - 1] = '\0'; - show_map_specific_text (city->Body.X, city->Body.Y, msg, true); - } - } + if (is->current_config.enable_districts) { + + // Remove any district previously on the tile, if city just founded + if (reason == CGR_FOUNDED) { + Tile * city_tile = tile_at (city->Body.X, city->Body.Y); + if ((city_tile != NULL) && (city_tile != p_null_tile)) { + struct district_instance * inst = get_district_instance (city_tile); + if (inst != NULL) + handle_district_removed (city_tile, inst->district_id, city->Body.X, city->Body.Y, false); } } + + bool receive_buildings = is->current_config.cities_with_mutual_district_receive_buildings; + bool receive_wonders = is->current_config.cities_with_mutual_district_receive_wonders; + + // Grant buildings and wonders from nearby completed districts owned by other cities of same civ, if enabled + if (receive_buildings) { + grant_existing_district_buildings_to_city (city); + } + + // Grant wonders nearby in same territory, regardless of city (i.e., orphaned wonders from destroyed cities), if enabled + if (receive_wonders) { + grant_nearby_wonders_to_city (city); + } + + if (is->current_config.enable_distribution_hub_districts) { + refresh_distribution_hubs_for_city (city); + } } } void -grant_existing_district_buildings_to_city (City * city) +on_lose_city (Leader * leader, City * city, enum city_loss_reason reason) { - if (! is->current_config.enable_districts || - (! is->current_config.cities_with_mutual_district_receive_buildings && - ! is->current_config.cities_with_mutual_district_receive_wonders) || - (city == NULL)) - return; + // If leader is an AI and AI MCS is enabled, remove any extra palaces the city has + if (((*p_human_player_bits & (1<ID)) == 0) && + (is->current_config.ai_multi_city_start > 1)) + remove_extra_palaces (city, NULL); - int civ_id = city->Body.CivID; - int current_improv_id = city->Body.Order_ID; - bool prev_flag = is->sharing_buildings_by_districts_in_progress; - is->sharing_buildings_by_districts_in_progress = true; + if (is->current_config.enable_districts) { + if (is->current_config.enable_distribution_hub_districts) + is->distribution_hub_totals_dirty = true; - for (int n = 0; n < is->workable_tile_count; n++) { - int dx, dy; - patch_ni_to_diff_for_work_area (n, &dx, &dy); - int tile_x = city->Body.X + dx; - int tile_y = city->Body.Y + dy; - wrap_tile_coords (&p_bic_data->Map, &tile_x, &tile_y); - Tile * tile = tile_at (tile_x, tile_y); - if (tile == p_null_tile) - continue; - if (tile->vtable->m38_Get_Territory_OwnerID (tile) != civ_id) - continue; + forget_pending_building_order (city); + for (int district_id = 0; district_id < is->district_count; district_id++) + clear_city_district_request (city, district_id); - struct district_instance * inst = get_district_instance (tile); - if (inst == NULL) - continue; - int district_id = inst->district_type; - if ((district_id < 0) || (district_id >= is->district_count)) - continue; - if (! district_is_complete (tile, district_id)) - continue; + if (is->current_config.enable_wonder_districts) + release_wonder_district_reservation (city); + } else if (is->current_config.enable_distribution_hub_districts) + is->distribution_hub_totals_dirty = true; +} - struct district_infos * info = &is->district_infos[district_id]; - if (info->dependent_building_count <= 0) - continue; +// Returns -1 if the location is unusable, 0-9 if it's usable but doesn't satisfy all criteria, and 10 if it couldn't be better +int +eval_starting_location (Map * map, int const * alt_starting_locs, int alt_starting_loc_count, int tile_x, int tile_y, int civ_id) +{ + Tile * tile = tile_at (tile_x, tile_y); + if ((tile != p_null_tile) && + (patch_Map_check_city_location (map, __, tile_x, tile_y, civ_id, true) == CLV_OK) && + (tile->vtable->m15_Check_Goody_Hut (tile, __, 0) == 0) && + (tile->vtable->m40_get_TileUnit_ID (tile) == -1)) { + int tr = 0; - int tx, ty; - if (! district_instance_get_coords (inst, tile, &tx, &ty)) - continue; + // Avoid this location if it's too close to another starting location. If it's much too close, it's ruled out entirely. + int closest_dist = INT_MAX; + for (int n = 1; n <= map->Civ_Count; n++) + if (map->Starting_Locations[n] >= 0) { + int other_x, other_y; + tile_index_to_coords (map, map->Starting_Locations[n], &other_x, &other_y); + int dist = Map_get_x_dist (map, __, tile_x, other_x) + Map_get_y_dist (map, __, tile_y, other_y); + if (dist < closest_dist) + closest_dist = dist; + } + for (int n = 0; n < alt_starting_loc_count; n++) + if (alt_starting_locs[n] >= 0) { + int other_x, other_y; + tile_index_to_coords (map, alt_starting_locs[n], &other_x, &other_y); + int dist = Map_get_x_dist (map, __, tile_x, other_x) + Map_get_y_dist (map, __, tile_y, other_y); + if (dist < closest_dist) + closest_dist = dist; + } + if (closest_dist < map->Civ_Distance/3) + return -1; + else if (closest_dist >= 2*map->Civ_Distance/3) + tr += 1; - FOR_CITIES_OF (coi, civ_id) { - City * other = coi.city; - if ((other == NULL) || (other == city)) - continue; + // Avoid tiny islands + // tr += map->vtable->m33_Get_Continent (map, __, tile->ContinentID)->Body.TileCount >= 20; - if (! city_radius_contains_tile (other, tx, ty)) - continue; + // Avoid garbage terrain, e.g. all desert or tundra + int break_even_food_tiles = 0; + FOR_TILES_AROUND (tai, 21, tile_x, tile_y) { + if (tai.n == 0) + continue; // Skip tile that would be covered by the city + int x, y; + tai_get_coords (&tai, &x, &y); + int tile_food = patch_Map_calc_food_yield_at (map, __, x, y, tai.tile->vtable->m50_Get_Square_BaseType (tai.tile), civ_id, 0, NULL); + int food_required = p_bic_data->General.FoodPerCitizen + (tai.tile->vtable->m35_Check_Is_Water (tai.tile) ? 1 : 0); + break_even_food_tiles += tile_food >= food_required; + } + tr += break_even_food_tiles >= 2; - if (tile->vtable->m38_Get_Territory_OwnerID (tile) != other->Body.CivID) - continue; + // Avoid wasting a food bonus + tr += (tile->ResourceType < 0) || (tile->ResourceType >= p_bic_data->ResourceTypeCount) || + (p_bic_data->ResourceTypes[tile->ResourceType].Food == 0); - for (int i = 0; i < info->dependent_building_count; i++) { - int building_id = info->dependent_building_ids[i]; - if (building_id < 0) - continue; + int max_score = 3; + return (10*tr)/max_score; + } else + return -1; +} - Improvement * building = &p_bic_data->Improvements[building_id]; - bool is_wonder = (building->Characteristics & (ITC_Wonder | ITC_Small_Wonder)) != 0; +City * +create_starter_city (Map * map, int civ_id, int tile_index) +{ + int x, y; + tile_index_to_coords (map, tile_index, &x, &y); + City * tr = Leader_create_city (&leaders[civ_id], __, x, y, leaders[civ_id].RaceID, -1, NULL, true); + if (tr != NULL) + on_gain_city (&leaders[civ_id], tr, CGR_PLACED_FOR_AI_MULTI_CITY_START); + return tr; +} - if (is_wonder) - continue; +void +set_up_ai_multi_city_start (Map * map, int city_count) +{ + // Set of bits determining which players are eligible for the two-city start + int eligibility_bits = 0, + count_eligible_civs = 0; + for (int n = 1; n < 32; n++) + if ((*p_player_bits & 1<Starting_Locations[n]) != p_null_tile)) { // has a valid starting location + eligibility_bits |= 1 << n; + count_eligible_civs++; + } - if (! patch_City_has_improvement (other, __, building_id, false)) - continue; + if ((city_count < 1) || (count_eligible_civs == 0)) // if we have nothing to do + return; - if (patch_City_has_improvement (city, __, building_id, false)) - continue; + char load_text[50]; + snprintf (load_text, sizeof load_text, "%s...", is->c3x_labels[CL_CREATING_CITIES]); + load_text[(sizeof load_text) - 1] = '\0'; + Main_GUI_label_loading_bar (&p_main_screen_form->GUI, __, 1, load_text); - City_add_or_remove_improvement (city, __, building_id, 1, false); + // Generate alternate sets of starting locations. We need one set for each extra city we're going to create per player. Each set only needs to + // include starting locations for eligible players, all others will be left as -1. + int alt_starting_loc_count = 32 * (city_count - 1); + int * alt_starting_locs = malloc (alt_starting_loc_count * sizeof *alt_starting_locs); { + for (int n = 0; n < alt_starting_loc_count; n++) + alt_starting_locs[n] = -1; - // If city already building it, switch to a defensive unit instead - if (current_improv_id == building_id) { - City_Order defensive_order = { .OrderID = -1, .OrderType = 0 }; - if (choose_defensive_unit_order (city, &defensive_order)) { - City_set_production (city, __, defensive_order.OrderType, defensive_order.OrderID, false); + for (int i_loc = 0; i_loc < alt_starting_loc_count; i_loc++) { + int civ_id = i_loc % 32; + if ((civ_id != 0) && (eligibility_bits & 1<current_config.max_tries_to_place_fp_city; try++) { + int i_loc = rand_int (p_rand_object, __, map->TileCount); + int x_loc, y_loc; + tile_index_to_coords (map, i_loc, &x_loc, &y_loc); + if ((x_loc >= 0) && (y_loc >= 0)) { + int val = eval_starting_location (map, alt_starting_locs, alt_starting_loc_count, x_loc, y_loc, civ_id); + if (val >= 10) { + best_loc_index = i_loc; + break; + } else if (val > best_loc_val) { + best_loc_val = val; + best_loc_index = i_loc; + } } } - if (is->current_config.show_message_when_building_received_by_mutual_district && - city->Body.CivID == p_main_screen_form->Player_CivID) { - char msg[300]; - snprintf (msg, sizeof msg, "%s %s %s %s %s %s %s", - city->Body.CityName, - is->c3x_labels[CL_RECEIVED], - p_bic_data->Improvements[building_id].Name.S, - is->c3x_labels[CL_FROM_SHARED], - is->district_configs[district_id].name, - is->c3x_labels[CL_WITH], - other->Body.CityName); - msg[(sizeof msg) - 1] = '\0'; - show_map_specific_text (city->Body.X, city->Body.Y, msg, true); - } + if (best_loc_index >= 0) + alt_starting_locs[i_loc] = best_loc_index; } } } - is->sharing_buildings_by_districts_in_progress = prev_flag; -} + int count_cities_created = 0; + int count_eligible_civs_handled = 0; + for (int civ_id = 1; civ_id < 32; civ_id++) + if (eligibility_bits & 1<Starting_Locations[civ_id]; + + // Create the first starting city for the AI. This one is its capital and is located at its actual starting + // location. Afterward, delete its starting settler so it's as if the settler founded the city. + { + Unit * starting_settler = NULL; + FOR_UNITS_ON (tai, tile_at_index (map, sloc)) { + if (p_bic_data->UnitTypes[tai.unit->Body.UnitTypeID].AI_Strategy & UTAI_Settle) { + starting_settler = tai.unit; + break; + } + } + + create_starter_city (map, civ_id, sloc); + count_cities_created++; + + if (starting_settler != NULL) + patch_Unit_despawn (starting_settler, __, 0, 1, 0, 0, 0, 0, 0); + + } + + // Memoize all of the AI's starting units + clear_memo (); + FOR_UNITS_ON (tai, tile_at_index (map, sloc)) + memoize ((int)tai.unit); + + int extra_city_count = 0; + for (int i_city = 1; i_city < city_count; i_city++) { + int loc = alt_starting_locs[(i_city-1)*32 + civ_id]; + City * city; + if ((loc >= 0) && + (NULL != (city = create_starter_city (map, civ_id, loc)))) { + count_cities_created++; + extra_city_count++; + + // Spawn palace substitute in new city + if (extra_city_count-1 < is->current_config.count_ai_multi_start_extra_palaces) + patch_City_add_or_remove_improvement (city, __, is->current_config.ai_multi_start_extra_palaces[extra_city_count - 1], 1, true); + + // Move starting units over to the new city. Do this by moving every Nth unit where N is the number of extra + // cities we've founded so far plus one. ("Extra" cities are the ones created after the capital.) E.g., if + // this is the 1st city after the capital, move every 2nd unit to it. If it's the 2nd, move every 3rd, etc. + for (int n = 0; n < is->memo_len; n++) + if (n % (extra_city_count+1) == extra_city_count) + patch_Unit_move ((Unit *)is->memo[n], __, city->Body.X, city->Body.Y); -//We need to forwards-declare this -void __fastcall -patch_City_manage_by_governor (City * this, int edx, bool manage_professions); + } + } -void __fastcall -patch_City_add_or_remove_improvement (City * this, int edx, int improv_id, int add, bool param_3) -{ - int init_maintenance = this->Body.Improvements_Maintenance; - Improvement * improv = &p_bic_data->Improvements[improv_id]; - bool is_wonder_removal = (! add) && - ((improv->Characteristics & (ITC_Wonder | ITC_Small_Wonder)) != 0) && - (! is->is_placing_scenario_things); - int init_work_area_radius = get_work_ring_limit_total(this); + // Update progress report + count_eligible_civs_handled++; + int progress = 100 * count_eligible_civs_handled / count_eligible_civs; + snprintf (load_text, sizeof load_text, "%s %d%%", is->c3x_labels[CL_CREATING_CITIES], progress); + load_text[(sizeof load_text) - 1] = '\0'; + Main_GUI_label_loading_bar (&p_main_screen_form->GUI, __, 1, load_text); - // The enable_negative_pop_pollution feature changes the rules so that improvements flagged as removing pop pollution and having a negative - // pollution amount contribute to the city's pop pollution instead of building pollution. Here we make sure that such improvements do not - // contribute to building pollution by temporarily zeroing out their pollution stat when they're added to or removed from a city. - if (is->current_config.enable_negative_pop_pollution && - (improv->ImprovementFlags & ITF_Removes_Population_Pollution) && - (improv->Pollution < 0)) { - int saved_pollution_amount = improv->Pollution; - improv->Pollution = 0; - City_add_or_remove_improvement (this, __, improv_id, add, param_3); - improv->Pollution = saved_pollution_amount; - } else - City_add_or_remove_improvement (this, __, improv_id, add, param_3); + } - if (is->current_config.enable_districts && is_wonder_removal && ((improv->Characteristics & ITC_Wonder) != 0)) { - if (is->current_config.destroyed_wonders_can_be_built_again) - set_wonder_built_flag (improv_id, false); + free (alt_starting_locs); + // Sanity check + int any_adjacent_cities = 0; { + if (p_cities->Cities != NULL) + for (int city_index = 0; (city_index <= p_cities->LastIndex) && ! any_adjacent_cities; city_index++) { + City * city = get_city_ptr (city_index); + if (city != NULL) + FOR_TILES_AROUND (tai, 9, city->Body.X, city->Body.Y) + if ((tai.n > 0) && (NULL != get_city_ptr (tai.tile->vtable->m45_Get_City_ID (tai.tile)))) { + any_adjacent_cities = 1; + break; + } + } + } + int any_missing_fp_cities = (count_cities_created < city_count * count_eligible_civs); + if (any_adjacent_cities || any_missing_fp_cities) { PopupForm * popup = get_popup_form (); - set_popup_str_param (0, improv->Name.S, -1, -1); - popup->vtable->set_text_key_and_flags ( - popup, __, is->mod_script_path, - is->current_config.destroyed_wonders_can_be_built_again ? "C3X_DISTRICT_WONDER_DESTROYED_REBUILD" : "C3X_DISTRICT_WONDER_DESTROYED", - -1, 0, 0, 0 - ); + popup->vtable->set_text_key_and_flags (popup, __, is->mod_script_path, "C3X_WARNING", -1, 0, 0, 0); + char s[100]; + snprintf (s, sizeof s, "^%s", is->c3x_labels[CL_MCS_FAILED_SANITY_CHECK]); + s[(sizeof s) - 1] = '\0'; + PopupForm_add_text (popup, __, s, 0); + if (any_adjacent_cities) { + snprintf (s, sizeof s, "^%s", is->c3x_labels[CL_MCS_ADJACENT_CITIES]); + s[(sizeof s) - 1] = '\0'; + PopupForm_add_text (popup, __, s, 0); + } + if (any_missing_fp_cities) { + snprintf (s, sizeof s, "^%s", is->c3x_labels[CL_MCS_MISSING_CITIES]); + s[(sizeof s) - 1] = '\0'; + PopupForm_add_text (popup, __, s, 0); + } patch_show_popup (popup, __, 0, 0); } +} - // If the city just finished a wonder and was using a wonder district for that, set the wonder - // as completed (which switches the art over and prevents the tile from being used again) - int x, y; - if (add && is->current_config.enable_districts && is->current_config.enable_wonder_districts) { - if (improv->Characteristics & (ITC_Wonder | ITC_Small_Wonder)) { - int matched_windex = find_wonder_config_index_by_improvement_id (improv_id); +void __fastcall +patch_Map_process_after_placing (Map * this, int edx, bool param_1) +{ + if ((is->current_config.ai_multi_city_start > 0) && (*p_current_turn_no == 0)) + set_up_ai_multi_city_start (this, is->current_config.ai_multi_city_start); - if (matched_windex >= 0) { - int city_x = this->Body.X; - int city_y = this->Body.Y; - for (int n = 0; n < is->workable_tile_count; n++) { - int dx, dy; - patch_ni_to_diff_for_work_area (n, &dx, &dy); - x = city_x + dx, y = city_y + dy; - wrap_tile_coords (&p_bic_data->Map, &x, &y); - Tile * t = tile_at (x, y); - if (t == p_null_tile) continue; - if (t->vtable->m38_Get_Territory_OwnerID (t) != this->Body.CivID) continue; + Map_process_after_placing (this, __, param_1); +} - struct district_instance * inst = get_district_instance (t); - if (inst == NULL || inst->district_type != WONDER_DISTRICT_ID) continue; - if (! district_is_complete (t, inst->district_type)) continue; +void __fastcall +patch_Map_impl_generate (Map * this, int edx, int seed, bool is_multiplayer_game, int num_seafaring_civs) +{ + Map_impl_generate (this, __, seed, is_multiplayer_game, num_seafaring_civs); - struct wonder_district_info * info = &inst->wonder_info; - if (info->state != WDS_UNDER_CONSTRUCTION) continue; - if (info->city_id != this->Body.ID) continue; + if (is->current_config.enable_natural_wonders) + place_natural_wonders_on_map (); +} - // Mark this wonder district as completed with the wonder - info->city = this; - info->city_id = this->Body.ID; - info->state = WDS_COMPLETED; - info->wonder_index = matched_windex; - break; - } - } - } - } - - //Calculate if work_area has shrunk, and if so, redistribute citizens. - int post_work_area_radius = get_work_ring_limit_total(this); - if (post_work_area_radius < init_work_area_radius) { - patch_City_manage_by_governor(this, __, false); - } +int __fastcall +patch_City_get_net_commerce (City * this, int edx, int kind, bool include_science_age) +{ + int base = City_get_net_commerce (this, __, kind, include_science_age); - // Update things in case we've added or removed a mill. This is only necessary while in-game. If the game is still loading the scenario, it - // will recompute resources after it's done and we'll recompute yields and happiness ourselves. - if (! is->is_placing_scenario_things) { - // Collect info about this mill, if in fact the added or removed improvement is a mill. If it's not, all these vars will be left - // false. "generates_input" tracks whether or not the mill generates a resource that's an input for another mill. - bool is_non_local_mill, is_yielding_mill, generates_input; { - is_non_local_mill = is_yielding_mill = generates_input = false; - for (int n = 0; n < is->current_config.count_mills; n++) { - struct mill * mill = &is->current_config.mills[n]; - if (mill->improv_id == improv_id) { - is_non_local_mill |= (mill->flags & MF_LOCAL) == 0; - is_yielding_mill |= (mill->flags & MF_YIELDS) != 0; - generates_input |= (is->mill_input_resource_bits[mill->resource_id >> 3] & (1 << (mill->resource_id & 7))) != 0; - } - } - } + if ((kind == 1) && // beakers, as opposed to 2 which is gold + (is->current_config.ai_research_multiplier != 100) && + ((*p_human_player_bits & 1<Body.CivID) == 0)) + return (base * is->current_config.ai_research_multiplier + 50) / 100; + else + return base; +} - // If the mill generates a resource that's added to the trade network or it's generating an input (potentially used locally to - // generate a traded resource) then rebuild the resource network. This is not necessary if the improvement also affects trade routes - // since the base method will have already done this recomputation. - if ((is_non_local_mill || generates_input) && - ((improv->ImprovementFlags & ITF_Allows_Water_Trade) == 0) && - ((improv->ImprovementFlags & ITF_Allows_Air_Trade) == 0) && - ((improv->WonderFlags & ITW_Safe_Sea_Travel) == 0)) - patch_Trade_Net_recompute_resources (is->trade_net, __, 0); +// Cuts research spending while total expenditures > treasury. Returns whether spending has been cut. +bool +cut_unaffordable_research_spending (Leader * leader, bool skip_popup) +{ + // The rate cap limits AI players' spending on the gold slider (in Leader::ai_adjust_sliders) however it does not apply to human players when + // adjusting sliders manually on the domestic advisor screen. + int gold_rate_cap = ((*p_human_player_bits & 1<ID) != 0) ? 10 : p_bic_data->Governments[leader->GovernmentType].RateCap; - // If the mill adds yields or might be a link in a resource production chain that does, recompute yields in the city. - if (is_yielding_mill || generates_input) - patch_City_recompute_yields_and_happiness (this); + int treasury = leader->Gold_Encoded + leader->Gold_Decrement; + bool reduced_spending = false; + while (leader->science_slider > 0 && + leader->gold_slider < gold_rate_cap && + treasury + Leader_compute_income (leader) < 0) { + leader->science_slider -= 1; + leader->gold_slider += 1; + Leader_recompute_economy (leader); + reduced_spending = true; + } + if (reduced_spending && ! skip_popup && leader->ID == p_main_screen_form->Player_CivID) { + PopupForm * popup = get_popup_form (); + popup->vtable->set_text_key_and_flags (popup, __, is->mod_script_path, "C3X_FORCE_CUT_RESEARCH_SPENDING", -1, 0, 0, 0); + patch_show_popup (popup, __, 0, 0); } + return reduced_spending; +} - // Adding or removing an obsolete improvement should not change the total maintenance since obsolete improvs shouldn't cost maintenance. In - // the base game, they usually do since the game doesn't update maintenance costs when buildings are obsoleted, but with that bug patched we - // can enforce the correct behavior. - if (is->current_config.patch_maintenance_persisting_for_obsolete_buildings && - (improv->ObsoleteID >= 0) && Leader_has_tech (&leaders[this->Body.CivID], __, improv->ObsoleteID)) - this->Body.Improvements_Maintenance = init_maintenance; +void __fastcall +adjust_sliders_preproduction (Leader * this) +{ + if ((*p_human_player_bits & 1<ID) == 0) { + // Replicate the behavior of the original code for AI players. (apply_machine_code_edits overwrites an equivalent branch & call in the + // original code with a call to this method.) + this->vtable->ai_adjust_sliders (this); - // If AI MCS is enabled and we've added the Palace to a city, then remove any extra palaces from that city. If we're in the process of - // capturing a city, it's necessary to exclude that city as a possible destination for removed extra palaces. - if ((is->current_config.ai_multi_city_start > 1) && - (! is->is_placing_scenario_things) && - add && (improv->ImprovementFlags & ITF_Center_of_Empire) && - ((*p_human_player_bits & (1 << this->Body.CivID)) == 0)) - remove_extra_palaces (this, is->currently_capturing_city); + // If aggressively penalize bankruptcy is on, cut the AI's research spending if it can't afford it. This may be redundant as + // ai_adjust_sliders will already cut AI spending but maybe not all the way to zero. Do this only every third turn so the AI is not + // completely prevented from researching. + if (is->current_config.aggressively_penalize_bankruptcy && (*p_current_turn_no + this->ID) % 3 == 0) + cut_unaffordable_research_spending (this, true); - // If sharing wonders in hotseat mode, we must recompute improvement maintenance for all human players when any one of them gains or loses a - // wonder that grants free improvements. - if ((! is->is_placing_scenario_things) && - is->current_config.share_wonders_in_hotseat && + // If human player would go bankrupt, try reducing their research spending to avoid that + } else if (is->current_config.cut_research_spending_to_avoid_bankruptcy) + cut_unaffordable_research_spending (this, false); +} + +int __fastcall +patch_City_get_improvement_maintenance (City * this, int edx, int improv_id) +{ + // Check if this improvment is provided for free by another player via shared wonder effects + int civ_id = this->Body.CivID; + bool free_from_sharing = false; + if (is->current_config.share_wonders_in_hotseat && (*p_is_offline_mp_game && ! *p_is_pbem_game) && // is hotseat game - ((1 << this->Body.CivID) & *p_human_player_bits)) { // is this city owned by a human player + ((1 << civ_id) & *p_human_player_bits)) { // is this city owned by a human player + + // Check if any other human player in the game has this improv in their auto improvs table, including for this city's continent + bool has_free_improv = false; + Tile * city_tile = tile_at (this->Body.X, this->Body.Y); + int continent_id = city_tile->vtable->m46_Get_ContinentID (city_tile); + int cont_coded_key = (continent_id + 1) * p_bic_data->ImprovementsCount + improv_id;; unsigned player_bits = *(unsigned *)p_human_player_bits >> 1; int n_player = 1; while (player_bits != 0) { - if ((player_bits & 1) && (n_player != this->Body.CivID)) - Leader_recompute_buildings_maintenance (&leaders[n_player]); + if ((player_bits & 1) && (n_player != civ_id)) + if (Hash_Table_look_up (&leaders[n_player].Auto_Improvements, __, improv_id , NULL) || + Hash_Table_look_up (&leaders[n_player].Auto_Improvements, __, cont_coded_key, NULL)) { + free_from_sharing = true; + break; + } player_bits >>= 1; n_player++; } } - // Optionally share district-dependent buildings or wonders to other cities in range of the same district - bool allow_wonder_sharing = is->current_config.cities_with_mutual_district_receive_wonders; - bool allow_building_sharing = is->current_config.cities_with_mutual_district_receive_buildings; + if (! free_from_sharing) + return City_get_improvement_maintenance (this, __, improv_id); + else + return 0; +} + +int __fastcall +patch_Leader_count_maintenance_free_units (Leader * this) +{ + if ((is->current_config.extra_unit_maintenance_per_shields <= 0) && (this->ID != 0)) + return Leader_count_maintenance_free_units (this); + else { + int tr = 0; + for (int n = 0; n <= p_units->LastIndex; n++) { + Unit * unit = get_unit_ptr (n); + if ((unit != NULL) && (unit->Body.CivID == this->ID)) { + UnitType * type = &p_bic_data->UnitTypes[unit->Body.UnitTypeID]; + + // If this is a free unit + if ((unit->Body.RaceID != this->RaceID) || (type->requires_support == 0)) + tr++; - if ((! is->is_placing_scenario_things) && add && - is->current_config.enable_districts && (allow_building_sharing || allow_wonder_sharing) && - (! is->sharing_buildings_by_districts_in_progress)) { - int required_district_id; - if (itable_look_up (&is->district_building_prereqs, improv_id, &required_district_id)) { - bool is_wonder = (improv->Characteristics & (ITC_Wonder | ITC_Small_Wonder)) != 0; - if ((! is_wonder && allow_building_sharing) || (is_wonder && allow_wonder_sharing)) { - is->sharing_buildings_by_districts_in_progress = true; - copy_building_with_cities_in_radius (this, improv_id, required_district_id, x, y); - is->sharing_buildings_by_districts_in_progress = false; + // If this unit belongs to the barbs and has a tribe ID set (indicating it was spawned in a barb camp) then count it + // as a free unit in order not to bankrupt the barb player if the barb player controls any cities. A tribe ID of 75 is + // the generic ID that's assigned to barb units when they're spawned with no specific tribe ID. + else if ((unit->Body.CivID == 0) && (unit->Body.barb_tribe_id >= 0) && (unit->Body.barb_tribe_id < 75)) + tr++; + + // Otherwise, if we're configured to apply extra maintenance based on shield cost, reduce the free unit count by + // however many times this unit's cost is above the threshold for extra maintenace. It's alright if the resulting free + // unit count is negative since all callers of this function subtract its return value from the total unit count to + // obtain the count of units that must be paid for. + else if (is->current_config.extra_unit_maintenance_per_shields > 0) + tr -= type->Cost / is->current_config.extra_unit_maintenance_per_shields; } } + return tr; } } -void __fastcall -patch_Fighter_begin (Fighter * this, int edx, Unit * attacker, int attack_direction, Unit * defender) +int __fastcall +patch_Leader_sum_unit_maintenance (Leader * this, int edx, int government_id) { - Fighter_begin (this, __, attacker, attack_direction, defender); - - // Apply override of retreat eligibility - // Must use this->defender instead of the defender argument since the argument is often NULL, in which case Fighter_begin finds a defender on - // the target tile and stores it in this->defender. Also must check that against NULL since Fighter_begin might fail to find a defender. - enum UnitTypeClasses class = p_bic_data->UnitTypes[this->attacker->Body.UnitTypeID].Unit_Class; - if ((this->defender != NULL) && ((class == UTC_Land) || (class == UTC_Sea))) { - enum retreat_rules retreat_rules = (class == UTC_Land) ? is->current_config.land_retreat_rules : is->current_config.sea_retreat_rules; - if (retreat_rules != RR_STANDARD) { - if (retreat_rules == RR_NONE) - this->attacker_eligible_to_retreat = this->defender_eligible_to_retreat = 0; - else if (retreat_rules == RR_ALL_UNITS) { - if (! UnitType_has_ability (&p_bic_data->UnitTypes[this->attacker->Body.UnitTypeID], __, UTA_Immobile)) - this->attacker_eligible_to_retreat = 1; - if (! UnitType_has_ability (&p_bic_data->UnitTypes[this->defender->Body.UnitTypeID], __, UTA_Immobile)) - this->defender_eligible_to_retreat = 1; - } else if (retreat_rules == RR_IF_FASTER) { - int diff = patch_Unit_get_max_move_points (this->attacker) - patch_Unit_get_max_move_points (this->defender); - this->attacker_eligible_to_retreat = diff > 0; - this->defender_eligible_to_retreat = diff < 0; + if (is->current_config.extra_unit_maintenance_per_shields <= 0) + return Leader_sum_unit_maintenance (this, __, government_id); + else if (this->Cities_Count > 0) { + int maint_free_count = patch_Leader_count_maintenance_free_units (this); + int cost_per_unit, base_free_count; + get_unit_support_info (this->ID, government_id, &cost_per_unit, &base_free_count); + int ai_free_count; { + if (*p_human_player_bits & 1<ID) + ai_free_count = 0; + else { + Difficulty_Level * difficulty = &p_bic_data->DifficultyLevels[*p_game_difficulty]; + ai_free_count = difficulty->Bonus_For_Each_City * this->Cities_Count + difficulty->Additional_Free_Support; } - this->defender_eligible_to_retreat &= city_at (this->defender_location_x, this->defender_location_y) == NULL; } - } + return not_below (0, (this->Unit_Count - base_free_count - ai_free_count - maint_free_count) * cost_per_unit); + } else + return 0; } -void __fastcall -patch_Unit_despawn (Unit * this, int edx, int civ_id_responsible, byte param_2, byte param_3, byte param_4, byte param_5, byte param_6, byte param_7) +int +sum_improvements_maintenance_to_pay (Leader * leader, int govt_id) { - // Determine whether this despawn happened involuntarily, i.e. whether it's because of the actions of a player other than its owner. This - // includes cases where the unit was destroyed in combat, captured, kicked from territory with nowhere to go, etc. - int ret_addr = ((int *)&civ_id_responsible)[-1]; - bool involuntary = - ret_addr == DESPAWN_TO_FIGHT_1_RETURN || ret_addr == DESPAWN_TO_FIGHT_2_RETURN - || ret_addr == DESPAWN_TO_DO_BOMBARD_TILE_RETURN || ret_addr == DESPAWN_TO_CRUISE_MISSILE_DEFENDER_RETURN - || ret_addr == DESPAWN_TO_BOUNCE_TRESPASSING_UNITS_RETURN || ret_addr == DESPAWN_TO_NUKE_DAMAGE_RETURN - || ret_addr == DESPAWN_RECURSIVE_RETURN || ret_addr == DESPAWN_TO_DO_CAPTURE_UNITS_RETURN - || ret_addr == DESPAWN_TO_TRY_FLYING_OVER_TILE_RETURN; - - int owner_id = this->Body.CivID; - int type_id = this->Body.UnitTypeID; - UnitType * type = &p_bic_data->UnitTypes[type_id]; - Tile * tile = tile_at (this->Body.X, this->Body.Y); + if (is->current_config.aggressively_penalize_bankruptcy) + // We're going to replace the logic that charges maintenance, and the replacement will charge both unit & building maintenance b/c + // they're intertwined under the new rules. Return zero here so the base game code doesn't charge any building maintenance on its own. + return 0; + else + return Leader_sum_improvements_maintenance (leader, __, govt_id); +} - // Clear extra DBs, airdrops, wait records, and transport ties used by this unit - itable_remove (&is->extra_defensive_bombards, this->Body.ID); - itable_remove (&is->airdrops_this_turn, this->Body.ID); - itable_remove (&is->waiting_units, this->Body.ID); - itable_remove (&is->unit_transport_ties, this->Body.ID); +// The sum_improvements_maintenance function is called in two places as part of the randomization of whether improv or unit maintenance gets paid +// first. Redirect both of these calls to one function of our own. +int __fastcall patch_Leader_sum_improv_maintenance_to_pay_1 (Leader * this, int edx, int govt_id) { return sum_improvements_maintenance_to_pay (this, govt_id); } +int __fastcall patch_Leader_sum_improv_maintenance_to_pay_2 (Leader * this, int edx, int govt_id) { return sum_improvements_maintenance_to_pay (this, govt_id); } - // If we're despawning the stored ZoC defender, clear that variable so we don't despawn it again in check_life_after_zoc - if (this == is->zoc_defender) - is->zoc_defender = NULL; +int +compare_buildings_to_sell (void const * a, void const * b) +{ + int maint_a = (*(int const *)a >> 26) & 31, + maint_b = (*(int const *)b >> 26) & 31; + return maint_b - maint_a; +} - if (this == is->sb_next_up) - is->sb_next_up = NULL; +// Returns the final improv cost after buildings were sold +int +sell_unaffordable_buildings (Leader * leader, int improv_cost, int unit_cost) +{ + int treasury = leader->Gold_Encoded + leader->Gold_Decrement; - if (this == is->last_selected_unit.ptr) - is->last_selected_unit.ptr = NULL; + clear_memo (); - // Remove this unit from the list of extra units to despawn after capturing - int original_count = is->count_extra_capture_despawns; - for (int n_src = 0, n_dest = 0; n_src < original_count; n_src++) { - if (is->extra_capture_despawns[n_src] != this) { - is->extra_capture_despawns[n_dest] = is->extra_capture_despawns[n_src]; - n_dest++; - } else - is->count_extra_capture_despawns -= 1; - } + // Memoize all buildings this player owns that we might potentially sell. B/c the memo can only contain ints, we must pack the maintenance + // cost, improv ID, and city ID all into one int. The city ID is stored in the lowest 13 bits, then the improv ID in the next 13, and finally + // the maintenance amount in the 5 above those. + FOR_CITIES_OF (coi, leader->ID) + for (int n = 0; n < p_bic_data->ImprovementsCount; n++) { + Improvement * improv = &p_bic_data->Improvements[n]; - // If the unit being despawned is a land transport or helicopter, we may have to make sure to despawn its passengers as well because the base - // game won't. In general, the passengers are lost if the transport was despawned involuntarily, i.e. because of another player's attack or - // something like that, or if the passengers couldn't survive outside the transport, specifically because it's a helicopter at sea. - bool must_despawn_passengers = false; { - if (is_land_transport (this)) - must_despawn_passengers = involuntary && (is->current_config.land_transport_rules & LTR_NO_ESCAPE); + int unsellable_flags = + ITF_Center_of_Empire | + ITF_50_Luxury_Output | + ITF_50_Tax_Output | + ITF_Reduces_Corruption | + ITF_Increases_Luxury_Trade | + ITF_Allows_City_Level_2 | + ITF_Allows_City_Level_3 | + ITF_Capitalization | + ITF_Allows_Water_Trade | + ITF_Allows_Air_Trade | + ITF_Increases_Shields_In_Water | + ITF_Increases_Food_In_Water | + ITF_Increases_Trade_In_Water; - else if (type->Unit_Class == UTC_Air && type->Transport_Capacity > 0) { // if "this" is a helicopter - Unit * container = get_unit_ptr (this->Body.Container_Unit); - bool on_carrier = container != NULL && p_bic_data->UnitTypes[container->Body.UnitTypeID].Unit_Class == UTC_Sea && - Unit_has_ability (container, __, UTA_Transports_Only_Aircraft); - bool passengers_could_survive = Tile_has_city (tile) || ! tile->vtable->m35_Check_Is_Water (tile); + // Only sell improvements that aren't contributing gold, even indirectly through e.g. happiness or boosting shield production + // for Wealth + bool sellable = + ((improv->Characteristics & (ITC_Small_Wonder | ITC_Wonder)) == 0) && + ((improv->ImprovementFlags & unsellable_flags) == 0) && + (improv->Happy_Faces_All <= 0) && (improv->Happy_Faces <= 0) && + (improv->Production <= 0); - // If no-defense-from-inside is off, passengers in helicopters will fight to defend their tile, so they wouldn't be left - // inside the helicopter when their tile is taken. If it's on, an occupied heli can be destroyed or captured by a land unit, - // and we must make sure to despawn the passengers in that case because they can't remain on the tile. Hence, - // no-defense-from-inside implies no-escape in almost all circumstances (nukes are the exception). - enum special_helicopter_rules special_rules = is->current_config.special_helicopter_rules; - if (((special_rules & SHR_ALLOW_ON_CARRIERS) && on_carrier) || (special_rules & (SHR_NO_DEFENSE_FROM_INSIDE | SHR_NO_ESCAPE))) - must_despawn_passengers = involuntary || ! passengers_could_survive; + if (sellable && patch_City_has_improvement (coi.city, __, n, 0)) { + int maint = patch_City_get_improvement_maintenance (coi.city, __, n); + if (maint > 0) + memoize ((not_above (31, maint) << 26) | (n << 13) | coi.city_id); + } } - } - - bool prev_always_despawn_passengers = is->always_despawn_passengers; - if (must_despawn_passengers) { - // If we've been called from do_capture_units, record the passengers in the list of units to be despawned after do_capture_units - // returns. We can't simply despawn the units now even by setting always_despawn_passengers because the despawning messes up an - // iterator over tile units inside do_capture_units. - if (ret_addr == DESPAWN_TO_DO_CAPTURE_UNITS_RETURN) { - FOR_UNITS_ON (uti, tile) - if (uti.unit->Body.Container_Unit == this->Body.ID) { - reserve (sizeof is->extra_capture_despawns[0], // item size - (void **)&is->extra_capture_despawns, // ptr to items - &is->extra_capture_despawns_capacity, // ptr to capacity - is->count_extra_capture_despawns); // count - is->extra_capture_despawns[is->count_extra_capture_despawns] = uti.unit; - is->count_extra_capture_despawns += 1; - } + // Sort the list of buildings so the highest maintenance ones come first + qsort (is->memo, is->memo_len, sizeof is->memo[0], compare_buildings_to_sell); - } else - is->always_despawn_passengers = true; + // Sell buildings until we can cover maintenance costs or until we run out of ones to sell + int count_sold = 0; + while ((improv_cost + unit_cost > treasury) && (count_sold < is->memo_len)) { + int improv_id = ((1<<13) - 1) & (is->memo[count_sold] >> 13), + city_id = ((1<<13) - 1) & is->memo[count_sold]; + City * city = get_city_ptr (city_id); + improv_cost -= patch_City_get_improvement_maintenance (city, __, improv_id); + City_sell_improvement (city, __, improv_id, false); + treasury = leader->Gold_Encoded + leader->Gold_Decrement; + count_sold++; } - Unit_despawn (this, __, civ_id_responsible, param_2, param_3, param_4, param_5, param_6, param_7); - - is->always_despawn_passengers = prev_always_despawn_passengers; - - change_unit_type_count (&leaders[owner_id], type_id, -1); -} - -bool __fastcall -patch_Unit_do_capture_units (Unit * this, int edx, int tile_x, int tile_y, int owner_civ_id) -{ - is->count_extra_capture_despawns = 0; - - bool tr = Unit_do_capture_units (this, __, tile_x, tile_y, owner_civ_id); - - // Here we rely on patch_Unit_despawn to remove despawned units from the list - while (is->count_extra_capture_despawns > 0) - patch_Unit_despawn (is->extra_capture_despawns[0], __, this->Body.ID, 0, 0, 0, 0, 0, 0); + // Show popup informing the player that their buildings were force sold + if ((leader->ID == p_main_screen_form->Player_CivID) && ! is_online_game ()) { + PopupForm * popup = get_popup_form (); + if (count_sold == 1) { + int improv_id = ((1<<13) - 1) & (is->memo[0] >> 13), + city_id = ((1<<13) - 1) & is->memo[0]; + set_popup_str_param (0, get_city_ptr (city_id)->Body.CityName , -1, -1); + set_popup_str_param (1, p_bic_data->Improvements[improv_id].Name.S, -1, -1); + popup->vtable->set_text_key_and_flags (popup, __, is->mod_script_path, "C3X_FORCE_SOLD_SINGLE_IMPROV", -1, 0, 0, 0); + patch_show_popup (popup, __, 0, 0); + } else if (count_sold > 1) { + set_popup_int_param (0, count_sold); + popup->vtable->set_text_key_and_flags (popup, __, is->mod_script_path, "C3X_FORCE_SOLD_MULTIPLE_IMPROVS", -1, 0, 0, 0); - return tr; -} + // Add list of sold improvements to popup + for (int n = 0; n < count_sold; n++) { + int improv_id = ((1<<13) - 1) & (is->memo[n] >> 13), + city_id = ((1<<13) - 1) & is->memo[n]; + char s[200]; + snprintf (s, sizeof s, "^ %s in %s", + p_bic_data->Improvements[improv_id].Name.S, + get_city_ptr (city_id)->Body.CityName); + s[(sizeof s) - 1] = '\0'; + PopupForm_add_text (popup, __, s, 0); + } -void __fastcall -patch_Map_Renderer_m71_Draw_Tiles (Map_Renderer * this, int edx, int param_1, int param_2, int param_3) -{ - // Restore the tile count if it was saved by recompute_resources. This is necessary because the Draw_Tiles method loops over all tiles. - if (is->saved_tile_count >= 0) { - p_bic_data->Map.TileCount = is->saved_tile_count; - is->saved_tile_count = -1; + patch_show_popup (popup, __, 0, 0); + } } - Map_Renderer_m71_Draw_Tiles (this, __, param_1, param_2, param_3); + return improv_cost; } -void __fastcall -patch_Main_Screen_Form_draw_city_hud (Main_Screen_Form * this, int edx, PCX_Image * canvas) +// Returns the final unit cost after disbanding +int +disband_unaffordable_units (Leader * leader, int improv_cost, int unit_cost, int cost_per_unit) { - Main_Screen_Form_draw_city_hud (this, __, canvas); - - if (!is->current_config.enable_natural_wonders || - !is->current_config.show_natural_wonder_name_on_map) - return; - - if (canvas == NULL) - canvas = &this->Base_Data.Canvas; - - if ((canvas == NULL) || (canvas->JGL.Image == NULL)) - return; - - FOR_TABLE_ENTRIES (tei, &is->district_tile_map) { - struct district_instance * inst = (struct district_instance *)(long)tei.value; - if ((inst == NULL) || (inst->district_type != NATURAL_WONDER_DISTRICT_ID)) - continue; - - int natural_id = inst->natural_wonder_info.natural_wonder_id; - if ((natural_id < 0) || (natural_id >= is->natural_wonder_count)) - continue; - - struct natural_wonder_district_config const * nw_cfg = &is->natural_wonder_configs[natural_id]; - if ((nw_cfg == NULL) || (nw_cfg->name == NULL) || (nw_cfg->name[0] == '\0')) - continue; - - Tile * tile = (Tile *)(long)tei.key; - if ((tile == NULL) || (tile == p_null_tile)) - continue; - - int tile_x, tile_y; - if (!tile_coords_from_ptr (&p_bic_data->Map, tile, &tile_x, &tile_y)) - continue; - - int screen_x, screen_y; - Main_Screen_Form_tile_to_screen_coords (this, __, tile_x, tile_y, &screen_x, &screen_y); - - int is_zoomed_out = (p_bic_data->is_zoomed_out != false); - int scale = is_zoomed_out ? 2 : 1; - int screen_width = 128 / scale; - int screen_height = 88 / scale; - int text_width = screen_width - (is_zoomed_out ? 4 : 8); - if (text_width < 12) - text_width = screen_width; - - int text_left = screen_x + (screen_width - text_width) / 2; - int y_offset = 88 - 64; - int draw_y = screen_y - y_offset; - int text_top = draw_y + screen_height + (is_zoomed_out ? 2 : 4); + int treasury = leader->Gold_Encoded + leader->Gold_Decrement; + int count_disbanded = 0; + Unit * to_disband; + char first_disbanded_name[32]; + while ((improv_cost + unit_cost > treasury) && + (unit_cost > 0) && + (NULL != (to_disband = leader->vtable->find_unsupported_unit (leader)))) { + if (count_disbanded == 0) { + char const * name_src = (to_disband->Body.Custom_Name.S[0] == '\0') + ? p_bic_data->UnitTypes[to_disband->Body.UnitTypeID].Name + : to_disband->Body.Custom_Name.S; + strncpy (first_disbanded_name, name_src, sizeof first_disbanded_name); + first_disbanded_name[(sizeof first_disbanded_name) - 1] = '\0'; + } + Unit_disband (to_disband); + count_disbanded++; + unit_cost -= cost_per_unit; + } - Object_66C3FC * font = get_font (10, FSF_NONE); - if (font != NULL) { - PCX_Image_set_text_effects (canvas, __, 0x80FFFFFF, 0x80000000, 1, 1); - PCX_Image_draw_centered_text (canvas, __, font, (char *)nw_cfg->name, text_left, text_top - 10, text_width, strlen (nw_cfg->name)); + // Show popup informing the player that their units were disbanded + if (leader->ID == p_main_screen_form->Player_CivID) { + PopupForm * popup = get_popup_form (); + if (count_disbanded == 1) { + set_popup_str_param (0, first_disbanded_name, -1, -1); + int online_flag = is_online_game () ? 0x4000 : 0; + popup->vtable->set_text_key_and_flags (popup, __, is->mod_script_path, "C3X_FORCE_DISBANDED_SINGLE_UNIT", -1, 0, online_flag, 0); + patch_show_popup (popup, __, 0, 0); + } else if ((count_disbanded > 1) && ! is_online_game ()) { + set_popup_int_param (0, count_disbanded); + popup->vtable->set_text_key_and_flags (popup, __, is->mod_script_path, "C3X_FORCE_DISBANDED_MULTIPLE_UNITS", -1, 0, 0, 0); + patch_show_popup (popup, __, 0, 0); } } + + return unit_cost; } -// Returns whether or not city has an "extra palace", a concept used by the AI multi-city start. Extra palaces are small wonders that reduce -// corruption (e.g. forbidden palace) that are listed under the ai_multi_start_extra_palaces config option. -bool -has_extra_palace (City * city) +int +compare_cities_by_production (void const * vp_id_a, void const * vp_id_b) { - for (int n = 0; n < is->current_config.count_ai_multi_start_extra_palaces; n++) { - int improv_id = is->current_config.ai_multi_start_extra_palaces[n]; - Improvement * improv = &p_bic_data->Improvements[improv_id]; - if ((improv->Characteristics & ITC_Small_Wonder) && - (improv->SmallWonderFlags & ITSW_Reduces_Corruption) && - (leaders[city->Body.CivID].Small_Wonders[improv_id] == city->Body.ID)) { - return true; - } - } - return false; + City * a = get_city_ptr (*(int const *)vp_id_a), + * b = get_city_ptr (*(int const *)vp_id_b); + return a->Body.ProductionIncome - b->Body.ProductionIncome; } -// Removes any extra palaces from this city to other cities owned by the same player (if possible). Does not check that the city belongs to an AI or -// that AI MCS is enabled. void -remove_extra_palaces (City * city, City * excluded_destination) +charge_maintenance_with_aggressive_penalties (Leader * leader) { - Leader * leader = &leaders[city->Body.CivID]; - int extra_palace_lost; - do { - // Search for an extra palace in the city and delete it. Remember its ID so we can put it elsewhere. - extra_palace_lost = -1; - for (int n = 0; n < is->current_config.count_ai_multi_start_extra_palaces; n++) { - int improv_id = is->current_config.ai_multi_start_extra_palaces[n]; - Improvement * improv = &p_bic_data->Improvements[improv_id]; - if ((improv->Characteristics & ITC_Small_Wonder) && - (improv->SmallWonderFlags & ITSW_Reduces_Corruption) && - (leader->Small_Wonders[improv_id] == city->Body.ID)) { - patch_City_add_or_remove_improvement (city, __, improv_id, 0, false); - extra_palace_lost = improv_id; - break; - } - } - - // Replace the lost extra palace like what happens to the real palace - if (extra_palace_lost >= 0) { - int best_rating = -1; - City * best_location = NULL; - FOR_CITIES_OF (coi, leader->ID) { - City * candidate = coi.city; - if ((candidate != city) && - (candidate->Body.ID != leader->CapitalID) && - (candidate != excluded_destination) && - ! has_extra_palace (candidate)) { + int cost_per_unit; + get_unit_support_info (leader->ID, leader->GovernmentType, &cost_per_unit, NULL); - // Rate this candidate as a possible (extra) palace location. The criteria we use to rate it are identical to - // what the base game uses to find a new location for the palace. - int rating = 0; - rating += candidate->Body.Population.Size; - rating += count_units_at (candidate->Body.X, candidate->Body.Y, UF_4, -1, 0, -1); - rating += 2 * City_count_citizens_of_race (candidate, __, leader->RaceID); - FOR_TILES_AROUND (tai, 0x121, candidate->Body.X, candidate->Body.Y) { - if (tai.n == 0) - continue; - City * neighbor = get_city_ptr (tai.tile->CityID); - if ((neighbor != NULL) && (neighbor != city) && (neighbor->Body.CivID == leader->ID)) { - int size = neighbor->Body.Population.Size; - if (size > p_bic_data->General.MaximumSize_City) rating += 3; - else if (size > p_bic_data->General.MaximumSize_Town) rating += 2; - else rating += 1; - } - } + int improv_cost = Leader_sum_improvements_maintenance (leader, __, leader->GovernmentType); - if (rating > best_rating) { - best_rating = rating; - best_location = candidate; - } - } + int unit_cost = 0; { + if (leader->Cities_Count > 0) // Players with no cities don't pay unit maintenance, per the original game rules + if (cost_per_unit > 0) { + int count_free_units = Leader_get_free_unit_count (leader, __, leader->GovernmentType) + patch_Leader_count_maintenance_free_units (leader); + unit_cost += not_below (0, (leader->Unit_Count - count_free_units) * cost_per_unit); } + } - if (best_location != NULL) - City_add_or_remove_improvement (best_location, __, extra_palace_lost, 1, false); - } - } while (extra_palace_lost >= 0); -} + int treasury = leader->Gold_Encoded + leader->Gold_Decrement; + if (improv_cost + unit_cost > treasury) { -void -handle_possible_duplicate_small_wonders(City * city, Leader * leader) -{ - if ((city == NULL) || (leader == NULL)) - return; + // For AIs, alternate between selling buildings and disbanding units when maintenance can't be paid + if (((1<ID & *p_human_player_bits) != 0) || ((leader->ID + *p_current_turn_no) % 2 == 0)) { + improv_cost = sell_unaffordable_buildings (leader, improv_cost, unit_cost); + unit_cost = disband_unaffordable_units (leader, improv_cost, unit_cost, cost_per_unit); + } else { + unit_cost = disband_unaffordable_units (leader, improv_cost, unit_cost, cost_per_unit); + improv_cost = sell_unaffordable_buildings (leader, improv_cost, unit_cost); + } - for (int n = 0; n < is->workable_tile_count; n++) { - int dx, dy; - patch_ni_to_diff_for_work_area (n, &dx, &dy); - int x = city->Body.X + dx, y = city->Body.Y + dy; - wrap_tile_coords (&p_bic_data->Map, &x, &y); - Tile * tile = tile_at (x, y); + treasury = leader->Gold_Encoded + leader->Gold_Decrement; // Update b/c selling buildings recovers some gold - if ((tile == NULL) || (tile == p_null_tile)) continue; + // If the player still can't afford maintenance, even after all that, start switching their cities to Wealth + int wealth_income = 0; + if (improv_cost + unit_cost > treasury + wealth_income) { + // Memoize all cities not already building wealth and sort by production (lowest first) + clear_memo (); + FOR_CITIES_OF (coi, leader->ID) + if ((coi.city->Body.Status & CSF_Capitalization) == 0) + memoize (coi.city_id); + qsort (is->memo, is->memo_len, sizeof is->memo[0], compare_cities_by_production); - // Make sure it's a completed wonder district - struct district_instance * inst = get_district_instance (tile); - if ((inst == NULL) || (inst->district_type != WONDER_DISTRICT_ID)) continue; - if (! district_is_complete (tile, WONDER_DISTRICT_ID)) continue; - struct wonder_district_info * info = &inst->wonder_info; - if ((info == NULL) || (info->state != WDS_COMPLETED)) continue; + int wealth_improv_id = -1; { + for (int n = 0; n < p_bic_data->ImprovementsCount; n++) + if (p_bic_data->Improvements[n].ImprovementFlags & ITF_Capitalization) { + wealth_improv_id = n; + break; + } + } - int improv_id = get_wonder_improvement_id_from_index (info->wonder_index); - if (improv_id < 0) continue; + if (wealth_improv_id >= 0) { + int n = 0, + switched_any = 0; - Improvement * improv = &p_bic_data->Improvements[improv_id]; + while ((n < is->memo_len) && (improv_cost + unit_cost > treasury + wealth_income)) { + City * city = get_city_ptr (is->memo[n]); + City_set_production (city, __, COT_Improvement, wealth_improv_id, false); + switched_any = 1; + wealth_income += City_get_income_from_wealth_build (city); + n++; + } - // Only check Small Wonders, which shouldn't be duplicated in a civ - if ((improv->Characteristics & ITC_Small_Wonder) != 0) { - City * owning_city = get_city_ptr (leader->Small_Wonders[improv_id]); - if (owning_city != NULL) { - // Another city of the conquering civ has this Small Wonder, so remove it from captured city and destroy the district. - // We run the district removal manually here rather than through "handle_district_removed", as that function removes Wonders - // using leader->Small_Wonders, which would unset the Small Wonder from the original owning city, which we don't want, - // and would only remove the Wonder if completed_wonder_districts_can_be_destroyed is true, which may not be the case - patch_City_add_or_remove_improvement (city, __, improv_id, 0, false); - remove_district_instance (tile); - tile->vtable->m51_Unset_Tile_Flags (tile, __, 0, TILE_FLAG_MINE, x, y); - tile->vtable->m60_Set_Ruins (tile, __, 1); + if (switched_any && (leader->ID == p_main_screen_form->Player_CivID)) { + PopupForm * popup = get_popup_form (); + Improvement * wealth = &p_bic_data->Improvements[wealth_improv_id]; + set_popup_str_param (0, wealth->Name.S, -1, -1); + set_popup_str_param (1, wealth->CivilopediaEntry.S, -1, -1); + popup->vtable->set_text_key_and_flags (popup, __, is->mod_script_path, "C3X_FORCE_BUILD_WEALTH", -1, 0, 0, 0); + patch_show_popup (popup, __, 0, 0); + } } } } + + Leader_set_treasury (leader, __, treasury - improv_cost - unit_cost); } -void -grant_nearby_wonders_to_city (City * city) +bool __fastcall +patch_Unit_has_king_ability_for_find_unsupported (Unit * this, int edx, enum UnitTypeAbilities king_ability) { - // Give a city any completed wonder districts in work radius, if cities_with_mutual_district_receive_wonders is true. - // This is essentially for cases where the original Wonder-constructing city is destroyed and a new city is built - // that can work the same wonder district tile. - if (! is->current_config.enable_districts || - ! is->current_config.enable_wonder_districts || - ! is->current_config.cities_with_mutual_district_receive_wonders || - (city == NULL)) - return; - - for (int n = 0; n < is->workable_tile_count; n++) { - int dx, dy; - patch_ni_to_diff_for_work_area (n, &dx, &dy); - int x = city->Body.X + dx, y = city->Body.Y + dy; - wrap_tile_coords (&p_bic_data->Map, &x, &y); - Tile * tile = tile_at (x, y); - - // Check if Wonder tile owned by same civ - if ((tile == NULL) || (tile == p_null_tile)) continue; - if (tile->vtable->m38_Get_Territory_OwnerID (tile) != city->Body.CivID) continue; + // If we're set to aggressively penalize bankruptcy and this unit doesn't require support, return that it is a king so it doesn't get + // disbanded. + if (is->current_config.aggressively_penalize_bankruptcy && ! p_bic_data->UnitTypes[this->Body.UnitTypeID].requires_support) + return true; - // Make sure Wonder is completed - struct district_instance * inst = get_district_instance (tile); - if ((inst == NULL) || (inst->district_type != WONDER_DISTRICT_ID)) continue; - if (! district_is_complete (tile, WONDER_DISTRICT_ID)) continue; - struct wonder_district_info * info = &inst->wonder_info; - if ((info == NULL) || (info->state != WDS_COMPLETED)) continue; + else + return Unit_has_ability (this, __, king_ability); +} - // Check that city doesn't already have the Wonder - int improv_id = get_wonder_improvement_id_from_index (info->wonder_index); - if (improv_id < 0) continue; +void __fastcall +patch_Leader_pay_unit_maintenance (Leader * this) +{ + if (! is->current_config.aggressively_penalize_bankruptcy) + Leader_pay_unit_maintenance (this); + else + charge_maintenance_with_aggressive_penalties (this); +} - if (patch_City_has_improvement (city, __, improv_id, false)) continue; +void __fastcall +patch_Main_Screen_Form_show_wltk_message (Main_Screen_Form * this, int edx, int tile_x, int tile_y, char * text_key, bool pause) +{ + patch_Main_Screen_Form_show_map_message (this, __, tile_x, tile_y, text_key, is->current_config.dont_pause_for_love_the_king_messages ? false : pause); +} - // Add the Wonder to the city - patch_City_add_or_remove_improvement (city, __, improv_id, 1, false); - } +void __fastcall +patch_Main_Screen_Form_show_wltk_ended_message (Main_Screen_Form * this, int edx, int tile_x, int tile_y, char * text_key, bool pause) +{ + patch_Main_Screen_Form_show_map_message (this, __, tile_x, tile_y, text_key, is->current_config.dont_pause_for_love_the_king_messages ? false : pause); } -void -on_gain_city (Leader * leader, City * city, enum city_gain_reason reason) +char __fastcall +patch_Tile_has_city_for_agri_penalty_exception (Tile * this) { - if (reason == CGR_FOUNDED) - is->turn_no_of_last_founding_for_settler_perfume[leader->ID] = *p_current_turn_no; + return is->current_config.no_penalty_exception_for_agri_fresh_water_city_tiles ? 0 : Tile_has_city (this); +} - // Handle extra palaces for AI multi-city start - if (((*p_human_player_bits & (1<ID)) == 0) && // If leader is an AI AND - (is->current_config.ai_multi_city_start > 1) && // AI multi-city start is enabled AND - (leader->Cities_Count > 1) && // city is not the only one the AI has (i.e. it's not the capital) AND - (reason != CGR_PLACED_FOR_SCENARIO) && (reason != CGR_PLACED_FOR_AI_MULTI_CITY_START)) { // city was not placed before the game started +int +show_razing_popup (void * popup_object, int popup_param_1, int popup_param_2, int razing_option) +{ + int response = patch_show_popup (popup_object, __, popup_param_1, popup_param_2); + if (is->current_config.prevent_razing_by_players && (response == razing_option)) { + PopupForm * popup = get_popup_form (); + popup->vtable->set_text_key_and_flags (popup, __, is->mod_script_path, "C3X_CANT_RAZE", -1, 0, 0, 0); + patch_show_popup (popup, __, 0, 0); + return 0; + } + return response; +} - // Find an extra palace that this player does not already have - int free_extra_palace = -1; - for (int n = 0; n < is->current_config.count_ai_multi_start_extra_palaces; n++) { - int improv_id = is->current_config.ai_multi_start_extra_palaces[n]; - Improvement * improv = &p_bic_data->Improvements[improv_id]; - if ((improv->Characteristics & ITC_Small_Wonder) && - (improv->SmallWonderFlags & ITSW_Reduces_Corruption) && - (NULL == get_city_ptr (leader->Small_Wonders[improv_id]))) { - free_extra_palace = improv_id; - break; - } - } +int __fastcall patch_show_popup_option_1_razes (void *this, int edx, int param_1, int param_2) { return show_razing_popup (this, param_1, param_2, 1); } +int __fastcall patch_show_popup_option_2_razes (void *this, int edx, int param_1, int param_2) { return show_razing_popup (this, param_1, param_2, 2); } - // Place that extra palace here - if (free_extra_palace >= 0) - City_add_or_remove_improvement (city, __, free_extra_palace, 1, false); - } +int __fastcall +patch_Context_Menu_add_abandon_city (Context_Menu * this, int edx, int item_id, char * text, bool checkbox, Sprite * image) +{ + if (is->current_config.prevent_razing_by_players) + return 0; // Return value is ignored by the caller + else + return Context_Menu_add_item (this, __, item_id, text, checkbox, image); +} - if (is->current_config.enable_districts || is->current_config.enable_natural_wonders) { - patch_City_recompute_yields_and_happiness (city); - patch_City_recompute_culture_income (city); - } +char * +check_pedia_upgrades_to_ptr (TextBuffer * this, char * str) +{ + Civilopedia_Form * pedia = p_civilopedia_form; + UnitType * unit_type = NULL; + if (is->current_config.indicate_non_upgradability_in_pedia && + (pedia->Current_Article_ID >= 0) && (pedia->Current_Article_ID <= pedia->Max_Article_ID) && + (NULL != (unit_type = pedia->Articles[pedia->Current_Article_ID]->unit_type)) && + ((unit_type->Special_Actions & UCV_Upgrade_Unit) == 0)) + return is->c3x_labels[CL_OBSOLETED_BY]; + else + return TextBuffer_check_ptr (this, __, str); +} - if (is->current_config.enable_districts) { +char * __fastcall patch_TextBuffer_check_pedia_upgrades_to_ptr_1 (TextBuffer * this, int edx, char * str) { return check_pedia_upgrades_to_ptr (this, str); } +char * __fastcall patch_TextBuffer_check_pedia_upgrades_to_ptr_2 (TextBuffer * this, int edx, char * str) { return check_pedia_upgrades_to_ptr (this, str); } - // Remove any district previously on the tile, if city just founded - if (reason == CGR_FOUNDED) { - Tile * city_tile = tile_at (city->Body.X, city->Body.Y); - if ((city_tile != NULL) && (city_tile != p_null_tile)) { - struct district_instance * inst = get_district_instance (city_tile); - if (inst != NULL) - handle_district_removed (city_tile, inst->district_type, city->Body.X, city->Body.Y, false); - } - } +bool __fastcall +patch_Unit_select_stealth_attack_target (Unit * this, int edx, int target_civ_id, int x, int y, bool allow_popup, Unit ** out_selected_target) +{ + is->added_any_stealth_target = 0; + return Unit_select_stealth_attack_target (this, __, target_civ_id, x, y, allow_popup, out_selected_target); +} - bool receive_buildings = is->current_config.cities_with_mutual_district_receive_buildings; - bool receive_wonders = is->current_config.cities_with_mutual_district_receive_wonders; +bool __fastcall +patch_Unit_can_stealth_attack (Unit * this, int edx, Unit * target) +{ + bool tr = Unit_can_stealth_attack (this, __, target); - // Grant buildings and wonders from nearby completed districts owned by other cities of same civ, if enabled - if (receive_buildings) { - grant_existing_district_buildings_to_city (city); - } + // If we're selecting a target for stealth attack via bombardment, we must filter out candidates we can't damage that way + if (tr && is->selecting_stealth_target_for_bombard && + ! can_damage_bombarding (&p_bic_data->UnitTypes[this->Body.UnitTypeID], target, tile_at (target->Body.X, target->Body.Y))) + return false; - // Grant wonders nearby in same territory, regardless of city (i.e., orphaned wonders from destroyed cities), if enabled - if (receive_wonders) { - grant_nearby_wonders_to_city (city); - } + else if (tr && is->current_config.exclude_invisible_units_from_stealth_attack && ! patch_Unit_is_visible_to_civ (target, __, this->Body.CivID, 1)) + return false; - if (is->current_config.enable_distribution_hub_districts) { - refresh_distribution_hubs_for_city (city); - } - } + else + return tr; } -void -on_lose_city (Leader * leader, City * city, enum city_loss_reason reason) +int __fastcall +patch_Tile_check_water_for_stealth_attack (Tile * this) { - // If leader is an AI and AI MCS is enabled, remove any extra palaces the city has - if (((*p_human_player_bits & (1<ID)) == 0) && - (is->current_config.ai_multi_city_start > 1)) - remove_extra_palaces (city, NULL); - - if (is->current_config.enable_districts) { - if (is->current_config.enable_distribution_hub_districts) - is->distribution_hub_totals_dirty = true; + // When stealth attack bombard is enabled, remove a special rule inside can_stealth_attack that prevents land units from stealth attacking + // onto sea tiles. This allows land artillery to stealth attack naval units. + return is->current_config.enable_stealth_attack_via_bombardment ? 0 : this->vtable->m35_Check_Is_Water (this); +} - forget_pending_building_order (city); - for (int district_id = 0; district_id < is->district_count; district_id++) - clear_city_district_request (city, district_id); +int __fastcall +patch_PopupSelection_add_stealth_attack_target (PopupSelection * this, int edx, char * text, int value) +{ + if (is->current_config.include_stealth_attack_cancel_option && (! is->added_any_stealth_target)) { + PopupSelection_add_item (this, __, is->c3x_labels[CL_NO_STEALTH_ATTACK], -1); + is->added_any_stealth_target = 1; + } - if (is->current_config.enable_wonder_districts) - release_wonder_district_reservation (city); - } else if (is->current_config.enable_distribution_hub_districts) - is->distribution_hub_totals_dirty = true; + Unit * unit; + if (is->current_config.show_hp_of_stealth_attack_options && + ((unit = get_unit_ptr (value)) != NULL)) { + char s[500]; + int max_hp = Unit_get_max_hp (unit); + snprintf (s, sizeof s, "(%d/%d) %s", max_hp - unit->Body.Damage, max_hp, text); + s[(sizeof s) - 1] = '\0'; + return PopupSelection_add_item (this, __, s, value); + } else + return PopupSelection_add_item (this, __, text, value); } -// Returns -1 if the location is unusable, 0-9 if it's usable but doesn't satisfy all criteria, and 10 if it couldn't be better -int -eval_starting_location (Map * map, int const * alt_starting_locs, int alt_starting_loc_count, int tile_x, int tile_y, int civ_id) +void __fastcall +patch_Unit_perform_air_recon (Unit * this, int edx, int x, int y) { - Tile * tile = tile_at (tile_x, tile_y); - if ((tile != p_null_tile) && - (patch_Map_check_city_location (map, __, tile_x, tile_y, civ_id, true) == CLV_OK) && - (tile->vtable->m15_Check_Goody_Hut (tile, __, 0) == 0) && - (tile->vtable->m40_get_TileUnit_ID (tile) == -1)) { - int tr = 0; + int moves_plus_one = this->Body.Moves + p_bic_data->General.RoadsMovementRate; - // Avoid this location if it's too close to another starting location. If it's much too close, it's ruled out entirely. - int closest_dist = INT_MAX; - for (int n = 1; n <= map->Civ_Count; n++) - if (map->Starting_Locations[n] >= 0) { - int other_x, other_y; - tile_index_to_coords (map, map->Starting_Locations[n], &other_x, &other_y); - int dist = Map_get_x_dist (map, __, tile_x, other_x) + Map_get_y_dist (map, __, tile_y, other_y); - if (dist < closest_dist) - closest_dist = dist; - } - for (int n = 0; n < alt_starting_loc_count; n++) - if (alt_starting_locs[n] >= 0) { - int other_x, other_y; - tile_index_to_coords (map, alt_starting_locs[n], &other_x, &other_y); - int dist = Map_get_x_dist (map, __, tile_x, other_x) + Map_get_y_dist (map, __, tile_y, other_y); - if (dist < closest_dist) - closest_dist = dist; - } - if (closest_dist < map->Civ_Distance/3) - return -1; - else if (closest_dist >= 2*map->Civ_Distance/3) - tr += 1; + bool was_intercepted = false; + if (is->current_config.intercept_recon_missions) { + // Temporarily add vision on the target tile so the game plays the animation if the unit is show down by ground AA + Tile_Body * tile = &tile_at (x, y)->Body; + int saved_vis = tile->Visibility; + tile->Visibility |= 1 << this->Body.CivID; + was_intercepted = Unit_try_flying_over_tile (this, __, x, y); + tile->Visibility = saved_vis; + } - // Avoid tiny islands - // tr += map->vtable->m33_Get_Continent (map, __, tile->ContinentID)->Body.TileCount >= 20; + if (! was_intercepted) { + Unit_perform_air_recon (this, __, x, y); + if (is->current_config.charge_one_move_for_recon_and_interception) + this->Body.Moves = moves_plus_one; + } +} - // Avoid garbage terrain, e.g. all desert or tundra - int break_even_food_tiles = 0; - FOR_TILES_AROUND (tai, 21, tile_x, tile_y) { - if (tai.n == 0) - continue; // Skip tile that would be covered by the city - int x, y; - tai_get_coords (&tai, &x, &y); - int tile_food = patch_Map_calc_food_yield_at (map, __, x, y, tai.tile->vtable->m50_Get_Square_BaseType (tai.tile), civ_id, 0, NULL); - int food_required = p_bic_data->General.FoodPerCitizen + (tai.tile->vtable->m35_Check_Is_Water (tai.tile) ? 1 : 0); - break_even_food_tiles += tile_food >= food_required; - } - tr += break_even_food_tiles >= 2; +int __fastcall +patch_Unit_get_interceptor_max_moves (Unit * this) +{ + // Stop fighters from intercepting multiple times per turn without blitz + if (is->current_config.charge_one_move_for_recon_and_interception && + (this->Body.Status & USF_USED_ATTACK != 0) && ! UnitType_has_ability (&p_bic_data->UnitTypes[this->Body.UnitTypeID], __, UTA_Blitz)) + return 0; - // Avoid wasting a food bonus - tr += (tile->ResourceType < 0) || (tile->ResourceType >= p_bic_data->ResourceTypeCount) || - (p_bic_data->ResourceTypes[tile->ResourceType].Food == 0); + else + return patch_Unit_get_max_move_points (this); +} - int max_score = 3; - return (10*tr)/max_score; +int __fastcall +patch_Unit_get_moves_after_interception (Unit * this) +{ + if (is->current_config.charge_one_move_for_recon_and_interception) { + this->Body.Status |= USF_USED_ATTACK; // Set status bit indicating that the interceptor has attacked this turn + return this->Body.Moves + p_bic_data->General.RoadsMovementRate; } else - return -1; + return patch_Unit_get_max_move_points (this); } -City * -create_starter_city (Map * map, int civ_id, int tile_index) +void __fastcall +patch_Unit_set_state_after_interception (Unit * this, int edx, int new_state) { - int x, y; - tile_index_to_coords (map, tile_index, &x, &y); - City * tr = Leader_create_city (&leaders[civ_id], __, x, y, leaders[civ_id].RaceID, -1, NULL, true); - if (tr != NULL) - on_gain_city (&leaders[civ_id], tr, CGR_PLACED_FOR_AI_MULTI_CITY_START); - return tr; + if (! is->current_config.charge_one_move_for_recon_and_interception) + Unit_set_state (this, __, new_state); + + // If fighters are supposed to be able to intercept multiple times per turn, then we can't knock them out of the interception state as soon as + // they intercept something like in the base game. Instead, record this interception event so that we can clear their state at the start of + // their next turn. + else { + struct interceptor_reset_list * irl = &is->interceptor_reset_lists[this->Body.CivID]; + reserve (sizeof irl->items[0], (void **)&irl->items, &irl->capacity, irl->count); + irl->items[irl->count++] = (struct interception) { + .unit_id = this->Body.ID, + .x = this->Body.X, + .y = this->Body.Y + }; + } } +// Goes through every entry in a table with unit IDs as keys and filters out any that belong to the given owner (or are invalid). void -set_up_ai_multi_city_start (Map * map, int city_count) +remove_unit_id_entries_owned_by (struct table * t, int owner_id) { - // Set of bits determining which players are eligible for the two-city start - int eligibility_bits = 0, - count_eligible_civs = 0; - for (int n = 1; n < 32; n++) - if ((*p_player_bits & 1<Starting_Locations[n]) != p_null_tile)) { // has a valid starting location - eligibility_bits |= 1 << n; - count_eligible_civs++; + if (t->len > 0) { + clear_memo (); + FOR_TABLE_ENTRIES (tei, t) { + Unit * unit = get_unit_ptr (tei.key); + if ((unit == NULL) || (unit->Body.CivID == owner_id)) + memoize (tei.key); } + for (int n = 0; n < is->memo_len; n++) + itable_remove (t, is->memo[n]); + } +} - if ((city_count < 1) || (count_eligible_civs == 0)) // if we have nothing to do - return; - - char load_text[50]; - snprintf (load_text, sizeof load_text, "%s...", is->c3x_labels[CL_CREATING_CITIES]); - load_text[(sizeof load_text) - 1] = '\0'; - Main_GUI_label_loading_bar (&p_main_screen_form->GUI, __, 1, load_text); - - // Generate alternate sets of starting locations. We need one set for each extra city we're going to create per player. Each set only needs to - // include starting locations for eligible players, all others will be left as -1. - int alt_starting_loc_count = 32 * (city_count - 1); - int * alt_starting_locs = malloc (alt_starting_loc_count * sizeof *alt_starting_locs); { - for (int n = 0; n < alt_starting_loc_count; n++) - alt_starting_locs[n] = -1; - - for (int i_loc = 0; i_loc < alt_starting_loc_count; i_loc++) { - int civ_id = i_loc % 32; - if ((civ_id != 0) && (eligibility_bits & 1<current_config.max_tries_to_place_fp_city; try++) { - int i_loc = rand_int (p_rand_object, __, map->TileCount); - int x_loc, y_loc; - tile_index_to_coords (map, i_loc, &x_loc, &y_loc); - if ((x_loc >= 0) && (y_loc >= 0)) { - int val = eval_starting_location (map, alt_starting_locs, alt_starting_loc_count, x_loc, y_loc, civ_id); - if (val >= 10) { - best_loc_index = i_loc; - break; - } else if (val > best_loc_val) { - best_loc_val = val; - best_loc_index = i_loc; - } - } - } - - if (best_loc_index >= 0) - alt_starting_locs[i_loc] = best_loc_index; +void __fastcall +patch_Leader_begin_turn (Leader * this) +{ + if (is->aerodrome_airlift_usage.len > 0) { + int civ_bit = 1 << this->ID; + clear_memo (); + FOR_TABLE_ENTRIES (tei, &is->aerodrome_airlift_usage) { + int mask = tei.value; + if (mask & civ_bit) { + int new_mask = mask & ~civ_bit; + memoize (tei.key); + memoize (new_mask); } } + for (int n = 0; n < is->memo_len; n += 2) { + int key = is->memo[n]; + int new_mask = is->memo[n + 1]; + if (new_mask == 0) + itable_remove (&is->aerodrome_airlift_usage, key); + else + itable_insert (&is->aerodrome_airlift_usage, key, new_mask); + } + clear_memo (); } - int count_cities_created = 0; - int count_eligible_civs_handled = 0; - for (int civ_id = 1; civ_id < 32; civ_id++) - if (eligibility_bits & 1<Starting_Locations[civ_id]; - - // Create the first starting city for the AI. This one is its capital and is located at its actual starting - // location. Afterward, delete its starting settler so it's as if the settler founded the city. - { - Unit * starting_settler = NULL; - FOR_UNITS_ON (tai, tile_at_index (map, sloc)) { - if (p_bic_data->UnitTypes[tai.unit->Body.UnitTypeID].AI_Strategy & UTAI_Settle) { - starting_settler = tai.unit; - break; - } - } - - create_starter_city (map, civ_id, sloc); - count_cities_created++; - - if (starting_settler != NULL) - patch_Unit_despawn (starting_settler, __, 0, 1, 0, 0, 0, 0, 0); - - } - - // Memoize all of the AI's starting units - clear_memo (); - FOR_UNITS_ON (tai, tile_at_index (map, sloc)) - memoize ((int)tai.unit); - - int extra_city_count = 0; - for (int i_city = 1; i_city < city_count; i_city++) { - int loc = alt_starting_locs[(i_city-1)*32 + civ_id]; - City * city; - if ((loc >= 0) && - (NULL != (city = create_starter_city (map, civ_id, loc)))) { - count_cities_created++; - extra_city_count++; - - // Spawn palace substitute in new city - if (extra_city_count-1 < is->current_config.count_ai_multi_start_extra_palaces) - patch_City_add_or_remove_improvement (city, __, is->current_config.ai_multi_start_extra_palaces[extra_city_count - 1], 1, true); - - // Move starting units over to the new city. Do this by moving every Nth unit where N is the number of extra - // cities we've founded so far plus one. ("Extra" cities are the ones created after the capital.) E.g., if - // this is the 1st city after the capital, move every 2nd unit to it. If it's the 2nd, move every 3rd, etc. - for (int n = 0; n < is->memo_len; n++) - if (n % (extra_city_count+1) == extra_city_count) - patch_Unit_move ((Unit *)is->memo[n], __, city->Body.X, city->Body.Y); + // Eject trespassers + is->do_not_bounce_invisible_units = true; + if (is->current_config.disallow_trespassing && (*p_current_turn_no > 0)) + for (int n = 1; n < 32; n++) + if ((*p_player_bits & (1 << n)) && + (n != this->ID) && + (! this->At_War[n]) && + ((this->Relation_Treaties[n] & 2) == 0)) // Check right of passage + Leader_bounce_trespassing_units (&leaders[n], __, this->ID); + is->do_not_bounce_invisible_units = false; - } - } + if (is->current_config.introduce_all_human_players_at_start_of_hotseat_game && + (*p_current_turn_no == 0) && + (*p_is_offline_mp_game && ! *p_is_pbem_game) && // is hotseat game + ((*p_human_player_bits & (1 << this->ID)) != 0)) + for (int n = 0; n < 32; n++) + if (*p_human_player_bits & (1 << n)) + Leader_make_contact (this, __, n, false); - // Update progress report - count_eligible_civs_handled++; - int progress = 100 * count_eligible_civs_handled / count_eligible_civs; - snprintf (load_text, sizeof load_text, "%s %d%%", is->c3x_labels[CL_CREATING_CITIES], progress); - load_text[(sizeof load_text) - 1] = '\0'; - Main_GUI_label_loading_bar (&p_main_screen_form->GUI, __, 1, load_text); + Leader_begin_turn (this); +} - } +void __fastcall +patch_Leader_begin_unit_turns (Leader * this) +{ + // Reset the states of all fighters that performed an interception on the previous turn. + struct interceptor_reset_list * irl = &is->interceptor_reset_lists[this->ID]; + for (int n = 0; n < irl->count; n++) { + struct interception * record = &irl->items[n]; + Unit * interceptor = get_unit_ptr (record->unit_id); + if ((interceptor != NULL) && + (interceptor->Body.CivID == this->ID) && + (interceptor->Body.X == record->x) && + (interceptor->Body.Y == record->y) && + (interceptor->Body.UnitState == UnitState_Intercept)) + Unit_set_state (interceptor, __, 0); + } + irl->count = 0; - free (alt_starting_locs); + // Reset extra defensive bombard and airdrop counters + remove_unit_id_entries_owned_by (&is->extra_defensive_bombards, this->ID); + remove_unit_id_entries_owned_by (&is->airdrops_this_turn , this->ID); + remove_unit_id_entries_owned_by (&is->unit_transport_ties , this->ID); - // Sanity check - int any_adjacent_cities = 0; { - if (p_cities->Cities != NULL) - for (int city_index = 0; (city_index <= p_cities->LastIndex) && ! any_adjacent_cities; city_index++) { - City * city = get_city_ptr (city_index); - if (city != NULL) - FOR_TILES_AROUND (tai, 9, city->Body.X, city->Body.Y) - if ((tai.n > 0) && (NULL != get_city_ptr (tai.tile->vtable->m45_Get_City_ID (tai.tile)))) { - any_adjacent_cities = 1; - break; - } - } - } - int any_missing_fp_cities = (count_cities_created < city_count * count_eligible_civs); - if (any_adjacent_cities || any_missing_fp_cities) { - PopupForm * popup = get_popup_form (); - popup->vtable->set_text_key_and_flags (popup, __, is->mod_script_path, "C3X_WARNING", -1, 0, 0, 0); - char s[100]; - snprintf (s, sizeof s, "^%s", is->c3x_labels[CL_MCS_FAILED_SANITY_CHECK]); - s[(sizeof s) - 1] = '\0'; - PopupForm_add_text (popup, __, s, 0); - if (any_adjacent_cities) { - snprintf (s, sizeof s, "^%s", is->c3x_labels[CL_MCS_ADJACENT_CITIES]); - s[(sizeof s) - 1] = '\0'; - PopupForm_add_text (popup, __, s, 0); - } - if (any_missing_fp_cities) { - snprintf (s, sizeof s, "^%s", is->c3x_labels[CL_MCS_MISSING_CITIES]); - s[(sizeof s) - 1] = '\0'; - PopupForm_add_text (popup, __, s, 0); + clear_memo (); + if (is->current_config.delete_off_map_ai_units && + ((*p_human_player_bits & (1 << this->ID)) == 0) && + (p_units->Units != NULL)) + for (int n = 0; n <= p_units->LastIndex; n++) { + Unit_Body * body = p_units->Units[n].Unit; + if ((body != NULL) && + ((int)body != offsetof (Unit, Body)) && + (body->CivID == this->ID) && + ! Map_in_range (&p_bic_data->Map, __, body->X, body->Y)) + memoize (body->ID); } - patch_show_popup (popup, __, 0, 0); - } + for (int n = 0; n < is->memo_len; n++) + patch_Unit_despawn (get_unit_ptr (is->memo[n]), __, 0, 1, 0, 0, 0, 0, 0); + + Leader_begin_unit_turns (this); } -void __fastcall -patch_Map_process_after_placing (Map * this, int edx, bool param_1) +Unit * __fastcall +patch_Fighter_find_actual_bombard_defender (Fighter * this, int edx, Unit * bombarder, int tile_x, int tile_y, int bombarder_civ_id, bool land_lethal, bool sea_lethal) { - if ((is->current_config.ai_multi_city_start > 0) && (*p_current_turn_no == 0)) - set_up_ai_multi_city_start (this, is->current_config.ai_multi_city_start); + if (is->bombard_stealth_target == NULL) + return Fighter_find_defender_against_bombardment (this, __, bombarder, tile_x, tile_y, bombarder_civ_id, land_lethal, sea_lethal); + else + return is->bombard_stealth_target; +} - Map_process_after_placing (this, __, param_1); +Unit * +select_stealth_attack_bombard_target (Unit * unit, int tile_x, int tile_y) +{ + bool land_lethal = Unit_has_ability (unit, __, UTA_Lethal_Land_Bombardment), + sea_lethal = Unit_has_ability (unit, __, UTA_Lethal_Sea_Bombardment); + Unit * defender = Fighter_find_defender_against_bombardment (&p_bic_data->fighter, __, unit, tile_x, tile_y, unit->Body.CivID, land_lethal, sea_lethal); + if (defender != NULL) { + Unit * target; + is->selecting_stealth_target_for_bombard = 1; + bool got_one = patch_Unit_select_stealth_attack_target (unit, __, defender->Body.CivID, tile_x, tile_y, true, &target); + is->selecting_stealth_target_for_bombard = 0; + return got_one ? target : NULL; + } else + return NULL; } -void __fastcall -patch_Map_impl_generate (Map * this, int edx, int seed, bool is_multiplayer_game, int num_seafaring_civs) +bool __fastcall +patch_Unit_try_flying_for_precision_strike (Unit * this, int edx, int x, int y) { - Map_impl_generate (this, __, seed, is_multiplayer_game, num_seafaring_civs); + bool is_cruise_missile = UnitType_has_ability (&p_bic_data->UnitTypes[this->Body.UnitTypeID], __, UTA_Cruise_Missile); + if (is->current_config.polish_precision_striking && + (p_bic_data->UnitTypes[this->Body.UnitTypeID].Unit_Class != UTC_Air) && + ! is_cruise_missile) + // This method returns -1 when some kind of error occurs. In that case, return true implying the unit was shot down so the caller + // doesn't do anything more. Otherwise, return false so it goes ahead and applies damage. + return Unit_play_bombard_fire_animation (this, __, x, y) == -1; - if (is->current_config.enable_natural_wonders) - place_natural_wonders_on_map (); + else if (is->current_config.polish_precision_striking && is_cruise_missile) { + Unit_animate_cruise_missile_strike (this, __, x, y); + return false; + + } else + return Unit_try_flying_over_tile (this, __, x, y); } -int __fastcall -patch_City_get_net_commerce (City * this, int edx, int kind, bool include_science_age) +void __fastcall +patch_Unit_play_bombing_anim_for_precision_strike (Unit * this, int edx, int x, int y) { - int base = City_get_net_commerce (this, __, kind, include_science_age); - - if ((kind == 1) && // beakers, as opposed to 2 which is gold - (is->current_config.ai_research_multiplier != 100) && - ((*p_human_player_bits & 1<Body.CivID) == 0)) - return (base * is->current_config.ai_research_multiplier + 50) / 100; - else - return base; + // Only play the bombing animation here if we haven't already played an animation in the above method. We don't want to play all animations + // here since the bombard fire animation can fail for whatever reason but this method can't handle failure. + bool is_cruise_missile = UnitType_has_ability (&p_bic_data->UnitTypes[this->Body.UnitTypeID], __, UTA_Cruise_Missile); + if ((! is->current_config.polish_precision_striking) || + ((p_bic_data->UnitTypes[this->Body.UnitTypeID].Unit_Class == UTC_Air) && ! is_cruise_missile)) + Unit_play_bombing_animation (this, __, x, y); } -// Cuts research spending while total expenditures > treasury. Returns whether spending has been cut. -bool -cut_unaffordable_research_spending (Leader * leader, bool skip_popup) +int __fastcall +patch_Unit_play_anim_for_bombard_tile (Unit * this, int edx, int x, int y) { - // The rate cap limits AI players' spending on the gold slider (in Leader::ai_adjust_sliders) however it does not apply to human players when - // adjusting sliders manually on the domestic advisor screen. - int gold_rate_cap = ((*p_human_player_bits & 1<ID) != 0) ? 10 : p_bic_data->Governments[leader->GovernmentType].RateCap; + Unit * stealth_attack_target = NULL; + if (((p_bic_data->UnitTypes[this->Body.UnitTypeID].Special_Actions & UCV_Stealth_Attack) != 0) && + is->current_config.enable_stealth_attack_via_bombardment && + (! is_online_game ()) && + patch_Leader_is_tile_visible (&leaders[this->Body.CivID], __, x, y)) + is->bombard_stealth_target = select_stealth_attack_bombard_target (this, x, y); - int treasury = leader->Gold_Encoded + leader->Gold_Decrement; - bool reduced_spending = false; - while (leader->science_slider > 0 && - leader->gold_slider < gold_rate_cap && - treasury + Leader_compute_income (leader) < 0) { - leader->science_slider -= 1; - leader->gold_slider += 1; - Leader_recompute_economy (leader); - reduced_spending = true; - } - if (reduced_spending && ! skip_popup && leader->ID == p_main_screen_form->Player_CivID) { - PopupForm * popup = get_popup_form (); - popup->vtable->set_text_key_and_flags (popup, __, is->mod_script_path, "C3X_FORCE_CUT_RESEARCH_SPENDING", -1, 0, 0, 0); - patch_show_popup (popup, __, 0, 0); - } - return reduced_spending; + return Unit_play_bombard_fire_animation (this, __, x, y); } void __fastcall -adjust_sliders_preproduction (Leader * this) +patch_Main_Screen_Form_issue_precision_strike_cmd (Main_Screen_Form * this, int edx, Unit * unit) { - if ((*p_human_player_bits & 1<ID) == 0) { - // Replicate the behavior of the original code for AI players. (apply_machine_code_edits overwrites an equivalent branch & call in the - // original code with a call to this method.) - this->vtable->ai_adjust_sliders (this); - - // If aggressively penalize bankruptcy is on, cut the AI's research spending if it can't afford it. This may be redundant as - // ai_adjust_sliders will already cut AI spending but maybe not all the way to zero. Do this only every third turn so the AI is not - // completely prevented from researching. - if (is->current_config.aggressively_penalize_bankruptcy && (*p_current_turn_no + this->ID) % 3 == 0) - cut_unaffordable_research_spending (this, true); - - // If human player would go bankrupt, try reducing their research spending to avoid that - } else if (is->current_config.cut_research_spending_to_avoid_bankruptcy) - cut_unaffordable_research_spending (this, false); + UnitType * type = &p_bic_data->UnitTypes[unit->Body.UnitTypeID]; + if ((! is->current_config.polish_precision_striking) || (type->Unit_Class == UTC_Air)) + Main_Screen_Form_issue_precision_strike_cmd (this, __, unit); + else { + // issue_precision_strike_cmd will use the unit type's operational range. To make it use bombard range instead, place that value in + // the operational range field temporarily. Conveniently, it's only necessary to do this temporary switch once, here, because the main + // screen form stores a copy of the range for its own use and the method to actually perform the strike doesn't check the range. + int saved_op_range = type->OperationalRange; + type->OperationalRange = type->Bombard_Range; + Main_Screen_Form_issue_precision_strike_cmd (this, __, unit); + type->OperationalRange = saved_op_range; + } } int __fastcall -patch_City_get_improvement_maintenance (City * this, int edx, int improv_id) +patch_Map_compute_neighbor_index_for_cm_strike (Map * this, int edx, int x_home, int y_home, int x_neigh, int y_neigh, int lim) { - // Check if this improvment is provided for free by another player via shared wonder effects - int civ_id = this->Body.CivID; - bool free_from_sharing = false; - if (is->current_config.share_wonders_in_hotseat && - (*p_is_offline_mp_game && ! *p_is_pbem_game) && // is hotseat game - ((1 << civ_id) & *p_human_player_bits)) { // is this city owned by a human player + int tr = Map_compute_neighbor_index (this, __, x_home, y_home, x_neigh, y_neigh, lim); - // Check if any other human player in the game has this improv in their auto improvs table, including for this city's continent - bool has_free_improv = false; - Tile * city_tile = tile_at (this->Body.X, this->Body.Y); - int continent_id = city_tile->vtable->m46_Get_ContinentID (city_tile); - int cont_coded_key = (continent_id + 1) * p_bic_data->ImprovementsCount + improv_id;; - unsigned player_bits = *(unsigned *)p_human_player_bits >> 1; - int n_player = 1; - while (player_bits != 0) { - if ((player_bits & 1) && (n_player != civ_id)) - if (Hash_Table_look_up (&leaders[n_player].Auto_Improvements, __, improv_id , NULL) || - Hash_Table_look_up (&leaders[n_player].Auto_Improvements, __, cont_coded_key, NULL)) { - free_from_sharing = true; - break; - } - player_bits >>= 1; - n_player++; - } - } + if ((tr >= 0) && + (is->bombarding_unit != NULL) && + ((p_bic_data->UnitTypes[is->bombarding_unit->Body.UnitTypeID].Special_Actions & UCV_Stealth_Attack) != 0) && + is->current_config.enable_stealth_attack_via_bombardment && + (! is_online_game ()) && + patch_Leader_is_tile_visible (&leaders[is->bombarding_unit->Body.CivID], __, x_neigh, y_neigh)) + is->bombard_stealth_target = select_stealth_attack_bombard_target (is->bombarding_unit, x_neigh, y_neigh); - if (! free_from_sharing) - return City_get_improvement_maintenance (this, __, improv_id); - else - return 0; + return tr; } int __fastcall -patch_Leader_count_maintenance_free_units (Leader * this) +patch_rand_bombard_target (void * this, int edx, int lim) { - if ((is->current_config.extra_unit_maintenance_per_shields <= 0) && (this->ID != 0)) - return Leader_count_maintenance_free_units (this); - else { - int tr = 0; - for (int n = 0; n <= p_units->LastIndex; n++) { - Unit * unit = get_unit_ptr (n); - if ((unit != NULL) && (unit->Body.CivID == this->ID)) { - UnitType * type = &p_bic_data->UnitTypes[unit->Body.UnitTypeID]; - - // If this is a free unit - if ((unit->Body.RaceID != this->RaceID) || (type->requires_support == 0)) - tr++; - - // If this unit belongs to the barbs and has a tribe ID set (indicating it was spawned in a barb camp) then count it - // as a free unit in order not to bankrupt the barb player if the barb player controls any cities. A tribe ID of 75 is - // the generic ID that's assigned to barb units when they're spawned with no specific tribe ID. - else if ((unit->Body.CivID == 0) && (unit->Body.barb_tribe_id >= 0) && (unit->Body.barb_tribe_id < 75)) - tr++; + // If we have a bombard stealth attack target set then return 2 so that the bombard damage will be applied to units not pop or buildings. + return (is->bombard_stealth_target == NULL) ? rand_int (this, __, lim) : 2; +} - // Otherwise, if we're configured to apply extra maintenance based on shield cost, reduce the free unit count by - // however many times this unit's cost is above the threshold for extra maintenace. It's alright if the resulting free - // unit count is negative since all callers of this function subtract its return value from the total unit count to - // obtain the count of units that must be paid for. - else if (is->current_config.extra_unit_maintenance_per_shields > 0) - tr -= type->Cost / is->current_config.extra_unit_maintenance_per_shields; - } - } - return tr; - } +int __fastcall +patch_rand_int_to_dodge_city_aa (void * this, int edx, int lim) +{ + int tr = rand_int (this, __, lim); + is->result_of_roll_to_dodge_city_aa = tr; + return tr; } int __fastcall -patch_Leader_sum_unit_maintenance (Leader * this, int edx, int government_id) +patch_Unit_get_defense_to_dodge_city_aa (Unit * this) { - if (is->current_config.extra_unit_maintenance_per_shields <= 0) - return Leader_sum_unit_maintenance (this, __, government_id); - else if (this->Cities_Count > 0) { - int maint_free_count = patch_Leader_count_maintenance_free_units (this); - int cost_per_unit, base_free_count; - get_unit_support_info (this->ID, government_id, &cost_per_unit, &base_free_count); - int ai_free_count; { - if (*p_human_player_bits & 1<ID) - ai_free_count = 0; - else { - Difficulty_Level * difficulty = &p_bic_data->DifficultyLevels[*p_game_difficulty]; - ai_free_count = difficulty->Bonus_For_Each_City * this->Cities_Count + difficulty->Additional_Free_Support; - } - } - return not_below (0, (this->Unit_Count - base_free_count - ai_free_count - maint_free_count) * cost_per_unit); - } else - return 0; + int defense = Unit_get_defense_strength (this); + if (is->current_config.show_message_after_dodging_sam && + (defense > is->result_of_roll_to_dodge_city_aa) && + (this->Body.CivID == p_main_screen_form->Player_CivID)) + show_map_specific_text (this->Body.X, this->Body.Y, is->c3x_labels[CL_DODGED_SAM], 0); + return defense; } -int -sum_improvements_maintenance_to_pay (Leader * leader, int govt_id) +int __fastcall +patch_Unit_get_defense_to_find_bombard_defender (Unit * this) { - if (is->current_config.aggressively_penalize_bankruptcy) - // We're going to replace the logic that charges maintenance, and the replacement will charge both unit & building maintenance b/c - // they're intertwined under the new rules. Return zero here so the base game code doesn't charge any building maintenance on its own. + // The caller is filtering out candidates with zero defense strength as possible targets to receive damage from bombardment. We can return 0 + // here to make sure "this" unit is not targeted. + + Unit * container; + + if (is->current_config.immunize_aircraft_against_bombardment && + (p_bic_data->UnitTypes[this->Body.UnitTypeID].Unit_Class == UTC_Air)) + return 0; + + else if ((is->current_config.special_helicopter_rules & SHR_NO_DEFENSE_FROM_INSIDE) && + (container = get_unit_ptr (this->Body.Container_Unit)) != NULL && + p_bic_data->UnitTypes[container->Body.UnitTypeID].Unit_Class == UTC_Air) return 0; + else - return Leader_sum_improvements_maintenance (leader, __, govt_id); + return Unit_get_defense_strength (this); } -// The sum_improvements_maintenance function is called in two places as part of the randomization of whether improv or unit maintenance gets paid -// first. Redirect both of these calls to one function of our own. -int __fastcall patch_Leader_sum_improv_maintenance_to_pay_1 (Leader * this, int edx, int govt_id) { return sum_improvements_maintenance_to_pay (this, govt_id); } -int __fastcall patch_Leader_sum_improv_maintenance_to_pay_2 (Leader * this, int edx, int govt_id) { return sum_improvements_maintenance_to_pay (this, govt_id); } - -int -compare_buildings_to_sell (void const * a, void const * b) +int __cdecl +patch_get_WindowsFileBox_from_ini (LPCSTR key, int param_2, int param_3) { - int maint_a = (*(int const *)a >> 26) & 31, - maint_b = (*(int const *)b >> 26) & 31; - return maint_b - maint_a; + // If the file path has already been determined, then avoid using the Windows file picker. This makes the later code to insert the path easier + // since we only have to intercept the opening of the civ-style file picker instead of both that and the Windows one. + if (is->load_file_path_override == NULL) + return get_int_from_conquests_ini (key, param_2, param_3); + else + return 0; } -// Returns the final improv cost after buildings were sold -int -sell_unaffordable_buildings (Leader * leader, int improv_cost, int unit_cost) +char const * __fastcall +patch_do_open_load_game_file_picker (void * this) { - int treasury = leader->Gold_Encoded + leader->Gold_Decrement; - - clear_memo (); + if (is->load_file_path_override != NULL) { + char const * tr = is->load_file_path_override; + is->load_file_path_override = NULL; + return tr; + } else + return open_load_game_file_picker (this); +} - // Memoize all buildings this player owns that we might potentially sell. B/c the memo can only contain ints, we must pack the maintenance - // cost, improv ID, and city ID all into one int. The city ID is stored in the lowest 13 bits, then the improv ID in the next 13, and finally - // the maintenance amount in the 5 above those. - FOR_CITIES_OF (coi, leader->ID) - for (int n = 0; n < p_bic_data->ImprovementsCount; n++) { - Improvement * improv = &p_bic_data->Improvements[n]; +int __fastcall +patch_show_intro_after_load_popup (void * this, int edx, int param_1, int param_2) +{ + if (! is->suppress_intro_after_load_popup) + return patch_show_popup (this, __, param_1, param_2); + else { + is->suppress_intro_after_load_popup = 0; + return 0; + } +} - int unsellable_flags = - ITF_Center_of_Empire | - ITF_50_Luxury_Output | - ITF_50_Tax_Output | - ITF_Reduces_Corruption | - ITF_Increases_Luxury_Trade | - ITF_Allows_City_Level_2 | - ITF_Allows_City_Level_3 | - ITF_Capitalization | - ITF_Allows_Water_Trade | - ITF_Allows_Air_Trade | - ITF_Increases_Shields_In_Water | - ITF_Increases_Food_In_Water | - ITF_Increases_Trade_In_Water; +void __fastcall patch_Trade_Net_recompute_city_connections (Trade_Net * this, int edx, int civ_id, bool redo_road_network, byte param_3, int redo_roads_for_city_id); - // Only sell improvements that aren't contributing gold, even indirectly through e.g. happiness or boosting shield production - // for Wealth - bool sellable = - ((improv->Characteristics & (ITC_Small_Wonder | ITC_Wonder)) == 0) && - ((improv->ImprovementFlags & unsellable_flags) == 0) && - (improv->Happy_Faces_All <= 0) && (improv->Happy_Faces <= 0) && - (improv->Production <= 0); +void * __cdecl +patch_do_load_game (char * param_1) +{ + void * tr = do_load_game (param_1); - if (sellable && patch_City_has_improvement (coi.city, __, n, 0)) { - int maint = patch_City_get_improvement_maintenance (coi.city, __, n); - if (maint > 0) - memoize ((not_above (31, maint) << 26) | (n << 13) | coi.city_id); + if (is->current_config.restore_unit_directions_on_game_load && (p_units->Units != NULL)) + for (int n = 0; n <= p_units->LastIndex; n++) { + Unit * unit = get_unit_ptr (n); + if ((unit != NULL) && (unit->Body.UnitState != UnitState_Fortifying)) { + if (Map_in_range (&p_bic_data->Map, __, unit->Body.X, unit->Body.Y) && + Map_in_range (&p_bic_data->Map, __, unit->Body.PrevMoveX, unit->Body.PrevMoveY)) { + int dx = unit->Body.X - unit->Body.PrevMoveX, dy = unit->Body.Y - unit->Body.PrevMoveY; + int dir = -1; + if ((dx == 1) && (dy == -1)) dir = DIR_NE; + else if ((dx == 2) && (dy == 0)) dir = DIR_E; + else if ((dx == 1) && (dy == 1)) dir = DIR_SE; + else if ((dx == 0) && (dy == 2)) dir = DIR_S; + else if ((dx == -1) && (dy == 1)) dir = DIR_SW; + else if ((dx == -2) && (dy == 0)) dir = DIR_W; + else if ((dx == -1) && (dy == -1)) dir = DIR_NW; + else if ((dx == 0) && (dy == -2)) dir = DIR_N; + if (dir >= 0) + unit->Body.Animation.summary.direction = dir; + } } } - // Sort the list of buildings so the highest maintenance ones come first - qsort (is->memo, is->memo_len, sizeof is->memo[0], compare_buildings_to_sell); + // Apply era aliases + for (int n = 0; n < 32; n++) + if (*p_player_bits & (1 << n)) + apply_era_specific_names (&leaders[n]); - // Sell buildings until we can cover maintenance costs or until we run out of ones to sell - int count_sold = 0; - while ((improv_cost + unit_cost > treasury) && (count_sold < is->memo_len)) { - int improv_id = ((1<<13) - 1) & (is->memo[count_sold] >> 13), - city_id = ((1<<13) - 1) & is->memo[count_sold]; - City * city = get_city_ptr (city_id); - improv_cost -= patch_City_get_improvement_maintenance (city, __, improv_id); - City_sell_improvement (city, __, improv_id, false); - treasury = leader->Gold_Encoded + leader->Gold_Decrement; - count_sold++; + if (is->current_config.apply_grid_ini_setting_on_game_load) { + int grid_on = get_int_from_conquests_ini ("GridOn", 0, 0); + Map_Renderer * mr = &p_bic_data->Map.Renderer; + if (grid_on && ! mr->MapGrid_Flag) + mr->vtable->m68_Toggle_Grid (mr); } - // Show popup informing the player that their buildings were force sold - if ((leader->ID == p_main_screen_form->Player_CivID) && ! is_online_game ()) { - PopupForm * popup = get_popup_form (); - if (count_sold == 1) { - int improv_id = ((1<<13) - 1) & (is->memo[0] >> 13), - city_id = ((1<<13) - 1) & is->memo[0]; - set_popup_str_param (0, get_city_ptr (city_id)->Body.CityName , -1, -1); - set_popup_str_param (1, p_bic_data->Improvements[improv_id].Name.S, -1, -1); - popup->vtable->set_text_key_and_flags (popup, __, is->mod_script_path, "C3X_FORCE_SOLD_SINGLE_IMPROV", -1, 0, 0, 0); - patch_show_popup (popup, __, 0, 0); - } else if (count_sold > 1) { - set_popup_int_param (0, count_sold); - popup->vtable->set_text_key_and_flags (popup, __, is->mod_script_path, "C3X_FORCE_SOLD_MULTIPLE_IMPROVS", -1, 0, 0, 0); - - // Add list of sold improvements to popup - for (int n = 0; n < count_sold; n++) { - int improv_id = ((1<<13) - 1) & (is->memo[n] >> 13), - city_id = ((1<<13) - 1) & is->memo[n]; - char s[200]; - snprintf (s, sizeof s, "^ %s in %s", - p_bic_data->Improvements[improv_id].Name.S, - get_city_ptr (city_id)->Body.CityName); - s[(sizeof s) - 1] = '\0'; - PopupForm_add_text (popup, __, s, 0); - } - - patch_show_popup (popup, __, 0, 0); - } - } + return tr; +} - return improv_cost; +void * +load_game_ex (char const * file_path, int suppress_intro_popup) +{ + is->suppress_intro_after_load_popup = suppress_intro_popup; + is->load_file_path_override = file_path; + return patch_do_load_game (NULL); } -// Returns the final unit cost after disbanding -int -disband_unaffordable_units (Leader * leader, int improv_cost, int unit_cost, int cost_per_unit) +int __fastcall +patch_show_movement_phase_popup (void * this, int edx, int param_1, int param_2) { - int treasury = leader->Gold_Encoded + leader->Gold_Decrement; - int count_disbanded = 0; - Unit * to_disband; - char first_disbanded_name[32]; - while ((improv_cost + unit_cost > treasury) && - (unit_cost > 0) && - (NULL != (to_disband = leader->vtable->find_unsupported_unit (leader)))) { - if (count_disbanded == 0) { - char const * name_src = (to_disband->Body.Custom_Name.S[0] == '\0') - ? p_bic_data->UnitTypes[to_disband->Body.UnitTypeID].Name - : to_disband->Body.Custom_Name.S; - strncpy (first_disbanded_name, name_src, sizeof first_disbanded_name); - first_disbanded_name[(sizeof first_disbanded_name) - 1] = '\0'; - } - Unit_disband (to_disband); - count_disbanded++; - unit_cost -= cost_per_unit; - } + int tr = patch_show_popup (this, __, param_1, param_2); - // Show popup informing the player that their units were disbanded - if (leader->ID == p_main_screen_form->Player_CivID) { - PopupForm * popup = get_popup_form (); - if (count_disbanded == 1) { - set_popup_str_param (0, first_disbanded_name, -1, -1); - int online_flag = is_online_game () ? 0x4000 : 0; - popup->vtable->set_text_key_and_flags (popup, __, is->mod_script_path, "C3X_FORCE_DISBANDED_SINGLE_UNIT", -1, 0, online_flag, 0); - patch_show_popup (popup, __, 0, 0); - } else if ((count_disbanded > 1) && ! is_online_game ()) { - set_popup_int_param (0, count_disbanded); - popup->vtable->set_text_key_and_flags (popup, __, is->mod_script_path, "C3X_FORCE_DISBANDED_MULTIPLE_UNITS", -1, 0, 0, 0); - patch_show_popup (popup, __, 0, 0); - } + int player_civ_id = p_main_screen_form->Player_CivID; + int replay_for_players = is->replay_for_players; // Store this b/c it gets reset on game load + if (replay_for_players & 1<showing_hotseat_replay = true; + + patch_do_save_game (hotseat_resume_save_path, 1, 0); + load_game_ex (hotseat_replay_save_path, 1); + p_main_screen_form->Player_CivID = player_civ_id; + + // Re-enable the GUI so the minimap is visible during the replay. We must also reset the minimap & redraw it so that it reflects the + // player we just seated (above) instead of leftover data from the last player. + Main_GUI * main_gui = &p_main_screen_form->GUI; + main_gui->is_enabled = 1; + Navigator_Data_reset (&main_gui->Navigator_Data); + main_gui->Base.vtable->m73_call_m22_Draw ((Base_Form *)main_gui); + + perform_interturn (); + load_game_ex (hotseat_resume_save_path, 1); + p_main_screen_form->is_now_loading_game = 0; + + // Restore the replay_for_players variable b/c it gets cleared when loading a game. Also mask out the bit for the player we just + // showed the replay to. + is->replay_for_players = replay_for_players & ~(1<showing_hotseat_replay = false; } - return unit_cost; + return tr; } +// Returns a set of player bits containing only those players that are human and can see at least one AI unit. For speed and simplicity, does not +// account for units' invisibility ability, units are considered visible as long as they're on a visible tile. int -compare_cities_by_production (void const * vp_id_a, void const * vp_id_b) +find_human_players_seeing_ai_units () { - City * a = get_city_ptr (*(int const *)vp_id_a), - * b = get_city_ptr (*(int const *)vp_id_b); - return a->Body.ProductionIncome - b->Body.ProductionIncome; + int tr = 0; + Map * map = &p_bic_data->Map; + if (map->Tiles != NULL) + for (int n_tile = 0; n_tile < map->TileCount; n_tile++) { + Tile * tile = map->Tiles[n_tile]; + Tile_Body * body = &tile->Body; + int human_vis_bits = (body->FOWStatus | body->V3 | body->Visibility | body->field_D0_Visibility) & *p_human_player_bits; + if (human_vis_bits != 0) // If any human players can see this tile + for (int n_player = 0; n_player < 32; n_player++) + if (human_vis_bits & 1<TileUnitID, &unused); + Unit * unit = get_unit_ptr (unit_id); + if ((unit != NULL) && + ((*p_human_player_bits & 1<Body.CivID) == 0)) { + tr |= 1<ID, leader->GovernmentType, &cost_per_unit, NULL); + int save_replay = is->current_config.replay_ai_moves_in_hotseat_games && + (*p_is_offline_mp_game && ! *p_is_pbem_game); // offline MP but not PBEM => we're in a hotseat game + int ai_unit_vis_before; + if (save_replay) { + ai_unit_vis_before = find_human_players_seeing_ai_units (); + int toggleable_rules = *p_toggleable_rules; + *p_toggleable_rules |= TR_PRESERVE_RANDOM_SEED; // Make sure preserve random seed is on for the replay save + patch_do_save_game (hotseat_replay_save_path, 1, 0); + *p_toggleable_rules = toggleable_rules; + } - int improv_cost = Leader_sum_improvements_maintenance (leader, __, leader->GovernmentType); + is->players_saw_ai_unit = 0; // Clear bits. After perform_interturn, each set bit will indicate a player that has seen an AI unit move + long long ts_before; + QueryPerformanceCounter ((LARGE_INTEGER *)&ts_before); + is->time_spent_paused_during_popup = 0; + is->time_spent_computing_city_connections = 0; + is->count_calls_to_recompute_city_connections = 0; + unsigned saved_prefs = *p_preferences; + if (is->current_config.measure_turn_times) + *p_preferences &= ~(P_ANIMATE_BATTLES | P_SHOW_FRIEND_MOVES | P_SHOW_ENEMY_MOVES); - int unit_cost = 0; { - if (leader->Cities_Count > 0) // Players with no cities don't pay unit maintenance, per the original game rules - if (cost_per_unit > 0) { - int count_free_units = Leader_get_free_unit_count (leader, __, leader->GovernmentType) + patch_Leader_count_maintenance_free_units (leader); - unit_cost += not_below (0, (leader->Unit_Count - count_free_units) * cost_per_unit); + perform_interturn (); + + if (is->current_config.day_night_cycle_mode) { + if (is->day_night_cycle_img_state == IS_OK) { + int new_hour = calculate_current_day_night_cycle_hour (); + if (new_hour != is->current_day_night_cycle) { + is->current_day_night_cycle = new_hour; + p_main_screen_form->vtable->m73_call_m22_Draw ((Base_Form *)p_main_screen_form); } + } } - int treasury = leader->Gold_Encoded + leader->Gold_Decrement; - if (improv_cost + unit_cost > treasury) { + if (is->current_config.enable_city_work_radii_highlights && is->highlight_city_radii) { + is->highlight_city_radii = false; + clear_highlighted_worker_tiles_for_districts (); + p_main_screen_form->vtable->m73_call_m22_Draw ((Base_Form *)p_main_screen_form); + } - // For AIs, alternate between selling buildings and disbanding units when maintenance can't be paid - if (((1<ID & *p_human_player_bits) != 0) || ((leader->ID + *p_current_turn_no) % 2 == 0)) { - improv_cost = sell_unaffordable_buildings (leader, improv_cost, unit_cost); - unit_cost = disband_unaffordable_units (leader, improv_cost, unit_cost, cost_per_unit); - } else { - unit_cost = disband_unaffordable_units (leader, improv_cost, unit_cost, cost_per_unit); - improv_cost = sell_unaffordable_buildings (leader, improv_cost, unit_cost); - } + if (is->current_config.show_ai_city_location_desirability_if_settler && is->city_loc_display_perspective >= 0) { + is->city_loc_display_perspective = -1; + p_main_screen_form->vtable->m73_call_m22_Draw ((Base_Form *)p_main_screen_form); // Trigger map redraw + } - treasury = leader->Gold_Encoded + leader->Gold_Decrement; // Update b/c selling buildings recovers some gold + if (is->current_config.measure_turn_times) { + long long ts_after; + QueryPerformanceCounter ((LARGE_INTEGER *)&ts_after); + long long perf_freq; + QueryPerformanceFrequency ((LARGE_INTEGER *)&perf_freq); + int turn_time_in_ms = 1000 * (ts_after - ts_before - is->time_spent_paused_during_popup) / perf_freq; + int city_con_time_in_ms = 1000 * is->time_spent_computing_city_connections / perf_freq; + int road_time_in_ms = 1000 * is->time_spent_filling_roads / perf_freq; - // If the player still can't afford maintenance, even after all that, start switching their cities to Wealth - int wealth_income = 0; - if (improv_cost + unit_cost > treasury + wealth_income) { - // Memoize all cities not already building wealth and sort by production (lowest first) - clear_memo (); - FOR_CITIES_OF (coi, leader->ID) - if ((coi.city->Body.Status & CSF_Capitalization) == 0) - memoize (coi.city_id); - qsort (is->memo, is->memo_len, sizeof is->memo[0], compare_cities_by_production); + PopupForm * popup = get_popup_form (); + popup->vtable->set_text_key_and_flags (popup, __, is->mod_script_path, "C3X_INFO", -1, 0, 0, 0); + char msg[1000]; - int wealth_improv_id = -1; { - for (int n = 0; n < p_bic_data->ImprovementsCount; n++) - if (p_bic_data->Improvements[n].ImprovementFlags & ITF_Capitalization) { - wealth_improv_id = n; - break; - } + struct c3x_opt { + bool is_active; + char * name; + } opts[] = {{is->current_config.optimize_improvement_loops, "improv. loops"}, + {is->current_config.enable_trade_net_x && (is->tnx_init_state == IS_OK), "trade net x"}}; + char opt_list[1000]; + memset (opt_list, 0, sizeof opt_list); + strncpy (opt_list, "^C3X optimizations: ", sizeof opt_list); + bool any_active_opts = false; + for (int n = 0; n < ARRAY_LEN (opts); n++) + if (opts[n].is_active) { + char * cursor = &opt_list[strlen (opt_list)]; + snprintf (cursor, opt_list + (sizeof opt_list) - cursor, "%s%s", any_active_opts ? ", " : "", opts[n].name); + any_active_opts = true; } + if (! any_active_opts) { + char * cursor = &opt_list[strlen (opt_list)]; + strncpy (cursor, "None", opt_list + (sizeof opt_list) - cursor); + } + PopupForm_add_text (popup, __, (char *)opt_list, false); - if (wealth_improv_id >= 0) { - int n = 0, - switched_any = 0; - - while ((n < is->memo_len) && (improv_cost + unit_cost > treasury + wealth_income)) { - City * city = get_city_ptr (is->memo[n]); - City_set_production (city, __, COT_Improvement, wealth_improv_id, false); - switched_any = 1; - wealth_income += City_get_income_from_wealth_build (city); - n++; - } + snprintf (msg, sizeof msg, "^Turn time: %d.%03d sec", turn_time_in_ms/1000, turn_time_in_ms%1000); + PopupForm_add_text (popup, __, (char *)msg, false); + snprintf (msg, sizeof msg, "^ Recomputing city connections: %d.%03d sec (%d calls)", + city_con_time_in_ms/1000, city_con_time_in_ms%1000, + is->count_calls_to_recompute_city_connections); + PopupForm_add_text (popup, __, (char *)msg, false); + snprintf (msg, sizeof msg, "^ Flood filling road network: %d.%03d sec", + road_time_in_ms/1000, road_time_in_ms%1000); + PopupForm_add_text (popup, __, (char *)msg, false); + patch_show_popup (popup, __, 0, 0); - if (switched_any && (leader->ID == p_main_screen_form->Player_CivID)) { - PopupForm * popup = get_popup_form (); - Improvement * wealth = &p_bic_data->Improvements[wealth_improv_id]; - set_popup_str_param (0, wealth->Name.S, -1, -1); - set_popup_str_param (1, wealth->CivilopediaEntry.S, -1, -1); - popup->vtable->set_text_key_and_flags (popup, __, is->mod_script_path, "C3X_FORCE_BUILD_WEALTH", -1, 0, 0, 0); - patch_show_popup (popup, __, 0, 0); - } - } - } + *p_preferences = saved_prefs; } - Leader_set_treasury (leader, __, treasury - improv_cost - unit_cost); -} - -bool __fastcall -patch_Unit_has_king_ability_for_find_unsupported (Unit * this, int edx, enum UnitTypeAbilities king_ability) -{ - // If we're set to aggressively penalize bankruptcy and this unit doesn't require support, return that it is a king so it doesn't get - // disbanded. - if (is->current_config.aggressively_penalize_bankruptcy && ! p_bic_data->UnitTypes[this->Body.UnitTypeID].requires_support) - return true; + if (save_replay) { + int last_human_player_bit = 0; { + for (int n = 0; n < 32; n++) + if (*p_human_player_bits & 1<replay_for_players = (ai_unit_vis_before | (is->players_saw_ai_unit & *p_human_player_bits)) & ~last_human_player_bit; + } } -void __fastcall -patch_Leader_pay_unit_maintenance (Leader * this) +void __cdecl +patch_initialize_map_music (int civ_id, int era_id, bool param_3) { - if (! is->current_config.aggressively_penalize_bankruptcy) - Leader_pay_unit_maintenance (this); - else - charge_maintenance_with_aggressive_penalties (this); + if (! is->showing_hotseat_replay) + initialize_map_music (civ_id, era_id, param_3); } -void __fastcall -patch_Main_Screen_Form_show_wltk_message (Main_Screen_Form * this, int edx, int tile_x, int tile_y, char * text_key, bool pause) +void __stdcall +patch_deinitialize_map_music () { - patch_Main_Screen_Form_show_map_message (this, __, tile_x, tile_y, text_key, is->current_config.dont_pause_for_love_the_king_messages ? false : pause); + if (! is->showing_hotseat_replay) + deinitialize_map_music (); } void __fastcall -patch_Main_Screen_Form_show_wltk_ended_message (Main_Screen_Form * this, int edx, int tile_x, int tile_y, char * text_key, bool pause) -{ - patch_Main_Screen_Form_show_map_message (this, __, tile_x, tile_y, text_key, is->current_config.dont_pause_for_love_the_king_messages ? false : pause); -} - -char __fastcall -patch_Tile_has_city_for_agri_penalty_exception (Tile * this) -{ - return is->current_config.no_penalty_exception_for_agri_fresh_water_city_tiles ? 0 : Tile_has_city (this); -} - -int -show_razing_popup (void * popup_object, int popup_param_1, int popup_param_2, int razing_option) -{ - int response = patch_show_popup (popup_object, __, popup_param_1, popup_param_2); - if (is->current_config.prevent_razing_by_players && (response == razing_option)) { - PopupForm * popup = get_popup_form (); - popup->vtable->set_text_key_and_flags (popup, __, is->mod_script_path, "C3X_CANT_RAZE", -1, 0, 0, 0); - patch_show_popup (popup, __, 0, 0); - return 0; - } - return response; -} - -int __fastcall patch_show_popup_option_1_razes (void *this, int edx, int param_1, int param_2) { return show_razing_popup (this, param_1, param_2, 1); } -int __fastcall patch_show_popup_option_2_razes (void *this, int edx, int param_1, int param_2) { return show_razing_popup (this, param_1, param_2, 2); } - -int __fastcall -patch_Context_Menu_add_abandon_city (Context_Menu * this, int edx, int item_id, char * text, bool checkbox, Sprite * image) -{ - if (is->current_config.prevent_razing_by_players) - return 0; // Return value is ignored by the caller - else - return Context_Menu_add_item (this, __, item_id, text, checkbox, image); -} - -char * -check_pedia_upgrades_to_ptr (TextBuffer * this, char * str) +patch_Fighter_do_bombard_tile (Fighter * this, int edx, Unit * unit, int neighbor_index, int mp_tile_x, int mp_tile_y) { - Civilopedia_Form * pedia = p_civilopedia_form; - UnitType * unit_type = NULL; - if (is->current_config.indicate_non_upgradability_in_pedia && - (pedia->Current_Article_ID >= 0) && (pedia->Current_Article_ID <= pedia->Max_Article_ID) && - (NULL != (unit_type = pedia->Articles[pedia->Current_Article_ID]->unit_type)) && - ((unit_type->Special_Actions & UCV_Upgrade_Unit) == 0)) - return is->c3x_labels[CL_OBSOLETED_BY]; - else - return TextBuffer_check_ptr (this, __, str); -} + // Unit::score_kill will be called if the bombarder destroys its target, and that is the only way score_kill can be called while this method + // is running. So if we're configured to stop enslaving from bombard, turn off enslaving while it's running. + is->do_not_enslave_units = is->current_config.prevent_enslaving_by_bombardment; -char * __fastcall patch_TextBuffer_check_pedia_upgrades_to_ptr_1 (TextBuffer * this, int edx, char * str) { return check_pedia_upgrades_to_ptr (this, str); } -char * __fastcall patch_TextBuffer_check_pedia_upgrades_to_ptr_2 (TextBuffer * this, int edx, char * str) { return check_pedia_upgrades_to_ptr (this, str); } + // Check if we're going to do PTW-like targeting, if not fall back on the base game's do_bombard_tile method. We'll also fall back on that + // method in the case where we're in an online game and the bombard can't happen b/c the tile is occupied by another battle. In that case, no + // bombard is possible but we'll call the base method anyway since it will show a little message saying as much. + if (itable_look_up_or (&is->current_config.ptw_arty_types, unit->Body.UnitTypeID, 0) && + (is->bombard_stealth_target == NULL) && + ! (is_online_game () && mp_check_current_combat (p_mp_object, __, mp_tile_x, mp_tile_y))) { -bool __fastcall -patch_Unit_select_stealth_attack_target (Unit * this, int edx, int target_civ_id, int x, int y, bool allow_popup, Unit ** out_selected_target) -{ - is->added_any_stealth_target = 0; - return Unit_select_stealth_attack_target (this, __, target_civ_id, x, y, allow_popup, out_selected_target); -} + City * city; { + int dx, dy; + neighbor_index_to_diff (neighbor_index, &dx, &dy); + int tile_x = unit->Body.X + dx, tile_y = unit->Body.Y + dy; + wrap_tile_coords (&p_bic_data->Map, &tile_x, &tile_y); + city = city_at (tile_x, tile_y); + } -bool __fastcall -patch_Unit_can_stealth_attack (Unit * this, int edx, Unit * target) -{ - bool tr = Unit_can_stealth_attack (this, __, target); + int rv; + if ((city != NULL) && ((rv = rand_int (p_rand_object, __, 3)) < 2)) + Fighter_damage_city_by_bombardment (this, __, unit, city, rv, 0); + else + Fighter_do_bombard_tile (this, __, unit, neighbor_index, mp_tile_x, mp_tile_y); - // If we're selecting a target for stealth attack via bombardment, we must filter out candidates we can't damage that way - if (tr && is->selecting_stealth_target_for_bombard && - ! can_damage_bombarding (&p_bic_data->UnitTypes[this->Body.UnitTypeID], target, tile_at (target->Body.X, target->Body.Y))) - return false; + } else + Fighter_do_bombard_tile (this, __, unit, neighbor_index, mp_tile_x, mp_tile_y); - else if (tr && is->current_config.exclude_invisible_units_from_stealth_attack && ! patch_Unit_is_visible_to_civ (target, __, this->Body.CivID, 1)) - return false; + is->do_not_enslave_units = false; +} - else - return tr; +bool __fastcall +patch_Unit_check_king_for_defense_priority (Unit * this, int edx, enum UnitTypeAbilities king_ability) +{ + return (! is->current_config.ignore_king_ability_for_defense_priority) || (*p_toggleable_rules & (TR_REGICIDE | TR_MASS_REGICIDE)) ? + Unit_has_ability (this, __, king_ability) : + false; } -int __fastcall -patch_Tile_check_water_for_stealth_attack (Tile * this) +void WINAPI +patch_get_local_time_for_unit_ini (LPSYSTEMTIME lpSystemTime) { - // When stealth attack bombard is enabled, remove a special rule inside can_stealth_attack that prevents land units from stealth attacking - // onto sea tiles. This allows land artillery to stealth attack naval units. - return is->current_config.enable_stealth_attack_via_bombardment ? 0 : this->vtable->m35_Check_Is_Water (this); + GetLocalTime (lpSystemTime); + if (is->current_config.no_elvis_easter_egg && (lpSystemTime->wMonth == 1) && (lpSystemTime->wDay == 8)) + lpSystemTime->wDay = 9; } -int __fastcall -patch_PopupSelection_add_stealth_attack_target (PopupSelection * this, int edx, char * text, int value) +bool __fastcall +patch_Leader_could_buy_tech_for_trade_screen (Leader * this, int edx, int tech_id, int from_civ_id) { - if (is->current_config.include_stealth_attack_cancel_option && (! is->added_any_stealth_target)) { - PopupSelection_add_item (this, __, is->c3x_labels[CL_NO_STEALTH_ATTACK], -1); - is->added_any_stealth_target = 1; - } + // Temporarily remove the untradable flag so this tech is listed on the screen instead of skipped over. After all the items have been + // assembled, we'll go back and disable the untradable techs. + if (is->current_config.show_untradable_techs_on_trade_screen) { + int saved_flags = p_bic_data->Advances[tech_id].Flags; + p_bic_data->Advances[tech_id].Flags &= ~ATF_Cannot_Be_Traded; + bool tr = this->vtable->could_buy_tech (this, __, tech_id, from_civ_id); + p_bic_data->Advances[tech_id].Flags = saved_flags; + return tr; - Unit * unit; - if (is->current_config.show_hp_of_stealth_attack_options && - ((unit = get_unit_ptr (value)) != NULL)) { - char s[500]; - int max_hp = Unit_get_max_hp (unit); - snprintf (s, sizeof s, "(%d/%d) %s", max_hp - unit->Body.Damage, max_hp, text); - s[(sizeof s) - 1] = '\0'; - return PopupSelection_add_item (this, __, s, value); } else - return PopupSelection_add_item (this, __, text, value); + return this->vtable->could_buy_tech (this, __, tech_id, from_civ_id); } void __fastcall -patch_Unit_perform_air_recon (Unit * this, int edx, int x, int y) +patch_DiploForm_assemble_tradable_items (DiploForm * this) { - int moves_plus_one = this->Body.Moves + p_bic_data->General.RoadsMovementRate; - - bool was_intercepted = false; - if (is->current_config.intercept_recon_missions) { - // Temporarily add vision on the target tile so the game plays the animation if the unit is show down by ground AA - Tile_Body * tile = &tile_at (x, y)->Body; - int saved_vis = tile->Visibility; - tile->Visibility |= 1 << this->Body.CivID; - was_intercepted = Unit_try_flying_over_tile (this, __, x, y); - tile->Visibility = saved_vis; - } + DiploForm_assemble_tradable_items (this); - if (! was_intercepted) { - Unit_perform_air_recon (this, __, x, y); - if (is->current_config.charge_one_move_for_recon_and_interception) - this->Body.Moves = moves_plus_one; - } + // Disable (gray out) all untradable techs + if (is->current_config.show_untradable_techs_on_trade_screen) + for (int n = 0; n < p_bic_data->AdvanceCount; n++) + if (p_bic_data->Advances[n].Flags & ATF_Cannot_Be_Traded) { + this->tradable_technologies[n].can_be_bought = 0; + this->tradable_technologies[n].can_be_sold = 0; + } } -int __fastcall -patch_Unit_get_interceptor_max_moves (Unit * this) +bool __fastcall +patch_City_can_trade_via_water (City * this) { - // Stop fighters from intercepting multiple times per turn without blitz - if (is->current_config.charge_one_move_for_recon_and_interception && - (this->Body.Status & USF_USED_ATTACK != 0) && ! UnitType_has_ability (&p_bic_data->UnitTypes[this->Body.UnitTypeID], __, UTA_Blitz)) - return 0; - - else - return patch_Unit_get_max_move_points (this); + if (is->current_config.optimize_improvement_loops) { + for (int n = 0; n < is->water_trade_improvs.count; n++) + if (has_active_building (this, is->water_trade_improvs.items[n])) + return true; + return false; + } else + return City_can_trade_via_water (this); } -int __fastcall -patch_Unit_get_moves_after_interception (Unit * this) +bool __fastcall +patch_City_can_trade_via_air (City * this) { - if (is->current_config.charge_one_move_for_recon_and_interception) { - this->Body.Status |= USF_USED_ATTACK; // Set status bit indicating that the interceptor has attacked this turn - return this->Body.Moves + p_bic_data->General.RoadsMovementRate; + if (is->current_config.optimize_improvement_loops) { + for (int n = 0; n < is->air_trade_improvs.count; n++) + if (has_active_building (this, is->air_trade_improvs.items[n])) + return true; + return false; } else - return patch_Unit_get_max_move_points (this); + return City_can_trade_via_air (this); } -void __fastcall -patch_Unit_set_state_after_interception (Unit * this, int edx, int new_state) +int __fastcall +patch_City_get_building_defense_bonus (City * this) { - if (! is->current_config.charge_one_move_for_recon_and_interception) - Unit_set_state (this, __, new_state); + bool cancel_great_wall_boost = is->current_config.enable_districts && is->current_config.disable_great_wall_city_defense_bonus; - // If fighters are supposed to be able to intercept multiple times per turn, then we can't knock them out of the interception state as soon as - // they intercept something like in the base game. Instead, record this interception event so that we can clear their state at the start of - // their next turn. - else { - struct interceptor_reset_list * irl = &is->interceptor_reset_lists[this->Body.CivID]; - reserve (sizeof irl->items[0], (void **)&irl->items, &irl->capacity, irl->count); - irl->items[irl->count++] = (struct interception) { - .unit_id = this->Body.ID, - .x = this->Body.X, - .y = this->Body.Y - }; - } -} + if (is->current_config.optimize_improvement_loops) { + int tr = 0; + int is_size_level_1 = (this->Body.Population.Size <= p_bic_data->General.MaximumSize_City) && + (this->Body.Population.Size <= p_bic_data->General.MaximumSize_Town); + for (int n = 0; n < is->combat_defense_improvs.count; n++) { + int improv_id = is->combat_defense_improvs.items[n]; + Improvement * improv = &p_bic_data->Improvements[improv_id]; + if ((is_size_level_1 || (improv->Combat_Bombard == 0)) && has_active_building (this, improv_id)) { + int multiplier; + if ((improv->Combat_Bombard > 0) && + (! cancel_great_wall_boost) && + (patch_Leader_count_any_shared_wonders_with_flag (&leaders[(this->Body).CivID], __, ITW_Doubles_City_Defenses, NULL) > 0)) + multiplier = 2; + else + multiplier = 1; -// Goes through every entry in a table with unit IDs as keys and filters out any that belong to the given owner (or are invalid). -void -remove_unit_id_entries_owned_by (struct table * t, int owner_id) -{ - if (t->len > 0) { - clear_memo (); - FOR_TABLE_ENTRIES (tei, t) { - Unit * unit = get_unit_ptr (tei.key); - if ((unit == NULL) || (unit->Body.CivID == owner_id)) - memoize (tei.key); + int building_defense = multiplier * improv->Combat_Defence; + if (building_defense > tr) + tr = building_defense; + } } - for (int n = 0; n < is->memo_len; n++) - itable_remove (t, is->memo[n]); - } + return tr; + } else + return City_get_building_defense_bonus (this); } -void __fastcall -patch_Leader_begin_turn (Leader * this) +bool __fastcall +patch_City_shows_harbor_icon (City * this) { - if (is->aerodrome_airlift_usage.len > 0) { - int civ_bit = 1 << this->ID; - clear_memo (); - FOR_TABLE_ENTRIES (tei, &is->aerodrome_airlift_usage) { - int mask = tei.value; - if (mask & civ_bit) { - int new_mask = mask & ~civ_bit; - memoize (tei.key); - memoize (new_mask); - } - } - for (int n = 0; n < is->memo_len; n += 2) { - int key = is->memo[n]; - int new_mask = is->memo[n + 1]; - if (new_mask == 0) - itable_remove (&is->aerodrome_airlift_usage, key); - else - itable_insert (&is->aerodrome_airlift_usage, key, new_mask); - } - clear_memo (); - } - - // Eject trespassers - is->do_not_bounce_invisible_units = true; - if (is->current_config.disallow_trespassing && (*p_current_turn_no > 0)) - for (int n = 1; n < 32; n++) - if ((*p_player_bits & (1 << n)) && - (n != this->ID) && - (! this->At_War[n]) && - ((this->Relation_Treaties[n] & 2) == 0)) // Check right of passage - Leader_bounce_trespassing_units (&leaders[n], __, this->ID); - is->do_not_bounce_invisible_units = false; - - if (is->current_config.introduce_all_human_players_at_start_of_hotseat_game && - (*p_current_turn_no == 0) && - (*p_is_offline_mp_game && ! *p_is_pbem_game) && // is hotseat game - ((*p_human_player_bits & (1 << this->ID)) != 0)) - for (int n = 0; n < 32; n++) - if (*p_human_player_bits & (1 << n)) - Leader_make_contact (this, __, n, false); - - Leader_begin_turn (this); + return is->current_config.city_icons_show_unit_effects_not_trade ? + City_count_improvements_with_flag (this, __, ITF_Veteran_Sea_Units) > 0 : + patch_City_can_trade_via_water (this); } -void __fastcall -patch_Leader_begin_unit_turns (Leader * this) +bool __fastcall +patch_City_shows_airport_icon (City * this) { - // Reset the states of all fighters that performed an interception on the previous turn. - struct interceptor_reset_list * irl = &is->interceptor_reset_lists[this->ID]; - for (int n = 0; n < irl->count; n++) { - struct interception * record = &irl->items[n]; - Unit * interceptor = get_unit_ptr (record->unit_id); - if ((interceptor != NULL) && - (interceptor->Body.CivID == this->ID) && - (interceptor->Body.X == record->x) && - (interceptor->Body.Y == record->y) && - (interceptor->Body.UnitState == UnitState_Intercept)) - Unit_set_state (interceptor, __, 0); - } - irl->count = 0; - - // Reset extra defensive bombard and airdrop counters - remove_unit_id_entries_owned_by (&is->extra_defensive_bombards, this->ID); - remove_unit_id_entries_owned_by (&is->airdrops_this_turn , this->ID); - remove_unit_id_entries_owned_by (&is->unit_transport_ties , this->ID); + return is->current_config.city_icons_show_unit_effects_not_trade ? + City_count_improvements_with_flag (this, __, ITF_Veteran_Air_Units) > 0 : + patch_City_can_trade_via_air (this); +} - clear_memo (); - if (is->current_config.delete_off_map_ai_units && - ((*p_human_player_bits & (1 << this->ID)) == 0) && - (p_units->Units != NULL)) - for (int n = 0; n <= p_units->LastIndex; n++) { - Unit_Body * body = p_units->Units[n].Unit; - if ((body != NULL) && - ((int)body != offsetof (Unit, Body)) && - (body->CivID == this->ID) && - ! Map_in_range (&p_bic_data->Map, __, body->X, body->Y)) - memoize (body->ID); - } - for (int n = 0; n < is->memo_len; n++) - patch_Unit_despawn (get_unit_ptr (is->memo[n]), __, 0, 1, 0, 0, 0, 0, 0); +int __fastcall +patch_Unit_eval_escort_requirement (Unit * this) +{ + UnitType * type = &p_bic_data->UnitTypes[this->Body.UnitTypeID]; + int ai_strat = type->AI_Strategy; + bool owned_by_ai = (*p_human_player_bits & 1<Body.CivID) == 0; // We must not reduce the escort requirement for human-owned units + // because that will interfere with group movement of units. - Leader_begin_unit_turns (this); + // Apply special escort rules + if (owned_by_ai && is->current_config.dont_escort_unflagged_units && ! UnitType_has_ability (type, __, UTA_Requires_Escort)) + return 0; + else if (owned_by_ai && is->current_config.use_offensive_artillery_ai && (ai_strat & UTAI_Artillery)) + return 1; + + else { + int base = Unit_eval_escort_requirement (this); + if (owned_by_ai && (ai_strat & (UTAI_Naval_Transport | UTAI_Naval_Carrier | UTAI_Naval_Missile_Transport))) + return not_above (is->current_config.max_ai_naval_escorts, base); + else + return base; + } } -Unit * __fastcall -patch_Fighter_find_actual_bombard_defender (Fighter * this, int edx, Unit * bombarder, int tile_x, int tile_y, int bombarder_civ_id, bool land_lethal, bool sea_lethal) +bool __fastcall +patch_Unit_has_enough_escorters_present (Unit * this) { - if (is->bombard_stealth_target == NULL) - return Fighter_find_defender_against_bombardment (this, __, bombarder, tile_x, tile_y, bombarder_civ_id, land_lethal, sea_lethal); + UnitType * type = &p_bic_data->UnitTypes[this->Body.UnitTypeID]; + if (is->current_config.dont_escort_unflagged_units && ! UnitType_has_ability (type, __, UTA_Requires_Escort)) + return true; else - return is->bombard_stealth_target; + return Unit_has_enough_escorters_present (this); } -Unit * -select_stealth_attack_bombard_target (Unit * unit, int tile_x, int tile_y) +void __fastcall +patch_Unit_check_escorter_health (Unit * this, int edx, bool * has_any_escort_present, bool * any_escorter_cant_heal) { - bool land_lethal = Unit_has_ability (unit, __, UTA_Lethal_Land_Bombardment), - sea_lethal = Unit_has_ability (unit, __, UTA_Lethal_Sea_Bombardment); - Unit * defender = Fighter_find_defender_against_bombardment (&p_bic_data->fighter, __, unit, tile_x, tile_y, unit->Body.CivID, land_lethal, sea_lethal); - if (defender != NULL) { - Unit * target; - is->selecting_stealth_target_for_bombard = 1; - bool got_one = patch_Unit_select_stealth_attack_target (unit, __, defender->Body.CivID, tile_x, tile_y, true, &target); - is->selecting_stealth_target_for_bombard = 0; - return got_one ? target : NULL; + UnitType * type = &p_bic_data->UnitTypes[this->Body.UnitTypeID]; + if (is->current_config.dont_escort_unflagged_units && ! UnitType_has_ability (type, __, UTA_Requires_Escort)) { + *has_any_escort_present = true; + *any_escorter_cant_heal = true; // Returning true here indicates the unit should not stop to wait for its escorter(s) to heal. } else - return NULL; + Unit_check_escorter_health (this, __, has_any_escort_present, any_escorter_cant_heal); } -bool __fastcall -patch_Unit_try_flying_for_precision_strike (Unit * this, int edx, int x, int y) +void __fastcall +patch_Leader_unlock_technology (Leader * this, int edx, int tech_id, bool param_2, bool param_3, bool param_4) { - bool is_cruise_missile = UnitType_has_ability (&p_bic_data->UnitTypes[this->Body.UnitTypeID], __, UTA_Cruise_Missile); - if (is->current_config.polish_precision_striking && - (p_bic_data->UnitTypes[this->Body.UnitTypeID].Unit_Class != UTC_Air) && - ! is_cruise_missile) - // This method returns -1 when some kind of error occurs. In that case, return true implying the unit was shot down so the caller - // doesn't do anything more. Otherwise, return false so it goes ahead and applies damage. - return Unit_play_bombard_fire_animation (this, __, x, y) == -1; + int * p_stack = (int *)&tech_id; + int ret_addr = p_stack[-1]; - else if (is->current_config.polish_precision_striking && is_cruise_missile) { - Unit_animate_cruise_missile_strike (this, __, x, y); - return false; + Leader_unlock_technology (this, __, tech_id, param_2, param_3, param_4); - } else - return Unit_try_flying_over_tile (this, __, x, y); + // If this method was not called during game initialization + if ((ret_addr != ADDR_UNLOCK_TECH_AT_INIT_1) && + (ret_addr != ADDR_UNLOCK_TECH_AT_INIT_2) && + (ret_addr != ADDR_UNLOCK_TECH_AT_INIT_3)) { + + // If this tech obsoletes some building and we're configured to fix the maintenance bug then recompute city maintenance. + if (is->current_config.patch_maintenance_persisting_for_obsolete_buildings) { + bool obsoletes_anything = false; + for (int n = 0; n < p_bic_data->ImprovementsCount; n++) + if (p_bic_data->Improvements[n].ObsoleteID == tech_id) { + obsoletes_anything = true; + break; + } + if (obsoletes_anything) + Leader_recompute_buildings_maintenance (this); + } + } } -void __fastcall -patch_Unit_play_bombing_anim_for_precision_strike (Unit * this, int edx, int x, int y) +int __fastcall +patch_City_get_improv_maintenance_for_ui (City * this, int edx, int improv_id) { - // Only play the bombing animation here if we haven't already played an animation in the above method. We don't want to play all animations - // here since the bombard fire animation can fail for whatever reason but this method can't handle failure. - bool is_cruise_missile = UnitType_has_ability (&p_bic_data->UnitTypes[this->Body.UnitTypeID], __, UTA_Cruise_Missile); - if ((! is->current_config.polish_precision_striking) || - ((p_bic_data->UnitTypes[this->Body.UnitTypeID].Unit_Class == UTC_Air) && ! is_cruise_missile)) - Unit_play_bombing_animation (this, __, x, y); + Improvement * improv = &p_bic_data->Improvements[improv_id]; + if (is->current_config.patch_maintenance_persisting_for_obsolete_buildings && + (improv->ObsoleteID >= 0) && Leader_has_tech (&leaders[this->Body.CivID], __, improv->ObsoleteID)) + return 0; + else + return patch_City_get_improvement_maintenance (this, __, improv_id); } +// Patch for barbarian diagonal bug. This bug is a small mistake in the original code, maybe a copy+paste error. The original code tries to loop over +// tiles around the barb unit by incrementing a neighbor index and coverting it to tile coords (like normal). The problem is that after it converts +// the neighbor index to dx and dy, it adds dx to both coords of the unit's position instead of using dy. The fix is simply to subtract off dx and add +// in dy when the Y coord is passed to Map::wrap_vert. +void __cdecl +patch_neighbor_index_to_diff_for_barb_ai (int neighbor_index, int * out_x, int * out_y) +{ + neighbor_index_to_diff (neighbor_index, out_x, out_y); + is->barb_diag_patch_dy_fix = *out_y - *out_x; +} int __fastcall -patch_Unit_play_anim_for_bombard_tile (Unit * this, int edx, int x, int y) +patch_Map_wrap_vert_for_barb_ai (Map * this, int edx, int y) { - Unit * stealth_attack_target = NULL; - if (((p_bic_data->UnitTypes[this->Body.UnitTypeID].Special_Actions & UCV_Stealth_Attack) != 0) && - is->current_config.enable_stealth_attack_via_bombardment && - (! is_online_game ()) && - patch_Leader_is_tile_visible (&leaders[this->Body.CivID], __, x, y)) - is->bombard_stealth_target = select_stealth_attack_bombard_target (this, x, y); + return Map_wrap_vert (this, __, is->current_config.patch_barbarian_diagonal_bug ? (y + is->barb_diag_patch_dy_fix) : y); +} - return Unit_play_bombard_fire_animation (this, __, x, y); +int +count_workable_tiles_for_city (City * city) +{ + if (city == NULL) + return 0; + + int workable = 0; + FOR_WORK_AREA_AROUND (wai, city->Body.X, city->Body.Y) { + Tile * tile = wai.tile; + if ((tile == NULL) || (tile == p_null_tile)) + continue; + + if (tile->Body.CityAreaID != city->Body.ID) + continue; + + struct district_instance * inst = get_district_instance (tile); + if ((inst != NULL) && district_is_complete (tile, inst->district_id)) + continue; + + workable++; + } + return workable; } -void __fastcall -patch_Main_Screen_Form_issue_precision_strike_cmd (Main_Screen_Form * this, int edx, Unit * unit) +int +compute_auto_distribution_hub_goal (Leader * leader, int city_count) { - UnitType * type = &p_bic_data->UnitTypes[unit->Body.UnitTypeID]; - if ((! is->current_config.polish_precision_striking) || (type->Unit_Class == UTC_Air)) - Main_Screen_Form_issue_precision_strike_cmd (this, __, unit); - else { - // issue_precision_strike_cmd will use the unit type's operational range. To make it use bombard range instead, place that value in - // the operational range field temporarily. Conveniently, it's only necessary to do this temporary switch once, here, because the main - // screen form stores a copy of the range for its own use and the method to actually perform the strike doesn't check the range. - int saved_op_range = type->OperationalRange; - type->OperationalRange = type->Bombard_Range; - Main_Screen_Form_issue_precision_strike_cmd (this, __, unit); - type->OperationalRange = saved_op_range; + int total_unused_tiles = 0; + int stagnating_cities = 0; + int slow_growth_cities = 0; + int very_low_production_cities = 0; + int low_production_cities = 0; + + FOR_CITIES_OF (coi, leader->ID) { + City * city = coi.city; + if (city == NULL) + continue; + + int pop_size = city->Body.Population.Size; + int workable_tiles = count_workable_tiles_for_city (city); + if ((workable_tiles > 0) && (pop_size < workable_tiles)) + total_unused_tiles += workable_tiles - pop_size; + + int net_food = city->Body.FoodIncome; + if (net_food <= 0) + stagnating_cities++; + else if (net_food <= 2) + slow_growth_cities++; + + int net_shields = city->Body.ProductionIncome + city->Body.ProductionLoss; + if (net_shields < 0) + net_shields = 0; + if (net_shields <= 3) + very_low_production_cities++; + else if (net_shields <= 6) + low_production_cities++; } + + int base_desired = (city_count + 3) / 4; + + int tiles_per_chunk = is->workable_tile_count; + if (tiles_per_chunk <= 0) + tiles_per_chunk = 1; + int unused_bonus = (total_unused_tiles + tiles_per_chunk * 3 - 1) / (tiles_per_chunk * 3); + + int food_pressure = stagnating_cities * 2 + slow_growth_cities; + int food_bonus = (food_pressure + 2) / 3; + + int production_pressure = very_low_production_cities * 2 + low_production_cities; + int production_bonus = (production_pressure + 2) / 3; + + int desired = base_desired + unused_bonus + food_bonus + production_bonus; + int max_reasonable = (city_count + 1) / 2; + int max_per_100 = is->current_config.max_distribution_hub_count_per_100_cities; + if (max_per_100 > 0) { + int capped = (city_count * max_per_100 + 99) / 100; + if (capped >= 0) + max_reasonable = capped; + } + if (desired > max_reasonable) + desired = max_reasonable; + if (desired < 1) + desired = 1; + + return desired; } -int __fastcall -patch_Map_compute_neighbor_index_for_cm_strike (Map * this, int edx, int x_home, int y_home, int x_neigh, int y_neigh, int lim) +void +ai_update_distribution_hub_goal_for_leader (Leader * leader) { - int tr = Map_compute_neighbor_index (this, __, x_home, y_home, x_neigh, y_neigh, lim); + if (leader == NULL) + return; + if (! is->current_config.enable_districts || + ! is->current_config.enable_distribution_hub_districts) + return; - if ((tr >= 0) && - (is->bombarding_unit != NULL) && - ((p_bic_data->UnitTypes[is->bombarding_unit->Body.UnitTypeID].Special_Actions & UCV_Stealth_Attack) != 0) && - is->current_config.enable_stealth_attack_via_bombardment && - (! is_online_game ()) && - patch_Leader_is_tile_visible (&leaders[is->bombarding_unit->Body.CivID], __, x_neigh, y_neigh)) - is->bombard_stealth_target = select_stealth_attack_bombard_target (is->bombarding_unit, x_neigh, y_neigh); + int civ_id = leader->ID; + if ((1 << civ_id) & *p_human_player_bits) + return; + + int city_count = leader->Cities_Count; + if (city_count <= 0) + return; + + int desired = 0; + if (is->current_config.ai_distribution_hub_build_strategy == ADHBS_AUTO) + desired = compute_auto_distribution_hub_goal (leader, city_count); + else { + int ideal_per_100 = is->current_config.ai_ideal_distribution_hub_count_per_100_cities; + if (ideal_per_100 <= 0) + return; + desired = (city_count * ideal_per_100) / 100; + } + if (desired <= 0) + return; + + int current = 0; + FOR_TABLE_ENTRIES (tei, &is->distribution_hub_records) { + struct distribution_hub_record * rec = (struct distribution_hub_record *)tei.value; + if ((rec != NULL) && (rec->civ_id == civ_id)) + current++; + } + + int in_progress = 0; + FOR_TABLE_ENTRIES (tei, &is->district_tile_map) { + Tile * tile = (Tile *)tei.key; + int mapped_district_id = tei.value; + if ((tile != NULL) && + (mapped_district_id == DISTRIBUTION_HUB_DISTRICT_ID) && + ! district_is_complete (tile, DISTRIBUTION_HUB_DISTRICT_ID)) { + int owner = tile->vtable->m38_Get_Territory_OwnerID (tile); + if (owner == civ_id) + in_progress++; + } + } + + int pending = 0; + FOR_TABLE_ENTRIES (tei, &is->city_pending_district_requests[civ_id]) { + struct pending_district_request * req = (struct pending_district_request *)tei.value; + if ((req != NULL) && (req->district_id == DISTRIBUTION_HUB_DISTRICT_ID)) + pending++; + } + + int planned = current + in_progress + pending; + if (planned >= desired) + return; + + City * capital = get_city_ptr (leader->CapitalID); + bool has_capital = (capital != NULL); + int capital_x = has_capital ? capital->Body.X : 0; + int capital_y = has_capital ? capital->Body.Y : 0; + + const int yield_weight = 40; + const int capital_distance_weight = 45; + const int desired_min_capital_distance = 8; + const int proximity_penalty_scale = 300; + + while (planned < desired) { + City * best_city = NULL; + int best_tile_x = 0; + int best_tile_y = 0; + int best_score = INT_MIN; + + FOR_CITIES_OF (coi, civ_id) { + City * city = coi.city; + if (city == NULL) + continue; + if (city_has_required_district (city, DISTRIBUTION_HUB_DISTRICT_ID)) + continue; + + if (find_pending_district_request (city, DISTRIBUTION_HUB_DISTRICT_ID) != NULL) + continue; + + int tile_x, tile_y; + Tile * candidate = find_tile_for_district (city, DISTRIBUTION_HUB_DISTRICT_ID, &tile_x, &tile_y); + if (candidate == NULL) + continue; + + int yield_sum = compute_city_tile_yield_sum (city, tile_x, tile_y); + int distance_to_capital = 0; + if (has_capital) + distance_to_capital = compute_wrapped_manhattan_distance (city->Body.X, city->Body.Y, capital_x, capital_y); + + int closeness_penalty = 0; + if (has_capital && (distance_to_capital < desired_min_capital_distance)) + closeness_penalty = (desired_min_capital_distance - distance_to_capital) * proximity_penalty_scale; - return tr; -} + int score = yield_sum * yield_weight + distance_to_capital * capital_distance_weight - closeness_penalty; + if (tile_has_resource (candidate)) + score -= 500; -int __fastcall -patch_rand_bombard_target (void * this, int edx, int lim) -{ - // If we have a bombard stealth attack target set then return 2 so that the bombard damage will be applied to units not pop or buildings. - return (is->bombard_stealth_target == NULL) ? rand_int (this, __, lim) : 2; -} + if (score > best_score) { + best_score = score; + best_city = city; + best_tile_x = tile_x; + best_tile_y = tile_y; + } + } -int __fastcall -patch_rand_int_to_dodge_city_aa (void * this, int edx, int lim) -{ - int tr = rand_int (this, __, lim); - is->result_of_roll_to_dodge_city_aa = tr; - return tr; -} + if (best_city == NULL) + break; -int __fastcall -patch_Unit_get_defense_to_dodge_city_aa (Unit * this) -{ - int defense = Unit_get_defense_strength (this); - if (is->current_config.show_message_after_dodging_sam && - (defense > is->result_of_roll_to_dodge_city_aa) && - (this->Body.CivID == p_main_screen_form->Player_CivID)) - show_map_specific_text (this->Body.X, this->Body.Y, is->c3x_labels[CL_DODGED_SAM], 0); - return defense; + mark_city_needs_district (best_city, DISTRIBUTION_HUB_DISTRICT_ID); + planned++; + } } -int __fastcall -patch_Unit_get_defense_to_find_bombard_defender (Unit * this) +bool +assign_ai_fallback_production (City * city, int disallowed_improvement_id) { - // The caller is filtering out candidates with zero defense strength as possible targets to receive damage from bombardment. We can return 0 - // here to make sure "this" unit is not targeted. + if (city == NULL) + return false; - Unit * container; + City_Order new_order = { .OrderID = -1, .OrderType = 0 }; + patch_City_ai_choose_production (city, __, &new_order); - if (is->current_config.immunize_aircraft_against_bombardment && - (p_bic_data->UnitTypes[this->Body.UnitTypeID].Unit_Class == UTC_Air)) - return 0; + bool order_ok = false; + if (new_order.OrderType == COT_Improvement) { + if ((new_order.OrderID >= 0) && + (new_order.OrderID < p_bic_data->ImprovementsCount) && + (new_order.OrderID != disallowed_improvement_id) && + ! city_requires_district_for_improvement (city, new_order.OrderID, NULL) && + ! is_wonder_or_small_wonder_already_being_built (city, new_order.OrderID)) + order_ok = true; + } else if (new_order.OrderType == COT_Unit) { + if ((new_order.OrderID >= 0) && + (new_order.OrderID < p_bic_data->UnitTypeCount) && + (p_bic_data->UnitTypes[new_order.OrderID].Unit_Class == UTC_Land) && + patch_City_can_build_unit (city, __, new_order.OrderID, 1, 0, 0)) + order_ok = true; + } - else if ((is->current_config.special_helicopter_rules & SHR_NO_DEFENSE_FROM_INSIDE) && - (container = get_unit_ptr (this->Body.Container_Unit)) != NULL && - p_bic_data->UnitTypes[container->Body.UnitTypeID].Unit_Class == UTC_Air) - return 0; + char ss[200]; + snprintf (ss, sizeof ss, "assign_ai_fallback_production: Remembering fallback pending building order for city %d (%s): id %d\n", + city->Body.ID, city->Body.CityName, disallowed_improvement_id); + (*p_OutputDebugStringA) (ss); + remember_pending_building_order (city, disallowed_improvement_id); - else - return Unit_get_defense_strength (this); -} + if (order_ok) { + City_set_production (city, __, new_order.OrderType, new_order.OrderID, false); + return true; + } -int __cdecl -patch_get_WindowsFileBox_from_ini (LPCSTR key, int param_2, int param_3) -{ - // If the file path has already been determined, then avoid using the Windows file picker. This makes the later code to insert the path easier - // since we only have to intercept the opening of the civ-style file picker instead of both that and the Windows one. - if (is->load_file_path_override == NULL) - return get_int_from_conquests_ini (key, param_2, param_3); - else - return 0; -} + City_Order defensive_order = { .OrderID = -1, .OrderType = 0 }; + if (choose_defensive_unit_order (city, &defensive_order)) { + UnitType * def_type = &p_bic_data->UnitTypes[defensive_order.OrderID]; + if (def_type->Unit_Class == UTC_Land) { + City_set_production (city, __, defensive_order.OrderType, defensive_order.OrderID, false); + return true; + } + } -char const * __fastcall -patch_do_open_load_game_file_picker (void * this) -{ - if (is->load_file_path_override != NULL) { - char const * tr = is->load_file_path_override; - is->load_file_path_override = NULL; - return tr; - } else - return open_load_game_file_picker (this); + return false; } -int __fastcall -patch_show_intro_after_load_popup (void * this, int edx, int param_1, int param_2) +void __fastcall +patch_Leader_do_production_phase (Leader * this) { - if (! is->suppress_intro_after_load_popup) - return patch_show_popup (this, __, param_1, param_2); - else { - is->suppress_intro_after_load_popup = 0; - return 0; - } -} + recompute_resources_if_necessary (); -void __fastcall patch_Trade_Net_recompute_city_connections (Trade_Net * this, int edx, int civ_id, bool redo_road_network, byte param_3, int redo_roads_for_city_id); + if (is->current_config.enable_districts) { + assign_workers_for_pending_districts (this); -void * __cdecl -patch_do_load_game (char * param_1) -{ - void * tr = do_load_game (param_1); + if ((is->current_config.enable_canal_districts || is->current_config.enable_bridge_districts) && + (is->current_config.ai_builds_bridges || is->current_config.ai_builds_canals)) + assign_workers_for_ai_candidate_bridge_or_canals (this); - if (is->current_config.restore_unit_directions_on_game_load && (p_units->Units != NULL)) - for (int n = 0; n <= p_units->LastIndex; n++) { - Unit * unit = get_unit_ptr (n); - if ((unit != NULL) && (unit->Body.UnitState != UnitState_Fortifying)) { - if (Map_in_range (&p_bic_data->Map, __, unit->Body.X, unit->Body.Y) && - Map_in_range (&p_bic_data->Map, __, unit->Body.PrevMoveX, unit->Body.PrevMoveY)) { - int dx = unit->Body.X - unit->Body.PrevMoveX, dy = unit->Body.Y - unit->Body.PrevMoveY; - int dir = -1; - if ((dx == 1) && (dy == -1)) dir = DIR_NE; - else if ((dx == 2) && (dy == 0)) dir = DIR_E; - else if ((dx == 1) && (dy == 1)) dir = DIR_SE; - else if ((dx == 0) && (dy == 2)) dir = DIR_S; - else if ((dx == -1) && (dy == 1)) dir = DIR_SW; - else if ((dx == -2) && (dy == 0)) dir = DIR_W; - else if ((dx == -1) && (dy == -1)) dir = DIR_NW; - else if ((dx == 0) && (dy == -2)) dir = DIR_N; - if (dir >= 0) - unit->Body.Animation.summary.direction = dir; - } - } - } + bool ai_player = ((*p_human_player_bits & (1 << this->ID)) == 0); + int auto_dynamic_district_ids[COUNT_DISTRICT_TYPES]; + int auto_dynamic_district_count = 0; - // Apply era aliases - for (int n = 0; n < 32; n++) - if (*p_player_bits & (1 << n)) - apply_era_specific_names (&leaders[n]); + // For dynamic districts, the AI will never be triggered to build them if they have no dependent buildings. + // Determine which dynamic districts the AI could build. In the city loop after, mark which cities need them + if (ai_player) { + for (int district_id = is->special_district_count; district_id < is->district_count; district_id++) { + struct district_config * cfg = &is->district_configs[district_id]; + struct district_infos * info = &is->district_infos[district_id]; - if (is->current_config.apply_grid_ini_setting_on_game_load) { - int grid_on = get_int_from_conquests_ini ("GridOn", 0, 0); - Map_Renderer * mr = &p_bic_data->Map.Renderer; - if (grid_on && ! mr->MapGrid_Flag) - mr->vtable->m68_Toggle_Grid (mr); - } + if (info->dependent_building_count > 0) continue; + if (cfg->command == -1) continue; - return tr; -} + if (! leader_can_build_district (this, district_id)) + continue; -void * -load_game_ex (char const * file_path, int suppress_intro_popup) -{ - is->suppress_intro_after_load_popup = suppress_intro_popup; - is->load_file_path_override = file_path; - return patch_do_load_game (NULL); -} + if (auto_dynamic_district_count < ARRAY_LEN (auto_dynamic_district_ids)) + auto_dynamic_district_ids[auto_dynamic_district_count++] = district_id; + } + } -int __fastcall -patch_show_movement_phase_popup (void * this, int edx, int param_1, int param_2) -{ - int tr = patch_show_popup (this, __, param_1, param_2); + // Special exception for AI to build Central Rail Hub, which is available in Industrial era but Mass Transit, + // the only building dependent on it, is in the Modern era. Without this the AI wouldn't build in Industrial era. + if (is->current_config.enable_central_rail_hub_districts) { + if (leader_can_build_district (this, CENTRAL_RAIL_HUB_DISTRICT_ID)) + auto_dynamic_district_ids[auto_dynamic_district_count++] = CENTRAL_RAIL_HUB_DISTRICT_ID; + } - int player_civ_id = p_main_screen_form->Player_CivID; - int replay_for_players = is->replay_for_players; // Store this b/c it gets reset on game load - if (replay_for_players & 1<showing_hotseat_replay = true; + if (is->current_config.enable_distribution_hub_districts) { + if (leader_can_build_district (this, DISTRIBUTION_HUB_DISTRICT_ID)) + ai_update_distribution_hub_goal_for_leader (this); + } - patch_do_save_game (hotseat_resume_save_path, 1, 0); - load_game_ex (hotseat_replay_save_path, 1); - p_main_screen_form->Player_CivID = player_civ_id; + FOR_CITIES_OF (coi, this->ID) { + City * city = coi.city; + if (city == NULL) continue; - // Re-enable the GUI so the minimap is visible during the replay. We must also reset the minimap & redraw it so that it reflects the - // player we just seated (above) instead of leftover data from the last player. - Main_GUI * main_gui = &p_main_screen_form->GUI; - main_gui->is_enabled = 1; - Navigator_Data_reset (&main_gui->Navigator_Data); - main_gui->Base.vtable->m73_call_m22_Draw ((Base_Form *)main_gui); + bool at_neighborhood_cap = is->current_config.enable_neighborhood_districts && city_is_at_neighborhood_cap (city); - perform_interturn (); - load_game_ex (hotseat_resume_save_path, 1); - p_main_screen_form->is_now_loading_game = 0; + // Mark any needed dynamic districts for AI players. This isn't the most intelligent approach (we're not weighing district benefits), + // but it's simple and works reasonably well + if (ai_player && (auto_dynamic_district_count > 0)) { + for (int i = 0; i < auto_dynamic_district_count; i++) { + int district_id = auto_dynamic_district_ids[i]; - // Restore the replay_for_players variable b/c it gets cleared when loading a game. Also mask out the bit for the player we just - // showed the replay to. - is->replay_for_players = replay_for_players & ~(1<showing_hotseat_replay = false; - } + int target_x = 0, target_y = 0; + if (find_tile_for_district (city, district_id, &target_x, &target_y) == NULL) + continue; - return tr; -} + mark_city_needs_district (city, district_id); + } + } -// Returns a set of player bits containing only those players that are human and can see at least one AI unit. For speed and simplicity, does not -// account for units' invisibility ability, units are considered visible as long as they're on a visible tile. -int -find_human_players_seeing_ai_units () -{ - int tr = 0; - Map * map = &p_bic_data->Map; - if (map->Tiles != NULL) - for (int n_tile = 0; n_tile < map->TileCount; n_tile++) { - Tile * tile = map->Tiles[n_tile]; - Tile_Body * body = &tile->Body; - int human_vis_bits = (body->FOWStatus | body->V3 | body->Visibility | body->field_D0_Visibility) & *p_human_player_bits; - if (human_vis_bits != 0) // If any human players can see this tile - for (int n_player = 0; n_player < 32; n_player++) - if (human_vis_bits & 1<TileUnitID, &unused); - Unit * unit = get_unit_ptr (unit_id); - if ((unit != NULL) && - ((*p_human_player_bits & 1<Body.CivID) == 0)) { - tr |= 1<current_config.replay_ai_moves_in_hotseat_games && - (*p_is_offline_mp_game && ! *p_is_pbem_game); // offline MP but not PBEM => we're in a hotseat game - int ai_unit_vis_before; - if (save_replay) { - ai_unit_vis_before = find_human_players_seeing_ai_units (); - int toggleable_rules = *p_toggleable_rules; - *p_toggleable_rules |= TR_PRESERVE_RANDOM_SEED; // Make sure preserve random seed is on for the replay save - patch_do_save_game (hotseat_replay_save_path, 1, 0); - *p_toggleable_rules = toggleable_rules; - } + if (city->Body.Order_Type == COT_Unit) { + int unit_id = city->Body.Order_ID; + int req_district_id = -1; + bool needs_halt = false; + + if ((unit_id >= 0) && (unit_id < p_bic_data->UnitTypeCount)) { + UnitType * type = &p_bic_data->UnitTypes[unit_id]; + if ((type->Unit_Class == UTC_Air) && + is->current_config.enable_aerodrome_districts && + is->current_config.air_units_use_aerodrome_districts_not_cities && + (! city_has_required_district (city, AERODROME_DISTRICT_ID))) { + req_district_id = AERODROME_DISTRICT_ID; + needs_halt = true; + } else if ((type->Unit_Class == UTC_Sea) && + is->current_config.enable_port_districts && + is->current_config.naval_units_use_port_districts_not_cities && + (! city_has_required_district (city, PORT_DISTRICT_ID))) { + req_district_id = PORT_DISTRICT_ID; + needs_halt = true; + } + } - is->players_saw_ai_unit = 0; // Clear bits. After perform_interturn, each set bit will indicate a player that has seen an AI unit move - long long ts_before; - QueryPerformanceCounter ((LARGE_INTEGER *)&ts_before); - is->time_spent_paused_during_popup = 0; - is->time_spent_computing_city_connections = 0; - is->count_calls_to_recompute_city_connections = 0; - unsigned saved_prefs = *p_preferences; - if (is->current_config.measure_turn_times) - *p_preferences &= ~(P_ANIMATE_BATTLES | P_SHOW_FRIEND_MOVES | P_SHOW_ENEMY_MOVES); + if (needs_halt) { + char ss[200]; + snprintf (ss, sizeof ss, "patch_Leader_do_production_phase: City %d (%s) halting unit %d due to missing district %d\n", + city->Body.ID, city->Body.CityName, unit_id, req_district_id); + (*p_OutputDebugStringA) (ss); - perform_interturn (); + if (ai_player) + mark_city_needs_district (city, req_district_id); - if (is->current_config.day_night_cycle_mode) { - if (is->day_night_cycle_img_state == IS_OK) { - int new_hour = calculate_current_day_night_cycle_hour (); - if (new_hour != is->current_day_night_cycle) { - is->current_day_night_cycle = new_hour; - p_main_screen_form->vtable->m73_call_m22_Draw ((Base_Form *)p_main_screen_form); + City_Order defensive_order = { .OrderID = -1, .OrderType = 0 }; + if (choose_defensive_unit_order (city, &defensive_order)) { + UnitType * def_type = &p_bic_data->UnitTypes[defensive_order.OrderID]; + if (def_type->Unit_Class == UTC_Land) + City_set_production (city, __, defensive_order.OrderType, defensive_order.OrderID, false); + } + + continue; + } } - } - } - if (is->current_config.enable_city_work_radii_highlights && is->highlight_city_radii) { - is->highlight_city_radii = false; - clear_highlighted_worker_tiles_for_districts (); - p_main_screen_form->vtable->m73_call_m22_Draw ((Base_Form *)p_main_screen_form); - } + if (city->Body.Order_Type != COT_Improvement) continue; + int i_improv = city->Body.Order_ID; - if (is->current_config.measure_turn_times) { - long long ts_after; - QueryPerformanceCounter ((LARGE_INTEGER *)&ts_after); - long long perf_freq; - QueryPerformanceFrequency ((LARGE_INTEGER *)&perf_freq); - int turn_time_in_ms = 1000 * (ts_after - ts_before - is->time_spent_paused_during_popup) / perf_freq; - int city_con_time_in_ms = 1000 * is->time_spent_computing_city_connections / perf_freq; - int road_time_in_ms = 1000 * is->time_spent_filling_roads / perf_freq; + // Check if production needs to be halted due to missing district + int req_district_id = -1; + char const * district_description = NULL; + bool needs_halt = false; - PopupForm * popup = get_popup_form (); - popup->vtable->set_text_key_and_flags (popup, __, is->mod_script_path, "C3X_INFO", -1, 0, 0, 0); - char msg[1000]; + // Check buildings & wonders dependent on districts + if (city_requires_district_for_improvement (city, i_improv, &req_district_id)) { + if (req_district_id >= 0) { + needs_halt = true; + district_description = is->district_configs[req_district_id].name; + } + } - struct c3x_opt { - bool is_active; - char * name; - } opts[] = {{is->current_config.optimize_improvement_loops, "improv. loops"}, - {is->current_config.enable_trade_net_x && (is->tnx_init_state == IS_OK), "trade net x"}}; - char opt_list[1000]; - memset (opt_list, 0, sizeof opt_list); - strncpy (opt_list, "^C3X optimizations: ", sizeof opt_list); - bool any_active_opts = false; - for (int n = 0; n < ARRAY_LEN (opts); n++) - if (opts[n].is_active) { - char * cursor = &opt_list[strlen (opt_list)]; - snprintf (cursor, opt_list + (sizeof opt_list) - cursor, "%s%s", any_active_opts ? ", " : "", opts[n].name); - any_active_opts = true; + // Wonders + char ss[200]; + if (is->current_config.enable_wonder_districts) { + snprintf (ss, sizeof ss, "patch_Leader_do_production_phase: Checking wonder improv %d for city %d (%s)\n", + i_improv, city->Body.ID, city->Body.CityName); + (*p_OutputDebugStringA) (ss); + if (city_is_currently_building_wonder (city)) { + snprintf (ss, sizeof ss, "patch_Leader_do_production_phase: City %d (%s) is building wonder improv %d\n", + city->Body.ID, city->Body.CityName, i_improv); + (*p_OutputDebugStringA) (ss); + bool wonder_requires_district = false; + if (i_improv >= 0) { + struct district_building_prereq_list * prereq_list = get_district_building_prereq_list (i_improv); + if (district_building_prereq_list_contains (prereq_list, WONDER_DISTRICT_ID)) + wonder_requires_district = true; + } + + if (wonder_requires_district) { + bool has_wonder_district = reserve_wonder_district_for_city (city, i_improv); + if (! has_wonder_district) { + snprintf (ss, sizeof ss, "patch_Leader_do_production_phase: City %d (%s) lacks Wonder District for wonder improv %d\n", + city->Body.ID, city->Body.CityName, i_improv); + (*p_OutputDebugStringA) (ss); + needs_halt = true; + req_district_id = WONDER_DISTRICT_ID; + district_description = "Wonder District"; + } + } else { + release_wonder_district_reservation (city); + snprintf (ss, sizeof ss, "patch_Leader_do_production_phase: City %d (%s) wonder improv %d does not require Wonder District\n", + city->Body.ID, city->Body.CityName, i_improv); + (*p_OutputDebugStringA) (ss); + } + } else { + release_wonder_district_reservation (city); + } } - if (! any_active_opts) { - char * cursor = &opt_list[strlen (opt_list)]; - strncpy (cursor, "None", opt_list + (sizeof opt_list) - cursor); - } - PopupForm_add_text (popup, __, (char *)opt_list, false); - snprintf (msg, sizeof msg, "^Turn time: %d.%03d sec", turn_time_in_ms/1000, turn_time_in_ms%1000); - PopupForm_add_text (popup, __, (char *)msg, false); - snprintf (msg, sizeof msg, "^ Recomputing city connections: %d.%03d sec (%d calls)", - city_con_time_in_ms/1000, city_con_time_in_ms%1000, - is->count_calls_to_recompute_city_connections); - PopupForm_add_text (popup, __, (char *)msg, false); - snprintf (msg, sizeof msg, "^ Flood filling road network: %d.%03d sec", - road_time_in_ms/1000, road_time_in_ms%1000); - PopupForm_add_text (popup, __, (char *)msg, false); - patch_show_popup (popup, __, 0, 0); + // If production needs to be halted, handle the reassignment and messaging + if (needs_halt) { + snprintf (ss, sizeof ss, "patch_Leader_do_production_phase: City %d (%s) halting improv %d due to missing district %d\n", + city->Body.ID, city->Body.CityName, i_improv, req_district_id); + (*p_OutputDebugStringA) (ss); + // Switch production to another option + if (ai_player) { + mark_city_needs_district (city, req_district_id); + assign_ai_fallback_production (city, i_improv); + } else { + City_Order defensive_order = { .OrderID = -1, .OrderType = 0 }; + if (choose_defensive_unit_order (city, &defensive_order)) { + UnitType * def_type = &p_bic_data->UnitTypes[defensive_order.OrderID]; + if (def_type->Unit_Class == UTC_Land) + City_set_production (city, __, defensive_order.OrderType, defensive_order.OrderID, false); + } + } - *p_preferences = saved_prefs; + // Show message to human player + if (! ai_player && (city->Body.CivID == p_main_screen_form->Player_CivID)) { + char msg[160]; + char const * bname = p_bic_data->Improvements[i_improv].Name.S; + snprintf (msg, sizeof msg, "%s %s %s", bname, is->c3x_labels[CL_CONSTRUCTION_HALTED_DUE_TO_MISSING_DISTRICT], district_description); + msg[(sizeof msg) - 1] = '\0'; + show_map_specific_text (city->Body.X, city->Body.Y, msg, true); + } + continue; + } + snprintf (ss, sizeof ss, "patch_Leader_do_production_phase: City %d (%s) improv %d passed missing district check\n", + city->Body.ID, city->Body.CityName, i_improv); + (*p_OutputDebugStringA) (ss); + } } - if (save_replay) { - int last_human_player_bit = 0; { - for (int n = 0; n < 32; n++) - if (*p_human_player_bits & 1<current_config.enable_city_capture_by_barbarians && (this->ID == 0)) { + int any_barb_cities = 0; + FOR_CITIES_OF (coi, this->ID) { + any_barb_cities = 1; + break; } + if (any_barb_cities) + is->force_barb_activity_for_cities = 1; + } - // Set player bits indicating which players should see the replay. This includes all human players that could see an AI unit at the - // start of the interturn or that saw an AI unit move or bombard during the interturn, excluding the last human player. - is->replay_for_players = (ai_unit_vis_before | (is->players_saw_ai_unit & *p_human_player_bits)) & ~last_human_player_bit; + // If barbs are force-activated, make sure their activity level is at least 1 (sedentary). + int * p_barb_activity = &p_bic_data->Map.World.Final_Barbarians_Activity; + int saved_barb_activity = *p_barb_activity; + if (is->force_barb_activity_for_cities && (*p_barb_activity <= 0)) + *p_barb_activity = 1; + + Leader_do_production_phase (this); + + if (is->force_barb_activity_for_cities) { + *p_barb_activity = saved_barb_activity; + is->force_barb_activity_for_cities = 0; } } -void __cdecl -patch_initialize_map_music (int civ_id, int era_id, bool param_3) +// This function counts the number of players in the game. The return value will be compared to the number of cities on the map to determine if there +// are enough cities (per player) to unlock barb production. If barb activity is forced on, return zero so that the check always passes. +int __cdecl +patch_count_player_bits_for_barb_prod (unsigned int bit_field) { - if (! is->showing_hotseat_replay) - initialize_map_music (civ_id, era_id, param_3); + return is->force_barb_activity_for_cities ? 0 : count_set_bits (bit_field); } -void __stdcall -patch_deinitialize_map_music () +Tile * __fastcall +patch_Map_get_tile_to_check_visibility (Map * this, int edx, int index) { - if (! is->showing_hotseat_replay) - deinitialize_map_music (); + Tile * tr = Map_get_tile (this, __, index); + int is_hotseat_game = *p_is_offline_mp_game && ! *p_is_pbem_game; + if (is_hotseat_game && is->current_config.share_visibility_in_hotseat) { + int human_bits = *p_human_player_bits; + is->dummy_tile->Body.Fog_Of_War = tr->Body.Fog_Of_War | ((tr->Body.Fog_Of_War & human_bits) != 0 ? human_bits : 0); + is->dummy_tile->Body.FOWStatus = tr->Body.FOWStatus | ((tr->Body.FOWStatus & human_bits) != 0 ? human_bits : 0); + is->dummy_tile->Body.V3 = tr->Body.V3 | ((tr->Body.V3 & human_bits) != 0 ? human_bits : 0); + is->dummy_tile->Body.Visibility = tr->Body.Visibility | ((tr->Body.Visibility & human_bits) != 0 ? human_bits : 0); + is->dummy_tile->Body.field_D0_Visibility = tr->Body.field_D0_Visibility | ((tr->Body.field_D0_Visibility & human_bits) != 0 ? human_bits : 0); + tr = is->dummy_tile; + } + is->tile_returned_for_visibility_check = tr; + return tr; } -void __fastcall -patch_Fighter_do_bombard_tile (Fighter * this, int edx, Unit * unit, int neighbor_index, int mp_tile_x, int mp_tile_y) +Tile * __fastcall +patch_Map_get_tile_to_check_visibility_again (Map * this, int edx, int index) { - // Unit::score_kill will be called if the bombarder destroys its target, and that is the only way score_kill can be called while this method - // is running. So if we're configured to stop enslaving from bombard, turn off enslaving while it's running. - is->do_not_enslave_units = is->current_config.prevent_enslaving_by_bombardment; + return is->tile_returned_for_visibility_check; +} - // Check if we're going to do PTW-like targeting, if not fall back on the base game's do_bombard_tile method. We'll also fall back on that - // method in the case where we're in an online game and the bombard can't happen b/c the tile is occupied by another battle. In that case, no - // bombard is possible but we'll call the base method anyway since it will show a little message saying as much. - if (itable_look_up_or (&is->current_config.ptw_arty_types, unit->Body.UnitTypeID, 0) && - (is->bombard_stealth_target == NULL) && - ! (is_online_game () && mp_check_current_combat (p_mp_object, __, mp_tile_x, mp_tile_y))) { +// Same as above except this method uses the FOWStatus field instead of Fog_Of_War +Tile * __fastcall +patch_Map_get_tile_for_fow_status_check (Map * this, int edx, int index) +{ + Tile * tile = Map_get_tile (this, __, index); + int is_hotseat_game = *p_is_offline_mp_game && ! *p_is_pbem_game; + if (is_hotseat_game && is->current_config.share_visibility_in_hotseat) { + is->dummy_tile->Body.FOWStatus = ((tile->Body.FOWStatus & *p_human_player_bits) != 0) << p_main_screen_form->Player_CivID; + return is->dummy_tile; + } else + return tile; +} - City * city; { - int dx, dy; - neighbor_index_to_diff (neighbor_index, &dx, &dy); - int tile_x = unit->Body.X + dx, tile_y = unit->Body.Y + dy; - wrap_tile_coords (&p_bic_data->Map, &tile_x, &tile_y); - city = city_at (tile_x, tile_y); - } +Tile * __cdecl +patch_tile_at_to_check_visibility (int x, int y) +{ + return patch_Map_get_tile_to_check_visibility (&p_bic_data->Map, __, (p_bic_data->Map.Width >> 1) * y + (x >> 1)); +} - int rv; - if ((city != NULL) && ((rv = rand_int (p_rand_object, __, 3)) < 2)) - Fighter_damage_city_by_bombardment (this, __, unit, city, rv, 0); - else - Fighter_do_bombard_tile (this, __, unit, neighbor_index, mp_tile_x, mp_tile_y); +Tile * __cdecl +patch_tile_at_to_check_visibility_again (int x, int y) +{ + return is->tile_returned_for_visibility_check; +} + +unsigned __fastcall +patch_Tile_m42_Get_Overlays (Tile * this, int edx, byte visible_to_civ) +{ + unsigned base_vis_overlays = Tile_m42_Get_Overlays (this, __, visible_to_civ); + if ((visible_to_civ != 0) && // if we're seeing from a player's persp. instead of seeing the actual overlays AND + is->current_config.share_visibility_in_hotseat && // shared hotseat vis is enabled AND + ((1 << visible_to_civ) & *p_human_player_bits) && // the perspective is of a human player AND + (base_vis_overlays != this->Overlays) && // that player can't already see all the actual overlays AND + (*p_is_offline_mp_game && ! *p_is_pbem_game)) { // we're in a hotseat game + + // Check if there's another human player that can see the actual overlays. If so, give that info to this player and return it. + unsigned player_bits = *(unsigned *)p_human_player_bits >> 1; + int n_player = 1; + while (player_bits != 0) { + if ((player_bits & 1) && (n_player != visible_to_civ) && + (Tile_m42_Get_Overlays (this, __, n_player) == this->Overlays)) { + this->Body.Visibile_Overlays[visible_to_civ] = this->Overlays; + return this->Overlays; + } + player_bits >>= 1; + n_player++; + } + return base_vis_overlays; } else - Fighter_do_bombard_tile (this, __, unit, neighbor_index, mp_tile_x, mp_tile_y); - - is->do_not_enslave_units = false; + return base_vis_overlays; } -bool __fastcall -patch_Unit_check_king_for_defense_priority (Unit * this, int edx, enum UnitTypeAbilities king_ability) +int __fastcall +patch_Tile_check_water_for_sea_zoc (Tile * this) { - return (! is->current_config.ignore_king_ability_for_defense_priority) || (*p_toggleable_rules & (TR_REGICIDE | TR_MASS_REGICIDE)) ? - Unit_has_ability (this, __, king_ability) : - false; + if ((is->current_config.special_zone_of_control_rules & (SZOCR_AMPHIBIOUS | SZOCR_AERIAL)) == 0) + return this->vtable->m35_Check_Is_Water (this); + else + return 1; // The caller will skip ZoC logic if this is a land tile without a city because the targeted unit is a sea unit. Instead + // return 1, so all tiles are considered sea tiles, so we can run the ZoC logic for land units or air units on land. } -void WINAPI -patch_get_local_time_for_unit_ini (LPSYSTEMTIME lpSystemTime) +int __fastcall +patch_Tile_check_water_for_land_zoc (Tile * this) { - GetLocalTime (lpSystemTime); - if (is->current_config.no_elvis_easter_egg && (lpSystemTime->wMonth == 1) && (lpSystemTime->wDay == 8)) - lpSystemTime->wDay = 9; + // Same as above except this time we want to consider all tiles to be land + return ((is->current_config.special_zone_of_control_rules & (SZOCR_AMPHIBIOUS | SZOCR_AERIAL)) == 0) ? + this->vtable->m35_Check_Is_Water (this) : + 0; } -bool __fastcall -patch_Leader_could_buy_tech_for_trade_screen (Leader * this, int edx, int tech_id, int from_civ_id) + +int __fastcall +patch_Unit_get_attack_strength_for_sea_zoc (Unit * this) { - // Temporarily remove the untradable flag so this tech is listed on the screen instead of skipped over. After all the items have been - // assembled, we'll go back and disable the untradable techs. - if (is->current_config.show_untradable_techs_on_trade_screen) { - int saved_flags = p_bic_data->Advances[tech_id].Flags; - p_bic_data->Advances[tech_id].Flags &= ~ATF_Cannot_Be_Traded; - bool tr = this->vtable->could_buy_tech (this, __, tech_id, from_civ_id); - p_bic_data->Advances[tech_id].Flags = saved_flags; - return tr; + return (p_bic_data->UnitTypes[this->Body.UnitTypeID].Unit_Class == UTC_Sea) ? Unit_get_attack_strength (this) : 0; +} - } else - return this->vtable->could_buy_tech (this, __, tech_id, from_civ_id); +int __fastcall +patch_Unit_get_attack_strength_for_land_zoc (Unit * this) +{ + return (p_bic_data->UnitTypes[this->Body.UnitTypeID].Unit_Class == UTC_Land) ? Unit_get_attack_strength (this) : 0; } -void __fastcall -patch_DiploForm_assemble_tradable_items (DiploForm * this) +Unit * __fastcall +patch_Main_Screen_Form_find_visible_unit (Main_Screen_Form * this, int edx, int tile_x, int tile_y, Unit * excluded) { - DiploForm_assemble_tradable_items (this); + struct unit_display_override * override = &is->unit_display_override; + if ((override->unit_id >= 0) && (override->tile_x == tile_x) && (override->tile_y == tile_y)) { + Unit * unit = get_unit_ptr (override->unit_id); + if (unit != NULL) { + if ((unit->Body.X == tile_x) && (unit->Body.Y == tile_y)) + return unit; + } + } - // Disable (gray out) all untradable techs - if (is->current_config.show_untradable_techs_on_trade_screen) - for (int n = 0; n < p_bic_data->AdvanceCount; n++) - if (p_bic_data->Advances[n].Flags & ATF_Cannot_Be_Traded) { - this->tradable_technologies[n].can_be_bought = 0; - this->tradable_technologies[n].can_be_sold = 0; - } + return Main_Screen_Form_find_visible_unit (this, __, tile_x, tile_y, excluded); } -bool __fastcall -patch_City_can_trade_via_water (City * this) +void __fastcall +patch_Animator_play_zoc_animation (Animator * this, int edx, Unit * unit, AnimationType anim_type, bool param_3) { - if (is->current_config.optimize_improvement_loops) { - for (int n = 0; n < is->water_trade_improvs.count; n++) - if (has_active_building (this, is->water_trade_improvs.items[n])) - return true; - return false; - } else - return City_can_trade_via_water (this); + if (p_bic_data->UnitTypes[unit->Body.UnitTypeID].Unit_Class != UTC_Air) + Animator_play_one_shot_unit_animation (this, __, unit, anim_type, param_3); } bool __fastcall -patch_City_can_trade_via_air (City * this) +patch_Fighter_check_zoc_anim_visibility (Fighter * this, int edx, Unit * attacker, Unit * defender, bool param_3) { - if (is->current_config.optimize_improvement_loops) { - for (int n = 0; n < is->air_trade_improvs.count; n++) - if (has_active_building (this, is->air_trade_improvs.items[n])) - return true; + // If we've reached this point in the code (in the calling method) then a unit has been selected to exert zone of control and it has passed + // its dice roll to cause damage. Stash its pointer for possible use later. + is->zoc_interceptor = attacker; + + // If an air unit was selected, pre-emptively undo the damage from ZoC since we'll want to run our own bit of logic to do that (the air unit + // may still get shot down). Return false from this function to skip over all of the animation logic in the caller since it wouldn't work for + // aircraft. + if (p_bic_data->UnitTypes[attacker->Body.UnitTypeID].Unit_Class == UTC_Air) { + defender->Body.Damage -= 1; return false; - } else - return City_can_trade_via_air (this); -} -int __fastcall -patch_City_get_building_defense_bonus (City * this) -{ - if (is->current_config.optimize_improvement_loops) { - int tr = 0; - int is_size_level_1 = (this->Body.Population.Size <= p_bic_data->General.MaximumSize_City) && - (this->Body.Population.Size <= p_bic_data->General.MaximumSize_Town); - for (int n = 0; n < is->combat_defense_improvs.count; n++) { - int improv_id = is->combat_defense_improvs.items[n]; - Improvement * improv = &p_bic_data->Improvements[improv_id]; - if ((is_size_level_1 || (improv->Combat_Bombard == 0)) && has_active_building (this, improv_id)) { - int multiplier; - if ((improv->Combat_Bombard > 0) && - (patch_Leader_count_any_shared_wonders_with_flag (&leaders[(this->Body).CivID], __, ITW_Doubles_City_Defenses, NULL) > 0)) - multiplier = 2; - else - multiplier = 1; + // Repeat a check done by the caller. We've deleted this check to ensure that this function always gets called so we can grab the interceptor. + } else if (attacker->Body.Animation.field_111 == 0) + return false; - int building_defense = multiplier * improv->Combat_Defence; - if (building_defense > tr) - tr = building_defense; + else { + bool tr = Fighter_check_combat_anim_visibility (this, __, attacker, defender, param_3); + + // If necessary, set up to ensure the unit's attack animation is visible. This means forcing it to the top of its stack and + // temporarily unfortifying it if it's fortified. (If it's fortified, the animation is occasionally not visible. Don't know why.) + if (tr && is->current_config.show_zoc_attacks_from_mid_stack) { + is->unit_display_override = (struct unit_display_override) { attacker->Body.ID, attacker->Body.X, attacker->Body.Y }; + if (attacker->Body.UnitState == UnitState_Fortifying) { + Unit_set_state (attacker, __, 0); + is->refortify_interceptor_after_zoc = true; } } + return tr; - } else - return City_get_building_defense_bonus (this); + } } -bool __fastcall -patch_City_shows_harbor_icon (City * this) +int __fastcall +patch_City_sum_buildings_naval_power_for_zoc (City * this) { - return is->current_config.city_icons_show_unit_effects_not_trade ? - City_count_improvements_with_flag (this, __, ITF_Veteran_Sea_Units) > 0 : - patch_City_can_trade_via_water (this); + // Cancel out city's naval power if the unit has only one HP left. This prevents coastal fortresses from knocking that last hit point off + // passing units when lethal ZoC is enabled, because to make that work we delete an earlier check excusing 1-HP units from ZoC. + if ((is->zoc_defender != NULL) && + ((Unit_get_max_hp (is->zoc_defender) - is->zoc_defender->Body.Damage) <= 1)) + return 0; + + else + return City_sum_buildings_naval_power (this); } -bool __fastcall -patch_City_shows_airport_icon (City * this) +void __fastcall +patch_Fighter_apply_zone_of_control (Fighter * this, int edx, Unit * unit, int from_x, int from_y, int to_x, int to_y) { - return is->current_config.city_icons_show_unit_effects_not_trade ? - City_count_improvements_with_flag (this, __, ITF_Veteran_Air_Units) > 0 : - patch_City_can_trade_via_air (this); + is->zoc_interceptor = NULL; + is->zoc_defender = unit; + is->refortify_interceptor_after_zoc = false; + struct unit_display_override saved_udo = is->unit_display_override; + Fighter_apply_zone_of_control (this, __, unit, from_x, from_y, to_x, to_y); + + // Actually exert ZoC if an air unit managed to do so. + if ((is->zoc_interceptor != NULL) && (p_bic_data->UnitTypes[is->zoc_interceptor->Body.UnitTypeID].Unit_Class == UTC_Air)) { + bool intercepted = Unit_try_flying_over_tile (is->zoc_interceptor, __, from_x, from_y); + if (! intercepted) { + Unit_play_bombing_animation (is->zoc_interceptor, __, from_x, from_y); + unit->Body.Damage = not_below (0, unit->Body.Damage + 1); + } + } + + if (is->refortify_interceptor_after_zoc) + Unit_set_state (is->zoc_interceptor, __, UnitState_Fortifying); + is->unit_display_override = saved_udo; } +// These two patches replace two function calls in Unit::move_to_adjacent_tile that come immediately after the unit has been subjected to zone of +// control. These calls recheck that the move is valid, not sure why. Here they're patched to indicate that the move in invalid when the unit was +// previously killed by ZoC. This causes move_to_adjacent_tile to return early without running the code that would place the unit on the destination +// tile and, for example, capturing an enemy city there. int __fastcall -patch_Unit_eval_escort_requirement (Unit * this) +patch_Trade_Net_get_move_cost_after_zoc (Trade_Net * this, int edx, int from_x, int from_y, int to_x, int to_y, Unit * unit, int civ_id, unsigned param_7, int neighbor_index, Trade_Net_Distance_Info * dist_info) { - UnitType * type = &p_bic_data->UnitTypes[this->Body.UnitTypeID]; - int ai_strat = type->AI_Strategy; - bool owned_by_ai = (*p_human_player_bits & 1<Body.CivID) == 0; // We must not reduce the escort requirement for human-owned units - // because that will interfere with group movement of units. - - // Apply special escort rules - if (owned_by_ai && is->current_config.dont_escort_unflagged_units && ! UnitType_has_ability (type, __, UTA_Requires_Escort)) - return 0; - else if (owned_by_ai && is->current_config.use_offensive_artillery_ai && (ai_strat & UTAI_Artillery)) - return 1; - - else { - int base = Unit_eval_escort_requirement (this); - if (owned_by_ai && (ai_strat & (UTAI_Naval_Transport | UTAI_Naval_Carrier | UTAI_Naval_Missile_Transport))) - return not_above (is->current_config.max_ai_naval_escorts, base); - else - return base; - } + return ((is->current_config.special_zone_of_control_rules & SZOCR_LETHAL) == 0) || ((Unit_get_max_hp (unit) - unit->Body.Damage) > 0) ? + patch_Trade_Net_get_movement_cost (this, __, from_x, from_y, to_x, to_y, unit, civ_id, param_7, neighbor_index, dist_info) : + -1; } -bool __fastcall -patch_Unit_has_enough_escorters_present (Unit * this) +AdjacentMoveValidity __fastcall +patch_Unit_can_move_after_zoc (Unit * this, int edx, int neighbor_index, int param_2) { - UnitType * type = &p_bic_data->UnitTypes[this->Body.UnitTypeID]; - if (is->current_config.dont_escort_unflagged_units && ! UnitType_has_ability (type, __, UTA_Requires_Escort)) - return true; - else - return Unit_has_enough_escorters_present (this); + return ((is->current_config.special_zone_of_control_rules & SZOCR_LETHAL) == 0) || ((Unit_get_max_hp (this) - this->Body.Damage) > 0) ? + patch_Unit_can_move_to_adjacent_tile (this, __, neighbor_index, param_2) : + AMV_1; } -void __fastcall -patch_Unit_check_escorter_health (Unit * this, int edx, bool * has_any_escort_present, bool * any_escorter_cant_heal) +// Checks unit's HP after it was possibly hit by ZoC and deals with the consequences if it's dead. Does nothing if config option to make ZoC lethal +// isn't set or if interceptor is NULL. Returns true if the unit was killed, false otherwise. +bool +check_life_after_zoc (Unit * unit, Unit * interceptor) { - UnitType * type = &p_bic_data->UnitTypes[this->Body.UnitTypeID]; - if (is->current_config.dont_escort_unflagged_units && ! UnitType_has_ability (type, __, UTA_Requires_Escort)) { - *has_any_escort_present = true; - *any_escorter_cant_heal = true; // Returning true here indicates the unit should not stop to wait for its escorter(s) to heal. + if ((is->current_config.special_zone_of_control_rules & SZOCR_LETHAL) && (interceptor != NULL) && + ((Unit_get_max_hp (unit) - unit->Body.Damage) <= 0)) { + + // Call Unit::score_kill but turn off enslaving if we're configured to stop enslaving after bombardment and the ZoC was performed by + // bombardment, which is always the case for cross-domain ZoC or when bombard str > attack. + UnitType * interceptor_type = &p_bic_data->UnitTypes[interceptor->Body.UnitTypeID]; + if (is->current_config.prevent_enslaving_by_bombardment && + ((p_bic_data->UnitTypes[unit->Body.UnitTypeID].Unit_Class != interceptor_type->Unit_Class) || + (interceptor_type->Bombard_Strength >= Unit_get_attack_strength (interceptor)))) + is->do_not_enslave_units = true; + Unit_score_kill (interceptor, __, unit, false); + is->do_not_enslave_units = false; + + if ((! is_online_game ()) && Fighter_check_combat_anim_visibility (&p_bic_data->fighter, __, interceptor, unit, true)) + Animator_play_one_shot_unit_animation (&p_main_screen_form->animator, __, unit, AT_DEATH, false); + + bool prev_always_despawn_passengers = is->always_despawn_passengers; + is->always_despawn_passengers = is_land_transport (unit) && (is->current_config.land_transport_rules & LTR_NO_ESCAPE); + patch_Unit_despawn (unit, __, interceptor->Body.CivID, 0, 0, 0, 0, 0, 0); + is->always_despawn_passengers = prev_always_despawn_passengers; + + return true; } else - Unit_check_escorter_health (this, __, has_any_escort_present, any_escorter_cant_heal); + return false; } -void __fastcall -patch_Leader_unlock_technology (Leader * this, int edx, int tech_id, bool param_2, bool param_3, bool param_4) +int __fastcall +patch_Unit_move_to_adjacent_tile (Unit * this, int edx, int neighbor_index, bool param_2, int param_3, byte param_4) { - int * p_stack = (int *)&tech_id; - int ret_addr = p_stack[-1]; + is->moving_unit_to_adjacent_tile = true; - Leader_unlock_technology (this, __, tech_id, param_2, param_3, param_4); + bool const allow_worker_coast = is->current_config.enable_districts && is->current_config.workers_can_enter_coast && is_worker (this); + bool const allow_bridge_walk = is->current_config.enable_districts && + is->current_config.enable_bridge_districts && + (p_bic_data->UnitTypes[this->Body.UnitTypeID].Unit_Class == UTC_Land); + bool const allow_canal_sail = is->current_config.enable_districts && + is->current_config.enable_canal_districts && + (p_bic_data->UnitTypes[this->Body.UnitTypeID].Unit_Class == UTC_Sea); + bool coast_override_active = false; + enum UnitStateType prev_state = this->Body.UnitState; + int prev_container = this->Body.Container_Unit; + + if (allow_worker_coast || allow_bridge_walk || allow_canal_sail) { + int nx, ny; + get_neighbor_coords (&p_bic_data->Map, this->Body.X, this->Body.Y, neighbor_index, &nx, &ny); + Tile * dest = tile_at (nx, ny); + if (dest != NULL) { + bool should_override = false; + if (allow_worker_coast && + dest->vtable->m35_Check_Is_Water (dest) && + (dest->vtable->m50_Get_Square_BaseType (dest) == SQ_Coast)) { + bool is_human = (*p_human_player_bits & (1 << this->Body.CivID)) != 0; + if (is_human) { + should_override = true; + } else { + struct district_worker_record * rec = get_tracked_worker_record (this); + struct pending_district_request * req = (rec != NULL) ? rec->pending_req : NULL; + if (req != NULL) { + City * req_city = (req->city != NULL) ? req->city : get_city_ptr (req->city_id); + if ((req->district_id >= 0) && + (req->district_id < is->district_count) && + (req_city != NULL) && + city_radius_contains_tile (req_city, nx, ny) && + (req->target_x == nx) && (req->target_y == ny)) { + struct district_config const * cfg = &is->district_configs[req->district_id]; + if ((cfg->buildable_square_types_mask & (1 << SQ_Coast)) != 0) + should_override = true; + } + } + } + } - // If this method was not called during game initialization - if ((ret_addr != ADDR_UNLOCK_TECH_AT_INIT_1) && - (ret_addr != ADDR_UNLOCK_TECH_AT_INIT_2) && - (ret_addr != ADDR_UNLOCK_TECH_AT_INIT_3)) { + if (! should_override && allow_bridge_walk) { + struct district_instance * inst = get_district_instance (dest); + if ((inst != NULL) && + (inst->district_id == BRIDGE_DISTRICT_ID) && + district_is_complete (dest, inst->district_id)) + should_override = true; + } - // If this tech obsoletes some building and we're configured to fix the maintenance bug then recompute city maintenance. - if (is->current_config.patch_maintenance_persisting_for_obsolete_buildings) { - bool obsoletes_anything = false; - for (int n = 0; n < p_bic_data->ImprovementsCount; n++) - if (p_bic_data->Improvements[n].ObsoleteID == tech_id) { - obsoletes_anything = true; - break; - } - if (obsoletes_anything) - Leader_recompute_buildings_maintenance (this); + if (! should_override && allow_canal_sail) { + struct district_instance * inst = get_district_instance (dest); + if ((inst != NULL) && + (inst->district_id == CANAL_DISTRICT_ID) && + district_is_complete (dest, inst->district_id)) + should_override = true; + } + + if (should_override) { + coast_override_active = true; + is->coast_walk_unit = this; + is->coast_walk_transport_override = false; + is->coast_walk_prev_state = prev_state; + is->coast_walk_prev_container = prev_container; + } + } + } + + is->zoc_interceptor = is->zoc_defender = NULL; + int tr = Unit_move_to_adjacent_tile (this, __, neighbor_index, param_2, param_3, param_4); + if ((this == is->zoc_defender) && check_life_after_zoc (this, is->zoc_interceptor)) + tr = ! is_online_game (); // This is what the original method returns when the unit was destroyed in combat + + if (coast_override_active) { + is->coast_walk_unit = NULL; + is->coast_walk_transport_override = false; + + if (this->Body.Container_Unit == this->Body.ID) { + this->Body.Container_Unit = is->coast_walk_prev_container; + Unit_set_state (this, __, is->coast_walk_prev_state); } } + + is->temporarily_disallow_lethal_zoc = false; + is->moving_unit_to_adjacent_tile = false; + return tr; } int __fastcall -patch_City_get_improv_maintenance_for_ui (City * this, int edx, int improv_id) +patch_Unit_teleport (Unit * this, int edx, int tile_x, int tile_y, Unit * unit_telepad) { - Improvement * improv = &p_bic_data->Improvements[improv_id]; - if (is->current_config.patch_maintenance_persisting_for_obsolete_buildings && - (improv->ObsoleteID >= 0) && Leader_has_tech (&leaders[this->Body.CivID], __, improv->ObsoleteID)) - return 0; - else - return patch_City_get_improvement_maintenance (this, __, improv_id); + is->zoc_interceptor = NULL; + int tr = Unit_teleport (this, __, tile_x, tile_y, unit_telepad); + check_life_after_zoc (this, is->zoc_interceptor); + return tr; } -// Patch for barbarian diagonal bug. This bug is a small mistake in the original code, maybe a copy+paste error. The original code tries to loop over -// tiles around the barb unit by incrementing a neighbor index and coverting it to tile coords (like normal). The problem is that after it converts -// the neighbor index to dx and dy, it adds dx to both coords of the unit's position instead of using dy. The fix is simply to subtract off dx and add -// in dy when the Y coord is passed to Map::wrap_vert. -void __cdecl -patch_neighbor_index_to_diff_for_barb_ai (int neighbor_index, int * out_x, int * out_y) -{ - neighbor_index_to_diff (neighbor_index, out_x, out_y); - is->barb_diag_patch_dy_fix = *out_y - *out_x; -} -int __fastcall -patch_Map_wrap_vert_for_barb_ai (Map * this, int edx, int y) +bool +can_do_defensive_bombard (Unit * unit, UnitType * type) { - return Map_wrap_vert (this, __, is->current_config.patch_barbarian_diagonal_bug ? (y + is->barb_diag_patch_dy_fix) : y); + if ((type->Bombard_Strength > 0) && (! Unit_has_ability (unit, __, UTA_Cruise_Missile))) { + if (cannot_defend_inside_transport (unit)) + return false; + + if ((unit->Body.Status & USF_USED_DEFENSIVE_BOMBARD) == 0) // has not already done DB this turn + return true; + + // If the "blitz" special DB rule is activated and this unit has blitz, check if it still has an extra DB to use + else if ((is->current_config.special_defensive_bombard_rules & SDBR_BLITZ) && UnitType_has_ability (type, __, UTA_Blitz)) { + int extra_dbs = itable_look_up_or (&is->extra_defensive_bombards, unit->Body.ID, 0); + return type->Movement > extra_dbs + 1; + + } else + return false; + } else + return false; } -void -ai_update_distribution_hub_goal_for_leader (Leader * leader) +Unit * __fastcall +patch_Fighter_find_defensive_bombarder (Fighter * this, int edx, Unit * attacker, Unit * defender) { - if (leader == NULL) - return; - if (! is->current_config.enable_districts || - ! is->current_config.enable_distribution_hub_districts) - return; - - int civ_id = leader->ID; - if ((1 << civ_id) & *p_human_player_bits) - return; + int special_db_rules = is->current_config.special_defensive_bombard_rules; + if ((special_db_rules == 0) && + ((is->current_config.land_transport_rules & LTR_NO_DEFENSE_FROM_INSIDE) == 0) && + ((is->current_config.special_helicopter_rules & SHR_NO_DEFENSE_FROM_INSIDE) == 0)) + return Fighter_find_defensive_bombarder (this, __, attacker, defender); + else { + enum UnitTypeClasses attacker_class = p_bic_data->UnitTypes[attacker->Body.UnitTypeID].Unit_Class; + int attacker_has_one_hp = Unit_get_max_hp (attacker) - attacker->Body.Damage <= 1; - int ideal_per_100 = is->current_config.ai_ideal_distribution_hub_count_per_100_cities; - if (ideal_per_100 <= 0) - return; + Tile * defender_tile = tile_at (defender->Body.X, defender->Body.Y); + if ((Unit_get_defense_strength (attacker) < 1) || // if attacker cannot defend OR + (defender_tile == NULL) || (defender_tile == p_null_tile) || // defender tile is invalid OR + (((special_db_rules & SDBR_LETHAL) == 0) && attacker_has_one_hp) || // (DB is non-lethal AND attacker has one HP remaining) OR + ((special_db_rules & SDBR_NOT_INVISIBLE) && ! patch_Unit_is_visible_to_civ (attacker, __, defender->Body.CivID, 1))) // (invisible units are immune to DB AND attacker is invisible) + return NULL; - int city_count = leader->Cities_Count; - if (city_count <= 0) - return; + Unit * tr = NULL; + int highest_strength = -1; + enum UnitTypeAbilities lethal_bombard_req = (attacker_class == UTC_Sea) ? UTA_Lethal_Sea_Bombardment : UTA_Lethal_Land_Bombardment; + FOR_UNITS_ON (uti, defender_tile) { + Unit * candidate = uti.unit; + UnitType * candidate_type = &p_bic_data->UnitTypes[candidate->Body.UnitTypeID]; + if (can_do_defensive_bombard (candidate, candidate_type) && + (candidate_type->Bombard_Strength > highest_strength) && + (candidate != defender) && + (Unit_get_containing_army (candidate) != defender) && + ((attacker_class == candidate_type->Unit_Class) || + ((special_db_rules & SDBR_AERIAL) && + (candidate_type->Unit_Class == UTC_Air) && + (candidate_type->Air_Missions & UCV_Bombing)) || + ((special_db_rules & SDBR_DOCKED_VS_LAND) && + (candidate_type->Unit_Class == UTC_Sea) && + (get_city_ptr (defender_tile->CityID) != NULL))) && + ((! attacker_has_one_hp) || UnitType_has_ability (candidate_type, __, lethal_bombard_req))) { + tr = candidate; + highest_strength = candidate_type->Bombard_Strength; + } + } + return tr; + } +} - int desired = (city_count * ideal_per_100) / 100; - if (desired <= 0) - return; +void __fastcall +patch_Fighter_damage_by_db_in_main_loop (Fighter * this, int edx, Unit * bombarder, Unit * defender) +{ + if (p_bic_data->UnitTypes[bombarder->Body.UnitTypeID].Unit_Class == UTC_Air) { + if (Unit_try_flying_over_tile (bombarder, __, defender->Body.X, defender->Body.Y)) + return; // intercepted + else if (patch_Main_Screen_Form_is_unit_visible_to_player (p_main_screen_form, __, defender->Body.X, defender->Body.Y, bombarder)) + Unit_play_bombing_animation (bombarder, __, defender->Body.X, defender->Body.Y); + } - int current = 0; - FOR_TABLE_ENTRIES (tei, &is->distribution_hub_records) { - struct distribution_hub_record * rec = (struct distribution_hub_record *)(long)tei.value; - if ((rec != NULL) && (rec->civ_id == civ_id)) - current++; + // If the unit has already performed DB this turn, then record that it's consumed one of its extra DBs + if (bombarder->Body.Status & USF_USED_DEFENSIVE_BOMBARD) { + int extra_dbs = itable_look_up_or (&is->extra_defensive_bombards, bombarder->Body.ID, 0); + itable_insert (&is->extra_defensive_bombards, bombarder->Body.ID, extra_dbs + 1); } - int in_progress = 0; - FOR_TABLE_ENTRIES (tei, &is->district_tile_map) { - Tile * tile = (Tile *)(long)tei.key; - int mapped_district_id = tei.value; - if ((tile != NULL) && - (mapped_district_id == DISTRIBUTION_HUB_DISTRICT_ID) && - ! district_is_complete (tile, DISTRIBUTION_HUB_DISTRICT_ID)) { - int owner = tile->vtable->m38_Get_Territory_OwnerID (tile); - if (owner == civ_id) - in_progress++; + int damage_before = defender->Body.Damage; + Fighter_damage_by_defensive_bombard (this, __, bombarder, defender); + int damage_after = defender->Body.Damage; + + is->dbe.bombarder = bombarder; + is->dbe.defender = defender; + if (damage_after > damage_before) { + is->dbe.damage_done = true; + int max_hp = Unit_get_max_hp (defender); + int dead_before = damage_before >= max_hp, dead_after = damage_after >= max_hp; + + // If the unit was killed by defensive bombard, play its death animation then toggle off animations for the rest of the combat so it + // doesn't look like anything else happens. Technically, the combat continues and the dead unit is guarantted to lose because the + // patch to get_combat_odds ensures the dead unit has no chance of winning a round. + if (dead_before ^ dead_after) { + is->dbe.defender_was_destroyed = true; + if ((! is_online_game ()) && Fighter_check_combat_anim_visibility (this, __, bombarder, defender, true)) + Animator_play_one_shot_unit_animation (&p_main_screen_form->animator, __, defender, AT_DEATH, false); + is->dbe.saved_animation_setting = this->play_animations; + this->play_animations = 0; } } +} - int pending = 0; - FOR_TABLE_ENTRIES (tei, &is->city_pending_district_requests[civ_id]) { - struct pending_district_request * req = (struct pending_district_request *)(long)tei.value; - if ((req != NULL) && (req->district_id == DISTRIBUTION_HUB_DISTRICT_ID)) - pending++; - } +int __fastcall +patch_Fighter_get_odds_for_main_combat_loop (Fighter * this, int edx, Unit * attacker, Unit * defender, bool bombarding, bool ignore_defensive_bonuses) +{ + // If the attacker was destroyed by defensive bombard, return a number that will ensure the defender wins the first round of combat, otherwise + // the zero HP attacker might go on to win an absurd victory. (The attacker in the overall combat is the defender during DB). + if (is->dbe.defender_was_destroyed) + return 1025; - int planned = current + in_progress + pending; - if (planned >= desired) - return; + else + return Fighter_get_combat_odds (this, __, attacker, defender, bombarding, ignore_defensive_bonuses); +} - City * capital = get_city_ptr (leader->CapitalID); - bool has_capital = (capital != NULL); - int capital_x = has_capital ? capital->Body.X : 0; - int capital_y = has_capital ? capital->Body.Y : 0; +byte __fastcall +patch_Fighter_fight (Fighter * this, int edx, Unit * attacker, int attack_direction, Unit * defender_or_null) +{ + byte tr = Fighter_fight (this, __, attacker, attack_direction, defender_or_null); + is->dbe = (struct defensive_bombard_event) {0}; + return tr; +} - const int yield_weight = 40; - const int capital_distance_weight = 45; - const int desired_min_capital_distance = 8; - const int proximity_penalty_scale = 300; +void __fastcall +patch_Unit_score_kill_by_defender (Unit * this, int edx, Unit * victim, bool was_attacking) +{ + // This function is called when the defender wins in combat. If the attacker was actually killed by defensive bombardment, then award credit + // for that kill to the defensive bombarder not the defender in combat. + if (is->dbe.defender_was_destroyed) { + is->do_not_enslave_units = is->current_config.prevent_enslaving_by_bombardment; + Unit_score_kill (is->dbe.bombarder, __, victim, was_attacking); + is->do_not_enslave_units = false; + p_bic_data->fighter.play_animations = is->dbe.saved_animation_setting; - while (planned < desired) { - City * best_city = NULL; - int best_tile_x = 0; - int best_tile_y = 0; - int best_score = INT_MIN; + } else + Unit_score_kill (this, __, victim, was_attacking); +} - FOR_CITIES_OF (coi, civ_id) { - City * city = coi.city; - if (city == NULL) - continue; - if (city_has_required_district (city, DISTRIBUTION_HUB_DISTRICT_ID)) - continue; +void __fastcall +patch_Unit_play_attack_anim_for_def_bombard (Unit * this, int edx, int direction) +{ + // Don't play any animation for air units, the animations are instead handled in the patch for damage_by_defensive_bombard + if (p_bic_data->UnitTypes[this->Body.UnitTypeID].Unit_Class != UTC_Air) { - if (find_pending_district_request (city, DISTRIBUTION_HUB_DISTRICT_ID) != NULL) - continue; + // Make sure the unit is displayed if it's in an army and we're configured for that + struct unit_display_override saved_udo = is->unit_display_override; + Unit * container; + if (is->current_config.show_armies_performing_defensive_bombard && + (container = get_unit_ptr (this->Body.Container_Unit)) != NULL && + Unit_has_ability (container, __, UTA_Army)) + is->unit_display_override = (struct unit_display_override) { this->Body.ID, this->Body.X, this->Body.Y }; - int tile_x, tile_y; - Tile * candidate = find_tile_for_distribution_hub_district (city, &tile_x, &tile_y); - if (candidate == NULL) - continue; + Unit_play_attack_animation (this, __, direction); - int yield_sum = compute_city_tile_yield_sum (city, tile_x, tile_y); - int distance_to_capital = 0; - if (has_capital) - distance_to_capital = compute_wrapped_manhattan_distance (city->Body.X, city->Body.Y, capital_x, capital_y); + is->unit_display_override = saved_udo; + } +} - int closeness_penalty = 0; - if (has_capital && (distance_to_capital < desired_min_capital_distance)) - closeness_penalty = (desired_min_capital_distance - distance_to_capital) * proximity_penalty_scale; +bool +can_precision_strike_tile_improv_at (Unit * unit, int x, int y) +{ + Tile * tile; + return is->current_config.allow_precision_strikes_against_tile_improvements && // we're configured to allow prec. strikes vs tiles AND + ((tile = tile_at (x, y)) != p_null_tile) && // get tile, make sure it's valid AND + is_explored (tile, unit->Body.CivID) && // tile has been explored by attacker AND + has_any_destructible_overlays (tile, true); // it has something that can be destroyed by prec. strike +} - int score = yield_sum * yield_weight + distance_to_capital * capital_distance_weight - closeness_penalty; - if (tile_has_resource (candidate)) - score -= 500; +// Same as above function except this one applies to the V3 field instead of FOWStatus +Tile * __cdecl +patch_tile_at_for_v3_check (int x, int y) +{ + Tile * tile = tile_at (x, y); + int is_hotseat_game = *p_is_offline_mp_game && ! *p_is_pbem_game; + if (is_hotseat_game && is->current_config.share_visibility_in_hotseat) { + is->dummy_tile->Body.V3 = ((tile->Body.V3 & *p_human_player_bits) != 0) << p_main_screen_form->Player_CivID; + return is->dummy_tile; + } else + return tile; +} - if (score > best_score) { - best_score = score; - best_city = city; - best_tile_x = tile_x; - best_tile_y = tile_y; +bool __fastcall +patch_Unit_check_contact_bit_6_on_right_click (Unit * this, int edx, int civ_id) +{ + bool tr = Unit_check_contact_bit_6 (this, __, civ_id); + if ((! tr) && + is->current_config.share_visibility_in_hotseat && + (*p_is_offline_mp_game && ! *p_is_pbem_game) && // is hotseat game + ((1 << civ_id) & *p_human_player_bits)) { // is civ_id a human player + if ((1 << this->Body.CivID) & *p_human_player_bits) + tr = true; + + else { + // Check if any other human player has contact + unsigned player_bits = *(unsigned *)p_human_player_bits >> 1; + int n_player = 1; + while (player_bits != 0) { + if ((player_bits & 1) && (n_player != civ_id) && + Unit_check_contact_bit_6 (this, __, n_player)) { + tr = true; + break; + } + player_bits >>= 1; + n_player++; } } + } + return tr; +} - if (best_city == NULL) - break; +bool __fastcall +patch_Unit_check_precision_strike_target (Unit * this, int edx, int tile_x, int tile_y) +{ + return Unit_check_precision_strike_target (this, __, tile_x, tile_y) || can_precision_strike_tile_improv_at (this, tile_x, tile_y); +} - mark_city_needs_district (best_city, DISTRIBUTION_HUB_DISTRICT_ID); - planned++; - } +void __fastcall +patch_Unit_play_attack_animation_vs_tile (Unit * this, int edx, int direction) +{ + if (is->current_config.polish_precision_striking && + UnitType_has_ability (&p_bic_data->UnitTypes[this->Body.UnitTypeID], __, UTA_Cruise_Missile) && + (is->attacking_tile_x != this->Body.X) && (is->attacking_tile_y != this->Body.Y)) + Unit_animate_cruise_missile_strike (this, __, is->attacking_tile_x, is->attacking_tile_y); + else + Unit_play_attack_animation (this, __, direction); } -bool -assign_ai_fallback_production (City * city, int disallowed_improvement_id) +void __fastcall +patch_Unit_attack_tile (Unit * this, int edx, int x, int y, int bombarding) { - if (city == NULL) - return false; + Tile * target_tile = NULL; + bool had_district_before = false; + int district_id_before = -1; + int tile_x = x; + int tile_y = y; - City_Order new_order = { .OrderID = -1, .OrderType = 0 }; - patch_City_ai_choose_production (city, __, &new_order); + if (is->current_config.enable_districts) { - bool order_ok = false; - if (new_order.OrderType == COT_Improvement) { - if ((new_order.OrderID >= 0) && - (new_order.OrderID < p_bic_data->ImprovementsCount) && - (new_order.OrderID != disallowed_improvement_id) && - ! city_requires_district_for_improvement (city, new_order.OrderID, NULL) && - ! is_wonder_or_small_wonder_already_being_built (city, new_order.OrderID)) - order_ok = true; - } else if (new_order.OrderType == COT_Unit) { - if ((new_order.OrderID >= 0) && - (new_order.OrderID < p_bic_data->UnitTypeCount) && - patch_City_can_build_unit (city, __, new_order.OrderID, 1, 0, 0)) - order_ok = true; + // Check if this is a completed wonder district that cannot be destroyed + if (is->current_config.enable_wonder_districts && + !is->current_config.completed_wonder_districts_can_be_destroyed) { + wrap_tile_coords (&p_bic_data->Map, &tile_x, &tile_y); + target_tile = tile_at (tile_x, tile_y); + if ((target_tile != NULL) && (target_tile != p_null_tile)) { + struct wonder_district_info * info = get_wonder_district_info (target_tile); + if (info != NULL && info->state == WDS_COMPLETED) { + // This tile has a completed wonder district and they can't be destroyed + if ((*p_human_player_bits & (1 << this->Body.CivID)) != 0) { + PopupForm * popup = get_popup_form (); + popup->vtable->set_text_key_and_flags (popup, __, is->mod_script_path, "C3X_CANT_PILLAGE", -1, 0, 0, 0); + patch_show_popup (popup, __, 0, 0); + } + return; + } + } + } + + wrap_tile_coords (&p_bic_data->Map, &tile_x, &tile_y); + target_tile = tile_at (tile_x, tile_y); + if ((target_tile != NULL) && (target_tile != p_null_tile)) { + struct district_instance * inst = get_district_instance (target_tile); + if (inst != NULL) { + had_district_before = true; + district_id_before = inst->district_id; + } + } } - char ss[200]; - snprintf (ss, sizeof ss, "assign_ai_fallback_production: Remembering fallback pending building order for city %d (%s): id %d\n", - city->Body.ID, city->Body.CityName, disallowed_improvement_id); - (*p_OutputDebugStringA) (ss); - remember_pending_building_order (city, disallowed_improvement_id); + is->attacking_tile_x = x; + is->attacking_tile_y = y; + if (bombarding) + is->unit_bombard_attacking_tile = this; - if (order_ok) { - City_set_production (city, __, new_order.OrderType, new_order.OrderID, false); - return true; - } + Unit_attack_tile (this, __, x, y, bombarding); - City_Order defensive_order = { .OrderID = -1, .OrderType = 0 }; - if (choose_defensive_unit_order (city, &defensive_order)) { - City_set_production (city, __, defensive_order.OrderType, defensive_order.OrderID, false); - return true; + // Check if the district was destroyed by the attack + if (had_district_before && (target_tile != NULL) && (target_tile != p_null_tile) && district_id_before != NATURAL_WONDER_DISTRICT_ID) { + struct district_instance * inst_after = get_district_instance (target_tile); + bool has_district_after = (inst_after != NULL); + int district_id_after = (inst_after != NULL) ? inst_after->district_id : -1; + + // If the district existed before but not after, or the tile no longer has a mine, the district was destroyed + if (!has_district_after || !(target_tile->vtable->m42_Get_Overlays (target_tile, __, 0) & TILE_FLAG_MINE)) { + bool is_water_tile = target_tile != NULL && target_tile->vtable->m35_Check_Is_Water (target_tile); + handle_district_destroyed_by_attack (target_tile, tile_x, tile_y, ! is_water_tile); + } } - return false; + is->unit_bombard_attacking_tile = NULL; + is->attacking_tile_x = is->attacking_tile_y = -1; } void __fastcall -patch_Leader_do_production_phase (Leader * this) +patch_Unit_do_precision_strike (Unit * this, int edx, int x, int y) { - recompute_resources_if_necessary (); + if ((city_at (x, y) == NULL) && can_precision_strike_tile_improv_at (this, x, y)) + patch_Unit_attack_tile (this, __, x, y, 1); + else + Unit_do_precision_strike (this, __, x, y); - if (is->current_config.enable_districts) { - assign_workers_for_pending_districts (this); + if (is->current_config.polish_precision_striking && + UnitType_has_ability (&p_bic_data->UnitTypes[this->Body.UnitTypeID], __, UTA_Cruise_Missile)) + patch_Unit_despawn (this, __, 0, false, false, 0, 0, 0, 0); +} - if (is->current_config.enable_distribution_hub_districts) { - // Check if AI has the tech prereq for distribution hubs before updating goals - int prereq_id = is->district_infos[DISTRIBUTION_HUB_DISTRICT_ID].advance_prereq_id; - if ((prereq_id < 0) || Leader_has_tech (this, __, prereq_id)) - ai_update_distribution_hub_goal_for_leader (this); +int __fastcall +patch_Unit_get_max_moves_after_barricade_attack (Unit * this) +{ + if (is->current_config.dont_end_units_turn_after_bombarding_barricade && (this == is->unit_bombard_attacking_tile)) + return this->Body.Moves + p_bic_data->General.RoadsMovementRate; + else + return patch_Unit_get_max_move_points (this); +} + +City * __cdecl +patch_city_at_in_find_bombard_defender (int x, int y) +{ + // The caller (Fighter::find_defender_against_bombardment) has a set of lists of bombard priority/eligibility in its stack memory. The list + // for land units bombarding land tiles normally restricts the targets to land units by containing [UTC_Land, -1, -1]. If we're configured to + // remove that restriction, modify the list so it contains instead [UTC_Land, UTC_Sea, UTC_Air]. Conveniently, the offset from this function's + // first parameter to the list is 0x40 bytes in all executables. + if (is->current_config.remove_land_artillery_target_restrictions) { + enum UnitTypeClasses * list = (void *)((byte *)&x + 0x40); + list[1] = UTC_Sea; + list[2] = UTC_Air; + } + + return city_at (x, y); +} + +bool __fastcall +patch_Unit_check_bombard_target (Unit * this, int edx, int tile_x, int tile_y) +{ + bool base = Unit_check_bombard_target (this, __, tile_x, tile_y); + Tile * tile; + int overlays; + if (base && + is->current_config.disallow_useless_bombard_vs_airfields && + ! Unit_has_ability (this, __, UTA_Nuclear_Weapon) && + ((tile = tile_at (tile_x, tile_y)) != p_null_tile) && + ((overlays = tile->vtable->m42_Get_Overlays (tile, __, 0)) & 0x20000000) && // if tile has an airfield AND + (overlays == 0x20000000)) { // tile only has an airfield + UnitType * this_type = &p_bic_data->UnitTypes[this->Body.UnitTypeID]; + + // Check that a bombard attack vs this tile would not be wasted. It won't be if either (1) there are no units on the tile, (2) there + // is a unit that can be damaged by bombarding, or (3) there are no units that can be damaged and no air units. The rules for + // bombardment are that you can't damage aircraft in an airfield and you also can't destroy an airfield from underneath aircraft. + int any_units = 0, + any_vulnerable_units = 0, + any_air_units = 0; + FOR_UNITS_ON (uti, tile) { + enum UnitTypeClasses class = p_bic_data->UnitTypes[uti.unit->Body.UnitTypeID].Unit_Class; + any_units = 1; + any_air_units |= class == UTC_Air; + any_vulnerable_units |= (class != UTC_Air) && (Unit_get_defense_strength (uti.unit) > 0) && + can_damage_bombarding (this_type, uti.unit, tile); } + return (! any_units) || // case (1) above + any_vulnerable_units || // case (2) + ((! any_air_units) && (! any_vulnerable_units)); // case (3) - FOR_CITIES_OF (coi, this->ID) { - City * city = coi.city; - if (city == NULL) continue; + } else + return base; +} + +bool __fastcall +patch_Unit_can_disembark_anything (Unit * this, int edx, int tile_x, int tile_y) +{ + Tile * this_tile = tile_at (this->Body.X, this->Body.Y); + Tile * target_tile = tile_at (tile_x, tile_y); + bool base = Unit_can_disembark_anything (this, __, tile_x, tile_y); - bool is_human = (*p_human_player_bits & (1 << city->Body.CivID)) != 0; - bool at_neighborhood_cap = is->current_config.enable_neighborhood_districts && city_is_at_neighborhood_cap (city); + // Apply units per tile limit + if (base) { + bool stack_limited_for_all = true; + FOR_UNITS_ON (uti, this_tile) + if ((uti.unit->Body.Container_Unit == this->Body.ID) && is_below_stack_limit (target_tile, this->Body.CivID, uti.unit->Body.UnitTypeID)) { + stack_limited_for_all = false; + break; + } + if (stack_limited_for_all) + return false; + } - if (at_neighborhood_cap) { - if (is_human) - maybe_show_neighborhood_growth_warning (city); - else - ensure_neighborhood_request_for_city (city); + // Apply trespassing restriction. First check if this civ may move into (tile_x, tile_y) without trespassing. If it would be trespassing, then + // we can only disembark anything if this transport has a passenger that can ignore the restriction. Without this check, the game can enter an + // infinite loop under rare circumstances. + if (base && + is->current_config.disallow_trespassing && + check_trespassing (this->Body.CivID, this_tile, target_tile)) { + bool any_exempt_passengers = false; + FOR_UNITS_ON (uti, this_tile) + if ((uti.unit->Body.Container_Unit == this->Body.ID) && is_allowed_to_trespass (uti.unit)) { + any_exempt_passengers = true; + break; } + return any_exempt_passengers; - if (city->Body.Order_Type != COT_Improvement) continue; - int i_improv = city->Body.Order_ID; + } else + return base; +} - // Check if production needs to be halted due to missing district - int req_district_id = -1; - char const * district_description = NULL; - bool needs_halt = false; +int __fastcall +patch_Unit_get_defense_for_bombardable_unit_check (Unit * this) +{ + // Returning a defense value of zero indicates this unit is not a target for bombardment. If configured, exclude all air units from + // bombardment so the attacks target tile improvements instead. Do this only if the tile has another improvement in addition to an airfield, + // or we could destroy the airfield itself. + Tile * tile; + int overlays; + if (is->current_config.allow_bombard_of_other_improvs_on_occupied_airfield && // If configured AND + (p_bic_data->UnitTypes[this->Body.UnitTypeID].Unit_Class == UTC_Air) && // "this" is an air unit AND + ((tile = tile_at (this->Body.X, this->Body.Y)) != p_null_tile) && // "this" is on a valid tile AND + ((overlays = tile->vtable->m42_Get_Overlays (tile, __, 0)) & 0x20000000) && // tile has an airfield AND + (overlays != 0x20000000)) // tile does not only have an airfield + return 0; - // Check buildings & wonders dependent on districts - if (itable_look_up (&is->district_building_prereqs, i_improv, &req_district_id)) { - if (! city_has_required_district (city, req_district_id)) { - needs_halt = true; - district_description = is->district_configs[req_district_id].name; - } - } + else + return Unit_get_defense_strength (this); +} - // Wonders - if (is->current_config.enable_wonder_districts) { - if (city_is_currently_building_wonder (city)) { - bool wonder_requires_district = false; - if (i_improv >= 0) { - int required_id; - if (itable_look_up (&is->district_building_prereqs, i_improv, &required_id) && - (required_id == WONDER_DISTRICT_ID)) - wonder_requires_district = true; - } +void __fastcall +patch_Demographics_Form_m22_draw (Demographics_Form * this) +{ + Demographics_Form_m22_draw (this); - if (wonder_requires_district) { - bool has_wonder_district = reserve_wonder_district_for_city (city); - if (! has_wonder_district) { - needs_halt = true; - req_district_id = WONDER_DISTRICT_ID; - district_description = "Wonder District"; - } - } else { - release_wonder_district_reservation (city); - } - } else { - release_wonder_district_reservation (city); + if (is->current_config.show_total_city_count) { + // There's proably a better way to get the city count, but better safe than sorry. I don't know if it's possible for the city list to + // contain holes so the surest thing is to check every possible ID. + int city_count = 0; { + if (p_cities->Cities != NULL) + for (int n = 0; n <= p_cities->LastIndex; n++) { + City_Body * body = p_cities->Cities[n].City; + city_count += (body != NULL) && ((int)body != offsetof (City, Body)); } - } + } - // If production needs to be halted, handle the reassignment and messaging - if (needs_halt) { - // Switch production to another option - if (! is_human) { - mark_city_needs_district (city, req_district_id); - assign_ai_fallback_production (city, i_improv); - } else { - City_Order defensive_order = { .OrderID = -1, .OrderType = 0 }; - if (choose_defensive_unit_order (city, &defensive_order)) { - City_set_production (city, __, defensive_order.OrderType, defensive_order.OrderID, false); - } - } + PCX_Image * canvas = &this->Base.Data.Canvas; - // Show message to human player - if (is_human && (city->Body.CivID == p_main_screen_form->Player_CivID)) { - char msg[160]; - char const * bname = p_bic_data->Improvements[i_improv].Name.S; - snprintf (msg, sizeof msg, "%s %s %s", bname, is->c3x_labels[CL_CONSTRUCTION_HALTED_DUE_TO_MISSING_DISTRICT], district_description); - msg[(sizeof msg) - 1] = '\0'; - show_map_specific_text (city->Body.X, city->Body.Y, msg, true); - } - continue; + // Draw backdrop + { + char temp_path[2*MAX_PATH]; + PCX_Image backdrop; + PCX_Image_construct (&backdrop); + get_mod_art_path ("CityCountBackdrop.pcx", temp_path, sizeof temp_path); + PCX_Image_read_file (&backdrop, __, temp_path, NULL, 0, 0x100, 2); + if (backdrop.JGL.Image != NULL) { + int w = backdrop.JGL.Image->vtable->m54_Get_Width (backdrop.JGL.Image); + PCX_Image_draw_onto (&backdrop, __, canvas, (1024 - w) / 2, 720); } + backdrop.vtable->destruct (&backdrop, __, 0); } - } - - // Force-activate the barbs if there are any barb cities on the map and barb city capturing is enabled. This is necessary for barb city - // production to work given how it's currently implemented. - if (is->current_config.enable_city_capture_by_barbarians && (this->ID == 0)) { - int any_barb_cities = 0; - FOR_CITIES_OF (coi, this->ID) { - any_barb_cities = 1; - break; - } - if (any_barb_cities) - is->force_barb_activity_for_cities = 1; - } - - // If barbs are force-activated, make sure their activity level is at least 1 (sedentary). - int * p_barb_activity = &p_bic_data->Map.World.Final_Barbarians_Activity; - int saved_barb_activity = *p_barb_activity; - if (is->force_barb_activity_for_cities && (*p_barb_activity <= 0)) - *p_barb_activity = 1; - - Leader_do_production_phase (this); - if (is->force_barb_activity_for_cities) { - *p_barb_activity = saved_barb_activity; - is->force_barb_activity_for_cities = 0; + // Draw text on top of the backdrop + char s[100]; + snprintf (s, sizeof s, "%s %d / %d", is->c3x_labels[CL_TOTAL_CITIES], city_count, is->city_limit); + s[(sizeof s) - 1] = '\0'; + PCX_Image_set_text_effects (canvas, __, 0x80000000, -1, 2, 2); // Set text color to black + PCX_Image_draw_centered_text (canvas, __, get_font (14, FSF_NONE), s, 1024/2 - 100, 730, 200, strlen (s)); } } -// This function counts the number of players in the game. The return value will be compared to the number of cities on the map to determine if there -// are enough cities (per player) to unlock barb production. If barb activity is forced on, return zero so that the check always passes. -int __cdecl -patch_count_player_bits_for_barb_prod (unsigned int bit_field) -{ - return is->force_barb_activity_for_cities ? 0 : count_set_bits (bit_field); -} - -Tile * __fastcall -patch_Map_get_tile_to_check_visibility (Map * this, int edx, int index) +int __fastcall +patch_Leader_get_optimal_city_number (Leader * this) { - Tile * tr = Map_get_tile (this, __, index); - int is_hotseat_game = *p_is_offline_mp_game && ! *p_is_pbem_game; - if (is_hotseat_game && is->current_config.share_visibility_in_hotseat) { - int human_bits = *p_human_player_bits; - is->dummy_tile->Body.Fog_Of_War = tr->Body.Fog_Of_War | ((tr->Body.Fog_Of_War & human_bits) != 0 ? human_bits : 0); - is->dummy_tile->Body.FOWStatus = tr->Body.FOWStatus | ((tr->Body.FOWStatus & human_bits) != 0 ? human_bits : 0); - is->dummy_tile->Body.V3 = tr->Body.V3 | ((tr->Body.V3 & human_bits) != 0 ? human_bits : 0); - is->dummy_tile->Body.Visibility = tr->Body.Visibility | ((tr->Body.Visibility & human_bits) != 0 ? human_bits : 0); - is->dummy_tile->Body.field_D0_Visibility = tr->Body.field_D0_Visibility | ((tr->Body.field_D0_Visibility & human_bits) != 0 ? human_bits : 0); - tr = is->dummy_tile; + if (! is->current_config.strengthen_forbidden_palace_ocn_effect) + return Leader_get_optimal_city_number (this); + else { + int num_sans_fp = Leader_get_optimal_city_number (this), // OCN w/o contrib from num of FPs + fp_count = patch_Leader_count_wonders_with_small_flag (this, __, ITSW_Reduces_Corruption, NULL), + s_diff = p_bic_data->DifficultyLevels[this->player_difficulty].Optimal_Cities, // Difficulty scaling, called "percentage of optimal cities" in the editor + base_ocn = p_bic_data->WorldSizes[p_bic_data->Map.World.World_Size].OptimalCityCount; + return num_sans_fp + (s_diff * fp_count * base_ocn + 50) / 100; // Add 50 to round off } - is->tile_returned_for_visibility_check = tr; - return tr; -} - -Tile * __fastcall -patch_Map_get_tile_to_check_visibility_again (Map * this, int edx, int index) -{ - return is->tile_returned_for_visibility_check; } -// Same as above except this method uses the FOWStatus field instead of Fog_Of_War -Tile * __fastcall -patch_Map_get_tile_for_fow_status_check (Map * this, int edx, int index) +int __fastcall +patch_Leader_count_forbidden_palaces_for_ocn (Leader * this, int edx, enum ImprovementTypeSmallWonderFeatures flag, City * city_or_null) { - Tile * tile = Map_get_tile (this, __, index); - int is_hotseat_game = *p_is_offline_mp_game && ! *p_is_pbem_game; - if (is_hotseat_game && is->current_config.share_visibility_in_hotseat) { - is->dummy_tile->Body.FOWStatus = ((tile->Body.FOWStatus & *p_human_player_bits) != 0) << p_main_screen_form->Player_CivID; - return is->dummy_tile; - } else - return tile; + if (! is->current_config.strengthen_forbidden_palace_ocn_effect) + return patch_Leader_count_wonders_with_small_flag (this, __, flag, city_or_null); + else + return 0; // We'll add in the FP effect later with a different weight } -Tile * __cdecl -patch_tile_at_to_check_visibility (int x, int y) +void __fastcall +patch_Trade_Net_recompute_city_connections (Trade_Net * this, int edx, int civ_id, bool redo_road_network, byte param_3, int redo_roads_for_city_id) { - return patch_Map_get_tile_to_check_visibility (&p_bic_data->Map, __, (p_bic_data->Map.Width >> 1) * y + (x >> 1)); -} + is->is_computing_city_connections = true; + long long ts_before; + QueryPerformanceCounter ((LARGE_INTEGER *)&ts_before); -Tile * __cdecl -patch_tile_at_to_check_visibility_again (int x, int y) -{ - return is->tile_returned_for_visibility_check; -} + if (is->tnx_init_state == IS_OK) { + if (is->tnx_cache == NULL) { + is->tnx_cache = is->create_tnx_cache (&p_bic_data->Map); + is->set_up_before_building_network (is->tnx_cache); + } else if (! is->keep_tnx_cache) + is->set_up_before_building_network (is->tnx_cache); + } -unsigned __fastcall -patch_Tile_m42_Get_Overlays (Tile * this, int edx, byte visible_to_civ) -{ - unsigned base_vis_overlays = Tile_m42_Get_Overlays (this, __, visible_to_civ); - if ((visible_to_civ != 0) && // if we're seeing from a player's persp. instead of seeing the actual overlays AND - is->current_config.share_visibility_in_hotseat && // shared hotseat vis is enabled AND - ((1 << visible_to_civ) & *p_human_player_bits) && // the perspective is of a human player AND - (base_vis_overlays != this->Overlays) && // that player can't already see all the actual overlays AND - (*p_is_offline_mp_game && ! *p_is_pbem_game)) { // we're in a hotseat game + Trade_Net_recompute_city_connections (this, __, civ_id, redo_road_network, param_3, redo_roads_for_city_id); - // Check if there's another human player that can see the actual overlays. If so, give that info to this player and return it. - unsigned player_bits = *(unsigned *)p_human_player_bits >> 1; - int n_player = 1; - while (player_bits != 0) { - if ((player_bits & 1) && (n_player != visible_to_civ) && - (Tile_m42_Get_Overlays (this, __, n_player) == this->Overlays)) { - this->Body.Visibile_Overlays[visible_to_civ] = this->Overlays; - return this->Overlays; - } - player_bits >>= 1; - n_player++; - } + if (is->current_config.enable_districts && + is->current_config.enable_distribution_hub_districts) + is->distribution_hub_totals_dirty = true; - return base_vis_overlays; - } else - return base_vis_overlays; + long long ts_after; + QueryPerformanceCounter ((LARGE_INTEGER *)&ts_after); + is->time_spent_computing_city_connections += ts_after - ts_before; + is->count_calls_to_recompute_city_connections++; + is->is_computing_city_connections = false; } -int __fastcall -patch_Tile_check_water_for_sea_zoc (Tile * this) +void __fastcall +patch_Map_build_trade_network (Map * this) { - if ((is->current_config.special_zone_of_control_rules & (SZOCR_AMPHIBIOUS | SZOCR_AERIAL)) == 0) - return this->vtable->m35_Check_Is_Water (this); - else - return 1; // The caller will skip ZoC logic if this is a land tile without a city because the targeted unit is a sea unit. Instead - // return 1, so all tiles are considered sea tiles, so we can run the ZoC logic for land units or air units on land. + if ((is->tnx_init_state == IS_OK) && (is->tnx_cache != NULL)) + is->set_up_before_building_network (is->tnx_cache); + is->keep_tnx_cache = true; + Map_build_trade_network (this); + is->keep_tnx_cache = false; + if (is->current_config.enable_districts && is->current_config.enable_distribution_hub_districts) + is->distribution_hub_totals_dirty = true; } -int __fastcall -patch_Tile_check_water_for_land_zoc (Tile * this) +void __fastcall +patch_Trade_Net_recompute_city_cons_and_res (Trade_Net * this, int edx, bool param_1) { - // Same as above except this time we want to consider all tiles to be land - return ((is->current_config.special_zone_of_control_rules & (SZOCR_AMPHIBIOUS | SZOCR_AERIAL)) == 0) ? - this->vtable->m35_Check_Is_Water (this) : - 0; -} + if ((is->tnx_init_state == IS_OK) && (is->tnx_cache != NULL)) + is->set_up_before_building_network (is->tnx_cache); + is->keep_tnx_cache = true; + Trade_Net_recompute_city_cons_and_res (this, __, param_1); + is->keep_tnx_cache = false; -int __fastcall -patch_Unit_get_attack_strength_for_sea_zoc (Unit * this) -{ - return (p_bic_data->UnitTypes[this->Body.UnitTypeID].Unit_Class == UTC_Sea) ? Unit_get_attack_strength (this) : 0; + if (is->current_config.enable_districts && is->current_config.enable_distribution_hub_districts) + is->distribution_hub_totals_dirty = true; } int __fastcall -patch_Unit_get_attack_strength_for_land_zoc (Unit * this) +patch_Trade_Net_set_unit_path_to_fill_road_net (Trade_Net * this, int edx, int from_x, int from_y, int to_x, int to_y, Unit * unit, int civ_id, int flags, int * out_path_length_in_mp) { - return (p_bic_data->UnitTypes[this->Body.UnitTypeID].Unit_Class == UTC_Land) ? Unit_get_attack_strength (this) : 0; -} + int tr; + long long ts_before; + QueryPerformanceCounter ((LARGE_INTEGER *)&ts_before); -Unit * __fastcall -patch_Main_Screen_Form_find_visible_unit (Main_Screen_Form * this, int edx, int tile_x, int tile_y, Unit * excluded) -{ - struct unit_display_override * override = &is->unit_display_override; - if ((override->unit_id >= 0) && (override->tile_x == tile_x) && (override->tile_y == tile_y)) { - Unit * unit = get_unit_ptr (override->unit_id); - if (unit != NULL) { - if ((unit->Body.X == tile_x) && (unit->Body.Y == tile_y)) - return unit; - } - } + if ((is->tnx_init_state == IS_OK) && (is->tnx_cache != NULL)) { + is->flood_fill_road_network (is->tnx_cache, from_x, from_y, civ_id); + tr = 0; // Return value is not used by caller anyway + } else + tr = Trade_Net_set_unit_path (this, __, from_x, from_y, to_x, to_y, unit, civ_id, flags, out_path_length_in_mp); - return Main_Screen_Form_find_visible_unit (this, __, tile_x, tile_y, excluded); + long long ts_after; + QueryPerformanceCounter ((LARGE_INTEGER *)&ts_after); + is->time_spent_filling_roads += ts_after - ts_before; + return tr; } -void __fastcall -patch_Animator_play_zoc_animation (Animator * this, int edx, Unit * unit, AnimationType anim_type, bool param_3) +bool +set_up_gdi_plus () { - if (p_bic_data->UnitTypes[unit->Body.UnitTypeID].Unit_Class != UTC_Air) - Animator_play_one_shot_unit_animation (this, __, unit, anim_type, param_3); -} + if (is->gdi_plus.init_state == IS_UNINITED) { + is->gdi_plus.init_state = IS_INIT_FAILED; + is->gdi_plus.gp_graphics = NULL; -bool __fastcall -patch_Fighter_check_zoc_anim_visibility (Fighter * this, int edx, Unit * attacker, Unit * defender, bool param_3) -{ - // If we've reached this point in the code (in the calling method) then a unit has been selected to exert zone of control and it has passed - // its dice roll to cause damage. Stash its pointer for possible use later. - is->zoc_interceptor = attacker; + struct startup_input { + UINT32 GdiplusVersion; + void * DebugEventCallback; + BOOL SuppressBackgroundThread; + BOOL SuppressExternalCodecs; + } startup_input = {1, NULL, FALSE, FALSE}; - // If an air unit was selected, pre-emptively undo the damage from ZoC since we'll want to run our own bit of logic to do that (the air unit - // may still get shot down). Return false from this function to skip over all of the animation logic in the caller since it wouldn't work for - // aircraft. - if (p_bic_data->UnitTypes[attacker->Body.UnitTypeID].Unit_Class == UTC_Air) { - defender->Body.Damage -= 1; - return false; + is->gdi_plus.module = LoadLibraryA ("gdiplus.dll"); + if (is->gdi_plus.module == NULL) { + MessageBoxA (NULL, "Failed to load gdiplus.dll!", "Error", MB_ICONERROR); + goto end_init; + } - // Repeat a check done by the caller. We've deleted this check to ensure that this function always gets called so we can grab the interceptor. - } else if (attacker->Body.Animation.field_111 == 0) - return false; + int (WINAPI * GdiplusStartup) (ULONG_PTR * out_token, struct startup_input *, void * startup_output) = + (void *)(*p_GetProcAddress) (is->gdi_plus.module, "GdiplusStartup"); - else { - bool tr = Fighter_check_combat_anim_visibility (this, __, attacker, defender, param_3); + is->gdi_plus.CreateFromHDC = (void *)(*p_GetProcAddress) (is->gdi_plus.module, "GdipCreateFromHDC"); + is->gdi_plus.DeleteGraphics = (void *)(*p_GetProcAddress) (is->gdi_plus.module, "GdipDeleteGraphics"); + is->gdi_plus.SetSmoothingMode = (void *)(*p_GetProcAddress) (is->gdi_plus.module, "GdipSetSmoothingMode"); + is->gdi_plus.SetPenDashStyle = (void *)(*p_GetProcAddress) (is->gdi_plus.module, "GdipSetPenDashStyle"); + is->gdi_plus.CreatePen1 = (void *)(*p_GetProcAddress) (is->gdi_plus.module, "GdipCreatePen1"); + is->gdi_plus.DeletePen = (void *)(*p_GetProcAddress) (is->gdi_plus.module, "GdipDeletePen"); + is->gdi_plus.DrawLineI = (void *)(*p_GetProcAddress) (is->gdi_plus.module, "GdipDrawLineI"); + if ((is->gdi_plus.CreateFromHDC == NULL) || (is->gdi_plus.DeleteGraphics == NULL) || + (is->gdi_plus.SetSmoothingMode == NULL) || (is->gdi_plus.SetPenDashStyle == NULL) || + (is->gdi_plus.CreatePen1 == NULL) || (is->gdi_plus.DeletePen == NULL) || + (is->gdi_plus.DrawLineI == NULL)) { + MessageBoxA (NULL, "Failed to get GDI+ proc addresses!", "Error", MB_ICONERROR); + goto end_init; + } - // If necessary, set up to ensure the unit's attack animation is visible. This means forcing it to the top of its stack and - // temporarily unfortifying it if it's fortified. (If it's fortified, the animation is occasionally not visible. Don't know why.) - if (tr && is->current_config.show_zoc_attacks_from_mid_stack) { - is->unit_display_override = (struct unit_display_override) { attacker->Body.ID, attacker->Body.X, attacker->Body.Y }; - if (attacker->Body.UnitState == UnitState_Fortifying) { - Unit_set_state (attacker, __, 0); - is->refortify_interceptor_after_zoc = true; - } + int status = GdiplusStartup (&is->gdi_plus.token, &startup_input, NULL); + if (status != 0) { + char s[200]; + snprintf (s, sizeof s, "Failed to initialize GDI+! Startup status: %d", status); + MessageBoxA (NULL, s, "Error", MB_ICONERROR); + goto end_init; } - return tr; + is->gdi_plus.init_state = IS_OK; + end_init: + ; } + + return is->gdi_plus.init_state == IS_OK; } int __fastcall -patch_City_sum_buildings_naval_power_for_zoc (City * this) +patch_OpenGLRenderer_initialize (OpenGLRenderer * this, int edx, PCX_Image * texture) { - // Cancel out city's naval power if the unit has only one HP left. This prevents coastal fortresses from knocking that last hit point off - // passing units when lethal ZoC is enabled, because to make that work we delete an earlier check excusing 1-HP units from ZoC. - if ((is->zoc_defender != NULL) && - ((Unit_get_max_hp (is->zoc_defender) - is->zoc_defender->Body.Damage) <= 1)) - return 0; + if ((is->current_config.draw_lines_using_gdi_plus == LDO_NEVER) || + ((is->current_config.draw_lines_using_gdi_plus == LDO_WINE) && ! is->running_on_wine)) + return OpenGLRenderer_initialize (this, __, texture); - else - return City_sum_buildings_naval_power (this); + // Initialize GDI+ instead + else { + if (! set_up_gdi_plus ()) + return 2; + if (is->gdi_plus.gp_graphics != NULL) { + is->gdi_plus.DeleteGraphics (is->gdi_plus.gp_graphics); + is->gdi_plus.gp_graphics = NULL; + } + int status = is->gdi_plus.CreateFromHDC (texture->JGL.Image->DC, &is->gdi_plus.gp_graphics); + if (status == 0) { + is->gdi_plus.SetSmoothingMode (is->gdi_plus.gp_graphics, 4); // 4 = SmoothingModeAntiAlias from GdiPlusEnums.h + return 0; + } else + return 2; + } } void __fastcall -patch_Fighter_apply_zone_of_control (Fighter * this, int edx, Unit * unit, int from_x, int from_y, int to_x, int to_y) +patch_OpenGLRenderer_set_color (OpenGLRenderer * this, int edx, unsigned int rgb555) { - is->zoc_interceptor = NULL; - is->zoc_defender = unit; - is->refortify_interceptor_after_zoc = false; - struct unit_display_override saved_udo = is->unit_display_override; - Fighter_apply_zone_of_control (this, __, unit, from_x, from_y, to_x, to_y); - - // Actually exert ZoC if an air unit managed to do so. - if ((is->zoc_interceptor != NULL) && (p_bic_data->UnitTypes[is->zoc_interceptor->Body.UnitTypeID].Unit_Class == UTC_Air)) { - bool intercepted = Unit_try_flying_over_tile (is->zoc_interceptor, __, from_x, from_y); - if (! intercepted) { - Unit_play_bombing_animation (is->zoc_interceptor, __, from_x, from_y); - unit->Body.Damage = not_below (0, unit->Body.Damage + 1); + // Convert rgb555 to rgb888 + unsigned int rgb888 = 0; { + unsigned int mask = 31; + int shift = 3; + for (int n = 0; n < 3; n++) { + rgb888 |= (rgb555 & mask) << shift; + mask <<= 5; + shift += 3; } } - if (is->refortify_interceptor_after_zoc) - Unit_set_state (is->zoc_interceptor, __, UnitState_Fortifying); - is->unit_display_override = saved_udo; + is->ogl_color = (is->ogl_color & 0xFF000000) | rgb888; + OpenGLRenderer_set_color (this, __, rgb555); } -// These two patches replace two function calls in Unit::move_to_adjacent_tile that come immediately after the unit has been subjected to zone of -// control. These calls recheck that the move is valid, not sure why. Here they're patched to indicate that the move in invalid when the unit was -// previously killed by ZoC. This causes move_to_adjacent_tile to return early without running the code that would place the unit on the destination -// tile and, for example, capturing an enemy city there. -int __fastcall -patch_Trade_Net_get_move_cost_after_zoc (Trade_Net * this, int edx, int from_x, int from_y, int to_x, int to_y, Unit * unit, int civ_id, unsigned param_7, int neighbor_index, Trade_Net_Distance_Info * dist_info) +void __fastcall +patch_OpenGLRenderer_set_opacity (OpenGLRenderer * this, int edx, unsigned int alpha) { - return ((is->current_config.special_zone_of_control_rules & SZOCR_LETHAL) == 0) || ((Unit_get_max_hp (unit) - unit->Body.Damage) > 0) ? - patch_Trade_Net_get_movement_cost (this, __, from_x, from_y, to_x, to_y, unit, civ_id, param_7, neighbor_index, dist_info) : - -1; + is->ogl_color = (is->ogl_color & 0x00FFFFFF) | (alpha << 24); + OpenGLRenderer_set_opacity (this, __, alpha); } -AdjacentMoveValidity __fastcall -patch_Unit_can_move_after_zoc (Unit * this, int edx, int neighbor_index, int param_2) + +void __fastcall +patch_OpenGLRenderer_set_line_width (OpenGLRenderer * this, int edx, int width) { - return ((is->current_config.special_zone_of_control_rules & SZOCR_LETHAL) == 0) || ((Unit_get_max_hp (this) - this->Body.Damage) > 0) ? - patch_Unit_can_move_to_adjacent_tile (this, __, neighbor_index, param_2) : - AMV_1; + is->ogl_line_width = width; + OpenGLRenderer_set_line_width (this, __, width); +} + +void __fastcall +patch_OpenGLRenderer_enable_line_dashing (OpenGLRenderer * this) +{ + is->ogl_line_stipple_enabled = true; + OpenGLRenderer_enable_line_dashing (this); +} + +void __fastcall +patch_OpenGLRenderer_disable_line_dashing (OpenGLRenderer * this) +{ + is->ogl_line_stipple_enabled = false; + OpenGLRenderer_disable_line_dashing (this); +} + +void __fastcall +patch_OpenGLRenderer_draw_line (OpenGLRenderer * this, int edx, int x1, int y1, int x2, int y2) +{ + if ((is->current_config.draw_lines_using_gdi_plus == LDO_NEVER) || + ((is->current_config.draw_lines_using_gdi_plus == LDO_WINE) && ! is->running_on_wine)) + OpenGLRenderer_draw_line (this, __, x1, y1, x2, y2); + + else if ((is->gdi_plus.init_state == IS_OK) && (is->gdi_plus.gp_graphics != NULL)) { + void * gp_pen; + int unit_world = 0; // = UnitWorld from gdiplusenums.h + int status = is->gdi_plus.CreatePen1 (is->ogl_color, (float)is->ogl_line_width, unit_world, &gp_pen); + if (status == 0) { + if (is->ogl_line_stipple_enabled) + is->gdi_plus.SetPenDashStyle (gp_pen, 1); // 1 = DashStyleDash from GdiPlusEnums.h + is->gdi_plus.DrawLineI (is->gdi_plus.gp_graphics, gp_pen, x1, y1, x2, y2); + is->gdi_plus.DeletePen (gp_pen); + } + } } -// Checks unit's HP after it was possibly hit by ZoC and deals with the consequences if it's dead. Does nothing if config option to make ZoC lethal -// isn't set or if interceptor is NULL. Returns true if the unit was killed, false otherwise. -bool -check_life_after_zoc (Unit * unit, Unit * interceptor) +int __fastcall +patch_Tile_check_water_for_retreat_on_defense (Tile * this) { - if ((is->current_config.special_zone_of_control_rules & SZOCR_LETHAL) && (interceptor != NULL) && - ((Unit_get_max_hp (unit) - unit->Body.Damage) <= 0)) { - - // Call Unit::score_kill but turn off enslaving if we're configured to stop enslaving after bombardment and the ZoC was performed by - // bombardment, which is always the case for cross-domain ZoC or when bombard str > attack. - UnitType * interceptor_type = &p_bic_data->UnitTypes[interceptor->Body.UnitTypeID]; - if (is->current_config.prevent_enslaving_by_bombardment && - ((p_bic_data->UnitTypes[unit->Body.UnitTypeID].Unit_Class != interceptor_type->Unit_Class) || - (interceptor_type->Bombard_Strength >= Unit_get_attack_strength (interceptor)))) - is->do_not_enslave_units = true; - Unit_score_kill (interceptor, __, unit, false); - is->do_not_enslave_units = false; + Unit * defender = p_bic_data->fighter.defender; - if ((! is_online_game ()) && Fighter_check_combat_anim_visibility (&p_bic_data->fighter, __, interceptor, unit, true)) - Animator_play_one_shot_unit_animation (&p_main_screen_form->animator, __, unit, AT_DEATH, false); + // Under the standard game rules, defensively retreating onto a water tile is not allowed. Set retreat_blocked to true if "this" is a water + // tile and we're not configured to allow retreating onto water tiles. + bool retreat_blocked; { + if (this->vtable->m35_Check_Is_Water (this)) { + if ( is->current_config.allow_defensive_retreat_on_water + && defender != NULL + && p_bic_data->UnitTypes[defender->Body.UnitTypeID].Unit_Class == UTC_Sea + && ( is->current_config.limit_defensive_retreat_on_water_to_types.len == 0 + || (bool)itable_look_up_or (&is->current_config.limit_defensive_retreat_on_water_to_types, defender->Body.UnitTypeID, 0))) + retreat_blocked = false; + else + retreat_blocked = true; + } else + retreat_blocked = false; + } - bool prev_always_despawn_passengers = is->always_despawn_passengers; - is->always_despawn_passengers = is_land_transport (unit) && (is->current_config.land_transport_rules & LTR_NO_ESCAPE); - patch_Unit_despawn (unit, __, interceptor->Body.CivID, 0, 0, 0, 0, 0, 0); - is->always_despawn_passengers = prev_always_despawn_passengers; + // Check stack limit + if ((! retreat_blocked) && + (defender != NULL) && + ! is_below_stack_limit (this, defender->Body.CivID, defender->Body.UnitTypeID)) + retreat_blocked = true; - return true; - } else - return false; + // The return from this call is only used to filter the given tile as a possible retreat destination based on whether it's water or not + return (int)retreat_blocked; } int __fastcall -patch_Unit_move_to_adjacent_tile (Unit * this, int edx, int neighbor_index, bool param_2, int param_3, byte param_4) +patch_City_count_airports_for_airdrop (City * this, int edx, enum ImprovementTypeFlags airport_flag) { - is->moving_unit_to_adjacent_tile = true; - - is->zoc_interceptor = is->zoc_defender = NULL; - int tr = Unit_move_to_adjacent_tile (this, __, neighbor_index, param_2, param_3, param_4); - if ((this == is->zoc_defender) && check_life_after_zoc (this, is->zoc_interceptor)) - tr = ! is_online_game (); // This is what the original method returns when the unit was destroyed in combat + if (is->current_config.allow_airdrop_without_airport) + return 1; + else + return City_count_improvements_with_flag (this, __, airport_flag); +} - is->temporarily_disallow_lethal_zoc = false; - is->moving_unit_to_adjacent_tile = false; +int __fastcall +patch_Leader_get_cont_city_count_for_worker_req (Leader * this, int edx, int cont_id) +{ + int tr = Leader_get_city_count_on_continent (this, __, cont_id); + if (is->current_config.ai_worker_requirement_percent != 100) + tr = (tr * is->current_config.ai_worker_requirement_percent + 50) / 100; return tr; } int __fastcall -patch_Unit_teleport (Unit * this, int edx, int tile_x, int tile_y, Unit * unit_telepad) +patch_Leader_get_city_count_for_worker_prod_cap (Leader * this) { - is->zoc_interceptor = NULL; - int tr = Unit_teleport (this, __, tile_x, tile_y, unit_telepad); - check_life_after_zoc (this, is->zoc_interceptor); + int tr = this->Cities_Count; + // Don't scale down the cap since it's pretty low to begin with + if (is->current_config.ai_worker_requirement_percent > 100) + tr = (tr * is->current_config.ai_worker_requirement_percent + 50) / 100; return tr; } -bool -can_do_defensive_bombard (Unit * unit, UnitType * type) +// If "only_improv_id" is >= 0, will only return yields from mills attached to that improv, otherwise returns all yields from all improvs +void +gather_mill_yields (City * city, int only_improv_id, int * out_food, int * out_shields, int * out_commerce) { - if ((type->Bombard_Strength > 0) && (! Unit_has_ability (unit, __, UTA_Cruise_Missile))) { - if (cannot_defend_inside_transport (unit)) - return false; - - if ((unit->Body.Status & USF_USED_DEFENSIVE_BOMBARD) == 0) // has not already done DB this turn - return true; - - // If the "blitz" special DB rule is activated and this unit has blitz, check if it still has an extra DB to use - else if ((is->current_config.special_defensive_bombard_rules & SDBR_BLITZ) && UnitType_has_ability (type, __, UTA_Blitz)) { - int extra_dbs = itable_look_up_or (&is->extra_defensive_bombards, unit->Body.ID, 0); - return type->Movement > extra_dbs + 1; - - } else - return false; - } else - return false; + int food = 0, shields = 0, commerce = 0; + for (int n = 0; n < is->current_config.count_mills; n++) { + struct mill * mill = &is->current_config.mills[n]; + if ((mill->flags & MF_YIELDS) && + ((only_improv_id < 0) || (mill->improv_id == only_improv_id)) && + can_generate_resource (city->Body.CivID, mill) && + has_active_building (city, mill->improv_id) && + has_resources_required_by_building (city, mill->improv_id)) { + Resource_Type * res = &p_bic_data->ResourceTypes[mill->resource_id]; + food += res->Food; + shields += res->Shield; + commerce += res->Commerce; + } + } + *out_food = food; + *out_shields = shields; + *out_commerce = commerce; } -Unit * __fastcall -patch_Fighter_find_defensive_bombarder (Fighter * this, int edx, Unit * attacker, Unit * defender) +int __fastcall +patch_City_calc_tile_yield_while_gathering (City * this, int edx, YieldKind kind, int tile_x, int tile_y) { - int special_db_rules = is->current_config.special_defensive_bombard_rules; - if ((special_db_rules == 0) && - ((is->current_config.land_transport_rules & LTR_NO_DEFENSE_FROM_INSIDE) == 0) && - ((is->current_config.special_helicopter_rules & SHR_NO_DEFENSE_FROM_INSIDE) == 0)) - return Fighter_find_defensive_bombarder (this, __, attacker, defender); - else { - enum UnitTypeClasses attacker_class = p_bic_data->UnitTypes[attacker->Body.UnitTypeID].Unit_Class; - int attacker_has_one_hp = Unit_get_max_hp (attacker) - attacker->Body.Damage <= 1; + int tr = City_calc_tile_yield_at (this, __, kind, tile_x, tile_y); - Tile * defender_tile = tile_at (defender->Body.X, defender->Body.Y); - if ((Unit_get_defense_strength (attacker) < 1) || // if attacker cannot defend OR - (defender_tile == NULL) || (defender_tile == p_null_tile) || // defender tile is invalid OR - (((special_db_rules & SDBR_LETHAL) == 0) && attacker_has_one_hp) || // (DB is non-lethal AND attacker has one HP remaining) OR - ((special_db_rules & SDBR_NOT_INVISIBLE) && ! patch_Unit_is_visible_to_civ (attacker, __, defender->Body.CivID, 1))) // (invisible units are immune to DB AND attacker is invisible) - return NULL; + // Include yields from generated resources + if ((this->Body.X == tile_x) && (this->Body.Y == tile_y)) { + int mill_food, mill_shields, mill_commerce; + gather_mill_yields (this, -1, &mill_food, &mill_shields, &mill_commerce); + if (kind == YK_FOOD) tr += mill_food; + else if (kind == YK_SHIELDS) tr += mill_shields; + else if (kind == YK_COMMERCE) tr += mill_commerce; - Unit * tr = NULL; - int highest_strength = -1; - enum UnitTypeAbilities lethal_bombard_req = (attacker_class == UTC_Sea) ? UTA_Lethal_Sea_Bombardment : UTA_Lethal_Land_Bombardment; - FOR_UNITS_ON (uti, defender_tile) { - Unit * candidate = uti.unit; - UnitType * candidate_type = &p_bic_data->UnitTypes[candidate->Body.UnitTypeID]; - if (can_do_defensive_bombard (candidate, candidate_type) && - (candidate_type->Bombard_Strength > highest_strength) && - (candidate != defender) && - (Unit_get_containing_army (candidate) != defender) && - ((attacker_class == candidate_type->Unit_Class) || - ((special_db_rules & SDBR_AERIAL) && - (candidate_type->Unit_Class == UTC_Air) && - (candidate_type->Air_Missions & UCV_Bombing)) || - ((special_db_rules & SDBR_DOCKED_VS_LAND) && - (candidate_type->Unit_Class == UTC_Sea) && - (get_city_ptr (defender_tile->CityID) != NULL))) && - ((! attacker_has_one_hp) || UnitType_has_ability (candidate_type, __, lethal_bombard_req))) { - tr = candidate; - highest_strength = candidate_type->Bombard_Strength; - } + if (is->current_config.enable_districts || is->current_config.enable_natural_wonders) { + int bonus_food = 0, bonus_shields = 0, bonus_gold = 0; + calculate_city_center_district_bonus (this, &bonus_food, &bonus_shields, &bonus_gold); + if (kind == YK_FOOD) tr += bonus_food; + else if (kind == YK_SHIELDS) tr += bonus_shields; + else if (kind == YK_COMMERCE) tr += bonus_gold; } - return tr; } + + return tr; } -void __fastcall -patch_Fighter_damage_by_db_in_main_loop (Fighter * this, int edx, Unit * bombarder, Unit * defender) +bool __fastcall +patch_Leader_record_export (Leader * this, int edx, int importer_civ_id, int resource_id) { - if (p_bic_data->UnitTypes[bombarder->Body.UnitTypeID].Unit_Class == UTC_Air) { - if (Unit_try_flying_over_tile (bombarder, __, defender->Body.X, defender->Body.Y)) - return; // intercepted - else if (patch_Main_Screen_Form_is_unit_visible_to_player (p_main_screen_form, __, defender->Body.X, defender->Body.Y, bombarder)) - Unit_play_bombing_animation (bombarder, __, defender->Body.X, defender->Body.Y); - } + bool exported = Leader_record_export (this, __, importer_civ_id, resource_id); + if (exported && + (is->mill_input_resource_bits[resource_id>>3] & (1 << (resource_id & 7)))) // if the traded resource is an input to any mill + is->must_recompute_resources_for_mill_inputs = true; + return exported; +} - // If the unit has already performed DB this turn, then record that it's consumed one of its extra DBs - if (bombarder->Body.Status & USF_USED_DEFENSIVE_BOMBARD) { - int extra_dbs = itable_look_up_or (&is->extra_defensive_bombards, bombarder->Body.ID, 0); - itable_insert (&is->extra_defensive_bombards, bombarder->Body.ID, extra_dbs + 1); +bool __fastcall +patch_Leader_erase_export (Leader * this, int edx, int importer_civ_id, int resource_id) +{ + bool erased = Leader_erase_export (this, __, importer_civ_id, resource_id); + if (erased && + (is->mill_input_resource_bits[resource_id>>3] & (1 << (resource_id & 7)))) // if the traded resource is an input to any mill + is->must_recompute_resources_for_mill_inputs = true; + return erased; +} + +int __fastcall +patch_Sprite_draw_improv_img_on_city_form (Sprite * this, int edx, PCX_Image * canvas, int pixel_x, int pixel_y, PCX_Color_Table * color_table) +{ + int tr = Sprite_draw (this, __, canvas, pixel_x, pixel_y, color_table); + + int generated_resources[16]; + int generated_resource_count = 0; + for (int n = 0; (n < is->current_config.count_mills) && (generated_resource_count < ARRAY_LEN (generated_resources)); n++) { + struct mill * mill = &is->current_config.mills[n]; + if ((mill->improv_id == is->drawing_icons_for_improv_id) && + ((mill->flags & MF_SHOW_BONUS) || (p_bic_data->ResourceTypes[mill->resource_id].Class != RC_Bonus)) && + (! ((mill->flags & MF_HIDE_NON_BONUS) && (p_bic_data->ResourceTypes[mill->resource_id].Class != RC_Bonus))) && + can_generate_resource (p_city_form->CurrentCity->Body.CivID, mill) && + has_active_building (p_city_form->CurrentCity, mill->improv_id) && + has_resources_required_by_building (p_city_form->CurrentCity, mill->improv_id)) + generated_resources[generated_resource_count++] = mill->resource_id; } - int damage_before = defender->Body.Damage; - Fighter_damage_by_defensive_bombard (this, __, bombarder, defender); - int damage_after = defender->Body.Damage; + if ((generated_resource_count > 0) && (is->resources_sheet != NULL) && (is->resources_sheet->JGL.Image != NULL) && (TransparentBlt != NULL)) { + JGL_Image * jgl_canvas = canvas->JGL.Image, + * jgl_sheet = is->resources_sheet->JGL.Image; - is->dbe.bombarder = bombarder; - is->dbe.defender = defender; - if (damage_after > damage_before) { - is->dbe.damage_done = true; - int max_hp = Unit_get_max_hp (defender); - int dead_before = damage_before >= max_hp, dead_after = damage_after >= max_hp; + HDC canvas_dc = jgl_canvas->vtable->acquire_dc (jgl_canvas); + if (canvas_dc != NULL) { + HDC sheet_dc = jgl_sheet->vtable->acquire_dc (jgl_sheet); + if (sheet_dc != NULL) { - // If the unit was killed by defensive bombard, play its death animation then toggle off animations for the rest of the combat so it - // doesn't look like anything else happens. Technically, the combat continues and the dead unit is guarantted to lose because the - // patch to get_combat_odds ensures the dead unit has no chance of winning a round. - if (dead_before ^ dead_after) { - is->dbe.defender_was_destroyed = true; - if ((! is_online_game ()) && Fighter_check_combat_anim_visibility (this, __, bombarder, defender, true)) - Animator_play_one_shot_unit_animation (&p_main_screen_form->animator, __, defender, AT_DEATH, false); - is->dbe.saved_animation_setting = this->play_animations; - this->play_animations = 0; + for (int n = 0; n < generated_resource_count; n++) { + int icon_id = p_bic_data->ResourceTypes[generated_resources[n]].IconID, + sheet_row = icon_id / 6, + sheet_col = icon_id % 6; + + int dy = (n * 160 / not_below (1, generated_resource_count - 1) + 5) / 10; + TransparentBlt (canvas_dc, // dest DC + pixel_x + 15, pixel_y + dy, 24, 24, // dest x, y, width, height + sheet_dc, // src DC + 9 + 50*sheet_col, 9 + 50*sheet_row, 33, 33, // src x, y, width, height + 0xFF00FF); // transparent color (RGB) + } + + jgl_sheet->vtable->release_dc (jgl_sheet, __, 1); + } + jgl_canvas->vtable->release_dc (jgl_canvas, __, 1); } } + return tr; } -int __fastcall -patch_Fighter_get_odds_for_main_combat_loop (Fighter * this, int edx, Unit * attacker, Unit * defender, bool bombarding, bool ignore_defensive_bonuses) +void __cdecl +patch_draw_improv_icons_on_city_screen (Base_List_Control * control, int improv_id, int item_index, int offset_x, int offset_y) { - // If the attacker was destroyed by defensive bombard, return a number that will ensure the defender wins the first round of combat, otherwise - // the zero HP attacker might go on to win an absurd victory. (The attacker in the overall combat is the defender during DB). - if (is->dbe.defender_was_destroyed) - return 1025; - - else - return Fighter_get_combat_odds (this, __, attacker, defender, bombarding, ignore_defensive_bonuses); + is->drawing_icons_for_improv_id = improv_id; + draw_improv_icons_on_city_screen (control, improv_id, item_index, offset_x, offset_y); + is->drawing_icons_for_improv_id = -1; } -byte __fastcall -patch_Fighter_fight (Fighter * this, int edx, Unit * attacker, int attack_direction, Unit * defender_or_null) +int __fastcall +patch_City_get_tourism_amount_to_draw (City * this, int edx, int improv_id) { - byte tr = Fighter_fight (this, __, attacker, attack_direction, defender_or_null); - is->dbe = (struct defensive_bombard_event) {0}; - return tr; -} + is->tourism_icon_counter = 0; -void __fastcall -patch_Unit_score_kill_by_defender (Unit * this, int edx, Unit * victim, bool was_attacking) -{ - // This function is called when the defender wins in combat. If the attacker was actually killed by defensive bombardment, then award credit - // for that kill to the defensive bombarder not the defender in combat. - if (is->dbe.defender_was_destroyed) { - is->do_not_enslave_units = is->current_config.prevent_enslaving_by_bombardment; - Unit_score_kill (is->dbe.bombarder, __, victim, was_attacking); - is->do_not_enslave_units = false; - p_bic_data->fighter.play_animations = is->dbe.saved_animation_setting; + int mill_food, mill_shields, mill_commerce; + gather_mill_yields (this, improv_id, &mill_food, &mill_shields, &mill_commerce); + int combined_commerce = mill_commerce + City_get_tourism_amount (this, __, improv_id); - } else - Unit_score_kill (this, __, victim, was_attacking); + is->convert_displayed_tourism_to_food = mill_food; + is->convert_displayed_tourism_to_shields = mill_shields; + is->combined_tourism_and_mill_commerce = combined_commerce; + return int_abs (mill_food) + int_abs (mill_shields) + int_abs (combined_commerce); } -void __fastcall -patch_Unit_play_attack_anim_for_def_bombard (Unit * this, int edx, int direction) +int __fastcall +patch_Sprite_draw_tourism_gold (Sprite * this, int edx, PCX_Image * canvas, int pixel_x, int pixel_y, PCX_Color_Table * color_table) { - // Don't play any animation for air units, the animations are instead handled in the patch for damage_by_defensive_bombard - if (p_bic_data->UnitTypes[this->Body.UnitTypeID].Unit_Class != UTC_Air) { - - // Make sure the unit is displayed if it's in an army and we're configured for that - struct unit_display_override saved_udo = is->unit_display_override; - Unit * container; - if (is->current_config.show_armies_performing_defensive_bombard && - (container = get_unit_ptr (this->Body.Container_Unit)) != NULL && - Unit_has_ability (container, __, UTA_Army)) - is->unit_display_override = (struct unit_display_override) { this->Body.ID, this->Body.X, this->Body.Y }; - - Unit_play_attack_animation (this, __, direction); - - is->unit_display_override = saved_udo; + // Replace the yield sprite we're drawing with food or a shield if needed. + Sprite * sprite = NULL; { + if (is->tourism_icon_counter < int_abs (is->convert_displayed_tourism_to_food)) { + if (is->convert_displayed_tourism_to_food >= 0) + sprite = &p_city_form->City_Icons_Images.Icon_15_Food; + else { + init_red_food_icon (); + if (is->red_food_icon_state == IS_OK) + sprite = &is->red_food_icon; + } + } else if (is->tourism_icon_counter < int_abs (is->convert_displayed_tourism_to_food) + int_abs (is->convert_displayed_tourism_to_shields)) + sprite = (is->convert_displayed_tourism_to_shields >= 0) ? &p_city_form->City_Icons_Images.Icon_13_Shield : &p_city_form->City_Icons_Images.Icon_05_Shield_Outcome; + else if (is->combined_tourism_and_mill_commerce < 0) + sprite = &p_city_form->City_Icons_Images.Icon_17_Gold_Outcome; + else + sprite = this; } -} -bool -can_precision_strike_tile_improv_at (Unit * unit, int x, int y) -{ - Tile * tile; - return is->current_config.allow_precision_strikes_against_tile_improvements && // we're configured to allow prec. strikes vs tiles AND - ((tile = tile_at (x, y)) != p_null_tile) && // get tile, make sure it's valid AND - is_explored (tile, unit->Body.CivID) && // tile has been explored by attacker AND - has_any_destructible_overlays (tile, true); // it has something that can be destroyed by prec. strike + int tr = 0; // return value is not used by caller + if (sprite != NULL) + tr = Sprite_draw (sprite, __, canvas, pixel_x, pixel_y, color_table); + is->tourism_icon_counter++; + return tr; } -// Same as above function except this one applies to the V3 field instead of FOWStatus -Tile * __cdecl -patch_tile_at_for_v3_check (int x, int y) +Unit * __fastcall +patch_Leader_spawn_unit (Leader * this, int edx, int type_id, int tile_x, int tile_y, int barb_tribe_id, int id, bool param_6, LeaderKind leader_kind, int race_id) { - Tile * tile = tile_at (x, y); - int is_hotseat_game = *p_is_offline_mp_game && ! *p_is_pbem_game; - if (is_hotseat_game && is->current_config.share_visibility_in_hotseat) { - is->dummy_tile->Body.V3 = ((tile->Body.V3 & *p_human_player_bits) != 0) << p_main_screen_form->Player_CivID; - return is->dummy_tile; - } else - return tile; -} + int spawn_x = tile_x, + spawn_y = tile_y; -bool __fastcall -patch_Unit_check_contact_bit_6_on_right_click (Unit * this, int edx, int civ_id) -{ - bool tr = Unit_check_contact_bit_6 (this, __, civ_id); - if ((! tr) && - is->current_config.share_visibility_in_hotseat && - (*p_is_offline_mp_game && ! *p_is_pbem_game) && // is hotseat game - ((1 << civ_id) & *p_human_player_bits)) { // is civ_id a human player - if ((1 << this->Body.CivID) & *p_human_player_bits) - tr = true; + if (is->current_config.enable_districts) { + UnitType * type = &p_bic_data->UnitTypes[type_id]; + if ((type->Unit_Class == UTC_Air) && + is->current_config.enable_aerodrome_districts && + is->current_config.air_units_use_aerodrome_districts_not_cities) { + City * spawn_city = city_at (tile_x, tile_y); + if ((spawn_city != NULL) && (spawn_city->Body.CivID == this->ID)) { + int district_x, district_y; + Tile * district_tile = get_completed_district_tile_for_city (spawn_city, AERODROME_DISTRICT_ID, &district_x, &district_y); + if ((district_tile != NULL) && (district_tile != p_null_tile) && + (district_tile->Territory_OwnerID == this->ID) && + is_below_stack_limit (district_tile, this->ID, type_id)) { + spawn_x = district_x; + spawn_y = district_y; + } + } + } - else { - // Check if any other human player has contact - unsigned player_bits = *(unsigned *)p_human_player_bits >> 1; - int n_player = 1; - while (player_bits != 0) { - if ((player_bits & 1) && (n_player != civ_id) && - Unit_check_contact_bit_6 (this, __, n_player)) { - tr = true; - break; + if ((type->Unit_Class == UTC_Sea) && + is->current_config.enable_port_districts && + is->current_config.naval_units_use_port_districts_not_cities) { + City * spawn_city = city_at (tile_x, tile_y); + if ((spawn_city != NULL) && (spawn_city->Body.CivID == this->ID)) { + int district_x, district_y; + Tile * district_tile = get_completed_district_tile_for_city (spawn_city, PORT_DISTRICT_ID, &district_x, &district_y); + if ((district_tile != NULL) && (district_tile != p_null_tile) && + (district_tile->Territory_OwnerID == this->ID) && + is_below_stack_limit (district_tile, this->ID, type_id)) { + spawn_x = district_x; + spawn_y = district_y; } - player_bits >>= 1; - n_player++; } } } + + Unit * tr = Leader_spawn_unit (this, __, type_id, spawn_x, spawn_y, barb_tribe_id, id, param_6, leader_kind, race_id); + if (tr != NULL) + change_unit_type_count (this, type_id, 1); return tr; } -bool __fastcall -patch_Unit_check_precision_strike_target (Unit * this, int edx, int tile_x, int tile_y) +Unit * __fastcall +patch_Leader_spawn_captured_unit (Leader * this, int edx, int type_id, int tile_x, int tile_y, int barb_tribe_id, int id, bool param_6, LeaderKind leader_kind, int race_id) { - return Unit_check_precision_strike_target (this, __, tile_x, tile_y) || can_precision_strike_tile_improv_at (this, tile_x, tile_y); + Unit * tr = patch_Leader_spawn_unit (this, __, type_id, tile_x, tile_y, barb_tribe_id, id, param_6, leader_kind, race_id); + if ((tr != NULL) && is->moving_unit_to_adjacent_tile) + is->temporarily_disallow_lethal_zoc = true; + return tr; } void __fastcall -patch_Unit_play_attack_animation_vs_tile (Unit * this, int edx, int direction) +patch_Leader_enter_new_era (Leader * this, int edx, bool param_1, bool no_online_sync) { - if (is->current_config.polish_precision_striking && - UnitType_has_ability (&p_bic_data->UnitTypes[this->Body.UnitTypeID], __, UTA_Cruise_Missile) && - (is->attacking_tile_x != this->Body.X) && (is->attacking_tile_y != this->Body.Y)) - Unit_animate_cruise_missile_strike (this, __, is->attacking_tile_x, is->attacking_tile_y); - else - Unit_play_attack_animation (this, __, direction); + Leader_enter_new_era (this, __, param_1, no_online_sync); + apply_era_specific_names (this); +} + +char * __fastcall +patch_Leader_get_player_title_for_intro_popup (Leader * this) +{ + // Normally the era-specific names are applied later, after game loading is finished, but in this case we apply the player's names ahead of + // time so they appear on the intro popup. "this" will always refer to the human player in this call. + apply_era_specific_names (this); + return Leader_get_title (this); } void __fastcall -patch_Unit_attack_tile (Unit * this, int edx, int x, int y, int bombarding) +patch_City_spawn_unit_if_done (City * this) { - Tile * target_tile = NULL; - bool had_district_before = false; - int district_id_before = -1; - int tile_x = x; - int tile_y = y; + bool skip_spawn = false; - if (is->current_config.enable_districts) { + // Apply unit limit. If this city's owner has reached the limit for the unit type it's building then force it to build something else. + int available; + if ((this->Body.Order_Type == COT_Unit) && + get_available_unit_count (&leaders[this->Body.CivID], this->Body.Order_ID, &available) && + (available <= 0)) { + int limited_unit_type_id = this->Body.Order_ID; - // Check if this is a completed wonder district that cannot be destroyed - if (is->current_config.enable_wonder_districts && - !is->current_config.completed_wonder_districts_can_be_destroyed) { - wrap_tile_coords (&p_bic_data->Map, &tile_x, &tile_y); - target_tile = tile_at (tile_x, tile_y); - if ((target_tile != NULL) && (target_tile != p_null_tile)) { - struct wonder_district_info * info = get_wonder_district_info (target_tile); - if (info != NULL && info->state == WDS_COMPLETED) { - // This tile has a completed wonder district and they can't be destroyed - if ((*p_human_player_bits & (1 << this->Body.CivID)) != 0) { - PopupForm * popup = get_popup_form (); - popup->vtable->set_text_key_and_flags (popup, __, is->mod_script_path, "C3X_CANT_PILLAGE", -1, 0, 0, 0); - patch_show_popup (popup, __, 0, 0); + if (*p_human_player_bits & 1<Body.CivID) { + // Find another type ID to build instead of the limited one + int replacement_type_id = -1; { + int limited_unit_strat = p_bic_data->UnitTypes[limited_unit_type_id].AI_Strategy, + shields_in_box = this->Body.StoredProduction; + UnitType * replacement_type; + for (int can_type_id = 0; can_type_id < p_bic_data->UnitTypeCount; can_type_id++) + if (patch_City_can_build_unit (this, __, can_type_id, 1, 0, 0)) { + UnitType * candidate_type = &p_bic_data->UnitTypes[can_type_id]; + + // If we haven't found a replacement yet, use this one + if (replacement_type_id < 0) { + replacement_type_id = can_type_id; + replacement_type = candidate_type; + + // Keep the prev replacement if it doesn't waste shields but this candidate would + } else if ((replacement_type->Cost >= shields_in_box) && (candidate_type->Cost < shields_in_box)) + continue; + + // Keep the prev if it shares an AI strategy with the limited unit but this candidate doesn't + else if (((replacement_type->AI_Strategy & limited_unit_strat) != 0) && + ((candidate_type ->AI_Strategy & limited_unit_strat) == 0)) + continue; + + // At this point we know switching to the candidate would not cause us to waste shields and would not + // give us a worse match role-wise to the original limited unit. So pick it if it's better somehow, + // either a better role match or more expensive. + else if ((candidate_type->Cost > replacement_type->Cost) || + (((replacement_type->AI_Strategy & limited_unit_strat) == 0) && + ((candidate_type ->AI_Strategy & limited_unit_strat) != 0))) { + replacement_type_id = can_type_id; + replacement_type = candidate_type; + } } - return; + } + + if (replacement_type_id >= 0) { + City_set_production (this, __, COT_Unit, replacement_type_id, false); + if (this->Body.CivID == p_main_screen_form->Player_CivID) { + PopupForm * popup = get_popup_form (); + set_popup_str_param (0, this->Body.CityName, -1, -1); + set_popup_str_param (1, p_bic_data->UnitTypes[limited_unit_type_id].Name, -1, -1); + int limit = -1; + get_unit_limit (&leaders[this->Body.CivID], limited_unit_type_id, &limit); + set_popup_int_param (2, limit); + set_popup_str_param (3, p_bic_data->UnitTypes[replacement_type_id].Name, -1, -1); + popup->vtable->set_text_key_and_flags (popup, __, is->mod_script_path, "C3X_LIMITED_UNIT_CHANGE", -1, 0, 0, 0); + int response = patch_show_popup (popup, __, 0, 0); + if (response == 0) + *p_zoom_to_city_after_update = true; } } - } - wrap_tile_coords (&p_bic_data->Map, &tile_x, &tile_y); - target_tile = tile_at (tile_x, tile_y); - if ((target_tile != NULL) && (target_tile != p_null_tile)) { - struct district_instance * inst = get_district_instance (target_tile); - if (inst != NULL) { - had_district_before = true; - district_id_before = inst->district_type; - } + } else { + City_Order order; + patch_City_ai_choose_production (this, __, &order); + City_set_production (this, __, order.OrderType, order.OrderID, false); } + + // If the player changed production to something other than a unit, don't spawn anything + if (this->Body.Order_Type != COT_Unit) + skip_spawn = true; + + // Just as a final check, if we weren't able to switch production off the limited unit, prevent it from being spawned so the limit + // doesn't get violated. + if ((this->Body.Order_Type == COT_Unit) && (this->Body.Order_ID == limited_unit_type_id)) + skip_spawn = true; } - is->attacking_tile_x = x; - is->attacking_tile_y = y; - if (bombarding) - is->unit_bombard_attacking_tile = this; + // Check district requirements for air and naval units + if ((! skip_spawn) && (this->Body.Order_Type == COT_Unit) && is->current_config.enable_districts) { + int unit_id = this->Body.Order_ID; + UnitType * type = &p_bic_data->UnitTypes[unit_id]; + bool needs_district = false; + int required_district_id = -1; - Unit_attack_tile (this, __, x, y, bombarding); + // Air units require aerodrome + if ((type->Unit_Class == UTC_Air) && + is->current_config.enable_aerodrome_districts && + is->current_config.air_units_use_aerodrome_districts_not_cities && + (! city_has_required_district (this, AERODROME_DISTRICT_ID))) { + needs_district = true; + required_district_id = AERODROME_DISTRICT_ID; + } + // Naval units require port + else if ((type->Unit_Class == UTC_Sea) && + is->current_config.enable_port_districts && + is->current_config.naval_units_use_port_districts_not_cities && + (! city_has_required_district (this, PORT_DISTRICT_ID))) { + needs_district = true; + required_district_id = PORT_DISTRICT_ID; + } - // Check if the district was destroyed by the attack - if (had_district_before && (target_tile != NULL) && (target_tile != p_null_tile) && district_id_before != NATURAL_WONDER_DISTRICT_ID) { - struct district_instance * inst_after = get_district_instance (target_tile); - bool has_district_after = (inst_after != NULL); - int district_id_after = (inst_after != NULL) ? inst_after->district_type : -1; + if (needs_district) { + bool is_human = (*p_human_player_bits & (1 << this->Body.CivID)) != 0; - // If the district existed before but not after, or the tile no longer has a mine, the district was destroyed - if (!has_district_after || !(target_tile->vtable->m42_Get_Overlays (target_tile, __, 0) & TILE_FLAG_MINE)) { - handle_district_destroyed_by_attack (target_tile, tile_x, tile_y, true); + // Mark district needed for AI + if (! is_human) + mark_city_needs_district (this, required_district_id); + + // Find fallback land unit + City_Order defensive_order = { .OrderID = -1, .OrderType = 0 }; + if (choose_defensive_unit_order (this, &defensive_order)) { + UnitType * def_type = &p_bic_data->UnitTypes[defensive_order.OrderID]; + if (def_type->Unit_Class == UTC_Land) + City_set_production (this, __, defensive_order.OrderType, defensive_order.OrderID, false); + } + + // Show message to human player + if (is_human && (this->Body.CivID == p_main_screen_form->Player_CivID)) { + char msg[160]; + char const * unit_name = type->Name; + char const * district_name = is->district_configs[required_district_id].name; + snprintf (msg, sizeof msg, "%s %s %s", unit_name, is->c3x_labels[CL_CONSTRUCTION_HALTED_DUE_TO_MISSING_DISTRICT], district_name); + msg[(sizeof msg) - 1] = '\0'; + show_map_specific_text (this->Body.X, this->Body.Y, msg, true); + } + + skip_spawn = true; } } - is->unit_bombard_attacking_tile = NULL; - is->attacking_tile_x = is->attacking_tile_y = -1; + if (! skip_spawn) + City_spawn_unit_if_done (this); } void __fastcall -patch_Unit_do_precision_strike (Unit * this, int edx, int x, int y) -{ - if ((city_at (x, y) == NULL) && can_precision_strike_tile_improv_at (this, x, y)) - patch_Unit_attack_tile (this, __, x, y, 1); - else - Unit_do_precision_strike (this, __, x, y); - - if (is->current_config.polish_precision_striking && - UnitType_has_ability (&p_bic_data->UnitTypes[this->Body.UnitTypeID], __, UTA_Cruise_Missile)) - patch_Unit_despawn (this, __, 0, false, false, 0, 0, 0, 0); -} - -int __fastcall -patch_Unit_get_max_moves_after_barricade_attack (Unit * this) +patch_Leader_upgrade_all_units (Leader * this, int edx, int type_id) { - if (is->current_config.dont_end_units_turn_after_bombarding_barricade && (this == is->unit_bombard_attacking_tile)) - return this->Body.Moves + p_bic_data->General.RoadsMovementRate; - else - return patch_Unit_get_max_move_points (this); + is->penciled_in_upgrade_count = 0; + Leader_upgrade_all_units (this, __, type_id); } -City * __cdecl -patch_city_at_in_find_bombard_defender (int x, int y) +void __fastcall +patch_Main_Screen_Form_upgrade_all_units (Main_Screen_Form * this, int edx, int type_id) { - // The caller (Fighter::find_defender_against_bombardment) has a set of lists of bombard priority/eligibility in its stack memory. The list - // for land units bombarding land tiles normally restricts the targets to land units by containing [UTC_Land, -1, -1]. If we're configured to - // remove that restriction, modify the list so it contains instead [UTC_Land, UTC_Sea, UTC_Air]. Conveniently, the offset from this function's - // first parameter to the list is 0x40 bytes in all executables. - if (is->current_config.remove_land_artillery_target_restrictions) { - enum UnitTypeClasses * list = (void *)((byte *)&x + 0x40); - list[1] = UTC_Sea; - list[2] = UTC_Air; - } - - return city_at (x, y); + is->penciled_in_upgrade_count = 0; + Main_Screen_Form_upgrade_all_units (this, __, type_id); } bool __fastcall -patch_Unit_check_bombard_target (Unit * this, int edx, int tile_x, int tile_y) +patch_Unit_can_perform_upgrade_all (Unit * this, int edx, int unit_command_value) { - bool base = Unit_check_bombard_target (this, __, tile_x, tile_y); - Tile * tile; - int overlays; + bool base = patch_Unit_can_perform_command (this, __, unit_command_value); + + // Deal with unit limits. If the unit type we're upgrading to is limited, we need to pencil in the upgrade to make sure that we don't queue up + // so many upgrades that we exceed the limit. + City * city; + int upgrade_id, available; if (base && - is->current_config.disallow_useless_bombard_vs_airfields && - ! Unit_has_ability (this, __, UTA_Nuclear_Weapon) && - ((tile = tile_at (tile_x, tile_y)) != p_null_tile) && - ((overlays = tile->vtable->m42_Get_Overlays (tile, __, 0)) & 0x20000000) && // if tile has an airfield AND - (overlays == 0x20000000)) { // tile only has an airfield - UnitType * this_type = &p_bic_data->UnitTypes[this->Body.UnitTypeID]; + (is->current_config.unit_limits.len > 0) && + (NULL != (city = city_at (this->Body.X, this->Body.Y))) && + (0 <= (upgrade_id = City_get_upgraded_type_id (city, __, this->Body.UnitTypeID))) && + get_available_unit_count (&leaders[this->Body.CivID], upgrade_id, &available)) { - // Check that a bombard attack vs this tile would not be wasted. It won't be if either (1) there are no units on the tile, (2) there - // is a unit that can be damaged by bombarding, or (3) there are no units that can be damaged and no air units. The rules for - // bombardment are that you can't damage aircraft in an airfield and you also can't destroy an airfield from underneath aircraft. - int any_units = 0, - any_vulnerable_units = 0, - any_air_units = 0; - FOR_UNITS_ON (uti, tile) { - enum UnitTypeClasses class = p_bic_data->UnitTypes[uti.unit->Body.UnitTypeID].Unit_Class; - any_units = 1; - any_air_units |= class == UTC_Air; - any_vulnerable_units |= (class != UTC_Air) && (Unit_get_defense_strength (uti.unit) > 0) && - can_damage_bombarding (this_type, uti.unit, tile); + // Find penciled in upgrade. Add a new one if we don't already have one. + struct penciled_in_upgrade * piu = NULL; { + for (int n = 0; n < is->penciled_in_upgrade_count; n++) + if (is->penciled_in_upgrades[n].unit_type_id == upgrade_id) { + piu = &is->penciled_in_upgrades[n]; + break; + } + if (piu == NULL) { + reserve (sizeof is->penciled_in_upgrades[0], (void **)&is->penciled_in_upgrades, &is->penciled_in_upgrade_capacity, is->penciled_in_upgrade_count); + piu = &is->penciled_in_upgrades[is->penciled_in_upgrade_count]; + is->penciled_in_upgrade_count += 1; + piu->unit_type_id = upgrade_id; + piu->count = 0; + } } - return (! any_units) || // case (1) above - any_vulnerable_units || // case (2) - ((! any_air_units) && (! any_vulnerable_units)); // case (3) - - } else - return base; -} - -bool __fastcall -patch_Unit_can_disembark_anything (Unit * this, int edx, int tile_x, int tile_y) -{ - Tile * this_tile = tile_at (this->Body.X, this->Body.Y); - Tile * target_tile = tile_at (tile_x, tile_y); - bool base = Unit_can_disembark_anything (this, __, tile_x, tile_y); - // Apply units per tile limit - if (base) { - bool stack_limited_for_all = true; - FOR_UNITS_ON (uti, this_tile) - if ((uti.unit->Body.Container_Unit == this->Body.ID) && is_below_stack_limit (target_tile, this->Body.CivID, uti.unit->Body.UnitTypeID)) { - stack_limited_for_all = false; - break; - } - if (stack_limited_for_all) + // If we can have more units of the type we're upgrading to, pencil in another upgrade and return true. Otherwise return false so this + // unit isn't considered one of the upgradable ones. + if (piu->count < available) { + piu->count += 1; + return true; + } else return false; - } - - // Apply trespassing restriction. First check if this civ may move into (tile_x, tile_y) without trespassing. If it would be trespassing, then - // we can only disembark anything if this transport has a passenger that can ignore the restriction. Without this check, the game can enter an - // infinite loop under rare circumstances. - if (base && - is->current_config.disallow_trespassing && - check_trespassing (this->Body.CivID, this_tile, target_tile)) { - bool any_exempt_passengers = false; - FOR_UNITS_ON (uti, this_tile) - if ((uti.unit->Body.Container_Unit == this->Body.ID) && is_allowed_to_trespass (uti.unit)) { - any_exempt_passengers = true; - break; - } - return any_exempt_passengers; } else return base; } -int __fastcall -patch_Unit_get_defense_for_bombardable_unit_check (Unit * this) -{ - // Returning a defense value of zero indicates this unit is not a target for bombardment. If configured, exclude all air units from - // bombardment so the attacks target tile improvements instead. Do this only if the tile has another improvement in addition to an airfield, - // or we could destroy the airfield itself. - Tile * tile; - int overlays; - if (is->current_config.allow_bombard_of_other_improvs_on_occupied_airfield && // If configured AND - (p_bic_data->UnitTypes[this->Body.UnitTypeID].Unit_Class == UTC_Air) && // "this" is an air unit AND - ((tile = tile_at (this->Body.X, this->Body.Y)) != p_null_tile) && // "this" is on a valid tile AND - ((overlays = tile->vtable->m42_Get_Overlays (tile, __, 0)) & 0x20000000) && // tile has an airfield AND - (overlays != 0x20000000)) // tile does not only have an airfield - return 0; - - else - return Unit_get_defense_strength (this); -} - void __fastcall -patch_Demographics_Form_m22_draw (Demographics_Form * this) +patch_Fighter_animate_start_of_combat (Fighter * this, int edx, Unit * attacker, Unit * defender) { - Demographics_Form_m22_draw (this); - - if (is->current_config.show_total_city_count) { - // There's proably a better way to get the city count, but better safe than sorry. I don't know if it's possible for the city list to - // contain holes so the surest thing is to check every possible ID. - int city_count = 0; { - if (p_cities->Cities != NULL) - for (int n = 0; n <= p_cities->LastIndex; n++) { - City_Body * body = p_cities->Cities[n].City; - city_count += (body != NULL) && ((int)body != offsetof (City, Body)); - } - } + // Temporarily clear the attacker retreat eligibility flag when needed so naval units are rotated and animated properly. Normally the game + // does not use ranged animations when the attacker can retreat and, worse, not using ranged anims means it also ignores the + // rotate-before-attack setting. + bool restore_attacker_retreat_eligibility = false; + if ((is->current_config.sea_retreat_rules != RR_STANDARD) && + (p_bic_data->UnitTypes[attacker->Body.UnitTypeID].Unit_Class == UTC_Sea) && + Unit_has_ability (attacker, __, UTA_Ranged_Attack_Animation) && + Unit_has_ability (defender, __, UTA_Ranged_Attack_Animation) && + this->attacker_eligible_to_retreat) { + this->attacker_eligible_to_retreat = false; + restore_attacker_retreat_eligibility = true; + } - PCX_Image * canvas = &this->Base.Data.Canvas; + Fighter_animate_start_of_combat (this, __, attacker, defender); - // Draw backdrop - { - char temp_path[2*MAX_PATH]; - PCX_Image backdrop; - PCX_Image_construct (&backdrop); - get_mod_art_path ("CityCountBackdrop.pcx", temp_path, sizeof temp_path); - PCX_Image_read_file (&backdrop, __, temp_path, NULL, 0, 0x100, 2); - if (backdrop.JGL.Image != NULL) { - int w = backdrop.JGL.Image->vtable->m54_Get_Width (backdrop.JGL.Image); - PCX_Image_draw_onto (&backdrop, __, canvas, (1024 - w) / 2, 720); - } - backdrop.vtable->destruct (&backdrop, __, 0); - } + if (restore_attacker_retreat_eligibility) + this->attacker_eligible_to_retreat = true; +} - // Draw text on top of the backdrop - char s[100]; - snprintf (s, sizeof s, "%s %d / %d", is->c3x_labels[CL_TOTAL_CITIES], city_count, is->city_limit); - s[(sizeof s) - 1] = '\0'; - PCX_Image_set_text_effects (canvas, __, 0x80000000, -1, 2, 2); // Set text color to black - PCX_Image_draw_centered_text (canvas, __, get_font (14, FSF_NONE), s, 1024/2 - 100, 730, 200, strlen (s)); - } +Unit * __fastcall +patch_Leader_spawn_unit_from_building (Leader * this, int edx, int type_id, int tile_x, int tile_y, int barb_tribe_id, int id, bool param_6, LeaderKind leader_kind, int race_id) +{ + int available; + if (get_available_unit_count (this, type_id, &available) && (available <= 0)) + return NULL; + else + return patch_Leader_spawn_unit (this, __, type_id, tile_x, tile_y, barb_tribe_id, id, param_6, leader_kind, race_id); } int __fastcall -patch_Leader_get_optimal_city_number (Leader * this) +patch_City_count_improvs_enabling_upgrade (City * this, int edx, enum ImprovementTypeFlags flag) { - if (! is->current_config.strengthen_forbidden_palace_ocn_effect) - return Leader_get_optimal_city_number (this); - else { - int num_sans_fp = Leader_get_optimal_city_number (this), // OCN w/o contrib from num of FPs - fp_count = patch_Leader_count_wonders_with_small_flag (this, __, ITSW_Reduces_Corruption, NULL), - s_diff = p_bic_data->DifficultyLevels[this->player_difficulty].Optimal_Cities, // Difficulty scaling, called "percentage of optimal cities" in the editor - base_ocn = p_bic_data->WorldSizes[p_bic_data->Map.World.World_Size].OptimalCityCount; - return num_sans_fp + (s_diff * fp_count * base_ocn + 50) / 100; // Add 50 to round off - } + return is->current_config.allow_upgrades_in_any_city ? 1 : City_count_improvements_with_flag (this, __, flag); } -int __fastcall -patch_Leader_count_forbidden_palaces_for_ocn (Leader * this, int edx, enum ImprovementTypeSmallWonderFeatures flag, City * city_or_null) +City * __fastcall +patch_Leader_create_city_from_hut (Leader * this, int edx, int x, int y, int race_id, int param_4, char const * name, bool param_6) { - if (! is->current_config.strengthen_forbidden_palace_ocn_effect) - return patch_Leader_count_wonders_with_small_flag (this, __, flag, city_or_null); - else - return 0; // We'll add in the FP effect later with a different weight + City * tr = Leader_create_city (this, __, x, y, race_id, param_4, name, param_6); + if (tr != NULL) + on_gain_city (this, tr, CGR_POPPED_FROM_HUT); + return tr; } -void __fastcall -patch_Trade_Net_recompute_city_connections (Trade_Net * this, int edx, int civ_id, bool redo_road_network, byte param_3, int redo_roads_for_city_id) +City * __fastcall +patch_Leader_create_city_for_ai_respawn (Leader * this, int edx, int x, int y, int race_id, int param_4, char const * name, bool param_6) { - is->is_computing_city_connections = true; - long long ts_before; - QueryPerformanceCounter ((LARGE_INTEGER *)&ts_before); + City * tr = Leader_create_city (this, __, x, y, race_id, param_4, name, param_6); + if (tr != NULL) + on_gain_city (this, tr, CGR_PLACED_FOR_AI_RESPAWN); + return tr; +} - if (is->tnx_init_state == IS_OK) { - if (is->tnx_cache == NULL) { - is->tnx_cache = is->create_tnx_cache (&p_bic_data->Map); - is->set_up_before_building_network (is->tnx_cache); - } else if (! is->keep_tnx_cache) - is->set_up_before_building_network (is->tnx_cache); - } +City * __fastcall +patch_Leader_create_city_for_founding (Leader * this, int edx, int x, int y, int race_id, int param_4, char const * name, bool param_6) +{ + City * tr = Leader_create_city (this, __, x, y, race_id, param_4, name, param_6); + if (tr != NULL) + on_gain_city (this, tr, CGR_FOUNDED); + return tr; +} - Trade_Net_recompute_city_connections (this, __, civ_id, redo_road_network, param_3, redo_roads_for_city_id); +City * __fastcall +patch_Leader_create_city_for_scenario (Leader * this, int edx, int x, int y, int race_id, int param_4, char const * name, bool param_6) +{ + City * tr = Leader_create_city (this, __, x, y, race_id, param_4, name, param_6); + if (tr != NULL) + on_gain_city (this, tr, CGR_PLACED_FOR_SCENARIO); + return tr; +} - if (is->current_config.enable_districts && - is->current_config.enable_distribution_hub_districts) - is->distribution_hub_totals_dirty = true; - long long ts_after; - QueryPerformanceCounter ((LARGE_INTEGER *)&ts_after); - is->time_spent_computing_city_connections += ts_after - ts_before; - is->count_calls_to_recompute_city_connections++; - is->is_computing_city_connections = false; + +bool __fastcall +patch_Leader_do_capture_city (Leader * this, int edx, City * city, bool involuntary, bool converted) +{ + is->currently_capturing_city = city; + on_lose_city (&leaders[city->Body.CivID], city, converted ? CLR_CONVERTED : (involuntary ? CLR_CONQUERED : CLR_TRADED)); + + if (is->current_config.enable_districts && is->current_config.enable_wonder_districts) { + handle_possible_duplicate_small_wonders (city, this); + } + + bool tr = Leader_do_capture_city (this, __, city, involuntary, converted); + on_gain_city (this, city, converted ? CGR_CONVERTED : (involuntary ? CGR_CONQUERED : CGR_TRADED)); + is->currently_capturing_city = NULL; + return tr; } void __fastcall -patch_Map_build_trade_network (Map * this) +patch_City_raze (City * this, int edx, int civ_id_responsible, bool checking_elimination) { - if ((is->tnx_init_state == IS_OK) && (is->tnx_cache != NULL)) - is->set_up_before_building_network (is->tnx_cache); - is->keep_tnx_cache = true; - Map_build_trade_network (this); - is->keep_tnx_cache = false; - if (is->current_config.enable_districts && is->current_config.enable_distribution_hub_districts) - is->distribution_hub_totals_dirty = true; + on_lose_city (&leaders[this->Body.CivID], this, CLR_DESTROYED); + City_raze (this, __, civ_id_responsible, checking_elimination); + + // Delete the extra improvement bits records for this city + City_Improvements * improv_lists[2] = {&this->Body.Improvements_1, &this->Body.Improvements_2}; + for (int n = 0; n < ARRAY_LEN (improv_lists); n++) { + City_Improvements * improv_list = improv_lists[n]; + byte * extra_bits; + if (itable_look_up (&is->extra_city_improvs, (int)improv_list, (int *)&extra_bits)) { + free (extra_bits); + itable_remove (&is->extra_city_improvs, (int)improv_list); + } + } } void __fastcall -patch_Trade_Net_recompute_city_cons_and_res (Trade_Net * this, int edx, bool param_1) +patch_City_draw_hud_icon (City * this, int edx, PCX_Image * canvas, int pixel_x, int pixel_y) { - if ((is->tnx_init_state == IS_OK) && (is->tnx_cache != NULL)) - is->set_up_before_building_network (is->tnx_cache); + Leader * owner = &leaders[this->Body.CivID]; + int restore_capital = owner->CapitalID; - is->keep_tnx_cache = true; - Trade_Net_recompute_city_cons_and_res (this, __, param_1); - is->keep_tnx_cache = false; + // Temporarily set this city as the capital if it has an extra palace so it gets the capital star icon + if ((is->current_config.ai_multi_city_start > 1) && + ((*p_human_player_bits & (1 << owner->ID)) == 0) && + has_extra_palace (this)) + owner->CapitalID = this->Body.ID; - if (is->current_config.enable_districts && is->current_config.enable_distribution_hub_districts) - is->distribution_hub_totals_dirty = true; + City_draw_hud_icon (this, __, canvas, pixel_x, pixel_y); + owner->CapitalID = restore_capital; } -int __fastcall -patch_Trade_Net_set_unit_path_to_fill_road_net (Trade_Net * this, int edx, int from_x, int from_y, int to_x, int to_y, Unit * unit, int civ_id, int flags, int * out_path_length_in_mp) +bool __fastcall +patch_City_has_hud_icon (City * this) { - int tr; - long long ts_before; - QueryPerformanceCounter ((LARGE_INTEGER *)&ts_before); - - if ((is->tnx_init_state == IS_OK) && (is->tnx_cache != NULL)) { - is->flood_fill_road_network (is->tnx_cache, from_x, from_y, civ_id); - tr = 0; // Return value is not used by caller anyway - } else - tr = Trade_Net_set_unit_path (this, __, from_x, from_y, to_x, to_y, unit, civ_id, flags, out_path_length_in_mp); - - long long ts_after; - QueryPerformanceCounter ((LARGE_INTEGER *)&ts_after); - is->time_spent_filling_roads += ts_after - ts_before; - return tr; + return City_has_hud_icon (this) + || ( (is->current_config.ai_multi_city_start > 1) + && ((*p_human_player_bits & (1 << this->Body.CivID)) == 0) + && has_extra_palace (this)); } -bool -set_up_gdi_plus () +void __fastcall +patch_City_draw_on_map (City * this, int edx, Map_Renderer * map_renderer, int pixel_x, int pixel_y, int param_4, int param_5, int param_6, int param_7) { - if (is->gdi_plus.init_state == IS_UNINITED) { - is->gdi_plus.init_state = IS_INIT_FAILED; - is->gdi_plus.gp_graphics = NULL; + Leader * owner = &leaders[this->Body.CivID]; + int restore_capital = owner->CapitalID; - struct startup_input { - UINT32 GdiplusVersion; - void * DebugEventCallback; - BOOL SuppressBackgroundThread; - BOOL SuppressExternalCodecs; - } startup_input = {1, NULL, FALSE, FALSE}; + if (is->current_config.do_not_make_capital_cities_appear_larger) + owner->CapitalID = -1; - is->gdi_plus.module = LoadLibraryA ("gdiplus.dll"); - if (is->gdi_plus.module == NULL) { - MessageBoxA (NULL, "Failed to load gdiplus.dll!", "Error", MB_ICONERROR); - goto end_init; - } + // Temporarily set this city as the capital if it has an extra palace so it gets drawn with the next larger size + else if ((is->current_config.ai_multi_city_start > 1) && + ((*p_human_player_bits & (1 << owner->ID)) == 0) && + has_extra_palace (this)) + owner->CapitalID = this->Body.ID; - int (WINAPI * GdiplusStartup) (ULONG_PTR * out_token, struct startup_input *, void * startup_output) = - (void *)(*p_GetProcAddress) (is->gdi_plus.module, "GdiplusStartup"); + City_draw_on_map (this, __, map_renderer, pixel_x, pixel_y, param_4, param_5, param_6, param_7); + owner->CapitalID = restore_capital; +} - is->gdi_plus.CreateFromHDC = (void *)(*p_GetProcAddress) (is->gdi_plus.module, "GdipCreateFromHDC"); - is->gdi_plus.DeleteGraphics = (void *)(*p_GetProcAddress) (is->gdi_plus.module, "GdipDeleteGraphics"); - is->gdi_plus.SetSmoothingMode = (void *)(*p_GetProcAddress) (is->gdi_plus.module, "GdipSetSmoothingMode"); - is->gdi_plus.SetPenDashStyle = (void *)(*p_GetProcAddress) (is->gdi_plus.module, "GdipSetPenDashStyle"); - is->gdi_plus.CreatePen1 = (void *)(*p_GetProcAddress) (is->gdi_plus.module, "GdipCreatePen1"); - is->gdi_plus.DeletePen = (void *)(*p_GetProcAddress) (is->gdi_plus.module, "GdipDeletePen"); - is->gdi_plus.DrawLineI = (void *)(*p_GetProcAddress) (is->gdi_plus.module, "GdipDrawLineI"); - if ((is->gdi_plus.CreateFromHDC == NULL) || (is->gdi_plus.DeleteGraphics == NULL) || - (is->gdi_plus.SetSmoothingMode == NULL) || (is->gdi_plus.SetPenDashStyle == NULL) || - (is->gdi_plus.CreatePen1 == NULL) || (is->gdi_plus.DeletePen == NULL) || - (is->gdi_plus.DrawLineI == NULL)) { - MessageBoxA (NULL, "Failed to get GDI+ proc addresses!", "Error", MB_ICONERROR); - goto end_init; - } +// Writes a string to replace "Wake" or "Activate" on the right-click menu entry for the given unit. Some units might not have a replacement, in which +// case the function writes nothing and returns false. The string is written to out_str which must point to a buffer of at least str_capacity bytes. +bool +get_menu_verb_for_unit (Unit * unit, char * out_str, int str_capacity) +{ + struct state_desc { + enum c3x_label label; + bool is_doing_worker_job; + } state_descs[35] = { + {CL_IDLE , false}, // [No state] = 0x0 + {CL_FORTIFIED , false}, // Fortifying = 0x1 + {CL_MINING , true }, // Build_Mines = 0x2 + {CL_IRRIGATING , true }, // Irrigate = 0x3 + {CL_BUILDING_FORTRESS , true }, // Build_Fortress = 0x4 + {CL_BUILDING_ROAD , true }, // Build_Road = 0x5 + {CL_BUILDING_RAILROAD , true }, // Build_Railroad = 0x6 + {CL_PLANTING_FOREST , true }, // Plant_Forest = 0x7 + {CL_CLEARING_FOREST , true }, // Clear_Forest = 0x8 + {CL_CLEARING_WETLANDS , true }, // Clear_Wetlands = 0x9 + {CL_CLEARING_DAMAGE , true }, // Clear_Damage = 0xA + {CL_BUILDING_AIRFIELD , true }, // Build_Airfield = 0xB + {CL_BUILDING_RADAR_TOWER, true }, // Build_Radar_Tower = 0xC + {CL_BUILDING_OUTPOST , true }, // Build_Outpost = 0xD + {CL_BUILDING_BARRICADE , true }, // Build_Barricade = 0xE + {CL_INTERCEPTING , false}, // Intercept = 0xF + {CL_MOVING , false}, // Go_To = 0x10 + {CL_BUILDING_ROAD , true }, // Road_To_Tile = 0x11 + {CL_BUILDING_RAILROAD , true }, // Railroad_To_Tile = 0x12 + {CL_BUILDING_COLONY , true }, // Build_Colony = 0x13 + {CL_AUTOMATED , true }, // Auto_Irrigate = 0x14 + {CL_AUTOMATED , true }, // Build_Trade_Routes = 0x15 + {CL_AUTOMATED , true }, // Auto_Clear_Forest = 0x16 + {CL_AUTOMATED , true }, // Auto_Clear_Swamp = 0x17 + {CL_AUTOMATED , true }, // Auto_Clear_Pollution = 0x18 + {CL_AUTOMATED , true }, // Auto_Save_City_Tiles = 0x19 + {CL_EXPLORING , false}, // Explore = 0x1A + {CL_IN_STATE_27 , false}, // ? = 0x1B + {CL_IN_STATE_28 , false}, // Fleeing = 0x1C + {CL_IN_STATE_29 , false}, // ? = 0x1D + {CL_IN_STATE_30 , false}, // ? = 0x1E + {CL_BOMBARDING , false}, // Auto_Bombard = 0x1F + {CL_BOMBARDING , false}, // Auto_Air_Bombard = 0x20 + {CL_BOMBARDING , false}, // Auto_Precision_Strike = 0x21 + {CL_IDLE , false}, // Exhausted = 0x22 + }; + enum UnitStateType state = unit->Body.UnitState; + struct state_desc const * desc; + if ((state >= 0) && (state < ARRAY_LEN (state_descs)) && (desc = &state_descs[state]) && (desc->label >= 0) && (desc->label < COUNT_C3X_LABELS)) { + enum c3x_label label = desc->label; + Unit * container; + if (((label == CL_IDLE) || (label == CL_MOVING) || (label == CL_IN_STATE_29) || desc->is_doing_worker_job) && unit->Body.automated) + label = CL_AUTOMATED; + else if ((label == CL_FORTIFIED) && (NULL != (container = get_unit_ptr (unit->Body.Container_Unit))) && ! Unit_has_ability (container, __, UTA_Army)) + label = CL_TRANSPORTED; + else if ((label == CL_FORTIFIED) && (unit->Body.Status & (USF_SENTRY | USF_SENTRY_ENEMY_ONLY))) + label = CL_SENTRY; + else if ((label == CL_MINING) && is->current_config.enable_districts) { - int status = GdiplusStartup (&is->gdi_plus.token, &startup_input, NULL); - if (status != 0) { - char s[200]; - snprintf (s, sizeof s, "Failed to initialize GDI+! Startup status: %d", status); - MessageBoxA (NULL, s, "Error", MB_ICONERROR); - goto end_init; + // Check if this unit is actually building a district instead of a mine + Tile * tile = tile_at (unit->Body.X, unit->Body.Y); + struct district_instance * inst = get_district_instance (tile); + if ((tile != NULL) && (tile != p_null_tile) && inst != NULL) { + char const * district_name = is->district_configs[inst->district_id].name; + snprintf (out_str, str_capacity, "%s %s", is->c3x_labels[CL_BUILDING], district_name); + out_str[str_capacity - 1] = '\0'; + return true; + } } - is->gdi_plus.init_state = IS_OK; - end_init: - ; - } - - return is->gdi_plus.init_state == IS_OK; -} - -int __fastcall -patch_OpenGLRenderer_initialize (OpenGLRenderer * this, int edx, PCX_Image * texture) -{ - if ((is->current_config.draw_lines_using_gdi_plus == LDO_NEVER) || - ((is->current_config.draw_lines_using_gdi_plus == LDO_WINE) && ! is->running_on_wine)) - return OpenGLRenderer_initialize (this, __, texture); - - // Initialize GDI+ instead - else { - if (! set_up_gdi_plus ()) - return 2; - if (is->gdi_plus.gp_graphics != NULL) { - is->gdi_plus.DeleteGraphics (is->gdi_plus.gp_graphics); - is->gdi_plus.gp_graphics = NULL; - } - int status = is->gdi_plus.CreateFromHDC (texture->JGL.Image->DC, &is->gdi_plus.gp_graphics); - if (status == 0) { - is->gdi_plus.SetSmoothingMode (is->gdi_plus.gp_graphics, 4); // 4 = SmoothingModeAntiAlias from GdiPlusEnums.h - return 0; - } else - return 2; - } + strncpy (out_str, is->c3x_labels[label], str_capacity); + out_str[str_capacity - 1] = '\0'; + return true; + } else + return false; } void __fastcall -patch_OpenGLRenderer_set_color (OpenGLRenderer * this, int edx, unsigned int rgb555) +patch_MenuUnitItem_write_text_to_temp_str (MenuUnitItem * this) { - // Convert rgb555 to rgb888 - unsigned int rgb888 = 0; { - unsigned int mask = 31; - int shift = 3; - for (int n = 0; n < 3; n++) { - rgb888 |= (rgb555 & mask) << shift; - mask <<= 5; - shift += 3; + MenuUnitItem_write_text_to_temp_str (this); + + Unit * unit = this->unit; + char repl_verb[32]; + if (is->current_config.describe_states_of_units_on_menu && + (unit->Body.CivID == p_main_screen_form->Player_CivID) && + (Unit_get_containing_army (unit) == NULL) && + get_menu_verb_for_unit (unit, repl_verb, sizeof repl_verb)) { + char * verb = (unit->Body.UnitState == UnitState_Fortifying) ? (*p_labels)[LBL_WAKE] : (*p_labels)[LBL_ACTIVATE]; + char * verb_str_start = strstr (temp_str, verb); + if (verb_str_start != NULL) { + char s[500]; + char * verb_str_end = verb_str_start + strlen (verb); + snprintf (s, sizeof s, "%.*s%s%s", verb_str_start - temp_str, temp_str, repl_verb, verb_str_end); + s[(sizeof s) - 1] = '\0'; + strncpy (temp_str, s, sizeof s); } } - - is->ogl_color = (is->ogl_color & 0xFF000000) | rgb888; - OpenGLRenderer_set_color (this, __, rgb555); } void __fastcall -patch_OpenGLRenderer_set_opacity (OpenGLRenderer * this, int edx, unsigned int alpha) +patch_Tile_m74_Set_Square_Type_for_hill_gen (Tile * this, int edx, enum SquareTypes sq, int tile_x, int tile_y) { - is->ogl_color = (is->ogl_color & 0x00FFFFFF) | (alpha << 24); - OpenGLRenderer_set_opacity (this, __, alpha); + if ((sq == SQ_Volcano) && is->current_config.do_not_generate_volcanos) + sq = SQ_Mountains; + this->vtable->m74_Set_Square_Type (this, __, sq, tile_x, tile_y); } void __fastcall -patch_OpenGLRenderer_set_line_width (OpenGLRenderer * this, int edx, int width) +patch_Map_place_scenario_things (Map * this) { - is->ogl_line_width = width; - OpenGLRenderer_set_line_width (this, __, width); -} + is->is_placing_scenario_things = true; -void __fastcall -patch_OpenGLRenderer_enable_line_dashing (OpenGLRenderer * this) -{ - is->ogl_line_stipple_enabled = true; - OpenGLRenderer_enable_line_dashing (this); -} + Map_place_scenario_things (this); -void __fastcall -patch_OpenGLRenderer_disable_line_dashing (OpenGLRenderer * this) -{ - is->ogl_line_stipple_enabled = false; - OpenGLRenderer_disable_line_dashing (this); -} + // If there are any mills in the config then recompute yields & happiness in all cities. This must be done because we avoid doing this as + // mills are added to cities while placing scenario things. + if (is->current_config.count_mills > 0) + for (int n = 0; n <= p_cities->LastIndex; n++) { + City * city = get_city_ptr (n); + if (city != NULL) + patch_City_recompute_yields_and_happiness (city); + } -void __fastcall -patch_OpenGLRenderer_draw_line (OpenGLRenderer * this, int edx, int x1, int y1, int x2, int y2) -{ - if ((is->current_config.draw_lines_using_gdi_plus == LDO_NEVER) || - ((is->current_config.draw_lines_using_gdi_plus == LDO_WINE) && ! is->running_on_wine)) - OpenGLRenderer_draw_line (this, __, x1, y1, x2, y2); + if (is->current_config.enable_districts || + is->current_config.enable_natural_wonders || + is->current_config.enable_named_tiles) + load_scenario_districts_from_file (); - else if ((is->gdi_plus.init_state == IS_OK) && (is->gdi_plus.gp_graphics != NULL)) { - void * gp_pen; - int unit_world = 0; // = UnitWorld from gdiplusenums.h - int status = is->gdi_plus.CreatePen1 (is->ogl_color, (float)is->ogl_line_width, unit_world, &gp_pen); - if (status == 0) { - if (is->ogl_line_stipple_enabled) - is->gdi_plus.SetPenDashStyle (gp_pen, 1); // 1 = DashStyleDash from GdiPlusEnums.h - is->gdi_plus.DrawLineI (is->gdi_plus.gp_graphics, gp_pen, x1, y1, x2, y2); - is->gdi_plus.DeletePen (gp_pen); + if (is->current_config.enable_natural_wonders && + is->current_config.add_natural_wonders_to_scenarios_if_none) { + bool any_natural_wonders = false; + FOR_TABLE_ENTRIES (tei, &is->district_tile_map) { + struct district_instance * inst = (struct district_instance *)tei.value; + if ((inst != NULL) && + (inst->district_id == NATURAL_WONDER_DISTRICT_ID) && + (inst->natural_wonder_info.natural_wonder_id >= 0)) { + any_natural_wonders = true; + break; + } } + if (! any_natural_wonders) + place_natural_wonders_on_map (); } + is->is_placing_scenario_things = false; } -int __fastcall -patch_Tile_check_water_for_retreat_on_defense (Tile * this) +void +on_open_advisor (AdvisorKind kind) { - Unit * defender = p_bic_data->fighter.defender; - - // Under the standard game rules, defensively retreating onto a water tile is not allowed. Set retreat_blocked to true if "this" is a water - // tile and we're not configured to allow retreating onto water tiles. - bool retreat_blocked; { - if (this->vtable->m35_Check_Is_Water (this)) { - if ( is->current_config.allow_defensive_retreat_on_water - && defender != NULL - && p_bic_data->UnitTypes[defender->Body.UnitTypeID].Unit_Class == UTC_Sea - && ( is->current_config.limit_defensive_retreat_on_water_to_types.len == 0 - || (bool)itable_look_up_or (&is->current_config.limit_defensive_retreat_on_water_to_types, defender->Body.UnitTypeID, 0))) - retreat_blocked = false; - else - retreat_blocked = true; - } else - retreat_blocked = false; - } - - // Check stack limit - if ((! retreat_blocked) && - (defender != NULL) && - ! is_below_stack_limit (this, defender->Body.CivID, defender->Body.UnitTypeID)) - retreat_blocked = true; - - // The return from this call is only used to filter the given tile as a possible retreat destination based on whether it's water or not - return (int)retreat_blocked; + recompute_resources_if_necessary (); } -int __fastcall -patch_City_count_airports_for_airdrop (City * this, int edx, enum ImprovementTypeFlags airport_flag) -{ - if (is->current_config.allow_airdrop_without_airport) - return 1; - else - return City_count_improvements_with_flag (this, __, airport_flag); -} +bool __fastcall patch_Advisor_Base_Form_domestic_m95 (Advisor_Base_Form * this) { on_open_advisor (AK_DOMESTIC); return Advisor_Base_Form_domestic_m95 (this); } +bool __fastcall patch_Advisor_Base_Form_trade_m95 (Advisor_Base_Form * this) { on_open_advisor (AK_TRADE) ; return Advisor_Base_Form_trade_m95 (this); } +bool __fastcall patch_Advisor_Base_Form_military_m95 (Advisor_Base_Form * this) { on_open_advisor (AK_MILITARY); return Advisor_Base_Form_military_m95 (this); } +bool __fastcall patch_Advisor_Base_Form_foreign_m95 (Advisor_Base_Form * this) { on_open_advisor (AK_FOREIGN) ; return Advisor_Base_Form_foreign_m95 (this); } +bool __fastcall patch_Advisor_Base_Form_cultural_m95 (Advisor_Base_Form * this) { on_open_advisor (AK_CULTURAL); return Advisor_Base_Form_cultural_m95 (this); } +bool __fastcall patch_Advisor_Base_Form_science_m95 (Advisor_Base_Form * this) { on_open_advisor (AK_SCIENCE) ; return Advisor_Base_Form_science_m95 (this); } -int __fastcall -patch_Leader_get_cont_city_count_for_worker_req (Leader * this, int edx, int cont_id) +void __fastcall +patch_Main_Screen_Form_open_quick_build_chooser (Main_Screen_Form * this, int edx, City * city, int mouse_x, int mouse_y) { - int tr = Leader_get_city_count_on_continent (this, __, cont_id); - if (is->current_config.ai_worker_requirement_percent != 100) - tr = (tr * is->current_config.ai_worker_requirement_percent + 50) / 100; - return tr; + recompute_resources_if_necessary (); + Main_Screen_Form_open_quick_build_chooser (this, __, city, mouse_x, mouse_y); } int __fastcall -patch_Leader_get_city_count_for_worker_prod_cap (Leader * this) -{ - int tr = this->Cities_Count; - // Don't scale down the cap since it's pretty low to begin with - if (is->current_config.ai_worker_requirement_percent > 100) - tr = (tr * is->current_config.ai_worker_requirement_percent + 50) / 100; - return tr; -} - -// If "only_improv_id" is >= 0, will only return yields from mills attached to that improv, otherwise returns all yields from all improvs -void -gather_mill_yields (City * city, int only_improv_id, int * out_food, int * out_shields, int * out_commerce) +patch_Context_Menu_get_selected_item_on_unit_rcm (Context_Menu * this) { - int food = 0, shields = 0, commerce = 0; - for (int n = 0; n < is->current_config.count_mills; n++) { - struct mill * mill = &is->current_config.mills[n]; - if ((mill->flags & MF_YIELDS) && - ((only_improv_id < 0) || (mill->improv_id == only_improv_id)) && - can_generate_resource (city->Body.CivID, mill) && - has_active_building (city, mill->improv_id) && - has_resources_required_by_building (city, mill->improv_id)) { - Resource_Type * res = &p_bic_data->ResourceTypes[mill->resource_id]; - food += res->Food; - shields += res->Shield; - commerce += res->Commerce; + // In the base game, this method returns -1 for any disabled item which prevents the player from clicking those. We want players to be able to + // click unit items which have been disabled by the mod so they can interrupt the queued actions of units that have no moves left. + int index = this->Selected_Item; + if (index >= 0) { + if (is->current_config.enable_named_tiles && is->named_tile_menu_active) { + Context_Menu_Item * item = &this->Items[index]; + if (item->Menu_Item_ID == NAMED_TILE_MENU_ID) { + handle_named_tile_menu_selection (); + return -1; + } } + bool is_enabled = (this->Items[index].Status & 2) == 0; + bool is_unit_item = (this->Items[index].Menu_Item_ID - (0x13 + p_bic_data->UnitTypeCount)) >= 0; + return (is_enabled || is_unit_item) ? index : -1; } - *out_food = food; - *out_shields = shields; - *out_commerce = commerce; + return -1; } int __fastcall -patch_City_calc_tile_yield_while_gathering (City * this, int edx, YieldKind kind, int tile_x, int tile_y) +patch_Tile_check_water_to_block_pollution (Tile * this) { - int tr = City_calc_tile_yield_at (this, __, kind, tile_x, tile_y); + if (this->vtable->m35_Check_Is_Water (this)) + return 1; + else if (is->current_config.do_not_pollute_impassable_tiles) { + enum SquareTypes terrain_type = this->vtable->m50_Get_Square_BaseType (this); + return p_bic_data->TileTypes[terrain_type].Flags.Impassable; + } else + return 0; +} - // Include yields from generated resources - if ((this->Body.X == tile_x) && (this->Body.Y == tile_y)) { - int mill_food, mill_shields, mill_commerce; - gather_mill_yields (this, -1, &mill_food, &mill_shields, &mill_commerce); - if (kind == YK_FOOD) tr += mill_food; - else if (kind == YK_SHIELDS) tr += mill_shields; - else if (kind == YK_COMMERCE) tr += mill_commerce; +void __fastcall +patch_Tile_set_flag_for_eruption_damage (Tile * this, int edx, int param_1, int param_2, int x, int y) +{ + if (is->current_config.enable_districts) { + struct district_instance * inst = get_district_instance (this); + if (inst != NULL && inst->district_id >= 0 && inst->district_id < is->district_count) { + // District found - handle removal + int district_id = inst->district_id; + + // Notify human player if this tile is in their territory + int territory_owner = this->vtable->m38_Get_Territory_OwnerID (this); + if (territory_owner == p_main_screen_form->Player_CivID) { + char msg[160]; + char const * district_name = is->district_configs[district_id].name; + snprintf (msg, sizeof msg, "%s %s", district_name, is->c3x_labels[CL_DISTRICT_DESTROYED_BY_VOLCANO]); + msg[(sizeof msg) - 1] = '\0'; + show_map_specific_text (x, y, msg, true); + } - if (is->current_config.enable_districts || is->current_config.enable_natural_wonders) { - int bonus_food = 0, bonus_shields = 0, bonus_gold = 0; - calculate_city_center_district_bonus (this, &bonus_food, &bonus_shields, &bonus_gold); - if (kind == YK_FOOD) tr += bonus_food; - else if (kind == YK_SHIELDS) tr += bonus_shields; - else if (kind == YK_COMMERCE) tr += bonus_gold; + // Remove the district + handle_district_removed (this, district_id, x, y, false); + + // Clear the mine flags + this->vtable->m62_Set_Tile_BuildingID (this, __, -1); + this->vtable->m51_Unset_Tile_Flags (this, __, 0, TILE_FLAG_MINE, x, y); } } - return tr; + // Apply the normal eruption damage (lava flag) if allowed + if (! (is->current_config.do_not_pollute_impassable_tiles && + p_bic_data->TileTypes[this->vtable->m50_Get_Square_BaseType (this)].Flags.Impassable)) + this->vtable->m56_Set_Tile_Flags (this, __, param_1, param_2, x, y); } bool __fastcall -patch_Leader_record_export (Leader * this, int edx, int importer_civ_id, int resource_id) +patch_City_confirm_production_switch (City * this, int edx, int order_type, int order_id) { - bool exported = Leader_record_export (this, __, importer_civ_id, resource_id); - if (exported && - (is->mill_input_resource_bits[resource_id>>3] & (1 << (resource_id & 7)))) // if the traded resource is an input to any mill - is->must_recompute_resources_for_mill_inputs = true; - return exported; + bool tr = City_confirm_production_switch (this, __, order_type, order_id); + if (tr && + (order_type == COT_Improvement) && (order_id >= 0) && (order_id < p_bic_data->ImprovementsCount) && + (this->Body.CivID == p_main_screen_form->Player_CivID) && + is->current_config.warn_when_chosen_building_would_replace_another) { + Improvement * improv = &p_bic_data->Improvements[order_id]; + if (improv->ImprovementFlags & ITF_Replaces_Other_Buildings) { + Improvement * replaced = NULL; + for (int n = 0; n < p_bic_data->ImprovementsCount; n++) { + Improvement * other = &p_bic_data->Improvements[n]; + if ((other->ImprovementFlags & ITF_Replaces_Other_Buildings) && + patch_City_has_improvement (this, __, n, false)) { + replaced = other; + break; + } + } + if (replaced != NULL) { + PopupForm * popup = get_popup_form (); + set_popup_str_param (0, improv->Name.S, -1, -1); + set_popup_str_param (1, replaced->Name.S, -1, -1); + popup->vtable->set_text_key_and_flags (popup, __, is->mod_script_path, "C3X_WARN_ABOUT_BUILDING_REPLACEMENT", -1, 0, 0, 0); + if (patch_show_popup (popup, __, 0, 0) == 1) + return false; + } + } + } + return tr; } -bool __fastcall -patch_Leader_erase_export (Leader * this, int edx, int importer_civ_id, int resource_id) +byte const c3x_save_segment_bookend[4] = {0x22, 'C', '3', 'X'}; + +// Writes a string to the buffer and pads the end so it's four-byte aligned (assuming the existing contents is already so aligned). +void +serialize_aligned_text (char const * text, struct buffer * b) { - bool erased = Leader_erase_export (this, __, importer_civ_id, resource_id); - if (erased && - (is->mill_input_resource_bits[resource_id>>3] & (1 << (resource_id & 7)))) // if the traded resource is an input to any mill - is->must_recompute_resources_for_mill_inputs = true; - return erased; + int len = strlen (text); + if (len > 0) { + int padded_len = (len + 4) & ~3; // +1 for null terminator then +3 & ~3 for alignment + byte * p = buffer_allocate (b, padded_len); + strcpy (p, text); + for (int n = 0; n < padded_len - len; n++) + p[len + n] = (byte)0; + } } -int __fastcall -patch_Sprite_draw_improv_img_on_city_form (Sprite * this, int edx, PCX_Image * canvas, int pixel_x, int pixel_y, PCX_Color_Table * color_table) +void * __fastcall +patch_MappedFile_open_to_load_game (MappedFile * this, int edx, char * file_name, int sequential_access) { - int tr = Sprite_draw (this, __, canvas, pixel_x, pixel_y, color_table); + void * tr = MappedFile_open (this, __, file_name, sequential_access); + if (tr != NULL) + is->accessing_save_file = this; + return tr; +} - int generated_resources[16]; - int generated_resource_count = 0; - for (int n = 0; (n < is->current_config.count_mills) && (generated_resource_count < ARRAY_LEN (generated_resources)); n++) { - struct mill * mill = &is->current_config.mills[n]; - if ((mill->improv_id == is->drawing_icons_for_improv_id) && - ((mill->flags & MF_SHOW_BONUS) || (p_bic_data->ResourceTypes[mill->resource_id].Class != RC_Bonus)) && - (! ((mill->flags & MF_HIDE_NON_BONUS) && (p_bic_data->ResourceTypes[mill->resource_id].Class != RC_Bonus))) && - can_generate_resource (p_city_form->CurrentCity->Body.CivID, mill) && - has_active_building (p_city_form->CurrentCity, mill->improv_id) && - has_resources_required_by_building (p_city_form->CurrentCity, mill->improv_id)) - generated_resources[generated_resource_count++] = mill->resource_id; +void * __fastcall +patch_MappedFile_create_file_to_save_game (MappedFile * this, int edx, LPCSTR file_path, unsigned file_size, int is_shared) +{ + // Determine if we're currently applying settler perfume to any AI player + bool any_current_settler_perfume = false; + if (is->current_config.ai_settler_perfume_on_founding != 0) { + int duration = is->current_config.ai_settler_perfume_on_founding_duration; + for (int n = 0; n < 32; n++) { + int last_founding_turn = is->turn_no_of_last_founding_for_settler_perfume[n]; + if ((last_founding_turn != -1) && ((*p_current_turn_no - last_founding_turn) < duration)) + any_current_settler_perfume = true; + } } - if ((generated_resource_count > 0) && (is->resources_sheet != NULL) && (is->resources_sheet->JGL.Image != NULL) && (TransparentBlt != NULL)) { - JGL_Image * jgl_canvas = canvas->JGL.Image, - * jgl_sheet = is->resources_sheet->JGL.Image; - - HDC canvas_dc = jgl_canvas->vtable->acquire_dc (jgl_canvas); - if (canvas_dc != NULL) { - HDC sheet_dc = jgl_sheet->vtable->acquire_dc (jgl_sheet); - if (sheet_dc != NULL) { - - for (int n = 0; n < generated_resource_count; n++) { - int icon_id = p_bic_data->ResourceTypes[generated_resources[n]].IconID, - sheet_row = icon_id / 6, - sheet_col = icon_id % 6; + // Assemble mod save data + struct buffer mod_data = {0}; { + if (is->extra_defensive_bombards.len > 0) { + serialize_aligned_text ("extra_defensive_bombards", &mod_data); + itable_serialize (&is->extra_defensive_bombards, &mod_data); + } + if (is->airdrops_this_turn.len > 0) { + serialize_aligned_text ("airdrops_this_turn", &mod_data); + itable_serialize (&is->airdrops_this_turn, &mod_data); + } + if (is->unit_transport_ties.len > 0) { + serialize_aligned_text ("unit_transport_ties", &mod_data); + itable_serialize (&is->unit_transport_ties, &mod_data); + } + if (is->current_config.unit_cycle_search_criteria != UCSC_STANDARD && is->waiting_units.len > 0) { + serialize_aligned_text ("waiting_units", &mod_data); + itable_serialize (&is->waiting_units, &mod_data); + } + if ((p_bic_data->ImprovementsCount > 256) && (p_cities->Cities != NULL) && (is->extra_city_improvs.len > 0)) { + serialize_aligned_text ("extra_city_improvs", &mod_data); + int extra_improv_count = p_bic_data->ImprovementsCount - 256; + *(int *)buffer_allocate (&mod_data, sizeof(int)) = extra_improv_count; - int dy = (n * 160 / not_below (1, generated_resource_count - 1) + 5) / 10; - TransparentBlt (canvas_dc, // dest DC - pixel_x + 15, pixel_y + dy, 24, 24, // dest x, y, width, height - sheet_dc, // src DC - 9 + 50*sheet_col, 9 + 50*sheet_row, 33, 33, // src x, y, width, height - 0xFF00FF); // transparent color (RGB) + int count_entries = 0; { + for (int n = 0; n <= p_cities->LastIndex; n++) { + City_Body * body = p_cities->Cities[n].City; + if ((body != NULL) && ((int)body != offsetof (City, Body))) { + int unused; + if (itable_look_up (&is->extra_city_improvs, (int)&body->Improvements_1, &unused) || + itable_look_up (&is->extra_city_improvs, (int)&body->Improvements_2, &unused)) + count_entries++; + } } + } + *(int *)buffer_allocate (&mod_data, sizeof(int)) = count_entries; - jgl_sheet->vtable->release_dc (jgl_sheet, __, 1); + int ints_per_list = (extra_improv_count + 31) / 32; + int bytes_per_list = (extra_improv_count + 7) / 8; + for (int n = 0; n <= p_cities->LastIndex; n++) { + City_Body * body = p_cities->Cities[n].City; + if ((body != NULL) && ((int)body != offsetof (City, Body))) { + byte * extra_bit_lists[2]; + extra_bit_lists[0] = (byte *)itable_look_up_or (&is->extra_city_improvs, (int)&body->Improvements_1, 0); + extra_bit_lists[1] = (byte *)itable_look_up_or (&is->extra_city_improvs, (int)&body->Improvements_2, 0); + if ((extra_bit_lists[0] != NULL) || (extra_bit_lists[1] != NULL)) { + *(int *)buffer_allocate (&mod_data, sizeof(int)) = body->ID; + for (int k = 0; k < 2; k++) { + int list_size = sizeof(int) * ints_per_list; + int * list = (int *)buffer_allocate (&mod_data, list_size); + memset (list, 0, list_size); + if (extra_bit_lists[k] != NULL) + memcpy (list, extra_bit_lists[k], bytes_per_list); + } + } + } } - jgl_canvas->vtable->release_dc (jgl_canvas, __, 1); + } + if (any_current_settler_perfume) { + serialize_aligned_text ("turn_no_of_last_founding_for_settler_perfume", &mod_data); + void * area = buffer_allocate (&mod_data, sizeof is->turn_no_of_last_founding_for_settler_perfume); + memcpy (area, is->turn_no_of_last_founding_for_settler_perfume, sizeof is->turn_no_of_last_founding_for_settler_perfume); + } + if (is->current_config.day_night_cycle_mode != DNCM_OFF) { + serialize_aligned_text ("current_day_night_cycle", &mod_data); + int_to_bytes (buffer_allocate (&mod_data, sizeof is->current_day_night_cycle), is->current_day_night_cycle); + } + if (is->current_config.enable_districts && (is->district_count > 0)) { + serialize_aligned_text ("district_config_names", &mod_data); + int * entry_count = (int *)buffer_allocate (&mod_data, sizeof(int)); + *entry_count = is->district_count; + for (int district_id = 0; district_id < is->district_count; district_id++) { + *(int *)buffer_allocate (&mod_data, sizeof(int)) = district_id; + char const * name = is->district_configs[district_id].name; + if (name == NULL) + name = ""; + serialize_aligned_text (name, &mod_data); } } - return tr; -} - -void __cdecl -patch_draw_improv_icons_on_city_screen (Base_List_Control * control, int improv_id, int item_index, int offset_x, int offset_y) -{ - is->drawing_icons_for_improv_id = improv_id; - draw_improv_icons_on_city_screen (control, improv_id, item_index, offset_x, offset_y); - is->drawing_icons_for_improv_id = -1; -} - -int __fastcall -patch_City_get_tourism_amount_to_draw (City * this, int edx, int improv_id) -{ - is->tourism_icon_counter = 0; - - int mill_food, mill_shields, mill_commerce; - gather_mill_yields (this, improv_id, &mill_food, &mill_shields, &mill_commerce); - int combined_commerce = mill_commerce + City_get_tourism_amount (this, __, improv_id); - - is->convert_displayed_tourism_to_food = mill_food; - is->convert_displayed_tourism_to_shields = mill_shields; - is->combined_tourism_and_mill_commerce = combined_commerce; - return int_abs (mill_food) + int_abs (mill_shields) + int_abs (combined_commerce); -} -int __fastcall -patch_Sprite_draw_tourism_gold (Sprite * this, int edx, PCX_Image * canvas, int pixel_x, int pixel_y, PCX_Color_Table * color_table) -{ - // Replace the yield sprite we're drawing with food or a shield if needed. - Sprite * sprite = NULL; { - if (is->tourism_icon_counter < int_abs (is->convert_displayed_tourism_to_food)) { - if (is->convert_displayed_tourism_to_food >= 0) - sprite = &p_city_form->City_Icons_Images.Icon_15_Food; - else { - init_red_food_icon (); - if (is->red_food_icon_state == IS_OK) - sprite = &is->red_food_icon; + if (is->current_config.enable_districts) { + int entry_count = 0; + for (int civ_id = 0; civ_id < 32; civ_id++) { + FOR_TABLE_ENTRIES (tei, &is->city_pending_district_requests[civ_id]) { + struct pending_district_request * req = (struct pending_district_request *)tei.value; + if ((req != NULL) && (req->city_id >= 0)) + entry_count++; + } + } + if (entry_count > 0) { + serialize_aligned_text ("district_pending_requests", &mod_data); + int * chunk = (int *)buffer_allocate (&mod_data, sizeof(int) * (1 + 5 * entry_count)); + int * out = chunk + 1; + chunk[0] = entry_count; + for (int civ_id = 0; civ_id < 32; civ_id++) { + FOR_TABLE_ENTRIES (tei, &is->city_pending_district_requests[civ_id]) { + struct pending_district_request * req = (struct pending_district_request *)tei.value; + if ((req == NULL) || (req->city_id < 0)) + continue; + out[0] = req->city_id; + out[1] = req->district_id; + out[2] = req->assigned_worker_id; + out[3] = req->target_x; + out[4] = req->target_y; + out += 5; + } } - } else if (is->tourism_icon_counter < int_abs (is->convert_displayed_tourism_to_food) + int_abs (is->convert_displayed_tourism_to_shields)) - sprite = (is->convert_displayed_tourism_to_shields >= 0) ? &p_city_form->City_Icons_Images.Icon_13_Shield : &p_city_form->City_Icons_Images.Icon_05_Shield_Outcome; - else if (is->combined_tourism_and_mill_commerce < 0) - sprite = &p_city_form->City_Icons_Images.Icon_17_Gold_Outcome; - else - sprite = this; + } } - int tr = 0; // return value is not used by caller - if (sprite != NULL) - tr = Sprite_draw (sprite, __, canvas, pixel_x, pixel_y, color_table); - is->tourism_icon_counter++; - return tr; -} - -Unit * __fastcall -patch_Leader_spawn_unit (Leader * this, int edx, int type_id, int tile_x, int tile_y, int barb_tribe_id, int id, bool param_6, LeaderKind leader_kind, int race_id) -{ - int spawn_x = tile_x, - spawn_y = tile_y; - if (is->current_config.enable_districts && - is->current_config.air_units_use_aerodrome_districts_not_cities) { - int aerodrome_id = AERODROME_DISTRICT_ID; - if ((p_bic_data->UnitTypes[type_id].Unit_Class == UTC_Air) && (aerodrome_id >= 0)) { - City * spawn_city = city_at (tile_x, tile_y); - if ((spawn_city != NULL) && (spawn_city->Body.CivID == this->ID)) { - int district_x, district_y; - Tile * district_tile = get_completed_district_tile_for_city (spawn_city, aerodrome_id, &district_x, &district_y); - if ((district_tile != NULL) && (district_tile != p_null_tile) && - (district_tile->Territory_OwnerID == this->ID) && - is_below_stack_limit (district_tile, this->ID, type_id)) { - spawn_x = district_x; - spawn_y = district_y; - } + (is->city_pending_building_orders.len > 0)) { + int entry_count = 0; + FOR_TABLE_ENTRIES (tei, &is->city_pending_building_orders) { + City * city = (City *)tei.key; + int improv_id = tei.value; + if ((city != NULL) && (improv_id >= 0)) + entry_count++; + } + if (entry_count > 0) { + serialize_aligned_text ("building_pending_orders", &mod_data); + int * chunk = (int *)buffer_allocate (&mod_data, sizeof(int) * (1 + 2 * entry_count)); + int * out = chunk + 1; + chunk[0] = entry_count; + FOR_TABLE_ENTRIES (tei, &is->city_pending_building_orders) { + City * city = (City *)tei.key; + int improv_id = tei.value; + if ((city == NULL) || (improv_id < 0)) + continue; + out[0] = city->Body.ID; + out[1] = improv_id; + out += 2; } } } - Unit * tr = Leader_spawn_unit (this, __, type_id, spawn_x, spawn_y, barb_tribe_id, id, param_6, leader_kind, race_id); - if (tr != NULL) - change_unit_type_count (this, type_id, 1); - return tr; -} - -Unit * __fastcall -patch_Leader_spawn_captured_unit (Leader * this, int edx, int type_id, int tile_x, int tile_y, int barb_tribe_id, int id, bool param_6, LeaderKind leader_kind, int race_id) -{ - Unit * tr = patch_Leader_spawn_unit (this, __, type_id, tile_x, tile_y, barb_tribe_id, id, param_6, leader_kind, race_id); - if ((tr != NULL) && is->moving_unit_to_adjacent_tile) - is->temporarily_disallow_lethal_zoc = true; - return tr; -} - -void __fastcall -patch_Leader_enter_new_era (Leader * this, int edx, bool param_1, bool no_online_sync) -{ - Leader_enter_new_era (this, __, param_1, no_online_sync); - apply_era_specific_names (this); -} - -char * __fastcall -patch_Leader_get_player_title_for_intro_popup (Leader * this) -{ - // Normally the era-specific names are applied later, after game loading is finished, but in this case we apply the player's names ahead of - // time so they appear on the intro popup. "this" will always refer to the human player in this call. - apply_era_specific_names (this); - return Leader_get_title (this); -} - -void __fastcall -patch_City_spawn_unit_if_done (City * this) -{ - bool skip_spawn = false; - - // Apply unit limit. If this city's owner has reached the limit for the unit type it's building then force it to build something else. - int available; - if ((this->Body.Order_Type == COT_Unit) && - get_available_unit_count (&leaders[this->Body.CivID], this->Body.Order_ID, &available) && - (available <= 0)) { - int limited_unit_type_id = this->Body.Order_ID; - - if (*p_human_player_bits & 1<Body.CivID) { - // Find another type ID to build instead of the limited one - int replacement_type_id = -1; { - int limited_unit_strat = p_bic_data->UnitTypes[limited_unit_type_id].AI_Strategy, - shields_in_box = this->Body.StoredProduction; - UnitType * replacement_type; - for (int can_type_id = 0; can_type_id < p_bic_data->UnitTypeCount; can_type_id++) - if (patch_City_can_build_unit (this, __, can_type_id, 1, 0, 0)) { - UnitType * candidate_type = &p_bic_data->UnitTypes[can_type_id]; + if (is->current_config.enable_districts && (is->district_tile_map.len > 0)) { + serialize_aligned_text ("district_tile_map", &mod_data); + int entry_capacity = is->district_tile_map.len; + int * chunk = (int *)buffer_allocate (&mod_data, sizeof(int) * (1 + 9 * entry_capacity)); + int * out = chunk + 1; + int written = 0; + FOR_TABLE_ENTRIES (tei, &is->district_tile_map) { + Tile * tile = (Tile *)tei.key; + struct district_instance * inst = (struct district_instance *)tei.value; + if (inst == NULL) + continue; + int x, y; + if (! district_instance_get_coords (inst, tile, &x, &y)) + continue; + int wonder_city_id = inst->wonder_info.city_id; + if (wonder_city_id >= 0) { + City * info_city = get_city_ptr (wonder_city_id); + inst->wonder_info.city = info_city; + if (info_city == NULL) + wonder_city_id = -1; + } else + inst->wonder_info.city = NULL; + out[0] = x; + out[1] = y; + out[2] = inst->district_id; + out[3] = (int)inst->state; + out[4] = inst->built_by_civ_id; + out[5] = inst->completed_turn; + out[6] = (int)inst->wonder_info.state; + out[7] = wonder_city_id; + out[8] = inst->wonder_info.wonder_index; + out += 9; + written++; + } + chunk[0] = written; + int unused_entries = entry_capacity - written; + if (unused_entries > 0) { + int trimmed_bytes = unused_entries * 9 * (int)sizeof(int); + mod_data.length -= trimmed_bytes; + } + } - // If we haven't found a replacement yet, use this one - if (replacement_type_id < 0) { - replacement_type_id = can_type_id; - replacement_type = candidate_type; + if (is->current_config.enable_natural_wonders && (is->district_tile_map.len > 0)) { + int entry_capacity = 0; + FOR_TABLE_ENTRIES (tei, &is->district_tile_map) { + struct district_instance * inst = (struct district_instance *)tei.value; + if ((inst == NULL) || (inst->district_id != NATURAL_WONDER_DISTRICT_ID)) + continue; + if (inst->natural_wonder_info.natural_wonder_id < 0) + continue; + entry_capacity++; + } + if (entry_capacity > 0) { + serialize_aligned_text ("natural_wonder_districts", &mod_data); + int * chunk = (int *)buffer_allocate (&mod_data, sizeof(int) * (1 + 3 * entry_capacity)); + int * out = chunk + 1; + int written = 0; + FOR_TABLE_ENTRIES (tei, &is->district_tile_map) { + Tile * tile = (Tile *)tei.key; + struct district_instance * inst = (struct district_instance *)tei.value; + if ((inst == NULL) || + (inst->district_id != NATURAL_WONDER_DISTRICT_ID) || + (inst->natural_wonder_info.natural_wonder_id < 0)) + continue; + int x, y; + if (! district_instance_get_coords (inst, tile, &x, &y)) + continue; + out[0] = x; + out[1] = y; + out[2] = inst->natural_wonder_info.natural_wonder_id; + out += 3; + written++; + } + chunk[0] = written; + int unused_entries = entry_capacity - written; + if (unused_entries > 0) { + int trimmed_bytes = unused_entries * 3 * (int)sizeof(int); + mod_data.length -= trimmed_bytes; + } + } + } - // Keep the prev replacement if it doesn't waste shields but this candidate would - } else if ((replacement_type->Cost >= shields_in_box) && (candidate_type->Cost < shields_in_box)) - continue; + if (is->current_config.enable_districts && + is->current_config.enable_distribution_hub_districts && + (is->distribution_hub_records.len > 0)) { + serialize_aligned_text ("distribution_hub_records", &mod_data); + int entry_capacity = is->distribution_hub_records.len; + int * chunk = (int *)buffer_allocate (&mod_data, sizeof(int) * (1 + 3 * entry_capacity)); + int * out = chunk + 1; + int written = 0; + FOR_TABLE_ENTRIES (tei, &is->distribution_hub_records) { + struct distribution_hub_record * rec = (struct distribution_hub_record *)tei.value; + if (rec == NULL) + continue; + out[0] = rec->tile_x; + out[1] = rec->tile_y; + out[2] = rec->civ_id; + out += 3; + written++; + } + chunk[0] = written; + int unused_entries = entry_capacity - written; + if (unused_entries > 0) { + int trimmed_bytes = unused_entries * 3 * (int)sizeof(int); + mod_data.length -= trimmed_bytes; + } + } - // Keep the prev if it shares an AI strategy with the limited unit but this candidate doesn't - else if (((replacement_type->AI_Strategy & limited_unit_strat) != 0) && - ((candidate_type ->AI_Strategy & limited_unit_strat) == 0)) - continue; + if (is->current_config.enable_districts && + is->current_config.enable_aerodrome_districts && + is->current_config.air_units_use_aerodrome_districts_not_cities && + (is->aerodrome_airlift_usage.len > 0)) { + serialize_aligned_text ("aerodrome_airlift_usage", &mod_data); + int entry_capacity = is->aerodrome_airlift_usage.len; + int * chunk = (int *)buffer_allocate (&mod_data, sizeof(int) * (1 + 3 * entry_capacity)); + int * out = chunk + 1; + int written = 0; + FOR_TABLE_ENTRIES (tei, &is->aerodrome_airlift_usage) { + Tile * tile = (Tile *)tei.key; + int mask = tei.value; + if ((tile == NULL) || (tile == p_null_tile)) + continue; - // At this point we know switching to the candidate would not cause us to waste shields and would not - // give us a worse match role-wise to the original limited unit. So pick it if it's better somehow, - // either a better role match or more expensive. - else if ((candidate_type->Cost > replacement_type->Cost) || - (((replacement_type->AI_Strategy & limited_unit_strat) == 0) && - ((candidate_type ->AI_Strategy & limited_unit_strat) != 0))) { - replacement_type_id = can_type_id; - replacement_type = candidate_type; - } - } - } + int tile_x, tile_y; + if (! tile_coords_from_ptr (&p_bic_data->Map, tile, &tile_x, &tile_y)) + continue; - if (replacement_type_id >= 0) { - City_set_production (this, __, COT_Unit, replacement_type_id, false); - if (this->Body.CivID == p_main_screen_form->Player_CivID) { - PopupForm * popup = get_popup_form (); - set_popup_str_param (0, this->Body.CityName, -1, -1); - set_popup_str_param (1, p_bic_data->UnitTypes[limited_unit_type_id].Name, -1, -1); - int limit = -1; - get_unit_limit (&leaders[this->Body.CivID], limited_unit_type_id, &limit); - set_popup_int_param (2, limit); - set_popup_str_param (3, p_bic_data->UnitTypes[replacement_type_id].Name, -1, -1); - popup->vtable->set_text_key_and_flags (popup, __, is->mod_script_path, "C3X_LIMITED_UNIT_CHANGE", -1, 0, 0, 0); - int response = patch_show_popup (popup, __, 0, 0); - if (response == 0) - *p_zoom_to_city_after_update = true; - } + out[0] = tile_x; + out[1] = tile_y; + out[2] = mask; + out += 3; + written++; + } + chunk[0] = written; + int unused_entries = entry_capacity - written; + if (unused_entries > 0) { + int trimmed_bytes = unused_entries * 3 * (int)sizeof(int); + mod_data.length -= trimmed_bytes; } - - } else { - City_Order order; - patch_City_ai_choose_production (this, __, &order); - City_set_production (this, __, order.OrderType, order.OrderID, false); } - // If the player changed production to something other than a unit, don't spawn anything - if (this->Body.Order_Type != COT_Unit) - skip_spawn = true; + if (is->current_config.enable_named_tiles && (is->named_tile_map.len > 0)) { + serialize_aligned_text ("named_tiles", &mod_data); + int entry_capacity = is->named_tile_map.len; + int bytes_per_entry = (int)sizeof(int) * 2 + (int)sizeof(((struct named_tile_entry *)0)->name); + byte * chunk = (byte *)buffer_allocate (&mod_data, (int)sizeof(int) + bytes_per_entry * entry_capacity); + int * count = (int *)chunk; + byte * out = (byte *)(count + 1); + int written = 0; + FOR_TABLE_ENTRIES (tei, &is->named_tile_map) { + struct named_tile_entry * entry = (struct named_tile_entry *)tei.value; + if ((entry == NULL) || (entry->name[0] == '\0')) + continue; + Tile * tile = (Tile *)tei.key; + int tile_x = entry->tile_x; + int tile_y = entry->tile_y; + if ((tile != NULL) && (tile != p_null_tile)) + tile_coords_from_ptr (&p_bic_data->Map, tile, &tile_x, &tile_y); + ((int *)out)[0] = tile_x; + ((int *)out)[1] = tile_y; + out += sizeof(int) * 2; + memcpy (out, entry->name, sizeof entry->name); + out += sizeof entry->name; + written++; + } + *count = written; + int unused_entries = entry_capacity - written; + if (unused_entries > 0) { + int trimmed_bytes = unused_entries * bytes_per_entry; + mod_data.length -= trimmed_bytes; + } + } - // Just as a final check, if we weren't able to switch production off the limited unit, prevent it from being spawned so the limit - // doesn't get violated. - if ((this->Body.Order_Type == COT_Unit) && (this->Body.Order_ID == limited_unit_type_id)) - skip_spawn = true; + if (is->great_wall_auto_build != GWABS_NOT_STARTED) { + serialize_aligned_text ("great_wall_auto_build_state", &mod_data); + *(int *)buffer_allocate (&mod_data, sizeof(int)) = (int)is->great_wall_auto_build; + } + + if (is->ai_candidate_bridge_or_canals_initialized || (is->ai_candidate_bridge_or_canals_count > 0)) { + serialize_aligned_text ("ai_candidate_bridge_or_canals", &mod_data); + int * header = (int *)buffer_allocate (&mod_data, sizeof(int) * 3); + header[0] = is->ai_candidate_bridge_or_canals_initialized ? 1 : 0; + header[1] = is->ai_candidate_bridge_or_canals_count; + header[2] = is->ai_candidate_bridge_or_canals_capacity; + for (int ei = 0; ei < is->ai_candidate_bridge_or_canals_count; ei++) { + struct ai_candidate_bridge_or_canal_entry * entry = &is->ai_candidate_bridge_or_canals[ei]; + int tile_count = (int)entry->tile_count; + if ((tile_count <= 0) || (entry->tile_x == NULL) || (entry->tile_y == NULL)) + tile_count = 0; + + int field_count = 14; + int * chunk = (int *)buffer_allocate (&mod_data, sizeof(int) * (field_count + tile_count * 2)); + int pending_city_id = entry->pending_req.city_id; + if ((pending_city_id < 0) && (entry->pending_req.city != NULL)) + pending_city_id = entry->pending_req.city->Body.ID; + + chunk[0] = entry->district_id; + chunk[1] = (int)entry->owner_civ_id; + chunk[2] = tile_count; + chunk[3] = (entry->tile_capacity > 0) ? entry->tile_capacity : tile_count; + chunk[4] = entry->assigned_tile_index; + chunk[5] = entry->assigned_worker_id; + chunk[6] = entry->completed ? 1 : 0; + chunk[7] = pending_city_id; + chunk[8] = entry->pending_req.civ_id; + chunk[9] = entry->pending_req.district_id; + chunk[10] = entry->pending_req.assigned_worker_id; + chunk[11] = entry->pending_req.target_x; + chunk[12] = entry->pending_req.target_y; + chunk[13] = entry->pending_req.worker_assigned_turn; + + int * out = chunk + field_count; + for (int ti = 0; ti < tile_count; ti++) { + out[0] = entry->tile_x[ti]; + out[1] = entry->tile_y[ti]; + out += 2; + } + } + } } - if (! skip_spawn) - City_spawn_unit_if_done (this); + int metadata_size = (mod_data.length > 0) ? 12 : 0; // Two four-byte bookends plus one four-byte size, only written if there's any mod data + + void * tr = MappedFile_create_file (this, __, file_path, file_size + mod_data.length + metadata_size, is_shared); + if (tr != NULL) { + is->accessing_save_file = this; + if ((mod_data.length > 0) && (mod_data.length + metadata_size <= this->size)) { + // Write first bookend to mod's segment in the save data + byte * seg_start = (byte *)tr + file_size; + for (int n = 0; n < ARRAY_LEN (c3x_save_segment_bookend); n++) + seg_start[n] = c3x_save_segment_bookend[n]; + + // Write actual mod game data + memcpy ((byte *)tr + file_size + 4, mod_data.contents, mod_data.length); + + // Write size of mod data + byte * seg_end = (byte *)tr + file_size + 4 + mod_data.length; + int_to_bytes (seg_end, mod_data.length); + + // Finish off with another bookend + for (int n = 0; n < ARRAY_LEN (c3x_save_segment_bookend); n++) + seg_end[4+n] = c3x_save_segment_bookend[n]; + } + } + buffer_deinit (&mod_data); + return tr; } -void __fastcall -patch_Leader_upgrade_all_units (Leader * this, int edx, int type_id) +bool +match_save_chunk_name (byte ** cursor, char const * name) { - is->penciled_in_upgrade_count = 0; - Leader_upgrade_all_units (this, __, type_id); + if (strcmp (name, *cursor) == 0) { + // Move cursor past the string if it matched. Also move past any padding that was added to ensure alignment (see serialize_aligned_text). + *cursor = (byte *)((int)*cursor + strlen (name) + 4 & ~3); + return true; + } else + return false; } -void __fastcall -patch_Main_Screen_Form_upgrade_all_units (Main_Screen_Form * this, int edx, int type_id) +bool +match_save_segment_bookend (byte * b) { - is->penciled_in_upgrade_count = 0; - Main_Screen_Form_upgrade_all_units (this, __, type_id); + return memcmp (c3x_save_segment_bookend, b, ARRAY_LEN (c3x_save_segment_bookend)) == 0; } -bool __fastcall -patch_Unit_can_perform_upgrade_all (Unit * this, int edx, int unit_command_value) +int __cdecl +patch_move_game_data (byte * buffer, bool save_else_load) { - bool base = patch_Unit_can_perform_command (this, __, unit_command_value); + int tr = move_game_data (buffer, save_else_load); - // Deal with unit limits. If the unit type we're upgrading to is limited, we need to pencil in the upgrade to make sure that we don't queue up - // so many upgrades that we exceed the limit. - City * city; - int upgrade_id, available; - if (base && - (is->current_config.unit_limits.len > 0) && - (NULL != (city = city_at (this->Body.X, this->Body.Y))) && - (0 <= (upgrade_id = City_get_upgraded_type_id (city, __, this->Body.UnitTypeID))) && - get_available_unit_count (&leaders[this->Body.CivID], upgrade_id, &available)) { + if (! save_else_load) { + // Free all district_instance structs first + FOR_TABLE_ENTRIES (tei, &is->district_tile_map) { + struct district_instance * inst = (struct district_instance *)tei.value; + if (inst != NULL) + free (inst); + } + table_deinit (&is->district_tile_map); + FOR_TABLE_ENTRIES (tei, &is->named_tile_map) { + struct named_tile_entry * entry = (struct named_tile_entry *)tei.value; + if (entry != NULL) + free (entry); + } + table_deinit (&is->named_tile_map); + clear_all_tracked_workers (); + reset_ai_candidate_bridge_or_canals (); + } - // Find penciled in upgrade. Add a new one if we don't already have one. - struct penciled_in_upgrade * piu = NULL; { - for (int n = 0; n < is->penciled_in_upgrade_count; n++) - if (is->penciled_in_upgrades[n].unit_type_id == upgrade_id) { - piu = &is->penciled_in_upgrades[n]; + // Check for a mod save data section and load it if present + MappedFile * save; + int seg_size; + byte * seg; + if ((! save_else_load) && + ((save = is->accessing_save_file) != NULL) && + (save->size >= 8) && + match_save_segment_bookend ((byte *)((int)save->base_addr + save->size - 4)) && + ((seg_size = int_from_bytes ((byte *)((int)save->base_addr + save->size - 8))) > 0) && + (save->size >= seg_size + 12) && + match_save_segment_bookend ((byte *)((int)save->base_addr + save->size - seg_size - 12)) && + ((seg = malloc (seg_size)) != NULL)) { + memcpy (seg, (void *)((int)save->base_addr + save->size - seg_size - 8), seg_size); + + byte * cursor = seg; + char * error_chunk_name = NULL; + while (cursor < seg + seg_size) { + if (match_save_chunk_name (&cursor, "special save message")) { + char * msg = (char *)cursor; + cursor = (byte *)((int)cursor + strlen (msg) + 4 & ~3); + + PopupForm * popup = get_popup_form (); + popup->vtable->set_text_key_and_flags (popup, __, is->mod_script_path, "C3X_INFO", -1, 0, 0, 0); + PopupForm_add_text (popup, __, "This save contains a special message:", 0); + PopupForm_add_text (popup, __, msg, 0); + patch_show_popup (popup, __, 0, 0); + + } else if (match_save_chunk_name (&cursor, "extra_defensive_bombards")) { + int bytes_read = itable_deserialize (cursor, seg + seg_size, &is->extra_defensive_bombards); + if (bytes_read > 0) + cursor += bytes_read; + else { + error_chunk_name = "extra_defensive_bombards"; break; } - if (piu == NULL) { - reserve (sizeof is->penciled_in_upgrades[0], (void **)&is->penciled_in_upgrades, &is->penciled_in_upgrade_capacity, is->penciled_in_upgrade_count); - piu = &is->penciled_in_upgrades[is->penciled_in_upgrade_count]; - is->penciled_in_upgrade_count += 1; - piu->unit_type_id = upgrade_id; - piu->count = 0; - } - } - // If we can have more units of the type we're upgrading to, pencil in another upgrade and return true. Otherwise return false so this - // unit isn't considered one of the upgradable ones. - if (piu->count < available) { - piu->count += 1; - return true; - } else - return false; + } else if (match_save_chunk_name (&cursor, "airdrops_this_turn")) { + int bytes_read = itable_deserialize (cursor, seg + seg_size, &is->airdrops_this_turn); + if (bytes_read > 0) + cursor += bytes_read; + else { + error_chunk_name = "airdrops_this_turn"; + break; + } - } else - return base; -} + } else if (match_save_chunk_name (&cursor, "unit_transport_ties")) { + int bytes_read = itable_deserialize (cursor, seg + seg_size, &is->unit_transport_ties); + if (bytes_read > 0) + cursor += bytes_read; + else { + error_chunk_name = "unit_transport_ties"; + break; + } -void __fastcall -patch_Fighter_animate_start_of_combat (Fighter * this, int edx, Unit * attacker, Unit * defender) -{ - // Temporarily clear the attacker retreat eligibility flag when needed so naval units are rotated and animated properly. Normally the game - // does not use ranged animations when the attacker can retreat and, worse, not using ranged anims means it also ignores the - // rotate-before-attack setting. - bool restore_attacker_retreat_eligibility = false; - if ((is->current_config.sea_retreat_rules != RR_STANDARD) && - (p_bic_data->UnitTypes[attacker->Body.UnitTypeID].Unit_Class == UTC_Sea) && - Unit_has_ability (attacker, __, UTA_Ranged_Attack_Animation) && - Unit_has_ability (defender, __, UTA_Ranged_Attack_Animation) && - this->attacker_eligible_to_retreat) { - this->attacker_eligible_to_retreat = false; - restore_attacker_retreat_eligibility = true; - } + } else if (match_save_chunk_name (&cursor, "waiting_units")) { + int bytes_read = itable_deserialize (cursor, seg + seg_size, &is->waiting_units); + if (bytes_read > 0) { + cursor += bytes_read; + is->have_loaded_waiting_units = true; + } else { + error_chunk_name = "waiting_units"; + break; + } - Fighter_animate_start_of_combat (this, __, attacker, defender); + } else if (match_save_chunk_name (&cursor, "extra_city_improvs")) { + bool success = false; + int remaining_bytes = (seg + seg_size) - cursor; - if (restore_attacker_retreat_eligibility) - this->attacker_eligible_to_retreat = true; -} + // Read two int vars from this save chunk + int file_extra_improv_count, count_entries; + if (remaining_bytes >= 8) { + file_extra_improv_count = *((int *)cursor)++; + count_entries = *((int *)cursor)++; + remaining_bytes -= 8; + } else + goto done_with_extra_city_improvs; -Unit * __fastcall -patch_Leader_spawn_unit_from_building (Leader * this, int edx, int type_id, int tile_x, int tile_y, int barb_tribe_id, int id, bool param_6, LeaderKind leader_kind, int race_id) -{ - int available; - if (get_available_unit_count (this, type_id, &available) && (available <= 0)) - return NULL; - else - return patch_Leader_spawn_unit (this, __, type_id, tile_x, tile_y, barb_tribe_id, id, param_6, leader_kind, race_id); -} + // Check that the extra improv counts are valid. They must be greater than zero and what was stored in the save must + // match what we got from the current scenario data. + int extra_improv_count = not_below (0, p_bic_data->ImprovementsCount - 256); + if ((file_extra_improv_count <= 0) || (extra_improv_count != file_extra_improv_count)) + goto done_with_extra_city_improvs; -int __fastcall -patch_City_count_improvs_enabling_upgrade (City * this, int edx, enum ImprovementTypeFlags flag) -{ - return is->current_config.allow_upgrades_in_any_city ? 1 : City_count_improvements_with_flag (this, __, flag); -} + // The extra bits data in the save is stored in 32-bit chunks, "ints", to maintain alignment. Compute how many ints we + // need for each list of bits and check that reading all entries won't overrun the buffer. + int ints_per_list = (extra_improv_count + 31) / 32, + ints_per_entry = 1 + 2 * ints_per_list; + if (count_entries * ints_per_entry * sizeof(int) > remaining_bytes) + goto done_with_extra_city_improvs; + + // Main loop reading the extra bits data + for (int n = 0; n < count_entries; n++) { + City * city = get_city_ptr (*((int *)cursor)++); + if (city == NULL) + goto done_with_extra_city_improvs; + + byte * extra_bits_1 = calloc (sizeof (int), ints_per_list); + for (int k = 0; k < ints_per_list; k++) + ((int *)extra_bits_1)[k] = *((int *)cursor)++; + itable_insert (&is->extra_city_improvs, (int)&city->Body.Improvements_1, (int)extra_bits_1); + + byte * extra_bits_2 = calloc (sizeof (int), ints_per_list); + for (int k = 0; k < ints_per_list; k++) + ((int *)extra_bits_2)[k] = *((int *)cursor)++; + itable_insert (&is->extra_city_improvs, (int)&city->Body.Improvements_2, (int)extra_bits_2); + } -City * __fastcall -patch_Leader_create_city_from_hut (Leader * this, int edx, int x, int y, int race_id, int param_4, char const * name, bool param_6) -{ - City * tr = Leader_create_city (this, __, x, y, race_id, param_4, name, param_6); - if (tr != NULL) - on_gain_city (this, tr, CGR_POPPED_FROM_HUT); - return tr; -} + // Rebuild the trade network since it may have changed as a result of the additional buildings. This method also + // refreshes the free improvement tables and recomputes city happiness. + patch_Map_build_trade_network (&p_bic_data->Map); -City * __fastcall -patch_Leader_create_city_for_ai_respawn (Leader * this, int edx, int x, int y, int race_id, int param_4, char const * name, bool param_6) -{ - City * tr = Leader_create_city (this, __, x, y, race_id, param_4, name, param_6); - if (tr != NULL) - on_gain_city (this, tr, CGR_PLACED_FOR_AI_RESPAWN); - return tr; -} + success = true; -City * __fastcall -patch_Leader_create_city_for_founding (Leader * this, int edx, int x, int y, int race_id, int param_4, char const * name, bool param_6) -{ - City * tr = Leader_create_city (this, __, x, y, race_id, param_4, name, param_6); - if (tr != NULL) - on_gain_city (this, tr, CGR_FOUNDED); - return tr; -} + done_with_extra_city_improvs: + if (! success) { + error_chunk_name = "extra_city_improvs";; + break; + } -City * __fastcall -patch_Leader_create_city_for_scenario (Leader * this, int edx, int x, int y, int race_id, int param_4, char const * name, bool param_6) -{ - City * tr = Leader_create_city (this, __, x, y, race_id, param_4, name, param_6); - if (tr != NULL) - on_gain_city (this, tr, CGR_PLACED_FOR_SCENARIO); - return tr; -} + } else if (match_save_chunk_name (&cursor, "turn_no_of_last_founding_for_settler_perfume")) { + for (int n = 0; n < 32; n++) + is->turn_no_of_last_founding_for_settler_perfume[n] = *((int *)cursor)++; + } else if (match_save_chunk_name (&cursor, "current_day_night_cycle")) { + is->current_day_night_cycle = *((int *)cursor)++; + is->day_night_cycle_unstarted = false; + // The day/night cycle sprite proxies will have been cleared in patch_load_scenario. They will not necessarily be set + // up again in the usual way because Map_Renderer::load_images is not necessarily called when loading a save. The game + // skips reloading all graphics when loading a save while in-game with another that uses the same graphics (possibly + // only the standard graphics; I didn't test). If day/night cycle mode is active, restore the proxies now if they + // haven't already been. + if ((is->day_night_cycle_img_state == IS_OK) && ! is->day_night_cycle_img_proxies_indexed) + build_sprite_proxies_24 (&p_bic_data->Map.Renderer); -bool __fastcall -patch_Leader_do_capture_city (Leader * this, int edx, City * city, bool involuntary, bool converted) -{ - is->currently_capturing_city = city; - on_lose_city (&leaders[city->Body.CivID], city, converted ? CLR_CONVERTED : (involuntary ? CLR_CONQUERED : CLR_TRADED)); + // Because we've restored current_day_night_cycle from the save, set that is is not the first turn so the cycle + // doesn't get restarted. + is->day_night_cycle_unstarted = false; + + } else if (match_save_chunk_name (&cursor, "great_wall_auto_build_state")) { + int state = *((int *)cursor)++; + if ((state >= GWABS_NOT_STARTED) && (state <= GWABS_DONE)) + is->great_wall_auto_build = (enum great_wall_auto_build_state)state; + else + is->great_wall_auto_build = GWABS_NOT_STARTED; - if (is->current_config.enable_districts && is->current_config.enable_wonder_districts) { - handle_possible_duplicate_small_wonders (city, this); - } + } else if (match_save_chunk_name (&cursor, "great_wall_auto_build_is_done")) { + bool was_done = (*((int *)cursor)++ != 0); + is->great_wall_auto_build = was_done ? GWABS_DONE : GWABS_NOT_STARTED; + + } else if (match_save_chunk_name (&cursor, "district_pending_requests")) { + bool success = false; + int remaining_bytes = (seg + seg_size) - cursor; + if (remaining_bytes >= (int)sizeof(int)) { + int * ints = (int *)cursor; + int entry_count = *ints++; + cursor = (byte *)ints; + remaining_bytes -= (int)sizeof(int); + if ((entry_count >= 0) && + (remaining_bytes >= entry_count * 5 * (int)sizeof(int))) { + for (int civ_id = 0; civ_id < 32; civ_id++) { + FOR_TABLE_ENTRIES (tei, &is->city_pending_district_requests[civ_id]) { + struct pending_district_request * req = (struct pending_district_request *)tei.value; + if (req != NULL) + free (req); + } + table_deinit (&is->city_pending_district_requests[civ_id]); + } + success = true; + for (int n = 0; n < entry_count; n++) { + if (remaining_bytes < 5 * (int)sizeof(int)) { + success = false; + break; + } + int city_id = *ints++; + int district_id = *ints++; + int assigned_worker_id = *ints++; + int target_x = *ints++; + int target_y = *ints++; + cursor = (byte *)ints; + remaining_bytes -= 5 * (int)sizeof(int); + City * city = get_city_ptr (city_id); + if ((city == NULL) || (district_id < 0) || (district_id >= is->district_count)) + continue; + struct pending_district_request * req = create_pending_district_request (city, district_id); + if (req == NULL) + continue; + if ((assigned_worker_id >= 0) && (get_unit_ptr (assigned_worker_id) == NULL)) + assigned_worker_id = -1; + req->assigned_worker_id = assigned_worker_id; + req->target_x = target_x; + req->target_y = target_y; + } + if (! success) { + for (int civ_id = 0; civ_id < 32; civ_id++) + table_deinit (&is->city_pending_district_requests[civ_id]); + } + } + } + if (! success) { + error_chunk_name = "district_pending_requests"; + break; + } + } else if (match_save_chunk_name (&cursor, "building_pending_orders")) { + bool success = false; + int remaining_bytes = (seg + seg_size) - cursor; + if (remaining_bytes >= (int)sizeof(int)) { + int * ints = (int *)cursor; + int entry_count = *ints++; + cursor = (byte *)ints; + remaining_bytes -= (int)sizeof(int); + if ((entry_count >= 0) && + (remaining_bytes >= entry_count * 2 * (int)sizeof(int))) { + table_deinit (&is->city_pending_building_orders); + success = true; + for (int n = 0; n < entry_count; n++) { + if (remaining_bytes < 2 * (int)sizeof(int)) { + success = false; + break; + } + int city_id = *ints++; + int improv_id = *ints++; + cursor = (byte *)ints; + remaining_bytes -= 2 * (int)sizeof(int); + if (improv_id < 0) + continue; + City * city = get_city_ptr (city_id); + if (city == NULL) + continue; + itable_insert (&is->city_pending_building_orders, (int)city, improv_id); + } + if (! success) + table_deinit (&is->city_pending_building_orders); + } + } + if (! success) { + error_chunk_name = "building_pending_orders"; + break; + } + } else if (match_save_chunk_name (&cursor, "district_tile_map")) { + bool success = false; + int remaining_bytes = (seg + seg_size) - cursor; + if (remaining_bytes >= (int)sizeof(int)) { + int * ints = (int *)cursor; + int entry_count = *ints++; + cursor = (byte *)ints; + remaining_bytes -= (int)sizeof(int); + if (entry_count >= 0) { + int ints_per_entry = 9; + success = true; + int required_bytes = entry_count * ints_per_entry * (int)sizeof(int); + if (success && remaining_bytes >= required_bytes) { + FOR_TABLE_ENTRIES (tei, &is->district_tile_map) { + struct district_instance * inst = (struct district_instance *)tei.value; + if (inst != NULL) + free (inst); + } + table_deinit (&is->district_tile_map); + success = true; + for (int n = 0; n < entry_count; n++) { + if (remaining_bytes < ints_per_entry * (int)sizeof(int)) { + success = false; + break; + } + int x = *ints++; + int y = *ints++; + int district_id = *ints++; + int state_val = *ints++; + int built_by_civ_id = *ints++; + int completed_turn = *ints++; + int wonder_state = *ints++; + int wonder_city_id = *ints++; + int wonder_index = *ints++; + cursor = (byte *)ints; + remaining_bytes -= ints_per_entry * (int)sizeof(int); + if ((district_id < 0) || (district_id >= is->district_count)) + continue; + Tile * tile = tile_at (x, y); + if ((tile != NULL) && (tile != p_null_tile)) { + struct district_instance * inst = ensure_district_instance (tile, district_id, x, y); + if (inst != NULL) { + enum district_state new_state; + switch (state_val) { + case DS_COMPLETED: + new_state = DS_COMPLETED; + break; + case DS_UNDER_CONSTRUCTION: + new_state = DS_UNDER_CONSTRUCTION; + break; + default: + new_state = DS_UNDER_CONSTRUCTION; + break; + } + inst->state = new_state; + inst->built_by_civ_id = built_by_civ_id; + inst->completed_turn = completed_turn; + + inst->wonder_info.state = (enum wonder_district_state)wonder_state; + inst->wonder_info.city_id = (wonder_city_id >= 0) ? wonder_city_id : -1; + City * info_city = (wonder_city_id >= 0) ? get_city_ptr (wonder_city_id) : NULL; + inst->wonder_info.city = info_city; + if (info_city == NULL) + inst->wonder_info.city_id = -1; + inst->wonder_info.wonder_index = wonder_index; + set_tile_unworkable_for_all_cities (tile, x, y); + } + } + } + } + else + success = false; + } + } + if (! success) { + error_chunk_name = "district_tile_map"; + break; + } + } else if (match_save_chunk_name (&cursor, "natural_wonder_districts")) { + bool success = false; + int remaining_bytes = (seg + seg_size) - cursor; + if (remaining_bytes >= (int)sizeof(int)) { + int * ints = (int *)cursor; + int entry_count = *ints++; + cursor = (byte *)ints; + remaining_bytes -= (int)sizeof(int); + if ((entry_count >= 0) && (remaining_bytes >= entry_count * 3 * (int)sizeof(int))) { + success = true; + for (int n = 0; n < entry_count; n++) { + if (remaining_bytes < 3 * (int)sizeof(int)) { + success = false; + break; + } + int x = *ints++; + int y = *ints++; + int natural_id = *ints++; + cursor = (byte *)ints; + remaining_bytes -= 3 * (int)sizeof(int); + if ((natural_id < 0) || (natural_id >= is->natural_wonder_count)) + continue; + Tile * tile = tile_at (x, y); + if ((tile == NULL) || (tile == p_null_tile)) + continue; + struct district_instance * inst = ensure_district_instance (tile, NATURAL_WONDER_DISTRICT_ID, x, y); + if (inst == NULL) + continue; + inst->district_id = NATURAL_WONDER_DISTRICT_ID; + inst->state = DS_COMPLETED; + inst->natural_wonder_info.natural_wonder_id = natural_id; + set_tile_unworkable_for_all_cities (tile, x, y); + } + } + } + if (! success) { + error_chunk_name = "natural_wonder_districts"; + break; + } + } else if (match_save_chunk_name (&cursor, "distribution_hub_records")) { + bool success = false; + int remaining_bytes = (seg + seg_size) - cursor; + if (remaining_bytes >= (int)sizeof(int)) { + int * ints = (int *)cursor; + int entry_count = *ints++; + cursor = (byte *)ints; + remaining_bytes -= (int)sizeof(int); + if ((entry_count >= 0) && (remaining_bytes >= entry_count * 3 * (int)sizeof(int))) { + clear_distribution_hub_tables (); + success = true; + for (int n = 0; n < entry_count; n++) { + if (remaining_bytes < 3 * (int)sizeof(int)) { + success = false; + break; + } + int x = *ints++; + int y = *ints++; + int civ_id = *ints++; + cursor = (byte *)ints; + remaining_bytes -= 3 * (int)sizeof(int); + Tile * tile = tile_at (x, y); + if ((tile == NULL) || (tile == p_null_tile)) + continue; + on_distribution_hub_completed (tile, x, y); + struct distribution_hub_record * rec = get_distribution_hub_record (tile); + if (rec != NULL) + rec->civ_id = civ_id; + } + } + } + if (! success) { + error_chunk_name = "distribution_hub_records"; + break; + } + } else if (match_save_chunk_name (&cursor, "aerodrome_airlift_usage")) { + bool success = false; + int remaining_bytes = (seg + seg_size) - cursor; + if (remaining_bytes >= (int)sizeof(int)) { + int * ints = (int *)cursor; + int entry_count = *ints++; + cursor = (byte *)ints; + remaining_bytes -= (int)sizeof(int); + if ((entry_count >= 0) && + (remaining_bytes >= entry_count * 3 * (int)sizeof(int))) { + table_deinit (&is->aerodrome_airlift_usage); + success = true; + for (int n = 0; n < entry_count; n++) { + if (remaining_bytes < 3 * (int)sizeof(int)) { + success = false; + break; + } + int tile_x = *ints++; + int tile_y = *ints++; + int mask = *ints++; + cursor = (byte *)ints; + remaining_bytes -= 3 * (int)sizeof(int); - bool tr = Leader_do_capture_city (this, __, city, involuntary, converted); - on_gain_city (this, city, converted ? CGR_CONVERTED : (involuntary ? CGR_CONQUERED : CGR_TRADED)); - is->currently_capturing_city = NULL; - return tr; -} + Tile * tile = tile_at (tile_x, tile_y); + if ((tile == NULL) || (tile == p_null_tile)) + continue; -void __fastcall -patch_City_raze (City * this, int edx, int civ_id_responsible, bool checking_elimination) -{ - on_lose_city (&leaders[this->Body.CivID], this, CLR_DESTROYED); - City_raze (this, __, civ_id_responsible, checking_elimination); + itable_insert (&is->aerodrome_airlift_usage, (int)tile, mask); + } + if (! success) + table_deinit (&is->aerodrome_airlift_usage); + } + } + if (! success) { + error_chunk_name = "aerodrome_airlift_usage"; + break; + } - // Delete the extra improvement bits records for this city - City_Improvements * improv_lists[2] = {&this->Body.Improvements_1, &this->Body.Improvements_2}; - for (int n = 0; n < ARRAY_LEN (improv_lists); n++) { - City_Improvements * improv_list = improv_lists[n]; - byte * extra_bits; - if (itable_look_up (&is->extra_city_improvs, (int)improv_list, (int *)&extra_bits)) { - free (extra_bits); - itable_remove (&is->extra_city_improvs, (int)improv_list); - } - } -} + } else if (match_save_chunk_name (&cursor, "named_tiles")) { + bool success = false; + int remaining_bytes = (seg + seg_size) - cursor; + if (remaining_bytes >= (int)sizeof(int)) { + int * ints = (int *)cursor; + int entry_count = *ints++; + cursor = (byte *)ints; + remaining_bytes -= (int)sizeof(int); + int bytes_per_entry = (int)sizeof(int) * 2 + (int)sizeof(((struct named_tile_entry *)0)->name); + if ((entry_count >= 0) && (remaining_bytes >= entry_count * bytes_per_entry)) { + table_deinit (&is->named_tile_map); + success = true; + for (int n = 0; n < entry_count; n++) { + if (remaining_bytes < bytes_per_entry) { + success = false; + break; + } + int tile_x = *ints++; + int tile_y = *ints++; + cursor = (byte *)ints; + remaining_bytes -= (int)sizeof(int) * 2; -void __fastcall -patch_City_draw_hud_icon (City * this, int edx, PCX_Image * canvas, int pixel_x, int pixel_y) -{ - Leader * owner = &leaders[this->Body.CivID]; - int restore_capital = owner->CapitalID; + char name_buf[101]; + memcpy (name_buf, cursor, sizeof name_buf); + name_buf[(sizeof name_buf) - 1] = '\0'; + cursor += sizeof name_buf; + remaining_bytes -= sizeof name_buf; + ints = (int *)cursor; - // Temporarily set this city as the capital if it has an extra palace so it gets the capital star icon - if ((is->current_config.ai_multi_city_start > 1) && - ((*p_human_player_bits & (1 << owner->ID)) == 0) && - has_extra_palace (this)) - owner->CapitalID = this->Body.ID; + if (name_buf[0] == '\0') + continue; + Tile * tile = tile_at (tile_x, tile_y); + if ((tile == NULL) || (tile == p_null_tile)) + continue; - City_draw_hud_icon (this, __, canvas, pixel_x, pixel_y); - owner->CapitalID = restore_capital; -} + struct named_tile_entry * entry = calloc (1, sizeof *entry); + if (entry == NULL) { + success = false; + break; + } + entry->tile_x = tile_x; + entry->tile_y = tile_y; + strncpy (entry->name, name_buf, sizeof entry->name); + entry->name[(sizeof entry->name) - 1] = '\0'; + itable_insert (&is->named_tile_map, (int)tile, (int)entry); + } + if (! success) { + FOR_TABLE_ENTRIES (tei, &is->named_tile_map) { + struct named_tile_entry * entry = (struct named_tile_entry *)tei.value; + if (entry != NULL) + free (entry); + } + table_deinit (&is->named_tile_map); + } + } + } + if (! success) { + error_chunk_name = "named_tiles"; + break; + } -bool __fastcall -patch_City_has_hud_icon (City * this) -{ - return City_has_hud_icon (this) - || ( (is->current_config.ai_multi_city_start > 1) - && ((*p_human_player_bits & (1 << this->Body.CivID)) == 0) - && has_extra_palace (this)); -} + } else if (match_save_chunk_name (&cursor, "ai_candidate_bridge_or_canals")) { + bool success = false; + int remaining_bytes = (seg + seg_size) - cursor; + if (remaining_bytes >= (int)sizeof(int) * 3) { + int * ints = (int *)cursor; + int saved_initialized = *ints++; + int saved_count = *ints++; + int saved_capacity = *ints++; + cursor = (byte *)ints; + remaining_bytes -= (int)sizeof(int) * 3; -void __fastcall -patch_City_draw_on_map (City * this, int edx, Map_Renderer * map_renderer, int pixel_x, int pixel_y, int param_4, int param_5, int param_6, int param_7) -{ - Leader * owner = &leaders[this->Body.CivID]; - int restore_capital = owner->CapitalID; + if ((saved_count >= 0) && (saved_capacity >= 0)) { + reset_ai_candidate_bridge_or_canals (); + success = true; - if (is->current_config.do_not_make_capital_cities_appear_larger) - owner->CapitalID = -1; + int alloc_capacity = saved_capacity; + if (alloc_capacity < saved_count) + alloc_capacity = saved_count; + if (alloc_capacity > 0) { + is->ai_candidate_bridge_or_canals = (struct ai_candidate_bridge_or_canal_entry *)calloc (alloc_capacity, sizeof *is->ai_candidate_bridge_or_canals); + if (is->ai_candidate_bridge_or_canals == NULL) { + success = false; + alloc_capacity = 0; + } else + is->ai_candidate_bridge_or_canals_capacity = alloc_capacity; + } - // Temporarily set this city as the capital if it has an extra palace so it gets drawn with the next larger size - else if ((is->current_config.ai_multi_city_start > 1) && - ((*p_human_player_bits & (1 << owner->ID)) == 0) && - has_extra_palace (this)) - owner->CapitalID = this->Body.ID; + is->ai_candidate_bridge_or_canals_initialized = (saved_initialized != 0); + int loaded_count = 0; + for (int ei = 0; ei < saved_count; ei++) { + if (remaining_bytes < (int)sizeof(int) * 14) { + success = false; + break; + } + int district_id = *ints++; + int owner_civ_id = *ints++; + int tile_count = *ints++; + int tile_capacity = *ints++; + int assigned_tile_index = *ints++; + int assigned_worker_id = *ints++; + int completed = *ints++; + int pending_city_id = *ints++; + int pending_civ_id = *ints++; + int pending_district_id = *ints++; + int pending_assigned_worker_id = *ints++; + int pending_target_x = *ints++; + int pending_target_y = *ints++; + int pending_worker_assigned_turn = *ints++; + cursor = (byte *)ints; + remaining_bytes -= (int)sizeof(int) * 14; - City_draw_on_map (this, __, map_renderer, pixel_x, pixel_y, param_4, param_5, param_6, param_7); - owner->CapitalID = restore_capital; -} + if (tile_count < 0) { + success = false; + break; + } + if (remaining_bytes < tile_count * 2 * (int)sizeof(int)) { + success = false; + break; + } -// Writes a string to replace "Wake" or "Activate" on the right-click menu entry for the given unit. Some units might not have a replacement, in which -// case the function writes nothing and returns false. The string is written to out_str which must point to a buffer of at least str_capacity bytes. -bool -get_menu_verb_for_unit (Unit * unit, char * out_str, int str_capacity) -{ - struct state_desc { - enum c3x_label label; - bool is_doing_worker_job; - } state_descs[35] = { - {CL_IDLE , false}, // [No state] = 0x0 - {CL_FORTIFIED , false}, // Fortifying = 0x1 - {CL_MINING , true }, // Build_Mines = 0x2 - {CL_IRRIGATING , true }, // Irrigate = 0x3 - {CL_BUILDING_FORTRESS , true }, // Build_Fortress = 0x4 - {CL_BUILDING_ROAD , true }, // Build_Road = 0x5 - {CL_BUILDING_RAILROAD , true }, // Build_Railroad = 0x6 - {CL_PLANTING_FOREST , true }, // Plant_Forest = 0x7 - {CL_CLEARING_FOREST , true }, // Clear_Forest = 0x8 - {CL_CLEARING_WETLANDS , true }, // Clear_Wetlands = 0x9 - {CL_CLEARING_DAMAGE , true }, // Clear_Damage = 0xA - {CL_BUILDING_AIRFIELD , true }, // Build_Airfield = 0xB - {CL_BUILDING_RADAR_TOWER, true }, // Build_Radar_Tower = 0xC - {CL_BUILDING_OUTPOST , true }, // Build_Outpost = 0xD - {CL_BUILDING_BARRICADE , true }, // Build_Barricade = 0xE - {CL_INTERCEPTING , false}, // Intercept = 0xF - {CL_MOVING , false}, // Go_To = 0x10 - {CL_BUILDING_ROAD , true }, // Road_To_Tile = 0x11 - {CL_BUILDING_RAILROAD , true }, // Railroad_To_Tile = 0x12 - {CL_BUILDING_COLONY , true }, // Build_Colony = 0x13 - {CL_AUTOMATED , true }, // Auto_Irrigate = 0x14 - {CL_AUTOMATED , true }, // Build_Trade_Routes = 0x15 - {CL_AUTOMATED , true }, // Auto_Clear_Forest = 0x16 - {CL_AUTOMATED , true }, // Auto_Clear_Swamp = 0x17 - {CL_AUTOMATED , true }, // Auto_Clear_Pollution = 0x18 - {CL_AUTOMATED , true }, // Auto_Save_City_Tiles = 0x19 - {CL_EXPLORING , false}, // Explore = 0x1A - {CL_IN_STATE_27 , false}, // ? = 0x1B - {CL_IN_STATE_28 , false}, // Fleeing = 0x1C - {CL_IN_STATE_29 , false}, // ? = 0x1D - {CL_IN_STATE_30 , false}, // ? = 0x1E - {CL_BOMBARDING , false}, // Auto_Bombard = 0x1F - {CL_BOMBARDING , false}, // Auto_Air_Bombard = 0x20 - {CL_BOMBARDING , false}, // Auto_Precision_Strike = 0x21 - {CL_IDLE , false}, // Exhausted = 0x22 - }; - enum UnitStateType state = unit->Body.UnitState; - struct state_desc const * desc; - if ((state >= 0) && (state < ARRAY_LEN (state_descs)) && (desc = &state_descs[state]) && (desc->label >= 0) && (desc->label < COUNT_C3X_LABELS)) { - enum c3x_label label = desc->label; - Unit * container; - if (((label == CL_IDLE) || (label == CL_MOVING) || (label == CL_IN_STATE_29) || desc->is_doing_worker_job) && unit->Body.automated) - label = CL_AUTOMATED; - else if ((label == CL_FORTIFIED) && (NULL != (container = get_unit_ptr (unit->Body.Container_Unit))) && ! Unit_has_ability (container, __, UTA_Army)) - label = CL_TRANSPORTED; - else if ((label == CL_FORTIFIED) && (unit->Body.Status & (USF_SENTRY | USF_SENTRY_ENEMY_ONLY))) - label = CL_SENTRY; - else if ((label == CL_MINING) && is->current_config.enable_districts) { + int stored_tile_count = tile_count; + if (tile_count > AI_BRIDGE_CANAL_CANDIDATE_MAX_EVAL_TILES) + tile_count = AI_BRIDGE_CANAL_CANDIDATE_MAX_EVAL_TILES; + if (tile_capacity < tile_count) + tile_capacity = tile_count; + + struct ai_candidate_bridge_or_canal_entry * entry = NULL; + if ((alloc_capacity > 0) && (loaded_count < alloc_capacity)) + entry = &is->ai_candidate_bridge_or_canals[loaded_count]; + + if (entry != NULL) { + entry->district_id = district_id; + entry->owner_civ_id = (short)owner_civ_id; + entry->tile_count = (short)tile_count; + entry->tile_capacity = tile_capacity; + entry->assigned_tile_index = assigned_tile_index; + entry->assigned_worker_id = assigned_worker_id; + entry->completed = (completed != 0); + + entry->tile_x = (short *)calloc (sizeof *entry->tile_x, tile_count); + entry->tile_y = (short *)calloc (sizeof *entry->tile_y, tile_count); + if ((tile_count > 0) && ((entry->tile_x == NULL) || (entry->tile_y == NULL))) { + if (entry->tile_x != NULL) + free (entry->tile_x); + if (entry->tile_y != NULL) + free (entry->tile_y); + entry->tile_x = NULL; + entry->tile_y = NULL; + entry->tile_count = 0; + entry->tile_capacity = 0; + } - // Check if this unit is actually building a district instead of a mine - Tile * tile = tile_at (unit->Body.X, unit->Body.Y); - struct district_instance * inst = get_district_instance (tile); - if ((tile != NULL) && (tile != p_null_tile) && inst != NULL) { - char const * district_name = is->district_configs[inst->district_type].name; - snprintf (out_str, str_capacity, "%s %s", is->c3x_labels[CL_BUILDING], district_name); - out_str[str_capacity - 1] = '\0'; - return true; - } - } + for (int ti = 0; ti < stored_tile_count; ti++) { + int tx = *ints++; + int ty = *ints++; + if ((entry->tile_x != NULL) && (ti < tile_count)) { + entry->tile_x[ti] = (short)tx; + entry->tile_y[ti] = (short)ty; + } + } - strncpy (out_str, is->c3x_labels[label], str_capacity); - out_str[str_capacity - 1] = '\0'; - return true; - } else - return false; -} + entry->pending_req.city = (pending_city_id >= 0) ? get_city_ptr (pending_city_id) : NULL; + entry->pending_req.city_id = (entry->pending_req.city != NULL) ? pending_city_id : -1; + entry->pending_req.civ_id = pending_civ_id; + entry->pending_req.district_id = pending_district_id; + entry->pending_req.assigned_worker_id = pending_assigned_worker_id; + entry->pending_req.target_x = pending_target_x; + entry->pending_req.target_y = pending_target_y; + entry->pending_req.worker_assigned_turn = pending_worker_assigned_turn; + + if ((entry->assigned_worker_id >= 0) && (get_unit_ptr (entry->assigned_worker_id) == NULL)) + entry->assigned_worker_id = -1; + if ((entry->pending_req.assigned_worker_id >= 0) && (get_unit_ptr (entry->pending_req.assigned_worker_id) == NULL)) + entry->pending_req.assigned_worker_id = -1; + if ((entry->assigned_tile_index < 0) || (entry->assigned_tile_index >= entry->tile_count)) + entry->assigned_tile_index = -1; + + loaded_count++; + } else { + for (int ti = 0; ti < stored_tile_count; ti++) { + ints += 2; + } + } -void __fastcall -patch_MenuUnitItem_write_text_to_temp_str (MenuUnitItem * this) -{ - MenuUnitItem_write_text_to_temp_str (this); + cursor = (byte *)ints; + remaining_bytes -= stored_tile_count * 2 * (int)sizeof(int); + } + if (success) + is->ai_candidate_bridge_or_canals_count = loaded_count; + } + } + if (! success) { + error_chunk_name = "ai_candidate_bridge_or_canals"; + break; + } - Unit * unit = this->unit; - char repl_verb[32]; - if (is->current_config.describe_states_of_units_on_menu && - (unit->Body.CivID == p_main_screen_form->Player_CivID) && - (Unit_get_containing_army (unit) == NULL) && - get_menu_verb_for_unit (unit, repl_verb, sizeof repl_verb)) { - char * verb = (unit->Body.UnitState == UnitState_Fortifying) ? (*p_labels)[LBL_WAKE] : (*p_labels)[LBL_ACTIVATE]; - char * verb_str_start = strstr (temp_str, verb); - if (verb_str_start != NULL) { - char s[500]; - char * verb_str_end = verb_str_start + strlen (verb); - snprintf (s, sizeof s, "%.*s%s%s", verb_str_start - temp_str, temp_str, repl_verb, verb_str_end); - s[(sizeof s) - 1] = '\0'; - strncpy (temp_str, s, sizeof s); - } - } -} + } else if (match_save_chunk_name (&cursor, "district_config_names")) { + bool success = false; + bool mismatch_found = false; + bool count_mismatch = false; + char first_mismatch[200]; + first_mismatch[0] = '\0'; + int remaining_bytes = (seg + seg_size) - cursor; + if (remaining_bytes >= (int)sizeof(int)) { + int * ints = (int *)cursor; + int saved_count = *ints++; + cursor = (byte *)ints; + remaining_bytes -= (int)sizeof(int); + if (saved_count >= 0) { + success = true; + count_mismatch = (saved_count != is->district_count); + char * saved_names[saved_count]; + for (int n = 0; n < saved_count; n++) { + if (remaining_bytes < (int)sizeof(int)) { + success = false; + break; + } + ints = (int *)cursor; + int saved_id = *ints++; + cursor = (byte *)ints; + remaining_bytes -= (int)sizeof(int); -void __fastcall -patch_Tile_m74_Set_Square_Type_for_hill_gen (Tile * this, int edx, enum SquareTypes sq, int tile_x, int tile_y) -{ - if ((sq == SQ_Volcano) && is->current_config.do_not_generate_volcanos) - sq = SQ_Mountains; - this->vtable->m74_Set_Square_Type (this, __, sq, tile_x, tile_y); -} + int name_len = -1; + for (int k = 0; k < remaining_bytes; k++) { + if (cursor[k] == '\0') { + name_len = k; + break; + } + } + if (name_len < 0) { + success = false; + break; + } + int padded_len = (name_len + 4) & ~3; + if (padded_len > remaining_bytes) { + success = false; + break; + } -void __fastcall -patch_Map_place_scenario_things (Map * this) -{ - is->is_placing_scenario_things = true; + char * saved_name = (char *)cursor; + saved_names[n] = saved_name; + if (! mismatch_found) { + if ((saved_id < 0) || (saved_id >= is->district_count)) { + snprintf (first_mismatch, sizeof first_mismatch, "%s %d (\"%s\") %s", is->c3x_labels[CL_DISTRICT_ID], saved_id, saved_name, is->c3x_labels[CL_DISTRICT_IN_SAVE_BUT_MISSING_NOW]); + first_mismatch[(sizeof first_mismatch) - 1] = '\0'; + mismatch_found = true; + } else { + char const * current_name = is->district_configs[saved_id].name; + if (current_name == NULL) + current_name = ""; + if (strcmp (current_name, saved_name) != 0) { + snprintf (first_mismatch, sizeof first_mismatch, "%s %d \"%s\" %s \"%s\"", is->c3x_labels[CL_DISTRICT_ID], saved_id, saved_name, is->c3x_labels[CL_DISTRICT_NAME_MISMATCH], current_name); + first_mismatch[(sizeof first_mismatch) - 1] = '\0'; + mismatch_found = true; + } + } + } - Map_place_scenario_things (this); + cursor += padded_len; + remaining_bytes -= padded_len; + } + if (success && count_mismatch && (first_mismatch[0] == '\0')) { + snprintf (first_mismatch, sizeof first_mismatch, "%s %d %s %d", is->c3x_labels[CL_SAVE_FILE_HAD], saved_count, is->c3x_labels[CL_CURRENT_CONFIG_HAS_ONLY], is->district_count); + first_mismatch[(sizeof first_mismatch) - 1] = '\0'; + mismatch_found = true; + } + if (success && mismatch_found) { + PopupForm * popup = get_popup_form (); + popup->vtable->set_text_key_and_flags (popup, __, is->mod_script_path, "C3X_ERROR", -1, 0, 0, 0); - // If there are any mills in the config then recompute yields & happiness in all cities. This must be done because we avoid doing this as - // mills are added to cities while placing scenario things. - if (is->current_config.count_mills > 0) - for (int n = 0; n <= p_cities->LastIndex; n++) { - City * city = get_city_ptr (n); - if (city != NULL) - patch_City_recompute_yields_and_happiness (city); - } + char s[1000]; + snprintf (s, sizeof s, "%s %s", is->c3x_labels[CL_WARNING_DISTRICTS_CONFIG_MISMATCH], first_mismatch); + snprintf (s, sizeof s, "%s. %s", s, is->c3x_labels[CL_MAY_BE_OTHER_ERRORS_AS_WELL]); + s[(sizeof s) - 1] = '\0'; + PopupForm_add_text (popup, __, s, 0); - if (is->current_config.enable_districts) - load_scenario_districts_from_file (); + snprintf (s, sizeof s, "^"); + s[(sizeof s) - 1] = '\0'; + PopupForm_add_text (popup, __, s, 0); - is->is_placing_scenario_things = false; -} + snprintf (s, sizeof s, "^%s:", is->c3x_labels[CL_DISTRICTS_IN_SAVE_FILE]); + s[(sizeof s) - 1] = '\0'; + PopupForm_add_text (popup, __, s, 0); + for (int n = 0; n < saved_count; n++) { + snprintf (s, sizeof s, "^ (%d) %s", n, saved_names[n]); + s[(sizeof s) - 1] = '\0'; + PopupForm_add_text (popup, __, s, 0); + } -void -on_open_advisor (AdvisorKind kind) -{ - recompute_resources_if_necessary (); -} + snprintf (s, sizeof s, "^"); + s[(sizeof s) - 1] = '\0'; + PopupForm_add_text (popup, __, s, 0); -bool __fastcall patch_Advisor_Base_Form_domestic_m95 (Advisor_Base_Form * this) { on_open_advisor (AK_DOMESTIC); return Advisor_Base_Form_domestic_m95 (this); } -bool __fastcall patch_Advisor_Base_Form_trade_m95 (Advisor_Base_Form * this) { on_open_advisor (AK_TRADE) ; return Advisor_Base_Form_trade_m95 (this); } -bool __fastcall patch_Advisor_Base_Form_military_m95 (Advisor_Base_Form * this) { on_open_advisor (AK_MILITARY); return Advisor_Base_Form_military_m95 (this); } -bool __fastcall patch_Advisor_Base_Form_foreign_m95 (Advisor_Base_Form * this) { on_open_advisor (AK_FOREIGN) ; return Advisor_Base_Form_foreign_m95 (this); } -bool __fastcall patch_Advisor_Base_Form_cultural_m95 (Advisor_Base_Form * this) { on_open_advisor (AK_CULTURAL); return Advisor_Base_Form_cultural_m95 (this); } -bool __fastcall patch_Advisor_Base_Form_science_m95 (Advisor_Base_Form * this) { on_open_advisor (AK_SCIENCE) ; return Advisor_Base_Form_science_m95 (this); } + snprintf (s, sizeof s, "^%s \"%s\":", is->c3x_labels[CL_CURRENTLY_CONFIGURED_DISTRICTS], is->current_districts_config_path); + s[(sizeof s) - 1] = '\0'; + PopupForm_add_text (popup, __, s, 0); + for (int n = 0; n < is->district_count; n++) { + snprintf (s, sizeof s, "^ (%d) %s", n, is->district_configs[n].name); + s[(sizeof s) - 1] = '\0'; + PopupForm_add_text (popup, __, s, 0); + } -void __fastcall -patch_Main_Screen_Form_open_quick_build_chooser (Main_Screen_Form * this, int edx, City * city, int mouse_x, int mouse_y) -{ - recompute_resources_if_necessary (); - Main_Screen_Form_open_quick_build_chooser (this, __, city, mouse_x, mouse_y); -} + patch_show_popup (popup, __, 0, 0); + } + } + } + if (! success) { + error_chunk_name = "district_config_names"; + break; + } + + } else { + error_chunk_name = "N/A"; + break; + } + } -int __fastcall -patch_Context_Menu_get_selected_item_on_unit_rcm (Context_Menu * this) -{ - // In the base game, this method returns -1 for any disabled item which prevents the player from clicking those. We want players to be able to - // click unit items which have been disabled by the mod so they can interrupt the queued actions of units that have no moves left. - int index = this->Selected_Item; - if (index >= 0) { - bool is_enabled = (this->Items[index].Status & 2) == 0; - bool is_unit_item = (this->Items[index].Menu_Item_ID - (0x13 + p_bic_data->UnitTypeCount)) >= 0; - return (is_enabled || is_unit_item) ? index : -1; + if (error_chunk_name != NULL) { + char s[200]; + snprintf (s, sizeof s, "Failed to read mod save data. Error occured in chunk: %s", error_chunk_name); + s[(sizeof s) - 1] = '\0'; + pop_up_in_game_error (s); + } + + free (seg); } - return -1; -} -int __fastcall -patch_Tile_check_water_to_block_pollution (Tile * this) -{ - if (this->vtable->m35_Check_Is_Water (this)) - return 1; - else if (is->current_config.do_not_pollute_impassable_tiles) { - enum SquareTypes terrain_type = this->vtable->m50_Get_Square_BaseType (this); - return p_bic_data->TileTypes[terrain_type].Flags.Impassable; - } else - return 0; + return tr; } void __fastcall -patch_Tile_set_flag_for_eruption_damage (Tile * this, int edx, int param_1, int param_2, int x, int y) +patch_MappedFile_deinit_after_saving_or_loading (MappedFile * this) { - if (is->current_config.enable_districts) { - struct district_instance * inst = get_district_instance (this); - if (inst != NULL && inst->district_type >= 0 && inst->district_type < is->district_count) { - // District found - handle removal - int district_id = inst->district_type; - - // Notify human player if this tile is in their territory - int territory_owner = this->vtable->m38_Get_Territory_OwnerID (this); - if (territory_owner == p_main_screen_form->Player_CivID) { - char msg[160]; - char const * district_name = is->district_configs[district_id].name; - snprintf (msg, sizeof msg, "%s %s", district_name, is->c3x_labels[CL_DISTRICT_DESTROYED_BY_VOLCANO]); - msg[(sizeof msg) - 1] = '\0'; - show_map_specific_text (x, y, msg, true); - } + is->accessing_save_file = NULL; + MappedFile_deinit (this); +} - // Remove the district - handle_district_removed (this, district_id, x, y, false); +bool __fastcall +patch_Tile_m7_Check_Barbarian_Camp (Tile * this, int edx, int visible_to_civ) +{ + int * p_stack = (int *)&visible_to_civ; + int ret_addr = p_stack[-1]; - // Clear the mine flags - this->vtable->m62_Set_Tile_BuildingID (this, __, -1); - this->vtable->m51_Unset_Tile_Flags (this, __, 0, TILE_FLAG_MINE, x, y); + // If the barb unit AI is calling this method to check if there's a camp to defend, return true if we're allowing barb city capture and the + // tile has a city. This causes barb units to defend cities they've captured, otherwise they'll ignore them. + if ((ret_addr == ADDR_CHECK_BARB_CAMP_TO_DEFEND_RETURN) && + is->current_config.enable_city_capture_by_barbarians && + Tile_has_city (this)) + return true; + else + return Tile_m7_Check_Barbarian_Camp (this, __, visible_to_civ); +} + +bool __fastcall +patch_Unit_can_heal_at (Unit * this, int edx, int tile_x, int tile_y) +{ + if (is->current_config.enable_districts && + is->current_config.enable_port_districts && + (p_bic_data->UnitTypes[this->Body.UnitTypeID].Unit_Class == UTC_Sea)) { + Tile * tile = tile_at (tile_x, tile_y); + if (tile_has_friendly_port_district (tile, this->Body.CivID)) { + int occupier_id = get_tile_occupier_id (tile_x, tile_y, -1, true); + return (occupier_id == -1) || (occupier_id == this->Body.CivID); } } - // Apply the normal eruption damage (lava flag) if allowed - if (! (is->current_config.do_not_pollute_impassable_tiles && - p_bic_data->TileTypes[this->vtable->m50_Get_Square_BaseType (this)].Flags.Impassable)) - this->vtable->m56_Set_Tile_Flags (this, __, param_1, param_2, x, y); + return Unit_can_heal_at (this, __, tile_x, tile_y); } bool __fastcall -patch_City_confirm_production_switch (City * this, int edx, int order_type, int order_id) +patch_Unit_can_airdrop (Unit * this) { - bool tr = City_confirm_production_switch (this, __, order_type, order_id); - if (tr && - (order_type == COT_Improvement) && (order_id >= 0) && (order_id < p_bic_data->ImprovementsCount) && - (this->Body.CivID == p_main_screen_form->Player_CivID) && - is->current_config.warn_when_chosen_building_would_replace_another) { - Improvement * improv = &p_bic_data->Improvements[order_id]; - if (improv->ImprovementFlags & ITF_Replaces_Other_Buildings) { - Improvement * replaced = NULL; - for (int n = 0; n < p_bic_data->ImprovementsCount; n++) { - Improvement * other = &p_bic_data->Improvements[n]; - if ((other->ImprovementFlags & ITF_Replaces_Other_Buildings) && - patch_City_has_improvement (this, __, n, false)) { - replaced = other; - break; - } - } - if (replaced != NULL) { - PopupForm * popup = get_popup_form (); - set_popup_str_param (0, improv->Name.S, -1, -1); - set_popup_str_param (1, replaced->Name.S, -1, -1); - popup->vtable->set_text_key_and_flags (popup, __, is->mod_script_path, "C3X_WARN_ABOUT_BUILDING_REPLACEMENT", -1, 0, 0, 0); - if (patch_show_popup (popup, __, 0, 0) == 1) - return false; + UnitType * this_type = &p_bic_data->UnitTypes[this->Body.UnitTypeID]; + + bool allowed = Unit_can_airdrop (this); + + bool require_aerodrome = (is->current_config.enable_districts && + is->current_config.enable_aerodrome_districts && + is->current_config.air_units_use_aerodrome_districts_not_cities); + + if (require_aerodrome) { + Tile * tile = tile_at (this->Body.X, this->Body.Y); + bool has_aerodrome = false; + + if ((tile != NULL) && (tile != p_null_tile)) + has_aerodrome = tile_has_friendly_aerodrome_district (tile, this->Body.CivID, false); + + if (! has_aerodrome) + allowed = false; + else if (! allowed) { + if ((this_type->Unit_Class != UTC_Air) && + (this_type->Air_Missions & UCV_Airdrop) && + (this->Body.Moves == 0)) + allowed = true; + } + } + + // Possibly rule in this airdrop as allowed if it's by a paratrooper in a helicopter on a carrier and we're configured to allow airdrops under + // those circumstances. + if ((! allowed) && (is->current_config.special_helicopter_rules & SHR_PASSENGER_AIRDROP)) { + Unit * container = get_unit_ptr (this->Body.Container_Unit); + if (container != NULL && p_bic_data->UnitTypes[container->Body.UnitTypeID].Unit_Class == UTC_Air) { // if in helicopter + Unit * metacontainer = get_unit_ptr (container->Body.Container_Unit); + if (metacontainer != NULL && p_bic_data->UnitTypes[metacontainer->Body.UnitTypeID].Unit_Class == UTC_Sea && + Unit_has_ability (metacontainer, __, UTA_Transports_Only_Aircraft)) { // if that helicopter is on a carrier + // Allow the airdrop under the same restrictions as from an airfield + allowed = this_type->Unit_Class != UTC_Air && this->Body.Moves == 0; } } } - return tr; + + if (! allowed) + return false; + + return itable_look_up_or (&is->airdrops_this_turn, this->Body.ID, 0) == 0; } -byte const c3x_save_segment_bookend[4] = {0x22, 'C', '3', 'X'}; +bool __fastcall +patch_City_Improvements_contains (City_Improvements * this, int edx, int id) +{ + byte * extra_bits; + if ((id < 256) || ! is->current_config.remove_city_improvement_limit) + return City_Improvements_contains (this, __, id); + else if (itable_look_up (&is->extra_city_improvs, (int)this, (int *)&extra_bits)) { + int extra_id = id - 256; + return (extra_bits[extra_id>>3] >> (extra_id & 7)) & 1; + } else + return false; +} -// Writes a string to the buffer and pads the end so it's four-byte aligned (assuming the existing contents is already so aligned). -void -serialize_aligned_text (char const * text, struct buffer * b) +void __fastcall +patch_City_Improvements_set (City_Improvements * this, int edx, int id, bool add_else_remove) { - int len = strlen (text); - if (len > 0) { - int padded_len = (len + 4) & ~3; // +1 for null terminator then +3 & ~3 for alignment - byte * p = buffer_allocate (b, padded_len); - strcpy (p, text); - for (int n = 0; n < padded_len - len; n++) - p[len + n] = (byte)0; + if ((id < 256) || ! is->current_config.remove_city_improvement_limit) + return City_Improvements_set (this, __, id, add_else_remove); + else { + byte * extra_bits = (byte *)itable_look_up_or (&is->extra_city_improvs, (int)this, 0); + int extra_id = id - 256; + byte mask = 1 << (extra_id & 7); + if (add_else_remove) { + if (! extra_bits) { + int extra_bits_size = (not_below (0, p_bic_data->ImprovementsCount - 256) >> 3) + 1; + extra_bits = calloc (1, extra_bits_size); + itable_insert (&is->extra_city_improvs, (int)this, (int)extra_bits); + } + extra_bits[extra_id>>3] |= mask; + } else if ((! add_else_remove) && (extra_bits != NULL)) + extra_bits[extra_id>>3] &= ~mask; } } -void * __fastcall -patch_MappedFile_open_to_load_game (MappedFile * this, int edx, char * file_name, int sequential_access) +bool __fastcall +patch_Leader_has_tech_to_stop_disease (Leader * this, int edx, int id) { - void * tr = MappedFile_open (this, __, file_name, sequential_access); - if (tr != NULL) - is->accessing_save_file = this; - return tr; + if (! is->current_config.patch_disease_stopping_tech_flag_bug) + return Leader_has_tech (this, __, id); + else + return Leader_has_tech_with_flag (this, __, ATF_Disabled_Deseases_From_Flood_Plains); } -void * __fastcall -patch_MappedFile_create_file_to_save_game (MappedFile * this, int edx, LPCSTR file_path, unsigned file_size, int is_shared) +void __fastcall +patch_set_worker_animation (void * this, int edx, Unit * unit, int job_id) { - // Determine if we're currently applying settler perfume to any AI player - bool any_current_settler_perfume = false; - if (is->current_config.ai_settler_perfume_on_founding != 0) { - int duration = is->current_config.ai_settler_perfume_on_founding_duration; - for (int n = 0; n < 32; n++) { - int last_founding_turn = is->turn_no_of_last_founding_for_settler_perfume[n]; - if ((last_founding_turn != -1) && ((*p_current_turn_no - last_founding_turn) < duration)) - any_current_settler_perfume = true; - } + AnimationType anim_type; + + // If districts disabled or unit is null or job is not building mines, use base logic + if ((! is->current_config.enable_districts) || + (unit == NULL) || + (job_id != WJ_Build_Mines)) { + set_worker_animation(this, __, unit, job_id); + return; + } + + // If tile has a district under construction + Tile * tile = tile_at (unit->Body.X, unit->Body.Y); + struct district_instance * inst = get_district_instance (tile); + if (inst != NULL && + ! district_is_complete (tile, inst->district_id) && job_id == WJ_Build_Mines) { + + // Override and ensure build animation is used + job_id = AT_BUILD; } + + set_worker_animation(this, __, unit, job_id); +} - // Assemble mod save data - struct buffer mod_data = {0}; { - if (is->extra_defensive_bombards.len > 0) { - serialize_aligned_text ("extra_defensive_bombards", &mod_data); - itable_serialize (&is->extra_defensive_bombards, &mod_data); - } - if (is->airdrops_this_turn.len > 0) { - serialize_aligned_text ("airdrops_this_turn", &mod_data); - itable_serialize (&is->airdrops_this_turn, &mod_data); - } - if (is->unit_transport_ties.len > 0) { - serialize_aligned_text ("unit_transport_ties", &mod_data); - itable_serialize (&is->unit_transport_ties, &mod_data); - } - if (is->current_config.unit_cycle_search_criteria != UCSC_STANDARD && is->waiting_units.len > 0) { - serialize_aligned_text ("waiting_units", &mod_data); - itable_serialize (&is->waiting_units, &mod_data); - } - if ((p_bic_data->ImprovementsCount > 256) && (p_cities->Cities != NULL) && (is->extra_city_improvs.len > 0)) { - serialize_aligned_text ("extra_city_improvs", &mod_data); - int extra_improv_count = p_bic_data->ImprovementsCount - 256; - *(int *)buffer_allocate (&mod_data, sizeof(int)) = extra_improv_count; +void __fastcall +patch_Unit_work_simple_job (Unit * this, int edx, int job_id) +{ + is->lmify_tile_after_working_simple_job = NULL; - int count_entries = 0; { - for (int n = 0; n <= p_cities->LastIndex; n++) { - City_Body * body = p_cities->Cities[n].City; - if ((body != NULL) && ((int)body != offsetof (City, Body))) { - int unused; - if (itable_look_up (&is->extra_city_improvs, (int)&body->Improvements_1, &unused) || - itable_look_up (&is->extra_city_improvs, (int)&body->Improvements_2, &unused)) - count_entries++; - } - } - } - *(int *)buffer_allocate (&mod_data, sizeof(int)) = count_entries; + // Check if districts are enabled + if (is->current_config.enable_districts) { + int tile_x = this->Body.X; + int tile_y = this->Body.Y; + Tile * tile = tile_at (tile_x, tile_y); - int ints_per_list = (extra_improv_count + 31) / 32; - int bytes_per_list = (extra_improv_count + 7) / 8; - for (int n = 0; n <= p_cities->LastIndex; n++) { - City_Body * body = p_cities->Cities[n].City; - if ((body != NULL) && ((int)body != offsetof (City, Body))) { - byte * extra_bit_lists[2]; - extra_bit_lists[0] = (byte *)itable_look_up_or (&is->extra_city_improvs, (int)&body->Improvements_1, 0); - extra_bit_lists[1] = (byte *)itable_look_up_or (&is->extra_city_improvs, (int)&body->Improvements_2, 0); - if ((extra_bit_lists[0] != NULL) || (extra_bit_lists[1] != NULL)) { - *(int *)buffer_allocate (&mod_data, sizeof(int)) = body->ID; - for (int k = 0; k < 2; k++) { - int list_size = sizeof(int) * ints_per_list; - int * list = (int *)buffer_allocate (&mod_data, list_size); - memset (list, 0, list_size); - if (extra_bit_lists[k] != NULL) - memcpy (list, extra_bit_lists[k], bytes_per_list); - } - } - } - } - } - if (any_current_settler_perfume) { - serialize_aligned_text ("turn_no_of_last_founding_for_settler_perfume", &mod_data); - void * area = buffer_allocate (&mod_data, sizeof is->turn_no_of_last_founding_for_settler_perfume); - memcpy (area, is->turn_no_of_last_founding_for_settler_perfume, sizeof is->turn_no_of_last_founding_for_settler_perfume); - } - if (is->current_config.day_night_cycle_mode != DNCM_OFF) { - serialize_aligned_text ("current_day_night_cycle", &mod_data); - int_to_bytes (buffer_allocate (&mod_data, sizeof is->current_day_night_cycle), is->current_day_night_cycle); - } + if (tile != NULL && tile != p_null_tile) { + // Check if there's a completed district on this tile + struct district_instance * inst = get_district_instance (tile); + if (inst != NULL && district_is_complete(tile, inst->district_id)) { + int district_id = inst->district_id; + bool is_human = (*p_human_player_bits & (1 << this->Body.CivID)) != 0; - if (is->current_config.enable_districts && (is->district_count > 0)) { - serialize_aligned_text ("district_config_names", &mod_data); - int * entry_count = (int *)buffer_allocate (&mod_data, sizeof(int)); - *entry_count = is->district_count; - for (int district_id = 0; district_id < is->district_count; district_id++) { - *(int *)buffer_allocate (&mod_data, sizeof(int)) = district_id; - char const * name = is->district_configs[district_id].name; - if (name == NULL) - name = ""; - serialize_aligned_text (name, &mod_data); - } - } + // AI players only (human removal is handled via issue_district_worker_command) + if (!is_human) { + bool allow_removal = false; + if (district_id == WONDER_DISTRICT_ID) { + struct wonder_district_info * info = &inst->wonder_info; + allow_removal = (info->state == WDS_UNUSED); + } - if (is->current_config.enable_districts) { - int entry_count = 0; - for (int civ_id = 0; civ_id < 32; civ_id++) { - FOR_TABLE_ENTRIES (tei, &is->city_pending_district_requests[civ_id]) { - struct pending_district_request * req = (struct pending_district_request *)(long)tei.value; - if ((req != NULL) && (req->city_id >= 0)) - entry_count++; - } - } - if (entry_count > 0) { - serialize_aligned_text ("district_pending_requests", &mod_data); - int * chunk = (int *)buffer_allocate (&mod_data, sizeof(int) * (1 + 5 * entry_count)); - int * out = chunk + 1; - chunk[0] = entry_count; - for (int civ_id = 0; civ_id < 32; civ_id++) { - FOR_TABLE_ENTRIES (tei, &is->city_pending_district_requests[civ_id]) { - struct pending_district_request * req = (struct pending_district_request *)(long)tei.value; - if ((req == NULL) || (req->city_id < 0)) - continue; - out[0] = req->city_id; - out[1] = req->district_id; - out[2] = req->assigned_worker_id; - out[3] = req->target_x; - out[4] = req->target_y; - out += 5; + if (allow_removal) { + remove_district_instance (tile); + tile->vtable->m62_Set_Tile_BuildingID (tile, __, -1); + tile->vtable->m51_Unset_Tile_Flags (tile, __, 0, TILE_FLAG_MINE, tile_x, tile_y); + handle_district_removed (tile, district_id, tile_x, tile_y, false); + } } } } } - if (is->current_config.enable_districts && - (is->city_pending_building_orders.len > 0)) { - int entry_count = 0; - FOR_TABLE_ENTRIES (tei, &is->city_pending_building_orders) { - City * city = (City *)(long)tei.key; - int improv_id = tei.value; - if ((city != NULL) && (improv_id >= 0)) - entry_count++; - } - if (entry_count > 0) { - serialize_aligned_text ("building_pending_orders", &mod_data); - int * chunk = (int *)buffer_allocate (&mod_data, sizeof(int) * (1 + 2 * entry_count)); - int * out = chunk + 1; - chunk[0] = entry_count; - FOR_TABLE_ENTRIES (tei, &is->city_pending_building_orders) { - City * city = (City *)(long)tei.key; - int improv_id = tei.value; - if ((city == NULL) || (improv_id < 0)) - continue; - out[0] = city->Body.ID; - out[1] = improv_id; - out += 2; - } - } - } + Unit_work_simple_job (this, __, job_id); - if (is->current_config.enable_districts && (is->district_tile_map.len > 0)) { - serialize_aligned_text ("district_tile_map", &mod_data); - int entry_capacity = is->district_tile_map.len; - int * chunk = (int *)buffer_allocate (&mod_data, sizeof(int) * (1 + 7 * entry_capacity)); - int * out = chunk + 1; - int written = 0; - FOR_TABLE_ENTRIES (tei, &is->district_tile_map) { - Tile * tile = (Tile *)tei.key; - struct district_instance * inst = (struct district_instance *)(long)tei.value; - if (inst == NULL) - continue; - int x, y; - if (! district_instance_get_coords (inst, tile, &x, &y)) - continue; - int wonder_city_id = inst->wonder_info.city_id; - if (wonder_city_id >= 0) { - City * info_city = get_city_ptr (wonder_city_id); - inst->wonder_info.city = info_city; - if (info_city == NULL) - wonder_city_id = -1; - } else - inst->wonder_info.city = NULL; - out[0] = x; - out[1] = y; - out[2] = inst->district_type; - out[3] = (int)inst->state; - out[4] = (int)inst->wonder_info.state; - out[5] = wonder_city_id; - out[6] = inst->wonder_info.wonder_index; - out += 7; - written++; - } - chunk[0] = written; - int unused_entries = entry_capacity - written; - if (unused_entries > 0) { - int trimmed_bytes = unused_entries * 7 * (int)sizeof(int); - mod_data.length -= trimmed_bytes; - } - } + if (is->lmify_tile_after_working_simple_job != NULL) + is->lmify_tile_after_working_simple_job->vtable->m31_set_landmark (is->lmify_tile_after_working_simple_job, __, true); +} - if (is->current_config.enable_natural_wonders && (is->district_tile_map.len > 0)) { - int entry_capacity = 0; - FOR_TABLE_ENTRIES (tei, &is->district_tile_map) { - struct district_instance * inst = (struct district_instance *)(long)tei.value; - if ((inst == NULL) || (inst->district_type != NATURAL_WONDER_DISTRICT_ID)) - continue; - if (inst->natural_wonder_info.natural_wonder_id < 0) - continue; - entry_capacity++; - } - if (entry_capacity > 0) { - serialize_aligned_text ("natural_wonder_districts", &mod_data); - int * chunk = (int *)buffer_allocate (&mod_data, sizeof(int) * (1 + 3 * entry_capacity)); - int * out = chunk + 1; - int written = 0; - FOR_TABLE_ENTRIES (tei, &is->district_tile_map) { - Tile * tile = (Tile *)tei.key; - struct district_instance * inst = (struct district_instance *)(long)tei.value; - if ((inst == NULL) || - (inst->district_type != NATURAL_WONDER_DISTRICT_ID) || - (inst->natural_wonder_info.natural_wonder_id < 0)) - continue; - int x, y; - if (! district_instance_get_coords (inst, tile, &x, &y)) - continue; - out[0] = x; - out[1] = y; - out[2] = inst->natural_wonder_info.natural_wonder_id; - out += 3; - written++; - } - chunk[0] = written; - int unused_entries = entry_capacity - written; - if (unused_entries > 0) { - int trimmed_bytes = unused_entries * 3 * (int)sizeof(int); - mod_data.length -= trimmed_bytes; - } - } - } +void __fastcall +patch_Map_change_tile_terrain_by_worker (Map * this, int edx, enum SquareTypes new_terrain_type, int x, int y) +{ + Map_change_tile_terrain (this, __, new_terrain_type, x, y); - if (is->current_config.enable_districts && - is->current_config.enable_distribution_hub_districts && - (is->distribution_hub_records.len > 0)) { - serialize_aligned_text ("distribution_hub_records", &mod_data); - int entry_capacity = is->distribution_hub_records.len; - int * chunk = (int *)buffer_allocate (&mod_data, sizeof(int) * (1 + 3 * entry_capacity)); - int * out = chunk + 1; - int written = 0; - FOR_TABLE_ENTRIES (tei, &is->distribution_hub_records) { - struct distribution_hub_record * rec = (struct distribution_hub_record *)(long)tei.value; - if (rec == NULL) - continue; - out[0] = rec->tile_x; - out[1] = rec->tile_y; - out[2] = rec->civ_id; - out += 3; - written++; - } - chunk[0] = written; - int unused_entries = entry_capacity - written; - if (unused_entries > 0) { - int trimmed_bytes = unused_entries * 3 * (int)sizeof(int); - mod_data.length -= trimmed_bytes; - } - } + if (is->current_config.convert_to_landmark_after_planting_forest && (new_terrain_type == SQ_Forest)) + is->lmify_tile_after_working_simple_job = tile_at (x, y); +} - if (is->current_config.enable_districts && - is->current_config.enable_aerodrome_districts && - is->current_config.air_units_use_aerodrome_districts_not_cities && - (is->aerodrome_airlift_usage.len > 0)) { - serialize_aligned_text ("aerodrome_airlift_usage", &mod_data); - int entry_capacity = is->aerodrome_airlift_usage.len; - int * chunk = (int *)buffer_allocate (&mod_data, sizeof(int) * (1 + 3 * entry_capacity)); - int * out = chunk + 1; - int written = 0; - FOR_TABLE_ENTRIES (tei, &is->aerodrome_airlift_usage) { - Tile * tile = (Tile *)tei.key; - int mask = tei.value; - if ((tile == NULL) || (tile == p_null_tile)) - continue; +int __fastcall +patch_Leader_ai_eval_technology (Leader * this, int edx, int id, bool param_2, bool param_3) +{ + int base = Leader_ai_eval_technology (this, __, id, param_2, param_3); + return apply_perfume (PK_TECHNOLOGY, p_bic_data->Advances[id].Name, base); +} - int tile_x, tile_y; - if (! tile_coords_from_ptr (&p_bic_data->Map, tile, &tile_x, &tile_y)) - continue; +int __fastcall +patch_Leader_ai_eval_government (Leader * this, int edx, int id) +{ + int base = Leader_ai_eval_government (this, __, id); + return apply_perfume (PK_GOVERNMENT, p_bic_data->Governments[id].Name.S, base); +} - out[0] = tile_x; - out[1] = tile_y; - out[2] = mask; - out += 3; - written++; - } - chunk[0] = written; - int unused_entries = entry_capacity - written; - if (unused_entries > 0) { - int trimmed_bytes = unused_entries * 3 * (int)sizeof(int); - mod_data.length -= trimmed_bytes; +bool +roll_to_spare_unit_from_nuke (Unit * unit) +{ + int one_hp_destroy_chance = is->current_config.chance_for_nukes_to_destroy_max_one_hp_units; + UnitType * type = &p_bic_data->UnitTypes[unit->Body.UnitTypeID]; + if ((one_hp_destroy_chance < 100) && + (Unit_get_max_hp (unit) <= 1) && + (type->Defence > 0) && + ((type->Unit_Class == UTC_Land) || (type->Unit_Class == UTC_Sea))) + return ! (rand_int (p_rand_object, __, 100) < one_hp_destroy_chance); + else + return false; +} + +void __fastcall +patch_Unit_despawn_after_killed_by_nuke (Unit * this, int edx, int civ_id_responsible, byte param_2, byte param_3, byte param_4, byte param_5, byte param_6, byte param_7) +{ + if (roll_to_spare_unit_from_nuke (this)) + this->Body.Damage = Unit_get_max_hp (this) - 1; + else { + bool prev_always_despawn_passengers = is->always_despawn_passengers; + if ((is->current_config.land_transport_rules & LTR_NO_ESCAPE) && is_land_transport (this)) + is->always_despawn_passengers = true; + else if ((is->current_config.special_helicopter_rules & SHR_NO_ESCAPE) && p_bic_data->UnitTypes[this->Body.UnitTypeID].Unit_Class == UTC_Air && + p_bic_data->UnitTypes[this->Body.UnitTypeID].Transport_Capacity > 0) + is->always_despawn_passengers = true; + patch_Unit_despawn (this, __, civ_id_responsible, param_2, param_3, param_4, param_5, param_6, param_7); + is->always_despawn_passengers = prev_always_despawn_passengers; + } +} + +void __fastcall +patch_mp_despawn_after_killed_by_nuke (void * this, int edx, int unit_id, int civ_id_responsible, byte param_3, byte param_4, byte param_5, byte param_6) +{ + Unit * unit = get_unit_ptr (unit_id); + if ((unit != NULL) && roll_to_spare_unit_from_nuke (unit)) + unit->Body.Damage = Unit_get_max_hp (unit) - 1; + else + mp_despawn (this, __, unit_id, civ_id_responsible, param_3, param_4, param_5, param_6); +} + +bool __fastcall +patch_City_has_unprotected_improv_to_sell (City * this, int edx, int id) +{ + // Fall back to original logic only if the config doesn't alter the behavior. + if (! is->current_config.allow_sale_of_aqueducts_and_hospitals && !is->current_config.allow_sale_of_small_wonders) + return City_has_unprotected_improv (this, __, id); + + else if (patch_City_has_improvement (this, __, id, false)) { + Improvement * improv = &p_bic_data->Improvements[id]; + int max_pop_to_sell = INT_MAX; // By default, population-based sell isn't restricted + if (improv->ImprovementFlags & (ITF_Allows_City_Level_2 | ITF_Allows_City_Level_3)) { // Aqueduct/Hospital? + if (is->current_config.allow_sale_of_aqueducts_and_hospitals) { + if (improv->ImprovementFlags & ITF_Allows_City_Level_2) + max_pop_to_sell = p_bic_data->General.MaximumSize_Town; + else if (improv->ImprovementFlags & ITF_Allows_City_Level_3) + max_pop_to_sell = p_bic_data->General.MaximumSize_City; + } else { + // Do not allow selling these. + max_pop_to_sell = 0; } } - } - int metadata_size = (mod_data.length > 0) ? 12 : 0; // Two four-byte bookends plus one four-byte size, only written if there's any mod data + // Can't sell: + // - Great Wonders + // - Small Wonders, unless the config allows it + // - Capital + // - Aqueduct/Hospital if the city is too big for that population + return ((improv->Characteristics & ITC_Wonder) == 0) && + (is->current_config.allow_sale_of_small_wonders || ((improv->Characteristics & ITC_Small_Wonder) == 0)) && + ((improv->ImprovementFlags & ITF_Center_of_Empire) == 0) && + (this->Body.Population.Size <= max_pop_to_sell); - void * tr = MappedFile_create_file (this, __, file_path, file_size + mod_data.length + metadata_size, is_shared); - if (tr != NULL) { - is->accessing_save_file = this; - if ((mod_data.length > 0) && (mod_data.length + metadata_size <= this->size)) { - // Write first bookend to mod's segment in the save data - byte * seg_start = (byte *)tr + file_size; - for (int n = 0; n < ARRAY_LEN (c3x_save_segment_bookend); n++) - seg_start[n] = c3x_save_segment_bookend[n]; + } else + return false; +} - // Write actual mod game data - memcpy ((byte *)tr + file_size + 4, mod_data.contents, mod_data.length); +bool __fastcall +patch_UnitType_has_detector_ability_for_vis_check (UnitType * this, int edx, enum UnitTypeAbilities a) +{ + bool tr = UnitType_has_ability (this, __, a); - // Write size of mod data - byte * seg_end = (byte *)tr + file_size + 4 + mod_data.length; - int_to_bytes (seg_end, mod_data.length); + // Restrict detection by sea units to other sea units and non-sea units to other non-sea units + if (tr && + is->current_config.no_cross_shore_detection && + (is->checking_visibility_for_unit != NULL) && + ((this->Unit_Class == UTC_Sea) ^ (p_bic_data->UnitTypes[is->checking_visibility_for_unit->Body.UnitTypeID].Unit_Class == UTC_Sea))) + tr = false; - // Finish off with another bookend - for (int n = 0; n < ARRAY_LEN (c3x_save_segment_bookend); n++) - seg_end[4+n] = c3x_save_segment_bookend[n]; - } - } - buffer_deinit (&mod_data); return tr; } bool -match_save_chunk_name (byte ** cursor, char const * name) +is_airdrop_trespassing (Unit * unit, int target_x, int target_y) { - if (strcmp (name, *cursor) == 0) { - // Move cursor past the string if it matched. Also move past any padding that was added to ensure alignment (see serialize_aligned_text). - *cursor = (byte *)((int)*cursor + strlen (name) + 4 & ~3); - return true; + if (is->current_config.disallow_trespassing && + check_trespassing (unit->Body.CivID, tile_at (unit->Body.X, unit->Body.Y), tile_at (target_x, target_y))) { + bool allowed = is_allowed_to_trespass (unit); + + // If "unit" is an air unit that can carry others, like helicopters, then this airdrop is only allowed if all of its passengers are + // allowed to trespass. + UnitType * type = &p_bic_data->UnitTypes[unit->Body.UnitTypeID]; + if (allowed && (type->Unit_Class == UTC_Air) && (type->Transport_Capacity > 0)) + FOR_UNITS_ON (uti, tile_at (unit->Body.X, unit->Body.Y)) + if ((uti.unit->Body.Container_Unit == unit->Body.ID) && + (! is_allowed_to_trespass (uti.unit))) { + allowed = false; + break; + } + + return ! allowed; } else return false; } -bool -match_save_segment_bookend (byte * b) +bool __fastcall +patch_Unit_check_airdrop_target (Unit * this, int edx, int tile_x, int tile_y) { - return memcmp (c3x_save_segment_bookend, b, ARRAY_LEN (c3x_save_segment_bookend)) == 0; + return Unit_check_airdrop_target (this, __, tile_x, tile_y) && + is_below_stack_limit (tile_at (tile_x, tile_y), this->Body.CivID, this->Body.UnitTypeID) && + ! is_airdrop_trespassing (this, tile_x, tile_y); } -int __cdecl -patch_move_game_data (byte * buffer, bool save_else_load) +bool __fastcall +patch_Unit_can_airlift (Unit * this) { - int tr = move_game_data (buffer, save_else_load); + bool base = Unit_can_airlift (this); - if (! save_else_load) { - // Free all district_instance structs first - FOR_TABLE_ENTRIES (tei, &is->district_tile_map) { - struct district_instance * inst = (struct district_instance *)(long)tei.value; - if (inst != NULL) - free (inst); + if (! (is->current_config.enable_districts && + is->current_config.enable_aerodrome_districts && + is->current_config.air_units_use_aerodrome_districts_not_cities)) + return base; + + Tile * tile = tile_at (this->Body.X, this->Body.Y); + if ((tile == NULL) || (tile == p_null_tile)) + return base; + + bool allow_from_non_city = false; + if (base) { + City * city = city_at (this->Body.X, this->Body.Y); + if ((city == NULL) || (city->Body.CivID != this->Body.CivID)) + allow_from_non_city = true; + } + + if (allow_from_non_city) + return true; + + return tile_has_friendly_aerodrome_district (tile, this->Body.CivID, true); +} + +bool __fastcall +patch_Unit_check_airlift_target (Unit * this, int edx, int tile_x, int tile_y) +{ + bool base = Unit_check_airlift_target (this, __, tile_x, tile_y); + bool allowed = base; + + Tile * tile = tile_at (tile_x, tile_y); + + if (is->current_config.enable_districts && + is->current_config.enable_aerodrome_districts && + is->current_config.air_units_use_aerodrome_districts_not_cities) { + if ((tile == NULL) || (tile == p_null_tile)) { + allowed = false; + } else { + City * target_city = city_at (tile_x, tile_y); + if (allowed && + (target_city != NULL) && + (target_city->Body.CivID == this->Body.CivID)) + allowed = false; + + if (! allowed) + allowed = tile_has_friendly_aerodrome_district (tile, this->Body.CivID, false); } - table_deinit (&is->district_tile_map); - clear_all_tracked_workers (); } - // Check for a mod save data section and load it if present - MappedFile * save; - int seg_size; - byte * seg; - if ((! save_else_load) && - ((save = is->accessing_save_file) != NULL) && - (save->size >= 8) && - match_save_segment_bookend ((byte *)((int)save->base_addr + save->size - 4)) && - ((seg_size = int_from_bytes ((byte *)((int)save->base_addr + save->size - 8))) > 0) && - (save->size >= seg_size + 12) && - match_save_segment_bookend ((byte *)((int)save->base_addr + save->size - seg_size - 12)) && - ((seg = malloc (seg_size)) != NULL)) { - memcpy (seg, (void *)((int)save->base_addr + save->size - seg_size - 8), seg_size); + if (! allowed) + return false; - byte * cursor = seg; - char * error_chunk_name = NULL; - while (cursor < seg + seg_size) { - if (match_save_chunk_name (&cursor, "special save message")) { - char * msg = (char *)cursor; - cursor = (byte *)((int)cursor + strlen (msg) + 4 & ~3); + return is_below_stack_limit (tile, this->Body.CivID, this->Body.UnitTypeID); +} - PopupForm * popup = get_popup_form (); - popup->vtable->set_text_key_and_flags (popup, __, is->mod_script_path, "C3X_INFO", -1, 0, 0, 0); - PopupForm_add_text (popup, __, "This save contains a special message:", 0); - PopupForm_add_text (popup, __, msg, 0); - patch_show_popup (popup, __, 0, 0); +void __fastcall +patch_Unit_airlift (Unit * this, int edx, int tile_x, int tile_y) +{ + Tile * source_tile = NULL; + bool mark_usage = false; - } else if (match_save_chunk_name (&cursor, "extra_defensive_bombards")) { - int bytes_read = itable_deserialize (cursor, seg + seg_size, &is->extra_defensive_bombards); - if (bytes_read > 0) - cursor += bytes_read; - else { - error_chunk_name = "extra_defensive_bombards"; - break; - } + if (is->current_config.enable_districts && + is->current_config.enable_aerodrome_districts && + is->current_config.air_units_use_aerodrome_districts_not_cities) { + source_tile = tile_at (this->Body.X, this->Body.Y); + if (tile_has_friendly_aerodrome_district (source_tile, this->Body.CivID, true)) + mark_usage = true; + } - } else if (match_save_chunk_name (&cursor, "airdrops_this_turn")) { - int bytes_read = itable_deserialize (cursor, seg + seg_size, &is->airdrops_this_turn); - if (bytes_read > 0) - cursor += bytes_read; - else { - error_chunk_name = "airdrops_this_turn"; - break; - } + Unit_airlift (this, __, tile_x, tile_y); - } else if (match_save_chunk_name (&cursor, "unit_transport_ties")) { - int bytes_read = itable_deserialize (cursor, seg + seg_size, &is->unit_transport_ties); - if (bytes_read > 0) - cursor += bytes_read; - else { - error_chunk_name = "unit_transport_ties"; - break; - } + if (mark_usage && (source_tile != NULL) && (source_tile != p_null_tile)) { + int mask = itable_look_up_or (&is->aerodrome_airlift_usage, (int)source_tile, 0); + mask |= (1 << this->Body.CivID); + itable_insert (&is->aerodrome_airlift_usage, (int)source_tile, mask); + } +} - } else if (match_save_chunk_name (&cursor, "waiting_units")) { - int bytes_read = itable_deserialize (cursor, seg + seg_size, &is->waiting_units); - if (bytes_read > 0) { - cursor += bytes_read; - is->have_loaded_waiting_units = true; - } else { - error_chunk_name = "waiting_units"; - break; - } +int __fastcall +patch_City_count_airports_for_ai_airlift_target (City * this, int edx, enum ImprovementTypeFlags airport_flag) +{ + // When this function is called, the AI unit being moved is stored in register ESI. + Unit * unit; + __asm__ __volatile__("mov %%esi, %0" : "=r" (unit)); - } else if (match_save_chunk_name (&cursor, "extra_city_improvs")) { - bool success = false; - int remaining_bytes = (seg + seg_size) - cursor; + int tr = City_count_improvements_with_flag (this, __, airport_flag); - // Read two int vars from this save chunk - int file_extra_improv_count, count_entries; - if (remaining_bytes >= 8) { - file_extra_improv_count = *((int *)cursor)++; - count_entries = *((int *)cursor)++; - remaining_bytes -= 8; - } else - goto done_with_extra_city_improvs; + // Check the stack limit here. If the city's tile is at the limit, return that it has no airport so the AI can't airlift there. + if ((tr > 0) && ! is_below_stack_limit (tile_at (this->Body.X, this->Body.Y), this->Body.CivID, unit->Body.UnitTypeID)) + return 0; - // Check that the extra improv counts are valid. They must be greater than zero and what was stored in the save must - // match what we got from the current scenario data. - int extra_improv_count = not_below (0, p_bic_data->ImprovementsCount - 256); - if ((file_extra_improv_count <= 0) || (extra_improv_count != file_extra_improv_count)) - goto done_with_extra_city_improvs; + else + return tr; +} - // The extra bits data in the save is stored in 32-bit chunks, "ints", to maintain alignment. Compute how many ints we - // need for each list of bits and check that reading all entries won't overrun the buffer. - int ints_per_list = (extra_improv_count + 31) / 32, - ints_per_entry = 1 + 2 * ints_per_list; - if (count_entries * ints_per_entry * sizeof(int) > remaining_bytes) - goto done_with_extra_city_improvs; +int __fastcall +patch_Unit_ai_eval_airdrop_target (Unit * this, int edx, int tile_x, int tile_y) +{ + int tr = Unit_ai_eval_airdrop_target (this, __, tile_x, tile_y); - // Main loop reading the extra bits data - for (int n = 0; n < count_entries; n++) { - City * city = get_city_ptr (*((int *)cursor)++); - if (city == NULL) - goto done_with_extra_city_improvs; + // Prevent the AI from airdropping onto tiles in violation of the stack limit or trespassing restriction + if ((tr > 0) && + ((! is_below_stack_limit (tile_at (tile_x, tile_y), this->Body.CivID, this->Body.UnitTypeID)) || + is_airdrop_trespassing (this, tile_x, tile_y))) + tr = 0; - byte * extra_bits_1 = calloc (sizeof (int), ints_per_list); - for (int k = 0; k < ints_per_list; k++) - ((int *)extra_bits_1)[k] = *((int *)cursor)++; - itable_insert (&is->extra_city_improvs, (int)&city->Body.Improvements_1, (int)extra_bits_1); + return tr; +} - byte * extra_bits_2 = calloc (sizeof (int), ints_per_list); - for (int k = 0; k < ints_per_list; k++) - ((int *)extra_bits_2)[k] = *((int *)cursor)++; - itable_insert (&is->extra_city_improvs, (int)&city->Body.Improvements_2, (int)extra_bits_2); - } +bool __fastcall +patch_Unit_find_telepad_on_tile (Unit * this, int edx, int x, int y, bool show_selection_popup, Unit ** out_unit_telepad) +{ + if (! is_below_stack_limit (tile_at (x, y), this->Body.CivID, this->Body.UnitTypeID)) { + *out_unit_telepad = NULL; + return false; + } else + return Unit_find_telepad_on_tile (this, __, x, y, show_selection_popup, out_unit_telepad); +} - // Rebuild the trade network since it may have changed as a result of the additional buildings. This method also - // refreshes the free improvement tables and recomputes city happiness. - patch_Map_build_trade_network (&p_bic_data->Map); +bool __fastcall +patch_Unit_ai_go_to_capital (Unit * this) +{ + City * capital = get_city_ptr (leaders[this->Body.CivID].CapitalID); - success = true; + // Block going to capital if the capital's tile is at the stack limit. This stops the AI from airlifting there (would violate the limit) and + // saves it from trying to pathfind there. + if ((capital != NULL) && + ! is_below_stack_limit (tile_at (capital->Body.X, capital->Body.Y), this->Body.CivID, this->Body.UnitTypeID)) + return false; - done_with_extra_city_improvs: - if (! success) { - error_chunk_name = "extra_city_improvs";; - break; - } + return Unit_ai_go_to_capital (this); +} - } else if (match_save_chunk_name (&cursor, "turn_no_of_last_founding_for_settler_perfume")) { - for (int n = 0; n < 32; n++) - is->turn_no_of_last_founding_for_settler_perfume[n] = *((int *)cursor)++; +bool __fastcall +patch_Unit_is_in_rebase_range (Unit * this, int edx, int tile_x, int tile_y) +{ + bool in_range; - } else if (match_save_chunk_name (&cursor, "current_day_night_cycle")) { - is->current_day_night_cycle = *((int *)cursor)++; - is->day_night_cycle_unstarted = false; + // 6 is the game's standard value, so fall back on the base game logic in that case + if (is->current_config.rebase_range_multiplier == 6) + in_range = Unit_is_in_rebase_range (this, __, tile_x, tile_y); - // The day/night cycle sprite proxies will have been cleared in patch_load_scenario. They will not necessarily be set - // up again in the usual way because Map_Renderer::load_images is not necessarily called when loading a save. The game - // skips reloading all graphics when loading a save while in-game with another that uses the same graphics (possibly - // only the standard graphics; I didn't test). If day/night cycle mode is active, restore the proxies now if they - // haven't already been. - if ((is->day_night_cycle_img_state == IS_OK) && ! is->day_night_cycle_img_proxies_indexed) - build_sprite_proxies_24 (&p_bic_data->Map.Renderer); + else { + int op_range = p_bic_data->UnitTypes[this->Body.UnitTypeID].OperationalRange; + if (op_range < 1) + op_range = 500; - // Because we've restored current_day_night_cycle from the save, set that is is not the first turn so the cycle - // doesn't get restarted. - is->day_night_cycle_unstarted = false; - - } else if (match_save_chunk_name (&cursor, "district_pending_requests")) { - bool success = false; - int remaining_bytes = (seg + seg_size) - cursor; - if (remaining_bytes >= (int)sizeof(int)) { - int * ints = (int *)cursor; - int entry_count = *ints++; - cursor = (byte *)ints; - remaining_bytes -= (int)sizeof(int); - if ((entry_count >= 0) && - (remaining_bytes >= entry_count * 5 * (int)sizeof(int))) { - for (int civ_id = 0; civ_id < 32; civ_id++) { - FOR_TABLE_ENTRIES (tei, &is->city_pending_district_requests[civ_id]) { - struct pending_district_request * req = (struct pending_district_request *)(long)tei.value; - if (req != NULL) - free (req); - } - table_deinit (&is->city_pending_district_requests[civ_id]); - } - success = true; - for (int n = 0; n < entry_count; n++) { - if (remaining_bytes < 5 * (int)sizeof(int)) { - success = false; - break; - } - int city_id = *ints++; - int district_id = *ints++; - int assigned_worker_id = *ints++; - int target_x = *ints++; - int target_y = *ints++; - cursor = (byte *)ints; - remaining_bytes -= 5 * (int)sizeof(int); - City * city = get_city_ptr (city_id); - if ((city == NULL) || (district_id < 0) || (district_id >= is->district_count)) - continue; - struct pending_district_request * req = create_pending_district_request (city, district_id); - if (req == NULL) - continue; - if ((assigned_worker_id >= 0) && (get_unit_ptr (assigned_worker_id) == NULL)) - assigned_worker_id = -1; - req->assigned_worker_id = assigned_worker_id; - req->target_x = target_x; - req->target_y = target_y; - } - if (! success) { - for (int civ_id = 0; civ_id < 32; civ_id++) - table_deinit (&is->city_pending_district_requests[civ_id]); - } - } - } - if (! success) { - error_chunk_name = "district_pending_requests"; - break; - } - } else if (match_save_chunk_name (&cursor, "building_pending_orders")) { - bool success = false; - int remaining_bytes = (seg + seg_size) - cursor; - if (remaining_bytes >= (int)sizeof(int)) { - int * ints = (int *)cursor; - int entry_count = *ints++; - cursor = (byte *)ints; - remaining_bytes -= (int)sizeof(int); - if ((entry_count >= 0) && - (remaining_bytes >= entry_count * 2 * (int)sizeof(int))) { - table_deinit (&is->city_pending_building_orders); - success = true; - for (int n = 0; n < entry_count; n++) { - if (remaining_bytes < 2 * (int)sizeof(int)) { - success = false; - break; - } - int city_id = *ints++; - int improv_id = *ints++; - cursor = (byte *)ints; - remaining_bytes -= 2 * (int)sizeof(int); - if (improv_id < 0) - continue; - City * city = get_city_ptr (city_id); - if (city == NULL) - continue; - itable_insert (&is->city_pending_building_orders, (int)(long)city, improv_id); - } - if (! success) - table_deinit (&is->city_pending_building_orders); - } - } - if (! success) { - error_chunk_name = "building_pending_orders"; - break; - } - } else if (match_save_chunk_name (&cursor, "district_tile_map")) { - bool success = false; - int remaining_bytes = (seg + seg_size) - cursor; - if (remaining_bytes >= (int)sizeof(int)) { - int * ints = (int *)cursor; - int entry_count = *ints++; - cursor = (byte *)ints; - remaining_bytes -= (int)sizeof(int); - if (entry_count >= 0) { - int ints_per_entry = 7; - int required_bytes = entry_count * ints_per_entry * (int)sizeof(int); - if (remaining_bytes >= required_bytes) { - FOR_TABLE_ENTRIES (tei, &is->district_tile_map) { - struct district_instance * inst = (struct district_instance *)(long)tei.value; - if (inst != NULL) - free (inst); - } - table_deinit (&is->district_tile_map); - success = true; - for (int n = 0; n < entry_count; n++) { - if (remaining_bytes < ints_per_entry * (int)sizeof(int)) { - success = false; - break; - } - int x = *ints++; - int y = *ints++; - int district_id = *ints++; - int state_val = *ints++; - int wonder_state = *ints++; - int wonder_city_id = *ints++; - int wonder_index = *ints++; - cursor = (byte *)ints; - remaining_bytes -= ints_per_entry * (int)sizeof(int); - if ((district_id < 0) || (district_id >= is->district_count)) - continue; - Tile * tile = tile_at (x, y); - if ((tile != NULL) && (tile != p_null_tile)) { - struct district_instance * inst = ensure_district_instance (tile, district_id, x, y); - if (inst != NULL) { - enum district_state new_state; - switch (state_val) { - case DS_COMPLETED: - new_state = DS_COMPLETED; - break; - case DS_UNDER_CONSTRUCTION: - new_state = DS_UNDER_CONSTRUCTION; - break; - default: - new_state = DS_UNDER_CONSTRUCTION; - break; - } - inst->state = new_state; - - inst->wonder_info.state = (enum wonder_district_state)wonder_state; - inst->wonder_info.city_id = (wonder_city_id >= 0) ? wonder_city_id : -1; - City * info_city = (wonder_city_id >= 0) ? get_city_ptr (wonder_city_id) : NULL; - inst->wonder_info.city = info_city; - if (info_city == NULL) - inst->wonder_info.city_id = -1; - inst->wonder_info.wonder_index = wonder_index; - set_tile_unworkable_for_all_cities (tile, x, y); - } - } - } - } - } - } - if (! success) { - error_chunk_name = "district_tile_map"; - break; - } - } else if (match_save_chunk_name (&cursor, "natural_wonder_districts")) { - bool success = false; - int remaining_bytes = (seg + seg_size) - cursor; - if (remaining_bytes >= (int)sizeof(int)) { - int * ints = (int *)cursor; - int entry_count = *ints++; - cursor = (byte *)ints; - remaining_bytes -= (int)sizeof(int); - if ((entry_count >= 0) && (remaining_bytes >= entry_count * 3 * (int)sizeof(int))) { - success = true; - for (int n = 0; n < entry_count; n++) { - if (remaining_bytes < 3 * (int)sizeof(int)) { - success = false; - break; - } - int x = *ints++; - int y = *ints++; - int natural_id = *ints++; - cursor = (byte *)ints; - remaining_bytes -= 3 * (int)sizeof(int); - if ((natural_id < 0) || (natural_id >= is->natural_wonder_count)) - continue; - Tile * tile = tile_at (x, y); - if ((tile == NULL) || (tile == p_null_tile)) - continue; - struct district_instance * inst = ensure_district_instance (tile, NATURAL_WONDER_DISTRICT_ID, x, y); - if (inst == NULL) - continue; - inst->district_type = NATURAL_WONDER_DISTRICT_ID; - inst->state = DS_COMPLETED; - inst->natural_wonder_info.natural_wonder_id = natural_id; - set_tile_unworkable_for_all_cities (tile, x, y); + int x_dist = Map_get_x_dist (&p_bic_data->Map, __, tile_x, this->Body.X), + y_dist = Map_get_y_dist (&p_bic_data->Map, __, tile_y, this->Body.Y); + + in_range = ((x_dist + y_dist) >> 1) <= (op_range * is->current_config.rebase_range_multiplier); + } + + return in_range && is_below_stack_limit (tile_at (tile_x, tile_y), this->Body.CivID, this->Body.UnitTypeID); +} + +bool __fastcall +patch_Unit_check_rebase_target (Unit * this, int edx, int tile_x, int tile_y) +{ + // Check if this is an air unit + bool is_air_unit = (p_bic_data->UnitTypes[this->Body.UnitTypeID].Unit_Class == UTC_Air); + + // If districts are enabled and this is an air unit, check for aerodrome districts + if (is_air_unit && is->current_config.enable_districts && + is->current_config.enable_aerodrome_districts) { + + Tile * tile = tile_at (tile_x, tile_y); + if ((tile != NULL) && (tile != p_null_tile)) { + // Check if tile has a district + struct district_instance * inst = get_district_instance (tile); + if (inst != NULL) { + int district_id = inst->district_id; + // Check if this is an aerodrome district owned by this unit's civ + if ((district_id == AERODROME_DISTRICT_ID) && (tile->Territory_OwnerID == this->Body.CivID)) { + // Check if aerodrome is complete + if (district_is_complete (tile, district_id)) { + // Perform range check + bool in_range = patch_Unit_is_in_rebase_range (this, __, tile_x, tile_y); + if (in_range) { + return is_below_stack_limit (tile, this->Body.CivID, this->Body.UnitTypeID); } } } - if (! success) { - error_chunk_name = "natural_wonder_districts"; - break; + } + + // If air_units_use_aerodrome_districts_not_cities is enabled, check if there's a city and disallow it + if (is->current_config.air_units_use_aerodrome_districts_not_cities) { + City * target_city = city_at (tile_x, tile_y); + if (target_city != NULL && target_city->Body.CivID == this->Body.CivID) { + // There's a friendly city here - disallow landing since configured to use aerodromes/airfields/carriers only + return false; } - } else if (match_save_chunk_name (&cursor, "distribution_hub_records")) { - bool success = false; - int remaining_bytes = (seg + seg_size) - cursor; - if (remaining_bytes >= (int)sizeof(int)) { - int * ints = (int *)cursor; - int entry_count = *ints++; - cursor = (byte *)ints; - remaining_bytes -= (int)sizeof(int); - if ((entry_count >= 0) && (remaining_bytes >= entry_count * 3 * (int)sizeof(int))) { - clear_distribution_hub_tables (); - success = true; - for (int n = 0; n < entry_count; n++) { - if (remaining_bytes < 3 * (int)sizeof(int)) { - success = false; - break; - } - int x = *ints++; - int y = *ints++; - int civ_id = *ints++; - cursor = (byte *)ints; - remaining_bytes -= 3 * (int)sizeof(int); - Tile * tile = tile_at (x, y); - if ((tile == NULL) || (tile == p_null_tile)) - continue; - on_distribution_hub_completed (tile, x, y); - struct distribution_hub_record * rec = get_distribution_hub_record (tile); - if (rec != NULL) - rec->civ_id = civ_id; - } - } - } - if (! success) { - error_chunk_name = "distribution_hub_records"; - break; - } - } else if (match_save_chunk_name (&cursor, "aerodrome_airlift_usage")) { - bool success = false; - int remaining_bytes = (seg + seg_size) - cursor; - if (remaining_bytes >= (int)sizeof(int)) { - int * ints = (int *)cursor; - int entry_count = *ints++; - cursor = (byte *)ints; - remaining_bytes -= (int)sizeof(int); - if ((entry_count >= 0) && - (remaining_bytes >= entry_count * 3 * (int)sizeof(int))) { - table_deinit (&is->aerodrome_airlift_usage); - success = true; - for (int n = 0; n < entry_count; n++) { - if (remaining_bytes < 3 * (int)sizeof(int)) { - success = false; - break; - } - int tile_x = *ints++; - int tile_y = *ints++; - int mask = *ints++; - cursor = (byte *)ints; - remaining_bytes -= 3 * (int)sizeof(int); + } + } + } + + // 6 is the game's standard value, so fall back on the base game logic in that case + if (is->current_config.rebase_range_multiplier == 6) { + return Unit_check_rebase_target (this, __, tile_x, tile_y) && is_below_stack_limit (tile_at (tile_x, tile_y), this->Body.CivID, this->Body.UnitTypeID); + + // Otherwise, we have to redo the range check. Unlike Unit::is_in_rebase_range, the base method here does more than just check the range so we + // want to make sure to call it even if we determine that the target is in range. In that case, set the range to unlimited temporarily so the + // base game's range check passes. + } else { + if (patch_Unit_is_in_rebase_range (this, __, tile_x, tile_y)) { + int * p_op_range = &p_bic_data->UnitTypes[this->Body.UnitTypeID].OperationalRange; + int original_op_range = *p_op_range; + *p_op_range = 0; + bool tr = Unit_check_rebase_target (this, __, tile_x, tile_y); + *p_op_range = original_op_range; + return tr; + } else + return false; + } +} + +int __fastcall +patch_Sprite_draw_already_worked_tile_img (Sprite * this, int edx, PCX_Image * canvas, int pixel_x, int pixel_y, PCX_Color_Table * color_table) +{ + Sprite * to_draw = this; + + if (is->do_not_draw_already_worked_tile_img) + return 0; + + if (is->current_config.toggle_zoom_with_z_on_city_screen && p_bic_data->is_zoomed_out) { + + // Load sprite if necessary + if (is->tile_already_worked_zoomed_out_sprite_init_state == IS_UNINITED) { + is->tile_already_worked_zoomed_out_sprite_init_state = IS_INIT_FAILED; + PCX_Image * pcx = malloc (sizeof *pcx); + if (pcx != NULL) { + memset (pcx, 0, sizeof *pcx); + PCX_Image_construct (pcx); + char path[2*MAX_PATH]; + get_mod_art_path ("TileAlreadyWorkedZoomedOut.pcx", path, sizeof path); + PCX_Image_read_file (pcx, __, path, NULL, 0, 0x100, 2); + if (pcx->JGL.Image != NULL) { + Sprite * sprite = &is->tile_already_worked_zoomed_out_sprite; + memset (sprite, 0, sizeof *sprite); + Sprite_construct (sprite); + Sprite_slice_pcx (sprite, __, pcx, 0, 0, 64, 32, 1, 1); + is->tile_already_worked_zoomed_out_sprite_init_state = IS_OK; + } + pcx->vtable->destruct (pcx, __, 0); + free (pcx); + } + } + + if (is->tile_already_worked_zoomed_out_sprite_init_state == IS_OK) + to_draw = &is->tile_already_worked_zoomed_out_sprite; + } + + return Sprite_draw (to_draw, __, canvas, pixel_x, pixel_y, color_table); +} + +int __fastcall +patch_Tile_m43_Get_field_30_for_city_loc_eval (Tile * this) +{ + int tr = this->vtable->m43_Get_field_30 (this); + + // This patch function replaces two places where ai_eval_city_location calls Tile::m43_Get_field_30 to check the 18th bit, which indicates + // whether the tile is in the workable area of any city. If it is but the work area has been expanded, check that it's actually within the + // normal 20-tile area. This prevents work area expansion from causing AIs to space their cities further apart. + // The field_30_get_counter is a crazy workaround for the fact that the game doesn't have any way to determine a tile's coordinates given a + // pointer to its object. So we track the initial (x, y) for the tile we're evaluating and then count calls to this func to know what + // neighboring coords "this" corresponds to. + int get_counter = is->ai_evaling_city_field_30_get_counter; + if (((tr >> 17) & 1) && (is->current_config.city_work_radius >= 3)) { + bool found_city = false; + int this_x, this_y; { + int dx, dy; + patch_ni_to_diff_for_work_area (get_counter, &dx, &dy); + this_x = is->ai_evaling_city_loc_x + dx; + this_y = is->ai_evaling_city_loc_y + dy; + wrap_tile_coords (&p_bic_data->Map, &this_x, &this_y); + } + FOR_TILES_AROUND (tai, 21, this_x, this_y) + if (Tile_has_city (tai.tile)) { + found_city = true; + break; + } + if (! found_city) + tr &= ~(1 << 17); + } + get_counter++; + if (get_counter >= 21) + get_counter = 0; + is->ai_evaling_city_field_30_get_counter = get_counter; + + return tr; + +} + +// Intercept the call where the game gets a random index at which to start searching for a suitable tile to become polluted. Normally, it passes 20 as +// the limit here. We must replace that to cover a potentially modded work area. +int __fastcall +patch_rand_int_to_place_pollution (void * this, int edx, int lim) +{ + return rand_int (this, __, is->workable_tile_count - 1); +} + +void __fastcall +patch_Main_Screen_Form_t2s_coords_to_draw_yields (Main_Screen_Form * this, int edx, int tile_x, int tile_y, int * out_x, int * out_y) +{ + Main_Screen_Form_tile_to_screen_coords (this, __, tile_x, tile_y, out_x, out_y); + + // If we've zoomed out the map on the city screen, we may end up drawing the tile yields off screen depending on the map size. If this is the + // case, detected by negative screen coords, then shift the coords over by one map-screen back onto the actual screen. The shift amounts are + // 64*map_width/2 and 32*map_height/2 for x and y because (64, 32) is the size of a zoomed out tile and /2 is for overlap (I think). + if (p_bic_data->is_zoomed_out && (*out_x < 0)) + *out_x += p_bic_data->Map.Width << 5; + if (p_bic_data->is_zoomed_out && (*out_y < 0)) + *out_y += p_bic_data->Map.Height << 4; +} + +void +set_clip_area_to_map_view (City_Form * city_form) +{ + int left_margin = (p_bic_data->ScreenWidth - city_form->Background_Image.Width) / 2, + top_margin = (p_bic_data->ScreenHeight - city_form->Background_Image.Height) / 2; + RECT map_view_on_screen = { .left = left_margin, .top = top_margin + 92, .right = left_margin + 1024, .bottom = top_margin + 508 }; + JGL_Image * jgl_canvas = city_form->Base.Data.Canvas.JGL.Image; + jgl_canvas->vtable->m13_Set_Clip_Region (jgl_canvas, __, &map_view_on_screen); +} + +void +clear_clip_area (City_Form * city_form) +{ + JGL_Image * jgl_canvas = city_form->Base.Data.Canvas.JGL.Image; + jgl_canvas->vtable->m13_Set_Clip_Region (jgl_canvas, __, &jgl_canvas->Image_Rect); +} + +void +init_distribution_hub_icons () +{ + if (is->distribution_hub_icons_img_state != IS_UNINITED) + return; + + PCX_Image pcx; + PCX_Image_construct (&pcx); + + char ss[200]; + snprintf (ss, sizeof ss, "[C3X] init_distribution_hub_icons: state=%d\n", is->distribution_hub_icons_img_state); + (*p_OutputDebugStringA) (ss); + + char temp_path[2*MAX_PATH]; + get_mod_art_path ("Districts/DistributionHubIncomeIcons.pcx", temp_path, sizeof temp_path); + + PCX_Image_read_file (&pcx, __, temp_path, NULL, 0, 0x100, 2); + if ((pcx.JGL.Image == NULL) || + (pcx.JGL.Image->vtable->m54_Get_Width (pcx.JGL.Image) < 776) || + (pcx.JGL.Image->vtable->m55_Get_Height (pcx.JGL.Image) < 32)) { + (*p_OutputDebugStringA) ("[C3X] PCX file for distribution hub icons failed to load or is too small.\n"); + is->distribution_hub_icons_img_state = IS_INIT_FAILED; + goto cleanup; + } - Tile * tile = tile_at (tile_x, tile_y); - if ((tile == NULL) || (tile == p_null_tile)) - continue; + // Extract shield icon (index 4: x = 1 + 4*31 = 125, width 30) + Sprite_construct (&is->distribution_hub_shield_icon); + Sprite_slice_pcx (&is->distribution_hub_shield_icon, __, &pcx, 1 + 4*31, 1, 30, 30, 1, 1); - itable_insert (&is->aerodrome_airlift_usage, (int)tile, mask); - } - if (! success) - table_deinit (&is->aerodrome_airlift_usage); - } - } - if (! success) { - error_chunk_name = "aerodrome_airlift_usage"; - break; - } + // Extract shield corruption icon (index 5: x = 1 + 5*31 = 156, width 30) + Sprite_construct (&is->distribution_hub_corruption_icon); + Sprite_slice_pcx (&is->distribution_hub_corruption_icon, __, &pcx, 1 + 5*31, 1, 30, 30, 1, 1); - } else if (match_save_chunk_name (&cursor, "district_config_names")) { - bool success = false; - bool mismatch_found = false; - bool count_mismatch = false; - char first_mismatch[200]; - first_mismatch[0] = '\0'; - int remaining_bytes = (seg + seg_size) - cursor; - if (remaining_bytes >= (int)sizeof(int)) { - int * ints = (int *)cursor; - int saved_count = *ints++; - cursor = (byte *)ints; - remaining_bytes -= (int)sizeof(int); - if (saved_count >= 0) { - success = true; - count_mismatch = (saved_count != is->district_count); - char * saved_names[saved_count]; - for (int n = 0; n < saved_count; n++) { - if (remaining_bytes < (int)sizeof(int)) { - success = false; - break; - } - ints = (int *)cursor; - int saved_id = *ints++; - cursor = (byte *)ints; - remaining_bytes -= (int)sizeof(int); + // Extract small shield icon (index 13) + Sprite_construct (&is->distribution_hub_shield_icon_small); + Sprite_slice_pcx (&is->distribution_hub_shield_icon_small, __, &pcx, 1 + 13*31, 1, 30, 30, 1, 1); - int name_len = -1; - for (int k = 0; k < remaining_bytes; k++) { - if (cursor[k] == '\0') { - name_len = k; - break; - } - } - if (name_len < 0) { - success = false; - break; - } - int padded_len = (name_len + 4) & ~3; - if (padded_len > remaining_bytes) { - success = false; - break; - } + // Extract surplus food icon (index 6: x = 1 + 6*31 = 187, width 30) + Sprite_construct (&is->distribution_hub_food_icon); + Sprite_slice_pcx (&is->distribution_hub_food_icon, __, &pcx, 1 + 6*31, 1, 30, 30, 1, 1); - char * saved_name = (char *)cursor; - saved_names[n] = saved_name; - if (! mismatch_found) { - if ((saved_id < 0) || (saved_id >= is->district_count)) { - snprintf (first_mismatch, sizeof first_mismatch, "%s %d (\"%s\") %s", is->c3x_labels[CL_DISTRICT_ID], saved_id, saved_name, is->c3x_labels[CL_DISTRICT_IN_SAVE_BUT_MISSING_NOW]); - first_mismatch[(sizeof first_mismatch) - 1] = '\0'; - mismatch_found = true; - } else { - char const * current_name = is->district_configs[saved_id].name; - if (current_name == NULL) - current_name = ""; - if (strcmp (current_name, saved_name) != 0) { - snprintf (first_mismatch, sizeof first_mismatch, "%s %d \"%s\" %s \"%s\"", is->c3x_labels[CL_DISTRICT_ID], saved_id, saved_name, is->c3x_labels[CL_DISTRICT_NAME_MISMATCH], current_name); - first_mismatch[(sizeof first_mismatch) - 1] = '\0'; - mismatch_found = true; - } - } - } + // Extract small surplus food icon (index 15) + Sprite_construct (&is->distribution_hub_food_icon_small); + Sprite_slice_pcx (&is->distribution_hub_food_icon_small, __, &pcx, 1 + 15*31, 1, 30, 30, 1, 1); - cursor += padded_len; - remaining_bytes -= padded_len; - } - if (success && count_mismatch && (first_mismatch[0] == '\0')) { - snprintf (first_mismatch, sizeof first_mismatch, "%s %d %s %d", is->c3x_labels[CL_SAVE_FILE_HAD], saved_count, is->c3x_labels[CL_CURRENT_CONFIG_HAS_ONLY], is->district_count); - first_mismatch[(sizeof first_mismatch) - 1] = '\0'; - mismatch_found = true; - } - if (success && mismatch_found) { - PopupForm * popup = get_popup_form (); - popup->vtable->set_text_key_and_flags (popup, __, is->mod_script_path, "C3X_ERROR", -1, 0, 0, 0); + // Extract eaten food icon (index 7: x = 1 + 7*31 = 218, width 30) + Sprite_construct (&is->distribution_hub_eaten_food_icon); + Sprite_slice_pcx (&is->distribution_hub_eaten_food_icon, __, &pcx, 1 + 7*31, 1, 30, 30, 1, 1); - char s[1000]; - snprintf (s, sizeof s, "%s %s", is->c3x_labels[CL_WARNING_DISTRICTS_CONFIG_MISMATCH], first_mismatch); - snprintf (s, sizeof s, "%s %s", s, is->c3x_labels[CL_MAY_BE_OTHER_ERRORS_AS_WELL]); - s[(sizeof s) - 1] = '\0'; - PopupForm_add_text (popup, __, s, 0); + is->distribution_hub_icons_img_state = IS_OK; +cleanup: + pcx.vtable->destruct (&pcx, __, 0); +} - snprintf (s, sizeof s, "^"); - s[(sizeof s) - 1] = '\0'; - PopupForm_add_text (popup, __, s, 0); +void +draw_district_yields (City_Form * city_form, Tile * tile, int district_id, int screen_x, int screen_y) +{ + // Lazy load district icons + if (is->dc_icons_img_state == IS_UNINITED) + init_district_icons (); + if (is->dc_icons_img_state != IS_OK) + return; - snprintf (s, sizeof s, "^%s:", is->c3x_labels[CL_DISTRICTS_IN_SAVE_FILE]); - s[(sizeof s) - 1] = '\0'; - PopupForm_add_text (popup, __, s, 0); - for (int n = 0; n < saved_count; n++) { - snprintf (s, sizeof s, "^ (%d) %s", n, saved_names[n]); - s[(sizeof s) - 1] = '\0'; - PopupForm_add_text (popup, __, s, 0); - } + if (district_id < 0 || district_id >= is->district_count) + return; - snprintf (s, sizeof s, "^"); - s[(sizeof s) - 1] = '\0'; - PopupForm_add_text (popup, __, s, 0); + // Get district configuration + struct district_config const * config = &is->district_configs[district_id]; + struct district_instance * inst = get_district_instance (tile); - snprintf (s, sizeof s, "^%s \"%s\":", is->c3x_labels[CL_CURRENTLY_CONFIGURED_DISTRICTS], is->current_districts_config_path); - s[(sizeof s) - 1] = '\0'; - PopupForm_add_text (popup, __, s, 0); - for (int n = 0; n < is->district_count; n++) { - snprintf (s, sizeof s, "^ (%d) %s", n, is->district_configs[n].name); - s[(sizeof s) - 1] = '\0'; - PopupForm_add_text (popup, __, s, 0); - } + // Count total yields from bonuses + int food_bonus = 0, shield_bonus = 0, gold_bonus = 0, science_bonus = 0, culture_bonus = 0, happiness_bonus = 0; + get_effective_district_yields (inst, config, &food_bonus, &shield_bonus, &gold_bonus, &science_bonus, &culture_bonus, &happiness_bonus); + + int food_pos = food_bonus > 0 ? food_bonus : 0; + int food_neg = food_bonus < 0 ? -food_bonus : 0; + int shield_pos = shield_bonus > 0 ? shield_bonus : 0; + int shield_neg = shield_bonus < 0 ? -shield_bonus : 0; + int gold_pos = gold_bonus > 0 ? gold_bonus : 0; + int gold_neg = gold_bonus < 0 ? -gold_bonus : 0; + int science_pos = science_bonus > 0 ? science_bonus : 0; + int science_neg = science_bonus < 0 ? -science_bonus : 0; + int culture_pos = culture_bonus > 0 ? culture_bonus : 0; + int culture_neg = culture_bonus < 0 ? -culture_bonus : 0; + int happiness_pos = happiness_bonus > 0 ? happiness_bonus : 0; + int happiness_neg = happiness_bonus < 0 ? -happiness_bonus : 0; - patch_show_popup (popup, __, 0, 0); - } - } - } - if (! success) { - error_chunk_name = "district_config_names"; - break; - } - - } else { - error_chunk_name = "N/A"; - break; - } - } + int total_yield = 0; + total_yield += food_pos + food_neg; + total_yield += shield_pos + shield_neg; + total_yield += gold_pos + gold_neg; + total_yield += science_pos + science_neg; + total_yield += culture_pos + culture_neg; + total_yield += happiness_pos + happiness_neg; - if (error_chunk_name != NULL) { - char s[200]; - snprintf (s, sizeof s, "Failed to read mod save data. Error occured in chunk: %s", error_chunk_name); - s[(sizeof s) - 1] = '\0'; - pop_up_in_game_error (s); + if (total_yield <= 0) + return; + + // Get sprites + Sprite * food_sprite = &is->district_food_icon_small; + Sprite * shield_sprite = &is->district_shield_icon_small; + Sprite * commerce_sprite = &is->district_commerce_icon_small; + Sprite * science_sprite = &is->district_science_icon_small; + Sprite * culture_sprite = &is->district_culture_icon_small; + Sprite * happiness_sprite = &is->district_happiness_icon_small; + Sprite * food_negative_sprite = &is->district_negative_food_icon_small; + Sprite * shield_negative_sprite = &is->district_negative_shield_icon_small; + Sprite * commerce_negative_sprite = &is->district_negative_commerce_icon_small; + Sprite * science_negative_sprite = &is->district_negative_science_icon_small; + Sprite * culture_negative_sprite = &is->district_negative_culture_icon_small; + Sprite * happiness_negative_sprite = &is->district_unhappiness_icon_small; + + // Determine sprite dimensions + int sprite_width = food_sprite->Width3; + int sprite_height = food_sprite->Height; + + // Calculate total width of all icons + int total_width = total_yield * sprite_width; + + // Center the icons horizontally + int half_width = total_width >> 1; + int tile_width = p_bic_data->is_zoomed_out ? 64 : 128; + int max_offset = (tile_width >> 1) - 5; + if (half_width > max_offset) + half_width = max_offset; + + int pixel_x = screen_x - half_width; + int pixel_y = screen_y - (sprite_height >> 1); + + // Adjust spacing if icons would exceed tile width + int spacing = sprite_width; + if (total_width > tile_width - 10) { + if (total_yield > 1) { + spacing = (tile_width - 10 - sprite_width) / (total_yield - 1); + if (spacing < 1) + spacing = 1; + else if (spacing > sprite_width) + spacing = sprite_width; } + } - free (seg); + // Draw icons in order: shields, food, science, commerce, culture + for (int i = 0; i < shield_pos; i++) { + Sprite_draw (shield_sprite, __, &city_form->Base.Data.Canvas, pixel_x, pixel_y, NULL); + pixel_x += spacing; + } + for (int i = 0; i < shield_neg; i++) { + Sprite_draw (shield_negative_sprite, __, &city_form->Base.Data.Canvas, pixel_x, pixel_y, NULL); + pixel_x += spacing; + } + + for (int i = 0; i < food_pos; i++) { + Sprite_draw (food_sprite, __, &city_form->Base.Data.Canvas, pixel_x, pixel_y, NULL); + pixel_x += spacing; + } + for (int i = 0; i < food_neg; i++) { + Sprite_draw (food_negative_sprite, __, &city_form->Base.Data.Canvas, pixel_x, pixel_y, NULL); + pixel_x += spacing; + } + + for (int i = 0; i < science_pos; i++) { + Sprite_draw (science_sprite, __, &city_form->Base.Data.Canvas, pixel_x, pixel_y, NULL); + pixel_x += spacing; + } + for (int i = 0; i < science_neg; i++) { + Sprite_draw (science_negative_sprite, __, &city_form->Base.Data.Canvas, pixel_x, pixel_y, NULL); + pixel_x += spacing; + } + + for (int i = 0; i < gold_pos; i++) { + Sprite_draw (commerce_sprite, __, &city_form->Base.Data.Canvas, pixel_x, pixel_y, NULL); + pixel_x += spacing; + } + for (int i = 0; i < gold_neg; i++) { + Sprite_draw (commerce_negative_sprite, __, &city_form->Base.Data.Canvas, pixel_x, pixel_y, NULL); + pixel_x += spacing; + } + + for (int i = 0; i < culture_pos; i++) { + Sprite_draw (culture_sprite, __, &city_form->Base.Data.Canvas, pixel_x, pixel_y, NULL); + pixel_x += spacing; + } + for (int i = 0; i < culture_neg; i++) { + Sprite_draw (culture_negative_sprite, __, &city_form->Base.Data.Canvas, pixel_x, pixel_y, NULL); + pixel_x += spacing; + } + + for (int i = 0; i < happiness_pos; i++) { + Sprite_draw (happiness_sprite, __, &city_form->Base.Data.Canvas, pixel_x, pixel_y, NULL); + pixel_x += spacing; } + for (int i = 0; i < happiness_neg; i++) { + Sprite_draw (happiness_negative_sprite, __, &city_form->Base.Data.Canvas, pixel_x, pixel_y, NULL); + pixel_x += spacing; + } +} + +void +draw_distribution_hub_yields (City_Form * city_form, Tile * tile, int tile_x, int tile_y, int screen_x, int screen_y) +{ + // Get the distribution hub record for this tile + struct distribution_hub_record * rec = get_distribution_hub_record (tile); + if (rec == NULL) + return; + + City * anchor_city = get_connected_city_for_distribution_hub (rec); + if (! distribution_hub_accessible_to_city (rec, city_form->CurrentCity)) + return; + + int food_yield = rec->food_yield; + int shield_yield = rec->shield_yield; + int total_yield = food_yield + shield_yield; + + if (total_yield <= 0) + return; - return tr; -} + // Lazy load distribution hub icons + if (is->distribution_hub_icons_img_state == IS_UNINITED) + init_distribution_hub_icons (); + if (is->distribution_hub_icons_img_state != IS_OK) + return; -void __fastcall -patch_MappedFile_deinit_after_saving_or_loading (MappedFile * this) -{ - is->accessing_save_file = NULL; - MappedFile_deinit (this); -} + Sprite * food_sprite = &is->distribution_hub_food_icon_small; + Sprite * shield_sprite = &is->distribution_hub_shield_icon_small; -bool __fastcall -patch_Tile_m7_Check_Barbarian_Camp (Tile * this, int edx, int visible_to_civ) -{ - int * p_stack = (int *)&visible_to_civ; - int ret_addr = p_stack[-1]; + if (food_sprite->Width3 == 0) food_sprite = &is->distribution_hub_food_icon; + if (shield_sprite->Width3 == 0) shield_sprite = &is->distribution_hub_shield_icon; - // If the barb unit AI is calling this method to check if there's a camp to defend, return true if we're allowing barb city capture and the - // tile has a city. This causes barb units to defend cities they've captured, otherwise they'll ignore them. - if ((ret_addr == ADDR_CHECK_BARB_CAMP_TO_DEFEND_RETURN) && - is->current_config.enable_city_capture_by_barbarians && - Tile_has_city (this)) - return true; - else - return Tile_m7_Check_Barbarian_Camp (this, __, visible_to_civ); -} + int sprite_height = food_sprite->Height; + if (sprite_height == 0) sprite_height = shield_sprite->Height; -bool __fastcall -patch_Unit_can_airdrop (Unit * this) -{ - UnitType * this_type = &p_bic_data->UnitTypes[this->Body.UnitTypeID]; + int food_width = food_sprite->Width3; + int shield_width = shield_sprite->Width3; + if ((food_width <= 0) && (shield_width > 0)) food_width = shield_width; + if ((shield_width <= 0) && (food_width > 0)) shield_width = food_width; - bool allowed = Unit_can_airdrop (this); + // Calculate total width of all icons + int total_width = 0; + if (food_yield > 0) total_width += food_width * food_yield; + if (shield_yield > 0) total_width += shield_width * shield_yield; - bool require_aerodrome = (is->current_config.enable_districts && - is->current_config.enable_aerodrome_districts && - is->current_config.air_units_use_aerodrome_districts_not_cities); + // Center the icons horizontally + int half_width = total_width >> 1; + int tile_width = p_bic_data->is_zoomed_out ? 64 : 128; + int max_offset = (tile_width >> 1) - 5; + if (half_width > max_offset) half_width = max_offset; - if (require_aerodrome) { - Tile * tile = tile_at (this->Body.X, this->Body.Y); - bool has_aerodrome = false; + int pixel_x = screen_x - half_width; + int pixel_y = screen_y - (sprite_height >> 1); - if ((tile != NULL) && (tile != p_null_tile)) - has_aerodrome = tile_has_friendly_aerodrome_district (tile, this->Body.CivID, false); + // Adjust spacing if icons would exceed tile width + int spacing_food = food_width; + int spacing_shield = shield_width; - if (! has_aerodrome) - allowed = false; - else if (! allowed) { - if ((this_type->Unit_Class != UTC_Air) && - (this_type->Air_Missions & UCV_Airdrop) && - (this->Body.Moves == 0)) - allowed = true; + if (total_width > tile_width - 10) { + if (total_yield > 1) { + int spacing = (tile_width - 10 - food_width) / (total_yield - 1); + if (spacing < 1) + spacing = 1; + else if (spacing > food_width) + spacing = food_width; + spacing_food = spacing; + spacing_shield = spacing; } } - // Possibly rule in this airdrop as allowed if it's by a paratrooper in a helicopter on a carrier and we're configured to allow airdrops under - // those circumstances. - if ((! allowed) && (is->current_config.special_helicopter_rules & SHR_PASSENGER_AIRDROP)) { - Unit * container = get_unit_ptr (this->Body.Container_Unit); - if (container != NULL && p_bic_data->UnitTypes[container->Body.UnitTypeID].Unit_Class == UTC_Air) { // if in helicopter - Unit * metacontainer = get_unit_ptr (container->Body.Container_Unit); - if (metacontainer != NULL && p_bic_data->UnitTypes[metacontainer->Body.UnitTypeID].Unit_Class == UTC_Sea && - Unit_has_ability (metacontainer, __, UTA_Transports_Only_Aircraft)) { // if that helicopter is on a carrier - // Allow the airdrop under the same restrictions as from an airfield - allowed = this_type->Unit_Class != UTC_Air && this->Body.Moves == 0; - } - } + // Draw food icons first + for (int i = 0; i < food_yield; i++) { + Sprite_draw (food_sprite, __, &city_form->Base.Data.Canvas, pixel_x, pixel_y, NULL); + pixel_x += spacing_food; } - if (! allowed) - return false; - - return itable_look_up_or (&is->airdrops_this_turn, this->Body.ID, 0) == 0; -} - -bool __fastcall -patch_City_Improvements_contains (City_Improvements * this, int edx, int id) -{ - byte * extra_bits; - if ((id < 256) || ! is->current_config.remove_city_improvement_limit) - return City_Improvements_contains (this, __, id); - else if (itable_look_up (&is->extra_city_improvs, (int)this, (int *)&extra_bits)) { - int extra_id = id - 256; - return (extra_bits[extra_id>>3] >> (extra_id & 7)) & 1; - } else - return false; + // Draw shield icons + for (int i = 0; i < shield_yield; i++) { + Sprite_draw (shield_sprite, __, &city_form->Base.Data.Canvas, pixel_x, pixel_y, NULL); + pixel_x += spacing_shield; + } } void __fastcall -patch_City_Improvements_set (City_Improvements * this, int edx, int id, bool add_else_remove) +patch_City_Form_draw_yields_on_worked_tiles (City_Form * this) { - if ((id < 256) || ! is->current_config.remove_city_improvement_limit) - return City_Improvements_set (this, __, id, add_else_remove); - else { - byte * extra_bits = (byte *)itable_look_up_or (&is->extra_city_improvs, (int)this, 0); - int extra_id = id - 256; - byte mask = 1 << (extra_id & 7); - if (add_else_remove) { - if (! extra_bits) { - int extra_bits_size = (not_below (0, p_bic_data->ImprovementsCount - 256) >> 3) + 1; - extra_bits = calloc (1, extra_bits_size); - itable_insert (&is->extra_city_improvs, (int)this, (int)extra_bits); - } - extra_bits[extra_id>>3] |= mask; - } else if ((! add_else_remove) && (extra_bits != NULL)) - extra_bits[extra_id>>3] &= ~mask; + // If we're zoomed in and the city work radius is at least 4 then it's likely we'll end up drawing things outside of the city screen's usual + // map area. Set the clip area to the map area so none of those draws are visible. + bool changed_clip_area = false; + if ((is->current_config.city_work_radius >= 4) && ! p_bic_data->is_zoomed_out) { + set_clip_area_to_map_view (this); + changed_clip_area = true; } -} -bool __fastcall -patch_Leader_has_tech_to_stop_disease (Leader * this, int edx, int id) -{ - if (! is->current_config.patch_disease_stopping_tech_flag_bug) - return Leader_has_tech (this, __, id); - else - return Leader_has_tech_with_flag (this, __, ATF_Disabled_Deseases_From_Flood_Plains); -} - -void __fastcall -patch_set_worker_animation (void * this, int edx, Unit * unit, int job_id) -{ - AnimationType anim_type; + if (is->current_config.enable_districts && this->CurrentCity != NULL) { + recompute_city_yields_with_districts (this->CurrentCity); + } - // If districts disabled or unit is null or job is not building mines, use base logic - if ((! is->current_config.enable_districts) || - (unit == NULL) || - (job_id != WJ_Build_Mines)) { - set_worker_animation(this, __, unit, job_id); - return; - } - - // If tile has a district under construction - Tile * tile = tile_at (unit->Body.X, unit->Body.Y); - struct district_instance * inst = get_district_instance (tile); - if (inst != NULL && - ! district_is_complete (tile, inst->district_type) && job_id == WJ_Build_Mines) { + is->do_not_draw_already_worked_tile_img = false; + City_Form_draw_yields_on_worked_tiles (this); - // Override and ensure build animation is used - job_id = AT_BUILD; + // If we're zoomed out then draw the yields again but this time skip drawing of the tile-already-worked sprites. This is because the + // transparent regions of those sprites get drawn overtop of the yield sprites when zoomed out, and that overdrawing deletes parts of the + // yields, i.e., it overwrites their colored pixels with transparent ones. The simplest solution is to draw the yields again after the + // already-worked sprites to ensure the former get drawn overtop of the latter. + if (p_bic_data->is_zoomed_out) { + is->do_not_draw_already_worked_tile_img = true; + City_Form_draw_yields_on_worked_tiles (this); } - - set_worker_animation(this, __, unit, job_id); -} -void __fastcall -patch_Unit_work_simple_job (Unit * this, int edx, int job_id) -{ - is->lmify_tile_after_working_simple_job = NULL; + // Draw district bonuses on district tiles + if (is->current_config.enable_districts || is->current_config.enable_natural_wonders) { + City * city = this->CurrentCity; + if (city == NULL) + goto skip_district_yields; - // Check if districts are enabled - if (is->current_config.enable_districts) { - int tile_x = this->Body.X; - int tile_y = this->Body.Y; - Tile * tile = tile_at (tile_x, tile_y); + int city_x = city->Body.X; + int city_y = city->Body.Y; + int civ_id = city->Body.CivID; - if (tile != NULL && tile != p_null_tile) { - // Check if there's a completed district on this tile - struct district_instance * inst = get_district_instance (tile); - if (inst != NULL && district_is_complete(tile, inst->district_type)) { - int district_id = inst->district_type; - bool is_human = (*p_human_player_bits & (1 << this->Body.CivID)) != 0; + // Calculate screen coordinates for city center + int center_screen_x, center_screen_y; + Main_Screen_Form_tile_to_screen_coords (p_main_screen_form, __, city_x, city_y, ¢er_screen_x, ¢er_screen_y); - // AI players only (human removal is handled via issue_district_worker_command) - if (!is_human) { - bool allow_removal = false; - if (district_id == WONDER_DISTRICT_ID) { - struct wonder_district_info * info = &inst->wonder_info; - allow_removal = (info->state == WDS_UNUSED); - } + int tile_half_width = p_bic_data->is_zoomed_out ? 32 : 64; + int tile_half_height = p_bic_data->is_zoomed_out ? 16 : 32; + center_screen_x += tile_half_width; + if (center_screen_x < 0) + center_screen_x += p_bic_data->Map.Width * tile_half_width; + center_screen_y += tile_half_height; + if (center_screen_y < 0) + center_screen_y += p_bic_data->Map.Height * tile_half_height; - if (allow_removal) { - remove_district_instance (tile); - tile->vtable->m62_Set_Tile_BuildingID (tile, __, -1); - tile->vtable->m51_Unset_Tile_Flags (tile, __, 0, TILE_FLAG_MINE, tile_x, tile_y); - handle_district_removed (tile, district_id, tile_x, tile_y, false); - } - } - } - } - } + int remaining_utilized_neighborhoods = 0; + if (is->current_config.enable_districts && is->current_config.enable_neighborhood_districts) + remaining_utilized_neighborhoods = count_utilized_neighborhoods_in_city_radius (city); - Unit_work_simple_job (this, __, job_id); + FOR_DISTRICTS_AROUND (wai, city_x, city_y, true) { + struct district_instance * inst = wai.district_inst; + int district_id = inst->district_id; - if (is->lmify_tile_after_working_simple_job != NULL) - is->lmify_tile_after_working_simple_job->vtable->m31_set_landmark (is->lmify_tile_after_working_simple_job, __, true); -} + bool is_distribution_hub = district_id == DISTRIBUTION_HUB_DISTRICT_ID; + bool is_natural_wonder = district_id == NATURAL_WONDER_DISTRICT_ID; -void __fastcall -patch_Map_change_tile_terrain_by_worker (Map * this, int edx, enum SquareTypes new_terrain_type, int x, int y) -{ - Map_change_tile_terrain (this, __, new_terrain_type, x, y); + // Skip distribution hubs if the feature is not enabled + if (is_distribution_hub && (!is->current_config.enable_districts || !is->current_config.enable_distribution_hub_districts)) + continue; + + if (is_natural_wonder && (!is->current_config.enable_natural_wonders)) + continue; + + if (is_distribution_hub) + continue; - if (is->current_config.convert_to_landmark_after_planting_forest && (new_terrain_type == SQ_Forest)) - is->lmify_tile_after_working_simple_job = tile_at (x, y); -} + if (!is_natural_wonder && (!is->current_config.enable_districts)) + continue; -int __fastcall -patch_Leader_ai_eval_technology (Leader * this, int edx, int id, bool param_2, bool param_3) -{ - int base = Leader_ai_eval_technology (this, __, id, param_2, param_3); - return apply_perfume (PK_TECHNOLOGY, p_bic_data->Advances[id].Name, base); -} + // For neighborhood districts, check if population is high enough to utilize them + if (is->current_config.enable_districts && + is->current_config.enable_neighborhood_districts && + district_id == NEIGHBORHOOD_DISTRICT_ID) { + // Only draw yields if this neighborhood is utilized + if (remaining_utilized_neighborhoods <= 0) + continue; + remaining_utilized_neighborhoods--; + } -int __fastcall -patch_Leader_ai_eval_government (Leader * this, int edx, int id) -{ - int base = Leader_ai_eval_government (this, __, id); - return apply_perfume (PK_GOVERNMENT, p_bic_data->Governments[id].Name.S, base); -} + // Calculate screen coordinates for this tile + int screen_x = center_screen_x + (wai.dx * tile_half_width); + int screen_y = center_screen_y + (wai.dy * tile_half_height); -bool -roll_to_spare_unit_from_nuke (Unit * unit) -{ - int one_hp_destroy_chance = is->current_config.chance_for_nukes_to_destroy_max_one_hp_units; - UnitType * type = &p_bic_data->UnitTypes[unit->Body.UnitTypeID]; - if ((one_hp_destroy_chance < 100) && - (Unit_get_max_hp (unit) <= 1) && - (type->Defence > 0) && - ((type->Unit_Class == UTC_Land) || (type->Unit_Class == UTC_Sea))) - return ! (rand_int (p_rand_object, __, 100) < one_hp_destroy_chance); - else - return false; -} + // Call the appropriate drawing function + draw_district_yields (this, wai.tile, district_id, screen_x, screen_y); + } -void __fastcall -patch_Unit_despawn_after_killed_by_nuke (Unit * this, int edx, int civ_id_responsible, byte param_2, byte param_3, byte param_4, byte param_5, byte param_6, byte param_7) -{ - if (roll_to_spare_unit_from_nuke (this)) - this->Body.Damage = Unit_get_max_hp (this) - 1; - else { - bool prev_always_despawn_passengers = is->always_despawn_passengers; - if ((is->current_config.land_transport_rules & LTR_NO_ESCAPE) && is_land_transport (this)) - is->always_despawn_passengers = true; - else if ((is->current_config.special_helicopter_rules & SHR_NO_ESCAPE) && p_bic_data->UnitTypes[this->Body.UnitTypeID].Unit_Class == UTC_Air && - p_bic_data->UnitTypes[this->Body.UnitTypeID].Transport_Capacity > 0) - is->always_despawn_passengers = true; - patch_Unit_despawn (this, __, civ_id_responsible, param_2, param_3, param_4, param_5, param_6, param_7); - is->always_despawn_passengers = prev_always_despawn_passengers; - } -} + // Draw distribution hub yields around a larger radius so connected hubs outside the work area are shown + if (is->current_config.enable_districts && is->current_config.enable_distribution_hub_districts) { + int const max_tiles = workable_tile_counts[7]; -void __fastcall -patch_mp_despawn_after_killed_by_nuke (void * this, int edx, int unit_id, int civ_id_responsible, byte param_3, byte param_4, byte param_5, byte param_6) -{ - Unit * unit = get_unit_ptr (unit_id); - if ((unit != NULL) && roll_to_spare_unit_from_nuke (unit)) - unit->Body.Damage = Unit_get_max_hp (unit) - 1; - else - mp_despawn (this, __, unit_id, civ_id_responsible, param_3, param_4, param_5, param_6); -} + for (int ni = 0; ni < max_tiles; ni++) { + int dx, dy; + patch_ni_to_diff_for_work_area (ni, &dx, &dy); -bool __fastcall -patch_City_has_unprotected_improv_to_sell (City * this, int edx, int id) -{ - // Fall back to original logic only if the config doesn't alter the behavior. - if (! is->current_config.allow_sale_of_aqueducts_and_hospitals && !is->current_config.allow_sale_of_small_wonders) - return City_has_unprotected_improv (this, __, id); + int tile_x = city_x + dx; + int tile_y = city_y + dy; + wrap_tile_coords (&p_bic_data->Map, &tile_x, &tile_y); - else if (patch_City_has_improvement (this, __, id, false)) { - Improvement * improv = &p_bic_data->Improvements[id]; - int max_pop_to_sell = INT_MAX; // By default, population-based sell isn't restricted - if (improv->ImprovementFlags & (ITF_Allows_City_Level_2 | ITF_Allows_City_Level_3)) { // Aqueduct/Hospital? - if (is->current_config.allow_sale_of_aqueducts_and_hospitals) { - if (improv->ImprovementFlags & ITF_Allows_City_Level_2) - max_pop_to_sell = p_bic_data->General.MaximumSize_Town; - else if (improv->ImprovementFlags & ITF_Allows_City_Level_3) - max_pop_to_sell = p_bic_data->General.MaximumSize_City; - } else { - // Do not allow selling these. - max_pop_to_sell = 0; + Tile * tile = tile_at (tile_x, tile_y); + if ((tile == NULL) || (tile == p_null_tile)) + continue; + + struct district_instance * inst = get_district_instance (tile); + if ((inst == NULL) || (inst->district_id != DISTRIBUTION_HUB_DISTRICT_ID)) + continue; + + int screen_x = center_screen_x + (dx * tile_half_width); + int screen_y = center_screen_y + (dy * tile_half_height); + draw_distribution_hub_yields (this, tile, tile_x, tile_y, screen_x, screen_y); } } + } - // Can't sell: - // - Great Wonders - // - Small Wonders, unless the config allows it - // - Capital - // - Aqueduct/Hospital if the city is too big for that population - return ((improv->Characteristics & ITC_Wonder) == 0) && - (is->current_config.allow_sale_of_small_wonders || ((improv->Characteristics & ITC_Small_Wonder) == 0)) && - ((improv->ImprovementFlags & ITF_Center_of_Empire) == 0) && - (this->Body.Population.Size <= max_pop_to_sell); - - } else - return false; +skip_district_yields: + if (changed_clip_area) + clear_clip_area (this); } bool __fastcall -patch_UnitType_has_detector_ability_for_vis_check (UnitType * this, int edx, enum UnitTypeAbilities a) +patch_City_Form_draw_highlighted_yields (City_Form * this, int edx, int tile_x, int tile_y, int neighbor_index) { - bool tr = UnitType_has_ability (this, __, a); + // Make sure we don't draw outside the map area + bool changed_clip_area = false; + if ((is->current_config.city_work_radius >= 4) && ! p_bic_data->is_zoomed_out) { + set_clip_area_to_map_view (this); + changed_clip_area = true; + } - // Restrict detection by sea units to other sea units and non-sea units to other non-sea units - if (tr && - is->current_config.no_cross_shore_detection && - (is->checking_visibility_for_unit != NULL) && - ((this->Unit_Class == UTC_Sea) ^ (p_bic_data->UnitTypes[is->checking_visibility_for_unit->Body.UnitTypeID].Unit_Class == UTC_Sea))) - tr = false; + bool tr = City_Form_draw_highlighted_yields (this, __, tile_x, tile_y, neighbor_index); + if (changed_clip_area) + clear_clip_area (this); return tr; } -bool -is_airdrop_trespassing (Unit * unit, int target_x, int target_y) +void __fastcall +patch_City_Form_draw_border_around_workable_tiles (City_Form * this) { - if (is->current_config.disallow_trespassing && - check_trespassing (unit->Body.CivID, tile_at (unit->Body.X, unit->Body.Y), tile_at (target_x, target_y))) { - bool allowed = is_allowed_to_trespass (unit); + // Make sure we don't draw outside the map area + bool changed_clip_area = false; + if ((is->current_config.city_work_radius >= 4) && ! p_bic_data->is_zoomed_out) { + set_clip_area_to_map_view (this); + changed_clip_area = true; + } - // If "unit" is an air unit that can carry others, like helicopters, then this airdrop is only allowed if all of its passengers are - // allowed to trespass. - UnitType * type = &p_bic_data->UnitTypes[unit->Body.UnitTypeID]; - if (allowed && (type->Unit_Class == UTC_Air) && (type->Transport_Capacity > 0)) - FOR_UNITS_ON (uti, tile_at (unit->Body.X, unit->Body.Y)) - if ((uti.unit->Body.Container_Unit == unit->Body.ID) && - (! is_allowed_to_trespass (uti.unit))) { - allowed = false; - break; - } + City_Form_draw_border_around_workable_tiles (this); - return ! allowed; - } else - return false; + if (changed_clip_area) + clear_clip_area (this); } bool __fastcall -patch_Unit_check_airdrop_target (Unit * this, int edx, int tile_x, int tile_y) +patch_City_stop_working_polluted_tile (City * this, int edx, int neighbor_index) { - return Unit_check_airdrop_target (this, __, tile_x, tile_y) && - is_below_stack_limit (tile_at (tile_x, tile_y), this->Body.CivID, this->Body.UnitTypeID) && - ! is_airdrop_trespassing (this, tile_x, tile_y); + if (is->current_config.do_not_unassign_workers_from_polluted_tiles && + (*p_human_player_bits & (1 << this->Body.CivID))) + return false; // do nothing; return value is not used + else + return City_stop_working_tile (this, __, neighbor_index); } -bool __fastcall -patch_Unit_can_airlift (Unit * this) +void __fastcall +patch_City_manage_by_governor (City * this, int edx, bool manage_professions) { - bool base = Unit_can_airlift (this); - - if (! (is->current_config.enable_districts && - is->current_config.enable_aerodrome_districts && - is->current_config.air_units_use_aerodrome_districts_not_cities)) - return base; - - Tile * tile = tile_at (this->Body.X, this->Body.Y); - if ((tile == NULL) || (tile == p_null_tile)) - return base; + int * p_stack = (int *)&manage_professions; + int ret_addr = p_stack[-1]; - bool allow_from_non_city = false; - if (base) { - City * city = city_at (this->Body.X, this->Body.Y); - if ((city == NULL) || (city->Body.CivID != this->Body.CivID)) - allow_from_non_city = true; - } + // Do nothing if called after spawning pollution but didn't unassign worker + if ((ret_addr == ADDR_MANAGE_CITY_AFTER_POLLUTION_RETURN) && + is->current_config.do_not_unassign_workers_from_polluted_tiles && + (*p_human_player_bits & (1 << this->Body.CivID))) + return; - if (allow_from_non_city) - return true; + City_manage_by_governor (this, __, manage_professions); +} - return tile_has_friendly_aerodrome_district (tile, this->Body.CivID, true); +City * __cdecl +patch_find_nearest_city_for_ai_alliance_eval (int tile_x, int tile_y, int owner_id, int continent_id, int ignore_owner_id, int perspective_id, City * ignore_city) +{ + City * tr = find_nearest_city (tile_x, tile_y, owner_id, continent_id, ignore_owner_id, perspective_id, ignore_city); + if (is->current_config.patch_division_by_zero_in_ai_alliance_eval && (*p_nearest_city_distance == 0)) + *p_nearest_city_distance = 1; + return tr; } -bool __fastcall -patch_Unit_check_airlift_target (Unit * this, int edx, int tile_x, int tile_y) +int __fastcall +patch_Unit_get_max_move_points (Unit * this) { - bool base = Unit_check_airlift_target (this, __, tile_x, tile_y); - bool allowed = base; + if (Unit_has_ability (this, __, UTA_Army) && is->current_config.patch_empty_army_movement) { + int slowest_member_mp = INT_MAX; + bool any_units_in_army = false; + FOR_UNITS_ON (uti, tile_at (this->Body.X, this->Body.Y)) { + if (uti.unit->Body.Container_Unit == this->Body.ID) { + any_units_in_army = true; + slowest_member_mp = not_above (Unit_get_max_move_points (uti.unit), slowest_member_mp); + } + } + if (any_units_in_army) + return slowest_member_mp + p_bic_data->General.RoadsMovementRate; + else + return get_max_move_points (&p_bic_data->UnitTypes[this->Body.UnitTypeID], this->Body.CivID); + } else + return Unit_get_max_move_points (this); +} - Tile * tile = tile_at (tile_x, tile_y); +char __fastcall +patch_Unit_is_visible_to_civ_for_bouncing (Unit * this, int edx, int civ_id, int param_2) +{ + // If we're set not to kick out invisible units and this one is invis. then report that it's not seen so it's left alone + if (is->do_not_bounce_invisible_units && Unit_has_ability (this, __, UTA_Invisible)) + return 0; + else + return patch_Unit_is_visible_to_civ (this, __, civ_id, param_2); +} - if (is->current_config.enable_districts && - is->current_config.enable_aerodrome_districts && - is->current_config.air_units_use_aerodrome_districts_not_cities) { - if ((tile == NULL) || (tile == p_null_tile)) { - allowed = false; - } else { - City * target_city = city_at (tile_x, tile_y); - if (allowed && - (target_city != NULL) && - (target_city->Body.CivID == this->Body.CivID)) - allowed = false; +void __fastcall +patch_Leader_make_peace (Leader * this, int edx, int civ_id) +{ + Leader_make_peace (this, __, civ_id); - if (! allowed) - allowed = tile_has_friendly_aerodrome_district (tile, this->Body.CivID, false); - } + if (is->current_config.disallow_trespassing && + (! this->At_War[civ_id]) && // Make sure the war actually ended + ((this->Relation_Treaties[civ_id] & 2) == 0)) { // Check right of passage (just in case) + is->do_not_bounce_invisible_units = true; + Leader_bounce_trespassing_units (&leaders[civ_id], __, this->ID); + is->do_not_bounce_invisible_units = false; } - - if (! allowed) - return false; - - return is_below_stack_limit (tile, this->Body.CivID, this->Body.UnitTypeID); } -void __fastcall -patch_Unit_airlift (Unit * this, int edx, int tile_x, int tile_y) +// This patch function replaces calls to Leader::count_wonders_with_flag but is only valid in cases where it only matters whether the return value is +// zero or non-zero. This function will not return the actual count, just zero if no wonders are present and some >0 value if there is at least one. +int __fastcall +patch_Leader_count_any_shared_wonders_with_flag (Leader * this, int edx, enum ImprovementTypeWonderFeatures flag, City * only_in_city) { - Tile * source_tile = NULL; - bool mark_usage = false; - - if (is->current_config.enable_districts && - is->current_config.enable_aerodrome_districts && - is->current_config.air_units_use_aerodrome_districts_not_cities) { - source_tile = tile_at (this->Body.X, this->Body.Y); - if (tile_has_friendly_aerodrome_district (source_tile, this->Body.CivID, true)) - mark_usage = true; - } + int tr = Leader_count_wonders_with_flag (this, __, flag, only_in_city); - Unit_airlift (this, __, tile_x, tile_y); + if ((tr == 0) && + (only_in_city == NULL) && + is->current_config.share_wonders_in_hotseat && + (*p_is_offline_mp_game && ! *p_is_pbem_game) && // is hotseat game + ((1 << this->ID) & *p_human_player_bits)) { // is "this" a human player - if (mark_usage && (source_tile != NULL) && (source_tile != p_null_tile)) { - int mask = itable_look_up_or (&is->aerodrome_airlift_usage, (int)source_tile, 0); - mask |= (1 << this->Body.CivID); - itable_insert (&is->aerodrome_airlift_usage, (int)source_tile, mask); + // Sum up wonders owned by other human players + unsigned player_bits = *(unsigned *)p_human_player_bits >> 1; + int n_player = 1; + while (player_bits != 0) { + if ((player_bits & 1) && (n_player != this->ID)) + if (Leader_count_wonders_with_flag (&leaders[n_player], __, flag, only_in_city) > 0) { + tr = 1; + break; + } + player_bits >>= 1; + n_player++; + } } + + return tr; } int __fastcall -patch_City_count_airports_for_ai_airlift_target (City * this, int edx, enum ImprovementTypeFlags airport_flag) +patch_Leader_count_wonders_with_flag_ignore_great_wall (Leader * this, int edx, enum ImprovementTypeWonderFeatures flag, City * only_in_city) { - // When this function is called, the AI unit being moved is stored in register ESI. - Unit * unit; - __asm__ __volatile__("mov %%esi, %0" : "=r" (unit)); + return patch_Leader_count_any_shared_wonders_with_flag (this, __, flag, only_in_city); - int tr = City_count_improvements_with_flag (this, __, airport_flag); - - // Check the stack limit here. If the city's tile is at the limit, return that it has no airport so the AI can't airlift there. - if ((tr > 0) && ! is_below_stack_limit (tile_at (this->Body.X, this->Body.Y), this->Body.CivID, unit->Body.UnitTypeID)) + if (is->current_config.enable_districts && + is->current_config.disable_great_wall_city_defense_bonus && + flag == ITW_Doubles_City_Defenses) return 0; - else - return tr; + return patch_Leader_count_any_shared_wonders_with_flag (this, __, flag, only_in_city); } +int const shared_small_wonder_flags = + ITSW_Increases_Chance_of_Leader_Appearance | + ITSW_Build_Larger_Armies | + ITSW_Treasury_Earns_5_Percent | + ITSW_Decreases_Success_Of_Missile_Attacks | + ITSW_Allows_Spy_Missions | + ITSW_Allows_Healing_In_Enemy_Territory | + ITSW_Requires_Victorous_Army | + ITSE_Requires_Elite_Naval_Units; + + int __fastcall -patch_Unit_ai_eval_airdrop_target (Unit * this, int edx, int tile_x, int tile_y) +patch_Leader_count_wonders_with_small_flag (Leader * this, int edx, enum ImprovementTypeSmallWonderFeatures flag, City * city_or_null) { - int tr = Unit_ai_eval_airdrop_target (this, __, tile_x, tile_y); + int tr = Leader_count_wonders_with_small_flag (this, __, flag, city_or_null); - // Prevent the AI from airdropping onto tiles in violation of the stack limit or trespassing restriction - if ((tr > 0) && - ((! is_below_stack_limit (tile_at (tile_x, tile_y), this->Body.CivID, this->Body.UnitTypeID)) || - is_airdrop_trespassing (this, tile_x, tile_y))) - tr = 0; + // If "this" is a human player who's sharing wonders in hotseat and the flag is one of the ones that gets shared, include the wonders owned by + // all other humans in the game + if ((city_or_null == NULL) && + (flag & shared_small_wonder_flags) && + is->current_config.share_wonders_in_hotseat && + (*p_is_offline_mp_game && ! *p_is_pbem_game) && // is hotseat game + ((1 << this->ID) & *p_human_player_bits)) { // is "this" a human player + + unsigned player_bits = *(unsigned *)p_human_player_bits >> 1; + int n_player = 1; + while (player_bits != 0) { + if ((player_bits & 1) && (n_player != this->ID)) + tr += Leader_count_wonders_with_small_flag (&leaders[n_player], __, flag, city_or_null); + player_bits >>= 1; + n_player++; + } + } return tr; } -bool __fastcall -patch_Unit_find_telepad_on_tile (Unit * this, int edx, int x, int y, bool show_selection_popup, Unit ** out_unit_telepad) +int +find_human_player_with_small_wonder (int improv_id) { - if (! is_below_stack_limit (tile_at (x, y), this->Body.CivID, this->Body.UnitTypeID)) { - *out_unit_telepad = NULL; - return false; - } else - return Unit_find_telepad_on_tile (this, __, x, y, show_selection_popup, out_unit_telepad); + unsigned player_bits = *(unsigned *)p_human_player_bits >> 1; + int n_player = 1; + while (player_bits != 0) { + if (player_bits & 1) + if (leaders[n_player].Small_Wonders[improv_id] != -1) + return n_player; + player_bits >>= 1; + n_player++; + } + return -1; } bool __fastcall -patch_Unit_ai_go_to_capital (Unit * this) +patch_Leader_can_build_city_improvement (Leader * this, int edx, int i_improv, bool param_2) { - City * capital = get_city_ptr (leaders[this->Body.CivID].CapitalID); + Improvement * improv = &p_bic_data->Improvements[i_improv]; + bool restore = false; + bool already_shared = false; + int saved_status, saved_required_building_count, saved_armies_count; + if ((improv->Characteristics & (ITC_Small_Wonder | ITC_Wonder)) && // if the improv in question is a great or small wonder + is->current_config.share_wonders_in_hotseat && // if we're configured to share wonder effects + (*p_is_offline_mp_game && ! *p_is_pbem_game) && // if in a hotseat game + ((1 << this->ID) & *p_human_player_bits)) { // if "this" is a human player - // Block going to capital if the capital's tile is at the stack limit. This stops the AI from airlifting there (would violate the limit) and - // saves it from trying to pathfind there. - if ((capital != NULL) && - ! is_below_stack_limit (tile_at (capital->Body.X, capital->Body.Y), this->Body.CivID, this->Body.UnitTypeID)) - return false; + // If another human player has already built this small wonder and it has no non-shared effects, then make it unbuildable + if ((improv->Characteristics & ITC_Small_Wonder) && + (find_human_player_with_small_wonder (i_improv) != -1) && + ((improv->SmallWonderFlags & ~shared_small_wonder_flags) == 0)) + already_shared = true; - return Unit_ai_go_to_capital (this); -} + else { + restore = true; + saved_status = this->Status; + if (improv->RequiredBuildingID != -1) + saved_required_building_count = this->Improvement_Counts[improv->RequiredBuildingID]; + saved_armies_count = this->Armies_Count; -bool __fastcall -patch_Unit_is_in_rebase_range (Unit * this, int edx, int tile_x, int tile_y) -{ - bool in_range; + // Loop over all other human players in the game + unsigned player_bits = *(unsigned *)p_human_player_bits >> 1; + int n_player = 1; + while (player_bits != 0) { + if ((player_bits & 1) && (n_player != this->ID)) { - // 6 is the game's standard value, so fall back on the base game logic in that case - if (is->current_config.rebase_range_multiplier == 6) - in_range = Unit_is_in_rebase_range (this, __, tile_x, tile_y); + // Combine status bits + this->Status |= leaders[n_player].Status & (LSF_HAS_VICTORIOUS_ARMY | LSF_HAS_ELITE_NAVAL_UNIT); - else { - int op_range = p_bic_data->UnitTypes[this->Body.UnitTypeID].OperationalRange; - if (op_range < 1) - op_range = 500; + // Combine building counts for the required building if there is one + if (improv->RequiredBuildingID != -1) + this->Improvement_Counts[improv->RequiredBuildingID] += leaders[n_player].Improvement_Counts[improv->RequiredBuildingID]; - int x_dist = Map_get_x_dist (&p_bic_data->Map, __, tile_x, this->Body.X), - y_dist = Map_get_y_dist (&p_bic_data->Map, __, tile_y, this->Body.Y); + // Combine army counts + this->Armies_Count += leaders[n_player].Armies_Count; - in_range = ((x_dist + y_dist) >> 1) <= (op_range * is->current_config.rebase_range_multiplier); + } + player_bits >>= 1; + n_player++; + } + } } - return in_range && is_below_stack_limit (tile_at (tile_x, tile_y), this->Body.CivID, this->Body.UnitTypeID); + bool tr = (! already_shared) && Leader_can_build_city_improvement (this, __, i_improv, param_2); + + if (restore) { + this->Status = saved_status; + if (improv->RequiredBuildingID != -1) + this->Improvement_Counts[improv->RequiredBuildingID] = saved_required_building_count; + + this->Armies_Count = saved_armies_count; + } + return tr; + } -bool __fastcall -patch_Unit_check_rebase_target (Unit * this, int edx, int tile_x, int tile_y) +void __fastcall +patch_City_add_happiness_from_buildings (City * this, int edx, int * inout_happiness, int * inout_unhappiness) { - // Check if this is an air unit - bool is_air_unit = (p_bic_data->UnitTypes[this->Body.UnitTypeID].Unit_Class == UTC_Air); + // If we're in a hotseat game with shared wonder effects, merge all human player's improvement counts together so the base function checks for + // happiness from improvements owned by other human players. + Leader * owner = &leaders[this->Body.CivID]; + bool restore_improv_counts = false; + if (is->current_config.share_wonders_in_hotseat && + (*p_is_offline_mp_game && ! *p_is_pbem_game) && // is hotseat game + ((1 << this->Body.CivID) & *p_human_player_bits)) { // "this" is owned by a human player - // If districts are enabled and this is an air unit, check for aerodrome districts - if (is_air_unit && is->current_config.enable_districts && - is->current_config.enable_aerodrome_districts) { + // Ensure the space we've set aside for saving the real improv counts is large enough + if ((is->saved_improv_counts == NULL) || (is->saved_improv_counts_capacity < p_bic_data->ImprovementsCount)) { + free (is->saved_improv_counts); + is->saved_improv_counts = calloc (p_bic_data->ImprovementsCount, sizeof is->saved_improv_counts[0]); + is->saved_improv_counts_capacity = (is->saved_improv_counts != NULL) ? p_bic_data->ImprovementsCount : 0; + } - Tile * tile = tile_at (tile_x, tile_y); - if ((tile != NULL) && (tile != p_null_tile)) { - // Check if tile has a district - struct district_instance * inst = get_district_instance (tile); - if (inst != NULL) { - int district_id = inst->district_type; - int aerodrome_id = AERODROME_DISTRICT_ID; - // Check if this is an aerodrome district owned by this unit's civ - if ((aerodrome_id >= 0) && (district_id == aerodrome_id) && (tile->Territory_OwnerID == this->Body.CivID)) { - // Check if aerodrome is complete - if (district_is_complete (tile, district_id)) { - // Perform range check - bool in_range = patch_Unit_is_in_rebase_range (this, __, tile_x, tile_y); - if (in_range) { - return is_below_stack_limit (tile, this->Body.CivID, this->Body.UnitTypeID); - } - } - } - } - // If air_units_use_aerodrome_districts_not_cities is enabled, check if there's a city and disallow it - if (is->current_config.air_units_use_aerodrome_districts_not_cities) { - City * target_city = city_at (tile_x, tile_y); - if (target_city != NULL && target_city->Body.CivID == this->Body.CivID) { - // There's a friendly city here - disallow landing since configured to use aerodromes/airfields/carriers only - return false; - } + if (is->saved_improv_counts != NULL) { + // Save the owner's real improv counts and remember to restore them before returning + restore_improv_counts = true; + for (int n = 0; n < p_bic_data->ImprovementsCount; n++) + is->saved_improv_counts[n] = owner->Improvement_Counts[n]; + + // Add in improv counts for wonders from all other human players in the game + unsigned player_bits = *(unsigned *)p_human_player_bits >> 1; + int n_player = 1; + while (player_bits != 0) { + if ((player_bits & 1) && (n_player != owner->ID)) + for (int n = 0; n < p_bic_data->ImprovementsCount; n++) + if (p_bic_data->Improvements[n].Characteristics & (ITC_Wonder | ITC_Small_Wonder)) + owner->Improvement_Counts[n] += leaders[n_player].Improvement_Counts[n]; + player_bits >>= 1; + n_player++; } } + } - // 6 is the game's standard value, so fall back on the base game logic in that case - if (is->current_config.rebase_range_multiplier == 6) { - return Unit_check_rebase_target (this, __, tile_x, tile_y) && is_below_stack_limit (tile_at (tile_x, tile_y), this->Body.CivID, this->Body.UnitTypeID); + City_add_happiness_from_buildings (this, __, inout_happiness, inout_unhappiness); + + if (is->current_config.enable_districts) { + int district_happy = 0; + calculate_district_happiness_bonus (this, &district_happy); + + if (district_happy != 0) + *inout_happiness += district_happy; + } - // Otherwise, we have to redo the range check. Unlike Unit::is_in_rebase_range, the base method here does more than just check the range so we - // want to make sure to call it even if we determine that the target is in range. In that case, set the range to unlimited temporarily so the - // base game's range check passes. - } else { - if (patch_Unit_is_in_rebase_range (this, __, tile_x, tile_y)) { - int * p_op_range = &p_bic_data->UnitTypes[this->Body.UnitTypeID].OperationalRange; - int original_op_range = *p_op_range; - *p_op_range = 0; - bool tr = Unit_check_rebase_target (this, __, tile_x, tile_y); - *p_op_range = original_op_range; - return tr; - } else - return false; + if (restore_improv_counts) { + for (int n = 0; n < p_bic_data->ImprovementsCount; n++) + owner->Improvement_Counts[n] = is->saved_improv_counts[n]; } } int __fastcall -patch_Sprite_draw_already_worked_tile_img (Sprite * this, int edx, PCX_Image * canvas, int pixel_x, int pixel_y, PCX_Color_Table * color_table) +patch_City_count_other_cont_happiness_buildings (City * this, int edx, int improv_id) { - Sprite * to_draw = this; - - if (is->do_not_draw_already_worked_tile_img) - return 0; + int tr = City_count_other_buildings_on_continent (this, __, improv_id); - if (is->current_config.toggle_zoom_with_z_on_city_screen && p_bic_data->is_zoomed_out) { + // If it's a hotseat game where we're sharing wonders, "improv_id" refers to a wonder, and "this" is owned by a human player + if (is->current_config.share_wonders_in_hotseat && + (*p_is_offline_mp_game && ! *p_is_pbem_game) && // is hotseat game + ((p_bic_data->Improvements[improv_id].Characteristics & (ITC_Wonder | ITC_Small_Wonder)) != 0) && + ((1 << this->Body.CivID) & *p_human_player_bits)) { - // Load sprite if necessary - if (is->tile_already_worked_zoomed_out_sprite_init_state == IS_UNINITED) { - is->tile_already_worked_zoomed_out_sprite_init_state = IS_INIT_FAILED; - PCX_Image * pcx = malloc (sizeof *pcx); - if (pcx != NULL) { - memset (pcx, 0, sizeof *pcx); - PCX_Image_construct (pcx); - char path[2*MAX_PATH]; - get_mod_art_path ("TileAlreadyWorkedZoomedOut.pcx", path, sizeof path); - PCX_Image_read_file (pcx, __, path, NULL, 0, 0x100, 2); - if (pcx->JGL.Image != NULL) { - Sprite * sprite = &is->tile_already_worked_zoomed_out_sprite; - memset (sprite, 0, sizeof *sprite); - Sprite_construct (sprite); - Sprite_slice_pcx (sprite, __, pcx, 0, 0, 64, 32, 1, 1); - is->tile_already_worked_zoomed_out_sprite_init_state = IS_OK; - } - pcx->vtable->destruct (pcx, __, 0); - free (pcx); + // Add in instances of this improvment on this continent owned by other human players + Tile * this_city_tile = tile_at (this->Body.X, this->Body.Y); + int this_cont_id = this_city_tile->vtable->m46_Get_ContinentID (this_city_tile); + for (int city_index = 0; city_index <= p_cities->LastIndex; city_index++) { + City * city = get_city_ptr (city_index); + if ((city != NULL) && (city != this) && + (city->Body.CivID != this->Body.CivID) && // if city is owned by a different player AND + ((1 << city->Body.CivID) & *p_human_player_bits)) { // that player is a human + Tile * that_city_tile = tile_at (city->Body.X, city->Body.Y); + int that_cont_id = that_city_tile->vtable->m46_Get_ContinentID (that_city_tile); + if ((this_cont_id == that_cont_id) && patch_City_has_improvement (city, __, improv_id, true)) + tr++; } } - if (is->tile_already_worked_zoomed_out_sprite_init_state == IS_OK) - to_draw = &is->tile_already_worked_zoomed_out_sprite; } - return Sprite_draw (to_draw, __, canvas, pixel_x, pixel_y, color_table); + return tr; } -int __fastcall -patch_Tile_m43_Get_field_30_for_city_loc_eval (Tile * this) +void __fastcall +patch_Leader_update_great_library_unlocks (Leader * this) { - int tr = this->vtable->m43_Get_field_30 (this); + // If it's a hotseat game with shared wonder effects and "this" is a human player, share contacts among all human players so that "this" gets + // techs from civs known to any human player in the game. Save the real contact info and restore it afterward. NOTE: Contact info has to go + // two ways here; we must mark that "this" has contacted the AIs and vice-versa otherwise the techs won't be granted. That's why we must save + // & restore all contact bits for all players. + bool restore_contacts = false; + struct contact_set { + int contacts[32]; + } saved_contact_sets[32]; + if (is->current_config.share_wonders_in_hotseat && + (*p_is_offline_mp_game && ! *p_is_pbem_game) && // is hotseat game + ((1 << this->ID) & *p_human_player_bits)) { // "this" is a human player - // This patch function replaces two places where ai_eval_city_location calls Tile::m43_Get_field_30 to check the 18th bit, which indicates - // whether the tile is in the workable area of any city. If it is but the work area has been expanded, check that it's actually within the - // normal 20-tile area. This prevents work area expansion from causing AIs to space their cities further apart. - // The field_30_get_counter is a crazy workaround for the fact that the game doesn't have any way to determine a tile's coordinates given a - // pointer to its object. So we track the initial (x, y) for the tile we're evaluating and then count calls to this func to know what - // neighboring coords "this" corresponds to. - int get_counter = is->ai_evaling_city_field_30_get_counter; - if (((tr >> 17) & 1) && (is->current_config.city_work_radius >= 3)) { - bool found_city = false; - int this_x, this_y; { - int dx, dy; - patch_ni_to_diff_for_work_area (get_counter, &dx, &dy); - this_x = is->ai_evaling_city_loc_x + dx; - this_y = is->ai_evaling_city_loc_y + dy; - wrap_tile_coords (&p_bic_data->Map, &this_x, &this_y); + restore_contacts = true; + for (int n = 0; n < 32; n++) + for (int k = 0; k < 32; k++) + saved_contact_sets[n].contacts[k] = leaders[n].Contacts[k]; + + unsigned human_player_bits = *(unsigned *)p_human_player_bits >> 1; + int n_human = 1; + while (human_player_bits != 0) { + if ((human_player_bits & 1) && (n_human != this->ID)) // n_human is ID of a human player other than "this" + for (int n_ai = 0; n_ai < 32; n_ai++) + if ((*p_player_bits & (1 << n_ai)) && ((*p_human_player_bits & (1 << n_ai)) == 0)) { + // If the human and AI players have contact, mark "this" as having contact with the AI and vice-versa + if (leaders[n_human].Contacts[n_ai] & 1) { + this->Contacts[n_ai] |= 1; + leaders[n_ai].Contacts[this->ID] |= 1; + } + } + human_player_bits >>= 1; + n_human++; } - FOR_TILES_AROUND (tai, 21, this_x, this_y) - if (Tile_has_city (tai.tile)) { - found_city = true; - break; - } - if (! found_city) - tr &= ~(1 << 17); } - get_counter++; - if (get_counter >= 21) - get_counter = 0; - is->ai_evaling_city_field_30_get_counter = get_counter; - return tr; + Leader_update_great_library_unlocks (this); + if (restore_contacts) + for (int n = 0; n < 32; n++) + for (int k = 0; k < 32; k++) + leaders[n].Contacts[k] = saved_contact_sets[n].contacts[k]; } -// Intercept the call where the game gets a random index at which to start searching for a suitable tile to become polluted. Normally, it passes 20 as -// the limit here. We must replace that to cover a potentially modded work area. -int __fastcall -patch_rand_int_to_place_pollution (void * this, int edx, int lim) +bool __fastcall +patch_Leader_has_wonder_doubling_happiness_from (Leader * this, int edx, int improv_id) { - return rand_int (this, __, is->workable_tile_count - 1); -} + bool tr = Leader_has_wonder_doubling_happiness_from (this, __, improv_id); -void __fastcall -patch_Main_Screen_Form_t2s_coords_to_draw_yields (Main_Screen_Form * this, int edx, int tile_x, int tile_y, int * out_x, int * out_y) -{ - Main_Screen_Form_tile_to_screen_coords (this, __, tile_x, tile_y, out_x, out_y); + // If we're sharing wonder effects in a hotseat game and "this" is a human player, return true when another human player in the game has a + // wonder doubling happiness + if ((! tr) && + is->current_config.share_wonders_in_hotseat && + (*p_is_offline_mp_game && ! *p_is_pbem_game) && // is hotseat game + ((1 << this->ID) & *p_human_player_bits)) { // "this" is a human player + unsigned human_player_bits = *(unsigned *)p_human_player_bits >> 1; + int n_human = 1; + while (human_player_bits != 0) { + if ((human_player_bits & 1) && + (n_human != this->ID) && + Leader_has_wonder_doubling_happiness_from (&leaders[n_human], __, improv_id)) { + tr = true; + break; + } + human_player_bits >>= 1; + n_human++; + } + } - // If we've zoomed out the map on the city screen, we may end up drawing the tile yields off screen depending on the map size. If this is the - // case, detected by negative screen coords, then shift the coords over by one map-screen back onto the actual screen. The shift amounts are - // 64*map_width/2 and 32*map_height/2 for x and y because (64, 32) is the size of a zoomed out tile and /2 is for overlap (I think). - if (p_bic_data->is_zoomed_out && (*out_x < 0)) - *out_x += p_bic_data->Map.Width << 5; - if (p_bic_data->is_zoomed_out && (*out_y < 0)) - *out_y += p_bic_data->Map.Height << 4; + return tr; } -void -set_clip_area_to_map_view (City_Form * city_form) +int __fastcall +patch_Sprite_draw_citizen_head (Sprite * this, int edx, PCX_Image * canvas, int pixel_x, int pixel_y, PCX_Color_Table * color_table) { - int left_margin = (p_bic_data->ScreenWidth - city_form->Background_Image.Width) / 2, - top_margin = (p_bic_data->ScreenHeight - city_form->Background_Image.Height) / 2; - RECT map_view_on_screen = { .left = left_margin, .top = top_margin + 92, .right = left_margin + 1024, .bottom = top_margin + 508 }; - JGL_Image * jgl_canvas = city_form->Base.Data.Canvas.JGL.Image; - jgl_canvas->vtable->m13_Set_Clip_Region (jgl_canvas, __, &map_view_on_screen); + // Reset variable + is->specialist_icon_drawing_running_x = INT_MIN; + + return Sprite_draw (this, __, canvas, pixel_x, pixel_y, color_table); } -void -clear_clip_area (City_Form * city_form) +int +adjust_specialist_yield_icon_x (int pixel_x, int width) { - JGL_Image * jgl_canvas = city_form->Base.Data.Canvas.JGL.Image; - jgl_canvas->vtable->m13_Set_Clip_Region (jgl_canvas, __, &jgl_canvas->Image_Rect); + if (is->current_config.fix_overlapping_specialist_yield_icons) { + if (is->specialist_icon_drawing_running_x == INT_MIN) // first icon drawn + is->specialist_icon_drawing_running_x = pixel_x; + int tr = is->specialist_icon_drawing_running_x; + is->specialist_icon_drawing_running_x += width; + return tr; + } else + return pixel_x; } -void -init_distribution_hub_icons () +int __fastcall +patch_Sprite_draw_entertainer_yield_icon (Sprite * this, int edx, PCX_Image * canvas, int pixel_x, int pixel_y, PCX_Color_Table * color_table) { - if (is->distribution_hub_icons_img_state != IS_UNINITED) - return; - - PCX_Image pcx; - PCX_Image_construct (&pcx); - - char ss[200]; - snprintf (ss, sizeof ss, "[C3X] init_distribution_hub_icons: state=%d\n", is->distribution_hub_icons_img_state); - (*p_OutputDebugStringA) (ss); - - char temp_path[2*MAX_PATH]; - get_mod_art_path ("Districts/DistributionHubIncomeIcons.pcx", temp_path, sizeof temp_path); - - PCX_Image_read_file (&pcx, __, temp_path, NULL, 0, 0x100, 2); - if ((pcx.JGL.Image == NULL) || - (pcx.JGL.Image->vtable->m54_Get_Width (pcx.JGL.Image) < 776) || - (pcx.JGL.Image->vtable->m55_Get_Height (pcx.JGL.Image) < 32)) { - (*p_OutputDebugStringA) ("[C3X] PCX file for distribution hub icons failed to load or is too small.\n"); - is->distribution_hub_icons_img_state = IS_INIT_FAILED; - goto cleanup; - } - - // Extract shield icon (index 4: x = 1 + 4*31 = 125, width 30) - Sprite_construct (&is->distribution_hub_shield_icon); - Sprite_slice_pcx (&is->distribution_hub_shield_icon, __, &pcx, 1 + 4*31, 1, 30, 30, 1, 1); - - // Extract shield corruption icon (index 5: x = 1 + 5*31 = 156, width 30) - Sprite_construct (&is->distribution_hub_corruption_icon); - Sprite_slice_pcx (&is->distribution_hub_corruption_icon, __, &pcx, 1 + 5*31, 1, 30, 30, 1, 1); - - // Extract small shield icon (index 13) - Sprite_construct (&is->distribution_hub_shield_icon_small); - Sprite_slice_pcx (&is->distribution_hub_shield_icon_small, __, &pcx, 1 + 13*31, 1, 30, 30, 1, 1); - - // Extract surplus food icon (index 6: x = 1 + 6*31 = 187, width 30) - Sprite_construct (&is->distribution_hub_food_icon); - Sprite_slice_pcx (&is->distribution_hub_food_icon, __, &pcx, 1 + 6*31, 1, 30, 30, 1, 1); - - // Extract small surplus food icon (index 15) - Sprite_construct (&is->distribution_hub_food_icon_small); - Sprite_slice_pcx (&is->distribution_hub_food_icon_small, __, &pcx, 1 + 15*31, 1, 30, 30, 1, 1); - - // Extract eaten food icon (index 7: x = 1 + 7*31 = 218, width 30) - Sprite_construct (&is->distribution_hub_eaten_food_icon); - Sprite_slice_pcx (&is->distribution_hub_eaten_food_icon, __, &pcx, 1 + 7*31, 1, 30, 30, 1, 1); - - is->distribution_hub_icons_img_state = IS_OK; -cleanup: - pcx.vtable->destruct (&pcx, __, 0); + int width = p_city_form->City_Icons_Images.Icon_12_Happy_Faces.Width / 2; + return Sprite_draw (this, __, canvas, adjust_specialist_yield_icon_x (pixel_x, width), pixel_y, color_table); +} +int __fastcall +patch_Sprite_draw_scientist_yield_icon (Sprite * this, int edx, PCX_Image * canvas, int pixel_x, int pixel_y, PCX_Color_Table * color_table) +{ + int width = p_city_form->City_Icons_Images.Icon_16_Science.Width / 2; + return Sprite_draw (this, __, canvas, adjust_specialist_yield_icon_x (pixel_x, width), pixel_y, color_table); } - -void -init_district_icons () +int __fastcall +patch_Sprite_draw_tax_collector_yield_icon (Sprite * this, int edx, PCX_Image * canvas, int pixel_x, int pixel_y, PCX_Color_Table * color_table) { - if (is->dc_icons_img_state != IS_UNINITED) - return; + int width = p_city_form->City_Icons_Images.Icon_14_Gold.Width / 2; + return Sprite_draw (this, __, canvas, adjust_specialist_yield_icon_x (pixel_x, width), pixel_y, color_table); +} +int __fastcall +patch_Sprite_draw_civil_engineer_yield_icon (Sprite * this, int edx, PCX_Image * canvas, int pixel_x, int pixel_y, PCX_Color_Table * color_table) +{ + int width = p_city_form->City_Icons_Images.Icon_13_Shield.Width / 2; + return Sprite_draw (this, __, canvas, adjust_specialist_yield_icon_x (pixel_x, width), pixel_y, color_table); +} +int __fastcall +patch_Sprite_draw_police_officer_yield_icon (Sprite * this, int edx, PCX_Image * canvas, int pixel_x, int pixel_y, PCX_Color_Table * color_table) +{ + int width = p_city_form->City_Icons_Images.Icon_17_Gold_Outcome.Width / 2; + return Sprite_draw (this, __, canvas, adjust_specialist_yield_icon_x (pixel_x, width), pixel_y, color_table); +} - char ss[200]; - snprintf (ss, sizeof ss, "[C3X] init_district_icons: state=%d\n", is->dc_icons_img_state); - (*p_OutputDebugStringA) (ss); +void __fastcall +patch_City_add_building_if_done (City * this) +{ + // If sharing small wonders in hotseat, check whether the city is building a small wonder that's no longer available to the player b/c its + // effects are provided from another. + int improv_id = this->Body.Order_ID; + Improvement * improv = &p_bic_data->Improvements[improv_id]; + int already_built_by_id; + if ((improv->Characteristics & ITC_Small_Wonder) && + is->current_config.share_wonders_in_hotseat && + (*p_is_offline_mp_game && ! *p_is_pbem_game) && // is hotseat game + ((1 << this->Body.CivID) & *p_human_player_bits) && // city owned by a human player + ((already_built_by_id = find_human_player_with_small_wonder (improv_id)) != -1) && // SW already built by another human player + ((improv->SmallWonderFlags & ~shared_small_wonder_flags) == 0)) { // SW has no non-shared effects - PCX_Image pcx; - PCX_Image_construct (&pcx); + // Switch city production to something else and notify the player + this->vtable->set_production_to_most_expensive_option (this); + if ((this->Body.CivID == p_main_screen_form->Player_CivID) && (already_built_by_id != p_main_screen_form->Player_CivID)) { + char * new_build_name = (this->Body.Order_Type == COT_Improvement) ? + p_bic_data->Improvements[this->Body.Order_ID].Name.S : + p_bic_data->UnitTypes[this->Body.Order_ID].Name; + PopupForm * popup = get_popup_form (); + set_popup_str_param (0, this->Body.CityName, -1, -1); + set_popup_str_param (1, improv->Name.S, -1, -1); + set_popup_str_param (2, Leader_get_civ_noun (&leaders[already_built_by_id]), -1, -1); + set_popup_str_param (3, new_build_name, -1, -1); + popup->vtable->set_text_key_and_flags (popup, __, is->mod_script_path, "C3X_SHARED_WONDER_CHANGE", -1, 0, 0, 0); + int response = patch_show_popup (popup, __, 0, 0); + if (response == 0) + *p_zoom_to_city_after_update = true; + } - char temp_path[2*MAX_PATH]; - get_mod_art_path ("Districts/DistrictIncomeIcons.pcx", temp_path, sizeof temp_path); + // As in the base logic, if production gets switched, the game doesn't check if it might still complete on the same turn. + return; + } - PCX_Image_read_file (&pcx, __, temp_path, NULL, 0, 0x100, 2); - if ((pcx.JGL.Image == NULL) || - (pcx.JGL.Image->vtable->m54_Get_Width (pcx.JGL.Image) < 776) || - (pcx.JGL.Image->vtable->m55_Get_Height (pcx.JGL.Image) < 32)) { - (*p_OutputDebugStringA) ("[C3X] PCX file for district icons failed to load or is too small.\n"); - is->dc_icons_img_state = IS_INIT_FAILED; - goto cleanup; + // If production ended up on a wonder, make sure the city can actually build it; otherwise fall back to a defensive unit. + int order_type = this->Body.Order_Type; + int order_id = this->Body.Order_ID; + if (is->current_config.enable_districts && order_type == COT_Improvement) { + Improvement * new_improv = &p_bic_data->Improvements[order_id]; // Improvement might have changed + if (new_improv->Characteristics & (ITC_Wonder | ITC_Small_Wonder)) { + if (! (City_can_build_improvement (this, __, order_id, false) && city_meets_district_prereqs_to_build_improvement (this, order_id, true))) { + char ss[256]; + snprintf (ss, sizeof ss, "patch_City_add_building_if_done: City '%s' cannot complete building of wonder '%s'; switching to defensive unit.\n", + this->Body.CityName, + new_improv->Name.S); + (*p_OutputDebugStringA) (ss); + City_Order defensive_order = { .OrderID = -1, .OrderType = 0 }; + if (choose_defensive_unit_order (this, &defensive_order)) { + UnitType * def_type = &p_bic_data->UnitTypes[defensive_order.OrderID]; + if (def_type->Unit_Class == UTC_Land) { + City_set_production (this, __, defensive_order.OrderType, defensive_order.OrderID, false); + return; + } + } + } + } } - // Extract science icon (index 1: x = 1 + 1*31 = 32, width 30) - Sprite_construct (&is->district_science_icon); - Sprite_slice_pcx (&is->district_science_icon, __, &pcx, 1 + 1*31, 1, 30, 30, 1, 1); - - // Extract commerce icon (index 2: x = 1 + 2*31 = 63, width 30) - Sprite_construct (&is->district_commerce_icon); - Sprite_slice_pcx (&is->district_commerce_icon, __, &pcx, 1 + 2*31, 1, 30, 30, 1, 1); + City_add_building_if_done (this); +} - // Extract shield icon (index 4: x = 1 + 4*31 = 125, width 30) - Sprite_construct (&is->district_shield_icon); - Sprite_slice_pcx (&is->district_shield_icon, __, &pcx, 1 + 4*31, 1, 30, 30, 1, 1); +bool __fastcall +patch_City_can_build_upgrade_type (City * this, int edx, int unit_type_id, bool exclude_upgradable, int param_3, bool allow_kings) +{ + UnitType * type = &p_bic_data->UnitTypes[unit_type_id]; + if (is->current_config.prevent_old_units_from_upgrading_past_ability_block && + ((type->Special_Actions & UCV_Upgrade_Unit) == 0) && + (type->Available_To & (1 << leaders[this->Body.CivID].RaceID))) + exclude_upgradable = false; - // Extract corruption icon (index 5: x = 1 + 5*31 = 156, width 30) - Sprite_construct (&is->district_corruption_icon); - Sprite_slice_pcx (&is->district_corruption_icon, __, &pcx, 1 + 5*31, 1, 30, 30, 1, 1); + return patch_City_can_build_unit (this, __, unit_type_id, exclude_upgradable, param_3, allow_kings); +} - // Extract food icon (index 6: x = 1 + 6*31 = 187, width 30) - Sprite_construct (&is->district_food_icon); - Sprite_slice_pcx (&is->district_food_icon, __, &pcx, 1 + 6*31, 1, 30, 30, 1, 1); +void __fastcall +patch_Main_GUI_position_elements (Main_GUI * this) +{ + Main_GUI_position_elements (this); - // Extract food eaten icon (index 7: x = 1 + 7*31 = 218, width 30) - Sprite_construct (&is->district_food_eaten_icon); - Sprite_slice_pcx (&is->district_food_eaten_icon, __, &pcx, 1 + 7*31, 1, 30, 30, 1, 1); + // Double size of minimap if configured + bool want_larger_minimap = (is->current_config.double_minimap_size == MDM_ALWAYS) || + ((is->current_config.double_minimap_size == MDM_HIGH_DEF) && (p_bic_data->ScreenWidth >= 1920)); + if (want_larger_minimap && (init_large_minimap_frame () == IS_OK)) { + this->Mini_Map_Click_Rect.top -= 105; + this->Mini_Map_Click_Rect.right += 229; + } +} - // Extract small shield icon (index 13: x = 1 + 13*31 = 404, width 30) - Sprite_construct (&is->district_shield_icon_small); - Sprite_slice_pcx (&is->district_shield_icon_small, __, &pcx, 1 + 13*31, 1, 30, 30, 1, 1); +#define PEDIA_DESC_LINES_PER_PAGE 38 - // Extract small commerce icon (index 14: x = 1 + 14*31 = 435, width 30) - Sprite_construct (&is->district_commerce_icon_small); - Sprite_slice_pcx (&is->district_commerce_icon_small, __, &pcx, 1 + 14*31, 1, 30, 30, 1, 1); +// Returns whether or not the line should be drawn +bool +do_next_line_for_pedia_desc (PCX_Image * canvas, int * inout_y) +{ + if (is->cmpd.drawing_lines && is->current_config.allow_multipage_civilopedia_descriptions) { + int first_line_on_shown_page = is->cmpd.shown_page * PEDIA_DESC_LINES_PER_PAGE; + int page = is->cmpd.line_count / PEDIA_DESC_LINES_PER_PAGE; + is->cmpd.line_count += 1; + is->cmpd.last_page = (page > is->cmpd.last_page) ? page : is->cmpd.last_page; - // Extract small food icon (index 15: x = 1 + 15*31 = 466, width 30) - Sprite_construct (&is->district_food_icon_small); - Sprite_slice_pcx (&is->district_food_icon_small, __, &pcx, 1 + 15*31, 1, 30, 30, 1, 1); + if (page == is->cmpd.shown_page) { + *inout_y -= is->cmpd.shown_page * PEDIA_DESC_LINES_PER_PAGE * PCX_Image_get_text_line_height (canvas); + return true; + } else + return false; + } + return true; +} - // Extract small science icon (index 16: x = 1 + 16*31 = 497, width 30) - Sprite_construct (&is->district_science_icon_small); - Sprite_slice_pcx (&is->district_science_icon_small, __, &pcx, 1 + 16*31, 1, 30, 30, 1, 1); +int __fastcall +patch_PCX_Image_do_draw_centered_text_in_wrap_func (PCX_Image * this, int edx, char * str, int x, int y, int width, unsigned str_len) +{ + if (do_next_line_for_pedia_desc (this, &y)) + return PCX_Image_do_draw_centered_text (this, __, str, x, y, width, str_len); + else + return 0; // Caller does not use return value +} - // Extract small culture icon (index 18: x = 1 + 18*31 = 559, width 30) - Sprite_construct (&is->district_culture_icon_small); - Sprite_slice_pcx (&is->district_culture_icon_small, __, &pcx, 1 + 18*31, 1, 30, 30, 1, 1); +int __fastcall +patch_PCX_Image_draw_text_in_wrap_func (PCX_Image * this, int edx, char * str, int x, int y, int str_len) +{ + if (do_next_line_for_pedia_desc (this, &y)) + return PCX_Image_draw_text (this, __, str, x, y, str_len); + else + return PCX_Image_draw_text (this, __, " ", x, y, 1); // Caller uses the return value here so draw an empty string isntead of doing nothing +} - is->dc_icons_img_state = IS_OK; -cleanup: - pcx.vtable->destruct (&pcx, __, 0); +// Steam code is slightly different; this no-len version of draw_text gets called sometimes. It's the same as draw_text instead it computes the string +// length itself instead of taking it in as a parameter. +int __fastcall +patch_PCX_Image_draw_text_no_len_in_wrap_func (PCX_Image * this, int edx, char * str, int x, int y) +{ + return patch_PCX_Image_draw_text_in_wrap_func (this, __, str, x, y, strlen (str)); } void -draw_district_yields (City_Form * city_form, Tile * tile, int district_id, int screen_x, int screen_y) +draw_civilopedia_article (void (__fastcall * base) (Civilopedia_Article *), Civilopedia_Article * article) { - // Lazy load district icons - if (is->dc_icons_img_state == IS_UNINITED) - init_district_icons (); - if (is->dc_icons_img_state != IS_OK) - return; - - if (district_id < 0 || district_id >= is->district_count) - return; - - // Get district configuration - struct district_config const * config = &is->district_configs[district_id]; - struct district_instance * inst = get_district_instance (tile); - - // Count total yields from bonuses - int food_bonus = 0, shield_bonus = 0, gold_bonus = 0, science_bonus = 0, culture_bonus = 0; - get_effective_district_yields (inst, config, &food_bonus, &shield_bonus, &gold_bonus, &science_bonus, &culture_bonus); - - int total_yield = 0; - if (food_bonus > 0) total_yield += food_bonus; - if (shield_bonus > 0) total_yield += shield_bonus; - if (gold_bonus > 0) total_yield += gold_bonus; - if (science_bonus > 0) total_yield += science_bonus; - if (culture_bonus > 0) total_yield += culture_bonus; - - if (total_yield <= 0) - return; + // If the article changed then clear things from the old one + if (is->cmpd.article != article) { + is->cmpd.last_page = 0; + is->cmpd.shown_page = 0; + is->cmpd.article = article; + } - // Get sprites - Sprite * food_sprite = &is->district_food_icon_small; - Sprite * shield_sprite = &is->district_shield_icon_small; - Sprite * commerce_sprite = &is->district_commerce_icon_small; - Sprite * science_sprite = &is->district_science_icon_small; - Sprite * culture_sprite = &is->district_culture_icon_small; + is->cmpd.line_count = 0; + is->cmpd.drawing_lines = article->show_description; - // Determine sprite dimensions - int sprite_width = food_sprite->Width3; - int sprite_height = food_sprite->Height; + base (article); - // Calculate total width of all icons - int total_width = total_yield * sprite_width; + is->cmpd.drawing_lines = false; +} - // Center the icons horizontally - int half_width = total_width >> 1; - int tile_width = p_bic_data->is_zoomed_out ? 64 : 128; - int max_offset = (tile_width >> 1) - 5; - if (half_width > max_offset) - half_width = max_offset; +void __fastcall +patch_Civilopedia_Article_m01_Draw_GCON_or_RACE (Civilopedia_Article * this) +{ + draw_civilopedia_article (Civilopedia_Article_m01_Draw_GCON_or_RACE, this); +} - int pixel_x = screen_x - half_width; - int pixel_y = screen_y - (sprite_height >> 1); +void __fastcall +patch_Civilopedia_Article_m01_Draw_UNIT (Civilopedia_Article * this) +{ + draw_civilopedia_article (Civilopedia_Article_m01_Draw_UNIT, this); +} - // Adjust spacing if icons would exceed tile width - int spacing = sprite_width; - if (total_width > tile_width - 10) { - if (total_yield > 1) { - spacing = (tile_width - 10 - sprite_width) / (total_yield - 1); - if (spacing < 1) - spacing = 1; - else if (spacing > sprite_width) - spacing = sprite_width; - } - } +void __fastcall +patch_Civilopedia_Form_m53_On_Control_Click (Civilopedia_Form * this, int edx, CivilopediaControlID control_id) +{ + Civilopedia_Article * current_article = (p_civilopedia_form->Current_Article_ID >= 0) ? p_civilopedia_form->Articles[p_civilopedia_form->Current_Article_ID] : NULL; - // Draw icons in order: shields, food, science, commerce, culture - for (int i = 0; i < shield_bonus; i++) { - Sprite_draw (shield_sprite, __, &city_form->Base.Data.Canvas, pixel_x, pixel_y, NULL); - pixel_x += spacing; - } + // "Effects" button leaves description mode, returns to showing effects + if ((control_id == PEDIA_MULTIPAGE_EFFECTS_BUTTON_ID) && (current_article != NULL)) { + current_article->show_description = false; + is->cmpd.shown_page = 0; + play_sound_effect (26); // 26 = SE_SELECT + p_civilopedia_form->Base.vtable->m73_call_m22_Draw ((Base_Form *)p_civilopedia_form); - for (int i = 0; i < food_bonus; i++) { - Sprite_draw (food_sprite, __, &city_form->Base.Data.Canvas, pixel_x, pixel_y, NULL); - pixel_x += spacing; - } + // "Previous" button shows the previous page of a multi-page description or switches to effects mode if on the first page + } else if (control_id == PEDIA_MULTIPAGE_PREV_BUTTON_ID) { + if (is->cmpd.shown_page > 0) + is->cmpd.shown_page -= 1; + else + current_article->show_description = false; + play_sound_effect (26); + p_civilopedia_form->Base.vtable->m73_call_m22_Draw ((Base_Form *)p_civilopedia_form); - for (int i = 0; i < science_bonus; i++) { - Sprite_draw (science_sprite, __, &city_form->Base.Data.Canvas, pixel_x, pixel_y, NULL); - pixel_x += spacing; - } + } else if ((control_id == CCID_DESCRIPTION_BTN) && // if description/more/prev button was clicked AND + (current_article != NULL) && current_article->show_description && // currently showing a description of an article AND + (is->cmpd.last_page > 0)) { // this is a multi-page description - for (int i = 0; i < gold_bonus; i++) { - Sprite_draw (commerce_sprite, __, &city_form->Base.Data.Canvas, pixel_x, pixel_y, NULL); - pixel_x += spacing; - } + // Show the next page of the multi-page description unless it's a two-page description and we're on the second page, in which case go + // back to the first. + if ((is->cmpd.last_page == 1) && (is->cmpd.shown_page == 1)) + is->cmpd.shown_page = 0; + else + is->cmpd.shown_page = not_above (is->cmpd.last_page, is->cmpd.shown_page + 1); + play_sound_effect (26); + p_civilopedia_form->Base.vtable->m73_call_m22_Draw ((Base_Form *)p_civilopedia_form); - for (int i = 0; i < culture_bonus; i++) { - Sprite_draw (culture_sprite, __, &city_form->Base.Data.Canvas, pixel_x, pixel_y, NULL); - pixel_x += spacing; - } + } else + Civilopedia_Form_m53_On_Control_Click (this, __, control_id); } -void -draw_distribution_hub_yields (City_Form * city_form, Tile * tile, int tile_x, int tile_y, int screen_x, int screen_y) +void __fastcall +patch_Civilopedia_Form_m22_Draw (Civilopedia_Form * this) { - // Get the distribution hub record for this tile - struct distribution_hub_record * rec = get_distribution_hub_record (tile); - if (rec == NULL) - return; - - City * anchor_city = get_connected_city_for_distribution_hub (rec); - if (! distribution_hub_accessible_to_city (rec, city_form->CurrentCity)) - return; - - int food_yield = rec->food_yield; - int shield_yield = rec->shield_yield; - int total_yield = food_yield + shield_yield; + // Make sure the new buttons are not visible and the multipage variables are cleared when we exit description mode + if ((this->Current_Article_ID < 0) || ! this->Articles[this->Current_Article_ID]->show_description) { + is->cmpd.shown_page = is->cmpd.last_page = 0; + if (is->cmpd.effects_btn != NULL) + is->cmpd.effects_btn->vtable->m02_Show_Disabled ((Base_Form *)is->cmpd.effects_btn); + if (is->cmpd.previous_btn != NULL) + is->cmpd.previous_btn->vtable->m02_Show_Disabled ((Base_Form *)is->cmpd.previous_btn); + } - if (total_yield <= 0) - return; + Civilopedia_Form_m22_Draw (this); +} - // Lazy load distribution hub icons - if (is->distribution_hub_icons_img_state == IS_UNINITED) - init_distribution_hub_icons (); - if (is->distribution_hub_icons_img_state != IS_OK) - return; +int __fastcall +patch_Button_initialize_civilopedia_description (Button * this, int edx, char * text, int control_id, int x, int y, int width, int height, Base_Form * parent, int param_8) +{ + Civilopedia_Article * current_article = (p_civilopedia_form->Current_Article_ID >= 0) ? p_civilopedia_form->Articles[p_civilopedia_form->Current_Article_ID] : NULL; + if (current_article == NULL) + return Button_initialize (this, __, text, control_id, x, y, width, height, parent, param_8); - Sprite * food_sprite = &is->distribution_hub_food_icon_small; - Sprite * shield_sprite = &is->distribution_hub_shield_icon_small; + // Set button visibility for multi-page descriptions if we're showing such a thing right now + bool show_desc_btn = true, show_effects_btn = false, show_previous_btn = false; + char * desc_btn_text = text; + if (current_article->show_description && (is->cmpd.last_page > 0)) { - if (food_sprite->Width3 == 0) food_sprite = &is->distribution_hub_food_icon; - if (shield_sprite->Width3 == 0) shield_sprite = &is->distribution_hub_shield_icon; + // Tribe articles act like one long descripton. + if ((current_article->article_kind == CAK_TRIBE) || (current_article->article_kind == CAK_GAME_CONCEPT)) { - int sprite_height = food_sprite->Height; - if (sprite_height == 0) sprite_height = shield_sprite->Height; + // Show the more button as long as we're not on the last page. Show the previous always since we're in description mode. + show_previous_btn = true; + desc_btn_text = (*p_labels)[LBL_MORE]; + if (is->cmpd.shown_page >= is->cmpd.last_page) + show_desc_btn = false; - int food_width = food_sprite->Width3; - int shield_width = shield_sprite->Width3; - if ((food_width <= 0) && (shield_width > 0)) food_width = shield_width; - if ((shield_width <= 0) && (food_width > 0)) shield_width = food_width; + // Unit articles have separate description/effects modes. + } else if (current_article->article_kind == CAK_UNIT) { - // Calculate total width of all icons - int total_width = 0; - if (food_yield > 0) total_width += food_width * food_yield; - if (shield_yield > 0) total_width += shield_width * shield_yield; + // For a two-page description, show the effects button and the description button which will act as a next/previous button + if (is->cmpd.last_page == 1) { + show_effects_btn = true; + desc_btn_text = is->cmpd.shown_page == 0 ? (*p_labels)[LBL_MORE] : (*p_labels)[LBL_PREVIOUS]; - // Center the icons horizontally - int half_width = total_width >> 1; - int tile_width = p_bic_data->is_zoomed_out ? 64 : 128; - int max_offset = (tile_width >> 1) - 5; - if (half_width > max_offset) half_width = max_offset; + // For a three or more page description, show the effects button, and show the description button only if we're not on the + // last page (b/c it's the next button), and show the previous button only if we're not on the first page. If the desc button + // is visible, make it say "More". + } else { + show_effects_btn = true; + if (is->cmpd.shown_page >= is->cmpd.last_page) + show_desc_btn = false; + else + desc_btn_text = (*p_labels)[LBL_MORE]; + show_previous_btn = is->cmpd.shown_page > 0; + } + } + } - int pixel_x = screen_x - half_width; - int pixel_y = screen_y - (sprite_height >> 1); + int tr = Button_initialize (this, __, desc_btn_text, control_id, x, y, width, height, parent, param_8); - // Adjust spacing if icons would exceed tile width - int spacing_food = food_width; - int spacing_shield = shield_width; + if (! show_desc_btn) + this->vtable->m02_Show_Disabled ((Base_Form *)this); - if (total_width > tile_width - 10) { - if (total_yield > 1) { - int spacing = (tile_width - 10 - food_width) / (total_yield - 1); - if (spacing < 1) - spacing = 1; - else if (spacing > food_width) - spacing = food_width; - spacing_food = spacing; - spacing_shield = spacing; - } + if (is->cmpd.effects_btn != NULL) { + if (show_effects_btn) + is->cmpd.effects_btn->vtable->m01_Show_Enabled ((Base_Form *)is->cmpd.effects_btn, __, 0); + else + is->cmpd.effects_btn->vtable->m02_Show_Disabled ((Base_Form *)is->cmpd.effects_btn); } - // Draw food icons first - for (int i = 0; i < food_yield; i++) { - Sprite_draw (food_sprite, __, &city_form->Base.Data.Canvas, pixel_x, pixel_y, NULL); - pixel_x += spacing_food; + if (is->cmpd.previous_btn != NULL) { + if (show_previous_btn) + is->cmpd.previous_btn->vtable->m01_Show_Enabled ((Base_Form *)is->cmpd.previous_btn, __, 0); + else + is->cmpd.previous_btn->vtable->m02_Show_Disabled ((Base_Form *)is->cmpd.previous_btn); } - // Draw shield icons - for (int i = 0; i < shield_yield; i++) { - Sprite_draw (shield_sprite, __, &city_form->Base.Data.Canvas, pixel_x, pixel_y, NULL); - pixel_x += spacing_shield; - } + return tr; } -void __fastcall -patch_City_Form_draw_yields_on_worked_tiles (City_Form * this) +int __fastcall +patch_Civilopedia_Form_m68_Show_Dialog (Civilopedia_Form * this, int edx, int param_1, void * param_2, void * param_3) { - // If we're zoomed in and the city work radius is at least 4 then it's likely we'll end up drawing things outside of the city screen's usual - // map area. Set the clip area to the map area so none of those draws are visible. - bool changed_clip_area = false; - if ((is->current_config.city_work_radius >= 4) && ! p_bic_data->is_zoomed_out) { - set_clip_area_to_map_view (this); - changed_clip_area = true; - } + memset (&is->cmpd, 0, sizeof is->cmpd); - if (is->current_config.enable_districts && this->CurrentCity != NULL) { - recompute_city_yields_with_districts (this->CurrentCity); - } + Button * bs[] = {malloc (sizeof Button), malloc (sizeof Button)}; + for (int n = 0; n < ARRAY_LEN (bs); n++) { + if (bs[n] == NULL) + continue; + Button_construct (bs[n]); - is->do_not_draw_already_worked_tile_img = false; - City_Form_draw_yields_on_worked_tiles (this); + int desc_btn_x = 535, desc_btn_y = 222, desc_btn_height = 17; - // If we're zoomed out then draw the yields again but this time skip drawing of the tile-already-worked sprites. This is because the - // transparent regions of those sprites get drawn overtop of the yield sprites when zoomed out, and that overdrawing deletes parts of the - // yields, i.e., it overwrites their colored pixels with transparent ones. The simplest solution is to draw the yields again after the - // already-worked sprites to ensure the former get drawn overtop of the latter. - if (p_bic_data->is_zoomed_out) { - is->do_not_draw_already_worked_tile_img = true; - City_Form_draw_yields_on_worked_tiles (this); - } + Button_initialize (bs[n], __, + n == 0 ? (*p_labels)[LBL_EFFECTS] : (*p_labels)[LBL_PREVIOUS], + n == 0 ? PEDIA_MULTIPAGE_EFFECTS_BUTTON_ID : PEDIA_MULTIPAGE_PREV_BUTTON_ID, // control ID + desc_btn_x, // location x + desc_btn_y + (n == 0 ? -2 : 2) * desc_btn_height, // location y + MOD_INFO_BUTTON_WIDTH, MOD_INFO_BUTTON_HEIGHT, // width, height + (Base_Form *)this, // parent + 0); // ? - // Draw district bonuses on district tiles - if (is->current_config.enable_districts || is->current_config.enable_natural_wonders) { - City * city = this->CurrentCity; - if (city == NULL) - goto skip_district_yields; + for (int k = 0; k < 3; k++) + bs[n]->Images[k] = &this->Description_Btn_Images[k]; - int city_x = city->Body.X; - int city_y = city->Body.Y; - int civ_id = city->Body.CivID; + // Do now draw the button until needed + bs[n]->vtable->m02_Show_Disabled ((Base_Form *)bs[n]); + } + is->cmpd.effects_btn = bs[0]; + is->cmpd.previous_btn = bs[1]; - // Calculate screen coordinates for city center - int center_screen_x, center_screen_y; - Main_Screen_Form_tile_to_screen_coords (p_main_screen_form, __, city_x, city_y, ¢er_screen_x, ¢er_screen_y); + int tr = Civilopedia_Form_m68_Show_Dialog (this, __, param_1, param_2, param_3); - int tile_half_width = p_bic_data->is_zoomed_out ? 32 : 64; - int tile_half_height = p_bic_data->is_zoomed_out ? 16 : 32; - center_screen_x += tile_half_width; - if (center_screen_x < 0) - center_screen_x += p_bic_data->Map.Width * tile_half_width; - center_screen_y += tile_half_height; - if (center_screen_y < 0) - center_screen_y += p_bic_data->Map.Height * tile_half_height; + for (int n = 0; n < ARRAY_LEN (bs); n++) + if (bs[n] != NULL) { + bs[n]->vtable->destruct ((Base_Form *)bs[n], __, 0); + free (bs[n]); + } + is->cmpd.effects_btn = is->cmpd.previous_btn = NULL; - int remaining_utilized_neighborhoods = 0; - if (is->current_config.enable_districts && is->current_config.enable_neighborhood_districts) - remaining_utilized_neighborhoods = count_utilized_neighborhoods_in_city_radius (city); + return tr; +} - // Iterate through all neighbor tiles - for (int neighbor_index = 0; neighbor_index < is->workable_tile_count; neighbor_index++) { - int dx, dy; - patch_ni_to_diff_for_work_area (neighbor_index, &dx, &dy); - int tile_x = city_x + dx; - int tile_y = city_y + dy; - wrap_tile_coords (&p_bic_data->Map, &tile_x, &tile_y); +void +init_district_images () +{ + if (is_online_game () || is->dc_img_state != IS_UNINITED) + return; - Tile * tile = tile_at (tile_x, tile_y); - if (tile == NULL || tile == p_null_tile) continue; - if (tile->vtable->m38_Get_Territory_OwnerID (tile) != civ_id) continue; - struct district_instance * inst = get_district_instance (tile); - if (inst == NULL) continue; - int district_id = inst->district_type; - if (!district_is_complete (tile, district_id)) continue; - if (tile_has_enemy_unit (tile, civ_id)) continue; - if (tile->vtable->m20_Check_Pollution (tile, __, 0)) continue; + char art_dir[200]; + char temp_path[2*MAX_PATH]; - bool is_distribution_hub = district_id == DISTRIBUTION_HUB_DISTRICT_ID; - bool is_natural_wonder = district_id == NATURAL_WONDER_DISTRICT_ID; + is->dc_img_state = IS_INIT_FAILED; - // Skip distribution hubs if the feature is not enabled - if (is_distribution_hub && (!is->current_config.enable_districts || !is->current_config.enable_distribution_hub_districts)) - continue; + PCX_Image pcx; + PCX_Image_construct (&pcx); + + // For each district type + for (int dc = 0; dc < is->district_count; dc++) { + struct district_config const * cfg = &is->district_configs[dc]; + int variant_count = cfg->img_path_count; + if (variant_count <= 0) + continue; - if (is_natural_wonder && (!is->current_config.enable_natural_wonders)) - continue; + int era_count = cfg->vary_img_by_era ? 4 : 1; + int column_count = cfg->img_column_count; + int sprite_width = (cfg->custom_width > 0) ? cfg->custom_width : 128; + int sprite_height = (cfg->custom_height > 0) ? cfg->custom_height : 64; - if (!is_distribution_hub && !is_natural_wonder && - (!is->current_config.enable_districts)) + // For each cultural variant + for (int variant_i = 0; variant_i < variant_count; variant_i++) { + if (cfg->img_paths[variant_i] == NULL) continue; - // For neighborhood districts, check if population is high enough to utilize them - if (!is_distribution_hub && - is->current_config.enable_districts && - is->current_config.enable_neighborhood_districts && - district_id == NEIGHBORHOOD_DISTRICT_ID) { - // Only draw yields if this neighborhood is utilized - if (remaining_utilized_neighborhoods <= 0) - continue; - remaining_utilized_neighborhoods--; + // Read PCX file + snprintf (art_dir, sizeof art_dir, "Districts/1200/%s", cfg->img_paths[variant_i]); + get_mod_art_path (art_dir, temp_path, sizeof temp_path); + PCX_Image_read_file (&pcx, __, temp_path, NULL, 0, 0x100, 2); + + if (pcx.JGL.Image == NULL) { + + char ss[200]; + snprintf (ss, sizeof ss, "init_district_images: failed to load district images from %s", temp_path); + pop_up_in_game_error (ss); + + (*p_OutputDebugStringA) ("[C3X] Failed to load districts sprite sheet.\n"); + for (int dc2 = 0; dc2 < COUNT_DISTRICT_TYPES; dc2++) + for (int variant_i2 = 0; variant_i2 < ARRAY_LEN (is->district_img_sets[dc2].imgs); variant_i2++) + for (int era_i = 0; era_i < 4; era_i++) { + for (int col = 0; col < ARRAY_LEN (is->district_img_sets[dc2].imgs[variant_i2][era_i]); col++) { + Sprite * sprite = &is->district_img_sets[dc2].imgs[variant_i2][era_i][col]; + if (sprite->vtable != NULL) + sprite->vtable->destruct (sprite, __, 0); + } + } + pcx.vtable->destruct (&pcx, __, 0); + return; } - // Calculate screen coordinates for this tile - int screen_x = center_screen_x + (dx * tile_half_width); - int screen_y = center_screen_y + (dy * tile_half_height); + // For each era + for (int era_i = 0; era_i < era_count; era_i++) { - // Call the appropriate drawing function - if (is_distribution_hub) { - draw_distribution_hub_yields (this, tile, tile_x, tile_y, screen_x, screen_y); - } else { - draw_district_yields (this, tile, district_id, screen_x, screen_y); + // For each column in the image (variations on the district image for that era) + for (int col_i = 0; col_i < column_count; col_i++) { + Sprite_construct (&is->district_img_sets[dc].imgs[variant_i][era_i][col_i]); + + int x = sprite_width * col_i, + y = sprite_height * era_i; + Sprite_slice_pcx (&is->district_img_sets[dc].imgs[variant_i][era_i][col_i], __, &pcx, x, y, sprite_width, sprite_height, 1, 1); + } } + + pcx.vtable->clear_JGL (&pcx); } } + // Load abandoned district images (land + maritime) + get_mod_art_path ("Districts/1200/Abandoned.pcx", temp_path, sizeof temp_path); + PCX_Image_read_file (&pcx, __, temp_path, NULL, 0, 0x100, 2); -skip_district_yields: - if (changed_clip_area) - clear_clip_area (this); -} - -bool __fastcall -patch_City_Form_draw_highlighted_yields (City_Form * this, int edx, int tile_x, int tile_y, int neighbor_index) -{ - // Make sure we don't draw outside the map area - bool changed_clip_area = false; - if ((is->current_config.city_work_radius >= 4) && ! p_bic_data->is_zoomed_out) { - set_clip_area_to_map_view (this); - changed_clip_area = true; + if (pcx.JGL.Image == NULL) { + char ss[200]; + snprintf (ss, sizeof ss, "init_district_images: failed to load abandoned district images from %s", temp_path); + pop_up_in_game_error (ss); + for (int dc2 = 0; dc2 < COUNT_DISTRICT_TYPES; dc2++) + for (int variant_i2 = 0; variant_i2 < ARRAY_LEN (is->district_img_sets[dc2].imgs); variant_i2++) + for (int era_i = 0; era_i < 4; era_i++) { + for (int col = 0; col < ARRAY_LEN (is->district_img_sets[dc2].imgs[variant_i2][era_i]); col++) { + Sprite * sprite = &is->district_img_sets[dc2].imgs[variant_i2][era_i][col]; + if (sprite->vtable != NULL) + sprite->vtable->destruct (sprite, __, 0); + } + } + pcx.vtable->destruct (&pcx, __, 0); + return; } - bool tr = City_Form_draw_highlighted_yields (this, __, tile_x, tile_y, neighbor_index); + Sprite_construct (&is->abandoned_district_img); + Sprite_slice_pcx (&is->abandoned_district_img, __, &pcx, 0, 0, 128, 64, 1, 1); - if (changed_clip_area) - clear_clip_area (this); - return tr; -} + Sprite_construct (&is->abandoned_maritime_district_img); + Sprite_slice_pcx (&is->abandoned_maritime_district_img, __, &pcx, 128, 0, 128, 64, 1, 1); + pcx.vtable->clear_JGL (&pcx); -void __fastcall -patch_City_Form_draw_border_around_workable_tiles (City_Form * this) -{ - // Make sure we don't draw outside the map area - bool changed_clip_area = false; - if ((is->current_config.city_work_radius >= 4) && ! p_bic_data->is_zoomed_out) { - set_clip_area_to_map_view (this); - changed_clip_area = true; - } + // Load wonder district images (dynamically per wonder) + if (is->current_config.enable_wonder_districts) { + char const * last_img_path = NULL; + PCX_Image wpcx; + PCX_Image_construct (&wpcx); + bool pcx_loaded = false; - City_Form_draw_border_around_workable_tiles (this); + for (int wi = 0; wi < is->wonder_district_count; wi++) { + struct wonder_district_config * cfg = &is->wonder_district_configs[wi]; + char const * img_path = cfg->img_path; + if (img_path == NULL) + img_path = "Wonders.pcx"; - if (changed_clip_area) - clear_clip_area (this); -} + // Load new image file if different from previous + if ((last_img_path == NULL) || (strcmp (img_path, last_img_path) != 0)) { + if (pcx_loaded) + wpcx.vtable->clear_JGL (&wpcx); -bool __fastcall -patch_City_stop_working_polluted_tile (City * this, int edx, int neighbor_index) -{ - if (is->current_config.do_not_unassign_workers_from_polluted_tiles && - (*p_human_player_bits & (1 << this->Body.CivID))) - return false; // do nothing; return value is not used - else - return City_stop_working_tile (this, __, neighbor_index); -} + snprintf (art_dir, sizeof art_dir, "Districts/1200/%s", img_path); + get_mod_art_path (art_dir, temp_path, sizeof temp_path); + PCX_Image_read_file (&wpcx, __, temp_path, NULL, 0, 0x100, 2); -void __fastcall -patch_City_manage_by_governor (City * this, int edx, bool manage_professions) -{ - int * p_stack = (int *)&manage_professions; - int ret_addr = p_stack[-1]; + if (wpcx.JGL.Image == NULL) { + char ss[200]; + snprintf (ss, sizeof ss, "init_district_images: failed to load wonder district images from %s", temp_path); + pop_up_in_game_error (ss); + pcx_loaded = false; + continue; + } - // Do nothing if called after spawning pollution but didn't unassign worker - if ((ret_addr == ADDR_MANAGE_CITY_AFTER_POLLUTION_RETURN) && - is->current_config.do_not_unassign_workers_from_polluted_tiles && - (*p_human_player_bits & (1 << this->Body.CivID))) - return; + pcx_loaded = true; + last_img_path = img_path; + } - City_manage_by_governor (this, __, manage_professions); -} + if (! pcx_loaded) + continue; -City * __cdecl -patch_find_nearest_city_for_ai_alliance_eval (int tile_x, int tile_y, int owner_id, int continent_id, int ignore_owner_id, int perspective_id, City * ignore_city) -{ - City * tr = find_nearest_city (tile_x, tile_y, owner_id, continent_id, ignore_owner_id, perspective_id, ignore_city); - if (is->current_config.patch_division_by_zero_in_ai_alliance_eval && (*p_nearest_city_distance == 0)) - *p_nearest_city_distance = 1; - return tr; -} + struct wonder_district_image_set * set = &is->wonder_district_img_sets[wi]; -int __fastcall -patch_Unit_get_max_move_points (Unit * this) -{ - if (Unit_has_ability (this, __, UTA_Army) && is->current_config.patch_empty_army_movement) { - int slowest_member_mp = INT_MAX; - bool any_units_in_army = false; - FOR_UNITS_ON (uti, tile_at (this->Body.X, this->Body.Y)) { - if (uti.unit->Body.Container_Unit == this->Body.ID) { - any_units_in_army = true; - slowest_member_mp = not_above (Unit_get_max_move_points (uti.unit), slowest_member_mp); + Sprite_construct (&set->img); + int x = 128 * cfg->img_column; + int y = 64 * cfg->img_row; + Sprite_slice_pcx (&set->img, __, &wpcx, x, y, 128, 64, 1, 1); + + Sprite_construct (&set->construct_img); + int cx = 128 * cfg->img_construct_column; + int cy = 64 * cfg->img_construct_row; + Sprite_slice_pcx (&set->construct_img, __, &wpcx, cx, cy, 128, 64, 1, 1); + + if (cfg->enable_img_alt_dir) { + Sprite_construct (&set->alt_dir_img); + int ax = 128 * cfg->img_alt_dir_column; + int ay = 64 * cfg->img_alt_dir_row; + Sprite_slice_pcx (&set->alt_dir_img, __, &wpcx, ax, ay, 128, 64, 1, 1); + + Sprite_construct (&set->alt_dir_construct_img); + int acx = 128 * cfg->img_alt_dir_construct_column; + int acy = 64 * cfg->img_alt_dir_construct_row; + Sprite_slice_pcx (&set->alt_dir_construct_img, __, &wpcx, acx, acy, 128, 64, 1, 1); } } - if (any_units_in_army) - return slowest_member_mp + p_bic_data->General.RoadsMovementRate; - else - return get_max_move_points (&p_bic_data->UnitTypes[this->Body.UnitTypeID], this->Body.CivID); - } else - return Unit_get_max_move_points (this); -} -char __fastcall -patch_Unit_is_visible_to_civ_for_bouncing (Unit * this, int edx, int civ_id, int param_2) -{ - // If we're set not to kick out invisible units and this one is invis. then report that it's not seen so it's left alone - if (is->do_not_bounce_invisible_units && Unit_has_ability (this, __, UTA_Invisible)) - return 0; - else - return patch_Unit_is_visible_to_civ (this, __, civ_id, param_2); -} + if (pcx_loaded) + wpcx.vtable->clear_JGL (&wpcx); + wpcx.vtable->destruct (&wpcx, __, 0); + } -void __fastcall -patch_Leader_make_peace (Leader * this, int edx, int civ_id) -{ - Leader_make_peace (this, __, civ_id); + if (is->current_config.enable_natural_wonders && (is->natural_wonder_count > 0)) { + char const * last_img_path = NULL; + PCX_Image nwpcx; + PCX_Image_construct (&nwpcx); + bool pcx_loaded = false; - if (is->current_config.disallow_trespassing && - (! this->At_War[civ_id]) && // Make sure the war actually ended - ((this->Relation_Treaties[civ_id] & 2) == 0)) { // Check right of passage (just in case) - is->do_not_bounce_invisible_units = true; - Leader_bounce_trespassing_units (&leaders[civ_id], __, this->ID); - is->do_not_bounce_invisible_units = false; - } -} + for (int ni = 0; ni < is->natural_wonder_count; ni++) { + struct natural_wonder_district_config * cfg = &is->natural_wonder_configs[ni]; + if (cfg->name == NULL) + continue; -// This patch function replaces calls to Leader::count_wonders_with_flag but is only valid in cases where it only matters whether the return value is -// zero or non-zero. This function will not return the actual count, just zero if no wonders are present and some >0 value if there is at least one. -int __fastcall -patch_Leader_count_any_shared_wonders_with_flag (Leader * this, int edx, enum ImprovementTypeWonderFeatures flag, City * only_in_city) -{ - int tr = Leader_count_wonders_with_flag (this, __, flag, only_in_city); + char const * img_path = cfg->img_path; + if ((last_img_path == NULL) || (strcmp (img_path, last_img_path) != 0)) { + if (pcx_loaded) + nwpcx.vtable->clear_JGL (&nwpcx); - if ((tr == 0) && - (only_in_city == NULL) && - is->current_config.share_wonders_in_hotseat && - (*p_is_offline_mp_game && ! *p_is_pbem_game) && // is hotseat game - ((1 << this->ID) & *p_human_player_bits)) { // is "this" a human player + snprintf (art_dir, sizeof art_dir, "Districts/1200/%s", img_path); + get_mod_art_path (art_dir, temp_path, sizeof temp_path); + PCX_Image_read_file (&nwpcx, __, temp_path, NULL, 0, 0x100, 2); - // Sum up wonders owned by other human players - unsigned player_bits = *(unsigned *)p_human_player_bits >> 1; - int n_player = 1; - while (player_bits != 0) { - if ((player_bits & 1) && (n_player != this->ID)) - if (Leader_count_wonders_with_flag (&leaders[n_player], __, flag, only_in_city) > 0) { - tr = 1; - break; + if (nwpcx.JGL.Image == NULL) { + char ss[200]; + snprintf (ss, sizeof ss, "init_district_images: failed to load natural wonder images from %s", temp_path); + pop_up_in_game_error (ss); + pcx_loaded = false; + continue; } - player_bits >>= 1; - n_player++; + + pcx_loaded = true; + last_img_path = img_path; + } + + if (! pcx_loaded) + continue; + + Sprite_construct (&is->natural_wonder_img_sets[ni].img); + int x = 128 * cfg->img_column; + int y = 88 * cfg->img_row; + Sprite_slice_pcx (&is->natural_wonder_img_sets[ni].img, __, &nwpcx, x, y, 128, 88, 1, 1); } + + if (pcx_loaded) + nwpcx.vtable->clear_JGL (&nwpcx); + nwpcx.vtable->destruct (&nwpcx, __, 0); } - return tr; + is->dc_img_state = IS_OK; + pcx.vtable->destruct (&pcx, __, 0); } -int const shared_small_wonder_flags = - ITSW_Increases_Chance_of_Leader_Appearance | - ITSW_Build_Larger_Armies | - ITSW_Treasury_Earns_5_Percent | - ITSW_Decreases_Success_Of_Missile_Attacks | - ITSW_Allows_Spy_Missions | - ITSW_Allows_Healing_In_Enemy_Territory | - ITSW_Requires_Victorous_Army | - ITSE_Requires_Elite_Naval_Units; - - -int __fastcall -patch_Leader_count_wonders_with_small_flag (Leader * this, int edx, enum ImprovementTypeSmallWonderFeatures flag, City * city_or_null) +bool +tile_coords_has_city_with_building_in_district_radius (int tile_x, int tile_y, int district_id, int i_improv) { - int tr = Leader_count_wonders_with_small_flag (this, __, flag, city_or_null); + Tile * center = tile_at (tile_x, tile_y); - // If "this" is a human player who's sharing wonders in hotseat and the flag is one of the ones that gets shared, include the wonders owned by - // all other humans in the game - if ((city_or_null == NULL) && - (flag & shared_small_wonder_flags) && - is->current_config.share_wonders_in_hotseat && - (*p_is_offline_mp_game && ! *p_is_pbem_game) && // is hotseat game - ((1 << this->ID) & *p_human_player_bits)) { // is "this" a human player + if ((center == NULL) || (center == p_null_tile)) return false; + int owner_id = center->Territory_OwnerID; + if (owner_id <= 0) return false; - unsigned player_bits = *(unsigned *)p_human_player_bits >> 1; - int n_player = 1; - while (player_bits != 0) { - if ((player_bits & 1) && (n_player != this->ID)) - tr += Leader_count_wonders_with_small_flag (&leaders[n_player], __, flag, city_or_null); - player_bits >>= 1; - n_player++; - } + FOR_CITIES_AROUND (wai, tile_x, tile_y) { + if (has_active_building (wai.city, i_improv)) + return true; } - return tr; + return false; } -int -find_human_player_with_small_wonder (int improv_id) +bool +wonder_allows_river (struct wonder_district_config const * cfg) { - unsigned player_bits = *(unsigned *)p_human_player_bits >> 1; - int n_player = 1; - while (player_bits != 0) { - if (player_bits & 1) - if (leaders[n_player].Small_Wonders[improv_id] != -1) - return n_player; - player_bits >>= 1; - n_player++; + unsigned int build_mask = wonder_buildable_square_type_mask (cfg); + if (build_mask == 0) + build_mask = district_default_buildable_mask (); + if ((build_mask & square_type_mask_bit (SQ_RIVER)) != 0) + return true; + if (cfg->has_buildable_only_on_overlays && ((cfg->buildable_only_on_overlays_mask & DOM_RIVER) != 0)) + return true; + return false; +} + +Tile * +get_tile_sprite_indices (int tile_x, int tile_y, int * out_sheet_index, int * out_sprite_index) +{ + wrap_tile_coords (&p_bic_data->Map, &tile_x, &tile_y); + Tile * tile = tile_at (tile_x, tile_y); + + if (tile != NULL && tile != p_null_tile) { + *out_sheet_index = (tile->SquareParts >> 8) & 0xFF; + *out_sprite_index = tile->SquareParts & 0xFF; + } else { + *out_sheet_index = -1; + *out_sprite_index = -1; } - return -1; + + return tile; } -bool __fastcall -patch_Leader_can_build_city_improvement (Leader * this, int edx, int i_improv, bool param_2) +void +align_district_with_river (Tile * tile, int * out_pixel_x, int * out_pixel_y, enum direction * out_dir) { - Improvement * improv = &p_bic_data->Improvements[i_improv]; - bool restore = false; - bool already_shared = false; - int saved_status, saved_required_building_count, saved_armies_count; - if ((improv->Characteristics & (ITC_Small_Wonder | ITC_Wonder)) && // if the improv in question is a great or small wonder - is->current_config.share_wonders_in_hotseat && // if we're configured to share wonder effects - (*p_is_offline_mp_game && ! *p_is_pbem_game) && // if in a hotseat game - ((1 << this->ID) & *p_human_player_bits)) { // if "this" is a human player + if ((tile == NULL) || (tile == p_null_tile)) + return; - // If another human player has already built this small wonder and it has no non-shared effects, then make it unbuildable - if ((improv->Characteristics & ITC_Small_Wonder) && - (find_human_player_with_small_wonder (i_improv) != -1) && - ((improv->SmallWonderFlags & ~shared_small_wonder_flags) == 0)) - already_shared = true; + enum direction dir = DIR_ZERO; + if (! get_primary_river_direction (tile, &dir)) + return; - else { - restore = true; - saved_status = this->Status; - if (improv->RequiredBuildingID != -1) - saved_required_building_count = this->Improvement_Counts[improv->RequiredBuildingID]; - saved_armies_count = this->Armies_Count; + int dx, dy; + int offset = 36; + direction_to_offset (dir, &dx, &dy); - // Loop over all other human players in the game - unsigned player_bits = *(unsigned *)p_human_player_bits >> 1; - int n_player = 1; - while (player_bits != 0) { - if ((player_bits & 1) && (n_player != this->ID)) { + dy = 0; + switch (dir) { + case DIR_N: dy = -offset; break; + case DIR_NE: dy = -offset/2; break; + case DIR_E: dy = 0; break; + case DIR_SE: dy = offset/2; break; + case DIR_S: dy = offset; break; + case DIR_SW: dy = offset/2; break; + case DIR_W: dy = 0; break; + case DIR_NW: dy = -offset/2; break; + default: break; + } - // Combine status bits - this->Status |= leaders[n_player].Status & (LSF_HAS_VICTORIOUS_ARMY | LSF_HAS_ELITE_NAVAL_UNIT); + if (out_pixel_x != NULL) + *out_pixel_x += dx; + if (out_pixel_y != NULL) + *out_pixel_y += dy; + if (out_dir != NULL) + *out_dir = dir; +} - // Combine building counts for the required building if there is one - if (improv->RequiredBuildingID != -1) - this->Improvement_Counts[improv->RequiredBuildingID] += leaders[n_player].Improvement_Counts[improv->RequiredBuildingID]; +void +align_variant_and_pixel_offsets_with_coastline (Tile * tile, int * out_variant, int * out_pixel_x, int * out_pixel_y) +{ + if ((tile == NULL) || (tile == p_null_tile) || (out_variant == NULL)) + return; - // Combine army counts - this->Armies_Count += leaders[n_player].Armies_Count; + int owner_id = tile->Territory_OwnerID; + if (owner_id <= 0) + return; - } - player_bits >>= 1; - n_player++; - } - } - } + struct district_instance * inst = get_district_instance (tile); + if (inst == NULL) + return; - bool tr = (! already_shared) && Leader_can_build_city_improvement (this, __, i_improv, param_2); + int tile_x = inst->tile_x; + int tile_y = inst->tile_y; + Map * map = &p_bic_data->Map; - if (restore) { - this->Status = saved_status; - if (improv->RequiredBuildingID != -1) - this->Improvement_Counts[improv->RequiredBuildingID] = saved_required_building_count; + City * closest_city = NULL; + int closest_dx = 0, closest_dy = 0; - this->Armies_Count = saved_armies_count; + FOR_CITIES_AROUND (wai, tile_x, tile_y) { + City * city = wai.city; + int ndx = city->Body.X - tile_x; + int ndy = city->Body.Y - tile_y; + + if (map->Flags & 1) { + int half_width = map->Width / 2; + if (ndx > half_width) + ndx -= map->Width; + else if (ndx < -half_width) + ndx += map->Width; + } + if (map->Flags & 2) { + int half_height = map->Height / 2; + if (ndy > half_height) + ndy -= map->Height; + else if (ndy < -half_height) + ndy += map->Height; + } + + closest_city = city; + closest_dx = ndx; + closest_dy = ndy; + break; } - return tr; + if (closest_city == NULL) + return; + + bool city_is_directly_above_port = (closest_dx == 0) && (closest_dy == -2); + bool city_is_directly_below_port = (closest_dx == 0) && (closest_dy == 2); + bool city_is_directly_west_of_port = (closest_dx < 0) && (closest_dy == 0); + bool city_is_directly_east_of_port = (closest_dx > 0) && (closest_dy == 0); + bool city_is_directly_northeast_of_port = (closest_dx == 1) && (closest_dy == -1); + bool city_is_directly_southeast_of_port = (closest_dx == 1) && (closest_dy == 1); + bool city_is_directly_southwest_of_port = (closest_dx == -1) && (closest_dy == 1); + bool city_is_directly_northwest_of_port = (closest_dx == -1) && (closest_dy == -1); + + // Variant indices; can't use direction enum as values are slightly different + int NONE = -1; + int NW = 0; + int NE = 1; + int SE = 2; + int SW = 3; + *out_variant = NONE; + + enum direction anchor = NONE; + bool direct_diagonal = false; + + // Direct diagonals + if (city_is_directly_northeast_of_port) { *out_variant = SW; anchor = DIR_NE; direct_diagonal = true; } + else if (city_is_directly_southeast_of_port) { *out_variant = NW; anchor = DIR_SE; direct_diagonal = true; } + else if (city_is_directly_southwest_of_port) { *out_variant = NE; anchor = DIR_SW; direct_diagonal = true; } + else if (city_is_directly_northwest_of_port) { *out_variant = SE; anchor = DIR_NW; direct_diagonal = true; } + + // City either in a direct cardinal direction or not adjacent, check relative directions + else { + bool city_is_west_of_port = (closest_dx < 0); + bool city_is_east_of_port = (closest_dx > 0); + bool city_is_north_of_port = (closest_dy < 0); + bool city_is_south_of_port = (closest_dy > 0); + bool northwest_tile_is_land = tile_is_land (owner_id, tile_x - 1, tile_y - 1, true); + bool north_tile_is_land = tile_is_land (owner_id, tile_x, tile_y - 2, true); + bool northeast_tile_is_land = tile_is_land (owner_id, tile_x + 1, tile_y - 1, true); + bool east_tile_is_land = tile_is_land (owner_id, tile_x + 2, tile_y, true); + bool southeast_tile_is_land = tile_is_land (owner_id, tile_x + 1, tile_y + 1, true); + bool south_tile_is_land = tile_is_land (owner_id, tile_x, tile_y + 2, true); + bool southwest_tile_is_land = tile_is_land (owner_id, tile_x - 1, tile_y + 1, true); + bool west_tile_is_land = tile_is_land (owner_id, tile_x - 2, tile_y, true); + + if (city_is_directly_above_port) { + if (northwest_tile_is_land && ! tile_is_land (owner_id, tile_x + 1, tile_y + 1, true)) { + *out_variant = SE; anchor = DIR_NW; + } else if (northeast_tile_is_land && ! tile_is_land (owner_id, tile_x - 1, tile_y + 1, true)) { + *out_variant = SW; anchor = DIR_NE; + } else { + *out_variant = SW; anchor = DIR_NE; + *out_pixel_x -= 4; *out_pixel_y -= 4; + } + } else if (city_is_directly_below_port) { + if (southeast_tile_is_land) { *out_variant = NW; anchor = DIR_SE; } + else if (southwest_tile_is_land) { *out_variant = NE; anchor = DIR_SW; } + else { + *out_variant = NE; anchor = DIR_SW; + *out_pixel_x += 4; *out_pixel_y += 4; + } + } else if (city_is_directly_west_of_port) { + if (northwest_tile_is_land) { *out_variant = SE; anchor = DIR_NW; } + else if (southwest_tile_is_land) { *out_variant = NE; anchor = DIR_SW; } + else { + *out_variant = SE; anchor = DIR_NW; + *out_pixel_x -= 30; *out_pixel_y += 24; + } + } else if (city_is_directly_east_of_port) { + if (northeast_tile_is_land) { *out_variant = SW; anchor = DIR_NE; } + else if (southeast_tile_is_land) { *out_variant = NW; anchor = DIR_SE; } + else { + *out_variant = SW; anchor = DIR_NE; + *out_pixel_x += 30; *out_pixel_y -= 24; + } + } else if (city_is_north_of_port && city_is_west_of_port) { + if (northwest_tile_is_land) { *out_variant = SE; anchor = DIR_NW; } + else if (southwest_tile_is_land) { *out_variant = NE; anchor = DIR_SW; } + } else if (city_is_north_of_port && city_is_east_of_port) { + if (northeast_tile_is_land) { *out_variant = SW; anchor = DIR_NE; } + else if (southeast_tile_is_land) { *out_variant = NW; anchor = DIR_SE; } + } else if (city_is_south_of_port && city_is_east_of_port) { + if (southeast_tile_is_land) { *out_variant = NW; anchor = DIR_SE; } + else if (northeast_tile_is_land) { *out_variant = SW; anchor = DIR_NE; } + } else if (city_is_south_of_port && city_is_west_of_port) { + if (southwest_tile_is_land) { *out_variant = NE; anchor = DIR_SW; } + else if (northwest_tile_is_land) { *out_variant = SE; anchor = DIR_NW; } + } + + // No ideal direction, pick based on any owner land tiles around port + if (*out_variant == NONE) { + if (northeast_tile_is_land) { *out_variant = SW; anchor = DIR_NE; } + else if (southeast_tile_is_land) { *out_variant = NW; anchor = DIR_SE; } + else if (southwest_tile_is_land) { *out_variant = NE; anchor = DIR_SW; } + else if (northwest_tile_is_land) { *out_variant = SE; anchor = DIR_NW; } + else if (north_tile_is_land) { *out_variant = SW; anchor = DIR_N; } + else if (east_tile_is_land) { *out_variant = SW; anchor = DIR_E; } + else if (south_tile_is_land) { *out_variant = NE; anchor = DIR_S; } + else if (west_tile_is_land) { *out_variant = SE; anchor = DIR_W; } + } + + // Same civ doesn't own any adjacent land tiles, pick based on *any* land tiles + if (*out_variant == NONE) { + bool northwest_tile_is_land = tile_is_land (owner_id, tile_x - 1, tile_y - 1, false); + bool north_tile_is_land = tile_is_land (owner_id, tile_x, tile_y - 2, false); + bool northeast_tile_is_land = tile_is_land (owner_id, tile_x + 1, tile_y - 1, false); + bool east_tile_is_land = tile_is_land (owner_id, tile_x + 2, tile_y, false); + bool southeast_tile_is_land = tile_is_land (owner_id, tile_x + 1, tile_y + 1, false); + bool south_tile_is_land = tile_is_land (owner_id, tile_x, tile_y + 2, false); + bool southwest_tile_is_land = tile_is_land (owner_id, tile_x - 1, tile_y + 1, false); + bool west_tile_is_land = tile_is_land (owner_id, tile_x - 2, tile_y, false); + if (northeast_tile_is_land) { *out_variant = SW; anchor = DIR_NE; } + else if (southeast_tile_is_land) { *out_variant = NW; anchor = DIR_SE; } + else if (southwest_tile_is_land) { *out_variant = NE; anchor = DIR_SW; } + else if (northwest_tile_is_land) { *out_variant = SE; anchor = DIR_NW; } + else if (north_tile_is_land) { *out_variant = SW; anchor = DIR_N; } + else if (east_tile_is_land) { *out_variant = SW; anchor = DIR_E; } + else if (south_tile_is_land) { *out_variant = NE; anchor = DIR_S; } + else if (west_tile_is_land) { *out_variant = SE; anchor = DIR_W; } + else { *out_variant = SW; anchor = DIR_NE; } // Somehow no land tiles at all? Default to NE + } + } + + Tile * anchor_tile; + int anchor_sheet_index, anchor_sprite_index; + switch (anchor) { + case DIR_N: anchor_tile = get_tile_sprite_indices (tile_x, tile_y - 2, &anchor_sheet_index, &anchor_sprite_index); break; + case DIR_NE: anchor_tile = get_tile_sprite_indices (tile_x + 1, tile_y - 1, &anchor_sheet_index, &anchor_sprite_index); break; + case DIR_E: anchor_tile = get_tile_sprite_indices (tile_x + 2, tile_y, &anchor_sheet_index, &anchor_sprite_index); break; + case DIR_SE: anchor_tile = get_tile_sprite_indices (tile_x + 1, tile_y + 1, &anchor_sheet_index, &anchor_sprite_index); break; + case DIR_S: anchor_tile = get_tile_sprite_indices (tile_x, tile_y + 2, &anchor_sheet_index, &anchor_sprite_index); break; + case DIR_SW: anchor_tile = get_tile_sprite_indices (tile_x - 1, tile_y + 1, &anchor_sheet_index, &anchor_sprite_index); break; + case DIR_W: anchor_tile = get_tile_sprite_indices (tile_x - 2, tile_y, &anchor_sheet_index, &anchor_sprite_index); break; + case DIR_NW: anchor_tile = get_tile_sprite_indices (tile_x - 1, tile_y - 1, &anchor_sheet_index, &anchor_sprite_index); break; + default: anchor_sheet_index = -1; anchor_sprite_index = -1; break; + } + + bool anchor_is_hill = anchor_tile != NULL && anchor_tile->vtable->m50_Get_Square_BaseType (anchor_tile) == SQ_Hills; + bool anchor_is_mountain = anchor_tile != NULL && anchor_tile->vtable->m50_Get_Square_BaseType (anchor_tile) == SQ_Mountains; + + // Determine general pixel offsets based on direction & anchor + if (*out_variant == SW && anchor == DIR_NE) { *out_pixel_x -= 0; *out_pixel_y += 6; } + else if (*out_variant == SE && anchor == DIR_NW) { *out_pixel_x -= 2; *out_pixel_y += 6; } + else if (*out_variant == NW && anchor == DIR_SE) { *out_pixel_x -= 0; *out_pixel_y -= 2; } + else if (*out_variant == SW && anchor == DIR_N) { *out_pixel_x -= 56; *out_pixel_y -= 26; } + else if (*out_variant == SW && anchor == DIR_E) { *out_pixel_x += 36; *out_pixel_y += 18; } + else if (*out_variant == NE && anchor == DIR_S) { *out_pixel_x += 70; *out_pixel_y += 30; } + else if (*out_variant == SE && anchor == DIR_W) { *out_pixel_x -= 20; *out_pixel_y += 14; } + + // Handle edge cases. Tedious, but looks quite a bit better so worth it + if (! (anchor_is_hill || anchor_is_mountain) && ! direct_diagonal) { + if (*out_variant == SE && anchor == DIR_W) { *out_pixel_x -= 20; *out_pixel_y += 20; } + + // Sheet 0 + if (anchor_sheet_index == 0) { + if (*out_variant == SW || *out_variant == NW) { *out_pixel_x += 32; } + else if (*out_variant == SE || *out_variant == NE) { *out_pixel_x -= 32; } + + if ((*out_variant == NE || *out_variant == NW) && anchor == DIR_S && anchor_sprite_index == 26) { *out_pixel_x -= 4; *out_pixel_y += 30; } + else if (*out_variant == NE && anchor == DIR_SW && anchor_sprite_index == 20) { *out_pixel_x -= 2; *out_pixel_y += 4; } + + if (*out_variant == SW && (anchor_sprite_index == 0 || anchor_sprite_index == 4 || anchor_sprite_index == 10 || anchor_sprite_index == 1)) { *out_pixel_x +=2; *out_pixel_y -= 8; } + else if (*out_variant == SW && anchor == DIR_N && (anchor_sprite_index == 6)) { *out_pixel_x -= 6; *out_pixel_y -= 8; } + else if (*out_variant == SE && anchor == DIR_W && (anchor_sprite_index == 18)) { *out_pixel_x += 6; *out_pixel_y += 4; } + else if (*out_variant == NE && anchor == DIR_SW && (anchor_sprite_index == 51)) { *out_pixel_x += 20; *out_pixel_y -= 20; } + else if (*out_variant == SW && anchor == DIR_NE && (anchor_sprite_index == 0)) { *out_pixel_x -= 4; *out_pixel_y += 4; } + else if (*out_variant == NW && anchor == DIR_SE && (anchor_sprite_index == 44)) { *out_pixel_x -= 8; *out_pixel_y -= 12; } + } + // Sheet 1 + else if (anchor_sheet_index == 1) { + if (*out_variant == SW || *out_variant == NW) { *out_pixel_x += 10; } + else if (*out_variant == SE || *out_variant == NE) { *out_pixel_x -= 10; } + + if (*out_variant == SW && (anchor_sprite_index == 7 || anchor_sprite_index == 17 || anchor_sprite_index == 16)) { *out_pixel_x +=10; *out_pixel_y -= 8; } + } + // Sheet 3 + else if (anchor_sheet_index == 2) { + if (*out_variant == SW && (anchor_sprite_index == 6)) { *out_pixel_x +=2; *out_pixel_y -= 8; } + } + // Sheet 3 + else if (anchor_sheet_index == 3) { + if (*out_variant == SW || *out_variant == NW) { *out_pixel_x += 16; } + else if (*out_variant == SE || *out_variant == NE) { *out_pixel_x -= 16; } + + if (*out_variant == SW && (anchor_sprite_index == 43 || anchor_sprite_index == 40)) { *out_pixel_x +=6; *out_pixel_y -= 12; } + else if (*out_variant == SW && (anchor_sprite_index == 35 || anchor_sprite_index == 39)) { *out_pixel_x +=6; *out_pixel_y -= 12; } + else if (*out_variant == SE && (anchor_sprite_index == 50)) { *out_pixel_x -=6; *out_pixel_y -= 12; } + else if (*out_variant == SE && (anchor_sprite_index == 26)) { *out_pixel_x += 50; *out_pixel_y -= 2; } + } + // Sheet 4 + else if (anchor_sheet_index == 4) { + if (*out_variant == SW && (anchor_sprite_index == 71)) { *out_pixel_x +=2; *out_pixel_y -= 8; } + } + // Sheet 5 + else if (anchor_sheet_index == 5) { + if (*out_variant == SW || *out_variant == NW) { *out_pixel_x += 18; } + else if (*out_variant == SE || *out_variant == NE) { *out_pixel_x -= 18; } + + if ((*out_variant == SW || *out_variant == SE) && anchor_sprite_index == 18) { *out_pixel_y -= 10; } + else if (*out_variant == SW && anchor_sprite_index == 73) { *out_pixel_x -=4; *out_pixel_y -= 10; } + else if (*out_variant == NW && anchor == DIR_SE && anchor_sprite_index == 42) { *out_pixel_y -= 8; } + else if ((*out_variant == NW || *out_variant == SW) && anchor_sprite_index == 6) { *out_pixel_x += 12; *out_pixel_y -= 6; } + else if ((*out_variant == SW) && anchor_sprite_index == 16) { *out_pixel_x -=8; *out_pixel_y -= 10; } + } + } + else if (direct_diagonal && *out_variant == SW && anchor == DIR_NE) { *out_pixel_x -=8; *out_pixel_y -= 8; } + else if (direct_diagonal && *out_variant == SE && anchor == DIR_NW) { *out_pixel_x -=8; *out_pixel_y -= 8; } + else if (direct_diagonal && *out_variant == NW && anchor == DIR_SE) { *out_pixel_x +=6; *out_pixel_y += 6; } + else if (direct_diagonal && *out_variant == NE && anchor == DIR_SW) { *out_pixel_x +=6; *out_pixel_y += 6; } } -void __fastcall -patch_City_add_happiness_from_buildings (City * this, int edx, int * inout_happiness, int * inout_unhappiness) +bool +wonder_should_use_alternative_direction_image (int tile_x, int tile_y, int owner_id, struct wonder_district_config const * cfg) { - // If we're in a hotseat game with shared wonder effects, merge all human player's improvement counts together so the base function checks for - // happiness from improvements owned by other human players. - Leader * owner = &leaders[this->Body.CivID]; - bool restore_improv_counts = false; - if (is->current_config.share_wonders_in_hotseat && - (*p_is_offline_mp_game && ! *p_is_pbem_game) && // is hotseat game - ((1 << this->Body.CivID) & *p_human_player_bits)) { // "this" is owned by a human player + if (owner_id <= 0) + return false; - // Ensure the space we've set aside for saving the real improv counts is large enough - if ((is->saved_improv_counts == NULL) || (is->saved_improv_counts_capacity < p_bic_data->ImprovementsCount)) { - free (is->saved_improv_counts); - is->saved_improv_counts = calloc (p_bic_data->ImprovementsCount, sizeof is->saved_improv_counts[0]); - is->saved_improv_counts_capacity = (is->saved_improv_counts != NULL) ? p_bic_data->ImprovementsCount : 0; - } + // We only care about the nearest same-civ city in the work area around the tile. + // Assumes the base wonder art (img_row/column) faces west and the alt art faces east. + // To "face away" from the nearest city, we pick the alt art when that city lies to the east. + Tile * center = is->current_render_tile; + if ((center == NULL) || (center == p_null_tile)) + return false; + // If on a river and the wonder allows river alignment, make sure we face the river instead + bool allow_river = wonder_allows_river (cfg); + if (allow_river) { + enum direction river_dir = DIR_ZERO; + if (get_primary_river_direction (center, &river_dir)) { + int dx, dy; - if (is->saved_improv_counts != NULL) { - // Save the owner's real improv counts and remember to restore them before returning - restore_improv_counts = true; - for (int n = 0; n < p_bic_data->ImprovementsCount; n++) - is->saved_improv_counts[n] = owner->Improvement_Counts[n]; + if (direction_to_offset (river_dir, &dx, &dy)) { + // I'm not completely sure of the logic here, but this seems to match the vanilla behavior + // in terms of having the wonder face the general direction of the river flow + if (dx == 2) + return false; - // Add in improv counts for wonders from all other human players in the game - unsigned player_bits = *(unsigned *)p_human_player_bits >> 1; - int n_player = 1; - while (player_bits != 0) { - if ((player_bits & 1) && (n_player != owner->ID)) - for (int n = 0; n < p_bic_data->ImprovementsCount; n++) - if (p_bic_data->Improvements[n].Characteristics & (ITC_Wonder | ITC_Small_Wonder)) - owner->Improvement_Counts[n] += leaders[n_player].Improvement_Counts[n]; - player_bits >>= 1; - n_player++; + return dx > 0; } } + } + + // Else face away from the nearest same-civ city + Map * map = &p_bic_data->Map; + int best_dist = INT_MAX; + int best_dx = 0; + int city_dx = 0; + int city_dy = 0; + + FOR_CITIES_AROUND (wai, tile_x, tile_y) { + City * city = wai.city; + if ((city == NULL) || (city->Body.CivID != owner_id)) + continue; + + int dx = city->Body.X - tile_x; + int dy = city->Body.Y - tile_y; + + if (map->Flags & 1) { + int half_width = map->Width >> 1; + if (dx > half_width) + dx -= map->Width; + else if (dx < -half_width) + dx += map->Width; + } + if (map->Flags & 2) { + int half_height = map->Height >> 1; + if (dy > half_height) + dy -= map->Height; + else if (dy < -half_height) + dy += map->Height; + } + int dist = int_abs (dx) + int_abs (dy); + // Pick the closest city; if tied, favor the one with a clearer east/west offset so we know which way to face. + if ((dist < best_dist) || + ((dist == best_dist) && ((int_abs (dx) < int_abs (best_dx)) || (best_dx == 0)))) { + best_dist = dist; + best_dx = dx; + city_dx = city->Body.X; + city_dy = city->Body.Y; + } } - City_add_happiness_from_buildings (this, __, inout_happiness, inout_unhappiness); + bool city_is_directly_above_port = city_dx == tile_x && city_dy == tile_y - 2; + bool city_is_directly_below_port = city_dx == tile_x && city_dy == tile_y + 2; - if (restore_improv_counts) { - for (int n = 0; n < p_bic_data->ImprovementsCount; n++) - owner->Improvement_Counts[n] = is->saved_improv_counts[n]; + if (city_is_directly_above_port || city_is_directly_below_port) { + bool northeast_tile_is_land = tile_is_land (owner_id, tile_x + 1, tile_y - 1, true); + bool east_tile_is_land = tile_is_land (owner_id, tile_x + 2, tile_y, true); + bool southeast_tile_is_land = tile_is_land (owner_id, tile_x + 1, tile_y + 1, true); + + if (northeast_tile_is_land || east_tile_is_land || southeast_tile_is_land) + return true; } + + if ((best_dist == INT_MAX) || (best_dx == 0)) + return false; + + return best_dx > 0; } -int __fastcall -patch_City_count_other_cont_happiness_buildings (City * this, int edx, int improv_id) +void +draw_district_on_map_or_canvas(Sprite * sprite, Map_Renderer * map_renderer, int pixel_x, int pixel_y) { - int tr = City_count_other_buildings_on_continent (this, __, improv_id); + if (is->tile_info_open) { + PCX_Image * canvas = (PCX_Image *)map_renderer; + patch_Sprite_draw (sprite, __, canvas, pixel_x, pixel_y, NULL); + return; + } - // If it's a hotseat game where we're sharing wonders, "improv_id" refers to a wonder, and "this" is owned by a human player - if (is->current_config.share_wonders_in_hotseat && - (*p_is_offline_mp_game && ! *p_is_pbem_game) && // is hotseat game - ((p_bic_data->Improvements[improv_id].Characteristics & (ITC_Wonder | ITC_Small_Wonder)) != 0) && - ((1 << this->Body.CivID) & *p_human_player_bits)) { + patch_Sprite_draw_on_map (sprite, __, map_renderer, pixel_x, pixel_y, 1, 1, (p_bic_data->is_zoomed_out != false) + 1, 0); +} - // Add in instances of this improvment on this continent owned by other human players - Tile * this_city_tile = tile_at (this->Body.X, this->Body.Y); - int this_cont_id = this_city_tile->vtable->m46_Get_ContinentID (this_city_tile); - for (int city_index = 0; city_index <= p_cities->LastIndex; city_index++) { - City * city = get_city_ptr (city_index); - if ((city != NULL) && (city != this) && - (city->Body.CivID != this->Body.CivID) && // if city is owned by a different player AND - ((1 << city->Body.CivID) & *p_human_player_bits)) { // that player is a human - Tile * that_city_tile = tile_at (city->Body.X, city->Body.Y); - int that_cont_id = that_city_tile->vtable->m46_Get_ContinentID (that_city_tile); - if ((this_cont_id == that_cont_id) && patch_City_has_improvement (city, __, improv_id, true)) - tr++; - } - } +bool +district_allows_river (struct district_config const * cfg) +{ + unsigned int build_mask = cfg->buildable_square_types_mask; + if (build_mask == 0) + build_mask = district_default_buildable_mask (); + if ((build_mask & square_type_mask_bit (SQ_RIVER)) != 0) + return true; + if (cfg->has_buildable_on_overlays && ((cfg->buildable_on_overlays_mask & DOM_RIVER) != 0)) + return true; + return false; +} +int +get_energy_grid_image_index (int tile_x, int tile_y) +{ + struct district_infos * info = &is->district_infos[ENERGY_GRID_DISTRICT_ID]; + for (int i = 0; i < info->dependent_building_count; i++) { + // Zero is "no building"; Buildings start from index one + int column_index = i + 1; + int building_id = info->dependent_building_ids[i]; + if (tile_coords_has_city_with_building_in_district_radius (tile_x, tile_y, ENERGY_GRID_DISTRICT_ID, building_id)) + return column_index; } - return tr; + return 0; } -void __fastcall -patch_Leader_update_great_library_unlocks (Leader * this) +int +get_bridge_image_index (Tile * tile, int tile_x, int tile_y) +{ + int SW_NE = 0; + int NW_SE = 1; + int N_S = 2; + int W_E = 3; + + if ((tile->vtable->m42_Get_Overlays (tile, __, 0) & TILE_FLAG_RAILROAD) != 0) { + SW_NE += 4; + NW_SE += 4; + N_S += 4; + W_E += 4; + } + + bool bridge_n = tile_has_district_at (tile_x, tile_y - 2, BRIDGE_DISTRICT_ID); + bool bridge_s = tile_has_district_at (tile_x, tile_y + 2, BRIDGE_DISTRICT_ID); + bool bridge_w = tile_has_district_at (tile_x - 2, tile_y, BRIDGE_DISTRICT_ID); + bool bridge_e = tile_has_district_at (tile_x + 2, tile_y, BRIDGE_DISTRICT_ID); + bool bridge_ne = tile_has_district_at (tile_x + 1, tile_y - 1, BRIDGE_DISTRICT_ID); + bool bridge_nw = tile_has_district_at (tile_x - 1, tile_y - 1, BRIDGE_DISTRICT_ID); + bool bridge_se = tile_has_district_at (tile_x + 1, tile_y + 1, BRIDGE_DISTRICT_ID); + bool bridge_sw = tile_has_district_at (tile_x - 1, tile_y + 1, BRIDGE_DISTRICT_ID); + + if (bridge_n || bridge_s || bridge_w || bridge_e || bridge_ne || bridge_nw || bridge_se || bridge_sw) { + int ns_count = (bridge_n ? 1 : 0) + (bridge_s ? 1 : 0); + int we_count = (bridge_w ? 1 : 0) + (bridge_e ? 1 : 0); + int swne_count = (bridge_sw ? 1 : 0) + (bridge_ne ? 1 : 0); + int nwse_count = (bridge_nw ? 1 : 0) + (bridge_se ? 1 : 0); + + if (swne_count == 2) return SW_NE; + if (nwse_count == 2) return NW_SE; + if (ns_count == 2) return N_S; + if (we_count == 2) return W_E; + + if (bridge_ne || bridge_sw) return SW_NE; + if (bridge_nw || bridge_se) return NW_SE; + if (bridge_n || bridge_s) return N_S; + if (bridge_w || bridge_e) return W_E; + } + + int owner_id = tile->Territory_OwnerID; + bool north_land = tile_is_land (owner_id, tile_x, tile_y - 2, false); + bool south_land = tile_is_land (owner_id, tile_x, tile_y + 2, false); + bool west_land = tile_is_land (owner_id, tile_x - 2, tile_y, false); + bool east_land = tile_is_land (owner_id, tile_x + 2, tile_y, false); + bool ne_land = tile_is_land (owner_id, tile_x + 1, tile_y - 1, false); + bool nw_land = tile_is_land (owner_id, tile_x - 1, tile_y - 1, false); + bool se_land = tile_is_land (owner_id, tile_x + 1, tile_y + 1, false); + bool sw_land = tile_is_land (owner_id, tile_x - 1, tile_y + 1, false); + + bool north_link = north_land || bridge_n; + bool south_link = south_land || bridge_s; + bool west_link = west_land || bridge_w; + bool east_link = east_land || bridge_e; + bool ne_link = ne_land || bridge_ne; + bool nw_link = nw_land || bridge_nw; + bool se_link = se_land || bridge_se; + bool sw_link = sw_land || bridge_sw; + + if (sw_link && ne_link) return SW_NE; + if (nw_link && se_link) return NW_SE; + if (north_link && south_link) return N_S; + if (west_link && east_link) return W_E; + + if (ne_link || sw_link) return SW_NE; + if (nw_link || se_link) return NW_SE; + if (north_link || south_link) return N_S; + if (west_link || east_link) return W_E; + + return SW_NE; +} + +void +get_bridge_directions (Tile * tile, int tile_x, int tile_y, int * out_dir1, int * out_dir2) { - // If it's a hotseat game with shared wonder effects and "this" is a human player, share contacts among all human players so that "this" gets - // techs from civs known to any human player in the game. Save the real contact info and restore it afterward. NOTE: Contact info has to go - // two ways here; we must mark that "this" has contacted the AIs and vice-versa otherwise the techs won't be granted. That's why we must save - // & restore all contact bits for all players. - bool restore_contacts = false; - struct contact_set { - int contacts[32]; - } saved_contact_sets[32]; - if (is->current_config.share_wonders_in_hotseat && - (*p_is_offline_mp_game && ! *p_is_pbem_game) && // is hotseat game - ((1 << this->ID) & *p_human_player_bits)) { // "this" is a human player + int dir1 = -1; + int dir2 = -1; + int index = get_bridge_image_index (tile, tile_x, tile_y); - restore_contacts = true; - for (int n = 0; n < 32; n++) - for (int k = 0; k < 32; k++) - saved_contact_sets[n].contacts[k] = leaders[n].Contacts[k]; + switch (index) { + case 0: dir1 = DIR_SW; dir2 = DIR_NE; break; + case 1: dir1 = DIR_NW; dir2 = DIR_SE; break; + case 2: dir1 = DIR_N; dir2 = DIR_S; break; + case 3: dir1 = DIR_W; dir2 = DIR_E; break; + default: break; + } - unsigned human_player_bits = *(unsigned *)p_human_player_bits >> 1; - int n_human = 1; - while (human_player_bits != 0) { - if ((human_player_bits & 1) && (n_human != this->ID)) // n_human is ID of a human player other than "this" - for (int n_ai = 0; n_ai < 32; n_ai++) - if ((*p_player_bits & (1 << n_ai)) && ((*p_human_player_bits & (1 << n_ai)) == 0)) { - // If the human and AI players have contact, mark "this" as having contact with the AI and vice-versa - if (leaders[n_human].Contacts[n_ai] & 1) { - this->Contacts[n_ai] |= 1; - leaders[n_ai].Contacts[this->ID] |= 1; - } + *out_dir1 = dir1; + *out_dir2 = dir2; +} + +void +get_canal_directions (Tile * tile, int tile_x, int tile_y, bool out_water_dirs[9], int * out_dir1, int * out_dir2) +{ + bool canal_at_n = tile_has_district_at (tile_x, tile_y - 2, CANAL_DISTRICT_ID); + bool canal_at_s = tile_has_district_at (tile_x, tile_y + 2, CANAL_DISTRICT_ID); + bool canal_at_w = tile_has_district_at (tile_x - 2, tile_y, CANAL_DISTRICT_ID); + bool canal_at_e = tile_has_district_at (tile_x + 2, tile_y, CANAL_DISTRICT_ID); + bool canal_at_ne = tile_has_district_at (tile_x + 1, tile_y - 1, CANAL_DISTRICT_ID); + bool canal_at_nw = tile_has_district_at (tile_x - 1, tile_y - 1, CANAL_DISTRICT_ID); + bool canal_at_se = tile_has_district_at (tile_x + 1, tile_y + 1, CANAL_DISTRICT_ID); + bool canal_at_sw = tile_has_district_at (tile_x - 1, tile_y + 1, CANAL_DISTRICT_ID); + bool water_n = tile_is_water (tile_x, tile_y - 2); + bool water_s = tile_is_water (tile_x, tile_y + 2); + bool water_w = tile_is_water (tile_x - 2, tile_y); + bool water_e = tile_is_water (tile_x + 2, tile_y); + bool water_ne = tile_is_water (tile_x + 1, tile_y - 1); + bool water_nw = tile_is_water (tile_x - 1, tile_y - 1); + bool water_se = tile_is_water (tile_x + 1, tile_y + 1); + bool water_sw = tile_is_water (tile_x - 1, tile_y + 1); + bool canal_or_water_n = canal_at_n || water_n; + bool canal_or_water_s = canal_at_s || water_s; + bool canal_or_water_w = canal_at_w || water_w; + bool canal_or_water_e = canal_at_e || water_e; + bool canal_or_water_ne = canal_at_ne || water_ne; + bool canal_or_water_nw = canal_at_nw || water_nw; + bool canal_or_water_se = canal_at_se || water_se; + bool canal_or_water_sw = canal_at_sw || water_sw; + + bool canal_dirs[9] = { + false, canal_at_ne, canal_at_e, canal_at_se, + canal_at_s, canal_at_sw, canal_at_w, canal_at_nw, canal_at_n + }; + bool water_dirs[9] = { + false, water_ne, water_e, water_se, + water_s, water_sw, water_w, water_nw, water_n + }; + bool available_dirs[9] = { + false, canal_or_water_ne, canal_or_water_e, canal_or_water_se, + canal_or_water_s, canal_or_water_sw, canal_or_water_w, canal_or_water_nw, canal_or_water_n + }; + + // Avoid acute angles (adjacent directions) that look cramped. + int disallowed_pairs[][2] = { + { DIR_N, DIR_NE }, { DIR_NE, DIR_E }, { DIR_E, DIR_SE }, { DIR_SE, DIR_S }, + { DIR_S, DIR_SW }, { DIR_SW, DIR_W }, { DIR_W, DIR_NW }, { DIR_NW, DIR_N } + }; + int disallowed_pair_count = sizeof(disallowed_pairs) / sizeof(disallowed_pairs[0]); + int pref_dirs[8] = { DIR_NE, DIR_NW, DIR_SE, DIR_SW, DIR_N, DIR_E, DIR_S, DIR_W }; + + int dir1 = -1; + int dir2 = -1; + + bool has_canal_dir = canal_dirs[DIR_N] || canal_dirs[DIR_NE] || canal_dirs[DIR_E] || canal_dirs[DIR_SE] || + canal_dirs[DIR_S] || canal_dirs[DIR_SW] || canal_dirs[DIR_W] || canal_dirs[DIR_NW]; + + if (has_canal_dir) { + for (int i = 0; i < 8 && dir1 == -1; i++) { + int d = pref_dirs[i]; + if (canal_dirs[d]) + dir1 = d; + } + } else { + for (int i = 0; i < 8 && dir1 == -1; i++) { + int d = pref_dirs[i]; + if (available_dirs[d]) + dir1 = d; + } + } + + if (dir1 >= 0) { + for (int pass = 0; pass < 2 && dir2 == -1; pass++) { + bool * dirs = (pass == 0) ? canal_dirs : available_dirs; + for (int i = 0; i < 8; i++) { + int d = pref_dirs[i]; + if (d == dir1 || ! dirs[d]) + continue; + bool pair_is_too_close = false; + for (int k = 0; k < disallowed_pair_count; k++) { + if ((dir1 == disallowed_pairs[k][0] && d == disallowed_pairs[k][1]) || + (dir1 == disallowed_pairs[k][1] && d == disallowed_pairs[k][0])) { + pair_is_too_close = true; + break; } - human_player_bits >>= 1; - n_human++; + } + if (pair_is_too_close) + continue; + dir2 = d; + break; + } } } - Leader_update_great_library_unlocks (this); + int draw_dir1 = dir1; + int draw_dir2 = dir2; + if ((draw_dir1 >= 0) && (draw_dir2 >= 0)) { + int weight1 = 0; + int weight2 = 0; - if (restore_contacts) - for (int n = 0; n < 32; n++) - for (int k = 0; k < 32; k++) - leaders[n].Contacts[k] = saved_contact_sets[n].contacts[k]; -} + if (draw_dir1 == DIR_S) weight1 = 2; + else if ((draw_dir1 == DIR_SE) || (draw_dir1 == DIR_SW)) weight1 = 1; + else if ((draw_dir1 == DIR_NE) || (draw_dir1 == DIR_NW) || (draw_dir1 == DIR_N)) weight1 = -1; -bool __fastcall -patch_Leader_has_wonder_doubling_happiness_from (Leader * this, int edx, int improv_id) -{ - bool tr = Leader_has_wonder_doubling_happiness_from (this, __, improv_id); + if (draw_dir2 == DIR_S) weight2 = 2; + else if ((draw_dir2 == DIR_SE) || (draw_dir2 == DIR_SW)) weight2 = 1; + else if ((draw_dir2 == DIR_NE) || (draw_dir2 == DIR_NW) || (draw_dir2 == DIR_N)) weight2 = -1; - // If we're sharing wonder effects in a hotseat game and "this" is a human player, return true when another human player in the game has a - // wonder doubling happiness - if ((! tr) && - is->current_config.share_wonders_in_hotseat && - (*p_is_offline_mp_game && ! *p_is_pbem_game) && // is hotseat game - ((1 << this->ID) & *p_human_player_bits)) { // "this" is a human player - unsigned human_player_bits = *(unsigned *)p_human_player_bits >> 1; - int n_human = 1; - while (human_player_bits != 0) { - if ((human_player_bits & 1) && - (n_human != this->ID) && - Leader_has_wonder_doubling_happiness_from (&leaders[n_human], __, improv_id)) { - tr = true; - break; - } - human_player_bits >>= 1; - n_human++; + if (weight1 > weight2) { + int tmp = draw_dir1; + draw_dir1 = draw_dir2; + draw_dir2 = tmp; } } - return tr; -} - -int __fastcall -patch_Sprite_draw_citizen_head (Sprite * this, int edx, PCX_Image * canvas, int pixel_x, int pixel_y, PCX_Color_Table * color_table) -{ - // Reset variable - is->specialist_icon_drawing_running_x = INT_MIN; + // Manual overrides - overall algorithm works pretty well, but handle corner cases + if (!has_canal_dir && water_dirs[DIR_NE] && water_dirs[DIR_SW]) { draw_dir1 = DIR_NE; draw_dir2 = DIR_SW; } + if (draw_dir1 == DIR_NE && draw_dir2 == DIR_S && water_dirs[DIR_N] && water_dirs[DIR_NE]) { draw_dir1 = DIR_N; draw_dir2 = DIR_S; } + if (draw_dir1 == DIR_NE && draw_dir2 == DIR_SE && water_dirs[DIR_SE] && water_dirs[DIR_SW]) { draw_dir2 = DIR_SW; } + if (draw_dir1 == DIR_NE && draw_dir2 == DIR_SE && water_dirs[DIR_NE] && water_dirs[DIR_N]) { draw_dir1 = DIR_N; } + if (draw_dir1 == DIR_NE && draw_dir2 == DIR_NW && water_dirs[DIR_S]) { draw_dir2 = DIR_S; } + if (draw_dir1 == DIR_NE && draw_dir2 == DIR_NW && canal_dirs[DIR_NE] && water_dirs[DIR_SW]) { draw_dir2 = DIR_SW; } + if (draw_dir1 == DIR_NE && draw_dir2 == DIR_S && canal_dirs[DIR_NE] && water_dirs[DIR_S]) { draw_dir2 = DIR_SW; } + if (draw_dir1 == DIR_NW && draw_dir2 == DIR_NE && canal_dirs[DIR_NW] && water_dirs[DIR_SE]) { draw_dir2 = DIR_SE; } + if (draw_dir1 == DIR_NW && draw_dir2 == DIR_S && canal_dirs[DIR_NW] && water_dirs[DIR_S]) { draw_dir2 = DIR_SE; } - return Sprite_draw (this, __, canvas, pixel_x, pixel_y, color_table); + *out_dir1 = draw_dir1; + *out_dir2 = draw_dir2; + for (int i = 0; i < 9; i++) + out_water_dirs[i] = water_dirs[i]; } -int -adjust_specialist_yield_icon_x (int pixel_x, int width) -{ - if (is->current_config.fix_overlapping_specialist_yield_icons) { - if (is->specialist_icon_drawing_running_x == INT_MIN) // first icon drawn - is->specialist_icon_drawing_running_x = pixel_x; - int tr = is->specialist_icon_drawing_running_x; - is->specialist_icon_drawing_running_x += width; - return tr; - } else - return pixel_x; +void +draw_canal_on_map_or_canvas(Sprite * sprite, int tile_x, int tile_y, int dir, bool water_dirs[9], Map_Renderer * map_renderer, int draw_x, int draw_y) +{ + int y_offset = 9; + int x_offset = y_offset * 2; + + draw_district_on_map_or_canvas(sprite, map_renderer, draw_x, draw_y); + + // In certain cases, add an additional draw if adjacent to water so that the canal appears to extend far enough + if (dir == DIR_N && water_dirs[DIR_N]) + draw_district_on_map_or_canvas(sprite, map_renderer, draw_x, draw_y - y_offset); + else if (dir == DIR_NE && water_dirs[DIR_NE]) + draw_district_on_map_or_canvas(sprite, map_renderer, draw_x + x_offset, draw_y - y_offset); + else if (dir == DIR_E && water_dirs[DIR_E]) + draw_district_on_map_or_canvas(sprite, map_renderer, draw_x + x_offset, draw_y); + else if (dir == DIR_SE && water_dirs[DIR_SE]) + draw_district_on_map_or_canvas(sprite, map_renderer, draw_x + x_offset, draw_y + y_offset); + else if (dir == DIR_S && water_dirs[DIR_S]) + draw_district_on_map_or_canvas(sprite, map_renderer, draw_x, draw_y + y_offset); + else if (dir == DIR_SW && water_dirs[DIR_SW]) + draw_district_on_map_or_canvas(sprite, map_renderer, draw_x - x_offset, draw_y + y_offset); + else if (dir == DIR_W && water_dirs[DIR_W]) + draw_district_on_map_or_canvas(sprite, map_renderer, draw_x - x_offset, draw_y); + else if (dir == DIR_NW && water_dirs[DIR_NW]) + draw_district_on_map_or_canvas(sprite, map_renderer, draw_x - x_offset, draw_y - y_offset); } -int __fastcall -patch_Sprite_draw_entertainer_yield_icon (Sprite * this, int edx, PCX_Image * canvas, int pixel_x, int pixel_y, PCX_Color_Table * color_table) +void +draw_canal_district (Tile * tile, int tile_x, int tile_y, Map_Renderer * map_renderer, int pixel_x, int pixel_y, int era) { - int width = p_city_form->City_Icons_Images.Icon_12_Happy_Faces.Width / 2; - return Sprite_draw (this, __, canvas, adjust_specialist_yield_icon_x (pixel_x, width), pixel_y, color_table); + struct district_config const * cfg = &is->district_configs[CANAL_DISTRICT_ID]; + int sprite_width = (cfg->custom_width > 0) ? cfg->custom_width : 128; + int sprite_height = (cfg->custom_height > 0) ? cfg->custom_height : 64; + int offset_x = pixel_x + cfg->x_offset; + int offset_y = pixel_y + cfg->y_offset; + int dir1_draw_x = offset_x - ((sprite_width - 128) / 2); + int dir1_draw_y = offset_y - (sprite_height - 64); + int dir2_draw_x = dir1_draw_x; + int dir2_draw_y = dir1_draw_y; + + int draw_dir1 = -1; + int draw_dir2 = -1; + bool water_dirs[9] = { false, false, false, false, false, false, false, false, false }; + get_canal_directions (tile, tile_x, tile_y, water_dirs, &draw_dir1, &draw_dir2); + + // Set offsets based on directions for (literal) edge cases + if (draw_dir1 == DIR_NE && draw_dir2 == DIR_S) { dir1_draw_x += 7; dir1_draw_y -= 2; } + else if (draw_dir1 == DIR_N && draw_dir2 == DIR_W) { dir2_draw_x -= 12; } + else if (draw_dir1 == DIR_N && draw_dir2 == DIR_SE) { dir2_draw_x += 4; } + + if (draw_dir1 >= 0) { + Sprite * sprite1 = &is->district_img_sets[CANAL_DISTRICT_ID].imgs[0][era][draw_dir1]; + draw_canal_on_map_or_canvas(sprite1, tile_x, tile_y, draw_dir1, water_dirs, map_renderer, dir1_draw_x, dir1_draw_y); + } + + if (draw_dir2 >= 0) { + Sprite * sprite2 = &is->district_img_sets[CANAL_DISTRICT_ID].imgs[0][era][draw_dir2]; + draw_canal_on_map_or_canvas(sprite2, tile_x, tile_y, draw_dir2, water_dirs, map_renderer, dir2_draw_x, dir2_draw_y); + } } -int __fastcall -patch_Sprite_draw_scientist_yield_icon (Sprite * this, int edx, PCX_Image * canvas, int pixel_x, int pixel_y, PCX_Color_Table * color_table) -{ - int width = p_city_form->City_Icons_Images.Icon_16_Science.Width / 2; - return Sprite_draw (this, __, canvas, adjust_specialist_yield_icon_x (pixel_x, width), pixel_y, color_table); + +void +draw_great_wall_district (Tile * tile, int tile_x, int tile_y, Map_Renderer * map_renderer, int pixel_x, int pixel_y) +{ + bool wall_nw = tile_has_district_at (tile_x - 1, tile_y - 1, GREAT_WALL_DISTRICT_ID); + bool wall_ne = tile_has_district_at (tile_x + 1, tile_y - 1, GREAT_WALL_DISTRICT_ID); + bool wall_se = tile_has_district_at (tile_x + 1, tile_y + 1, GREAT_WALL_DISTRICT_ID); + bool wall_sw = tile_has_district_at (tile_x - 1, tile_y + 1, GREAT_WALL_DISTRICT_ID); + bool wall_s = tile_has_district_at (tile_x, tile_y + 2, GREAT_WALL_DISTRICT_ID); + bool wall_n = tile_has_district_at (tile_x, tile_y - 2, GREAT_WALL_DISTRICT_ID); + + bool water_ne = tile_is_water (tile_x - 1, tile_y - 1); + bool water_nw = tile_is_water (tile_x + 1, tile_y - 1); + bool water_w = tile_is_water (tile_x - 2, tile_y); + bool water_n = tile_is_water (tile_x, tile_y + 2); + bool water_sw = tile_is_water (tile_x - 1, tile_y + 1); + bool water_se = tile_is_water (tile_x + 1, tile_y + 1); + + Sprite * sprites = is->district_img_sets[GREAT_WALL_DISTRICT_ID].imgs[0][0]; + Sprite * base = &sprites[0]; + + // Rotate around clockwise NW -> SW to get the perspective right + if (wall_nw) draw_district_on_map_or_canvas(&sprites[DIR_NW], map_renderer, pixel_x, pixel_y); + if (wall_ne) draw_district_on_map_or_canvas(&sprites[DIR_NE], map_renderer, pixel_x, pixel_y); + + if (!wall_nw && !wall_ne && wall_n && water_ne) + draw_district_on_map_or_canvas(&sprites[DIR_N], map_renderer, pixel_x, pixel_y); + if (!wall_ne && wall_n && water_nw) + draw_district_on_map_or_canvas(&sprites[DIR_N], map_renderer, pixel_x, pixel_y); + if (water_sw && water_nw && !water_w) + draw_district_on_map_or_canvas(&sprites[DIR_W], map_renderer, pixel_x, pixel_y); + if (water_n && !wall_nw && !wall_ne) + draw_district_on_map_or_canvas(&sprites[DIR_N], map_renderer, pixel_x, pixel_y); + + // Base pillar + draw_district_on_map_or_canvas(base, map_renderer, pixel_x, pixel_y); + + if (wall_sw) draw_district_on_map_or_canvas(&sprites[DIR_SW], map_renderer, pixel_x, pixel_y); + if (wall_se) draw_district_on_map_or_canvas(&sprites[DIR_SE], map_renderer, pixel_x, pixel_y); + + if (wall_s && !wall_sw && !wall_se) + draw_district_on_map_or_canvas(&sprites[DIR_S], map_renderer, pixel_x, pixel_y); } -int __fastcall -patch_Sprite_draw_tax_collector_yield_icon (Sprite * this, int edx, PCX_Image * canvas, int pixel_x, int pixel_y, PCX_Color_Table * color_table) -{ - int width = p_city_form->City_Icons_Images.Icon_14_Gold.Width / 2; - return Sprite_draw (this, __, canvas, adjust_specialist_yield_icon_x (pixel_x, width), pixel_y, color_table); + +void +draw_district_generated_resource_on_tile (Map_Renderer * this, Tile * tile, struct district_instance * inst, + int tile_x, int tile_y, Map_Renderer * map_renderer, int pixel_x, int pixel_y, int visible_to_civ_id) +{ + int base_resource = Tile_get_resource_visible_to (tile, __, visible_to_civ_id); + int district_resource = -1; + + if (inst->state == DS_COMPLETED) { + int district_id = inst->district_id; + if ((district_id >= 0) && (district_id < is->district_count)) { + struct district_config * cfg = &is->district_configs[district_id]; + if (cfg->generated_resource_id >= 0) { + int owner_id = tile->vtable->m38_Get_Territory_OwnerID (tile); + if ((owner_id >= 0) && ((visible_to_civ_id < 0) || (owner_id == visible_to_civ_id))) { + int res_id = cfg->generated_resource_id; + if (visible_to_civ_id >= 0) { + if (district_can_generate_resource (visible_to_civ_id, cfg)) + district_resource = res_id; + } else + district_resource = res_id; + } + } + } + } + + if (district_resource < 0) { + Map_Renderer_m09_Draw_Tile_Resources(this, __, visible_to_civ_id, tile_x, tile_y, map_renderer, pixel_x, pixel_y); + return; + } + + int tile_width = p_bic_data->is_zoomed_out ? 64 : 128; + int offset = tile_width >> 2; + int left_x = pixel_x - (offset >> 1); + int right_x = pixel_x + (offset >> 1); + + if (base_resource >= 0) + Map_Renderer_m09_Draw_Tile_Resources(this, __, visible_to_civ_id, tile_x, tile_y, map_renderer, left_x, pixel_y); + + int icon_id = p_bic_data->ResourceTypes[district_resource].IconID; + if (icon_id >= 0 && icon_id < 36 && map_renderer != NULL && map_renderer->Resources != NULL) { + Sprite * sprite = &map_renderer->Resources[icon_id]; + int tile_height = tile_width >> 1; + int sprite_width = sprite->Width; + int sprite_height = sprite->Height; + if (sprite_width <= 0) sprite_width = sprite->Width3; + if (sprite_height <= 0) sprite_height = sprite->Height3; + int center_x = (tile_width - sprite_width) >> 1; + int center_y = (tile_height - sprite_height) >> 1; + int draw_x = (base_resource >= 0) ? right_x : pixel_x; + draw_x += center_x; + int draw_y = pixel_y + center_y; + int draw_scale = (p_bic_data->is_zoomed_out != false) + 1; + patch_Sprite_draw_on_map (sprite, __, map_renderer, draw_x, draw_y, 1, 1, draw_scale, 0); + } } -int __fastcall -patch_Sprite_draw_civil_engineer_yield_icon (Sprite * this, int edx, PCX_Image * canvas, int pixel_x, int pixel_y, PCX_Color_Table * color_table) + +int +count_completed_buildings_in_district_radius (int tile_x, int tile_y, int district_id) { - int width = p_city_form->City_Icons_Images.Icon_13_Shield.Width / 2; - return Sprite_draw (this, __, canvas, adjust_specialist_yield_icon_x (pixel_x, width), pixel_y, color_table); + struct district_config const * cfg = &is->district_configs[district_id]; + struct district_infos * district_info = &is->district_infos[district_id]; + int completed_count = 0; + for (int i = 0; i < district_info->dependent_building_count; i++) { + int building_id = district_info->dependent_building_ids[i]; + if ((building_id >= 0) && tile_coords_has_city_with_building_in_district_radius (tile_x, tile_y, district_id, building_id)) + completed_count++; + } + return completed_count; } -int __fastcall -patch_Sprite_draw_police_officer_yield_icon (Sprite * this, int edx, PCX_Image * canvas, int pixel_x, int pixel_y, PCX_Color_Table * color_table) + +void +draw_district_on_map_or_canvas_by_buildings (Sprite * base_sprite, Map_Renderer * map_renderer, int district_id, int variant, int era, int tile_x, int tile_y, int draw_x, int draw_y) { - int width = p_city_form->City_Icons_Images.Icon_17_Gold_Outcome.Width / 2; - return Sprite_draw (this, __, canvas, adjust_specialist_yield_icon_x (pixel_x, width), pixel_y, color_table); + struct district_config const * cfg = &is->district_configs[district_id]; + struct district_infos * district_info = &is->district_infos[district_id]; + + draw_district_on_map_or_canvas(base_sprite, map_renderer, draw_x, draw_y); + + for (int i = 0; i < district_info->dependent_building_count; i++) { + // Zero is "base texture"; Actual building column art starts from index one + int column_index = i + 1; + int building_id = district_info->dependent_building_ids[i]; + if ((building_id >= 0) && tile_coords_has_city_with_building_in_district_radius (tile_x, tile_y, district_id, building_id)) { + Sprite * district_sprite = &is->district_img_sets[district_id].imgs[variant][era][column_index]; + draw_district_on_map_or_canvas(district_sprite, map_renderer, draw_x, draw_y); + } + } } -void __fastcall -patch_City_add_building_if_done (City * this) +void +draw_district_on_tile (Map_Renderer * this, Tile * tile, struct district_instance * inst, int tile_x, int tile_y, Map_Renderer * map_renderer, int pixel_x, int pixel_y, int visible_to_civ_id) { - // If sharing small wonders in hotseat, check whether the city is building a small wonder that's no longer available to the player b/c its - // effects are provided from another. - int improv_id = this->Body.Order_ID; - Improvement * improv = &p_bic_data->Improvements[improv_id]; - int already_built_by_id; - if ((improv->Characteristics & ITC_Small_Wonder) && - is->current_config.share_wonders_in_hotseat && - (*p_is_offline_mp_game && ! *p_is_pbem_game) && // is hotseat game - ((1 << this->Body.CivID) & *p_human_player_bits) && // city owned by a human player - ((already_built_by_id = find_human_player_with_small_wonder (improv_id)) != -1) && // SW already built by another human player - ((improv->SmallWonderFlags & ~shared_small_wonder_flags) == 0)) { // SW has no non-shared effects + int district_id = inst->district_id; + if (is->dc_img_state == IS_UNINITED) + init_district_images (); - // Switch city production to something else and notify the player - this->vtable->set_production_to_most_expensive_option (this); - if ((this->Body.CivID == p_main_screen_form->Player_CivID) && (already_built_by_id != p_main_screen_form->Player_CivID)) { - char * new_build_name = (this->Body.Order_Type == COT_Improvement) ? - p_bic_data->Improvements[this->Body.Order_ID].Name.S : - p_bic_data->UnitTypes[this->Body.Order_ID].Name; - PopupForm * popup = get_popup_form (); - set_popup_str_param (0, this->Body.CityName, -1, -1); - set_popup_str_param (1, improv->Name.S, -1, -1); - set_popup_str_param (2, Leader_get_civ_noun (&leaders[already_built_by_id]), -1, -1); - set_popup_str_param (3, new_build_name, -1, -1); - popup->vtable->set_text_key_and_flags (popup, __, is->mod_script_path, "C3X_SHARED_WONDER_CHANGE", -1, 0, 0, 0); - int response = patch_show_popup (popup, __, 0, 0); - if (response == 0) - *p_zoom_to_city_after_update = true; + if (is->dc_img_state != IS_OK) + return; + + // Natural Wonder + if (district_id == NATURAL_WONDER_DISTRICT_ID) { + if (! is->current_config.enable_natural_wonders) + return; + + int natural_id = inst->natural_wonder_info.natural_wonder_id; + if ((natural_id >= 0) && (natural_id < is->natural_wonder_count)) { + Sprite * nsprite = &is->natural_wonder_img_sets[natural_id].img; + int y_offset = 88 - 64; // Height of wonder img minus height of tile + int draw_y = pixel_y - y_offset; + + draw_district_on_map_or_canvas(nsprite, map_renderer, pixel_x, draw_y); + } + return; + } + + // Districts + if (is->current_config.enable_districts) { + if (district_id < 0 || district_id >= is->district_count) return; + bool completed = district_is_complete (tile, district_id); + + if (! completed) + return; + + struct district_config const * cfg = &is->district_configs[district_id]; + struct district_infos * district_info = &is->district_infos[district_id]; + int territory_owner_id = tile->Territory_OwnerID; + int variant = 0; + int era = 0; + int culture = 0; + int buildings = 0; + int draw_pixel_x = pixel_x; + int draw_pixel_y = pixel_y; + enum direction river_dir = DIR_ZERO; + + // If in a territory, use owner's culture/era + if (territory_owner_id > 0) { + Leader * leader = &leaders[territory_owner_id]; + culture = p_bic_data->Races[leader->RaceID].CultureGroupID; + if (cfg->vary_img_by_culture) + variant = culture; + if (cfg->vary_img_by_era) + era = leader->Era; + if (cfg->align_to_coast) + align_variant_and_pixel_offsets_with_coastline (tile, &variant, &draw_pixel_x, &draw_pixel_y); + + // Else render abandoned if not a Wonder, Natural Wonder, Bridge, or Canal + } else if (district_id != WONDER_DISTRICT_ID && district_id != NATURAL_WONDER_DISTRICT_ID && district_id != BRIDGE_DISTRICT_ID && district_id != CANAL_DISTRICT_ID) { + Sprite * abandoned_sprite = &is->abandoned_district_img; + if (tile->vtable->m35_Check_Is_Water (tile) && is->abandoned_maritime_district_img.vtable != NULL) + abandoned_sprite = &is->abandoned_maritime_district_img; + if (abandoned_sprite->vtable != NULL) { + draw_district_on_map_or_canvas(abandoned_sprite, map_renderer, draw_pixel_x + cfg->x_offset, draw_pixel_y + cfg->y_offset); + } + return; + } + + // If out of a territory (and not abandoned) but builder is known, use builder's era & culture + if (territory_owner_id < 0 && inst->built_by_civ_id >= 0) { + Leader * builder = &leaders[inst->built_by_civ_id]; + culture = p_bic_data->Races[builder->RaceID].CultureGroupID; + if (cfg->vary_img_by_culture) + variant = culture; + if (cfg->vary_img_by_era) + era = builder->Era; + } + + int sprite_width = (cfg->custom_width > 0) ? cfg->custom_width : 128; + int sprite_height = (cfg->custom_height > 0) ? cfg->custom_height : 64; + int offset_x = draw_pixel_x + cfg->x_offset; + int offset_y = draw_pixel_y + cfg->y_offset; + int draw_x = offset_x - ((sprite_width - 128) / 2); + int draw_y = offset_y - (sprite_height - 64); + Sprite (*sprites)[MAX_DISTRICT_ERA_COUNT][MAX_DISTRICT_COLUMN_COUNT] = is->district_img_sets[district_id].imgs; + + // Render + switch (district_id) { + case WONDER_DISTRICT_ID: + { + if (! is->current_config.enable_wonder_districts) + return; + + struct wonder_district_info * info = get_wonder_district_info (tile); + if (info == NULL) + return; + + int construct_windex = -1; + Sprite * wsprite = NULL; + + // Completed + if (info->state == WDS_COMPLETED) { + int windex = info->wonder_index; + if ((windex < 0) || (windex >= is->wonder_district_count)) + return; + + struct wonder_district_config * wcfg = &is->wonder_district_configs[windex]; + struct wonder_district_image_set * set = &is->wonder_district_img_sets[windex]; + + if (wonder_allows_river(wcfg)) + align_district_with_river (tile, &draw_pixel_x, &draw_pixel_y, &river_dir); + + bool use_alt_dir = wcfg->enable_img_alt_dir && wonder_should_use_alternative_direction_image (tile_x, tile_y, territory_owner_id, wcfg); + wsprite = (use_alt_dir && (set->alt_dir_img.vtable != NULL)) ? &set->alt_dir_img : &set->img; + + draw_district_on_map_or_canvas(wsprite, map_renderer, offset_x, offset_y); + return; + + // Under construction + } else if (wonder_district_tile_under_construction (tile, tile_x, tile_y, &construct_windex) && (construct_windex >= 0)) { + if (construct_windex >= is->wonder_district_count) + return; + + struct wonder_district_config * wcfg = &is->wonder_district_configs[construct_windex]; + struct wonder_district_image_set * set = &is->wonder_district_img_sets[construct_windex]; + bool use_alt_dir = wcfg->enable_img_alt_dir && wonder_should_use_alternative_direction_image (tile_x, tile_y, territory_owner_id, wcfg); + wsprite = (use_alt_dir && (set->alt_dir_construct_img.vtable != NULL)) ? &set->alt_dir_construct_img : &set->construct_img; + + draw_district_on_map_or_canvas(wsprite, map_renderer, offset_x, offset_y); + return; + + // Unused + } else { + draw_district_on_map_or_canvas(&sprites[variant][era][buildings], map_renderer, draw_x, draw_y); + return; + } + break; + } + case NEIGHBORHOOD_DISTRICT_ID: + { + if (! is->current_config.enable_neighborhood_districts) + return; + + unsigned v = (unsigned)tile_x * 0x9E3779B1u + (unsigned)tile_y * 0x85EBCA6Bu; + v ^= v >> 16; + v *= 0x7FEB352Du; + v ^= v >> 15; + v *= 0x846CA68Bu; + v ^= v >> 16; + buildings = clamp(0, 3, (int)(v & 3u)); /* final 0..3 */ + variant = culture; + draw_district_on_map_or_canvas(&sprites[variant][era][buildings], map_renderer, draw_x, draw_y); + return; + } + case DISTRIBUTION_HUB_DISTRICT_ID: + if (! is->current_config.enable_distribution_hub_districts) + return; + + draw_district_on_map_or_canvas(&sprites[variant][era][buildings], map_renderer, draw_x, draw_y); + return; + case ENERGY_GRID_DISTRICT_ID: + { + if (! is->current_config.enable_energy_grid_districts) + return; + + buildings = get_energy_grid_image_index (tile_x, tile_y); + draw_district_on_map_or_canvas(&sprites[variant][era][buildings], map_renderer, draw_x, draw_y); + return; + } + case BRIDGE_DISTRICT_ID: + { + if (! is->current_config.enable_bridge_districts) + return; + + buildings = get_bridge_image_index (tile, tile_x, tile_y); + draw_district_on_map_or_canvas(&sprites[variant][era][buildings], map_renderer, draw_x, draw_y); + return; + } + case CANAL_DISTRICT_ID: + { + if (! is->current_config.enable_canal_districts) + return; + + draw_canal_district (tile, tile_x, tile_y, map_renderer, pixel_x, pixel_y, era); + return; + } + case GREAT_WALL_DISTRICT_ID: + { + if (! is->current_config.enable_great_wall_districts) + return; + + draw_great_wall_district (tile, tile_x, tile_y, map_renderer, pixel_x, pixel_y); + return; + } + default: + { + // Draw by counting number of completed buildings in radius + if (cfg->render_strategy == DRS_BY_COUNT) { + buildings = count_completed_buildings_in_district_radius (tile_x, tile_y, district_id); + draw_district_on_map_or_canvas(&sprites[variant][era][buildings], map_renderer, draw_x, draw_y); + return; + } + // Draw by checking each building individually and layering images + else if (cfg->render_strategy == DRS_BY_BUILDING) { + Sprite * base_sprite = &is->district_img_sets[district_id].imgs[variant][era][0]; + draw_district_on_map_or_canvas_by_buildings(base_sprite, map_renderer, district_id, variant, era, tile_x, tile_y, draw_x, draw_y); + return; + } + } } - - // As in the base logic, if production gets switched, the game doesn't check if it might still complete on the same turn. - return; } - City_add_building_if_done (this); + Map_Renderer_m12_Draw_Tile_Buildings(this, __, visible_to_civ_id, tile_x, tile_y, map_renderer, pixel_x, pixel_y); } -bool __fastcall -patch_City_can_build_upgrade_type (City * this, int edx, int unit_type_id, bool exclude_upgradable, int param_3, bool allow_kings) +void __fastcall +patch_Map_Renderer_m12_Draw_Tile_Buildings(Map_Renderer * this, int edx, int visible_to_civ_id, int tile_x, int tile_y, Map_Renderer * map_renderer, int pixel_x, int pixel_y) { - UnitType * type = &p_bic_data->UnitTypes[unit_type_id]; - if (is->current_config.prevent_old_units_from_upgrading_past_ability_block && - ((type->Special_Actions & UCV_Upgrade_Unit) == 0) && - (type->Available_To & (1 << leaders[this->Body.CivID].RaceID))) - exclude_upgradable = false; + if (! is->current_config.enable_districts && ! is->current_config.enable_natural_wonders) { + Map_Renderer_m12_Draw_Tile_Buildings(this, __, visible_to_civ_id, tile_x, tile_y, map_renderer, pixel_x, pixel_y); + return; + } - return patch_City_can_build_unit (this, __, unit_type_id, exclude_upgradable, param_3, allow_kings); + Tile * tile = is->current_render_tile; + if ((tile == NULL) || (tile == p_null_tile)) + return; + + struct district_instance * inst = is->current_render_tile_district; + if (inst == NULL) { + Map_Renderer_m12_Draw_Tile_Buildings(this, __, visible_to_civ_id, tile_x, tile_y, map_renderer, pixel_x, pixel_y); + return; + } + + // Draw resources first if needed + if (is->district_configs[inst->district_id].draw_over_resources) + draw_district_generated_resource_on_tile (this, tile, inst, tile_x, tile_y, map_renderer, pixel_x, pixel_y, visible_to_civ_id); + + draw_district_on_tile (this, tile, inst, tile_x, tile_y, map_renderer, pixel_x, pixel_y, visible_to_civ_id); } void __fastcall -patch_Main_GUI_position_elements (Main_GUI * this) +patch_Map_Renderer_m09_Draw_Tile_Resources (Map_Renderer * this, int edx, int visible_to_civ_id, int tile_x, int tile_y, Map_Renderer * map_renderer, int pixel_x, int pixel_y) { - Main_GUI_position_elements (this); + if (! is->current_config.enable_districts) { + Map_Renderer_m09_Draw_Tile_Resources(this, __, visible_to_civ_id, tile_x, tile_y, map_renderer, pixel_x, pixel_y); + return; + } - // Double size of minimap if configured - bool want_larger_minimap = (is->current_config.double_minimap_size == MDM_ALWAYS) || - ((is->current_config.double_minimap_size == MDM_HIGH_DEF) && (p_bic_data->ScreenWidth >= 1920)); - if (want_larger_minimap && (init_large_minimap_frame () == IS_OK)) { - this->Mini_Map_Click_Rect.top -= 105; - this->Mini_Map_Click_Rect.right += 229; + Tile * tile = is->current_render_tile; + if ((tile == NULL) || (tile == p_null_tile)) { + Map_Renderer_m09_Draw_Tile_Resources(this, __, visible_to_civ_id, tile_x, tile_y, map_renderer, pixel_x, pixel_y); + return; } -} -#define PEDIA_DESC_LINES_PER_PAGE 38 + struct district_instance * inst = is->current_render_tile_district; + if (inst == NULL) { + Map_Renderer_m09_Draw_Tile_Resources(this, __, visible_to_civ_id, tile_x, tile_y, map_renderer, pixel_x, pixel_y); + return; + } -// Returns whether or not the line should be drawn -bool -do_next_line_for_pedia_desc (PCX_Image * canvas, int * inout_y) + // Resources that should be drawn below district are already drawn, skip in that case + if (is->district_configs[inst->district_id].draw_over_resources) + return; + + draw_district_generated_resource_on_tile (this, tile, inst, tile_x, tile_y, map_renderer, pixel_x, pixel_y, visible_to_civ_id); +} + +void __fastcall +patch_Map_Renderer_m11_Draw_Tile_Irrigation (Map_Renderer *this, int edx, int visible_to_civ, int tile_x, int tile_y, int param_4, int param_5, int param_6) { - if (is->cmpd.drawing_lines && is->current_config.allow_multipage_civilopedia_descriptions) { - int first_line_on_shown_page = is->cmpd.shown_page * PEDIA_DESC_LINES_PER_PAGE; - int page = is->cmpd.line_count / PEDIA_DESC_LINES_PER_PAGE; - is->cmpd.line_count += 1; - is->cmpd.last_page = (page > is->cmpd.last_page) ? page : is->cmpd.last_page; + if (! is->current_config.enable_districts) { + Map_Renderer_m11_Draw_Tile_Irrigation (this, __, visible_to_civ, tile_x, tile_y, param_4, param_5, param_6); + return; + } - if (page == is->cmpd.shown_page) { - *inout_y -= is->cmpd.shown_page * PEDIA_DESC_LINES_PER_PAGE * PCX_Image_get_text_line_height (canvas); - return true; - } else - return false; + struct district_instance * inst = is->current_render_tile_district; + if (inst == NULL) { + Map_Renderer_m11_Draw_Tile_Irrigation (this, __, visible_to_civ, tile_x, tile_y, param_4, param_5, param_6); + return; } - return true; + + // If it has a completed district that serves as pseudo-irrigation source, suppress drawing irrigation + if (is->district_configs[inst->district_id].allow_irrigation_from && district_is_complete (is->current_render_tile, inst->district_id)) + return; + + Map_Renderer_m11_Draw_Tile_Irrigation (this, __, visible_to_civ, tile_x, tile_y, param_4, param_5, param_6); } -int __fastcall -patch_PCX_Image_do_draw_centered_text_in_wrap_func (PCX_Image * this, int edx, char * str, int x, int y, int width, unsigned str_len) +bool __fastcall +patch_Tile_has_city_or_district (Tile * this) { - if (do_next_line_for_pedia_desc (this, &y)) - return PCX_Image_do_draw_centered_text (this, __, str, x, y, width, str_len); - else - return 0; // Caller does not use return value + bool has_city = Tile_has_city (this); + if (is->current_config.enable_districts || is->current_config.enable_natural_wonders) { + return has_city || (get_district_instance (this) != NULL); + } + return has_city; } int __fastcall -patch_PCX_Image_draw_text_in_wrap_func (PCX_Image * this, int edx, char * str, int x, int y, int str_len) +patch_Tile_check_water_for_navigator_cell_coloring (Tile * this) { - if (do_next_line_for_pedia_desc (this, &y)) - return PCX_Image_draw_text (this, __, str, x, y, str_len); + if (! is->current_config.show_territory_colors_on_water_tiles_in_minimap) + return this->vtable->m35_Check_Is_Water (this); else - return PCX_Image_draw_text (this, __, " ", x, y, 1); // Caller uses the return value here so draw an empty string isntead of doing nothing + return 0; } -// Steam code is slightly different; this no-len version of draw_text gets called sometimes. It's the same as draw_text instead it computes the string -// length itself instead of taking it in as a parameter. -int __fastcall -patch_PCX_Image_draw_text_no_len_in_wrap_func (PCX_Image * this, int edx, char * str, int x, int y) +bool +is_skippable_popup (char * text_key) { - return patch_PCX_Image_draw_text_in_wrap_func (this, __, str, x, y, strlen (str)); + char * skippable_keys[] = {"SUMMARY_END_GOLDEN_AGE", "SUMMARY_END_SCIENCE_AGE", "SUMMARY_NEW_SMALL_WONDER", // unimportant domestic things + "WONDERPRODUCE", // another civ completed a wonder + "MAKEPEACE", "MUTUALPROTECTIONPACT", "MILITARYALLIANCE", "SUMMARY_DECLARE_WAR", // diplo events not involving the player + "TRADEEMBARGOENDS", // embargo vs player ends + "SUMMARY_CIV_DESTROYED_BY_CIV", "SUMMARY_CIV_DESTROYED", // foreign civs destroyed not by player + "LOSTGOOD", // 'We lost our supply of ...!' + "TRADEEMBARGO", "MILITARYALLIANCEWARONUS", "MILITARYALLIANCEAGAINSTUS", // trade embargo or alliance vs player + "SUMMARY_TRAVELERS_REPORT"}; // another civs starts a wonder + + for (int n = 0; n < ARRAY_LEN (skippable_keys); n++) + if (strcmp (text_key, skippable_keys[n]) == 0) + return true; + return false; } -void -draw_civilopedia_article (void (__fastcall * base) (Civilopedia_Article *), Civilopedia_Article * article) +int __fastcall +patch_PopupForm_impl_begin_showing_popup (PopupForm * this) { - // If the article changed then clear things from the old one - if (is->cmpd.article != article) { - is->cmpd.last_page = 0; - is->cmpd.shown_page = 0; - is->cmpd.article = article; - } + if (is_online_game () || + (! is->current_config.convert_some_popups_into_online_mp_messages) || + (! is_skippable_popup (this->text_key))) + return PopupForm_impl_begin_showing_popup (this); - is->cmpd.line_count = 0; - is->cmpd.drawing_lines = article->show_description; + else { + unsigned saved_prefs = *p_preferences; + int saved_flags = this->field_1BF0[0xE4]; - base (article); + *p_preferences |= P_SHOW_FEWER_MP_POPUPS; + this->field_1BF0[0xE4] |= 0x4000; + int tr = PopupForm_impl_begin_showing_popup (this); - is->cmpd.drawing_lines = false; -} + *(bool *)(p_main_screen_form->animator.field_18E4 + 10) = true; // Set what must be a dirty flag + Animator_update (&p_main_screen_form->animator); // Make sure message appears -void __fastcall -patch_Civilopedia_Article_m01_Draw_GCON_or_RACE (Civilopedia_Article * this) -{ - draw_civilopedia_article (Civilopedia_Article_m01_Draw_GCON_or_RACE, this); + this->field_1BF0[0xE4] = saved_flags; + *p_preferences = saved_prefs; + + return tr; + } } -void __fastcall -patch_Civilopedia_Article_m01_Draw_UNIT (Civilopedia_Article * this) +bool __stdcall +patch_is_online_game_for_show_popup () { - draw_civilopedia_article (Civilopedia_Article_m01_Draw_UNIT, this); + return is->current_config.convert_some_popups_into_online_mp_messages ? true : is_online_game (); } -void __fastcall -patch_Civilopedia_Form_m53_On_Control_Click (Civilopedia_Form * this, int edx, CivilopediaControlID control_id) +bool +ai_move_district_worker (Unit * worker, struct district_worker_record * rec) { - Civilopedia_Article * current_article = (p_civilopedia_form->Current_Article_ID >= 0) ? p_civilopedia_form->Articles[p_civilopedia_form->Current_Article_ID] : NULL; + if ((worker == NULL) || (rec == NULL)) + return false; - // "Effects" button leaves description mode, returns to showing effects - if ((control_id == PEDIA_MULTIPAGE_EFFECTS_BUTTON_ID) && (current_article != NULL)) { - current_article->show_description = false; - is->cmpd.shown_page = 0; - play_sound_effect (26); // 26 = SE_SELECT - p_civilopedia_form->Base.vtable->m73_call_m22_Draw ((Base_Form *)p_civilopedia_form); + char ss[200]; + snprintf (ss, sizeof ss, "ai_move_district_worker: Worker ID %d assigned to build district\n", worker->Body.ID); + (*p_OutputDebugStringA) (ss); - // "Previous" button shows the previous page of a multi-page description or switches to effects mode if on the first page - } else if (control_id == PEDIA_MULTIPAGE_PREV_BUTTON_ID) { - if (is->cmpd.shown_page > 0) - is->cmpd.shown_page -= 1; - else - current_article->show_description = false; - play_sound_effect (26); - p_civilopedia_form->Base.vtable->m73_call_m22_Draw ((Base_Form *)p_civilopedia_form); + // Check the original request city made for district + struct pending_district_request * req = rec->pending_req; + if ((req == NULL) || + (req->assigned_worker_id != worker->Body.ID) || + (req->target_x < 0) || (req->target_y < 0)) + return false; + + int district_id = req->district_id; + snprintf (ss, sizeof ss, "ai_move_district_worker: Worker ID %d moving to (%d,%d) to build district\n", worker->Body.ID, req->target_x, req->target_y); + (*p_OutputDebugStringA) (ss); + + City * request_city = get_city_ptr (req->city_id); + if (request_city == NULL) { + clear_tracked_worker_assignment (rec); + remove_pending_district_request (req); + return false; + } + req->city = request_city; + struct district_config * cfg = &is->district_configs[district_id]; + + // If the worker has arrived + if ((worker->Body.X == req->target_x) && (worker->Body.Y == req->target_y)) { + + snprintf (ss, sizeof ss, "ai_move_district_worker: Worker ID %d arrived at (%d,%d) to build district\n", worker->Body.ID, worker->Body.X, worker->Body.Y); + (*p_OutputDebugStringA) (ss); + + Tile * tile = tile_at (worker->Body.X, worker->Body.Y); + struct district_instance * inst = get_district_instance (tile); + bool do_replacement = false; + + // If there is a completed district here already + if ((inst != NULL) && district_is_complete (tile, inst->district_id)) { + int existing_district_id = inst->district_id; + + snprintf (ss, sizeof ss, "ai_move_district_worker: Worker ID %d found existing district ID %d at (%d,%d)\n", worker->Body.ID, existing_district_id, worker->Body.X, worker->Body.Y); + (*p_OutputDebugStringA) (ss); + + if (existing_district_id == req->district_id) { + clear_city_district_request (request_city, req->district_id); + clear_tracked_worker_assignment (rec); + return false; + } + + // Allow replacement of unused wonder districts + if (existing_district_id == WONDER_DISTRICT_ID) { + struct wonder_district_info * info = &inst->wonder_info; + if (info->state == WDS_UNUSED) + do_replacement = true; + // Allow replace of Great Wall if the civ has the tech to make it obsolete + } else if (existing_district_id == GREAT_WALL_DISTRICT_ID) { + + } else { + snprintf (ss, sizeof ss, "ai_move_district_worker: Worker ID %d cannot replace existing district ID %d at (%d,%d), cancelling request\n", worker->Body.ID, existing_district_id, worker->Body.X, worker->Body.Y); + (*p_OutputDebugStringA) (ss); + clear_city_district_request (request_city, req->district_id); + return false; + } + + if (!do_replacement) { + return false; // Nothing left to do here + } + } + + snprintf (ss, sizeof ss, "ai_move_district_worker: Checking for duplicate districts near city at (%d,%d)\n", request_city->Body.X, request_city->Body.Y); + (*p_OutputDebugStringA) (ss); + + // One final check: do we still need the district? Check for any dupes nearby + if (req->district_id != DISTRIBUTION_HUB_DISTRICT_ID && req->district_id != NEIGHBORHOOD_DISTRICT_ID) { + FOR_DISTRICTS_AROUND (wai, request_city->Body.X, request_city->Body.Y, false) { + if (wai.tile == tile) + continue; + + if (wai.district_inst->district_id == req->district_id) { + snprintf (ss, sizeof ss, "ai_move_district_worker: Found duplicate district ID %d near city at (%d,%d), cancelling request\n", req->district_id, request_city->Body.X, request_city->Body.Y); + (*p_OutputDebugStringA) (ss); + clear_city_district_request (request_city, req->district_id); + return false; + } + } + } + + // Need to make sure distro hubs get roads. If this will be one, build the road first to be sure + if (req->district_id == DISTRIBUTION_HUB_DISTRICT_ID) { + bool has_road = (*tile->vtable->m25_Check_Roads)(tile, __, 0); + if (! has_road && ! cfg->auto_add_road) { + Unit_set_state(worker, __, UnitState_Build_Road); + worker->Body.Job_ID = WJ_Build_Road; + return true; + } + } - } else if ((control_id == CCID_DESCRIPTION_BTN) && // if description/more/prev button was clicked AND - (current_article != NULL) && current_article->show_description && // currently showing a description of an article AND - (is->cmpd.last_page > 0)) { // this is a multi-page description + enum SquareTypes base_type = tile->vtable->m50_Get_Square_BaseType (tile); + unsigned int overlay_flags = tile->vtable->m42_Get_Overlays (tile, __, 0); + unsigned int removable_flags = overlay_flags & (destructible_overlays & ~(TILE_FLAG_ROAD | TILE_FLAG_RAILROAD)); + bool district_buildable_here = district_is_buildable_on_square_type (&is->district_configs[req->district_id], tile); - // Show the next page of the multi-page description unless it's a two-page description and we're on the second page, in which case go - // back to the first. - if ((is->cmpd.last_page == 1) && (is->cmpd.shown_page == 1)) - is->cmpd.shown_page = 0; - else - is->cmpd.shown_page = not_above (is->cmpd.last_page, is->cmpd.shown_page + 1); - play_sound_effect (26); - p_civilopedia_form->Base.vtable->m73_call_m22_Draw ((Base_Form *)p_civilopedia_form); + // Remove any existing improvements + tile->vtable->m62_Set_Tile_BuildingID (tile, __, -1); + tile->vtable->m51_Unset_Tile_Flags (tile, __, 0, removable_flags, worker->Body.X, worker->Body.Y); + if (do_replacement) { + remove_district_instance (tile); + handle_district_removed (tile, req->district_id, worker->Body.X, worker->Body.Y, false); + } - } else - Civilopedia_Form_m53_On_Control_Click (this, __, control_id); -} + snprintf (ss, sizeof ss, "ai_move_district_worker: Checking for craters or pollution at worker ID %d location (%d,%d)\n", worker->Body.ID, worker->Body.X, worker->Body.Y); + (*p_OutputDebugStringA) (ss); -void __fastcall -patch_Civilopedia_Form_m22_Draw (Civilopedia_Form * this) -{ - // Make sure the new buttons are not visible and the multipage variables are cleared when we exit description mode - if ((this->Current_Article_ID < 0) || ! this->Articles[this->Current_Article_ID]->show_description) { - is->cmpd.shown_page = is->cmpd.last_page = 0; - if (is->cmpd.effects_btn != NULL) - is->cmpd.effects_btn->vtable->m02_Show_Disabled ((Base_Form *)is->cmpd.effects_btn); - if (is->cmpd.previous_btn != NULL) - is->cmpd.previous_btn->vtable->m02_Show_Disabled ((Base_Form *)is->cmpd.previous_btn); - } + if (tile->vtable->m21_Check_Crates (tile, __, 0) || tile->vtable->m20_Check_Pollution (tile, __, 0)) { + snprintf (ss, sizeof ss, "ai_move_district_worker: Worker ID %d clearing craters or pollution at (%d,%d)\n", worker->Body.ID, worker->Body.X, worker->Body.Y); + (*p_OutputDebugStringA) (ss); + Unit_set_state(worker, __, UnitState_Clear_Damage); + worker->Body.Job_ID = WJ_Clean_Pollution; + return true; + } - Civilopedia_Form_m22_Draw (this); -} + snprintf (ss, sizeof ss, "ai_move_district_worker: Checking for forest or wetlands at worker ID %d location (%d,%d)\n", worker->Body.ID, worker->Body.X, worker->Body.Y); + (*p_OutputDebugStringA) (ss); -int __fastcall -patch_Button_initialize_civilopedia_description (Button * this, int edx, char * text, int control_id, int x, int y, int width, int height, Base_Form * parent, int param_8) -{ - Civilopedia_Article * current_article = (p_civilopedia_form->Current_Article_ID >= 0) ? p_civilopedia_form->Articles[p_civilopedia_form->Current_Article_ID] : NULL; - if (current_article == NULL) - return Button_initialize (this, __, text, control_id, x, y, width, height, parent, param_8); + // Clear any forest/wetlands + if (! district_buildable_here && base_type == SQ_Forest) { + Unit_set_state(worker, __, UnitState_Clear_Forest); + worker->Body.Job_ID = WJ_Clean_Forest; + return true; + } else if (! district_buildable_here && ((base_type == SQ_Jungle) || (base_type == SQ_Swamp))) { + Unit_set_state(worker, __, UnitState_Clear_Wetlands); + worker->Body.Job_ID = WJ_Clear_Swamp; + return true; + } - // Set button visibility for multi-page descriptions if we're showing such a thing right now - bool show_desc_btn = true, show_effects_btn = false, show_previous_btn = false; - char * desc_btn_text = text; - if (current_article->show_description && (is->cmpd.last_page > 0)) { + snprintf (ss, sizeof ss, "ai_move_district_worker: Worker ID %d starting construction of district at (%d,%d)\n", worker->Body.ID, worker->Body.X, worker->Body.Y); + (*p_OutputDebugStringA) (ss); - // Tribe articles act like one long descripton. - if ((current_article->article_kind == CAK_TRIBE) || (current_article->article_kind == CAK_GAME_CONCEPT)) { + // Clear any existing improvements (irrigation and mines) + tile->vtable->m62_Set_Tile_BuildingID (tile, __, -1); + tile->vtable->m51_Unset_Tile_Flags (tile, __, 0, removable_flags, worker->Body.X, worker->Body.Y); - // Show the more button as long as we're not on the last page. Show the previous always since we're in description mode. - show_previous_btn = true; - desc_btn_text = (*p_labels)[LBL_MORE]; - if (is->cmpd.shown_page >= is->cmpd.last_page) - show_desc_btn = false; + // Start construction of district + inst = ensure_district_instance (tile, req->district_id, req->target_x, req->target_y); + inst->built_by_civ_id = worker->Body.CivID; + Unit_set_state(worker, __, UnitState_Build_Mines); + worker->Body.Job_ID = WJ_Build_Mines; // Build district + return true; - // Unit articles have separate description/effects modes. - } else if (current_article->article_kind == CAK_UNIT) { + // Else if the worker needs to be sent + } else { + if ((worker->Body.UnitState != UnitState_Go_To) || + (worker->Body.path_dest_x != req->target_x) || + (worker->Body.path_dest_y != req->target_y)) { + int path_result = patch_Trade_Net_set_unit_path (is->trade_net, __, + worker->Body.X, worker->Body.Y, req->target_x, req->target_y, + worker, worker->Body.CivID, 0x41, NULL); + if (path_result > 0) { - // For a two-page description, show the effects button and the description button which will act as a next/previous button - if (is->cmpd.last_page == 1) { - show_effects_btn = true; - desc_btn_text = is->cmpd.shown_page == 0 ? (*p_labels)[LBL_MORE] : (*p_labels)[LBL_PREVIOUS]; + snprintf (ss, sizeof ss, "ai_move_district_worker: Worker ID %d path set to (%d,%d) to build district\n", worker->Body.ID, req->target_x, req->target_y); + (*p_OutputDebugStringA) (ss); - // For a three or more page description, show the effects button, and show the description button only if we're not on the - // last page (b/c it's the next button), and show the previous button only if we're not on the first page. If the desc button - // is visible, make it say "More". + Unit_set_escortee (worker, __, -1); + Unit_set_state (worker, __, UnitState_Go_To); + worker->Body.path_dest_x = req->target_x; + worker->Body.path_dest_y = req->target_y; } else { - show_effects_btn = true; - if (is->cmpd.shown_page >= is->cmpd.last_page) - show_desc_btn = false; - else - desc_btn_text = (*p_labels)[LBL_MORE]; - show_previous_btn = is->cmpd.shown_page > 0; + clear_tracked_worker_assignment (rec); + return false; } } } + return false; +} - int tr = Button_initialize (this, __, desc_btn_text, control_id, x, y, width, height, parent, param_8); +bool +ai_worker_try_tile_improvement_district (Unit * worker) +{ + if (! is->current_config.enable_districts || worker == NULL) return false; + if (! is_worker (worker)) return false; + if (worker->Body.Auto_CityID < 0) return false; - if (! show_desc_btn) - this->vtable->m02_Show_Disabled ((Base_Form *)this); + City * city = get_city_ptr (worker->Body.Auto_CityID); + if (city == NULL) return false; + if (city->Body.CivID != worker->Body.CivID) return false; - if (is->cmpd.effects_btn != NULL) { - if (show_effects_btn) - is->cmpd.effects_btn->vtable->m01_Show_Enabled ((Base_Form *)is->cmpd.effects_btn, __, 0); - else - is->cmpd.effects_btn->vtable->m02_Show_Disabled ((Base_Form *)is->cmpd.effects_btn); - } + int civ_id = worker->Body.CivID; + int tile_x = worker->Body.X; + int tile_y = worker->Body.Y; + Tile * tile = tile_at (tile_x, tile_y); + if ((tile == NULL) || (tile == p_null_tile)) return false; + if (tile->CityID >= 0) return false; + if (tile->vtable->m38_Get_Territory_OwnerID (tile) != civ_id) return false; + if (! city_radius_contains_tile (city, tile_x, tile_y)) return false; + if (get_district_instance (tile) != NULL) return false; - if (is->cmpd.previous_btn != NULL) { - if (show_previous_btn) - is->cmpd.previous_btn->vtable->m01_Show_Enabled ((Base_Form *)is->cmpd.previous_btn, __, 0); - else - is->cmpd.previous_btn->vtable->m02_Show_Disabled ((Base_Form *)is->cmpd.previous_btn); + // Evaluate whether the best improvement here is irrigation, mines, or a district, using heuristics based on city needs + int food_weight = (city->Body.FoodIncome < city->Body.FoodRequired) ? 3 : 1; + int shield_weight = (city->Body.ProductionIncome < city->Body.Population.Size) ? 2 : 1; + int unhappy_percent = + city->Body.UnhappyNoReasonPercent + + city->Body.UnhappyCrowdedPercent + + city->Body.UnhappyWarWearinessPercent + + city->Body.UnhappyAgresssionPercent + + city->Body.UnhappyPropagandaPercent + + city->Body.UnhappyDraftPercent + + city->Body.UnhappyOppressionPercent + + city->Body.UnhappyThisCityImprovementsPercent + + city->Body.UnhappyOtherCityImprovementsPercent; + int happiness_weight = (unhappy_percent > 0) ? 2 : 1; + int gold_weight = 1; + int resource_boost = 5 * (food_weight + shield_weight + gold_weight + happiness_weight); + + int irrigation_score = INT_MIN; + if (Leader_can_do_worker_job (&leaders[civ_id], __, WJ_Irrigate, tile_x, tile_y, 0) && + ! tile->vtable->m17_Check_Irrigation (tile, __, 0)) { + int base_type = tile->vtable->m50_Get_Square_BaseType (tile); + int bonus = p_bic_data->TileTypes[base_type].IrrigationBonus; + if (bonus < 0) + bonus = 0; + irrigation_score = bonus * food_weight; + } + + int mine_score = INT_MIN; + if (Leader_can_do_worker_job (&leaders[civ_id], __, WJ_Build_Mines, tile_x, tile_y, 0) && + ! tile->vtable->m18_Check_Mines (tile, __, 0)) { + int base_type = tile->vtable->m50_Get_Square_BaseType (tile); + int bonus = p_bic_data->TileTypes[base_type].MiningBonus; + if (bonus < 0) + bonus = 0; + mine_score = bonus * shield_weight; } - return tr; -} - -int __fastcall -patch_Civilopedia_Form_m68_Show_Dialog (Civilopedia_Form * this, int edx, int param_1, void * param_2, void * param_3) -{ - memset (&is->cmpd, 0, sizeof is->cmpd); - - Button * bs[] = {malloc (sizeof Button), malloc (sizeof Button)}; - for (int n = 0; n < ARRAY_LEN (bs); n++) { - if (bs[n] == NULL) + int best_score = INT_MIN; + int best_district_id = -1; + for (int i = 0; i < is->district_count; i++) { + struct district_config const * cfg = &is->district_configs[i]; + if (cfg->ai_build_strategy != DABS_TILE_IMPROVEMENT) + continue; + if (! can_build_district_on_tile (tile, i, civ_id)) continue; - Button_construct (bs[n]); - int desc_btn_x = 535, desc_btn_y = 222, desc_btn_height = 17; + struct district_instance temp = {0}; + temp.district_id = i; + temp.tile_x = (short)tile_x; + temp.tile_y = (short)tile_y; - Button_initialize (bs[n], __, - n == 0 ? (*p_labels)[LBL_EFFECTS] : (*p_labels)[LBL_PREVIOUS], - n == 0 ? PEDIA_MULTIPAGE_EFFECTS_BUTTON_ID : PEDIA_MULTIPAGE_PREV_BUTTON_ID, // control ID - desc_btn_x, // location x - desc_btn_y + (n == 0 ? -2 : 2) * desc_btn_height, // location y - MOD_INFO_BUTTON_WIDTH, MOD_INFO_BUTTON_HEIGHT, // width, height - (Base_Form *)this, // parent - 0); // ? + int food = 0, shields = 0, gold = 0, science = 0, culture = 0, happiness = 0; + get_effective_district_yields (&temp, cfg, &food, &shields, &gold, &science, &culture, &happiness); - for (int k = 0; k < 3; k++) - bs[n]->Images[k] = &this->Description_Btn_Images[k]; + int score = food * food_weight + shields * shield_weight + gold * gold_weight; + score += (science + culture) / 2; + score += happiness * happiness_weight; + if ((cfg->generated_resource_id >= 0) && + ! patch_City_has_resource (city, __, cfg->generated_resource_id)) + score += resource_boost; - // Do now draw the button until needed - bs[n]->vtable->m02_Show_Disabled ((Base_Form *)bs[n]); + if (score > best_score) { + best_score = score; + best_district_id = i; + } } - is->cmpd.effects_btn = bs[0]; - is->cmpd.previous_btn = bs[1]; - - int tr = Civilopedia_Form_m68_Show_Dialog (this, __, param_1, param_2, param_3); - for (int n = 0; n < ARRAY_LEN (bs); n++) - if (bs[n] != NULL) { - bs[n]->vtable->destruct ((Base_Form *)bs[n], __, 0); - free (bs[n]); - } - is->cmpd.effects_btn = is->cmpd.previous_btn = NULL; + if ((best_district_id >= 0) && + (best_score >= irrigation_score) && + (best_score >= mine_score)) { + unsigned int overlay_flags = tile->vtable->m42_Get_Overlays (tile, __, 0); + unsigned int removable_flags = overlay_flags & (destructible_overlays & ~(TILE_FLAG_ROAD | TILE_FLAG_RAILROAD)); + tile->vtable->m62_Set_Tile_BuildingID (tile, __, -1); + if (removable_flags != 0) + tile->vtable->m51_Unset_Tile_Flags (tile, __, 0, removable_flags, tile_x, tile_y); + ensure_district_instance (tile, best_district_id, tile_x, tile_y); + Unit_set_state (worker, __, UnitState_Build_Mines); + worker->Body.Job_ID = WJ_Build_Mines; + return true; + } - return tr; + return false; } -void -init_district_images () +void __fastcall +patch_Unit_ai_move_terraformer (Unit * this) { - if (is_online_game () || is->dc_img_state != IS_UNINITED) - return; + Map * map = &p_bic_data->Map; + int type_id = this->Body.UnitTypeID; + int civ_id = this->Body.CivID; + Tile * tile = tile_at (this->Body.X, this->Body.Y); + bool is_human = (*p_human_player_bits & (1 << this->Body.CivID)) != 0; - char art_dir[200]; + + if (is->current_config.enable_districts && ! is_human) { + update_tracked_worker_for_unit (this); + struct district_instance * inst = get_district_instance (tile); - is->dc_img_state = IS_INIT_FAILED; + if (inst != NULL && inst->district_id != NATURAL_WONDER_DISTRICT_ID && + tile->vtable->m50_Get_Square_BaseType (tile) != SQ_Coast) { + // Roads should be made after district builds. The district is complete but + // worker is still likely on the tile, so check here and build road if needed + struct district_config * cfg = &is->district_configs[inst->district_id]; + bool has_road = (*tile->vtable->m25_Check_Roads)(tile, __, 0); + if (! has_road && ! cfg->auto_add_road) { + Unit_set_state(this, __, UnitState_Build_Road); + this->Body.Job_ID = WJ_Build_Road; + return; + } - PCX_Image pcx; - PCX_Image_construct (&pcx); + // Same check for railroads + bool can_build_railroad = Leader_can_do_worker_job (&leaders[this->Body.CivID], __, WJ_Build_Railroad, this->Body.X, this->Body.Y, 0); + bool has_railroad = (*tile->vtable->m23_Check_Railroads)(tile, __, 0); + if (can_build_railroad && !has_railroad && ! cfg->auto_add_railroad) { + Unit_set_state(this, __, UnitState_Build_Railroad); + this->Body.Job_ID = WJ_Build_Railroad; + return; + } + } - // For each district type - for (int dc = 0; dc < is->district_count; dc++) { - struct district_config const * cfg = &is->district_configs[dc]; - int variant_count = cfg->img_path_count; - if (variant_count <= 0) - continue; + struct district_worker_record * rec = get_tracked_worker_record (this); + if (rec != NULL && rec->pending_req != NULL) { + if (ai_move_district_worker (this, rec)) + return; + } - int era_count = cfg->vary_img_by_era ? 4 : 1; - int column_count = cfg->max_building_index + 1; + if ((this->Body.Auto_CityID >= 0) && ai_worker_try_tile_improvement_district (this)) + return; + } - // For each cultural variant - for (int variant_i = 0; variant_i < variant_count; variant_i++) { - if (cfg->img_paths[variant_i] == NULL) - continue; - // Read PCX file - snprintf (art_dir, sizeof art_dir, "%s\\Art\\Districts\\1200\\%s", is->mod_rel_dir, cfg->img_paths[variant_i]); + bool pop_else_caravan; + if ((tile != NULL) && (tile != p_null_tile) && + (type_id >= 0) && (type_id < p_bic_data->UnitTypeCount) && + is_material_unit (&p_bic_data->UnitTypes[type_id], &pop_else_caravan) && + ((pop_else_caravan && is->current_config.enable_pop_unit_ai) || + ((! pop_else_caravan) && is->current_config.enable_caravan_unit_ai))) { + ai_move_material_unit (this); + return; + } + + Unit_ai_move_terraformer (this); +} + +bool __fastcall +patch_Unit_ai_can_sacrifice (Unit * this, int edx, bool requires_city) +{ + int sacrifice_action = UCV_Sacrifice & 0x0FFFFFFF; // Mask out top four category bits + UnitType * type = &p_bic_data->UnitTypes[this->Body.UnitTypeID]; + if (is->current_config.patch_ai_can_sacrifice_without_special_ability && ((type->Special_Actions & sacrifice_action) == 0)) + return false; + else + return Unit_ai_can_sacrifice (this, __, requires_city); +} - PCX_Image_read_file (&pcx, __, art_dir, NULL, 0, 0x100, 2); +int __cdecl +patch_get_building_defense_bonus_at (int x, int y, int param_3) +{ + // Get base building defense bonus first + int base = get_building_defense_bonus_at (x, y, param_3); - if (pcx.JGL.Image == NULL) { + // If districts are disabled, return base + if (!is->current_config.enable_districts) + return base; - char ss[200]; - snprintf (ss, sizeof ss, "init_district_images: failed to load district images from %s", art_dir); - pop_up_in_game_error (ss); + Tile * tile = tile_at (x, y); + if ((tile == NULL) || (tile == p_null_tile)) + return base; - (*p_OutputDebugStringA) ("[C3X] Failed to load districts sprite sheet.\n"); - for (int dc2 = 0; dc2 < COUNT_DISTRICT_TYPES; dc2++) - for (int variant_i2 = 0; variant_i2 < ARRAY_LEN (is->district_img_sets[dc2].imgs); variant_i2++) - for (int era_i = 0; era_i < 4; era_i++) { - for (int col = 0; col < ARRAY_LEN (is->district_img_sets[dc2].imgs[variant_i2][era_i]); col++) { - Sprite * sprite = &is->district_img_sets[dc2].imgs[variant_i2][era_i][col]; - if (sprite->vtable != NULL) - sprite->vtable->destruct (sprite, __, 0); - } - } - pcx.vtable->destruct (&pcx, __, 0); - return; - } + struct district_instance * inst = get_district_instance (tile); + if (inst != NULL) { + struct district_config const * cfg = &is->district_configs[inst->district_id]; + int bonus = cfg->defense_bonus_percent; + bonus += apply_district_bonus_entries (inst, &cfg->defense_bonus_extras, inst->district_id); + return bonus; + } - // For each era - for (int era_i = 0; era_i < era_count; era_i++) { + return base; +} - // For each column in the image (variations on the district image for that era) - for (int col_i = 0; col_i < column_count; col_i++) { - Sprite_construct (&is->district_img_sets[dc].imgs[variant_i][era_i][col_i]); +void __fastcall +patch_Unit_select (Unit * this) +{ + if (is->current_config.enable_districts) { + Tile * tile = tile_at (this->Body.X, this->Body.Y); + + struct district_instance * inst = get_district_instance (tile); + if (inst != NULL && is_worker(this) && inst->district_id >= 0 && inst->district_id <= is->district_count && + ! district_is_complete (tile, inst->district_id)) { + int district_id = inst->district_id; + PopupForm * popup = get_popup_form (); + int remaining_turns = get_worker_remaining_turns_to_complete (this, __, 0); + set_popup_str_param (0, (char*)is->district_configs[district_id].display_name, -1, -1); + set_popup_int_param (1, remaining_turns); + popup->vtable->set_text_key_and_flags (popup, __, is->mod_script_path, "C3X_CONFIRM_CANCEL_BUILD_DISTRICT", -1, 0, 0, 0); + + int sel = patch_show_popup (popup, __, 0, 0); + if (sel == 0) { + Unit_set_escortee (this, __, -1); + Unit_set_state (this, __, 0); - int x = 128 * col_i, - y = 64 * era_i; - Sprite_slice_pcx (&is->district_img_sets[dc].imgs[variant_i][era_i][col_i], __, &pcx, x, y, 128, 64, 1, 1); + bool other_workers_present = false; + FOR_UNITS_ON (uti, tile) { + Unit * unit = uti.unit; + if ((unit != NULL) && (unit != this) && + (unit->Body.UnitState == UnitState_Build_Mines) && + (p_bic_data->UnitTypes[unit->Body.UnitTypeID].Worker_Actions != 0)) { + other_workers_present = true; + break; + } } + if (! other_workers_present) { + remove_district_instance (tile); + } + } else { + return; } - - pcx.vtable->clear_JGL (&pcx); } } - // Load wonder district images (dynamically per wonder) - if (is->current_config.enable_wonder_districts) { - char const * last_img_path = NULL; - PCX_Image wpcx; - PCX_Image_construct (&wpcx); - bool pcx_loaded = false; - - for (int wi = 0; wi < is->wonder_district_count; wi++) { - char const * img_path = is->wonder_district_configs[wi].img_path; - if (img_path == NULL) - img_path = "Wonders.pcx"; - // Load new image file if different from previous - if ((last_img_path == NULL) || (strcmp (img_path, last_img_path) != 0)) { - if (pcx_loaded) - wpcx.vtable->clear_JGL (&wpcx); + if (is->current_config.show_ai_city_location_desirability_if_settler) { + int unit_type_id = this->Body.UnitTypeID; + int worker_actions = p_bic_data->UnitTypes[unit_type_id].Worker_Actions; + int new_perspective = (worker_actions >= 1 && (worker_actions & (UCV_Build_City)) && !is_worker(this)) ? p_main_screen_form->Player_CivID : -1; - snprintf(art_dir, sizeof art_dir, "%s\\Art\\Districts\\1200\\%s", is->mod_rel_dir, img_path); - PCX_Image_read_file (&wpcx, __, art_dir, NULL, 0, 0x100, 2); + if (new_perspective != is->city_loc_display_perspective) { + is->city_loc_display_perspective = new_perspective; + p_main_screen_form->vtable->m73_call_m22_Draw ((Base_Form *)p_main_screen_form); // Trigger map redraw + } + } - if (wpcx.JGL.Image == NULL) { - char ss[200]; - snprintf (ss, sizeof ss, "init_district_images: failed to load wonder district images from %s", art_dir); - pop_up_in_game_error (ss); - pcx_loaded = false; - continue; - } + // Sometimes clearing of highlighted tiles doesn't trigger when CTRL lifted, so double-check here + if (is->current_config.enable_city_work_radii_highlights && is->highlight_city_radii) { + is->highlight_city_radii = false; + clear_highlighted_worker_tiles_and_redraw (); + } - pcx_loaded = true; - last_img_path = img_path; - } + Unit_select (this); +} - if (! pcx_loaded) - continue; +void __fastcall +patch_City_Form_draw_food_income_icons (City_Form * this) +{ + // Call original function first + City_Form_draw_food_income_icons (this); - Sprite_construct (&is->wonder_district_img_sets[wi].img); - int x = 128 * is->wonder_district_configs[wi].img_column; - int y = 64 * is->wonder_district_configs[wi].img_row; - Sprite_slice_pcx (&is->wonder_district_img_sets[wi].img, __, &wpcx, x, y, 128, 64, 1, 1); + if (! is->current_config.enable_districts && ! is->current_config.enable_natural_wonders) + return; - Sprite_construct (&is->wonder_district_img_sets[wi].construct_img); - int cx = 128 * is->wonder_district_configs[wi].img_construct_column; - int cy = 64 * is->wonder_district_configs[wi].img_construct_row; - Sprite_slice_pcx (&is->wonder_district_img_sets[wi].construct_img, __, &wpcx, cx, cy, 128, 64, 1, 1); - } + // Get current city + City * city = this->CurrentCity; + if (city == NULL) + return; - if (pcx_loaded) - wpcx.vtable->clear_JGL (&wpcx); - wpcx.vtable->destruct (&wpcx, __, 0); + // Calculate standard district food bonus + int standard_district_food = 0; + FOR_DISTRICTS_AROUND (wai, city->Body.X, city->Body.Y, true) { + int district_id = wai.district_inst->district_id; + int food_bonus = 0; + get_effective_district_yields (wai.district_inst, &is->district_configs[district_id], &food_bonus, NULL, NULL, NULL, NULL, NULL); + standard_district_food += food_bonus; } - if (is->current_config.enable_natural_wonders && (is->natural_wonder_count > 0)) { - char const * last_img_path = NULL; - PCX_Image nwpcx; - PCX_Image_construct (&nwpcx); - bool pcx_loaded = false; - - for (int ni = 0; ni < is->natural_wonder_count; ni++) { - struct natural_wonder_district_config * cfg = &is->natural_wonder_configs[ni]; - if (cfg->name == NULL) - continue; - - char const * img_path = cfg->img_path; - if ((last_img_path == NULL) || (strcmp (img_path, last_img_path) != 0)) { - if (pcx_loaded) - nwpcx.vtable->clear_JGL (&nwpcx); - - snprintf (art_dir, sizeof art_dir, "%s\\Art\\Districts\\1200\\%s", is->mod_rel_dir, img_path); - PCX_Image_read_file (&nwpcx, __, art_dir, NULL, 0, 0x100, 2); + // Get distribution hub food bonus + int distribution_hub_food = 0; + if (is->current_config.enable_districts && is->current_config.enable_distribution_hub_districts) + get_distribution_hub_yields_for_city (city, &distribution_hub_food, NULL); - if (nwpcx.JGL.Image == NULL) { - char ss[200]; - snprintf (ss, sizeof ss, "init_district_images: failed to load natural wonder images from %s", art_dir); - pop_up_in_game_error (ss); - pcx_loaded = false; - continue; - } + // Total district food + int total_district_food = standard_district_food + distribution_hub_food; + if (total_district_food <= 0) + return; - pcx_loaded = true; - last_img_path = img_path; - } + // Lazy load icons + if (is->current_config.enable_districts && is->current_config.enable_distribution_hub_districts) { + if (is->distribution_hub_icons_img_state == IS_UNINITED) + init_distribution_hub_icons (); + if (is->distribution_hub_icons_img_state != IS_OK) + return; + } + if (is->dc_icons_img_state == IS_UNINITED) + init_district_icons (); + if (is->dc_icons_img_state != IS_OK) + return; - if (! pcx_loaded) - continue; + int food_income = city->Body.FoodIncome; + int food_required = city->Body.FoodRequired; - Sprite_construct (&is->natural_wonder_img_sets[ni].img); - int x = 128 * cfg->img_column; - int y = 88 * cfg->img_row; - Sprite_slice_pcx (&is->natural_wonder_img_sets[ni].img, __, &nwpcx, x, y, 128, 88, 1, 1); + // Calculate how standard district food icons are distributed + int standard_food_eaten = 0; + int standard_food_surplus = 0; + if (standard_district_food > 0) { + if (standard_district_food >= food_income) { + standard_food_surplus = food_income; + standard_food_eaten = standard_district_food - food_income; + } else { + standard_food_surplus = standard_district_food; + standard_food_eaten = 0; } - - if (pcx_loaded) - nwpcx.vtable->clear_JGL (&nwpcx); - nwpcx.vtable->destruct (&nwpcx, __, 0); } - is->dc_img_state = IS_OK; - pcx.vtable->destruct (&pcx, __, 0); -} + // Calculate how distribution hub food icons are distributed + int hub_food_eaten = 0; + int hub_food_surplus = 0; + int remaining_income = food_income - standard_food_surplus; + if (distribution_hub_food > 0) { + if (distribution_hub_food >= remaining_income) { + hub_food_surplus = remaining_income; + hub_food_eaten = distribution_hub_food - remaining_income; + } else { + hub_food_surplus = distribution_hub_food; + hub_food_eaten = 0; + } + } -bool -tile_coords_has_city_with_building_in_district_radius (int tile_x, int tile_y, int district_id, int i_improv) -{ - Tile * center = tile_at (tile_x, tile_y); + // Draw eaten district food icons (left side, from right to left) + int total_eaten = standard_food_eaten + hub_food_eaten; + if (total_eaten > 0) { + Sprite * base_eaten_sprite = &(this->City_Icons_Images).Icon_07; + int eaten_sprite_width = base_eaten_sprite->Width; + int eaten_sprite_height = base_eaten_sprite->Height; + struct tagRECT * eaten_rect = &this->Food_Consumption_Rect; + int eaten_rect_width = eaten_rect->right - eaten_rect->left; - if ((center == NULL) || (center == p_null_tile)) - return false; + int eaten_spacing = eaten_sprite_width; + if ((food_required > 1) && (food_required * eaten_sprite_width - eaten_rect_width != 0) && + (eaten_rect_width <= food_required * eaten_sprite_width)) { + eaten_spacing = (eaten_rect_width - eaten_sprite_width) / (food_required - 1); + if (eaten_spacing < 1) + eaten_spacing = 1; + else if (eaten_spacing > eaten_sprite_width) + eaten_spacing = eaten_sprite_width; + } - int owner_id = center->Territory_OwnerID; - if (owner_id <= 0) - return false; + int eaten_x_offset = eaten_spacing * (food_required - 1); + // Draw standard district eaten first + for (int i = 0; i < standard_food_eaten && i < food_required; i++) { + int x = eaten_rect->left + eaten_x_offset; + int y = eaten_rect->top + ((eaten_rect->bottom - eaten_rect->top >> 1) - + (eaten_sprite_height >> 1)); + Sprite_draw (&is->district_food_eaten_icon, __, &(this->Base).Data.Canvas, x, y, NULL); + eaten_x_offset -= eaten_spacing; + } + // Draw distribution hub eaten + for (int i = 0; i < hub_food_eaten && eaten_x_offset >= 0; i++) { + int x = eaten_rect->left + eaten_x_offset; + int y = eaten_rect->top + ((eaten_rect->bottom - eaten_rect->top >> 1) - + (eaten_sprite_height >> 1)); + Sprite_draw (&is->distribution_hub_eaten_food_icon, __, &(this->Base).Data.Canvas, x, y, NULL); + eaten_x_offset -= eaten_spacing; + } + } - // Loop over tiles in work radius around the center tile - for (int n = 0; n < is->workable_tile_count; n++) { - int dx, dy; - patch_ni_to_diff_for_work_area (n, &dx, &dy); - int x = tile_x + dx, y = tile_y + dy; - wrap_tile_coords (&p_bic_data->Map, &x, &y); - Tile * t = tile_at (x, y); - if ((t == NULL) || (t == p_null_tile)) - continue; + // Draw surplus district food icons (right side, from right to left) + int total_surplus = standard_food_surplus + hub_food_surplus; + if (total_surplus > 0) { + Sprite * base_surplus_sprite = &(this->City_Icons_Images).Icon_06; + int surplus_sprite_width = base_surplus_sprite->Width; + int surplus_sprite_height = base_surplus_sprite->Height; + struct tagRECT * surplus_rect = &this->Food_Storage_Rect; + int surplus_rect_width = surplus_rect->right - surplus_rect->left; - // Only consider cities belonging to the same civ as the territory owner - City * city = get_city_ptr (t->vtable->m45_Get_City_ID (t)); - if ((city != NULL) && (city->Body.CivID == owner_id)) { - if (has_active_building (city, i_improv)) - return true; + int surplus_spacing = surplus_sprite_width; + if ((food_income > 1) && (food_income * surplus_sprite_width - surplus_rect_width != 0) && + (surplus_rect_width <= food_income * surplus_sprite_width)) { + surplus_spacing = (surplus_rect_width - surplus_sprite_width) / (food_income - 1); + if (surplus_spacing < 1) + surplus_spacing = 1; + else if (surplus_spacing > surplus_sprite_width) + surplus_spacing = surplus_sprite_width; } - } - return false; + int surplus_x_offset = 0; + // Draw standard district surplus first + for (int i = 0; i < standard_food_surplus && i < food_income; i++) { + int x = surplus_rect->right - surplus_x_offset - surplus_sprite_width; + int y = surplus_rect->top + ((surplus_rect->bottom - surplus_rect->top >> 1) - + (surplus_sprite_height >> 1)) - 1; + Sprite_draw (&is->district_food_icon, __, &(this->Base).Data.Canvas, x, y, NULL); + surplus_x_offset += surplus_spacing; + } + // Draw distribution hub surplus + for (int i = 0; i < hub_food_surplus && surplus_x_offset < surplus_rect_width; i++) { + int x = surplus_rect->right - surplus_x_offset - surplus_sprite_width; + int y = surplus_rect->top + ((surplus_rect->bottom - surplus_rect->top >> 1) - + (surplus_sprite_height >> 1)) - 1; + Sprite_draw (&is->distribution_hub_food_icon, __, &(this->Base).Data.Canvas, x, y, NULL); + surplus_x_offset += surplus_spacing; + } + } } -void __fastcall -patch_Map_Renderer_m12_Draw_Tile_Buildings(Map_Renderer * this, int edx, int param_1, int tile_x, int tile_y, Map_Renderer * map_renderer, int pixel_x, int pixel_y) +void +recompute_district_and_distribution_hub_shields_for_city_view (City * city) { - if (! is->current_config.enable_districts && ! is->current_config.enable_natural_wonders) { - Map_Renderer_m12_Draw_Tile_Buildings(this, __, param_1, tile_x, tile_y, map_renderer, pixel_x, pixel_y); + if ((! is->current_config.enable_districts && ! is->current_config.enable_natural_wonders) || (city == NULL)) return; - } - Tile * tile = tile_at (tile_x, tile_y); - if ((tile == NULL) || (tile == p_null_tile)) - return; + int city_id = city->Body.ID; + int city_x = city->Body.X; + int city_y = city->Body.Y; - struct district_instance * inst = get_district_instance (tile); - if (inst == NULL) { - Map_Renderer_m12_Draw_Tile_Buildings(this, __, param_1, tile_x, tile_y, map_renderer, pixel_x, pixel_y); - return; - } + // District yields are injected through the city center tile in patch_City_calc_tile_yield_while_gathering. + // Grab the base yield (no districts) directly from the original function, then compute the + // district bonus that calculate_city_center_district_bonus will layer on afterward. + int city_center_base_shields = City_calc_tile_yield_at (city, __, YK_SHIELDS, city_x, city_y); + int total_district_shield_bonus = 0; + calculate_city_center_district_bonus (city, NULL, &total_district_shield_bonus, NULL); - int district_id = inst->district_type; - if (is->dc_img_state == IS_UNINITED) - init_district_images (); + // Distribution hub contribution is tracked separately for icon rendering. + int distribution_hub_shields = 0; + if (is->current_config.enable_distribution_hub_districts) + get_distribution_hub_yields_for_city (city, NULL, &distribution_hub_shields); + if (distribution_hub_shields < 0) + distribution_hub_shields = 0; + if (distribution_hub_shields > total_district_shield_bonus) + distribution_hub_shields = total_district_shield_bonus; - if (is->dc_img_state != IS_OK) - return; + int standard_district_shields = total_district_shield_bonus - distribution_hub_shields; + if (standard_district_shields < 0) + standard_district_shields = 0; - // Natural Wonder - if (district_id == NATURAL_WONDER_DISTRICT_ID) { - if (! is->current_config.enable_natural_wonders) - return; + // Recompute yields with districts active so ProductionIncome/Loss reflect the city view. + recompute_city_yields_with_districts (city); + int total_production_income = city->Body.ProductionIncome; + int total_production_loss = city->Body.ProductionLoss; + int total_net_shields = total_production_income + total_production_loss; // ProductionLoss stored as negative - int natural_id = inst->natural_wonder_info.natural_wonder_id; - if ((natural_id >= 0) && (natural_id < is->natural_wonder_count)) { - Sprite * nsprite = &is->natural_wonder_img_sets[natural_id].img; - int y_offset = 88 - 64; // Height of wonder img minus height of tile - int draw_y = pixel_y - y_offset; - patch_Sprite_draw_on_map (nsprite, __, this, pixel_x, draw_y, 1, 1, (p_bic_data->is_zoomed_out != false) + 1, 0); - } - return; - } + // Remove the district bonus from the gross tile production and recompute corruption on that base value. + int city_center_with_districts = city_center_base_shields + total_district_shield_bonus; + int gross_without_specials = city->Body.Tiles_Production - (city_center_with_districts - city_center_base_shields); + if (gross_without_specials < 0) + gross_without_specials = 0; - // Districts - if (is->current_config.enable_districts) { - if (district_id < 0 || district_id >= is->district_count) - return; - bool completed = district_is_complete (tile, district_id); + int base_corruption_abs = patch_City_compute_corrupted_yield (city, __, gross_without_specials, true); + if (base_corruption_abs < 0) + base_corruption_abs = 0; + int base_production_loss = -base_corruption_abs; - if (! completed) - return; + // Corruption becomes more negative as it increases. + int additional_corruption = total_production_loss - base_production_loss; - struct district_config const * cfg = &is->district_configs[district_id]; - int territory_owner_id = tile->Territory_OwnerID; - int variant = 0; - int era = 0; - int culture = 0; - int buildings = 0; - Sprite * district_sprite; + int district_shields_remaining = standard_district_shields; + int hub_shields_remaining = distribution_hub_shields; - if (territory_owner_id > 0) { - Leader * leader = &leaders[territory_owner_id]; - culture = p_bic_data->Races[leader->RaceID].CultureGroupID; - if (cfg->vary_img_by_culture) - variant = culture; - if (cfg->vary_img_by_era) - era = leader->Era; - } else if (district_id != WONDER_DISTRICT_ID && district_id != NATURAL_WONDER_DISTRICT_ID) { - // Render abandoned district, special index 5 - variant = 5; - district_sprite = &is->district_img_sets[0].imgs[variant][era][buildings]; - patch_Sprite_draw_on_map (district_sprite, __, this, pixel_x, pixel_y, 1, 1, (p_bic_data->is_zoomed_out != false) + 1, 0); - return; + if (additional_corruption < 0) { + int extra_loss = -additional_corruption; + + if (district_shields_remaining > 0) { + int from_districts = (extra_loss < district_shields_remaining) ? extra_loss : district_shields_remaining; + district_shields_remaining -= from_districts; + extra_loss -= from_districts; } - switch (district_id) { - case NEIGHBORHOOD_DISTRICT_ID: - { - unsigned v = (unsigned)tile_x * 0x9E3779B1u + (unsigned)tile_y * 0x85EBCA6Bu; - v ^= v >> 16; - v *= 0x7FEB352Du; - v ^= v >> 15; - v *= 0x846CA68Bu; - v ^= v >> 16; - buildings = clamp(0, 3, (int)(v & 3u)); /* final 0..3 */ - variant = culture; - break; - } - case DISTRIBUTION_HUB_DISTRICT_ID: - break; - case WONDER_DISTRICT_ID: - { - if (! is->current_config.enable_wonder_districts) - return; + if ((extra_loss > 0) && (hub_shields_remaining > 0)) { + int from_hub = (extra_loss < hub_shields_remaining) ? extra_loss : hub_shields_remaining; + hub_shields_remaining -= from_hub; + extra_loss -= from_hub; + } + } - struct wonder_district_info * info = get_wonder_district_info (tile); - if (info == NULL) - return; + int non_district_shields_remaining = total_net_shields - district_shields_remaining - hub_shields_remaining; + if (non_district_shields_remaining < 0) + non_district_shields_remaining = 0; - int construct_windex = -1; - if (info->state == WDS_COMPLETED) { - Sprite * wsprite = &is->wonder_district_img_sets[info->wonder_index].img; - patch_Sprite_draw_on_map (wsprite, __, this, pixel_x, pixel_y, 1, 1, (p_bic_data->is_zoomed_out != false) + 1, 0); - return; - } else if (wonder_district_tile_under_construction (tile, tile_x, tile_y, &construct_windex) && (construct_windex >= 0)) { - Sprite * csprite = &is->wonder_district_img_sets[construct_windex].construct_img; - patch_Sprite_draw_on_map (csprite, __, this, pixel_x, pixel_y, 1, 1, (p_bic_data->is_zoomed_out != false) + 1, 0); - return; - } - break; - } - default: - { - struct district_infos * info = &is->district_infos[district_id]; - int completed_count = 0; - for (int i = 0; i < info->dependent_building_count; i++) { - int building_id = info->dependent_building_ids[i]; - if ((building_id >= 0) && - tile_coords_has_city_with_building_in_district_radius (tile_x, tile_y, district_id, building_id)) - completed_count++; - } - buildings = completed_count; - break; - } - } + int total_corruption = -total_production_loss; + if (total_corruption < 0) + total_corruption = 0; + int district_corruption = standard_district_shields - district_shields_remaining; + int hub_corruption = distribution_hub_shields - hub_shields_remaining; + int base_corruption = total_corruption - district_corruption - hub_corruption; + if (base_corruption < 0) + base_corruption = 0; - district_sprite = &is->district_img_sets[district_id].imgs[variant][era][buildings]; - patch_Sprite_draw_on_map (district_sprite, __, this, pixel_x, pixel_y, 1, 1, (p_bic_data->is_zoomed_out != false) + 1, 0); - return; - } + is->non_district_shield_icons_remaining = non_district_shields_remaining; + is->district_shield_icons_remaining = district_shields_remaining; + is->distribution_hub_shield_icons_remaining = hub_shields_remaining; - Map_Renderer_m12_Draw_Tile_Buildings(this, __, param_1, tile_x, tile_y, map_renderer, pixel_x, pixel_y); + is->corruption_shield_icons_remaining = base_corruption; + is->district_corruption_icons_remaining = district_corruption; + is->distribution_hub_corruption_icons_remaining = hub_corruption; } -bool __fastcall -patch_Tile_has_city_or_district (Tile * this) +void __fastcall +patch_City_draw_production_income_icons (City * this, int edx, int canvas, int * rect_ptr) { - bool has_city = Tile_has_city (this); - if (is->current_config.enable_districts || is->current_config.enable_natural_wonders) { - return has_city || (get_district_instance (this) != NULL); + if (! is->current_config.enable_districts && ! is->current_config.enable_natural_wonders) { + City_draw_production_income_icons (this, __, canvas, rect_ptr); + return; } - return has_city; -} -int __fastcall -patch_Tile_check_water_for_navigator_cell_coloring (Tile * this) -{ - if (! is->current_config.show_territory_colors_on_water_tiles_in_minimap) - return this->vtable->m35_Check_Is_Water (this); - else - return 0; -} + recompute_district_and_distribution_hub_shields_for_city_view (this); + City_draw_production_income_icons (this, __, canvas, rect_ptr); -bool -is_skippable_popup (char * text_key) -{ - char * skippable_keys[] = {"SUMMARY_END_GOLDEN_AGE", "SUMMARY_END_SCIENCE_AGE", "SUMMARY_NEW_SMALL_WONDER", // unimportant domestic things - "WONDERPRODUCE", // another civ completed a wonder - "MAKEPEACE", "MUTUALPROTECTIONPACT", "MILITARYALLIANCE", "SUMMARY_DECLARE_WAR", // diplo events not involving the player - "TRADEEMBARGOENDS", // embargo vs player ends - "SUMMARY_CIV_DESTROYED_BY_CIV", "SUMMARY_CIV_DESTROYED", // foreign civs destroyed not by player - "LOSTGOOD", // 'We lost our supply of ...!' - "TRADEEMBARGO", "MILITARYALLIANCEWARONUS", "MILITARYALLIANCEAGAINSTUS", // trade embargo or alliance vs player - "SUMMARY_TRAVELERS_REPORT"}; // another civs starts a wonder + is->corruption_shield_icons_remaining = 0; + is->district_corruption_icons_remaining = 0; + is->distribution_hub_corruption_icons_remaining = 0; - for (int n = 0; n < ARRAY_LEN (skippable_keys); n++) - if (strcmp (text_key, skippable_keys[n]) == 0) - return true; - return false; + is->non_district_shield_icons_remaining = 0; + is->district_shield_icons_remaining = 0; + is->distribution_hub_shield_icons_remaining = 0; } int __fastcall -patch_PopupForm_impl_begin_showing_popup (PopupForm * this) +patch_Sprite_draw_production_income_icon (Sprite * this, int edx, PCX_Image * canvas, int pixel_x, int pixel_y, PCX_Color_Table * color_table) { - if (is_online_game () || - (! is->current_config.convert_some_popups_into_online_mp_messages) || - (! is_skippable_popup (this->text_key))) - return PopupForm_impl_begin_showing_popup (this); - - else { - unsigned saved_prefs = *p_preferences; - int saved_flags = this->field_1BF0[0xE4]; - - *p_preferences |= P_SHOW_FEWER_MP_POPUPS; - this->field_1BF0[0xE4] |= 0x4000; - int tr = PopupForm_impl_begin_showing_popup (this); + if (is->distribution_hub_icons_img_state == IS_UNINITED) + init_distribution_hub_icons (); + if (is->dc_icons_img_state == IS_UNINITED) + init_district_icons (); - *(bool *)(p_main_screen_form->animator.field_18E4 + 10) = true; // Set what must be a dirty flag - Animator_update (&p_main_screen_form->animator); // Make sure message appears + if ((is->current_config.enable_districts || is->current_config.enable_natural_wonders) && is->dc_icons_img_state == IS_OK) { + Sprite to_draw = *this; + if (is->corruption_shield_icons_remaining > 0 || + is->district_corruption_icons_remaining > 0 || + is->distribution_hub_corruption_icons_remaining > 0) { - this->field_1BF0[0xE4] = saved_flags; - *p_preferences = saved_prefs; + if (is->corruption_shield_icons_remaining > 0) { + is->corruption_shield_icons_remaining--; + } else if (is->district_corruption_icons_remaining > 0) { + to_draw = is->district_corruption_icon; + is->district_corruption_icons_remaining--; + } else if (is->distribution_hub_icons_img_state == IS_OK) { + to_draw = is->distribution_hub_corruption_icon; + is->distribution_hub_corruption_icons_remaining--; + } + } + else if (is->non_district_shield_icons_remaining > 0 || + is->district_shield_icons_remaining > 0 || + is->distribution_hub_shield_icons_remaining > 0) { - return tr; + if (is->non_district_shield_icons_remaining > 0) { + is->non_district_shield_icons_remaining--; + } else if (is->district_shield_icons_remaining > 0) { + to_draw = is->district_shield_icon; + is->district_shield_icons_remaining--; + } else if (is->distribution_hub_icons_img_state == IS_OK) { + to_draw = is->distribution_hub_shield_icon; + is->distribution_hub_shield_icons_remaining--; + } + + } + return Sprite_draw (&to_draw, __, canvas, pixel_x, pixel_y, color_table); } -} - -bool __stdcall -patch_is_online_game_for_show_popup () -{ - return is->current_config.convert_some_popups_into_online_mp_messages ? true : is_online_game (); + return Sprite_draw (this, __, canvas, pixel_x, pixel_y, color_table); } bool -ai_move_district_worker (Unit * worker, struct district_worker_record * rec) +district_tile_needs_defense (Tile * tile, int tile_x, int tile_y, struct district_instance * inst, int civ_id, int work_radius) { - if ((worker == NULL) || (rec == NULL)) - return false; + if ((tile == NULL) || (tile == p_null_tile)) return false; + if (inst == NULL) return false; - char ss[200]; - snprintf (ss, sizeof ss, "ai_move_district_worker: Worker ID %d assigned to build district\n", worker->Body.ID); - (*p_OutputDebugStringA) (ss); + int district_id = inst->district_id; + struct district_config const * cfg = &is->district_configs[district_id]; + if (! district_is_complete (tile, district_id)) return false; + if (tile->vtable->m38_Get_Territory_OwnerID (tile) != civ_id) return false; - // Check the original request city made for district - struct pending_district_request * req = rec->pending_req; - if ((req == NULL) || - (req->assigned_worker_id != worker->Body.ID) || - (req->target_x < 0) || (req->target_y < 0)) + // Check if already has enough defenders (2 for aerodromes, distribution hubs, destroyable completed wonders) + int defender_count = count_units_at (tile_x, tile_y, UF_DEFENDER_VIS_TO_A_OF_CLASS_B, civ_id, 0, -1); + int max_defenders = + (district_id == AERODROME_DISTRICT_ID || district_id == DISTRIBUTION_HUB_DISTRICT_ID || + (cfg->defense_bonus_percent > 0 && district_id != NEIGHBORHOOD_DISTRICT_ID) || + (district_id == WONDER_DISTRICT_ID && is->current_config.completed_wonder_districts_can_be_destroyed)) + ? 2 : 1; + if (defender_count >= max_defenders) return false; - int district_id = req->district_id; - snprintf (ss, sizeof ss, "ai_move_district_worker: Worker ID %d moving to (%d,%d) to build district\n", worker->Body.ID, req->target_x, req->target_y); - (*p_OutputDebugStringA) (ss); + // Distribution hubs always need defense + if (district_id == DISTRIBUTION_HUB_DISTRICT_ID) + return true; - City * request_city = get_city_ptr (req->city_id); - if (request_city == NULL) { - clear_tracked_worker_assignment (rec); - remove_pending_district_request (req); + // Wonder districts need defense if under construction or completed (not unused) + if (district_id == WONDER_DISTRICT_ID && is->current_config.completed_wonder_districts_can_be_destroyed) { + enum wonder_district_state state = inst->wonder_info.state; + if (state == WDS_UNDER_CONSTRUCTION || state == WDS_COMPLETED) + return true; + // Unused wonder districts don't need defense return false; } - req->city = request_city; - - // If the worker has arrived - if ((worker->Body.X == req->target_x) && (worker->Body.Y == req->target_y)) { - snprintf (ss, sizeof ss, "ai_move_district_worker: Worker ID %d arrived at (%d,%d) to build district\n", worker->Body.ID, worker->Body.X, worker->Body.Y); - (*p_OutputDebugStringA) (ss); + return any_enemies_near (&leaders[civ_id], tile_x, tile_y, -1, work_radius); +} - Tile * tile = tile_at (worker->Body.X, worker->Body.Y); - struct district_instance * inst = get_district_instance (tile); - bool do_replacement = false; +int +compute_turns_required_for_path (Unit * unit, int path_length) +{ + if (path_length < 1) + return 0; - // If there is a completed district here already - if ((inst != NULL) && district_is_complete (tile, inst->district_type)) { - int existing_district_id = inst->district_type; + int max_mp = patch_Unit_get_max_move_points (unit); + if (max_mp <= 0) + return 9999; - snprintf (ss, sizeof ss, "ai_move_district_worker: Worker ID %d found existing district ID %d at (%d,%d)\n", worker->Body.ID, existing_district_id, worker->Body.X, worker->Body.Y); - (*p_OutputDebugStringA) (ss); + int moves_used_this_turn = max_mp - unit->Body.Moves; + moves_used_this_turn = not_below (0, moves_used_this_turn); + moves_used_this_turn = not_above (moves_used_this_turn, 9999); - if (existing_district_id == req->district_id) { - clear_city_district_request (request_city, req->district_id); - clear_tracked_worker_assignment (rec); - return false; - } + int remaining_mp = path_length - moves_used_this_turn; + if (remaining_mp < 0) + remaining_mp = 0; - if (existing_district_id == WONDER_DISTRICT_ID) { - struct wonder_district_info * info = &inst->wonder_info; - if (info->state == WDS_UNUSED) - do_replacement = true; - } + return (max_mp - 1 + remaining_mp) / max_mp + 1; +} - if (!do_replacement) { - return false; // Nothing left to do here - } - } +void +maybe_update_best_district_target (Unit * unit, + int civ_id, + int max_distance, + int unit_x, + int unit_y, + int tile_x, + int tile_y, + int base_score, + int * best_score, + int * best_x, + int * best_y, + int * best_path_length, + int * evaluated_paths, + int max_path_checks) +{ + if (*evaluated_paths >= max_path_checks) + return; - snprintf (ss, sizeof ss, "ai_move_district_worker: Checking for duplicate districts near city at (%d,%d)\n", request_city->Body.X, request_city->Body.Y); - (*p_OutputDebugStringA) (ss); + int path_length; + int path_result = Trade_Net_set_unit_path (is->trade_net, __, unit_x, unit_y, tile_x, tile_y, + unit, civ_id, 0x81, &path_length); + *evaluated_paths += 1; + if (path_result <= 0) + return; - // One final check: do we still need the district? Check for any dupes nearby - if (req->district_id != DISTRIBUTION_HUB_DISTRICT_ID && req->district_id != NEIGHBORHOOD_DISTRICT_ID) { - int civ_id = worker->Body.CivID; - FOR_TILES_AROUND (tai, is->workable_tile_count, request_city->Body.X, request_city->Body.Y) { - Tile * nearby = tai.tile; - if (nearby == p_null_tile || nearby == tile) continue; - if (nearby->vtable->m38_Get_Territory_OwnerID (nearby) != civ_id) continue; - - struct district_instance * nearby_inst = get_district_instance (nearby); - if (nearby_inst != NULL && nearby_inst->district_type == req->district_id) { - snprintf (ss, sizeof ss, "ai_move_district_worker: Found duplicate district ID %d near city at (%d,%d), cancelling request\n", req->district_id, request_city->Body.X, request_city->Body.Y); - (*p_OutputDebugStringA) (ss); - clear_city_district_request (request_city, req->district_id); - return false; - } - } - } + if (max_distance > 0) { + int turns = compute_turns_required_for_path (unit, path_length); + if (turns > max_distance) + return; + } - // Need to make sure distro hubs get roads. If this will be one, build the road first to be sure - if (req->district_id == DISTRIBUTION_HUB_DISTRICT_ID) { - bool has_road = (*tile->vtable->m25_Check_Roads)(tile, __, 0); - if (! has_road) { - Unit_set_state(worker, __, UnitState_Build_Road); - worker->Body.Job_ID = WJ_Build_Road; - return true; - } - } + int score = base_score - path_length; + if ((score > *best_score) || ((score == *best_score) && (path_length < *best_path_length))) { + *best_score = score; + *best_x = tile_x; + *best_y = tile_y; + *best_path_length = path_length; + } +} - enum SquareTypes base_type = tile->vtable->m50_Get_Square_BaseType (tile); - unsigned int overlay_flags = tile->vtable->m42_Get_Overlays (tile, __, 0); - unsigned int removable_flags = overlay_flags & 0xfc; +// Patch seek_colony to actively search for undefended districts +int __fastcall +patch_Unit_seek_colony (Unit * this, int edx, bool for_own_defense, int max_distance) +{ + // Only intercept if defending own assets and districts are enabled + if (!for_own_defense || + !is->current_config.enable_districts || + !is->current_config.ai_defends_districts) + return Unit_seek_colony (this, __, for_own_defense, max_distance); - // Remove any existing improvements - tile->vtable->m62_Set_Tile_BuildingID (tile, __, -1); - tile->vtable->m51_Unset_Tile_Flags (tile, __, 0, removable_flags, worker->Body.X, worker->Body.Y); - if (do_replacement) { - remove_district_instance (tile); - handle_district_removed (tile, req->district_id, worker->Body.X, worker->Body.Y, false); - } + int civ_id = this->Body.CivID; + int unit_x = this->Body.X; + int unit_y = this->Body.Y; - snprintf (ss, sizeof ss, "ai_move_district_worker: Checking for craters or pollution at worker ID %d location (%d,%d)\n", worker->Body.ID, worker->Body.X, worker->Body.Y); - (*p_OutputDebugStringA) (ss); + Tile * current_tile = tile_at (unit_x, unit_y); + if ((current_tile == NULL) || (current_tile == p_null_tile)) + return Unit_seek_colony (this, __, for_own_defense, max_distance); - if (tile->vtable->m21_Check_Crates (tile, __, 0) || tile->vtable->m20_Check_Pollution (tile, __, 0)) { - snprintf (ss, sizeof ss, "ai_move_district_worker: Worker ID %d clearing craters or pollution at (%d,%d)\n", worker->Body.ID, worker->Body.X, worker->Body.Y); - (*p_OutputDebugStringA) (ss); - Unit_set_state(worker, __, UnitState_Clear_Damage); - worker->Body.Job_ID = WJ_Clean_Pollution; - return true; - } + int continent_id = current_tile->vtable->m46_Get_ContinentID (current_tile); - snprintf (ss, sizeof ss, "ai_move_district_worker: Checking for forest or wetlands at worker ID %d location (%d,%d)\n", worker->Body.ID, worker->Body.X, worker->Body.Y); - (*p_OutputDebugStringA) (ss); + const int search_radius = 20; + const int max_path_checks = 64; + const int wonder_base_score = 1000; + const int regular_base_score = 500; - // Clear any forest/wetlands - if (base_type == SQ_Forest) { - Unit_set_state(worker, __, UnitState_Clear_Forest); - worker->Body.Job_ID = WJ_Clean_Forest; - return true; - } else if ((base_type == SQ_Jungle) || (base_type == SQ_Swamp)) { - Unit_set_state(worker, __, UnitState_Clear_Wetlands); - worker->Body.Job_ID = WJ_Clear_Swamp; - return true; - } + int best_x = -1; + int best_y = -1; + int best_score = INT_MIN; + int best_path_length = INT_MAX; + int evaluated_paths = 0; - snprintf (ss, sizeof ss, "ai_move_district_worker: Worker ID %d starting construction of district at (%d,%d)\n", worker->Body.ID, worker->Body.X, worker->Body.Y); - (*p_OutputDebugStringA) (ss); + bool abort_search = false; - // Clear any existing improvements (irrigation and mines) - tile->vtable->m62_Set_Tile_BuildingID (tile, __, -1); - tile->vtable->m51_Unset_Tile_Flags (tile, __, 0, removable_flags, worker->Body.X, worker->Body.Y); + if ((is->current_config.enable_wonder_districts && is->current_config.completed_wonder_districts_can_be_destroyed) || + is->current_config.enable_distribution_hub_districts) { + for (int dx = -search_radius; (dx <= search_radius) && !abort_search; dx++) { + for (int dy = -search_radius; dy <= search_radius; dy++) { + if (evaluated_paths >= max_path_checks) { + abort_search = true; + break; + } - // Start construction of district - inst = ensure_district_instance (tile, req->district_id, req->target_x, req->target_y); - Unit_set_state(worker, __, UnitState_Build_Mines); - worker->Body.Job_ID = WJ_Build_Mines; // Build district - return true; + int target_x = Map_wrap_horiz (&p_bic_data->Map, __, unit_x + dx); + int target_y = Map_wrap_vert (&p_bic_data->Map, __, unit_y + dy); + Tile * district_tile = tile_at (target_x, target_y); + if ((district_tile == NULL) || (district_tile == p_null_tile)) + continue; - // Else if the worker needs to be sent - } else { - if ((worker->Body.UnitState != UnitState_Go_To) || - (worker->Body.path_dest_x != req->target_x) || - (worker->Body.path_dest_y != req->target_y)) { - int path_result = patch_Trade_Net_set_unit_path (is->trade_net, __, - worker->Body.X, worker->Body.Y, req->target_x, req->target_y, - worker, worker->Body.CivID, 0x41, NULL); - if (path_result > 0) { + struct district_instance * inst = get_district_instance (district_tile); + if (inst == NULL) + continue; - snprintf (ss, sizeof ss, "ai_move_district_worker: Worker ID %d path set to (%d,%d) to build district\n", worker->Body.ID, req->target_x, req->target_y); - (*p_OutputDebugStringA) (ss); + int tile_x, tile_y; + if (! district_instance_get_coords (inst, district_tile, &tile_x, &tile_y)) + continue; + if (district_tile->vtable->m46_Get_ContinentID (district_tile) != continent_id) + continue; + if (! district_tile_needs_defense (district_tile, tile_x, tile_y, inst, civ_id, 5)) + continue; - Unit_set_escortee (worker, __, -1); - Unit_set_state (worker, __, UnitState_Go_To); - worker->Body.path_dest_x = req->target_x; - worker->Body.path_dest_y = req->target_y; - } else { - clear_tracked_worker_assignment (rec); - return false; + maybe_update_best_district_target (this, civ_id, max_distance, unit_x, unit_y, + tile_x, tile_y, wonder_base_score, + &best_score, &best_x, &best_y, + &best_path_length, &evaluated_paths, + max_path_checks); } } } - return false; -} -void __fastcall -patch_Unit_ai_move_terraformer (Unit * this) -{ - Map * map = &p_bic_data->Map; - int type_id = this->Body.UnitTypeID; - int civ_id = this->Body.CivID; - Tile * tile = tile_at (this->Body.X, this->Body.Y); - bool is_human = (*p_human_player_bits & (1 << this->Body.CivID)) != 0; + if ((best_x < 0) && (evaluated_paths < max_path_checks)) { + for (int dx = -search_radius; (dx <= search_radius) && (evaluated_paths < max_path_checks); dx++) { + for (int dy = -search_radius; dy <= search_radius; dy++) { + if (evaluated_paths >= max_path_checks) + break; - - if (is->current_config.enable_districts && ! is_human) { - update_tracked_worker_for_unit (this); - struct district_instance * inst = get_district_instance (tile); + int target_x = Map_wrap_horiz (&p_bic_data->Map, __, unit_x + dx); + int target_y = Map_wrap_vert (&p_bic_data->Map, __, unit_y + dy); + Tile * district_tile = tile_at (target_x, target_y); + if ((district_tile == NULL) || (district_tile == p_null_tile)) + continue; - if (inst != NULL && inst->district_type != NATURAL_WONDER_DISTRICT_ID) { - // Roads should be made after district builds. The district is complete but - // worker is still likely on the tile, so check here and build road if needed - bool has_road = (*tile->vtable->m25_Check_Roads)(tile, __, 0); - if (! has_road) { - Unit_set_state(this, __, UnitState_Build_Road); - this->Body.Job_ID = WJ_Build_Road; - return; - } + struct district_instance * inst = get_district_instance (district_tile); + if ((inst == NULL) || (inst->district_id == WONDER_DISTRICT_ID)) + continue; - // Same check for railroads - bool can_build_railroad = Leader_can_do_worker_job (&leaders[this->Body.CivID], __, WJ_Build_Railroad, this->Body.X, this->Body.Y, 0); - bool has_railroad = (*tile->vtable->m23_Check_Railroads)(tile, __, 0); - if (can_build_railroad && !has_railroad) { - Unit_set_state(this, __, UnitState_Build_Railroad); - this->Body.Job_ID = WJ_Build_Railroad; - return; - } - } + int tile_x, tile_y; + if (! district_instance_get_coords (inst, district_tile, &tile_x, &tile_y)) + continue; + if (district_tile->vtable->m46_Get_ContinentID (district_tile) != continent_id) + continue; + if (! district_tile_needs_defense (district_tile, tile_x, tile_y, inst, civ_id, 5)) + continue; - struct district_worker_record * rec = get_tracked_worker_record (this); - if (rec->pending_req != NULL) { - if (ai_move_district_worker (this, rec)) - return; + maybe_update_best_district_target (this, civ_id, max_distance, unit_x, unit_y, + tile_x, tile_y, regular_base_score, + &best_score, &best_x, &best_y, + &best_path_length, &evaluated_paths, + max_path_checks); + } } } - bool pop_else_caravan; - if ((tile != NULL) && (tile != p_null_tile) && - (type_id >= 0) && (type_id < p_bic_data->UnitTypeCount) && - is_material_unit (&p_bic_data->UnitTypes[type_id], &pop_else_caravan) && - ((pop_else_caravan && is->current_config.enable_pop_unit_ai) || - ((! pop_else_caravan) && is->current_config.enable_caravan_unit_ai))) { - ai_move_material_unit (this); - return; + if ((best_x >= 0) && (best_y >= 0)) { + int result = Trade_Net_set_unit_path (is->trade_net, __, unit_x, unit_y, best_x, best_y, + this, civ_id, 0x181, NULL); + return result; } - Unit_ai_move_terraformer (this); + return Unit_seek_colony (this, __, for_own_defense, max_distance); } +// Patch has_colony to make units stay on districts, only patches within Unit::ai_move_defensive_unit bool __fastcall -patch_Unit_ai_can_sacrifice (Unit * this, int edx, bool requires_city) -{ - int sacrifice_action = UCV_Sacrifice & 0x0FFFFFFF; // Mask out top four category bits - UnitType * type = &p_bic_data->UnitTypes[this->Body.UnitTypeID]; - if (is->current_config.patch_ai_can_sacrifice_without_special_ability && ((type->Special_Actions & sacrifice_action) == 0)) - return false; - else - return Unit_ai_can_sacrifice (this, __, requires_city); -} - -int __cdecl -patch_get_building_defense_bonus_at (int x, int y, int param_3) +patch_Tile_has_district_or_colony (Tile * this) { - // Get base building defense bonus first - int base = get_building_defense_bonus_at (x, y, param_3); - - // If districts are disabled, return base - if (!is->current_config.enable_districts) - return base; - - Tile * tile = tile_at (x, y); - if ((tile == NULL) || (tile == p_null_tile)) - return base; + if (is->current_config.enable_districts && + is->current_config.ai_defends_districts) { - struct district_instance * inst = get_district_instance (tile); - if (inst != NULL) { - return is->district_configs[inst->district_type].defense_bonus_percent; - } + struct district_instance * inst = get_district_instance (this); + return (inst != NULL) && district_is_complete (this, inst->district_id); + } - return base; + // Fallback to original has_colony logic + return Tile_has_colony (this); } -void __fastcall -patch_Unit_select (Unit * this) +int __fastcall +patch_Buildings_Info_get_age_in_years_for_tourism (Buildings_Info * this, int edx, int building_index) { - if (is->current_config.enable_districts) { - Tile * tile = tile_at (this->Body.X, this->Body.Y); - - struct district_instance * inst = get_district_instance (tile); - if (inst != NULL && inst->district_type >= 0 && inst->district_type <= is->district_count && - ! district_is_complete (tile, inst->district_type)) { - int district_id = inst->district_type; - PopupForm * popup = get_popup_form (); - int remaining_turns = get_worker_remaining_turns_to_complete (this, __, 0); - set_popup_str_param (0, (char*)is->district_configs[district_id].name, -1, -1); - set_popup_int_param (1, remaining_turns); - popup->vtable->set_text_key_and_flags (popup, __, is->mod_script_path, "C3X_CONFIRM_CANCEL_BUILD_DISTRICT", -1, 0, 0, 0); - - int sel = patch_show_popup (popup, __, 0, 0); - if (sel == 0) { - Unit_set_escortee (this, __, -1); - Unit_set_state (this, __, 0); + int base = Buildings_Info_get_age_in_years (this, __, building_index); + if (is->current_config.tourism_time_scale_percent == 100) + return base; + else if (is->current_config.tourism_time_scale_percent <= 0) + return INT_MAX; + else + return (base * 100 + 50) / is->current_config.tourism_time_scale_percent; +} - bool other_workers_present = false; - FOR_UNITS_ON (uti, tile) { - Unit * unit = uti.unit; - if ((unit != NULL) && (unit != this) && - (unit->Body.UnitState == UnitState_Build_Mines) && - (p_bic_data->UnitTypes[unit->Body.UnitTypeID].Worker_Actions != 0)) { - other_workers_present = true; - break; - } - } - if (! other_workers_present) { - remove_district_instance (tile); - } - } else { - return; - } - } - } +int __fastcall +patch_Sprite_draw_minimap_frame (Sprite * this, int edx, Sprite * alpha, int param_2, PCX_Image * canvas, int x, int y, int param_6) +{ + bool want_larger_minimap = (is->current_config.double_minimap_size == MDM_ALWAYS) || + ((is->current_config.double_minimap_size == MDM_HIGH_DEF) && (p_bic_data->ScreenWidth >= 1920)); + if (want_larger_minimap && (init_large_minimap_frame () == IS_OK)) + return Sprite_draw_for_hud (&is->double_size_box_left_color_pcx, __, &is->double_size_box_left_alpha_pcx, param_2, canvas, x, y, param_6); + else + return Sprite_draw_for_hud (this, __, alpha, param_2, canvas, x, y, param_6); +} - // Sometimes clearing of highlighted tiles doesn't trigger when CTRL lifted, so double-check here - if (is->current_config.enable_city_work_radii_highlights && is->highlight_city_radii) { - is->highlight_city_radii = false; - clear_highlighted_worker_tiles_and_redraw (); - } +int __fastcall +patch_City_get_turns_to_build_2_for_ai_move_leader (City * this, int edx, City_Order * order, bool param_2) +{ + // Initialize order variable to city's current build. This is not done in the base logic and can cause crashes. + City_Order current_order = { .OrderID = this->Body.Order_ID, .OrderType = this->Body.Order_Type }; + if (is->current_config.patch_crash_in_leader_unit_ai) + order = ¤t_order; - Unit_select (this); + return City_get_turns_to_build_2 (this, __, order, param_2); } void __fastcall -patch_City_Form_draw_food_income_icons (City_Form * this) +patch_City_set_production (City * this, int edx, int order_type, int order_id, bool ask_to_confirm) { - // Call original function first - City_Form_draw_food_income_icons (this); - - if (! is->current_config.enable_districts && ! is->current_config.enable_natural_wonders) - return; - - // Get current city - City * city = this->CurrentCity; - if (city == NULL) - return; - - int city_id = city->Body.ID; - int civ_id = city->Body.CivID; - - // Calculate standard district food bonus - int standard_district_food = 0; - FOR_TILES_AROUND (tai, is->workable_tile_count, city->Body.X, city->Body.Y) { - if (tai.tile == NULL || tai.tile == p_null_tile) continue; - if (tai.tile->Territory_OwnerID != civ_id) continue; - if (tile_has_enemy_unit (tai.tile, civ_id)) continue; - if (tai.tile->vtable->m20_Check_Pollution (tai.tile, __, 0)) continue; - struct district_instance * inst = get_district_instance (tai.tile); - if (inst == NULL) continue; - int district_id = inst->district_type; - if ((district_id < 0) || (district_id >= is->district_count)) continue; - if (! district_is_complete (tai.tile, district_id)) continue; - - int food_bonus = 0; - get_effective_district_yields (inst, &is->district_configs[district_id], &food_bonus, NULL, NULL, NULL, NULL); - standard_district_food += food_bonus; - } - - // Get distribution hub food bonus - int distribution_hub_food = 0; - if (is->current_config.enable_districts && is->current_config.enable_distribution_hub_districts) - get_distribution_hub_yields_for_city (city, &distribution_hub_food, NULL); - - // Total district food - int total_district_food = standard_district_food + distribution_hub_food; - if (total_district_food <= 0) - return; - - // Lazy load icons - if (is->current_config.enable_districts && is->current_config.enable_distribution_hub_districts) { - if (is->distribution_hub_icons_img_state == IS_UNINITED) - init_distribution_hub_icons (); - if (is->distribution_hub_icons_img_state != IS_OK) - return; - } - if (is->dc_icons_img_state == IS_UNINITED) - init_district_icons (); - if (is->dc_icons_img_state != IS_OK) + City_set_production (this, __, order_type, order_id, ask_to_confirm); + + if (! is->current_config.enable_districts || ! is->current_config.enable_wonder_districts) return; - int food_income = city->Body.FoodIncome; - int food_required = city->Body.FoodRequired; - - // Calculate how standard district food icons are distributed - int standard_food_eaten = 0; - int standard_food_surplus = 0; - if (standard_district_food > 0) { - if (standard_district_food >= food_income) { - standard_food_surplus = food_income; - standard_food_eaten = standard_district_food - food_income; - } else { - standard_food_surplus = standard_district_food; - standard_food_eaten = 0; - } - } + // If the human player, we need to set/unset a wonder district for this city, depending + // on what is being built. The human player wouldn't be able to choose a wonder if a wonder + // district wasn't available, so we don't need to worry about feasibility here. + // The AI uses a different mechanism to reserve wonder districts via patch_Leader_do_production_phase. + bool is_human = (*p_human_player_bits & (1 << this->Body.CivID)) != 0; + if (! is_human) + return; - // Calculate how distribution hub food icons are distributed - int hub_food_eaten = 0; - int hub_food_surplus = 0; - int remaining_income = food_income - standard_food_surplus; - if (distribution_hub_food > 0) { - if (distribution_hub_food >= remaining_income) { - hub_food_surplus = remaining_income; - hub_food_eaten = distribution_hub_food - remaining_income; - } else { - hub_food_surplus = distribution_hub_food; - hub_food_eaten = 0; + char ss[256]; + bool release_reservation = true; + if (this->Body.Order_Type == COT_Improvement) { + Improvement * improv = &p_bic_data->Improvements[this->Body.Order_ID]; + if (improv->Characteristics & (ITC_Wonder | ITC_Small_Wonder)) { + if (reserve_wonder_district_for_city (this, this->Body.Order_ID)) { + release_reservation = false; + } } } - // Draw eaten district food icons (left side, from right to left) - int total_eaten = standard_food_eaten + hub_food_eaten; - if (total_eaten > 0) { - Sprite * base_eaten_sprite = &(this->City_Icons_Images).Icon_07; - int eaten_sprite_width = base_eaten_sprite->Width; - int eaten_sprite_height = base_eaten_sprite->Height; - struct tagRECT * eaten_rect = &this->Food_Consumption_Rect; - int eaten_rect_width = eaten_rect->right - eaten_rect->left; + if (release_reservation) { + release_wonder_district_reservation (this); + } +} - int eaten_spacing = eaten_sprite_width; - if ((food_required > 1) && (food_required * eaten_sprite_width - eaten_rect_width != 0) && - (eaten_rect_width <= food_required * eaten_sprite_width)) { - eaten_spacing = (eaten_rect_width - eaten_sprite_width) / (food_required - 1); - if (eaten_spacing < 1) - eaten_spacing = 1; - else if (eaten_spacing > eaten_sprite_width) - eaten_spacing = eaten_sprite_width; +int __fastcall +patch_Tile_m71_Check_Worker_Job (Tile * this) +{ + if (is->current_config.enable_natural_wonders) { + struct district_instance * inst = get_district_instance (this); + if ((inst != NULL) && + (inst->district_id == NATURAL_WONDER_DISTRICT_ID) && + (inst->natural_wonder_info.natural_wonder_id >= 0)) { + return -1; // No worker job allowed on natural wonders } + } + return Tile_m71_Check_Worker_Job (this); +} - int eaten_x_offset = eaten_spacing * (food_required - 1); - // Draw standard district eaten first - for (int i = 0; i < standard_food_eaten && i < food_required; i++) { - int x = eaten_rect->left + eaten_x_offset; - int y = eaten_rect->top + ((eaten_rect->bottom - eaten_rect->top >> 1) - - (eaten_sprite_height >> 1)); - Sprite_draw (&is->district_food_eaten_icon, __, &(this->Base).Data.Canvas, x, y, NULL); - eaten_x_offset -= eaten_spacing; +int __fastcall +patch_Tile_get_road_bonus (Tile * this) +{ + if (is->current_config.enable_natural_wonders) { + struct district_instance * inst = get_district_instance (this); + if ((inst != NULL) && (inst->district_id == NATURAL_WONDER_DISTRICT_ID)) { + return 0; } - // Draw distribution hub eaten - for (int i = 0; i < hub_food_eaten && eaten_x_offset >= 0; i++) { - int x = eaten_rect->left + eaten_x_offset; - int y = eaten_rect->top + ((eaten_rect->bottom - eaten_rect->top >> 1) - - (eaten_sprite_height >> 1)); - Sprite_draw (&is->distribution_hub_eaten_food_icon, __, &(this->Base).Data.Canvas, x, y, NULL); - eaten_x_offset -= eaten_spacing; + } + if (is->current_config.enable_districts && is->current_config.enable_bridge_districts) { + struct district_instance * inst = get_district_instance (this); + if ((inst != NULL) && (inst->district_id == BRIDGE_DISTRICT_ID)) { + return 1; } } + return Tile_get_road_bonus (this); +} - // Draw surplus district food icons (right side, from right to left) - int total_surplus = standard_food_surplus + hub_food_surplus; - if (total_surplus > 0) { - Sprite * base_surplus_sprite = &(this->City_Icons_Images).Icon_06; - int surplus_sprite_width = base_surplus_sprite->Width; - int surplus_sprite_height = base_surplus_sprite->Height; - struct tagRECT * surplus_rect = &this->Food_Storage_Rect; - int surplus_rect_width = surplus_rect->right - surplus_rect->left; +bool __fastcall +patch_Unit_has_army_ability_to_perform_unload (Unit * this, int edx, enum UnitTypeAbilities a) +{ + return is->current_config.allow_unload_from_army ? false : Unit_has_ability (this, __, a); +} - int surplus_spacing = surplus_sprite_width; - if ((food_income > 1) && (food_income * surplus_sprite_width - surplus_rect_width != 0) && - (surplus_rect_width <= food_income * surplus_sprite_width)) { - surplus_spacing = (surplus_rect_width - surplus_sprite_width) / (food_income - 1); - if (surplus_spacing < 1) - surplus_spacing = 1; - else if (surplus_spacing > surplus_sprite_width) - surplus_spacing = surplus_sprite_width; - } +void __fastcall +patch_Unit_disembark (Unit * this, int edx, int tile_x, int tile_y) +{ + Unit * container = get_unit_ptr (this->Body.Container_Unit); + bool unloading_from_army = is->current_config.allow_unload_from_army && container != NULL && Unit_has_ability (container, __, UTA_Army); - int surplus_x_offset = 0; - // Draw standard district surplus first - for (int i = 0; i < standard_food_surplus && i < food_income; i++) { - int x = surplus_rect->right - surplus_x_offset - surplus_sprite_width; - int y = surplus_rect->top + ((surplus_rect->bottom - surplus_rect->top >> 1) - - (surplus_sprite_height >> 1)) - 1; - Sprite_draw (&is->district_food_icon, __, &(this->Base).Data.Canvas, x, y, NULL); - surplus_x_offset += surplus_spacing; - } - // Draw distribution hub surplus - for (int i = 0; i < hub_food_surplus && surplus_x_offset < surplus_rect_width; i++) { - int x = surplus_rect->right - surplus_x_offset - surplus_sprite_width; - int y = surplus_rect->top + ((surplus_rect->bottom - surplus_rect->top >> 1) - - (surplus_sprite_height >> 1)) - 1; - Sprite_draw (&is->distribution_hub_food_icon, __, &(this->Base).Data.Canvas, x, y, NULL); - surplus_x_offset += surplus_spacing; + // If removing a unit from an army, transfer a proportional amount of damage to the unit removed + int damage_transferred = 0; + if (unloading_from_army) { + int army_damage_percent = container->Body.Damage * 100 / Unit_get_max_hp (container), + max_damage = Unit_get_max_hp (this) - 1 - this->Body.Damage; + damage_transferred = clamp (0, max_damage, (army_damage_percent * Unit_get_max_hp (this) + 50) / 100); + } + + Unit_disembark (this, __, tile_x, tile_y); + + if (unloading_from_army) { + this->Body.Damage = not_above (Unit_get_max_hp (this) - 1, this->Body.Damage + damage_transferred); + container->Body.Damage = not_below (0, container->Body.Damage - damage_transferred); + + if (this->Body.ID == container->Body.army_top_defender_id) { + Unit * new_top_defender = NULL; + FOR_UNITS_ON (uti, tile_at (container->Body.X, container->Body.Y)) + if (uti.unit->Body.Container_Unit == container->Body.ID) { + if (new_top_defender == NULL) + new_top_defender = uti.unit; + else if (Fighter_prefer_first_defender_1 (&p_bic_data->fighter, __, uti.unit, Unit_get_defense_strength (uti.unit), new_top_defender, Unit_get_defense_strength (new_top_defender), true)) + new_top_defender = uti.unit; + } + container->Body.army_top_defender_id = (new_top_defender != NULL) ? new_top_defender->Body.ID : -1; } } } -void -recompute_district_and_distribution_hub_shields_for_city_view (City * city) +bool __fastcall +patch_Unit_has_ability_no_load_non_army_passengers (Unit * this, int edx, enum UnitTypeAbilities army_ability) { - if ((! is->current_config.enable_districts && ! is->current_config.enable_natural_wonders) || (city == NULL)) - return; + UnitType * transport_type = &p_bic_data->UnitTypes[is->can_load_transport->Body.UnitTypeID], + * passenger_type = &p_bic_data->UnitTypes[this ->Body.UnitTypeID]; - int city_id = city->Body.ID; - int city_x = city->Body.X; - int city_y = city->Body.Y; + // This call comes from Unit::can_load at the point where it's determined that the passenger (this) has transport capacity > 0 and is checking + // whether it's an army. If not, it can't be loaded. Add two exceptions here for land transports, if configured, one to allow LTs to load into + // naval units and another to allow empty LTs to load into armies. + if (is->current_config.land_transport_rules != 0) + if ((passenger_type->Unit_Class == UTC_Land) && ! Unit_has_ability (this, __, army_ability)) { // if it's a land transport + if ((is->current_config.land_transport_rules & LTR_LOAD_ONTO_BOAT) && (transport_type->Unit_Class == UTC_Sea)) + return true; + if ((is->current_config.land_transport_rules & LTR_JOIN_ARMY) && + Unit_has_ability (is->can_load_transport, __, army_ability) && (Unit_count_contained_units (this) == 0)) + return true; + } - // District yields are injected through the city center tile in patch_City_calc_tile_yield_while_gathering. - // Grab the base yield (no districts) directly from the original function, then compute the - // district bonus that calculate_city_center_district_bonus will layer on afterward. - int city_center_base_shields = City_calc_tile_yield_at (city, __, YK_SHIELDS, city_x, city_y); - int total_district_shield_bonus = 0; - calculate_city_center_district_bonus (city, NULL, &total_district_shield_bonus, NULL); + // Similarly, allow helicopters to be loaded onto carriers if so configured + if ((is->current_config.special_helicopter_rules & SHR_ALLOW_ON_CARRIERS) && + passenger_type->Unit_Class == UTC_Air && + transport_type->Unit_Class == UTC_Sea && + Unit_has_ability (is->can_load_transport, __, UTA_Transports_Only_Aircraft)) + return true; - // Distribution hub contribution is tracked separately for icon rendering. - int distribution_hub_shields = 0; - if (is->current_config.enable_distribution_hub_districts) - get_distribution_hub_yields_for_city (city, NULL, &distribution_hub_shields); - if (distribution_hub_shields < 0) - distribution_hub_shields = 0; - if (distribution_hub_shields > total_district_shield_bonus) - distribution_hub_shields = total_district_shield_bonus; + return Unit_has_ability (this, __, army_ability); +} - int standard_district_shields = total_district_shield_bonus - distribution_hub_shields; - if (standard_district_shields < 0) - standard_district_shields = 0; +bool __fastcall +patch_Unit_has_ability_no_load_transport_into_army (Unit * this, int edx, enum UnitTypeAbilities army_ability) +{ + // Similar to above, here it checks if the target unit is an army and rejects the load if it is (again, already determined that the passenger + // has transport capacity). Modify this rule to return false for land transports trying to join armies if configured. We don't have to check + // that the LT is empty since that is already disallowed by the modified check above. + bool is_army = Unit_has_ability (this, __, army_ability); + if ((is->current_config.land_transport_rules & LTR_JOIN_ARMY) && is_army && + (p_bic_data->UnitTypes[is->can_load_passenger->Body.UnitTypeID].Unit_Class == UTC_Land)) + return false; - // Recompute yields with districts active so ProductionIncome/Loss reflect the city view. - recompute_city_yields_with_districts (city); - int total_production_income = city->Body.ProductionIncome; - int total_production_loss = city->Body.ProductionLoss; - int total_net_shields = total_production_income + total_production_loss; // ProductionLoss stored as negative + else + return is_army; +} - // Remove the district bonus from the gross tile production and recompute corruption on that base value. - int city_center_with_districts = city_center_base_shields + total_district_shield_bonus; - int gross_without_specials = city->Body.Tiles_Production - (city_center_with_districts - city_center_base_shields); - if (gross_without_specials < 0) - gross_without_specials = 0; +bool __fastcall +patch_Fighter_unit_can_defend (Fighter * this, int edx, Unit * unit, int tile_x, int tile_y) +{ + if (cannot_defend_inside_transport (unit)) + return false; + else + return Fighter_unit_can_defend (this, __, unit, tile_x, tile_y); +} - int base_corruption_abs = patch_City_compute_corrupted_yield (city, __, gross_without_specials, true); - if (base_corruption_abs < 0) - base_corruption_abs = 0; - int base_production_loss = -base_corruption_abs; +bool __fastcall +patch_Leader_is_enemy_unit_for_ground_aa (Leader * this, int edx, Unit * bomber) +{ + // When this function is called, the potential interceptor unit is stored in register ESI. + Unit * interceptor; + __asm__ __volatile__("mov %%esi, %0" : "=r" (interceptor)); + + Unit * container = get_unit_ptr (interceptor->Body.Container_Unit); + bool in_transport = (container != NULL) && ! Unit_has_ability (container, __, UTA_Army); + if (in_transport && + (is->current_config.land_transport_rules & LTR_NO_DEFENSE_FROM_INSIDE) && + p_bic_data->UnitTypes[container->Body.UnitTypeID].Unit_Class == UTC_Land) + return false; + else if (in_transport && + (is->current_config.special_helicopter_rules & SHR_NO_DEFENSE_FROM_INSIDE) && + p_bic_data->UnitTypes[container->Body.UnitTypeID].Unit_Class == UTC_Air) + return false; + else if (in_transport && + is->current_config.no_land_anti_air_from_inside_naval_transport && + (p_bic_data->UnitTypes[container->Body.UnitTypeID].Unit_Class == UTC_Sea)) + return false; + else + return Leader_is_enemy_unit (this, __, bomber); +} + +bool __fastcall +patch_Unit_has_army_ability_for_passenger_despawn (Unit * this, int edx, enum UnitTypeAbilities army_ability) +{ + // If the unit has the army ability, the game will always despawn the passengers. Otherwise there are exceptions like land unit passengers + // won't be despawned if the transport is on a land tile. + return is->always_despawn_passengers || Unit_has_ability (this, __, army_ability); +} - // Corruption becomes more negative as it increases. - int additional_corruption = total_production_loss - base_production_loss; +int __cdecl +patch_count_units_at_in_try_capturing (int x, int y, enum unit_filter filter, int arg_a, int arg_b, int arg_c) +{ + Tile * tile = tile_at (x, y); - int district_shields_remaining = standard_district_shields; - int hub_shields_remaining = distribution_hub_shields; + // If one of the no-defense-from-inside rules is in force, count units like the function normally would except skip units that are inside + // transports from which they can't defend. Otherwise, if those transports have 0 defense, the passengers will prevent them from being + // captured but not fight themselves, making movement onto their tile impossible. + if (tile->vtable->m35_Check_Is_Water (tile) == 0 && + ((is->current_config.land_transport_rules & LTR_NO_DEFENSE_FROM_INSIDE) || (is->current_config.special_helicopter_rules & SHR_NO_DEFENSE_FROM_INSIDE))) { + int tr = 0; + FOR_UNITS_ON (tai, tile) { + if ((arg_b == -1 || p_bic_data->UnitTypes[tai.unit->Body.UnitTypeID].Unit_Class == arg_b) && + (arg_a == -1 || patch_Unit_is_visible_to_civ (tai.unit, __, arg_a, 1)) && + (Unit_get_defense_strength (tai.unit) > 0) && + ! cannot_defend_inside_transport (tai.unit)) + tr++; + } + return tr; - if (additional_corruption < 0) { - int extra_loss = -additional_corruption; + } else + return count_units_at (x, y, filter, arg_a, arg_b, arg_c); +} - if (district_shields_remaining > 0) { - int from_districts = (extra_loss < district_shields_remaining) ? extra_loss : district_shields_remaining; - district_shields_remaining -= from_districts; - extra_loss -= from_districts; - } +void __fastcall +patch_Map_generate_resources (Map * this, int edx, int secondary_seed) +{ + int const bic_tag_good = 0x444f4f47; // = 'GOOD' + int resource_type_count = this->vtable->m35_Get_BIC_Sub_Data (this, __, bic_tag_good, -1, NULL); + bool * ratios_to_clear = NULL; + int lux_percent = is->current_config.luxury_randomized_appearance_rate_percent; + int seed = (this->Seed + 0x180E3) * secondary_seed; - if ((extra_loss > 0) && (hub_shields_remaining > 0)) { - int from_hub = (extra_loss < hub_shields_remaining) ? extra_loss : hub_shields_remaining; - hub_shields_remaining -= from_hub; - extra_loss -= from_hub; + // To change the randomized appearance rate for luxuries, fill in an appearance rate for any that would be randomized (rate set == 0) using + // the same process the game would to randomize the rate but with different limits. That process involves taking two random numbers from 0 to + // 25 then adding them together and to 50 to produce a random rate from 50 to 100 biased toward the middle of that range. + if (lux_percent != 100 && + (ratios_to_clear = calloc (1, resource_type_count)) != NULL) { + for (int n = 0; n < resource_type_count; n++) { + Resource_Type * res; + this->vtable->m35_Get_BIC_Sub_Data (this, __, bic_tag_good, n, (void **)&res); + if (res->Class == RC_Luxury && res->AppearanceRatio == 0) { + ratios_to_clear[n] = true; + int half_lux_percent = (lux_percent * 50 + 50) / 100, + quarter_lux_percent = (lux_percent * 25 + 50) / 100; + res->AppearanceRatio = not_below (1, half_lux_percent + rand_int (&seed, __, quarter_lux_percent) + rand_int (&seed, __, quarter_lux_percent)); + } } } - int non_district_shields_remaining = total_net_shields - district_shields_remaining - hub_shields_remaining; - if (non_district_shields_remaining < 0) - non_district_shields_remaining = 0; - - int total_corruption = -total_production_loss; - if (total_corruption < 0) - total_corruption = 0; - int district_corruption = standard_district_shields - district_shields_remaining; - int hub_corruption = distribution_hub_shields - hub_shields_remaining; - int base_corruption = total_corruption - district_corruption - hub_corruption; - if (base_corruption < 0) - base_corruption = 0; - - is->non_district_shield_icons_remaining = non_district_shields_remaining; - is->district_shield_icons_remaining = district_shields_remaining; - is->distribution_hub_shield_icons_remaining = hub_shields_remaining; + Map_generate_resources (this, __, secondary_seed); - is->corruption_shield_icons_remaining = base_corruption; - is->district_corruption_icons_remaining = district_corruption; - is->distribution_hub_corruption_icons_remaining = hub_corruption; + if (ratios_to_clear != NULL) { + for (int n = 0; n < resource_type_count; n++) + if (ratios_to_clear[n]) { + Resource_Type * res; + this->vtable->m35_Get_BIC_Sub_Data (this, __, bic_tag_good, n, (void **)&res); + res->AppearanceRatio = 0; + } + free (ratios_to_clear); + } } -void __fastcall -patch_City_draw_production_income_icons (City * this, int edx, int canvas, int * rect_ptr) +PassBetweenValidity __fastcall +patch_Unit_can_pass_between (Unit * this, int edx, int from_x, int from_y, int to_x, int to_y, int param_5) { - if (! is->current_config.enable_districts && ! is->current_config.enable_natural_wonders) { - City_draw_production_income_icons (this, __, canvas, rect_ptr); - return; - } + PassBetweenValidity base = Unit_can_pass_between (this, __, from_x, from_y, to_x, to_y, param_5); - recompute_district_and_distribution_hub_shields_for_city_view (this); - City_draw_production_income_icons (this, __, canvas, rect_ptr); + if (great_wall_blocks_civ (tile_at (to_x, to_y), this->Body.CivID)) + return PBV_GENERIC_INVALID_MOVE; - is->corruption_shield_icons_remaining = 0; - is->district_corruption_icons_remaining = 0; - is->distribution_hub_corruption_icons_remaining = 0; + if (is->current_config.enable_districts && is->current_config.workers_can_enter_coast && + base != PBV_OK && is_worker(this)) { + Tile * source = tile_at (from_x, from_y); + if (source != NULL && + source->vtable->m35_Check_Is_Water (source) && + (source->vtable->m50_Get_Square_BaseType (source) == SQ_Coast)) + return PBV_OK; - is->non_district_shield_icons_remaining = 0; - is->district_shield_icons_remaining = 0; - is->distribution_hub_shield_icons_remaining = 0; -} + Tile * dest = tile_at (to_x, to_y); + if ((dest != NULL) && + dest->vtable->m35_Check_Is_Water (dest) && + (dest->vtable->m50_Get_Square_BaseType (dest) == SQ_Coast)) { + bool is_human = (*p_human_player_bits & (1 << this->Body.CivID)) != 0; -int __fastcall -patch_Sprite_draw_production_income_icon (Sprite * this, int edx, PCX_Image * canvas, int pixel_x, int pixel_y, PCX_Color_Table * color_table) -{ - if (is->distribution_hub_icons_img_state == IS_UNINITED) - init_distribution_hub_icons (); - if (is->dc_icons_img_state == IS_UNINITED) - init_district_icons (); + // If human, okay to enter coast tile + if (is_human) + return PBV_OK; - if ((is->current_config.enable_districts || is->current_config.enable_natural_wonders) && is->dc_icons_img_state == IS_OK) { - Sprite to_draw = *this; - if (is->corruption_shield_icons_remaining > 0 || - is->district_corruption_icons_remaining > 0 || - is->distribution_hub_corruption_icons_remaining > 0) { + struct district_worker_record * rec = get_tracked_worker_record (this); + struct pending_district_request * req = (rec != NULL) ? rec->pending_req : NULL; + if (req != NULL) + return PBV_OK; + } + } - if (is->corruption_shield_icons_remaining > 0) { - is->corruption_shield_icons_remaining--; - } else if (is->district_corruption_icons_remaining > 0) { - to_draw = is->district_corruption_icon; - is->district_corruption_icons_remaining--; - } else if (is->distribution_hub_icons_img_state == IS_OK) { - to_draw = is->distribution_hub_corruption_icon; - is->distribution_hub_corruption_icons_remaining--; + if (is->current_config.enable_districts && + is->current_config.enable_bridge_districts && + base != PBV_OK && + (p_bic_data->UnitTypes[this->Body.UnitTypeID].Unit_Class == UTC_Land)) { + Tile * dest = tile_at (to_x, to_y); + if ((dest != NULL) && (dest != p_null_tile)) { + struct district_instance * inst = get_district_instance (dest); + if ((inst != NULL) && + (inst->district_id == BRIDGE_DISTRICT_ID) && + district_is_complete (dest, inst->district_id)) { + return PBV_OK; } } - else if (is->non_district_shield_icons_remaining > 0 || - is->district_shield_icons_remaining > 0 || - is->distribution_hub_shield_icons_remaining > 0) { + } - if (is->non_district_shield_icons_remaining > 0) { - is->non_district_shield_icons_remaining--; - } else if (is->district_shield_icons_remaining > 0) { - to_draw = is->district_shield_icon; - is->district_shield_icons_remaining--; - } else if (is->distribution_hub_icons_img_state == IS_OK) { - to_draw = is->distribution_hub_shield_icon; - is->distribution_hub_shield_icons_remaining--; + if (is->current_config.enable_districts && + is->current_config.enable_canal_districts && + base != PBV_OK && + (p_bic_data->UnitTypes[this->Body.UnitTypeID].Unit_Class == UTC_Sea)) { + Tile * dest = tile_at (to_x, to_y); + if ((dest != NULL) && (dest != p_null_tile)) { + struct district_instance * inst = get_district_instance (dest); + if ((inst != NULL) && + (inst->district_id == CANAL_DISTRICT_ID) && + district_is_complete (dest, inst->district_id)) { + return PBV_OK; } - } - return Sprite_draw (&to_draw, __, canvas, pixel_x, pixel_y, color_table); } - return Sprite_draw (this, __, canvas, pixel_x, pixel_y, color_table); + + return base; } -bool -district_tile_needs_defense (Tile * tile, int tile_x, int tile_y, struct district_instance * inst, int civ_id, int work_radius) +Unit * __fastcall +patch_Unit_select_transport (Unit * this, int edx, int tile_x, int tile_y, bool do_show_popup) { - if ((tile == NULL) || (tile == p_null_tile)) return false; - if (inst == NULL) return false; - - int district_id = inst->district_type; - if (! district_is_complete (tile, district_id)) return false; - if (tile->vtable->m38_Get_Territory_OwnerID (tile) != civ_id) return false; - - // Check if already has enough defenders (2 for aerodromes, distribution hubs, destroyable completed wonders) - int defender_count = count_units_at (tile_x, tile_y, UF_DEFENDER_VIS_TO_A_OF_CLASS_B, civ_id, 0, -1); - int max_defenders = - (district_id == AERODROME_DISTRICT_ID || district_id == DISTRIBUTION_HUB_DISTRICT_ID || - (district_id == WONDER_DISTRICT_ID && is->current_config.completed_wonder_districts_can_be_destroyed)) - ? 2 : 1; - if (defender_count >= max_defenders) - return false; + Unit * transport = Unit_select_transport (this, __, tile_x, tile_y, do_show_popup); - // Distribution hubs always need defense - if (district_id == DISTRIBUTION_HUB_DISTRICT_ID) - return true; + if (is->current_config.enable_districts && + (transport == NULL) && (this == is->coast_walk_unit)) { + Tile * dest = tile_at (tile_x, tile_y); + if (dest != NULL) { + bool allow_move = false; + if (is->current_config.workers_can_enter_coast && is_worker (this) && + dest->vtable->m35_Check_Is_Water (dest) && + (dest->vtable->m50_Get_Square_BaseType (dest) == SQ_Coast)) + allow_move = true; + + if (! allow_move && is->current_config.enable_bridge_districts && + (p_bic_data->UnitTypes[this->Body.UnitTypeID].Unit_Class == UTC_Land)) { + struct district_instance * inst = get_district_instance (dest); + if (inst != NULL && inst->district_id == BRIDGE_DISTRICT_ID && + district_is_complete (dest, inst->district_id)) { + allow_move = true; + } + } - // Wonder districts need defense if under construction or completed (not unused) - if (district_id == WONDER_DISTRICT_ID && is->current_config.completed_wonder_districts_can_be_destroyed) { - enum wonder_district_state state = inst->wonder_info.state; - if (state == WDS_UNDER_CONSTRUCTION || state == WDS_COMPLETED) - return true; - // Unused wonder districts don't need defense - return false; + if (allow_move) { + is->coast_walk_transport_override = true; + return this; // Fake a transport so the move logic proceeds + } + } } - return any_enemies_near (&leaders[civ_id], tile_x, tile_y, -1, work_radius); + return transport; } -int -compute_turns_required_for_path (Unit * unit, int path_length) +// Returns true if the given tile is a water district owned by an enemy of the unit +bool +is_enemy_maritime_district_tile (Unit * unit, Tile * tile) { - if (path_length < 1) - return 0; + if ((unit == NULL) || (tile == NULL) || (tile == p_null_tile)) + return false; - int max_mp = patch_Unit_get_max_move_points (unit); - if (max_mp <= 0) - return 9999; + if (! is->current_config.enable_districts) + return false; - int moves_used_this_turn = max_mp - unit->Body.Moves; - moves_used_this_turn = not_below (0, moves_used_this_turn); - moves_used_this_turn = not_above (moves_used_this_turn, 9999); + if (! tile->vtable->m35_Check_Is_Water (tile)) + return false; - int remaining_mp = path_length - moves_used_this_turn; - if (remaining_mp < 0) - remaining_mp = 0; + struct district_instance * inst = get_district_instance (tile); + if (inst == NULL) + return false; - return (max_mp - 1 + remaining_mp) / max_mp + 1; + int owner = tile->vtable->m38_Get_Territory_OwnerID (tile); + if ((owner <= 0) || (owner == unit->Body.CivID)) + return false; + + Leader * leader = &leaders[unit->Body.CivID]; + return leader->At_War[owner]; } -void -maybe_update_best_district_target (Unit * unit, - int civ_id, - int max_distance, - int unit_x, - int unit_y, - int tile_x, - int tile_y, - int base_score, - int * best_score, - int * best_x, - int * best_y, - int * best_path_length, - int * evaluated_paths, - int max_path_checks) +// Boost pillage desirability for maritime districts +int __fastcall +patch_Unit_ai_eval_pillage_target (Unit * this, int edx, int tile_x, int tile_y) { - if (*evaluated_paths >= max_path_checks) - return; + int base = Unit_ai_eval_pillage_target (this, __, tile_x, tile_y); - int path_length; - int path_result = Trade_Net_set_unit_path (is->trade_net, __, unit_x, unit_y, tile_x, tile_y, - unit, civ_id, 0x81, &path_length); - *evaluated_paths += 1; - if (path_result <= 0) - return; + if (! is->current_config.enable_districts || ! is->current_config.enable_port_districts) + return base; - if (max_distance > 0) { - int turns = compute_turns_required_for_path (unit, path_length); - if (turns > max_distance) - return; + Tile * tile = tile_at (tile_x, tile_y); + if (is_enemy_maritime_district_tile (this, tile)) { + // Double the base score so districts outrank generic coast targets + base += base + 0x300; } - int score = base_score - path_length; - if ((score > *best_score) || ((score == *best_score) && (path_length < *best_path_length))) { - *best_score = score; - *best_x = tile_x; - *best_y = tile_y; - *best_path_length = path_length; - } + return base; } -// Patch seek_colony to actively search for undefended districts -int __fastcall -patch_Unit_seek_colony (Unit * this, int edx, bool for_own_defense, int max_distance) +// Light-weight hunt for nearby friendly ports; returns true if a path/command was issued +bool +try_path_to_friendly_port_district (Unit * unit, bool require_damaged, bool require_undefended) { - // Only intercept if defending own assets and districts are enabled - if (!for_own_defense || - !is->current_config.enable_districts || - !is->current_config.ai_defends_districts) - return Unit_seek_colony (this, __, for_own_defense, max_distance); + if ((unit == NULL) || + ! is->current_config.enable_districts || + ! is->current_config.enable_port_districts) + return false; + + if (unit->Body.Container_Unit >= 0) // Don't redirect cargo + return false; - int civ_id = this->Body.CivID; - int unit_x = this->Body.X; - int unit_y = this->Body.Y; + if (unit->Body.Moves <= 0) + return false; - Tile * current_tile = tile_at (unit_x, unit_y); - if ((current_tile == NULL) || (current_tile == p_null_tile)) - return Unit_seek_colony (this, __, for_own_defense, max_distance); + if (require_damaged && (unit->Body.Damage <= 0)) + return false; - int continent_id = current_tile->vtable->m46_Get_ContinentID (current_tile); + if (p_bic_data->UnitTypes[unit->Body.UnitTypeID].Unit_Class != UTC_Sea) + return false; - const int search_radius = 20; - const int max_path_checks = 64; - const int wonder_base_score = 1000; - const int regular_base_score = 500; + int sea_id = unit->vtable->get_sea_id (unit); + if (sea_id < 0) + return false; - int best_x = -1; - int best_y = -1; - int best_score = INT_MIN; - int best_path_length = INT_MAX; - int evaluated_paths = 0; + int best_x = -1, best_y = -1, best_path_len = INT_MAX; + const int search_radius = 10; + for (int dy = -search_radius; dy <= search_radius; dy++) { + for (int dx = -search_radius; dx <= search_radius; dx++) { + int tx = unit->Body.X + dx, ty = unit->Body.Y + dy; + wrap_tile_coords (&p_bic_data->Map, &tx, &ty); - bool abort_search = false; + Tile * tile = tile_at (tx, ty); + if ((tile == NULL) || (tile == p_null_tile)) + continue; - if ((is->current_config.enable_wonder_districts && is->current_config.completed_wonder_districts_can_be_destroyed) || - is->current_config.enable_distribution_hub_districts) { - for (int dx = -search_radius; (dx <= search_radius) && !abort_search; dx++) { - for (int dy = -search_radius; dy <= search_radius; dy++) { - if (evaluated_paths >= max_path_checks) { - abort_search = true; + if ((short)tile->vtable->m46_Get_ContinentID (tile) != sea_id) + continue; + + if (! tile_has_friendly_port_district (tile, unit->Body.CivID)) + continue; + + int occupier_id = get_tile_occupier_id (tx, ty, -1, true); + if ((occupier_id != -1) && (occupier_id != unit->Body.CivID)) + continue; + + if (require_undefended) { + bool has_friendly_sea_unit = false; + FOR_UNITS_ON (uti, tile) { + Unit * on_tile = uti.unit; + if ((on_tile == NULL) || (on_tile->Body.Container_Unit >= 0)) + continue; + if (on_tile->Body.CivID != unit->Body.CivID) + continue; + if (p_bic_data->UnitTypes[on_tile->Body.UnitTypeID].Unit_Class != UTC_Sea) + continue; + if (on_tile->Body.ID == unit->Body.ID) + continue; + has_friendly_sea_unit = true; break; } - - int target_x = Map_wrap_horiz (&p_bic_data->Map, __, unit_x + dx); - int target_y = Map_wrap_vert (&p_bic_data->Map, __, unit_y + dy); - Tile * district_tile = tile_at (target_x, target_y); - if ((district_tile == NULL) || (district_tile == p_null_tile)) + if (has_friendly_sea_unit) continue; + } - struct district_instance * inst = get_district_instance (district_tile); - if (inst == NULL) - continue; + if (! is_below_stack_limit (tile, unit->Body.CivID, UTC_Sea)) + continue; - int tile_x, tile_y; - if (! district_instance_get_coords (inst, district_tile, &tile_x, &tile_y)) - continue; - if (district_tile->vtable->m46_Get_ContinentID (district_tile) != continent_id) - continue; - if (! district_tile_needs_defense (district_tile, tile_x, tile_y, inst, civ_id, 5)) - continue; + int path_len = 0; + int path_result = patch_Trade_Net_set_unit_path (is->trade_net, __, unit->Body.X, unit->Body.Y, + tx, ty, unit, unit->Body.CivID, 0x81, &path_len); + if (path_result <= 0) + continue; - maybe_update_best_district_target (this, civ_id, max_distance, unit_x, unit_y, - tile_x, tile_y, wonder_base_score, - &best_score, &best_x, &best_y, - &best_path_length, &evaluated_paths, - max_path_checks); + if (path_len < best_path_len) { + best_path_len = path_len; + best_x = tx; + best_y = ty; } } } - if ((best_x < 0) && (evaluated_paths < max_path_checks)) { - for (int dx = -search_radius; (dx <= search_radius) && (evaluated_paths < max_path_checks); dx++) { - for (int dy = -search_radius; dy <= search_radius; dy++) { - if (evaluated_paths >= max_path_checks) - break; + if (unit->Body.X == best_x && unit->Body.Y == best_y) { + Unit_set_escortee (unit, __, -1); + Unit_set_state (unit, __, UnitState_Fortifying); + return true; + } + else if (best_x >= 0) { + patch_Trade_Net_set_unit_path (is->trade_net, __, unit->Body.X, unit->Body.Y, best_x, best_y, + unit, unit->Body.CivID, 0x81, NULL); + Unit_set_escortee (unit, __, -1); + Unit_set_state (unit, __, UnitState_Go_To); + unit->Body.path_dest_x = best_x; + unit->Body.path_dest_y = best_y; + return true; + } - int target_x = Map_wrap_horiz (&p_bic_data->Map, __, unit_x + dx); - int target_y = Map_wrap_vert (&p_bic_data->Map, __, unit_y + dy); - Tile * district_tile = tile_at (target_x, target_y); - if ((district_tile == NULL) || (district_tile == p_null_tile)) - continue; + return false; +} - struct district_instance * inst = get_district_instance (district_tile); - if ((inst == NULL) || (inst->district_type == WONDER_DISTRICT_ID)) - continue; +void __fastcall +patch_Unit_ai_move_naval_power_unit (Unit * this) +{ + if (! is->current_config.enable_districts || + ! is->current_config.enable_port_districts || + ! is->current_config.naval_units_use_port_districts_not_cities) { + Unit_ai_move_naval_power_unit (this); + return; + } - int tile_x, tile_y; - if (! district_instance_get_coords (inst, district_tile, &tile_x, &tile_y)) - continue; - if (district_tile->vtable->m46_Get_ContinentID (district_tile) != continent_id) - continue; - if (! district_tile_needs_defense (district_tile, tile_x, tile_y, inst, civ_id, 5)) - continue; + Tile * here = tile_at (this->Body.X, this->Body.Y); + if (here == NULL) { + Unit_ai_move_naval_power_unit (this); + return; + } - maybe_update_best_district_target (this, civ_id, max_distance, unit_x, unit_y, - tile_x, tile_y, regular_base_score, - &best_score, &best_x, &best_y, - &best_path_length, &evaluated_paths, - max_path_checks); - } + // If we're on a port and the sole unit, fortify + if (is->current_config.ai_defends_districts && + tile_has_friendly_port_district (here, this->Body.CivID) && + count_units_at (this->Body.X, this->Body.Y, UF_0, this->Body.CivID, 0, -1) == 1) { + Unit_set_escortee (this, __, -1); + Unit_set_state (this, __, UnitState_Fortifying); + return; + } + + // If we're already sitting on an enemy maritime district, pillage it immediately + if (this->Body.Container_Unit < 0) { + if (is_enemy_maritime_district_tile (this, here) && + patch_Unit_can_pillage (this, __, this->Body.X, this->Body.Y) && + (patch_Unit_ai_eval_pillage_target (this, __, this->Body.X, this->Body.Y) > 0)) { + patch_Unit_attack_tile (this, __, this->Body.X, this->Body.Y, false); + return; } } - if ((best_x >= 0) && (best_y >= 0)) { - int result = Trade_Net_set_unit_path (is->trade_net, __, unit_x, unit_y, best_x, best_y, - this, civ_id, 0x181, NULL); - return result; + // If damaged and CAN heal at current location (e.g. port district), fortify to heal + if (this->Body.Damage > 0 && patch_Unit_can_heal_at (this, __, this->Body.X, this->Body.Y)) { + Unit_set_escortee (this, __, -1); + Unit_set_state (this, __, UnitState_Fortifying); + return; } - return Unit_seek_colony (this, __, for_own_defense, max_distance); + // If damaged and cannot heal here, try to path to a friendly port district + if ((this->Body.Damage > 0) && try_path_to_friendly_port_district (this, true, false)) + return; + + Unit_ai_move_naval_power_unit (this); + return; } -// Patch has_colony to make units stay on districts, only patches within Unit::ai_move_defensive_unit -bool __fastcall -patch_Tile_has_district_or_colony (Tile * this) +void __fastcall +patch_Unit_ai_move_naval_transport (Unit * this) { - if (is->current_config.enable_districts && - is->current_config.ai_defends_districts) { + if (! is->current_config.enable_districts || + ! is->current_config.enable_port_districts || + ! is->current_config.naval_units_use_port_districts_not_cities) { + Unit_ai_move_naval_transport (this); + return; + } - struct district_instance * inst = get_district_instance (this); - return (inst != NULL) && district_is_complete (this, inst->district_type); + // If damaged and CAN heal at current location (e.g. port district), fortify to heal + if ((this->Body.Damage > 0) && + patch_Unit_can_heal_at (this, __, this->Body.X, this->Body.Y)) { + Unit_set_escortee (this, __, -1); + Unit_set_state (this, __, UnitState_Fortifying); + return; } - // Fallback to original has_colony logic - return Tile_has_colony (this); -} + // If damaged and cannot heal here, try to path to a friendly port district + if ((this->Body.Damage > 0) && try_path_to_friendly_port_district (this, true, false)) + return; -int __fastcall -patch_Buildings_Info_get_age_in_years_for_tourism (Buildings_Info * this, int edx, int building_index) -{ - int base = Buildings_Info_get_age_in_years (this, __, building_index); - if (is->current_config.tourism_time_scale_percent == 100) - return base; - else if (is->current_config.tourism_time_scale_percent <= 0) - return INT_MAX; - else - return (base * 100 + 50) / is->current_config.tourism_time_scale_percent; + Unit_ai_move_naval_transport (this); } -int __fastcall -patch_Sprite_draw_minimap_frame (Sprite * this, int edx, Sprite * alpha, int param_2, PCX_Image * canvas, int x, int y, int param_6) +void __fastcall +patch_Unit_ai_move_naval_missile_transport (Unit * this) { - bool want_larger_minimap = (is->current_config.double_minimap_size == MDM_ALWAYS) || - ((is->current_config.double_minimap_size == MDM_HIGH_DEF) && (p_bic_data->ScreenWidth >= 1920)); - if (want_larger_minimap && (init_large_minimap_frame () == IS_OK)) - return Sprite_draw_for_hud (&is->double_size_box_left_color_pcx, __, &is->double_size_box_left_alpha_pcx, param_2, canvas, x, y, param_6); - else - return Sprite_draw_for_hud (this, __, alpha, param_2, canvas, x, y, param_6); + if (! is->current_config.enable_districts || + ! is->current_config.enable_port_districts || + ! is->current_config.naval_units_use_port_districts_not_cities) { + patch_Unit_ai_move_naval_missile_transport (this); + return; + } + + // If damaged and CAN heal at current location (e.g. port district), fortify to heal + if ((this->Body.Damage > 0) && + patch_Unit_can_heal_at (this, __, this->Body.X, this->Body.Y)) { + Unit_set_escortee (this, __, -1); + Unit_set_state (this, __, UnitState_Fortifying); + return; + } + + // If damaged and cannot heal here, try to path to a friendly port district + if ((this->Body.Damage > 0) && try_path_to_friendly_port_district (this, true, false)) + return; + + Unit_ai_move_naval_missile_transport (this); } -int __fastcall -patch_City_get_turns_to_build_2_for_ai_move_leader (City * this, int edx, City_Order * order, bool param_2) +void __fastcall +patch_Unit_ai_move_air_bombard_unit (Unit * this) { - // Initialize order variable to city's current build. This is not done in the base logic and can cause crashes. - City_Order current_order = { .OrderID = this->Body.Order_ID, .OrderType = this->Body.Order_Type }; - if (is->current_config.patch_crash_in_leader_unit_ai) - order = ¤t_order; + if (! (is->current_config.enable_districts && + is->current_config.enable_aerodrome_districts && + is->current_config.air_units_use_aerodrome_districts_not_cities)) { + Unit_ai_move_air_bombard_unit (this); + return; + } - return City_get_turns_to_build_2 (this, __, order, param_2); + if (this->Body.Damage > 0) { + Unit_set_escortee (this, __, -1); + Unit_set_state (this, __, UnitState_Fortifying); + return; + } + + int best_target = 0; + int target_x = -1, target_y = -1; + int op_range = p_bic_data->UnitTypes[this->Body.UnitTypeID].OperationalRange; + int grid = op_range * 2 + 1; + if (1 < grid * grid) { + for (int n = 1; n < grid * grid; n++) { + int dx, dy; + neighbor_index_to_diff (n, &dx, &dy); + int x = Map_wrap_horiz (&p_bic_data->Map, __, this->Body.X + dx); + int y = Map_wrap_vert (&p_bic_data->Map, __, this->Body.Y + dy); + if (Map_in_range (&p_bic_data->Map, __, x, y)) { + int score = Unit_ai_eval_bombard_target (this, __, x, y, 1); + if (score > best_target) { + best_target = score; + target_x = x; + target_y = y; + } + } + } + } + + int best_base_score = 0x7fffffff; + int base_x = -1, base_y = -1; + FOR_AERODROMES_AROUND (this) { + if (! is_below_stack_limit (aerodrome_tile, this->Body.CivID, + p_bic_data->UnitTypes[this->Body.UnitTypeID].Unit_Class)) + continue; + + int count = count_units_at (aerodrome_x, aerodrome_y, UF_AI_STRAT_A_VIS_TO_B, 6, -1, -1); + int x_dist = Map_get_x_dist (&p_bic_data->Map, __, aerodrome_x, this->Body.X); + int y_dist = Map_get_y_dist (&p_bic_data->Map, __, aerodrome_y, this->Body.Y); + int score = (count * 10) + ((x_dist + y_dist) >> 1); + if ((this->Body.X == aerodrome_x) && (this->Body.Y == aerodrome_y)) + score -= 20; + + if (score < best_base_score) { + best_base_score = score; + base_x = aerodrome_x; + base_y = aerodrome_y; + } + } + + if ((base_x >= 0) && ((this->Body.X != base_x) || (this->Body.Y != base_y))) { + Unit_set_escortee (this, __, -1); + patch_Unit_move (this, __, base_x, base_y); + return; + } + + if ((target_x >= 0) && (target_y >= 0)) { + Unit_set_escortee (this, __, -1); + patch_Unit_bombard_tile (this, __, target_x, target_y); + return; + } + + Unit_set_escortee (this, __, -1); + Unit_set_state (this, __, UnitState_Fortifying); } void __fastcall -patch_City_set_production (City * this, int edx, int order_type, int order_id, bool ask_to_confirm) +patch_Unit_ai_move_air_defense_unit (Unit * this) { - City_set_production (this, __, order_type, order_id, ask_to_confirm); - - if (! is->current_config.enable_districts || ! is->current_config.enable_wonder_districts) + if (! (is->current_config.enable_districts && + is->current_config.enable_aerodrome_districts && + is->current_config.air_units_use_aerodrome_districts_not_cities)) { + Unit_ai_move_air_defense_unit (this); return; + } - // If the human player, we need to set/unset a wonder district for this city, depending - // on what is being built. The human player wouldn't be able to choose a wonder if a wonder - // district wasn't available, so we don't need to worry about feasibility here. - // The AI uses a different mechanism to reserve wonder districts via patch_Leader_do_production_phase. - bool is_human = (*p_human_player_bits & (1 << this->Body.CivID)) != 0; - if (! is_human) + Unit_ai_move_air_defense_unit (this); + return; + + Unit_set_state (this, __, 0); + if (this->Body.Damage > 0) { + Unit_set_escortee (this, __, -1); + Unit_set_state (this, __, UnitState_Fortifying); return; + } - bool release_reservation = true; - if (order_type == COT_Improvement) { - Improvement * improv = &p_bic_data->Improvements[order_id]; - if (improv->Characteristics & (ITC_Wonder | ITC_Small_Wonder)) { - if (reserve_wonder_district_for_city (this)) - release_reservation = false; + int best_base_score = 0x7fffffff; + int base_x = -1, base_y = -1; + FOR_AERODROMES_AROUND (this) { + if (! is_below_stack_limit (aerodrome_tile, this->Body.CivID, + p_bic_data->UnitTypes[this->Body.UnitTypeID].Unit_Class)) + continue; + + int count = count_units_at (aerodrome_x, aerodrome_y, UF_AI_STRAT_A_VIS_TO_B, 7, -1, -1); + int x_dist = Map_get_x_dist (&p_bic_data->Map, __, aerodrome_x, this->Body.X); + int y_dist = Map_get_y_dist (&p_bic_data->Map, __, aerodrome_y, this->Body.Y); + int score = (count * 10) + ((x_dist + y_dist) >> 1); + if ((this->Body.X == aerodrome_x) && (this->Body.Y == aerodrome_y)) + score -= 20; + + if (score < best_base_score) { + best_base_score = score; + base_x = aerodrome_x; + base_y = aerodrome_y; } } - if (release_reservation) - release_wonder_district_reservation (this); + if ((base_x >= 0) && ((this->Body.X != base_x) || (this->Body.Y != base_y))) { + Unit_set_escortee (this, __, -1); + patch_Unit_move (this, __, base_x, base_y); + return; + } + + Unit_set_escortee (this, __, -1); + Unit_set_state (this, __, UnitState_Intercept); } -int __fastcall -patch_Tile_m71_Check_Worker_Job (Tile * this) +void __fastcall +patch_Unit_ai_move_air_transport (Unit * this) { - if (is->current_config.enable_natural_wonders) { - struct district_instance * inst = get_district_instance (this); - if ((inst != NULL) && - (inst->district_type == NATURAL_WONDER_DISTRICT_ID) && - (inst->natural_wonder_info.natural_wonder_id >= 0)) { - return -1; // No worker job allowed on natural wonders + if (! (is->current_config.enable_districts && + is->current_config.enable_aerodrome_districts && + is->current_config.air_units_use_aerodrome_districts_not_cities)) { + Unit_ai_move_air_transport (this); + return; + } + + Unit_ai_move_air_transport (this); + return; + + if (this->Body.Damage < 1) { + if (Unit_can_airdrop (this)) { + int best_score = 0; + int best_x = -1, best_y = -1; + int op_range = p_bic_data->UnitTypes[this->Body.UnitTypeID].OperationalRange; + int grid = op_range * 2 + 1; + if (1 < grid * grid) { + for (int n = 1; n < grid * grid; n++) { + int dx, dy; + neighbor_index_to_diff (n, &dx, &dy); + int x = Map_wrap_horiz (&p_bic_data->Map, __, this->Body.X + dx); + int y = Map_wrap_vert (&p_bic_data->Map, __, this->Body.Y + dy); + if (Map_in_range (&p_bic_data->Map, __, x, y)) { + int score = Unit_ai_eval_airdrop_target (this, __, x, y); + if (score > best_score) { + best_score = score; + best_x = x; + best_y = y; + } + } + } + if ((best_x >= 0) && (best_y >= 0)) { + Unit_set_escortee (this, __, -1); + Unit_airdrop (this, __, best_x, best_y); + return; + } + } + } + + int best_score = -1; + int base_x = -1, base_y = -1; + FOR_AERODROMES_AROUND (this) { + if (! is_below_stack_limit (aerodrome_tile, this->Body.CivID, + p_bic_data->UnitTypes[this->Body.UnitTypeID].Unit_Class)) + continue; + + int score = count_units_at (aerodrome_x, aerodrome_y, UF_AI_STRAT_A_VIS_TO_B, 0, -1, -1) + + count_units_at (aerodrome_x, aerodrome_y, UF_AI_STRAT_A_VIS_TO_B, 1, -1, -1) + 1; + if (count_units_at (aerodrome_x, aerodrome_y, UF_AI_STRAT_A_VIS_TO_B, 9, -1, -1) == 0) + score *= 2; + int cont_id = aerodrome_tile->vtable->m46_Get_ContinentID (aerodrome_tile); + if ((cont_id >= 0) && (cont_id < p_bic_data->Map.Continent_Count) && + (p_bic_data->Map.Continents[cont_id].Body.TileCount > 0x15)) + score *= 2; + + if (score > best_score) { + best_score = score; + base_x = aerodrome_x; + base_y = aerodrome_y; + } + } + + if ((base_x >= 0) && ((this->Body.X != base_x) || (this->Body.Y != base_y))) { + Unit_set_escortee (this, __, -1); + patch_Unit_move (this, __, base_x, base_y); + if (Unit_count_contained_units (this) > 0) { + patch_Unit_disembark_passengers (this, __, this->Body.X, this->Body.Y); + } + return; } } - return Tile_m71_Check_Worker_Job (this); + + if (Unit_count_contained_units (this) > 0) + patch_Unit_disembark_passengers (this, __, this->Body.X, this->Body.Y); + + Unit_set_escortee (this, __, -1); + Unit_set_state (this, __, UnitState_Fortifying); +} + +bool __fastcall +patch_Tile_has_colony_ignore_extraterritorial (Tile * tile) +{ + if (is->current_config.allow_extraterritorial_colonies) + return false; + + return Tile_has_colony (tile); } int __fastcall -patch_Tile_get_road_bonus (Tile * this) +patch_Leader_get_attitude_toward (Leader * this, int edx, int civ_id, int param_2) { - if (is->current_config.enable_natural_wonders) { - struct district_instance * inst = get_district_instance (this); - if ((inst != NULL) && (inst->district_type == NATURAL_WONDER_DISTRICT_ID)) { - return 0; + int score = Leader_get_attitude_toward (this, __, civ_id, param_2); + if (!is->current_config.allow_extraterritorial_colonies) + return score; + + // Note, ideally we'd loop over p_colonies like in vanilla, but it's not clear what the + // structure is of it, so for now just loop over all tiles + + int penalty = is->current_config.per_extraterritorial_colony_relation_penalty; + if (penalty != 0) { + int this_civ_id = this->ID; + Map * map = &p_bic_data->Map; + for (int index = 0; index < map->TileCount; index++) { + int x, y; + tile_index_to_coords (map, index, &x, &y); + Tile * tile = tile_at (x, y); + if ((tile == NULL) || (tile == p_null_tile)) + continue; + if (tile->vtable->m38_Get_Territory_OwnerID (tile) != this_civ_id) + continue; + if (! Tile_has_colony (tile)) + continue; + if (tile->vtable->m70_Get_Tile_Building_OwnerID (tile) != civ_id) + continue; + score -= penalty; } } - return Tile_get_road_bonus (this); + return score; } bool __fastcall -patch_Unit_has_army_ability_to_perform_unload (Unit * this, int edx, enum UnitTypeAbilities a) +patch_Tile_m17_Check_Irrigation (Tile * this, int edx, int visible_to_civ_id) { - return is->current_config.allow_unload_from_army ? false : Unit_has_ability (this, __, a); + bool base = Tile_m17_Check_Irrigation (this, __, visible_to_civ_id); + if (base) + return true; + + if (! is->current_config.enable_districts) + return base; + + struct district_instance * inst = get_district_instance (this); + if (inst == NULL) + return base; + + if (is->district_configs[inst->district_id].allow_irrigation_from && district_is_complete (this, inst->district_id)) + return true; + + return base; } -void __fastcall -patch_Unit_disembark (Unit * this, int edx, int tile_x, int tile_y) +int __fastcall +patch_Unit_ai_eval_bombard_target (Unit * this, int edx, int tile_x, int tile_y, int param_3) { - Unit * container = get_unit_ptr (this->Body.Container_Unit); - bool unloading_from_army = is->current_config.allow_unload_from_army && container != NULL && Unit_has_ability (container, __, UTA_Army); + int score = Unit_ai_eval_bombard_target (this, __, tile_x, tile_y, param_3); - // If removing a unit from an army, transfer a proportional amount of damage to the unit removed - int damage_transferred = 0; - if (unloading_from_army) { - int army_damage_percent = container->Body.Damage * 100 / Unit_get_max_hp (container), - max_damage = Unit_get_max_hp (this) - 1 - this->Body.Damage; - damage_transferred = clamp (0, max_damage, (army_damage_percent * Unit_get_max_hp (this) + 50) / 100); - } + if (! (is->current_config.enable_districts && + is->current_config.enable_great_wall_districts)) + return score; - Unit_disembark (this, __, tile_x, tile_y); + Tile * tile = tile_at (tile_x, tile_y); + if ((tile == NULL) || (tile == p_null_tile)) + return score; - if (unloading_from_army) { - this->Body.Damage = not_above (Unit_get_max_hp (this) - 1, this->Body.Damage + damage_transferred); - container->Body.Damage = not_below (0, container->Body.Damage - damage_transferred); + struct district_instance * inst = get_district_instance (tile); + if ((inst == NULL) || (inst->district_id != GREAT_WALL_DISTRICT_ID) || (! district_is_complete (tile, GREAT_WALL_DISTRICT_ID))) + return score; - if (this->Body.ID == container->Body.army_top_defender_id) { - Unit * new_top_defender = NULL; - FOR_UNITS_ON (uti, tile_at (container->Body.X, container->Body.Y)) - if (uti.unit->Body.Container_Unit == container->Body.ID) { - if (new_top_defender == NULL) - new_top_defender = uti.unit; - else if (Fighter_prefer_first_defender_1 (&p_bic_data->fighter, __, uti.unit, Unit_get_defense_strength (uti.unit), new_top_defender, Unit_get_defense_strength (new_top_defender), true)) - new_top_defender = uti.unit; + int obsolete_id = is->district_infos[GREAT_WALL_DISTRICT_ID].obsoleted_by_id; + if ((obsolete_id >= 0) && Leader_has_tech (&leaders[this->Body.CivID], __, obsolete_id)) + return score; + + int owner_id = tile->vtable->m38_Get_Territory_OwnerID (tile); + if ((owner_id <= 0) || (owner_id == this->Body.CivID)) + return score; + if (! this->vtable->is_enemy_of_civ (this, __, owner_id, false)) + return score; + + bool has_unit_on_tile = false; + bool has_enemy_on_tile = false; + Leader * me = &leaders[this->Body.CivID]; + FOR_UNITS_ON (uti, tile) { + UnitType const * unit_type = &p_bic_data->UnitTypes[uti.unit->Body.UnitTypeID]; + if (patch_Unit_is_visible_to_civ (uti.unit, __, me->ID, 0)) { + has_unit_on_tile = true; + if (me->At_War[uti.unit->Body.CivID]) { + if ((unit_type->Defence > 0) || (unit_type->Attack > 0)) { + has_enemy_on_tile = true; + break; } - container->Body.army_top_defender_id = (new_top_defender != NULL) ? new_top_defender->Body.ID : -1; + } else + break; } } -} -bool __fastcall -patch_Unit_has_ability_no_load_non_army_passengers (Unit * this, int edx, enum UnitTypeAbilities army_ability) -{ - UnitType * transport_type = &p_bic_data->UnitTypes[is->can_load_transport->Body.UnitTypeID], - * passenger_type = &p_bic_data->UnitTypes[this ->Body.UnitTypeID]; + if (has_unit_on_tile && ! has_enemy_on_tile) + return score; - // This call comes from Unit::can_load at the point where it's determined that the passenger (this) has transport capacity > 0 and is checking - // whether it's an army. If not, it can't be loaded. Add two exceptions here for land transports, if configured, one to allow LTs to load into - // naval units and another to allow empty LTs to load into armies. - if (is->current_config.land_transport_rules != 0) - if ((passenger_type->Unit_Class == UTC_Land) && ! Unit_has_ability (this, __, army_ability)) { // if it's a land transport - if ((is->current_config.land_transport_rules & LTR_LOAD_ONTO_BOAT) && (transport_type->Unit_Class == UTC_Sea)) - return true; - if ((is->current_config.land_transport_rules & LTR_JOIN_ARMY) && - Unit_has_ability (is->can_load_transport, __, army_ability) && (Unit_count_contained_units (this) == 0)) - return true; - } + // Boost score to prioritize Great Wall targets + if (score < 0x6000) + score = 0x6000; - // Similarly, allow helicopters to be loaded onto carriers if so configured - if ((is->current_config.special_helicopter_rules & SHR_ALLOW_ON_CARRIERS) && - passenger_type->Unit_Class == UTC_Air && - transport_type->Unit_Class == UTC_Sea && - Unit_has_ability (is->can_load_transport, __, UTA_Transports_Only_Aircraft)) - return true; + // Additional boost if there is no unit on it, more likely to destroy it + if (! has_enemy_on_tile) + score += 0x200; - return Unit_has_ability (this, __, army_ability); + return score; } -bool __fastcall -patch_Unit_has_ability_no_load_transport_into_army (Unit * this, int edx, enum UnitTypeAbilities army_ability) +void __fastcall +patch_Unit_heal_at_start_of_turn (Unit * this) { - // Similar to above, here it checks if the target unit is an army and rejects the load if it is (again, already determined that the passenger - // has transport capacity). Modify this rule to return false for land transports trying to join armies if configured. We don't have to check - // that the LT is empty since that is already disallowed by the modified check above. - bool is_army = Unit_has_ability (this, __, army_ability); - if ((is->current_config.land_transport_rules & LTR_JOIN_ARMY) && is_army && - (p_bic_data->UnitTypes[is->can_load_passenger->Body.UnitTypeID].Unit_Class == UTC_Land)) - return false; + if (is->current_config.enable_districts) { + Tile * tile = tile_at ((this->Body).X, (this->Body).Y); + if ((tile != NULL) && (tile != p_null_tile)) { + struct district_instance * inst = get_district_instance (tile); + if (inst != NULL && district_is_complete (tile, inst->district_id) && + is->district_configs[inst->district_id].heal_units_in_one_turn) { + int territory_owner = tile->vtable->m38_Get_Territory_OwnerID (tile); + if (territory_owner == this->Body.CivID) { + (this->Body).Damage = 0; + return; + } + } + } + } - else - return is_army; + Unit_heal_at_start_of_turn (this); } -bool __fastcall -patch_Fighter_unit_can_defend (Fighter * this, int edx, Unit * unit, int tile_x, int tile_y) +// Makes naval AI treat enemy port districts as valid targets to path toward. +int __cdecl +patch_get_tile_occupier_id_in_Unit_ai_move_naval_power_unit (int x, int y, int pov_civ_id, bool respect_unit_invisibility) { - if (cannot_defend_inside_transport (unit)) - return false; - else - return Fighter_unit_can_defend (this, __, unit, tile_x, tile_y); -} + int base = get_tile_occupier_id (x, y, pov_civ_id, respect_unit_invisibility); + if (base != -1) + return base; -bool __fastcall -patch_Leader_is_enemy_unit_for_ground_aa (Leader * this, int edx, Unit * bomber) -{ - // When this function is called, the potential interceptor unit is stored in register ESI. - Unit * interceptor; - __asm__ __volatile__("mov %%esi, %0" : "=r" (interceptor)); + if (! is->current_config.enable_districts || ! is->current_config.enable_port_districts) + return -1; - Unit * container = get_unit_ptr (interceptor->Body.Container_Unit); - bool in_transport = (container != NULL) && ! Unit_has_ability (container, __, UTA_Army); - if (in_transport && - (is->current_config.land_transport_rules & LTR_NO_DEFENSE_FROM_INSIDE) && - p_bic_data->UnitTypes[container->Body.UnitTypeID].Unit_Class == UTC_Land) - return false; - else if (in_transport && - (is->current_config.special_helicopter_rules & SHR_NO_DEFENSE_FROM_INSIDE) && - p_bic_data->UnitTypes[container->Body.UnitTypeID].Unit_Class == UTC_Air) - return false; - else if (in_transport && - is->current_config.no_land_anti_air_from_inside_naval_transport && - (p_bic_data->UnitTypes[container->Body.UnitTypeID].Unit_Class == UTC_Sea)) - return false; - else - return Leader_is_enemy_unit (this, __, bomber); + Tile * tile = tile_at (x, y); + if (tile == NULL || tile == p_null_tile) + return -1; + + struct district_instance * inst = get_district_instance (tile); + if (inst == NULL) + return -1; + + return (*tile->vtable->m38_Get_Territory_OwnerID) (tile); } -bool __fastcall -patch_Unit_has_army_ability_for_passenger_despawn (Unit * this, int edx, enum UnitTypeAbilities army_ability) +// Returns a non-zero score for enemy port district tiles so they pass the threshold check and are bombard targets +int __fastcall +patch_Fighter_eval_tile_vulnerability_in_Unit_ai_move_naval_power_unit (Fighter * this, int edx, Unit * unit, int tile_x, int tile_y) { - // If the unit has the army ability, the game will always despawn the passengers. Otherwise there are exceptions like land unit passengers - // won't be despawned if the transport is on a land tile. - return is->always_despawn_passengers || Unit_has_ability (this, __, army_ability); + int base = Fighter_eval_tile_vulnerability (this, __, unit, tile_x, tile_y); + if (base > 0) + return base; + + if (! is->current_config.enable_districts || ! is->current_config.enable_port_districts) + return base; + + Tile * tile = tile_at (tile_x, tile_y); + if (tile == NULL || tile == p_null_tile) + return base; + + struct district_instance * inst = get_district_instance (tile); + if (inst == NULL) + return base; + + int owner = (*tile->vtable->m38_Get_Territory_OwnerID) (tile); + if (owner <= 0 || owner == unit->Body.CivID) + return base; + + if (! leaders[unit->Body.CivID].At_War[owner]) + return base; + + // Return a score high enough to pass threshold (iVar13 * 8 + 0x100) + // 0x300 should be sufficient for most cases + return 0x300; } +// Returns the territory owner for enemy port districts so the score isn't reduced and naval units move to enemy ports +// (to subsequently pillage them) int __cdecl -patch_count_units_at_in_try_capturing (int x, int y, enum unit_filter filter, int arg_a, int arg_b, int arg_c) +patch_get_combat_occupier_in_Unit_ai_move_naval_power_unit (int tile_x, int tile_y, int civ_id, byte ignore_visibility) { - Tile * tile = tile_at (x, y); + int base = get_combat_occupier (tile_x, tile_y, civ_id, ignore_visibility); + if (base != -1) + return base; - // If one of the no-defense-from-inside rules is in force, count units like the function normally would except skip units that are inside - // transports from which they can't defend. Otherwise, if those transports have 0 defense, the passengers will prevent them from being - // captured but not fight themselves, making movement onto their tile impossible. - if (tile->vtable->m35_Check_Is_Water (tile) == 0 && - ((is->current_config.land_transport_rules & LTR_NO_DEFENSE_FROM_INSIDE) || (is->current_config.special_helicopter_rules & SHR_NO_DEFENSE_FROM_INSIDE))) { - int tr = 0; - FOR_UNITS_ON (tai, tile) { - if ((arg_b == -1 || p_bic_data->UnitTypes[tai.unit->Body.UnitTypeID].Unit_Class == arg_b) && - (arg_a == -1 || patch_Unit_is_visible_to_civ (tai.unit, __, arg_a, 1)) && - (Unit_get_defense_strength (tai.unit) > 0) && - ! cannot_defend_inside_transport (tai.unit)) - tr++; - } - return tr; + if (! is->current_config.enable_districts || ! is->current_config.enable_port_districts) + return base; - } else - return count_units_at (x, y, filter, arg_a, arg_b, arg_c); -} + Tile * tile = tile_at (tile_x, tile_y); + if (tile == NULL || tile == p_null_tile) + return base; -void __fastcall -patch_Map_generate_resources (Map * this, int edx, int secondary_seed) -{ - int const bic_tag_good = 0x444f4f47; // = 'GOOD' - int resource_type_count = this->vtable->m35_Get_BIC_Sub_Data (this, __, bic_tag_good, -1, NULL); - bool * ratios_to_clear = NULL; - int lux_percent = is->current_config.luxury_randomized_appearance_rate_percent; - int seed = (this->Seed + 0x180E3) * secondary_seed; + struct district_instance * inst = get_district_instance (tile); + if (inst == NULL) + return base; - // To change the randomized appearance rate for luxuries, fill in an appearance rate for any that would be randomized (rate set == 0) using - // the same process the game would to randomize the rate but with different limits. That process involves taking two random numbers from 0 to - // 25 then adding them together and to 50 to produce a random rate from 50 to 100 biased toward the middle of that range. - if (lux_percent != 100 && - (ratios_to_clear = calloc (1, resource_type_count)) != NULL) { - for (int n = 0; n < resource_type_count; n++) { - Resource_Type * res; - this->vtable->m35_Get_BIC_Sub_Data (this, __, bic_tag_good, n, (void **)&res); - if (res->Class == RC_Luxury && res->AppearanceRatio == 0) { - ratios_to_clear[n] = true; - int half_lux_percent = (lux_percent * 50 + 50) / 100, - quarter_lux_percent = (lux_percent * 25 + 50) / 100; - res->AppearanceRatio = not_below (1, half_lux_percent + rand_int (&seed, __, quarter_lux_percent) + rand_int (&seed, __, quarter_lux_percent)); - } - } - } + int owner = (*tile->vtable->m38_Get_Territory_OwnerID) (tile); + if (owner <= 0 || owner == civ_id) + return base; - Map_generate_resources (this, __, secondary_seed); + if (! leaders[civ_id].At_War[owner]) + return base; - if (ratios_to_clear != NULL) { - for (int n = 0; n < resource_type_count; n++) - if (ratios_to_clear[n]) { - Resource_Type * res; - this->vtable->m35_Get_BIC_Sub_Data (this, __, bic_tag_good, n, (void **)&res); - res->AppearanceRatio = 0; - } - free (ratios_to_clear); - } + return owner; } int __fastcall @@ -25421,6 +35100,21 @@ patch_Main_Screen_Form_set_selected_unit (Main_Screen_Form * this, int edx, Unit if (is->current_config.unit_cycle_search_criteria != UCSC_STANDARD) clear_selectable_units_list (this, false); + bool redraw = false; + if (is->current_config.show_ai_city_location_desirability_if_settler) { + int new_perspective = -1; + if (unit != NULL) { + int unit_type_id = unit->Body.UnitTypeID; + int worker_actions = p_bic_data->UnitTypes[unit_type_id].Worker_Actions; + new_perspective = (worker_actions >= 1 && (worker_actions & (UCV_Build_City)) && !is_worker(unit)) ? p_main_screen_form->Player_CivID : -1; + } + + if (new_perspective != is->city_loc_display_perspective) { + is->city_loc_display_perspective = new_perspective; + redraw = true; + } + } + Main_Screen_Form_set_selected_unit (this, __, unit, param_2); // If selecting a new unit, must insert it into the list to ensure it's actually selected. @@ -25428,6 +35122,9 @@ patch_Main_Screen_Form_set_selected_unit (Main_Screen_Form * this, int edx, Unit UnitIDList_insert_before (&this->selectable_units, __, unit->Body.ID, NULL); this->unit_cycle_cursor = this->selectable_units.first; } + + if (redraw && ! this->is_now_loading_game) + p_main_screen_form->vtable->m73_call_m22_Draw ((Base_Form *)p_main_screen_form); } Unit * __fastcall @@ -25513,6 +35210,25 @@ patch_City_m22 (City * this, int edx, bool param_1) } } +Unit * __fastcall +patch_Unit_select_army_member_for_combat (Unit * this, int edx, int param_1, char param_2) +{ + if (is->current_config.patch_empty_army_combat_crash) { + int unit_count = 0; + Tile * tile = tile_at (this->Body.X, this->Body.Y); + if (tile != NULL && tile != p_null_tile) { + FOR_UNITS_ON (uti, tile) { + Unit * unit = uti.unit; + if ((unit != NULL) && (unit->Body.Container_Unit == this->Body.ID)) + unit_count += Unit_count_contained_units (unit) + 1; + } + } + if (unit_count == 0) + return this; + } + + return Unit_select_army_member_for_combat (this, __, param_1, param_2); +} // TCC requires a main function be defined even though it's never used. int main () { return 0; }