Logo Pending

“Interesting. I wonder if this is related to the “BOTH” button that got cut in SQ4.” — @ATMcashpoint

I don’t know about the both button that ScummVM adds to some SCI games, but there’s quite literally no way it could work by just adding a third button state. There’s a fair bit of script logic that’d need to be overhauled. Here’s why that is, and here’s how I did it in The Dating Pool.

I could have used the SCI Companion template game to compare against and document, but to be honest it’s a bit of a mess, as you could expect from a decompilation. The leaked system scripts are much neater to work with, even though the actual script code is basically identical.

; Exactly the same as in the template BUT...(if(!= aTalker -1)(talkerSet add: aTalker); Pass both the buffer AND the tuple, no matter our settings.; That does mean that msgkey may be null, but say won't use it in; that case anyway.(aTalker
modNum: theMod
say: @theBuf msgkey
)(++ curSequence))

In SQ4CD, the Narrator has extra noun, verb, and sequence properties that get set to allow the text to work. It’s really quite a bit of a mess, and my hat’s off to whoever on the ScummVM team got that Both mode to work. I was going to document it but got lost trying, it’s that wild.

On to the Narrator and by extension Talker!

Original Talker.sc:

(method(say theBuf whoCares)(if theIconBar (theIconBar disable:))(if(not initialized)(selfinit:))(= caller
(if(and(> argc 1) whoCares)
whoCares
else
null
)); Figure out what to do with the message.; Note that in one case, theBuf is a string...(if(& gMessageType TEXT_MSG); (method (startText theBuf &tmp strLength)(selfstartText: theBuf)); ...but in the other it's a tuple!(if(& gMessageType CD_MSG); (method (startAudio theKeys &tmp m n v c s)(selfstartAudio: theBuf)); cut a bit...; start___ will have set ticks to the length; of the string or recording. We add one more; second regardless.(= ticks (+ ticks 60 gameTime))(return true))

Now, this works fine. If I record a quick bit of gibberish, then load up the game, switch to Both, and click, I get a perfectly readable message and hear my gibber. But if I were to revert my little change and use the original code…

That’s what we in the business call mistaking a bunch of numbers for a valid string. I specifically get this result because the first value in the tuple is the module number, which is 110 (0x6E ‘n‘) in this case, and all numbers in SCI are 16-bit so there’s a terminating null right after.

What’s funny is that after all this, I can’t see how SQ4 is supposed to support Both mode, and ScummVM only needs to add that third button state. There is no patch to adjust the script, and I can’t for the life of me figure out how this would work:

The weird part is that I can’t find anywhere those properties are set.

…At least with the KQ6/LB2 patches they actually do overhaul quite a bit of the scripts’ logic, which are otherwise just the same system scripts as above. Not the way I did it for my game, but clearly in a way that works out.

It’s basically true, but there are some interesting details about AGI’s priority screen. For starters, it’s also the control screen.

Any color over a particular number is considered priority, while the lowest few are control. Thus, black is blocking, green is trigger, and blue is water. But if the control lines are drawn on top of the priority info, how do you not get unsightly gaps? If Gwydion were standing behind that table, wouldn’t you see his legs through that black gap? Turns out no, you wouldn’t. For lack of AGI source, here’s a part of ScummVM:

In plain English, that means that when determining the priority of a given background pixel, if that pixel is a control color, you scan down to the next valid color:

But wait, this introduces errors! There are gaps in the seat and wall! And you know what? This works out fine because you can’t actually get to those points and be standing on a lower priority band. It’s all sneaky design in the end.

In SCI0, the control screen was split off from the priority screen. Black became the default value, white meant blocking, and all the others meant whatever the room programmers wanted them to mean. In a room with water, blue was the obvious choice but in a dry room blue might as well be a trigger. If something wasn’t a trigger, it was the hotspot for non-squarish background features.

In SCI1, vectored visual screens were deprecated. Instead, the background was basically a single Draw Bitmap command, followed by vector commands tracing the priority and control screens.

In SCI11, the control screen was deprecated — it was still available, but hardly used if at all. Walkable areas were now denoted with polygons, as were feature hotspots. Trigger areas were either polygons or IsInRect checks, but the priority screen worked the same as always. Priority screens were still vector traces, though.

It wasn’t until SCI2 that the system would radically change again, dropping the control screen and vectors altogether. Instead, the priority screen would be drawn at the same time as the visual screen: piece by piece.

Quite a difference in technique. They’re not even limited to four bits anymore — these are signed word priorities!

Drawing order that is. How does SCI know which bits of a character or whatever go behind which pieces of the background? It’s quite ingenious really.

You take your background image, first. Ignore the lonely king in the middle there, he’s not important right now.

Divide the screen up into fifteen bands. We use the standard CGA colors by convention and I left out black for a little bit of clarity. I didn’t leave out white — that’s the nearest you can be, in front of everything. Note that each screen can set their own thickness for each individual band. Given this information, we can draw a priority screen.

When View objects are drawn, such as Mr. Built-Like-A-Quarterback up there, they are first sorted by their Y coordinate, from furthest to the north to closest to the south. This implicitly places them on given priority bands. Graham for example is right on the edge of the dark gray band, priority 8. That way, when he’s being drawn, the engine can tell what part of the scenery is in front of him and skip those pixels simply by comparing his priority with that of the priority screen, kinda like—

Hey! Get back here!

As you can see, because basically all of the light colors but gray rank higher than dark gray, much of the view isn’t drawn.

If two Views stand on the same priority band, there’s still no problem — they’re drawn in Y order. This has been the case all the way since AGI. SCI2 and later build their priority screens a little differently, but that’s about as much of a technicality as the difference between AGI and SCI0, in that the specific implementation differs, and quite a lot, but the basic technique stays the same.

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+.

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.)

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:

(local
; Correct answers' hashes, in original order.; Determined by https://helmet.kafuka.org/sci/kq4_cp.html[answers 8] = [666393526377365453383441])(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 7010 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.

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.

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.