Resource icon

C3X: EXE Mod including Bug Fixes, Stack Bombard, and Much More Release 23

For a taste of what replacing the unit AI would involve, here's the replacement leader unit AI:
Code:
void __fastcall
patch_Unit_ai_move_leader (Unit * this)
{
    Tile * tile = tile_at (this->Body.X, this->Body.Y);
    int continent_id = tile->vtable->m46_Get_ContinentID (tile);

    // Duplicate some logic from the base function. I'm pretty sure this disbands the unit if it's on a continent with no cities of the same civ.
    if (leaders[this->Body.CivID].ContinentStat1[continent_id] == 0) {
        Unit_disband (this);
        return;
    }

    // Flee if the unit is near an enemy without adequate escort
    int has_adequate_escort; {
        int escorter_count = 0;
        int any_healthy_escorters = 0;
        int index;
        for (int escorter_id = Unit_next_escorter_id (this, __, &index, 1); escorter_id >= 0; escorter_id = Unit_next_escorter_id (this, __, &index, 0)) {
            Unit * escorter = get_unit_ptr (escorter_id);
            if ((escorter != NULL) && (escorter->Body.X == this->Body.X) && (escorter->Body.Y == this->Body.Y)) {
                escorter_count++;
                int remaining_health = Unit_get_max_hp (escorter) - escorter->Body.Damage;
                any_healthy_escorters |= remaining_health >= 3;
            }
        }
        has_adequate_escort = (escorter_count > 0) && any_healthy_escorters;
    }
    if ((! has_adequate_escort) && any_enemies_near_unit (this, 49)) {
        Unit_set_state (this, __, UnitState_Fleeing);
        byte done = this->vtable->work (this);
        if (done || (this->Body.UnitState != 0))
            return;
    }

    // Move along path if the unit already has one set
    byte next_move = Unit_pop_next_move_from_path (this);
    if (next_move > 0) {
        this->vtable->Move (this, __, next_move, 0);
        return;
    }

    // Start a science age if we can
    // It would be nice to compute a value for this and compare it to the option of rushing production but it's hard to come up with a valuation
    if (is->current_config.patch_science_age_bug && Unit_ai_can_start_science_age (this)) {
        Unit_start_science_age (this);
        return;
    }

    // Estimate the value of creating an army. First test if that's even a possiblity and if not leave the value at -1 so rushing production is
    // always preferable (note we can't use Unit_ai_can_form_army for this b/c it returns false for units not in a city). The value of forming
    // an army is "infinite" if we don't already have one and otherwise it depends on the army unit's shield cost +/- 25% for each point of
    // aggression divided by the number of armies already in the field.
    int num_armies = leaders[this->Body.CivID].Armies_Count;
    int form_army_value = -1;
    if ((this->Body.leader_kind & LK_Military) &&
        ((num_armies + 1) * p_bic_data->General.ArmySupportCities <= leaders[this->Body.CivID].Cities_Count) &&
        (p_bic_data->General.BuildArmyUnitID >= 0) &&
        (p_bic_data->General.BuildArmyUnitID < p_bic_data->UnitTypeCount)) {
        if (num_armies < 1)
            form_army_value = INT_MAX;
        else {
            form_army_value = p_bic_data->UnitTypes[p_bic_data->General.BuildArmyUnitID].Cost;
            int aggression_level = p_bic_data->Races[leaders[this->Body.CivID].RaceID].AggressionLevel; // aggression level varies between -2 and +2
            form_army_value = (form_army_value * (4 + aggression_level)) / 4;
            if (num_armies > 1)
                form_army_value /= num_armies;
        }
    }

    // Estimate the value of rushing production in every city on this continent and remember the highest one
    City * best_rush_loc = NULL;
    int best_rush_value = -1;
    if (p_cities->Cities != NULL)
        for (int n = 0; n <= p_cities->LastIndex; n++) {
            City * city = get_city_ptr (n);
            if ((city != NULL) && (city->Body.CivID == this->Body.CivID)) {
                Tile * city_tile = tile_at (city->Body.X, city->Body.Y);
                if ((continent_id == city_tile->vtable->m46_Get_ContinentID (city_tile)) &&
                    Unit_can_hurry_production (this, __, city, 0)) {
                    // Base value is equal to the number of shields rushing would save
                    int value = City_get_order_cost (city) - City_get_order_progress (city) - city->Body.ProductionIncome;
                    if (value <= 0)
                        continue;

                    // Consider distance: Reduce the value by the number of shields produced while the leader is en route to rush.
                    // This isn't an exact measure b/c the unit can spend more than its max MP per turn but it should be a close
                    // estimate.
                    int dist_in_turns; {
                        int dist_in_mp;
                        Trade_Net_set_unit_path (p_trade_net, __, this->Body.X, this->Body.Y, city->Body.X, city->Body.Y, this, this->Body.CivID, 1, &dist_in_mp);
                        dist_in_mp += this->Body.Moves; // Add MP already spent this turn to the distance
                        int max_mp = Unit_get_max_move_points (this);
                        if ((dist_in_mp >= 0) && (max_mp > 0))
                            dist_in_turns = dist_in_mp / max_mp;
                        else
                            continue; // No path or unit cannot move
                    }
                    value -= dist_in_turns * city->Body.ProductionIncome;
                    if (value <= 0)
                        continue;

                    // Consider corruption: Scale down the value of rushing an improvement by the corruption rate of the city.
                    // This is to reflect the fact that infrastructure is more valuable in less corrupt cities. But do not apply
                    // this to wonders since their benefit is in most cases not lessened by local corruption.
                    Improvement * improv = (city->Body.Order_Type == COT_Improvement) ? &p_bic_data->Improvements[city->Body.Order_ID] : NULL;
                    int is_wonder = (improv != NULL) && (0 != (improv->Characteristics & (ITC_Small_Wonder | ITC_Wonder)));
                    if ((improv != NULL) && (! is_wonder)) {
                        int good_shields = city->Body.ProductionIncome;
                        int corrupt_shields = city->Body.ProductionLoss;
                        if (good_shields + corrupt_shields > 0)
                            value = (value * good_shields) / (good_shields + corrupt_shields);
                        else
                            continue;
                    }

                    if ((value > 0) && (value > best_rush_value)) {
                        best_rush_loc = city;
                        best_rush_value = value;
                    }
                }
            }
        }

    // Hurry production or form an army depending on the estimated values of doing so. We might have to move to a (different) city if that's where
    // we want to rush production or if we want to form an army but aren't already in a city.
    City * in_city = get_city_ptr (tile->vtable->m45_Get_City_ID (tile));
    City * moving_to_city = NULL;
    if ((best_rush_loc != NULL) && (best_rush_value > form_army_value)) {
        if (best_rush_loc == in_city) {
            Unit_hurry_production (this);
            return;
        } else
            moving_to_city = best_rush_loc;
    } else if ((form_army_value > -1) && (Unit_ai_can_form_army (this))) {
        Unit_form_army (this);
        return;
    } else if (in_city == NULL) {
        // Nothing to do. Try to find a close, established city to move to & wait in.
        int lowest_unattractiveness = INT_MAX;
        if ((in_city == NULL) && (p_cities->Cities != NULL))
            for (int n = 0; n <= p_cities->LastIndex; n++) {
                City * city = get_city_ptr (n);
                if ((city != NULL) && (city->Body.CivID == this->Body.CivID)) {
                    Tile * city_tile = tile_at (city->Body.X, city->Body.Y);
                    if (continent_id == city_tile->vtable->m46_Get_ContinentID (city_tile)) {
                        // TODO: Technically this distance calculation should account for edge wrapping
                        int unattractiveness = 10 * (int_abs (this->Body.X - city->Body.X) + int_abs (this->Body.Y - city->Body.Y));
                        unattractiveness = (10 + unattractiveness) / (10 + city->Body.Population.Size);
                        if (city->Body.CultureIncome > 0)
                            unattractiveness /= 5;
                        if (unattractiveness < lowest_unattractiveness) {
                            lowest_unattractiveness = unattractiveness;
                            moving_to_city = city;
                        }
                    }
                }
            }
    }
    if (moving_to_city) {
        int first_move = Trade_Net_set_unit_path (p_trade_net, __, this->Body.X, this->Body.Y, moving_to_city->Body.X, moving_to_city->Body.Y, this, this->Body.CivID, 0x101, NULL);
        if (first_move > 0) {
            Unit_set_escortee (this, __, -1);
            this->vtable->Move (this, __, first_move, 0);
            return;
        }
    }

    // Nothing to do, nowhere to go, just fortify in place.
    Unit_set_escortee (this, __, -1);
    Unit_set_state (this, __, UnitState_Fortifying);
}
It's 170 lines and took me a few days of free time to write including EXE analysis and debugging. Replacing all the unit AI would be a big job but still just small enough to be tempting. I estimate it would take maybe 2 months if mod development continues apace. Then again, in the software industry, a good rule of thumb to estimate time requirements is to take your best guess and multiply it by 5, and a 10 month job is not tempting.

As for what the replacement AI does, it's nothing exotic. It's similar to the base AI without the bugs and with some better heuristics. For example, it will always form an army if the AI player doesn't already have one and it knows rushing infrastructure is not worth much in highly corrupt cities (wonders excepted). It will always start a science age if it can, that's maybe not optimal but I couldn't think of a practical way to compute the value of a science age in shields so that it could be compared with rushing production. One "cute" feature is that it knows about the science age bug and the mod settings so it won't start one if the patch is not applied.



Unfortunately, I have to report that my version of Civ3Conquests is also incompatible with your patch.
That's too bad. It's looking like the GOG version of Complete is unique since so far no one has reported that they have a different version that matches it.
 
Flintlock, have I understood this correctly, that with the new code even AI military Great Leaders now can start a scientific Golden Age while the human player cannot do this ? Please make the new code about military leaders optional, so it can be disabled if needed.

I think in all cases it is more important for AI civs to receive armies instead of a scientific Golden Age. Only in the case when the number of cities is not enough to provide an army, it should be reflected about triggering a scientific Golden Age.
 
The replacement AI still enforces the normal rules that military leaders are needed to create armies and scientific leaders are needed to start science ages, and additionally it fixes the bug that allowed either of them to rush any kind of city project. My intention with this mod is always to leave the game rules unchanged unless they are deliberately changed by the player editing the config file.
 
Flintlock thank you very much for the answer and of course for all your highly appreciated work. :)
 
Is it possible to fix the AI's terraforming behavior like Soren did for PTW? Apparently they managed to get it to not irrigate penalized tiles in despotism. Also, the AI doesn't know how to clear marshes, build airfields and irrigate those mined plains after electricity.
 
So, I will expose my ignorance in several ways.

First, does the AI differentiate other civilizations in anything more than a rudimentary way (e.g. expansionist)?
Would it be possible to make multiple AI's such that the Arabs are different from the Americans in their expansionist ways?
Is that even desirable?
I can imagine a configurator depicting how each AI will perform on a micro level.
 
So, I will expose my ignorance in several ways.

First, does the AI differentiate other civilizations in anything more than a rudimentary way (e.g. expansionist)?
Would it be possible to make multiple AI's such that the Arabs are different from the Americans in their expansionist ways?
Is that even desirable?
I can imagine a configurator depicting how each AI will perform on a micro level.

The build often flags in the civilization part of the editor is what makes the AI play it differently. That's why AI Persia almost always outperforms AI Ottomans despite having the same traits. The Ottomans have "Artillery" and "Happiness" as their build often flags, which make no sense while Persia has "Offensive Units".

In the stock game the highest number of ticked boxes for build often is 5. According to Alexmann who made the AU mod a long time ago the max number for them is 6 before it's diluted. In order to have the AI perform decently all of them need "Worker, Production and Trade" as build often at least. The rest you can play around to suit your taste.
 
Is it possible to fix the AI's terraforming behavior like Soren did for PTW? Apparently they managed to get it to not irrigate penalized tiles in despotism. Also, the AI doesn't know how to clear marshes, build airfields and irrigate those mined plains after electricity.
Preventing the AI from irrigating grassland in Despotism should be easy. I expect I could find some point in the code where it checks if irrigating is possible/desirable and inject some code to indicate that it's not if doing so wouldn't improve the tile yield. Implementing new worker behaviors would be fairly difficult. I don't know what would be involved in getting the AI to replace tile improvements, that might be tough too.
First, does the AI differentiate other civilizations in anything more than a rudimentary way (e.g. expansionist)?
Would it be possible to make multiple AI's such that the Arabs are different from the Americans in their expansionist ways?
Is that even desirable?
So far, reading mostly the unit AI, I haven't seen any differences between civs. I know there are differences in AI selection of city production, but overall I would say the AI is almost identical for all civs. It would be nice to have multiple AIs, I've often thought (playing Civ 4, this isn't as big an issue in 3) that it's sad how the civs that like starting wars aren't actually any good at fighting them. I can think of two problems with multiple AIs: the first is the obvious one, it's a lot of work to implement, and the second is that it creates another layer of things that need to be balanced or games will become too predictable.
 
Speaking of old PTW codes, is it possible to choose between C3C and PTW codes for things like land bombard targeting and sea power flag unit behavior? Just like with barbarians, C3C ruined AI naval strength by forcibly tying down their sea power units to escorting transports. These units would just impotently sail along to get bombarded and then finished off without ever firing back.
 
I just now had a look at the naval power unit AI. It's 900 lines long, and I don't see any easy way to make that kind of change. Another solution is to reduce the escort requirement on naval transports to 1 or 2 ships and (I think) I know how to do that easily.
 
I was jut now scanning through the code in your spoiler, and my immediate thought was how reusable a function like thig coulfbr:

int has_adequate_escort; {
int escorter_count = 0;
int any_healthy_escorters = 0;
int index;
for (int escorter_id = Unit_next_escorter_id (this, __, &index, 1); escorter_id >= 0; escorter_id = Unit_next_escorter_id (this, __, &index, 0)) {
Unit * escorter = get_unit_ptr (escorter_id);
if ((escorter != NULL) && (escorter->Body.X == this->Body.X) && (escorter->Body.Y == this->Body.Y)) {
escorter_count++;
int remaining_health = Unit_get_max_hp (escorter) - escorter->Body.Damage;
any_healthy_escorters |= remaining_health >= 3;
}
}
has_adequate_escort = (escorter_count > 0) && any_healthy_escorters;
}
if ((! has_adequate_escort) && any_enemies_near_unit (this, 49)) {
Unit_set_state (this, __, UnitState_Fleeing);
byte done = this->vtable->work (this);
if (done || (this->Body.UnitState != 0))
return;
}

// Move along path if the unit already has one set
byte next_move = Unit_pop_next_move_from_path (this);
if (next_move > 0) {
this->vtable->Move (this, __, next_move, 0);
return;
}
 
I was jut now scanning through the code in your spoiler, and my immediate thought was how reusable a function like thig coulfbr:
In fact, there's already a function in the base game that computes something like has_adequate_escort but I didn't use it because I don't like its logic, it ignores the fact that escorters are damaged if they can't heal in place.
 
C3X Release 6
New in this version:
- Add option to increase AI artillery and bomber production
- Allow AI to grab artillery escorts from city defenders so its arty can be used in the field
- Improve leader unit AI: fix bug allowing rush of any city build, prefer forming armies, select better rush targets
- Improve AI army inclusion criteria: exclude HN & defensive units, fix movement check
- Fix for immobile unit disembark crash
- Fix for for houseboat bug
- Set LAA bit when creating modded EXE

Hard to sum this one up in a nice little GIF. Overall it allows the AI to use artillery and armies effectively which greatly spices up the game's combat. I wrote some more details about what's changed as comments in the config file, which I'll just quote here:
Code:
[== AI ENHANCEMENT ==]
; Allows artillery unit AI to grab escorts from city defenders, causing the AI to use its artillery offensively instead of leaving it in its cities.
use_offensive_artillery_ai = true

; Set to a number to encourage the AI to build more artillery, set to false or <= 0 for no change in AI unit build choices. The number is how many
; artillery units the AI will try to keep around for every 100 non-artillery combat land units it has. The encouragement only applies if the AI is
; already above a minimum of one defensive unit per city and one offensive unit per two cities. Default: 20.
ai_build_artillery_ratio = 20

; The above option controls how many artillery the AI will keep around, this one controls how aggressively it builds up to that limit. More
; specifically it controls how valuable (in percent) the AI thinks artillery is as a damage dealer compared to attacking units. Default: 50.
ai_artillery_value_damage_percent = 50

; Like the artillery ratio, shifts the AI's build priority from fighters to bombers to keep around this many bombers per 100 fighters. Default: 70.
ai_build_bomber_ratio = 70

; Completely replaces the leader unit AI with custom code. The custom version is similar in spirit to the original but fixes several major issues:
; 1. It fixes a bug that allowed the AI to rush any kind of city production, including units and (with an MGL) wonders
; 2. Prioritizes army formation over rushing production when appropriate
; 3. When rushing, prioritizes wonders and costly improvements in high value cities
replace_leader_unit_ai = true

; Replaces the AI code that determines whether a unit would be a good addition to an army. The replacement code fixes a major bug that prevented the
; AI from filling its armies, prevents the AI from including hidden nationality units in armies to avoid crashes, and removes some logic that
; encouraged the AI to mix unit types in armies.
fix_ai_army_composition = true

Edit: Forgot the link (does it matter? it's the same as always): https://forums.civfanatics.com/resources/c3x.28759/



Looking forward to the next version, I'm going to take a break from tinkering with the AI, which honestly got pretty exhausting. R7 will for sure include the ability to adjust the minimum distance between cities, in fact I already have that feature mostly done, it just needs some polish. Another thing I intend to do is add two convenience features to the trade screen: (1) have the "set gold amount" popup default to the best value and (2) add some arrow buttons to quickly switch between civs. Maybe I'll implement some other things as well, but I'd like to get R7 done in 2-3 weeks like the older versions. R6 took much longer because I kept thinking I'll just add one little thing, do one little adjustment, etc., and then testing the AI changes took quite a while as well.
 
300px-You%27re_Breathtaking.jpg
 
I completely second, what Vuldacon posted. :) Flintlock, with the new version of your patch, you dived deeply into the core problems of C3C, into problems that I thought that will never be touched by anyone. You did boldly go, where no other civer has be gone before. :goodjob:

I´m very excited about the reports coming from civers especially about the AI use of artillery. Unfortunately in the next days my civtime is somewhat limited due to RL.
 
Can you explain this to me? My antivirus found this after running the INSTALL.bat
srjnMUr
 
Back
Top Bottom