Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
6298b37
Unit Counter Commit
gxia0005-hue Apr 25, 2026
466a838
Implement use_civ4_style_best_defender
gxia0005-hue Apr 27, 2026
5301ed6
Merge origin/master into UnitCounters (sync fork)
gxia0005-hue Apr 27, 2026
5ab9a96
Refactor terrain handling in combat odds calculation to ensure accura…
gxia0005-hue Apr 27, 2026
909d962
Change line endings to CRLF
gxia0005-hue Apr 29, 2026
efc1ccd
Turn all files' line endings to LF
gxia0005-hue Apr 29, 2026
56f08f9
renormalize files
gxia0005-hue Apr 30, 2026
7a452b0
Revert "renormalize files"
gxia0005-hue Apr 30, 2026
697ba84
Merge remote-tracking branch 'origin/merge' into merge
gxia0005-hue Apr 30, 2026
1ef481c
remove redundant method
gxia0005-hue Apr 30, 2026
da876ef
Feat: Add combat_experience condition to counter_rule
gxia0005-hue May 2, 2026
ce2df2c
Feat: Fix combat animation
gxia0005-hue May 8, 2026
dd86ea1
Fix counter-aware defender selection and combat display sync
gxia0005-hue May 8, 2026
242f4ba
Add unit counter bombard modifiers and target display handling
gxia0005-hue May 11, 2026
ab1d52b
Merge branch 'master' of https://github.com/bingyu-893/C3XUnitCounters
gxia0005-hue May 11, 2026
8caf8cc
Fix line endings
maxpetul May 14, 2026
6197fca
Merge branch 'maxpetul:master' into master
bingyu-893 May 17, 2026
7b7cffa
Fix line endings and restore unrelated master changes in PR 31
gxia0005-hue May 17, 2026
ce44d9f
Add AGENTS.md
gxia0005-hue May 19, 2026
401dd79
Feat: sync with master branch and display-odds
gxia0005-hue May 19, 2026
8798d9d
Merge branch 'master' into Fix-combat-animation
gxia0005-hue May 19, 2026
2d4f29a
remove AGENT.md
gxia0005-hue May 19, 2026
ed79901
feat: fix known issue with combat animation
gxia0005-hue May 25, 2026
28d9075
Align counter defender display with combat selection
gxia0005-hue May 31, 2026
b62bd52
-Use base defender selection for counter-adjusted strengths
gxia0005-hue May 31, 2026
1e9752d
Remove temporary unit counter defender debug code.
gxia0005-hue Jun 1, 2026
db61da2
Merge branch 'master' of https://github.com/bingyu-893/C3XUnitCounters
gxia0005-hue Jun 1, 2026
9e4c09f
Use fix_line_endings.py to fix line endings in injected_code.c
gxia0005-hue Jun 1, 2026
55afbcf
Fix line endings in TEST_INJECTED_CODE_COMPILE.bat andfix_line_ending…
gxia0005-hue Jun 1, 2026
9a5fee8
Fix line endings in default.c3x_config.ini (LF -> CRLF)
gxia0005-hue Jun 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 69 additions & 2 deletions C3X.h
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

#include <stdbool.h>

#define NOVIRTUALKEYCODES // Keycodes defined in Civ3Conquests.h instead
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
12 changes: 8 additions & 4 deletions civ_prog_objects.csv
Original file line number Diff line number Diff line change
Expand Up @@ -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)"
Expand Down Expand Up @@ -412,15 +412,15 @@ 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", ""
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)"
Expand All @@ -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", ""
Expand Down Expand Up @@ -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)"
Expand Down
71 changes: 71 additions & 0 deletions default.c3x_config.ini
Original file line number Diff line number Diff line change
Expand Up @@ -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 ===]
[==================]
Expand Down
Loading