diff --git a/src/main/java/entralinked/model/player/PlayerManager.java b/src/main/java/entralinked/model/player/PlayerManager.java index 2d0d299..733b21e 100644 --- a/src/main/java/entralinked/model/player/PlayerManager.java +++ b/src/main/java/entralinked/model/player/PlayerManager.java @@ -1,7 +1,9 @@ package entralinked.model.player; import java.io.File; +import java.io.FileOutputStream; import java.io.IOException; +import java.io.InputStream; import java.nio.file.Files; import java.util.Collection; import java.util.Collections; @@ -25,6 +27,7 @@ public class PlayerManager { private final ObjectMapper mapper = new ObjectMapper().enable(SerializationFeature.INDENT_OUTPUT); private final Map playerMap = new ConcurrentHashMap<>(); private final File dataDirectory = new File("players"); + private final File gameSaveDirectory = new File(dataDirectory, "savedata"); public PlayerManager() { logger.info("Loading player data ..."); @@ -137,6 +140,37 @@ public class PlayerManager { return player; } + /** + * Stores a game save file read from the specified input stream to a file. + * The output file is generated as follows: + * + * {@code new File(gameSaveDirectory, "SAV-%s.bin".formatted(gameSyncId))} + * + * @return {@code true} if the game save file was written successfully, otherwise {@code false}. + */ + public boolean storePlayerGameSaveFile(Player player, InputStream inputStream) { + // Try create directories + if(!gameSaveDirectory.exists() && !gameSaveDirectory.mkdirs()) { + return false; + } + + // Write the save data + try(FileOutputStream outputStream = new FileOutputStream(getPlayerGameSaveFile(player))) { + inputStream.transferTo(outputStream); + return true; + } catch(IOException e) { + logger.error("Could not write game save data for player {}", player.getGameSyncId()); + return false; + } + } + + /** + * @return The game save data {@link File} object associated with this player. + */ + public File getPlayerGameSaveFile(Player player) { + return new File(gameSaveDirectory, "SAV-%s.bin".formatted(player.getGameSyncId())); + } + /** * @return {@code true} if a player with the specified Game Sync ID exists, {@code false} otherwise. */ diff --git a/src/main/java/entralinked/network/http/pgl/PglHandler.java b/src/main/java/entralinked/network/http/pgl/PglHandler.java index 7329969..c2613ae 100644 --- a/src/main/java/entralinked/network/http/pgl/PglHandler.java +++ b/src/main/java/entralinked/network/http/pgl/PglHandler.java @@ -1,5 +1,7 @@ package entralinked.network.http.pgl; +import java.io.File; +import java.io.FileInputStream; import java.io.IOException; import java.util.HashSet; import java.util.List; @@ -118,6 +120,7 @@ public class PglHandler implements HttpHandler { case "sleepily.bitlist" -> this::handleGetSleepyList; case "account.playstatus" -> this::handleGetAccountStatus; case "savedata.download" -> this::handleDownloadSaveData; + case "savedata.getbw" -> this::handleMemoryLink; default -> throw new IllegalArgumentException("Invalid GET request type: " + request.type()); }; @@ -245,6 +248,40 @@ public class PglHandler implements HttpHandler { outputStream.writeBytes(0, (20 - items.size())); } + /** + * GET handler for {@code /dsio/gw?p=savedata.getbw} + */ + private void handleMemoryLink(PglRequest request, Context ctx) throws IOException { + LEOutputStream outputStream = new LEOutputStream(ctx.outputStream()); + Player player = playerManager.getPlayer(request.gameSyncId()); + + // Check if player exists + if(player == null) { + writeStatusCode(outputStream, 8); // Invalid Game Sync ID + return; + } + + // Check if the save file belongs to Black or White + if(player.getGameVersion().isVersion2()) { + writeStatusCode(outputStream, 10); // Not a Black or White save + return; + } + + // Check if the game save data exists + File file = playerManager.getPlayerGameSaveFile(player); + + if(!file.exists()) { + writeStatusCode(outputStream, 5); // No game save data exists for this Game Sync ID + return; + } + + // Send the save data! + try(FileInputStream inputStream = new FileInputStream(file)) { + writeStatusCode(outputStream, 0); + inputStream.transferTo(outputStream); + } + } + /** * POST base handler for {@code /dsio/gw} */ @@ -295,41 +332,44 @@ public class PglHandler implements HttpHandler { * POST handler for {@code /dsio/gw?p=savedata.upload} */ private void handleUploadSaveData(PglRequest request, Context ctx) throws IOException { - // Read save data - ServletInputStream inputStream = ctx.req().getInputStream(); - inputStream.skip(0x1D300); // Skip to dream world data - inputStream.skip(8); // Skip to Pokémon data - PkmnInfo info = PkmnInfoReader.readPokeInfo(inputStream); - - // Don't care about anything else -- continue reading bytes until we're done. - while(!inputStream.isFinished()) { - inputStream.read(); - } - // Prepare response LEOutputStream outputStream = new LEOutputStream(ctx.outputStream()); + + // Check if player exists, has the same game version and does not already have a Pokémon tucked in Player player = playerManager.getPlayer(request.gameSyncId()); - // Check if player exists - if(player == null) { + if(player == null || player.getGameVersion() != request.gameVersion() + || (player.getStatus() != PlayerStatus.AWAKE && !configuration.allowOverwritingPlayerDreamInfo())) { + // Skip everything + ServletInputStream inputStream = ctx.req().getInputStream(); + + while(!inputStream.isFinished()) { + inputStream.read(); + } + + // Write error response writeStatusCode(outputStream, 1); // Unauthorized return; } - // Check if player doesn't already have a Pokémon tucked in - if(player.getStatus() != PlayerStatus.AWAKE) { - logger.warn("Player {} tried to upload save data while already asleep", player.getGameSyncId()); - - // Return error if not allowed - if(!configuration.allowOverwritingPlayerDreamInfo()) { - writeStatusCode(outputStream, 4); // Already dreaming - return; - } + // Try to store save data + if(!playerManager.storePlayerGameSaveFile(player, ctx.bodyInputStream())) { + writeStatusCode(outputStream, 4); // Game save data IO error + return; + } + + // Read save data + PkmnInfo dreamerInfo = null; + + try(FileInputStream inputStream = new FileInputStream(playerManager.getPlayerGameSaveFile(player))) { + inputStream.skip(0x1D300); // Skip to dream world data + inputStream.skip(8); // Skip to Pokémon data + dreamerInfo = PkmnInfoReader.readPokeInfo(inputStream); } // Update and save player information player.setStatus(PlayerStatus.SLEEPING); - player.setDreamerInfo(info); + player.setDreamerInfo(dreamerInfo); if(!playerManager.savePlayer(player)) { logger.warn("Save data failure for player {}", player.getGameSyncId());