Porting Blood Nova to DOS

In January and February of 2022, I ran a Kickstarter campaign for a new retro style point and click adventure game Blood Nova. As one of my responsibilities is to create the macOS and Linux builds, I thought that, just for fun, I'd look into creating a build for MS-DOS too. Having worked on it as a side-project for a few weeks now, I'm confident that such a port is possible. This post is intended to explain a little about the process and the challenges involved.

Blood Nova being created in Game Maker Studio.

The primary build of Blood Nova is being made for Windows using GameMaker Studio, which also supports doing builds for macOS and Linux. MS-DOS, however, is not a supported target platform (imagine that!). As the game runs in 320x180 resolution with a very limited colour palette of just a few dozen entries, and doesn't rely on blistering fast animations, full motion video, or complicated 3D worlds, there isn't really anything about the game itself that precludes running on DOS. That, coupled with the fact that a point and click adventure like this requires only a small subset of the functions GameMaker provides, made me think that, rather than doing a full port with an entirely different codebase, I should look into creating a runtime environment for DOS that allows using the original codebase with only minor alterations. This, while slower than a true port, would tremendously simplify maintenance of the code and keeping the content and features of the main version in sync with the DOS version. So, think of this DOS version not so much as the game being ported, but the engine. What I've created is similar to the ScummVM system allowing older games to run on modern systems, but the other way around.

As not everybody will be interested in every aspect of the process, here is a quick table of contents allowing you to skip to the sections that you might be interested in.

GameMaker Language

GameMaker has its own custom scripting language, called GML, which is highly similar to JavaScript/ECMAScript. There are some differences (some of which are very significant), but much of the syntax is identical. There are several JS interpreters written in C that can be embedded in host programmes like the runtime environment I envision. Examples of such interpreters are Sami Varaala's Duktape, Fabrice Bellard's QuickJS and MuJS. For my current development efforts, I have used Duktape as I have experience with that because I use it in my other games. Its primary focus is not on speed, which may prove to be a problem especially on the now obsolete hardware that is appropriate for the DOS era. So in the future, I may have to replace it with QuickJS or MuJS, provided I can build them with DJGPP (a DOS port of the GCC compiler).

The RHIDE environment for developing using the DJGPP C++ compiler.

One thing that is very confusing about GameMaker is that it has slightly different terminology for things than is common in the development world. What GM calls an "object" is what is normally referred to as a "class". And the actual instances of those class are called "instances" (which isn't too confusing, as the rest of the world also often calls them instances, however, they are also often called simply "objects", which leads to confusion with GM's use of that term). I will try to avoid the use of the word "object" and instead use "class" and "instance" only.

The first thing I created is a separate translator programme. It has several tasks:

  1. Parsing all the GML scripts, classes and events into an AST which can then be output as text again in either GML form (outputting essentially the same as the input code, just stripped of all comments and with strictly uniform formatting) or JavaScript form (in many cases identical to the GML code, but with adjustments made for the differences between GML and JS)
  2. Keeping track of which GM functions are actively being used by the GML code in Blood Nova, so that I know exactly which subset of GM to implement in the runtime
  3. Injecting custom scripts to override certain GML behaviours and to manage the runtime. This is done in JavaScript as much as possible, rather than in the native C++ code of the runtime environment. That makes it a lot easier to switch to another JS interpreter if/when needed
  4. Precompiling the JS code to bytecode for the JS interpreter, to improve startup performance of the DOS runtime environment
  5. Converting all the metadata from the GM project into a format that is easier to handle for the runtime environment
  6. Converting graphics from PNG format to PCX to comply with the VGA palette and for ease of decoding as well as performance on DOS era hardware
  7. Converting sound effects from MP3 or OGG format to WAV format at a lower fidelity. Although they will take up more space in WAV format, this is offset by the reduced samplerate (11025Hz vs 44100Hz) and the reduced resolution (8bit vs 16bit). Still, the sound quality is lower
  8. Replacing the digital music tracks with MIDI versions, with custom versions for General MIDI and the venerable Roland MT-32
  9. Combining the results of all the above into a large packed data file, like a TAR archive (similar to a ZIP file, but without compression) for easier distribution, and to overcome the lack of long filenames on DOS

with, self and other

The with construct deserves special mention, because it is one of the most radically different things between GML and JS, is being used extensively in pretty much any GML code and is absolutely crucial to have it working properly to be able to run anything.

Even though GML has classes and instances, it's not a proper object-oriented language, lacking such features as constructors/destructors, method calls, encapsulation etc. What it does have is events like Create, Destroy, Step and Draw at the class level, and potentially some instance creation code for a specific instance in a room. While those features are lacking, it does have other features that are very appropriate for game development. While most languages would require the developer to keep track of which instances are active in the game, and iterate over those as needed, GML provides a with construct that loops over all active instances of a class. This means that, whereas in, say, C++, you'd have to do something like:

typedef std::list <CAlien *> LIST_OF_ALIENS; LIST_OF_ALIENS active_aliens; LIST_OF_ALIENS::iterator itor = active_aliens.begin (); while (itor != active_aliens.end ()) { CAlien *a = *itor++; a->x += 3; }

to move all aliens to the right by 3 pixels, GML instead allows you to do simply:

with (CAlien) x += 3;

JavaScript doesn't keep a list of all instances created with the class. It does have a with construct, though unlike the GML version, it is not a loop, instead operating on a single instance. So instead, the runtime has an object corresponding to the class, which has a member $instances where it keeps references to all instances created. The reason for starting the variable name with a $ sign is simply that GML does not accept $ as part of an identifier, so there is no possibility of conflicting variable names when I prefix the automatically generated ones with that symbol. So, the above code gets translated into something like:

_context_push (); for (var $itor in CAlien.$instances) { with (CAlien.$instances [$itor]) { $context = self; x += 3; } } $context = _context_pop ();

Since nested with blocks are possible, there must be a way to access either the inner or outer object explicitly. This done through the self keyword for the inner instance or whatever instance an event is operating on (the counterpart of JavaScript's this keyword) and other for the outer instance (the self at the scope above the current with block). An example use of this is to make all aliens move towards any player characters that are within 100 pixels of them:

with (CPlayer) { with (CAlien) { if (point_distance (self.x, other.x, self.y, other.y) < 100) { move_towards_point (other.x, other.y, 5); } } }

To keep track of the current context and the other reference, a stack of contexts is kept and managed through the _context_push and _context_pop functions.

Additionally, any functions in scripts that aren't part of an event handler, are still executed with the scope of an object. To replicate this behaviour, my translator programme injects a with (this) { ... } to surround the body of every GML function after translation to JavaScript.

Shims, redirects and stub functions

Because GML isn't object oriented in the way that JavaScript or most other modern language are, outside the classes and instances that are placed on the game screen, all of its functionality is in functions rather than method calls. As the things these functions do are for a large part trivial, there are many shims that can be trivially implemented in JavaScript directly, greatly simplifying the code of the runtime host programme. For example:

function array_length(a) { return a.length; } function ds_list_delete (list, pos) { list.splice (pos, 1); } function variable_struct_get (struct, name) { return struct ? struct [name] : undefined; }

Others can be redirected directly to their JS counterparts entirely:

json_encode = JSON.stringify;

And finally, some functionality being used is totally irrelevant to a DOS environment, so these functions are implemented by ignoring them:

function application_surface_draw_enable (flag) {}

Graphics

Blood Nova's backgrounds are drawn at a resolution of 320x180, to have a 16:9 ratio, which fits neatly into the 320x200 resolution of standard VGA mode 13h. The palette for the game's graphics contains just 42 entries, so that too is perfectly suitable for 8-bit colour, with enough space in the palette left over to do multiple versions of the base palette to enable blending.

The Blood Nova palette with multiple versions for blending.

In game, there are several cases where blending is used to achieve translucency effects. The major ones are the semi-transparent black backgrounds behind the text, a dark background for the inventory grid, and a purple background for the map grid. Unfortunately, 8-bit colour doesn't allow for perfect blending of arbitrary colours. However, those three common instances of translucency can be reproduced perfectly.

Showing off the blending effect behind the inventory panel.

As you can see in the image above, the background of the inventory panel is translucent. Any other uses of blending (which amounts to fading sprites in and out, such as during a cutscene) have to be approximated through the use of dithering. Honestly, I was quite surprised to find that it doesn't look absolutely terrible. A good example of the dithering is the letters of the Blood Nova logo on the main menu screen fading in.

Showing off the dithering effect on the Blood Nova logo.

Sound

There are plenty of sound effects in Blood Nova, all as high quality stereo recordings. Although it isn't a very common occurrence, it can happen that multiple sound effects play simultaneously, or that one-off sound effects play over a continuous background loop. This means that it won't do to just have those one-off sounds playing and cutting each other off if there is more than one at a time. To overcome that, continuous mixing of multiple channels is required. On a modern PC, this is handled by the sound hardware or even on the CPU with very little impact. On a DOS era computer, a Gravis UltraSound can handle the mixing in hardware, but for the much more common Sound Blaster family of cards, it has to be done in the CPU and the impact on performance is not insignificant. The lower the sound quality, the lower the aount of CPU power required. Additionally, the original sound effects are compressed MP3 files (and one OGG file), which requires yet more CPU power to decode.

The Creative Labs Sound Blaster card version 2.

Beside processing requirements, I'm also looking to keep the disk space requirements as low as possible. This is to make it possible to have a set of physical install disks (or downloadable disk images) as a means for distributing the game. To make the sound system workable and use up as little disk space as possible, I ended up decreasing the sound quality to 11kHz 8bit mono. Yes, this doesn't sound nearly as good as the originals, but I feel it is an acceptable compromise and also adds a bit of a retro charm to the sound. Here's a short video demonstrating the sound playback.

Writing (and especially debugging) the sound mixer code was an exercise in masochism, as the machine I'm writing the code on isn't very powerful and doesn't have great sound capabilities. Also, when programming under DOS, the debugging facilities aren't nearly as advanced as when writing under a modern OS with modern tools. That, and interrupt handlers are notoriously difficult to debug. Still, the results are worth it. I have elected to support only version 2 of the Sound Blaster card or later, including the SB Pro, SB16, and SB AWE. This is because the 1.0 and 1.5 revisions don't support auto-initialising DMA, which can be worked around, but that can result in audible clicks.

Music

Blood Nova's soundtrack is composed primarily by Donovan Jonk of Megahammer Studios. It would be quite possible to just use the digital tracks from the mainline version and play them back using the sound mixer described above. However, this would require a significant amount of disk space for the sampled tracks, and it wouldn't be very common for a DOS game not distributed on CD to have a fully digital soundtrack. More appropriate for the era was the use of an AdLib or Sound Blaster for the lower end, a Gravis UltraSound in the mid-range or at the high end of the spectrum, a Roland MT-32 or one of its General MIDI successors.

The Roland MT-32 MIDI module.

Therefore, the DOS version will also make use of a MIDI soundtrack. These tracks will be played back through the MIDI port through a General MIDI compatible synthesizer for best results, with a custom version being created for the Roland MT-32 (which has different instrument patches and velocity and panning response and different MIDI channel assignments). A cut-down version can be played through the OPL2 FM chip on the AdLib and Sound Blaster cards and I'm also exploring the possibility of implementing a Gravis UltraSound driver.

Note that the tune in the video above is from the Gravis UltraSound driver CD, played back through a Roland Sound Canvas MIDI synthesizer. The song is designed to show off the abilities of a MIDI synthesizer and, while it is great for demonstration purposes and testing whether the code works correctly, it sounds nothing like the actual Blood Nova sound track will. I can't take credit for writing this tune or for the sound quality of the Roland synth.

Conclusion

One final thing to end on. To make the nostalgia experience complete, there should be a configuration utility to choose the sound devices and I opted to model mine on the old style Sierra SCi installers.

I hope this will have given you a bit of insight into the process and the effort that goes into creating a DOS port of a modern retro game. The DOS version of Blood Nova will not be available separately, as I don't have the publishing rights for it, but will be freely available to people who have backed the game on Kickstarter, and may be offered as an extra download for the Steam and Itch versions too.

If you haven't backed the project on Kickstarter, you will be able to get the game on Steam when it is released, so go right ahead and wishlist Blood Nova on Steam.

Comments

No comments, yet...

Post a comment