1. We have added a Gift Upgrades feature that allows you to gift an account upgrade to another member, just in time for the holiday season. You can see the gift option when going to the Account Upgrades screen, or on any user profile screen.
    Dismiss Notice

Sub Bug Fix and other Adventures in EXE Modding

Discussion in 'Civ3 - Creation & Customization' started by Flintlock, Jan 28, 2021.

  1. Flintlock

    Flintlock King

    Joined:
    Sep 25, 2004
    Messages:
    686
    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.



    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.
     
    Predator145, Shmelkin and tjs282 like this.
  2. Civinator

    Civinator Blue Lion Supporter

    Joined:
    May 5, 2005
    Messages:
    7,043
    Gender:
    Male
    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.
     
  3. Flintlock

    Flintlock King

    Joined:
    Sep 25, 2004
    Messages:
    686
    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.
     
    Predator145 likes this.
  4. Civinator

    Civinator Blue Lion Supporter

    Joined:
    May 5, 2005
    Messages:
    7,043
    Gender:
    Male
    Flintlock thank you very much for the answer and of course for all your highly appreciated work. :)
     
  5. Predator145

    Predator145 Warlord

    Joined:
    May 22, 2020
    Messages:
    141
    Gender:
    Male
    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.
     
  6. ynot56

    ynot56 Warlord

    Joined:
    Jun 29, 2008
    Messages:
    158
    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.
     
  7. Predator145

    Predator145 Warlord

    Joined:
    May 22, 2020
    Messages:
    141
    Gender:
    Male
    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.
     
  8. Flintlock

    Flintlock King

    Joined:
    Sep 25, 2004
    Messages:
    686
    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.
    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.
     
    tjs282 and Predator145 like this.
  9. Predator145

    Predator145 Warlord

    Joined:
    May 22, 2020
    Messages:
    141
    Gender:
    Male
    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.
     
  10. Flintlock

    Flintlock King

    Joined:
    Sep 25, 2004
    Messages:
    686
    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.
     
    tjs282, Nathiri and Predator145 like this.
  11. Civinator

    Civinator Blue Lion Supporter

    Joined:
    May 5, 2005
    Messages:
    7,043
    Gender:
    Male
    Flintlockock, this would be very good fixes. :thumbsup:
     
  12. Ozymandias

    Ozymandias I saw the Great Library burn. Supporter

    Joined:
    Nov 5, 2001
    Messages:
    9,840
    Gender:
    Male
    Location:
    The lone and level sands
    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;
    }
     
  13. Flintlock

    Flintlock King

    Joined:
    Sep 25, 2004
    Messages:
    686
    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.
     
  14. Flintlock

    Flintlock King

    Joined:
    Sep 25, 2004
    Messages:
    686
    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.
     
  15. Nanuk

    Nanuk Warlord

    Joined:
    Jan 12, 2017
    Messages:
    216
    Location:
    USA
     
    Flintlock likes this.
  16. Vuldacon

    Vuldacon Dedicated to Excellence Supporter

    Joined:
    Nov 14, 2001
    Messages:
    6,759
    Gender:
    Male
    Location:
    USA
    Flintlock... Your Work is in two words a Profound Accomplishment and yes, take a well deserved break... don't want you burning out :clap::dance::beer::thanx:
     
    Flintlock and Takeo like this.
  17. Civinator

    Civinator Blue Lion Supporter

    Joined:
    May 5, 2005
    Messages:
    7,043
    Gender:
    Male
    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.
     
    Flintlock likes this.
  18. Predator145

    Predator145 Warlord

    Joined:
    May 22, 2020
    Messages:
    141
    Gender:
    Male
    Another giant milestone. It seems like the hardest riddles have been solved.
     
    Flintlock likes this.
  19. Lionic

    Lionic King

    Joined:
    Feb 3, 2010
    Messages:
    703
    This is the most impressive development since the release of C3C itself.
     
  20. Scary

    Scary Chieftain

    Joined:
    Jun 1, 2021
    Messages:
    1
    Can you explain this to me? My antivirus found this after running the INSTALL.bat
     

Share This Page