Logo Pending


A Short Tangent – KQ6 Drop Caps

How does King’s Quest VI do that thing where if the Narrator says something, it includes a fancy drop cap? Well, it’s sorta similar to how KQ5 did, but implemented better. Mostly because it’s a newer game and not basically the first on a new engine.

Requirements: a view full of letters, all the same size, and a font where the tab character is as wide as one of these letters.

First, some technical backstory. Narrator is a special class with methods to speak a specific message. It will decide on its own if it should display the passed message in a window of some kind, or play it back as spoken audio. It will also handle timing, disposing of the window and everything at the end. The messages themselves are provided by the Messager class.

A Talker is a kind of Narrator that builds on the above to add talking heads. Fortunately we won’t have to go any deeper into those because this post is all about KQ6’s drop caps, and it’s only Narrator who uses those.

Specifically, KQ6 uses a special DropCapNarrator, in fact. It’s just like a regular Narrator, but overrides the display method to add some extra bits:

(class DropCapNarrator of Narrator
  (properties
    ; Narrator properties elided.
    strView 945 ; the view with the letters in
  )
 
  (method (display theText &tmp theTalkWidth newWindow theFirst theDropCap)
    ; Ensure we don't clip the screen edge.
    (if (> (+ x talkWidth) 318)
      (= theTalkWidth (- 318 x))
    else
      (= theTalkWidth talkWidth)
    )
 
    ; Clone up a new copy of one of those fancy woody windows.
    ((= newWindow (gWindow new:))
      color: color
      back: back
    )
    ; That is, set newWindow's colors to our own.
 
    ; If we have mouse support and we're not using the invisible
    ; cursor, remember what we -do- use and go invisible.
    (if (and (not (HaveMouse)) (!= gCursorNumber 996))
      (= saveCursor gCursorNumber)
      (gGame setCursor: 996)
    else
      (= saveCursor null)
    )
 
    ; Now we're ready for the magic.
 
    ; Grab the first letter of the message text we're to show.
    (= theFirst (StrAt theText 0))
    (if (and (>= 90 theFirst) (>= theFirst 65))
      ; The first character is between 'A' and 'Z' inclusive.
      ; Replace that first character with a tab.
      (StrAt theText 0 9)
      ; Remember, the tab is as wide as a drop cap!
 
      ; Create a new DIcon for our cap and set it up.
      ((= theDropCap (DIcon new:))
        view: strView
        ; Loop 0 is A to M, loop 1 is N to Z.
        loop: (+ 0 (/ (- theFirst 65) 13))
        cel: (mod (- theFirst 65) 13)
      )
 
      ; Now display our message on screen...
      (Print
        window: newWindow
        posn: x y
        font: font
        width: theTalkWidth
        title: (if showTitle name else null)
        ; ...but with the text shifted down a little...
        addText: theText 0 7
        ; ...and that DIcon on top of it.
        addIcon: newDIcon 0 0 0 0
        modeless: true
        init:
      )
    else
      ; Not a valid drop cap so we can skip all that.
      (Print
        window: newWindow
        posn: x y
        font: font
        width: theTalkWidth
        title: (if showTitle name else null)
        ; See? No positioning or DIcon here.
        addText: theText
        modeless: true
        init:
      )
    )
    ; The rest is up to the regular Narrator. 
  )  
)

Personally, I’d set up most of those properties, then stop and consider if we have a drop cap. Duplicating all those sends… it’s still better than what KQ5 did.

[ , ] 2 Comments on A Short Tangent – KQ6 Drop Caps

On SCI Windows – Part 4 – King’s Quest 6

King’s Quest VI is particularly interesting in that the window frame looks like it has eight parts — four edges, four corners — but the actual view only has the corners. Compared to the simple lines of KQ5, those thick wooden beams have to be views, right?

 But as you can see, they’re not in there.

So how is that woody frame drawn then? The answer: they totally are lines.

(class Kq6Window of SysWindow
  (properties
    colorOne 32
    colorFive 18
    tlColorTwo 17
    tlColorThree 18
    tlColorFour 17
    brColorTwo 18
    brColorThree 17
    brColorFour 16
  )

We interrupt this programming to give a bit of visual aid. Here’s those color properties again, but with the actual colors next to them:

Maybe you can see how they fit together, from the preview on top of the post and the property names.

(method (open &tmp oldPort)
    ; The established setup
    (= screens VISUAL)
    (if (!= priority -1) (= screens (| screens PRIORITY)))
 
    ; Make room for the edges
    (= lsTop (- top 5))
    (= lsLeft (- left 5))
    (= lsRight (+ right 6))
    (= lsBottom (+ bottom 6))
 
    ; Set our custom style
    (= type 128)
 
    ; Always top priority
    (= priority 15)
    (super open:)
 
    ; Now we draw, on the whole screen.
    (= oldPort (GetPort))
    (SetPort 0)
 
    ; Call the method shown below.
    (self drawEdgedWindow: screens)
 
    ; Add the corner pieces
    (DrawCel 930 0 0 (- left 5) (- top 5) -1)
    (DrawCel 930 0 1 (- left 5) (- bottom 1) -1)
    (DrawCel 930 0 2 (- right 1) (- top 5) -1)
    (DrawCel 930 0 3 (- right 1) (- bottom 1) -1)
 
    ; Show what we have wrought.
    (Graph grUPDATE_BOX lsTop lsLeft lsBottom lsRight 1)
    (SetPort oldPort)
  )
 
  ; This is where the actual magic happens.
  (method (drawEdgedWindow screens &tmp line color)
    ; Fill in the window
    (Graph grFILL_BOX top left (+ bottom 1) (+ right 1) screens back priority)
 
    ; Draw the top and left edges first
    (for ((= line 1)) (< line 6) ((++ line))
      (= color
        (switch line
          (1 colorOne) ; inside
          (2 tlColorTwo) ; dark
          (3 tlColorThree) ; light
          (4 tlColorFour) ; dark
          (5 colorFive) ; light
        )
      )
      ; Top edge
      (Graph grDRAW_LINE (- top line) (- left line) (- top line) (+ right line) color priority -1)
      ; Left edge
      (Graph grDRAW_LINE (- top line) (- left line) (+ bottom line) (- left line) color priority -1)
    )
 
    ; Draw the bottom and right edges
    (for ((= line 1)) (< line 6) ((++ line))
      (= color
        (switch line
          (1 colorOne) ; inside
          (2 brColorTwo) ; light
          (3 brColorThree) ; dark
          (4 brColorFour) ; very dark
          (5 colorFive) ; light
        )
      )
      ; Bottom edge
      (Graph grDRAW_LINE (+ bottom line) (- left line) (+ bottom line) (+ right line) color priority -1)
      ; Right edge
      (Graph grDRAW_LINE (- top line) (+ right line) (+ bottom line) (+ right line) color priority -1)
    )
  )
)

Those switch blocks could’ve been left out if they used, say, an array with the color values, but this way they can be exposed as properties. You win some, you lose some.

The drop cap is another story altogether.

[ , ] Leave a Comment

On SCI Windows – Part 3 – King’s Quest 5

Compared to a BorderWindow, the custom frame used in King’s Quest 5 looks pretty straightforward. Indeed, it’s a few lines and some decorative corner pieces, mostly.

 Here they are right now.

Normally, the color properties would be set on startup depending on the detected color depth. Let’s pretend otherwise. Other than that, the below code is effectively unchanged — all I did was add comments.

(class myWindow of SysWindow
  (properties
    ; SysWindow properties elided.
    back 23
    color 8
    lineColor 19
  )
 
  (method (open &tmp screens theTop theLeft theBottom theRight celHigh celWide)
    ; Determine the size of our corner pieces.
    (= celHigh (CelHigh 944 0 0))
    (= celWide (CelWide 944 0 0))
 
    ; Draw on the main screen.
    (SetPort 0)
 
    ; Make some room.
    (= theTop (- top 8))
    (= theLeft (- left 8))
    (= theBottom (+ bottom 8))
    (= theRight (+ right 8))
 
    (= screens VISUAL)
    (if (!= priority -1) (= screens (| screens PRIORITY)))
 
    ; Save what's underneath us.
    (= underBits (Graph grSAVE_BOX theTop theLeft (+ theBottom 2) (+ theRight 2) screens))
 
    ; Draw a drop shadow
    (Graph grFILL_BOX (+ theTop 2) (+ theLeft 2) (+ theBottom 2) (+ theRight 2) screens 0 priority)
 
    ; Draw our fill
    (Graph grFILL_BOX theTop theLeft theBottom theRight screens back priority)
 
    ; Draw the corner pieces
    (DrawCel 944 0 0 theLeft theTop -1)
    (DrawCel 944 0 1 theLeft (- theBottom celHigh) -1)
    (DrawCel 944 0 2 (- theRight celHigh) theTop -1)
    (DrawCel 944 0 3 (- theRight celHigh) (- theBottom celHigh) -1)
 
    ; Top edges...
    (Graph grDRAW_LINE theTop (+ theLeft celWide) theTop (- theRight celWide) lineColor -1 -1)
    (Graph grDRAW_LINE (+ theTop 2) (+ theLeft celWide) (+ theTop 2) (- theRight celWide) lineColor -1 -1)
    ; Bottom...
    (Graph grDRAW_LINE (- theBottom 1) (+ theLeft celWide) (- theBottom 1) (- theRight celWide) lineColor -1 -1)
    (Graph grDRAW_LINE (- theBottom 3) (+ theLeft celWide) (- theBottom 3) (- theRight celWide) lineColor -1 -1)
    ; Left...
    (Graph grDRAW_LINE (+ theTop celHigh) theLeft (- theBottom celHigh) theLeft lineColor -1 -1 )
    (Graph grDRAW_LINE (+ theTop celHigh) (+ theLeft 2) (- theBottom celHigh) (+ theLeft 2) lineColor -1 -1)
    ; and right.
    (Graph grDRAW_LINE (+ theTop celHigh) (- theRight 1) (- theBottom celHigh) (- theRight 1) lineColor -1 -1)
    (Graph grDRAW_LINE (+ theTop celHigh) (- theRight 3) (- theBottom celHigh) (- theRight 3) lineColor -1 -1)
 
    ; Show what we have wrought.
    (Graph grUPDATE_BOX theTop theLeft (+ theBottom 2) (+ theRight 2) 1)
 
    ; Only now do we change our window type to custom and open it.
    (= type 129)
    (super open:)
 
    (= top theTop)
    (= left theLeft)
    (= bottom (+ theBottom 2))
    (= right (+ theRight 2))
  )
)

Don’t let the math discourage you. Because the corner pieces are drawn before the edge lines, the latter have to be carefully drawn right up against the former. You’ll see in the next part how you can get away with doing the edges first and then drawing the corners.

[ , ] Leave a Comment

AGI versus SCI – A Comparison

AGI, which Sierra used up until around the release of King’s Quest IV – The Perils of Rosella, was a strictly linear scripting language with a fairly simple bytecode. Much simpler than SCI’s object-oriented virtual machine. But how do they compare?

Inspired by this one particular book that’s ostensibly about SCI but contains only AGI snippets from what I’ve seen, here’s the first playable room in King’s Quest IV, and how it’s initialized. I’ve left out a bunch of stuff for clarity. Because it’s so simple, AGI bytecode is pretty easy to decompile:

if (justEntered)
{
  set.horizon(84);
 
  if (!nightTime) { v152 = 1; }
  else { v152 = 101; }
  load.pic(v152);
  draw.pic(v152);
  discard.pic(v152);
 
  //Place Ego according to the previous room
  if (lastRoom == 1) { position(ego, 141, 82); }
  if (lastRoom == 2) { position(ego, 107, 82); }
  if (lastRoom == 9) { position(ego, 96, 82); }
  if (lastRoom == 10) { position(ego, 80, 82); }
  if ((lastRoom == 11 || lastRoom == 15)) { position(ego, 70, 82); }
  if ((lastRoom == 12 || lastRoom == 14)) { position(ego, 60, 82); }
 
  //Add some waves in the water.
  animate.obj(o3);
  load.view(55);
  set.view(o3, 55);
  set.loop(o3, 4);
  set.priority(o3, 5);
  ignore.objs(o3);
  cycle.time(o3, 3);
  position(o3, 64, 152);
  draw(o3);
 
  draw(ego);
  show.pic();
}

Hmm. Compare that to its SCI equivalent. SCI bytecode is much harder to decompile. You can disassemble it, but until SCI Companion came out it wasn’t possible to decompile it. Still here we are:

(instance Room1 of Room
  (properties
    picture 1
    north 25
    south 7
    west 31
    east 2
    horizon 100
  )
 
  (method (init)
    (if gNightTime (= picture 101))
    (super init:)
    (self setRegions: 503 501 504 506)
    (wave1
      isExtra: 1
      view: 665
      loop: 0
      cel: 0
      posn: 203 76
      setPri: 0
      ignoreActors:
      cycleSpeed: 3
      init:
    )
    ; Other waves left out for clarity
    (waves add: wave1 wave2 wave3)
    (wave1 setScript: waveActions)
 
    ; This part is simplified significantly.
    (switch gPreviousRoom
      (south (gEgo posn: 225 188))
      (north (gEgo x: 225 y: (+ horizon 1)))
      (0 (gEgo x: 220 y: 135))
      (east (gEgo x: 318)) ; Y stays the same.
    )
    (gEgo init:)
  )
)

You might notice that there’s no equivalent to draw.pic and discard.pic and such. The Room class handles that by itself the moment Room1 calls (super init:).

[ , , ] Leave a Comment

King’s Quest 4 Copy Protection

King’s Quest 4 – The Perils of Rosella starts with a copy protection challenge right off the bat.

If you were playing the original 1988 version you could just enter the magic word “bobalu” and be done with it, but the 1989 version removed this.

It’s a pretty simple challenge-reply system, but the interesting bit is how your answers are considered. If someone were to somehow find the challenges they would also find the answers in the same order, but there’s a catch: the answers are hashed.

Very simply so, but they are. And of course the script code containing the challenges and answer hashes is compressed in the RESOURCE.001 file, and the whole thing is script code and nobody outside of Sierra could be expected to be able to read that stuff back in 1988. Sure, maybe some people could but still good luck figuring out how this worked. Even the backdoor phrase was compressed.

But now it’s 2018, nearly 2019, and I for one have made happy use of the tools now available to us, mostly to slake my own thirst for knowledge. So here’s how it works.

The copy protection script has several local variables: a random number from 1 to 79, the challenge text, the correct answer’s hash, a buffer for the user’s input, the hash for said input, and some work variables.

On startup, the random number is chosen. Then, in a big ol’ switch statement, the correct hash is decided on:

(switch (= randomPick (Random 1 79))
  (1 (= requestSum 431))
  (2 (= requestSum 521))
  (3 (= requestSum 535))
  ;...
  (79 (= requestSum 686))
)

In another big ol’ switch (instead of doing it at once?), the matching challenge is set up:

(switch randomPick
  (1 (= requestText "On page 2, what is the fourth word of the first sentence?"))
  (2 (= requestText "On page 2, what is the fourth word of the second paragraph?"))
  (3 (= requestText "On page 3, what is the fourth word in the first paragraph?"))
  ;...
  (79 (= requestText "In the section TIPS FOR NEW ADVENTURE PLAYERS, what is the eighth word in the first paragraph of tip #2 (STAY OUT OF DANGER)?"))
)

Incidentally, I said our guess would be stored in a buffer variable, that is an array in memory large enough to contain it, but I did not say any such thing about the challenge text. That’s because it’s stored as a pointer to the text, in the place it was loaded to as part of the script. From then on these challenges don’t mutate in any way. Our input can be literally anything.

Anyway, after displaying the challenge, we have our input in a buffer. This is where the magic happens:

(= i 0)
(while (< i (StrLen @userInput))
  (= ch (& (= ch (StrAt @userInput i)) $005f))
  (StrAt @userInput i ch)
  (= inputSum (+ inputSum ch))
  (++ i)
)

Iterating through the user’s input, we read the next line inside-out. Using the StrAt function we fetch the next character and store its value in our work variable. Then we use some binary magic on that same value to turn it into UPPERCASE, and assign that to our work var. Now, as I write this I feel like this can be simplified a little bit…

(= ch (& (StrAt @userInput i) $005f))

…Yeah, that seems nice. I don’t think it’d hurt functionality to do this. Anyway, the next line shows how SCI function and kernel calls can be variadic as all get out — given three arguments, StrAt will set the character on the given spot. In the third line, we add the character’s value to our running sum.

And that’s it! We can now compare our input to the expected answer, and either continue on to the title screen or display an error and quit.

But that’s not all there is to it. First, for some reason, the input is uppercased and then stored again, character by character. This is so the 1988 release can compare it against the magic backdoor word, which is also in uppercase. This seems like an awful waste when you could compare it against a number instead. Not to mention, the 1989 release doesn’t even have the backdoor and still does all this. (For the record, that would be 437.)

Second, this is such a simple method that there are guaranteed to be words with the same hashes. For example, “voice” and “licks” are both 374.

But yeah, that’s just about all there is to know about the copy protection in King’s Quest 4 – The Perils of Rosella.

[ , , ] Leave a Comment

On snakes

“Watch out! A poisonous snake!”

Who among us who have played King’s Quest V – Absence Makes the Heart Go Yonder doesn’t remember this iconic line?

Thing is, it is of course wrong. The snake is venomous, in that it bites you and you die. Indeed, the narrator gets it right when you look at the snake: “A large, venomous snake blocks Graham’s passage to the east.”

I have nine different versions of this game and only two of them get it somewhat right.

The original diskette version with the separate “walk” and “travel” icons? Poisonous. The CD version? Poisonous, even with some script changes!

The Amiga version? Poisonous.

The v55 and v62 EGA releases? Poisonous.

The French diskette version? Venimeux.

The German diskette version that I acquired while I was composing this post? Ein Giftschlange.

Now, I myself am Dutch, and I can confirm that in Dutch too, something venomous and something poisonous are both giftig. If there was a Dutch version of KQ5, Cedric would likely say this:

(made with Foone’s death generator because it was quicker than modding.)

The Japanese PC-98 version?  毒, doku.

Even though doku means poison, a dokuhebi (properly 毒蛇) is very much a venomous snake.

(Note from December 15: this brings to mind a thing from Orphan Subs’ Stop! Hibari-kun! release about the word wani being both crocodile and alligator.)

Of course, there is one more version left – I only listed eight so far. The ninth is a real slap in the face.

Yes, let’s rewrite the entire game so it can run on the NES and not finally fix this while you have the chance.

So basically English is the only language I’ve seen KQ5 in where there’s separate words for “it bites you, you die” and “you bite it, you die”, and none of the English versions get it right!

[ , ] 1 Comment on On snakes

No windows, but no DOS either

But why did AGI have that big black command line bar to begin with?

Because the original didn’t have popup windows:

By then the picture format and all that was pretty much set or something like that. With SCI, they could do it all from scratch, using their AGI experience as merely a guideline, and images could go up to 320×190.

[ , , ] Leave a Comment

Adventure game background art

Ah, Sierra. You gotta love the beautiful background art in their VGA games. From the cartoony…

…to the outright pretty…

…you have to admit someone was credit to team.

But then there’s games where they seemingly dropped the ball (in my opinion) for whatever style-related reason…

…or simply didn’t do much “background art” at all…

And that makes me feel…

Well, okay, I guess, about using edited Gmod screenshots from Letrune in “The Dating Pool”, turning this…

…into this:

[ , , , , , , ] Leave a Comment