Or: How to Make a Text Adventure Game Out of DOS Batch Files
Wing Commander: Privateer (often just called Privateer) is a space simulator game for PC, released by Origin Systems in 1993. It’s largely of the Elite lineage, but set in the Wing Commander universe and with a bit more focus on fast action and dogfighting than on simulation detail, a design concept also inherited from Wing Commander. I highly recommend trying the game if that sort of thing sounds interesting to you. If you don’t have a copy, you can buy one from GOG.com; what you’ll get from there the CD-ROM re-release which includes the expansion pack Righteous Fire, the speech pack, and a few other added features.
That’s all I’m going to say about Privateer. We’re here to talk about something else.
If you want to follow along with this article, you'll find all the files I'm talking about in the install directory of either the original or the GOG version. If you don't have or want a copy of Privateer, you can find just the relevant files nicely packaged and ready to run on the Internet Archive.
I had a copy of Privateer as a kid, and I was also constantly curious about what was going on with my computer, so obviously one of the first things I did was have a look in the game’s installation directory.
What I found there was all the usual DOS game stuff: a setup utility where you can tell the game about your sound card(s), some configuration files, lots of data files, and the game executables. But there’s another file too that didn’t look like it fit any of those categories. It’s about 10KB and it’s called
TABTNE.VDA. So naturally I needed to have a look in there.
Since I didn’t have any actual tools at the time, I opened the file in
EDIT.COM. And it was… text? I didn’t expect readable text. Okay, maybe readable is a stretch, but it is ASCII text. In fact it looked like a batch file, the old DOS version of what today we would think of as a shell script. The first line is
@echo off, which is a telltale sign;
echo off is a special form of the normal
echo command that disables the DOS shell’s normal behavior of automatically printing every command in a batch file to the console, and the
@ at the front disables echoing just that one command. So tons of DOS/Windows batch files start with
@echo off, and nothing else really does. I knew I was looking at a batch file.
Let’s Run a Questionable Script
At the time I had no concept of computer security (in my defense, hardly anybody did), so my obvious next move is to run this thing and see what happens. In hindsight, I think there must have been instructions somewhere on how to get it to work, but I didn’t have any, so let’s go through it the hard way.
I’ve got Privateer installed into an MS-DOS 6 virtual machine, so I’ll be showing you some transcripts from its command line as we go through this. The obvious first step is to rename the file with a
.BAT extension and run it:
C:\PRIVATER>ren tabtne.vba tabtne.bat C:\PRIVATER>tabtne.bat First you have to give this file its rightful name. C:\PRIVATER>
Yes, the name of the directory is missing an "e". That's because this is DOS, and file names have to follow the old 8.3 format, so that's how the game's install program handles the title being too long.
… Okay. Well. Let’s look at the file again. At the top there’s a bunch of setup and command line handling and such. Line 21 says
if not exist advent.bat goto c0 1. Scrolling a few lines later, we find the label called
:c0, which is what prints that message. So
advent.bat must be the file’s “rightful” name. We’ll rename it to that and run it again:
C:\PRIVATER>ren tabtne.bat advent.bat C:\PRIVATER>advent Please place this file in an empty directory. Then type 'advent setup'. C:\PRIVATER>
So, it needs to be in an empty directory, huh. That might be a clue to how this thing works. But for now we’ll just do as it asks and give it a directory and run the setup.
C:\PRIVATER>mkdir C:\advent C:\PRIVATER>copy advent.bat c:\advent 1 file(s) copied C:\PRIVATER>cd ..\advent C:\ADVENT>advent setup Type 'advent' to start. C:\ADVENT>
Okay! Progress! We must finally be done settings things up.
But wait a sec. What did that
setup command actually do? What setup could a batch file possibly need?
C:\ADVENT>dir Volume in drive C is MS-DOS_6 Volume Serial Number is 4F1F-8F60 Directory of C:\ADVENT . <DIR> 05-03-20 7:38p .. <DIR> 05-03-20 7:38p ADVENT BAT 9,843 08-10-92 8:04p D BAT 15 05-03-20 7:39p DROP BAT 18 05-03-20 7:39p E BAT 15 05-03-20 7:39p GET BAT 18 05-03-20 7:39p GO BAT 16 05-03-20 7:39p INV BAT 17 05-03-20 7:39p N BAT 15 05-03-20 7:39p QUIT BAT 15 05-03-20 7:39p S BAT 15 05-03-20 7:39p L BAT 18 05-03-20 7:39p TAKE BAT 18 05-03-20 7:39p U BAT 15 05-03-20 7:39p W BAT 15 05-03-20 7:39p LOOK BAT 18 05-03-20 7:39p SAY BAT 17 05-03-20 7:39p USE BAT 17 05-03-20 7:39p I BAT 17 05-03-20 7:39p EXAMINE BAT 18 05-03-20 7:39p 21 file(s) 10,140 bytes
… it made a bunch more tiny batch files. Which all have names that look suspiciously like verbs you would type in to a text adventure game parser (
W to move to the north, south, east, or west, for example). And it wanted us to call it “advent.” Surely this thing is not an entire text adventure game, hidden away inside of a batch file. Surely.
Let’s see what’s in those tiny verb batch files:
C:\ADVENT>type N.BAT @advent go n
So these all just call the “main” batch file to pass along commands (again we see the
@ to disable echo).
If you’re curious about how creating batch files from within a batch file works, here’s a few snippets of the setup code, with comments added (
REM for “remark” starts a comment line in batch files):
REM There's one of these blocks for each verb batch file. REM Check if the file we're trying to create already exists. if exist d.bat goto e1 REM If it doesn't, echo the correct command string into it. echo @advent go d > d.bat REM Here's another example that writes a different verb. e1: if exist drop.bat goto e2 REM This time the verb has a subject, so this verb's batch file REM needs to pass its first argument through to advent.bat. echo @advent drop %%1% > drop.bat REM Skipping over more of those blocks here. REM Print the instruction message. echo Type 'advent' to start.
Actually Running the Script
Now, finally, for real, let’s start it up:
C:\ADVENT>advent [ ... the screen clears ... ] --- Art D's First Batch Adventure --- A few notes before you begin: 1. Do not let your commands stray beyond two words. 2. Type 'quit' to quit. 3. Don't type DOS commands while playing (except 'dir' for a verb list). You are walking along a sunny north/south path near a small stream. A very recent landslide prevents your return to the south. There is a sharp knife here. >
Yeah, this certainly looks like the first screen of a normal text adventure game. We’re playing a text adventure game implemented entirely as a batch file, and hidden as an easter egg within another, commercially released game.
"Art D", by the way, I think must be Arthur DiBianca, who is credited as a programmer on the original Privateer and as project lead and sole programmer (among other things) for both its expansion pack Righteous Fire and the CD-ROM re-release that combined both (the original versions were sold on floppy disks).
But what does it mean about not typing any DOS commands? Aren’t we inside a game? It took our DOS prompt away, how can any DOS commands even work? Let’s try the one it does suggest and see what happens.
>dir Volume in drive C is MS-DOS_6 Volume Serial Number is 4F1F-8F60 Directory of C:\ADVENT . <DIR> 05-03-20 7:38p .. <DIR> 05-03-20 7:38p ADVENT BAT 9,843 08-10-92 8:04p D BAT 15 05-03-20 7:39p DROP BAT 18 05-03-20 7:39p E BAT 15 05-03-20 7:39p GET BAT 18 05-03-20 7:39p GO BAT 16 05-03-20 7:39p INV BAT 17 05-03-20 7:39p N BAT 15 05-03-20 7:39p QUIT BAT 15 05-03-20 7:39p S BAT 15 05-03-20 7:39p L BAT 18 05-03-20 7:39p TAKE BAT 18 05-03-20 7:39p U BAT 15 05-03-20 7:39p W BAT 15 05-03-20 7:39p LOOK BAT 18 05-03-20 7:39p SAY BAT 17 05-03-20 7:39p USE BAT 17 05-03-20 7:39p I BAT 17 05-03-20 7:39p EXAMINE BAT 18 05-03-20 7:39p 21 file(s) 10,140 bytes
Wait, that’s… that’s DOS’s
dir output. From the same directory we were already in. It’s identical to what we saw before. This thing has definitely not reimplemented
dir. It must have just dropped us right back into the DOS shell. Plus it’s confirmed our guess from before: the batch files that the setup command created really are our verbs for playing the game. But it’s lying to us a bit; if we try other DOS commands, they do still work:
>more < n.bat @advent go n >
So yeah. All it’s done is mess with our prompt to make it look like a custom thing and not a normal DOS prompt, which is totally what it still is. We’re going to play a text adventure game inside of
We’re Playing a Game Now, Apparently
Since we’re in a text adventure, let’s do the natural thing and pick up the inventory item it told us is here:
>take knife Taken. >i You are carrying a knife. >
But wait! Wait, I say! We know now that all we just did was run a couple of files called
I.BAT, which each just exited and left us back at the shell. So how does the game keep track of an inventory? The location the player is currently in is another kind of important thing for the game to know, so it must be storing that somewhere too.
The possibilities for something like this in DOS are rather limited, especially when you’re running entirely out of batch files; all the really sneaky things you could do would require running your own native code, which isn’t an option here.
We know the game can create files because that’s what its setup routine does. So let’s check for any new files it might have just made.
>dir [ ... everything is the same as before ... ] >
There’s no new files, and none of the sizes or times are different either. It’s not using any aspect of the file system to store its state.
That pretty much leaves environment variables as our only other option. Since normal DOS commands work fine while we’re in the game, we have the tools we need to check and see if it’s made any changes to the environment. What that actually means here in unmodified MS-DOS 6 is really only that we can dump the entire environment, but let’s try that.
>set COMSPEC=C:\DOS\COMMAND.COM PATH=C:\WINDOWS;C:\DOS TEMP=C:\DOS _R=$p$g _L=path _BOX=road _LAMP=nowh _SHOES=gymn PROMPT=$g _KNIFE=poss _U=echo You are carrying _T=X >
It sure has made some environment variables; everything that starts with an
_ was created by the game. It’s also edited the
PROMPT to remove the current directory; the original value of
PROMPT is being kept around in
_R so that it can be restored when we quit the game. In fact, I bet all that the
quit command does is restore the prompt and delete all the
_ variables. Let’s look at its code:
:cquit prompt %_R% set _R= set _L= set _T= set _D= set _BOX= set _KNIFE= set _LAMP= set _SHOES= REM "echo." prints a blank line. echo. echo Your environment is clear. You may proceed. echo. REM Exit the script. goto e
So let’s take stock. We can see four variables with names that look like inventory items,
_KNIFE. Doesn’t seem like very many, but the whole game is one 10 KB batch file, what do you want.
_KNIFE is set to
poss, and since we picked up the knife I’m assuming that’s short for “possessed”. The other three have values that look like names of locations, so that seems like it’s storing where those items currently are at.
_L also looks like the name of a place, so that must be where the game stores the player’s current location. We don’t know yet what
_T are for.
Unfortunately the way this system of keeping track of game state works means that our progress isn’t saved at all, unless you manually copy out all of those environment variables somewhere yourself. But this game doesn’t seem long enough for that to become a problem. Again: 10 KB batch file. The Markdown file I am writing this blog post in is almost twice that long. There’s also the risk of name collisions with existing environment variables, the game doesn’t do anything to handle that, but the leading underscores and the particular names chosen make that seem unlikely.
From here, the rest of how things work is not complicated. Each of the tiny verb batch files calls into
ADVENT.BAT with certain arguments for the verb it wants to execute, and there’s a simple parser in there that just
GOTO’s the right label for whatever command you ran. Those labels then have those own if/goto chains that do different things based on the other parameters and on those environment variables. And that’s it, that’s how it works, there really isn’t any more to it. I’ll show you one example. One of the available commands is called
drop; it takes an item out of your inventory and leaves it in whatever location you’re currently at. Here’s is the entire implementation of
REM Near the beginning of the file: if '%1'=='drop' goto cdrop REM Later on: :cdrop if '%2'=='' goto dwhat if '%2'=='box' set _T=%_BOX% if '%2'=='knife' set _T=%_KNIFE% if '%2'=='lamp' set _T=%_LAMP% if '%2'=='shoes' set _T=%_SHOES% if '%_T%'=='poss' goto dg if not '%_T%'=='' goto db echo Drop the %2? I don't think I quite understand you. goto e :dwhat echo 'Take' ain't the only transitive verb, either. goto e :db echo You aren't carrying the %2. goto e :dg set _%2=%_L% echo Dropped the %2. goto e
Even though this isn’t complicated, there are a few things to explain.
e label is at the end of the file and just exits, so going there leaves you back at the modified DOS prompt.
%2 and so on are special variables containing the command-line arguments. If we don’t pass the name of an object to drop, the game prints a message complaining at us, if we pass a name that it doesn’t know, we get a different message, and there’s a third message for if we try to drop an item we don’t have.
set is the DOS command that sets environment variables, so we can see now that, indeed, those are where all the game state is. We even see how that
_T one that we didn’t understand earlier is used; it’s just temporary storage. And if the parameter validation passes, all the game does to implement the command is set the environment variable with the same name as the object (but with an underscore prepended) to the value of the current location, which is stored in the variable
In DOS (and still today in some places in Windows), surrounding the name of an environment variable in
% is how you retrieve its value, so the
set _T commands are setting
_T to the value of one of the four inventory object variables (which we’ve already seen hold the name of the location where that object currently is).
And that’s how the whole game works. The other commands are all the same, with different checks and different messages based on whatever the relevant environment variables are. That’s all you need to have for a complete text adventure game.
The game is short, and I don’t want to spoil anything here, so I won’t show any more of it. But if you do play through to the end, you’ll receive this message:
As you emerge from the cellar, its entrance collapses behind you! You find yourself in a sunny clearing with a statue of a smiling crowned man. On its base is an inscription: 'You have passed the first test. Your reward is a single word: <redacted>. Remember it well, for it will aid you on the dark road that lies ahead. Play Art D's Next Batch Adventure and fulfill your destiny!'
I even redacted the secret world, that’s how serious I am about not spoiling anything.
But yes! There is an Art D’s Next Batch Adventure! Privateer was successful enough to get an expansion pack, called Righteous Fire, and so was Art D’s First Batch Adventure deemed worthy of a sequel to include with it. You’ll find the batch file for this one in the same place, but the file name you’re looking for now is
TABTXE.NDA. There’s even a third Batch Adventure, included in a World War I flight simulator called Wings of Glory, which Art D isn’t actually credited on (it appears he had left Origin to do his own thing by then).
And that’s it! I’m out of things to talk about here. I don’t have any conclusion to draw or point I was trying to make, I just hope you enjoyed going along on this nostalgia trip with me to see this crazy, awesome, ridiculous thing.
Code snippets that I quote here will not be identical to what’s in the original file, because I’ve undone a bit of what looks like intentional obfuscation. ↩