Skip to content

Commit d5c2a57

Browse files
committed
Improve Game Sync ID processing
Also fixes an exception that occurs when a negative PID is sent through Memory Link.
1 parent afc79c3 commit d5c2a57

File tree

5 files changed

+96
-29
lines changed

5 files changed

+96
-29
lines changed

src/main/java/entralinked/model/player/PlayerManager.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import com.fasterxml.jackson.databind.SerializationFeature;
1919

2020
import entralinked.GameVersion;
21+
import entralinked.utility.GsidUtility;
2122

2223
/**
2324
* Manager class for managing {@link Player} information (Global Link users)
@@ -117,9 +118,14 @@ private void loadPlayer(File inputFile) {
117118
Player player = mapper.readValue(inputFile, PlayerDto.class).toPlayer();
118119
String gameSyncId = player.getGameSyncId();
119120

121+
// Check if Game Sync ID is valid
122+
if(!GsidUtility.isValidGameSyncId(gameSyncId)) {
123+
throw new IOException("Invalid Game Sync ID: %s".formatted(gameSyncId));
124+
}
125+
120126
// Check for duplicate Game Sync ID
121127
if(doesPlayerExist(gameSyncId)) {
122-
throw new IOException("Duplicate Game Sync ID %s".formatted(gameSyncId));
128+
throw new IOException("Duplicate Game Sync ID: %s".formatted(gameSyncId));
123129
}
124130

125131
player.setDataDirectory(inputFile.getParentFile());

src/main/java/entralinked/network/http/pgl/PglHandler.java

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -321,6 +321,13 @@ private void handleDownloadSaveData(PglRequest request, Context ctx) throws IOEx
321321
*/
322322
private void handleMemoryLink(PglRequest request, Context ctx) throws IOException {
323323
LEOutputStream outputStream = new LEOutputStream(ctx.outputStream());
324+
325+
// Check if Game Sync ID is valid
326+
if(!GsidUtility.isValidGameSyncId(request.gameSyncId())) {
327+
writeStatusCode(outputStream, 8); // Invalid Game Sync ID
328+
return;
329+
}
330+
324331
Player player = playerManager.getPlayer(request.gameSyncId());
325332
User user = ctx.attribute("user");
326333

@@ -472,9 +479,9 @@ private void handleCreateAccount(PglRequest request, Context ctx) throws IOExcep
472479
// Prepare response
473480
LEOutputStream outputStream = new LEOutputStream(ctx.outputStream());
474481

475-
// Make sure Game Sync ID is present
476-
if(request.gameSyncId() == null) {
477-
writeStatusCode(outputStream, 1); // Unauthorized
482+
// Check if Game Sync ID is valid
483+
if(!GsidUtility.isValidGameSyncId(request.gameSyncId())) {
484+
writeStatusCode(outputStream, 8); // Invalid Game Sync ID
478485
return;
479486
}
480487

@@ -511,6 +518,12 @@ private void handleCreateData(PglRequest request, Context ctx) throws IOExceptio
511518
LEOutputStream outputStream = new LEOutputStream(ctx.outputStream());
512519
String gameSyncId = GsidUtility.stringifyGameSyncId(Integer.parseInt(ctx.body().replace("\u0000", ""))); // So quirky
513520

521+
// Check if Game Sync ID is valid
522+
if(!GsidUtility.isValidGameSyncId(request.gameSyncId())) {
523+
writeStatusCode(outputStream, 8); // Invalid Game Sync ID
524+
return;
525+
}
526+
514527
// Check if player doesn't exist already
515528
if(playerManager.doesPlayerExist(gameSyncId)) {
516529
writeStatusCode(outputStream, 2); // Duplicate Game Sync ID

src/main/java/entralinked/serialization/GsidDeserializer.java

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,6 @@ protected GsidDeserializer(Class<?> type) {
2525

2626
@Override
2727
public String deserialize(JsonParser parser, DeserializationContext context) throws IOException {
28-
int gsid = parser.getIntValue();
29-
30-
if(gsid < 0) {
31-
throw new IOException("Game Sync ID cannot be a negative number.");
32-
}
33-
34-
return GsidUtility.stringifyGameSyncId(gsid);
28+
return GsidUtility.stringifyGameSyncId(parser.getValueAsInt(-1));
3529
}
3630
}

src/main/java/entralinked/utility/GsidUtility.java

Lines changed: 63 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,40 +2,87 @@
22

33
import java.util.regex.Pattern;
44

5+
/**
6+
* Game Sync ID generation process:
7+
*
8+
* Let's take example PID "1231499195".
9+
* Start by storing both the PID and its checksum (35497) in working variable "ugsid".
10+
* We do this by simply shifting the checksum 32 bits to the left: ugsid = pid | (checksum << 32) = 0x8AA949672FBB
11+
*
12+
* Calculating each character is pretty straightforward.
13+
* We just use the 5 least significant bits of ugsid as the index for the character table.
14+
* Character 1: 0x8AA949672FBB & 0x1F = 27 = '5'
15+
*
16+
* After each character, we shift ugsid 5 bits to the right. Since ugsid contains 48 bits of data,
17+
* taking the 5 least significant bits each time gives us enough indexes for (if we round up) exactly 10 characters.
18+
* If we take a look at the full value of ugsid (0x8AA949672FBB) in binary and split it into sections of 5 bits,
19+
* we'll actually already be able to see the entire Game Sync ID in reverse:
20+
*
21+
* Character: 'E' 'L' 'X' 'F' 'E' 'Y' 'Q' 'M' '7' '5'
22+
* Chartable index: 4 10 21 5 4 22 14 11 29 27
23+
* Binary: XX100 01010 10101 00101 00100 10110 01110 01011 11101 11011
24+
*
25+
* Adding all of the characters together gets us the Game Sync ID "57MQYEFXLE".
26+
*
27+
* Reversing this process to retrieve the PID and checksum is very straightforward.
28+
* Simply go through each character, find the index of it in the character table & left shift the total value 5 bits each time.
29+
* If we do this with the Game Sync ID we just created, then it should give us back the value of ugsid: 0x8AA949672FBB
30+
* To then retrieve the PID, simply do: ugsid & 0xFFFFFFFF = 1231499195
31+
* To retrieve the checksum, simply do: (ugsid >> 32) & 0xFFFF = 35497
32+
* We can then validate the Game Sync ID if we want to by comparing the checksums.
33+
*/
534
public class GsidUtility {
635

736
public static final String GSID_CHARTABLE = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
837
public static final Pattern GSID_PATTERN = Pattern.compile("[A-HJ-NP-Z2-9]{10}");
938

1039
/**
11-
* Stringifies the specified numerical Game Sync ID
40+
* Stringifies the specified numerical Game Sync ID.
1241
*
1342
* Black 2 - {@code sub_21B480C} (overlay #199)
1443
*/
1544
public static String stringifyGameSyncId(int gsid) {
1645
char[] output = new char[10];
17-
int index = 0;
46+
long checksum = Crc16.calc(gsid);
47+
long ugsid = gsid | (checksum << 32);
1848

19-
// v12 = gsid
20-
// v5 = sub_204405C(gsid, 4u)
21-
// v8 = v5 + __CFSHR__(v12, 31) + (v12 >> 31)
22-
23-
// uses unsigned ints for bitshift operations
24-
long ugsid = gsid;
25-
long checksum = Crc16.calc(gsid); // + __CFSHR__(v12, 31) + (v12 >> 31); ??
26-
27-
// do while v4 < 10
2849
for(int i = 0; i < output.length; i++) {
29-
index = (int)((ugsid & 0x1F) & 0x1FFFF); // chartable string is unicode, so normally multiplies by 2
30-
ugsid = (ugsid >> 5) | (checksum << 27);
31-
checksum >>= 5;
32-
output[i] = GSID_CHARTABLE.charAt(index); // sub_2048734(v4, chartable + index)
50+
int index = (int)((ugsid >> (5 * i)) & 0x1F);
51+
output[i] = GSID_CHARTABLE.charAt(index);
3352
}
3453

3554
return new String(output);
3655
}
3756

57+
/**
58+
* Determines if a Game Sync ID is valid by checking its length, characters & checksum.
59+
*
60+
* @return {@code true} if the Game Sync ID is valid, otherwise {@code false}.
61+
*/
3862
public static boolean isValidGameSyncId(String gsid) {
39-
return GSID_PATTERN.matcher(gsid).matches();
63+
if(gsid == null) {
64+
return false;
65+
}
66+
67+
int length = gsid.length();
68+
long ugsid = 0;
69+
70+
if(length != 10) {
71+
return false;
72+
}
73+
74+
for(int i = 0; i < length; i++) {
75+
int index = GSID_CHARTABLE.indexOf(gsid.charAt(i));
76+
77+
if(index == -1) {
78+
return false;
79+
}
80+
81+
ugsid |= (long)index << (5 * i);
82+
}
83+
84+
int output = (int)(ugsid & 0xFFFFFFFF);
85+
int checksum = (int)((ugsid >> 32) & 0xFFFF);
86+
return output >= 0 && Crc16.calc(output) == checksum;
4087
}
4188
}

src/test/java/entralinked/utility/GsidUtilityTest.java

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,15 +31,22 @@ void testInvalidGameSyncIds() {
3131
// Illegal length (should be 10)
3232
assertFalse(GsidUtility.isValidGameSyncId("Y67UEN38K"));
3333
assertFalse(GsidUtility.isValidGameSyncId("3ER5K8MBN4C"));
34+
35+
// Invalid checksum
36+
assertFalse(GsidUtility.isValidGameSyncId("VFWM2Q2ADH"));
37+
assertFalse(GsidUtility.isValidGameSyncId("44DAWDA4SH"));
38+
assertFalse(GsidUtility.isValidGameSyncId("J6F55U7FUE"));
39+
assertFalse(GsidUtility.isValidGameSyncId("8FAB4ZF6JF"));
40+
assertFalse(GsidUtility.isValidGameSyncId("HWLNS77HWD"));
3441
}
3542

3643
@Test
3744
@DisplayName("Test if valid Game Sync IDs are seen as valid")
3845
void testValidGameSyncIds() {
3946
assertTrue(GsidUtility.isValidGameSyncId("VFWM2QAXNF"));
4047
assertTrue(GsidUtility.isValidGameSyncId("44DAWDJKJ8"));
41-
assertTrue(GsidUtility.isValidGameSyncId("J6F55UB2X9"));
42-
assertTrue(GsidUtility.isValidGameSyncId("8FAB4Z3EN9"));
48+
assertTrue(GsidUtility.isValidGameSyncId("J6F55UB2XD"));
49+
assertTrue(GsidUtility.isValidGameSyncId("8FAB4Z3END"));
4350
assertTrue(GsidUtility.isValidGameSyncId("HWLNS7BTNB"));
4451
}
4552
}

0 commit comments

Comments
 (0)