Skip to content

Customizable Unit Counter System & Civ4-Style Best Defender Selection#31

Open
bingyu-893 wants to merge 13 commits intomaxpetul:masterfrom
bingyu-893:master
Open

Customizable Unit Counter System & Civ4-Style Best Defender Selection#31
bingyu-893 wants to merge 13 commits intomaxpetul:masterfrom
bingyu-893:master

Conversation

@bingyu-893
Copy link
Copy Markdown

Hi @maxpetul,

I really love your C3X mod; it’s made playing Civ 3 so much easier and more enjoyable. This has inspired me to develop some new features for C3X, so I’ve had a go at creating a feature: a configurable unit counter (rock-paper-scissors) system, alongside an optional Civilization 4 style "Best Defender" selection logic for stack combat.

Here is the overall structure of the code:

1. Configuration & Toggles (in default.c3x_config.ini)

  • enable_unit_counters: Global toggle. If false, the entire system is bypassed.
  • unit_group: Allows grouping unit types for easier rule matching (syntax matches building_prereqs_for_units).
  • counter_rule: The core ruleset. Supports modifiers for both attacker and defender (e.g., self_atk_pct, enemy_def_pct). The logic dictates that the first unit listed in the rule is always self, and the second is enemy, applying bidirectionally regardless of who initiates the attack.
  • use_civ4_style_best_defender: When enabled (requires enable_unit_counters), melee combat will dynamically select the defender that is hardest to defeat based on post-counter combat odds, aligning both the actual combat and the top unit displayed on the map stack.

2. Data Structures (C3X.h)

  • Added struct unit_counter_group (Name + array of UnitTypeID).
  • Added struct counter_rule for granular matching:
    • Matches specific unit IDs, UCM_ANY (*), or UCM_GROUP.
    • Supports environmental conditions: terrain_type, only_in_city, district_id, and ignore_terrain (negates defender's terrain bonuses).

3. Core Algorithm

  • Implemented apply_counter_rules(...) to handle modifier calculation.
  • It initializes aa (attacker multiplier) and dd (defender multiplier) to 100. It then iterates through all configured counter_rules and applies cumulative percentage multipliers for all matching criteria (chained multiplication for multiple valid rules).
  • Sets the ignore_terrain flag if any matching rule mandates it.

4. Combat Context & Engine Hooks

  • Introduced counter_combat_ctx to store active modifiers (attacker_atk_pct, defender_def_pct, ignore_terrain).
  • Hooked patch_Unit_get_attack_strength and patch_Unit_get_defense_strength to return the modified base * pct / 100 when the context is active for the current attacker/defender.
  • Hooked patch_Fighter_get_odds_for_main_combat_loop. Because the original Fighter_get_combat_odds reads raw UnitType.Attack / Defence, the implementation temporarily alters the UnitType table stats based on apply_counter_rules, runs the probe, and restores them.

5. Civ4 Best Defender Logic & UI Synchronization

  • Target Selection: find_civ4_best_defender_against enumerates valid defenders in a tile, simulates apply_counter_rules, and invokes Fighter_get_combat_odds. Selects the candidate with the highest defense odds. In case of a tie, falls back to Fighter_prefer_first_defender_1 (comparing modified effective defense) to maintain vanilla "cheapest unit defends first" behavior.
  • UI/Combat Sync: Hooked patch_Main_Screen_Form_find_visible_unit to ensure the top unit of the stack matches the predicted best defender. Additionally, hooked patch_Fighter_fight to override the engine's pre-selected defender when attacking from an adjacent tile, completely eliminating the "UI shows one unit, combat engages another" mismatch.

6. Out of Scope

  • This system currently does not alter target selection for Bombardment, Defensive Bombardment, or Interception (yet).

Disclaimer: > As I am still familiarizing myself with this level of C engine hooking, I used AI assistance heavily for the code generation. If you spot any unconventional logic or areas that need optimization, please bear with me—I would really appreciate your feedback and guidance on improving it!

…te application of counter rules.

- Introduced a new boolean to manage terrain skipping based on the context of the call, preventing stale data issues from previous computations.
@maxpetul
Copy link
Copy Markdown
Owner

Hello. Thanks for the contribution. I glanced over it and it looks alright except for a few issues:

  • Your commit 466a838 touches every line in injected_code.c and a couple of other files. You must have changed the line endings from DOS-style to Unix-style. That's a problem because it interferes with git blame. Please fix this and make sure your changes match the existing line ending format.
  • The district option for counter_rules only works for special districts, that should be mentioned in the config at least. It would be nice if it could work for dynamic districts too, though I'm guessing it doesn't work that way because dynamic districts are loaded after the main mod config so their names couldn't be recognized.
  • It should be noted in the config that the valid values for terrain names are the same as for the buildable_on field in the districts config. Those are not the same as what's specified in the BIQ, in particular there are some extra terrains that are recognized like "snow-forest", and only English names are recognized.

- support dynamic district types
- Add details of terrain types to config file
-turn off some features in default.c3x_config.ini
-Actually, the line endings are already LF, but in my last commit I made a typo.
@maxpetul
Copy link
Copy Markdown
Owner

The support for dynamic districts looks good. However, whatever you did to change the line endings to CRLF didn't work; the endings in injected_code.c and other files are still LF. You'll know the line endings are fixed when GitHub no longer shows that this PR touches every line in the files. For example, if you go to the "files changed" tab, for injected_code.c, it shows +37,115 and -36,558, indicating that this PR changers every line in the file. Those numbers should be much smaller showing only your changes. Same applies to civ_prog_objects.csv, default.c3x_config.ini, and C3X.h.

This reverts commit 56f08f9.
- Added a new condition to counter_rule that checks the combat_experience of the unit.
- This allows for more dynamic and challenging combat scenarios based on the unit's experience level.
@bingyu-893
Copy link
Copy Markdown
Author

Hi @maxpetul,

Just wanted to give you a quick update! I've fixed the line ending issue you pointed out earlier. Additionally, I've implemented a new feature: players can now customize extra combat bonuses based on a unit's experience level directly within the counter_rule.
For my next step, I'm planning to add a feature that displays the combat odds directly in the game. However, I'm not entirely sure how to approach adding or drawing text on the game's UI. Could you give me some pointers or advice on how to hook into the UI to display this kind of information? Any guidance would be greatly appreciated!

@maxpetul
Copy link
Copy Markdown
Owner

maxpetul commented May 3, 2026

Hello again.

I've fixed the line ending issue you pointed out earlier.

Thank you, the line ending issue is fixed now. However that has revealed some other issues with this PR, it's undoing some recent commits. For example, looking at the changes to injected_code.c around wai_init_cities, it's undoing the _iter_count that I inserted to fix a crash. It's also undoing the max_contiguous_canal_districts checks that instafluff inserted, the "If the palace has been set as a small wonder..." fix, and many other recent changes.

For my next step, I'm planning to add a feature that displays the combat odds directly in the game... Could you give me some pointers or advice on how to hook into the UI to display this kind of information?

Are you planning to add that to this PR or start a new one? I'll leave it up to you but I'd prefer if you finished this one off first then started a new one for the combat odds display. As for advice, unfortunately I don't know where you'd hook into the UI, I'd have to look around for that. It also depends how you want to display the odds. I'm imaging a box that appears in one corner of the map display when the player hovers over a target, like in Civ 4. So you'd want to hook into the mouse hover event for Main_Screen_Form, but again, I haven't located that yet. Other than that, you'd probably want to draw a background for the box, that would be PCX_Image_read_file to load it and PCX_Image_draw_onto to draw it onto the Main_Screen_Form's canvas. Then to add text you'd simply use PCX_Image_draw_text, there are a lot of examples of that already in the mod code. I experimented with something like this a few years ago, I'll dig up the branch and see if it's useful.

@bingyu-893
Copy link
Copy Markdown
Author

However that has revealed some other issues with this PR, it's undoing some recent commits.

Thanks for catching that! I will fetch the latest changes from the upstream branch and merge them into mine. Since my new features are mostly isolated to the counter system, there shouldn't be any functional conflicts. I'll get this sorted out soon.

I'll leave it up to you but I'd prefer if you finished this one off first then started a new one for the combat odds display.

I agree. Splitting this into two PRs is definitely a much better idea for easier management. I'll focus on polishing and finishing up this PR first.

you'd probably want to draw a background for the box, that would be PCX_Image_read_file to load it and PCX_Image_draw_onto to draw it onto the Main_Screen_Form's canvas. Then to add text you'd simply use PCX_Image_draw_text, there are a lot of examples of that already in the mod code.

Thank you for the pointers on the UI side, I will research these methods once I start on the new PR.

@maxpetul
Copy link
Copy Markdown
Owner

maxpetul commented May 5, 2026

I had a look at that old branch I mentioned. It's probably not going to be useful since back then I was attempting to add a new form to the game with its own widgets such as buttons. That's overkill if all you want to do is display some text.

However, I did some poking around today exploring how the display of combat odds might be implemented. It's actually a feature I've had on my mind for years but have never gotten around to doing myself, so I'm eager to see it done. To capture which tile the mouse is hovering over, you'd want to hook the 28th method (m27) in the Main Screen Form vtable, which I've named Main_Screen_Form::process_mouse_hover. That method is called whenever the user moves the mouse on the main map display in-game or over the main menu. Here's a small example of tracking which tile the mouse is hovering over: 70fc4dc

I'm still not sure what's the best way to draw the box with some text in it onto the screen. Drawing to the main screen's canvas in process_mouse_hover sort of works but it's not consistent with the rest of the drawing logic so whatever is drawn that way is likely to get overwritten. It might work simply to hook Main_Screen_Form::m22_Draw, I'll try that next. If that doesn't work, I'll have to look into how the game draws the box to the right that shows passengers in the currently selected unit (if any) and imitate that.

@maxpetul
Copy link
Copy Markdown
Owner

maxpetul commented May 5, 2026

Main_Screen_Form::m22_Draw doesn't actually draw anything, it only sets a dirty flag in the Animator object. That flag is consumed by Animator::update, which is where the actual drawing happens. So if you want to add something to the screen, that's the place to do it. Here's another example: 3dea8ca. It displays the coords of the tile the mouse is hovering over in the top left of the screen under the menu buttons. Hope this helps.

@maxpetul
Copy link
Copy Markdown
Owner

maxpetul commented May 8, 2026

I experimented with this PR in game and found a few issues. There's one big issue in that combat animations are messed up and the unit shown on the tile when you attack it is not necessarily the one you end up fighting. My test was that I created a scenario where the player can use cavalry to attack a tile with an enemy rifleman and guerilla. The rifleman was modded to have +7 bonus HP and the guerilla to have -2 bonus HP and +1 defense. The test was to make sure that the game selected the rifleman as the defender since it's more likely to win combat although the guerilla is more likely to win a single round, when use_civ4_style_best_defender is on. What I discovered is that the game will show the guerilla as the top defender but when you attack you end up fighting the rifleman. And another bug, the game will continue showing the guerilla while combat is going on, it will be just standing there while the rifleman fights underneath, not shown.

Then there are the smaller issues:

  • In parse_unit_counter_group, the call to skip_punctuation for a comma at the end of the outer while loop should not be there. The format for the unit_group option should expect commas between groups, not separating units listed within each group. read_recognizables already expects commas separating each of the 'recognizables' that it parses, so inner commas conflict with that. Units within each group should be separated by spaces, as is done in the building_prereqs_for_units option. In other words, valid settings for unit_group should look like this: [Subs: Submarine "Nuclear Submarine", AntiAir: Flak "Mobile SAM"].
  • Civ 4 style defender selection wouldn't work for army members because there's a separate function that picks which member fights each round of combat for an army. I've called it Unit::select_army_member_for_combat and it's already got an inlead on it.
  • This is a very minor thing but inside load_config, you've placed the branches to handle "unit_group" and "counter_rule" in the wrong place. Notice they're under a comment that says "if key was previously misspelled". They should instead be under the comment that says "if key is for something special". I would place them at the end of that block underneath the branch for "aircraft_victory_animation".

It's not clear to me what the purpose of the use_civ4_style_best_defender option is, anyway. If it's intended to make defender selection logic aware of the unit counter rules, as the comment in the config claims, why even have an option for that? Why would anyone turn it off? I think anyone who's using the counter rules would want them to apply to defender selection, so might as well have it active all the time without an option. However, the logic in find_civ4_best_defender_against does additional things like skip over invisible defenders to "avoid revealing them". I don't remember if that's how Civ 4 works, but it's a strange rule to have in Civ 3, in my opinion. If you want an option for that, that's fine, but it shouldn't be tangled in with applying counter rules; it should be possible to apply the counter rules to defender selection without changing anything else about it. On a related note, I wonder why you didn't patch Fighter::prefer_first_defender_1 to modify how defenders are chosen. It seems to me that would be simpler and more reliable than modifying find_visible_unit and fight, but maybe I'm missing something.

@bingyu-893
Copy link
Copy Markdown
Author

Thank you for the detailed testing and feedback! The issues you brought up, especially regarding the UI and combat animation mismatch, are exactly what I've been trying to wrap my head around these past few days. I also completely missed the interaction with Armies (select_army_member_for_combat), so I really appreciate you pointing that out.

Unfortunately, my schedule is quite packed at the moment, so realistically it might take me until late May to get all these bugs fixed and the underlying logic refactored as you suggested.

Regardless, thank you so much for all the guidance. I will get back to working on this PR as soon as I have the time!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants