Jump to content
IGNORED

Fun with tiles: another weekend experiment


cmadruga

Recommended Posts

Could anyone help me understand how $AD becomes $2C

Look at 5:44.

 

I understand he rotated $AD (8 bits) in a loop 173 times. 

It doesn't say if he rotated it left or right, but anyway.

 

Just looking at the bits, $2C does not look like a possible outcome of rotating $AD.

Or maybe I misunderstood the operation?

 

I'm not great with this bitwise stuff, so I appreciate the help.

 

 

PS.: another question I have is whether other versions of GB calculated the account name checksum using PETSCII as well...

Anyone ever tried using C64 account numbers on other versions?

 

PSS.: I checked out this account number generator and there is definitely a difference in how it operates depending on the platform.

 

https://www.perifractic.com/takeout-shop/ghostbusters-password-generator/

Edited by cmadruga
Link to comment
Share on other sites

Perhaps this will help:

https://github.com/lagomorph/gbaccount/blob/master/gbaccount.py

 

 

It is not just a shift/rotate.  From that script:

 

The account number obfuscation algorithm:

Both the name and the account balance are used to generate the account number. Account balances are up to six digits. The lowest two digits are not used and treated as zero. The account number is built from three bytes. Two bytes come from the four BCD digits of the account balance. The remaining byte is computed using the account balance and name. Let's call it a check byte.

 

Computing the check byte:

  • Add the high and low byte of the account balance ignoring any overflow. If the result is zero add one. This will be the initial value of the check byte.
  • Perform an eight bit checksum over the (uppercase) name. If the result is zero add one. This will act as an iteration count for the next step.
  • Left shift and xor the current check byte value several times as shown in checkByte(). The next value of the check byte will be the current one rotated left with the low bit rotated in from the high bit of the result of the left shifts and xors. Repeat this step using the iteration count. The result will be the final check byte.

The three bytes used to generate the account number are:

<BCD balance high> <check byte> <BCD balance low>

 

Break these bytes into groups of three bits each. Each group of three bits

will be a digit in the account number. The digits are first grouped into

pairs and then the list of pairs are reversed to give the correct order

in the account number.

Link to comment
Share on other sites

18 minutes ago, DZ-Jay said:
  • Left shift and xor the current check byte value several times as shown in checkByte(). The next value of the check byte will be the current one rotated left with the low bit rotated in from the high bit of the result of the left shifts and xors. Repeat this step using the iteration count. The result will be the final check byte.

Ahh... there is xor done besides the shift... perfect. Thank you very much!

Link to comment
Share on other sites

7 minutes ago, cmadruga said:

Ahh... there is xor done besides the shift... perfect. Thank you very much!

Actually, looking at the python code, it's just a rotate left.  Those XORs are merely to simulate an 8-bit rotation operation on a 32-bit word language:  it essentially isolates the overflow from the lower byte, and puts it back on the least-significant bit.

 

The example in the source code shows that it's just a rotate.  The issue you had is that the video does not describe it correctly:  It does not rotate the checksum of the name; it operates on the sum of the high and low bytes of the 16-bit balance amount.  So $AD (the checksum of the name string) is just the number of times to rotate, but you do not rotate $AD.

 

    -dZ.

Edited by DZ-Jay
Link to comment
Share on other sites

The account generator doesn't accept empty string for the name, but the game does. For instance if you leave the name blank and use account number 614, you will start with $300,000 (not sure why the generator has fractional dollars, I've never seen that in the game). If you type in 458, you will start with $1,000,000 which displays as :00,000 in the game IIRC because it is not supposed to be possible to have more than $999,999 in your balance. At least the C64 version. As you see from the generator, it also doesn't allow bigger amounts of money than $999,900.

Link to comment
Share on other sites

12 hours ago, DZ-Jay said:

Actually, looking at the python code, it's just a rotate left.  Those XORs are merely to simulate an 8-bit rotation operation on a 32-bit word language:  it essentially isolates the overflow from the lower byte, and puts it back on the least-significant bit.

 

The example in the source code shows that it's just a rotate.  The issue you had is that the video does not describe it correctly:  It does not rotate the checksum of the name; it operates on the sum of the high and low bytes of the 16-bit balance amount.  So $AD (the checksum of the name string) is just the number of times to rotate, but you do not rotate $AD.

 

    -dZ.

Hmm ... on second thought, you still need to implement the shift and XOR combination because the CP1610 is 16-bits, and you need to rotate within the 8-bit space.  Although if you copy the lower 8-bits into the upper ones (using SWAP Rx, 2), you can then use a RLC (Rotate-Left through Carry) to simulate the 8-bit rotation.  But if you are using IntyBASIC, I don't think you have direct access to the SWAP or RLC, so you're better off replicating what the Python script does. :)

 

      -dZ.

Link to comment
Share on other sites

Ok, if this is just a rotate left, something doesn't look right.

The source code gives this example for rotating $2C 5 times.

 

$2c -->
  1: $58
  2: $b0
  3: $61
  4: $c3
 

5: $87

 

 

I don't get how it arrives at $C3 on the 4th iteration. Shouldn't that be $C2 ?

After all, 

0110 0001 = $61

 

If you rotate that left, you should get

1100 0010 = $C2  ... and not the $C3 shown?

 

What am I missing?

 
   
   
   
   
 

 

Link to comment
Share on other sites

I don't know Python, but I suspect it's about this part of the code:

 

for _ in range(iters?
  tmp = (cb << 1) & 0xff
  tmp ^= cb
  tmp = (tmp << 1) & 0xff
  tmp ^= cb
  tmp = (tmp << 2) & 0xff
  tmp ^= cb
  cb = (cb << 1) & 0xff
  if tmp & 0x80:
  cb |= 0x01
   
 

return cb

 

 

I was able to reproduce the output with this code in Intybasic. Am I on the right path?

 

    #name_checksum=$2C
    FOR I=1 TO 5
        #name_checksum=#name_checksum*2
        IF (#name_checksum AND $100)<>0 THEN #name_checksum=(#name_checksum AND $FF)+1
        IF (I%4=0) AND (#name_checksum AND $80)<>0 THEN #name_checksum=#name_checksum OR 1
    NEXT I

Link to comment
Share on other sites

11 hours ago, carlsson said:

The account generator doesn't accept empty string for the name, but the game does. For instance if you leave the name blank and use account number 614, you will start with $300,000 (not sure why the generator has fractional dollars, I've never seen that in the game). If you type in 458, you will start with $1,000,000 which displays as :00,000 in the game IIRC because it is not supposed to be possible to have more than $999,999 in your balance. At least the C64 version. As you see from the generator, it also doesn't allow bigger amounts of money than $999,900.

 

According the the generator source:

 

Quote

The account number obfuscation algorithm:

Both the name and the account balance are used to generate the account number.

Account balances are up to six digits. The lowest two digits are not used and treated as zero. The account number is built from three bytes. Two bytes come from the four BCD digits of the account balance. The remaining byte is computed using the account balance and name. Let's call it a check byte.

 

So there are no fractional digits.  It's a 6-digit integer (up to $999,999.00) where the last two digits are ignored and treated as zero ($999,900.00).

 

According to the source code notes, if the 8-bit checksum of the name is zero, then it adds one, but I do not see that in the implementation, so maybe it's an oversight.  Since the original C=64 supports an empty name, perhaps @cmadruga should fix that. ;)

 

    -dZ.

Link to comment
Share on other sites

Yesterday I tried the online generator with some old scores I had written down and it worked on all but the ones with empty name.

 

However the generator displays $XX XX 00.00 of which I understand that the two zeroes before the dot are not part of the code, but the two zeroes after the dot are not even shown in the game. For instance CALLE and code 03666100 yields $38300.00 but the game will only display $38300.

Edited by carlsson
Link to comment
Share on other sites

This reminds me that I will need to revise my logic for the payment received when capturing ghosts.

 

Players are supposed to receive between $300 and $1000 depending on how fast they answered the call.

 

I was not rounding that reward to multiples of $100, but given how the account system works I guess I will need to do that.

Link to comment
Share on other sites

1 hour ago, cmadruga said:

Ok, if this is just a rotate left, something doesn't look right.

The source code gives this example for rotating $2C 5 times.

 

$2c -->
  1: $58
  2: $b0
  3: $61
  4: $c3
 

5: $87

 

 

I don't get how it arrives at $C3 on the 4th iteration. Shouldn't that be $C2 ?

After all, 

0110 0001 = $61

 

If you rotate that left, you should get

1100 0010 = $C2  ... and not the $C3 shown?

 

What am I missing?

 
   
   
   
   
 

 

I saw the same thing, I thought it was a typo.  However, the code does something odd:  it tests bit #7 ($80).  So it occurs to me that it's some sort of parity check, or maybe additional entropy introduced for obfuscation.  It certainly looks like a normal left-rotation of bits, except that it diverges on some values, as we see in the example.

 

So the algorithm is quite simple:

  • Rotate left one bit
  • If the upper-most (#7) is 1, or it to the lower-most bit (if cb AND $80 then cb = CB OR 1)
  • Repeat for each iteration (i.e., the total computed by the character checksum)

 

 

 

Edited by DZ-Jay
Link to comment
Share on other sites

Note that the checksum computed from the name is the number of iterations, not the starting account number check byte.  The starting check byte is instead computed by adding the high and low bytes of the BCD balance together.

 

 

Once you have those two values computed (the name checksum and the balance code), then you can use them both to compute the account number check byte:

#name_checksum = $05  ' Iterations
#balance_code  = $2C  ' BCD high-byte + low-byte
FOR I = 1 to #name_checksum
  #balance_code = #balance_code * 2
  IF (#balance_code AND $100) THEN #balance_code = (#balance_code AND $FF) + 1  ' Rotate left through 8 bits
  IF (#balance_code AND  $80) THEN #balance_code = (#balance_code OR 1)         ' Set LSB based on MSB (of 8 bits)
NEXT

The algorithm, as I stated above is this:

  • For each iteration:
    • Rotate the code one bit the the left
    • If the MSB (bit #7) is set, then set the LSB (bit #0)
  • Repeat

 

Edited by DZ-Jay
Link to comment
Share on other sites

One more thing, the account generation code presumes that the balance is stored as two BCD (Binary Coded Decimal) bytes.  That is, each byte represents two decimal digits, encoded as two 4-bit values.

 

So if you have something like $999,900.00, it is stored as four "9" digits like this:

+---+---+---+---+---+---+---+---+    +---+---+---+---+---+---+---+---+
| 1 | 0 | 0 | 1 | 1 | 0 | 0 | 1 |    | 1 | 0 | 0 | 1 | 1 | 0 | 0 | 1 |
+---+---+---+---+---+---+---+---+    +---+---+---+---+---+---+---+---+
 \_____________/ \_____________/      \_____________/ \_____________/
        |               |                    |               |
     9 = 1001        9 = 1001             9 = 1001        9 = 1001

 

The account generator algorithm starts by taking those two bytes and adding them together.

 

   -dZ.

Link to comment
Share on other sites

26 minutes ago, DZ-Jay said:

 


#name_checksum = $05  ' Iterations
#balance_code  = $2C  ' BCD high-byte + low-byte
FOR I = 1 to #name_checksum
  #balance_code = #balance_code * 2
  IF (#balance_code AND $100) THEN #balance_code = (#balance_code AND $FF) + 1  ' Rotate left through 8 bits
  IF (#balance_code AND  $80) THEN #balance_code = (#balance_code OR 1)         ' Set LSB based on MSB (of 8 bits)
NEXT

 

The output of this code does not match what the source says should happen.

It leads to :

 

$2C -> $58 (ok) -> $B1 (not ok) -> $63 (not ok) ...

 

I don't think the comparison with $80 is supposed to happen with each iteration.

 

Edited by cmadruga
Link to comment
Share on other sites

1 hour ago, cmadruga said:

The output of this code does not match what the source says should happen.

It leads to :

 

$2C -> $58 (ok) -> $B1 (not ok) -> $63 (not ok) ...

 

I don't think the comparison with $80 is supposed to happen with each iteration.

 

You are right, I missed the "%4" from your code, but that's not right either.  Looking more at the code, I don't think it is a simulation of rotate at all; it only looks like that with the example.  Your %4 also is also a misdirection, although it works on the $2C example value as well.

 

The Python code clearly shows that the $80 is tested on every iteration (in Python, nested code blocks are defined by indentation, so all indented code following the "for" statement is part of the iteration block).  The key is that the test is not performed against the shifted value in the iteration (#check_byte), but against a computed temporary value, which is a combination of shifts and XORs.  The actual check-byte is merely shifted to the left, not rotated.

 

I think that's just more obfuscation.  David Crane has said that they spent considerable effort in obfuscating the code to keep curious players from discovering the algorithm by observation alone, and to prevent randomly entered values from working.

 

So the correct logic in IntyBASIC should be:

#name_checksum = $05  ' Iterations
#check_byte    = $2C  ' BCD high-byte + low-byte
tmp            = 0

FOR I = 1 to #name_checksum
  ' Compute temporary obfuscation code
  tmp = ((#check_byte * 2) XOR #check_byte)
  tmp =         ((tmp * 2) XOR #check_byte)
  tmp =         ((tmp * 4) XOR #check_byte)

  ' Compute check-byte
  #check_byte = (#check_byte * 2)
  IF (tmp AND $80) THEN
    #check_byte = (#check_byte OR 1)
  END IF
NEXT

 

    -dZ.

 

Edited by DZ-Jay
Link to comment
Share on other sites

2 minutes ago, carlsson said:

Now I wonder if that $500 version of this game correctly implements account numbers according to either of the existing schemes... :roll:

Sadly, not.

It seems to jump from the first screen below to the second while making you always go with the Hearse(?).

 

image.png.1de07d99d5468c1204ff7ddff8ecab42.png    image.png.f2de5b1def7f30e9ae9946b71f08c73d.png

 

 

  • Like 1
Link to comment
Share on other sites

3 minutes ago, cmadruga said:

Sadly, not.

It seems to jump from the first screen below to the second while making you always go with the Hearse(?).

 

image.png.1de07d99d5468c1204ff7ddff8ecab42.png    image.png.f2de5b1def7f30e9ae9946b71f08c73d.png

 

 

But you must know that that was part of the fabulous design, and it is definitely superior -- enough to warrant at least half of the price.

 

   -dZ.

  • Haha 1
Link to comment
Share on other sites

41 minutes ago, DZ-Jay said:

You are right, I missed the "%4" from your code, but that's not right either.  Looking more at the code, I don't think it is a simulation of rotate at all; it only looks like that with the example.  Your %4 also is also a misdirection, although it works on the $2C example value as well.

 

The Python code clearly shows that the $80 is tested on every iteration (in Python, nested code blocks are defined by indentation, so all indented code following the "for" statement is part of the iteration block).  The key is that the test is not performed against the shifted value in the iteration (#check_byte), but against a computed temporary value, which is a combination of shifts and XORs.  The actual check-byte is merely shifted to the left, not rotated.

 

I think that's just more obfuscation.  David Crane has said that they spent considerable effort in obfuscating the code to keep curious players from discovering the algorithm by observation alone, and to prevent randomly entered values from working.

 

So the correct logic in IntyBASIC should be:


#name_checksum = $05  ' Iterations
#check_byte    = $2C  ' BCD high-byte + low-byte
tmp            = 0

FOR I = 1 to #name_checksum
  ' Compute temporary obfuscation code
  tmp = ((#check_byte * 2) XOR #check_byte)
  tmp =         ((tmp * 2) XOR #check_byte)
  tmp =         ((tmp * 4) XOR #check_byte)

  ' Compute check-byte
  #check_byte = (#check_byte * 2)
  IF (tmp AND $80) THEN
    #check_byte = (#check_byte OR 1)
  END IF
NEXT

 

    -dZ.

 

 

This version seems to diverge on the 3rd iteration as well...

I'm getting $0161 instead of $61.

 

Anyway, I appreciate the help, didn't mean to take too much of your time.

 

 

 

Link to comment
Share on other sites

6 minutes ago, cmadruga said:

 

This version seems to diverge on the 3rd iteration as well...

I'm getting $0161 instead of $61.

 

Anyway, I appreciate the help, didn't mean to take too much of your time.

 

 

 

 

Well, that's easy, it's because I forgot to "AND $FF" to the 16-bit variable.  DOH!

 

In reality, all these variables should be 8-bits, because the original software was written for 8-bit processors, so no values would go higher than a byte, and so the algorithms sort of expect it.  This is why the BCD balance amount is stored in two bytes.  You could store it in 16-bit memory, but then you'll have to manipulate it if you want to add both upper and lower bytes.

 

I should really check my code before posting.  :dunce:

 

Here's the updated code

name_checksum = $05  ' Iterations
check_byte    = $2C  ' BCD high-byte + low-byte
tmp           = 0

FOR I = 1 to name_checksum
  ' Compute temporary obfuscation code
  tmp = ((check_byte * 2) XOR check_byte)
  tmp =        ((tmp * 2) XOR check_byte)
  tmp =        ((tmp * 4) XOR check_byte)

  ' Compute check-byte
  check_byte = (check_byte * 2)
  IF (tmp AND $80) THEN
    check_byte = (check_byte OR 1)
  END IF
NEXT

Because the variables are 8-bit, the overflow will be discarded on every store.  Alternatively, just add "AND $FF" to all expressions where the value is stored on a 16-bit variable.

 

   -dZ.

Edited by DZ-Jay
Link to comment
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.
Note: Your post will require moderator approval before it will be visible.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

Loading...
  • Recently Browsing   0 members

    • No registered users viewing this page.
×
×
  • Create New...