Monday, February 3, 2025

Ultima III - Strange things going on

That's right, I'm having fun with the MS-DOS version of Origin system's breakthrough title: Ultima III Exodus.

Having almost entirely reversed the executable part of the game I felt eager to share some of the information I gathered through this process.

First, the basics. There are 3 executable files that constitute the core of the game, plus one "overlay" file. They are:

  • ULTIMA.COM: the opening animation and splash screen
  • BOOTUP.COM: allows to create and manage characters and the party
  • EXODUS.COM: the main game
  • DUNGEON.DAT: and "overlay" loaded by EXODUS.COM when the player enters a dungeon; it does the 3D-sort-of rendering of the dungeon.

The executable files seem to have been written in assembly language exclusively. I use Microsoft's Macro Assembler (MASM) version 4.0 for the build and it works fine, though I have no way to be sure which assembler was used by the original coder (who by the way is none other than James R. Van Artsdalen, the same who did the MS-DOS port of Ultima IV).

Being MSDOS's COM files, the start address of the 3 executable files is 100h (256 in decimal) and this address cannot be changed to another arbitrary address, unlike the MZ-type executable. And so here is one of the first strange thing about this game, happening in EXODUS.COM. This program's entry point is not at address 100h; as a matter of fact, there is no executable code at this address and if you try to launch it from the MSDOS prompt, chances are you will lock your PC or cause a crash.

So what is going on here ? EXODUS.COM is meant to be loaded at address 100h, like any other COM file, but its entry point is then at address 24C4h. I think this was intended as some kind of "protection", to prevent users from launching the main part of the game directly. Actually, BOOTUP.COM is responsible for launching EXODUS.COM. When option "Journey Onward" is selected, BOOTUP.COM loads (there is a little hack involving part of the PSP used to create a loader but I will not explain the details here) EXODUS.COM at address 100h and jumps to the address 24C4h.

Well, it is not that simple; BOOTUP.COM actually reads the 16-bit value at address 1328h which contains the correct entry point address: 24C4h. Why not directly jump to the entry point ? That it just an hypothesis, but I think that the address 24C4h is really arbitrary and the programmer didn't want to change/rebuild its BOOTUP.COM code every time there was a change in EXODUS.COM, so he chose to store this arbitrary address at a fixed address in the program. Why is 1328h less arbitrary ? Because 1328h is 1228h + 100h and 1228h this is the exact size of the resources files (SOSARIA.ULT, AMBROSIA.ULT, LCB.ULT, ...) that are meant to be loaded at address 100h.

 Another strange thing ? Ok here we go (this one I originally posted on BlueSky):

I found this intriguing piece of code in EXODUS.COM. It starts at offset 73F0h (when loaded in memory):

xxxx:73F0 B42C MOV    AH,2C
xxxx:73F2 CD15 INT    15

It took me some time to understand that it was a mistake from the programmer. Can you spot it ?‪
I am almost sure that this is what was intended:

xxxx:73F2 CD21 INT    21

The programmer must have forgotten to put the h-prefix in his source code (instead of "int 21h", he wrote "int 21").

Fortunately, the consequence is minor. To explain it may take some time though.

If you know a little MS-DOS programming, you may know that "INT 21h" is the standard way to call a system service who will be identified by the value of register AH.
In this case, 2Ch, the system time is requested. The value is returned as:

  • hours in register CH
  • minutes in register CL
  • seconds in register DH
  • hundredths in register DL

EXODUS.COM only uses the value from register DH, seconds.
Here is another piece of code found at 3 different places:

MOV    AH,2Ch
INT    21h
MOV    BL,DH
ADD    BL,5
CMP    BL,60
JB    _below60
SUB    BL,60
_below60:

(aka, get the current second count and add 5 secs.)

Then the program enters a loop which will be exited on one of the two conditions:

  • a key has been pressed
  • the current second count is equal to the value computed previously

In other words: gives the player 5 seconds to press a key.

This code is used when the program is in "prompt mode":
World map, Dungeon, or Combat.

Let's focus on the World Map prompt; there is a call to the function at 7347h at each iteration of the prompt loop we just described. This function manages a particular game object: the Whirlpool !

On entering this function, the value of register BX (containing the counts of second + 5) is pushed on the stack, and, most of the time, after the Whirlpool's information has been refreshed, the value of register BX is popped from the stack, and no harm is done.

BUT in case the player's boat is engulfed ...
‪... function at 2168h is called; in this function, some text is displayed, some disk accessed, some graphic stuff done ... let's say that this takes time. And when the function returns, remember that we are still inside the prompt loop. Since the pushed value of register BX is certainly overdue, we need to compute a new one.

The previously pushed value of register BX is popped and ... ignored, and we have then the piece of code, the one at 73F0h.
Which means that in this particular case, function 7347h, which is expected to leave a valid timer value in register BX, may returns something completely random.

What are the consequences ?

Fortunately, this is not a big deal. Let's say we have the best case, the bad case, and the worst case:

  • best case: register BL contains a value different from the current seconds counter; the player has a delay of 5 to 255 seconds.
  • bad case: the player has less than 5 seconds until the next timeout.
  • worst case: register BL value is equal to the current seconds timer; there is no delay left.

And what happens when the delay is overdue ? The same thing as when the player issues a "pass" command. The entire game's world is updated of 1 step while the player does nothing.

As I said before, no big deal...

 

Well, that's it for today. I hope I will be able to post more Strange Things on Ultima III very soon.

No comments: