diff --git a/C3X.h b/C3X.h index aff4ec2a..4e583453 100644 --- a/C3X.h +++ b/C3X.h @@ -1,4 +1,3 @@ - #include #define NOVIRTUALKEYCODES // Keycodes defined in Civ3Conquests.h instead @@ -208,6 +207,43 @@ 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_defensive_bonuses; // true = defender receives no defensive bonuses + + // Effects (percent values, 100 = no change) + int self_atk_pct; + int self_def_pct; + int enemy_atk_pct; + int enemy_def_pct; + int self_bombard_pct; + int enemy_bombard_pct; +}; + struct c3x_config { bool enable_stack_bombard; bool enable_disorder_warning; @@ -371,6 +407,11 @@ 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 enable_trade_net_x; bool optimize_improvement_loops; @@ -1842,7 +1883,33 @@ struct injected_state { // not on that tile, there is no effect. This is only intended to be used on a temporary basis. struct unit_display_override { int unit_id, tile_x, tile_y; - } unit_display_override; + } unit_display_override, unit_display_override_2; + bool combat_unit_display_override_active; + struct unit_display_override saved_combat_unit_display_override; + struct unit_display_override post_combat_defender_display_override; + int post_combat_defender_display_attacker_id; + bool bombard_target_display_override_active; + struct unit_display_override saved_bombard_target_display_override; + struct unit_display_override saved_bombard_target_display_override_2; + struct unit_display_override current_bombard_target_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_defensive_bonuses; // Counter rule makes the defender receive no defensive bonuses + } counter_combat_ctx; + // Set while Fighter::begin is choosing a defender so Fighter::prefer_first_defender_1 can apply counter-adjusted strengths. + struct counter_defender_selection_context { + bool active; + Unit * attacker; + int tile_x; + int tile_y; + } counter_defender_selection_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 53787e13..1bc156d6 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", "" @@ -420,7 +420,7 @@ repl call, 0x4A7CB2, 0x4AE96A, 0x4A7D42, "Unit_get_attack_strength_for_land_zoc" repl call, 0x4A7DFD, 0x4AEAB3, 0x4A7E8D, "Unit_get_attack_strength_for_land_zoc", "" repl call, 0x4A7E08, 0x4AEABE, 0x4A7E98, "Unit_get_attack_strength_for_land_zoc", "" inlead, 0x4E3E90, 0x4EC6E0, 0x4E3F50, "Main_Screen_Form_find_visible_unit", "Unit * (__fastcall *) (Main_Screen_Form * this, int edx, int tile_x, int tile_y, Unit * excluded)" -define, 0x4F00F0, 0x4F9100, 0x4F01B0, "Animator_play_one_shot_unit_animation", "void (__fastcall *) (Animator * this, int edx, Unit * unit, AnimationType anim_type, bool param_3)" +inlead, 0x4F00F0, 0x4F9100, 0x4F01B0, "Animator_play_one_shot_unit_animation", "void (__fastcall *) (Animator * this, int edx, Unit * unit, AnimationType anim_type, bool param_3)" repl call, 0x4A81A4, 0x4AEE83, 0x4A8234, "Animator_play_zoc_animation", "" repl call, 0x4A81E2, 0x4AEEC1, 0x4A8272, "Animator_play_zoc_animation", "" inlead, 0x4A76B0, 0x4AE370, 0x4A7740, "Fighter_apply_zone_of_control", "void (__fastcall *) (Fighter * this, int edx, Unit * unit, int from_x, int from_y, int to_x, int to_y)" @@ -440,6 +440,10 @@ define, 0x5BF558, 0x5CE0EA, 0x5BF268, "ADDR_EXISTING_BATTLE_CREATED_UNIT_CHEC inlead, 0x4A1AE0, 0x4A86E0, 0x4A1B70, "Fighter_find_defensive_bombarder", "Unit * (__fastcall *) (Fighter * this, int edx, Unit * attacker, Unit * defender)" define, 0x5BCA90, 0x5CB620, 0x5BC7A0, "Unit_get_containing_army", "Unit * (__fastcall *) (Unit * this)" define, 0x4A0ED0, 0x4A7A90, 0x4A0F60, "Fighter_get_combat_odds", "int (__fastcall *) (Fighter * this, int edx, Unit * attacker, Unit * defender, bool bombarding, bool ignore_defensive_bonuses)" +repl call, 0x4A3292, 0x4A9EE2, 0x4A3322, "Fighter_get_odds_for_bombardment", "" +repl call, 0x4A3635, 0x4AA285, 0x4A36C5, "Fighter_get_odds_for_bombardment", "" +repl call, 0x4A3F61, 0x4AAC0D, 0x4A3FF1, "Fighter_get_odds_for_bombardment", "" +repl call, 0x4A7343, 0x4AE003, 0x4A73D3, "Fighter_get_odds_for_bombardment", "" repl call, 0x4A5AF9, 0x4AC799, 0x4A5B89, "Fighter_get_odds_for_main_combat_loop", "" define, 0x4A3280, 0x4A9ED0, 0x4A3310, "Fighter_damage_by_defensive_bombard", "void (__fastcall *) (Fighter * this, int edx, Unit * bombarder, Unit * defender)" repl call, 0x4A57C5, 0x4AC477, 0x4A5855, "Fighter_damage_by_db_in_main_loop", "" @@ -857,7 +861,7 @@ inlead, 0x4E5580, 0x4EDEB0, 0x4E5640, "Main_Screen_Form_draw_city_hud", "voi inlead, 0x4BFF80, 0x4C7580, 0x4C0010, "City_can_build_improvement", "bool (__fastcall *) (City * this, int edx, int i_improv, bool apply_strict_rules)" repl call, 0x5C1C53, 0x5D07F6, 0x5C1963, "Unit_has_army_ability_to_perform_unload", "" inlead, 0x5C59B0, 0x5D4740, 0x5C56C0, "Unit_disembark", "void (__fastcall *) (Unit * this, int edx, int tile_x, int tile_y)" -define, 0x4A12D0, 0x4A7E80, 0x4A1360, "Fighter_prefer_first_defender_1", "bool (__fastcall *) (Fighter * this, int edx, Unit * first, int first_strength, Unit * second, int second_strength, bool param_5)" +inlead, 0x4A12D0, 0x4A7E80, 0x4A1360, "Fighter_prefer_first_defender_1", "bool (__fastcall *) (Fighter * this, int edx, Unit * first, int first_strength, Unit * second, int second_strength, bool param_5)" repl call, 0x5C5C82, 0x5D4A23, 0x5C5992, "Unit_has_ability_no_load_non_army_passengers", "" repl call, 0x5C5C93, 0x5D4A34, 0x5C59A3, "Unit_has_ability_no_load_transport_into_army", "" inlead, 0x4A1590, 0x4A8140, 0x4A1620, "Fighter_unit_can_defend", "bool (__fastcall *) (Fighter * this, int edx, Unit * unit, int tile_x, int tile_y)" diff --git a/default.c3x_config.ini b/default.c3x_config.ini index 9581a80e..56fe6eb8 100644 --- a/default.c3x_config.ini +++ b/default.c3x_config.ini @@ -926,6 +926,77 @@ 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 = false + +; unit_group defines named groups for counter_rule. Separate groups with commas. Within each group, +; separate unit names with spaces, as in building_prereqs_for_units. +; Example: unit_group = [Subs: Submarine "Nuclear Submarine", AntiAir: Flak "Mobile SAM"] +; Counter rules automatically affect normal defender selection, including army member selection, +; and unit-target bombard defender selection when enable_unit_counters is true. +; Unit display uses normal combat counter odds first, then bombard counters if no combat counter applies. +; During bombard actions, the unit actually targeted by bombardment is temporarily displayed on top. +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-bombard value -- The first unit's bombard strength becomes N% when bombarding the second unit +; enemy-bombard value -- The second unit's bombard strength becomes N% when bombarding the first unit +; 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-defensive-bonuses —— Ignores the enemy’s defensive bonuses +; Can be used in conjunction with enemy-def +; 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-defensive-bonuses self-atk 150] +; → When a Knight attacks any unit, its attack power is multiplied by 1.5 and it ignores the enemy’s defensive bonuses. +; 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 = [Catapult vs Spearman self-bombard 150] +; -> When catapults bombard spearmen, their bombard strength is multiplied by 1.5. +; +; ───────────────────────────────────────────────────────────────────────────── + +counter_rule = [] + [==================] [=== AESTHETICS ===] [==================] diff --git a/injected_code.c b/injected_code.c index 4ad52eb4..5d0d5d19 100644 --- a/injected_code.c +++ b/injected_code.c @@ -211,6 +211,14 @@ 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); +Unit * find_counter_best_defender_against (Unit * attacker, Tile * tile, int tile_x, int tile_y, Unit * excluded, bool require_visible, bool * out_any_counter_effect); +Unit * find_counter_base_visible_defender_against (Main_Screen_Form * form, Unit * attacker, int tile_x, int tile_y, Unit * excluded); +bool unit_has_valid_type_id (Unit * unit); +int __cdecl patch_get_building_defense_bonus_at (int x, int y, int param_3); + // 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 +764,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); @@ -787,6 +816,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)"); @@ -1344,6 +1380,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) { @@ -2691,8 +2728,24 @@ load_config (char const * file_path, int path_is_relative_to_mod_dir) } } if (! value_ok) - handle_config_error (&p, CPE_BAD_VALUE); - // END ToC-26 + handle_config_error (&p, CPE_BAD_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); + // if key is for an obsolete option } else if (slice_matches_str (&p.key, "patch_disembark_immobile_bug")) { if (read_int (&value, &ival)) @@ -2744,6 +2797,8 @@ load_config (char const * file_path, int path_is_relative_to_mod_dir) handle_config_error (&p, CPE_BAD_BOOL_VALUE); } else if (slice_matches_str (&p.key, "move_trade_net_object")) { ; // No nothing. This setting no longer serves any purpose. + } else if (slice_matches_str (&p.key, "use_civ4_style_best_defender")) { + ; // Obsolete. Counter rules now always affect normal defender selection when enable_unit_counters is on. // if key was previously misspelled } else if (slice_matches_str (&p.key, "share_visibility_in_hoseat")) { @@ -7965,6 +8020,609 @@ 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_ignore_defensive_bonuses_token (struct string_slice const * token) +{ + return slice_matches_str (token, "ignore-defensive-bonuses") || + slice_matches_str (token, "ignore_defensive_bonuses") || + slice_matches_str (token, "ignore-terrain"); +} + +bool +is_counter_rule_option_token (struct string_slice const * token) +{ + return slice_matches_str (token, "in-city") || + is_counter_rule_ignore_defensive_bonuses_token (token) || + 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, "self-bombard") || + slice_matches_str (token, "enemy-bombard") || + 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; + } + } + *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, + .self_bombard_pct = 100, + .enemy_bombard_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 (is_counter_rule_ignore_defensive_bonuses_token (&token)) { + r->ignore_defensive_bonuses = 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 (slice_matches_str (&token, "self-bombard")) { + if (! parse_int (&cur, &r->self_bombard_pct)) + return RPR_PARSE_ERROR; + } else if (slice_matches_str (&token, "enemy-bombard")) { + if (! parse_int (&cur, &r->enemy_bombard_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; +} + +bool +counter_rule_environment_matches (struct c3x_config * cfg, + struct counter_rule * r, + Tile * target_tile) +{ + if ((target_tile == NULL) || (target_tile == p_null_tile)) + return false; + + if (r->only_in_city && ! Tile_has_city (target_tile)) + return false; + if (r->terrain_mask != 0 && + ! tile_matches_square_type_mask (target_tile, r->terrain_mask)) + return false; + if (r->district_name != NULL && + ! ((r->district_id != -1) && + cfg->enable_districts && + district_is_complete (target_tile, r->district_id))) + return false; + + return true; +} + +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_defensive_bonuses) +{ + int a_type = attacker->Body.UnitTypeID; + int d_type = defender->Body.UnitTypeID; + + int aa = 100, dd = 100; + bool ignore_defensive_bonuses = 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 (! counter_rule_environment_matches (cfg, r, def_tile)) + 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_defensive_bonuses = + ignore_defensive_bonuses || + r->ignore_defensive_bonuses; + } + + *out_attacker_atk = aa; + *out_defender_def = dd; + *out_ignore_defensive_bonuses = ignore_defensive_bonuses; +} + +int +get_counter_rule_bombard_modifier (struct c3x_config * cfg, + Unit * bombarder, Unit * target, + Tile * target_tile) +{ + if (! (cfg->enable_unit_counters && + (bombarder != NULL) && + (target != NULL) && + (bombarder->Body.UnitTypeID >= 0) && + (bombarder->Body.UnitTypeID < p_bic_data->UnitTypeCount) && + (target->Body.UnitTypeID >= 0) && + (target->Body.UnitTypeID < p_bic_data->UnitTypeCount))) + return 100; + + int b_type = bombarder->Body.UnitTypeID; + int t_type = target->Body.UnitTypeID; + int bombard_pct = 100; + + for (int i = 0; i < cfg->count_counter_rules; i++) { + struct counter_rule * r = &cfg->counter_rules[i]; + + bool forward = unit_matches_counter_side (cfg, b_type, + r->attacker_match, r->attacker_group) && + unit_matches_counter_side (cfg, t_type, + r->defender_match, r->defender_group); + + bool reverse = unit_matches_counter_side (cfg, b_type, + r->defender_match, r->defender_group) && + unit_matches_counter_side (cfg, t_type, + r->attacker_match, r->attacker_group); + + if (forward && + ! counter_rule_experience_conditions_match ( + r, + bombarder->Body.Combat_Experience, + target->Body.Combat_Experience)) + forward = false; + + if (reverse && + ! counter_rule_experience_conditions_match ( + r, + target->Body.Combat_Experience, + bombarder->Body.Combat_Experience)) + reverse = false; + + if ((! forward && ! reverse) || + ! counter_rule_environment_matches (cfg, r, target_tile)) + continue; + + if (forward) + bombard_pct = bombard_pct * r->self_bombard_pct / 100; + if (reverse) + bombard_pct = bombard_pct * r->enemy_bombard_pct / 100; + } + + return bombard_pct; +} + +int +counter_adjusted_bombard_strength (int base_strength, int bombard_pct) +{ + long long adjusted = (long long)base_strength * bombard_pct / 100; + if (adjusted < 0) + return 0; + if (adjusted > 0x3FFFFFFF) + return 0x3FFFFFFF; + return (int)adjusted; +} + +int +counter_adjusted_bombard_target_defense (int base_defense_strength, int bombard_pct) +{ + if (bombard_pct <= 0) + return 0x3FFFFFFF; + + long long adjusted = (long long)base_defense_strength * 100 / bombard_pct; + if (adjusted < 0) + return 0; + if (adjusted > 0x3FFFFFFF) + return 0x3FFFFFFF; + return (int)adjusted; +} + bool district_is_included_by_final_config (int district_id) { @@ -11431,6 +12089,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) { @@ -11818,6 +12499,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') @@ -16488,6 +17171,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) @@ -16974,11 +17662,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 @@ -17867,6 +18550,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)}, @@ -17964,7 +18648,6 @@ patch_init_floating_point () {"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)}, @@ -18011,6 +18694,7 @@ 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)}, }; struct integer_config_option { @@ -18300,6 +18984,16 @@ patch_init_floating_point () is->combat_defense_improvs = (struct improv_id_list) {0}; is->unit_display_override = (struct unit_display_override) {-1, -1, -1}; + is->unit_display_override_2 = (struct unit_display_override) {-1, -1, -1}; + is->combat_unit_display_override_active = false; + is->saved_combat_unit_display_override = (struct unit_display_override) {-1, -1, -1}; + is->post_combat_defender_display_override = (struct unit_display_override) {-1, -1, -1}; + is->post_combat_defender_display_attacker_id = -1; + is->bombard_target_display_override_active = false; + is->saved_bombard_target_display_override = (struct unit_display_override) {-1, -1, -1}; + is->saved_bombard_target_display_override_2 = (struct unit_display_override) {-1, -1, -1}; + is->current_bombard_target_display_override = (struct unit_display_override) {-1, -1, -1}; + is->counter_defender_selection_ctx = (struct counter_defender_selection_context) {0}; is->dbe = (struct defensive_bombard_event) {0}; @@ -18783,6 +19477,43 @@ recompute_resources_if_necessary () patch_Trade_Net_recompute_resources (is->trade_net, __, false); } +void +set_bombard_target_display_override (Unit * target) +{ + if ((! is->current_config.enable_unit_counters) || + (is->bombarding_unit == NULL) || + (target == NULL)) + return; + + struct unit_display_override override = { + target->Body.ID, target->Body.X, target->Body.Y + }; + + if (! is->bombard_target_display_override_active) { + is->saved_bombard_target_display_override = is->unit_display_override; + is->saved_bombard_target_display_override_2 = is->unit_display_override_2; + is->bombard_target_display_override_active = true; + } + + is->current_bombard_target_display_override = override; + is->unit_display_override = override; + is->unit_display_override_2 = override; +} + +void +clear_bombard_target_display_override () +{ + if (! is->bombard_target_display_override_active) + return; + + is->unit_display_override = is->saved_bombard_target_display_override; + is->unit_display_override_2 = is->saved_bombard_target_display_override_2; + is->saved_bombard_target_display_override = (struct unit_display_override) {-1, -1, -1}; + is->saved_bombard_target_display_override_2 = (struct unit_display_override) {-1, -1, -1}; + is->current_bombard_target_display_override = (struct unit_display_override) {-1, -1, -1}; + is->bombard_target_display_override_active = false; +} + void __fastcall patch_Unit_bombard_tile (Unit * this, int edx, int x, int y) { @@ -18804,6 +19535,7 @@ patch_Unit_bombard_tile (Unit * this, int edx, int x, int y) is->bombarding_unit = this; record_ai_unit_seen (this, x, y); Unit_bombard_tile (this, __, x, y); + clear_bombard_target_display_override (); is->bombard_stealth_target = NULL; is->bombarding_unit = NULL; @@ -19169,7 +19901,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); @@ -20259,7 +20994,8 @@ issue_stack_unit_mgmt_command (Unit * unit, int command) // 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; { + int available = INT_MAX; + { City * city; int upgrade_id; // ToC-26: also check unit_type_to_group so group-limited upgrade types are caught @@ -20267,8 +21003,8 @@ issue_stack_unit_mgmt_command (Unit * unit, int command) is->current_config.unit_type_to_group.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); + (0 <= (upgrade_id = City_get_upgraded_type_id (city, __, unit_type_id))) && + get_available_unit_count (&leaders[unit->Body.CivID], upgrade_id, &available)) { // ToC-27: If source and target are in the same unit_limit_group with no individual // limit on the target, upgrading is net-zero on the group count (source removed, // target added). Reset available to INT_MAX so the loop below allows all units to @@ -20285,7 +21021,7 @@ issue_stack_unit_mgmt_command (Unit * unit, int command) available = INT_MAX; // same-group upgrade: net-zero group count change } } - } + } // END ToC-26 and ToC-27 int cost = 0; @@ -25008,10 +25744,210 @@ patch_City_add_or_remove_improvement (City * this, int edx, int improv_id, int a } } +void +clear_post_combat_defender_display_override () +{ + struct unit_display_override old_override = + is->post_combat_defender_display_override; + if ((old_override.unit_id >= 0) && + (is->unit_display_override.unit_id == old_override.unit_id) && + (is->unit_display_override.tile_x == old_override.tile_x) && + (is->unit_display_override.tile_y == old_override.tile_y)) + is->unit_display_override = (struct unit_display_override) {-1, -1, -1}; + if ((old_override.unit_id >= 0) && + (is->unit_display_override_2.unit_id == old_override.unit_id) && + (is->unit_display_override_2.tile_x == old_override.tile_x) && + (is->unit_display_override_2.tile_y == old_override.tile_y)) + is->unit_display_override_2 = (struct unit_display_override) {-1, -1, -1}; + + is->post_combat_defender_display_override = + (struct unit_display_override) {-1, -1, -1}; + is->post_combat_defender_display_attacker_id = -1; +} + +void +set_post_combat_defender_display_override (Unit * attacker, int tile_x, + int tile_y, Unit * excluded) +{ + clear_post_combat_defender_display_override (); + + if (! (is->current_config.enable_unit_counters && + (attacker != NULL) && + (attacker->Body.UnitTypeID >= 0) && + (attacker->Body.UnitTypeID < p_bic_data->UnitTypeCount))) + return; + + Tile * tile = tile_at (tile_x, tile_y); + Unit * next_defender = find_counter_base_visible_defender_against ( + p_main_screen_form, attacker, tile_x, tile_y, excluded); + if (next_defender == NULL) + next_defender = find_counter_best_defender_against ( + attacker, tile, tile_x, tile_y, excluded, false, NULL); + if (next_defender == NULL) + return; + + is->post_combat_defender_display_override = + (struct unit_display_override) { + next_defender->Body.ID, + next_defender->Body.X, + next_defender->Body.Y + }; + is->post_combat_defender_display_attacker_id = attacker->Body.ID; +} + +void +refresh_post_combat_defender_display_override_after_despawn (Unit * destroyed_defender) +{ + if (! (is->combat_unit_display_override_active && + (destroyed_defender != NULL) && + (p_bic_data->fighter.attacker != NULL) && + (destroyed_defender->Body.X == p_bic_data->fighter.defender_location_x) && + (destroyed_defender->Body.Y == p_bic_data->fighter.defender_location_y))) + return; + + set_post_combat_defender_display_override ( + p_bic_data->fighter.attacker, + destroyed_defender->Body.X, + destroyed_defender->Body.Y, + destroyed_defender); +} + +void +refresh_post_combat_defender_display_override_after_fight (Fighter * fighter, + int attacker_id) +{ + if (fighter == NULL) + return; + + Unit * attacker = get_unit_ptr (attacker_id); + if (attacker == NULL) + return; + + set_post_combat_defender_display_override ( + attacker, + fighter->defender_location_x, + fighter->defender_location_y, + NULL); +} + +Unit * +get_post_combat_defender_display_override (int tile_x, int tile_y, + Unit * excluded) +{ + struct unit_display_override * override = + &is->post_combat_defender_display_override; + if ((override->unit_id < 0) || + (override->tile_x != tile_x) || + (override->tile_y != tile_y)) + return NULL; + + Unit * unit = get_unit_ptr (override->unit_id); + if ((unit == NULL) || + (unit == excluded) || + (unit->Body.X != tile_x) || + (unit->Body.Y != tile_y) || + ((is->post_combat_defender_display_attacker_id >= 0) && + (get_unit_ptr (is->post_combat_defender_display_attacker_id) == NULL))) { + clear_post_combat_defender_display_override (); + return NULL; + } + + return unit; +} + +bool +get_counter_defender_selection_tile (Unit * attacker, int attack_direction, + Unit * defender, int * out_tile_x, + int * out_tile_y, + bool * out_direction_matches_tile) +{ + if (out_direction_matches_tile != NULL) + *out_direction_matches_tile = false; + + if (! unit_has_valid_type_id (attacker)) + return false; + + int direction_tile_x = 0, + direction_tile_y = 0; + bool have_direction_tile = false; + if ((attack_direction >= 0) && (attack_direction < 8)) { + get_neighbor_coords (&p_bic_data->Map, attacker->Body.X, + attacker->Body.Y, attack_direction, + &direction_tile_x, &direction_tile_y); + Tile * direction_tile = tile_at (direction_tile_x, direction_tile_y); + if ((direction_tile != NULL) && (direction_tile != p_null_tile)) + have_direction_tile = true; + } + + if (defender != NULL) { + *out_tile_x = defender->Body.X; + *out_tile_y = defender->Body.Y; + if (out_direction_matches_tile != NULL) + *out_direction_matches_tile = + have_direction_tile && + (direction_tile_x == defender->Body.X) && + (direction_tile_y == defender->Body.Y); + Tile * defender_tile = tile_at (*out_tile_x, *out_tile_y); + return (defender_tile != NULL) && (defender_tile != p_null_tile); + } + + if (! have_direction_tile) + return false; + + *out_tile_x = direction_tile_x; + *out_tile_y = direction_tile_y; + if (out_direction_matches_tile != NULL) + *out_direction_matches_tile = true; + return true; +} + +bool +counter_defender_selection_can_pass_null_defender (Unit * attacker, + bool direction_matches_tile) +{ + if (! (unit_has_valid_type_id (attacker) && direction_matches_tile)) + return false; + + UnitType * attacker_type = &p_bic_data->UnitTypes[attacker->Body.UnitTypeID]; + if ((attacker_type->Special_Actions & UCV_Stealth_Attack) != 0) + return false; + + return true; +} + void __fastcall patch_Fighter_begin (Fighter * this, int edx, Unit * attacker, int attack_direction, Unit * defender) { - Fighter_begin (this, __, attacker, attack_direction, defender); + struct counter_defender_selection_context saved_selection_ctx = + is->counter_defender_selection_ctx; + Unit * defender_for_begin = defender; + int defender_tile_x = 0, + defender_tile_y = 0; + bool direction_matches_tile = false; + if (is->current_config.enable_unit_counters && + get_counter_defender_selection_tile ( + attacker, attack_direction, defender, + &defender_tile_x, &defender_tile_y, + &direction_matches_tile)) { + is->counter_defender_selection_ctx = + (struct counter_defender_selection_context) { + true, attacker, defender_tile_x, defender_tile_y + }; + if (counter_defender_selection_can_pass_null_defender ( + attacker, direction_matches_tile)) + defender_for_begin = NULL; + } + + Fighter_begin (this, __, attacker, attack_direction, defender_for_begin); + + is->counter_defender_selection_ctx = saved_selection_ctx; + + if (is->combat_unit_display_override_active && + unit_has_valid_type_id (this->defender)) { + is->unit_display_override = (struct unit_display_override) { + this->defender->Body.ID, this->defender->Body.X, this->defender->Body.Y + }; + } // 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 @@ -25076,6 +26012,13 @@ patch_Unit_despawn (Unit * this, int edx, int civ_id_responsible, byte param_2, if (this == is->zoc_defender) is->zoc_defender = NULL; + refresh_post_combat_defender_display_override_after_despawn (this); + + if (this->Body.ID == is->unit_display_override.unit_id) + is->unit_display_override = (struct unit_display_override) {-1, -1, -1}; + if (this->Body.ID == is->unit_display_override_2.unit_id) + is->unit_display_override_2 = (struct unit_display_override) {-1, -1, -1}; + if (this == is->sb_next_up) is->sb_next_up = NULL; @@ -26531,13 +27474,153 @@ patch_Leader_begin_unit_turns (Leader * this) Leader_begin_unit_turns (this); } +bool +bombard_counter_target_is_eligible (Unit * bombarder, Unit * target, + UnitType * bombarder_type, Tile * target_tile, + int bombarder_civ_id, bool require_visible, + int * out_defense_strength) +{ + if ((bombarder == NULL) || + (target == NULL) || + (bombarder_type == NULL) || + (target_tile == NULL) || + (target_tile == p_null_tile) || + (bombarder->Body.UnitTypeID < 0) || + (bombarder->Body.UnitTypeID >= p_bic_data->UnitTypeCount) || + (target->Body.UnitTypeID < 0) || + (target->Body.UnitTypeID >= p_bic_data->UnitTypeCount) || + (target->Body.Container_Unit >= 0) || + (target->Body.CivID == bombarder_civ_id) || + ! target->vtable->is_enemy_of_civ (target, __, bombarder_civ_id, 0) || + (require_visible && + ! patch_Unit_is_visible_to_civ (target, __, bombarder_civ_id, 0)) || + ! can_damage_bombarding (bombarder_type, target, target_tile)) + return false; + + int defense_strength = Unit_get_defense_strength (target); + if (defense_strength <= 0) + return false; + + if (out_defense_strength != NULL) + *out_defense_strength = defense_strength; + return true; +} + +Unit * +find_counter_best_bombard_defender_against (Unit * bombarder, int tile_x, + int tile_y, int bombarder_civ_id, + bool require_visible, + Unit * excluded) +{ + if (! is->current_config.enable_unit_counters || + (is->current_config.count_counter_rules <= 0) || + (bombarder == NULL) || + (bombarder->Body.UnitTypeID < 0) || + (bombarder->Body.UnitTypeID >= p_bic_data->UnitTypeCount)) + return NULL; + + wrap_tile_coords (&p_bic_data->Map, &tile_x, &tile_y); + Tile * target_tile = tile_at (tile_x, tile_y); + if ((target_tile == NULL) || (target_tile == p_null_tile)) + return NULL; + + UnitType * bombarder_type = &p_bic_data->UnitTypes[bombarder->Body.UnitTypeID]; + Unit * best = NULL; + int best_adjusted_defense = -1, + best_base_defense = -1, + best_hp = -1, + best_cost = -1; + bool any_counter_effect = false; + + FOR_UNITS_ON (uti, target_tile) { + Unit * candidate = uti.unit; + int base_defense = 0; + if (candidate == excluded) + continue; + if (! bombard_counter_target_is_eligible ( + bombarder, candidate, bombarder_type, target_tile, + bombarder_civ_id, require_visible, &base_defense)) + continue; + + int bombard_pct = get_counter_rule_bombard_modifier ( + &is->current_config, bombarder, candidate, target_tile); + if (bombard_pct != 100) + any_counter_effect = true; + + int adjusted_defense = + counter_adjusted_bombard_target_defense (base_defense, bombard_pct); + int hp = Unit_get_max_hp (candidate) - candidate->Body.Damage; + int cost = p_bic_data->UnitTypes[candidate->Body.UnitTypeID].Cost; + + if ((best == NULL) || + (adjusted_defense > best_adjusted_defense) || + ((adjusted_defense == best_adjusted_defense) && + (base_defense > best_base_defense)) || + ((adjusted_defense == best_adjusted_defense) && + (base_defense == best_base_defense) && + (hp > best_hp)) || + ((adjusted_defense == best_adjusted_defense) && + (base_defense == best_base_defense) && + (hp == best_hp) && + (cost > best_cost))) { + best = candidate; + best_adjusted_defense = adjusted_defense; + best_base_defense = base_defense; + best_hp = hp; + best_cost = cost; + } + } + + return any_counter_effect ? best : NULL; +} + +Unit * +find_counter_or_base_bombard_defender_against (Unit * bombarder, int tile_x, + int tile_y, + int bombarder_civ_id, + bool require_visible, + Unit * excluded) +{ + Unit * defender = find_counter_best_bombard_defender_against ( + bombarder, tile_x, tile_y, bombarder_civ_id, require_visible, + excluded); + if (defender != NULL) + return defender; + + if (! unit_has_valid_type_id (bombarder)) + return NULL; + + bool land_lethal = + Unit_has_ability (bombarder, __, UTA_Lethal_Land_Bombardment), + sea_lethal = + Unit_has_ability (bombarder, __, UTA_Lethal_Sea_Bombardment); + defender = Fighter_find_defender_against_bombardment ( + &p_bic_data->fighter, __, bombarder, tile_x, tile_y, + bombarder_civ_id, land_lethal, sea_lethal); + + if ((defender == NULL) || + (defender == excluded) || + (require_visible && + ! patch_Unit_is_visible_to_civ (defender, __, bombarder_civ_id, 0))) + return NULL; + + return defender; +} + 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->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; + Unit * defender = is->bombard_stealth_target; + + if (defender == NULL) + defender = find_counter_best_bombard_defender_against ( + bombarder, tile_x, tile_y, bombarder_civ_id, true, NULL); + + if (defender == NULL) + defender = Fighter_find_defender_against_bombardment (this, __, bombarder, tile_x, tile_y, bombarder_civ_id, land_lethal, sea_lethal); + + set_bombard_target_display_override (defender); + return defender; } Unit * @@ -26545,7 +27628,10 @@ 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); + Unit * defender = find_counter_best_bombard_defender_against ( + unit, tile_x, tile_y, unit->Body.CivID, true, NULL); + if (defender == NULL) + 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; @@ -26589,13 +27675,13 @@ patch_Unit_play_bombing_anim_for_precision_strike (Unit * this, int edx, int x, int __fastcall patch_Unit_play_anim_for_bombard_tile (Unit * this, int edx, int x, 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); + set_bombard_target_display_override (is->bombard_stealth_target); return Unit_play_bombard_fire_animation (this, __, x, y); } @@ -27806,19 +28892,138 @@ 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; } +Unit * find_counter_best_visible_defender_against (Unit * attacker, Tile * tile, int tile_x, int tile_y, Unit * excluded); +Unit * find_counter_best_visible_defender_against_with_effect (Unit * attacker, Tile * tile, int tile_x, int tile_y, Unit * excluded, bool * out_any_counter_effect); + +Unit * +find_counter_base_visible_defender_against (Main_Screen_Form * form, + Unit * attacker, + int tile_x, + int tile_y, + Unit * excluded) +{ + if (! (is->current_config.enable_unit_counters && + (form != NULL) && + unit_has_valid_type_id (attacker))) + return NULL; + + Tile * tile = tile_at (tile_x, tile_y); + if ((tile == NULL) || + (tile == p_null_tile) || + ! tile_has_enemy_unit (tile, attacker->Body.CivID)) + return NULL; + + struct counter_defender_selection_context saved_selection_ctx = + is->counter_defender_selection_ctx; + is->counter_defender_selection_ctx = + (struct counter_defender_selection_context) { + true, attacker, tile_x, tile_y + }; + + Unit * result = Main_Screen_Form_find_visible_unit ( + form, __, tile_x, tile_y, excluded); + + is->counter_defender_selection_ctx = saved_selection_ctx; + + if ((result == NULL) || + (result == excluded) || + ! unit_has_valid_type_id (result) || + (result->Body.CivID == attacker->Body.CivID) || + ! result->vtable->is_enemy_of_civ (result, __, attacker->Body.CivID, 0)) + return NULL; + + return result; +} + +bool +main_screen_form_is_bombard_display_mode (Main_Screen_Form * form) +{ + if (form == NULL) + return false; + + if ((form->Mode_Action == UMA_Bombard) || + (form->Mode_Action == UMA_Air_Bombard) || + (form->Mode_Action == UMA_Auto_Bombard) || + (form->Mode_Action == UMA_Auto_Air_Bombard)) + return true; + + Command_Button * buttons = form->GUI.Unit_Command_Buttons; + for (int n = 0; n < ARRAY_LEN (form->GUI.Unit_Command_Buttons); n++) { + int command = buttons[n].Command; + if (((buttons[n].Button.Base_Data.Status2 & 1) != 0) && + ((command == UCV_Bombard) || + (command == UCV_Bombing) || + (command == UCV_Auto_Bombard) || + (command == UCV_Auto_Air_Bombard))) + return true; + } + + return false; +} + 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; + bool unit_counter_display_enabled = + 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)); + bool unit_counter_bombard_display_mode = + main_screen_form_is_bombard_display_mode (this); + + struct unit_display_override * overrides[] = { + &is->unit_display_override_2, + &is->unit_display_override, + }; + for (int i = 0; i < ARRAY_LEN (overrides); i++) { + struct unit_display_override * override = overrides[i]; + 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 != excluded) && + (unit->Body.X == tile_x) && + (unit->Body.Y == tile_y)) { + return unit; + } + } + } + } + + if (unit_counter_display_enabled && unit_counter_bombard_display_mode) { + 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 * bombard_best = find_counter_or_base_bombard_defender_against ( + this->Current_Unit, tile_x, tile_y, + this->Current_Unit->Body.CivID, true, excluded); + if (bombard_best != NULL) + return bombard_best; } + return Main_Screen_Form_find_visible_unit ( + this, __, tile_x, tile_y, excluded); } - return Main_Screen_Form_find_visible_unit (this, __, tile_x, tile_y, excluded); + Unit * post_combat_defender = + get_post_combat_defender_display_override (tile_x, tile_y, + excluded); + if (post_combat_defender != NULL) + return post_combat_defender; + + // Default selection display should match the normal combat defender. + // Bombardment has its own display branch above. + if (unit_counter_display_enabled && + ! unit_counter_bombard_display_mode && + (this->Mode_Action != UMA_Precision_Strike) && + (this->Mode_Action != UMA_Auto_Precision_Strike)) { + Unit * combat_best = find_counter_base_visible_defender_against ( + this, this->Current_Unit, tile_x, tile_y, excluded); + if (combat_best != NULL) + return combat_best; + } + + return Main_Screen_Form_find_visible_unit ( + this, __, tile_x, tile_y, excluded); } void __fastcall @@ -27828,6 +29033,29 @@ patch_Animator_play_zoc_animation (Animator * this, int edx, Unit * unit, Animat Animator_play_one_shot_unit_animation (this, __, unit, anim_type, param_3); } +void __fastcall +patch_Animator_play_one_shot_unit_animation (Animator * this, int edx, Unit * unit, AnimationType anim_type, bool param_3) +{ + struct unit_display_override saved_udo = is->unit_display_override; + struct unit_display_override saved_udo_2 = is->unit_display_override_2; + bool force_unit_to_top = (unit != NULL) && (anim_type == AT_DEATH); + if (force_unit_to_top) { + is->unit_display_override = (struct unit_display_override) { + unit->Body.ID, unit->Body.X, unit->Body.Y + }; + is->unit_display_override_2 = (struct unit_display_override) { + unit->Body.ID, unit->Body.X, unit->Body.Y + }; + } + + Animator_play_one_shot_unit_animation (this, __, unit, anim_type, param_3); + + if (force_unit_to_top && ! is->combat_unit_display_override_active) { + is->unit_display_override = saved_udo; + is->unit_display_override_2 = saved_udo_2; + } +} + bool __fastcall patch_Fighter_check_zoc_anim_visibility (Fighter * this, int edx, Unit * attacker, Unit * defender, bool param_3) { @@ -28109,7 +29337,11 @@ Unit * __fastcall patch_Fighter_find_defensive_bombarder (Fighter * this, int edx, Unit * attacker, Unit * defender) { int special_db_rules = is->current_config.special_defensive_bombard_rules; - if ((special_db_rules == 0) && + bool apply_counter_bombard_rules = + is->current_config.enable_unit_counters && + (is->current_config.count_counter_rules > 0); + if ((! apply_counter_bombard_rules) && + (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); @@ -28126,12 +29358,20 @@ patch_Fighter_find_defensive_bombarder (Fighter * this, int edx, Unit * attacker Unit * tr = NULL; int highest_strength = -1; + Tile * bombard_target_tile = tile_at (attacker->Body.X, attacker->Body.Y); 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]; + int candidate_strength = counter_adjusted_bombard_strength ( + candidate_type->Bombard_Strength, + get_counter_rule_bombard_modifier ( + &is->current_config, + candidate, + attacker, + bombard_target_tile)); if (can_do_defensive_bombard (candidate, candidate_type) && - (candidate_type->Bombard_Strength > highest_strength) && + (candidate_strength > highest_strength) && (candidate != defender) && (Unit_get_containing_army (candidate) != defender) && ((attacker_class == candidate_type->Unit_Class) || @@ -28143,7 +29383,7 @@ patch_Fighter_find_defensive_bombarder (Fighter * this, int edx, Unit * attacker (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; + highest_strength = candidate_strength; } } return tr; @@ -28153,10 +29393,22 @@ patch_Fighter_find_defensive_bombarder (Fighter * this, int edx, Unit * attacker void __fastcall patch_Fighter_damage_by_db_in_main_loop (Fighter * this, int edx, Unit * bombarder, Unit * defender) { + // Defensive bombard may briefly show the bombarder, then possibly the defender dying. + // Keep the bombard target visible in the secondary slot while the bombarder animates. + struct unit_display_override saved_udo = is->unit_display_override; + struct unit_display_override saved_udo_2 = is->unit_display_override_2; + struct unit_display_override bombard_target_udo = { + defender->Body.ID, defender->Body.X, defender->Body.Y + }; + is->unit_display_override = bombard_target_udo; + is->unit_display_override_2 = bombard_target_udo; + 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)) + if (Unit_try_flying_over_tile (bombarder, __, defender->Body.X, defender->Body.Y)) { + is->unit_display_override = saved_udo; + is->unit_display_override_2 = saved_udo_2; return; // intercepted - else if (patch_Main_Screen_Form_is_unit_visible_to_player (p_main_screen_form, __, defender->Body.X, defender->Body.Y, bombarder)) + } 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); } @@ -28182,34 +29434,584 @@ patch_Fighter_damage_by_db_in_main_loop (Fighter * this, int edx, Unit * bombard // 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); + if ((! is_online_game ()) && Fighter_check_combat_anim_visibility (this, __, bombarder, defender, true)) { + patch_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; } } + + if (is->dbe.defender_was_destroyed) { + is->unit_display_override = bombard_target_udo; + is->unit_display_override_2 = bombard_target_udo; + } else if (is->combat_unit_display_override_active && (this->defender != NULL)) { + is->unit_display_override = (struct unit_display_override) { + this->defender->Body.ID, this->defender->Body.X, this->defender->Body.Y + }; + is->unit_display_override_2 = saved_udo_2; + } else { + is->unit_display_override = saved_udo; + is->unit_display_override_2 = saved_udo_2; + } +} + +int __fastcall +patch_Fighter_get_odds_for_bombardment (Fighter * this, int edx, Unit * attacker, Unit * defender, bool bombarding, bool ignore_defensive_bonuses) +{ + if (! (bombarding && + is->current_config.enable_unit_counters && + (attacker != NULL) && + (defender != NULL) && + (attacker->Body.UnitTypeID >= 0) && + (attacker->Body.UnitTypeID < p_bic_data->UnitTypeCount) && + (defender->Body.UnitTypeID >= 0) && + (defender->Body.UnitTypeID < p_bic_data->UnitTypeCount))) + return Fighter_get_combat_odds (this, __, attacker, defender, bombarding, ignore_defensive_bonuses); + + Tile * target_tile = tile_at (defender->Body.X, defender->Body.Y); + int bombard_pct = get_counter_rule_bombard_modifier ( + &is->current_config, + attacker, + defender, + target_tile); + + if (bombard_pct == 100) + return Fighter_get_combat_odds (this, __, attacker, defender, bombarding, ignore_defensive_bonuses); + + UnitType * attacker_type = &p_bic_data->UnitTypes[attacker->Body.UnitTypeID]; + int saved_bombard_strength = attacker_type->Bombard_Strength; + attacker_type->Bombard_Strength = + counter_adjusted_bombard_strength (saved_bombard_strength, bombard_pct); + + int result = Fighter_get_combat_odds (this, __, attacker, defender, bombarding, ignore_defensive_bonuses); + + attacker_type->Bombard_Strength = saved_bombard_strength; + return result; } 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->combat_unit_display_override_active && (defender != NULL)) + is->unit_display_override = (struct unit_display_override) { + defender->Body.ID, defender->Body.X, defender->Body.Y + }; + 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 defensive-bonus skipping when we actually ran + // apply_counter_rules for this call. Otherwise counter_combat_ctx can be + // stale from an earlier combat + // round or a future odds probe. + bool ignore_defensive_bonuses_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 rule_ignore_defensive_bonuses; + apply_counter_rules (cfg, attacker, defender, def_tile, + &aa, &dd, + &rule_ignore_defensive_bonuses); + + 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_defensive_bonuses = + rule_ignore_defensive_bonuses; + ignore_defensive_bonuses_for_odds = + ignore_defensive_bonuses || rule_ignore_defensive_bonuses; + } + + int result = Fighter_get_combat_odds (this, __, attacker, defender, bombarding, + ignore_defensive_bonuses_for_odds); + is->counter_combat_ctx.active = false; + return result; } -byte __fastcall -patch_Fighter_fight (Fighter * this, int edx, Unit * attacker, int attack_direction, Unit * defender_or_null) +bool +unit_has_valid_type_id (Unit * unit) +{ + return (unit != NULL) && + (unit->Body.UnitTypeID >= 0) && + (unit->Body.UnitTypeID < p_bic_data->UnitTypeCount); +} + +long long +divide_counter_strength (long long numerator, int denominator) +{ + if (denominator <= 0) + return 0x3FFFFFFF; + return numerator / denominator; +} + +bool +counter_tile_has_river_edge (Tile * tile, enum direction dir) +{ + if ((tile == NULL) || (tile == p_null_tile)) + return false; + + int bit = direction_to_neighbor_bit (dir); + return (bit >= 0) && + ((tile->vtable->m37_Get_River_Code (tile) & (1 << bit)) != 0); +} + +bool +counter_attack_crosses_river (Unit * attacker, Unit * defender, Tile * def_tile) +{ + if (! (unit_has_valid_type_id (attacker) && + unit_has_valid_type_id (defender))) + return false; + + Tile * atk_tile = tile_at (attacker->Body.X, attacker->Body.Y); + if ((atk_tile == NULL) || (atk_tile == p_null_tile)) + return false; + + int def_to_atk = Map_compute_neighbor_index ( + &p_bic_data->Map, __, + defender->Body.X, defender->Body.Y, + attacker->Body.X, attacker->Body.Y, + 8); + int atk_to_def = Map_compute_neighbor_index ( + &p_bic_data->Map, __, + attacker->Body.X, attacker->Body.Y, + defender->Body.X, defender->Body.Y, + 8); + + return ((def_to_atk > 0) && (def_to_atk <= 8) && + counter_tile_has_river_edge (def_tile, + (enum direction)def_to_atk)) || + ((atk_to_def > 0) && (atk_to_def <= 8) && + counter_tile_has_river_edge (atk_tile, + (enum direction)atk_to_def)); +} + +int +counter_defender_selection_defensive_bonus_percent (Unit * attacker, + Unit * defender, + Tile * def_tile) +{ + if (! unit_has_valid_type_id (defender)) + return 0; + + UnitType * defender_type = &p_bic_data->UnitTypes[defender->Body.UnitTypeID]; + if (defender_type->Unit_Class != UTC_Land) + return 0; + + int bonus = 0; + + enum SquareTypes terrain = def_tile->vtable->m50_Get_Square_BaseType (def_tile); + if ((terrain >= 0) && (terrain < p_bic_data->TileTypesCount)) { + Tile_Type * terrain_type = &p_bic_data->TileTypes[terrain]; + bonus += def_tile->vtable->m30_Check_is_LM (def_tile) ? + terrain_type->LM_DefenceBonus : + terrain_type->DefenceBonus; + } + + City * city = city_at (defender->Body.X, defender->Body.Y); + if (city != NULL) { + int city_bonus_index = 0; + if (city->Body.Population.Size > p_bic_data->General.MaximumSize_City) + city_bonus_index = 2; + else if (city->Body.Population.Size > p_bic_data->General.MaximumSize_Town) + city_bonus_index = 1; + bonus += p_bic_data->General.DefenceBonus_Cities[city_bonus_index]; + } + + bonus += patch_get_building_defense_bonus_at ( + defender->Body.X, defender->Body.Y, 0); + + if (def_tile->vtable->m14_Check_Barricade (def_tile, __, 0)) + bonus += 2 * p_bic_data->General.DefenceBonus_Fortress; + else if (def_tile->vtable->m13_Check_Fortress (def_tile, __, 0)) + bonus += p_bic_data->General.DefenceBonus_Fortress; + + if (defender->Body.UnitState == UnitState_Fortifying) + bonus += p_bic_data->General.DefenceBonus_Fortification; + + if (counter_attack_crosses_river (attacker, defender, def_tile)) + bonus += p_bic_data->General.DefenceBonus_River; + + return bonus; +} + +int +counter_adjusted_defender_strength (Unit * attacker, + Unit * defender, + int defender_strength, + bool include_defensive_bonuses) +{ + if (! (is->current_config.enable_unit_counters && + unit_has_valid_type_id (attacker) && + unit_has_valid_type_id (defender))) + return defender_strength; + + Tile * def_tile = tile_at (defender->Body.X, defender->Body.Y); + if ((def_tile == NULL) || (def_tile == p_null_tile)) + return defender_strength; + + int attacker_atk_pct, defender_def_pct; + bool ignore_defensive_bonuses; + apply_counter_rules (&is->current_config, attacker, defender, def_tile, + &attacker_atk_pct, &defender_def_pct, + &ignore_defensive_bonuses); + + if ((attacker_atk_pct == 100) && + (defender_def_pct == 100) && + (! include_defensive_bonuses || ignore_defensive_bonuses)) + return defender_strength; + + if (attacker_atk_pct <= 0) + return 0x3FFFFFFF; + + long long adjusted = (long long)defender_strength * defender_def_pct / 100; + if (include_defensive_bonuses && ! ignore_defensive_bonuses) { + int bonus = + counter_defender_selection_defensive_bonus_percent ( + attacker, defender, def_tile); + adjusted = adjusted * (100 + bonus) / 100; + } + + adjusted = divide_counter_strength ( + adjusted * 100, + attacker_atk_pct); + if (adjusted < 0) + return 0; + if (adjusted > 0x3FFFFFFF) + return 0x3FFFFFFF; + return (int)adjusted; +} + +int +counter_adjusted_attacker_strength (Unit * attacker, Unit * defender, int attacker_strength) +{ + if (! (is->current_config.enable_unit_counters && + unit_has_valid_type_id (attacker) && + unit_has_valid_type_id (defender))) + return attacker_strength; + + Tile * def_tile = tile_at (defender->Body.X, defender->Body.Y); + if ((def_tile == NULL) || (def_tile == p_null_tile)) + return attacker_strength; + + int attacker_atk_pct, defender_def_pct; + bool ignore_defensive_bonuses; + apply_counter_rules (&is->current_config, attacker, defender, def_tile, + &attacker_atk_pct, &defender_def_pct, + &ignore_defensive_bonuses); + + if ((attacker_atk_pct == 100) && (defender_def_pct == 100)) + return attacker_strength; + + if (defender_def_pct <= 0) + return 0x3FFFFFFF; + + long long adjusted = divide_counter_strength ( + (long long)attacker_strength * attacker_atk_pct, + defender_def_pct); + if (adjusted < 0) + return 0; + if (adjusted > 0x3FFFFFFF) + return 0x3FFFFFFF; + return (int)adjusted; +} + +Unit * +select_counter_best_attacking_army_member (Unit * army, Unit * defender) +{ + if (! (is->current_config.enable_unit_counters && + unit_has_valid_type_id (army) && + unit_has_valid_type_id (defender) && + Unit_has_ability (army, __, UTA_Army))) + return NULL; + + Tile * tile = tile_at (army->Body.X, army->Body.Y); + if ((tile == NULL) || (tile == p_null_tile)) + return NULL; + + Unit * best = NULL; + int best_strength = -1; + int best_base_strength = -1; + FOR_UNITS_ON (uti, tile) { + Unit * unit = uti.unit; + if ((unit == NULL) || + (unit->Body.Container_Unit != army->Body.ID) || + ! unit_has_valid_type_id (unit)) + continue; + + int base_strength = Unit_get_attack_strength (unit); + if (base_strength <= 0) + continue; + + int strength = counter_adjusted_attacker_strength (unit, defender, base_strength); + if ((best == NULL) || + (strength > best_strength) || + ((strength == best_strength) && (base_strength > best_base_strength))) { + best = unit; + best_strength = strength; + best_base_strength = base_strength; + } + } + + return best; +} + +Unit * +counter_attacker_for_defender_selection (Unit * attacker, Unit * defender) +{ + if (unit_has_valid_type_id (attacker) && + unit_has_valid_type_id (defender) && + Unit_has_ability (attacker, __, UTA_Army)) { + Unit * member = select_counter_best_attacking_army_member (attacker, defender); + if (member != NULL) + return member; + } + return attacker; +} + +bool __fastcall +patch_Fighter_prefer_first_defender_1 (Fighter * this, int edx, Unit * first, int first_strength, Unit * second, int second_strength, bool param_5) +{ + Unit * attacker = (this != NULL) ? this->attacker : NULL; + if (! unit_has_valid_type_id (attacker) && + is->counter_defender_selection_ctx.active) + attacker = is->counter_defender_selection_ctx.attacker; + + if (is->current_config.enable_unit_counters && + unit_has_valid_type_id (attacker) && + unit_has_valid_type_id (first) && + unit_has_valid_type_id (second) && + (first->Body.CivID != attacker->Body.CivID) && + (second->Body.CivID != attacker->Body.CivID) && + first->vtable->is_enemy_of_civ (first, __, attacker->Body.CivID, 0) && + second->vtable->is_enemy_of_civ (second, __, attacker->Body.CivID, 0)) { + Unit * first_attacker = counter_attacker_for_defender_selection (attacker, first); + Unit * second_attacker = counter_attacker_for_defender_selection (attacker, second); + Tile * first_tile = tile_at (first->Body.X, first->Body.Y); + Tile * second_tile = tile_at (second->Body.X, second->Body.Y); + int unused_atk_pct, unused_def_pct; + bool first_ignore_defensive_bonuses = false, + second_ignore_defensive_bonuses = false; + if ((first_tile != NULL) && (first_tile != p_null_tile)) + apply_counter_rules ( + &is->current_config, first_attacker, first, + first_tile, &unused_atk_pct, &unused_def_pct, + &first_ignore_defensive_bonuses); + if ((second_tile != NULL) && (second_tile != p_null_tile)) + apply_counter_rules ( + &is->current_config, second_attacker, second, + second_tile, &unused_atk_pct, &unused_def_pct, + &second_ignore_defensive_bonuses); + bool include_defensive_bonuses = + first_ignore_defensive_bonuses || + second_ignore_defensive_bonuses; + first_strength = counter_adjusted_defender_strength ( + first_attacker, first, first_strength, + include_defensive_bonuses); + second_strength = counter_adjusted_defender_strength ( + second_attacker, second, second_strength, + include_defensive_bonuses); + + bool result = Fighter_prefer_first_defender_1 ( + this, __, first, first_strength, second, second_strength, + param_5); + return result; + } + + return Fighter_prefer_first_defender_1 (this, __, first, first_strength, second, second_strength, param_5); +} + +Unit * +find_counter_best_defender_against (Unit * attacker, Tile * tile, int tile_x, + int tile_y, Unit * excluded, + bool require_visible, + bool * out_any_counter_effect) { - byte tr = Fighter_fight (this, __, attacker, attack_direction, defender_or_null); + if (out_any_counter_effect != NULL) + *out_any_counter_effect = false; + + if (! (unit_has_valid_type_id (attacker) && + (tile != NULL) && + (tile != p_null_tile))) + return NULL; + + int attacker_civ = attacker->Body.CivID; + + 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; + struct counter_defender_selection_context saved_selection_ctx = + is->counter_defender_selection_ctx; + is->counter_defender_selection_ctx = + (struct counter_defender_selection_context) { + true, attacker, tile_x, tile_y + }; + + Unit * best = NULL; + bool best_pass_had_counter_effect = false; + for (int pass = 0; pass < 2; pass++) { + bool skip_requires_escort_units = pass == 0; + bool pass_had_counter_effect = false; + + FOR_UNITS_ON (uti, tile) { + Unit * unit = uti.unit; + if ((unit == NULL) || + (unit == excluded) || + (unit->Body.Container_Unit >= 0) || + ! unit_has_valid_type_id (unit) || + (unit->Body.CivID == attacker_civ) || + ! unit->vtable->is_enemy_of_civ (unit, __, attacker_civ, 0) || + (Unit_get_defense_strength (unit) <= 0) || + (require_visible && + ! patch_Unit_is_visible_to_civ (unit, __, attacker_civ, 0))) + continue; + + UnitType * unit_type = + &p_bic_data->UnitTypes[unit->Body.UnitTypeID]; + if (skip_requires_escort_units && + UnitType_has_ability (unit_type, __, UTA_Requires_Escort)) + continue; + + p_bic_data->fighter.attacker = attacker; + p_bic_data->fighter.defender = unit; + 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; + + if (! Fighter_unit_can_defend (&p_bic_data->fighter, __, unit, tile_x, tile_y)) + continue; + + if (out_any_counter_effect != NULL) { + Unit * counter_attacker = + counter_attacker_for_defender_selection ( + attacker, unit); + int attacker_atk_pct, defender_def_pct; + bool ignore_defensive_bonuses; + apply_counter_rules ( + &is->current_config, counter_attacker, unit, tile, + &attacker_atk_pct, &defender_def_pct, + &ignore_defensive_bonuses); + if ((attacker_atk_pct != 100) || + (defender_def_pct != 100) || + ignore_defensive_bonuses) + pass_had_counter_effect = true; + } + + if ((best == NULL) || + patch_Fighter_prefer_first_defender_1 ( + &p_bic_data->fighter, __, + unit, Unit_get_defense_strength (unit), + best, Unit_get_defense_strength (best), + true)) + best = unit; + } + + if (best != NULL) { + best_pass_had_counter_effect = pass_had_counter_effect; + break; + } + } + + if (out_any_counter_effect != NULL) + *out_any_counter_effect = best_pass_had_counter_effect; + + 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; + is->counter_defender_selection_ctx = saved_selection_ctx; + + return best; +} + +Unit * +find_counter_best_visible_defender_against (Unit * attacker, Tile * tile, int tile_x, int tile_y, Unit * excluded) +{ + return find_counter_best_defender_against (attacker, tile, tile_x, + tile_y, excluded, true, NULL); +} + +Unit * +find_counter_best_visible_defender_against_with_effect (Unit * attacker, Tile * tile, int tile_x, int tile_y, Unit * excluded, bool * out_any_counter_effect) +{ + return find_counter_best_defender_against (attacker, tile, tile_x, + tile_y, excluded, true, + out_any_counter_effect); +} + +byte __fastcall +patch_Fighter_fight (Fighter * this, int edx, Unit * attacker, + int attack_direction, Unit * defender_or_null) +{ + bool saved_combat_udo_active = is->combat_unit_display_override_active; + struct unit_display_override saved_combat_udo = is->saved_combat_unit_display_override; + int attacker_id = (attacker != NULL) ? attacker->Body.ID : -1; + clear_post_combat_defender_display_override (); + is->saved_combat_unit_display_override = is->unit_display_override; + is->combat_unit_display_override_active = true; + is->unit_display_override_2 = (struct unit_display_override) {-1, -1, -1}; + + Unit * defender_for_fight = defender_or_null; + int defender_tile_x = 0, + defender_tile_y = 0; + bool direction_matches_tile = false; + if (is->current_config.enable_unit_counters && + get_counter_defender_selection_tile ( + attacker, attack_direction, defender_or_null, + &defender_tile_x, &defender_tile_y, + &direction_matches_tile) && + counter_defender_selection_can_pass_null_defender ( + attacker, direction_matches_tile)) + defender_for_fight = NULL; + + byte tr = Fighter_fight (this, __, attacker, attack_direction, + defender_for_fight); + + refresh_post_combat_defender_display_override_after_fight (this, + attacker_id); + + is->unit_display_override = saved_combat_udo_active ? + is->saved_combat_unit_display_override : + (struct unit_display_override) {-1, -1, -1}; + is->unit_display_override_2 = (struct unit_display_override) {-1, -1, -1}; + is->saved_combat_unit_display_override = saved_combat_udo; + is->combat_unit_display_override_active = saved_combat_udo_active; 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 && + (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) { @@ -28231,16 +30033,23 @@ 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) { - // Make sure the unit is displayed if it's in an army and we're configured for that + // Defensive bombard can come from a mid-stack unit. Force the bombarder to the top + // only for its attack animation, then restore the defender/top-unit override. 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)) + Unit * container = get_unit_ptr (this->Body.Container_Unit); + bool in_army = (container != NULL) && Unit_has_ability (container, __, UTA_Army); + if ((! in_army) || is->current_config.show_armies_performing_defensive_bombard) is->unit_display_override = (struct unit_display_override) { this->Body.ID, this->Body.X, this->Body.Y }; + bool restore_fortify = this->Body.UnitState == UnitState_Fortifying; + if (restore_fortify) + Unit_set_state (this, __, 0); + Unit_play_attack_animation (this, __, direction); + if (restore_fortify) + Unit_set_state (this, __, UnitState_Fortifying); + is->unit_display_override = saved_udo; } } @@ -36569,7 +38378,7 @@ patch_Unit_ai_move_air_transport (Unit * this) 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); @@ -36656,6 +38465,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) { @@ -37023,14 +38840,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) { @@ -37174,6 +38983,47 @@ patch_Unit_select_army_member_for_combat (Unit * this, int edx, int param_1, cha return this; } + if (is->current_config.enable_unit_counters && + Unit_has_ability (this, __, UTA_Army) && + unit_has_valid_type_id (p_bic_data->fighter.defender) && + (p_bic_data->fighter.defender->Body.CivID != this->Body.CivID) && + p_bic_data->fighter.defender->vtable->is_enemy_of_civ ( + p_bic_data->fighter.defender, __, this->Body.CivID, 0)) { + Unit * best_attacker = select_counter_best_attacking_army_member ( + this, p_bic_data->fighter.defender); + if (best_attacker != NULL) + return best_attacker; + } + + if (is->current_config.enable_unit_counters && + Unit_has_ability (this, __, UTA_Army) && + unit_has_valid_type_id (p_bic_data->fighter.attacker) && + (p_bic_data->fighter.attacker->Body.CivID != this->Body.CivID) && + this->vtable->is_enemy_of_civ (this, __, p_bic_data->fighter.attacker->Body.CivID, 0)) { + Tile * tile = tile_at (this->Body.X, this->Body.Y); + Unit * best = NULL; + 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_get_defense_strength (unit) <= 0)) + continue; + if ((best == NULL) || + patch_Fighter_prefer_first_defender_1 ( + &p_bic_data->fighter, __, + unit, Unit_get_defense_strength (unit), + best, Unit_get_defense_strength (best), + true)) + best = unit; + } + } + if (best != NULL) { + this->Body.army_top_defender_id = best->Body.ID; + return best; + } + } + return Unit_select_army_member_for_combat (this, __, param_1, param_2); } @@ -37195,6 +39045,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; }