Add menu option to import save files for Memory Link
Some checks failed
Build / dist (push) Has been cancelled

This commit is contained in:
kuroppoi 2025-04-15 01:19:17 +02:00
parent abc32e5051
commit b37f8b5624
3 changed files with 121 additions and 2 deletions

View File

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

View File

@ -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;
}

View File

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