Logo Pending


SCI decompilation and Weird Loops

They’re not really that weird on the face of it, but that depends on who’s looking.

The decompiler in SCI Companion is a work of art. You can tell because I didn’t write it. (I only fixed a thing or two.) But there are some things that it can’t figure out, and when that happens the function or method body is replaced with a raw asm block. For example, the copy protection in Laura Bow 2 – The Dagger of Amon Ra has loop in it that SCI Companion can’t hack.

It’s a bit much to take in but the important bits are as follows: this code block (rm18::init) has four discrete segments. The first isn’t shown here and sets up a few simple things. The second (code_0087) is a regular loop, where temp0 counts up from zero to eleven. When it hits twelve, the loop is broken and we go to section three, code_00ad. Section three is a weird loop. If you look at the check at the top we see this:

pushi #size
pushi 0
lofsa tempList
send 4
bnt code_0116

Which basically means that when (tempList size?) returns zero/is false, we skip to section four. At the bottom of the section, right before the label for code_0116, there’s the command that makes section three a loop; jmp code_00ad.

So that means that section three keeps repeating until tempList is out of items. Section two put a bunch of values in it, and section three then takes items out at random and puts them into goodList, effectively randomizing the order. The items, incidentally, are the tiles depicting the various Egyptian gods that the copy protection is all about, clones of egyptProp given increasing cel values. Section three positions them as they’re added to goodList. It’s a good routine, Brent.

The problem that trips up SCI Companion and makes it spit out the stuff in those two pictures is that it doesn’t recognize the second loop for what it is. Counting from one value to another by a given increment? Easy. Iterating over a collection? It can figure those out. But picking items from a bag until it’s empty? That’s not on the menu.

To make this decompile, then, we first need to break the loop by commenting out that last jmp command. A single ; suffices. Compile the script resource, then go back and re-decompile it. A conditional loop, of course, consists of a check and a jump. We removed the jump so now it’s just the check:

(method (init &tmp i theTile theX theY)
  (LoadMany rsVIEW 18) ; load the tiles
  (super init:)
  (gGame handsOn:)
  (gIconBar disable: 0 1 3 4 5 6 7)
  (goodList add:)
  (tempList add:)
  (= theX -32)
  (= theY 46)
  (= i 0)
 
  ; Instantiate twelve tiles, with increasing cel numbers.
  (while (< i 12)
    (tempList add: ((egyptProp new:) cel: i yourself:))
    (++ i)
  )
 
  ; This should be "(while (tempList size?)" but we removed the jump, remember?
  (if (tempList size?)
    ; Pick a tile number.
    (= i (Random 0 (- (tempList size?) 1)))
    ; Get the i-th tile.
    (= theTile (tempList at: i))
 
    ; Set up the tile's position on the grid and add it to goodList.
    (goodList add:
      (theTile
         x: (= theX (+ theX 48))
         y: theY
         yourself:
      )
    )
    ; Once we're halfway through, CRLF to the next row.
    (if (== (goodList size?) 6)
      (= theX -32)
      (= theY 111)
    )
 
    ; Actually remove the tile from tempList so we won't pick it again.
    (tempList delete: theTile)
  )
 
  ; Section four
  (gGame handsOff:)
  (self setScript: sInitEm)
)

The cool part is that once we replace that if with a while and compile the script, the result is effectively the same as the original. Only some of the specific opcode choices are different. For example, the original uses the two-byte pushi 1 throughout (also 0 and 2), but SCI Companion’s script compiler prefers to use the one-byte push1 there. The same values are pushed regardless.

[ , , , ] Leave a Comment

Print, PrintD, and the other Print

So as established, there are three different Prints in SCI.

  • Print the function, in SCI0
  • PrintD the function, supplementing Print, in SCI1
  • Print the class, in SCI11.

They each have their own strengths and weaknesses, of course.

SCI0 SCI1 SCI11
Max text items 1 infinite
Max buttons 6 infinite
Max icons 1 infinite
Max input fields 1 infinite
Animated icons¹ yes no yes
Size to fit yes
Size to max width yes no yes
Auto-dismiss yes no yes
Auto-layout² yes no
Position yes
Font yes
Text tuples³ yes no yes

¹: Animated icons require the ability to pass a reference to a DCIcon object instead of a view/loop/cel tuples.

²: Items added by PrintD flow to the right with a four pixel margin. Pass the #new command argument to reset the flow to the left edge and below the last item, or the #x/#y modifiers to shift the last item’s position. In the Print class, every item added must be manually positioned as everything defaults to the top-left. The Print function has its limits specifically because it automatically lays out the controls.

³: The Print class being from SCI11, it takes noun/verb/case/seq tuples.

That comes down to the following actions:

SCI0 Print: required string or tuple for text (may be empty), mode, font, width, time, title, at, draw, edit, button (up to six times), icon, dispose, window, first

SCI1 PrintD: new, at, title, first, text, button, icon, edit, x, y

SCI11 Print: addButton, addEdit, addIcon, addText, addTextF, addTitle, posn methods, plus mode, font, width, ticks, modeless, and saveCursor properties

…And then I messed everything up by rewriting PrintD as a wrapper around the Print class so it runs on SCI11, adding everything but auto-dismiss and animated icon support. It’s available from my SCI stash, of course. Hell, by this time tomorrow those last two things may well be included.

SCI11 PrintD: all of SCI1’s, plus modNum, cue, font, and width

[ ] Leave a Comment

A key difference between SCI0/1 and SCI11

Oddly, this one’s entirely script based.

I was reminded of this when I watched one of Space Quest Historian’s videos, where he played the Space Quest 2 remake by Infamous Quests. Now, one thing he mentions about the version he plays in that video is that besides the narrator being muted so he can narrate it himself, well…

but recently Steve Alexander, who was one of the co-CEOs of Infamous Quests had a little peek around the old game files and decided to spruce it up a bit, fix some old bugs, add some you know, touch-ups here and there. Most importantly, at least according to him, replace the standard AGS font with something that looks a little more Sierra-ish. And that’s the version I’m going to play.

All well and good but then the main menu appeared and I noticed just how important this font thing seemed to be.

One thing immediately came to mind when I saw this. Notice how the button cuts into the caption? If I was a betting cat, I’d say the original version’s main font was just slightly shorter than this replacement. Also when I prepared the image I noticed each of these buttons has a different height, but that’s not the issue here — that’s just sloppy work.

This thing with the buttons cutting into the message text? It can’t happen accidentally in SCI0/1, but it can easily happen in SCI11. How is that?

In SCI1, the system scripts include a PrintD function. It’s pretty powerful on the face of it:

(PrintD
	"Would you like to skip\nthe introduction or\nwatch the whole thing?"
	#at 100 60
	#new
	#button "Skip it" 0
	#new
	#button "Watch it" 1
	#new
	#button "Restore a Game" 2
)

And that gives you this:

The function itself will track how large every item should be as they are defined, and adjust the final size of the window accordingly. Just to simplify the explanation a bit. And then it’ll return the value of a given button so the game knows how to react.

(Update: SCI0 had Print which worked not entirely unlike this, and SCI1 added PrintD as a more streamlined variant with #new support, because…)

In SCI11, Print is now an object. You have to call a bunch of methods on it, in sequence, then finish with an init: call, and that will trigger its display and return the value chosen. But one important distinction is that addText:addButton:, and its ilk… don’t automatically position themselves. You have to do that yourself.

That’s why these games came with a built-in dialog creation tool, among others. It’d create code like this:

; DialogEditor v1.0
; by Brian K. Hughes
(Print
	posn:		0 28,
	font:		0,
	addText:	{bluh bluh} 71 18,
	addButton:	0 {button} 71 30,
	addButton:	1 {button} 0 0,
	init:
)

The Sierra programmers could then integrate that into their game scripts. But importantly, those manually-positioned buttons could overlap other controls.

(Update: … PrintD also had a dialog editor made for it. This is pretty explicitly stated in the changelogs.)

AGS dialog boxes also use manual positioning, just with a nice WYSIWYG form editor. So if you take a perfectly good (yeah right) introduction menu and change the font for one with a higher X-height and don’t adjust things… you get that SQ2 window.

Update just because: this is what the SQ4 intro menu would look like with font.001 instead. That is, with SCI1’s PrintD and its automatic layout.

[ , , , ] Leave a Comment

Space Quest 4 – Roger Wilco and the Failed Attempt at Being Cute

While playing Space Quest 4 – Roger Wilco and The Time Rippers, you travel back and forth between several different time periods, each designated by a sequel number. The introduction and ending are set in SQ4, the main plot in SQ12 – Vohaul’s Revenge II, and from there you travel to SQ1 – The Sarien Encounter, SQ10 – Latex Babes of Estros, and in an easter egg, SQ3 – The Pirates of Pestulon.

While in the SQ1 era, you revisit Ulence Flats. Literally, you arrive just after the original Roger is done there and leaves. In the SCI remake of SQ1, you actually see the time pod from SQ4 arrive right behind you when you leave the area, but SQ4 came out before the SQ1 remake so you revisit the AGI version instead. Sort of. The resolution’s all wrong, but who cares?

One thing you’ll quickly notice is that instead of the usual BorderWindow frames, this part of the game uses a custom frame meant to evoke AGI’s. It is, however, immediately clear (at least to me) that they fucked it up.

That’s one unsightly black border! Also, the window is light gray instead of white but that’s not the issue here.

If you look closely and remember what I wrote about window style bits before, you might recognize that it’s drawing a white light gray window with a red border, and then a nwTRANSPARENT window on top. What does the actual code say? Here’s Sq1Window, which is used in lieu of BorderWindow in all Ulence Flats rooms:

(class Sq1Window of SysWindow
  (properties
    underBits 0
    pUnderBits 0
    bordWid 3
  )
 
  (method (dispose)
    (SetPort 0)
    (Graph grRESTORE_BOX underBits)
    (Graph grRESTORE_BOX pUnderBits)
    (Graph grREDRAW_BOX lsTop lsLeft lsBottom lsRight)
    (super dispose:)
  )
 
  (method (open &tmp temp0 temp1)
    (SetPort 0)
    (= color gColor)
    (= back gBack)
    (= temp1 1)
    (if (!= priority -1) (= temp1 (| temp1 $0002)))
    (= lsTop (- top bordWid))
    (= lsLeft (- left bordWid))
    (= lsRight (+ right bordWid))
    (= lsBottom (+ bottom bordWid))
    (= underBits (Graph grSAVE_BOX lsTop lsLeft lsBottom lsRight 1))
    (if (!= priority -1)
      (= pUnderBits (Graph grSAVE_BOX lsTop lsLeft lsBottom lsRight 2))
    )
    ; Draw the background
    (Graph grFILL_BOX lsTop lsLeft lsBottom lsRight temp1 back priority)
    ; Draw the border
    (Graph grDRAW_LINE (+ lsTop 1) (+ lsLeft 1) (+ lsTop 1) (- lsRight 2) global131 priority)
    (Graph grDRAW_LINE (- lsBottom 2) (+ lsLeft 1) (- lsBottom 2) (- lsRight 2) global131 priority)
    (Graph grDRAW_LINE (+ lsTop 1) (+ lsLeft 1) (- lsBottom 2) (+ lsLeft 1) global131 priority)
    (Graph grDRAW_LINE (+ lsTop 1) (- lsRight 2) (- lsBottom 2) (- lsRight 2) global131 priority)
    (Graph grUPDATE_BOX lsTop lsLeft lsBottom lsRight 1)
    ; Open a logical window for the contents to be drawn into
    (= type 129)
    (super open:)
  )
)

And here’s the trick: not only does it open a logical window after drawing it, but it opens one with the wrong style.

Fix idea #1. Open the logical window before drawing it.

(method (open &tmp temp0 temp1)
  (= type 129)
  (super open:)
  (SetPort 0)
  (= color gColor)
  ;...
)

That ain’t it. Not only does it put the contents of the window in the corner of the screen (because we’re using SetPort wrong) but when the window closes, the frame appears behind it:

No, no. The way I solved it was like this:

(method (open &tmp port temp1)
  ; temp0 was unused so we're taking it for proper SetPorting.
  (= color gColor)
  (= back gBack)
  ; Set our type to ONLY wCustom, not wCustom|wNoSave, and open.
  (= type 128)
  (super open:)
  ; Nothing will have appeared because wCustom don't draw anything, but a port has been set up!
  ; Switch to drawing on the whole screen but also *save the window's port*.
  (= port (GetPort)) ; 2022-07-29 update, hi sluicebox
  (SetPort 0)
 
  (= temp1 1)
  ; ...
  (Graph grUPDATE_BOX lsTop lsLeft lsBottom lsRight 1)
 
  ; Reset to the window's port.
  (SetPort port)
)

And that fixes everything!

No extra black border, no misplaced contents, and no leftovers! If you have the CD-ROM edition, you may be able to drop this file in the game’s directory and rename it to 706.scr.

Now, eagle-eyed viewers might notice that in the very broken screenshot, the text was drawn on a white background, while the window itself is light gray. This is because each graphical port in the system tracks its own color settings, among other things. A logical window is a kind of port, as is the screen as a whole. Since the broken version called (SetPort 0) after (super open:), its contents were drawn on the screen port, not the window’s. And the screen port, by default, has a white background color.

Shoutouts to the Space Quest Historian for putting me on this track with his playthrough video. And as such, see ya on the Chronostream, time jockeh!

[ , , ] 2 Comments on Space Quest 4 – Roger Wilco and the Failed Attempt at Being Cute

Frame sizes

And I don’t mean graphical windows.

I’ll admit it, I’m only passingly familiar with the Sierra PMachine’s internal workings. I know much more about the SC script code than the PMachine bytecode it compiles to. Specifically, I know it’s a stack machine with a single accumulator and that literally everything is a 16-bit value but while researching that thing from last time I finally figured out how sends work.

Let’s go through some examples of various things you might do in SCI code.

Let’s say we have a local variable that we want to set to a particular value: (= aLocal 42). Simple enough, right? That translates to ldi 42, sal aLocal. Well, technically the disassembler has no notion of what the local variables are named so that’d actually come out as sal local0 or whatever our local’s place in the list is but the instructions are clear: set the accumulator to a value, then store the accumulator in a local variable. A global variable is much the same but would use sag instead.

Here’s a slightly more complicated one:

(if aLocal
	(Printf "lol")
)
_:
	lal aLocal
	bnt notTrue
	push1 
	lofss strLol
	calle 921 1 2 //Printf, script 921 export 1.
notTrue:
	//rest of the script

See what’s going on there? First we use lal to load a local var’s value to the accumulator. Branch to another spot if that value is not truthy (non-zero), skipping over the Printf. Then we first push the amount of arguments given (just the one), and load the offset of our string to the stack. Then we call an external function by number. With one 16-bit argument on the stack taking two bytes, we look back that far plus another two to reach the 1, which is our argument count, so that Printf can later tell how many arguments it was given. Likewise, (Printf "lol" 42) would become

_:
	push2 
	lofss strLol
	pushi 42
	calle 921 1 4

We pass a frame size, as they’re called, of four, so we look back that many bytes plus two in the stack, passing an argc of two, a pointer to our string, and the number 42 to Printf.

How about a little trickery? (Printf "lol" (+ aLocal 1)) perhaps?

_:
	push2 
	lofss strLol
	lsl aLocal
	ldi 1 
	add 
	push 
	calle 921 1 4

So first we load a local to the stack, not the accumulator, then load the immediate value 1 to the accumulator (there is no accumulator version of push1 after all). The add command takes the value on top of the stack and adds it to the accumulator, leaving the result in the accumulator. Then push takes the accumulator and puts it on the stack, thus giving a stack of two arguments, the offset to “lol”, and the value of aLocal plus one. We look back four bytes, plus another two, and we know what to tell Printf.

And that’s how Kawa learned what a frame size actually is.

[ ] Leave a Comment

Selectors and different ways to push them

Earlier today, some 19 hours ago at the time of writing, Eric Oakford opened an issue on the SCI Companion GitHub repo. Eric is working on a big decompilation project, taking mostly demo versions of SCI games and trying to wrangle them into a recompilable state.

In the issue he’d opened, Eric described how the demo version of Leisure Suit Larry 3 didn’t decompile right, while the actual LSL3 worked fine. Here’s an example of the problem:

One of the first things a Room instance would normally do in its init method is to call (super init:), letting the Room class itself do its setup before anything specific to that room is done, like setting up actors, scripts, features, and walk polygons. In PMachine byte code that statement looks like this:

39 57       pushi 87    // the "init" selector
76          push0       // init takes no parameters
57 36 04    super Rm 4  // four bytes (two words) worth of stack to send

Now, the SCI PMachine is a stack machine, with two parts that are important to know about: the stack (because duh) and the accumulator. You can load numbers onto the accumulator, push them directly onto the stack, push the accumulator onto the stack, duplicate the top item, etcetera. Every value on that stack is a 16-bit number, as is the accumulator. Pointers? Just 16-bit numbers. Characters returned by StrAt? 16-bit numbers, even if it’s just ASCII codes. Properties and methods to invoke in a send? Yup.

There’s a separate table, vocabulary 997, that lists the name of every selector — every method and property of a class or instance. And that’s where it says that selector #87 is init.

Noting that superself, and send are all three sides of the same weirdly-shaped coin, the decompiler can tell that there should be a (super ...) command in the output, four bytes of stack space back. Since it’s not actually running anything it has no actual stack, but it can look back to find two push operations that’ll fit the bill just as well. It can tell that the first value should be a selector, so whatever is being pushed is taken to be one, which is correct — 87 is the init selector. Then the next value is the amount of arguments given to init, which is zero.

But everything went wrong in the demo. This is how rm200, the overlook with the binoculars and memorial plaque, starts in the demo:

35 57       ldi 87
36          push 
39 00       pushi 0
57 36 04    super Rm 4

The actual values on the stack stay the same — 87 0 — but the way they’re put on there is subtly different, and that tripped SCI Companion up.

Instead of (super init:), it decompiled the above as (super species?).

In fact, all selectors in code blocks were species. Six hours ago at the time of writing, I figured out the problem.

Now, the SCI Companion code is super hairy and I really couldn’t do it justice by just including snippets here but the gist of it?

Every operation may have a couple operands. One method in the decompiler returns what the first operand for a given operation in the byte code may be. For pushi, that’d be the immediate value to push. For push0push1, and push2, that’d be zero, one, and two. For ldi it’s exactly the same as pushi, just that the operand is to be put in the accumulator, not the stack. By default, this method just assumes zero.

Notice how push has no operands at all? Why would it? It pushes the accumulator’s value, after all. The only difference between pushi 87 and ldi 87, push is that in the latter case, the accumulator is also 87. The accumulator doesn’t matter to a send, only the contents of the stack. And pushi 0 is just push0 with one extra byte. And that makes these two snippets effectively the same with regards to actual execution in an SCI interpreter.

So what happens when the decompiler sees the LSL3 demo’s scripts, is that it looks back for two pushes, as it should. It finds the first, push, which should be a selector. But the helper method that returns the value being pushed can’t return any operand — there are no operands here! So it returns the default, zero. Some confusion about it possibly being a variable later, it decides that it must be species. And then it does this for all the sends in the demo, because they all push their values the same way.

The fix came to me when I saw the case for the dup operation in that very same method that’s supposed to return the value that’d be pushed. It too takes no operands, yet does return a value that’s only zero if it should be. Turns out it scans back a bit, looking at the previous operation in the bytecode stream, and steals its value by calling the same helper method again, but aimed at the previous operation. The fix then is to make push also steal its predecessor’s value. I did decide to special-case things for now, though. It’ll only do the stealy thing if it’s an ldipush pair, like in the LSL3 demo.

But it does work.

 

Addendum: You might wonder why the incorrect decompilation was (super species?) with a question mark instead of a colon. The decompiler and interpreter alike can tell which selectors on a given class are supposed to be properties, and which are methods. When invoking a method or setting a property, the standard is to use a colon, like in (theSong number: 4 play:), which is a property set followed by an arg-less method, and to use a question mark for property gets, like in (= theX (gEgo x?)). And since species is a property and there was no argument, it was taken to be a property get.

[ , ] 1 Comment on Selectors and different ways to push them

How to SCI – Old vs New

To make an SCI game today, you can just grab a copy of SCI Companion and use that to import graphics and music, do the scripting, text… basically everything but drawing bitmap-based backgrounds. You get a copy of the interpreter matching the template you chose, the system scripts are all set up for you, and you can just hit Compile, watch it work, and it’ll even automatically run the game for you in DOSBox, perhaps even starting you off in a specific room.

But dear lord do we have it easy nowadays.

In the old days, making an SCI game involved several separate utilities, many of them interface-less command line tools, and a particular network setup. That is, the tools expect to be invoked from a specific hard drive letter, as they are provided from one point of the network. There’s another where the system programmers keep the latest builds of the interpreter and system scripts, and the team for a given game has a batch script file to pull the latest into that game’s working directory. Writing the actual script code is roughly the same as it is now, but instead of a dedicated script editor they mostly used Brief. To test their changes, the programmers had to invoke SC, the Script Compiler. Given that Brief was apparently pretty extensible, this could probably be done from there.

While we mostly work directly on RESOURCE.### files, Sierra’s games were developed what I call loose-leaf style. Each type of resource was stored in its own folder, and a “wherefile” specified where each of them could be found — they were basically just RESOURCE.CFG by another name, really. And that name was literally WHERE. Turns out you can specify which configuration file you want the interpreter to use.

view=../view
sound=../sound
pic=../pic
font=../system
cursor=../system
videoDrv=/system/ega320.drv
soundDrv=/system/std.drv
kbdDrv=/system/ibmkbd.drv
joydrv=/system/joystick.drv

To make the game, they didn’t use makefiles. They used batch scripts that invoked SC and compiled the .sc source files to .scr files in the SCRIPT directory, and copied over the script resources from SYSTEM.

Given how relative paths work, running another particular batch file would run the interpreter from one directory while in another, from which point the paths given above can be considered valid. Since they didn’t have the “start at the room specified in this file” feature that SCI Companion’s template game adds, we get the game-specific debug modes that ask for a room number on startup, as extensively documented elsewhere.

And then, when the game is considered fit to ship, they build a list of which resources go on which disk, pass that to yet another command-line tool, which goes through all that and produces the RESOURCE.### files. Copy the result and there you go.

That list does not need to include all resources though. Indeed, as there are things that are included in the game data but left unused, there are some things that never got on a release disk in the first place. Let’s just say some things in the Larry source assets are even raunchier than you’d expect.

[ , ] Leave a Comment

Ball Road

The dictionary for AGI and SCI games’ text parser input is stored in alphabetical order. This allows a prefix-based compression:

  • another
  • any
  • appear
  • appearance
  • apple
  • at
  • attack

Though the formats for the two engines’ dictionaries are completely different, they share this one aspect. Each of these words is then assigned a group number which is then used to store the said specs. I’ve written about that before. The thing is that when you decompile a game script, you can’t tell which synonym from a given group was originally used. And that’s why when you decompile Leisure Suit Larry 2 and look in the scripts regarding really any female character you’ll see them being called bimbos.

(if (or (Said 'call/bimbo,agent') (Said 'get,buy/ticket'))

That is of course because “bimbo” is in the same group (#42) as “woman” and “lady”, but alphabetically comes before them. “Agent” is in its own group (#50) together with various other jobs. You can call this particular woman either by gender or by profession. You can even call her a KGB agent and the game will allow it. By that same token, “call” is in the same group as the “talk” you’d expect to see here (#11).

But the decompiler has little to no idea of these things.

I have recently acquired the full source code for Larry 2, and that shows a slightly different, more sensible word choice:

(if (or (Said 'talk/girl, clerk') (Said 'get, buy/ticket'))

That is of course because these are the actual word groups being used here:

11 42 50
talk
speak
converse
call
woman
girl
lady
stewardess
blond
chick
blonde
slut
broad
bimbo
maid
receptionist
secretary
mother
mama
momma
mom
clerk
waiter
waitress
bartender
storekeeper
shopkeeper
agent
kgb
kgbishna
custom
attendant
shark

You can see how alphabetical order would mess that up.

And by that same token I can now securely say that the debug cheat code in Larry 3 is not in fact “ascot backdrop”.

“Backdrop” in Larry 3 is in group #1063 together with “put”, “drop”, “release”, “set”, “stash”, and various other “put something here” verbs. You know by now how the smart fella who discovered the debug cheat may have gone about it, and how “backdrop” would be the first word in that group. The canonical phrase however, is

Ascot Place

Because of course if I have the Larry 2 code why wouldn’t I have Larry 3 as well?

(cond
  ((Said 'ascot/place')
    (^= debugging TRUE)
    (if debugging
      (Print "Hi, Al!")
    else
      (Print "\"Goodbye.\"")
    )
  )

The question is… why is this the debug phrase?

And the answer? It’s a callback to Larry 2:

And just like that this post’s title makes a little sense.

[ , , , ] Leave a Comment

Priorities Revisited

I just spent way too long drawing little hexagons. Here’s why.

As you know by now, SCI0 up to SCI11 use vector-based priority and control screens, while SCI2 introduced bitmap-based priority screens while also removing the control screen entirely. That means when I render a background for The Dating Pool, I have to trace out everything you can stand behind, by hand, as vectors. And a while back I replaced the simple railing on the space station scenes with hexagons, so I had to re-vector the priority screen to match.

I didn’t draw any hexagons where the booths are because you can’t stand close enough for it to matter. Saves a lotta work for me. That’s still a lot of Line and Fill commands though. And that’s just one screen up there. There’s three.

Here’s what it’d look look if we targeted SCI2, hypothetically:

Much easier to produce, perhaps. I could just load it into my editor of choice, select everything that’d be closer, and cut that out onto a new layer. Rinse and repeat. And despite appearances here, the priority layers wouldn’t all be the full screen size either. They’d actually only be… 320×17 pixels for the one and 320×22 pixels for the other. Could be even smaller if I cut the booth layer into three separate pieces, perhaps.

Note that the background layer is completely missing the colors from the railing and booth layers. Why compress that twice or more after all?

[ , ] 2 Comments on Priorities Revisited

SCI Drivers, how do they work?

Quite well, thank you.

Now, before I begin I should mention that the extended memory and mouse support in SCI is baked into the interpreter itself, and in SCI32 so are the VGA and VESA video drivers. Oddly, the keyboard is not. But anyway!

An SCI DRV file is a piece of standalone code that is loaded on startup and provides a set of standardized routines for the interpreter to call, even if a given feature isn’t technically supported such as palette rotation in EGA or most things on PC speaker. Their format is actually pretty straightforward.

The first four bytes of the file are a single instruction that jumps to the entry point routine. Whenever a driver function is called for, the BP register is set to the requested function and that jump is called. The entry point routine then uses a lookup table, indexed on BP, to call the requested function, and returns. Easy as pie, really. But that leaves the format.

The next four bytes are the number 0x87654321. Yeah, backwards. I know, wild times. This would be used to check if a given DRV file really is a valid driver, but SCI11 at least seems not to care.

Following is a byte that specifies what type of driver we’re talking about. The install/setup program uses this to determine which list to show it in. It’s otherwise useless, especially with my own install program. From zero to seven, these types are: video, music, voice, input, keyboard, communication, mouse, and memory.

The rest of the data can go basically anywhere — they’re Pascal-style (length-prefixed) strings that should specify their file name and description, but some of them are named dude and one is  . Just a single space. And the descriptions? You wouldn’t be able to tell VGA320.DRV apart from VGA320BW.DRV if you followed this information ‘cos they both say they’re “VGA or IBM PS2, Models 25 & 30 – 256 Colors”. Except one of them is optimized for grayscale-only monitors. So yeah.

Next up is the EXTDRV marker, 0xFEDCBA98, directly followed by a four-byte value whose meaning depends on the type. Again, not actually used, purely for external bookkeeping.

Value Video Keyboard Sound
1 MDA IBM Speaker
2 Hercules Tandy AdLib
4 CGA NEC Sound Blaster/DAC
8 PC Jr. Creative Music System
10 Tandy Tandy 3-Voice
20 EGA Tandy DAC
40 MCGA PS/1 3-Voice
80 VGA PS/1 DAC
100 CGA two-color Sound Blaster Pro
200 CGA four-color MPU-401
400 Explorer Disney Sound Source
800 CD-Audio
1000 ProAudio FM
2000 ProAudio DAC
4000 Windows Sound Source
8000 No MIDI

These values can be combined, so MTBLAST.DRV for example reports 0x204, or “MPU-401 with Sound Blaster DAC”. Of course, this would only make sense for the sound drivers but what can you do?

After this, the actual driver code resumes with the dispatch table, a list of function pointers to each of the features the driver supports. Some may point to null functions, but none of them are themselves null. Because that’d be bad. Directly after this is all the general-purpose variable memory that the driver may need, all preset. For example VGA320.DRV has a standard palette and “wait hand” cursor of its own that it throws up initially.

There are three functions that all drivers must have. The rest depends on their type. These are Detect, Initialize, and Terminate, in that order. The first simply returns a few metrics. For video it’s the number of colors, for music it’s the device ID (to decide which channels to play) and how many voices it can handle. The Initialize function actually sets up the device, switching video modes and setting up timers or what have you. What Terminate does ought to be obvious.

And that’s how SCI drivers work.

[ ] Leave a Comment