I had so much fun in my last post simplifying The Colonel’s Bequest that I thought I would do somthing similar for my other SCI games.

As a reminder: I don’t really want to bypass the copy protection. I like seeing it; it’s a nostalgia thing. I just don’t want to have to get the answer correct to get to the other side of it.

The patch I produced is at the bottom of this post.

Getting Started

As before, I used the excellent SCI Companion software, rather than write my own decompiler–though I will probably do that one day for fun.

To start, I decompiled all the game scripts, and searched through the text for hints as to were the copy protection scheme was. In this case, none of the text resources were helpful, because the script had all the strings baked in. Fortunately, the script had a very obvious name: copyProtect. It’s room 701.

script list

The Protection Scheme

The copy protection scheme is set up as follows:

(switch (= local0 (Random 1 79))
  (1 (= local2 431))
     ... elided the rest ...
)
(switch local0
  (1
    (= local1
      {On page 2, what is the fourth word of the first sentence?}
    )
  )
      ... elided the rest ...
)

So, local0 has the random question’s id, local2 has a hash of the correct answer, and local1 has the question string. Let’s see how they are used…

First, it calls a procedure to put up the dialog box, and place the user input into an array local3:

(proc255_0
  (Format @global300 701 0 local1)
  134
  7
  15
  66
  global23
  140
  290
  82
  @local3
  30
)

Then, as an interesting aside: if global215 is set, the user can just type a room number to jump to. At least, that’s what it looks like to me. Debugging aid, I guess:

(if
  (and global215 (= local33 (ReadNumber @local3)))
    (TheMenuBar draw:)
    (SL enable:)
    (self newRoom: local33)
    (return)
)

Next, we get to the hashing section. It looks like kernel_102 does the heavy lifting, and the hash is accumulated in local35:

(= local33 0)
(while (< local33 (StrLen @local3))
  (= local34
    (& (= local34 (kernel_102 @local3 local33)) $005f)
  )
  (kernel_102 @local3 local33 local34)
  (= local35 (+ local35 local34))
  (++ local33)
)

That might be of interest, but I’m hoping I can just bypass the check itself rather than manipulating the hash. Here’s the check, which holds a fun surprise:

(cond 
  ((not (StrCmp @local3 {BOBALU})) (global2 newRoom: 700))
  ((== local35 local2) (global2 newRoom: 700))
  (else (proc255_0 701 1) (= global4 1))
)

Do you see the BOBALU string constant? It looks like that just lets you through regardless of the hash. I tried it, and it works! I’m sure people knew about this, but it was news to me. Ok, so then we check the hash in the second line. The third line is the failure case.

Perhaps, let’s just change this entire cond expression into the code for (global2 newRoom: 700). No check at all against what I typed, then. Here’s the disassembly of that section:

        code_0a0f
  0a0f:7a               push2 
  0a10:5b 02 03         lea 2 3 
  0a13:36               push 
  0a14:72 1776          lofsa $218d // BOBALU
  0a17:36               push 
  0a18:43 49 04         callk StrCmp 4 

  0a1b:18               not 
  0a1c:30 000e          bnt code_0a2d 
  0a1f:38 01bc          pushi 1bc // $1bc newRoom
  0a22:78               push1 
  0a23:38 02bc          pushi 2bc // $2bc sel_700
  0a26:81 02            lag  
  0a28:4a 06            send 6 

  0a2a:32 0025          jmp code_0a52 

        code_0a2d
  0a2d:8b 23            lsl local35 
  0a2f:83 02            lal local2 
  0a31:1a               eq? 
  0a32:30 000e          bnt code_0a43 
  0a35:38 01bc          pushi 1bc // $1bc newRoom
  0a38:78               push1 
  0a39:38 02bc          pushi 2bc // $2bc sel_700
  0a3c:81 02            lag  
  0a3e:4a 06            send 6 

  0a40:32 000f          jmp code_0a52 

        code_0a43
  0a43:7a               push2 
  0a44:38 02bd          pushi 2bd // $2bd sel_701
  0a47:78               push1 
  0a48:46 00ff 0000 04  calle ff procedure_0000 4 //  

  0a4e:35 01            ldi 1 
  0a50:a1 04            sag  

        code_0a52
  0a52:48               ret 
  0a53:00               bnot 

I’m just going to take the newRoom 700 part and follow it with a ret instruction, and see what happens…

hex edit screenshot

… and it works! I can type whatever:

whatever screenshot

The Patch

Just put this script.701 patch as script.701 in your game directory, and it should work.

It’s nice that this change is specific to the copy protection script, because it gives some confidence that the overall game isn’t broken by the change I made. Also, none of the original game assets were modified. That’s the nice thing about the SCI engine… it looks for uncompressed overrides of the in-game scripts on the file-system. If you delete the patch file (or rename it), the game will resume checking your answer.