EXE Patching Case Study: Making min city distance adjustable with C3X

Flintlock

Emperor
Joined
Sep 25, 2004
Messages
1,040
I'm going to describe the process of making a simple EXE modification with C3X so you guys can get an idea of what's involved. For some background, if you haven't already read it, here's some info about how my mod works: https://forums.civfanatics.com/threads/analysis-exe-patching.666413/page-2#post-16076100. The goal of the modification is to make the minimum distance between cities adjustable, to allow it to be increased or decreased by a given number of tiles.

The first step, a minor one, is to put together a scenario that allows us to test our changes. Here it is:
1 - Test Scenario Setup.gif

Here we can slide the settler around to see where the game will let us found a new city, if the "found city" command button disappears then that location is invalid.

Secondly, or for the first real step, we must analyze the game's executable to figure out how it determines city location validity so we know where & how to make changes. I've been using Ghidra to do this, I posted my project folder here: https://forums.civfanatics.com/thre...es-in-exe-modding.666881/page-7#post-16066922 (though it's out of date by this point) and a bit about how to load it up. The analysis is the hardest to give an overview of because it often has to be done ad hoc. Often reading the decompiled code isn't enough and it's necessary to observe the game running in a debugger, probe its memory (I have a Python script to help with that), construct special scenarios, inject code to monitor & show the game's internal state, etc., whatever you can think of to figure out what's going on. Fortunately none of that is necessary in this case because I already have a starting point: the method Unit::can_perform_command. I don't remember how I found this function, it was some months ago when I starting work on stack bombard. It's about 1030 lines decompiled so I can't paste the whole thing here but internally it basically just switches over an integer code for a given command on a given unit. Conveniently, Antal already decoded the unit command codes (in the Unit_Command_Values enum in Civ3Conquests.h) so we can easily see that the found city command is code 0x20000002. Searching for that string in the decompiled code turns up this:
Code:
case 0x20000002:
    iVar8 = FUN_005f3160(&bic_data.Map,(this->Body).X,(this->Body).Y,(this->Body).CivID,0x01);
    if (iVar8 == 0) {
        return true;
    }
break;
So the function at 0x5F3160 determines if it's possible to found a city. Note that the settler unit is not considered, it's a function of the tile x & y coords, civ ID (i.e. player index: 0 = barbs, 1 = human in a SP game, 2+ are the AIs), and what looks like a boolean parameter.

Next we have to analyze this function. Here again Antal is a big help, most of its calls are virtual calls to a Tile object and Antal has already named most of the functions in the Tile vtable. If we didn't have Antal's work to build off of, we'd have to do something like run the game with a breakpoint set at this function and see when it returns non-zero values (we already know zero means a valid city location). Here's the decompiled function, I've already named & typed the parameters based on the values passed at the call site:
Code:
int __thiscall FUN_005f3160(Map *this,int tile_x,int tile_y,int civ_id,char param_4)
{
    Map_vtable *pMVar1;
    int iVar2;
    bool bVar3;
    char cVar4;
    Tile *pTVar5;
    int iVar6;
    undefined4 uVar7;
    int iVar8;
    int iVar9;
    int unaff_EBX;
    int unaff_ESI;
    int unaff_EDI;
    int iVar10;
    int iStack8;

    if (param_4 != 0) {
        pTVar5 = (*this->vtable->m12_Get_Tile_by_XY)(this,tile_x,tile_y);
        bVar3 = Tile::has_city(pTVar5);
        if (bVar3 != false) {
            return 2;
        }
    }
    pTVar5 = (*this->vtable->m12_Get_Tile_by_XY)(this,tile_x,tile_y);
    cVar4 = (*pTVar5->vtable->m26_Check_Tile_Building)();
    if (cVar4 != '\0') {
        pTVar5 = (*this->vtable->m12_Get_Tile_by_XY)(this,tile_x,tile_y);
        iVar6 = (*pTVar5->vtable->m70_Get_Tile_Building_OwnerID)();
        if (iVar6 != civ_id) {
            return 2;
        }
    }
    pTVar5 = (*this->vtable->m12_Get_Tile_by_XY)(this,tile_x,tile_y);
    cVar4 = (*pTVar5->vtable->m7_Check_Barbarian_Camp)(0);
    if (cVar4 == '\0') {
        pTVar5 = (*this->vtable->m12_Get_Tile_by_XY)(this,tile_x,tile_y);
        pMVar1 = this->vtable;
        uVar7 = (*pTVar5->vtable->m50_Get_Square_BaseType)(register0x00000010);
        (*(code *)pMVar1->m35_Get_BIC_Sub_Data)(0x52524554,uVar7);
        if (*(char *)(unaff_EBX + 0x78) == '\0') {
            pTVar5 = (*this->vtable->m12_Get_Tile_by_XY)(this,tile_x,tile_y);
            cVar4 = (*pTVar5->vtable->m35_Check_Is_Water)();
            return 4 - (uint)(cVar4 != '\0');
        }
        iVar6 = 1;
        iVar10 = 0;
        do {
            if (8 < iVar6) {
                return iVar10;
            }
            neighbor_index_to_displacement(iVar6,(int *)&stack0xffffffe8,(int *)&stack0xffffffec);
            iVar9 = unaff_EDI + tile_x;
            if ((this->Flags & 1U) != 0) {
                iVar8 = this->Width;
                if (iVar9 < 0) {
                    iVar9 = iVar8 + iVar9;
                }
                else {
                    if (iVar8 <= iVar9) {
                        iVar9 = iVar9 - iVar8;
                    }
                }
            }
            iVar8 = unaff_ESI + tile_y;
            if ((this->Flags & 2U) != 0) {
                iVar2 = this->Height;
                if (iVar8 < 0) {
                    iVar8 = iVar2 + iVar8;
                }
                else {
                    if (iVar2 <= iVar8) {
                        iVar8 = iVar8 - iVar2;
                    }
                }
            }
            if ((((-1 < iVar9) && (iVar9 < this->Width)) && (-1 < iVar8)) && (iVar8 < this->Height)) {
                pTVar5 = (*this->vtable->m12_Get_Tile_by_XY)(this,iVar9,iVar8);
                bVar3 = Tile::has_city(pTVar5);
                tile_y = iStack8;
                if (bVar3 != false) {
                    iVar10 = 5;
                }
            }
            iVar6 = iVar6 + 1;
        } while (iVar10 == 0);
        return iVar10;
    }
    return 2;
}
The decompiler gets confused by functions with many virtual calls, like this one, so some calls are missing parameters and some variables have incorrect storage, specifically the "unaff", "stack", and "register" ones. Still, the function is understandable. Notice how it returns 2 if the tile is already blocked by a building or, if param_4 is set, by a city, returns 4 if the terrain disallows city founding (Get_Square_BaseType gets the terrain type and Get_BIC_Sub_Data with the code 0x52524554 gets terrain data) and subtracts 1 to return 3 for water specifically. The most relevant part is the do-while loop which loops over surrounding tiles and returns 5 if any of them has a city already on it. We can sum up our findings by saying the function returns the following enum:
Code:
typedef enum city_loc_validity
{
    CLV_OK = 0,
    CLV_1,
    CLV_BLOCKED,
    CLV_WATER,
    CLV_INVALID_TERRAIN,
    CLV_CITY_TOO_CLOSE
} CityLocValidity;
I'm going to name the function "check_city_location" and name param_4 "check_for_city_on_tile". It's also necessary to add the enum definition above to Civ3Conquests.h so it's part of the mod.

The third step is modifying the EXE. We will intercept the return value of check_city_location, i.e., we will replace it with a function that calls the original then runs some additional logic to alter the value that gets returned. Replacing the function is actually very easy because I have entirely automated the process at this point. Add the following entry to the array in civ_prog_objects.h:
Code:
{OJ_INLEAD, "Map_check_city_location", "CityLocValidity (__fastcall *) (Map *this, int edx, int tile_x, int tile_y, int civ_id, byte check_for_city_on_tile)", 0x5F3160, 0x0},
This array contains the names, types, and locations of all EXE objects that we care about, in addition it contains a task for the patcher to perform on each one. Each entry has five fields, explained here, in order:
  • Job: This tells the patcher what to do with the object. OJ_INLEAD tells it to replace a function by means of an inlead. Other options are OJ_DEFINE which merely defines the object for the injected code and OJ_REPL_VPTR which replaces a function by means of overwriting its entry in a vtable.
  • Name: The patcher defines a preprocessor macro with this name to make the object available to the injected code. In this case, the macro will expand to a function pointer pointing to an inlead to check_city_location which we can call to access the original function. Note I've prepended "Map_" to the name because the function is a method of the Map object, this is just convention.
  • Type: The C type of this object. The macro mentioned above will include a type cast, this is its contents.
  • GOG address: The address of the object in the GOG EXE.
  • Steam address: The address of the object in the Steam EXE. For example purposes just leave this as zero, meaning this example will only work for the GOG EXE. Finding the real value is a matter of locating the same function in the Steam EXE.
Notice the type field is almost the same as the function's type in the decompiler. There's one minor difference in that I used "byte" instead of "bool" for param_4. The injected code is all C so there is no bool type out of the box, we could define one of course, I just haven't done so, and not for any particular reason. Instead I use byte which is a typedef of unsigned char. There is also one major difference, the use of the fastcall convention instead of the thiscall convention. This is also related to the awkwardness of interfacing C++ with C. C has no classes and hence no use for the thiscall convention, and the compiler that the mod uses, TCC, doesn't even recognize it. As a work around we can use the fastcall convention with a dummy second parameter. This works because in fastcall the first parameter is stored in register ecx, like the this pointer in thiscall, and the second parameter gets stored in edx, which is unused in thiscall and trashed across calls, and all the remaining parameters get pushed on the stack like usual. This is not a big deal but it's something to be aware of, the dummy second parameter is necessary when defining most C++ functions for C and when calling them from the injected code as we'll see soon.

Now head over to injected_code.c and write a replacement function. Start with a simple replacement as a sanity check and a nice small example:
Code:
CityLocValidity __fastcall
patch_Map_check_city_location (Map * this, int edx, int tile_x, int tile_y, int civ_id, byte check_for_city_on_tile)
{
    CityLocValidity base_result = Map_check_city_location (this, __, tile_x, tile_y, civ_id, check_for_city_on_tile);
    return (base_result == CLV_BLOCKED) ? CLV_BLOCKED : CLV_OK;   
}
Note the two underscores as the second argument, that's a macro that expands to 0. The underscores are a visual reminder that that's a meaningless placeholder argument. As for what this replacement actually does, it modifies the check_city_location function to allow cities anywhere except tiles that are blocked by a building or city. Loading up the test scenario, we see that, as expected, this allows us to found cities on mountains and next to existing cities:
2 - Founding Anywhere.gif

So, fundamentally, that's it! If you can write C, you can write any arbitrary logic in this function to determine what the game considers a valid city location. To fulfill the original objective, we need to modify how the CLV_CITY_TOO_CLOSE check is performed to allow the minimum distance to be adjusted by a given number of tiles. Here's the code to do so, I've added some comments to explain how it works:
Code:
CityLocValidity __fastcall
patch_Map_check_city_location (Map *this, int edx, int tile_x, int tile_y, int civ_id, byte check_for_city_on_tile)
{
    int const adjustment_to_min_city_distance = 2; // TODO: Load this from the config file
    CityLocValidity base_result = Map_check_city_location (this, __, tile_x, tile_y, civ_id, check_for_city_on_tile);

    // If adjustment is zero, make no change
    if (adjustment_to_min_city_distance == 0)
        return base_result;

    // If adjustment is negative, ignore the CITY_TOO_CLOSE objection to city placement. The base code enforces a minimum separation of 1 and the
    // separation cannot go below zero.
    else if (adjustment_to_min_city_distance < 0)
        return (base_result == CLV_CITY_TOO_CLOSE) ? CLV_OK : base_result;

    // If we have an increased separation we might have to exclude some locations the base code allows.
    else if ((adjustment_to_min_city_distance > 0) && (base_result == CLV_OK)) {
        // Check tiles around (x, y) for a city. Because the base result is CLV_OK, we don't have to check neighboring tiles, just those at
        // distance 2, 3, ... up to (an including) the adjustment + 1
        for (int dist = 2; dist <= adjustment_to_min_city_distance + 1; dist++) {

            // "vertices" stores the unwrapped coords of the tiles at the vertices of the square of tiles at distance "dist" around
            // (tile_x, tile_y). The order of the vertices is north, east, south, west.
            struct vertex {
                int x, y;
            } vertices[4] = {
                {tile_x         , tile_y - 2*dist},
                {tile_x + 2*dist, tile_y         },
                {tile_x         , tile_y + 2*dist},
                {tile_x - 2*dist, tile_y         }
            };

            // neighbor index for direction of tiles along edge starting from each vertex
            // values correspond to directions: southeast, southwest, northwest, northeast
            int edge_dirs[4] = {3, 5, 7, 1};

            // Loop over verts and check tiles along their associated edges. The N vert is associated with the NE edge, the E vert with
            // the SE edge, etc.
            for (int vert = 0; vert < 4; vert++) {
                wrap_tile_coords (&p_bic_data->Map, &vertices[vert].x, &vertices[vert].y);
                int  dx, dy;
                neighbor_index_to_displacement (edge_dirs[vert], &dx, &dy);
                for (int j = 0; j < 2*dist; j++) { // loop over tiles along this edge
                    int cx = vertices[vert].x + j * dx,
                        cy = vertices[vert].y + j * dy;
                    wrap_tile_coords (&p_bic_data->Map, &cx, &cy);
                    if (city_at (cx, cy))
                        return CLV_CITY_TOO_CLOSE;
                }
            }

        }
        return base_result;

    } else
        return base_result;
}
Load up the test scenario again. Now because the min distance is increased by 2 (see the first line of the replacement function) we expect to be able to found on the second ring of desert but not inside of it.
3 - Min Sep 3 Tiles.gif

So it works. The last thing to do for the mod would be to load the min distance adjustment from the config file instead of hard coding it, but the code to make that happen is not special or interesting so I'm going to leave it out of this demo. But the jist of it is that the mod injects some data into the program in addition to the code. Part of the data is a config struct that gets filled out when the program is launched with values parsed from default.c3x_config.ini. So making the adjustment value configurable is a matter of adding it as a field to that struct, adding an entry to the base config struct that's used if the config file is missing, and adding a case to the parser to load that value.

That's it. One last remark, if you have a recent version of C3X you can add this feature to it easily. There are only three changes that need to be made: the CityLocValidity enum must be added to Civ3Conquests.h, the entry for Map_check_city_location must be added to the array in civ_prog_objects.h, and the patch_Map_check_city_location function must be added to injected_code.c.
 
Flintlock, for me it is not easy to follow these explanations, but my respect for your work is gigantic. :hatsoff:

So the function at 0x5F3160 determines if it's possible to found a city. Note that the settler unit is not considered, it's a function of the tile x & y coords, civ ID (i.e. player index: 0 = barbs, 1 = human in a SP game, 2+ are the AIs), and what looks like a boolean parameter.

Can it be, that this boolean parameter concerns the type of terrain, as in the Firaxis editor it can be set for every kind of terrain, if this terrain allows cities, or is the terrain only be checked if param_4 is set, by a city, returns 4 if the terrain disallows city founding (Get_Square_BaseType gets the terrain type and Get_BIC_Sub_Data with the code 0x52524554 gets terrain data) ?
 
Can it be, that this boolean parameter concerns the type of terrain, as in the Firaxis editor it can be set for every kind of terrain, if this terrain allows cities, or is the terrain only be checked if param_4 is set, by a city, returns 4 if the terrain disallows city founding (Get_Square_BaseType gets the terrain type and Get_BIC_Sub_Data with the code 0x52524554 gets terrain data) ?
The terrain check is always performed. param_4, which I eventually named check_for_city_on_tile, controls whether the function checks for an existing city on the tile, then if there is one it returns the error 2 (= CLV_BLOCKED).
 
@Flintlock, thanks for explaining all this stuff in such detail! I think I understood enough of it to give it a try myself. However, there are still a few open points:
  1. I use neither the GoG nor the Steam exe, but my original CD installation with the No-CD patch from PCGames Magazine. (See https://forums.civfanatics.com/threads/civ-3-windows-update-kb3086255-safedisc.552308/ ) What would be required to make your Ghidra project work with that? If I started by decompiling my exe, can I then somehow carry over the knowledge, you accumulated so far about the already identified functions and their entry points, the identified C++ objects and structures etc, to my project? Or would I have to do all the reverse engineering from scratch?
  2. And the above would probably be a big obstacle for distributing our modified game to the player base, wouldn't it? There are about half a dozen different executables out there that people use. (All of them must be different, because all CD releases (Civ3 Conquests, Civ3 Complete, Chronicles of Civilization and probably a couple more...) originally came with some sort of copy protection. Each came with different CDs, so the code needs to check for a different CD to be present in the drive, so the code must be different... Even if the function entry points are off by just one byte, the mod will no longer work...) Maintaining so many different versions of C3X would be a nightmare... (considering that if we just get one function address wrong, the game probably just crashes once that function is used...)
    You said elsewhere that your project can be used in two ways now (since R5): as a "memory patcher" or for compiling a "replacement executable". I assume, the replacement exe can then by used by everybody, independent of which installation that person is using and which exe first came with that installation?
    In that case we could solve that problem by including a pre-compiled exe in the mod instead of install.bat/run.bat, right?
    Of course, people would have to "trust" us that we don't install a virus on their system... (But then: a script that compiles an executable on my disc -- using software from the NSA -- requires the same level of "trust"... :mischief:)
  3. Just in case that Ozymandias succeeds in getting his hands on the original source code, all the work done in this project here can easily be carried over, right? Instead of replacing functions, we could just "port" the new code from those replacement functions back to the "original" functions in the Firaxis code base.
    However, if we can't get the code and decide to start from scratch with C# and Godot, then the work invested here is basically "lost", right?
  4. I noticed many years ago, that Civ3Conquests.exe refuses to start, if it finds that MS Visual Studio (devenv.exe) is running... Does this affect all the exes, can we replace the function that does this check, or isn't it even worth to bother, as Visual Studio is not really helpful for doing the reverse engineering?
 
@Lanzelot
  1. You could import all of the data types at least. That would hopefully be easy since Ghidra has features to export and import types to/from C header files, although when I imported Antal's work (exported from IDA) it was a massive pain and required me to write various custom scripts to process the header to get it into a form Ghidra would accept. The real problem, like you mentioned, is that the addresses of functions and global variables are all different across EXE versions. So far I've been bridging the difference between the GOG executable, which I work on primarily, and the Steam one, but it's a tedious job that I wouldn't want to do for even a third version. This job could be automated, but it's not trivial to do so since it requires some judgement, the same function in two different executables has similar length, similar outgoing calls, similar incoming calls, usually the same string references, but it's never exactly the same in any way except signature.
  2. If we're going to try to support all the different executables out there it would be worth it to automate the process of finding function addresses. The alternative is to distribute the modded EXE, that would work for the most part except for some small issues like the Steam EXE has its own special labels.txt and when I was using Antal's EXE I found it didn't work with the conquests.ini file I was using with the GOG EXE, and vice-versa, changing the INI so it worked with Antal's broke GOG. Unfortunately there's no practical way to do mods of this kind without distributing an executable and just saying "trust me it's not a virus". Even though C3X itself is entirely open source implemented in C, the mod has to contain a precompiled C compiler (it's in tcc/tcc.exe and tcc/libtcc.dll) because on Windows there's no system C compiler. Even if it were implemented in Python or whatever, it would need to include an interpreter.
  3. Yes, I imagine it would be easy to port C3X over to the base game if we had the source code. It would mostly be a matter of mapping the function & type names Antal and I have assigned to the ones Firaxis used. The only challenge I can think of is that we would probably want to rearchitect parts of the mod, for example stack bombard doesn't have its own mode action (a mode action is like go-to, bombard, rebase, etc.), because there's no practical way I could see to add one. Instead it awkwardly piggy-backs off of bombard mode, but there's no reason to implement it like that with the ability to change the source.
  4. Interesting. I ran a quick search on the GOG EXE for the string "devenv" and nothing came up. If that check is still present, it's unfortunately not done in an obvious way. I don't use Visual Studio personally, it might be useful since for reverse engineering you often need to use a debugger. For that I've been using OllyDbg, I used to use GDB and still keep it in mind since it has one major feature OllyDbg doesn't, Python scriptability.
 
that would work for the most part except for some small issues like the Steam EXE has its own special labels.txt and when I was using Antal's EXE I found it didn't work with the conquests.ini file I was using with the GOG EXE, and vice-versa, changing the INI so it worked with Antal's broke GOG.

That is unfortunate indeed. Sounds like the different exes have different ini-parsers and these are not very robust, i.e. the ini-parser barfs, if it finds a property in the conquests.ini that it doesn't know (or if the conquests.ini is lacking a property that the parser considers to be mandatory). Must be something of the kind?!
In that case we might be able to overcome that problem by also replacing the ini-parser-function with our own version (which would be able to handle all possible properties and/or just ignores unknown properties).

So rather than trying to support a handful of different exes and their binary structure, the best way forward seems to me:
  1. Everyone who wants to contribute, works on one exe (let's say the GoG exe, as that is the one where you have done the most work already. I would then need to get a copy of that somehow.)
  2. Fix the ini-parser
  3. Compile the end-result into a new exe and distribute that

Regarding Visual Studio: I just tried again with the No-CD exe I am currently using, and the check seems to be gone: I can start it and even attach to the process no problem.
 
It seems the steam exe (other than the GOG exe) has a special programming that it can read that line Unknown between Ping and host in the labels text file. At least this was the answer I received after wondering why steam didn´t put the line Unknown at the end of the labels text file, where it couldn´t play havoc with existing mods and scenarios: https://forums.civfanatics.com/thre...-from-this-civ3-discord.665577/#post-16099185

Flintlock here I repeat some thoughts about the minimum distance between cities, that I wrote in another thread (better is better):

Flintlock, as you posted you are currently working on an adjustable minimum distance for cities, please take into consideration, that a minimum distance of one to the next city (meaning founding a city directly next to the tile of an existing city) can be a lot of fun when creating canals, but can be a horror when attacking other civs. So, if possible, a distance of 1 between cities should only be possible when founding a city next to a city of the same civ, but never when bordering to a city of another civ.
 
Flintlock, as you posted you are currently working on an adjustable minimum distance for cities, please take into consideration, that a minimum distance of one to the next city (meaning founding a city directly next to the tile of an existing city) can be a lot of fun when creating canals, but can be a horror when attacking other civs. So, if possible, a distance of 1 between cities should only be possible when founding a city next to a city of the same civ, but never when bordering to a city of another civ.
Alright, that would be easy to implement. I'll make a note of it so I don't forget.

The adjustable min city distance feature will be included in the mod in R7. I could try to slip it into R6 at the last minute but last time I tried that I ended up breaking something (that was R4, which needed a fixed version a few days after release).
 
Flintlock, thank you for implementing this and please take all your time you need. :) p.s. Vuldacon had the same opinion about this setting.
 
... Back to the notion of a "Release" - @Flintlock: you, @Lanzelot, @Puppeteer and @WildWeazel are doing some mind boggling work. BUT, IMHO, if this is the New Direction for this project (which I applaud) I think that coordinated releases of new .exes is critical for:
  • Overall community understanding, acceptance, and use.
  • Feedback re: any "hiccups" (a.k.a., "testing.")
  • Integration with existing "favorite" .exes like the "NoRaze" / ">512 City patch" is (once again, IMHO) CRITICAL for all of these extraordinary efforts not to devolve into an impossible to build GW, like the Tower Of Babel.
  • I also can't imagine that it would be anything less than ideal for new features to be directly integrated with @Quintillus' editor.
I understand how this might feel like an anathema to your (collective and specific) breakneck discoveries and implementations but, when all is said and done, wouldn't you like to see your work actually be in widespread use?
 
I agree that individual Experimentation and Testing has been an important step for understanding and to find what can be accomplished.
That said, I also agree with Eric that a collective development decision in in order as well as a collaborative effort to combine the work.
 
Integration with existing "favorite" .exes like the "NoRaze" / ">512 City patch" is (once again, IMHO) CRITICAL

Ozy, the >512 City patch was never working correctly in any version (indeed it is working very, very poorly and in a very limited way only for the human player) and therefore it would not be 'Integrated', but must be invented completely new up from the basics if really needed, and the NoRaze Patch is integrated in the Flintlock patch.
 
Ozy, the >512 City patch was never working correctly in any version (indeed it is working very, very poorly and in a very limited way only for the human player) and therefore it would not be 'Integrated', but must be invented completely new up from the basics if really needed, and the NoRaze Patch is integrated in the Flintlock patch.

:hammer2: I'd forgotten about the "> 512" if (for no other reason) I never saw any use for it, for myself.

Irrespective, @Flintlock & @Quintillus - Flintlock is providing options in his ever evolving .exe which I firmly believe would be better served via editor than by editing a config file (which would certainly make many would-be, non-tech-savvy players flinch at.) A couple of obvious selections would be:
  • Limiting railroad movement to a certain number of tiles. (BTW, I hope "infinite" is still an option: it would take far less time than most turns represent to fully transit the Trans-Siberian RR.)
  • No raze Y/N.
 
On another note - antal1987 left some interesting tidbits that I'm certainly curious about, and (given that his work is scattered all over the C&C forum) I'm wondering if anyone is thinking about revisiting, specifically:
 
Top Bottom