Modding Civ I - Playing 1-to-1 or... alone !

Discussion in 'Civ1 - General Discussions' started by darkpanda, Jul 1, 2013.

  1. darkpanda

    darkpanda Dark Prince

    Joined:
    Oct 28, 2007
    Messages:
    611
    One more small hack to CIV.EXE, this time enabling to play against only 1 ... or even 0 opponent!

    To make this possible, you need to edit the following 2 byte sequences in your CIV.EXE:

    Code:
    [COLOR="Red"][B]Original[/B][/COLOR]:
    0002F976h: 83 7E F6 [B][COLOR="Red"]02[/COLOR][/B]
    0002F998h: B8 06 00 50 B8 [B][COLOR="Red"]02[/COLOR][/B] 00 50
    
    [COLOR="Blue"][B]Patched[/B][/COLOR]:
    0002F976h: 83 7E F6 [B][COLOR="Blue"]00[/COLOR][/B]
    0002F998h: B8 06 00 50 B8 [B][COLOR="Blue"]00[/COLOR][/B] 00 50
    
    The offsets above are given for EN versions of Civ (474.01, 474.05 and 475.01).
    For FR versions, the offsets are 30D76h and 30D98h.

    Hereunder, what it looks like in Civ:



    Notes:
    - as you can see in the selection screen, I also hacked Civ to enable "8 Civilizations", but it doesn't seem to have any effect...
    - when playing along (1 Civilization), I could build a city and some units without the game ending in victory...


    Cheers!
     

    Attached Files:

  2. kirkham7

    kirkham7 Warlord

    Joined:
    Dec 22, 2012
    Messages:
    288
    Location:
    Hayward, CA
    Awesome! This is amazing. One question though, is there a way to be able to put 2 civilizations and then get to pick from the whole list? That would be really interesting.
     
  3. darkpanda

    darkpanda Dark Prince

    Joined:
    Oct 28, 2007
    Messages:
    611
    It's probably a lot more complex to hack (or even impossible), but sounds interesting indeed :) I'll have a look...
     
  4. kirkham7

    kirkham7 Warlord

    Joined:
    Dec 22, 2012
    Messages:
    288
    Location:
    Hayward, CA
    Cool. I'm not sure if it's possible either, but it sounds like a great idea to be able to enable all civilizations no matter how many you choose. I'd like to be able to choose the Aztecs when on 2 civilizations...
     
  5. darkpanda

    darkpanda Dark Prince

    Joined:
    Oct 28, 2007
    Messages:
    611
    I'm looking at this, having some progress, but nothing workable so far... I'll let you know if I can make it possible with some hack.
     
  6. kirkham7

    kirkham7 Warlord

    Joined:
    Dec 22, 2012
    Messages:
    288
    Location:
    Hayward, CA
    Thanks. It would be interesting to have all civilizations open to you, and the computer, that way you never know which civilization you will face off against...
     
  7. darkpanda

    darkpanda Dark Prince

    Joined:
    Oct 28, 2007
    Messages:
    611
    Since I'm working on it, I thought I'd write some kind of "blog" of what's going on, step by step...

    1. First step is: hack CIV so that is shows all Civilizations to select from instead of limiting to the total number of Civs:

    What CIV does

    When showing the Civ selection dialog, CIV first lists all Civs from 1 (Roman) to levelOfCompetition+1 (included), then lists all Civ from 9 (Russian) to 8 + levelOfCompetition+1 (included).

    Examples:
    • if levelOfCompetition is 2, meaning that there are 2 computer-controlled opponents,
      Civ will lists all civs from 1 to (2+1)=3 included and from 9 to 8+(2+1)=11 included,
      that is:
      Code:
                  1 = Roman
                  2 = Babylonian
                  3 = German
                  9 = Russian
                 10 = Zulu
                 11 = French
    • if levelOfCompetition is 5, meaning that there are 5 computer-controlled opponents,
      Civ will lists all civs from 1 to (5+1)=6 included and from 9 to 8+(5+1)=14 included,
      that is:
      Code:
                  1 = Roman
                  2 = Babylonian
                  3 = German
                  4 = Egyptians
                  5 = Americans
                  6 = Greeks
                  9 = Russian
                 10 = Zulu
                 11 = French
                 12 = Aztecs
                 13 = Chinese
                 14 = English

    What we need to patch

    We need to tell CIV to show all Civs, regardless of the value of "levelOfCompetition".
    The actual code assembly for what I described above is a sequence of 2 loops, each loop looking like this
    Code:
        mov    varLoop, 1              ; loop counter initialized to 1
    loop_start:
        mov    ax, LevelOfCompetition  ; ax now equals levelOfCompetition
        inc    ax                      ; ax now equals levelOfCompetition+1
        inc    ax                      ; ax now equals levelOfCompetition+2
        cmp    varLoop, ax             ; CIV compares the varLoop value and "ax"
        jl     short <some location>   ; if varLoop is strictly below ax (<) it displays the Civ name (else breaks out of the loop)
    some_location:
        .... code to display Civ name ...
        inc    varLoop                 ; varLoop incremented by 1 (varLoop++)
        <jump back to loop_start>
    
    Written in C/C++ or Java, this is a typical for loop:
    Code:
    for(varLoop = 1; varLoop < levelOfCompetition+2; varLoop++) {
        // display name of Civ with ID 'varLoop'
    }
    
    So how can we alter it to ignore the level of competition when displaying selectable Civs ?

    The obvious way is to change the loop condition to "varLoop < 8"; but doing in this in assembly is actually tricky because assembly instructions have variable byte-length... Let me explain:
    • the assembly "cmp varLoop, ax" is a comparison between a value in memory (varLoop is a local variable in the stack) and a register (ax); in x86 assembly this is written (hex) "39 46 F6";
    • however, "cmp varLoop, 8" is a comparison between a value in memory and a direct value, which is rather coded as (hex) "83 46 F6 08";
    • so the binary code for "cmp varLoop, 8" takes 4 bytes, which is bigger than the binary code for "cmp varLoop, ax" which only takes 3 bytes, so we cannot patch it this way...

    The next best option would be to replace the initial "mov ax, levelOfCompetition" by hardcoding the value of ax at this stage, for example "mov ax, 6"; let's see the binary:
    • "mov ax, levelOfCompetition" is a value copy between memory and a register; in binary, it is written as (hex) "A1 14 E2"; without verifying, I am guessing that opcode "A1" is for instruction "mov memory value to AX", and "14 E2" is the address of the memory value to copy to ax... but that's really just a guess
    • then, "mov ax, 6" is a hard-coded value assignment to ax register; in binary it is written as (hex) "B8 06 00"; again, I am not verifying in the x86 reference, but I guess that B8 is the opcode for "write hard-coded short to ax"
    • bingo! both instructions have the same byte-length, and so we can now (attempt to) apply the patch


    The actual patch in CIV.EXE:

    As explained above, CIV has 2 similar, consecutive loops to display the available Civs, so we need to patch both loops.
    The original code and locations for the 2 occurences "mov ax, levelOfCompetition" differ by version:
    Code:
              EN 474.01: "[B]A1 14 E2[/B]" located at [B]0x2FB53[/B] and [B]0x2FB8D[/B]
              EN 474.05: "[B]A1 F4 D2[/B]" located at [B]0x2FB58[/B] and [B]0x2FB92[/B]
              FR 474.05: "[B]A1 34 D8[/B]" located at [B]0x30F58[/B] and [B]0x30F92[/B]
    
    Now the idea is to replace those occurrences with "mov ax, 6", which is spelled "B8 06 00" in x86 binary.

    After applying the patch, it is now possible to select any Civ in the selection screen, even with only 2 opponents, YAY!

    But wait a minute there: I selected 4 Civs total (level of competition = 3), then I chose to be the Greeks, and I end up
    being Shaka of the Zulus!!!

    Indeed, there is still some work to do...

    TO BE CONTINUED
     
  8. Renergy

    Renergy Warlord

    Joined:
    Nov 2, 2010
    Messages:
    197
    Location:
    Czech Republic
    I always wondered (and still do) why is there this limitation in the first place? Doesn't have the slightest clue, doesn't make the slightest sense to me... Word "bizarre" comes to mind...

    Perhaps, just an implied way to force the player to try to play with more civs? I mean - US were the primary market, and to play as an American you have to play against at least 5 opponents. This reason doesn't seem that much probable, though. Really can't think of a better one...

    Good luck darkpanda with remedying this weirdness!
     
  9. darkpanda

    darkpanda Dark Prince

    Joined:
    Oct 28, 2007
    Messages:
    611
    Thanks Renergy, I'm quite certain I'll need a lot of it!

    Pursuing the investigation, let's look at what going in CIV right after displaying the list of Civs (side note for those interested: I'm looking at CIV.EXE using IDA Free Pro v5):

    Code:
      // right before this, the player's selected Civ is stored in register
      // 'ax', starting from 0 for the first Civ in the dialog box
    00    mov     PlayerCivColor, ax      ; Store user choice in PlayerCivColor (global variable)
    01    mov     ax, LevelOfCompetition  ; Move LevelOfCompetition in ax
    02    cmp     PlayerCivColor, ax      ; Compare PlayerCivColor and ax (= LevelOfCompetition)
    03    jg      short _3E8              ; If PlayerCivColor is strictly greater than ax, jump to _3E8
    04    inc     PlayerCivColor          ; Increment PlayerCivColor by 1
    05    jmp     short _3F5              ; Unconditional jump to _3F5
    06 _3E8:
    07    mov     ax, PlayerCivColor      ; Move PlayerCivColor in ax
    08    sub     ax, LevelOfCompetition  ; Subtract LevelOfCompetition from ax (= PlayerCivColor)
    09    add     ax, 8                   ; Add 8 to ax
    0A    mov     PlayerCivColor, ax      ; Move back ax in PlayerCivColor
    0B _3F5:
    
    If I rewrite the assembly above in C/Java style code, this would look as below:
    Code:
        int PlayerCivColor = getUserSelection();
        if ( PlayerCivColor > LevelOfCompetition ) {
            PlayerCivColor = 8 + (PlayerCivColor - LevelOfCompetition);
        } else {
            PlayerCivColor ++;
        }
    
    In plain english, CIV assumes that if the Civ ID selected by the player is above "LevelOfCompetition", it just means that the player selected the Civ in the second half of the list, so it "normalizes it" to match the actual Civ ID.

    Example:
    • If I selected to play with 3 civilizations, LevelOfCompetition is 2
    • As explained in my previous post, CIV proposes 6 Civs for selection: Roman, Babylonian, German, Russian, Zulu or French
    • The selection dialog uses the following list IDs to return the user selection:
      Code:
      0 = Roman
      1 = Babylonian
      2 = German
      3 = Russian
      4 = Zulu
      5 = French
      
    • If the selected ID is 0, 1 or 2, then ( PlayerCivColor > LevelOfCompetition ) is false, and PlayerCivColor is simply incremented by 1, ending up being 1, 2 or 3, the IDs that match Romans, Babylonians or Germans inside Civ logic
    • If the selected ID is 3, 4 or 5, then ( PlayerCivColor > LevelOfCompetition ) is true; in this case, PlayerCivColor is first shifted back by removing the first half of the list (subtracting LevelOfCompetition), then added 8 to match the IDs of the second set of Civs inside CIV logic; so selected ID 3, 4 or 5 end up being Civ IDs 9, 10 or 11

    Now, when we started hacking the selection dialog in my previous post, we also started to mess up the remaining logic:
    • In my example before, I selected 4 total Civs, i.e. level of competition is 3
    • But the dialog box shows all 14 Civs because of the hack
    • So when I select the "Greeks", the dialog box returns ID 5, which corresponds to the 6th item in the list (the list starts at ID 0 for the first item)
    • But then CIV tries to normalize this value based on the logic above: list ID 5 *is* above level of competition 3, so PlayerCivColor ends up being 8 + (5 - 3) = 8 + 2 = 10, i.e. the Civ ID for the Zulus

    How can we fix this ?

    Here again, the solution is to hack the conditional check... But we also need to hack the computation occuring in case we are in the second half of the list:
    • like before, we change the value assigned to 'ax' before the comparison: "mov ax, LevelOfCompetition" become "mov ax, 6"
    • then we change the calculation of the real Civ ID to subtract '6' instead of the real 'LevelOfCompetition, i.e. "sub ax, LevelOfCompetition" becomes "sub ax, 6"...
    • Let's verify that we can do it:
      • "sub ax, LevelOfCompetition" translates to x86 binary as (hex) "2B 06 14 E2"
      • "sub ax, 6" translates to x86 binary as (hex) "2D 06 00"
      • here we have the problem opposite from before: the patched code is shorter than the original code... Fortunately, this is easier to deal with, by adding 'neutral' code, namely the NOP operation which does nothing... in x86 binary, its opcode is 0x90, so the final patched code is "90 2D 06 00"

    The code and locations differ by versions, again:
    Code:
        EN 47401: condition code "A1 14 E2" at 0x2FBD9; subtraction code "2B 06 14 E2" at 0x2FBEB
        EN 47405: [I]coming soon[/I]
        FR 47405: [I]coming soon[/I]
    

    Let's patch this, and voila! I can now select the Aztecs and be called Montezuma, even with only 3 Civs playing !!

    But wait a minute here: the game starts and I have actually no settler at all... I am left in complete darkness... ! Even worse, as "End of Turn" is turned off by default, the game is actually unwrapping turn after turn all by istelf, and I can't do anything about it... What's happening ? I'm playing a CIV game where my Civ does not exist !!!

    Well, still more work to do for this to be playable, it seems... :)

    TO BE CONTINUED
     
  10. kirkham7

    kirkham7 Warlord

    Joined:
    Dec 22, 2012
    Messages:
    288
    Location:
    Hayward, CA
    If the map worked during that, it would be interesting to watch the civilizations build up and destroy each other.
     
  11. darkpanda

    darkpanda Dark Prince

    Joined:
    Oct 28, 2007
    Messages:
    611
    Indeed, but as you are no longer considered an active player, you just never have the hand to just save the game or even press Shift+56... You just can't do anything.
     
  12. kirkham7

    kirkham7 Warlord

    Joined:
    Dec 22, 2012
    Messages:
    288
    Location:
    Hayward, CA
    That's too bad.
     
  13. darkpanda

    darkpanda Dark Prince

    Joined:
    Oct 28, 2007
    Messages:
    611
    We'll never know the reason for this limitation... Further investigation unfortunately ends up with an issue very hard to circumvent.

    CIV relies on a data byte that we could call ActiveCivs (also present in .SVE savegames) to control the initialization of Civs.

    The initialization of this byte ActiveCivs is done with the following code, after the player selected the number of total Civs in the game:
    Code:
      // right before the code below start, the
      // number of Civs (minus 1) selected by 
      // the player is stored in register 'ax'
        mov     LevelOfCompetition, ax         
        mov     ax, 0FFh
        mov     cl, 6
        sub     cl, byte ptr LevelOfCompetition
        sar     ax, cl
        mov     ActiveCivilizations, ax
    
    Basically, the value of ActiveCivs is the byte value 0b11111111 (written in binary), right-shifted by (6 - LevelOfCompetition) increments:
    • If LevelOfCompetition is 2 (3 total Civs):
      • then 6 - LevelOfCompetition = 4
      • and ActiveCivs = 0b11111111 >> 4 = 0b00001111
      • so we see that ActiveCivs covers Barbarians (bit 0) and the 3 first Civs (white, green, blue) at bits 1, 2 and 3
    • If LevelOfCompetition is 5 (6 total Civs):
      • then 6 - LevelOfCompetition = 1
      • and ActiveCivs = 0b11111111 >> 1 = 0b01111111
      • so we see that ActiveCivs covers all Civs except the grays (bit 7)

    So in our previous example, I elected to play with the Aztecs, eventhough the ActiveCivs bit for the yellows (bit 4) was 0, and thus my Civ was not initialized at all.

    I tried a first hack where I simply removed the "right-shift" operation, which left the value of ActiveCivs to 0b11111111 whatever the LevelOfCompetition, but then all Civs are active and initialized regardless of the player's choice of number of opponents.

    Implementing a randomization of Civs would require to actual code the randomization of ActiveCivs in assembly/binary, taking into account the player's selection of Civs, and finding room where to put this code in CIV.EXE... For the moment, this is definitely beyond my reach, I must admit......

    But never say never ;)

    TO BE CONTINUED
     
  14. Nesretnik

    Nesretnik Chieftain

    Joined:
    Jul 17, 2013
    Messages:
    29
    Location:
    Konjscina, Croatia
    "Svaka &#269;ast" (which, loosely translated, means ":goodjob:") to you for yet another breakthrough, darkpanda!

    ...which now emboldens me to suggest putting an item from my 'wish' list onto your 'to do' list. I never play one of the set civs, but always hit ESC to input a name of my own. However, this means playing with a colour that's chosen for me by the game. I'd be very grateful if your investigations uncovered a way of choosing which civ was replaced by the ESC-created civ.

    This, of course, is not high-priority. You're doing great work, and I have no desire to distract you from it.
     
  15. darkpanda

    darkpanda Dark Prince

    Joined:
    Oct 28, 2007
    Messages:
    611
    The CIV code that selects a Civ for you when you don't is nothing more than a "random" process...

    Problem is: when you press ESC, the GUI routine displaying the list of Civs returns "-1", and so it is not possible to detect which Civ was highlighted when you pressed ESC... So interactively choosing your custom Civ, all the while keeping current Civ selection functionality seems quite complicated to achieve...

    What is possible however, is to make CIV always ask you for the Tribe name, even when you don't press ESC... In this case, CIV will use the Civ you selected in the list, but still ask for the Tribe name, so you can control the color you want.

    To do this, the patch is as follows:
    Code:
      [B]EN 47401[/B]: replace bytes "[COLOR="Red"]75 24[/COLOR]" at offset [B]0x2FC97 [/B]with bytes "[B][COLOR="Blue"]90 90[/COLOR][/B]"
      [B]EN 47405[/B]: replace bytes "[COLOR="Red"]75 24[/COLOR]" at offset [B]0x2FC9C [/B]with bytes "[COLOR="Blue"][B]90 90[/B][/COLOR]"
      [B]FR 47405[/B]: replace bytes "[COLOR="Red"]75 24[/COLOR]" at offset [B]0x3109C [/B]with bytes "[COLOR="Blue"][B]90 90[/B][/COLOR]"
    
    Don't worry about my priorities or To-Do list, they are very unstable, and if a patch is fun and simple enough, I don't mind investigating it ;)

    Enjoy!

    Note: for those interested, this patch is simply bypassing a conditional jump (overwriting the JNZ code with NOP NOP instructions) that checks whether the Civ was player-selected or not, and that normally pops up the custom Tribe name only if it was selected randomly by pressing ESC... Bypassing this jumps makes Civ always go to the custom Tribe name dialog even when the the Civ was selected manually by the player)
     
  16. Nesretnik

    Nesretnik Chieftain

    Joined:
    Jul 17, 2013
    Messages:
    29
    Location:
    Konjscina, Croatia
    Brilliant! This is exactly what I was hoping for. Thanks a bunch!

    BTW, the offset for EN 474.03 is 0x2F49C.
     

Share This Page