Logo Pending


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

Noses are a waste of bandwidth

<Maxane> But then again, rather not ;-p
<Kawa> NOSES ARE A WASTE OF BANDWIDTH

So yeah. That’s me referencing some old IRC thing moments before I decided to write this post, but it got me thinking. There’s the joke that adding a nose to your classic :) smileys is a waste of bandwidth, but how does it stack up against modern emoji?

This is just a short observation, mind you. Done in minutes.

Display Characters UTF8 bytes
:) 2 3A 29
:-) 3 3A 2D 29
🙂 1* F0 9F 99 82

There’s really not much to it, innit?

* depending on how you define ’em.

[ ] Leave a Comment

The Universal Translator

The Universal Translator, as seen in Star Trek, regularly raises questions.

For example, why do the users not hear double voices? But more commonly, how do characters keep saying untranslated klingon words? How can they keep saying p’takh? How can Worf call his embarrassing pimple a gorch without the UT biting him in his klingon ass and making him call it a pimple?

@RikerGoogling has repeated the question a couple times, why the UT doesn’t seem to work on Klingonese. I keep replying, it’s intentional on the speaker’s part. But let’s me go into way too much detail.

How the UT determines intent, I don’t know. It doesn’t matter. Maybe something else makes it skip those words,  but think about it: how would you explain something about a particular language when every word you say ends up rendered in English, or whatever the listener speaks? It’s not just Klingonese either — Picard would sometimes curse in French, right there on the bridge. Likewise, the senior crew singing “For He’s a Jolly Good Fellow” in Klingonese? They meant for it to remain the way they said it, so it was. Even if their manual translation was a bit rough.

So what about the lack of overlapping sounds? Well, I don’t care what you think about Discovery, but one particular scene stood out to me.

You know those channel logos in the corner of the TV screen? Or for you viewers of certain Japanese imports, the (much more obvious) clock? Eventually, you kinda stop seeing them. They’re still there, but you don’t actively register them like you do the rest of whatever it is you’re watching. Or in a cheap dub where the original voice tracks are faintly heard underneath the louder dub track. Background noise, this mild headache I’ve had since before I care to remember, it all just gets tuned out. And that brings me to that one scene in Discovery.

Michael Burnham sneaks onto the klingon vessel and eventually takes out her communicator, enabling its UT feature. We hear the communicator repeat what the klingons say, in English, with a slight delay and text-to-speech quality. Revealing herself, Burnham addresses the leader in English, only for the communicator to echo her words in Klingonese. And shortly into the conversation, the delay disappears and in fact, the klingon leader himself starts speaking fluent English.

I think that’s meant to represent Burnham, but perhaps mostly the viewers, getting used to the UT’s effect. Eventually, you don’t even hear the original lines any more, and your mind just ignores the delay.

There’s only one counter-argument that comes to mind: people without universal translators who listen to people with. Ferengi speaking their own language to 20th century humans, who (once the devices are fixed) can understand the ferengi perfectly well? All the above can’t explain that.

But at least the thing where they keep speaking untranslated klingon is covered.

[ ] Leave a Comment