Logo Pending


String functions in SCI11 and SCI2 compared

Leaving out the oddly-named StrSplit in SCI01, let’s get into the other string functions we’ve got. I have an idea that I’d like to ponder, y’see?

First up, in the old 16-bit SCI, or at least SCI11, we have the following kernel functions:

(StrCmp strA strB) Compares strA to strB until a null in strA or a mismatch. Returns 0 if the two strings match, something lower than zero if the first mismatch is lower in strA, something higher if it’s in strB.
(StrCmp strA strB maxLen) Same as (StrCmp strA strB), but only up to the first maxLen characters.
(StrLen str) Returns the number of characters in str.
(StrCpy strDest strSrc) Copies characters from strSrc into strDest, up to and including the null terminator. It’s up to you to ensure it fits.
(StrCpy strDest strSrc maxLen) If maxLen is positive, copies characters from strSrc to strDest up to and including the null terminator or up to maxLen characters. A terminator is ensured. If maxLen is negative, simply copies that many characters and damn the terminators.
(StrEnd str) Returns a pointer to the end of str. Effectively, str += strlen(str);.
(StrCat strA strB) Appends strB at the end of strA. It’s up to you to ensure this fits.
(StrAt str pos) Returns the character at pos in str.
(StrAt str pos newChar) Same as (StrAt str pos), but places newChar at pos, returning what was there.
(Format strDest format args...) Takes the format string and all the args, and prints it all to strDest. The format and any args for an %s placeholder can also be far text pairs.
(ReadNumber str) Tries to parse str as a string of digits and returns their value.

That’s a fair amount. It’s nice to have StrAt when you consider all numbers are inherently 16 bits wide and as such you can’t just manually work your way around a string. We’ve seen it around in hash calculations and dropcaps.

As an aside, the Format entry mentions far text pairs. Those refer to text resources, where instead of doing something like (Display "Hello World!") you’d do something like (Display 100 4) and have a text resource #100, where line #4 is “Hello World!”. This allows for more efficient memory use and ease of translation. In SCI0, you could only have up to 1000 resources of each type, from 0 to 999, while a script’s internal strings would be referenced with pointers that are always higher than 1000. This allows both the interpreter and scripts to tell the difference, fetching the actual string when called for. In the original SC compiler, there were in fact two ways to write strings. You could use "double quotes" as usual, or {curly braces}. One of these would be left as “near” strings in the script resource, the other would be automagically compiled into the script’s matching text resource as “far” strings. Neither SCI Companion nor Studio support this, and you can write any string in either style. I personally prefer the quotes.

Now, in SCI2 and later most of these separate kernel calls were consolidated into a single one with a bunch of subcommands, String. A few of these are wrappers around the Array kernel call, considering SCI2 strings are implemented as arrays of type string, but there are plenty proper string functions. Any function that may resize the string returns its new address.

(String StrNew size) Creates a new string data block (array of type String) of the given size.
(String StrSize str) Returns the size of the string.
(String StrAt str pos) Returns the character at pos in the string, or zero if it’s not that long.
(String StrAtPut str pos newChar) Sets the character at pos in the string, resizing it if it’s not that long.
(String StrFree str) Deallocates the string data block’s memory space.
(String StrFill str startPos length fillVal) Sets a whole range in the string to the given fillVal, resizing if needed.
(String StrCpy strDest destPos strSrc srcPos len) Copies a chunk of characters from strSrc to strDest, resizing if needed.
(String StrCmp strA strB) Compares strA and strB, as in SCI11.
(String StrCmp strA strB maxLen) Compares strA and strB up to maxLen, as in SCI11.
(String StrDup str) Duplicates the string block and returns the address of the duplicate.
(String StrGetData str) Returns a pointer to the string’s actual data.
(String StrLen str) Returns the length of the string’s actual data, up to the null terminator, as opposed to its containing array’s capacity.
(String StrFormat format args...) Takes the format and all args, printing it all to a new string, then returns the address of that new string.
(String StrFormatAt strDest format args...) Same as StrFormat but you provide an existing string to format to.
(String StrToInt str) Tries to parse str as a string of digits and returns their value.
(String StrTrim str flags) Removes whitespace from str. If flags is 1, all whitespace at the end is removed. If it’s 4, all whitespace at the front is removed. If it’s 2, everything inbetween is removed. These can be combined.
(String StrTrim str flags notThis) Same, but doesn’t consider notThis to be whitespace.
(String StrUpr str) Converts the string to uppercase.
(String StrLwr str) Converts the string to lowercase.
(String StrTrn strSrc strSrcPat strDestPat strDest) I honestly haven’t a clue. I never understood this one.

Now consider the following: these are all one and the same kernel call, and they include some functions that aren’t in the 16-bit interpreters such as case-folding and trimming. Wouldn’t it be nice? They don’t even have to be based on arrays, even if that’s a feature I’ve been working on backporting to SCI11+.

[ ] Leave a Comment

SCI01/1 Multilanguage games and telephone country codes

What?

Yeah. Telephone country codes. Those SCI games that let you switch between two languages without exiting? They used telephone country codes internally.

(instance sq4 of Game
  (properties
    parseLang 0
    printLang 0
  )
  ;...
)
 
(procedure (byLang german spanish french italian other)
  (switch (gGame printLang?)
    (49 german)
    (34 spanish)
    (33 french)
    (39 italian)
    (else other)
  )
)

Unfortunately, I don’t have the source code for an SCI interpreter that has the string splitting function needed — it only has the telephone number codes. So I’ll go with what ScummVM does.

Given a call to StrSplit with a parameter like You have an empty jar.#FVous avez un vase vide., the current printLang is matched with a separator character. In this case, if it’s zero we cut off and return the left part of the string. If it’s nonzero (say it’s 33), it’s matched to F for French. Looking for the split marker #, we then look at the next character and see if it’s our request. If it is, if we found a #F, we return the right part of the string. But what if we don’t find the right language? Let’s say for example I took the “see ya on the chronostream” message in the French version of SQ4 and made a Dutch secondary line?

What happens is, the interpreter gives up. ScummVM or the original, they just return the whole string.

But then of course, Dutch isn’t a supported language at all. The interpreter only recognizes the country codes for English, Japanese, German, French, Spanish, Italian, and Portuguese. And two of those aren’t supported by the game script I started this post off with. Surely if I gave the French SQ4 a German line it’d react differently?

Well, yeah. If the split marker is for a language the interpreter can recognize, it just returns the left part.

(Bonus: For no good reason beyond a little harmless pride in my country, I added the number for Dutch to the list in SCI11+. Which is stupid. It has no StrSplit function… but it could get one. Which would be stupid because we have patchDir support and can use it to just switch languages externally.)

… but why telephone numbers though?

Answer: numbers are easy to work with I guess.

[ , ] Leave a Comment

Combining PQ2 and KQ4… in SCI11

If you were so inclined, you can easily take that mashup I just posted and convert it to SCI11.

Just take the doit method from earlier and replace the two Print calls:

(Print
  addText:
    "TO: Detective Bonds\nFROM: Captain Hall\nSUBJECT: ID of evidence photo\n\nPlease provide the LAST name of the person pictured in the attached evidence photo for homicide case 186751.\n\nPlease respond in box below, ASAP!\n"
    40 0
  addIcon: 923 2 myPick 0 0
  addEdit: @yourAnswer 20 40 100
  init:
)

and

(Prints "Sorry Bonds, you'll need to do better than that!")

The main difference is that you’ll have to provide your own coordinates. You can tell that the ones I put are very rough. I mean to port the SCI0 Print procedure to SCI11 as ClassicPrint some day. Don’t be fooled — Prints is merely a simple wrapper procedure:

(procedure (Prints)
  (Print addText: &rest init:)
)
[ , ] Leave a Comment

Combining PQ2 and KQ4’s copy protection scripts

I said I would, right?

(local
  ; Correct answers' hashes, in original order.
  ; Determined by https://helmet.kafuka.org/sci/kq4_cp.html
  [answers 8] = [666 393 526 377 365 453 383 441]
)
 
(instance CopyProtection of Room
  (method (doit &tmp i ch hash myPick [yourAnswer 40])
 
    ; Just like in PQ2, we grab the current time, then mask out
    ; the lower bits to limit the range to a number from 0 to 7.
    (= myPick (& (GetTime gtTIME_OF_DAY) 7))
 
    ; Clear out the first character of our answer to effectively
    ; make it blank.
    (= yourAnswer 0)
 
    ; Request our input as before...
    (Print
      "TO: Detective Bonds\nFROM: Captain Hall\nSUBJECT: ID of evidence photo\n\nPlease provide the LAST name of the person pictured in the attached evidence photo for homicide case 186751.\n\nPlease respond in box below, ASAP!\n"
      #icon 701 0 myPick
      #edit @yourAnswer 20
    )
 
    ; Now we use some trickery from KQ4, but different.
    (= hash 0)
    (= i 0)
    ; While the character at position i is nonzero...
    (while (= ch (StrAt @yourAnswer i))
      ; Anything between 'a' and 'z' gets turned to uppercase.
      ; We don't bother putting it *back* in yourAnswer though.
      (if (and (>= ch 97) (<= ch 122)) (= ch (- ch 32)))
 
      ; Add this value to our running total.
      (= hash (+ hash ch))
      (++ i)
    )
 
    ; Either the hash we calculated is the correct one, or
    ; we entered "bobalu".
    (if (or
          (== hash [answers myPick])
          (== hash 437)
        )
      (gRoom newRoom: 1) ; or wherever your game starts.
    else
      (Print "Sorry Bonds, you'll need to do better than that!")
      (= gQuitGame true)
    )
  )
)

And presto! I’d talk about some of the other games’ copy protection schemes but for example KQ5’s doesn’t pass the decompiler. Probably because of a difficulty involving endless loops. Still, feel free to suggest something.

[ , , ] Leave a Comment

Sorry, Bonds – Police Quest 2 Copy Protection

Last for now in the set on copy protection is Police Quest 2. I might go into some others, I dunno, and I have something planned where I optimize the hell out of the PQ2 copy protection script by means of KQ4. But let’s get down to it.

(local
  [yourAnswer 40]
)
 
(procedure (ToUpper &tmp i ch)
  (= i 0)
  (while (= ch (StrAt @yourAnswer i))
    ; If ch is between 'a' and 'z'...
    (if (and (>= ch 97) (<= ch 122))
      ; ...change it to uppercase.
      (StrAt @yourAnswer i (- ch 32))
    )
    (++ i)
  )
)
 
(instance rm701 of Rm
  (method (doit &tmp myPick)
    (= myPick (& (GetTime gtTIME_OF_DAY) 7))
    (= yourAnswer 0)
 
    (Print "TO: Detective Bonds\n
            FROM: Captain Hall\n
            SUBJECT: ID of evidence photo\n
            \n
            Please provide the LAST name of the person pictured in
            the attached evidence photo for homicide case 186751.\n
            \n
            Please respond in box below, ASAP!\n"
            #icon 701 0 myPick
            #edit @yourAnswer 20
    )
 
    (= gQuit true)
    (ToUpper)
 
    ; Like in C, StrCmp returns zero if the strings are the same.
    ; Zero is false, so we use a not to make equal be true.
    (switch myPick
      (0    (if (not (StrCmp @yourAnswer "GRANANDEZ")) (= gQuit false)))
      (1    (if (not (StrCmp @yourAnswer "SIMMS"))     (= gQuit false)))
      (2    (if (not (StrCmp @yourAnswer "TASELLI"))   (= gQuit false)))
      (3    (if (not (StrCmp @yourAnswer "COLBY"))     (= gQuit false)))
      (4    (if (not (StrCmp @yourAnswer "BAINS"))     (= gQuit false)))
      (5    (if (not (StrCmp @yourAnswer "SNIDER"))    (= gQuit false)))
      (6    (if (not (StrCmp @yourAnswer "JONES"))     (= gQuit false)))
      (else (if (not (StrCmp @yourAnswer "DICKEY"))    (= gQuit false)))
    )
    (if gQuit
      (Print "Sorry Bonds, you'll need to do better than that!")
      ; With gQuit set, we'll exit at the end of this doit cycle.
    else
      (gGame restart:)
      ; The main game object can tell if we're restarting, so it'll
      ; put us in the first playable scene instead of the title screen.
    )
  )
)

Gee, I can think of a way to improve this already. Let’s turn yourAnswer from a local to a temp, and inline ToUpper:

(method (doit &tmp myPick i ch [yourAnswer 40])
  ; ...
  (= gQuit true)
 
  (= i 0)
  (while (= ch (StrAt @yourAnswer i))
    (if (and (>= ch 97) (<= ch 122))
      (StrAt @yourAnswer i (- ch 32))
    )
    (++ i)
  )
  ; ...
)

And that’s just one improvement. It really helps that, unlike the subtitle typing in Larry 5, we only uppercase one thing once. Tune in next time to see what could be done.

[ , , , ] 2 Comments on Sorry, Bonds – Police Quest 2 Copy Protection

How Meta

Or “How Various Parts of This Site Hold Up in the Past”.

Specifically, how do they hold up in NCSA Mosaic 2.1.1, Netscape Navigator 4.04, Internet Explorer 2 and 5, Opera 3.20, and Opera 10? All but IE2 run on a Windows 98 virtual machine, while IE2 runs directly on my actual Windows 7 installation. Why? Because it can.

Mosaic, Netscape, and Opera 3.20 are the earliest versions I could find that deigned to run. IE2 is something I jokingly copied off an NT ISO, while IE5 came with the Win98 VM if I remember correctly. Opera 10 is the latest version that runs on Win98, and even then I needed KernelEx.

First part I’ll test is the webpage for The Dating Pool, seen here. 23 requests totaling 212 kilobytes. As a retro page that should by all rights make whoever did the Captain Marvel promo page resign in shame, you’d expect good results. And indeed:

Every single one of them renders it adequately well, with no missing parts.

Next up is the index page for the local copy of all my Ranma ½ fanfics, seen here. Six requests, 33 kilobytes. This too is very much a retro page so I have high hopes.

Everything is awesome. But now we get a little crazy. We open this very blog. 31 requests, 302 kilobytes. A blog that’s UTF-8 encoded and is full of CSS, Javascript, and (*gasp*) PNG. There’s no way this can go right.

…About as I’d expected. Mosaic didn’t know what to do with the page’s content type and crashed in the attempt. Netscape 4 already had PNG support so that’s nice but no styling at all and a fair bit of JS errors to dismiss. IE2 doesn’t know what a CSS is, nor a PNG if you were to scroll down. IE5 manages nicely, putting the sidebar on the bottom as you would expect from a floating element in a broken box model but also doesn’t do any scrolling — I had to select and drag to check the rest! Opera 3.20 is passably readable, not fit to figure out UTF-8 nor PNG. If I’d gone with 3.5 it’d probably look incrementally better with its new CSS support. Opera 10 does it best being the most modern browser on the VM.


(via @plasmarob)

[ ] Leave a Comment

Little Black Book – Larry 2 Copy Protection

It’s been a while now since I last explained a Sierra game’s copy protection, so why not go at it again?

Compared to KQ4, LSL2 has only a few different options, and it involves no math.

(local
  phoneNum     ; The correct answer.
  [input 6]    ; Our guess. 11 characters max, but 16-bit values so 12/2=6
  ; The photo parts are put in hunk space instead of heap or something.
  dressView
  faceView
  hairView
  earringView
)
 
(instance rm10 of Rm
  (properties
    picture 10
    style dpOPEN_CHECKBOARD
  )
 
  (method (init &tmp dress face hair earring)
    (ProgramControl)
 
    ; Preload the photo and logo parts
    (Load rsVIEW 60)
 
    (super init:)
 
    ; For each part of the logo, create a new view in hunk space, and addToPic it.
    ; Adding a view to the pic causes it to be disposed, so in the end none of the logo parts take space.
    ((View new:) view: 60 loop: 4 cel: 0 posn:  72  52 setPri: 1 addToPic:) ; Logo
    ((View new:) view: 60 loop: 4 cel: 1 posn: 122  36 setPri: 0 addToPic:) ; S
    ((View new:) view: 60 loop: 4 cel: 2 posn: 141  35 setPri: 0 addToPic:) ; I
    ((View new:) view: 60 loop: 4 cel: 3 posn: 161  35 setPri: 0 addToPic:) ; E
    ((View new:) view: 60 loop: 4 cel: 4 posn: 190  35 setPri: 0 addToPic:) ; R
    ((View new:) view: 60 loop: 4 cel: 4 posn: 221  35 setPri: 0 addToPic:) ; R
    ((View new:) view: 60 loop: 4 cel: 5 posn: 252  35 setPri: 0 addToPic:) ; A
    ((View new:) view: 60 loop: 5 cel: 0 posn:  13 113 setPri: 7 addToPic:) ; Left girl
    ((View new:) view: 60 loop: 6 cel: 0 posn: 306 113 setPri: 7 addToPic:) ; Right girl
 
    ; Now create views for the photo parts.
    ((= dressView   (View new:)) view: 60 loop: 0 cel: 0 setPri: 1 posn: 154 981 init:)
    ((= faceView    (View new:)) view: 60 loop: 1 cel: 5 setPri: 2 posn: 154 981 init:)
    ((= hairView    (View new:)) view: 60 loop: 2 cel: 1 setPri: 3 posn: 154 981 init:)
    ((= earringView (View new:)) view: 60 loop: 3 cel: 4 setPri: 4 posn: 154 981 init:)
 
    ; I left out most of the answers in the interest of fairplay and conciseness.
    (switch (Random 1 16)
      (1 (= dress 0) (= face 5) (= hair 1) (= earring 4) (= phoneNum "555-7448"))
      (2 (= dress 1) (= face 5) (= hair 2) (= earring 4) (= phoneNum "555-5968"))
      ; ...
      (15 (= dress 2) (= face 4) (= hair 0) (= earring 2) (= phoneNum "555-5633"))
      (16 (= dress 1) (= face 1) (= hair 3) (= earring 2) (= phoneNum "555-5834"))
    )
 
    ; Now apply the traits chosen above.
    (dressView   posn: 154 81 setLoop: 0 setCel: dress)
    (faceView    posn: 154 81 setLoop: 1 setCel: face)
    (hairView    posn: 154 81 setLoop: 2 setCel: hair)
    (earringView posn: 154 81 setLoop: 3 setCel: earring)
 
    (Animate)
    (Display "© 1988 by Sierra On-Line, Inc." dsCOORD 60 176 dsCOLOR 1 dsBACKGROUND 3)
  )
 
  (method (doit)
    (Format @input "555-")
    (Print
      "Please find this girl's picture in your little black book, then type her telephone number here:"
      #at -1 144
      #width 248
      #font gFont
      #edit @input 11 ; add a textbox, max length 11.
    )
 
    ; Reminder: like in C, StrCmp returns zero if the inputs are equal, and zero is false. Hence the not.
    (if (not (StrCmp @input phoneNum))
      (gRoom newRoom: 90)
    else
      (Print "Sorry, but you need to spend more time staring at beautiful women!
              In order to play this game, you must have the original documentation.
              If you've lost your little black book, please telephone Sierra's
              Customer Support Department at the number printed on your disks.")
      (= gQuit true)
    )
  )
)

I can think of a few ways to improve this, and in fact I already did in some places — this is not the original LSL2 copy protection! Can you think of more ways to improve this script?

[ , , , ] Leave a Comment

What do SQ5 and LSL5 have in common?

Things being typed out in their introduction.

In Space Quest 5, the introduction has a sequence where Roger Wilco dictates a captain’s log. The text appears word-for-word, though it’s pretty easy to modify the script code and make it character-by-character. But before I can explain the intricacies of that scene, I must ensure you know how a Scriptworks.

No, not a script file, a Script object. They are in essence state machines, with a state property, a changeState method, a cue method, and two timer properties, cycles and seconds. The changeState method is the beef of it all, containing the code for each state. The cue method simply takes the current state, adds one, then invokes changeState with that value. The doit method checks if either of the two timers is nonzero, decrements them accordingly, and invokes cue when time runs out. In general, you only need to implement your own changeState. Other objects may also cue a script. A good example would be waiting for an Actor to move to a particular position. They would know which Script is waiting for them and cue them at the appropriate time. Until then, the Script sits on its thumbs.

Knowing that, let’s take a look at the captain’s log:

; Earlier in the file...
(local
  [logInput 200]
  [logWritten 200]
  ; Remember: these are 16-bit values, so 400 characters.
  page
  length
  cursor
)
 
(instance sCaptainsLog of Script
  (method (changeState newState)
    (switch (= state newState)
      (0
        (Message msgGET 104 1 0 0 page @logInput) ; Grab the current page worth of text.
        (= length (StrLen @logInput)) ; Remember how long the line is.
        (= cursor 0) ; Reset our typing cursor
        (= cycles 1) ; Near-immediately continue -- we could self-cue though?
      )
      (1
        (repeat
          ; Copy one character...
          (StrAt @logWritten cursor (StrAt @logInput cursor))
          ; ...and add a terminating zero so we don't write garbage.
          (StrAt @logWritten (++ cursor) 0)
 
          ; Stop copying if we find...
          (if
            (or
              (== (StrAt @logInput (- cursor 1)) 32) ; a space...
              (== (StrAt @logInput (- cursor 1)) 0) ; or the end of the text.
            )
            (break)
          )
        )
 
        ; Put what we've copied so far on screen.
        (Display @logWritten dsCOORD 40 10 dsCOLOR 254 dsFONT 1307 dsWIDTH 250)
 
        ; If we haven't reached the end of the text yet...
        (if (> length cursor)
          (-- state) ; rewind to state 0 so the next cue is for state 1 again!
          (= ticks 15)
        )
      )
      (2
        ; The rest doesn't matter here.
      )
    )
  )
)

If one wished to write character-by-character, you’d just remove most of the repeat:

(StrAt @logWritten cursor (StrAt @logInput cursor))
(StrAt @logWritten (++ cursor) 0)
 
(Display @logWritten dsCOORD 40 10 dsCOLOR 254 dsFONT 1307 dsWIDTH 250)
 
(if (> length cursor)
  (-- state)
  (= ticks 15)
)

That was pretty easy, all things considered. I think the more interesting example to study would be the subtitle in Leisure Suit Larry 5 – Passionate Patti does Pittsburgh a Little Undercover Work.

First, we need a few local variables and, just to keep things orderly, a procedure:

(local
  cursor
  typePos
  [subtitle 50] = "Passionate Patti does PittsbuA Little Undercover Work"
  ; Notice this: "Pittsbu" is character 22 to 29.
  [backspaces 12]
)
(procedure (typeAway &tmp [oneChar 2] char underbits)
  ; Grab the current character to type.
  (= char (StrAt @subtitle cursor))
  ; Put it in a string -- just that character and a terminating null.
  (Format @oneChar "%c" char)
 
  ; Draw it, preserving what was underneath.
  (= underbits (Display @oneChar dsCOORD typePos 160 dsCOLOR global128 dsWIDTH 7 dsALIGN alLEFT dsFONT global173 dsSAVEPIXELS))
 
  (if (and (< 21 cursor) (< cursor 29)) ; Are we typing "Pittsburgh"?
    (= [backspaces (- cursor 22)] underbits) ; Then put those underbits pointers in the backspaces array.
  else
    (UnLoad 133 underbits) ; No need for these underbits, throw 'em away.
  )
 
  (= typePos (+ typePos 7)) ; Advance our drawing cursor...
  (if (== 32 char) (= typePos (- typePos 2))) ; ...but make the space a little bit thinner.
)

Further down there’s a Script that handles the entire opening cartoon. I’ll skip the boilerplate and only show the relevant parts.

(14
  (patti setMotion: MoveTo 335 140) ; This all happens while Patti walks past.
  (= cursor 0) ; Reset our type.
  (= typePos 3)
  (larry setScript: sLarryCartoon) ; Larry reacts on his own.
  (= cycles 1)
)
(15
  (typeAway) ; Type ONE character.
  (typewriter play:) ; Make some noise...
 
  (if (< (++ cursor) 29) ; Increment the cursor -- if it's not at the 'u' in "Pittsbu" yet...
    (= cycles (Random 3 5)) ; ...randomly delay the next character...
    (-- state) ; ...and rewind as shown in SQ5.
  else
    (= cycles 10) ; We've reached the 'u'. Wait and cue up state 16.
  )
)
(16
  ; Grab the right underbits from our array and restore it.
  (Display "" dsRESTORE [backspaces (- (-- cursor) 22)])
  ; Underbits include not only pixel data but also their bounds, so that's easy.
 
  (backSpace play:)
  (if (> cursor 22) ; Are we not yet back at the space before "Pittsbu"?
    (= cycles 4) ; Then rewind at four cycles a backspace.
    (-- state)
  else
    (= typePos 153) ; Reset to where the 'P' was...
    (= cursor 29) ; ...and skip to the 'A' in "A Little"
    (= cycles 10)
  )
)
(17
  ; The rest is much the same as state 15...
  (typeAway)
  (typewriter play:)
 
  ; ...except for this check.
  (if (< (++ cursor) (StrLen @subtitle))
    (-- state)
    (= cycles (Random 3 5))
  else
    (= cycles 10)
  )
)
[ , , , ] Leave a Comment

Fanfics? Sure!

Letrune suggested via the new suggestion box that I could blog about fanfics. Well, I got a few Ranma ½ stories I guess? Let me just put up a page to quickly list and link them… there we go!

Anyway, the chronologically first one would be Ranmya (available on Archive of our Own and locally), where our hero is born and raised a girl, gets cursed to be a cat, then immediate and unwisely turns and tries to cure herself, resulting in a mixed curse. And then she hooks up with Nabiki.

I’d been sitting on the first version of this story, then unnamed, for several years before finally continuing, then rewriting the first bits for style points. Ranmya is relatively heavy on the sex compared to the others, and has a fair bit of Weird Shit™ going on especially in the dream chapter which features some lines in the conlang I developed for the Firrhna Project. Why? Because I can.

 

Next in line is Good and Pure (available on AO3 and locally), which was inspired by a severe lack of Ranma/Kasumi fics. It starts out pretty much in a canon-compatible situation, with Ranma engaged to Akane, but quickly derails into a Ransumi story. Kasumi is a precious cinnamon bun, too good, too pure for this world.

The thing about pure things is that its all the sweeter to corrupt them.

 

There’s a one-shot about Shampoo too, I’m a Kittycat (AO3, local), where the important change is that Shampoo is not a human who turns into a cat, but the other way around. During the competition, her instincts flare up and make her mess up the Kiss of Death.

 

My most recent work is yet another damn Ranma AU. I’m sorry, I should’ve title-cased and italicized that. (AO3, local) The setting is easy enough to explain: there’s no turning back. Once you’re cursed and take on another form, that’s it. This story features Rankane shipping, a defiant lesbian tomboy and her equally defiant pet cat, no pandas, and when I get off my lazy ass and write, a very confused Ryōga.

[ , ] Leave a Comment

Unusual arrow keys and cursor behavior

Most players of SCI games would naturally gravitate to using the mouse if it’s an SCI1 game or later. With SCI0 using a text parser and no pathfinding it strikes me personally as more efficient to just use the arrow keys in those games. But that’s just me. The important part here is that some SCI11 games don’t respond to the arrow keys the same way as others do.

SCI0 being effectively stateless in a way, pressing the arrow keys will cause your character to start walking in that direction. Clicking somewhere causes them to walk there in a straight line until something gets in the way. In SCI1 the icon bar introduced a kind of state in that you have several active cursors. In all but a few adventure games, pressing the arrow keys will cause your character to move if the current cursor is Walk. Otherwise, the cursor itself moves around. So if the current cursor is, let’s say, Look, the little eye moves instead of the main character. Unless of course you’d already set them on their way. And of course (unless you’re playing the original KQ5) clicking somewhere with the Walk cursor makes your character find their way around obstacles towards that point.

Two exceptions to this rule that I know of are Space Quest 5 and King’s Quest 6. It turns out if you press the arrow keys with the Walk cursor out, the cursor itself moves. Why is that?

Obviously, there’s something in the game scripts that causes both the usual and specific behavior. Let’s look at both.

The usual behavior is enabled by the Ego class and its handleEvent method. Called from User, this method checks for direction and walk events. If it’s the former, an arrow key was pressed or a joystick was nudged. If it’s the latter, we clicked the Walk cursor somewhere. How do we know it’s Walk? Because User only calls this method if it was. Any other cursor, it runs further down and ends up calling another script that handles moving the cursor around if you press an arrow key.

(method (handleEvent pEvent &tmp eType dir)
  (= eType (pEvent type?))
  (cond
    ; First, see if we have a script that wants the event.
    ((and script (script handleEvent: pEvent)) true)
 
    ; See if it's a direction event
    ((& eType evJOYSTICK)
      (= dir (pEvent message?))
 
      ; don't claim dirStop/CENTER
      (if (and (== dir CENTER) (& eType evKEYBOARD))
        (return (pEvent claimed?))
      )
 
      ; Pressing the same key that started a motion again should stop
      (if (and (& eType evKEYBOARD) ; it's a key
               (== dir (gUser prevDir?)) ; same dir as before
               (IsObject mover)) ; ego is moving
        (= dir CENTER)
      )
 
      ; In the case of a keyDown event, keep the previous direction,
      ; so we know what key stops ego.
      (gUser prevDir: dir)
 
      ; Actor::setDirection sets ego off to walk.
      (self setDirection: dir)
      (pEvent claimed: true)
    )
 
    ((& eType evVERB)
      (if (& eType evMOVE)
        (switch gEgoUseObstacles
          (0 (self setMotion: MoveTo (pEvent x?) (+ (pEvent y?) z)))
          (1 (self setMotion: PolyPath (pEvent x?) (+ (pEvent y?) z)))
          (2 (self setMotion: PolyPath (pEvent x?) (+ (pEvent y?) z) null false))
        )
        (gUser prevDir: 0)
        (pEvent claimed: true)
      else
        (super handleEvent: pEvent)
      )
    )
    (else (super handleEvent: pEvent))
  )
  (return (pEvent claimed?))
)

You might notice the bit about gEgoUseObstacles.  Setting that to zero effectively makes clicks act like in SCI0. At least to the player — the script code is somewhat different.

Now that just leaves the question of why these two games don’t react like that. And it’s really very simple. KQ6 and SQ5 do it each in their own slightly different way, but the basic trick is the same. Because KQ6 seems a bit neater to me, I’ll show that one first:

(instance ego of Ego
  ; Irrelevancies elided
 
  (method (handleEvent pEvent)
    (return
      (if (& (pEvent type?) evJOYSTICK)
        ; Refuse to handle any directional inputs
        (return false)
      else
        ; Let the original Ego class figure it out
        (super handleEvent: pEvent &rest)
      )
    )
  )
)

Simple, huh? Because this returns false for any direction events, User will always end up percolating the event down to PseudoMouse and as such the Walk cursor is moved by the keyboard or joystick instead of the player character. Now let’s see how this compares to Space Quest 5.

(class SQEgo of Ego
  ; Irrelevancies elided
 
  (method (handleEvent pEvent &tmp eType eMsg)
    (= eType (pEvent type?))
    (= eMsg (pEvent message?)) ; woo wasted effort!
    (return
      (cond 
        ((and script (script handleEvent: pEvent)) true)
        ((& eType evJOYSTICK) (return false))
        (else (super handleEvent: pEvent &rest))
      )
    )
  )
)

Seems pretty clear considering what we already know. So much so I won’t bother documenting it much.

The SCI Companion SCI11 template game is based on SQ5 so by default this too applies to any game you make with it. The obvious way to remove this then is to remove GameEgo::handleEvent. I did, and I haven’t looked back since.

[ , ] Leave a Comment