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

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

On SCI Windows – Part 1 – SysWindow

Welcome to the first of a series of posts about window styles in SCI games. In this first entry I’ll go over the most basic of window styles available, the kernel-drawn one.

A regular old SCI window is drawn by the kernel’s RDrawWindow function. It can draw or not draw various parts. The type property that you can set on a SysWindow object yields the following possibilities:

nwNORMAL (stdWindow for those of us with the original source) looks like exactly that. A filled frame with a shadow. There’s nothing particularly awesome about these.
nwTRANSPARENT (wNoSave) windows forego the filling-in, revealing that the drop shadow isn’t two lines but a full rectangle. As the proper name implies, this skips taking a little screenshot of what’s underneath so when the window is closed things can be restored quickly.
nwNOFRAME (wNoBorder) windows only have the fill.
nwTITLE (wTitled) windows extend upwards a little bit to make room for a title bar. If the title property is zero, the title bar is left empty. If it’s not, it better point to a valid string.
Besides these basic styles, you can also combine them. Surprising or not, there is a proper window in this image, with a port, location, pen color and everything.
Some combinations are a little silly to look at…
…while others are simply useless. If there’s no frame, there’s no title bar!
The fill is of course not always white (assuming it’s applied in the first place) — setting the back property to the desired color palette index handles the fill, and the color property likewise affects the contents, as demonstrated in Leisure Suit Larry 3.

One last style bit is 127/0x80 wCustom, which has no SCI Studio/Companion counterpart constant and makes the kernel not draw anything at all. This is specifically for custom-drawn window frames, as we’ll cover in the next part.

The logic for RDrawWindow goes a little like this:

  1. If we don’t have wNoSave set, save the underbits.
  2. If we don’t have wCustom set
    1. If we don’t have wNoBorder set
      1. Draw the frame and drop shadow. We’ve already raised the roof in RNewWindow.
      2. If we have wTitle set, draw the bar and (if nonzero), the title text. We now have something like style 1 or 5.
    2. Shrink the window rectangle a bit.
    3. If we don’t have wNoSave set, draw the window interior. Basically, do style 2.
  3. Show what we have wrought.¹

And that’s all there is to know about how a SysWindow is drawn to the screen.

¹: actual comment at this step.

[ ] 1 Comment on On SCI Windows – Part 1 – SysWindow

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

Going in-depth on SCI map files

One thing that stood out to me when I look at my collection of Sierra SCI games is that basically all the 16-color and early 256-color games (that is, SCI0, SCI01, SCI1) have multiple resource volumes, but the SCI11 games do not. Not because a lot of them came on CD either, the diskette versions, once installed, had a single resource volume too.

Why do you suppose that is? Why do the diskettes for SCI11 games contain a single resource.000 file that’s been split across them all for the install script to merge back together on the hard drive?

Because the map format doesn’t allow it.

The resource.map file specifies which resources can be found on which disks. For SCI0, it’s a list of six-byte entries. The first two encode the type (upper five bits) and number (the lower 11 bits), the next four have the volume number (upper six) and absolute offset in the volume (the rest). If a resource appears on multiple disks, it’s listed once for every appearance. In SCI01, there are only four bits for the volume number, trading amount for space.

In the later versions, the map file starts off with a list of offsets for each type listed in the map. With this list and a contract that the map entries are sorted by type, the interpreter can look up a given type of resource much faster. Since we already know that this part of the list only contains resources of a given type, we can use the full 16 bit range for the numbers. In SCI1, the next four bytes are just like in SCI01. In SCI11 however there’s only three, and there’s no volume number. Then in SCI2 it’s a straight-up plain 32 bit offset value.

The trick with SCI11 having only a 24 bit range for its offsets is that the value is shifted. They must be aligned on a two byte boundary so that the offset range is effectively doubled again.

As a practical example, let’s look at The Dating Pool. If I open its resource.map in my favorite hex editor and try to look up the second view, it’d go like this.

The first few bytes are 80 2B 00 81 EE 00 82 BB 01. We know the format, so this means that views (type 80) start at offset 002B and background pictures (81) start at offset 00EE, which means the views end there. Skipping ahead to the given offset, we see this: 00 00 00 00 00 05 00 02 26 00. Obviously the first resource (view 0) is at offset zero. Splitting this up into handy chunks, 0000 000000 0005 002602, we see that view 5 (which is the second one in the game data) is at offset 2602 << 1 = 4C04. Open up resource.000 and go there, and the first thing we see is confirmation: 80 05 00 36 44 36 44 00 00. View number 5, 4436 bytes in size both unpacked and packed, no compression, followed by the actual view data. Then there’s a single null byte for padding, and view 10 begins.

[ ] Leave a Comment

Animation Shop

December 23rd last year, I was working on some example material for my seqmaker tool, a converter/player that turns image sequences into SEQ files that SCI11 can play. The DOS versions of King’s Quest 6 and Gabriel Knight 1 used them extensively. At first I wanted to use a GIF sequence from Prince of Persia, the original one, because it has very few colors (32) and the only animated elements in that sequence are the Prince and a touchplate.

The only tool I had available to me to manipulate the GIF was ffmpeg. It wasn’t very helpful as after I’d made the SEQ file and watched it in examine mode, which blanks out everything outside of the changed region, I found that there was very subtle dithering.

One would expect, when the Prince starts to turn around, that he’d be the only thing stored, much like this:

“No. Fuck you and the horse you dithered in on.”

This is simply not acceptable!

To think I’d already spent too long manually undithering the solid black background! So in the end I went with the first shot from Hotel Mario instead. Worked out great.

Now, years ago I used to have a program called Jasc Animation Shop. By the creators of Paint Shop Pro, which I use to this day. The old 8.0 version from Jasc, before Corel bent it over. Importantly, Animation Shop was meant for GIFs, but also supported AVI and could import from sequences. Even more importantlier, it could export to GIF with full quality control. Or at least full enough to ensure it wouldn’t add dither noise. Today I found a copy and tried the thing again for a laugh.

Beautiful.

[ ] Leave a Comment

What even is an adventure game?

That’s a question SCI doesn’t even bother asking.

Weird, I know, considering it’s an adventure game engine.

Or is it? Turns out it’s really not. The “adventure game” part is almost entirely a matter of scripts.

In SCI proper, there is no concept of a player, of a room, of basically anything concrete. The engine only cares about a few data types like List, for which it provides function calls to use them, that the Cast list’s contents are View objects in a duck-typing sense, and that this list is global variable #5. Also, script zero export zero is the starting point. From there, you’re mostly on your own.

A room in SCI is just a particular kind of object that sets up the things you can find inside, handles room-specific inputs, and… that’s basically it. The SCI engine doesn’t even know the difference between an abstract View (some possibly-animated non-background element), an atmospheric effect, an NPC, or the player character. Note that all of those things are still View, in a class-inheritance sense.

The biggest blow to the idea that SCI is an adventure game engine has to be the board games though. Between Jones in the Fast Lane and the Hoyle series, I don’t think anyone could claim otherwise for long.

Contrast that with a certain other engine or two.

[ ] Leave a Comment

On fonts

SCI, being rooted in MS-DOS and from a time before Unicode (fun fact, the first draft proposal dates back to 1988, when King’s Quest 4 came out as the first SCI game), SCI uses an 8-bit string format. That is, each character in a string is one byte, and that’s all it can be. Making strings one of very few standard data types in SCI that aren’t 16-bits and requiring a dedicated kernel call to manipulate (as seen in KQ4 Copy Protection) but that’s not the point here.

American releases of SCI games would normally have font resources ranging only up to 128 characters, with the Sierra logo at 0x01 and ~ at 0x7E. Only caring about newlines, all other characters are considered printable. European releases would include usually not 256 but 226 characters, up to ß at 0xE1, basically copying code page 437 but leaving out the graphical elements among others. This means, of course, that a Russian translation of such a game would require another custom font copying code page 866 instead.

And then there’s the whole thing where SCI Companion uses the Win-1252 code page (it’s not exactly a Unicode application) which makes translated games look pretty wild:

Ich glaube Dir gern, daá Du das tun m”chtest!

That doesn’t look quite right. That’s supposed to be “Ich glaube Dir gern, daß Du das tun möchtest!” And indeed, comparing things between DOS-437 and Win-1252, we see that á and ß are both encoded in the same byte value.

That’s the kind of bullshit Unicode was made for, isn’t it?

So what I did for my SCI11+ project, of which one version is used in The Dating Pool, is to add optional basic Unicode support, so you can write text data in UTF-8 and not have to worry about things all that much. There are however two major problems with this idea. One of them is that SCI Companion is not a Unicode-aware program, so you can’t use that to write the text data. That’s easily solved with external resource editors that are. The second is more insidious — the fonts.

What I found out about SCI font resources is this: their header fields are way too wide.

typedef struct
{
  word lowChar;
  word highChar;
  word pointSize;
  word charRecs[0];
} Font;

lowChar is always zero, but the interpreter does acknowledge it. highChar is, as discussed above, always 128, 226, or 256. The fact that it’s exclusive is basically the only reason to have it not be a char-type value.

.if bx < es:[si].highChar && bx >= es:[si].lowChar

See? Exclusive. But the takeaway here is obvious. SCI font files can contain up to 65,535 characters. That’s enough to cover the Basic Multilingual Plane. As such, I’ve added handling of double- and triple-byte UTF-8 sequences to SCI11+. I’ve tested it, too:

Switching to another, non-extended font, I expected to see "Tend ", and that’s exactly what I got. The routine I linked to would decode an ō and dutifully pass it on down to StdChar, which would see that 0x14D is way higher than 226 and simply draw a blank.

(Now, between the first draft of this post and its publication, I’ve further enhanced this system to not decode anything if the font has fewer than 256 characters, falling back to code page 437 or whatever, just not doing anything special.)

That leaves one last issue, which is mostly a matter of wasted space. I like my quotation marks to be proper curly, and in Win-1252 as The Dating Pool uses (because why shouldn’t it?) this is easy — just draw a  and in the font at 0x93 and 0x94,  and be done with it. But in Unicode, these two characters are part of the General Punctuation block, which starts all the way at U+2000. That would mean defining up to that many dummy characters. A two-byte pointer, two size bytes, and a single byte with at best one bit set per character.

That’s bullshit.

As such, I’d propose to cheat like hell and move the General Punctuation block so it covers the much earlier Combining Diacritical Marks block. It’d be way too much of a nightmare to support those. So while measuring and drawing, detect if you’re in the 0x2000 to 0x206F range, subtract 0x2000, add 0x300, and use that character instead. Or have the custom resource tools  that we’d need anyway do it.

(Again, just before publication, I came up with an idea to have a new font generation tool that takes a bitmap of the font and converts it. The trick for space-saving is that it would recognize graphics it’d already processed and simply place a pointer to the first one. Instead of five bytes per dummy, it’d use only two for all but the first. Savings!)

(Update: I made the thing.)

At any rate, your input is appreciated.

Except for yours, Covarr.

[ , ] Leave a Comment