(In this series, I’m going to go over the process of porting Quake II to MS-DOS. Now of course the question will pop up why? And the answer is simple enough, after [HCI]Mara’akate added in gamespy support, something was noticeably off, and that is fewer and fewer people are playing Quake 1 these days. So what to do?

Well apparently Quake II has an active following, so it’s time to move our creeky but favorite OS, MS-DOS into the Quake II Arena!

So the first thing to do is to grab a copy of the GPL source to Quake 2, along with a copy of the game, and get started on making a ‘null’ version of the game. Null versions of the game have no graphical output, no sound, and ‘function’ at a very basic level. It’s something to strive for as a first base in hitting that home run of a functional port. Thankfully iD wrote really portable and modular software. Unfortunately they tended to let their ports drift as they were writing the engine so the null code doesn’t actually work out of the box. And the project makfiles leave a little to be desired for me, as they have a bunch of i386/Dec Alpha magic which doesn’t leave much in the room for weird ports like MS-DOS.

Now I should mention that before I’d gotten started the first thing I decided that like QuakeWorld for MS-DOS (and OS/2) I would use GCC as it is a known working compiler out of the box. If you can, don’t fight so many battles of unknowns at once. Another thing is that I am going to cross compile from OS X and test with DOSBox. Of course you may want to use something else, and I know my tastes drift, but for now this is what I’m using. I’m using my old OSX to DJGPP cross compiler with GCC 2.95.3, which serves me well.

So the first thing was to compile and run the null version with native tools on OS X. After a bit of struggle I got here:

$ ./q2

Added packfile ./baseq2/pak0.pak (3307 files)

Added packfile ./baseq2/pak1.pak (279 files)

Added packfile ./baseq2/pak2.pak (2 files)

Console initialized.

Sys_Error: ref_soft::R_BeginFrame() – catastrophic mode change failure

Well that’s great, how to figure this one out?

One thing I did to make it easier to work with the flow of Quake is to make the Sys_Error procedure contain a divide by zero. Now why would I purposely put a divide by zero in the code? Simple it lets me back trace the code when Quake catches it’s own faults so I can see what went wrong where, vs what would look like a clean exit. From my example:

(lldb) bt

* thread #1: tid = 0xf14f9, 0x000c0853 q2`Sys_Error(error=0x000c17d4) + 98 at sys_null.c:28, queue = ‘com.apple.main-thread’, stop reason = EXC_ARITHMETIC (code=EXC_I386_DIV, subcode=0x0)

* frame #0: 0x000c0853 q2`Sys_Error(error=0x000c17d4) + 98 at sys_null.c:28

frame #1: 0x0003a0cb q2`Com_Error(code=0, fmt=0x000c17d4) + 368 at common.c:215

frame #2: 0x000c0523 q2`VID_Error(err_level=0, fmt=0x000cc8a8) + 81 at vid_null.c:45

frame #3: 0x000a6d38 q2`R_BeginFrame(camera_separation=0) + 592 at r_main.c:1145

frame #4: 0x000a4cfc q2`R_Init(hInstance=0x00000000, wndProc=0x00000000) + 276 at r_main.c:333

frame #5: 0x000c0740 q2`VID_Init + 362 at vid_null.c:122

frame #6: 0x00007900 q2`CL_Init + 55 at cl_main.c:1795

frame #7: 0x0003c2cb q2`Qcommon_Init(argc=1, argv=0xbffffc40) + 754 at common.c:1469

frame #8: 0x000c09ac q2`main(argc=1, argv=0xbffffc40) + 24 at sys_null.c:128

frame #9: 0x000019ad q2`_start + 212

frame #10: 0x000018d8 q2`start + 40

(lldb)

Now I know that R_BeginFrame was where the error was occurring, and looking at the code:

if ( ( err = SWimp_SetMode( &vid.width, &vid.height, sw_mode->value, vid_fullscreen->value ) ) == rserr_ok )

{

…

else

{

ri.Sys_Error( ERR_FATAL, “ref_soft::R_BeginFrame() – catastrophic mode change failure

” );

}

We can see that the vid.width/vid.height aren’t being setup correctly. Turns out there was a bunch more work to be done setting up vid_null!

After looking closer at the files, I notice as I’m stitching them together is that Quake II relies on DLL’s as part of it’s base functionality. I drifted out of Quake after Quake 1, so I never played II before. So I didn’t know this. Obviously DJGPP doesn’t support DLL’s that can be loaded and un-loaded at will (Yes I know about DXE’s, but as the FAQ states, they cannot be un-loaded. And I’m not going to fight DJGPP’s LIBC). So looking further in the source, I saw these fun defines:

-DREF_HARD_LINKED -DGAME_HARD_LINKED

So at one point there was support for ‘hard linking’ in the ‘REF’ video driver, and the ‘game’ logic driver. But it did kind of drift out of the code. But looking at the Win32 version I could see that putting this functionality back in should be easy. And to be honest if I learned any lesson from this, is that I should have been pulling the Win32 version apart by injecting null into it until it ran as a null platform, then use that as the basis. Lesson learned. Always start with a known good! Quake II was built with Visual C++ 6, but I only have Visual C++ 4.2 installed on Crossover. Yes I know again this is me being difficult. But it didn’t take much time to get a simple project that has two DLL’s and a Win32 exe running. Then I took on the ‘ref’ video driver and got that linking inside of the main EXE. Now with one DLL ‘eliminated’ it was time to work on the game dll.

The game DLL posed the biggest challenge because it passes a reference to internal functions to it, and exports various functions back to the game engine. So I ended up altering the engine to not call the game import/export directly but setting it up myself. The hardest thing was that I couldn’t pull in the game header file, but rather I had to copy the prototypes myself. Another interesting thing with the way Quake II works is that the game dll has to be able to be unloaded and loaded at will. It wasn’t hard to simulate this, but I wasn’t expecting it. Again this is probably because I never really played Quake II.

Now that I had Quake II building without DLL’s I could then take the next step of removing all the IO and re-replacing it with the null code, and now I had something that looked like it was doing something.

Added packfile ./baseq2/pak0.pak (3307 files)

Added packfile ./baseq2/pak1.pak (279 files)

Added packfile ./baseq2/pak2.pak (2 files)

execing default.cfg

execing config.cfg

NULLsock Initialized

Console initialized.

768k surface cache

ref_soft version: SOFT 0.01

——- sound initialization ——-

Sys_FindFirst [./baseq2/autoexec.cfg]: UNIMPLEMENTED!

==== InitGame ====

——- Server Initialization ——-

0 entities inhibited

0 teams with 0 entities

————————————-

====== Quake2 Initialized ======

loopback: client_connect

==== ShutdownGame ====

Sys_Error: Draw_Pic: bad coordinates

This turns out because I didn’t allocate the screen properly. Looking at the code:

if ((x < 0) || (x + w > vid.width) ||

(y + h > vid.height))

{

ri.Sys_Error (ERR_FATAL,”Draw_Pic: bad coordinates”);

}

We can see it pretty plainly.

Now since we were going somewhere I started to write some MS-DOS code, and switch out of the null set of mind!

First a simple VGA mode 13 setup which gives us 320×200 resolution with 256 available colours. And for good measure I did a simple VGA palette setup that I knew worked from a prior program. Next we just blit our buffer onto the screen, and we get this:

Which is exciting and disappointing at the same time. I then took the palette code from DOS Quake, and got something just as ugly. I tried the code from OS/2. Same thing. I tried all kinds of things and was going nowhere.

At this point Mara’akate added in the Linux clock code, and now we had this!

It wasn’t until much more digging around I saw some 320×240 screen setups that I realized there was something wrong there, and then I saw this gem in the linux port’s code:

/*

** SWimp_SetPalette

**

** System specific palette setting routine. A NULL palette means

** to use the existing palette. The palette is expected to be in

** a padded 4-byte xRGB format.

*/

In traditional VGA palette setups it’s 768 bytes that needs to be read/and pushed to the card. I even checked Quake 1 is 768 bytes, but now in Quake II, it’s 1024 bytes! OOPS! Sometimes (ok a lot of times) you really need to check other ports or a ‘known good’ to see how they did things.

Pretty awesome!

So where to go from here? Obviously things like better keyboard input, the mouse, sound and networking need to be done.

Continued in pt2, pt3, pt4, and part5.