scummbler.py campfire_intro.txt
Scummbler is a program that compiles text scripts into a binary format suitable for use in Lucasarts games (which uses a bytecode scripting language called Script Creation Utility for Maniac Mansion, or SCUMM). Currently, it only supports creating scripts for V5 of the SCUMM scripting engine.
Scummbler has been designed to parse text files from ScummVM's descumm tool. This allows you to easily convert back and forth between bytecode and human-readable code.
Because Scummbly is quite low level (it is merely a human readable version of the SCUMM bytecode), I don’t think it’s appropriate for creating any new games; instead, it is more suited for modifying the scripts of existing games to extend their functionality. If you’re looking to make your own SCUMM-based adventure game, you should investigate ScummC and ScummGen.
Before you dive into Scummbly, it might be wise to check out the technical reference for the SCUMM engine on the ScummVM wiki. Also, Alban Bedel, the author of ScummC, has some great info on his website, although it focuses on SCUMM V6.
For the purposes of this document, I will refer to the textual script as Scummbly, and the bytecode script as SCUMM. Any oddities in the textual script that are artifacts of descumm’s output, I will refer to as descumm.
Run Scummbler from the command line, passing in one or more filenames of the scripts you want to parse.
scummbler.py campfire_intro.txt
The resulting file will be named as the input file, with either .out.LSCR or .out.SCRP appended to the filename (depending on whether it’s a local or global script). For example, campfire_intro.txt becomes campfire_intro.txt.out.LSCR.
This section describes the base elements that make up Scummbly.
Constants can be either bytes or words in size; this will depend on the instruction you are using.
You can also define aliases for variables or constants (see Compiler Directives).
Variables are specified by their type and an index. They can be global word variables, local script variables, or bit variables.
Var[26] Local[4] Bit[105]
There is a limit to the number of each variable type that can be used; this is defined within the game’s index file (*.000).
Variables can have indirect pointers:
Var[26 + 8] Var[26 + Var[28]]
The first example has an offset of 8, the second example has an offset of whatever the contents of Var[28] are. If Var[28] contains the value 8, it will reference the same variable that Var[26 + 8] would (i.e. Var[34]).
There are a number of global variables that are reserved for use by the SPUTM engine; these can be found in scummbler_vars.py, and are also listed in Appendix A. You can substitute any of these names for the appropraite variable.
/* These two instructions are functionally the same */ VAR_MOUSE_X -= 3 Var[44] -= 3
Note that variable names are case sensitive; unnamed variables must begin with a capital, and named variables must be in uppercase.
You can also define aliases for variables or constants (see Compiler Directives).
Generally, when a function requires a large number of values, it will take them in a list. This list is a series of values (constants or variables, sometimes both and sometimes only one type depending on the instruction), surrounded by square brackets.
chainScript(3, [Local[0], Local[1]]);
String literals are surrounded by quotes.
setObjectName(124,"bucket o' mud")
Strings may contain the the karat character ^. This can have two meanings: - if followed by a series of digits, the digits are interpreted as a number and treated as an escape character/raw byte. - otherwise, the SPUTM engine uses the ^ character to represent & display three ellipses ... .
Each line in Scummbly is considered its own instruction, with the exception of if blocks (discussed later). Instructions can be categorised into four basic types:
1) Inline operations - basic mathematical operations that act on a single variable
Local[1] += 7
2) Expression mode - complex, chained mathematical operations that can also call other functions
Exprmode Local[1] = ((124 + Var[309) * 6)
3) Boolean expressions - functions used in if statements and unless instructions.
if (Local[2] > Var[308]) {
4) Functions - performs complex actions
walkActorTo(Var[1],179,44)
Arguments for functions are seperated by commas.
There are also complex functions that can have sub-instructions. Sometimes these sub-instructions are part of the grammar for the function, other times they stand as a seperate function.
Scummbly uses C-style comments.
/* Hi! I'm a C-style comment! I'll make rude remarks behind your back so the ladies all laugh when you walk past! */
Labels are used to define targets for any jumping instructions (unless or goto instructions). They appear at the start of a line and are contained within square brackets.
[start] Local[1]++; goto start;
Instead of labels as such, descumm outputs the hex offset of each instruction in the script. Scummbler will interpret these offsets as labels.
You cannot have two labels with the same name.
To maintain compatibility with descumm, Scummbly will successfully parse but ignore a few extra things. These are:
the opcode (in parentheses between any label and the instruction)*
a trailing semi-colon (descumm is inconsistent in its usage).
[0030] (1A) Var[107] = 0;
*Note that the opcode is required for the startScript function, see its entry for more information.
Scummbly supports if/else branches, and do/while, while and C-style for loops.
if (Var[108] < 12) { ... } else if (Var[108] > 12) { ... } else { ... }
Behind the scenes, these blocks are just nice looking unless/goto statements; you can have an empty block which acts as a jump when a certain condition is met. You can get some funny output from descumm, when it thinks a conditional jump is an if blog. Also, scripts originally written in SCUMM have dummy jumps at the end of every block in an else if construct; descumm comments them out, so if you take a script from SCUMM → descumm → Scummbler, you will not get an output script that is identical to the original SCUMM script.
do { ... } while (Var[108] < 12)
while (Var[108] < 12) { ... }
for (Var[108] = 0; Var[108] < 12; Var[108]++) { ... }
In for loops, the initialiser and counting expressions are optional, and can be any Inline Operation. The loop test expression can be any Boolean Expression.
The last instruction in a script must always be the stopObjectCode instruction. You can optionally include the text END (this is to maintain compatibility with descumm).
... [0042] (A0) stopObjectCode() END
These directive must appear at the start of the script, before any instructions.
You can specify a script number; if you do, the output file will be a considered a local script, and output with the appropriate header information.
Script# 201 ...
You can define aliases for constants and variables using the #define keyword. The names can be composed of any alphanumeric character and underscores. To use them within the script, you need to prepend the $ character.
#define Fink 12 #define HAS_BEARD Bit[149] print($Fink, Text("A beard?!")); $HAS_BEARD = 1
Note that type information is lost when using #define, so you can easily end up using a word in an instruction that only accepts a variable!
All inline operations act on a target variable and an argument (either a variable or a word constant).
Local[1] = 7 /* move */ Local[1] += 7 /* addition */ Local[1] -= 7 /* subtraction */ Local[1] *= 7 /* multiplication */ Local[1] /= 7 /* division */ Local[1]++ /* increment */ Local[1]-- /* decrement */ Local[1] &= 7 /* logical AND */ Local[1] |= 7 /* logical OR */
Expression mode is a way to chain mutliple operations together. You can perform any mathematical operations (excluding logical AND, logical OR, increment and decrement), and can also call any functions that return a value. Each operation and pair of values must be surrounded by parentheses.
Exprmode Local[1] = (7 * 4); Exprmode Var[100] = ((120 + Var[165]) - 1);
When including a function, it must be surrounded by angular brackets, and must move the returning value into VAR_RESULT.
Exprmode Local[7] = ((<VAR_RESULT = getActorWidth(Local[2])> / 2) + 4);
These are specific instructions that set a flag in the SPUTM engine, indicating whether the tested condition is true or false. Most boolean operations must test a variable (you can’t have statements like if (1)...).
You can test for zero equality or inequality by just having the variable name.
if (Var[234]) {
if (!Var[234]) {
These are your standard mathematical comparisons.
if (Var[234] == 84) { if (Var[234] != 84) { if (Var[234] < 84) { if (Var[234] <= 84) { if (Var[234] > 84) { if (Var[234] >= 84) {
Compares the class of the given object number (var or word) against a list of class numbers, returns true if the object matches all of the given classes.
if (classOfIs(Var[234], [12, 36, 37])) {
descumm forgets to add a closing parenthesis to this instruction.
Branches if the given actor (var or byte) is in the given box (var or byte).
if (isActorInBox(Var[234], Var[170])) {
actorFollowCamera(VarOrByte actor)
Sets the camera to follow the given actor.
Var result = actorFromPos(VarOrWord x, VarOrWord y)
Returns the actor located at the given co-ordinates.
ActorOps(VarOrByte actor, List sub-instructions)
Unknown(VarOrByte arg1)
A dummy case, does nothing (in ScummVM).
Costume(VarOrByte cost)
Sets the actor’s costume.
WalkSpeed(VarOrByte xspeed, VarOrByte yspeed)
Sets the actor’s walking speed for x and y.
Sound(VarOrByte sound)
Associates a sound with the actor (footsteps?)
WalkAnimNr(VarOrByte walkanim)
Sets the current frame of the walking animation.
TalkAnimNr(VarOrByte startanim, VarOrByte endanim)
Sets the start and stop frames of the talking animation.
StandAnimNr(VarOrByte standanim)
Sets the current frame of the standing animation.
Nothing(VarOrByte arg1, VarOrByte arg2, VarOrByte arg3)
Unknown; seems to be unused (at least in ScummVM).
Init()
Initializes actor.
Elevation(VarOrWord elev)
Sets the actor’s elevation to the given value.
DefaultAnims()
Initializes the actor’s animation frames.
Init frame = 1
Walk frame = 2
Stand frame = 3
Talk start frame = 4
Talk stop frame = 5
Palette(VarOrByte index, VarOrByte value)
Sets the colour at the given index to a new value (another entry in the colour lookup table/CLUT). Index must be between 0 and 31 (the number of colours in a costume).
TalkColor(VarOrByte colour)
Sets the actor’s talk colour.
Name(String actorname)
Sets the actor’s name to the given string.
InitAnimNr(VarOrByte initanim)
Sets the current frame of the initial animation.
Width(VarOrByte width)
Sets the actor’s width.
Scale(VarOrByte xscale, VarOrByte yscale)
Sets the actor’s X and Y scales.
NeverZClip()
Turns off all Z-clipping for the actor.
SetZClip(VarOrByte zplane)
Forces the actor’s Z-clipping for the given Z-plane.
IgnoreBoxes()
Ignores boxes, turns off Z-clipping, and puts actor somewhere if the actor is in the current room?
FollowBoxes()
Follows boxes, turns off Z-clipping, and puts actor somewhere if the actor is in the current room?
AnimSpeed(VarOrByte speed)
Sets the animation speed.
ShadowMode(VarOrByte mode)
Sets the shadow mode.
ActorOps(8,[Init(),Costume(28),IgnoreBoxes(),NeverZClip(),WalkAnimNr(7),StandAnimNr(7),InitAnimNr(7),WalkSpeed(15,15)]);
animateCostume(VarOrByte actor, VarOrByte anim)
Starts the given animation for the given actor.
breakHere()
Deschedules the currently running thread. Execution continues at the next instruction when the thread’s next timeslot comes around.
chainScript(VarOrByte script, List arguments)
Replaces the currently running script with another one. The current script is terminated immediately and the new script (determined by the given script ID), is executed in the same thread. The new script has its local variables initialised to the list args. Uninitialised variables have undefined values.
CursorShow()
Turns the cursor on.
CursorHide()
Turns the cursor off.
UserputOn()
Enables user input.
UserputOff()
Disables user input.
CursorSoftOn()
Increments the cursor’s state?
CursorSoftOff()
Decrements the cursor’s state?
UserputSoftOn()
Increments "user input" counter (when greater than 0, user input is enabled).
UserputSoftOff()
Decrements "user input" counter (when 0 or less, user input is disabled).
SetCursorImg(VarOrByte cursor, VarOrByte charimg)
Changes the cursor image to a new one, based on image in a character set. Only used in Loom.
setCursorHotspot(VarOrByte index, VarOrByte x, VarOrByte y)
Changes the hotspot of a cursor. Only used in Loom.
Note that it starts with a lowercase letter.
InitCursor(VarOrByte cursor)
Changes the current cursor. Must be between 0 and 3 inclusive.
InitCharset(VarOrByte charset)
Initializes the given character set.
CursorCommand(List colours)
Initializes the character set data & colours to the given arguments? Must have 16 arguments?
cutscene(List arguments)
??? If required, runs a script passing the given arguments. Cutscene data given by the first argument in the list?
debug(VarOrWord arg1) debug?(VarOrWord arg1)
Passes the parameter to the interpreter’s debugger. What this does is entirely platform-specific (and may do nothing).
descumm outputs a question mark in the function name, but this is optional in Scummbly.
delay(Delay del)
Suspends the current thread for the appropriate number of 1/60ths of a second. The Delay argument is a 24-bit constant.
delayVarialbe(Var del)
var is dereferenced and the current thread suspended for that number of 1/60ths of a second.
doSentence(VarOrByte verb, VarOrWord objectA, VarOrWord objectB) doSentence(STOP)
Performs a verb sentence action, e.g. "Use monkey on hydrant". If verb is STOP, the current sentence script is stopped, status cleared etc.
drawBox(VarOrWord left, VarOrWord top, VarOrWord right, VarOrWord bottom, VarOrByte colour)
Draws a solid box on the backbuffer from (left, top) to (right, bottom) in the given colour.
drawObject(VarOrWord object) drawObject(VarOrWord object))
Adds the object to the drawing queue. descumm adds an extra closing parenthesis.
drawObject(VarOrWord object, setXY(VarOrWord x, VarOrWord y))
Moves the given object to the given co-ordinates, and adds it to the drawing queue.
drawObject(VarOrWord object, setImage(VarOrWord image))
Sets the state of the object, adds it to the drawing queue.
dummy(A7)
V5 specific - The KIXX XL release of Monkey Island 2 (Amiga disk) used this opcode as a dummy, in order to remove copy protection and keep level selection.
endCutscene
Ends the cutscene, performing cleanup if necessary.
descumm outputs this instructions without a trailing semi-colon.
faceActor(VarOrByte actor, VarOrWord object)
Tells actor to face object (presumably based on X/Y co-ordinates).
Var result = findInventory(VarOrByte owner, VarOrByte index)
Searches for all objects owned by owner (an actor or object), and returns the one at the given index (offset).
Var result = findObject(VarOrByte x, VarOrByte y)
Returns the first "touchable" local object at the given co-ordinates. Will not match on the bottom or right-hand edges of an object.
descumm checks for words here while ScummVM looks for bytes; Scummbler looks for bytes.
freezeScripts(VarOrByte flag)
Freezes all scripts (by setting the high bit on each script’s status). If flag is >= $80, all freeze resistent scripts will also be frozen. If flag is 0, all scripts are unfrozen. Freezing effects are cumulative - i.e. if script A is frozen twice, and unfrozen once, it will still be frozen until it is unfrozen a second time.
Var result = getActorCostume(VarOrByte actor)
Returns the actor’s current costume.
Var result = getActorElevation(VarOrByte actor)
Returns the actor’s current elevation.
Var result = getActorFacing(VarOrByte actor)
Returns the current direction the actor is facing.
Var result = getActorMoving(VarOrByte actor)
Returns the actor’s moving status (0 or 1).
Var result = getActorRoom(VarOrByte actor)
Returns the room the actor currently occupies.
Var result = getActorScale(VarOrByte actor)
Returns the actor’s current scale.
Var result = getActorWalkBox(VarOrByte actor)
Returns the actor’s walkbox.
Var result = getActorWidth(VarOrByte actor)
Returns the actor’s width.
Var result = getActorX(VarOrWord actor)
Returns the actor’s current X co-ordinate.
Var result = getActorY(VarOrWord actor)
Returns the actor’s current Y co-ordinate.
Var result = getAnimCounter(VarOrByte actor)
Returns the actor’s animation counter.
Var result = getClosestObjActor(VarOrByte actor)
Returns the object closest to the actor. Items that are more than 255 units (pixels in SCUMM V5) away from the actor will not be detected.
Var result = getDist(VarOrWord x, VarOrWord y)
Returns the distance between two actors/objects (objA and objB).
Var result = getInventoryCount(VarOrByte actor)
Returns the number of items in an actor’s inventory.
Var result = getObjectOwner(VarOrWord object)
Returns the owner of an object.
Var result = getObjectState(VarOrWord object)
Returns the state of the object.
Var result = getRandomNr(VarOrByte seed)
Returns a new random number based on the given seed.
Var result = getStringWidth(VarOrByte)
Gets the length of the string pointed to by strptr (either a variable or direct byte), stores the returned value in result.
Var result = getVerbEntryPoint(VarOrWord object, VarOrWord verb)
Returns the entry point for the response code/script for when the given verb is applied to the given object. i.e. gets the start of the script to run for the specific user interaction.
Var result = isScriptRunning(VarOrByte script)
Returns 1 if the given script is running, 0 if it’s not.
Var result = isSoundRunning(VarOrByte sound)
Returns 1 if sound is running, returns 0 if sound is not running or if the given sound is 0.
lights(VarOrByte arg1, Byte arg2, Byte arg3)
If arg3 is 0, the current lights variable is set to arg1. If arg3 is 1, the size of the flashlight light is determined by arg1 and arg2, for width and height in stripes (which are equivalent to 8 pixels each).
loadRoom(VarOrByte room)
Loads the given room into memory.
loadRoomWithEgo(VarOrWord object, VarOrByte room, Word x, Word y)
Places the Ego actor in room at object’s position. If x is not -1, Ego will start moving towards the given x and y co-ordinates. If object is not in the given room an error will occur.
setBoxFlags(VarOrByte box, VarOrByte val)
Sets the box’s flags to the given value.
setBoxScale(VarOrByte box, VarOrByte val)
Sets the box’s scale.
SetBoxSlot(VarOrByte box, VarOrByte val)
Sets the box’s scale to ((val - 1) OR $8000).
createBoxMatrix()
Initializes the matrix of boxes, by determining the distances and shortest paths between boxes.
beginOverride beginOverride()
beginOverride must be immediately followed by a goto instruction.
endOverride endOverride()
Overrides are used by cutscenes; after a beginOverride instruction, if a cutscene is skipped (e.g. if the user presses the ESC key), the script will jump to the instruction pointed to by the goto that follows beginOverride (not necessarily the location of endOverride).
descumm omits the parentheses for both instructions.
panCameraTo(VarOrWord x)
pickupObject(VarOrWord object, VarOrByte room)
print(VarOrByte actor, List sub-instructions)
Sets text properties for the given actor and displays dialogue.
printEgo(List sub-instructions)
Sets text properties for the player character (VAR_EGO) and displays dialogue.
Pos(VarOrWord x, VarOrWord y)
Sets the position of the text that follows.
Color(VarOrByte colour)
Sets the colour of the text.
Clipped(VarOrWord right)
Clips the text’s right-hand side (possibly for wrapping).
RestoreBG(VarOrWord width, VarOrWord height)
Erases characters (not used in ScummVM).
Center()
Centres the text.
Left()
Left-aligns the text.
Overhead()
Overhead-aligns the text.
Text(String text)
Prints the text string that follows. text can contain escape characters (escaped with the ^ character).
PseudoRoom(Byte val, List resources) PseudoRoom(Byte val, '[IG]')
Resources are bytes. You can pass the special text IG instead of any values in the list.
putActor(Byte actor, VarOrWord x, VarOrWord y)
Puts the actor at the given co-ordinates.
putActorAtObject(Byte actor, VarOrWord object)
Puts the actor at the given object’s position. If the object can not be found, a default position is returned (the centre of the screen in ScummVM).
putActorInRoom(Byte actor, Byte room)
Puts the actor in the given room.
Resource.loadScript(VarOrByte res) Resource.loadSound(VarOrByte res) Resource.loadCostume(VarOrByte res) Resource.loadRoom(VarOrByte res)
Loads the given resource.
Resource.nukeScript(VarOrByte res) Resource.nukeSound(VarOrByte res) Resource.nukeCostume(VarOrByte res) Resource.nukeRoom(VarOrByte res)
Obliterates the resource from memory.
Resource.lockScript(VarOrByte res) Resource.lockSound(VarOrByte res) Resource.lockCostume(VarOrByte res) Resource.lockRoom(VarOrByte res)
Locks the resource.
Resource.unlockScript(VarOrByte res) Resource.unlockSound(VarOrByte res) Resource.unlockCostume(VarOrByte res) Resource.unlockRoom(VarOrByte res)
Unlocks the resource.
Resource.clearHeap()
Clears the heap.
Resource.loadCharset(VarOrByte charset)
Loads the given character set into memory.
Resource.nukeCharset(VarOrByte charset)
Obliterates the given character set from memory.
Resource.loadFlObject(VarOrByte room, VarOrWord object)
Loads an object from the given room resource.
RoomScroll(VarOrWord minX, VarOrWord maxX)
Clamps the given arguments so scrolling does not extend past the width of the room or screen.
SetScreen(VarOrWord b, VarOrWord h)
Initialises a screen.
descumm outputs an extra closing parenthesis for RoomIntensity, SetScreen, SetPalColor and possibly others.
ShakeOn()
Starts the room shaking.
ShakeOff()
Ends the room shaking.
RoomIntensity(VarOrByte scale, VarOrByte startcolour, VarOrByte endcolour)
Lightens/darkens the room’s palette.
descumm outputs an extra closing parenthesis for RoomIntensity, SetScreen, SetPalColor and possibly others.
saveLoad(VarOrByte loadflag, VarOrByte loadslot) saveLoad?(VarOrByte loadflag, VarOrByte loadslot)
Saves temporary state of the room/game (possibly autosave).
screenEffect(VarOrWord effect) screenEffect?(VarOrWord effect)
If effect is 0, fades in the room. Otherwise, fades out with the given effect (taken from the high byte of effect).
1 = iris effect 2 = box wipe (upper-left to bottom-right) 3 = box wipe (upper-right to bottem-left) 4 = inverse box wipe.
colorCycleDelay(VarOrByte index, VarOrByte delay)
Starts colour cycling with given delay between changes. index is between 0 and 16
SetPalColor(VarOrWord red, VarOrWord green, VarOrWord blue, VarOrByte index)
Adjusts the room’s palette.
descumm outputs an extra closing parenthesis for RoomIntensity, SetScreen, SetPalColor and possibly others.
SetRoomScale(VarOrByte scale1, VarOrByte y1, VarOrByte scale2, VarOrByte y2, VarOrByte slot) Unused(VarOrByte scale1, VarOrByte y1, VarOrByte scale2, VarOrByte y2, VarOrByte slot)
Sets the room’s y scales (given slot may be 1 greater than actual slot).
Unused is what descumm calls SetRoomScale. They are the same function.
setRGBRoomIntensity(VarOrWord redscale, VarOrWord greenscale, VarOrWord bluescale, VarOrWord start, VarOrWord end)
Lightens/darkens the room’s palette, with different scales for red, green and blue.
setRoomShadow(VarOrWord redscale, VarOrWord greenscale, VarOrWord bluescale, VarOrWord start, VarOrWord end)
Lightens/darkens the shadow palette, with different scales for red, green, and blue.
saveString(VarOrByte slot, String text)
Saves a string resource to the given file?
loadString(VarOrByte slot, String text)
Loads a string resource from a given file?
palManipulate(VarOrByte slot, VarOrByte start, VarOrByte end, VarOrByte time)
Manipulates palettes, strings?
descumm outputs some functions with question marks in the name; these are optional.
saveVerbs(VarOrByte start, VarOrByte end, VarOrByte mode)
For all verbs between start and end, sets the "saveid" to mode.
restoreVerbs(VarOrByte start, VarOrByte end, VarOrByte mode)
For all verbs between start and end and matching mode, kills any existing verb, sets the saveid to 0 and generally inits the verb.
deleteVerbs(VarOrByte start, VarOrByte end, VarOrByte mode)
For all verbs between start and end and matching mode, kills any existing verb.
setCameraAt(VarOrWord x)
Sets the camera’s x position.
setClass(VarOrWord actor, List classes)
Makes object inherit from all of the given classes. A class of "0" will clear all of the object’s existing class data.
setObjectName(VarOrWord object, String name)
Sets the given object’s name.
setOwnerOf(VarOrWord object, VarOrByte owner)
Sets the owner of the object.
setState(VarOrWord object, VarOrByte state)
Sets the state of the object.
setVarRange(Var startvar, List values) setVarRange(Var startvar, Byte numvalues, List values)
This assigns a list of values to a range of variables.
Values can be either bytes or words, but not both, and is determined by the maximum value present.
descumm outputs the number of values in the list as numvalues, this is optional.
setVarRange(Var[109], [1, 2, 3, 4, 5, 6]) setVarRange(Var[109],6,[1,2,3,4,5,6])
The above instructions are the same, the second instruction just explicitly states the number of values in the list that follows. After running this instruction, the affected variables will look like this:
Var[109] = 1 Var[110] = 2 Var[111] = 3 Var[112] = 4 Var[113] = 5 Var[114] = 6
soundKludge(List imuseinst)
soundKludge adds instructions to the queue for the iMUSE dynamic music system. If the first item in the list is -1, the existing sound queue is processed. Otherwise, the list of instructions is added to the queue. The instructions are just normal variables or constants.
Alban Bedel, the author of ScummC, has some info on iMUSE at his website: http://alban.dotsec.net/15.html
startMusic(VarOrByte song)
Adds the music into the queue to be played.
startObject(VarOrWord object, VarOrByte script, List arguments)
Starts the object’s script (OBCD blocks), passing the given arguments.
startScript(VarOrByte script, List arguments[, R][, F]) (0A) startScript(VarOrByte script, List arguments)
Spawns a new thread running the code in script script. The new script has its local variables initialised to the list args. Uninitialised variables have undefined values.
The R and F flags indicate whether the script is recursive and/or freeze resistent.
descumm does not output these flags, so Scummbler will look at the opcode value. However, you should not include both the opcode and the flags, as they will conflict and lead to ambiguity as to the intended meaning.
startScript(124, [Var[278]]) (0A) startScript(124, [Var[278]])
Both of the above instructions start script 124 normally.
startScript(124, [Var[278]], F) (2A) startScript(124, [Var[278]])
Both of the above instructions start script 124 with the freeze resistent flag set.
(2A) startScript(124, [Var[278]], R)
This will start script 124 with both freeze resistent and recursive flags set, even though the freeze resistent flag has not been specified in the function call, as the flag is present in the original opcode.
startSound(VarOrByte sound)
Adds the given sound to the sound queue to be played.
stopMusic()
Stops all sounds.
stopObjectCode()
Marks the calling script as dead, to be later pruned from the thread pool. Cleans up residual arrays. This must always be the last instruction in every script.
stopObjectScript(VarOrWord objscript)
Marks the given object script as dead, to be later pruned from the thread pool. Cleans up residual arrays.
stopScript(VarOrByte script)
Marks the given script as dead, to be later pruned from the thread pool. Cleans up residual arrays.
stopSound(VarOrByte sound)
Stops the given sound.
PutCodeInString(VarOrByte resID, String text)
Loads the string into the resource slot given by resID.
CopyString(VarOrByte destinationID, VarOrByte sourceID)
Creates a duplicate of the string at sourceID into destinationID. The old string at destinationID is lost.
SetStringChar(VarOrByte resID, VarOrByte index, Byte char)
Writes char at the given position index of the string resource located at slot resID. Out of bounds accesses cause undefined behaviour.
Var result = GetStringChar(VarOrByte resID, VarOrByte index)
Reads a byte (an ASCII character) at the given position index of the string resource located at slot resID, and writes it to result. Out of bounds accesses cause undefined behaviour.
CreateString(VarOrByte resID, VarOrByte size)
Allocates or frees a string resource, located at resource slot resID. The string length is initialised to size; if size is zero, the string is freed.
systemOps(Byte operation)
operation must be:
`1` or `RESTART` (to restart the game) `2` or `PAUSE` (to pause/unpause the game) `3` or `QUIT` (to quit the game)
RESTART, PAUSE, and QUIT are special tokens and should not be enclosed in quotes.
VerbOps(VarOrByte verb, List sub-instruction)
Image(VarOrWord image)
Assigns an image (object) to a verb.
Text(String name)
Assigns the name to the verb slot.
Color(VarOrByte colour)
Sets the colour of the verb.
HiColor(VarOrByte hicolour)
Sets the highlight colour of the verb.
SetXY(VarOrWord x, VarOrWord y)
Sets the verb’s top-left co-ordinates.
On()
Makes this verb active.
Off()
Makes this verb inactive.
Delete()
Kills this verb.
New()
Creates a verb in the slot for the given verb. If the slot is 0 (verb doesn’t already exist), will add it to the next unusued slot; if this exceeds the global maximum number of verbs an error will be raised.
DimColor(VarOrByte colour)
Sets the dim colour of the verb.
Dim()
Dims this verb.
Key(VarOrByte key)
Sets the key code associated with this verb.
Center()
Centres the verb.
SetToString(VarOrWord stringID)
Loads the given string resource into the verb’s slot. If either the string resource or verb slot is not found (0), the verb resource is nuked.
SetToObject(VarOrWord object, VarOrByte room)
Assigns an object from the given room to the verb (if the object’s image is not already the given object).
BackColor(VarOrByte colour)
Sets the background colour of the verb.
WaitForActor(VarOrByte actor)
If the given actor is moving, breaks this script’s execution and resumes at this instruction again.
WaitForMessage()
If the global variable VAR_HAVE_MSG (Var[3]) is not zero, breaks this script’s execution and resumes at this instruction again.
WaitForCamera()
If the camera has not reached its destination x position, breaks this script’s execution and resumes at this instruction again.
WaitForSentence()
If there is a sentence present, breaks this script’s execution and resumes at this instruction again
walkActorTo(VarOrByte actor, VarOrWord x, VarOrWord y)
Sets the actor to begin walking to the given position.
walkActorToActor(VarOrByte walkingactor, VarOrByte targetactor, Byte distance)
Walks walkingactor towards targetactor by the given distance.
walkActorToObject(VarOrByte actor, VarOrWord object)
Sets the actor to begin walking to the given object’s position.
This is the campfire cutscene from the beginning of Monkey Island 2, as decompiled by descumm.
Script# 202 [0000] (40) cutscene([1]) [0005] (0C) Resource.loadCharset(4) [0008] (58) beginOverride [000A] (18) goto 049D; [000D] (11) animateCostume(11,5) [0010] (2E) delay(60) [0014] (D8) printEgo([Text("So I bust into the church and say, `Now you're in for it, you bilious bag of barnacle bait!`")]); [0073] (AE) WaitForMessage() [0075] (D8) printEgo([Text("^and then LeChuck cries, `Guybrush! Have mercy!`^255^3`I can't take it anymore!`")]); [00C5] (AE) WaitForMessage() [00C7] (11) animateCostume(11,7) [00CA] (13) ActorOps(11,[TalkAnimNr(6,7)]); [00D0] (14) print(12,[Text("I think I know how he must have felt.")]); [00F9] (AE) WaitForMessage() [00FB] (14) print(11,[Text("Yeah, if I hear this story one more time, I'm gonna be crying myself.")]); [0144] (AE) WaitForMessage() [0146] (14) print(12,[Text("Don't you have any NEW stories?")]); [0169] (2E) delay(30) [016D] (13) ActorOps(11,[TalkAnimNr(4,5)]); [0173] (11) animateCostume(11,5) [0176] (AE) WaitForMessage() [0178] (D8) printEgo([Text("Well, actually, that's why I'm here on Scabb Island.^255^3I'm on a whole new adventure.")]); [01CE] (AE) WaitForMessage() [01D0] (14) print(11,[Text("Growing a mustache?")]); [01E7] (AE) WaitForMessage() [01E9] (D8) printEgo([Text("No.^255^3Bigger than that.")]); [0202] (AE) WaitForMessage() [0204] (14) print(11,[Text("A beard?!?")]); [0212] (AE) WaitForMessage() [0214] (D8) printEgo([Text("No, I'm in search of treasure.^255^3The biggest treasure of them all.")]); [0258] (AE) WaitForMessage() [025A] (11) animateCostume(11,7) [025D] (D8) printEgo([Text("A treasure so valuable and so well hidden, that it haunts the dreams of every pirate on the seas.")]); [02C1] (AE) WaitForMessage() [02C3] (11) animateCostume(11,5) [02C6] (14) print(11,[Text("You mean^")]); [02D3] (AE) WaitForMessage() [02D5] (11) animateCostume(11,7) [02D8] (2E) delay(60) [02DC] (11) animateCostume(11,5) [02DF] (2E) delay(30) [02E3] (11) animateCostume(11,4) [02E6] (14) print(12,[Text("Big Whoop? ^255^2")]); [0305] (14) print(255,[Color(235),Text(" Big Whoop?")]); [0334] (AE) WaitForMessage() [0336] (11) animateCostume(12,5) [0339] (11) animateCostume(11,5) [033C] (AE) WaitForMessage() [033E] (D8) printEgo([Text("None other.")]); [034C] (AE) WaitForMessage() [034E] (14) print(12,[Text("Then, why'd you come here?^255^3There's no treasure on Scabb Island!")]); [0392] (AE) WaitForMessage() [0394] (D8) printEgo([Text("Well, I didn't know that before!^255^3Now I'm trying to charter a ship and look someplace else.")]); [03F2] (AE) WaitForMessage() [03F4] (D8) printEgo([Text("When I return, I'll have riches galore, and a whole new story.")]); [0435] (AE) WaitForMessage() [0437] (14) print(12,[Text("Or you'll have died trying.")]); [0456] (AE) WaitForMessage() [0458] (13) ActorOps(11,[TalkAnimNr(6,7)]); [045E] (14) print(11,[Text("Either way, we won't have to hear about LeChuck anymore.")]); [049A] (AE) WaitForMessage() [049C] (80) breakHere() [049D] (58) endOverride [049F] (2C) InitCharset(2) [04A2] (1A) Bit[164] = 1; [04A7] (AD) putActorInRoom(VAR_EGO,4) [04AB] (81) putActor(VAR_EGO,591,140) [04B2] (33) screenEffect?(129)) [04B6] (C0) endCutscene() [04B7] (2C) CursorShow() [04B9] (2C) UserputOn() [04BB] (0A) startScript(16,[2]) [04C1] (0A) startScript(22,[]) [04C4] (D2) actorFollowCamera(VAR_EGO) [04C7] (A0) stopObjectCode() END
The very first line tells us that this is a local script; that is, it is stored within a room container, and cannot be called from any other room.
Script# 202
So, what can we do? Let’s give Guybrush a new story to tell. First, let’s define a few values for convenience. If you load up Monkey Island 2, you will discover that Fink says the line "I think I know how he must have felt.", and Bart says the line "Yeah, if I hear this story one more time, I’m gonna be crying myself.". If we look at the calling code…
... [00CA] (13) ActorOps(11,[TalkAnimNr(6,7)]); [00D0] (14) print(12,[Text("I think I know how he must have felt.")]); [00F9] (AE) WaitForMessage() [00FB] (14) print(11,[Text("Yeah, if I hear this story one more time, I'm gonna be crying myself.")]); ...
we can determine that actor 12 is Fink and actor 11 is Bart. If you look at the ActorOps instruction above it, we can see that Bart’s talking animation is changed; from playing the game we can see that it makes Bart face Fink. So, let’s modify the start of the script to define some known values!
#define BART 11 #define FINK 12 #define BART_ANIM_FF 6 #define BART_ANIM_FF_STOP 7 #define BART_ANIM_FG 4 #define BART_ANIM_FG_STOP 5 Script# 202 [0000] (40) cutscene([1]) ...
Now, let’s insert our new conversation, making use of these define values.
[0146] (14) print(12,[Text("Don't you have any NEW stories?")]); [0169] (2E) delay(30) [016D] (13) ActorOps(11,[TalkAnimNr(4,5)]); [0173] (11) animateCostume(11,5) [0176] (AE) WaitForMessage() /* Here starts our new Scummbly! */ /* Escape characters ^255^3 splits the text across two screens.*/ printEgo([Text("Uh... well...^255^3Did I tell you I travelled here by strapping two turtles to my feet?")]); WaitForMessage() print($FINK,[Text("Wasn't that in a movie?")]); WaitForMessage() animateCostume($BART,$BART_ANIM_FF_STOP) /* Bart faces Fink */ ActorOps($BART,[TalkAnimNr($BART_ANIM_FF,$BART_ANIM_FF_STOP)]); /* Change talking animation so Bart faces fink */ print($BART,[Text("I thought it was a theme park ride.")]); WaitForMessage() print($FINK,[Text("Well, I thought the ride was based on the movie.")]); WaitForMessage() /* "^" (not followed by digits) acts as "..." */ printEgo([Text("Uh^^255^3guys?")]); WaitForMessage() animateCostume($BART,$BART_ANIM_FG_STOP) /* Bart faces Guybrush */ delay(60); /* Wait 1 second */ animateCostume($BART,$BART_ANIM_FF_STOP) /* Bart faces Fink */ print($BART,[Text("Not exactly^ the movie was inspired by an earlier version of the ride.")]); WaitForMessage() print($FINK,[Text("Oh, I see.^255^3So it goes, ride, movie, ride?")]); WaitForMessage() print($FINK,[Text("Yeah, pretty much.")]); WaitForMessage() printEgo([Text("^ guuuuuyyyyysss?")]); WaitForMessage() delay(120); /* Wait 2 seconds */ print($FINK,[Text("What was that movie called?")]); WaitForMessage() animateCostume($BART,$BART_ANIM_FG_STOP) printEgo([Text("OKAY!^255^3I STOLE THAT STORY FROM A MOVIE!^255^3CAN WE MOVE ON NOW?!")]) WaitForMessage() delay(60); /* Bart nervously glances at Fink */ animateCostume($BART,$BART_ANIM_FF_STOP) delay(60); animateCostume($BART,$BART_ANIM_FG_STOP) delay(60); ActorOps($BART,[TalkAnimNr($BART_ANIM_FG,$BART_ANIM_FG_STOP)]); /* Bart address Guybrush again */ print(11,[Text("You stole that story from a RIDE-")]); delay(50); printEgo([Text("AAAUUUUGGGGHH!")]) WaitForMessage() animateCostume($BART,$BART_ANIM_FF_STOP) delay(60) print($FINK,[Text("So^^255^3^any new stories that you DIDN'T steal from a movie-slash-ride?")]); WaitForMessage() animateCostume($BART,$BART_ANIM_FG_STOP) /* And here ends our new Scummbly... *sniff* */ [0178] (D8) printEgo([Text("Well, actually, that's why I'm here on Scabb Island.^255^3I'm on a whole new adventure.")]);
Our final script now looks like this:
#define BART 11 #define FINK 12 #define BART_ANIM_FF 6 #define BART_ANIM_FF_STOP 7 #define BART_ANIM_FG 4 #define BART_ANIM_FG_STOP 5 Script# 202 [0000] (40) cutscene([1]) [0005] (0C) Resource.loadCharset(4) [0008] (58) beginOverride [000A] (18) goto 049D; [000D] (11) animateCostume(11,5) [0010] (2E) delay(60) [0014] (D8) printEgo([Text("So I bust into the church and say, `Now you're in for it, you bilious bag of barnacle bait!`")]); [0073] (AE) WaitForMessage() [0075] (D8) printEgo([Text("^and then LeChuck cries, `Guybrush! Have mercy!`^255^3`I can't take it anymore!`")]); [00C5] (AE) WaitForMessage() [00C7] (11) animateCostume(11,7) [00CA] (13) ActorOps(11,[TalkAnimNr(6,7)]); [00D0] (14) print(12,[Text("I think I know how he must have felt.")]); [00F9] (AE) WaitForMessage() [00FB] (14) print(11,[Text("Yeah, if I hear this story one more time, I'm gonna be crying myself.")]); [0144] (AE) WaitForMessage() [0146] (14) print(12,[Text("Don't you have any NEW stories?")]); [0169] (2E) delay(30) [016D] (13) ActorOps(11,[TalkAnimNr(4,5)]); [0173] (11) animateCostume(11,5) [0176] (AE) WaitForMessage() /* Here starts our new Scummbly! */ /* Escape characters ^255^3 splits the text across two screens.*/ printEgo([Text("Uh... well...^255^3Did I tell you I travelled here by strapping two turtles to my feet?")]); WaitForMessage() print($FINK,[Text("Wasn't that in a movie?")]); WaitForMessage() animateCostume($BART,$BART_ANIM_FF_STOP) /* Bart faces Fink */ ActorOps($BART,[TalkAnimNr($BART_ANIM_FF,$BART_ANIM_FF_STOP)]); /* Change talking animation so Bart faces fink */ print($BART,[Text("I thought it was a theme park ride.")]); WaitForMessage() print($FINK,[Text("Well, I thought the ride was based on the movie.")]); WaitForMessage() /* "^" (not followed by digits) acts as "..." */ printEgo([Text("Uh^^255^3guys?")]); WaitForMessage() animateCostume($BART,$BART_ANIM_FG_STOP) /* Bart faces Guybrush */ delay(60); /* Wait 1 second */ animateCostume($BART,$BART_ANIM_FF_STOP) /* Bart faces Fink */ print($BART,[Text("Not exactly^ the movie was inspired by an earlier version of the ride.")]); WaitForMessage() print($FINK,[Text("Oh, I see.^255^3So it goes, ride, movie, ride?")]); WaitForMessage() print($FINK,[Text("Yeah, pretty much.")]); WaitForMessage() printEgo([Text("^ guuuuuyyyyysss?")]); WaitForMessage() delay(120); /* Wait 2 seconds */ print($FINK,[Text("What was that movie called?")]); WaitForMessage() animateCostume($BART,$BART_ANIM_FG_STOP) printEgo([Text("OKAY!^255^3I STOLE THAT STORY FROM A MOVIE!^255^3CAN WE MOVE ON NOW?!")]) WaitForMessage() delay(60); /* Bart nervously glances at Fink */ animateCostume($BART,$BART_ANIM_FF_STOP) delay(60); animateCostume($BART,$BART_ANIM_FG_STOP) delay(60); ActorOps($BART,[TalkAnimNr($BART_ANIM_FG,$BART_ANIM_FG_STOP)]); /* Bart address Guybrush again */ print(11,[Text("You stole that story from a RIDE-")]); delay(50); printEgo([Text("AAAUUUUGGGGHH!")]) WaitForMessage() animateCostume($BART,$BART_ANIM_FF_STOP) delay(60) print($FINK,[Text("So^^255^3^any new stories that you DIDN'T steal from a movie-slash-ride?")]); WaitForMessage() animateCostume($BART,$BART_ANIM_FG_STOP) /* And here ends our new Scummbly... *sniff* */ [0178] (D8) printEgo([Text("Well, actually, that's why I'm here on Scabb Island.^255^3I'm on a whole new adventure.")]); [01CE] (AE) WaitForMessage() [01D0] (14) print(11,[Text("Growing a mustache?")]); [01E7] (AE) WaitForMessage() [01E9] (D8) printEgo([Text("No.^255^3Bigger than that.")]); [0202] (AE) WaitForMessage() [0204] (14) print(11,[Text("A beard?!?")]); [0212] (AE) WaitForMessage() [0214] (D8) printEgo([Text("No, I'm in search of treasure.^255^3The biggest treasure of them all.")]); [0258] (AE) WaitForMessage() [025A] (11) animateCostume(11,7) [025D] (D8) printEgo([Text("A treasure so valuable and so well hidden, that it haunts the dreams of every pirate on the seas.")]); [02C1] (AE) WaitForMessage() [02C3] (11) animateCostume(11,5) [02C6] (14) print(11,[Text("You mean^")]); [02D3] (AE) WaitForMessage() [02D5] (11) animateCostume(11,7) [02D8] (2E) delay(60) [02DC] (11) animateCostume(11,5) [02DF] (2E) delay(30) [02E3] (11) animateCostume(11,4) [02E6] (14) print(12,[Text("Big Whoop? ^255^2")]); [0305] (14) print(255,[Color(235),Text(" Big Whoop?")]); [0334] (AE) WaitForMessage() [0336] (11) animateCostume(12,5) [0339] (11) animateCostume(11,5) [033C] (AE) WaitForMessage() [033E] (D8) printEgo([Text("None other.")]); [034C] (AE) WaitForMessage() [034E] (14) print(12,[Text("Then, why'd you come here?^255^3There's no treasure on Scabb Island!")]); [0392] (AE) WaitForMessage() [0394] (D8) printEgo([Text("Well, I didn't know that before!^255^3Now I'm trying to charter a ship and look someplace else.")]); [03F2] (AE) WaitForMessage() [03F4] (D8) printEgo([Text("When I return, I'll have riches galore, and a whole new story.")]); [0435] (AE) WaitForMessage() [0437] (14) print(12,[Text("Or you'll have died trying.")]); [0456] (AE) WaitForMessage() [0458] (13) ActorOps(11,[TalkAnimNr(6,7)]); [045E] (14) print(11,[Text("Either way, we won't have to hear about LeChuck anymore.")]); [049A] (AE) WaitForMessage() [049C] (80) breakHere() [049D] (58) endOverride [049F] (2C) InitCharset(2) [04A2] (1A) Bit[164] = 1; [04A7] (AD) putActorInRoom(VAR_EGO,4) [04AB] (81) putActor(VAR_EGO,591,140) [04B2] (33) screenEffect?(129)) [04B6] (C0) endCutscene() [04B7] (2C) CursorShow() [04B9] (2C) UserputOn() [04BB] (0A) startScript(16,[2]) [04C1] (0A) startScript(22,[]) [04C4] (D2) actorFollowCamera(VAR_EGO) [04C7] (A0) stopObjectCode() END
Because Scummbler treats the bits in square brackets as labels, we preserve the existing jump at [000A] that goes to [049D], even though the number of instructions between them has increased.
# /* 0 */ "VAR_RESULT", "VAR_EGO", "VAR_CAMERA_POS_X", "VAR_HAVE_MSG", # /* 4 */ "VAR_ROOM", "VAR_OVERRIDE", "VAR_MACHINE_SPEED", "VAR_ME", # /* 8 */ "VAR_NUM_ACTOR", "VAR_CURRENT_LIGHTS", "VAR_CURRENTDRIVE", "VAR_TMR_1", # /* 12 */ "VAR_TMR_2", "VAR_TMR_3", "VAR_MUSIC_TIMER", "VAR_ACTOR_RANGE_MIN", # /* 16 */ "VAR_ACTOR_RANGE_MAX", "VAR_CAMERA_MIN_X", "VAR_CAMERA_MAX_X", "VAR_TIMER_NEXT", # /* 20 */ "VAR_VIRT_MOUSE_X", "VAR_VIRT_MOUSE_Y", "VAR_ROOM_RESOURCE", "VAR_LAST_SOUND", # /* 24 */ "VAR_CUTSCENEEXIT_KEY", "VAR_TALK_ACTOR", "VAR_CAMERA_FAST_X", "VAR_SCROLL_SCRIPT", # /* 28 */ "VAR_ENTRY_SCRIPT", "VAR_ENTRY_SCRIPT2", "VAR_EXIT_SCRIPT", "VAR_EXIT_SCRIPT2", # /* 32 */ "VAR_VERB_SCRIPT", "VAR_SENTENCE_SCRIPT", "VAR_INVENTORY_SCRIPT", "VAR_CUTSCENE_START_SCRIPT", # /* 36 */ "VAR_CUTSCENE_END_SCRIPT", "VAR_CHARINC", "VAR_WALKTO_OBJ", "VAR_DEBUGMODE", # /* 40 */ "VAR_HEAPSPACE", "VAR_TALK_ACTOR", "VAR_RESTART_KEY", "VAR_PAUSE_KEY", # /* 44 */ "VAR_MOUSE_X", "VAR_MOUSE_Y", "VAR_TIMER", "VAR_TMR_4", # /* 48 */ "VAR_SOUNDCARD", "VAR_VIDEOMODE", "VAR_MAINMENU_KEY", "VAR_FIXEDDISK", # /* 52 */ "VAR_CURSORSTATE", "VAR_USERPUT", "VAR_V5_TALK_STRING_Y", None, # NULL # /* 56 */ "VAR_SOUNDRESULT", "VAR_TALKSTOP_KEY", None, # NULL "VAR_FADE_DELAY", # /* 60 */ "VAR_NOSUBTITLES", None, # NULL None, # NULL None, # NULL # /* 64 */ "VAR_SOUNDPARAM", "VAR_SOUNDPARAM2", "VAR_SOUNDPARAM3", "VAR_INPUTMODE", # /* 68 */ "VAR_MEMORY_PERFORMANCE", "VAR_VIDEO_PERFORMANCE", "VAR_ROOM_FLAG", "VAR_GAME_LOADED", # /* 72 */ "VAR_NEW_ROOM"