diff --git a/src/main/java/entralinked/gui/view/MainView.java b/src/main/java/entralinked/gui/view/MainView.java index 6a542f2..2c56521 100644 --- a/src/main/java/entralinked/gui/view/MainView.java +++ b/src/main/java/entralinked/gui/view/MainView.java @@ -7,10 +7,15 @@ import java.awt.Dimension; import java.awt.Font; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; +import java.io.File; +import java.io.FileInputStream; import java.io.IOException; import java.net.URISyntaxException; import java.net.URL; +import java.nio.file.Files; +import java.nio.file.StandardCopyOption; import java.util.List; +import java.util.Objects; import java.util.concurrent.CompletableFuture; import javax.swing.BorderFactory; @@ -19,6 +24,7 @@ import javax.swing.JFrame; import javax.swing.JLabel; import javax.swing.JMenu; import javax.swing.JMenuBar; +import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.JTabbedPane; @@ -38,8 +44,14 @@ import com.formdev.flatlaf.intellijthemes.FlatOneDarkIJTheme; import com.formdev.flatlaf.util.ColorFunctions; import entralinked.Entralinked; +import entralinked.GameVersion; +import entralinked.gui.FileChooser; import entralinked.gui.panels.DashboardPanel; +import entralinked.model.player.Player; +import entralinked.model.player.PlayerManager; import entralinked.utility.ConsumerAppender; +import entralinked.utility.GsidUtility; +import entralinked.utility.LEInputStream; import entralinked.utility.SwingUtility; /** @@ -52,9 +64,13 @@ public class MainView { public static final Color TEXT_COLOR_ERROR = Color.RED.darker(); private final StyleContext styleContext = StyleContext.getDefaultStyleContext(); private final AttributeSet fontAttribute = styleContext.addAttribute(SimpleAttributeSet.EMPTY, StyleConstants.FontFamily, "Consolas"); + private final Entralinked entralinked; + private final JFrame frame; private final JLabel statusLabel; public MainView(Entralinked entralinked) { + this.entralinked = entralinked; + // Set look and feel FlatOneDarkIJTheme.setup(); UIManager.getDefaults().put("Component.focusedBorderColor", UIManager.get("Component.borderColor")); @@ -112,10 +128,15 @@ public class MainView { tabbedPane.addTab("Dashboard", new DashboardPanel(entralinked)); // Create window - JFrame frame = new JFrame("Entralinked"); + frame = new JFrame("Entralinked"); // Create menu bar JMenuBar menuBar = new JMenuBar(); + JMenu toolsMenu = new JMenu("Tools"); + toolsMenu.add(SwingUtility.createAction("Import save file (Memory Link)", () -> FileChooser.showFileOpenDialog(frame, selection -> { + importSaveFile(selection.file()); + }))); + menuBar.add(toolsMenu); JMenu helpMenu = new JMenu("Help"); helpMenu.add(SwingUtility.createAction("Update PID (Error 60000)", () -> new PidToolDialog(entralinked, frame))); helpMenu.add(SwingUtility.createAction("GitHub", () -> { @@ -159,4 +180,77 @@ public class MainView { public void setStatusLabelText(String text) { statusLabel.setText(text); } + + private void importSaveFile(File file) { + // Check file size + if(file.length() != 524288) { + JOptionPane.showMessageDialog(frame, "Invalid file length.\n" + + "Expected 524288 bytes, got %s.".formatted(file.length()), "Attention", JOptionPane.WARNING_MESSAGE); + return; + } + + PlayerManager playerManager = entralinked.getPlayerManager(); + Player player = null; + GameVersion version = null; + + try(LEInputStream inputStream = new LEInputStream(new FileInputStream(file))) { + inputStream.skipNBytes(0x19400); // Skip to trainer info + inputStream.skipNBytes(0x4); + String name = inputStream.readUTF16(7); + inputStream.skipNBytes(0x2); + int trainerId = inputStream.readInt(); + int profileId = inputStream.readInt(); + inputStream.skipNBytes(0x2); + int language = inputStream.read(); + int romCode = inputStream.read(); + version = GameVersion.lookup(romCode, language); + + // Check game version + if(version == null || version.isVersion2()) { + JOptionPane.showMessageDialog(frame, "This is not a Black & White save file.", "Attention", JOptionPane.WARNING_MESSAGE); + return; + } + + String gameSyncId = GsidUtility.stringifyGameSyncId(profileId == 0 ? Objects.hash(name, trainerId) & 0x7FFFFFFF : profileId); + player = playerManager.doesPlayerExist(gameSyncId) ? playerManager.getPlayer(gameSyncId) : playerManager.registerPlayer(gameSyncId, version); + } catch(Exception e) { + SwingUtility.showExceptionInfo(frame, "Failed to read save data.", e); + return; + } + + // Check if player exists + if(player == null) { + JOptionPane.showMessageDialog(frame, "Failed to create player data.", "Attention", JOptionPane.WARNING_MESSAGE); + return; + } + + // Check version mismatch + if(player.getGameVersion() != version) { + if(!SwingUtility.showIgnorableConfirmDialog(frame, + "The game version stored in the profile data does not match.\n" + + "Do you want to overwrite it and import the save file anyway?", "Attention")) { + return; + } + + player.setGameVersion(version); + + // Try to save player data + if(!playerManager.savePlayer(player)) { + JOptionPane.showMessageDialog(frame, "Failed to save player data.", "Attention", JOptionPane.WARNING_MESSAGE); + return; + } + } + + // Copy save file + try { + Files.copy(file.toPath(), player.getSaveFile().toPath(), StandardCopyOption.REPLACE_EXISTING); + } catch(Exception e) { + SwingUtility.showExceptionInfo(frame, "Failed to import save data.", e); + return; + } + + JOptionPane.showMessageDialog(frame, "Save file has been imported successfully!\n" + + "You can now use Memory Link with the following Game Sync ID:\n\n%s".formatted(player.getGameSyncId()), + "Attention", JOptionPane.INFORMATION_MESSAGE); + } } diff --git a/src/main/java/entralinked/model/player/PlayerManager.java b/src/main/java/entralinked/model/player/PlayerManager.java index 9ed6f58..d54dc8c 100644 --- a/src/main/java/entralinked/model/player/PlayerManager.java +++ b/src/main/java/entralinked/model/player/PlayerManager.java @@ -186,7 +186,13 @@ public class PlayerManager { public Player registerPlayer(String gameSyncId, GameVersion version) { // Check for duplicate Game Sync ID if(playerMap.containsKey(gameSyncId)) { - logger.warn("Attempted to register duplicate player {}", gameSyncId); + logger.warn("Attempted to register duplicate Game Sync ID: {}", gameSyncId); + return null; + } + + // Check if Game Sync ID is valid + if(!GsidUtility.isValidGameSyncId(gameSyncId)) { + logger.warn("Attempted to register invalid Game Sync ID: {}", gameSyncId); return null; } diff --git a/src/main/java/entralinked/utility/LEInputStream.java b/src/main/java/entralinked/utility/LEInputStream.java index 379b2fb..7de8ccf 100644 --- a/src/main/java/entralinked/utility/LEInputStream.java +++ b/src/main/java/entralinked/utility/LEInputStream.java @@ -48,4 +48,23 @@ public class LEInputStream extends FilterInputStream { public double readDouble() throws IOException { return Double.longBitsToDouble(readLong()); } + + public String readUTF16(int length) throws IOException { + char[] charBuffer = new char[length]; + int read = 0; + + for(int i = 0; i < charBuffer.length; i++) { + int c = readShort() & 0xFFFF; + + if(c == 0xFFFF) { + break; + } + + charBuffer[i] = (char)c; + read++; + } + + skipNBytes((length - (read + 1)) * 2); + return new String(charBuffer, 0, read); + } }