+DZ-Jay Posted June 6, 2021 Share Posted June 6, 2021 This by itself is worth quite a bit there's a meme in there somewhere ... 4 minutes ago, cmadruga said: "YOU ARE IN BANKRUPT! ALL YOUR GOST ARE BELONG TO US!" 1 Quote Link to comment Share on other sites More sharing options...
+cmadruga Posted June 6, 2021 Author Share Posted June 6, 2021 3 minutes ago, DZ-Jay said: I had not seen those screenshots before. God, those graphics alone are worth $500.00. How could you ever pretend to fill in those shoes ... ? I know... I know... ??? Quote Link to comment Share on other sites More sharing options...
Sinjinhawke Posted June 6, 2021 Share Posted June 6, 2021 Well I would like your game with the box for the released version. The box is probably $300 alone. Quote Link to comment Share on other sites More sharing options...
+cmadruga Posted June 11, 2021 Author Share Posted June 11, 2021 (edited) 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 June 11, 2021 by cmadruga Quote Link to comment Share on other sites More sharing options...
+DZ-Jay Posted June 11, 2021 Share Posted June 11, 2021 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. Quote Link to comment Share on other sites More sharing options...
+DZ-Jay Posted June 11, 2021 Share Posted June 11, 2021 (edited) Oh, it turns out that the link I sent is the source code for the generator you linked to, Perifractic's Ghostbusters Account Generator. Edited June 11, 2021 by DZ-Jay Quote Link to comment Share on other sites More sharing options...
+cmadruga Posted June 11, 2021 Author Share Posted June 11, 2021 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! Quote Link to comment Share on other sites More sharing options...
+DZ-Jay Posted June 11, 2021 Share Posted June 11, 2021 (edited) 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 June 11, 2021 by DZ-Jay Quote Link to comment Share on other sites More sharing options...
carlsson Posted June 11, 2021 Share Posted June 11, 2021 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. Quote Link to comment Share on other sites More sharing options...
+DZ-Jay Posted June 12, 2021 Share Posted June 12, 2021 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. Quote Link to comment Share on other sites More sharing options...
+cmadruga Posted June 12, 2021 Author Share Posted June 12, 2021 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? Quote Link to comment Share on other sites More sharing options...
+cmadruga Posted June 12, 2021 Author Share Posted June 12, 2021 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 Quote Link to comment Share on other sites More sharing options...
+DZ-Jay Posted June 12, 2021 Share Posted June 12, 2021 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. Quote Link to comment Share on other sites More sharing options...
carlsson Posted June 12, 2021 Share Posted June 12, 2021 (edited) 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 June 12, 2021 by carlsson Quote Link to comment Share on other sites More sharing options...
+cmadruga Posted June 12, 2021 Author Share Posted June 12, 2021 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. Quote Link to comment Share on other sites More sharing options...
+DZ-Jay Posted June 12, 2021 Share Posted June 12, 2021 (edited) 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 June 12, 2021 by DZ-Jay Quote Link to comment Share on other sites More sharing options...
+DZ-Jay Posted June 12, 2021 Share Posted June 12, 2021 (edited) 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 June 12, 2021 by DZ-Jay Quote Link to comment Share on other sites More sharing options...
+DZ-Jay Posted June 12, 2021 Share Posted June 12, 2021 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. Quote Link to comment Share on other sites More sharing options...
+cmadruga Posted June 12, 2021 Author Share Posted June 12, 2021 (edited) 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 June 12, 2021 by cmadruga Quote Link to comment Share on other sites More sharing options...
+DZ-Jay Posted June 12, 2021 Share Posted June 12, 2021 (edited) 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 June 12, 2021 by DZ-Jay Quote Link to comment Share on other sites More sharing options...
carlsson Posted June 12, 2021 Share Posted June 12, 2021 Now I wonder if that $500 version of this game correctly implements account numbers according to either of the existing schemes... Quote Link to comment Share on other sites More sharing options...
+cmadruga Posted June 12, 2021 Author Share Posted June 12, 2021 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... Sadly, not. It seems to jump from the first screen below to the second while making you always go with the Hearse(?). 1 Quote Link to comment Share on other sites More sharing options...
+DZ-Jay Posted June 12, 2021 Share Posted June 12, 2021 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(?). 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. 1 Quote Link to comment Share on other sites More sharing options...
+cmadruga Posted June 12, 2021 Author Share Posted June 12, 2021 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. Quote Link to comment Share on other sites More sharing options...
+DZ-Jay Posted June 12, 2021 Share Posted June 12, 2021 (edited) 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. 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 June 12, 2021 by DZ-Jay Quote Link to comment Share on other sites More sharing options...
Recommended Posts
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.