UPDATE: According to inXile this fix will probably be incorporated into the next patch.
UPDATE2: This has been incorporated into patch 2.
Wasteland (the original) has a problem where it randomly freezes after playing for some time.
From the known issues FAQ:
The game freeze frequently.
o This is a heritage issue which may be circumvented by using only the keyboard to control the game.
In general, fixing legacy issues is a quasi-impossibility as the original is not buildable.
I'm not completely sure about what they mean by "not buildable", but frankly I would rather that inXile focus on T:ToN and Wasteland 2 than wasting resources on fixing legacy code from 25 years ago. However nothing prevents me from "wasting" resources on fixing 25 year old dos games. So I took it upon myself to localize and fix the issue.
The short version is this: find rom\DATA\WLA.BIN in your steamapps/GoG folder and open it in your favourite hex editor, go to offset 0E5E where it says "77 1F" and replace with "EB 1F". The long version below explains how I figured this out, and probably requires a good understanding of x86 assembler and the PC platform to read.
The first step was to get a useful build and debugging environment up and running. So I downloaded inXile's modified DOSBox. To build that you need:
- SDL 1.2
- libcurses (use standard PDCurses on windows – win32a won't work with DOSBox out of the box)
You will need to change the library and include search paths if you are going to build it on windows with Visual Studio.
DOSBox has a build in debugger that can be activated at build time which is very useful for debugging dos applications. This forum post from VOGONS explains how to activate and use it. I made a build with heavy debugging enabled which has some nifty features, though it slows down DOSBox tremendously. The modified DOSBox expects to see the game files inside a "rom" directory like under the steamapps folder, so copy the neccessary game files over to wherever you are running it from.
So now to actually analyze the executable. I found this wiki with some useful information about the different files. Especially important is that WLA.BIN is overlaid from address zero of the code segment (it is overlaid over the start of the code segment of WL.EXE). Also WL.EXE has been packed with LINK /EXEPACK. Luckily unpackers exists, though in the end I didn't actually have any use for unpacking it.
What I ended up doing was playing Wasteland with a debugger attached to DOSBox. This turned out to be quite useful as information about the internal state of the emulated hardware isn't easily accessible using the DOSBox debugger. So i played the game until it froze. Then I activated the DOSBox debugger (actually I couldn't get the Alt+Pause combination working so I ended up using my Visual Studio debugger to pretend a breakpoint was hit by returning true in DEBUG_HeavyIsBreakpoint() in debug.c).
After getting the DOSBox debugger to break into the frozen Wasteland I could use the MEMDUMP command to dump all the memory (1MB from 0000:0000), and some sed / xxd magic to turn the generated text file into a binary file. I could then load that dump into IDA and begin setting up segments.
DOSBox with heavy debugging enabled has the useful LOG command which dumps a complete trace of executed instructions and registers. Dumping 10000 instructions to figure out why the game was stuck gave me this pattern:
01A6:00000E78 jns 00000E7A ($+0) (down) EAX:0000167C EBX:000024E2 ECX:00003E80 EDX:00000E66 ESI:00000002 EDI:0000CAFF EBP:00000000 ESP:000001E0 DS:0E88 ES:A000 FS:0000 GS:0000 SS:2AA6 CF:0 ZF:0 SF:0 OF:0 AF:1 PF:0 IF:0 01A6:00000E7A cmp ax,cx EAX:0000167C EBX:000024E2 ECX:00003E80 EDX:00000E66 ESI:00000002 EDI:0000CAFF EBP:00000000 ESP:000001E0 DS:0E88 ES:A000 FS:0000 GS:0000 SS:2AA6 CF:0 ZF:0 SF:0 OF:0 AF:1 PF:0 IF:0 01A6:00000E7C jc 00000E6C ($-12) (up) EAX:0000167C EBX:000024E2 ECX:00003E80 EDX:00000E66 ESI:00000002 EDI:0000CAFF EBP:00000000 ESP:000001E0 DS:0E88 ES:A000 FS:0000 GS:0000 SS:2AA6 CF:1 ZF:0 SF:1 OF:0 AF:0 PF:1 IF:0 01A6:00000E6C in al,40 EAX:0000167C EBX:000024E2 ECX:00003E80 EDX:00000E66 ESI:00000002 EDI:0000CAFF EBP:00000000 ESP:000001E0 DS:0E88 ES:A000 FS:0000 GS:0000 SS:2AA6 CF:1 ZF:0 SF:1 OF:0 AF:0 PF:1 IF:0 01A6:00000E6E mov dl,al EAX:000016FE EBX:000024E2 ECX:00003E80 EDX:00000E66 ESI:00000002 EDI:0000CAFF EBP:00000000 ESP:000001E0 DS:0E88 ES:A000 FS:0000 GS:0000 SS:2AA6 CF:1 ZF:0 SF:1 OF:0 AF:0 PF:1 IF:0 01A6:00000E70 in al,40 EAX:000016FE EBX:000024E2 ECX:00003E80 EDX:00000EFE ESI:00000002 EDI:0000CAFF EBP:00000000 ESP:000001E0 DS:0E88 ES:A000 FS:0000 GS:0000 SS:2AA6 CF:1 ZF:0 SF:1 OF:0 AF:0 PF:1 IF:0 01A6:00000E72 mov dh,al EAX:00001607 EBX:000024E2 ECX:00003E80 EDX:00000EFE ESI:00000002 EDI:0000CAFF EBP:00000000 ESP:000001E0 DS:0E88 ES:A000 FS:0000 GS:0000 SS:2AA6 CF:1 ZF:0 SF:1 OF:0 AF:0 PF:1 IF:0 01A6:00000E74 mov ax,bx EAX:00001607 EBX:000024E2 ECX:00003E80 EDX:000007FE ESI:00000002 EDI:0000CAFF EBP:00000000 ESP:000001E0 DS:0E88 ES:A000 FS:0000 GS:0000 SS:2AA6 CF:1 ZF:0 SF:1 OF:0 AF:0 PF:1 IF:0 01A6:00000E76 sub ax,dx EAX:000024E2 EBX:000024E2 ECX:00003E80 EDX:000007FE ESI:00000002 EDI:0000CAFF EBP:00000000 ESP:000001E0 DS:0E88 ES:A000 FS:0000 GS:0000 SS:2AA6 CF:1 ZF:0 SF:1 OF:0 AF:0 PF:1 IF:0 01A6:00000E78 jns 00000E7A ($+0) (down) EAX:00001CE4 EBX:000024E2 ECX:00003E80 EDX:000007FE ESI:00000002 EDI:0000CAFF EBP:00000000 ESP:000001E0 DS:0E88 ES:A000 FS:0000 GS:0000 SS:2AA6 CF:0 ZF:0 SF:0 OF:0 AF:1 PF:1 IF:0
And kept going like that. Especially EDX kept counting down to zero and then restarting from around 0x24DE – namely slightly lower than the value of EBX of 0x24E2. The address being read from in the in statements – 0x40 – is the address of Programmable Interval Timer 0. Wikipedia has more information about how it works here. Using the Visual Studio debugger to dump the state of the timer (pit from timer.cpp) it turned out to be running in mode 3 – "Square Wave Generator" mode – with a loaded counter value of 0x24E3. The important thing here is that it counted down from 0x24E3 to zero and then restarted. To get past the jc (/jb) jump at 01A6:0E7C it would have to end up with ebx – timer0 >= 0x3E80 which would require timer0 > 0x24E2. I'm not sure about the behaviour on PC platforms in general, but in DOSBox this is actually impossible as the counter value is rounded down to nearest even value when the timer is configured in Square Wave Generator mode. For slightly smaller values in ebx it may run forever or for a very long time depending on the cycle length of the loop relative to the cycle time of the timer on a particular machine / emulator.
The actual code in question turns out to be the function that draws the cursor. Here is the annotated assembler from IDA after some digging around and figuring out what the global variables were:
code1:0E45 ; =============== S U B R O U T I N E ======================================= code1:0E45 code1:0E45 code1:0E45 drawCursor proc near ; CODE XREF: j_drawCursorj code1:0E45 cmp mouseEnabled, 0 code1:0E4A jz short end_clean code1:0E4C mov dx, 3DAh ; dx is the CGA/VGA Input Status #1 Register code1:0E4F code1:0E4F waitForVSync: ; CODE XREF: drawCursor+Dj code1:0E4F in al, dx ; Video status bits: code1:0E4F ; 0: retrace. 1=display is in vert or horiz retrace. code1:0E4F ; 1: 1=light pen is triggered; 0=armed code1:0E4F ; 2: 1=light pen switch is open; 0=closed code1:0E4F ; 3: 1=vertical sync pulse is occurring. code1:0E50 and al, 8 code1:0E52 jz short waitForVSync code1:0E54 mov al, 0 code1:0E56 call NI_updatePalette ; Dummied stub? code1:0E59 code1:0E59 loc_28B9: ; DATA XREF: stack:01DEo code1:0E59 cmp mouseRow, 50 code1:0E5E ja short after_wait ; wait if mouseRow <= 50 code1:0E60 cli code1:0E61 mov cx, 3E80h code1:0E64 ; So after a VSync we wait for - something...? code1:0E64 ; Timer 0 is configured in mode 3: Square Wave Generator code1:0E64 ; read/write state: 2xRead/2xWrite bits 0..7 then 8..15 of counter value code1:0E64 ; counter = 0x24E3 code1:0E64 ; Read counter value into bx code1:0E64 in al, 40h ; Timer 8253-5 (AT: 8254.2). code1:0E66 mov bl, al code1:0E68 in al, 40h ; Timer 8253-5 (AT: 8254.2). code1:0E6A mov bh, al code1:0E6C ; Read timer into dx code1:0E6C code1:0E6C loc_28CC: ; CODE XREF: drawCursor+37j code1:0E6C in al, 40h ; Timer 8253-5 (AT: 8254.2). code1:0E6C ; STUCK HERE!! (01A6:00000E6C) code1:0E6E mov dl, al code1:0E70 in al, 40h ; Timer 8253-5 (AT: 8254.2). code1:0E72 mov dh, al code1:0E74 mov ax, bx code1:0E76 sub ax, dx ; ax = starttime (bx) - nowtime (dx) code1:0E78 jns short $+2 ; huh? wierd nop?? code1:0E7A cmp ax, cx code1:0E7C jb short loc_28CC ; LOOPS BACK if unsigned(starttime - nowtime) < 0x3E80 code1:0E7E sti code1:0E7F code1:0E7F after_wait: ; CODE XREF: drawCursor+19j code1:0E7F mov al, 4 code1:0E81 call NI_updatePalette code1:0E84 call sub_2862 code1:0E87 call sub_27AD code1:0E8A mov al, 3 code1:0E8C call NI_updatePalette code1:0E8F code1:0E8F end_clean: ; CODE XREF: drawCursor+5j code1:0E8F xor bx, bx code1:0E91 xor dx, dx code1:0E93 mov word_1764B, 1 code1:0E99 retn code1:0E99 drawCursor endp
This has some interesting peculiarities. This is part of the WLA.BIN file which seems to have been hand crafted in assembly – it definitely contains some constructs that I doubt any compiler would ever create as well as some dead code lying around. The function NI_updatePalette is an example of the latter – it only consists of a retn instruction but is followed by some seemingly unreferenced and vestigial code.
Another thing to note is "jns short $+2". If ax-dx >= 0 then it just continues to the next instruction. Otherwise it – well – "jumps" to the next instruction (the instruction is 2 bytes long). That is; it does absolutely nothing. My guess is that it should probably have been something else, but it's not entirely clear what. The whole logic of this function seems to be kinda broken as we saw before. Here in pseudo c:
$bx = 0;
$dx = 0;
word_1764B = 1;
while (!(CgaInputStatus1() & CGA_VSYNC))
//Wait for VSync
if (mouseRow <= 50)
//Timer 0 is initialized to mode 3: Square Wave Generator with count from 0x24E3
//Latch mode is 2xRead/2xWrite
const unsigned int waitTime = 0x3E80; //$cx
unsigned int startTime = GetTimer0(); //$bx
while (startTime – GetTimer0() < waitTime)
sub_2862(); // Writes something to video ram
sub_27AD(); // Also writes something to video ram
I haven't investigated the last two functions but from a cursory glance they checks graphics status and do the actual writing to the video ram. The important part here is the "Wait…" loop. It only triggers when the mouse is within the top 50 pixels of the screen. The point is probably to avoid writing the mouse cursor to video ram while the cathode ray is displaying those pixles to avoid screen flicker. However the logic is obviously broken. The comparision to 0x3E80 (which is 16000 in decimal) seems completely non-sensical. As the timer counts down from 9443 (0x24e3) we will always have
Furthermore the comparision is unsigned, so to break out of the loop we must have for positive difference
or for negative difference
But as 65536 – 9443 = 56093 > 16000 we have that the first case is always false and the second is always true, so the loop might just as well have been
Considering that the loop never worked (at least the time waited varies widely depending on the value of the timer when the function is called) it doesn't seems like it would harm to just deactivate it. Also I'm not even sure how far DOSBox goes towards actually emulating a cathode ray (though I've heard of a few games that actually did things like changing the palette in the middle of writing a frame to get more available colors), so it may actually be immune to flicker caused by that.
So the fix I propose is to simply replace the conditional jump at code1:0E5E with an uncoditional one which is just changing the single byte opcode 0x77 into 0xEB. As I mentioned before that address is within WLA.BIN, so it can even be done without having to repack WL.EXE