-
-
Notifications
You must be signed in to change notification settings - Fork 62
DukeScript
DukeScript is a textual file format used by Duke Nukem II to implement various UI, most notably the help/info 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.
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 //
. 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 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 type and number of arguments depends on the command. Arguments are separated by a space.
Commands are usually executed in linear fashion from top to bottom, although there are some mechanisms for more complex execution sequences. But let's focus on regular execution first.
Many commands are displaying images or text on screen. With most commands, this will be drawn on top of what's already visible on screen, which makes it possible to build up an image out of smaller elements.
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:
- Full-screen images (
LOADRAW
command) come with a matching palette, and will thus always appear the same. - 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 theGETPAL
command instead to load a specific palette.
With all of this in mind, let's have a look at the available 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.
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) 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 3,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/implement - but that's what they did.
SETCURRENTPAGE
- TBD
WAIT
- pauses script execution until the user presses any key. Animations like the news reporter's talking mouth or the spinning arrow menu selection cursor continue to play.
DELAY <duration>
- pauses script execution until the specified amount of time has elapsed, or the user presses any key. Animations keep playing, like with WAIT
. 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.
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 continue to play while a DELAY
or WAIT
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.
These commands allow showing a message box on screen, which has a brief slide in animation, and automatically resizes to be big enough for the text that's to be shown. A message box is defined using multiple commands, one to start the declaration, and one subsequent command for each line of text.
CENTERWINDOW
- marks the start of a message box declaration. Any subsequent CWTEXT
and SKLINE
commands will be included in the message box. The message box declaration is complete once any other type of command besides the aforementioned two is encountered. At that point, the message box slide in animation is shown, the text is drawn into the message box, and then script execution continues.
CWTEXT <text>
- adds the given text to the message box as one line.
SKLINE
- adds an empty line to the message box.
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 at all the scripts were updated.
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.
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.
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:
TODO
The way menus are done in DukeScript is somewhat convoluted, it seems more like a hack on top of the paged content functionality to me. It would seem most natural to me to describe a menu by listing all the entries, and then handling selection etc. in the script runner. But DukeScript works differently.
Menus are almost the same as paged content, but all the pages are identical except for which menu item is highlighted/selected. In other words, there is one page for each menu item. Selecting different menu items is accomplished by switching pages. The script runner plays a sound effect each time a page is switched. Finally, 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.
The paged 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 once. The pages then only contain commands to redraw the previously selected item (to make it appear unselected again) as well as the newly selected item (to make it appear selected). This means that the text for all menu items still needs to be duplicated several times, but at least the background and other elements occur only once.
This is probably best illustrated by example:
TODO
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?
MENU <index>
TOGGS <x_pos> <count> [<y_pos> <id>]
Z <index>