diff --git a/C3X.h b/C3X.h index 1e5bcce9..7b0c97d2 100644 --- a/C3X.h +++ b/C3X.h @@ -1,4 +1,3 @@ - #include #define NOVIRTUALKEYCODES // Keycodes defined in Civ3Conquests.h instead @@ -197,6 +196,41 @@ enum perfume_kind { COUNT_PERFUME_KINDS }; +struct unit_counter_group { + char * name; + int * type_ids; + int count_type_ids; +}; + +// Attacker/defender match modes +#define UCM_ANY -1 // * Any unit type +#define UCM_GROUP -2 // Match using the group_name field + +struct counter_rule { + // Attacker side + int attacker_match; // UnitTypeID, or UCM_ANY / UCM_GROUP + char * attacker_group; // Used when attacker_match == UCM_GROUP + + // Defender side + int defender_match; + char * defender_group; + + // Environment conditions (0 / false means no restriction) + unsigned int terrain_mask; // SquareTypes mask, 0 = no restriction + bool only_in_city; + int district_id; // -1 = no restriction + char * district_name; // Resolved after district configs are loaded + unsigned int self_experience_mask; // 0 = no restriction + unsigned int enemy_experience_mask; // 0 = no restriction + bool ignore_terrain; // true = set defender terrain defense to 0 + + // Effects (percent values, 100 = no change) + int self_atk_pct; + int self_def_pct; + int enemy_atk_pct; + int enemy_def_pct; +}; + struct c3x_config { bool enable_stack_bombard; bool enable_disorder_warning; @@ -355,6 +389,12 @@ struct c3x_config { enum no_ai_patrol_override override_no_ai_patrol; enum barbarian_activity_override override_barbarian_activity_level_for_scenario_maps; bool initialize_preplaced_scenario_leaders_as_mgls; + bool enable_unit_counters; + struct unit_counter_group * unit_counter_groups; + int count_unit_counter_groups; + struct counter_rule * counter_rules; + int count_counter_rules; + bool use_civ4_style_best_defender; bool enable_trade_net_x; bool optimize_improvement_loops; @@ -1834,6 +1874,17 @@ struct injected_state { int unit_id, tile_x, tile_y; } unit_display_override; + // Set in patch_Fighter_get_odds_for_main_combat_loop, read by patch_Unit_get_attack/defense_strength. + // Stores counter multipliers for the current combat. Active only during Fighter_get_combat_odds call. + struct { + bool active; + Unit * attacker; + Unit * defender; + int attacker_atk_pct; // Attacker attack multiplier (combines forward self-atk and reverse enemy-atk) + int defender_def_pct; // Defender defense multiplier (combines forward enemy-def and reverse self-def) + bool ignore_terrain; + } counter_combat_ctx; + // Used to extract which unit (if any) exerted zone of control from within Fighter::apply_zone_of_control. Unit * zoc_interceptor; diff --git a/civ_prog_objects.csv b/civ_prog_objects.csv index 9234b28f..d16cb6a8 100644 --- a/civ_prog_objects.csv +++ b/civ_prog_objects.csv @@ -31,7 +31,7 @@ define, 0x499FE0, 0x49F9F0, 0x49A070, "is_online_game", "char (__stdcall * 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)" inlead, 0x5C1410, 0x5CFFA0, 0x5C1120, "Unit_bombard_tile", "void (__fastcall *) (Unit * this, int edx, int x, int y)" -define, 0x5BE820, 0x5CD420, 0x5BE530, "Unit_get_defense_strength", "int (__fastcall *) (Unit * this)" +inlead, 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, 0x5F9F30, 0x5EA610, "Tile_has_colony", "bool (__fastcall *) (Tile * this)" @@ -412,7 +412,7 @@ repl call, 0x4A784E, 0x4AE509, 0x4a78DE, "Tile_check_water_for_sea_zoc", "" define, 0x4A79C2, 0x4AE66D, 0x4A7A52, "ADDR_SKIP_LAND_UNITS_FOR_SEA_ZOC", "byte *" define, 0x4A7CAA, 0x4AE962, 0x4A7D3A, "ADDR_SKIP_SEA_UNITS_FOR_LAND_ZOC", "byte *" repl call, 0x4A7BF2, 0x4AE8AA, 0x4A7C82, "Tile_check_water_for_land_zoc", "" -define, 0x5BE6E0, 0x5CD2C0, 0x5BE3F0, "Unit_get_attack_strength", "int (__fastcall *) (Unit * this)" +inlead, 0x5BE6E0, 0x5CD2C0, 0x5BE3F0, "Unit_get_attack_strength", "int (__fastcall *) (Unit * this)" repl call, 0x4A79CA, 0x4AE675, 0x4A7A5A, "Unit_get_attack_strength_for_sea_zoc", "" repl call, 0x4A7B15, 0x4AE7BE, 0x4A7BA5, "Unit_get_attack_strength_for_sea_zoc", "" repl call, 0x4A7B20, 0x4AE7C9, 0x4A7BB0, "Unit_get_attack_strength_for_sea_zoc", "" @@ -942,7 +942,6 @@ repl call, 0x4D5464, 0x4DDC29, 0x4D5524, "PCX_Image_draw_pedia_unit_stats_2nd_co define, 0x1A5, 0x1A6, 0x1A5, "LBL_OPERATIONAL_RANGE", "int" define, 0x4D519C, 0x4DD953, 0x4D525C, "ADDR_AIR_UNIT_CHECK_TO_DRAW_PEDIA_STATS", "byte *" define, 0x1A8, 0x1A9, 0x1A8, "LBL_BOMBARD_RANGE", "int" - ignore, 0x5FC710, 0x0, 0x0, "PCX_Image_create_and_init_jgl_image", "int (__fastcall *) (PCX_Image * this, int edx, int width, int height, int bit_depth, int param_4, int param_5, int param_6)" ignore, 0x5FCC50, 0x0, 0x0, "PCX_Image_draw_region_to_location", "void (__fastcall *) (PCX_Image * this, int edx, PCX_Image * canvas, int src_x, int src_y, int dest_x, int dest_y, int width, int height)" ignore, 0x600050, 0x0, 0x0, "PCX_Image_fill", "void (__fastcall *) (PCX_Image * this, int edx, int color)" diff --git a/default.c3x_config.ini b/default.c3x_config.ini index 3699a067..aaf06bc8 100644 --- a/default.c3x_config.ini +++ b/default.c3x_config.ini @@ -557,10 +557,6 @@ ai_multi_start_extra_palaces = [] promote_wonder_decorruption_effect = false allow_military_leaders_to_hurry_wonders = false - -; Under the standard rules, a player can't spawn a battle-created unit from combat while they already have one. (The battle-created unit is specified -; in the scenario editor, typically it's a military great leader.) This option removes that rule, allowing players to spawn more MGLs from combat -; victories while they already have one or more on the board. allow_multiple_battle_created_units_per_player = false ; The AI's beaker production will be multiplied by this amount, as a percent. A value of 100 gives you the standard game behavior, a value of 75 @@ -891,7 +887,7 @@ special_capital_decorruption_effect = 10 ; zero: Set NoAIPatrol to 0, allowing AI units to patrol ; The purpose of this option is to enable you to configure NoAIPatrol like a C3X setting instead of globally. In particlar, it allows you to configure ; NoAIPatrol on or off on a per-scenario basis. -override_no_ai_patrol = none +override_no_ai_patrol = one ; Overrides the barbarian activity level when starting a new game for a scenario with a custom map. Normally, in that case, the game will use the ; barbarian settings kept in conquests.ini, ignoring what was set for the map in the editor. This option allows you to set an activity level on a @@ -906,6 +902,81 @@ override_barbarian_activity_level_for_scenario_maps = none ; types at the start of the game so they behave like normal MGLs spawned during a game. initialize_preplaced_scenario_leaders_as_mgls = false +enable_unit_counters = true + +; Civ 4 style best defender selection. +; When this is on (and enable_unit_counters is also true), every time an attack happens the engine picks the +; defender that is HARDEST for the current attacker to beat (the unit with the highest defender win rate +; once unit-counter rules are applied), instead of the vanilla "highest defense strength" rule. The same +; unit is also forced to the top of the enemy stack on the map whenever a player has an attacker selected, +; so the displayed unit stays in sync with what would actually fight. Re-targeting is automatic: switching +; the selected attacker (different counter match-ups) updates which enemy unit is shown as the best defender. +; Notes: +; - Has no effect when enable_unit_counters = false (then there's nothing to differentiate beyond defense). +; - Applies to both human and AI attackers, so the rule is consistent. +; - Does not change ranged-bombard or defensive-bombard target selection, only normal attacks. +use_civ4_style_best_defender = false + +; unit_group allows you gruop your units in a group,please refer to building_prereqs_for_units for the format. +; This function will not work if enable_unit_counters is false. +unit_group = [] +; ── counter_rule format ────────────────────────────────────────────────────────────── +; +; counter_rule = [Friendly vs Enemy Effect... Conditions...] +; +; 【Friendly / Enemy】 +; This can be one of the following three options: +; · The specific unit type, such as "Archer" (must match the name in BIQ exactly; names containing spaces must be enclosed in quotation marks) +; · A unit group name, such as melee (i.e. the group defined in the unit_group section above) +; · "*" represents any unit type (the asterisk must be enclosed in quotation marks) +; +; 【Effect】(Expressed as a percentage; when multiple rules apply to the same battle, their effects are multiplied.) +; +; "self" always refers to the first unit, and "enemy" always refers to the second unit, regardless of who is attacking whom: +; self-atk value — The unit’s attack power becomes N% of its original value +; Example: self-atk 150 = attack power ×1.5; self-atk 50 = attack power ×0.5 +; self-def value — Your defence becomes N% of the original value; +; enemy-atk value — The enemy’s attack becomes N% of the original value; +; enemy-def value — The enemy’s defence becomes N% of the original value; +; +; 【Conditions】(Optional; leaving blank indicates no restrictions; conditions take effect only when all are met simultaneously) +; in-city —— Takes effect only when the enemy is on a city tile +; terrain terrain_name -- Takes effect only when the enemy is on a tile of the specified terrain +; Uses the same lower-case English terrain tokens as districts_config buildable_on, not BIQ/localized names. +; Examples: grassland, hills, coast, snow-forest, snow-mountain, snow-volcano, lake +; ignore-terrain —— Ignores the enemy’s terrain defence bonus (sets their terrain bonus to zero) +; Can be used in conjunction with enemy-def; when used together, ignore-terrain takes precedence +; district district_name -- Only takes effect when the enemy is in a specified district (enable_districts must be enabled) +; District names are resolved after districts_config loads, so dynamic district names are supported. +; self-exp exp_name -- Only takes effect when the self unit has one of the specified combat experiences. +; enemy-exp exp_name -- Only takes effect when the enemy unit has one of the specified combat experiences. +; Accepts one or more scenario experience names, numeric IDs(in standard game, 0 is represents conscript, 1 is represents regular and so on), +; or English aliases: conscript, regular, veteran, elite. +; +; 【Examples】 +; counter_rule = [ranged vs melee self-atk 125] +; → When a ranged unit attacks a melee unit, its attack power is multiplied by 1.25 +; +; counter_rule = ["Knight" vs melee in-city enemy-def 150] +; → When knights attack melee units within a city, the enemy’s defence is multiplied by 1.5 +; +; counter_rule = ["Musketman" vs "Medival Infantry" terrain grassland self-atk 125] +; → When musketeers attack medieval infantry on grassland terrain, their attack power is multiplied by 1.25 +; +; counter_rule = ["Knight" vs "*" ignore-terrain self-atk 150] +; → When a Knight attacks any unit, its attack power is multiplied by 1.5 and it ignores the enemy’s terrain bonus. +; counter_rule = [Archer vs Swordsman self-atk 130 self-def 120] +; → When an archer attacks a swordsman: Archer’s attack power ×130% +; When a swordsman attacks an archer: Archer’s defence ×120% +; counter_rule = ["*" vs "*" self-exp veteran enemy-exp regular self-atk 125] +; -> When a veteran self unit attacks a regular enemy unit, its attack power is multiplied by 1.25. +; counter_rule = ["*" vs "*" self-exp 2 3 enemy-exp 0 1 self-atk 125] +; -> When self has experience ID 2 or 3, and enemy has experience ID 0 or 1, self attack is multiplied by 1.25. +; +; ───────────────────────────────────────────────────────────────────────────── + +counter_rule = ["*" vs "*" self-exp 2 3 enemy-exp 0 1 self-atk 2000] + [==================] [=== AESTHETICS ===] [==================] @@ -953,7 +1024,7 @@ 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 +enable_natural_wonders = true ; If a new scenario is loaded which has no natural wonders defined, add natural wonders. add_natural_wonders_to_scenarios_if_none = false diff --git a/injected_code.c b/injected_code.c index 8d889332..4e356589 100644 --- a/injected_code.c +++ b/injected_code.c @@ -211,6 +211,10 @@ get_city_ptr (int id) return NULL; } +// Forward declarations for unit counter system (defined after their dependencies) +enum recognizable_parse_result parse_unit_counter_group (char ** p_cursor, struct error_line ** p_unrecognized_lines, void * out_group); +enum recognizable_parse_result parse_counter_rule (char ** p_cursor, struct error_line ** p_unrecognized_lines, void * out_rule); + // 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); @@ -756,6 +760,27 @@ reset_to_base_config () cc->great_wall_auto_build_wonder_name = NULL; } + if (cc->unit_counter_groups != NULL) { + for (int n = 0; n < cc->count_unit_counter_groups; n++) { + free (cc->unit_counter_groups[n].name); + free (cc->unit_counter_groups[n].type_ids); + } + free (cc->unit_counter_groups); + cc->unit_counter_groups = NULL; + cc->count_unit_counter_groups = 0; + } + + if (cc->counter_rules != NULL) { + for (int n = 0; n < cc->count_counter_rules; n++) { + free (cc->counter_rules[n].attacker_group); + free (cc->counter_rules[n].defender_group); + free (cc->counter_rules[n].district_name); + } + free (cc->counter_rules); + cc->counter_rules = NULL; + cc->count_counter_rules = 0; + } + // Free unit limits table FOR_TABLE_ENTRIES (tei, &cc->unit_limits) free ((void *)tei.value); @@ -775,6 +800,13 @@ reset_to_base_config () // Overwrite the current config with the base config memcpy (&is->current_config, &is->base_config, sizeof is->current_config); + // These fields are heap-allocated and must not be inherited from base_config + // (base_config never owns valid pointers for them) + is->current_config.unit_counter_groups = NULL; + is->current_config.count_unit_counter_groups = 0; + is->current_config.counter_rules = NULL; + is->current_config.count_counter_rules = 0; + // Recreate loaded config names list with just the base config is->loaded_config_names = malloc (sizeof *is->loaded_config_names); is->loaded_config_names->name = strdup ("(base)"); @@ -1332,6 +1364,7 @@ parse_era_alias_list (char ** p_cursor, struct error_line ** p_unrecognized_line return RPR_PARSE_ERROR; } + enum recognizable_parse_result parse_civ_name_alias_list (char ** p_cursor, struct error_line ** p_unrecognized_lines, void * out_civ_era_alias_list) { @@ -2636,6 +2669,22 @@ load_config (char const * file_path, int path_is_relative_to_mod_dir) cfg->share_visibility_in_hotseat = ival != 0; else handle_config_error (&p, CPE_BAD_BOOL_VALUE); + } else if (slice_matches_str (&p.key, "unit_group")) { + if (0 <= (recog_err_offset = read_recognizables (&value, + &unrecognized_lines, + sizeof (struct unit_counter_group), + parse_unit_counter_group, + (void **)&cfg->unit_counter_groups, + &cfg->count_unit_counter_groups))) + handle_config_error_at (&p, value.str + recog_err_offset, CPE_BAD_VALUE); + } else if (slice_matches_str (&p.key, "counter_rule")) { + if (0 <= (recog_err_offset = read_recognizables (&value, + &unrecognized_lines, + sizeof (struct counter_rule), + parse_counter_rule, + (void **)&cfg->counter_rules, + &cfg->count_counter_rules))) + handle_config_error_at (&p, value.str + recog_err_offset, CPE_BAD_VALUE); } else { handle_config_error (&p, CPE_BAD_KEY); @@ -3834,12 +3883,11 @@ wai_init_cities (int x, int y) (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, _iter_count = 0; \ - _iter_count == 0 && \ + 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); \ - _iter_count++) + aerodrome_x = 0, aerodrome_y = 0) struct tile_rings_iter { int center_x, center_y; @@ -5824,8 +5872,7 @@ 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.max_contiguous_canal_districts <= 0)) && - ((! is->current_config.enable_bridge_districts) || (is->current_config.max_contiguous_bridge_districts <= 0))) + if ((! is->current_config.enable_canal_districts) && (! is->current_config.enable_bridge_districts)) return; Map * map = &p_bic_data->Map; @@ -5838,18 +5885,14 @@ generate_ai_canal_and_bridge_targets () if (block_size <= 0) block_size = 10; - if (is->current_config.enable_bridge_districts && - is->current_config.ai_builds_bridges && - (is->current_config.max_contiguous_bridge_districts > 0)) { + 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); } - if (is->current_config.enable_canal_districts && - is->current_config.ai_builds_canals && - (is->current_config.max_contiguous_canal_districts > 0)) { + if (is->current_config.enable_canal_districts && is->current_config.ai_builds_canals) { generate_ai_canal_candidates_by_block ( map, block_size, @@ -7813,6 +7856,495 @@ find_special_district_index_by_name (char const * name) return -1; } + +// --------------------------------------------------------------- +// Unit counter system +// --------------------------------------------------------------- + +bool +read_counter_rule_terrain_mask (struct string_slice const * terrain_name, unsigned int * out_mask) +{ + if ((terrain_name == NULL) || (out_mask == NULL)) + return false; + + struct string_slice trimmed = trim_string_slice (terrain_name, 1); + if (trimmed.len <= 0) + return false; + + if (slice_matches_str (&trimmed, "lake") || slice_matches_str (&trimmed, "lakes")) { + *out_mask = district_buildable_lake_mask_bit (); + return true; + } + + enum SquareTypes parsed; + if (! read_tile_terrain_type_value (&trimmed, &parsed)) + return false; + + if (parsed == (enum SquareTypes)SQ_INVALID) + *out_mask = all_square_types_mask () | district_buildable_lake_mask_bit (); + else + *out_mask = square_type_mask_bit (parsed); + + return *out_mask != 0; +} + +struct unit_counter_group * +find_unit_counter_group_by_name (struct c3x_config * cfg, char const * name) +{ + for (int i = 0; i < cfg->count_unit_counter_groups; i++) { + struct unit_counter_group * g = &cfg->unit_counter_groups[i]; + if (g->name && strcmp (g->name, name) == 0) + return g; + } + return NULL; +} + +bool +unit_type_in_group (struct unit_counter_group * g, int type_id) +{ + char const * name = p_bic_data->UnitTypes[type_id].Name; + for (int i = 0; i < g->count_type_ids; i++) + if (strcmp (p_bic_data->UnitTypes[g->type_ids[i]].Name, name) == 0) + return true; + return false; +} + +bool +unit_matches_counter_side (struct c3x_config * cfg, int type_id, + int match, char * group_name) +{ + if (match == UCM_ANY) + return true; + if (match == UCM_GROUP) { + struct unit_counter_group * g = + find_unit_counter_group_by_name (cfg, group_name); + return g && unit_type_in_group (g, type_id); + } + // Direct unit type match: compare by name rather than exact ID so that + // AI strategy duplicates (same name, different ID) are also matched. + return strcmp (p_bic_data->UnitTypes[match].Name, + p_bic_data->UnitTypes[type_id].Name) == 0; +} + +bool +slice_matches_str_case_insensitive (struct string_slice const * slice, char const * str) +{ + int str_len = strlen (str); + if (slice->len != str_len) + return false; + for (int i = 0; i < str_len; i++) + if (tolower ((unsigned char)slice->str[i]) != + tolower ((unsigned char)str[i])) + return false; + return true; +} + +bool +read_counter_rule_experience_value (struct string_slice const * exp_name, int * out_id) +{ + if ((exp_name == NULL) || (out_id == NULL)) + return false; + + struct string_slice trimmed = trim_string_slice (exp_name, 1); + if (trimmed.len <= 0) + return false; + + if (slice_matches_str (&trimmed, "*") || + slice_matches_str_case_insensitive (&trimmed, "any")) { + *out_id = -1; + return true; + } + + for (int i = 0; i < p_bic_data->CombatExperienceCount; i++) { + if (slice_matches_str_case_insensitive ( + &trimmed, p_bic_data->CombatExperience[i].Name.S)) { + *out_id = i; + return true; + } + } + + struct { + char const * name; + int rank; + } const default_aliases[] = { + { "conscript", 0 }, + { "regular", 1 }, + { "veteran", 2 }, + { "elite", 3 }, + }; + + for (int i = 0; i < ARRAY_LEN (default_aliases); i++) { + if (slice_matches_str_case_insensitive (&trimmed, default_aliases[i].name)) { + int count = p_bic_data->CombatExperienceCount; + if ((default_aliases[i].rank >= 0) && + (default_aliases[i].rank < count)) { + int * ids = malloc (count * sizeof ids[0]); + if (ids == NULL) + return false; + for (int j = 0; j < count; j++) + ids[j] = j; + for (int j = 0; j < count - 1; j++) { + for (int k = j + 1; k < count; k++) { + int j_id = ids[j], + k_id = ids[k], + j_hp = p_bic_data->CombatExperience[j_id].Base_Hit_Points, + k_hp = p_bic_data->CombatExperience[k_id].Base_Hit_Points; + if ((k_hp < j_hp) || ((k_hp == j_hp) && (k_id < j_id))) { + ids[j] = k_id; + ids[k] = j_id; + } + } + } + *out_id = ids[default_aliases[i].rank]; + free (ids); + return true; + } + } + } + + int id; + if (read_int (&trimmed, &id) && + (id >= 0) && + (id < p_bic_data->CombatExperienceCount)) { + *out_id = id; + return true; + } + + return false; +} + +bool +is_counter_rule_self_experience_token (struct string_slice const * token) +{ + return slice_matches_str (token, "self-exp") || + slice_matches_str (token, "self-experience") || + slice_matches_str (token, "self-combat-exp") || + slice_matches_str (token, "self-combat-experience") || + slice_matches_str (token, "self_combat_experience"); +} + +bool +is_counter_rule_enemy_experience_token (struct string_slice const * token) +{ + return slice_matches_str (token, "enemy-exp") || + slice_matches_str (token, "enemy-experience") || + slice_matches_str (token, "enemy-combat-exp") || + slice_matches_str (token, "enemy-combat-experience") || + slice_matches_str (token, "enemy_combat_experience"); +} + +bool +is_counter_rule_option_token (struct string_slice const * token) +{ + return slice_matches_str (token, "in-city") || + slice_matches_str (token, "ignore-terrain") || + slice_matches_str (token, "self-atk") || + slice_matches_str (token, "self-def") || + slice_matches_str (token, "enemy-atk") || + slice_matches_str (token, "enemy-def") || + slice_matches_str (token, "terrain") || + slice_matches_str (token, "district") || + is_counter_rule_self_experience_token (token) || + is_counter_rule_enemy_experience_token (token); +} + +enum recognizable_parse_result +read_counter_rule_experience_mask (char ** p_cursor, + struct error_line ** p_unrecognized_lines, + unsigned int * out_mask) +{ + char * cur = *p_cursor; + unsigned int mask = 0; + bool got_any_value = false; + bool unrestricted = false; + + while (1) { + char * before = cur; + struct string_slice exp_name; + if (! parse_string (&cur, &exp_name)) + break; + + if (is_counter_rule_option_token (&exp_name)) { + cur = before; + break; + } + + int exp_id; + if (! read_counter_rule_experience_value (&exp_name, &exp_id)) { + add_unrecognized_line (p_unrecognized_lines, &exp_name); + *p_cursor = cur; + return RPR_UNRECOGNIZED; + } + + got_any_value = true; + if (exp_id < 0) { + mask = 0; + unrestricted = true; + } else if (unrestricted) { + ; + } else if (exp_id < 8 * sizeof mask) { + mask |= 1U << exp_id; + } else { + add_unrecognized_line (p_unrecognized_lines, &exp_name); + *p_cursor = cur; + return RPR_UNRECOGNIZED; + } + } + + if (! got_any_value) { + *p_cursor = cur; + return RPR_PARSE_ERROR; + } + + *out_mask = mask; + *p_cursor = cur; + return RPR_OK; +} + +bool +counter_rule_experience_mask_matches (unsigned int mask, int experience_id) +{ + if (mask == 0) + return true; + if ((experience_id < 0) || (experience_id >= 8 * sizeof mask)) + return false; + return (mask & (1U << experience_id)) != 0; +} + +bool +counter_rule_experience_conditions_match (struct counter_rule * r, + int self_experience_id, + int enemy_experience_id) +{ + return counter_rule_experience_mask_matches (r->self_experience_mask, + self_experience_id) && + counter_rule_experience_mask_matches (r->enemy_experience_mask, + enemy_experience_id); +} + +enum recognizable_parse_result +parse_unit_counter_group (char ** p_cursor, + struct error_line ** p_unrecognized_lines, + void * out_group) +{ + char * cur = *p_cursor; + struct string_slice group_name; + if (! (parse_string (&cur, &group_name) && skip_punctuation (&cur, ':'))) + return RPR_PARSE_ERROR; + + struct unit_counter_group * g = out_group; + g->name = extract_slice (&group_name); + g->type_ids = NULL; + g->count_type_ids = 0; + + int any_unrecognized = 0; + struct string_slice type_name; + while (parse_string (&cur, &type_name)) { + // Loop through all unit types with this name, including AI strategy + // duplicates (same name, different ID), which the game creates internally. + int type_id = 0; + bool found_any = false; + while (find_unit_type_id_by_name (&type_name, type_id, &type_id)) { + g->type_ids = realloc (g->type_ids, + (g->count_type_ids + 1) * sizeof (int)); + g->type_ids[g->count_type_ids++] = type_id; + found_any = true; + type_id++; // continue search from next index + } + if (! found_any) { + add_unrecognized_line (p_unrecognized_lines, &type_name); + any_unrecognized = 1; + } + if (! skip_punctuation (&cur, ',')) + break; + } + *p_cursor = cur; + return any_unrecognized ? RPR_UNRECOGNIZED : RPR_OK; +} + +enum recognizable_parse_result +parse_counter_rule (char ** p_cursor, + struct error_line ** p_unrecognized_lines, + void * out_rule) +{ + char * cur = *p_cursor; + struct string_slice attacker_name, vs_token, defender_name; + + if (! parse_string (&cur, &attacker_name)) + return RPR_PARSE_ERROR; + if (! (parse_string (&cur, &vs_token) && + slice_matches_str (&vs_token, "vs"))) + return RPR_PARSE_ERROR; + if (! parse_string (&cur, &defender_name)) + return RPR_PARSE_ERROR; + + struct counter_rule * r = out_rule; + *r = (struct counter_rule) { + .attacker_match = UCM_ANY, + .defender_match = UCM_ANY, + .terrain_mask = 0, + .district_id = -1, + .district_name = NULL, + .self_experience_mask = 0, + .enemy_experience_mask = 0, + .self_atk_pct = 100, + .self_def_pct = 100, + .enemy_atk_pct = 100, + .enemy_def_pct = 100, + }; + + if (! slice_matches_str (&attacker_name, "*")) { + int type_id; + if (find_unit_type_id_by_name (&attacker_name, 0, &type_id)) + r->attacker_match = type_id; + else { + r->attacker_match = UCM_GROUP; + r->attacker_group = extract_slice (&attacker_name); + } + } + + if (! slice_matches_str (&defender_name, "*")) { + int type_id; + if (find_unit_type_id_by_name (&defender_name, 0, &type_id)) + r->defender_match = type_id; + else { + r->defender_match = UCM_GROUP; + r->defender_group = extract_slice (&defender_name); + } + } + + struct string_slice token; + while (parse_string (&cur, &token)) { + if (slice_matches_str (&token, "in-city")) { + r->only_in_city = true; + } else if (slice_matches_str (&token, "ignore-terrain")) { + r->ignore_terrain = true; + } else if (slice_matches_str (&token, "self-atk")) { + if (! parse_int (&cur, &r->self_atk_pct)) + return RPR_PARSE_ERROR; + } else if (slice_matches_str (&token, "self-def")) { + if (! parse_int (&cur, &r->self_def_pct)) + return RPR_PARSE_ERROR; + } else if (slice_matches_str (&token, "enemy-atk")) { + if (! parse_int (&cur, &r->enemy_atk_pct)) + return RPR_PARSE_ERROR; + } else if (slice_matches_str (&token, "enemy-def")) { + if (! parse_int (&cur, &r->enemy_def_pct)) + return RPR_PARSE_ERROR; + } else if (is_counter_rule_self_experience_token (&token)) { + enum recognizable_parse_result res = + read_counter_rule_experience_mask (&cur, + p_unrecognized_lines, + &r->self_experience_mask); + if (res != RPR_OK) + return res; + } else if (is_counter_rule_enemy_experience_token (&token)) { + enum recognizable_parse_result res = + read_counter_rule_experience_mask (&cur, + p_unrecognized_lines, + &r->enemy_experience_mask); + if (res != RPR_OK) + return res; + } else if (slice_matches_str (&token, "terrain")) { + struct string_slice terrain_name; + if (! parse_string (&cur, &terrain_name)) + return RPR_PARSE_ERROR; + if (! read_counter_rule_terrain_mask (&terrain_name, &r->terrain_mask)) { + add_unrecognized_line (p_unrecognized_lines, &terrain_name); + return RPR_UNRECOGNIZED; + } + } else if (slice_matches_str (&token, "district")) { + struct string_slice district_name; + if (! parse_string (&cur, &district_name)) + return RPR_PARSE_ERROR; + free (r->district_name); + r->district_name = extract_slice (&district_name); + r->district_id = -1; + } else { + break; + } + } + + *p_cursor = cur; + return RPR_OK; +} + +void +apply_counter_rules (struct c3x_config * cfg, + Unit * attacker, Unit * defender, Tile * def_tile, + int * out_attacker_atk, int * out_defender_def, + bool * out_ignore_terrain) +{ + int a_type = attacker->Body.UnitTypeID; + int d_type = defender->Body.UnitTypeID; + bool in_city = Tile_has_city (def_tile); + + int aa = 100, dd = 100; + bool ignore = false; + + for (int i = 0; i < cfg->count_counter_rules; i++) { + struct counter_rule * r = &cfg->counter_rules[i]; + + // Check forward match (attacker=rule attacker side, defender=rule defender side) + // Applied fields: self-atk (attacker attack), enemy-def (defender defense) + bool forward = unit_matches_counter_side (cfg, a_type, + r->attacker_match, r->attacker_group) && + unit_matches_counter_side (cfg, d_type, + r->defender_match, r->defender_group); + + // Check reverse match (attacker=rule defender side, defender=rule attacker side) + // Applied fields: self-def (rule attacker side is now defending), enemy-atk (rule defender side is now attacking) + bool reverse = unit_matches_counter_side (cfg, a_type, + r->defender_match, r->defender_group) && + unit_matches_counter_side (cfg, d_type, + r->attacker_match, r->attacker_group); + + if (forward && + ! counter_rule_experience_conditions_match ( + r, + attacker->Body.Combat_Experience, + defender->Body.Combat_Experience)) + forward = false; + + if (reverse && + ! counter_rule_experience_conditions_match ( + r, + defender->Body.Combat_Experience, + attacker->Body.Combat_Experience)) + reverse = false; + + if (! forward && ! reverse) + continue; + + // Environment checks are based on the defender's tile + if (r->only_in_city && ! in_city) + continue; + if (r->terrain_mask != 0 && + ! tile_matches_square_type_mask (def_tile, r->terrain_mask)) + continue; + if (r->district_name != NULL && + ! ((r->district_id != -1) && + cfg->enable_districts && + district_is_complete (def_tile, r->district_id))) + continue; + + if (forward) { + aa = aa * r->self_atk_pct / 100; // self-atk: attacker attack + dd = dd * r->enemy_def_pct / 100; // enemy-def: defender defense + } + if (reverse) { + aa = aa * r->enemy_atk_pct / 100; // enemy-atk: rule defender side now acts as attacker + dd = dd * r->self_def_pct / 100; // self-def: rule attacker side now acts as defender + } + if (forward || reverse) + ignore = ignore || r->ignore_terrain; + } + + *out_attacker_atk = aa; + *out_defender_def = dd; + *out_ignore_terrain = ignore; +} + bool district_is_included_by_final_config (int district_id) { @@ -11275,6 +11807,29 @@ find_district_index_by_name (char const * name) return -1; } +void +resolve_counter_rule_districts (struct error_line ** parse_errors) +{ + struct c3x_config * cfg = &is->current_config; + + for (int i = 0; i < cfg->count_counter_rules; i++) { + struct counter_rule * rule = &cfg->counter_rules[i]; + rule->district_id = -1; + + if ((rule->district_name == NULL) || (rule->district_name[0] == '\0')) + continue; + + int district_id = find_district_index_by_name (rule->district_name); + if (district_id >= 0) { + rule->district_id = district_id; + } else { + struct error_line * err = add_error_line (parse_errors); + snprintf (err->text, sizeof err->text, "^ counter_rule district \"%s\" not found", rule->district_name); + err->text[(sizeof err->text) - 1] = '\0'; + } + } +} + int find_wonder_district_index_by_name (char const * name) { @@ -11662,6 +12217,8 @@ void parse_building_and_tech_ids () resolve_district_bonus_building_entries (&is->district_configs[i].defense_bonus_extras, district_name, "defense_bonus_percent", &district_parse_errors); } + resolve_counter_rule_districts (&district_parse_errors); + // 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') @@ -12923,9 +13480,6 @@ district_line_is_straight (int tile_x, int tile_y, int district_id) bool bridge_district_tile_is_valid (int tile_x, int tile_y) { - if (is->current_config.max_contiguous_bridge_districts <= 0) - return false; - if (! tile_is_coastal_water (tile_x, tile_y)) return false; @@ -12946,27 +13500,29 @@ bridge_district_tile_is_valid (int tile_x, int tile_y) if (! has_adjacent_land_or_bridge) return false; - int max_bridges = is->current_config.max_contiguous_bridge_districts; + 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 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 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 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; + 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); } @@ -12974,15 +13530,14 @@ bridge_district_tile_is_valid (int tile_x, int tile_y) bool canal_district_tile_is_valid (int tile_x, int tile_y) { - if (is->current_config.max_contiguous_canal_districts <= 0) - return false; - if (tile_is_water (tile_x, tile_y)) return false; - 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; + 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; + } Map * map = &p_bic_data->Map; int const adj_dx[8] = { 0, 0, -2, 2, 1, 1, -1, -1 }; @@ -13030,7 +13585,6 @@ can_build_district_on_tile (Tile * tile, int district_id, int civ_id) 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_Canal) && !is->current_config.enable_canal_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; @@ -16282,6 +16836,11 @@ apply_machine_code_edits (struct c3x_config const * cfg, bool at_program_start) // Remove the standard rule that blocks battle-created units while the player already has one set_nopification (cfg->allow_multiple_battle_created_units_per_player, ADDR_EXISTING_BATTLE_CREATED_UNIT_CHECK, 6); + // Bypass air unit check when drawing pedia stats. If it passes, the check will draw the op. range instead of movement in the first column. + // replacing 0x75 (= jnz) with 0xEB (= uncond. jump) + WITH_MEM_PROTECTION (ADDR_AIR_UNIT_CHECK_TO_DRAW_PEDIA_STATS, 1, PAGE_EXECUTE_READWRITE) + *ADDR_AIR_UNIT_CHECK_TO_DRAW_PEDIA_STATS = is->current_config.expand_civilopedia_unit_stats ? 0xEB : 0x75; + // 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) @@ -16768,11 +17327,6 @@ apply_machine_code_edits (struct c3x_config const * cfg, bool at_program_start) // Insert amount added to building decorruption effect just for the capital WITH_MEM_PROTECTION (ADDR_ADD_CAPITAL_CORRUPTION_BUILDING_EFFECT, 3, PAGE_EXECUTE_READWRITE) *(ADDR_ADD_CAPITAL_CORRUPTION_BUILDING_EFFECT + 2) = clamp (0, 100, cfg->special_capital_decorruption_effect); - - // Bypass air unit check when drawing pedia stats. If it passes, the check will draw the op. range instead of movement in the first column. - // replacing 0x75 (= jnz) with 0xEB (= uncond. jump) - WITH_MEM_PROTECTION (ADDR_AIR_UNIT_CHECK_TO_DRAW_PEDIA_STATS, 1, PAGE_EXECUTE_READWRITE) - *ADDR_AIR_UNIT_CHECK_TO_DRAW_PEDIA_STATS = is->current_config.expand_civilopedia_unit_stats ? 0xEB : 0x75; } void @@ -17661,6 +18215,7 @@ patch_init_floating_point () {"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)}, @@ -17804,6 +18359,8 @@ patch_init_floating_point () {"allow_corruption_in_capital" , false, offsetof (struct c3x_config, allow_corruption_in_capital)}, {"allow_sale_of_small_wonders" , false, offsetof (struct c3x_config, allow_sale_of_small_wonders)}, {"initialize_preplaced_scenario_leaders_as_mgls" , false, offsetof (struct c3x_config, initialize_preplaced_scenario_leaders_as_mgls)}, + {"enable_unit_counters" , false, offsetof (struct c3x_config, enable_unit_counters)}, + {"use_civ4_style_best_defender" , false, offsetof (struct c3x_config, use_civ4_style_best_defender)}, }; struct integer_config_option { @@ -18135,8 +18692,6 @@ patch_init_floating_point () is->saved_improv_counts = NULL; is->saved_improv_counts_capacity = 0; - memset (is->pedia_unit_stats_second_column_strs, 0, sizeof is->pedia_unit_stats_second_column_strs); - memset (is->last_main_screen_key_up_events, 0, sizeof is->last_main_screen_key_up_events); reset_district_state (true); @@ -18960,7 +19515,10 @@ init_district_command_buttons () } // For each district type - for (int dc = 0; dc < is->district_count; dc++) { + int district_count = is->district_count; + if (district_count > COUNT_DISTRICT_TYPES) + district_count = COUNT_DISTRICT_TYPES; + for (int dc = 0; dc < 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); @@ -21555,18 +22113,7 @@ patch_Game_get_wonder_city_id (Game * this, int edx, int wonder_improvement_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. - int tr = (leader->Small_Wonders != NULL) ? leader->Small_Wonders[wonder_improvement_id] : -1; - - // If the palace has been set as a small wonder, the Small_Wonders entry for it may not have been updated properly. In that - // case, check the player's CapitalID as well. - City * capital; - if (tr == -1 && - improv->ImprovementFlags & ITF_Center_of_Empire && - (capital = get_city_ptr (leader->CapitalID)) != NULL && - patch_City_has_improvement (capital, __, wonder_improvement_id, false)) - tr = capital->Body.ID; - - return tr; + return (leader->Small_Wonders != NULL) ? leader->Small_Wonders[wonder_improvement_id] : -1; } } return Game_get_wonder_city_id (this, __, wonder_improvement_id); @@ -21728,12 +22275,11 @@ patch_Leader_can_do_worker_job (Leader * this, int edx, enum Worker_Jobs job, in 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) && - this->Body.leader_kind == LK_Military) { + 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 = LK_Military; + this->Body.leader_kind = actual_kind; return tr; } else return Unit_can_hurry_production (this, __, city, exclude_cheap_improvements); @@ -22750,19 +23296,6 @@ patch_Unit_disembark_passengers (Unit * this, int edx, int tile_x, int tile_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); - - bool blocked_by_district = false, blocked_by_district_for_wheeled = false; { - if (is->current_config.enable_districts) { - bool impassible, impassible_to_wheeled; - if (great_wall_blocks_civ (target, this->Body.CivID)) - blocked_by_district = blocked_by_district_for_wheeled = true; - else if (get_tile_district_impassibility (target, &impassible, &impassible_to_wheeled)) { - blocked_by_district = impassible; - blocked_by_district_for_wheeled = impassible || impassible_to_wheeled; - } - } - } - FOR_UNITS_ON (uti, tile) { Unit * escortee = get_unit_ptr (uti.unit->Body.escortee); if ( (escortee != NULL) @@ -22772,10 +23305,7 @@ patch_Unit_disembark_passengers (Unit * this, int edx, int tile_x, int tile_y) || ( 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) - || blocked_by_district - || ( blocked_by_district_for_wheeled - && Unit_has_ability (uti.unit, __, UTA_Wheeled)))) + || Unit_is_terrain_impassable (uti.unit, __, target_terrain))) Unit_set_escortee (uti.unit, __, -1); } } @@ -27508,6 +28038,10 @@ 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; } +// Forward declaration; defined further below near patch_Fighter_get_odds_for_main_combat_loop +// where its dependencies (apply_counter_rules, Fighter_get_combat_odds, counter_combat_ctx) live. +Unit * find_civ4_best_defender_against (Unit * attacker, Tile * tile, int tile_x, int tile_y); + Unit * __fastcall patch_Main_Screen_Form_find_visible_unit (Main_Screen_Form * this, int edx, int tile_x, int tile_y, Unit * excluded) { @@ -27520,6 +28054,25 @@ patch_Main_Screen_Form_find_visible_unit (Main_Screen_Form * this, int edx, int } } + // Civ 4-style 'best defender' display: when a player selects an attacker, the unit at the top of the enemy stack + // should be the one with the lowest win rate against that attacker. + // Only takes effect when both `use_civ4_style_best_defender` and `enable_unit_counters` are enabled, + // and provided the currently selected unit belongs to the local player and there is an enemy unit on the target square. + // + if (is->current_config.use_civ4_style_best_defender && + is->current_config.enable_unit_counters && + (this->Current_Unit != NULL) && + (this->Current_Unit->Body.CivID == this->Player_CivID) && + ((this->Current_Unit->Body.X != tile_x) || (this->Current_Unit->Body.Y != tile_y))) { + Tile * tile = tile_at (tile_x, tile_y); + if ((tile != NULL) && (tile != p_null_tile) && + tile_has_enemy_unit (tile, this->Current_Unit->Body.CivID)) { + Unit * best = find_civ4_best_defender_against (this->Current_Unit, tile, tile_x, tile_y); + if ((best != NULL) && (best != excluded)) + return best; + } + } + return Main_Screen_Form_find_visible_unit (this, __, tile_x, tile_y, excluded); } @@ -27895,23 +28448,293 @@ patch_Fighter_damage_by_db_in_main_loop (Fighter * this, int edx, Unit * bombard 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; - else - return Fighter_get_combat_odds (this, __, attacker, defender, bombarding, ignore_defensive_bonuses); + struct c3x_config * cfg = &is->current_config; + // Only OR in counter-rule terrain skipping when we actually ran apply_counter_rules for this + // call. Otherwise counter_combat_ctx.ignore_terrain can be stale from an earlier probe (e.g. + // find_civ4_best_defender_against) or an earlier combat round — especially risky after + // merges that add new odds call sites. + bool ignore_terrain_for_odds = ignore_defensive_bonuses; + if (cfg->enable_unit_counters && attacker != NULL && defender != NULL) { + Tile * def_tile = tile_at (this->defender_location_x, + this->defender_location_y); + int aa, dd; + bool ignore_terrain; + apply_counter_rules (cfg, attacker, defender, def_tile, + &aa, &dd, &ignore_terrain); + + is->counter_combat_ctx.active = true; + is->counter_combat_ctx.attacker = attacker; + is->counter_combat_ctx.defender = defender; + is->counter_combat_ctx.attacker_atk_pct = aa; + is->counter_combat_ctx.defender_def_pct = dd; + is->counter_combat_ctx.ignore_terrain = ignore_terrain; + ignore_terrain_for_odds = ignore_defensive_bonuses || ignore_terrain; + } + + int result = Fighter_get_combat_odds (this, __, attacker, defender, bombarding, + ignore_terrain_for_odds); + is->counter_combat_ctx.active = false; + return result; +} + +// Civ 4-style best defender: On the specified tile, pick the defender that is hardest for the +// attacker to beat (i.e. the one with the highest *defender* win rate against the attacker). +// Takes unit counter rules into account. Returns NULL if there is no suitable defender. +// +// IMPORTANT — odds semantics: vanilla Fighter_get_combat_odds returns the *defender*'s win +// chance (out of ~1024), not the attacker's. Confirmed by the existing patch at +// patch_Fighter_get_odds_for_main_combat_loop: when the attacker has been killed by defensive +// bombardment ("defender_was_destroyed"), the patch returns 1025 so the still-running combat +// loop sees ~100% defender win chance and finishes off the doomed attacker. Therefore "hardest +// to beat" = "highest defender win chance" = max odds. +// +// Notes: +// - Skips units with Container_Unit >= 0 (units inside armies/transports; only the top unit represents the group). +// - Skips units not visible to the attacker's civ, avoiding revealing invisible units. +// - Skips units of the same civ as the attacker (they cannot defend). +// - Skips units with defense strength <= 0 (incapable of combat). +// - Temporarily overwrites is->counter_combat_ctx internally and saves/restores the context before/after execution, +// to avoid disrupting the actual combat odds context. +Unit * +find_civ4_best_defender_against (Unit * attacker, Tile * tile, int tile_x, int tile_y) +{ + if ((attacker == NULL) || (tile == NULL) || (tile == p_null_tile)) + return NULL; + + // Defensive: validate attacker shape before we deref any of its fields downstream. If the + // pointer is stale or refers to a half-initialized unit, UnitTypeID will be out of range and + // we bail out instead of crashing inside Fighter_get_combat_odds / vtable calls. + int atk_tid = attacker->Body.UnitTypeID; + if ((atk_tid < 0) || (atk_tid >= p_bic_data->UnitTypeCount)) + return NULL; + + struct c3x_config * cfg = &is->current_config; + int attacker_civ = attacker->Body.CivID; + + // Backup the current counter ctx, as we'll be repeatedly overwriting it within the loop. + bool saved_active = is->counter_combat_ctx.active; + Unit * saved_attacker = is->counter_combat_ctx.attacker; + Unit * saved_defender = is->counter_combat_ctx.defender; + int saved_attacker_atk = is->counter_combat_ctx.attacker_atk_pct; + int saved_defender_def = is->counter_combat_ctx.defender_def_pct; + bool saved_ignore_terrain = is->counter_combat_ctx.ignore_terrain; + + // Backup the live Fighter struct fields. Vanilla Fighter_get_combat_odds may consult + // fighter.attacker / fighter.defender / fighter.defender_location_x/y to fetch terrain or + // state; if those still point at units from a previous combat (or are NULL during init), the + // vanilla code can dereference garbage and crash. We point them at the current candidate + // inside the loop and restore the originals at the end. + Unit * saved_fighter_attacker = p_bic_data->fighter.attacker; + Unit * saved_fighter_defender = p_bic_data->fighter.defender; + int saved_fighter_atk_x = p_bic_data->fighter.attacker_location_x; + int saved_fighter_atk_y = p_bic_data->fighter.attacker_location_y; + int saved_fighter_def_x = p_bic_data->fighter.defender_location_x; + int saved_fighter_def_y = p_bic_data->fighter.defender_location_y; + + Unit * best = NULL; + // We track the maximum defender win rate. Start below any possible result so the first + // eligible candidate always becomes the initial best. + int best_odds = -1; + + FOR_UNITS_ON (uti, tile) { + Unit * d = uti.unit; + if ((d == NULL) || (d == attacker)) + continue; + // Defensive: a unit on the tile_units linked list with an out-of-range type id is almost + // certainly stale or in some half-initialized state — skip it entirely so we never pass it + // to anything that dereferences UnitType. + int dtid = d->Body.UnitTypeID; + if ((dtid < 0) || (dtid >= p_bic_data->UnitTypeCount)) + continue; + if (d->Body.Container_Unit >= 0) + continue; + if (d->Body.CivID == attacker_civ) + continue; + // Must be a true enemy of the attacker (allies/peace treaties). + if (! d->vtable->is_enemy_of_civ (d, __, attacker_civ, 0)) + continue; + if (Unit_get_defense_strength (d) <= 0) + continue; + if (! patch_Unit_is_visible_to_civ (d, __, attacker_civ, 0)) + continue; + + // Apply unit counter multipliers (100 if unit counters are disabled, equivalent to vanilla win rate formula). + int aa = 100, dd = 100; + bool ignore_terrain = false; + if (cfg->enable_unit_counters) + apply_counter_rules (cfg, attacker, d, tile, &aa, &dd, &ignore_terrain); + + is->counter_combat_ctx.active = true; + is->counter_combat_ctx.attacker = attacker; + is->counter_combat_ctx.defender = d; + is->counter_combat_ctx.attacker_atk_pct = aa; + is->counter_combat_ctx.defender_def_pct = dd; + is->counter_combat_ctx.ignore_terrain = ignore_terrain; + + // Point the live Fighter struct at the (attacker, d) pair so the vanilla odds function + // reads valid pointers instead of stale ones. + p_bic_data->fighter.attacker = attacker; + p_bic_data->fighter.defender = d; + p_bic_data->fighter.attacker_location_x = attacker->Body.X; + p_bic_data->fighter.attacker_location_y = attacker->Body.Y; + p_bic_data->fighter.defender_location_x = tile_x; + p_bic_data->fighter.defender_location_y = tile_y; + + // IMPORTANT: vanilla Fighter_get_combat_odds reads attack/defense values directly from + // UnitType.Attack and UnitType.Defence — it does NOT route through Unit_get_attack/defense_strength + // in a way that this mod has hooked (Unit_get_defense_strength is not registered in + // civ_prog_objects.csv as an inlead, so patch_Unit_get_defense_strength never gets called from + // inside vanilla odds computation). To make our counter multipliers actually affect the odds we + // compute here, we monkey-patch the UnitType fields for the duration of the call and restore + // them immediately after. This is single-threaded mod code and the call window is microseconds, + // so no other reader can observe the temporary values. + UnitType * a_type = &p_bic_data->UnitTypes[atk_tid]; + UnitType * d_type = &p_bic_data->UnitTypes[dtid]; + int saved_a_attack = a_type->Attack; + int saved_d_defence = d_type->Defence; + if (aa != 100) + a_type->Attack = (saved_a_attack * aa) / 100; + if (dd != 100) + d_type->Defence = (saved_d_defence * dd) / 100; + + int odds = Fighter_get_combat_odds (&p_bic_data->fighter, __, attacker, d, false, ignore_terrain); + + a_type->Attack = saved_a_attack; + d_type->Defence = saved_d_defence; + + // odds = defender's win chance (see comment above the function). We want the candidate + // that is hardest for the attacker to beat, i.e. the one with the *highest* odds. + bool replace; + if (best == NULL) { + replace = true; + } else if (odds > best_odds) { + replace = true; + } else if (odds < best_odds) { + replace = false; + } else { + // Odds tie. Defer to vanilla's own defender comparator (cheaper-cost-defends-first + // etc.) — but we must give it the *effective* defense of each candidate against + // this attacker, otherwise it would compare base defenses (e.g. Swordsman base 2 vs + // Musketman base 4) and pick Musketman immediately, never reaching the cost + // tie-break that should decide between two units that are effectively equally hard + // to beat (Swordsman with a x2 counter has effective def 4, equal to Musketman's + // base 4 — vanilla's tie-break should then prefer the cheaper Swordsman). + // + // We can't rely on counter_combat_ctx flowing through Unit_get_defense_strength + // here (see the long comment above the odds call: the patch isn't actually hooked + // into vanilla code). So we apply the multiplier ourselves on top of whatever + // Unit_get_defense_strength returns for each candidate. + int best_aa = 100, best_dd = 100; + bool best_ignore = false; + if (cfg->enable_unit_counters) + apply_counter_rules (cfg, attacker, best, tile, &best_aa, &best_dd, &best_ignore); + + int d_def = (Unit_get_defense_strength (d) * dd ) / 100; + int best_def = (Unit_get_defense_strength (best) * best_dd) / 100; + + replace = Fighter_prefer_first_defender_1 (&p_bic_data->fighter, __, + d, d_def, best, best_def, true); + } + if (replace) { + best = d; + best_odds = odds; + } + } + + is->counter_combat_ctx.active = saved_active; + is->counter_combat_ctx.attacker = saved_attacker; + is->counter_combat_ctx.defender = saved_defender; + is->counter_combat_ctx.attacker_atk_pct = saved_attacker_atk; + is->counter_combat_ctx.defender_def_pct = saved_defender_def; + is->counter_combat_ctx.ignore_terrain = saved_ignore_terrain; + + // Restore the live Fighter struct so we don't leave it pointing at our probe values when a + // real combat (or other code path that reads it) starts. + p_bic_data->fighter.attacker = saved_fighter_attacker; + p_bic_data->fighter.defender = saved_fighter_defender; + p_bic_data->fighter.attacker_location_x = saved_fighter_atk_x; + p_bic_data->fighter.attacker_location_y = saved_fighter_atk_y; + p_bic_data->fighter.defender_location_x = saved_fighter_def_x; + p_bic_data->fighter.defender_location_y = saved_fighter_def_y; + + return best; } 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); +patch_Fighter_fight (Fighter * this, int edx, Unit * attacker, + int attack_direction, Unit * defender_or_null) +{ + // Civ 4-style best defender: compute the defender from the target tile (neighbor of the + // attacker) using counter-aware odds, then use that unit for combat. + // + // Vanilla usually *pre-resolves* a defender and passes it non-NULL. That selection uses base + // UnitType stats (and cost tie-breaks), not our counter rules — so combat could hit Musketman + // while Main_Screen_Form_find_visible_unit (which always calls find_civ4_best_defender_against) + // showed Swordsman. We therefore apply our pick whenever the attack is a normal 8-neighbor + // strike and either no defender was passed, or the passed defender still sits on the target + // tile (vanilla's stack defender). If vanilla passes a defender on some other tile, leave it + // alone (unusual / non-adjacent paths). + if (is->current_config.use_civ4_style_best_defender && + is->current_config.enable_unit_counters && + (attacker != NULL) && + // Strict guard: attack_direction must be a valid 8-neighbor index. Some non-combat code + // paths call Fighter_fight with sentinel values (-1 etc.); forwarding those into + // neighbor_index_to_diff would produce a junk target tile and crash. + (attack_direction >= 0) && (attack_direction < 8)) { + int dx = 0, dy = 0; + neighbor_index_to_diff (attack_direction, &dx, &dy); + // Belt-and-braces: dx/dy must lie on the 8-neighbor lattice. + if ((dx >= -1) && (dx <= 1) && (dy >= -1) && (dy <= 1) && ((dx != 0) || (dy != 0))) { + int target_x = attacker->Body.X + dx; + int target_y = attacker->Body.Y + dy; + wrap_tile_coords (&p_bic_data->Map, &target_x, &target_y); + Tile * target_tile = tile_at (target_x, target_y); + if ((target_tile != NULL) && (target_tile != p_null_tile)) { + bool vanilla_defender_on_target = + (defender_or_null != NULL) && + (defender_or_null->Body.X == target_x) && + (defender_or_null->Body.Y == target_y); + if ((defender_or_null == NULL) || vanilla_defender_on_target) { + Unit * picked = find_civ4_best_defender_against (attacker, target_tile, target_x, target_y); + if (picked != NULL) + defender_or_null = picked; + } + } + } + } + + byte tr = Fighter_fight (this, __, attacker, attack_direction, + defender_or_null); is->dbe = (struct defensive_bombard_event) {0}; + is->counter_combat_ctx.active = false; return tr; } +int __fastcall +patch_Unit_get_attack_strength (Unit * this) +{ + int base = Unit_get_attack_strength (this); + if (! is->counter_combat_ctx.active) + return base; + if (this == is->counter_combat_ctx.attacker) + return base * is->counter_combat_ctx.attacker_atk_pct / 100; + return base; +} + +int __fastcall +patch_Unit_get_defense_strength (Unit * this) +{ + int base = Unit_get_defense_strength (this); + if (! is->counter_combat_ctx.active) + return base; + if (this == is->counter_combat_ctx.defender) + return base * is->counter_combat_ctx.defender_def_pct / 100; + return base; +} + void __fastcall patch_Unit_score_kill_by_defender (Unit * this, int edx, Unit * victim, bool was_attacking) { @@ -28179,29 +29002,6 @@ patch_Unit_can_disembark_anything (Unit * this, int edx, int tile_x, int tile_y) return false; } - // Apply district restrictions on tile entry - if (base && is->current_config.enable_districts) { - if (great_wall_blocks_civ (target_tile, this->Body.CivID)) - return false; - - bool impassible = false, impassible_to_wheeled = false; - if (get_tile_district_impassibility (target_tile, &impassible, &impassible_to_wheeled)) { - if (impassible) - return false; - - if (impassible_to_wheeled) { - bool any_non_wheeled_passengers = false; - FOR_UNITS_ON (uti, this_tile) - if ((uti.unit->Body.Container_Unit == this->Body.ID) && ! Unit_has_ability (uti.unit, __, UTA_Wheeled)) { - any_non_wheeled_passengers = true; - break; - } - if (! any_non_wheeled_passengers) - 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. @@ -29392,13 +30192,6 @@ patch_Map_place_scenario_things (Map * this) Map_place_scenario_things (this); - if (is->current_config.initialize_preplaced_scenario_leaders_as_mgls && p_units->Units != NULL) - for (int n = 0; n <= p_units->LastIndex; n++) { - Unit * unit = get_unit_ptr (n); - if (unit != NULL && Unit_has_ability (unit, __, UTA_Leader) && unit->Body.leader_kind == 0) - unit->Body.leader_kind = LK_Military; - } - // 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) @@ -29448,10 +30241,7 @@ 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 (); - bool restore_named_tile_menu = is->named_tile_menu_active; - is->named_tile_menu_active = false; Main_Screen_Form_open_quick_build_chooser (this, __, city, mouse_x, mouse_y); - is->named_tile_menu_active = restore_named_tile_menu; } int __fastcall @@ -31972,13 +32762,11 @@ patch_find_nearest_city_for_ai_alliance_eval (int tile_x, int tile_y, int owner_ int __fastcall patch_Unit_get_max_move_points (Unit * this) { - bool is_army = Unit_has_ability (this, __, UTA_Army); - if (is_army && is->current_config.patch_empty_army_movement) { + 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)) { - // Must check 'uni.unit != this' to prevent recursive crash mentioned below - if (uti.unit != this && uti.unit->Body.Container_Unit == this->Body.ID) { + 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); } @@ -31987,15 +32775,6 @@ patch_Unit_get_max_move_points (Unit * this) 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); - - // If the unit is an army and has been set as contained inside itself for a coast walk, the game will crash due to recursive calls computing - // the max MP of each member. In that case, temporarily restore its true container. - } else if (is_army && is->coast_walk_transport_override && this->Body.Container_Unit == this->Body.ID) { - this->Body.Container_Unit = is->coast_walk_prev_container; - int tr = Unit_get_max_move_points (this); - this->Body.Container_Unit = this->Body.ID; - return tr; - } else return Unit_get_max_move_points (this); } @@ -35765,7 +36544,7 @@ try_path_to_friendly_port_district (Unit * unit, bool require_damaged, bool requ continue; } - if (! is_below_stack_limit (tile, unit->Body.CivID, unit->Body.UnitTypeID)) + if (! is_below_stack_limit (tile, unit->Body.CivID, UTC_Sea)) continue; int path_len = 0; @@ -35893,7 +36672,7 @@ patch_Unit_ai_move_naval_missile_transport (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_missile_transport (this); + patch_Unit_ai_move_naval_missile_transport (this); return; } @@ -35958,7 +36737,8 @@ patch_Unit_ai_move_air_bombard_unit (Unit * this) 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, this->Body.UnitTypeID)) + 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); @@ -36013,7 +36793,8 @@ patch_Unit_ai_move_air_defense_unit (Unit * this) 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, this->Body.UnitTypeID)) + 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); @@ -36084,11 +36865,12 @@ patch_Unit_ai_move_air_transport (Unit * this) 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, this->Body.UnitTypeID)) + 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; + 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); @@ -36175,6 +36957,14 @@ patch_Leader_get_attitude_toward (Leader * this, int edx, int civ_id, int param_ return score; } +void __fastcall +patch_UnitIDList_insert_after_init (UnitIDList * this, int edx, int id, UnitIDItem * item) +{ + // If using non-standard unit cycling, avoid calling this method b/c it sometimes causes a crash + if (is->current_config.unit_cycle_search_criteria == UCSC_STANDARD) + UnitIDList_insert_after (this, __, id, item); +} + bool __fastcall patch_Tile_m17_Check_Irrigation (Tile * this, int edx, int visible_to_civ_id) { @@ -36542,14 +37332,6 @@ patch_Main_Screen_Form_find_next_unit_for_cycling (Main_Screen_Form * this) } } -void __fastcall -patch_UnitIDList_insert_after_init (UnitIDList * this, int edx, int id, UnitIDItem * item) -{ - // If using non-standard unit cycling, avoid calling this method b/c it sometimes causes a crash - if (is->current_config.unit_cycle_search_criteria == UCSC_STANDARD) - UnitIDList_insert_after (this, __, id, item); -} - void __fastcall patch_City_m22 (City * this, int edx, bool param_1) { @@ -36714,6 +37496,5 @@ patch_Tile_check_water_for_canal_move_to_adjacent_tile_dest (Tile * this) return this->vtable->m35_Check_Is_Water (this); } - // TCC requires a main function be defined even though it's never used. int main () { return 0; }