ACS0 instruction set
From DoomWiki.org
This is a listing of the ACS0 instruction set, which is part of the Hexen BEHAVIOR lump. Each script must have a valid stream of ACS0 instructions, which consist of 1 or more 4-byte little-endian integers (or DWORDs). In the vanilla executable, undefined instructions, invalid variable indices, and stack under- or overflows will cause undefined behavior in the interpreter.
Valid ACS0 instructions range from 1 to 7 DWORDs in size, with the opcode as the first 4 bytes and any immediate arguments ("imm") following. The ACS0 interpreter is a pure stack machine, meaning that there are no explicit data registers. General memory access is not provided by the interpreter; only pre-defined arrays of script, map, and world variables are available for reading or writing.
In the vanilla interpreter, integer division and modulus by zero are undefined and unguarded, and will throw a native CPU exception.
Understanding of C programming language operators and syntax is required for some portions of the table below.
Contents
Opcodes[edit]
Opcode | DWORDs | Name | Args | Stack diff | Logic |
---|---|---|---|---|---|
0 | 1 | NOP | - | 0 | No effect |
1 | 1 | Terminate | - | 0 | Stops the script, terminating its thinker |
2 | 1 | Suspend | - | 0 | Suspends execution of the script until it is manually resumed |
3 | 2 | PushNumber | Imm | +1 | Pushes immediate argument onto the stack |
4 | 2 | LSpec1 | Imm, Stack | -1 | Executes immediate argument as line special number, with 1 argument popped from stack |
5 | 2 | LSpec2 | Imm, Stack x2 | -2 | Executes immediate argument as line special number, with 2 arguments popped from stack[notes 1] |
6 | 2 | LSpec3 | Imm, Stack x3 | -3 | Executes immediate argument as line special number, with 3 arguments popped from stack |
7 | 2 | LSpec4 | Imm, Stack x4 | -4 | Executes immediate argument as line special number, with 4 arguments popped from stack |
8 | 2 | LSpec5 | Imm, Stack x5 | -5 | Executes immediate argument as line special number, with 5 arguments popped from stack |
9 | 3 | LSpec1Direct | Imm x2 | 0 | Executes immediate argument 0 as line special number, with immediate argument 1 as args[notes 2] |
10 | 4 | LSpec2Direct | Imm x3 | 0 | Executes immediate argument 0 as line special number, with immediate arguments 1 - 2 as args |
11 | 5 | LSpec3Direct | Imm x4 | 0 | Executes immediate argument 0 as line special number, with immediate arguments 1 - 3 as args |
12 | 6 | LSpec4Direct | Imm x5 | 0 | Executes immediate argument 0 as line special number, with immediate arguments 1 - 4 as args |
13 | 7 | LSpec5Direct | Imm x6 | 0 | Executes immediate argument 0 as line special number, with immediate arguments 1 - 5 as args |
14 | 1 | Add | Stack x2 | -1 | Push(Pop() + Pop()) |
15 | 1 | Subtract | Stack x2 | -1 | t = Pop(); Push(Pop() - t) |
16 | 1 | Multiply | Stack x2 | -1 | Push(Pop() * Pop()) |
17 | 1 | Divide | Stack x2 | -1 | t = Pop(); Push(Pop() / t) |
18 | 1 | Modulus | Stack x2 | -1 | t = Pop(); Push(Pop() % t) |
19 | 1 | EQ | Stack x2 | -1 | Compare equality; Push(Pop() == Pop()) |
Opcode | DWORDs | Name | Args | Stack diff | Logic |
20 | 1 | NE | Stack x2 | -1 | Compare non-equality; Push(Pop() != Pop()) |
21 | 1 | LT | Stack x2 | -1 | Compare less-than; t = Pop(); Push(Pop() < t) |
22 | 1 | GT | Stack x2 | -1 | Compare greater-than; t = Pop(); Push(Pop() > t) |
23 | 1 | LE | Stack x2 | -1 | Compare less-than-or-equal; t = Pop(); Push(Pop() <= t) |
24 | 1 | GE | Stack x2 | -1 | Compare greater-than-or-equal; t = Pop(); Push(Pop() >= t) |
25 | 2 | AssignScriptVar | Imm, Stack | -1 | scriptvars[imm] = Pop() |
26 | 2 | AssignMapVar | Imm, Stack | -1 | mapvars[imm] = Pop() |
27 | 2 | AssignWorldVar | Imm, Stack | -1 | worldvars[imm] = Pop() |
28 | 2 | PushScriptVar | Imm | +1 | Push(scriptvars[imm]) |
29 | 2 | PushMapVar | Imm | +1 | Push(mapvars[imm]) |
30 | 2 | PushWorldVar | Imm | +1 | Push(worldvars[imm]) |
31 | 2 | AddScriptVar | Imm, Stack | -1 | scriptvars[imm] += Pop() |
32 | 2 | AddMapVar | Imm, Stack | -1 | mapvars[imm] += Pop() |
33 | 2 | AddWorldVar | Imm, Stack | -1 | worldvars[imm] += Pop() |
34 | 2 | SubScriptVar | Imm, Stack | -1 | scriptvars[imm] -= Pop() |
35 | 2 | SubMapVar | Imm, Stack | -1 | mapvars[imm] -= Pop() |
36 | 2 | SubWorldVar | Imm, Stack | -1 | worldvars[imm] -= Pop() |
37 | 2 | MulScriptVar | Imm, Stack | -1 | scriptvars[imm] *= Pop() |
38 | 2 | MulMapVar | Imm, Stack | -1 | mapvars[imm] *= Pop() |
39 | 2 | MulWorldVar | Imm, Stack | -1 | worldvars[imm] *= Pop() |
Opcode | DWORDs | Name | Args | Stack diff | Logic |
40 | 2 | DivScriptVar | Imm, Stack | -1 | scriptvars[imm] /= Pop() |
41 | 2 | DivMapVar | Imm, Stack | -1 | mapvars[imm] /= Pop() |
42 | 2 | DivWorldVar | Imm, Stack | -1 | worldvars[imm] /= Pop() |
43 | 2 | ModScriptVar | Imm, Stack | -1 | scriptvars[imm] %= Pop() |
44 | 2 | ModMapVar | Imm, Stack | -1 | mapvars[imm] %= Pop() |
45 | 2 | ModWorldVar | Imm, Stack | -1 | worldvars[imm] %= Pop() |
46 | 2 | IncScriptVar | Imm | 0 | scriptvars[imm] += 1 |
47 | 2 | IncMapVar | Imm | 0 | mapvars[imm] += 1 |
48 | 2 | IncWorldVar | Imm | 0 | worldvars[imm] += 1 |
49 | 2 | DecScriptVar | Imm | 0 | scriptvars[imm] -= 1 |
50 | 2 | DecMapVar | Imm | 0 | mapvars[imm] -= 1 |
51 | 2 | DecWorldVar | Imm | 0 | worldvars[imm] -= 1 |
52 | 2 | Goto | Imm | 0 | Unconditional jump to immediate absolute offset |
53 | 2 | IfGoto | Imm, Stack | -1 | Jump to immediate absolute offset if value popped from stack is not zero |
54 | 1 | Drop | - | -1 | Pops the stack, discarding the value |
55 | 1 | Delay | Stack | -1 | Sets script delay counter to popped value |
56 | 2 | DelayDirect | Imm | 0 | Sets script delay counter to immediate operand |
57 | 1 | Random | Stack x2 | -1 | high = Pop(); low = Pop(); Push(low + (P_Random() % (high - low + 1))) |
58 | 3 | RandomDirect | Imm x2 | +1 | low = imm0; high = imm1; Push(low + (P_Random() % (high - low + 1))) |
59 | 1 | ThingCount | Stack x2 | -1 | tid = Pop(); type = Pop(); Push(ThingCount(type, tid)) |
Opcode | DWORDs | Name | Args | Stack diff | Logic |
60 | 3 | ThingCountDirect | Imm x2 | +1 | type = imm0; tid = imm1; Push(ThingCount(type, tid)) |
61 | 1 | TagWait | Stack | -1 | Pop stack and set value to script waitValue; set script state to "waiting for tag" |
62 | 2 | TagWaitDirect | Imm | 0 | Assign imm0 to script waitValue; set script state to "waiting for tag" |
63 | 1 | PolyWait | Stack | -1 | Pop stack and assign value to script waitValue; set script state to "waiting for polyobject" |
64 | 2 | PolyWaitDirect | Imm | 0 | Assign imm0 to script waitValue; set script state to "waiting for polyobject" |
65 | 1 | ChangeFloor | Stack x2 | -2 | flat = strings[Pop()]; tag = Pop(); ChangeFloor(tag, flat) |
66 | 3 | ChangeFloorDirect | Imm x2 | 0 | tag = imm0; flat = strings[imm1]; ChangeFloor(tag, flat) |
67 | 1 | ChangeCeiling | Stack x2 | -2 | flat = strings[Pop()]; tag = Pop(); ChangeCeiling(tag, flat) |
68 | 3 | ChangeCeilingDirect | Imm x2 | 0 | tag = imm0; flat = strings[imm1]; ChangeCeiling(tag, flat) |
69 | 1 | Restart | - | 0 | Unconditional jump to beginning of script's bytecode |
70 | 1 | AndLogical | Stack x2 | ? | Push(Pop() && Pop())[notes 3] |
71 | 1 | OrLogical | Stack x2 | ? | Push(Pop() || Pop())[notes 3] |
72 | 1 | AndBitwise | Stack x2 | -1 | Push(Pop() & Pop()) |
73 | 1 | OrBitwise | Stack x2 | -1 | Push(Pop() | Pop()) |
74 | 1 | EorBitwise | Stack x2 | -1 | Push(Pop() ^ Pop()) |
75 | 1 | NegateLogical | Stack x2 | 0 | Push(!Pop()) |
76 | 1 | LShift | Stack x2 | -1 | t = Pop(); Push(Pop() << t) |
77 | 1 | RShift | Stack x2 | -1 | t = Pop(); Push(Pop() >> t) |
78 | 1 | UnaryMinus | Stack | 0 | Push(-Pop()) |
79 | 2 | IfNotGoto | Imm, Stack | -1 | Jump to immediate absolute offset if popped value is equal to zero |
Opcode | DWORDs | Name | Args | Stack diff | Logic |
80 | 1 | LineSide | - | +1 | Pushes side (0 or 1) on which the linedef was activated to start this script (always 0 if not started by a linedef) |
81 | 1 | ScriptWait | Stack | -1 | Pop value and assign to script waitValue; set script state to "waiting on script" |
82 | 2 | ScriptWaitDirect | Imm | 0 | Assign immediate operand to script waitValue; set script state to "waiting on script" |
83 | 1 | ClearLineSpecial | - | 0 | If this script was started from a linedef, that linedef's special will be set to 0. |
84 | 3 | CaseGoto | Imm x2, Stack | 0 or -1 | Conditional jump for switch case statements; CaseGoto() |
85 | 1 | BeginPrint | - | 0 | Clears print buffer |
86 | 1 | EndPrint | - | 0 | If script activator is valid player, send accumulated print buffer to that player; otherwise, send to local client player (consoleplayer) |
87 | 1 | PrintString | Stack | -1 | Concatenate strings[Pop()] with print buffer |
88 | 1 | PrintNumber | Stack | -1 | Pop value, convert integer to string as by snprintf with "%d" format specifier, and concatenate with print buffer |
89 | 1 | PrintCharacter | Stack | -1 | Pop value, convert to ASCII character, and concatenate with print buffer |
90 | 1 | PlayerCount | - | +1 | Push number of valid players currently in game |
91 | 1 | GameType | - | +1 | Push game type value: 0 == single player, 1 == cooperative, 2 == deathmatch |
92 | 1 | GameSkill | - | +1 | Push current game skill value as value from 0 (baby) to 4 (extra hard, ie. Nightmare) |
93 | 1 | Timer | - | +1 | Push the amount of tics that have passed in the current level (leveltime) |
94 | 1 | SectorSound | Stack x2 | -2 | volume = Pop(); name = strings[Pop()]; SectorSound(name, volume) |
95 | 1 | AmbientSound | Stack x2 | -2 | volume = Pop(); name = strings[Pop()]; S_StartSoundAtVolume(NULL, S_GetSoundID(name), volume) |
96 | 1 | SoundSequence | Stack | -1 | name = strings[Pop()]; SoundSequence(name) |
97 | 1 | SetLineTexture | Stack x4 | -4 | texture = strings[Pop()]; position = Pop(); side = Pop(); lineTag = Pop(); SetLineTexture(lineTag, side, position, texture) |
98 | 1 | SetLineBlocking | Stack x2 | -2 | Pop blocking flag (0 or 1), pop line id. Set all linedefs matching the id to blocking or non-blocking depending on flag value (1 or 0). |
99 | 1 | SetLineSpecial | Stack x7 | -7 | Pop arguments 4 through 0 in that order; pop special; pop line id. Set all linedefs matching the line id to have the indicated special and arguments. |
100 | 1 | ThingSound | Stack x3 | -3 | volume = Pop(); sound = strings[Pop()]; tid = Pop(); ThingSound(tid, sound, volume) |
101 | 1 | EndPrintBold | - | 0 | Send the accumulated print buffer to every valid player in the game as a yellow-colored important message |
Opcode | DWORDs | Name | Args | Stack diff | Logic |
Notes[edit]
- ↑ Arguments for stack-based LSpec opcodes are placed in the args array starting from the highest index, meaning they should be pushed in ascending order (ie., 0, 1, 2, 3, 4).
- ↑ Arguments for immediate LSpecDirect opcodes are placed in the args array in the same order as they occur in the opcode.
- ↑ 3.0 3.1 As written in the vanilla executable, these operations are both subject to short-circuiting logic. As a result, they may potentially leave the stack in an unpredictable state (either 0 or -1) depending on whether the first value popped is 0 or 1.
Algorithms[edit]
ThingCount[edit]
; return count number of things on level of type, tid, or tid and type function ThingCount(type, tid) -> integer: if tid and type are both zero: return zero if tid is non-zero: if thing type is non-zero: return number of things with tid and type else: return number of things with tid if tid is zero: return number of things with type
ChangeFloor[edit]
; change all tagged sector floor textures to the indicated flat ; the game will crash if that flat does not exist function ChangeFloor(tag, flat): let flatnum := R_FlatNumForName(flat) for sector in sectors: if sector.tag = tag: sector.floorpic := flatnum
ChangeCeiling[edit]
; change all tagged sector ceiling textures to the indicated flat ; the game will crash if that flat does not exist function ChangeCeiling(tag, flat): let flatnum := R_FlatNumForName(flat) for sector in sectors: if sector.tag = tag: sector.ceilingpic := flatnum
CaseGoto[edit]
; test top of stack for equality with immediate value ; if matches, pop top of stack and set script IP to immediate offset (goto) ; otherwise, leave stack as-is for next case branch or explicit drop instruction function CaseGoto(): let value := imm0 let offset := imm1 if Top() = value: ; peeks top value on stack ip := Offset() Drop() ; removes top value from stack
SectorSound[edit]
; play sound by name at volume either globally, or at the script activation ; line's front-sector sound origin if the script activation line is valid function SectorSound(name, volume): let mobj := null if ACScript.line is not null: mobj := ACScript.line.frontsector.soundorg S_StartSoundAtVolume(mobj, S_GetSoundID(name), volume)
SoundSequence[edit]
; Start named sound sequence on the script activation line's front sector. ; SN_StartSequenceName will have no effect if passed mobj = null. function SoundSequence(name): let mobj := null if ACScript.line is not null: mobj := ACScript.line.frontsector.soundorg SN_StartSequenceName(mobj, name)
SetLineTexture[edit]
; Set all linedefs' (with ID matching "tag") textures at indicated position (top, middle, or bottom) ; to the named texture function SetLineTexture(tag, side, position, texture): let texnum := R_TextureNumForName(texture) for line in lines: if line.id = tag: if position = TEXTURE_MIDDLE: ; TEXTURE_MIDDLE = 1 sides[line.sidenum[side]].midtexture := texnum else if position = TEXTURE_BOTTOM: ; TEXTURE_BOTTOM = 2 sides[line.sidenum[side]].bottomtexture := texnum else ; TEXTURE_MIDDLE = 0 sides[line.sidenum[side]].toptexture := texnum
ThingSound[edit]
; Play the named sound at indicated volume at every thing with the indicated tid function ThingSound(tid, name, volume): let soundid := S_GetSoundID(name) for mobj in mobjs: if mobj.tid = tid: S_StartSoundAtVolume(mobj, soundid, volume)
ACS | |
---|---|
Fundamentals: | ACS • ACS0 instruction set • BEHAVIOR |
Utilities: | ACC • DEACC • Descript • listacs |
Hexen usage: | Hexen scripted ambient sounds • Hexen scripted monster spawning • Mash monsters |
Other: | ACS mini game |