- - - -

AGI/Specifications/ResourcesAGI/Specifications/Resources

Logic resources

Introduction

At the heart of Sierra's Adventure Game Interpreter is the logic file. These files contain the code that makes up the game. Each room has a logic script that goes with it. This logic script governs what can take place in that room. Here is an example of what the programmer writes when a game is being created.

Example: KQ4. Room 7.

<syntax type="C++"> if (said( open, door)) // must be close enough {

  if (posn( ego, 86, 120, 106, 133))
{
if (!night)
{
if ( door.open)
{
}
else
{
set( game.control);
set.priority( ego, 11);
start.update( door);
end.of.loop( door, door.done);
}
}
else
{
print("You can't -- it's locked");
}
}
else
{
set( notCloseEnough);
}


} </syntax>

Command list and argument types

Table 6-1 and Table 6-2 show a list of all AGI commands and their argument types. The command names have been taken from debug messages contained in some AGI games.

Table 6-1: Test commands
Opcode Command Args 1 2 3 4 5
01 equaln 2 var num
02 equalv 2 var var
03 lessn 2 var num
04 lessv 2 var var
05 greatern 2 var num
06 greaterv 2 var var
07 isset 1 flag
08 issetv 1 var
09 has 1 item
0A obj.in.room 2 item var
0B posn 5 obj num num num num
0C controller 1 ctr
0D have.key 0
0E said - ...
0F compare.strings 2 str str
10 obj.in.box 5 obj num num num num
11 center.posn 5 obj num num num num
12 right.posn 5 obj num num num num

Table 6-2: Action Commands
Opcode Command Args 1 2 4 4 5 6 7
00 return 0
01 increment 1 var
02 decrement 1 var
03 assignn 2 var num
04 assignv 2 var var
07 subn 2 var num
08 subv 2 var var
09 lindirectv 2 var var
0A rindirect 2 var var
0B lindirectn 2 var num
0C set 1 flag
0D reset 1 flag
0E toggle 1 flag
0F set.v 1 var
10 reset.v 1 var
11 toggle.v 1 var
12 new.room 1 num
13 new.room.v 1 var
16 call 1 num
17 call.v 1 var
19 draw.pic 1 var
1A show.pic 0
1C overlay.pic 1 var
1D show.pri.screen 0
21 animate.obj 1 obj
22 unanimate.all 0
23 draw 1 obj
24 erase 1 obj
25 position 3 obj num num
26 position.v 3 obj var var
27 get.posn 3 obj var var
28 reposition 3 obj var var
29 set.view 2 obj num
2A set.view.v 2 obj var
2B set.loop 2 obj num
2C set.loop.v 2 obj var
2D fix.loop 1 obj
2E release.loop 1 obj
2F set.cel 2 obj num
30 set.cel.v 2 obj var
31 last.cel 2 obj var
32 current.cel 2 obj var
33 current.loop 2 obj var
34 current.view 2 obj var
35 number.of.loops 2 obj var
36 set.priority 2 obj num
37 set.priority.v 2 obj var
38 release.priority 1 obj
39 get.priority 2 obj var
3A stop.update 1 obj
3B start.update 1 obj
3C force.update 1 obj
3D ignore.horizon 1 obj
3E observe.horizon 1 obj
3F set.horizon 1 num
40 object.on.water 1 obj
41 object.on.land 1 obj
42 object.on.anything 1 obj
43 ignore.objs 1 obj
44 observe.objs 1 obj
45 distance 3 obj obj var
46 stop.cycling 1 obj
47 start.cycling 1 obj
48 normal.cycle 1 obj
49 end.of.loop 2 obj flag
4A reverse.cycle 1 obj
4B reverse.loop 2 obj flag
4C cycle.time 2 obj var
4D stop.motion 1 obj
4E start.motion 1 obj
4F step.size 2 obj var
50 step.time 2 obj var
51 move.obj 5 obj num
52 move.obj.v 5 obj var
53 follow.ego 3 obj num
54 wander 1 obj
55 normal.motion 1 obj
56 set.dir 2 obj var
57 get.dir 2 obj var
58 ignore.blocks 1 obj
59 observe.blocks 1 obj
5A block 4 num num
5B unblock 0
5C get 1 item
5D get.v 1 var
5E drop 1 item
5F put 2 item
60 put.v 2 var var
61 get.room.v 2 var var
63 sound 2 num flag
64 stop.sound 0
65 print 1 msg
66 print.v 1 var
67 display 3 num num msg
68 display.v 3 var var var
69 clear.lines 3 num num msg
6A text.screen 0
6B graphics 0
6C set.cursor.char 1 msg
6D set.text.attribute 2 num num
6E shake.screen 1 num
6F configure.screen 3 num num num
70 status.line.on 0
71 status.line.off 0
72 set.string 2 str msg
73 get.string 2 str msg
74 word.to.string 2 word str
75 parse 1 str
76 get.num 2 str var
77 prevent.input 0
78 accept.input 0
79 set.key 3 num num num
7A add.to.pic 7 num num num num num num num
7B add.to.pic.v 7 var var var var var var var
7C status 0
7D save.game 0
7E restore.game 0
7F init.disk 0
80 restart.game 0
81 show.obj 1 num
82 random 3 num num var
83 program.control 0
84 player.control 0
85 obj.status.v 1 var
86 quit 1 num
87 show.mem 0
88 pause 0
89 echo.line 0
8A cancel.line 0
8B init.joy 0
8C toggle.monitor 0
8D version 0
8E script.size 1 num
8F set.game.id 1 msg
90 log 1 msg
91 set.scan.start 0
92 reset.scan.start 0
93 reposition.to 3 obj num num
94 reposition.to.v 3 obj var var
95 trace.on 0
96 trace.info 3 num num num
97 print.at 4 msg num num num
98 print.at.v 4 var num num num
9A clear.text.rect 5 num num num num num
9B set.upper.left 2 num num
9F enable.member 1 ctr
A0 disable.member 1 ctr
A2 show.obj.v 1 var
A3 open.dialogue 0
A4 close.dialogue 0
A5 mul.n 2 var num
A6 mul.v 2 var var
A7 div.n 2 var num
A8 div.v 2 var var
A9 close.window 0
AA set.simple 1  ???
AB push.script 0
AC pop.script 0
AE set.pri.base 1 num
B0 hide.mouse 0|1
B2 show.mouse 0
B3 fence.mouse 4 num num num num
B4 mouse.posn 2 var var
B5 release.key 0

Logic resource format

The header of each logic script is seven bytes in length for games before 1988. After this date compression seems to have been introduced and the header was subsequently altered. This compression will be discussed at a later stage.

Offset Command
0-1 Signature (0x12--0x34)
2 Vol number that the resource is contained in
3-4 Length of the resource without the header
5-6 Offset of logic code message section

All text that can be printed to the screen from within a logic script is stored in an encrypted form at the end of the logic script.

Example: KQ1. Room 2.

12 34    Signature
01       vol.1
5F 06    Length = 0x065F
BA 02    Text start = 0x02BA


Opcodes

The logic code section starts immediately after the header and continues until the start of the message section has been reached. There are three sets of codes used in a logic script. Most codes will have between one and seven arguments inclusive. This is discussed later on. The first set of codes is the AGI commands themselves listed in Table 6-2, and they have the range 0x00--0xB6.

The second set of codes is as follows:

Code Command
FF if
FE else or goto
FD not
FC or

At present these are the only high value codes encountered. The if and or codes are like brackets, i.e. the code will be at the start and the end of the section of codes that it refers to. The following example will illustrate this:

Example: KQ1, Room 2.

  FF      'if' conditions start.
07      07 = isset
05      05 = flag 5
FF      'if' conditions close.


The above translates to:

<syntax type="C++"> if (isset(5)) </syntax>

which tests whether flag number 5 is set. The 0xFF effectively switches the interpreter into a condition checking mode which leads us to the set of codes listed in Table 6-1

0x00 - 0x12    Condition codes.


When the interpreter encounters a 0xFF it will then interpret the following code values as being in the condition code range until it encounters the next 0xFF which switches it back into normal AGI command mode. The two bytes immediately following the second 0xFF determine how many bytes this if statement lasts for before the if is ended. When the second 0xFF is encountered the interpreter, be it us or the machine, does three things:

1. Reads in the following two bytes.
2. Opens a bracket.
3. Switches to AGI command mode.

Example: KQ1, Room 2.

<syntax type="C++">

FF 07 05 FF if (isset(5)) 84 00 { // For 0x0084 bytes. 18 00 load.pic(0); 19 00 draw.pic(0); 1B 00 discard.pic(0); ... ...

              }			// Closed. 0x0084 bytes counted.


</syntax>

The else command and more on brackets

The else statement will always continue after an if bracket block. This next feature is important and has caused a number of hassles in the past. When an else statement follows an if, then the bracket distance given after the if statement will be three bytes longer (this is a consequence of the way the interpreter handles if and else codes which is discussed later).

Here's an example:

<syntax type="C++"> if (isset(231)) { FF 07 E7 FF 05 00

   printf("The door is already open.");   65 0F


} else { FE 11 00

   set(36);                               0C 24
prevent.input();                       77
start.update(5);                       3B 05
assignn(152, 3);                       03 98 03
cycle.time(5, 152);                    4C 05 98
end.of.loop(5, 232);                   49 05 E8
sound(70, 154);                        63 46 9A


} </syntax>

Usually you would expect the bracket distance to be 0x0002 but in the above case it is clearly 0x0005 which illustrates the difference between a straight if statement and an if..else structure. The situation is the same for nested if..else structures.

The else statements themselves are a lot like if statements except that they're test condition is given after the 0xFE code but is instead the inverse of the condition given by the above if statement. Only the bracket distance is given after the 0xFE code and then the AGI command clock that the else statement encompasses.

Test conditions

Conditions can be one of the following types:

FF 07 05 FF                         One condition tested, ie. isset(5)
FF FD 07 05 FF                      One condition NOTed, ie. !isset(5)
FF 07 05 07 06 FF                   Multiple conditions, ANDed.
FF FC 07 05 07 06 FC FF             Multiple conditions ORed.
FF FC 07 06 07 06 FC FD 07 08 FF    Combination.


These conditions translate to:

<syntax type="C++"> if (isset(5)) if (!isset(5)) if (isset(5) && isset(6)) if (isset(5) || isset(6)) if ((isset(5) || isset(6)) && !isset(7)) </syntax>

If multiple boolean expressions are grouped together, then there respective values are ANDed together. If multiple boolean expressions are grouped together and then surrounded by a pair of 0xFC codes, then their values are ORed together.

The 0xFD code only applies to the following condition code whose boolean value it inverts.

Arguments

You may well be asking how the interpreter knows how many arguments each code has and what type of argument each argument is. This information is stored in agidata.ovl in the MS-DOS version. Inside this file there is a table which contains four bytes for each AGI command and condition code. These four bytes are interpreted as follows:

Offset Description
0-1 Pointer to implementation code
2 Number of arguments
3 Type of arguments

The type of arguments value is interpreted as follows:

Bit        7     6     5     4     3     2     1       0
command( arg1, arg2, arg3, arg4, arg5, arg6, arg7); (unknown)


If the bit is set, argument is interpreted as a variable; otherwise the argument is interpreted as a number. It is unknown what bit 0 does since no AGI command or AGI condition code has more than seven arguments.

Examples:

• 0x80 Says that the commands first argument is a variable.
• 0x60 Says that the second and third arguments are variable numbers.

The messages section

The messages section of a logic script contains all the strings that can be displayed by that logic script. These strings are encrypted by xor'ing every eleven bytes with the string "Avis Durgan".

Example: KQ1, Room 2.

<syntax type="C++"> if (said(look, alligators)) {

   print("The alligators are swimming in the moat.");


} </syntax>

In the above example, the print statement is represented as:

65 08


The 0x08 is the number given to the string and corresponds to its position in the list of strings at the end of the logic script. The format of the message section is as follows:

Offset Description
0 Number of messages
1-2 Pointer to the end of the messages
3-4 Array of message pointers
... Array of message pointers
? Start of the text data. From this point the messages are encrypted with Avis Durgan (in their unencrypted form, each message is separated by a 0x00 value)

Implementation

The implementation for each AGI statement is found in the agi/ file. This is the AGI interpreter itself. The data in the agidata.ovl file is used to find the start of the implementation for an AGI statement. Below are a couple of examples:

Example: MH2, equaln.

<syntax type="ASM">

equaln (eg. if (work = 3) )

0D71 AC LODSB  ;get variable number 0D72 32FF XOR BH,BH 0D74 8AD8 MOV BL,AL 0D76 AC LODSB  ;get test number 0D77 3A870900 CMP AL,[BX+0009]  ;test if var = number 0D7B B000 MOV AL,00  ;return 0 if not equal 0D7D 7502 JNZ 0D81 0D7F FEC0 INC AL  ;return 1 if equal 0D81 C3 RET </syntax>

Example: MH2, equalv.

<syntax type="ASM">

equalv (eg. if (work = maxwork) )

0D82 AC LODSB  ;get first var number 0D83 32FF XOR BH,BH  ;clear bh 0D85 8AD8 MOV BL,AL  ;BX = variable number 0D87 8AA70900 MOV AH,[BX+0009]  ;get first var value 0D8B AC LODSB  ;get second var number 0D8C 8AD8 MOV BL,AL 0D8E 32C0 XOR AL,AL  ;return 0 if not equal 0D90 3AA70900 CMP AH,[BX+0009]  ;compare variables 0D94 7502 JNZ 0D98 0D96 FEC0 INC AL  ;return 1 if equal 0D98 C3 RET </syntax>

These two examples show the difference between how numbers and variables are dealt with. In the case of a variable, the variables number is used as an index into the table of variable values to get the value which is being tested. It appears that the variable table is at offset 0x0009 in the data segment.

How the interpreter handles the code

The following 8086 assembly language code is the actual code from the MS-DOS version of Manhunter: San Francisco. There are some calls to routines which aren't displayed. Take my word for it that they do what the comment says. For those of you who can't follow whats going on, I'll explain the interpretation in steps after the code block.

<syntax type="ASM">

Decoding a LOGIC file.

Handler for 'if' statement.
BH determines if its in an OR bracket (BH=1 means OR).
BL determines the nature of the evalutation (BL=1 means NOT)

1E6C:2F22 33DB XOR BX,BX 1E6C:2F24 AC LODSB  ;Get next byte 1E6C:2F25 3CFC CMP AL,FC  ;If less than 0xFC, then 1E6C:2F27 721C JB 2F45  ;jump to normal processing. 1E6C:2F29 7508 JNZ 2F33  ;If greater, jump to 'if' close. 1E6C:2F2B 84FF TEST BH,BH  ;(Could BH be the evaluation reg? 1E6C:2F2D 7551 JNZ 2F80  ;or whether its the second FC? 1E6C:2F2F FEC7 INC BH  ; 1E6C:2F31 EBF1 JMP 2F24  ;Go back to get next byte.

Test is false.
This routine basically skips over the rest of the codes until it finds the
closing 0xFF at which point it will load the following two bytes and add
them to the execution pointer SI.

1E6C:2F80 32FF XOR BH,BH 1E6C:2F82 32E4 XOR AH,AH 1E6C:2F84 AC LODSB  ; 1E6C:2F85 3CFF CMP AL,FF  ;If the closing 0XFF is found, 1E6C:2F87 741D JZ 2FA6  ;jump 2FA6. 1E6C:2F89 3CFC CMP AL,FC  ;If greater than FC, 1E6C:2F8B 73F7 JNB 2F84  ;get next byte. 1E6C:2F8D 3C0E CMP AL,0E  ;If 'said' then goto said handler 1E6C:2F8F 7507 JNZ 2F98  ;else goto normal handler. 1E6C:2F91 AC LODSB  ;Work out number of words in said 1E6C:2F92 D1E0 SHL AX,1  ;and jump over them. 1E6C:2F94 03F0 ADD SI,AX 1E6C:2F96 EBEC JMP 2F84 1E6C:2F98 8AD8 MOV BL,AL  ;Jump over arguments. 1E6C:2F9A D1E3 SHL BX,1 1E6C:2F9C D1E3 SHL BX,1 1E6C:2F9E 8A876407 MOV AL,[BX+0764]  ;Load up the number of arguments. 1E6C:2FA2 03F0 ADD SI,AX  ;Add to the execution pointer. 1E6C:2FA4 EBDE JMP 2F84 1E6C:2FA6 AD LODSW 1E6C:2FA7 03F0 ADD SI,AX  ;Skip over if (includes 3 else byte s) 1E6C:2FA9 E954FF JMP 2F00 </syntax>

Situation 1. Every logic script starts in normal AGI command execution mode. In this routine, if the code is below 0xFC, then it is presumed to be an AGI command. It will then call the main command execution routine which will jump to the relevant routine for the specific command using the jump table stored in agidata.ovl. The command is performed and it returns to the main execution routine where it loops back to the top and deals with the next code in the logic file.

Situation 2. If the code is an 0xFF code, then if jumps to the if statement handler. In this routine is basically assesses whether the whole test condition evaluates to true or to false. It does this by treating each test separately and calling the relevant test command routines using the jump table in the agidata.ovl file. Each test command routine will return a value in AL which says whether it is true or not. Depending on the NOTs and ORs, the whole expression is evaluated. If at any stage during the evaluation the routine decides that the expression will be false, it exits to another routine which skips the rest of the if statement and then adds the two byte word following the closing 0xFF code to the execution pointer. This usually has the affect of jumping over the if block of code. If the if handler gets to the ending 0xFF then it knows the expression is true simply because it hasn't exited out of the routine yet. At this stage it jumps over the two bytes following the closing 0xFF and then goes back to executing straight AGI commands.

/Situation 3. If in the normal execution of AGI commands, the code 0xFE is encountered, a very simple action takes place. The two bytes which follow form a 16-bit twos complement value which is added to execution pointer. This is all it does. Previously we said that the 0xFE code stood for the else statement which is in actual fact correct for over 90% of the time, but the small number of other occurrences are best described as goto statements. If you're confused by this, the following example will probably explain things.

Example:

<syntax type="C++"> if (said( open, door)) {

   // first block of AGI statements


} else {

   // second block of AGI statements


} </syntax>

The above example is how the original coder would have written the AGI code. If we now look at the following example, it is not hard to see that it would achieve the same thing.

<syntax type="C++"> if (!said( open, door)) goto label1;

   // first block of AGI statements
goto label2:


label1:

   // second block of AGI statements


label2: </syntax>

This is exactly how all ifs and elses are implemented in the logic code. The if statement is a conditional branch where the branch is taken if the condition is not met, while the else statement is a nonconditional jump. If a 0xFE code appears in the middle of some AGI code and wasn't actually originally coded as an else, then it was most likely a goto statement.

The said test command

The above assembly language code does raise a very important point. The said command can have a variable number of arguments. Its code is 0x0E, and the byte following this byte gives the number of two byte words that follow as parameters.

Examples:

<syntax type="C++"> if (said(marble)) FF 0E 01 1E 01 FF if (said( open, door)) FF 0E 02 37 02 73 00 FF </syntax>

In the above examples, the values 0x011E, 0x0237, and 0x0073 are just random word numbers that could stand for the words given.

Inner loops

At first I almost totally discarded the existence of loops in the AGI code because it seemed to me that execution of the logic script continually looped. Loop code like "while", "do..while", and "for" statements wouldn't be needed because you could just use a variable to increment with each pass and an if statement to test the value of the variable and take action if it was withing the desired range.

Example:

<syntax type="C++"> if (greatern(30, 45) && lessn(30, 55)) {

   print("You're in the hot zone!");
increment(30);


} </syntax>

I have found evidence of this sort of thing taking place which means that they must loop over continuously. I don't know whether this is something that the interpreter does itself or whether it is part of the AGI code, e.g. at the end of one logic script it calls another which then calls the first one again. With the existence of the conditional branching and unconditional branching nature of the if and else statement, it is easy to see that some of the structures such as "do..while" can infact be coded into logic scripts.

Example:

<syntax type="C++"> FF FD 0D FF 03 00 FE F7 FF

do { } while (!havekey); </syntax>

The above translation is a simple one which is taken from SQ2. The value 0xFFF7 is the twos complement notation for -9 which is the exact branching value to take the execution back to the start of the if statement. If the above example had AGI code between the 0x00 and the 0xFE, then there would be code within the brackets of the "do..while" structure. I don't know whether the original AGI coders used these statements or used goto statements to achieve the same result.