Logo Pending


Script resources – a dyad in the Force, as it were

ZvikaZ recently ran into an issue trying to hack Quest for Glory 1 VGA where they edited a particular script, and it worked fine, but when they then exported the .scr file and put it in a clean QFG1 folder, it broke in a particular way. One particular phrase stood out to me in particular:

There are ‘ch’ strings instead of the numerical values

I had a feeling what the problem might’ve been when I started reading the post but when I saw that part I knew exactly what happened.

Quest for Glory 1 VGA is an SCI11 game. That means the scripts are split up into .scr and .hep pairs, and ZvikaZ only copied the one file instead of both. One of them contains the actual script bytecode, but the other contains the amount of local variables, their default values, information on all the objects in the script, and all the text string literals in the script. It’s called a heap resource because that’s where it’s loaded.

Originally, the script and heap resources were one and the same. When a given script needed to be loaded, it would be loaded into heap memory and kept there until unloaded. And as explained before, a saved game is basically a compressed dump of the entire heap memory area, while hunk space contains all the other resources that the scripts, in turn, refer to. Now imagine for a second a script resource with a single class in it, with a single particularly big method, so that a mere fraction of the script resource describes the class, and contains any near strings and such, and all the rest of it is bytecode. Once loaded, the bytecode can’t be changed — only the class properties and any local variables can be, but all of that bytecode is still part of the heap. There’s only so much heap space available to a game, so as long as that script is resident, that bytecode will take up precious space.

SCI11 split the script resources up so that the bytecode parts would be kept in hunk space instead, swapped in from disk when actually needed by something from the script definitions in heap space. All that space taken up by PMachine bytecode is suddenly no longer part of the heap and this bad boy can fit so many script resources at once. And if your scripts use far text instead of near — text resources referenced by a module/line tuple that get loaded into hunk space, instead of "quoted strings like this" that are part of the script’s heap resource) anything those scripts try to say automatically also doesn’t take as much space. You trade a two-byte pointer for a four-byte tuple, but those numbers in turn may refer to a string of who knows what length. Savings!

ZvikaZ’s target was the script resource for QFG1‘s character creation screen, whose first class is a Room named chAlloc. That name appears in the heap resource. When ZvikaZ changed the script code and recompiled, the heap resource had its contents changed, including where exactly in the file the room’s definition started. Whatever mixed-up monstrosity resulted when ZvikaZ then tried to run the altered 203.scr against an untouched 203.hep didn’t function and notably printed ch instead of numerical statistics.

I’m honestly a little impressed it didn’t “oops” on the spot.

[ ] 1 Comment on Script resources – a dyad in the Force, as it were

SCI versions and naming

Did Sierra ever call the various versions of SCI the same names we use? We being the fans, the tool creators, and the ScummVM developers?

It’s unlikely.

One thing to keep in mind is that the interpreter was in near-constant development by one team, while other teams made the games. Every so often the game developers would pull in the latest interpreter and system scripts from a network share. Another thing to keep in mind is that the version numbers are a little weird in places, and that the games themselves had their own version numbers on top of that, so for example you could have King’s Quest 4 version 1.000.106 running on SCI 0.000.274, but also KQ4 1.000.111 on the same interpreter, released five days later, and the later update with the changed graphics that was version 1.006.003 running on SCI 0.000.502.

The first generation of SCI, the one we call “SCI0”, had versions starting with “0.000”, such as the KQ4 example above. This covers every single 16-color, parser-based, English-only game, with the lone exception of the Police Quest 2 PC-98 release. That was version “x.yyy.zzz”, no joke. This generation can also be subdivided into two blocks, where versions up to 0.000.343 had green button controls instead of using whatever the window color was set to, covering the ’88 versions of KQ4 and the first version of LSL2, and the rest covered all the other games.

What we call SCI01 had versions starting with “S.old”. At least “x.yyy.zzz” has the placeholder excuse but whatever. SCI01 games were just like SCI0 on the surface, but had support for multiple languages (previously introduced in version x.yyy.zzz), and saw no more releases than ’88 SCI0 — six of ’em. So technically there’s nothing about that version string to inspire “SCI01”, besides perhaps KQ1 using “S.old.010″ 🤔

Next up was SCI1, which came in both EGA and VGA and usually had versions starting with “1.000″. There is one game, Quest for Glory 2, with five different interpreter versions that still had the text parser (and technically one Christmas card) before it was removed in favor of the icon bar. Some SCI1 games again have interpreters with very strange versions — it appears Eco Quest and Space Quest 4, among others, had some Special Needs™, given interpreter version “1.ECO.013” and “1.SQ4.057″. But on the whole you could still tell from the first character in the version that these were SCI1 interpreters.

SCI11 removed the multi-language support in favor of things like scaling sprites and the Message resource type. All SCI11 interpreters in the wild use versions starting with”1.001“, except for the ones used in Laura Bow 2 (“2.000.274”), Quest for Glory 3 (“L.rry.083”), and Freddy Pharkas (“l.cfs.081”), among a straggler or three.

Up to now these were 16-bit real-mode applications. SCI2, with versions starting “2.000” was a 32-bit protected mode application instead, with the ability to use much more memory and run in a SuperVGA video mode. No SCI2 interpreter found in the wild seems to stray from this version pattern, mostly because all SCI2 games use version 2.000.000. SCI21, in turn, runs on interpreter version 2.100.002, although there are technically three different sub-versions of 2.100.002. That’s not confusing at all. And finally, SCI3 was only seen in interpreter version 3.000.000.

I’m thinking after the switch to 32-bits, they must’ve stopped automatically bumping version numbers on build.

So what does Sierra call them, then? Well, sources say that Sierra called the 32-bit interpreters SCI32, and the source code archive that I based SCI11+ on was SCI16.ZIP. But none of the changelogs and such seem to refer to SCI0, SCI1, or whatever.

 

 

Happy slightly belated new year 🥂

[ , ] Leave a Comment

Server move?

Well, that took the better part of my evening!

So I got an email earlier today from the provider behind my VPS saying they’re gonna stop doing VPS, that I should switch to a KVM, and they left me a coupon for six billing periods (that is, months) of free service for said KMS. Which is nicer than what Centarra did way back then, keeping a hundred euros in service credit for themselves.

And even though I barely have a clue what the difference is between a VPS and a KVM, I did manage to do it with hopefully as few hiccups as I can manage.

Thanks to Dark Kirb for putting my mind at ease, and to Emuz for handling the domain part.

Leave a Comment

Objects, functions, properties, and methods

Whether you’re trying to interpret SCI code in its source form, or compile it into bytecode, there are some inferences to make. Consider the following statements:

(foo1 bar:)
(foo2 bar: 42)
(foo3 69)
(foo4)

You can have object references, kernel calls, and local function calls, and those object references can be local instances or pointers which in turn can be stored in global variables, local variables, variables temporary to the current function or method, or properties of the current method’s object. How would you determine what each foo is?

First, you can see if there is a second item in the expression. If that item ends in :, like in the first two cases, you know that’s a selector so the identifier at the start must be an object reference of some sort. If it’s not, like in the third case, or if there is no second item at all, it must be a function or kernel call since anything else would be an error.

For the first two examples, we now know that foo must be an object. Having looked through the whole script before, we already have a list of all the parameters, temporary variables, local variables, and those from script 0, which are global. If either of those contains an item by that name, we know it’s a pointer to dereference. If it’s a local or imported object’s name, we’d be able to find that as well and can continue on. If it doesn’t appear in any of these six lists, the source code is in error.

For the other two examples, we know it must be a function or kernel call. There are three lists to check this time, being the local functions, imported functions, and kernels. Other than that, things are the same as before.

That leaves the matter of selectors. They can refer to either properties or methods, which are… actually rather trivial to tell apart considering the objects have two dictionaries, one for each type. Objects may have superclass chains reaching all the way to the Base Object and inherit properties and methods from those superclasses, but you might consider folding those superclasses’ dictionaries into the object instance’s so there’s only two to scan through.

Let’s say bar is a property. The first example would then mean “take the foo object and return its bar property’s value. Likewise the second would mean “set it to this expression.” If it’s a method, you’re given a pointer to that method’s code (which may be unique to that instance, having overwritten whatever the superclass chain started with) and you can pass it each non-selector argument in turn, until the next selector. I wrote about that before.

[ ] Leave a Comment

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 (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