Saving Memory in DOS

Once you start adding things to the original Wolfenstein 3D engine, you may quickly come across an "Abnormal Program Termination" error. This is because for all of Wolfenstein 3D's ingenuity, it was restricted by DOS's capabilities. Prominently, a restriction in the amount of memory available to the game.

As explained in MCS' guide "Abnormal Program Termination and what to do about it", the original source code only has 83 bytes of free data to work with, which can quickly be consumed when adding things such as new audio clips.

These problems have become largely obsolete, with the introduction and adoption of Wolf4GW and Wolf4SDL; both source ports that remove the hurdle of memory restrictions.

However for those who like to work in the original engine, there are many things you can do to free up space and have more room to work with in the engine. Included in this guide is some of the best tricks discovered by the community to free up a lot of memory!

The first major thing a person can do to free up memory is to actually download the modified source files provided in MCS' tutorial (that was linked earlier on this page). These files are cleaned up a bit, resulting in over 700 bytes of extra memory!

It is advised that you read it for a better understanding of why we perform these exercises, but MCS' tutorial linked above also has an important guide for finding out how much memory you have.

After compiling your Wolf3D source code, a file called WOLF3D.MAP within your project (Usually located in the \OBJ folder of your source code folder) will be updated. Open this file in a text editor like Notepad+ and scroll down to the bottom. The line you are looking for is the last in the lump:

WOLF3D.MAP

This line has the critical information for us, as MCS explains on his page, the EFAD (This may be slightly different if your code is already modified) is the "hexadecimal representation of the last used byte-address in the Data Group."

By using the HEX setting in Windows Calculator, we can use this hexidecimal code to determine how much memory is left in the project. In Windows 10, this can be done by switching the calculator to "Programmer" and selecting the HEX option.

The equation you will want to input will be F000 whatever your code is. In the case of the unedited source code in the screenshot above, this is EFAD.

53

While this looks like a number, it's actually still a hexidecimal code and will need to be translated to decimal numbers. To do this, switch to the DEC option in the calculator. The number you have will switch to a decimal unit. In this case 83, meaning that you have a total of 83 free bytes of memory to use for your game.

Of course, the goal is to expand this number and make it bigger so you can add more to your game. To get a jump start on this, MCS' guide again comes through, providing some DEBUG code changes, and modified files that will free up over 700 bytes!

This guide was original written by Sockman on the DieHard Wolfers Message Boards, and will free up 1670 bytes of memory.

You will want to open up your project in BORLANDC, and navigate to the list at the bottom of the window.

WOLF3D.PRJ

Towards the top of this list are three files that are actually unnecessary for your game, but take up a lot of space. These three files are:

  • C0.ASM
  • WOLFHACK.C
  • WHACK_A.ASM

Highlight each of these three files, and delete them from the project. Build your project to make sure it works, and that's it!

Chris Chokan shared an efficient replacement for some rather large sound tables in the engine on the DieHard Wolfers Message Board, which saves 736 bytes of memory.

To take advantage of this, open the WL_GAME.C file of your project, and locate the following chunk of code:

#define ATABLEMAX 15
byte righttable[ATABLEMAX][ATABLEMAX * 2] = { 
{ 8, 8, 8, 8, 8, 8, 8, 7, 7, 7, 7, 7, 7, 6, 0, 0, 0, 0, 0, 1, 3, 5, 8, 8, 8, 8, 8, 8, 8, 8}, 
{ 8, 8, 8, 8, 8, 8, 8, 7, 7, 7, 7, 7, 6, 4, 0, 0, 0, 0, 0, 2, 4, 6, 8, 8, 8, 8, 8, 8, 8, 8}, 
{ 8, 8, 8, 8, 8, 8, 8, 7, 7, 7, 7, 6, 6, 4, 1, 0, 0, 0, 1, 2, 4, 6, 8, 8, 8, 8, 8, 8, 8, 8}, 
{ 8, 8, 8, 8, 8, 8, 8, 7, 7, 7, 7, 6, 5, 4, 2, 1, 0, 1, 2, 3, 5, 7, 8, 8, 8, 8, 8, 8, 8, 8}, 
{ 8, 8, 8, 8, 8, 8, 8, 8, 7, 7, 7, 6, 5, 4, 3, 2, 2, 3, 3, 5, 6, 8, 8, 8, 8, 8, 8, 8, 8, 8}, 
{ 8, 8, 8, 8, 8, 8, 8, 8, 7, 7, 7, 6, 6, 5, 4, 4, 4, 4, 5, 6, 7, 8, 8, 8, 8, 8, 8, 8, 8, 8}, 
{ 8, 8, 8, 8, 8, 8, 8, 8, 8, 7, 7, 7, 6, 6, 5, 5, 5, 6, 6, 7, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8}, 
{ 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 7, 7, 7, 6, 6, 7, 7, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8}, 
{ 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8}, 
{ 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8}, 
{ 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8}, 
{ 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8}, 
{ 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8}, 
{ 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8}, 
{ 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8}
};

byte lefttable[ATABLEMAX][ATABLEMAX * 2] = {
{ 8, 8, 8, 8, 8, 8, 8, 8, 5, 3, 1, 0, 0, 0, 0, 0, 6, 7, 7, 7, 7, 7, 7, 8, 8, 8, 8, 8, 8, 8}, 
{ 8, 8, 8, 8, 8, 8, 8, 8, 6, 4, 2, 0, 0, 0, 0, 0, 4, 6, 7, 7, 7, 7, 7, 8, 8, 8, 8, 8, 8, 8}, 
{ 8, 8, 8, 8, 8, 8, 8, 8, 6, 4, 2, 1, 0, 0, 0, 1, 4, 6, 6, 7, 7, 7, 7, 8, 8, 8, 8, 8, 8, 8}, 
{ 8, 8, 8, 8, 8, 8, 8, 8, 7, 5, 3, 2, 1, 0, 1, 2, 4, 5, 6, 7, 7, 7, 7, 8, 8, 8, 8, 8, 8, 8}, 
{ 8, 8, 8, 8, 8, 8, 8, 8, 8, 6, 5, 3, 3, 2, 2, 3, 4, 5, 6, 7, 7, 7, 8, 8, 8, 8, 8, 8, 8, 8}, 
{ 8, 8, 8, 8, 8, 8, 8, 8, 8, 7, 6, 5, 4, 4, 4, 4, 5, 6, 6, 7, 7, 7, 8, 8, 8, 8, 8, 8, 8, 8}, 
{ 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 7, 6, 6, 5, 5, 5, 6, 6, 7, 7, 7, 8, 8, 8, 8, 8, 8, 8, 8, 8}, 
{ 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 7, 7, 6, 6, 7, 7, 7, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8}, 
{ 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8}, 
{ 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8}, 
{ 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8}, 
{ 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8}, 
{ 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8}, 
{ 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8}, 
{ 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8}
};

This entire section of code can be replaced with something a lot simpler:

#define ATABLEMAX 9
byte stereotable[ATABLEMAX][ATABLEMAX * 2] = {
{ 8, 7, 7, 7, 7, 7, 7, 6, 0, 0, 0, 0, 0, 1, 3, 5, 8, 8},
{ 8, 7, 7, 7, 7, 7, 6, 4, 0, 0, 0, 0, 0, 2, 4, 6, 8, 8},
{ 8, 7, 7, 7, 7, 6, 6, 4, 1, 0, 0, 0, 1, 2, 4, 6, 8, 8},
{ 8, 7, 7, 7, 7, 6, 5, 4, 2, 1, 0, 1, 2, 3, 5, 7, 8, 8},
{ 8, 8, 7, 7, 7, 6, 5, 4, 3, 2, 2, 3, 3, 5, 6, 8, 8, 8},
{ 8, 8, 7, 7, 7, 6, 6, 5, 4, 4, 4, 4, 5, 6, 7, 8, 8, 8},
{ 8, 8, 8, 7, 7, 7, 6, 6, 5, 5, 5, 6, 6, 7, 8, 8, 8, 8},
{ 8, 8, 8, 8, 8, 7, 7, 7, 6, 6, 7, 7, 8, 8, 8, 8, 8, 8},
{ 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8}
};

Slightly further down the file is a function called SetSoundLoc. Locate these two lines inside of it:

	leftchannel  =  lefttable[x][y + ATABLEMAX];
	rightchannel = righttable[x][y + ATABLEMAX];

In order for the game to properly read the new table, you will need to replace these lines with the following:

        leftchannel  = stereotable[x][8 - y];
        rightchannel = stereotable[x][y + ATABLEMAX];

Assuming you did this correctly, your game should compile successfully with these changes, and a lot of free memory.

Chris Chokan of the DieHard Wolfers Message Board worked out that since character speeds and tile coordinates have set limits, some variable types may be changed to more efficient sizes in the Thinking Actor Structure, saving 604 bytes of memory.

Open the WL_DEF.H file, and locate the Thinking Actor Structure:

//--------------------
//
// thinking actor structure
//
//--------------------

typedef struct objstruct
{
	activetype	active;
	int			ticcount;
	classtype	obclass;
	statetype	*state;

	byte		flags;				//	FL_SHOOTABLE, etc

	long		distance;			// if negative, wait for that door to open
	dirtype		dir;

	fixed 		x,y;
	unsigned	tilex,tiley;
	byte		areanumber;

	int	 		viewx;
	unsigned	viewheight;
	fixed		transx,transy;		// in global coord

	int 		angle;
	int			hitpoints;
	long		speed;

	int			temp1,temp2,temp3; //temp3 may be missing if you deleted it as per Dugtrio17's guide.
	struct		objstruct	*next,*prev;
} objtype;

The lines we're looking at are:

 unsigned tilex,tiley;

and

 long speed;

As the tile coordinates stored in tilexand tiley will never exceed values of 63 (A map is only 64×64 in size), they don't need to be unsigned variables (As an unsigned variable can go all the way up to 65535!).

Additionally, a long variable has a range of over 4 billion(!), while speed only goes a little above 10,000.

So instead, let's use smaller types of variables. A byte holds numbers from 0 to 255, which is a much better size for storing tile information. An int will hold plenty of room for the speed variable, as well, while not being quite as wasteful as a long.

Change those two lines to the following, instead:

	byte	        tilex,tiley;                // unsigned changed to byte
	int		speed;                      // long changed to int

If you compile your code, you should have saved a bunch of memory without doing anything drastic!

In the source code, there's a routine for each boss and spawning them in the game. However, when you look at the functions themselves, they are all actually very similar, barring a few minor changes.

As such, we can create a single routine to work with all of them, saving some memory and cleaning up the code a little.

First, go to WL_DEF.H and locate these lines:

void SpawnDeadGuard (int tilex, int tiley);
void SpawnBoss (int tilex, int tiley);
void SpawnGretel (int tilex, int tiley);
void SpawnTrans (int tilex, int tiley);
void SpawnUber (int tilex, int tiley);
void SpawnWill (int tilex, int tiley);
void SpawnDeath (int tilex, int tiley);
void SpawnAngel (int tilex, int tiley);
void SpawnSpectre (int tilex, int tiley);
void SpawnGhosts (int which, int tilex, int tiley);
void SpawnSchabbs (int tilex, int tiley);
void SpawnGift (int tilex, int tiley);
void SpawnFat (int tilex, int tiley);
void SpawnFakeHitler (int tilex, int tiley);
void SpawnHitler (int tilex, int tiley);

That's a lot of functions! We won't be needing most of these, so delete everything except the lines for SpawnDeadGuard and SpawnGhosts, then we will add a new function, leaving us with this:

​​​​​void SpawnDeadGuard (int tilex, int tiley);
void SpawnGhosts (int which, int tilex, int tiley);
void SpawnBoss (int tilex, int tiley, int boss);

Yes there was already a SpawnBoss routine amongst what we deleted, but this one has a new variable to consider; int boss.

Now that is done, go to ACT_2.C and locate the original SpawnBoss routine.

void SpawnBoss (int tilex, int tiley)
{
   unsigned   far *map,tile;

   SpawnNewObj (tilex,tiley,&s_bossstand);
   new->speed = SPDPATROL;

   new->obclass = bossobj;
   new->hitpoints = starthitpoints[gamestate.difficulty][en_boss];
   new->dir = south;
   new->flags |= FL_SHOOTABLE|FL_AMBUSH;
   if (!loadedgame)
     gamestate.killtotal++;
}

Compared to the SpawnGretel routine slightly further down:

void SpawnGretel (int tilex, int tiley)
{
   unsigned   far *map,tile;

   SpawnNewObj (tilex,tiley,&s_gretelstand);
   new->speed = SPDPATROL;

   new->obclass = gretelobj;
   new->hitpoints = starthitpoints[gamestate.difficulty][en_gretel];
   new->dir = north;
   new->flags |= FL_SHOOTABLE|FL_AMBUSH;
   if (!loadedgame)
     gamestate.killtotal++;
}

Virtually the same, outside of specific calls. Most bosses load a majority of the same information, so as such, we can combine these functions, and call on different elements depending on the boss we assign to the new function's int boss variable.

Inside of WL_ACT2.C, delete these two functions along with the functions for all the other bosses (You can see which functions to delete by referring to what functions we deleted earlier in WL_DEF.H!), and replace them with this one:

/*
===============
=
= SpawnBoss
=
===============
*/

void SpawnBoss (int tilex, int tiley, int boss)
{
    switch (boss)
    {
#ifndef SPEAR //Wolfenstein 3D Bosses
        case en_boss:
                SpawnNewObj (tilex,tiley,&s_bossstand);
                new->obclass = bossobj;
                break;

        case en_gretel:
                SpawnNewObj (tilex,tiley,&s_gretelstand);
                new->obclass = gretelobj;
                break;

        case en_fat:
                if (DigiMode != sds_Off)
                    s_fatdie2.tictime = 140;
                else
                    s_fatdie2.tictime = 5;
                SpawnNewObj (tilex,tiley,&s_fatstand);
                new->obclass = fatobj;
                break;

        case en_schabbs:
                if (DigiMode != sds_Off)
                    s_schabbdie2.tictime = 140;
                else
                    s_schabbdie2.tictime = 5;
                SpawnNewObj (tilex,tiley,&s_schabbstand);
                new->obclass = schabbobj;
                break;

        case en_gift:
                if (DigiMode != sds_Off)
                    s_schabbdie2.tictime = 140;
                else
                    s_schabbdie2.tictime = 5;
                SpawnNewObj (tilex,tiley,&s_giftstand);
                new->obclass = giftobj;
                break;

        case en_fake:
                SpawnNewObj (tilex,tiley,&s_fakestand);
                new->obclass = fakeobj;
                break;

        case en_hitler:
                if (DigiMode != sds_Off)
                    s_hitlerdie2.tictime = 140;
                else
                    s_hitlerdie2.tictime = 5;
                SpawnNewObj (tilex,tiley,&s_mechastand);
                new->obclass = mechahitlerobj;
                break;
#else // Spear of Destiny Bosses
   case en_spectre:
      SpawnNewObj (tilex,tiley,&s_spectrewait1);
      new->obclass = spectreobj;
      break;

   case en_angel:
      if (SoundBlasterPresent && DigiMode != sds_Off)
         s_angeldie11.tictime = 105;

      SpawnNewObj (tilex,tiley,&s_angelstand);
      new->obclass = angelobj;
      break;

   case en_death:
      if (SoundBlasterPresent && DigiMode != sds_Off)
         s_deathdie2.tictime = 105;

      SpawnNewObj (tilex,tiley,&s_deathstand);
      new->obclass = deathobj;
      break;

   case en_will:
      if (SoundBlasterPresent && DigiMode != sds_Off)
         s_willdie2.tictime = 70;

      SpawnNewObj (tilex,tiley,&s_willstand);
      new->obclass = willobj;
      break;

   case en_uber:
      if (SoundBlasterPresent && DigiMode != sds_Off)
         s_uberdie01.tictime = 70;

      SpawnNewObj (tilex,tiley,&s_uberstand);
      new->obclass = uberobj;
      break;

   case en_trans:
      if (SoundBlasterPresent && DigiMode != sds_Off)
         s_transdie01.tictime = 105;

      SpawnNewObj (tilex,tiley,&s_transstand);
      new->obclass = transobj;
      break;
#endif
    }
    new->hitpoints = starthitpoints[gamestate.difficulty][boss];
    new->dir = nodir;
    new->speed = SPDPATROL;
    new->flags |= FL_SHOOTABLE|FL_AMBUSH;
    if (!loadedgame)
    gamestate.killtotal++;
}

This new routine combines all the others into one, using a Switch statement to determine any differences between the bosses. The function includes the Spear of Destiny bosses, should you be building off of that game instead.

Now, inside of your GAME.C file you should be able to locate the following code:

#ifndef SPEAR
         case 214:
            SpawnBoss (x,y);
            break;
         case 197:
            SpawnGretel (x,y);
            break;
         case 215:
            SpawnGift (x,y);
            break;
         case 179:
            SpawnFat (x,y);
            break;
         case 196:
            SpawnSchabbs (x,y);
            break;
         case 160:
            SpawnFakeHitler (x,y);
            break;
         case 178:
            SpawnHitler (x,y);
            break; 

These are the statements that tell the game what map tiles in the map editor correspond with what enemy. You'll want to replace them all with our new SpawnBoss function.

#ifndef SPEAR
         case 214:
            SpawnBoss (x,y,en_boss);
            break;
         case 197:
            SpawnBoss (x,y,en_gretel);
            break;
         case 215:
            SpawnBoss (x,y,en_gift);
            break;
         case 179:
            SpawnBoss (x,y,en_fat);
            break;
         case 196:
            SpawnBoss (x,y,en_schabbs);
            break;
         case 160:
            SpawnBoss (x,y,en_fake);
            break;
         case 178:
            SpawnBoss (x,y,en_hitler);
            break;

You will also need to do this with the statements for the Spear of Destiny bosses and the Spectres.

Now, instead of calling a seperate routine for every boss, the game will reference the one routine (SpawnBoss), and check for which boss is called within that function.