Skip to content

DukeScript

Nikolai Wuttke edited this page Nov 3, 2022 · 21 revisions

DukeScript is a textual file format used by Duke Nukem II to implement various UI, most notably the help screens, the main story cutscene, and all of the menus. The story sequences shown after defeating a boss don't use DukeScript, and no in-game functionality is using it either.

DukeScript (a name made up by me) is not a scripting language as it doesn't allow arbitrary logic to be programmed. But it's a means to describe a series of actions that result in a UI being drawn/changed over time, and it's possible to execute those actions on request - "scripting" the UI.

Structure of a DukeScript file

A DukeScript file contains a collection of named scripts. A named script is introduced by stating its name on a single line, followed by an empty line. Each script consists of a list of commands, with each command occupying a single line starting with //. Empty lines between commands are allowed, and will be ignored. The command //END completes each named script. Here is an example file:

ExampleScript

//FADEOUT
//END

AnotherExampleScript

//LOADRAW MESSAGE.MNI
//FADEIN
//DELAY 600
//END

The example file above declares two scripts, named ExampleScript and AnotherExampleScript, with the first one containing only one command (FADEOUT), and the 2nd one containing 3 commands.

Commands and command execution

Commands consist of a command name identifying the command to run, and optionally one or more parameters/arguments. FADEIN and FADEOUT in the example above are commands without arguments. DELAY and LOADRAW are commands with one argument. The number of arguments and their type depends on the command. Arguments are separated by a space.

Commands are usually executed in linear fashion from top to bottom, unless paging or menus are used (see below). But let's focus on regular execution first.

Many commands are displaying images or text on screen. Drawing is done in a "canvas" style: Once a command has run once to draw something, the result will stay visible on the screen until something else is drawn on top of it.

The appearance of UI elements depends on the color palette. Duke Nukem II is running a 16-color VGA display mode, which means that the pixels in images don't contain color values, but indices into a palette (an array of color values). The same image can appear differently depending on which palette is loaded.

Two types of graphics can be displayed using DukeScript:

  1. Full-screen images (LOADRAW command) come with a matching palette, and will thus always appear the same.
  2. Smaller images aka sprites (XYTEXT command) don't have a palette associated with them, so their appearance depends on the currently active palette. Full-screen images change the palette for all subsequent sprites as well. If a script doesn't use full-screen images, it can use the GETPAL command instead to load a specific palette.

With all of this in mind, let's have a look at the available commands.

General drawing commands

FADEOUT

Does a fade-out of the whole screen. The screen will remain dark until a FADEIN command is issued. But other drawing commands still work while the screen is black, there won't be anything visible immediately but they still draw to the canvas. This makes it possible to compose an image out of several elements by issuing various drawing commands, and then making everything visible at once with a fade-in.

FADEIN

Does a fade-in of the whole screen.

LOADRAW <filename>

Loads full-screen image and palette from the given file, draws the image onto the screen (replacing anything previously visible), and sets the palette for any subsequent drawing commands.

Note that the palette doesn't actually become effective until the next FADEIN command. The LOADRAW command is thus typically placed in-between a pair of FADEOUT and FADEIN commands.

GETPAL <filename>

Loads a palette from the given file. Any subsequent sprite and text drawing commands will use this palette.

XYTEXT <x> <y> <text_or_image_spec>

Displays text, or a sprite, at the position specified by x and y. The unit for positions is tiles, i.e. blocks of 8x8 pixels. Position 0,0 is at the top-left of the screen. By default, this command draws the text given in the 3rd argument (rest of the line, may contain spaces) at the specified position, but there are special "markup" bytes that make it possible to draw larger text or an image instead.

Large text is triggered by a character with a value >= 0xF0. Any text following this marker byte will be drawn with a larger font, and colorized according to the low nibble of the marker byte, which is treated as an index into the color palette. E.g. a marker byte of F5 will use the color index 5. The marker byte can occur at the beginning of the text (entire text is drawn large) or in the middle of the text (text preceding the marker byte is drawn normally, all text afterwards is drawn large). The position of the large text will be offset to the right by 2 tiles from the specified position.

Drawing images/sprites is triggered by starting the text with a character value of 0xEF. The remaining text is then interpreted as a sequence of 2 numbers. The first number always has 3 digits and indicates the actor ID (index into ACTORINFO.MNI). The next 2 digits make up the second number, which indicates the animation frame to draw for the specified actor's sprite. There is no space between these two numbers. For some reason, the position is offset by 2 tiles to the right and 1 tile down when drawing sprites.

Here's an example (using an escape character for the marker byte):

//XYTEXT 2 5 \xEF13402

This will draw sprite 134, frame 2 at position 4,6.

I don't know why the ability to draw images was implemented this way, it seems a dedicated "draw sprite" command would have been easier to use - but that's what they did.

SETCURRENTPAGE

Has no effect in RigelEngine. In the original implementation, this command makes sure that any subsequent commands draw to the front buffer, and thus show up on screen.

Timing control commands

WAIT

Pauses script execution until the user presses any key. If a menu selection cursor is currently enabled, it will be animated. If the Escape key is pressed, script execution will be terminated immediately.

DELAY <duration>

Pauses script execution until the specified amount of time has elapsed, or the user presses any key. If the Escape key is pressed, script execution will be terminated immediately. Displays the talking news reporter animation if it was enabled using BABBLEON. The duration is specified using ticks, which are based on a 140 Hz timer. This means that a value of 140 results in waiting roughly one second, and a value of 1 is roughly 7 ms.

EXITTODEMO

Enables a 30 second timer. When the time has elapsed, it launches Duke Nukem's intro/demo loop (aka "attract mode"), where the game will repeatedly show a sequence consisting of the intro movies, a pre-recorded demo of the game etc. Any user input resets this timer back to 30 seconds. Used in the main menu only.

Specialized drawing commands

PAK

Press Any Key - this is a shorthand for displaying actor nr. 146, which is an image of the text "Press any key to continue", at position 0,0. It is meant to be used with the full-screen background image MESSAGE.MNI, which has a little menu navigation legend down at the bottom of the image. The "Press Any Key" actor image is designed to seamlessly replace this navigation legend.

KEYS

Shows the currently configured key names for movement, jumping and shooting, as 6 lines of text. Very specific to the options menu, where it's used. The position of the text cannot be specified.

GETNAMES <selected_index>

Shows the names of all saved games as 8 lines of big text. The line corresponding to the given selected index will be drawn in a brighter color. The positions of the lines are hardcoded.

BABBLEON <duration>

Starts a sprite-based animation of a talking mouth (actor ID 297), matching the "news reporter" background image STORY1.MNI. The animation will play while a DELAY command is running. The animation stops after the given duration has elapsed, or when a BABBLEOFF command is issued. The duration is specified in ticks (see above).

BABBLEOFF

Stops currently running news reporter talking mouth animation (started via BABBLEON), and draws the "closed mouth" sprite frame onto the screen, to restore the image back to a "not talking" state.

Message boxes

These commands allow showing a message box on screen. A message box is defined using multiple commands, one to draw the box/frame itself, and subsequent commands for drawing the text.

Note that it's technically possible to show sprites within message boxes, but the game doesn't do that in any of its scripts. And it would also require manual positioning, unlike the text drawing functions which automatically position text to be centered within the message box.

CENTERWINDOW <y> <width> <height>

Draws a message box frame of the given dimensions at the given y position. The message box' appearance is animated with a quick slide-in. The x position of the message box is determined automatically, making the box appear horizontally centered on screen. If a SHIFTWIN command appeared previously, the box' horizontal position will be shifted to the left by 3 tiles.

Also defines the vertical starting position for any subsequent CWTEXT and SKLINE commands.

CWTEXT <text>

Draws the given text, usually into a previously drawn message box frame. The x coordinate is determined automatically, to make the text appear centered within the message box. The y coordinate is also implicit. It's initially the top of the message box, with each CWTEXT or SKLINE command advancing it by one. This means that multiple CWTEXT commands in a row will produce three separate lines of text.

Note that there is no clipping to keep the text within the message box frame, the text will overflow outside of the message box if too long.

SKLINE

Adds an empty line to a previously drawn message box. It essentially advances the implicit y position used by CWTEXT as if a line of text had been drawn, but without drawing any text.

SHIFTWIN

Offsets any subsequent message boxes to the left by 3 tiles. Cannot be undone except by executing a new script. This is used for in-game message boxes, to make them appear centered inside the gameplay area of the screen (which is made smaller compared to the full screen due to the HUD). Note that this command has no arguments, the offset is hardcoded. But some of the DukeScript files supply an argument when using this command. My theory is that the command required an argument at some point, but this was changed during development, and not all of the scripts were updated.

Paged scripts

So far, we've seen purely linear scripts, which run through a list of actions from top to bottom (possibly including some waiting time at certain points). The game's main story cutscene is an example of a linear script. But DukeScript offers two additional ways to structure script execution, paged scripts and menus. Let's focus on the former first.

A paged script is a script that contains a list of sub-scripts called pages. When the script is executed, it runs the actions for the first page. Afterwards, it switches to the next page, runs its actions, etc. When the last page has been executed, control restarts with the first page.

By using WAIT as the last command on each page, script execution will halt until a key is pressed, and then move to the next page. This allows the user to "page" through content. The instructions/help screen is done this way, for example. This is enhanced further by switching to the previous page if the left or up arrow key is pressed. Any other key will switch to the next page.

The following commands are relevant for declaring paged scripts:

NOSOUNDS

Sets up paged content without menu functionality. If this command is omitted, a menu is declared instead (see below). More concretely, this command has two effects: No sound effect is played when switching pages (as the name suggests), and pressing Enter/Space will go to the next page instead of terminating the script.

PAGESSTART

Marks the beginning of the paged content declaration, and defines the first page. Any commands following this one are included in the first page.

APAGE

Marks the beginning of a new page. Any subsequent commands will be part of the newly created page. As soon as another APAGE command is encountered, the current page is completed and the next page is started.

PAGESEND

Marks the end of the paged content.

Here's an example for a paged script, a trimmed down version of the script for the "Instructions" screen:

Instructions

//PAGESSTART
//NOSOUNDS
//SETCURRENTPAGE
//FADEOUT
//LOADRAW KEYBORD1.MNI
//FADEIN
//WAIT

//APAGE
//FADEOUT
//LOADRAW ITEMS.MNI
//FADEIN
//WAIT

//APAGE
//FADEOUT
//LOADRAW HINTS.MNI
//FADEIN
//WAIT
//PAGESEND
//END

Notice how each page contains a FADEOUT and a FADEIN command - this makes for a smooth transition between pages instead of an abrupt switch.

Menus

The way menus are done in DukeScript is somewhat convoluted, it looks a bit like a hack on top of the paged content functionality. It would seem most natural to me to describe a menu by simply listing all the entries, and then handling selection etc. in the script runner. But DukeScript works differently.

Menus are practically the same as paged content, but all the pages are identical except for the selection state. Thus, selecting different menu items is accomplished by switching pages. The script runner plays a sound effect each time a page is switched. Pressing Enter is treated as confirming the current selection, and terminates the script (as opposed to switching to the next page). After the script has completed executing, the selected index can be retrieved and used to take appropriate action, e.g. starting a new game, or executing a different script to show another menu. These actions are not part of DukeScript, but are implemented as native code in the executable.

This kind of setup could be done by having each page contain all the content, but the scripts shipping with the game are more optimized to avoid most of the duplication. The script has commands before the paged content to draw the background and all the menu items. The individual pages only contain commands to redraw the previously selected item (to make it appear unselected again) as well as redraw the newly selected item (to make it appear selected). This means that the text for all menu items needs to be duplicated several times, but at least the background and other elements occur only once.

This is probably best illustrated by example:

Skill_Select

//FADEOUT
//LOADRAW MESSAGE.MNI
//XYTEXT 0 0 \xEF28300
//MENU 0


//PAGESSTART
//Z 9
//XYTEXT 9 9 óEasy
//XYTEXT 9 12 òMedium
//XYTEXT 9 15 òHard
//WAIT

//APAGE
//Z 12
//XYTEXT 9 9 òEasy
//XYTEXT 9 12 óMedium
//XYTEXT 9 15 òHard
//WAIT

//APAGE
//Z 15
//XYTEXT 9 9 òEasy
//XYTEXT 9 12 òMedium
//XYTEXT 9 15 óHard
//WAIT
//PAGESEND
//END

I'm not sure why menus were done this way, it seems quite cumbersome to edit the scripts and make sure everything is consistent. Maybe there was a helper program which would generate DukeScript out of a more high-level definition?

The basic principle is enhanced by a few additional commands.

MENU <index>

Sets up persistent selection for the current script. This means that the selected page index will be stored when the script terminates, and restored when the script is executed again later, even if other scripts have been run in the meantime. This isn't saved on disk however, so it gets reset when quitting and restarting the game.

The index argument specifies a slot to use for persisting selection. There are 20 persistent selection storage slots in total. Different scripts can use different slots in order to have their selection persisted independently of each other. The initial value for all slots is 0, except for slot 0, which is initially 2 (1-based indexing) - this is so that the difficulty selection menu, which uses that slot index, starts out at "Medium" by default.

TOGGS <x_pos> <count> [<y_pos> <id>]

Sets up a series (with count specifying the number) of check boxes at the specified x position (given in tiles). For each checkbox, a pair of y position (in tiles) and ID has to be specified. The IDs are used to define what happens when a checkbox is toggled. The original game handles the following IDs:

ID Effect
S Toggle Soundblaster sounds
L Toggle AdLib sounds
P Toggle PC Speaker sounds
M Toggle music

RigelEngine doesn't implement this functionality, since its options menu is completely different.

To specify three checkboxes at positions 4,4, 4,6, and 4,9 with IDs A, B, and C, you would write:

//TOGGS 4 3 4 A 6 B 9 C

Z <y_pos>

Enables the animated menu selection indicator (spinning arrow). The animation continues while a WAIT command is executing. The y_pos argument specifies the vertical position in tiles. Once enabled, the cursor cannot be disabled again except by executing a different script.