Logo Pending


On SCI Windows – Part 7 – The Dating Pool

It was inevitable, really, that I’d end this series with my own game. The Dating Pool uses a simplified take on BorderWindow, with the ability to change the thickness removed.

could’ve just used a BorderWindow, in fact, with bevelWid and shadowWid both set to 1 and I’d get the exact same result. This exact style was in fact one of the examples I’ve shown back there! But I did not, if only because I use titles sometimes.

(class cdWindow of SysWindow
  (properties
    topBordColor 7
    lftBordColor 6
    rgtBordColor 4
    botBordColor 3
    titleBack 3
    titleColor 255
  )
 
  (method (open &tmp oldPort screens)
    ; The established setup
    (= screens VISUAL)
    (if (!= priority -1) (|= screens PRIORITY))
 
    ; Make room for the edges
    (= lsTop (- top 2))
    (if title (-= lsTop 9))
    (= lsLeft (- left 2))
    (= lsRight (+ right 3))
    (= lsBottom (+ bottom 3))
 
    ; Set our custom style
    (= type 128)
 
    ; Always top priority
    (= priority 15)
    (super open:)
 
    ; Now we draw, on the whole screen.
    (= oldPort (GetPort))
    (SetPort 0)
 
    ; Draw our fill...
    (Graph grFILL_BOX top left bottom right screens back 15)
 
    ; Custom title bar support!
    (if title
      (-= top 9)
      (+= bottom 1)
      (Graph grFILL_BOX top left (+ top 9) right screens botBordColor titleBack)
      (Display title dsCOORD left top dsWIDTH (- right left) dsCOLOR titleColor dsFONT 999 dsMODE alCENTER)
    )
 
    ; Draw the frame
    (Graph grDRAW_LINE (- top 1) (- left 1) (- top 1) right topBordColor priority screens)
    (Graph grDRAW_LINE (- top 1) (- left 1) (- bottom 1) (- left 1) lftBordColor priority screens)
    (Graph grDRAW_LINE (- bottom 1) left (- bottom 1) right botBordColor priority screens)
    (Graph grDRAW_LINE top right (- bottom 2) right rgtBordColor priority screens)
 
    ; Draw our drop shadow
    (Graph grDRAW_LINE bottom (+ left 1) bottom right 0 priority screens)
    (Graph grDRAW_LINE top (+ right 1) bottom (+ right 1) 0 priority screens)
 
    ; Show what we have wrought.
    (Graph grUPDATE_BOX (- top 1) (- left 1) (+ bottom 1) (+ right 1) VISUAL)
    (SetPort oldPort)
  )
)

Eagle-eyed viewers might notice that the color properties are set to various grayscales while the image up top is green. This is simply because the instance of this cdWindow class has those colors. The control panel, in contrast, is tan.

And that’s the end of this series. If you have any other SCI games (preferably SCI11 or earlier) that you’d like to see, drop me a line in the comments below and I’ll see what I can do. Barring any of that though…

[ , , ] 2 Comments on On SCI Windows – Part 7 – The Dating Pool

On SCI Windows – Part 6 – Laura Bow, Frontier Pharmacist

It’s a double feature this time because these last two games’ windows are very similar.

These windows have decorations depending on your location or progress. Here are the view resources, both of them #994…

…and here is the code for the lb2Window first:

(instance lb2Win of SysWindow
  (properties
    ; This time, we set the custom bit up here.
    type 128
  )
 
  (method (open &tmp oldPort loop)
    ; Decide which decoration to show depending on our location.
    ; (These have been cut down a bit and the doubles were already
    ;  there. I don't know what's up with that.)
    (cond 
      ((OneOf gRoomNumber 280 210 330 240 260 300) (= loop 0))
      ((OneOf gRoomNumber 210 220 230 260 270 280) (= loop 1))
      ((OneOf gRoomNumber 100 105 110 120 140 150) (= loop 2))
      ((OneOf gRoomNumber 460 660 700 710 715 720) (= loop 4))
      ((OneOf gRoomNumber 335 340 350 355 360 370) (= loop 3))
      (else (= loop 4))
    )
 
    ; Make room for the edges, including the decorations.
    (= lsLeft (- left (/ (CelWide 994 loop 0) 2)))
    ; Yes, LB2 windows adjust for titles, despite the custom bit.
    (= lsTop (- top (if title 19 else 10)))
    (= lsRight (+ right (/ (CelWide 994 loop 0) 2)))
    ; Be at *least* as tall as the decoration.
    (= lsBottom (Max (+ bottom 3) (+ lsTop (CelHigh 994 loop 0) 3)))
 
    ; Always top priority
    (= priority 15)
    (super open:)
 
    ; Now we draw, on the whole screen.
    (= oldPort (GetPort))
    (SetPort 0)
 
    ; Draw our fill...
    (Graph grFILL_BOX top left bottom right 3 gBack 15)
    ; ...border...
    (Graph grDRAW_LINE (- top 1) (- left 1) (- top 1) right gFore 15)
    (Graph grDRAW_LINE (- top 1) (- left 1) bottom (- left 1) gFore 15)
    (Graph grDRAW_LINE bottom (- left 1) bottom right gFore 15)
    (Graph grDRAW_LINE (- top 1) right bottom right gFore 15)
    ; Show what we have wrought...
    (Graph grUPDATE_BOX top left bottom right 1)
    ; ...but also dirty the part where the decorations will go.
    (Graph grUPDATE_BOX lsTop lsLeft (+ lsTop (CelHigh 994 loop 0)) (+ lsLeft (CelWide 994 loop 0)) 1)
    (Graph grUPDATE_BOX lsTop (- lsRight (CelWide 994 loop 0))  (+ lsTop (CelHigh 994 loop 0)) lsRight 1)
    ; Why? I have no idea.
 
    ; Now draw the decorations!
    (DrawCel 994 loop 0 (+ lsLeft 1) (+ lsTop 1) -1)
    (DrawCel 994 loop 1 (- (- lsRight (CelWide 994 loop 0)) 1) (+ lsTop 1) -1)
 
    (SetPort oldPort)
  )
)

Don’t let the thicker right edge fool you — half that line is part of the decoration!

As I said, the code for fpWin is remarkably similar… yet different? Most of it’s just in how the thicker edges are drawn, but a significant bit is in the last part:

(instance fpWin of SysWindow
  (properties
    type 128
  )
 
  (method (open &tmp oldPort loop theLsTop theLsLeft theLsRight)
    ; Decide which decoration to show depending on the act.
    (switch gAct
      (1 (= loop 0))
      (2 (if (Bset 1) (= loop 2) else (= loop 1)))
      (3 (= loop 3))
      (4 (= loop 4))
      (5 (= loop 4))
    )
 
    ; Make room for the edges, including the decorations.
    (= lsLeft (- (- left 3) 15))
    (= lsTop (- (- top 3) (if title 25 else 15)))
    (= lsRight (+ right 3 15))
    (= lsBottom (Max (+ bottom 3) (+ lsTop (CelHigh 994 loop 0) 3)))
 
    ; Always top priority
    (= priority 15)
    (super open:)
 
    ; Now we draw, on the whole screen.
    (= oldPort (GetPort))
    (SetPort 0)
 
    ; Draw our fill...
    (Graph grFILL_BOX top left bottom right 3 gBack 15)
    (if title (= top (- top 10)))
 
    ; Inner box, middling dark...
    (Graph grDRAW_LINE (- top 1) (- left 1) (- top 1) right 17 15)
    (Graph grDRAW_LINE (- top 1) (- left 1) bottom (- left 1) 17 15)
    (Graph grDRAW_LINE bottom (- left 1) bottom right 17 15)
    (Graph grDRAW_LINE (- top 1) right bottom right 17 15)
    ; Middle box, lighter...
    (Graph grDRAW_LINE (- top 2) (- left 2) (- top 2) (+ right 1) 19 15)
    (Graph grDRAW_LINE (- top 2) (- left 2) (+ bottom 1) (- left 2) 19 15)
    (Graph grDRAW_LINE (+ bottom 1) (- left 2) (+ bottom 1) (+ right 1) 19 15)
    (Graph grDRAW_LINE (- top 2) (+ right 1) (+ bottom 1) (+ right 1) 19 15)
    ; And finally the outer, darkest box.
    (Graph grDRAW_LINE (- top 3) (- left 3) (- top 3) (+ right 2) 16 15)
    (Graph grDRAW_LINE (- top 3) (- left 3) (+ bottom 2) (- left 3) 16 15)
    (Graph grDRAW_LINE (+ bottom 2) (- left 3) (+ bottom 2) (+ right 2) 16 15)
    (Graph grDRAW_LINE (- top 3) (+ right 2) (+ bottom 2) (+ right 2) 16 15)
 
    ; Show what we have wrought.
    (Graph grUPDATE_BOX (- top 3) (- left 3) (+ bottom 3) (+ right 3) 1)
 
    ; Unlike LB2, FPFP does this part the hard way.
    (switch gAct
      (1
        (= theLsLeft (+ lsLeft 2))
        (= theLsRight (- (- lsRight 15) 14))
        (= theLsTop lsTop)
      )
      (2
        (if (Bset 1)
          (= theLsLeft lsLeft)
          (= theLsRight (- (- lsRight 15) 13))
          (= theLsTop (+ lsTop 2))
        else
          (= theLsLeft (+ lsLeft 8))
          (= theLsRight (- (- lsRight 15) 14))
          (= theLsTop (+ lsTop 4))
        )
      )
      (3
        (= theLsLeft (+ lsLeft 6))
        (= theLsRight (- (- lsRight 15) 40))
        (= theLsTop (+ lsTop 11))
      )
      (4
        (= theLsLeft (+ lsLeft 7))
        (= theLsRight (- (- lsRight 15) 14))
        (= theLsTop (+ lsTop 8))
      )
      (5
        (= theLsLeft (+ lsLeft 7))
        (= theLsRight (- (- lsRight 15) 14))
        (= theLsTop (+ lsTop 8))
      )
    )
 
    ; Now draw the decorations!
    (DrawCel 994 loop 0 theLsLeft theLsTop -1)
    (DrawCel 994 loop 1 theLsRight theLsTop -1)
 
    (SetPort oldPort)
  )
)

I don’t know why either. Maybe something silly about the decorations?

[ , , , ] Leave a Comment

On SCI Windows – Part 5 – Police Quest 4 but not

Police Quest IV – Open Season is an SCI2 game. As such, it had a radically different way of doing several things. It’s not too hard though to backport one of its elements, namely its translucent windows.

I’ve tweeted about this at length, and actually had to reimplement it all over again. But I did it.

(class TranslucentWindow of SysWindow
  (method (open &tmp oldPort screens scaleX scaleY)
    ; Your standard setup, KQ6 style...
    (= lsLeft (- left 1))
    (= lsRight (+ right 1))
    (= lsTop (- top 1))
    (= lsBottom (+ bottom 1))
 
    (= type 128)
    (= priority 15)
    (super open:)
 
    (= oldPort (GetPort))
    (SetPort 0)
    (= screens VISUAL)
    (if (!= priority -1) (= screens (| screens PRIORITY)))
 
    ; 128 is 100% of the source image size, which is 8x8.
    ; (128 / source) * target --> 16 * target.
    (= scaleX (* (- lsRight lsLeft) 16))
    (= scaleY (* (- lsBottom lsTop) 16))
    (DrawCel 923 1 0 lsLeft lsTop 15 scaleX scaleY)
 
    ; Draw the frame
    (Graph grDRAW_LINE lsTop lsLeft lsTop (- lsRight 1) 0 -1 -1)
    (Graph grDRAW_LINE (- lsBottom 1) lsLeft (- lsBottom 1) (- lsRight 1) 0 -1 -1)
    (Graph grDRAW_LINE lsTop lsLeft (- lsBottom 1) lsLeft 0 -1 -1)
    (Graph grDRAW_LINE lsTop (- lsRight 1) (- lsBottom 1) (- lsRight 1) 0 -1 -1)
 
    ; Show what we have wrought.
    (Graph grUPDATE_BOX (- lsTop 4) (- lsLeft 4) (+ lsBottom 4) (+ lsRight 4) 1)     
    (SetPort oldPort)
  )
)

You’ll also need to do something about DText, up in Interface.sc, or you’ll get ugly opaque text in a translucent window. I use my own extended interpreter so I get the drop shadow for free, but here’s the deal:

(class DText of DItem
  ; Rest of the class elided.
  (method (draw)
    ; We don't get to use hotspot rects anymore, sorry.
    (Display text dsFONT font dsCOORD nsTop nsLeft dsWIDTH (- nsRight nsLeft) dsCOLOR 7 dsBACKGROUND -1 dsALIGN mode)
  )
)

And yeah, the color’s pretty much fixed too — the DrawControl kernel call uses the current port’s colors, which would be the window’s. No such luck unless you significantly mess with things to not only add a color property to DText but also set it somehow while not actually invoking DText yourself so yeeeeaaah this might not be the window style for you.

[ , , ] Leave a Comment

On SCI Windows – Part NaN – The Colonel’s Bequest

I was going to cover Laura Bow – The Colonel’s Bequest but it turns out its custom window is almost exactly the same as the one in KQ5 and it’d be faster to just go over the differences.

  1. No drop shadow.
  2. No color properties.
  3. The corners are in two loops of two cels each.
  4. No memorizing the adjusted coordinates — the dispose method does the adjustments itself.

That’s literally all there is to it.

[ , , ] 2 Comments on On SCI Windows – Part NaN – The Colonel’s Bequest

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

On SCI Windows – Part 2 – BorderWindow

A popular style of window seen in SCI1 games like Police Quest and Space Quest is the BorderWindow. By default it’s thick and gray, but it can really have any thickness and set of five colors you want.

The default looks something like this. Your exact colors may vary as I use a custom palette instead of the standard SCI1 palette, and Space Quest 5 has somewhat tinted grays.
If one were to edit the BorderWindow script a little bit to enable title bars, and did it in a naive way, they’d get something like this. So let’s not do that.
The bevelWid and shadowWid properties can of course be changed on a per-window basis…
…as can the back and four edge colors, topBordColor et al.

But how does this work exactly? Let’s take the actual BorderWindow script code and walk through it.

(class BorderWindow of SysWindow
  (properties
    ; All other properties inherited from SysWindow
    back 5
    topBordColor 7
    lftBordColor 6
    rgtBordColor 4
    botBordColor 3
    bevelWid 3
    shadowWid 2
  )
 
  (method (dispose)
    (super dispose:)
    (SetPort 0)
  )
 
  (method (show)
    (Graph grUPDATE_BOX top left bottom right VISUAL)
  )
 
  (method (open &tmp oldPort screens)
    (SetPort 0) ; Use the entire screen.
 
    ; If we have a priority set, render to that screen too.
    (= screens VISUAL)
    (if (!= priority -1) (= screens (| screens PRIORITY)))
 
    ; Make some room in the "last seen" rect to fit the border.
    (= lsTop (- top bevelWid))
    (= lsLeft (- left bevelWid))
    (= lsRight (+ right bevelWid shadowWid))
    (= lsBottom (+ bottom bevelWid shadowWid))
 
    (= type 128) ; We are custom, as described in the
    ; previous post.
 
    (super open:) ; Let a SysWindow open itself.
    ; This actually just calls the NewWindow kernel call
    ; and saves the returned handle to our window property.
 
    (drawWindow top left bottom right back
      topBordColor lftBordColor botBordColor rgtBordColor
      bevelWid shadowWid
      priority screens
    )
 
    (= oldPort (GetPort)) ; Remember our current port.
    (SetPort 0) ; Use the whole screen again.
    ; Show what we have wrought.
    (Graph grUPDATE_BOX lsTop lsLeft lsBottom lsRight VISUAL)
    (SetPort oldPort)
  )
)

That’s not so bad. The meat of the dish is in drawWindow, obviously:

(procedure (drawWindow t l b r theColor topColor leftColor bottomColor rightColor theBevelWid theShadowWid thePri theMaps &tmp savePort i)
  ; Again, we remember the current port and use the whole screen.
  (= savePort (GetPort))
  (SetPort 0)
 
  ; Fill in the window background.
  (Graph grFILL_BOX t l (+ b 1) (+ r 1) theMaps theColor thePri)
 
  ; Extend our rect to include the bevel.
  (-= t theBevelWid)
  (-= l theBevelWid)
  (+= r theBevelWid)
  (+= b theBevelWid)
 
  ; Draw the top and bottom bevels as simple boxes.
  (Graph grFILL_BOX
    t l (+ t theBevelWid) r
    theMaps topColor thePri
  )
  (Graph grFILL_BOX
    (- b theBevelWid) l b r
    theMaps bottomColor thePri
  )
 
  ; Draw the left and right bevels line by line.
  (for ((= i 0)) (< i theBevelWid) ((++ i))
    (Graph grDRAW_LINE
      (+ t i) (+ l i) (- b (+ i 1)) (+ l i)
      leftColor thePri -1
    )
    (Graph grDRAW_LINE
      (+ t i) (- r (+ i 1)) (- b (+ i 1)) (- r (+ i 1))
      rightColor thePri -1
    )
  )
 
  ; Draw the shadow last. Unlike SysWindows,
  ; these are done as two boxes.
  (if theShadowWid
    (Graph grFILL_BOX
      (+ t theShadowWid) r
      (+ b theShadowWid) (+ r theShadowWid)
      theMaps 0 thePri
    )
    (Graph grFILL_BOX
      b (+ l theShadowWid)
      (+ b theShadowWid) r
      theMaps 0 thePri
    )
  )
 
  (SetPort savePort)
)

To visualize, a BorderWindow (sans shadow) renders like this:

Next time, we take a step back and look at King’s Quest.

[ , ] 1 Comment on On SCI Windows – Part 2 – BorderWindow

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.

[ , , , ] 7 Comments on King’s Quest 4 Copy Protection