Improve Game Sync ID processing

Also fixes an exception that occurs when a negative PID is sent through Memory Link.
This commit is contained in:
kuroppoi 2025-04-14 19:44:21 +02:00
parent afc79c39ad
commit d5c2a57c51
5 changed files with 96 additions and 29 deletions

View File

@ -18,6 +18,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import entralinked.GameVersion;
import entralinked.utility.GsidUtility;
/**
* Manager class for managing {@link Player} information (Global Link users)
@ -117,9 +118,14 @@ public class PlayerManager {
Player player = mapper.readValue(inputFile, PlayerDto.class).toPlayer();
String gameSyncId = player.getGameSyncId();
// Check if Game Sync ID is valid
if(!GsidUtility.isValidGameSyncId(gameSyncId)) {
throw new IOException("Invalid Game Sync ID: %s".formatted(gameSyncId));
}
// Check for duplicate Game Sync ID
if(doesPlayerExist(gameSyncId)) {
throw new IOException("Duplicate Game Sync ID %s".formatted(gameSyncId));
throw new IOException("Duplicate Game Sync ID: %s".formatted(gameSyncId));
}
player.setDataDirectory(inputFile.getParentFile());

View File

@ -321,6 +321,13 @@ public class PglHandler implements HttpHandler {
*/
private void handleMemoryLink(PglRequest request, Context ctx) throws IOException {
LEOutputStream outputStream = new LEOutputStream(ctx.outputStream());
// Check if Game Sync ID is valid
if(!GsidUtility.isValidGameSyncId(request.gameSyncId())) {
writeStatusCode(outputStream, 8); // Invalid Game Sync ID
return;
}
Player player = playerManager.getPlayer(request.gameSyncId());
User user = ctx.attribute("user");
@ -472,9 +479,9 @@ public class PglHandler implements HttpHandler {
// Prepare response
LEOutputStream outputStream = new LEOutputStream(ctx.outputStream());
// Make sure Game Sync ID is present
if(request.gameSyncId() == null) {
writeStatusCode(outputStream, 1); // Unauthorized
// Check if Game Sync ID is valid
if(!GsidUtility.isValidGameSyncId(request.gameSyncId())) {
writeStatusCode(outputStream, 8); // Invalid Game Sync ID
return;
}
@ -511,6 +518,12 @@ public class PglHandler implements HttpHandler {
LEOutputStream outputStream = new LEOutputStream(ctx.outputStream());
String gameSyncId = GsidUtility.stringifyGameSyncId(Integer.parseInt(ctx.body().replace("\u0000", ""))); // So quirky
// Check if Game Sync ID is valid
if(!GsidUtility.isValidGameSyncId(request.gameSyncId())) {
writeStatusCode(outputStream, 8); // Invalid Game Sync ID
return;
}
// Check if player doesn't exist already
if(playerManager.doesPlayerExist(gameSyncId)) {
writeStatusCode(outputStream, 2); // Duplicate Game Sync ID

View File

@ -25,12 +25,6 @@ public class GsidDeserializer extends StdDeserializer<String> {
@Override
public String deserialize(JsonParser parser, DeserializationContext context) throws IOException {
int gsid = parser.getIntValue();
if(gsid < 0) {
throw new IOException("Game Sync ID cannot be a negative number.");
}
return GsidUtility.stringifyGameSyncId(gsid);
return GsidUtility.stringifyGameSyncId(parser.getValueAsInt(-1));
}
}

View File

@ -2,40 +2,87 @@ package entralinked.utility;
import java.util.regex.Pattern;
/**
* Game Sync ID generation process:
*
* Let's take example PID "1231499195".
* Start by storing both the PID and its checksum (35497) in working variable "ugsid".
* We do this by simply shifting the checksum 32 bits to the left: ugsid = pid | (checksum << 32) = 0x8AA949672FBB
*
* Calculating each character is pretty straightforward.
* We just use the 5 least significant bits of ugsid as the index for the character table.
* Character 1: 0x8AA949672FBB & 0x1F = 27 = '5'
*
* After each character, we shift ugsid 5 bits to the right. Since ugsid contains 48 bits of data,
* taking the 5 least significant bits each time gives us enough indexes for (if we round up) exactly 10 characters.
* If we take a look at the full value of ugsid (0x8AA949672FBB) in binary and split it into sections of 5 bits,
* we'll actually already be able to see the entire Game Sync ID in reverse:
*
* Character: 'E' 'L' 'X' 'F' 'E' 'Y' 'Q' 'M' '7' '5'
* Chartable index: 4 10 21 5 4 22 14 11 29 27
* Binary: XX100 01010 10101 00101 00100 10110 01110 01011 11101 11011
*
* Adding all of the characters together gets us the Game Sync ID "57MQYEFXLE".
*
* Reversing this process to retrieve the PID and checksum is very straightforward.
* Simply go through each character, find the index of it in the character table & left shift the total value 5 bits each time.
* If we do this with the Game Sync ID we just created, then it should give us back the value of ugsid: 0x8AA949672FBB
* To then retrieve the PID, simply do: ugsid & 0xFFFFFFFF = 1231499195
* To retrieve the checksum, simply do: (ugsid >> 32) & 0xFFFF = 35497
* We can then validate the Game Sync ID if we want to by comparing the checksums.
*/
public class GsidUtility {
public static final String GSID_CHARTABLE = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
public static final Pattern GSID_PATTERN = Pattern.compile("[A-HJ-NP-Z2-9]{10}");
/**
* Stringifies the specified numerical Game Sync ID
* Stringifies the specified numerical Game Sync ID.
*
* Black 2 - {@code sub_21B480C} (overlay #199)
*/
public static String stringifyGameSyncId(int gsid) {
char[] output = new char[10];
int index = 0;
long checksum = Crc16.calc(gsid);
long ugsid = gsid | (checksum << 32);
// v12 = gsid
// v5 = sub_204405C(gsid, 4u)
// v8 = v5 + __CFSHR__(v12, 31) + (v12 >> 31)
// uses unsigned ints for bitshift operations
long ugsid = gsid;
long checksum = Crc16.calc(gsid); // + __CFSHR__(v12, 31) + (v12 >> 31); ??
// do while v4 < 10
for(int i = 0; i < output.length; i++) {
index = (int)((ugsid & 0x1F) & 0x1FFFF); // chartable string is unicode, so normally multiplies by 2
ugsid = (ugsid >> 5) | (checksum << 27);
checksum >>= 5;
output[i] = GSID_CHARTABLE.charAt(index); // sub_2048734(v4, chartable + index)
int index = (int)((ugsid >> (5 * i)) & 0x1F);
output[i] = GSID_CHARTABLE.charAt(index);
}
return new String(output);
}
/**
* Determines if a Game Sync ID is valid by checking its length, characters & checksum.
*
* @return {@code true} if the Game Sync ID is valid, otherwise {@code false}.
*/
public static boolean isValidGameSyncId(String gsid) {
return GSID_PATTERN.matcher(gsid).matches();
if(gsid == null) {
return false;
}
int length = gsid.length();
long ugsid = 0;
if(length != 10) {
return false;
}
for(int i = 0; i < length; i++) {
int index = GSID_CHARTABLE.indexOf(gsid.charAt(i));
if(index == -1) {
return false;
}
ugsid |= (long)index << (5 * i);
}
int output = (int)(ugsid & 0xFFFFFFFF);
int checksum = (int)((ugsid >> 32) & 0xFFFF);
return output >= 0 && Crc16.calc(output) == checksum;
}
}

View File

@ -31,6 +31,13 @@ public class GsidUtilityTest {
// Illegal length (should be 10)
assertFalse(GsidUtility.isValidGameSyncId("Y67UEN38K"));
assertFalse(GsidUtility.isValidGameSyncId("3ER5K8MBN4C"));
// Invalid checksum
assertFalse(GsidUtility.isValidGameSyncId("VFWM2Q2ADH"));
assertFalse(GsidUtility.isValidGameSyncId("44DAWDA4SH"));
assertFalse(GsidUtility.isValidGameSyncId("J6F55U7FUE"));
assertFalse(GsidUtility.isValidGameSyncId("8FAB4ZF6JF"));
assertFalse(GsidUtility.isValidGameSyncId("HWLNS77HWD"));
}
@Test
@ -38,8 +45,8 @@ public class GsidUtilityTest {
void testValidGameSyncIds() {
assertTrue(GsidUtility.isValidGameSyncId("VFWM2QAXNF"));
assertTrue(GsidUtility.isValidGameSyncId("44DAWDJKJ8"));
assertTrue(GsidUtility.isValidGameSyncId("J6F55UB2X9"));
assertTrue(GsidUtility.isValidGameSyncId("8FAB4Z3EN9"));
assertTrue(GsidUtility.isValidGameSyncId("J6F55UB2XD"));
assertTrue(GsidUtility.isValidGameSyncId("8FAB4Z3END"));
assertTrue(GsidUtility.isValidGameSyncId("HWLNS7BTNB"));
}
}