3 Commits
v1.3.6 ... dev

Author SHA1 Message Date
kuroppoi
d99608ff11 More Mystery Gift shenanigans 2024-07-07 21:38:46 +02:00
kuroppoi
acbb3d7f95 Crappy support for Mystery Gifts (#42) 2024-07-06 23:44:30 +02:00
kuroppoi
abe1b80c2d Store player save & version data on Game Sync data creation (#36) 2024-01-14 22:34:07 +01:00
13 changed files with 318 additions and 146 deletions

View File

@@ -3,5 +3,6 @@ package entralinked.model.dlc;
/** /**
* Simple record for DLC data. * Simple record for DLC data.
*/ */
@Deprecated
public record Dlc(String path, String name, String gameCode, String type, public record Dlc(String path, String name, String gameCode, String type,
int index, int projectedSize, int checksum, boolean checksumEmbedded) {} int index, int projectedSize, int checksum, boolean checksumEmbedded) {}

View File

@@ -6,6 +6,7 @@ import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.InputStreamReader; import java.io.InputStreamReader;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.StandardCopyOption;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
@@ -17,7 +18,9 @@ import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
import entralinked.utility.Crc16; import entralinked.utility.Crc16;
import entralinked.utility.MD5;
@Deprecated
public class DlcList { public class DlcList {
private static final Logger logger = LogManager.getLogger(); private static final Logger logger = LogManager.getLogger();
@@ -27,31 +30,32 @@ public class DlcList {
public DlcList() { public DlcList() {
logger.info("Loading DLC ..."); logger.info("Loading DLC ...");
// Extract defaults if external DLC directory is not present try(BufferedReader reader = new BufferedReader(new InputStreamReader(getClass().getResourceAsStream("/dlc.paths")))) {
if(!dataDirectory.exists()) { reader.lines().forEach(line -> {
logger.info("Extracting default DLC files ..."); String[] segments = line.split("\t");
BufferedReader reader = new BufferedReader(new InputStreamReader(getClass().getResourceAsStream("/dlc.paths")));
String line = null; if(segments.length != 2) {
return;
try {
while((line = reader.readLine()) != null) {
InputStream resource = getClass().getResourceAsStream(line);
File outputFile = new File("./%s".formatted(line));
// Create parent directories
if(outputFile.getParentFile() != null) {
outputFile.getParentFile().mkdirs();
}
// Copy resource to destination
if(resource != null) {
Files.copy(resource, outputFile.toPath());
}
} }
} catch (IOException e) {
logger.error("Could not extract default DLC files", e); String path = segments[0];
return; String hash = segments[1];
} File outputFile = new File("./%s".formatted(path));
if(outputFile.getParentFile() != null) {
outputFile.getParentFile().mkdirs();
}
try(InputStream inputStream = getClass().getResourceAsStream(path)){
if(!outputFile.exists() || !hash.equals(MD5.digest(outputFile))) {
Files.copy(inputStream, outputFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
}
} catch(IOException e) {
logger.error("Couldn't process resource '{}'", path, e);
}
});
} catch(IOException e) {
logger.error("Couldn't extract DLC data", e);
} }
// Just to be sure... // Just to be sure...
@@ -71,7 +75,7 @@ public class DlcList {
for(File subFile : file.listFiles()) { for(File subFile : file.listFiles()) {
// Check if file is directory // Check if file is directory
if(!subFile.isDirectory()) { if(!subFile.isDirectory()) {
logger.warn("Non-directory '{}' in DLC subfolder '{}'", file.getName(), subFile.getName()); logger.warn("Non-directory '{}' in DLC subfolder '{}'", subFile.getName(), file.getName());
continue; continue;
} }

View File

@@ -17,6 +17,8 @@ import org.apache.logging.log4j.Logger;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.databind.SerializationFeature;
import entralinked.GameVersion;
/** /**
* Manager class for managing {@link Player} information (Global Link users) * Manager class for managing {@link Player} information (Global Link users)
*/ */
@@ -175,7 +177,7 @@ public class PlayerManager {
* That is, the specified Game Sync ID wasn't already registered and the player data * That is, the specified Game Sync ID wasn't already registered and the player data
* was saved without any errors. * was saved without any errors.
*/ */
public Player registerPlayer(String gameSyncId) { public Player registerPlayer(String gameSyncId, GameVersion version) {
// Check for duplicate Game Sync ID // Check for duplicate Game Sync ID
if(playerMap.containsKey(gameSyncId)) { if(playerMap.containsKey(gameSyncId)) {
logger.warn("Attempted to register duplicate player {}", gameSyncId); logger.warn("Attempted to register duplicate player {}", gameSyncId);
@@ -193,6 +195,7 @@ public class PlayerManager {
// Construct player object // Construct player object
Player player = new Player(gameSyncId); Player player = new Player(gameSyncId);
player.setStatus(PlayerStatus.AWAKE); player.setStatus(PlayerStatus.AWAKE);
player.setGameVersion(version);
player.setDataDirectory(playerDataDirectory); player.setDataDirectory(playerDataDirectory);
// Try to save player data // Try to save player data

View File

@@ -1,18 +1,17 @@
package entralinked.model.user; package entralinked.model.user;
import java.io.File;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import entralinked.model.dlc.Dlc;
public class User { public class User {
private final String id; private final String id;
private final String password; // I debated hashing it, but.. it's a 3-digit password... private final String password; // I debated hashing it, but.. it's a 3-digit password...
private final Map<String, GameProfile> profiles = new HashMap<>(); private final Map<String, GameProfile> profiles = new HashMap<>();
private final Map<String, Dlc> dlcOverrides = new HashMap<>(); private final Map<String, File> dlcOverrides = new HashMap<>();
private int profileIdOverride; // For making it easier for the user to fix error 60000 private int profileIdOverride; // For making it easier for the user to fix error 60000
public User(String id, String password) { public User(String id, String password) {
@@ -59,11 +58,11 @@ public class User {
return Collections.unmodifiableMap(profiles); return Collections.unmodifiableMap(profiles);
} }
public void setDlcOverride(String type, Dlc target) { public void setDlcOverride(String type, File file) {
if(target == null) { if(file == null) {
dlcOverrides.remove(type); dlcOverrides.remove(type);
} else { } else {
dlcOverrides.put(type, target); dlcOverrides.put(type, file);
} }
} }
@@ -75,7 +74,7 @@ public class User {
return dlcOverrides.containsKey(type); return dlcOverrides.containsKey(type);
} }
public Dlc getDlcOverride(String type) { public File getDlcOverride(String type) {
return dlcOverrides.get(type); return dlcOverrides.get(type);
} }

View File

@@ -1,7 +1,11 @@
package entralinked.network.http.dls; package entralinked.network.http.dls;
import java.io.FileInputStream; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List; import java.util.List;
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.LogManager;
@@ -10,15 +14,14 @@ import org.apache.logging.log4j.Logger;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import entralinked.Entralinked; import entralinked.Entralinked;
import entralinked.model.dlc.Dlc; import entralinked.GameVersion;
import entralinked.model.dlc.DlcList;
import entralinked.model.user.ServiceSession; import entralinked.model.user.ServiceSession;
import entralinked.model.user.User; import entralinked.model.user.User;
import entralinked.model.user.UserManager; import entralinked.model.user.UserManager;
import entralinked.network.http.HttpHandler; import entralinked.network.http.HttpHandler;
import entralinked.network.http.HttpRequestHandler; import entralinked.network.http.HttpRequestHandler;
import entralinked.serialization.UrlEncodedFormFactory; import entralinked.serialization.UrlEncodedFormFactory;
import entralinked.utility.LEOutputStream; import entralinked.utility.MysteryGiftUtility;
import io.javalin.Javalin; import io.javalin.Javalin;
import io.javalin.http.Context; import io.javalin.http.Context;
import io.javalin.http.HttpStatus; import io.javalin.http.HttpStatus;
@@ -30,11 +33,10 @@ public class DlsHandler implements HttpHandler {
private static final Logger logger = LogManager.getLogger(); private static final Logger logger = LogManager.getLogger();
private final ObjectMapper mapper = new ObjectMapper(new UrlEncodedFormFactory()); private final ObjectMapper mapper = new ObjectMapper(new UrlEncodedFormFactory());
private final DlcList dlcList; private final File rootDirectory = new File("dlc");
private final UserManager userManager; private final UserManager userManager;
public DlsHandler(Entralinked entralinked) { public DlsHandler(Entralinked entralinked) {
this.dlcList = entralinked.getDlcList();
this.userManager = entralinked.getUserManager(); this.userManager = entralinked.getUserManager();
} }
@@ -64,6 +66,7 @@ public class DlsHandler implements HttpHandler {
HttpRequestHandler<DlsRequest> handler = switch(request.action()) { HttpRequestHandler<DlsRequest> handler = switch(request.action()) {
case "list" -> this::handleRetrieveDlcList; case "list" -> this::handleRetrieveDlcList;
case "contents" -> this::handleRetrieveDlcContent; case "contents" -> this::handleRetrieveDlcContent;
case "count" -> this::handleRetrieveDlcCount;
default -> throw new IllegalArgumentException("Invalid POST request action: " + request.action()); default -> throw new IllegalArgumentException("Invalid POST request action: " + request.action());
}; };
@@ -80,16 +83,48 @@ public class DlsHandler implements HttpHandler {
private void handleRetrieveDlcList(DlsRequest request, Context ctx) throws IOException { private void handleRetrieveDlcList(DlsRequest request, Context ctx) throws IOException {
User user = ctx.attribute("user"); User user = ctx.attribute("user");
String gameCode = getDlcGameCode(request.dlcGameCode()); String gameCode = getDlcGameCode(request.dlcGameCode());
String type = getRegionlessDlcType(request.dlcType()); String type = getDlcType(request.attr1());
String attr2 = request.attr2();
List<File> files = null;
// If an overriding DLC is present, send the data for that instead.
if(user.hasDlcOverride(type)) { if(user.hasDlcOverride(type)) {
ctx.result(dlcList.getDlcListString(List.of(user.getDlcOverride(type)))); files = Arrays.asList(user.getDlcOverride(type));
return; } else {
// Get list of files in DLC directory
File directory = getDlcDirectory(gameCode, type);
files = directory.isDirectory() ? Arrays.asList(directory.listFiles()) : new ArrayList<>();
} }
// TODO NOTE: I assume that in a conventional implementation, certain DLC attributes may be omitted from the request. if(attr2 != null) {
ctx.result(dlcList.getDlcListString(dlcList.getDlcList(gameCode, type, request.dlcIndex()))); // PGL content attr2 hack
files = Arrays.asList(files.get(Integer.parseInt(attr2) - 1));
} else {
// Mystery Gift randomness
Collections.shuffle(files);
}
StringBuilder builder = new StringBuilder();
int count = Math.min(files.size(), request.num());
// Create DLC list string
for(int i = 0; i < count; i++) {
File file = files.get(i);
if(type == null) {
// Generation 4 Mystery Gift
builder.append("%s\t\t\t\t\t%s\r\n".formatted(file.getName(), 936));
} else if(type.equals("MYSTERY")) {
// Generation 5 Mystery Gift
String gameFlag = GameVersion.lookup(request.gameCode()).isVersion2() ? "F00000" : "300000";
builder.append("%s\t\t%s\t%s\t\t%s\r\n".formatted(file.getName(), type, gameFlag, 720));
} else {
// PGL content
builder.append("%s\t\t%s\t%s\t\t%s\r\n".formatted(file.getName(), type, i + 1, file.length()));
}
}
// Send result
ctx.result(builder.toString());
} }
/** /**
@@ -98,33 +133,45 @@ public class DlsHandler implements HttpHandler {
private void handleRetrieveDlcContent(DlsRequest request, Context ctx) throws IOException { private void handleRetrieveDlcContent(DlsRequest request, Context ctx) throws IOException {
User user = ctx.attribute("user"); User user = ctx.attribute("user");
String gameCode = getDlcGameCode(request.dlcGameCode()); String gameCode = getDlcGameCode(request.dlcGameCode());
String type = getRegionlessDlcType(request.dlcType()); String type = getDlcType(request.attr1());
Dlc dlc = user.hasDlcOverride(type) ? user.getDlcOverride(type) : dlcList.getDlc(gameCode, type, request.dlcName()); File file = user.hasDlcOverride(type) ? user.getDlcOverride(type) : type != null
? new File(rootDirectory, "%s/%s/%s".formatted(gameCode, type, request.dlcName()))
: new File(rootDirectory, "%s/%s".formatted(gameCode, request.dlcName()));
// Check if the requested DLC exists // Check if the requested DLC exists
if(dlc == null) { if(!file.exists()) {
ctx.status(HttpStatus.NOT_FOUND); ctx.status(HttpStatus.NOT_FOUND);
return; return;
} }
// Write DLC data byte[] bytes = Files.readAllBytes(file.toPath());
try(FileInputStream inputStream = new FileInputStream(dlc.path())) {
LEOutputStream outputStream = new LEOutputStream(ctx.outputStream()); if(type == null) {
inputStream.transferTo(outputStream); // Generation 4 Mystery Gift
bytes = MysteryGiftUtility.createUniversalGiftData4(bytes, file.getName());
// If checksum is not part of the file, manually append it } else if(type.equals("MYSTERY")) {
if(!dlc.checksumEmbedded()) { // Generation 5 Mystery Gift
outputStream.writeShort(dlc.checksum()); bytes = MysteryGiftUtility.createUniversalGiftData5(bytes);
}
} }
// Send result
ctx.result(bytes);
}
/**
* POST handler for {@code /download action=count}
*/
private void handleRetrieveDlcCount(DlsRequest request, Context ctx) throws IOException {
ctx.result("1"); // TODO
} }
/** /**
* @return The game serial that should be used for downloading DLC based on the provided input. * @return The game serial that should be used for downloading DLC based on the provided input.
*/ */
private String getDlcGameCode(String gameCode) { private String getDlcGameCode(String gameCode) {
return switch(gameCode) { return switch(gameCode.substring(0, 3)) {
case "IRAJ" -> "IRAO"; case "IRA" -> "IRAO"; // BW & B2W2
case "ADA", "CPU", "IPG" -> "ADAE"; // DPPt & HGSS
default -> gameCode; default -> gameCode;
}; };
} }
@@ -132,13 +179,15 @@ public class DlsHandler implements HttpHandler {
/** /**
* @return The DLC type without the region identifier, or the input if it is an unknown type. * @return The DLC type without the region identifier, or the input if it is an unknown type.
*/ */
private String getRegionlessDlcType(String dlcType) { private String getDlcType(String attr1) {
return switch(dlcType) { if(attr1 == null || !attr1.contains("_")) {
case "CGEAR_E", "CGEAR_F", "CGEAR_I", "CGEAR_G", "CGEAR_S", "CGEAR_J", "CGEAR_K" -> "CGEAR"; return attr1;
case "CGEAR2_E", "CGEAR2_F", "CGEAR2_I", "CGEAR2_G", "CGEAR2_S", "CGEAR2_J", "CGEAR2_K" -> "CGEAR2"; }
case "ZUKAN_E", "ZUKAN_F", "ZUKAN_I", "ZUKAN_G", "ZUKAN_S", "ZUKAN_J", "ZUKAN_K" -> "ZUKAN";
case "MUSICAL_E", "MUSICAL_F", "MUSICAL_I", "MUSICAL_G", "MUSICAL_S", "MUSICAL_J", "MUSICAL_K" -> "MUSICAL"; return attr1.substring(0, attr1.lastIndexOf('_'));
default -> dlcType; }
};
private File getDlcDirectory(String gameCode, String dlcType) {
return dlcType == null ? new File(rootDirectory, gameCode) : new File(rootDirectory, "%s/%s".formatted(gameCode, dlcType));
} }
} }

View File

@@ -19,14 +19,14 @@ public record DlsRequest(
@JsonProperty(value = "action", required = true) String action, @JsonProperty(value = "action", required = true) String action,
@JsonProperty("gamecd") String dlcGameCode, @JsonProperty("gamecd") String dlcGameCode,
@JsonProperty("contents") String dlcName, // action=contents @JsonProperty("contents") String dlcName, // action=contents
@JsonProperty("attr1") String dlcType, // action=list @JsonProperty("attr1") String attr1, // action=list
@JsonProperty("attr2") int dlcIndex, // action=list @JsonProperty("attr2") String attr2, // action=list
@JsonProperty("offset") int offset, // Start offset in the list @JsonProperty("offset") int offset, // Start offset in the list
@JsonProperty("num") int num) { // Number of entries @JsonProperty("num") int num) { // Number of entries
@Override @Override
public String toString() { public String toString() {
return ("DlsRequest[gameCode=%s, action=%s, dlcGameCode=%s, dlcName=%s, dlcType=%s, dlcIndex=%s, offset=%s, num=%s]") return ("DlsRequest[gameCode=%s, action=%s, dlcGameCode=%s, dlcName=%s, attr1=%s, attr2=%s, offset=%s, num=%s]")
.formatted(gameCode, action, dlcGameCode, dlcName, dlcType, dlcIndex, offset, num); .formatted(gameCode, action, dlcGameCode, dlcName, attr1, attr2, offset, num);
} }
} }

View File

@@ -1,5 +1,6 @@
package entralinked.network.http.pgl; package entralinked.network.http.pgl;
import java.io.ByteArrayInputStream;
import java.io.File; import java.io.File;
import java.io.FileInputStream; import java.io.FileInputStream;
import java.io.IOException; import java.io.IOException;
@@ -16,7 +17,6 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import entralinked.Configuration; import entralinked.Configuration;
import entralinked.Entralinked; import entralinked.Entralinked;
import entralinked.model.avenue.AvenueVisitor; import entralinked.model.avenue.AvenueVisitor;
import entralinked.model.dlc.Dlc;
import entralinked.model.dlc.DlcList; import entralinked.model.dlc.DlcList;
import entralinked.model.pkmn.PkmnInfo; import entralinked.model.pkmn.PkmnInfo;
import entralinked.model.pkmn.PkmnInfoReader; import entralinked.model.pkmn.PkmnInfoReader;
@@ -225,8 +225,7 @@ public class PglHandler implements HttpHandler {
// Create or remove custom C-Gear skin DLC override // Create or remove custom C-Gear skin DLC override
if("custom".equals(cgearSkin)) { if("custom".equals(cgearSkin)) {
cgearSkinIndex = 1; cgearSkinIndex = 1;
user.setDlcOverride(cgearType, new Dlc(player.getCGearSkinFile().getAbsolutePath(), user.setDlcOverride(cgearType, player.getCGearSkinFile());
"custom", "IRAO", cgearType, cgearSkinIndex, 9730, 0, true));
} else { } else {
cgearSkinIndex = dlcList.getDlcIndex("IRAO", cgearType, cgearSkin); cgearSkinIndex = dlcList.getDlcIndex("IRAO", cgearType, cgearSkin);
user.removeDlcOverride(cgearType); user.removeDlcOverride(cgearType);
@@ -235,8 +234,7 @@ public class PglHandler implements HttpHandler {
// Create or remove custom Pokédex skin DLC override // Create or remove custom Pokédex skin DLC override
if("custom".equals(dexSkin)) { if("custom".equals(dexSkin)) {
dexSkinIndex = 1; dexSkinIndex = 1;
user.setDlcOverride("ZUKAN", new Dlc(player.getDexSkinFile().getAbsolutePath(), user.setDlcOverride("ZUKAN", player.getDexSkinFile());
"custom", "IRAO", "ZUKAN", dexSkinIndex, 25090, 0, true));
} else { } else {
dexSkinIndex = dlcList.getDlcIndex("IRAO", "ZUKAN", dexSkin); dexSkinIndex = dlcList.getDlcIndex("IRAO", "ZUKAN", dexSkin);
user.removeDlcOverride("ZUKAN"); user.removeDlcOverride("ZUKAN");
@@ -313,6 +311,7 @@ public class PglHandler implements HttpHandler {
byte[] nameBytes = visitor.name().getBytes(StandardCharsets.UTF_16LE); byte[] nameBytes = visitor.name().getBytes(StandardCharsets.UTF_16LE);
outputStream.write(nameBytes, 0, Math.min(14, nameBytes.length)); outputStream.write(nameBytes, 0, Math.min(14, nameBytes.length));
outputStream.writeBytes(-1, 14 - nameBytes.length); outputStream.writeBytes(-1, 14 - nameBytes.length);
outputStream.writeShort(0xFF); // Null terminator
// Full visitor type consists of a trainer class and what I call a 'personality' index // Full visitor type consists of a trainer class and what I call a 'personality' index
// that, along with the trainer class, determines which phrases the visitor uses. // that, along with the trainer class, determines which phrases the visitor uses.
@@ -321,7 +320,6 @@ public class PglHandler implements HttpHandler {
// For example, if the visitor type is '0', then shop type '0' would be a raffle. // For example, if the visitor type is '0', then shop type '0' would be a raffle.
// However, if the visitor type is '2', then shop type '0' results in a dojo instead. // However, if the visitor type is '2', then shop type '0' results in a dojo instead.
int visitorType = visitor.type().getClientId() + visitor.personality() * 8; int visitorType = visitor.type().getClientId() + visitor.personality() * 8;
outputStream.writeShort(-1); // Does nothing, seems to be read as part of the name.
outputStream.write(visitorType); outputStream.write(visitorType);
outputStream.write(visitor.shopType().ordinal() + (7 - visitorType * 2 % 7)); outputStream.write(visitor.shopType().ordinal() + (7 - visitorType * 2 % 7));
outputStream.writeShort(0); // Does nothing outputStream.writeShort(0); // Does nothing
@@ -355,6 +353,12 @@ public class PglHandler implements HttpHandler {
return; return;
} }
// Version null check because this can happen in specific cases
if(player.getGameVersion() == null) {
writeStatusCode(outputStream, 5); // No game save data exists for this Game Sync ID
return;
}
// Check if the save file belongs to Black or White // Check if the save file belongs to Black or White
if(player.getGameVersion().isVersion2()) { if(player.getGameVersion().isVersion2()) {
writeStatusCode(outputStream, 10); // Not a Black or White save writeStatusCode(outputStream, 10); // Not a Black or White save
@@ -485,13 +489,9 @@ public class PglHandler implements HttpHandler {
/** /**
* POST handler for {@code /dsio/gw?p=account.create.upload} * POST handler for {@code /dsio/gw?p=account.create.upload}
*/ */
private void handleCreateAccount(PglRequest request, Context ctx) throws IOException { private void handleCreateAccount(PglRequest request, Context ctx) throws IOException {
// It sends the entire save file, but we just skip through it because we don't need anything from it here // Have to read all the bytes first for some reason
ServletInputStream inputStream = ctx.req().getInputStream(); byte[] bytes = ctx.bodyAsBytes();
while(!inputStream.isFinished()) {
inputStream.read();
}
// Prepare response // Prepare response
LEOutputStream outputStream = new LEOutputStream(ctx.outputStream()); LEOutputStream outputStream = new LEOutputStream(ctx.outputStream());
@@ -509,11 +509,19 @@ public class PglHandler implements HttpHandler {
} }
// Try to register player // Try to register player
if(playerManager.registerPlayer(request.gameSyncId()) == null) { Player player = playerManager.registerPlayer(request.gameSyncId(), request.gameVersion());
if(player == null) {
writeStatusCode(outputStream, 3); // Registration error writeStatusCode(outputStream, 3); // Registration error
return; return;
} }
// Try to store save data
if(!playerManager.storePlayerGameSaveFile(player, new ByteArrayInputStream(bytes))) {
writeStatusCode(outputStream, 4); // Game save data IO error
return;
}
// Write status code // Write status code
writeStatusCode(outputStream, 0); writeStatusCode(outputStream, 0);
} }
@@ -534,7 +542,8 @@ public class PglHandler implements HttpHandler {
} }
// Try to register player // Try to register player
if(playerManager.registerPlayer(gameSyncId) == null) { // Regrettably, this request does not contain game save & version data.
if(playerManager.registerPlayer(gameSyncId, null) == null) {
writeStatusCode(outputStream, 3); // Registration error writeStatusCode(outputStream, 3); // Registration error
return; return;
} }

View File

@@ -1,6 +1,9 @@
package entralinked.utility; package entralinked.utility;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.security.MessageDigest; import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
@@ -18,12 +21,19 @@ public class MD5 {
private static MessageDigest digest; private static MessageDigest digest;
/** /**
* @return A hex-formatted MD5 hash of the specified input. * @return A hex-formatted MD5 hash of the specified input string.
*/ */
public static String digest(String string) { public static String digest(String string) {
return StringUtil.toHexStringPadded(digest(string.getBytes(StandardCharsets.ISO_8859_1))); return StringUtil.toHexStringPadded(digest(string.getBytes(StandardCharsets.ISO_8859_1)));
} }
/**
* @return A hex-formatted MD5 hash of the specified input file.
*/
public static String digest(File file) throws IOException {
return StringUtil.toHexStringPadded(digest(Files.readAllBytes(file.toPath())));
}
/** /**
* @return An MD5 hash of the specified input. * @return An MD5 hash of the specified input.
*/ */

View File

@@ -0,0 +1,94 @@
package entralinked.utility;
import java.nio.charset.StandardCharsets;
import org.bouncycastle.util.Arrays;
/**
* Haphazardly thrown-together utility for generating DLS Mystery Gift data & removing version/language locks.
*/
public class MysteryGiftUtility {
public static final String DPPGS_CHARTABLE = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿŒœŞşªºááá$¡¿!?,.…·/'“”„«»()♂♀+-*#=&~:;♠♣♥♦★◉○□△◇@♪%ááááááááááá ";
private static byte[] encodeDPPGS(String string) {
int length = string.length();
byte[] bytes = new byte[length * 2];
for(int i = 0; i < length; i++) {
char character = string.charAt(i);
int encoded = character == '\n' ? 0xE000 : DPPGS_CHARTABLE.indexOf(character) + 0x121;
if(encoded == 0x120) {
encoded = 0x1DE;
}
bytes[i * 2] = (byte)(encoded & 0xFF);
bytes[i * 2 + 1] = (byte)((encoded >> 8) & 0xFF);
}
return bytes;
}
public static byte[] createUniversalGiftData4(byte[] bytes) {
return createUniversalGiftData4(bytes, "Here's your Mystery Gift.\nEnjoy!");
}
public static byte[] createUniversalGiftData4(byte[] bytes, String title) {
// Check data size
if(bytes.length > 936) {
throw new IllegalArgumentException("Data too large: %s".formatted(bytes.length));
}
byte[] result = new byte[936];
if(bytes.length <= 856) {
// Create gift title data
System.arraycopy(bytes, 0x00, result, 0x50, bytes.length);
Arrays.fill(result, 0x00, 0x48, (byte)0xFF);
byte[] titleBytes = encodeDPPGS(title);
System.arraycopy(titleBytes, 0, result, 0, Math.min(0x48, titleBytes.length));
// Wonder card index (prevents duplicate redemptions)
int id = Crc16.calc(result, 0x00, 0x3A8); // Let's just use the checksum for now
result[0x4C] = (byte)(id & 0xFF);
result[0x4D] = (byte)((id >> 8) & 0xFF);
// Wonder card present flag?
result[0x4E] = (byte)(bytes.length == 0x358 ? 0x0D : 0x00);
} else {
System.arraycopy(bytes, 0, result, 0, bytes.length);
}
// Clear game version
result[0x48] = 0;
result[0x49] = 0;
return result;
}
public static byte[] createUniversalGiftData5(byte[] bytes) {
// Check data size
if(bytes.length > 720) {
throw new IllegalArgumentException("Data too large: %s".formatted(bytes.length));
}
byte[] result = new byte[720];
System.arraycopy(bytes, 0, result, 0, bytes.length);
result[0xCE] = 0; // Version flag
result[0x2CB] = 0; // Language code
// Create standard gift description if there is none
if(bytes.length == 204) {
Arrays.fill(result, 0xD0, 0x2CA, (byte)0xFF);
String description = "No description is available for this gift.";
byte[] descriptionBytes = description.replace('\n', '\uFFFE').getBytes(StandardCharsets.UTF_16LE);
System.arraycopy(descriptionBytes, 0, result, 0xD0, Math.min(0x1FA, descriptionBytes.length));
}
// Recalculate checksum
int checksum = Crc16.calc(result, 0, 0x2CE);
result[0x2CE] = (byte)(checksum & 0xFF);
result[0x2CF] = (byte)((checksum >> 8) & 0xFF);
return result;
}
}

View File

@@ -1,59 +1,62 @@
/dlc/IRAO/CGEAR/01 - Default.bin /dlc/IRAO/CGEAR/01 - Default.bin 19c71995ffb00f4bdc59bfa805eee8fa
/dlc/IRAO/CGEAR/02 - Meadow Munna.bin /dlc/IRAO/CGEAR/02 - Meadow Munna.bin d434decfee8faad9485560fbc9206273
/dlc/IRAO/CGEAR/03 - Aim for the Top.bin /dlc/IRAO/CGEAR/03 - Aim for the Top.bin c93f457ae1310bfe03f7ede622c7f63a
/dlc/IRAO/CGEAR/04 - Twinkle Minccino.bin /dlc/IRAO/CGEAR/04 - Twinkle Minccino.bin 86902dc9ec625e05673697367d492898
/dlc/IRAO/CGEAR/05 - PokeSma Purrloin.bin /dlc/IRAO/CGEAR/05 - PokeSma Purrloin.bin 0efe27a2cf7e48de2273ccba0fda2031
/dlc/IRAO/CGEAR/06 - Earful Audino.bin /dlc/IRAO/CGEAR/06 - Earful Audino.bin ee383864a67e6a04235c7f6a891a9539
/dlc/IRAO/CGEAR/07 - Happy Piplup.bin /dlc/IRAO/CGEAR/07 - Happy Piplup.bin 88d0dfac0dda854486b8b5fb839e0bfd
/dlc/IRAO/CGEAR/08 - Poison Jab Croagunk.bin /dlc/IRAO/CGEAR/08 - Poison Jab Croagunk.bin 7294c3e4db163beb98ea49be10d8eb2a
/dlc/IRAO/CGEAR/09 - Venusaur!.bin /dlc/IRAO/CGEAR/09 - Venusaur!.bin 7c51d55a63117bb166cc507f064f7cca
/dlc/IRAO/CGEAR/10 - Charizard!.bin /dlc/IRAO/CGEAR/10 - Charizard!.bin a8ccb7866393efda3422656aa2d8f292
/dlc/IRAO/CGEAR/11 - Blastoise!.bin /dlc/IRAO/CGEAR/11 - Blastoise!.bin e0a1b44450407af394f390c0b229e7a6
/dlc/IRAO/CGEAR/12 - V for Victory!.bin /dlc/IRAO/CGEAR/12 - V for Victory!.bin 847d6d230fe2513758d50adac52d54b8
/dlc/IRAO/CGEAR/13 - Virtual Pokemon.bin /dlc/IRAO/CGEAR/13 - Virtual Pokemon.bin 002263910ed018ff04922c4f3971b777
/dlc/IRAO/CGEAR/14 - Pokemon Cafe.bin /dlc/IRAO/CGEAR/14 - Pokemon Cafe.bin efd6e6de86ea892dda840bcc6952afc1
/dlc/IRAO/CGEAR/15 - Hero Reshiram.bin /dlc/IRAO/CGEAR/15 - Hero Reshiram.bin d89ceeb9a9e7774fa802c1aa2f38409a
/dlc/IRAO/CGEAR/16 - Hero Zekrom.bin /dlc/IRAO/CGEAR/16 - Hero Zekrom.bin 2548f0ed48e130649ff351ca418e3b7f
/dlc/IRAO/CGEAR/17 - 2011 Worlds C-Gear.bin /dlc/IRAO/CGEAR/17 - 2011 Worlds C-Gear.bin 70119e0a5e67fb4c9dda7b724e157784
/dlc/IRAO/CGEAR/18 - Zoroark!.bin /dlc/IRAO/CGEAR/18 - Zoroark!.bin 81e9241acb3270e2d490dd9966f3ecf1
/dlc/IRAO/CGEAR/19 - Spring Deerling.bin /dlc/IRAO/CGEAR/19 - Spring Deerling.bin ca785e48d49269a182ee0fe0981168a8
/dlc/IRAO/CGEAR/20 - Summer Deerling.bin /dlc/IRAO/CGEAR/20 - Summer Deerling.bin 0e8b6b527a411aeb302cfe07cb3679b2
/dlc/IRAO/CGEAR/21 - Autumn Deerling.bin /dlc/IRAO/CGEAR/21 - Autumn Deerling.bin 7155fca4dc915fc6cb87495fa7a9c743
/dlc/IRAO/CGEAR/22 - Winter Deerling.bin /dlc/IRAO/CGEAR/22 - Winter Deerling.bin 7f9d3448f40d0ea513c1f94e311e77bb
/dlc/IRAO/CGEAR/23 - CRUSTLE!.bin /dlc/IRAO/CGEAR/23 - CRUSTLE!.bin eb1a0cb83e330375c257cb61bbc80e84
/dlc/IRAO/CGEAR/24 - KLINK!.bin /dlc/IRAO/CGEAR/24 - KLINK!.bin f4ebe5cbb0de53c44f0c620215e840b4
/dlc/IRAO/CGEAR/25 - Ducklett Friends.bin /dlc/IRAO/CGEAR/25 - Ducklett Friends.bin bb359435efd5e98f48beba2cf71126d9
/dlc/IRAO/CGEAR/26 - Guidance Cobalion.bin /dlc/IRAO/CGEAR/26 - Guidance Cobalion.bin fcd86f0f98b1d770e28001c02f455c58
/dlc/IRAO/CGEAR/27 - Trial Terrakion.bin /dlc/IRAO/CGEAR/27 - Trial Terrakion.bin 6c57b06edcaa0aeb6b716ecd5d3f2336
/dlc/IRAO/CGEAR/28 - Rumination Virizion.bin /dlc/IRAO/CGEAR/28 - Rumination Virizion.bin b02dce72dfd25a4c8d417b87f3a99329
/dlc/IRAO/CGEAR/29 - Keldeo Ordinary Forme.bin /dlc/IRAO/CGEAR/29 - Keldeo Ordinary Forme.bin a83088c9cf11277049ed472d302b0dd6
/dlc/IRAO/CGEAR/30 - Aria of the Night Sky.bin /dlc/IRAO/CGEAR/30 - Aria of the Night Sky.bin 4306211f215bb0d397c92037812d37ae
/dlc/IRAO/CGEAR/31 - 2012 Worlds C-Gear.bin /dlc/IRAO/CGEAR/31 - 2012 Worlds C-Gear.bin 225505e34dab2ccd300e3b4bd7eda47f
/dlc/IRAO/CGEAR/32 - Red Genesect.bin /dlc/IRAO/CGEAR/32 - Red Genesect.bin 614be39e965dab594a5ffaac6bd5ae82
/dlc/IRAO/CGEAR2/01 - Default.bin /dlc/IRAO/CGEAR/33 - PCN Cup 2024 Summer.bin f51d9e9045b51dcc1d02cc8091c520e7
/dlc/IRAO/CGEAR2/02 - Meadow Munna.bin /dlc/IRAO/CGEAR2/01 - Default.bin d7ab435f5016ac3e6a7f444ccd25f424
/dlc/IRAO/CGEAR2/03 - Keldeo Resolute Forme.bin /dlc/IRAO/CGEAR2/02 - Meadow Munna.bin 98b944531d9206512bbc5f59f8229139
/dlc/IRAO/CGEAR2/04 - 2012 Worlds C-Gear.bin /dlc/IRAO/CGEAR2/03 - Keldeo Resolute Forme.bin e7bddaf878faa5b009caee7d96998954
/dlc/IRAO/CGEAR2/05 - Pumpkin Pikachu.bin /dlc/IRAO/CGEAR2/04 - 2012 Worlds C-Gear.bin 36d5948c310428b251b70ec1dcf0845a
/dlc/IRAO/CGEAR2/06 - V for Victory!.bin /dlc/IRAO/CGEAR2/05 - Pumpkin Pikachu.bin 7fd54cd2bd5310c40139b4329124aa2d
/dlc/IRAO/CGEAR2/07 - Hero Reshiram.bin /dlc/IRAO/CGEAR2/06 - V for Victory!.bin 4f9a1ca435561cfe49d892488b398caa
/dlc/IRAO/CGEAR2/08 - Hero Zekrom.bin /dlc/IRAO/CGEAR2/07 - Hero Reshiram.bin ec233268fb4d04cbb13b8b944552e94b
/dlc/IRAO/CGEAR2/09 - Aria of the Night Sky.bin /dlc/IRAO/CGEAR2/08 - Hero Zekrom.bin 4647c982bbbd3955b4d8e4c9309ff7ef
/dlc/IRAO/CGEAR2/10 - Sleeping Eevee.bin /dlc/IRAO/CGEAR2/09 - Aria of the Night Sky.bin 2f609aeda35b34b6386cfa66d57e0ad5
/dlc/IRAO/CGEAR2/11 - Venusaur!.bin /dlc/IRAO/CGEAR2/10 - Sleeping Eevee.bin 4882e949ae9c6de8cb2735942f0b1624
/dlc/IRAO/CGEAR2/12 - Charizard!.bin /dlc/IRAO/CGEAR2/11 - Venusaur!.bin 21becbbbba091cb1060ed19c4436d98f
/dlc/IRAO/CGEAR2/13 - Blastoise!.bin /dlc/IRAO/CGEAR2/12 - Charizard!.bin 75705d719bae83ac130e03598d85382d
/dlc/IRAO/CGEAR2/14 - Red Genesect.bin /dlc/IRAO/CGEAR2/13 - Blastoise!.bin 4f41e613d79826f48b8f8b9d1aa5747b
/dlc/IRAO/CGEAR2/15 - Black Kyurem.bin /dlc/IRAO/CGEAR2/14 - Red Genesect.bin 0d0951b899bff3d9e243a9dc61aff973
/dlc/IRAO/CGEAR2/16 - White Kyurem.bin /dlc/IRAO/CGEAR2/15 - Black Kyurem.bin ff4356dac514c364557bd685c932f43e
/dlc/IRAO/MUSICAL/01 - Charming Munna.bin /dlc/IRAO/CGEAR2/16 - White Kyurem.bin e51c69805f87a954a7fd563b920cc036
/dlc/IRAO/MUSICAL/02 - MELOETTAAA!!!.bin /dlc/IRAO/CGEAR2/17 - PCN Cup 2024 Summer.bin e925ddca5d9a52c49a3463916f6fcc0a
/dlc/IRAO/ZUKAN/01 - Default (Pink).bin /dlc/IRAO/MUSICAL/01 - Charming Munna.bin dd180c539b51305244c10380266ec2ac
/dlc/IRAO/ZUKAN/02 - Default (Red).bin /dlc/IRAO/MUSICAL/02 - MELOETTAAA!!!.bin f1a18eaf27c60b43020e9d5c0195eaed
/dlc/IRAO/ZUKAN/03 - Unova Trio (Pink).bin /dlc/IRAO/ZUKAN/01 - Default (Pink).bin 7150719a18ffacef993c58b112cfaf52
/dlc/IRAO/ZUKAN/04 - Unova Trio (Red).bin /dlc/IRAO/ZUKAN/02 - Default (Red).bin bc396d6403d9d296c1b4700d31b6eb42
/dlc/IRAO/ZUKAN/05 - Kanto Trio (Pink).bin /dlc/IRAO/ZUKAN/03 - Unova Trio (Pink).bin 2e80bbc0e824c9c17fb0b5480d6bcb7c
/dlc/IRAO/ZUKAN/06 - Kanto Trio (Red).bin /dlc/IRAO/ZUKAN/04 - Unova Trio (Red).bin b3e3eadbad327a8c4b3889768f79fd8e
/dlc/IRAO/ZUKAN/07 - Hugh.bin /dlc/IRAO/ZUKAN/05 - Kanto Trio (Pink).bin abbe82e2ff4c66d0557b9989da3f5c29
/dlc/IRAO/ZUKAN/08 - Bianca.bin /dlc/IRAO/ZUKAN/06 - Kanto Trio (Red).bin 7ba1f19f564e271d2e92810e174cd75e
/dlc/IRAO/ZUKAN/09 - Cheren.bin /dlc/IRAO/ZUKAN/07 - Hugh.bin 2f74e77b34edc1758226778e9f5e91fd
/dlc/IRAO/ZUKAN/08 - Bianca.bin f5c57f66af3e096c65850ee47882be90
/dlc/IRAO/ZUKAN/09 - Cheren.bin 2dad78cb515505a5de8022bbd8ecd2ca
/dlc/IRAO/MYSTERY/PCN Cup 2024 Summer.bin fed0ff82be6903777b2f107ce00e8e4c