Skip to navigation

Anti-cracker checks

The Sentinel is crammed full of traps and obfuscations for would-be crackers

Back in the 1980s, copy protection was a big deal, and most blockbuster games came with some kind of obfuscation or anti-copying code that would make it harder for would-be crackers to hack into the game code and, say, generate all 10,000 secret codes without having to play the game (as if anyone would want to do that!). Of course, the whole thing was a cat-and-mouse game that the crackers eventually won, but if the copy protection could put off all except the best hackers, then it might buy you enough time to stop any piracy from denting sales.

The original 1986 release of The Sentinel came wrapped up in plenty of tape protection code, but the version I've analysed on this site is the 1989 release from the Play It Again Sam 6 compilation, which came without any noticeable copy protection: apart from hiding the game files from the disc catalogue, the game binaries are otherwise completely unprotected. But despite this lack of media-based copy protection, there are still plenty of traps for would-be hackers, because buried inside the game itself is a fascinating collection of obfuscations and downright fiendish anti-cracker code that's mainly aimed at protecting the game's secrets, rather than preventing copying.

This anti-cracker code comes in four flavours, which we'll look at in turn. I've called them the cracker seed, the secret stash, stack hacking, and tower corruption, and they are split up and threaded throughout the game code in a way that makes life harder not just for crackers, but for software archaeologists too.

Take a deep breath... we're going in.

The cracker seed
----------------

Let's start with the cracker seed. This involves the following routines and variables, shown in the order in which they are run:

NameLocationDescription
SetCrackerSeedAfter part 3 of SmoothTileCornersSet up anti-cracker landscape-related data that can be checked in the CheckCrackerSeed routine
CrackerSeedBefore GetAngleInRadiansObfuscated storage for the high byte of the landscape number
AlterCrackerSeedAfter part 1 of CheckEnemyGazeCreate an altered version of the anti-cracker seed-related data
alteredSeedMain variable workspaceAn altered version of the anti-cracker seed-related data
CheckCrackerSeedAfter part 1 of SpawnCharacter3DCheck whether the anti-cracker seed-related data is correctly set up
CorruptSecretCodeCalled by CheckCrackerSeedCorrupt the generation process for the landscape's secret code by fetching one more seed number than necessary

The first step in this process happens in the SetCrackerSeed routine, which is embedded between parts 3 and 4 of the SmoothTileCorners routine. This code is run multiple times during the landscape generation process when smoothing strips of tile corners (see the deep dive on generating the landscape for details).

SetCrackerSeed is very simple: it just copies the contents of stripData+78 into the operand of the unused LDA #0 instruction at CrackerSeed. So what is stripData+78, and why are we copying it into this LDA instruction?

The answer can be found at the start of the GenerateLandscape routine, where we generate 81 seed numbers in a loop, storing the results in stripData. The main reason for doing this is to get the seed number generator to a point where the output is usable and random enough to use for generating the landscape, but there is another use, as part of the anti-cracker code.

These 81 seed numbers get stored in the stripData table in reverse order, from stripData+80 down to stripData+0. This means that the third seed number that's generated and stored at stripData+78 contains the high byte of the binary coded decimal version of the landscape number (so if the landscape number is 0123, stripData+78 contains 01, for example). This is because the seed generator is initialised using the BCD landscape number, and the third number out of the shift register is unchanged and still contains the initial value of that byte - see the deep dive on seed number generation for more details.

So stripData+78 contains part of the landscape number, but why do we store this in the operand of an LDA #0 instruction? This instruction appears just before the GetAngleInRadians routine, so to someone poking through the code, it looks like it might either be part of that routine or a different entry point. But in reality this instruction is unused, and the fact that it's in memory in the form &A9 &00 is pure distraction. It turns out that the &A9 byte is completely unused and the &00 byte is only used for storing the high byte of the BCD landscape number from stripData+78, but this is far from obvious without analysing the code in some depth.

For want of a better name, I've named this stored value the "cracker seed", because it contains a seed - the third seed - and it's part of the anti-cracker process. Similarly, for the unused instruction where we store the cracker seed, I've given it the label CrackerSeed, so the cracker seed is actually stored in CrackerSeed+1.

To get back to SetCrackerSeed, this routine simply stores the high byte of the BCD landscape number in CrackerSeed+1. It gets run every time we smooth a strip of tiles when generating the landscape, but it runs in the same way every time, so by the time the landscape is generated, we still have the contents of CrackerSeed+1 set to the cracker seed.

It's worth noting that the two instructions in SetCrackerSeed are written to be deliberately confusing as well. Here's the code:

   LDA stripData+78-32,X 
   STA CrackerSeed+1-32,X 

We know that by the time we get to this code, X is always 32, as we just worked through a whole strip of tile corners, so this boils down to the following:

   LDA stripData+78
   STA CrackerSeed+1

But if you're looking through the game binary, trying to disassemble this part of the code, then it is far from obvious what's going on here. Trust me, I've been there...

The next stage in the cracker seed saga is the AlterCrackerSeed routine, which is slipped into the middle of the CheckEnemyGaze routine. This latter routine is run throughout the game, as it checks what each of the enemies can see, so AlterCrackerSeed gets run a lot if you are playing the game properly.

AlterCrackerSeed does what it says: it takes the cracker seed and alters it, before storing the altered version in the alteredSeed variable. As with SetCrackerSeed, this code is run multiple times throughout the game, and each time it does the exact same calculation.

The calculation is pretty simple. We take the cracker seed, which is a BCD number, and extract the second digit, so if the landscape number is 0123, the cracker seed contains 01 and we extract the second digit, in this case 1. If the second digit is in the range 0 to 8, then we add 7 to it to get a number in the range 7 to 15; and if the second digit is 9, then we add 1 to it to get 10. We then store the result in alteredSeed.

The altered cracker seed has one important property: it is always greater than CrackerSeed+1, as the addition never overflows. This works because the cracker seed is a BCD number, so it has a maximum value of &99, in which case we will add 1 to it to get &A0 (as the addition is done in normal, non-BCD mode). If the cracker seed is in the range &x0 to &x8, then the addition will simply change the cracker seed into the range &x7 to &xF, without any overflow.

The scene is now set for the anti-cracker code itself. So far we've simply populated the cracker seed and the altered cracker seed, which has no effect on anything. But if the player wins against the Sentinel, then the last part of the code kicks in when the secret code is drawn on-screen in big 3D text. The deep dive on drawing 3D text using blocks describes the details, but each letter in the secret code screen is spawned on the landscape using 3D blocks, and that spawning is done in the SpawnCharacter3D routine. And sandwiched between parts 1 and 2 of this spawning routine is the anti-cracker routine CheckCrackerSeed, in which we check the cracker seeds that we have generated.

CheckCrackerSeed is run for every 3D character that we spawn as part of the secret code, so that's eight times (as the secret code contains eight characters). Each time it performs the same simple check: is the altered cracker seed bigger than the cracker seed? If it is then both our seeds are in the correct state, so we do nothing, but if the altered cracker seed is smaller than the cracker seed, something has gone wrong, so we call the CorruptSecretCode routine. Again, this innocent check is buried in obfuscated code that uses an index to make it far from obvious what's going on:

   LDA alteredSeed-7,X
   CMP CrackerSeed+1-7,X
   BCS csee1

   JSR CorruptSecretCode

  .csee1

In this case X is 7, so this code actually does the following:

   LDA alteredSeed
   CMP CrackerSeed+1
   BCS csee1

   JSR CorruptSecretCode

  .csee1

So as you can see, this just compares the cracker seed and the altered cracker seed and calls CorruptSecretCode if the altered seed is smaller than the cracker seed.

The CorruptSecretCode routine is sneaky. All it does is generate one extra landscape seed, and then it returns without doing anything else. The effect on the secret code that's being drawn is devastating, though; it corrupts it and makes it invalid. Of course, the player has no idea that this is happening as the game still displays a valid-looking secret code, but when they come to enter it on the main title screen, it will fail and with a "WRONG SECRET CODE" error.

So what kind of hacking might trigger a corrupted secret code? Well, the altered seed is zeroed in the ResetVariables routine along with the rest of the main variable workspace, so if we only run one of the SetCrackerSeed and AlterCrackerSeed routines rather than both, then the chances are that the landscape code will be corrupted. So this is a trap to catch any crackers who try to skip the landscape generation process, or skip playing the game, in their hunt for secret codes.

Interestingly, if we disable both these routines, then both CrackerSeed+1 and alteredSeed will be zero and the secret code will be correct. This seems like a missed opportunity; changing the fake LDA #0 instruction to LDA #&FF in CrackerSeed would trigger this trap even if both routines at SetCrackerSeed and AlterCrackerSeed were disabled, which would be even better. I guess even anti-cracker code can have its weaknesses.

The secret stash
----------------

Next up is the secret stash. This involves the following routines and variables, shown in the order in which they are run:

NameLocationDescription
SetSecretStashAfter DrawFlatTileSet the secret code stash offset
stashOffsetMain variable workspaceThe offset into the secretCodeStash where we store a set of generated values for later checking in the GetRowVisibility routine
secretCodeStashSame location as screenBufferRow0A stash for calculated values for each iteration in the CheckSecretCode routine
CheckSecretStashAfter part 1 of GetRowVisibilityCheck whether the secret code stash is correctly set up
doNotCheckSecretMain variable workspaceA flag to control whether we perform the secret code check that's buried in the GetRowVisibility routine
stashAddrMain variable workspaceThe address of the four bytes in the secretCodeStash that correspond to the landscape's secret code

The secret stash is an area of memory where the code stores seed numbers that are used to generate the landscape's secret code. As with the cracker seed, this protection is aimed at stopping hackers who try to circumvent parts of the code in their quest for secret codes.

The first step in setting up the secret stash is the SetSecretStash routine, which appears just after the DrawFlatTile routine. This latter routine draws flat tiles by checking where the tile is in the chess board layout of the landscape and giving the tile the correct colour; if the tile is blue then it passes through the SetSecretStash routine on the way to the tile-drawing routine in DrawOneFaceTile, otherwise it jumps straight to DrawOneFaceTile without going via SetSecretStash.

The first time we need to draw landscape tiles is during the landscape preview, which looks like this:

The landscape preview screen for landscape 0000 in the BBC Micro version of The Sentinel

The SetSecretStash routine is therefore run once for every flat blue tile that appears in the landscape preview, of which there will be quite a few. In terms of code, SetSecretStash is very simple; it just copies the middle byte of the pseudo-random number generator for the landscape seeds and stores it in the stashOffset variable. The generator is stored in seedNumberLFSR(4 3 2 1 0), so all SetSecretStash does is this:

  stashOffset = seedNumberLFSR+2

Of course, the code in SetSecretStash is obfuscated, and consists of this rather confusing pair of instructions:

  LDA seedNumberLFSR+2-8,X
  STA stashOffset-8,X

It turns out that at this point X is always 8, so this code actually does the following:

  LDA seedNumberLFSR+2
  STA stashOffset

You can read more about the pseudo-random number generator in the deep dive on seed number generation.

Because SetSecretStash is run for every blue tile, this means the value of stashOffset is overwritten until the last tile is drawn, so by the end of the landscape preview, stashOffset contains the middle byte of the seed generator from the point at which we drew the last flat blue tile in the preview.

Once the landscape has been generated and drawn in the preview, the final step is to generate the landscape's secret code and check that it matches the code entered by the player. This process is described in the deep dive on the landscape secret code, but essentially we generate a further 38 seed numbers, and then the next four seed numbers give us the secret code in BCD format, from high byte to low byte.

The CheckSecretCode routine implements this process, and it stores information about these extra seeds in a part of memory called the secret code stash at secretCodeStash. This seed information is stored in a block at an offset of stashOffset within the secret code stash, so this ensures that the stash moves around in memory depending on the number of flat blue tiles in the landscape, making the whole secret code generation and checking process that much harder to follow.

The next part of the secret stash check is the CheckSecretStash routine, which appears between parts 1 and 2 of GetRowVisibility. This latter routine is run as part of the tile visibility process, which works out which tiles are visible from the point of view of the player, to help speed up the landscape drawing process (see the deep dive on ray-casting for the tile visibility table for details).

The CheckSecretStash routine is run every time we analyse a new tile row for visibility, but it implements a flag in bit 7 of doNotCheckSecret that makes us execute the body of the routine only once, so that's only on the first run; all subsequent runs simply skip to part 2 of GetRowVisibility.

The CheckSecretStash routine works as follows. It does a check on the secret entry code for the current landscape to ensure that it matches the entered code in the keyboard input buffer. If the check fails, then the game restarts by jumping to MainTitleLoop to display the title screen.

Specifically, the code checks for the four bytes in the secretCodeStash that correspond to the results of the comparisons made in the CheckSecretCode routine. It calculates the address of these four bytes within the secret stash as follows:

  stashAddr(1 0) = secretCodeStash + stashOffset + 41

When the secretCodeStash gets populated in the CheckSecretCode routine, we add one byte for each iteration and comparison in the secret code generation process. That process starts by performing 38 iterations and storing the results in the secretCodeStash from offset stashOffset to stashOffset + 37. It then generates the four BCD numbers that make up the secret code, storing the results in the stash from offset stashOffset + 38 to stashOffset + 41. So stashAddr(1 0) points to the last of those bytes in the secretCodeStash, i.e. the byte at stashOffset + 41.

The value that is stashed in the secretCodeStash is the result of subtracting the entered code in inputBuffer from the generated code, which will be zero if they match (see the deep dive on the key logger for more about the input buffer). Then %01111111 is added to the result, so if the secretCodeStash contains %01111111, this means that particular byte matched, so if all four bytes at offset stashOffset + 38 to stashOffset + 41 equal %01111111, this means the secret code was deemed correct by CheckSecretCode.

Incidentally, the value of %01111111 in the above calculation is also part of the obfuscation, as it is a special constant that happens to be the value of the object flags for the Sentinel. CheckSecretCode actually sets valid secret code matches to the value of the Sentinel's object flags, rather than hard-coding %01111111, so if anyone has been hacking the state of the Sentinel object, this will break the checks as well. See the stack hacking section below for a more detailed explanation of this value.

So we check the four bytes to ensure they all equal %01111111, and if any of them don't match, then we jump to MainTitleLoop to restart the game.

The four code bytes have now been checked, but we have one more check to do: that of the comparison from just before the four bytes. This comparison would have been between the keyboard input buffer in inputBuffer+4 and a BCD number from the landscape's sequence of seed numbers.

When the landscape code is entered by the player, it is stored in inputBuffer as four ASCII codes that are then converted in-place into two BCD numbers, and the rest of the buffer is padded out with &FF, so inputBuffer+4 contains &FF at the point of comparison. &FF is not a valid BCD number, so it can never match a BCD number from the landscape's sequence of seed numbers, so we know that this comparison can never have matched.

So if stashAddr(1 0) contains %01111111 to indicate a match, then we know that the stash must have been modified by a cracker, so we restart the game.

The secret stash checks add another layer to the secret code checks, in that bypassing or modifying the secret code checks will trigger a mismatch between the results from those checks and the contents of the secret stash, and the game will restart instead of moving forward to play the landscape.

Stack hacking
-------------

Next up we have a couple of bouts of stack hacking. This involves the following routines, shown in the order in which they are run:

LocationDescription
SmoothTileCornersHack the stack to control calls to JumpToPreview
CheckSecretCodeHack the stack to control calls to PlayGame

Stack hacking is a bit simpler than the previous anti-cracker checks, but it's no less difficult to work out when you're looking at a code disassembly, as there are quite a few pitfalls for the unwary.

The first example of stack hacking is at the end of part 2 of SmoothTileCorners, where we come across this innocent-looking code:

  LDA #&5F
  STA &0100,X
  DEX
  LDA #&7D
  STA &0100,X  

The game stores quite a few variable blocks in page 1, so on the surface this looks like a simple case of storing some values in one of those blocks. But when you analyse the code, you realise that at this point, X is always &FF, so the code actually does this:

  LDA #&5F
  STA &01FF
  LDA #&7D
  STA &01FE

So this code pokes a two-byte address of &5F7D into the top two bytes of the 6502 stack (as the stack starts at &01FF and descends in memory). This is clearly stack manipulation of some kind, but what is it manipulating, and how does it affect the code?

Working back from the SmoothTileCorners routine, we find that the call chain is like this, where an arrow denotes a call to a subroutine using a JSR instruction:

  MainTitleLoop -> GenerateLandscape -> SmoothTileData -> SmoothTileCorners

The MainTitleLoop routine starts with the following code, which resets the stack pointer to point to &01FF:

  LDX #&FF
  TXS  

So by the time we get to the SmoothTileCorners routine, the stack contains three addresses. As the stack descends from &01FF, this means the highest entry in memory is the first address that we put on what is now the bottom of the stack, with the later additions being lower in memory but further up the stack. The stack therefore looks like this:

  • Bottom value in (&01FE &01FF) = return address for JSR GenerateLandscape
  • Middle value in (&01FC &01FD) = return address for JSR SmoothTileData
  • Top value in (&01FA &01FB) = return address for JSR SmoothTileCorners

The stack hacking in SmoothTileCorners therefore changes the return address on the bottom of the stack, which is the address that the RTS instruction in GenerateLandscape will return to. So this hack stops us returning to MainTitleLoop once the landscape has been generated, and instead jumps to the address we've hacked into the stack.

That address is &5F7D, so what's there? Well, this is what we find in the game binary at this address:

  &4C &30 &3F

This disassembles to the following instruction:

  JMP &3F30

So what's at address &3F30? Well, rather strangely it's one of three things: it's either an instruction in the middle of the 6845 CRTC chip setup code in the ConfigureMachine routine, or it's the contents of the secret code stash, or it's one of the screen buffers. Which of these it is depend on the specific location of the secret code stash and whether we've already played a landscape by this point. In any case, though, jumping to this address is probably going to crash the computer, and if it doesn't then the best we can hope for is that it tries to restart the game.

Of course, this is a clever bit of misdirection, and we need to understand exactly how the RTS instruction works if we're to follow this stack hack to its correct conclusion. When the RTS instruction is executed, it pops the address off the top of the stack, and then it increments that address before jumping to it. So when we execute the RTS in GenerateLandscape, it grabs our hacked address from the stack, which is &5F7D, and then increments it, to give &5F7E, and it jumps to that address rather than the one we followed above.

So what's at address &5F7E? It's the same binary code as before but without the &4C, so it looks like this:

  &30 &3F

This disassembles to the following instruction:

  BMI &5FBF

And what's at &5FBF? It turns out to be the PreviewLandscape routine, so this stack hacking stops us from returning to the main title loop after generating the landscape, and instead jumps to the routine to show the landscape preview.

As for the BMI in there, we need to look at the end of the GenerateLandscape routine to see why this works. This is how the routine ends:

  LDA #2
  JSR ProcessTileData
  RTS 

And if we look at the ProcessTileData routine, it ends like this:

  DEC zTile
  BPL proc1
  RTS

So this routine ends with the N flag being set, which means a BMI branch would be taken. Another way to think about this is that we have to pass through the BPL instruction to reach the RTS, so the flags can't be indicating a positive result, so they must be indicating a negative result, which would make a BMI instruction take the branch.

The set state of the N flag is therefore carried back up the chain when we return to GenerateLandscape and on to our hacked RTS address, which takes us to the BMI &5FBF instruction; and because the N flag is set, we always jump to PreviewLandscape. You can see this obfuscated branch in the documented JumpToPreview routine.

This stack hacking process does have a valid function - it isn't just a clever way of distracting would-be crackers and software archaeologists, though it's certainly that. The stack hacking code in part 2 of SmoothTileCorners is only run when bit 7 of doNotPlayLandscape is clear, which means we can configure the return behaviour by changing this bit.

This is used to support these two approaches:

  • The ResetVariables routine zeroes doNotPlayLandscape, so the default behaviour is to show the landscape preview by clearing bit 7.
  • However, when the player successfully finishes a landscape, we call the GetNextLandscape routine to generate the landscape's secret code to display on-screen. We calculate the secret code by generating the entire landscape, as described in the deep dive on the landscape secret code, so GetNextLandscape calls GenerateLandscape to do this. In this case we only want to generate the landscape to get the secret code, but we don't want to display the preview as we want to display the secret code screen instead; so we make sure that bit 7 of doNotPlayLandscape is set to ensure that GenerateLandscape returns like a normal subroutine, without any stack hacking.

This bit 7 override gives us another anti-cracker twist, as bit 7 of doNotPlayLandscape only ever gets set in one place, and that's in the PerformHyperspace routine, and specifically when the player performs a hyperspace from the Sentinel's tower to win the game. So if the crackers try to skip this step when generating secret codes, then bit 7 will remain clear, and instead of seeing the secret code screen on completion of a landscape, they will see the landscape preview instead.

The second stack hack in The Sentinel is even harder to work out. It can be found in part 2 of CheckSecretCode, where we come across the following code:

  PLA
  PLA
  CLC
  LDA &0100
  ADC #&B6
  PHA
  CLC
  ADC #&6E
  PHA
  RTS

At least this one doesn't try to hide the fact that it's hacking the stack: any time you see two PLA instructions in a row followed by a couple of PHA instructions, the chances are that it's manipulating a return address from the top of the stack. But &0100 is the opposite end of page 1 from the stack, and we seem to be generating a two-byte address out of a one-byte value at &0100, so what's going on?

The key is to identify the contents of &0100. It turns out that this is the location of the objectFlags table, which stores the object flags for all the objects on the landscape. The first byte in this table is at address &0100, so that corresponds to object #0, and that's a big clue.

Object #0 is always the Sentinel, and the Sentinel is always placed on top of the Sentinel's tower, so the object flags for the Sentinel are constructed as follows:

  • Bits 0-5 is the number of the object beneath this one.
  • Bit 6 is set to indicate that this object is on top of another object.
  • Bit 7 is clear to indicate that this object number is allocated to an object.

The Sentinel's tower is always the first object to be spawned, and object numbers are allocated from 63 and down, so this means the tower is always object #63, or %111111. So with this object number in bits 0 to 5, bit 6 set and bit 7 clear, this means the Sentinel's object flags are always %01111111.

The LDA &0100 instruction in our stack hack therefore always sets A to %01111111, and we can use this to work out the address that gets pushed onto the stack, starting with the first byte to be pushed onto the stack:

  First byte = %01111111 + &B6
             = &135

The overflow is ignored so this sets A to &35, and that gets pushed onto the stack. We then move on to the second byte:

  Second byte = first byte + &6E
              = &35 + &6E
              = &A3

The return address is stored on the stack with the high byte first and the low byte second, so the address on the stack is now &35A3. And remember that the RTS instruction increments the address when it takes it off the stack, so the address that the RTS will jump to will be &35A4.

This is the address of the PlayGame routine, so this stack hack is used to start the game once CheckSecretCode has finished checking the entered secret code against the calculated one. And as above, the stack hack is only applied when bit 7 of doNotPlayLandscape is clear, so we can control the flow.

This works in the same way as in the first stack hack. We check bit 7 of doNotPlayLandscape at the end of part 1 of CheckSecretCode, and if it is clear (the default behaviour) then we jump to part 2 to check the entered secret code against the calculated one. If it matches then we apply the stack hack, so that the RTS jumps to PlayGame to start the game, but if it doesn't match then we leave the stack alone and the RTS takes us back to PreviewLandscape to display the "WRONG SECRET CODE" error.

If bit 7 of doNotPlayLandscape is set (which is not the default behaviour), then this means we are calling CheckSecretCode from the GetNextLandscape routine, which means we don't want to play the game after generating the landscape, and we want to display the secret code instead of checking it. So we leave the stack alone, so that the RTS takes us back to GetNextLandscape in the normal way, to display the secret code that we just generated.

To finish off, we can reconstruct the calculations that would have been used in the source code to calculate the correct values to put on the stack in the stack hack. If we consider the calculation that's done during the hack for the high byte, then we get the following:

    objectFlags + HI(PlayGame-1) - %01111111
  = %01111111 + HI(PlayGame-1) - %01111111
  = HI(PlayGame-1)

And the same calculation for the low byte looks like this:

    high byte + LO(PlayGame-1) - HI(PlayGame-1)
  = HI(PlayGame-1) + LO(PlayGame-1) - HI(PlayGame-1)
  = LO(PlayGame-1)

So this gives us the following source code:

  CLC
  LDA objectFlags
  ADC #HI(PlayGame-1) - %01111111
  PHA
  CLC
  ADC #LO(PlayGame-1) - HI(PlayGame-1)
  PHA
  RTS

This will calculate the correct return address for different values of PlayGame, so we can play around with the source without breaking this particular part of the anti-cracker code.

Tower corruption
----------------

Finally, we have the tower corruption. This involves the following routine and variable:

NameLocationDescription
SetPlayerIsOnTowerAfter part 1 of ProcessActionKeysSet up the playerIsOnTower value for checking the game is won
playerIsOnTowerMain variable workspaceA flag to record whether the player is on top of the Sentinel's tower (6 = the player is on top of the Sentinel's tower

This hack involves running the SetPlayerIsOnTower routine whenever the player transfers into a new robot. Transfers are performed at the end of part 1 of ProcessActionKeys, and SetPlayerIsOnTower is run when the target object for the transfer is an object of type 0, i.e. a robot. This routine works out whether the player just transferred into a robot that's on the Sentinel's tower, and it stores the result in the playerIsOnTower variable.

We do this by taking object #X, which is the robot that the player just transferred into, and if it's on top of another object or stack of objects, we work our way down the stack, checking to see if the stack contains the Sentinel's tower. The Sentinel's tower is always the first object to be spawned, and object numbers are allocated from 63 and down, so this means the tower is always object #63, so that's what we look for while working through the object stack.

If we find object #63 in the object stack, then we set playerIsOnTower to the type of that object; so assuming that object #63 is indeed the tower, as it should be, then playerIsOnTower gets set to 6 when the player transfers into a robot that's on the tower. The default value of playerIsOnTower is set in the ResetVariables routine to 128, and this variable isn't set anywhere else, so when the player successfully finishes a landscape, which you can only do by transferring to a robot on the Sentinel's tower and performing a hyperspace, then playerIsOnTower will be set to 6, otherwise it will still be 128.

The anti-cracker aspect of this code is implemented in the SpawnSecretCode3D routine, which draws the landscape's secret code on-screen when a landscape is successfully completed. The secret code for a landscape is calculated by generating four seeds, one for each pair of numbers in the secret code, and this is done after the landscape generation process has finished (see the deep dive on the landscape secret code for details). By the time we get to draw the secret code in SpawnSecretCode3D, the seed generation system will have been set up so that the next four seeds will produce the secret code.

In order to loop through these four seeds, we set up a loop counter in X. To loop through four items, we would normally set the loop counter to 3, but instead we set it to the value of playerIsOnTower divided by 2. If the player completed the landscape properly by transferring to the Sentinel's tower, then playerIsOnTower will be set to 6, and the loop counter in X will get set to 3, which will loop through the four seeds that give us the secret code, drawing the correct code on-screen.

But if the player didn't transfer to the Sentinel's tower, perhaps because a cracker jumped straight to the secret code generator without bothering to play the game first, then playerIsOnTower will be set to 128, so the loop counter in X will be 64, and SpawnSecretCode3D will work through 65 seeds in total, 61 more than it should do. The secret code is only printed on-screen for the last four seeds produced, so this ensures that a completely wrong secret code is shown on-screen.

So if the player doesn't transfer into a robot on the Sentinel's tower, then the secret code screen will display a corrupted number that will give the "WRONG SECRET CODE" error when entered into the main title screen. It's very sneaky!

Other obfuscations
------------------

There are a couple of other obfuscations in the code that are worth mentioning.

The first is the bizarre layout of the tileData table, which stores data for all the tiles on the landscape. Instead of being laid out in the same way as the landscape, it interleaves the tile rows in a fairly confusing manner that doesn't appear to serve any purpose other than to hide the landscape structure from anyone snooping through the game's memory. See the deep dive on tile data for more details.

The second is the break handler that is installed when the game loads, which configures the computer to clear the computer's memory on a break event (such as the user pressing the BREAK key). This is done in the ConfigureMachine routine and is a fairly standard thing to do - indeed, there's an operating system call that lets you configure this behaviour, namely OSBYTE 200.

However, OSBYTE 200 was only introduced in version 1.00 of the operating system, so anyone running the game on MOS 0.10 could simply load the game, wait for it to decrypt and unfold into memory, and then press BREAK to access the loaded game code. So The Sentinel installs a break handler, copying the routine from ClearMemory into the cassette filing system workspace at &0380, which is unused now that the game is loaded, so it's a suitable location for our handler. It then sets the Break Intercept code to a JMP &0380 instruction, which will be called on a break event, thus clearing the game from memory for all operating systems, not just those that support OSBYTE 200.

And that's probably enough anti-cracker code for now. If you managed to pick your way through this article, well done - and now imagine doing this back in the day, with nothing more than a hex-dump printout and a basic disassembler. You may not approve of code-crackers, but you can't deny their tenacity...