SCI Specifications: Chapter 6 - SCI in action
Event handling in SCI
By Lars Skovlund
Version 1.0, 12. July 1999
This article will deal with the event manager in SCI. Like several other key parts of the interpreter, this one actively communicates with the SCI application. It directly writes to objects of the Event class, but more on that later.
The different input devices are polled differently:
- The keyboard is typically polled at each timer tick (which is 60 hz).
- SCI sets up a callback for the PC mouse driver, meaning that the mouse driver "polls itself" and sends information to the interpreter. On non-MS-DOS platforms, this would probably be done in the timer handler. [Note 1]
- The joystick is only polled when the script wants to.
Some parts of the event mechanism (in particular, keyboard management) are very PC specific, and a conversion will no doubt have to take place on other platforms.
Event types and modifiers
There are three types of events, distinguished by their "type" property. The possible values are listed below; they are laid out as a bitfield to allow for selective event retrieval, see later.
0x00 - Null event
0x01 - Mouse button event
0x02 - Mouse button release event
0x04 - Keyboard event
0x40 - Movement (joystick) event
This type is returned to the SCI event managers by the input device drivers along with a "message" and a set of "modifiers". This is the basic event structure, although some event types contain extra information. The latter field is a direct copy of the BIOS shift flags, laid out as follows:
bit 7 - Insert active
bit 6 - Caps lock active
bit 5 - Num lock active
bit 4 - Scroll lock active
bit 3 - Alt key pressed
bit 2 - Ctrl key pressed
bit 1 - Left shift key pressed
bit 0 - Right shift key pressed
It is obvious, then, that these keys by themselves don't generate any keyboard events. They can, however, be combined with other keys or mouse clicks to produce "shift-click" events, for instance.
The null events
These are generated when a script wants to see an event, but there isn't one to give. The current tick count and mouse position. The tick count, as explained in another document, is the time passed since the interpreter started, measured in 1/60ths of a second. It doesn't seem to be copied into the event object, however.
The mouse events
The mouse position is returned in extra fields in the event record.
If the middle or right button is pressed, this is reflected by the modifiers, in addition to the mouse event. The middle button is translated to the Ctrl key (i.e. set modifiers bit 2), the right button "holds down" both shift keys (setting bits 1 and 0). Every SCI interpreter (at least from 0.000.572 and up) does this, but to my knowledge it is used only in QfG2, where either a shift-click or a right-click is equivalent to typing "look ...".
The keyboard event
The keyboard driver also generates events. When a key is pressed, a keyboard event is generated, with the message field set to the scan code of the pressed key. It should be simple enough, right? Not quite so. The script may want to know if a direction key was pressed, and if so, which. It may call the KMapKeyToDir kernel function for this. KMapKeyToDir takes a keyboard event as input and converts it to a movement event, which is described next.
The movement event
The movement event is only generated by the joystick driver. However, on request, the keyboard driver can convert keyboard events into movement events as described above. The message field is just a direction code, mapped as follows:
8 | 1 | 2 |
7 | Center | 3 |
6 | 5 | 4 |
That is, the direction code starts at straight up (code 1), increasing with clockwise movement.
The Parser
Vocabulary file formats
By Lars Skovlund
Version 1.0, 30. July 1999
The main vocabulary (VOCAB.000)
The file begins with a list of 26 offsets. Each index corresponds to a letter in the (English) alphabet, and points to the first word starting with that letter. The offset is set to 0 if no words start with that letter. If an input word starts with an alphabetical letter, this table is used to speed up the vocabulary searching - though not strictly necessary, this speeds up the lookup process somewhat.
After the offset table are the actual words. A word definition consists of two parts: The actual text of the word, compressed in a special way, and a 24-bit (yes, three bytes) ID. The ID divided in 2 12-bit quantities, a word class (grammatically speaking) mask, and a group number. The class mask is used, among other things, for throwing away unnecessary words. "Take book", for instance, is a valid sentence in parser'ese, while it isn't in English.
The possible values are arranged as a bit field to allow for class masks, see later. Only one bit is actually tested by the interpreter. If a word class equals to 0xff ("anyword"), the word is excluded (allowing for parser'ese). The values go like this:
0x001 - number (not found in the vocabulary, set internally)
0x002 - special
0x004 - special
0x008 - special[Note 2]
0x010 - preposition
0x020 - article
0x040 - qualifying adjective
0x080 - relative pronoun
0x100 - noun
0x200 - indicative verb (such as "is", "went" as opposed to _do_ this or that, which is imperative)
0x400 - adverb
0x800 - imperative verb
The group number is used to implement synonyms (words with the same meaning), as well as by the Said instruction to identify words. There is also a way of using synonyms in code, see the appropriate document.
The compression works in this way: Each string starts with a byte-sized copy count. This many characters are retained from the previous string. The actual text comes after, in normal low-ascii. The last character in the text has its high bit set (no null termination!).
Here is an example of the compression scheme:
apple - 0,appl\0xE5
The byte count is 0 because we assume that "apple" is the first word beginning with an a (not likely, though!). 0xE5 is 0x65 (the ascii value for 'e') | 0x80. Watch now the next word:
athlete - 1,thlet\0xE5
Here, the initial letter is identical to that of its predecessor, so the copy count is 1. Another example:
atrocious - 2,rociou\0xF3
The suffix vocabulary (VOCAB.901)
Note: You may need to consult a grammar book to fully understand the following discussion.
The suffix vocabulary is structurally much simpler. It consists of variably-sized records with this layout:
NULL-TERMINATED | Suffix string |
WORD | The class mask for the suffix |
NULL-TERMINATED | Reduced string |
WORD | The output word class |
The suffix vocabulary is used by the interpreter in order to parse compound words, and other words which consist of more than one part. For instance, a simple plural noun like "enemies" is reduced to its singular form "enemy", "stunning" is converted to "stun" etc. The point is that the interpreter gets a second chance at figuring out the meaning if the word can not be identified as entered. A word which changes its class does might end up as a different word class, the correct class is always retained. Thus, "carefully", an adverb, is reduced to its adjectival form "careful", and found in the vocabulary as such, but it is still marked as an adverb.
The suffix vocabulary consists of variably-sized records with this layout:
NULL-TERMINATED | Suffix string |
WORD | The output word class |
NULL-TERMINATED | Reduced string |
WORD | The allowed class mask for the reduced word |
An asterisk (*) represents the word stem. Taking the above example with "enemies", the interpreter finds this record:
*ies
0x100
*y
0x100
word class 0x100 being a noun.
The interpreter then tries to replace "enemies" with "enemy" and finds that word in the vocabulary. "Enemy" is a noun (class 1), which it is also supposed to be, according to the suffix vocabulary. Since we succeeded, the word class is set to the output value (which is, incidentally, also 1).
Numbers
If the word turns out to be a number (written with numbers, that is), and that number is not listed explicitly in the vocabulary, it gets an ID of 0xFFD, and a word class of 0x100.
The tree vocabulary (VOCAB.900)
This vocabulary is used solely for building parse trees. It consists of a series of word values which end up in the data nodes on the tree. It doesn't make much sense without the original parsing code.
The black box: The magic behind Sierra's text parser
By Lars Skovlund
Version 0.1, 30. July 1999. Incomplete!
This document describes the process of parsing user input and relating it to game actions. This document does not describe the process of the user typing his command; only the "behind-the-scenes" work is described, hence the title.
The process of parsing is two-fold, mainly for speed reasons. The Parse kernel function takes the actual input string and generates a special "said" event (type 0x80) from it. This function is only called once per line. Parse can either accept or reject the input.
Note: A rejection can only occur if Parse fails to identify a word in the sentence.
Even if Parse accepts the sentence, it does not need to make sense. Still, syntax checks are made - see later.
Assuming that the parsing succeeded, the User object (which encapsulates the parser) then goes on to call the relevant event handlers. These event handlers in turn call the Said kernel function. This function is potentially called hundreds or even thousands of times, so it must execute as quickly as possible. Said simply determines from the pre-parsed input line whether or not a specific command is desired.
The Parse function must always work on an internal copy of the actual string, because the user must be able to recall his exact last input using the F3 key. The parser's first step is to convert the input line to pure lower case. This is because the vocabulary words are entered in lower case. The parser then searches the main vocabulary (VOCAB.000), hoping to find the word.
This doesn't necessarily happen yet. Consider, for example, the meaning of the word "carefully", which does not appear in the vocabulary, but is found anyway. This is due to the so-called suffix vocabulary, which is discussed in another document.
If the word still can't be found, the interpreter copies the failing word into a buffer temporarily allocated on the heap (remember, the interpreter operates on its own local buffers). It then calls the Game::wordFail method which prints an appropriate message. The interpreter then deallocates the buffer and exits (it does, however, still return an event. The claimed property of that event is set to TRUE to indicate that the event has already been responded to (error message printed)).
If the interpreter succeeds in identifying all the words, it then goes on to check the syntax of the sentence - it builds a parse tree. See the appropriate document.
If the syntax of the sentence is invalid, the interpreter calls Game::syntaxFail, passing the entire input line. As for the error situation, the event is claimed.
As mentioned in the beginning of this text, this function generates an event. This event, apart from its type id, does not contain any data. Rather, all pertinent data is kept in the interpreter.
The Said kernel function is called for each command which the game might respond to at any given time. Its only parameter is a pointer to a said information block which resides in script space. This block is described below (see the Section called Said specs).
The Said function first does some sanity checking on the event pointer which Parse stored earlier. It must be a said event (type property), and it must not have been handled by an earlier call to Said (claimed property).
It then word-extends the passed said block into a temporary buffer (command codes are byte-sized, remember?). This is supposedly just for convenience/speed, and not really needed.
The Parse tree
Note: This and the two following sections borrow some ideas and structures from abstract language theory. Readers might want to consider related literature.
Most of the information explained here was gathered by Lars Skovlund, and, before that, Dark Minister.
After tokenizing, looking up, and finally aliasing the data found in the parsed input string, the interpreter proceeds to build a parse tree TΠ from the input tokens
- I := w0, w1, w2 ... wn-1
where
- wj ∈ W γj ∈ Γ μj ∈ 2C wj := (γj, μj)
with W being the set of all words, Γ being the set of all word groups, C being the set of all class masks {1, 2, 4, 8, 10, 20, 40, 80, 100}, γj being the word group wj belongs to, and μj being its class mask, as described above.
For the following sections, we will define
- group: W → Γ group((γ, μ)) := γ classes: W → C classes((γ, μ)) := μ Cx := { w | w ∈ W. x ∈ class(w) }
To do that, it uses the class masks M as input for a pushdown automaton (PDA) A built from a parser grammar; if M was accepted by A, the parse tree TΠ will be built from the matching syntax tree to represent the semantics.
The PDA is defined by a grammar G=(V, Σ, P, s), most of which, along with its semantics, is stored in vocab.900. This resource contains a parser rule at every 20 bytes, starting with a non-terminal symbol v (one word) and a null-terminated list of up to five tuples (σi, mi), both of which are words. In these tuples, mi is a terminal or non-terminal symbol (determined by σi), and σi is the meaning of mi:
σi | Type | Meaning |
---|---|---|
0x141 | Non-terminal | Predicate part: This identifies the first part of a sentence |
0x142 | Non-terminal | Subject part: This identifies the second part of a sentence |
0x143 | Non-terminal | Suffix part: This identifies the third and last part of a sentence |
0x144 | Non-terminal | Reference part: This identifies words that reference another word in the same sentence part |
0x146 | Terminal | Match on class mask: Matches if (mi ∈ classes(wj)) |
0x14d | Terminal | Match on word group: Matches if (mi = group(wj)) |
0x154 | Terminal | "Force storage": Apparently, this was only used for debugging. |
With the notable exception of the first rule, these rules constitute P. V := { x | ∃R, R ∈ P. x ∈ R }; typically, V := {0x12f ... 0x13f}. s := m0 of the first rule encountered; in all games observed, it was set to 0x13c. Σ contains all word groups and class masks. For the sake of simplicity, we will consider rules matching composite class masks to be several rules. Here is a simplified example of what such a grammar might look like (the hexadecimal prefix '0x' is omitted for brevity):
Example 6-1. Parse grammar example
- G := ({12f ... 13e}, {C1, C2, C4 ... C100}, P, 13c) P := { 13c → 13b 134 13c → 13b 13d 133 13c → 13b 13d 13c → 13b 13c → 13b 13d 13b 13d 13b → 131 134 13b → 131 13d 13d 13b → 131 13d → 134 131 → C80 133 → C20 134 → C10 }
In addition to this grammar, each right-hand non-terminal mi carries its semantic value ρi, which is not relevant for constructing a syntax tree, but must be considered for the semantic tree TΠ. These values were omitted in the example above. As in the example above, the grammar is a context-free (type 2) grammar, almost in Chomsky Normal Form (CNF) in SCI; constructing a grammar with CNF rules from it would be trivial. [Note 3]
Example 6-2. Parser example
Parse is called with "open door".
- "open" ∈ (842, {C80}) (an imperative word of the word group 0x842) "door" ∈ (917, {C10}) (a substantive of the word group 0x917) I := (842, {C80}), (917, (C10)
I is clearly accepted by automatons based on the grammar described above. Here are two possible derivations:
- D0: 13c (13c → 13b 134) |- 13b 134 (13b → 131) |- 131 134 (131 → C80) |- C80 134 (134 → C10) |- C80 C10 D1: 13c (13c → 13b) |- 13b (13b → 131 134) |- 131 134 (131 → C80) |- C80 134 (134 → C10) |- C80 C10
Obviously, G is an ambiguous grammar. In SCI, rule precedence is implied by rule order, so the resulting left derivation tree is well-defined (in the example, it would be defined by D0)[Note 4].
Semantics
This is important, since the parser does much more than just accept or discard input. Using the semantic tags applied to each non-terminal on the right-hand side of a rule, it constructs what I will call the semantic parse tree TΠ, which attempts to describe what the input means. For each non-terminal rule
r := v0 → v1 v2 ... vn
there are semantic tags σr,1, σr,2 ... σr,n ∈ S, as explained above. TΠ is now constructed from the resulting derivation and the semantic tags associated with each non-terminal of the rule used. The construction algorithm is explained below with TΠ being constructed from nodes, which have the following structure:
- Nodes := {♦} ∪ S × V × (Nodes ∪ Γ)∗
Where S is the set of possible semantic values, and V is the set of non-terminals as defined in the grammar. We will also use the sequence γ0, γ1, γ2 ... γk-1, which will represent the word groups the input tokens belonged to (in the exact order they were accepted), and the sequence r0, r1, r2 ... rl-1, which will be the list of rules used to create the left derivation tree as described in the previous section.
Helper function sci_said_recursive: S × V × (V ∪ Σ)∗ → Node Parameters: s ∈ S, Rule r ∈ V × (V ∪ Σ): v0 → v1 v2 ... vi cnmr = cnr Node n := s, v0 FOR j := 1 TO i IF (vj ∈ Σ) THEN n := n, γcnγ cnγ := cnγ + 1 ELSE cnoldr := cnr cnr := cnr + 1 n := n, sci_said_recursive(σrmr,j, rcnoldr) FI ROF RETURN (n) Helper function get_children: Node → Nodes∗ get_children((s, v, n0, n1 ... nm)) := n0, n1 ... nm Algorithm SCI-SAID-TREE cnγ := 0; cnr := 1; ntemp := ntemp, SCI-SAID-RECURSIVE(0, r0) root(TΠ) := (141, 13f, get_children(ntemp))
Here is an example, based on the previous one:
Example 6-3. Semantic tree example
- k = 2: γ0 = 842, γ1 = 917 l = 4 r0 = 13c → 13b 134 σr0,1 = 141, σr0,2 = 142 r1 = 13b → 131 σr1,1 = 141 r2 = 131 → C80 r3 = 134 → C10 The resulting tree would look like this: (141 13f (141 13b (141 131 842) ) (142 134 917) )
Said specs
To test what the player wanted to say, SCI compares TΠ with a second tree, TΣ, which is built from a so-called Said spec. A Said spec is a variable-sized block in SCI memory which consists of a set of byte-sized operators and special tokens (stored in the range 0xf0 to 0xf9) and word groups (in big-endian notation, so that they don't conflict with the operators); it is terminated by the special token 0xff. The meanings of the operators and special tokens are as follows:
Operator | Byte representation | Meaning |
---|---|---|
, | f0 | "OR". Used to specify alternatives to words, such as "take , get". |
& | f1 | Unknown. Probably used for debugging. |
/ | f2 | Sentence part separator. Only two of these tokens may be used, since sentences are split into a maximum of three parts. |
( | f3 | Used together with ')' for grouping |
) | f4 | See '(' |
[ | f5 | Used together with '[' for optional grouping. "[ a ]" means "either a or nothing" |
] | f6 | See '['. |
# | f7 | Unknown. Assumed to have been used exclusively for debugging, if at all. |
< | f8 | Semantic reference operator (as in "get < up"). |
> | f9 | Instructs Said() not to claim the event passed to the previous Parse() call on a match. Used for successive matching. |
This sequence of operators and word groups is now used to build the Said tree TΣ. I will describe the algorithm used to generate TΣ by providing a grammar GΣ, with L(GΣ) containing all valid Said specs. The semantics will be provided under each rule with a double arrow:
GΣ = ({saidspec, optcont, leftspec, midspec, rightspec, word, cwordset, wordset, expr, cwordrefset, wordrefset, recref}, Γ, P, saidspec) P := { saidspec → leftspec optcont ⇒ (141 13f leftspec optcont) | leftspec midspec optcont ⇒ (141 13f leftspec midspec optcont) | leftspec midspec rightspec optcont ⇒ (141 13f leftspec midspec rightspec optcont) optcont → e ⇒ | > ⇒ (14b f900 f900) leftspec → e ⇒ | expr ⇒ (141 149 expr) midspec → / expr ⇒ (142 14a expr) | [ / expr ] ⇒ (152 142 (142 14a expr)) | / ⇒ rightspec → / expr ⇒ (143 14a expr) | [ / expr ] ⇒ (152 143 (143 14a expr)) | / ⇒ word → γ ∈ Γ ⇒ (141 153 γ) cwordset → wordset ⇒ (141 14f wordset) | [ wordset ] ⇒ (141 14f (152 14c (141 14f wordset))) wordset → word ⇒ word | ( expr ) ⇒ (141 14c expr) | wordset , wordset ⇒ wordset wordset | wordset , [ wordset ] ⇒ wordset wordset expr → cwordset cwordrefset ⇒ cwordset cwordrefset | cwordset ⇒ cwordset | cwordrefset ⇒ cwordrefset cwordrefset → wordrefset ⇒ wordrefset | [ wordrefset ] ⇒ (152 144 wordrefset) wordrefset → < wordset recref ⇒ (144 14f word) recref | < wordset ⇒ (144 14f word) | < [ wordset ] ⇒ (152 144 (144 14f wordset)) recref → < wordset recref ⇒ (141 144 (144 14f wordset) recref) | < wordset ⇒ (141 144 (144 14f wordset)) }
Matching the trees
The exact algorithm used to compare TΠ to TΣ is not known yet. The one described here is based on the approximation used in FreeSCI, which is very similar to the original SCI one.
First, we need to describe a set of functions for traversing the nodes of TΣ and TΠ, and doing some work. They will be operating on the sets N := { 0, i | (i - 1) ∈ N } (all non-negative integral numbers), B := {tt, ff} (the boolean lattice with two elements), and Nodes (which we defined earlier).
- first: Node → S first((s, v, n0, n1 ... ni)) := s
- second : Node → V second((s, v, n0, n1 ... ni)) := v
- word : Node → Γ word((s, v, γ)) := γ
- children : Node → Node∗ children((s, v, n0, n1 ... ni)) := { m | ∀m.m∈{ n0, n1 ... ni } ∧ m∈Node }
- all_children : Node → Node∗ all_children(n) := children(n) ∪ { m | ∃l.l∈all_children(n).m∈l }
- is_word : Node → B is_word((s, v, n0, n1 ... ni) = tt ⇔ (i = 0) ∧ n0 ∈ Γ
- contains_word : Node × S × Γ → B contains_word(n, s, γ) = tt γ = 0xfff[Note 5] ∨ (⇔ ∃m.m∈all_children(n).(s = second(m)) ∧ (is_word(m) ∧ (word(m) = γ)))
- verify_sentence_part_elements : Node × Node → B verify_sentence_part_elements(np, ns) = tt ⇔ (first(ns = 152) ∧ ((∀m.m ∈ Node.verify_sentence_part_elements(m, ns) ⇔ { w | ∃t.t ∈ all_children(m).w = word(t)} = ∅) ∨ ∃m ∈ children(ns).verify_sentence_part_elements(m, ns)) ) ∨ ((second(ns) = 153) ∧ (∃m.m ∈ children(ns).(∃o ∈ all_children(ns).(first(o) = first(np)) ∧ word(o) = word(m))) ) ∨ ((second(ns) ∈ {144, 14c}) ∧ (∃m.m ∈ children(ns).verify_sentence_part(m, ns)))
- verify_sentence_part : Node × Node → B verify_sentence_part(np, ns) = tt ⇔ ∀n.n ∈ children(ns):∃m.m∈children(np).(first(m) = first(n)) ∧ verify_sentence_part_elements(n, m)
- verify_sentence_part_brackets : Node × Node → B verify_sentence_part_brackets(np, ns) = tt ⇔ (first(np) = 152 ∧ (∀m.m∈Node.(first(m) = first(ns)) ∧ (second(m) = second(ns)). verify_sentence_part(np, m) ⇔ { w | ∃t.t ∈ all_children(m).w = word(t)} = ∅)) ∨ ((first(np) ∈ {141, 142, 143}) ∧ verify_sentence_part(np, ns))
With these functions, we can now define an algorithm for augmenting TΠ and TΣ:
- Algorithm SCI-AUGMENT matched := tt claim_on_match := tt FOREACH n ∈ root(TΣ) IF ((first(n) = 14b) ∧ (second(n) = f900)) THEN claim_on_match := ff ELSE IF ¬verify_sentence_part_brackets(n, root(TΠ)) THEN matched := ff HCAEROF
Augmenting succeeded if matched = tt; in this case, TΠ is one of the trees accepted by the description provided by TΣ. In this case, Said() will return 1. It will also claim the event previously provided to Parse(), unless claim_on_match = ff.
Views and animation in SCI
by Lars Skovlund
Version 0.2, 4. January 2002, with notes by Christoph Reichenbach
This chapter deals with a rather complex subject within SCI. The subsystem described here is one of the "bad boys" in SCI, since it calls functions in user space, as well as changing the value of various selectors. This document is not necessarily complete. There are several things I have not covered - because they are better off in a separate document, or simply because I haven't yet figured that part out. IOW, this stuff is incomplete. Things may change.
After drawing a pic on the screen (which is DrawPic's job, that doesn't surprise now, does it?), some views have to be added to it. There are two ways of doing this; the AddToPic and the Animate call. While AddToPic is used for static views, Animate lets each animated view in the cast list perform an "animation cycle".
An animation cycle is done entirely in SCI code (with the aid of some kernel calls). It involves two other objects; the mover and the cycler. The mover is responsible for controlling the motion of an actor towards a specific point, while the cycler changes the image of the actor, making him appear to walk, for instance.
The behavior of a view is controlled by its signal property. This property contains a bitfield which describes a lot of animation-related stuff. The bits can be roughly divided into two groups; the script and interpreter bits (I had called them Option and State bits at first, but that is not entirely accurate). The first group allows the script to influence the drawing pro- cess somewhat, the other are used internally by the interpreter. The two groups overlap a bit, though.
Table 6-1. SCI and FreeSCI signal bits
Bit # | Name | FreeSCI constant | Meaning |
---|---|---|---|
0 | _K_VIEW_SIG_FLAG_STOP_UPDATE | A view updating process has ended | |
1 | _K_VIEW_SIG_FLAG_UPDATED | The view object is being updated | |
2 | noUpd | _K_VIEW_SIG_FLAG_NO_UPDATE | Don't actually draw the view |
3 | _K_VIEW_SIG_FLAG_HIDDEN | The view is hidden from sight. Often, if an actor is supposed to enter and exit a room (such as the guards in the plazas in QfG2), this bit is used. When he's supposed to enter the room, bit 3 in his signal is cleared. When he leaves, bit 3 is set, but his SCI object is not deleted. | |
4 | fixPriOn | _K_VIEW_SIG_FLAG_FIX_PRI_ON | if this bit is set, the priority of the view never changes (if it isn't, the interpreter recalculates the priority automagically). |
5 | _K_VIEW_SIG_FLAG_ALWAYS_UPDATE | ||
6 | _K_VIEW_SIG_FLAG_FORCE_UPDATE | ||
7 | _K_VIEW_SIG_FLAG_REMOVE | The view should be removed from the screen (an interpreter bit - its corresponding script bit is bit 3). If bit 3 isn't set as well, the view reappears on the next frame. | |
8 | _K_VIEW_SIG_FLAG_FROZEN | Deactivates the mover object of the view (it is "frozen" - the view can still turn, however). | |
9 | isExtra | _K_VIEW_SIG_FLAG_IS_EXTRA | |
10 | _K_VIEW_SIG_FLAG_HIT_OBSTACLE | The view hit an obstacle on the last animation cycle | |
11 | _K_VIEW_SIG_FLAG_DOESNT_TURN | Meaningful for actors only. Means that the actor does not turn, even though he is walking the "wrong way". | |
12 | _K_VIEW_SIG_FLAG_NO_CYCLER | The view cycler is disabled. This makes the view float instead of walk. | |
13 | ignoreHorizon | _K_VIEW_SIG_FLAG_IGNORE_HORIZON | |
14 | ignrAct | _K_VIEW_SIG_FLAG_IGNORE_ACTOR | Actors can walk in the rectangle occupied by the view. The behavior of this bit is odd, and best expressed by example. The Guild Master in QfG1 has his bit 14 set. This means that ego (or someone else) can walk all the way to his chair (try sneaking in on him from behind). If we clear this bit, we can't sneak in on him. |
15 | _K_VIEW_SIG_FLAG_DISPOSE_ME | The view should be disposed | |
[a] | _K_VIEW_SIG_FLAG_FREESCI_PRIVATE | Used as an intermediate result by the interpreter; marks views that are going to have their nsRect/lsRect regions redrawn (for the test in the main draw algorithm's step 17.1., below) | |
[a] | _K_VIEW_SIG_FLAG_FRESCI_STOPUPD | View has been 'stopupdated'. This flag is set whenever the view has the STOP_UPDATE bit set, and cleared as soon as it moves again. Stopupdated views are collided against differently than normal views. |
Notes: a. This flag is used internally in FreeSCI; it can't be found in the view objects, only in their copies in the dynview widget list.
The unlisted bits are probably all interpreter bits. They don't seem to have an effect when set. Many bits seem to be involved in the decision whether to display a view or not. I have not completely figured this out.[Note 6]
Animate (see the Section called Kernel function 0x0b: Animate([DblList], [word]) in Chapter 5) can be called in two ways:
Animate(DblList cast, bool cycle)
Animate()
If the second syntax is used, the two parameters are assumed to be zero.
The cast list is just a list of the views to draw. Animate creates a backup of this list for updating purposes. However, this backup cast list isn't just a normal copy. The interpreter copies some selectors from the view (view, loop, cel, nsRect) and places them in a special data structure. This indicates to me that there is a possibility that the view objects may be deleted even though an update is anticipated.
The general pseudocode for Animate goes as follows:
0. Backup PicNotValid: PicNotValid' := PicNotValid 1. If we don't have a new cast: 1.1. if PicNotValid is set: 1.1.1. Redraw picture with opening animation 1.2. exit 2. For each view in the cast list: 2.1. If view is not frozen: 2.1.1. call view::doit(), performing an animation cycle 3. Prepare a list of y coordinates by traversing the cast list 4. For each view in the cast list: 4.1. If the view resource view::view has not been loaded yet: 4.1.1. Load view.nr, where nr=view::view 5. For each view in the cast list: 5.1. If view::loop is invalid, set view::loop := 0 5.2. If view::cel is invalid, set view::cel := 0 6. Sort the cast list, first by y, then by z 7. For each view in the cast list: Update view::nsRect (SetNowSeen()) 8. For each view in the cast list: Unless the views' priority is fixed, recalculate it 9. For each view in the cast list: 9.1. If NO_UPDATE is set for the view: 9.1.1. If the following holds: 9.1.1.1. (VIEW_UPDATED || FORCE_UPDATE) 9.1.1.2. || (!(VIEW_UPDATED || FORCE_UPDATE) && !IS_HIDDEN && REMOVE_VIEW) 9.1.1.3. || (!(VIEW_UPDATED || FORCE_UPDATE) && !IS_HIDDEN && !REMOVE_VIEW && ALWAYS_UPDATE) 9.1.1.4. || (!(VIEW_UPDATED || FORCE_UPDATE) && IS_HIDDEN && ALWAYS_UPDATE) 9.1.1.5. then increase PicNotValid by one. 9.1.2. Clear the STOP_UPDATE flag 9.2. otherwise: 9.2.1. If (STOP_UPDATE and !ALWAYS_UPDATE) or (!STOP_UPDATE and ALWAYS_UPDATE) 9.2.1.1. Increase PicNotValid by one 9.2.2. Clear the FORCE_UPDATE flag 10. If PicNotValid is now greater than zero, call the sub-algorithm described separately 11. For each view: If NO_UPDATE, IS_HIDDEN and ALWAYS_UPDATE are not set: 11.1. [12] Save the area covered by the view's nsRect, store the handle in view::underBits 11.2. [13] Draw the view object 11.3. [14] If the view IS_HIDDEN, clear the REMOVE_VIEW bit (don't need to hide it twice) 11.4. [15] Insert the view into the backup cast list 16. If PicNotValid', our copy of the initial value of PicNotValid, is non-zero: 16.1. Refresh entire screen with opening animation 16.2. PicNotValid := 0 17. For each view in the cast list: 17.1. [18] If the view was changed in step 10 and neither REMOVE_VIEW nor NO_UPDATE is set: 17.1.1. [19] Redraw the nsRect and lsRect areas 17.1.2. [20] Copy the nsRect to the lsRect 17.1.3. [21] If IS_HIDDEN, set REMOVE_VIEW as well 22. For each view in the reverse cast list: 22.1. [23] If neither NO_UPDATE nor REMOVE_VIEW is set: 22.1.1. Restore the underbits 22.1.2. Clear the underbits 22.2. [24] if DISPOSE_ME is set, call view::dispose to dispose it
With the sub-algorithm being:
1. For each view from the cast list: 1.1. [2] If NO_UPDATE is set: 1.1.1. [3] If REMOVE_VIEW is set: 1.1.1.1. If PicNotValid is 0, restore the area covered by view::underBits 1.1.1.2. Free view::underBits 1.1.2. [4] Clear FORCE_UPDATE 1.1.3. [5] If VIEW_UPDATED is set: Clear VIEW_UPDATED and NO_UPDATE 1.2. otherwise (if NO_UPDATE is not set): 1.2.1. Clear STOP_UPDATE 1.2.2. Set NO_UPDATE 6. For each view from the cast list: 6.1. [7] Draw the view 6.2. [8] If ALWAYS_UPDATE, clear STOP_UPDATE, VIEW_UPDATED, NO_UPDATE, FORCE_UPDATE 6.3. [9] Clip the nsRect against the boundaries of the "natural" priority band of the view 6.4. [10] If IGNORE_ACTOR is clear, fill the area found in 6.3. with 0xf on the control map 11. For each view from the view list: 11.1. if NO_UPDATE is set: 11.1.1. [12] If IS_HIDDEN, then set REMOVE_VIEW, otherwise: 11.1.1.1. clear REMOVE_VIEW 11.1.1.2. [13] Save the area covered by the nsRect in the underBits 14. For each view from the cast list: 14.1. If NO_UPDATE is set and IS_HIDDEN is clear: 14.1.1. [15] Draw the view
- Note, the ReAnimate subfunction (0x0D) of the Graph kernel call redraws parts of the maps using the cast list created by Animate, whereas the ShowBits call (0x0C) copies parts of the active map to the physical screen.
- Notes
- ↑ The default FreeSCI event mechanism uses libgii, which is completely event-based.
- ↑ The three special classes are apparently used for words with very specific semantics, such as "if", "not", "and" etc. It is unknown as of yet whether they receive special treatment by the parser.
- ↑ FreeSCI constructs a GNF (Greibach Normal Form) representation from these rules for parsing.
- ↑ In FreeSCI, you can use the "parse" console command to retreive all possible left derivation trees
- ↑ This is the so-called "anyword". Words with a word group of 0xfff match any other word.
- ↑ The bit names I have written come from some debug information I got from QfG2 - type "suck blue frog" then Ctrl-W to save the cast list!