Add Memory Link support

This commit is contained in:
kuroppoi
2023-06-26 01:29:26 +02:00
parent 87729ec185
commit aa7da4ec40
2 changed files with 97 additions and 23 deletions

View File

@@ -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<String, Player> 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.
*/

View File

@@ -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());