18 Commits
dev ... v1.4.2

Author SHA1 Message Date
kuroppoi
556eab6b45 Remove unneeded status checks (#91)
Some checks failed
Build / dist (push) Has been cancelled
2026-04-15 02:09:14 +02:00
kuroppoi
b831383284 Fix oversight causing Korean versions to be unable to download skins 2026-04-15 01:55:07 +02:00
kuroppoi
b37f8b5624 Add menu option to import save files for Memory Link
Some checks failed
Build / dist (push) Has been cancelled
2025-04-15 01:19:17 +02:00
kuroppoi
abc32e5051 Add negative PID test case 2025-04-14 19:52:51 +02:00
kuroppoi
d5c2a57c51 Improve Game Sync ID processing
Also fixes an exception that occurs when a negative PID is sent through Memory Link.
2025-04-14 19:44:21 +02:00
kuroppoi
afc79c39ad Fix sprite shadow cutoff
Some checks failed
Build / dist (push) Has been cancelled
2025-04-09 22:11:19 +02:00
kuroppoi
15c42165ed Add utility function for creating styled labels 2025-04-09 22:09:43 +02:00
kuroppoi
e663d91559 Forgot this one 2025-04-09 21:56:30 +02:00
kuroppoi
4031d833e8 GUI tweaks + add option to edit décor (#6) 2025-04-09 21:45:52 +02:00
kuroppoi
2385398b4b Add encounter form locking
Some checks failed
Build / dist (push) Has been cancelled
2025-04-07 17:13:34 +02:00
kuroppoi
840d5e956a Reduce duplicate code in download function
Some checks are pending
Build / dist (push) Waiting to run
2025-04-07 04:46:48 +02:00
kuroppoi
bf2cc1f6bc Forgot some things
Some checks failed
Build / dist (push) Has been cancelled
2025-04-04 02:08:42 +02:00
kuroppoi
f3a5df2ff6 Revert 90cb874520
Some checks are pending
Build / dist (push) Waiting to run
2025-04-03 03:49:09 +02:00
kuroppoi
d8bef2f184 Return to summary on login 2025-04-03 03:35:05 +02:00
Kuroppoi
31665f3f74 Update workflow 2025-04-03 03:21:11 +02:00
kuroppoi
356900d591 Add built-in user interface with randomize & legality options (#9 & #16) 2025-04-03 03:16:01 +02:00
kuroppoi
ff8615401e Alternate form bitmask
Some checks failed
Build / dist (push) Has been cancelled
2025-03-27 03:36:04 +01:00
kuroppoi
90cb874520 Testing something...
Some checks failed
Build / dist (push) Has been cancelled
2025-01-25 03:06:16 +01:00
73 changed files with 13454 additions and 672 deletions

View File

@@ -13,22 +13,22 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v3 uses: actions/checkout@v4.2.2
- name: Checkout submodules - name: Checkout submodules
run: git submodule update --init --recursive run: git submodule update --init --recursive
- name: Setup Java 17 - name: Setup Java 17
uses: actions/setup-java@v3 uses: actions/setup-java@v4.7.0
with: with:
java-version: 17 java-version: 17
distribution: temurin distribution: temurin
- name: Make gradlew executable - name: Make gradlew executable
run: chmod +x ./gradlew run: chmod +x ./gradlew
- name: Setup Gradle - name: Setup Gradle
uses: gradle/gradle-build-action@v2 uses: gradle/actions/setup-gradle@v3
- name: Run Gradle dist - name: Run Gradle dist
run: ./gradlew dist run: ./gradlew dist
- name: Upload artifact - name: Upload artifact
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v4.6.2
with: with:
name: entralinked name: entralinked
path: build/libs/entralinked.jar path: build/libs/entralinked.jar

View File

@@ -30,6 +30,8 @@ dependencies {
implementation 'com.formdev:flatlaf:3.1.1' implementation 'com.formdev:flatlaf:3.1.1'
implementation 'com.formdev:flatlaf-extras:3.1.1' implementation 'com.formdev:flatlaf-extras:3.1.1'
implementation 'com.formdev:flatlaf-intellij-themes:3.1.1' implementation 'com.formdev:flatlaf-intellij-themes:3.1.1'
implementation 'com.miglayout:miglayout-swing:4.2' // Finally, a good fucking layout manager.
implementation 'org.swinglabs.swingx:swingx-autocomplete:1.6.5-1'
} }
sourceSets { sourceSets {

View File

@@ -6,9 +6,7 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
public record Configuration( public record Configuration(
String hostName, String hostName,
boolean clearPlayerDreamInfoOnWake, boolean clearPlayerDreamInfoOnWake,
boolean allowOverwritingPlayerDreamInfo,
boolean allowPlayerGameVersionMismatch,
boolean allowWfcRegistrationThroughLogin) { boolean allowWfcRegistrationThroughLogin) {
public static final Configuration DEFAULT = new Configuration("local", true, false, false, true); public static final Configuration DEFAULT = new Configuration("local", true, true);
} }

View File

@@ -14,7 +14,7 @@ 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.gui.MainView; import entralinked.gui.view.MainView;
import entralinked.model.dlc.DlcList; import entralinked.model.dlc.DlcList;
import entralinked.model.player.PlayerManager; import entralinked.model.player.PlayerManager;
import entralinked.model.user.UserManager; import entralinked.model.user.UserManager;
@@ -119,7 +119,6 @@ public class Entralinked {
if(mainView != null) { if(mainView != null) {
SwingUtilities.invokeLater(() -> { SwingUtilities.invokeLater(() -> {
mainView.setDashboardButtonEnabled(true);
mainView.setStatusLabelText("Configure your DS to use the following DNS server: %s".formatted(hostIpAddress)); mainView.setStatusLabelText("Configure your DS to use the following DNS server: %s".formatted(hostIpAddress));
}); });
} }

View File

@@ -4,7 +4,7 @@ import java.util.HashMap;
import java.util.Map; import java.util.Map;
public enum GameVersion { public enum GameVersion {
// ================================== // ==================================
// Black Version & White Version // Black Version & White Version
// ================================== // ==================================
@@ -29,21 +29,35 @@ public enum GameVersion {
// Black Version 2 & White Version 2 // Black Version 2 & White Version 2
// ================================== // ==================================
BLACK_2_JAPANESE(23, 1, "IREJ", "ブラック2", true), BLACK_2_JAPANESE(23, 1, "IREJ", "ブラック2"),
BLACK_2_ENGLISH(23, 2, "IREO", "Black Version 2", true), BLACK_2_ENGLISH(23, 2, "IREO", "Black Version 2"),
BLACK_2_FRENCH(23, 3, "IREF", "Version Noire 2", true), BLACK_2_FRENCH(23, 3, "IREF", "Version Noire 2"),
BLACK_2_ITALIAN(23, 4, "IREI", "Versione Nera 2", true), BLACK_2_ITALIAN(23, 4, "IREI", "Versione Nera 2"),
BLACK_2_GERMAN(23, 5, "IRED", "Schwarze Edition 2", true), BLACK_2_GERMAN(23, 5, "IRED", "Schwarze Edition 2"),
BLACK_2_SPANISH(23, 7, "IRES", "Edicion Negra 2", true), BLACK_2_SPANISH(23, 7, "IRES", "Edicion Negra 2"),
BLACK_2_KOREAN(23, 8, "IREK", "블랙2", true), BLACK_2_KOREAN(23, 8, "IREK", "블랙2"),
WHITE_2_JAPANESE(22, 1, "IRDJ", "ホワイト2", true), WHITE_2_JAPANESE(22, 1, "IRDJ", "ホワイト2"),
WHITE_2_ENGLISH(22, 2, "IRDO", "White Version 2", true), WHITE_2_ENGLISH(22, 2, "IRDO", "White Version 2"),
WHITE_2_FRENCH(22, 3, "IRDF", "Version Blanche 2", true), WHITE_2_FRENCH(22, 3, "IRDF", "Version Blanche 2"),
WHITE_2_ITALIAN(22, 4, "IRDI", "Versione Bianca 2", true), WHITE_2_ITALIAN(22, 4, "IRDI", "Versione Bianca 2"),
WHITE_2_GERMAN(22, 5, "IRDD", "Weisse Edition 2", true), WHITE_2_GERMAN(22, 5, "IRDD", "Weisse Edition 2"),
WHITE_2_SPANISH(22, 7, "IRDS", "Edicion Blanca 2", true), WHITE_2_SPANISH(22, 7, "IRDS", "Edicion Blanca 2"),
WHITE_2_KOREAN(22, 8, "IRDK", "화이트2", true); WHITE_2_KOREAN(22, 8, "IRDK", "화이트2");
// Masks
public static final int BW_MASK = 0b110011111111;
public static final int B2W2_MASK = 0b001111111111;
public static final int ALL_MASK = BW_MASK | B2W2_MASK;
public static final int JAP_MASK = 0b111100000001;
public static final int ENG_MASK = 0b111100000010;
public static final int FRE_MASK = 0b111100000100;
public static final int ITA_MASK = 0b111100001000;
public static final int GER_MASK = 0b111100010000;
public static final int SPA_MASK = 0b111101000000;
public static final int KOR_MASK = 0b111110000000;
public static final int JAP_KOR_MASK = JAP_MASK | KOR_MASK;
public static final int NA_EUR_MASK = ENG_MASK | FRE_MASK | ITA_MASK | GER_MASK | SPA_MASK;
// Lookup maps // Lookup maps
private static final Map<String, GameVersion> mapBySerial = new HashMap<>(); private static final Map<String, GameVersion> mapBySerial = new HashMap<>();
@@ -52,7 +66,7 @@ public enum GameVersion {
static { static {
for(GameVersion version : values()) { for(GameVersion version : values()) {
mapBySerial.put(version.getSerial(), version); mapBySerial.put(version.getSerial(), version);
mapByCodes.put(version.getRomCode() << version.getLanguageCode(), version); mapByCodes.put(version.getBits(), version);
} }
} }
@@ -60,18 +74,12 @@ public enum GameVersion {
private final int languageCode; // Values are not tested private final int languageCode; // Values are not tested
private final String serial; private final String serial;
private final String displayName; private final String displayName;
private final boolean isVersion2;
private GameVersion(int romCode, int languageCode, String serial, String displayName, boolean isVersion2) { private GameVersion(int romCode, int languageCode, String serial, String displayName) {
this.romCode = romCode; this.romCode = romCode;
this.languageCode = languageCode; this.languageCode = languageCode;
this.serial = serial; this.serial = serial;
this.displayName = displayName; this.displayName = displayName;
this.isVersion2 = isVersion2;
}
private GameVersion(int romCode, int languageCode, String serial, String displayName) {
this(romCode, languageCode, serial, displayName, false);
} }
public static GameVersion lookup(String serial) { public static GameVersion lookup(String serial) {
@@ -79,7 +87,11 @@ public enum GameVersion {
} }
public static GameVersion lookup(int romCode, int languageCode) { public static GameVersion lookup(int romCode, int languageCode) {
return mapByCodes.get(romCode << languageCode); return mapByCodes.get(getBits(romCode, languageCode));
}
private static int getBits(int romCode, int languageCode) {
return (1 << (8 - (romCode - 23))) | (1 << languageCode - 1) & 0b111111111111;
} }
public int getRomCode() { public int getRomCode() {
@@ -99,6 +111,15 @@ public enum GameVersion {
} }
public boolean isVersion2() { public boolean isVersion2() {
return isVersion2; return checkMask(B2W2_MASK);
}
public boolean checkMask(int mask) {
int bits = getBits();
return (bits & mask) == bits;
}
public int getBits() {
return getBits(romCode, languageCode);
} }
} }

View File

@@ -0,0 +1,55 @@
package entralinked.gui;
import java.awt.Component;
import java.io.File;
import java.util.Collections;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Function;
import javax.swing.JFileChooser;
import javax.swing.filechooser.FileFilter;
public class FileChooser {
private static final JFileChooser fileChooser = new JFileChooser(".");
public static void showFileOpenDialog(Component parent, Consumer<FileSelection> handler) {
showFileOpenDialog(parent, Collections.emptyList(), handler);
}
public static void showFileOpenDialog(Component parent, FileFilter fileFilter, Consumer<FileSelection> handler) {
showFileOpenDialog(parent, List.of(fileFilter), handler);
}
public static void showFileOpenDialog(Component parent, List<FileFilter> fileFilters, Consumer<FileSelection> handler) {
showDialog(parent, fileFilters, fileChooser::showOpenDialog, handler);
}
private static void showDialog(Component parent, List<FileFilter> fileFilters, Function<Component, Integer> dialogFunction, Consumer<FileSelection> handler) {
FileFilter currentFilter = fileChooser.getFileFilter();
fileChooser.resetChoosableFileFilters();
fileChooser.setAcceptAllFileFilterUsed(fileFilters.isEmpty());
fileFilters.forEach(fileChooser::addChoosableFileFilter);
if(fileFilters.contains(currentFilter)) {
fileChooser.setFileFilter(currentFilter);
}
if(dialogFunction.apply(parent) == JFileChooser.APPROVE_OPTION) {
File file = fileChooser.getSelectedFile();
handler.accept(new FileSelection(file, fileChooser.getFileFilter(), getFileExtension(file)));
}
}
public static String getFileExtension(File file) {
String name = file.getName();
int index = name.lastIndexOf('.');
if(index == -1 || index + 1 == name.length()) {
return null;
}
return name.substring(index + 1).toLowerCase();
}
}

View File

@@ -0,0 +1,7 @@
package entralinked.gui;
import java.io.File;
import javax.swing.filechooser.FileFilter;
public record FileSelection(File file, FileFilter filter, String extension) {}

View File

@@ -0,0 +1,46 @@
package entralinked.gui;
import java.awt.Toolkit;
import javax.swing.text.AttributeSet;
import javax.swing.text.BadLocationException;
import entralinked.utility.GsidUtility;
public class GsidDocumentFilter extends SizeLimitDocumentFilter {
public GsidDocumentFilter() {
super(10);
}
@Override
public void insertString(FilterBypass fb, int offset, String text, AttributeSet attrs) throws BadLocationException {
replace(fb, offset, 0, text, attrs);
}
@Override
public void replace(FilterBypass fb, int offset, int length, String text, AttributeSet attrs) throws BadLocationException {
if(text == null) {
return;
}
StringBuilder builder = new StringBuilder();
boolean shouldBeep = false;
for(int i = 0; i < text.length(); i++) {
char c = Character.toUpperCase(text.charAt(i));
if(GsidUtility.GSID_CHARTABLE.indexOf(c) != -1) {
builder.append(c);
} else {
shouldBeep = true;
}
}
if(shouldBeep) {
Toolkit.getDefaultToolkit().beep();
}
super.replace(fb, offset, length, builder.toString(), attrs);
}
}

View File

@@ -0,0 +1,60 @@
package entralinked.gui;
import java.awt.FontMetrics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.image.BufferedImage;
import java.util.HashMap;
import java.util.Map;
import javax.imageio.ImageIO;
import entralinked.gui.data.DataManager;
public class ImageLoader {
private static final Map<String, BufferedImage> cache = new HashMap<>();
public static BufferedImage getImage(String path) {
return cache.computeIfAbsent(path, ImageLoader::loadImage);
}
private static BufferedImage loadImage(String path) {
try {
return ImageIO.read(DataManager.class.getResource(path));
} catch(Exception e) {
BufferedImage image = new BufferedImage(128, 128, BufferedImage.TYPE_INT_ARGB);
Graphics2D graphics = image.createGraphics();
graphics.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
graphics.drawString("IMAGE LOAD ERROR", 0, 16);
graphics.drawString("Please report this!", 0, 124);
drawWrappedString(graphics, path, 0, 32, image.getWidth());
drawWrappedString(graphics, e.getMessage(), 0, 80, image.getWidth());
graphics.dispose();
return image;
}
}
private static void drawWrappedString(Graphics2D graphics, String string, int x, int y, int width) {
int length = string.length();
FontMetrics metrics = graphics.getFontMetrics();
String line = "";
int currentY = y;
for(int i = 0; i < length; i++) {
char next = string.charAt(i);
if((!line.isEmpty() && x + metrics.stringWidth(line + next) >= width)) {
graphics.drawString(line, x, currentY);
line = "";
currentY += metrics.getHeight();
}
line += next;
if(i + 1 == length) {
graphics.drawString(line, x, currentY);
}
}
}
}

View File

@@ -0,0 +1,25 @@
package entralinked.gui;
import javax.swing.DefaultCellEditor;
import javax.swing.InputVerifier;
import javax.swing.JTextField;
@SuppressWarnings("serial")
public class InputVerifierCellEditor extends DefaultCellEditor {
private final InputVerifier verifier;
public InputVerifierCellEditor(InputVerifier verifier) {
this(verifier, new JTextField());
}
public InputVerifierCellEditor(InputVerifier verifier, JTextField textField) {
super(textField);
this.verifier = verifier;
}
@Override
public boolean stopCellEditing() {
return verifier.verify(editorComponent) && super.stopCellEditing();
}
}

View File

@@ -1,172 +0,0 @@
package entralinked.gui;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Desktop;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.io.IOException;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextPane;
import javax.swing.UIManager;
import javax.swing.text.AttributeSet;
import javax.swing.text.BadLocationException;
import javax.swing.text.DefaultCaret;
import javax.swing.text.Document;
import javax.swing.text.SimpleAttributeSet;
import javax.swing.text.StyleConstants;
import javax.swing.text.StyleContext;
import org.apache.logging.log4j.Level;
import com.formdev.flatlaf.intellijthemes.FlatOneDarkIJTheme;
import entralinked.Entralinked;
import entralinked.utility.ConsumerAppender;
import entralinked.utility.SwingUtility;
/**
* Simple Swing user interface.
*/
public class MainView {
public static final Color TEXT_COLOR = Color.WHITE.darker();
public static final Color TEXT_COLOR_WARN = Color.YELLOW.darker();
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 JButton dashboardButton;
private final JLabel statusLabel;
public MainView(Entralinked entralinked) {
// Set look and feel
FlatOneDarkIJTheme.setup();
UIManager.getDefaults().put("Component.focusedBorderColor", UIManager.get("Component.borderColor"));
// Create dashboard button
dashboardButton = new JButton("Open User Dashboard");
dashboardButton.setEnabled(false);
dashboardButton.setFocusable(false);
dashboardButton.addActionListener(event -> {
openUrl("http://127.0.0.1/dashboard/profile.html");
});
// Create status label
statusLabel = new JLabel("Entralinked is starting...", JLabel.CENTER);
// Create footer panel
JPanel footerPanel = new JPanel(new BorderLayout());
footerPanel.add(statusLabel, BorderLayout.CENTER);
footerPanel.add(dashboardButton, BorderLayout.LINE_END);
// Create console output
JTextPane consoleOutputPane = new JTextPane() {
@Override
public boolean getScrollableTracksViewportWidth() {
return getPreferredSize().width <= getParent().getSize().width;
}
@Override
public Dimension getPreferredSize() {
return getUI().getPreferredSize(this);
};
};
consoleOutputPane.setFont(new Font("Consola", Font.PLAIN, 12));
consoleOutputPane.setEditable(false);
((DefaultCaret)consoleOutputPane.getCaret()).setUpdatePolicy(DefaultCaret.ALWAYS_UPDATE);
// Create console output appender
ConsumerAppender.addConsumer("GuiOutput", message -> {
Document document = consoleOutputPane.getDocument();
Level level = message.level();
Color color = level == Level.ERROR ? TEXT_COLOR_ERROR : level == Level.WARN ? TEXT_COLOR_WARN : TEXT_COLOR;
AttributeSet colorAttribute = styleContext.addAttribute(fontAttribute, StyleConstants.Foreground, color);
try {
consoleOutputPane.getDocument().insertString(document.getLength(), message.formattedMessage(), colorAttribute);
} catch(BadLocationException e) {}
});
// Create console output scroll pane
JScrollPane scrollPane = new JScrollPane();
scrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS);
scrollPane.setViewportView(consoleOutputPane);
// Create main panel
JPanel panel = new JPanel(new BorderLayout());
panel.add(scrollPane, BorderLayout.CENTER);
panel.add(footerPanel, BorderLayout.PAGE_END);
// Create window
JFrame frame = new JFrame("Entralinked");
// Create menu bar
JMenuBar menuBar = new JMenuBar();
JMenu helpMenu = new JMenu("Help");
helpMenu.add(SwingUtility.createAction("Update PID (Error 60000)", () -> new PidToolDialog(entralinked, frame)));
helpMenu.add(SwingUtility.createAction("GitHub", () -> openUrl("https://github.com/kuroppoi/entralinked")));
menuBar.add(helpMenu);
// Set window properties
frame.addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(WindowEvent event) {
// Update status
dashboardButton.setEnabled(false);
statusLabel.setText("Entralinked is shutting down ...");
// Run asynchronously so it doesn't just awkwardly freeze
// Still scuffed but better than nothing I guess
CompletableFuture.runAsync(() -> {
entralinked.stopServers();
System.exit(0);
});
}
});
frame.setIconImages(List.of(
new ImageIcon(getClass().getResource("/icon-64x.png")).getImage(),
new ImageIcon(getClass().getResource("/icon-32x.png")).getImage(),
new ImageIcon(getClass().getResource("/icon-16x.png")).getImage()));
frame.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
frame.setMinimumSize(new Dimension(512, 288));
frame.setJMenuBar(menuBar);
frame.add(panel);
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
public void setDashboardButtonEnabled(boolean enabled) {
dashboardButton.setEnabled(enabled);
}
public void setStatusLabelText(String text) {
statusLabel.setText(text);
}
private void openUrl(String url) {
Desktop desktop = Desktop.getDesktop();
if(desktop != null && desktop.isSupported(Desktop.Action.BROWSE)) {
try {
desktop.browse(new URL(url).toURI());
} catch(IOException | URISyntaxException e) {
e.printStackTrace();
}
}
}
}

View File

@@ -0,0 +1,38 @@
package entralinked.gui;
import java.awt.Component;
import java.awt.Font;
import java.util.function.Function;
import javax.swing.DefaultListCellRenderer;
import javax.swing.JList;
import entralinked.utility.SwingUtility;
@SuppressWarnings({"serial", "unchecked"})
public class ModelListCellRenderer<T> extends DefaultListCellRenderer {
private final Class<T> type;
private final Function<T, String> textSupplier;
private final String nullValue;
public ModelListCellRenderer(Class<T> type, Function<T, String> textSupplier, String nullValue) {
this.type = type;
this.textSupplier = textSupplier;
this.nullValue = nullValue;
}
@Override
public Component getListCellRendererComponent(JList<?> list, Object value, int index, boolean isSelected, boolean cellHasFocus) {
Component component = super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
setText(value == null ? String.valueOf(nullValue) : type.isAssignableFrom(value.getClass()) ? textSupplier.apply((T)value) : String.valueOf(value));
Font font = component.getFont();
String text = getText();
if(font.canDisplayUpTo(text) != -1) {
component.setFont(SwingUtility.findSupportingFont(text, font));
}
return component;
}
}

View File

@@ -0,0 +1,38 @@
package entralinked.gui;
import java.awt.Component;
import java.awt.Font;
import java.util.function.Function;
import javax.swing.JTable;
import javax.swing.table.DefaultTableCellRenderer;
import entralinked.utility.SwingUtility;
@SuppressWarnings({"serial", "unchecked"})
public class ModelTableCellRenderer<T> extends DefaultTableCellRenderer {
private final Class<T> type;
private final Function<T, String> textSupplier;
private final String nullValue;
public ModelTableCellRenderer(Class<T> type, Function<T, String> textSupplier, String nullValue) {
this.type = type;
this.textSupplier = textSupplier;
this.nullValue = nullValue;
}
@Override
public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
Component component = super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
setText(value == null ? String.valueOf(nullValue) : type.isAssignableFrom(value.getClass()) ? textSupplier.apply((T)value) : String.valueOf(value));
Font font = component.getFont();
String text = getText();
if(font.canDisplayUpTo(text) != -1) {
component.setFont(SwingUtility.findSupportingFont(text, font));
}
return component;
}
}

View File

@@ -0,0 +1,40 @@
package entralinked.gui;
import java.awt.Toolkit;
import javax.swing.text.AttributeSet;
import javax.swing.text.BadLocationException;
import javax.swing.text.DocumentFilter;
public class SizeLimitDocumentFilter extends DocumentFilter {
private final int limit;
public SizeLimitDocumentFilter(int limit) {
this.limit = limit;
}
@Override
public void insertString(FilterBypass fb, int offset, String text, AttributeSet attrs) throws BadLocationException {
replace(fb, offset, 0, text, attrs);
}
@Override
public void replace(FilterBypass fb, int offset, int length, String text, AttributeSet attrs) throws BadLocationException {
if(text == null) {
return;
}
if(limit <= 0) {
super.replace(fb, offset, length, text, attrs);
return;
}
int finalLength = Math.min(text.length(), Math.max(0, limit - fb.getDocument().getLength() + length));
super.replace(fb, offset, length, text.substring(0, finalLength), attrs);
if(finalLength != text.length()) {
Toolkit.getDefaultToolkit().beep();
}
}
}

View File

@@ -0,0 +1,229 @@
package entralinked.gui.component;
import java.awt.Component;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Vector;
import java.util.function.Function;
import javax.swing.CellEditor;
import javax.swing.DefaultCellEditor;
import javax.swing.JComboBox;
import javax.swing.JTable;
import javax.swing.JTextField;
import javax.swing.ListCellRenderer;
import javax.swing.table.TableCellEditor;
import javax.swing.table.TableCellRenderer;
import org.jdesktop.swingx.autocomplete.AutoCompleteDecorator;
import org.jdesktop.swingx.autocomplete.ComboBoxCellEditor;
import org.jdesktop.swingx.autocomplete.ObjectToStringConverter;
import entralinked.gui.ModelListCellRenderer;
import entralinked.gui.ModelTableCellRenderer;
/**
* TODO look into custom table models because this current implementation is absolutely ass.
*/
@SuppressWarnings("serial")
public class ConfigTable extends JTable {
private final Map<Integer, TableCellRenderer> tableCellRenderers = new HashMap<>();
private final Map<Integer, ListCellRenderer<Object>> listCellRenderers = new HashMap<>();
private final Map<Integer, ObjectToStringConverter> converters = new HashMap<>();
private TableCellEditor[][] cellEditors;
private boolean[][] disabledCells;
@Override
public TableCellEditor getCellEditor(int row, int column) {
resizeArrays();
return cellEditors[row][column];
}
@Override
public TableCellRenderer getCellRenderer(int row, int column) {
return tableCellRenderers.getOrDefault(column, super.getCellRenderer(row, column));
}
@Override
public boolean isCellEditable(int row, int column) {
resizeArrays();
return !disabledCells[row][column] && cellEditors[row][column] != null;
}
public void setCellEditable(int row, int column, boolean editable) {
resizeArrays();
disabledCells[row][column] = !editable;
}
public void setCellEditor(int row, int column, TableCellEditor editor) {
resizeArrays();
cellEditors[row][column] = editor;
}
public <T> void setCellRenderers(int column, Class<T> type, Function<T, String> supplier) {
setCellRenderers(column, type, supplier, null);
}
@SuppressWarnings("unchecked")
public <T> void setCellRenderers(int column, Class<T> type, Function<T, String> supplier, String nullValue) {
tableCellRenderers.put(column, new ModelTableCellRenderer<T>(type, supplier, nullValue));
listCellRenderers.put(column, new ModelListCellRenderer<T>(type, supplier, nullValue));
converters.put(column, new ObjectToStringConverter() {
@Override
public String getPreferredStringForItem(Object item) {
return (item == null || !type.isAssignableFrom(item.getClass())) ? null : supplier.apply((T)item);
}
});
}
public void enableOption(int row, int column) {
if(isCellEditable(row, column)) {
return;
}
Object initialValue = "N/A";
CellEditor editor = getCellEditor(row, column);
if(editor instanceof DefaultCellEditor) {
Component component = ((DefaultCellEditor)editor).getComponent();
if(component instanceof JComboBox) {
JComboBox<?> comboBox = (JComboBox<?>)component;
if(comboBox.getItemCount() > 0) {
initialValue = comboBox.getItemAt(0);
}
} else if(component instanceof JTextField) {
initialValue = "";
}
}
enableOption(row, column, initialValue);
}
public void enableOption(int row, int column, Object initialValue) {
if(isCellEditable(row, column)) {
return;
}
getModel().setValueAt(initialValue, row, column);
setCellEditable(row, column, true);
}
public void disableOption(int row, int column) {
setCellEditable(row, column, false);
getModel().setValueAt("", row, column);
}
public void setOptionsAt(int row, int column, Collection<?> options) {
setOptionsAt(row, column, options, false);
}
public void setOptionsAt(int row, int column, Collection<?> options, boolean includeNullOption) {
if(options.isEmpty()) {
setCellEditor(row, column, null);
getModel().setValueAt("N/A", row, column);
return;
}
Vector<?> vector = new Vector<>(options);
if(includeNullOption) {
vector.add(0, null);
}
Object currentValue = getValueAt(row, column);
if(currentValue == null || !vector.contains(currentValue)) {
currentValue = vector.firstElement();
getModel().setValueAt(currentValue, row, column);
}
JComboBox<?> comboBox = new JComboBox<>(vector);
AutoCompleteDecorator.decorate(comboBox, converters.getOrDefault(column, ObjectToStringConverter.DEFAULT_IMPLEMENTATION));
if(listCellRenderers.containsKey(column)) {
comboBox.setRenderer(listCellRenderers.get(column));
}
setCellEditor(row, column, new ComboBoxCellEditor(comboBox));
}
public void randomizeSelection(int row, int column) {
randomizeSelection(row, column, true);
}
public void randomizeSelection(int row, int column, boolean allowNull) {
CellEditor editor = getCellEditor(row, column);
if(!(editor instanceof DefaultCellEditor)) {
return;
}
Component component = ((DefaultCellEditor)editor).getComponent();
if(component instanceof JComboBox) {
JComboBox<?> comboBox = (JComboBox<?>)component;
int count = comboBox.getItemCount();
if(count <= 1) {
return;
}
int index = (int)(Math.random() * count);
Object item = comboBox.getItemAt(index);
if(!allowNull) {
// TODO potential infinite loop
while(item == null) {
item = comboBox.getItemAt((int)(Math.random() * count));
}
}
getModel().setValueAt(item, row, column);
}
}
public <T> T getValueAt(int row, int column, Class<T> type) {
return getValueAt(row, column, type, null);
}
@SuppressWarnings("unchecked")
public <T> T getValueAt(int row, int column, Class<T> type, T def) {
Object value = getValueAt(row, column);
if(value != null && type.isAssignableFrom(value.getClass())) {
return (T)value;
}
return def;
}
private void resizeArrays() {
int rows = getRowCount();
int columns = getColumnCount();
if(cellEditors == null) {
cellEditors = new TableCellEditor[rows][columns];
disabledCells = new boolean[rows][columns];
return;
}
// Check row size
if(rows != cellEditors.length) {
cellEditors = Arrays.copyOf(cellEditors, rows);
disabledCells = Arrays.copyOf(disabledCells, rows);
}
// Check column size
if(rows > 0 && cellEditors[0].length != columns) {
for(int i = 0; i < rows; i++) {
cellEditors[i] = Arrays.copyOf(cellEditors[i], columns);
disabledCells[i] = Arrays.copyOf(disabledCells[i], columns);
}
}
}
}

View File

@@ -0,0 +1,90 @@
package entralinked.gui.component;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JPasswordField;
import javax.swing.JTextField;
import com.formdev.flatlaf.FlatClientProperties;
import net.miginfocom.swing.MigLayout;
@SuppressWarnings("serial")
public class PropertyDisplay extends JPanel {
private final Map<Integer, String> keys = new HashMap<>();
private final Map<String, JLabel> labels = new HashMap<>();
private final Map<String, JTextField> fields = new HashMap<>();
public PropertyDisplay() {
setLayout(new MigLayout("insets 0, fill"));
}
public void addProperty(int x, int y, String key, String label) {
addProperty(x, y, key, label, false);
}
public void addProperty(int x, int y, String key, String label, boolean hidden) {
int coordHash = Objects.hash(x, y);
String existingKey = keys.get(coordHash);
if(existingKey != null) {
removeProperty(existingKey);
}
removeProperty(key);
String labelConstraints = "cell %s %s, sg proplabel";
if(x > 0) {
labelConstraints += ", gapx 4";
}
JLabel labelComponent = new JLabel(label);
labels.put(key, labelComponent);
add(labelComponent, labelConstraints.formatted(x * 2, y));
add(createValueField(key, hidden), "cell %s %s, sg propfield, width 96".formatted(x * 2 + 1, y));
keys.put(coordHash, key);
}
public boolean removeProperty(String key) {
if(!labels.containsKey(key)) {
return false;
}
remove(labels.remove(key));
remove(fields.remove(key));
keys.values().remove(key);
return true;
}
public void setValue(String key, Object value) {
JTextField textField = fields.get(key);
if(textField != null) {
textField.setText(String.valueOf(value));
}
}
private JTextField createValueField(String key, boolean hidden) {
JTextField textField = null;
if(!hidden) {
textField = new JTextField();
} else {
textField = new JPasswordField();
textField.putClientProperty(FlatClientProperties.STYLE, "showRevealButton: true; showCapsLock: false");
}
textField.setEditable(false);
fields.put(key, textField);
return textField;
}
public JTextField getValueField(String key) {
return fields.get(key);
}
}

View File

@@ -0,0 +1,85 @@
package entralinked.gui.component;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.image.BufferedImage;
import javax.swing.JComponent;
@SuppressWarnings("serial")
public class ShadowedSprite extends JComponent {
public static final int OFFSET_X = 4;
public static final int OFFSET_Y = 2;
private BufferedImage image;
private BufferedImage shadowImage;
private int scale;
public ShadowedSprite() {
setOpaque(false);
}
@Override
public void paintComponent(Graphics graphics) {
super.paintComponent(graphics);
if(image == null) {
return;
}
Dimension size = getSize();
int width = image.getWidth() * scale;
int height = image.getHeight() * scale;
int x = size.width / 2 - width / 2 - OFFSET_X / 2;
int y = size.height / 2 - height / 2 - OFFSET_Y / 2;
graphics.drawImage(shadowImage, x + OFFSET_X, y + OFFSET_Y, width, height, null);
graphics.drawImage(image, x, y, width, height, null);
}
private void imageChanged() {
if(image == null) {
return;
}
int width = image.getWidth();
int height = image.getHeight();
shadowImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
for(int i = 0; i < image.getWidth(); i++) {
for(int j = 0; j < image.getHeight(); j++) {
shadowImage.setRGB(i, j, image.getRGB(i, j) & 0x7F000000);
}
}
}
private void updateSize(int width, int height) {
Dimension oldSize = getPreferredSize();
Dimension size = new Dimension(width, height);
setMinimumSize(size);
setPreferredSize(size);
if(oldSize.width != width || oldSize.height != height) {
revalidate();
}
}
public void setImage(BufferedImage image) {
setImage(image, 1);
}
public void setImage(BufferedImage image, int width, int height) {
setImage(image, 1, width, height);
}
public void setImage(BufferedImage image, int scale) {
setImage(image, scale, image == null ? 0 : image.getWidth(), image == null ? 0 : image.getHeight());
}
public void setImage(BufferedImage image, int scale, int width, int height) {
this.image = image;
this.scale = scale;
imageChanged();
updateSize(width + OFFSET_X, height + OFFSET_Y);
repaint();
}
}

View File

@@ -0,0 +1,16 @@
package entralinked.gui.data;
import java.util.List;
import com.fasterxml.jackson.annotation.JsonProperty;
public record Country(
@JsonProperty(required = true) int id,
@JsonProperty(required = true) String name,
List<Region> regions) {
@JsonProperty
public boolean hasRegions() {
return regions != null && !regions.isEmpty();
}
}

View File

@@ -0,0 +1,194 @@
package entralinked.gui.data;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import entralinked.GameVersion;
import entralinked.gui.ImageLoader;
import entralinked.model.pkmn.PkmnGender;
/**
* TODO name is too generic & model management is a bit confusing.
*/
public class DataManager {
private static final Map<Integer, String> abilities = new HashMap<>();
private static final Map<Integer, String> items = new HashMap<>();
private static final Map<Integer, String> moves = new HashMap<>();
private static final Map<Integer, PkmnSpecies> species = new HashMap<>();
private static final Map<Integer, Country> countries = new HashMap<>();
private static final Map<String, DreamWorldArea> dreamWorldAreas = new HashMap<>();
private static final Map<GameVersion, List<Encounter>> encounterCache = new HashMap<>();
public static void clearData() {
abilities.clear();
items.clear();
moves.clear();
species.clear();
countries.clear();
dreamWorldAreas.clear();
encounterCache.clear();
}
public static void loadData() throws IOException {
clearData();
ObjectMapper mapper = new ObjectMapper();
abilities.putAll(mapper.readValue(DataManager.class.getResource("/data/abilities.json"), new TypeReference<Map<Integer, String>>(){}));
items.putAll(mapper.readValue(DataManager.class.getResource("/data/items.json"), new TypeReference<Map<Integer, String>>(){}));
moves.putAll(mapper.readValue(DataManager.class.getResource("/data/moves.json"), new TypeReference<Map<Integer, String>>(){}));
species.putAll(mapper.readValue(DataManager.class.getResource("/data/species.json"), new TypeReference<Map<Integer, PkmnSpecies>>(){}));
countries.putAll(mapper.readValue(DataManager.class.getResource("/data/countries.json"), new TypeReference<Map<Integer, Country>>(){}));
dreamWorldAreas.putAll(mapper.readValue(DataManager.class.getResource("/data/legality.json"), new TypeReference<Map<String, DreamWorldArea>>(){}));
}
public static BufferedImage getPokemonSprite(int species) {
return getPokemonSprite(0, 0, false, false);
}
public static BufferedImage getPokemonSprite(PkmnSpecies species, int form, PkmnGender gender, boolean shiny) {
return getPokemonSprite(species, form, gender == PkmnGender.FEMALE, shiny);
}
public static BufferedImage getPokemonSprite(int species, int form, PkmnGender gender, boolean shiny) {
return getPokemonSprite(species, form, gender == PkmnGender.FEMALE, shiny);
}
public static BufferedImage getPokemonSprite(int species, int form, boolean female, boolean shiny) {
return getPokemonSprite(getSpecies(species), form, female, shiny);
}
public static BufferedImage getPokemonSprite(PkmnSpecies species, int form, boolean female, boolean shiny) {
String path = "/sprites/pokemon/normal/0.png";
if(species != null) {
path = "/sprites/pokemon/%s/%s%s%s.png".formatted(
shiny ? "shiny" : "normal",
species.hasFemaleSprite() && female ? "female/" : "",
species.id(), form == 0 ? "" : "-" + form);
}
return ImageLoader.getImage(path);
}
public static BufferedImage getItemSprite(int item) {
return ImageLoader.getImage("/sprites/items/%s.png".formatted(item));
}
public static String getAbilityName(int id) {
return abilities.getOrDefault(id, "Unknown (#%s)".formatted(id));
}
public static Set<Integer> getAbilityIds() {
return Collections.unmodifiableSet(abilities.keySet());
}
public static String getItemName(int id) {
return items.getOrDefault(id, "Unknown (#%s)".formatted(id));
}
public static Set<Integer> getItemIds() {
return Collections.unmodifiableSet(items.keySet());
}
public static String getMoveName(int id) {
return moves.getOrDefault(id, "Unknown (#%s)".formatted(id));
}
public static Set<Integer> getMoveIds() {
return Collections.unmodifiableSet(moves.keySet());
}
public static PkmnSpecies getSpecies(int id) {
return species.get(id);
}
public static Set<Integer> getSpeciesIds() {
return Collections.unmodifiableSet(species.keySet());
}
public static Collection<PkmnSpecies> getSpecies() {
return Collections.unmodifiableCollection(species.values());
}
public static Country getCountry(int id) {
return countries.get(id);
}
public static Collection<Country> getCountries() {
return Collections.unmodifiableCollection(countries.values());
}
// TODO functions might be computationally expensive
private static List<Encounter> getEncounters(GameVersion gameVersion) {
return encounterCache.computeIfAbsent(gameVersion, version -> dreamWorldAreas.values().stream()
.map(DreamWorldArea::encounters)
.flatMap(List::stream)
.filter(x -> x.versionMask() == 0 || version.checkMask(x.versionMask()))
.toList());
}
public static List<PkmnSpecies> getDownloadableSpecies(GameVersion gameVersion) {
return species.values().stream()
.filter(x -> x.downloadable() && (gameVersion.isVersion2() || x.id() <= 493))
.collect(Collectors.toCollection(ArrayList::new));
}
public static List<PkmnSpecies> getSpeciesOptions(GameVersion gameVersion) {
return getEncounters(gameVersion).stream()
.map(x -> species.get(x.species()))
.distinct()
.collect(Collectors.toCollection(ArrayList::new));
}
public static List<PkmnGender> getGenderOptions(GameVersion gameVersion, PkmnSpecies species) {
return getEncounters(gameVersion).stream()
.filter(x -> x.species() == species.id())
.map(x -> x.isGenderLocked() ? List.of(x.gender()) : species.getGenders())
.flatMap(List::stream)
.distinct()
.sorted((a, b) -> Integer.compare(a.ordinal(), b.ordinal()))
.collect(Collectors.toCollection(ArrayList::new));
}
public static List<PkmnForm> getFormOptions(GameVersion gameVersion, PkmnSpecies species, PkmnGender gender) {
return getEncounters(gameVersion).stream()
.filter(x -> x.species() == species.id() && (!x.isGenderLocked() || x.gender() == gender))
.flatMap(x -> species.hasForms() ? Stream.of(species.forms()).filter(form -> x.hasForm(form.id())) : Stream.empty())
.distinct()
.collect(Collectors.toCollection(ArrayList::new));
}
public static List<Integer> getMoveOptions(GameVersion gameVersion, PkmnSpecies species, PkmnGender gender, int form) {
return getEncounters(gameVersion).stream()
.filter(x -> x.species() == species.id() && (!x.isGenderLocked() || x.gender() == gender) && x.hasForm(form))
.map(Encounter::moves)
.flatMap(List::stream)
.distinct()
.collect(Collectors.toCollection(ArrayList::new));
}
public static List<Integer> getDownloadableItems(GameVersion gameVersion) {
return items.keySet().stream().filter(x -> gameVersion.isVersion2() || x <= 626).toList();
}
public static List<Integer> getItemOptions() {
return dreamWorldAreas.values().stream()
.map(DreamWorldArea::items)
.flatMap(List::stream)
.distinct()
.toList();
}
}

View File

@@ -0,0 +1,9 @@
package entralinked.gui.data;
import java.util.List;
import com.fasterxml.jackson.annotation.JsonProperty;
public record DreamWorldArea(
@JsonProperty(required = true) List<Encounter> encounters,
@JsonProperty(required = true) List<Integer> items) {}

View File

@@ -0,0 +1,30 @@
package entralinked.gui.data;
import java.util.List;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import entralinked.model.pkmn.PkmnGender;
public record Encounter(
@JsonProperty(required = true) int species,
@JsonProperty(required = true) List<Integer> moves,
PkmnGender gender, int formMask, int versionMask) {
@JsonIgnore
public boolean isGenderLocked() {
return gender != null;
}
@JsonIgnore
public boolean isFormLocked() {
return formMask != 0;
}
@JsonIgnore
public boolean hasForm(int form) {
int bits = 1 << form;
return !isFormLocked() || (bits & formMask) == bits;
}
}

View File

@@ -0,0 +1,7 @@
package entralinked.gui.data;
import com.fasterxml.jackson.annotation.JsonProperty;
public record PkmnForm(
@JsonProperty(required = true) int id,
@JsonProperty(required = true) String name) {}

View File

@@ -0,0 +1,29 @@
package entralinked.gui.data;
import java.util.List;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import entralinked.model.pkmn.PkmnGender;
public record PkmnSpecies(
@JsonProperty(required = true) int id,
@JsonProperty(required = true) String name,
boolean downloadable, boolean hasFemaleSprite, PkmnGender gender, PkmnForm[] forms) {
@JsonIgnore
public boolean isSingleGender() {
return gender != null;
}
@JsonIgnore
public boolean hasForms() {
return forms != null && forms.length > 0;
}
@JsonIgnore
public List<PkmnGender> getGenders() {
return isSingleGender() ? List.of(gender) : List.of(PkmnGender.MALE, PkmnGender.FEMALE);
}
}

View File

@@ -0,0 +1,7 @@
package entralinked.gui.data;
import com.fasterxml.jackson.annotation.JsonProperty;
public record Region(
@JsonProperty(required = true) int id,
@JsonProperty(required = true) String name) {}

View File

@@ -0,0 +1,381 @@
package entralinked.gui.panels;
import java.awt.Dimension;
import java.awt.Image;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.Files;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Vector;
import javax.imageio.ImageIO;
import javax.swing.BorderFactory;
import javax.swing.DefaultComboBoxModel;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JComboBox;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.filechooser.FileFilter;
import javax.swing.filechooser.FileNameExtensionFilter;
import entralinked.Entralinked;
import entralinked.GameVersion;
import entralinked.gui.FileChooser;
import entralinked.gui.ModelListCellRenderer;
import entralinked.model.player.Player;
import entralinked.utility.Crc16;
import entralinked.utility.LEOutputStream;
import entralinked.utility.SwingUtility;
import entralinked.utility.TiledImageUtility;
import net.miginfocom.swing.MigLayout;
@SuppressWarnings("serial")
public class CustomizationPanel extends JPanel {
@FunctionalInterface
private static interface SkinWriter {
public void writeSkin(OutputStream outputStream, BufferedImage image) throws IOException;
}
/**
* Internal model for combo boxes.
*/
private static record DlcOption(String type, String name, String path, boolean custom) {
public DlcOption(String type, String name, String path) {
this(type, name, path, false);
}
}
private static final FileFilter IMAGE_FILE_FILTER = new FileNameExtensionFilter("Image Files (*.png)", "png");
private static final FileFilter CGEAR_FILE_FILTER = new FileNameExtensionFilter("C-Gear Skin Files (*.bin, *.cgb, *.psk)", "bin", "cgb", "psk");
private static final FileFilter ZUKAN_FILE_FILTER = new FileNameExtensionFilter("Pokédex Skin Files (*.bin, *.pds)", "bin", "pds");
private static final byte[] NARC_HEADER = { 0x4E, 0x41, 0x52, 0x43, (byte)0xFE, (byte)0xFF, 0x00, 0x01 };
private static final Map<String, Image> skinCache = new HashMap<>();
private final Entralinked entralinked;
private final JComboBox<DlcOption> cgearComboBox;
private final JComboBox<DlcOption> zukanComboBox;
private final JComboBox<DlcOption> musicalComboBox;
private final JPanel optionPanel;
private Player player;
private GameVersion gameVersion;
private DlcOption customCGearSkin;
private DlcOption customDexSkin;
private DlcOption customMusical;
public CustomizationPanel(Entralinked entralinked) {
this.entralinked = entralinked;
setLayout(new MigLayout("align 50% 50%"));
// Create preview labels
JLabel cgearPreviewLabel = new JLabel("No preview available.", JLabel.CENTER);
cgearPreviewLabel.setPreferredSize(new Dimension(TiledImageUtility.SCREEN_WIDTH, TiledImageUtility.SCREEN_HEIGHT));
JLabel dexPreviewLabel = new JLabel("No preview available.", JLabel.CENTER);
dexPreviewLabel.setPreferredSize(new Dimension(TiledImageUtility.SCREEN_WIDTH, TiledImageUtility.SCREEN_HEIGHT));
// Create preview image panels
// Labels are added to a subpanel first otherwise the preferred size will include the border which causes issues
JPanel cgearPreviewPanel = new JPanel(new MigLayout("insets 0"));
cgearPreviewPanel.setBorder(BorderFactory.createTitledBorder("C-Gear Skin Preview"));
cgearPreviewPanel.add(cgearPreviewLabel);
JPanel dexPreviewPanel = new JPanel(new MigLayout("insets 0"));
dexPreviewPanel.setBorder(BorderFactory.createTitledBorder("Pokédex Skin Preview"));
dexPreviewPanel.add(dexPreviewLabel);
JPanel previewPanel = new JPanel();
previewPanel.add(cgearPreviewPanel);
previewPanel.add(dexPreviewPanel);
add(previewPanel, "spanx, align 50%, wrap");
// Create combo boxes
ModelListCellRenderer<DlcOption> renderer = new ModelListCellRenderer<>(DlcOption.class, DlcOption::name, "Do not change");
cgearComboBox = new JComboBox<>();
cgearComboBox.setMinimumSize(cgearComboBox.getPreferredSize());
cgearComboBox.setRenderer(renderer);
cgearComboBox.addActionListener(event -> updateSkinPreview(cgearComboBox, cgearPreviewLabel));
zukanComboBox = new JComboBox<>();
zukanComboBox.setMinimumSize(zukanComboBox.getPreferredSize());
zukanComboBox.setRenderer(renderer);
zukanComboBox.addActionListener(event -> updateSkinPreview(zukanComboBox, dexPreviewLabel));
musicalComboBox = new JComboBox<>();
musicalComboBox.setMinimumSize(musicalComboBox.getPreferredSize());
musicalComboBox.setRenderer(renderer);
// Create option panel
optionPanel = new JPanel(new MigLayout());
// Create C-Gear skin selector
createDlcOption("C-Gear Skin", cgearComboBox, () -> {
FileChooser.showFileOpenDialog(getRootPane(), Arrays.asList(IMAGE_FILE_FILTER, CGEAR_FILE_FILTER), selection -> {
File dst = player.getCGearSkinFile();
File file = selection.file();
FileFilter filter = selection.filter();
if(filter == IMAGE_FILE_FILTER) {
if(!importSkinImage(file, dst, (stream, image) -> TiledImageUtility.writeCGearSkin(stream, image, !gameVersion.isVersion2()))) {
return;
}
} else if(filter == CGEAR_FILE_FILTER) {
if(!importSkinFile(file, dst, 9730)) {
return;
}
} else {
return;
}
DlcOption option = new DlcOption(gameVersion.isVersion2() ? "CGEAR2" : "CGEAR", file.getName(), dst.getAbsolutePath(), true);
updateCustomOption(cgearComboBox, customCGearSkin, option);
customCGearSkin = option;
player.setCustomCGearSkin(customCGearSkin.name());
});
});
// Create Pokédex skin selector
createDlcOption("Pokédex Skin", zukanComboBox, () -> {
FileChooser.showFileOpenDialog(getRootPane(), Arrays.asList(IMAGE_FILE_FILTER, ZUKAN_FILE_FILTER), selection -> {
File dst = player.getDexSkinFile();
File file = selection.file();
FileFilter filter = selection.filter();
if(filter == IMAGE_FILE_FILTER) {
if(!importSkinImage(file, dst, (stream, image) -> TiledImageUtility.writeDexSkin(stream, image, TiledImageUtility.generateBackgroundColors(image)))) {
return;
}
} else if(filter == ZUKAN_FILE_FILTER) {
if(!importSkinFile(file, dst, 25090)) {
return;
}
} else {
return;
}
DlcOption option = new DlcOption("ZUKAN", file.getName(), dst.getAbsolutePath(), true);
updateCustomOption(zukanComboBox, customDexSkin, option);
customDexSkin = option;
player.setCustomDexSkin(customDexSkin.name());
});
});
// Create musical show selector
createDlcOption("Musical Show", musicalComboBox, () -> {
SwingUtility.showIgnorableHint(getRootPane(), "Please exercise caution when importing custom musicals.\n"
+ "Downloading invalid data might cause game crashes or other issues.", "Attention", JOptionPane.WARNING_MESSAGE);
FileChooser.showFileOpenDialog(getRootPane(), selection -> {
File dst = player.getMusicalFile();
File file = selection.file();
if(!importNarcFile(file, dst)) {
return;
}
DlcOption option = new DlcOption("MUSICAL", file.getName(), dst.getAbsolutePath(), true);
updateCustomOption(musicalComboBox, customMusical, option);
customMusical = option;
player.setCustomMusical(customMusical.name());
});
});
add(optionPanel, "spanx, align 50%");
}
public void loadProfile(Player player) {
this.player = player;
gameVersion = player.getGameVersion();
String cgearType = player.getGameVersion().isVersion2() ? "CGEAR2" : "CGEAR";
customCGearSkin = player.getCustomCGearSkin() == null ? null : new DlcOption(cgearType, player.getCustomCGearSkin(), player.getCGearSkinFile().getAbsolutePath(), true);
customDexSkin = player.getCustomDexSkin() == null ? null : new DlcOption("ZUKAN", player.getCustomDexSkin(), player.getDexSkinFile().getAbsolutePath(), true);
customMusical = player.getCustomMusical() == null ? null : new DlcOption("MUSICAL", player.getCustomMusical(), player.getMusicalFile().getAbsolutePath(), true);
updateDlcOptions(cgearComboBox, cgearType, player.getCGearSkin(), customCGearSkin);
updateDlcOptions(zukanComboBox, "ZUKAN", player.getDexSkin(), customDexSkin);
updateDlcOptions(musicalComboBox, "MUSICAL", player.getMusical(), customMusical);
}
public void saveProfile(Player player) {
DlcOption cgearSkin = (DlcOption)cgearComboBox.getSelectedItem();
DlcOption dexSkin = (DlcOption)zukanComboBox.getSelectedItem();
DlcOption musical = (DlcOption)musicalComboBox.getSelectedItem();
player.setCGearSkin(cgearSkin == null ? null : cgearSkin.custom() ? "custom" : cgearSkin.name());
player.setDexSkin(dexSkin == null ? null : dexSkin.custom() ? "custom" : dexSkin.name());
player.setMusical(musical == null ? null : musical.custom() ? "custom" : musical.name());
}
private void createDlcOption(String label, JComboBox<DlcOption> comboBox, Runnable importListener) {
optionPanel.add(new JLabel(label), "sizegroup label");
optionPanel.add(comboBox, "sizegroup option");
JButton importButton = new JButton("Import");
importButton.addActionListener(event -> importListener.run());
optionPanel.add(importButton, "wrap");
}
private void updateDlcOptions(JComboBox<DlcOption> comboBox, String type, String selectedOption, DlcOption customOption) {
Vector<DlcOption> options = new Vector<>();
if(customOption != null) {
options.add(customOption);
}
entralinked.getDlcList().getDlcList("IRAO", type).forEach(dlc -> options.add(new DlcOption(type, dlc.name(), dlc.path())));
DlcOption selection = selectedOption == null ? null : selectedOption.equals("custom") ? customOption : options.stream().filter(x -> selectedOption.equals(x.name())).findFirst().orElse(null);
options.add(0, null); // "Do not change" option
comboBox.setModel(new DefaultComboBoxModel<DlcOption>(options));
comboBox.setSelectedItem(selection);
}
private void updateCustomOption(JComboBox<DlcOption> comboBox, DlcOption oldValue, DlcOption newValue) {
DefaultComboBoxModel<DlcOption> model = (DefaultComboBoxModel<DlcOption>)comboBox.getModel();
if(oldValue != null) {
model.removeElement(oldValue);
skinCache.remove(oldValue.path());
}
model.insertElementAt(newValue, 1);
model.setSelectedItem(newValue);
}
private void updateSkinPreview(JComboBox<DlcOption> comboBox, JLabel previewLabel) {
Image preview = getSkinImage((DlcOption)comboBox.getSelectedItem());
previewLabel.setText(preview == null ? "No preview available." : "");
previewLabel.setIcon(preview == null ? null : new ImageIcon(preview));
}
private Image getSkinImage(DlcOption option) {
return option == null ? null : skinCache.computeIfAbsent(option.path(), path -> {
try(FileInputStream inputStream = new FileInputStream(path)) {
return switch(option.type()) {
case "CGEAR" -> TiledImageUtility.readCGearSkin(inputStream, true);
case "CGEAR2" -> TiledImageUtility.readCGearSkin(inputStream, false);
case "ZUKAN" -> TiledImageUtility.readDexSkin(inputStream, true);
default -> throw new IllegalArgumentException("Invalid type: " + option.type());
};
} catch(Exception e) {
SwingUtility.showExceptionInfo(getRootPane(), "Failed to load skin preview.", e);
return null;
}
});
}
private boolean importSkinFile(File src, File dst, int expectedSize) {
int sizeWithoutChecksum = expectedSize - 2;
int length = (int)src.length();
// Check content length
if(length != expectedSize && length != sizeWithoutChecksum) {
JOptionPane.showMessageDialog(getRootPane(), "Invalid content length, expected either %s or %s bytes."
.formatted(sizeWithoutChecksum, expectedSize), "Attention", JOptionPane.WARNING_MESSAGE);
return false;
}
try {
byte[] bytes = Files.readAllBytes(src.toPath());
boolean writeChecksum = true;
// Validate checksum
if(length == expectedSize) {
int checksum = Crc16.calc(bytes, 0, sizeWithoutChecksum);
int checksumInFile = (bytes[bytes.length - 2] & 0xFF) | ((bytes[bytes.length - 1] & 0xFF) << 8);
if(checksum != checksumInFile) {
JOptionPane.showMessageDialog(getRootPane(), "File checksum doesn't match.", "Attention", JOptionPane.WARNING_MESSAGE);
return false;
}
writeChecksum = false;
}
// Write to destination & append checksum if necessary
try(LEOutputStream outputStream = new LEOutputStream(new FileOutputStream(dst))) {
outputStream.write(bytes);
if(writeChecksum) {
outputStream.writeShort(Crc16.calc(bytes));
}
}
return true;
} catch(Exception e) {
SwingUtility.showExceptionInfo(getRootPane(), "Failed to import skin.", e);
}
return false;
}
private boolean importSkinImage(File src, File dst, SkinWriter writer) {
try {
BufferedImage image = ImageIO.read(src);
int width = TiledImageUtility.SCREEN_WIDTH;
int height = TiledImageUtility.SCREEN_HEIGHT;
if(image.getWidth() != width || image.getHeight() != height) {
JOptionPane.showMessageDialog(getRootPane(), "Image size must be %sx%s pixels.".formatted(width, height), "Attention", JOptionPane.WARNING_MESSAGE);
return false;
}
ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
writer.writeSkin(byteStream, image);
byte[] bytes = byteStream.toByteArray();
try(LEOutputStream outputStream = new LEOutputStream(new FileOutputStream(dst))) {
outputStream.write(bytes);
outputStream.writeShort(Crc16.calc(bytes));
}
return true;
} catch(IllegalArgumentException e) {
JOptionPane.showMessageDialog(getRootPane(), e.getMessage(), "Attention", JOptionPane.WARNING_MESSAGE);
} catch(Exception e) {
SwingUtility.showExceptionInfo(getRootPane(), "Failed to import skin image.", e);
}
return false;
}
private boolean importNarcFile(File src, File dst) {
try {
byte[] bytes = Files.readAllBytes(src.toPath());
int offset = 0;
// Check narc header
if(!Arrays.equals(bytes, 0, NARC_HEADER.length, NARC_HEADER, 0, NARC_HEADER.length)) {
if(bytes.length < 16 || !Arrays.equals(bytes, 16, 16 + NARC_HEADER.length, NARC_HEADER, 0, NARC_HEADER.length)) {
JOptionPane.showMessageDialog(getRootPane(), "Invalid or unsupported file.", "Attention", JOptionPane.WARNING_MESSAGE);
return false;
}
offset = 16;
}
// TODO utility function
int length = ((bytes[offset + NARC_HEADER.length + 3] & 0xFF) << 24)
| ((bytes[offset + NARC_HEADER.length + 2] & 0xFF) << 16)
| ((bytes[offset + NARC_HEADER.length + 1] & 0xFF) << 8)
| (bytes[offset + NARC_HEADER.length] & 0xFF);
if(offset + length >= bytes.length) {
JOptionPane.showMessageDialog(getRootPane(), "File data is malformed or corrupt.", "Attention", JOptionPane.WARNING_MESSAGE);
return false;
}
// Write to destination
try(LEOutputStream outputStream = new LEOutputStream(new FileOutputStream(dst))) {
outputStream.write(bytes, offset, length);
outputStream.writeShort(Crc16.calc(bytes, offset, length));
}
return true;
} catch(Exception e) {
SwingUtility.showExceptionInfo(getRootPane(), "Failed to import NARC file.", e);
}
return false;
}
}

View File

@@ -0,0 +1,218 @@
package entralinked.gui.panels;
import java.awt.BorderLayout;
import java.awt.CardLayout;
import java.awt.Desktop;
import java.awt.GridLayout;
import java.io.IOException;
import java.net.URISyntaxException;
import java.net.URL;
import javax.swing.JButton;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JTabbedPane;
import javax.swing.text.AbstractDocument;
import com.formdev.flatlaf.extras.components.FlatTabbedPane;
import com.formdev.flatlaf.extras.components.FlatTabbedPane.TabAreaAlignment;
import com.formdev.flatlaf.extras.components.FlatTextField;
import entralinked.Entralinked;
import entralinked.GameVersion;
import entralinked.gui.GsidDocumentFilter;
import entralinked.gui.data.DataManager;
import entralinked.model.player.Player;
import entralinked.model.player.PlayerStatus;
import entralinked.utility.GsidUtility;
import entralinked.utility.SwingUtility;
import net.miginfocom.swing.MigLayout;
@SuppressWarnings("serial")
public class DashboardPanel extends JPanel {
public static final String LOGIN_CARD = "login";
public static final String MAIN_CARD = "main";
private final Entralinked entralinked;
private final JLabel sessionLabel;
private final FlatTabbedPane tabbedPane;
private CardLayout layout;
private SummaryPanel summaryPanel;
private EncounterEditorPanel encounterPanel;
private ItemEditorPanel itemPanel;
private VisitorEditorPanel visitorPanel;
private CustomizationPanel customizePanel;
private MiscPanel miscPanel;
private Player player;
private boolean initialized;
public DashboardPanel(Entralinked entralinked) {
this.entralinked = entralinked;
// Create GSID field
FlatTextField gsidTextField = new FlatTextField();
gsidTextField.setPlaceholderText("XXXXXXXXXX");
((AbstractDocument)gsidTextField.getDocument()).setDocumentFilter(new GsidDocumentFilter());
// Create login button
JButton loginButton = new JButton("Log in");
loginButton.addActionListener(event -> login(gsidTextField.getText()));
// Create browser dashboard button
JLabel browserButton = SwingUtility.createButtonLabel("Browser dashboard (Legacy)", () -> {
try {
Desktop.getDesktop().browse(new URL("http://127.0.0.1/dashboard/profile.html").toURI());
} catch(IOException | URISyntaxException e) {
SwingUtility.showExceptionInfo(getRootPane(), "Failed to open URL.", e);
}
});
// Create login panel
JPanel loginPanel = new JPanel(new MigLayout("wrap, align 50% 50%"));
String loginDescription = """
<html>
Tuck in a Pokémon and enter your Game Sync ID to continue.<br/>
Your Game Sync ID can be found in 'Game Sync Settings' in the game's main menu.
</html>
""";
loginPanel.add(SwingUtility.createTitleLabel("Log in"));
loginPanel.add(SwingUtility.createDescriptionLabel(loginDescription));
loginPanel.add(new JLabel("Game Sync ID"), "gapy 8");
loginPanel.add(gsidTextField, "growx");
loginPanel.add(loginButton, "growx");
loginPanel.add(browserButton, "gapy 8, align 100%");
// Create save button
JButton saveButton = new JButton("Save profile");
saveButton.addActionListener(event -> {
// Try to save data
// TODO this is probably not thread-safe!!!
try {
encounterPanel.saveProfile(player);
itemPanel.saveProfile(player);
visitorPanel.saveProfile(player);
customizePanel.saveProfile(player);
miscPanel.saveProfile(player);
player.setStatus(PlayerStatus.WAKE_READY);
if(!entralinked.getPlayerManager().savePlayer(player)) {
JOptionPane.showMessageDialog(getRootPane(), "Failed to write player data to disk.", "Attention", JOptionPane.WARNING_MESSAGE);
return;
}
JOptionPane.showMessageDialog(getRootPane(), "Profile data has been saved successfully!\n"
+ "Use Game Sync to wake up your Pokémon and download your selected content.");
} catch(Exception e) {
SwingUtility.showExceptionInfo(getRootPane(), "Failed to save player data.", e);
}
});
// Create log out button
JButton logoutButton = new JButton("Log out");
logoutButton.addActionListener(event -> {
gsidTextField.setText("");
layout.show(this, LOGIN_CARD);
});
// Create main footer button panel
JPanel buttonPanel = new JPanel(new GridLayout(1, 2));
buttonPanel.add(saveButton);
buttonPanel.add(logoutButton);
// Create main footer panel
sessionLabel = new JLabel("", JLabel.CENTER);
JPanel footerPanel = new JPanel(new BorderLayout());
footerPanel.add(sessionLabel);
footerPanel.add(buttonPanel, BorderLayout.LINE_END);
// Create main panel
tabbedPane = new FlatTabbedPane();
tabbedPane.setTabPlacement(JTabbedPane.LEFT);
tabbedPane.setTabAreaAlignment(TabAreaAlignment.center);
JPanel mainPanel = new JPanel(new BorderLayout());
mainPanel.add(tabbedPane);
mainPanel.add(footerPanel, BorderLayout.PAGE_END);
// Create layout
layout = new CardLayout();
setLayout(layout);
add(LOGIN_CARD, loginPanel);
add(MAIN_CARD, mainPanel);
}
private void login(String gameSyncId) {
// Check if GSID is valid
if(!GsidUtility.isValidGameSyncId(gameSyncId)) {
JOptionPane.showMessageDialog(this, "Please enter a valid Game Sync ID.", "Attention", JOptionPane.WARNING_MESSAGE);
return;
}
Player player = entralinked.getPlayerManager().getPlayer(gameSyncId);
// Check if player exists
if(player == null) {
JOptionPane.showMessageDialog(this, "This Game Sync ID does not exist.\n"
+ "If you haven't already, please tuck in a Pokémon first.", "Attention", JOptionPane.WARNING_MESSAGE);
return;
}
GameVersion version = player.getGameVersion();
// Check if necessary data is present
if(player.getDreamerInfo() == null || version == null) {
JOptionPane.showMessageDialog(this, "Please use Game Sync to tuck in a Pokémon first.", "Attention", JOptionPane.INFORMATION_MESSAGE);
player = null;
return;
}
// Try to initialize dashboard
if(!initialized && !initializeDashboard()) {
return;
}
// Load player data
sessionLabel.setText("Game Version: %s, Game Sync ID: %s".formatted(version.getDisplayName(), gameSyncId));
tabbedPane.setEnabledAt(3, version.isVersion2());
try {
summaryPanel.loadProfile(player);
encounterPanel.loadProfile(player);
itemPanel.loadProfile(player);
visitorPanel.loadProfile(player);
customizePanel.loadProfile(player);
miscPanel.loadProfile(player);
} catch(Exception e) {
SwingUtility.showExceptionInfo(getRootPane(), "Failed to load player data.", e);
return;
}
this.player = player;
tabbedPane.setSelectedIndex(0);
layout.show(this, MAIN_CARD);
}
private boolean initializeDashboard() {
try {
DataManager.loadData();
summaryPanel = new SummaryPanel();
encounterPanel = new EncounterEditorPanel();
itemPanel = new ItemEditorPanel();
visitorPanel = new VisitorEditorPanel();
customizePanel = new CustomizationPanel(entralinked);
miscPanel = new MiscPanel();
} catch(Exception e) {
SwingUtility.showExceptionInfo(getRootPane(), "Failed to initialize dashboard.", e);
return false;
}
tabbedPane.add("Summary", summaryPanel);
tabbedPane.add("Entree Forest", encounterPanel);
tabbedPane.add("Dream Remnants", itemPanel);
tabbedPane.add("Join Avenue", visitorPanel);
tabbedPane.add("Customization", customizePanel);
tabbedPane.add("Miscellaneous", miscPanel);
initialized = true;
return true;
}
}

View File

@@ -0,0 +1,227 @@
package entralinked.gui.panels;
import java.awt.Image;
import java.util.Arrays;
import java.util.Collections;
import entralinked.GameVersion;
import entralinked.gui.data.DataManager;
import entralinked.gui.data.PkmnForm;
import entralinked.gui.data.PkmnSpecies;
import entralinked.model.pkmn.PkmnGender;
import entralinked.model.player.DreamAnimation;
import entralinked.model.player.DreamEncounter;
import entralinked.model.player.Player;
@SuppressWarnings("serial")
public class EncounterEditorPanel extends TableEditorPanel {
public static final int ENCOUNTER_COUNT = 10;
public static final int SPECIES_COLUMN = 1;
public static final int MOVE_COLUMN = 2;
public static final int FORM_COLUMN = 3;
public static final int GENDER_COLUMN = 4;
public static final int ANIMATION_COLUMN = 5;
private static final String[] COLUMN_NAMES = { "Species", "Move", "Form", "Gender", "Animation" };
private static final Class<?>[] COLUMN_TYPES = { PkmnSpecies.class, Integer.class, PkmnForm.class, PkmnGender.class, DreamAnimation.class };
private GameVersion gameVersion;
private boolean optionLock; // Used to prevent double option updates
public EncounterEditorPanel() {
super(ENCOUNTER_COUNT, COLUMN_NAMES, COLUMN_TYPES);
setTitle("Entree Forest");
setDescription("""
<html>
Use the table below to configure Entree Forest encounters.<br/>
Up to 10 different encounters can be configured at a time.<br/>
Please be aware that Unova Pokémon are exclusive to B2W2.
</html>
""");
table.setCellRenderers(SPECIES_COLUMN, PkmnSpecies.class, PkmnSpecies::name, "— — — — —");
table.setCellRenderers(MOVE_COLUMN, Integer.class, DataManager::getMoveName, "None");
table.setCellRenderers(FORM_COLUMN, PkmnForm.class, PkmnForm::name);
table.setCellRenderers(GENDER_COLUMN, PkmnGender.class, PkmnGender::getDisplayName);
table.setCellRenderers(ANIMATION_COLUMN, DreamAnimation.class, DreamAnimation::getDisplayName);
initializeOptions();
}
@Override
public void dataChanged(int row, int column, Object oldValue, Object newValue) {
if(optionLock) {
return;
}
if(column == SPECIES_COLUMN) {
if(newValue == null) {
disableSecondaryOptions(row);
return;
}
optionLock = true;
table.enableOption(row, GENDER_COLUMN);
table.enableOption(row, MOVE_COLUMN);
table.enableOption(row, FORM_COLUMN);
table.enableOption(row, ANIMATION_COLUMN);
updateGenderOptions(row);
updateFormOptions(row);
updateMoveOptions(row);
optionLock = false;
} else if(column == GENDER_COLUMN) {
if(!isLegalMode()) {
return; // Gender only affects form options if legal mode is enabled
}
if(newValue == null) {
table.disableOption(row, FORM_COLUMN);
return;
}
updateFormOptions(row);
if(oldValue == null) {
table.enableOption(row, FORM_COLUMN);
}
} else if(column == FORM_COLUMN) {
if(!isLegalMode()) {
return; // Form only affects move options if legal mode is enabled
}
if(newValue == null) {
table.disableOption(row, MOVE_COLUMN);
return;
}
updateMoveOptions(row);
if(oldValue == null) {
table.enableOption(row, MOVE_COLUMN);
}
}
}
@Override
public void legalModeChanged() {
for(int i = 0; i < ENCOUNTER_COUNT; i++) {
updateSpeciesOptions(i);
PkmnSpecies species = getSpecies(i);
if(species != null) {
optionLock = true;
updateGenderOptions(i);
updateFormOptions(i);
updateMoveOptions(i);
optionLock = false;
}
}
}
@Override
public void initializeOptions(int row) {
setOptions(row, ANIMATION_COLUMN, Arrays.asList(DreamAnimation.values()));
disableSecondaryOptions(row);
}
@Override
public void randomizeSelections(int row) {
table.randomizeSelection(row, SPECIES_COLUMN, false);
table.randomizeSelection(row, GENDER_COLUMN);
table.randomizeSelection(row, FORM_COLUMN);
table.randomizeSelection(row, MOVE_COLUMN);
table.randomizeSelection(row, ANIMATION_COLUMN);
}
@Override
public void clearSelections(int row) {
model.setValueAt(null, row, SPECIES_COLUMN);
}
@Override
public Image getSelectionIcon(int row) {
return row == -1 ? DataManager.getPokemonSprite(0) : DataManager.getPokemonSprite(getSpecies(row), getForm(row), getGender(row), false);
}
@Override
public boolean shouldUpdateSelectionIcon(int column) {
return column == SPECIES_COLUMN || column == GENDER_COLUMN || column == FORM_COLUMN;
}
private void updateSpeciesOptions(int row) {
setOptions(row, SPECIES_COLUMN,
isLegalMode() ? DataManager.getSpeciesOptions(gameVersion) : DataManager.getDownloadableSpecies(gameVersion),
(a, b) -> a.name().compareTo(b.name()), true);
}
private void updateGenderOptions(int row) {
PkmnSpecies species = getSpecies(row);
setOptions(row, GENDER_COLUMN, isLegalMode() ? DataManager.getGenderOptions(gameVersion, species) : species.getGenders());
}
private void updateFormOptions(int row) {
PkmnSpecies species = getSpecies(row);
setOptions(row, FORM_COLUMN, species.hasForms() ? (isLegalMode() ? DataManager.getFormOptions(gameVersion, species, getGender(row))
: Arrays.asList(species.forms())) : Collections.emptyList());
}
private void updateMoveOptions(int row) {
setOptions(row, MOVE_COLUMN,
isLegalMode() ? DataManager.getMoveOptions(gameVersion, getSpecies(row), getGender(row), getForm(row)) : DataManager.getMoveIds(),
(a, b) -> DataManager.getMoveName(a).compareTo(DataManager.getMoveName(b)), true);
}
public void loadProfile(Player player) {
gameVersion = player.getGameVersion();
table.clearSelection();
clearSelections();
legalModeToggle.setSelected(false);
legalModeChanged();
int row = 0;
for(DreamEncounter encounter : player.getEncounters()) {
PkmnSpecies species = DataManager.getSpecies(encounter.species());
model.setValueAt(species, row, SPECIES_COLUMN);
model.setValueAt(encounter.move() == 0 ? null : encounter.move(), row, MOVE_COLUMN);
model.setValueAt(encounter.gender(), row, GENDER_COLUMN);
model.setValueAt(encounter.animation(), row, ANIMATION_COLUMN);
if(species.hasForms()) {
model.setValueAt(species.forms()[encounter.form()], row, FORM_COLUMN);
}
row++;
}
}
public void saveProfile(Player player) {
player.setEncounters(computeSelectionList(row ->
new DreamEncounter(getSpecies(row).id(), getMove(row), getForm(row), getGender(row), getAnimation(row)),
row -> getSpecies(row) != null));
}
private void disableSecondaryOptions(int row) {
table.disableOption(row, MOVE_COLUMN);
table.disableOption(row, FORM_COLUMN);
table.disableOption(row, GENDER_COLUMN);
table.disableOption(row, ANIMATION_COLUMN);
}
private PkmnSpecies getSpecies(int row) {
return table.getValueAt(row, SPECIES_COLUMN, PkmnSpecies.class);
}
private int getMove(int row) {
return table.getValueAt(row, MOVE_COLUMN, Integer.class, 0);
}
private int getForm(int row) {
PkmnForm form = table.getValueAt(row, FORM_COLUMN, PkmnForm.class);
return form == null ? 0 : form.id();
}
private PkmnGender getGender(int row) {
return table.getValueAt(row, GENDER_COLUMN, PkmnGender.class);
}
private DreamAnimation getAnimation(int row) {
return table.getValueAt(row, ANIMATION_COLUMN, DreamAnimation.class);
}
}

View File

@@ -0,0 +1,132 @@
package entralinked.gui.panels;
import java.awt.Image;
import java.util.stream.IntStream;
import entralinked.GameVersion;
import entralinked.gui.data.DataManager;
import entralinked.model.player.DreamItem;
import entralinked.model.player.Player;
@SuppressWarnings("serial")
public class ItemEditorPanel extends TableEditorPanel {
public static final int ITEM_COUNT = 20;
public static final int MAX_ITEM_QUANTITY = 20;
public static final int ITEM_COLUMN = 1;
public static final int QUANTITY_COLUMN = 2;
private static final String[] COLUMN_NAMES = { "Item", "Quantity" };
private static final Class<?>[] COLUMN_TYPES = { Integer.class, Integer.class };
private GameVersion gameVersion;
public ItemEditorPanel() {
super(ITEM_COUNT, COLUMN_NAMES, COLUMN_TYPES);
setTitle("Dream Remnants");
setDescription("""
<html>
Use the table below to select the items you want to receive.<br/>
After waking up your Pokémon, talk to the boy near the<br/>
Entree Forest entrance in the Entralink to receive your items.
</html>
""");
table.setCellRenderers(ITEM_COLUMN, Integer.class, DataManager::getItemName, "— — — — —");
initializeOptions();
}
@Override
public void dataChanged(int row, int column, Object oldValue, Object newValue) {
if(column != ITEM_COLUMN) {
return;
}
if(newValue != null) {
if(oldValue != null) {
return; // Do nothing if new and old values are both non-null
}
table.enableOption(row, QUANTITY_COLUMN);
return;
}
table.disableOption(row, QUANTITY_COLUMN);
}
@Override
public void legalModeChanged() {
for(int i = 0; i < ITEM_COUNT; i++) {
updateItemOptions(i);
}
}
@Override
public void initializeOptions(int row) {
setOptions(row, QUANTITY_COLUMN, IntStream.rangeClosed(1, ITEM_COUNT).boxed().toList());
table.disableOption(row, QUANTITY_COLUMN);
}
@Override
public void randomizeSelections(int row) {
table.randomizeSelection(row, ITEM_COLUMN, false);
int quantity = 1;
while(quantity < MAX_ITEM_QUANTITY && Math.random() < 0.5) {
quantity++;
}
model.setValueAt(quantity, row, QUANTITY_COLUMN);
}
@Override
public void clearSelections(int row) {
model.setValueAt(null, row, ITEM_COLUMN);
}
@Override
public Image getSelectionIcon(int row) {
int item = row == -1 ? 0 : getItem(row);
return DataManager.getItemSprite(item);
}
@Override
public boolean shouldUpdateSelectionIcon(int column) {
return column == ITEM_COLUMN;
}
@Override
public int getSelectionIconScale() {
return 2;
}
public void loadProfile(Player player) {
gameVersion = player.getGameVersion();
table.clearSelection();
clearSelections();
legalModeToggle.setSelected(false);
legalModeChanged();
int row = 0;
for(DreamItem item : player.getItems()) {
model.setValueAt(item.id(), row, ITEM_COLUMN);
model.setValueAt(item.quantity(), row, QUANTITY_COLUMN);
row++;
}
}
public void saveProfile(Player player) {
player.setItems(computeSelectionList(row -> new DreamItem(getItem(row), getQuantity(row)), row -> getItem(row) != 0));
}
private void updateItemOptions(int row) {
setOptions(row, ITEM_COLUMN,
isLegalMode() ? DataManager.getItemOptions() : DataManager.getDownloadableItems(gameVersion),
(a, b) -> DataManager.getItemName(a).compareTo(DataManager.getItemName(b)), true);
}
private int getItem(int row) {
return table.getValueAt(row, ITEM_COLUMN, Integer.class, 0);
}
private int getQuantity(int row) {
return table.getValueAt(row, QUANTITY_COLUMN, Integer.class, 0);
}
}

View File

@@ -0,0 +1,152 @@
package entralinked.gui.panels;
import java.util.ArrayList;
import java.util.List;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JPanel;
import javax.swing.JSpinner;
import javax.swing.JTextField;
import javax.swing.SpinnerNumberModel;
import javax.swing.text.PlainDocument;
import entralinked.gui.SizeLimitDocumentFilter;
import entralinked.model.player.DreamDecor;
import entralinked.model.player.Player;
import entralinked.utility.SwingUtility;
import net.miginfocom.swing.MigLayout;
@SuppressWarnings("serial")
public class MiscPanel extends JPanel {
/**
* Container for keeping track of Décor options.
*/
private static class DecorOption {
private final JCheckBox checkBox;
private final JSpinner spinner;
private final JTextField nameField;
public DecorOption(JPanel parent, String label) {
spinner = new JSpinner(new SpinnerNumberModel(0, 0, 127, 1));
nameField = new JTextField();
((PlainDocument)nameField.getDocument()).setDocumentFilter(new SizeLimitDocumentFilter(12));
checkBox = new JCheckBox(label);
checkBox.addActionListener(event -> {
boolean active = checkBox.isSelected();
spinner.setEnabled(active);
nameField.setEnabled(active);
});
parent.add(checkBox);
parent.add(spinner);
parent.add(nameField, "growx");
setActive(true);
}
public void setActive(boolean active) {
checkBox.setSelected(active);
spinner.setEnabled(active);
nameField.setEnabled(active);
}
public boolean isActive() {
return checkBox.isSelected();
}
public void setId(int id) {
spinner.setValue(id);
}
public int getId() {
return (int)spinner.getValue();
}
public void setName(String name) {
nameField.setText(name);
}
public String getName() {
return nameField.getText();
}
}
public static final int DECOR_COUNT = 5;
private final List<DecorOption> decorOptions = new ArrayList<>();
private final JSpinner levelSpinner;
public MiscPanel() {
setLayout(new MigLayout("align 50% 50%, insets 0, wrap", "", "[]0[]"));
// Create decor panel
JPanel decorPanel = new JPanel(new MigLayout("insets 0, wrap 3, fill", "[][][grow]"));
String decorDescription = """
<html>
Configure Décor options to appear in Loblolly's studio.<br/>
Due to the closure of the Dream World, this function serves no real purpose<br/>
and is mostly meant for people who want to test or just play around with it.
</html>
""";
decorPanel.add(SwingUtility.createTitleLabel("Dream Décor"), "spanx, wrap");
decorPanel.add(SwingUtility.createDescriptionLabel(decorDescription), "spanx, wrap");
for(int i = 0; i < DECOR_COUNT; i++) {
decorOptions.add(new DecorOption(decorPanel, "Decor %s".formatted(i + 1)));
}
// Create decor option buttons
JButton resetButton = new JButton("Default");
resetButton.addActionListener(event -> {
for(int i = 0; i < DECOR_COUNT; i++) {
DecorOption option = decorOptions.get(i);
DreamDecor decor = DreamDecor.DEFAULT_DECOR.get(i);
option.setActive(true);
option.setId(decor.id());
option.setName(decor.name());
}
});
JButton clearButton = new JButton("Clear");
clearButton.addActionListener(event -> {
for(DecorOption option : decorOptions) {
option.setActive(false);
option.setId(0);
option.setName("");
}
});
// Create decor button panel
JPanel buttonPanel = new JPanel(new MigLayout("insets 0", "0[]"));
buttonPanel.add(resetButton);
buttonPanel.add(clearButton);
decorPanel.add(buttonPanel, "spanx, align right");
add(decorPanel, "spanx, growx");
// Create level panel
levelSpinner = new JSpinner(new SpinnerNumberModel(0, 0, 99, 1));
JPanel levelPanel = new JPanel(new MigLayout("insets 0, wrap"));
levelPanel.add(SwingUtility.createTitleLabel("Level Gain"));
levelPanel.add(SwingUtility.createDescriptionLabel("Amount of levels to gain on waking up."));
levelPanel.add(levelSpinner, "grow");
add(levelPanel, "");
}
public void loadProfile(Player player) {
levelSpinner.setValue(player.getLevelsGained());
List<DreamDecor> decorList = player.getDecor();
for(int i = 0; i < DECOR_COUNT; i++) {
DecorOption option = decorOptions.get(i);
DreamDecor decor = i < decorList.size() ? decorList.get(i) : null;
option.setActive(decor != null);
option.setId(decor == null ? 0 : decor.id());
option.setName(decor == null ? "" : decor.name());
}
}
public void saveProfile(Player player) {
player.setDecor(decorOptions.stream().filter(DecorOption::isActive).map(x -> new DreamDecor(x.getId(), x.getName())).toList());
player.setLevelsGained((int)levelSpinner.getValue());
}
}

View File

@@ -0,0 +1,110 @@
package entralinked.gui.panels;
import java.awt.Desktop;
import java.io.IOException;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import entralinked.gui.component.PropertyDisplay;
import entralinked.gui.component.ShadowedSprite;
import entralinked.gui.data.DataManager;
import entralinked.model.pkmn.PkmnInfo;
import entralinked.model.player.Player;
import entralinked.utility.SwingUtility;
import net.miginfocom.swing.MigLayout;
@SuppressWarnings("serial")
public class SummaryPanel extends JPanel {
public static final String KEY_NAME = "name";
public static final String KEY_OT = "trainer";
public static final String KEY_ID = "tid";
public static final String KEY_SID = "sid";
public static final String KEY_PID = "pid";
public static final String KEY_ABILITY = "ability";
public static final String KEY_NATURE = "nature";
public static final String KEY_GENDER = "gender";
public static final String KEY_LEVEL = "level";
public static final String KEY_ITEM = "item";
private static final String[] FLAVOR_TEXT = {
"*pokemon* is trying to grow berries in a nearby garden.",
"*pokemon* is itching to earn some Dream Points.",
"*pokemon* is looking at the Tree of Dreams from afar.",
"*pokemon* is playing a fun minigame (and you're not allowed to join, ha!)",
"*pokemon* is checking out the Friend Board.",
"*pokemon* is dreaming about when they first met *trainer*.",
"*pokemon* left a berry at the Tree of Dreams.",
"*pokemon* is trying to come up with more of these."
}; // TODO I think it would be fun to grab various bits of data from the save file and use them here
private final ShadowedSprite icon;
private final JLabel flavorTextLabel;
private final PropertyDisplay infoDisplay;
private Player player;
public SummaryPanel() {
setLayout(new MigLayout("align 50% 50%, gapy 0, insets 0"));
// Create header panel
flavorTextLabel = SwingUtility.createDescriptionLabel();
JPanel headerPanel = new JPanel(new MigLayout("fillx, insets 0"));
headerPanel.add(SwingUtility.createTitleLabel("Summary"), "wrap");
headerPanel.add(flavorTextLabel, "wrap");
headerPanel.add(SwingUtility.createDescriptionLabel("Tucked-in Pokémon info:"), "wrap");
add(headerPanel, "spanx");
// Create icon label
icon = new ShadowedSprite();
add(icon, "gapright 4");
// Create property display
infoDisplay = new PropertyDisplay();
infoDisplay.addProperty(0, 0, KEY_NAME, "Name");
infoDisplay.addProperty(0, 1, KEY_OT, "OT");
infoDisplay.addProperty(0, 2, KEY_ID, "ID No.");
infoDisplay.addProperty(0, 3, KEY_SID, "SID No.", true);
infoDisplay.addProperty(0, 4, KEY_PID, "PID", true);
infoDisplay.addProperty(1, 0, KEY_ABILITY, "Ability");
infoDisplay.addProperty(1, 1, KEY_NATURE, "Nature");
infoDisplay.addProperty(1, 2, KEY_GENDER, "Gender");
infoDisplay.addProperty(1, 3, KEY_LEVEL, "Level");
infoDisplay.addProperty(1, 4, KEY_ITEM, "Held Item");
add(infoDisplay, "wrap");
// Create save file location button
JLabel openSaveButton = SwingUtility.createButtonLabel("Open data directory", () -> {
SwingUtility.showIgnorableHint(getRootPane(), "The game sends incomplete/corrupt save files to the server.\n"
+ "Please do not rely on Game Sync to back up your save files.", "Attention", JOptionPane.WARNING_MESSAGE);
try {
Desktop.getDesktop().open(player.getDataDirectory());
} catch(IOException e) {
SwingUtility.showExceptionInfo(getRootPane(), "Couldn't open data directory.", e);
}
});
add(openSaveButton, "spanx, align 100%, gapy 8");
}
public void loadProfile(Player player) {
this.player = player;
PkmnInfo info = player.getDreamerInfo();
icon.setImage(DataManager.getPokemonSprite(info.species(), info.form(), info.gender(), info.isShiny()));
infoDisplay.setValue(KEY_NAME, info.nickname());
infoDisplay.setValue(KEY_OT, info.trainerName());
infoDisplay.setValue(KEY_ID, "%05d".formatted(info.trainerId()));
infoDisplay.setValue(KEY_SID, "%05d".formatted(info.trainerSecretId()));
infoDisplay.setValue(KEY_PID, "%08X".formatted(info.personality()));
infoDisplay.setValue(KEY_ABILITY, DataManager.getAbilityName(info.ability()));
infoDisplay.setValue(KEY_NATURE, info.nature().getDisplayName());
infoDisplay.setValue(KEY_GENDER, info.gender().getDisplayName());
infoDisplay.setValue(KEY_LEVEL, info.level());
infoDisplay.setValue(KEY_ITEM, info.heldItem() == 0 ? "None" : DataManager.getItemName(info.heldItem()));
SwingUtility.setTextFieldToggle(infoDisplay.getValueField(KEY_SID), false);
SwingUtility.setTextFieldToggle(infoDisplay.getValueField(KEY_PID), false);
String flavorText = FLAVOR_TEXT[(int)(Math.random() * FLAVOR_TEXT.length)]
.replace("*pokemon*", info.nickname())
.replace("*trainer*", info.trainerName());
flavorTextLabel.setText(flavorText);
}
}

View File

@@ -0,0 +1,247 @@
package entralinked.gui.panels;
import java.awt.Dimension;
import java.awt.Image;
import java.awt.image.BufferedImage;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.ListSelectionModel;
import javax.swing.table.DefaultTableModel;
import javax.swing.table.TableModel;
import entralinked.gui.component.ConfigTable;
import entralinked.gui.component.ShadowedSprite;
import entralinked.utility.SwingUtility;
import net.miginfocom.swing.MigLayout;
@SuppressWarnings("serial")
public abstract class TableEditorPanel extends JPanel {
protected final Map<Integer, Object> oldValues = new HashMap<>();
protected final int rowCount;
protected final ConfigTable table;
protected final TableModel model;
protected final JLabel titleLabel;
protected final JLabel descriptionLabel;
protected final ShadowedSprite selectionIcon;
protected JButton randomizeButton;
protected JButton clearAllButton;
protected JCheckBox legalModeToggle;
public TableEditorPanel(int rowCount, String[] columnNames, Class<?>[] columnTypes) {
this(rowCount, true, columnNames, columnTypes);
}
public TableEditorPanel(int rowCount, boolean incluceLegalMode, String[] columnNames, Class<?>[] columnTypes) {
if(columnNames.length != columnTypes.length) {
throw new IllegalArgumentException("Length mismatch between column names and column types");
}
this.rowCount = rowCount;
String[] columns = new String[columnNames.length + 1];
columns[0] = "No.";
System.arraycopy(columnNames, 0, columns, 1, columnNames.length);
// Create labels
titleLabel = SwingUtility.createTitleLabel();
descriptionLabel = SwingUtility.createDescriptionLabel();
selectionIcon = new ShadowedSprite();
// Create buttons & toggles
randomizeButton = new JButton("Random");
randomizeButton.addActionListener(event -> randomizeSelections());
clearAllButton = new JButton("Clear all");
clearAllButton.addActionListener(event -> clearSelections());
legalModeToggle = new JCheckBox("Legal mode");
legalModeToggle.addActionListener(event -> {
boolean selected = legalModeToggle.isSelected();
// Since we're clearing illegal settings, ask for confirmation first.
if(selected && !SwingUtility.showIgnorableConfirmDialog(getRootPane(), "Enable legal mode? Illegal selections will be cleared.", "Attention")) {
legalModeToggle.setSelected(false);
return;
}
legalModeChanged();
});
// Create layout
setLayout(new MigLayout("align 50% 50%, gapy 0, insets 0 8 0 8"));
// Create header panels
JPanel labelPanel = new JPanel(new MigLayout("insets 0, fill"));
labelPanel.add(titleLabel, "wrap");
labelPanel.add(descriptionLabel, "wrap");
JPanel headerPanel = new JPanel(new MigLayout("insets n n 0 n", "[][grow]"));
headerPanel.add(labelPanel);
headerPanel.add(selectionIcon, "align 100%");
add(headerPanel, "wrap, grow");
// Create table
model = new DefaultTableModel(columns, rowCount);
model.addTableModelListener(event -> {
int column = event.getColumn();
if(column == 0) {
return; // Ignore number column
}
if(shouldUpdateSelectionIcon(column)) {
updateSelectionIcon();
}
for(int i = event.getFirstRow(); i <= event.getLastRow(); i++) {
int index = i * rowCount + column;
Object oldValue = oldValues.get(index);
Object newValue = model.getValueAt(i, column);
Class<?> type = columnTypes[column - 1];
// Set to null if types don't match
if(oldValue != null && !type.isAssignableFrom(oldValue.getClass())) {
oldValue = null;
}
if(newValue != null && !type.isAssignableFrom(newValue.getClass())) {
newValue = null;
}
// Fire data changed if new value is different from old value
if(newValue != oldValue) {
dataChanged(i, column, oldValue, newValue);
oldValues.put(index, model.getValueAt(i, column)); // We use getValueAt again because dataChanged might have updated the value
}
}
});
table = new ConfigTable();
table.putClientProperty("terminateEditOnFocusLost", Boolean.TRUE);
table.getTableHeader().setReorderingAllowed(false);
table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
table.setModel(model);
table.getSelectionModel().addListSelectionListener(event -> updateSelectionIcon());
int width = Math.max(table.getPreferredSize().width, table.getPreferredScrollableViewportSize().width);
table.setPreferredScrollableViewportSize(new Dimension(width, table.getRowHeight() * rowCount));
table.getColumnModel().getColumn(0).setResizable(false);
table.getColumnModel().getColumn(0).setMinWidth(32);
table.getColumnModel().getColumn(0).setMaxWidth(32);
add(new JScrollPane(table), "spanx, grow");
updateSelectionIcon();
// Initialize rows
for(int i = 0; i < rowCount; i++) {
model.setValueAt(i + 1, i, 0);
}
// Create footer panel
JPanel footerPanel = new JPanel(new MigLayout("insets 0", "0[]"));
footerPanel.add(randomizeButton);
footerPanel.add(clearAllButton);
if(incluceLegalMode) {
footerPanel.add(legalModeToggle);
}
add(footerPanel, "spanx, gapy 4");
}
public abstract void initializeOptions(int row);
public abstract void randomizeSelections(int row);
public abstract void clearSelections(int row);
public abstract Image getSelectionIcon(int row);
public void dataChanged(int row, int column, Object oldValue, Object newValue) {
// Override
}
public void legalModeChanged() {
// Override
}
public boolean shouldUpdateSelectionIcon(int column) {
return true; // Override
}
public int getSelectionIconScale() {
return 1;
}
public void initializeOptions() {
for(int i = 0; i < rowCount; i++) {
initializeOptions(i);
}
}
public void randomizeSelections() {
for(int i = 0; i < rowCount; i++) {
randomizeSelections(i);
}
}
public void clearSelections() {
for(int i = 0; i < rowCount; i++) {
clearSelections(i);
}
}
public void setTitle(String text) {
titleLabel.setText(text);
}
public void setDescription(String text) {
descriptionLabel.setText(text);
}
public boolean isLegalMode() {
return legalModeToggle.isSelected();
}
private void updateSelectionIcon() {
BufferedImage image = (BufferedImage)getSelectionIcon(table.getSelectedRow());
selectionIcon.setImage(image, getSelectionIconScale(), 96, 96);
}
protected <T> void setOptions(int row, int column, Collection<T> options) {
setOptions(row, column, options, null);
}
protected <T> void setOptions(int row, int column, Collection<T> options, Comparator<T> sorter) {
setOptions(row, column, options, sorter, false);
}
protected <T> void setOptions(int row, int column, Collection<T> options, boolean includeNullOption) {
setOptions(row, column, options, null, includeNullOption);
}
protected <T> void setOptions(int row, int column, Collection<T> options, Comparator<T> sorter, boolean includeNullOption) {
List<T> list = new ArrayList<>(options);
if(sorter != null) {
list.sort(sorter);
}
table.setOptionsAt(row, column, list, includeNullOption);
}
protected <T> List<T> computeSelectionList(Function<Integer, T> supplier, Function<Integer, Boolean> rowFilter) {
List<T> list = new ArrayList<>();
for(int i = 0; i < rowCount; i++) {
if(rowFilter.apply(i)) {
list.add(supplier.apply(i));
}
}
return list;
}
}

View File

@@ -0,0 +1,272 @@
package entralinked.gui.panels;
import java.awt.Image;
import java.awt.Toolkit;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.stream.IntStream;
import java.util.stream.Stream;
import javax.swing.InputVerifier;
import javax.swing.JComponent;
import javax.swing.JTextField;
import entralinked.GameVersion;
import entralinked.gui.ImageLoader;
import entralinked.gui.InputVerifierCellEditor;
import entralinked.gui.data.Country;
import entralinked.gui.data.DataManager;
import entralinked.gui.data.PkmnSpecies;
import entralinked.gui.data.Region;
import entralinked.model.avenue.AvenueShopType;
import entralinked.model.avenue.AvenueVisitor;
import entralinked.model.avenue.AvenueVisitorType;
import entralinked.model.player.Player;
@SuppressWarnings("serial")
public class VisitorEditorPanel extends TableEditorPanel {
public static final int VISITOR_COUNT = 12;
public static final int TYPE_COLUMN = 1;
public static final int NAME_COLUMN = 2;
public static final int SHOP_COLUMN = 3;
public static final int GAME_COLUMN = 4;
public static final int COUNTRY_COLUMN = 5;
public static final int REGION_COLUMN = 6;
public static final int PHRASE_COLUMN = 7;
public static final int DREAMER_COLUMN = 8;
private static final String[] COLUMN_NAMES = { "Type", "Name", "Shop", "Game", "Country", "Region", "Phrase", "Pokémon" };
private static final Class<?>[] COLUMN_TYPES = { AvenueVisitorType.class, String.class, AvenueShopType.class, GameVersion.class, Country.class, Region.class, Integer.class, PkmnSpecies.class };
// Name tables for randomization
// TODO more languages would be neat but isn't really necessary
private static final String[] ENG_NAMES_M = { "Aleron", "Alpi", "Amando", "Anselmi", "Anton", "Arhippa", "Arpo", "Artos", "Assar", "Atilio", "Axel", "Azzo", "Jasper", "Jaylen", "Jephew", "Jimbo", "Joakim", "Joelle", "Jonas", "Jule", "Julian", "Julio", "Justan" };
private static final String[] ENG_NAMES_F = { "Agata", "Ainikki", "Alena", "Alibena", "Alwyn", "Anelma", "Anneli", "Annetta", "Antonie", "Armina", "Assunta", "Asta", "Jaclyn", "Jane", "Janette", "Jannis", "Jeanne", "Jenna", "Jess", "Joan", "Jocelyn", "Josie", "Judith", "Julie" };
private static final String[] JAP_NAMES_M = { "アートス", "アーポ", "アクセル", "アッサール", "アッツォ", "アッラン", "アティリオ", "アマンド", "アルピ", "アルヒッパ", "アンセルミ", "アントン", "ジェヒュー", "ジェリー", "ジェローム", "ジャコブ", "ジャスパー", "ジャレッド", "ジャン", "ジュール", "ジョエル", "ジョシュア", "ジョナス" };
private static final String[] JAP_NAMES_F = { "アーム", "アイニッキ", "アガタ", "アスタ", "アッスンタ", "アネルマ", "アラベッラ", "アリビーナ", "アレーナ", "アントニナ", "アンネッタ", "アンネリ", "ジェーン", "ジェシー", "ジェナ", "ジャサント", "ジャニス", "ジャンヌ", "ジュディス", "ジュリー", "ジョアン", "ジョイス", "ジョスリン", "ジョゼ" };
public VisitorEditorPanel() {
super(VISITOR_COUNT, false, COLUMN_NAMES, COLUMN_TYPES);
setTitle("Join Avenue");
setDescription("""
<html>
Use the table below to configure Join Avenue visitors.<br/>
Please be aware that new visitors will not appear if you have<br/>
already reached the max amount of concurrent visitors.
</html>
""");
table.setCellRenderers(TYPE_COLUMN, AvenueVisitorType.class, AvenueVisitorType::getDisplayName, "— — — — —");
table.setCellRenderers(SHOP_COLUMN, AvenueShopType.class, AvenueShopType::getDisplayName);
table.setCellRenderers(GAME_COLUMN, GameVersion.class, GameVersion::getDisplayName);
table.setCellRenderers(COUNTRY_COLUMN, Country.class, Country::name);
table.setCellRenderers(REGION_COLUMN, Region.class, Region::name);
table.setCellRenderers(DREAMER_COLUMN, PkmnSpecies.class, PkmnSpecies::name);
initializeOptions();
}
@Override
public void dataChanged(int row, int column, Object oldValue, Object newValue) {
if(column == TYPE_COLUMN) {
if(newValue != null) {
if(oldValue != null) {
return; // Do nothing if new and old values are both non-null
}
table.enableOption(row, SHOP_COLUMN);
table.enableOption(row, GAME_COLUMN, GameVersion.BLACK_2_ENGLISH); // Updates country -> region
table.enableOption(row, PHRASE_COLUMN);
table.enableOption(row, DREAMER_COLUMN);
table.enableOption(row, NAME_COLUMN, findRandomName(row));
model.setValueAt(findRandomName(row), row, NAME_COLUMN);
return;
}
disableSecondaryOptions(row);
} else if(column == GAME_COLUMN) {
if(newValue == null) {
table.disableOption(row, COUNTRY_COLUMN);
return;
}
GameVersion newVersion = (GameVersion)newValue;
boolean shouldUpdate = oldValue == null; // Update by default if previous value was null
// Check if language code changed from or to Japanese
if(oldValue != null) {
GameVersion oldVersion = (GameVersion)oldValue;
shouldUpdate |= (oldVersion.getLanguageCode() == 1 && newVersion.getLanguageCode() != 1)
|| (oldVersion.getLanguageCode() != 1 && newVersion.getLanguageCode() == 1);
}
if(shouldUpdate) {
// Japanese language visitors seem to only be able to appear if their set country is Japan
setOptions(row, COUNTRY_COLUMN, newVersion.getLanguageCode() == 1 ? Arrays.asList(DataManager.getCountry(105)) : DataManager.getCountries());
table.enableOption(row, COUNTRY_COLUMN);
}
} else if(column == COUNTRY_COLUMN) {
if(newValue == null) {
table.disableOption(row, REGION_COLUMN);
return;
}
Country country = (Country)newValue;
setOptions(row, REGION_COLUMN, country.hasRegions() ? country.regions() : Collections.emptyList(), (a, b) -> a.name().compareTo(b.name()));
table.enableOption(row, REGION_COLUMN);
}
}
@Override
public void initializeOptions(int row) {
setOptions(row, TYPE_COLUMN, Arrays.asList(AvenueVisitorType.values()), true);
setOptions(row, SHOP_COLUMN, Arrays.asList(AvenueShopType.values()));
setOptions(row, GAME_COLUMN, Arrays.asList(GameVersion.values()), (a, b) -> Integer.compare(a.getLanguageCode(), b.getLanguageCode()));
setOptions(row, PHRASE_COLUMN, IntStream.rangeClosed(0, 7).boxed().toList());
setOptions(row, DREAMER_COLUMN, DataManager.getSpecies(), (a, b) -> a.name().compareTo(b.name()));
InputVerifier nameVerifier = new InputVerifier() {
@Override
public boolean verify(JComponent input) {
String text = ((JTextField)input).getText();
String error = checkName(text, row);
if(error != null) {
// TODO show hint
Toolkit.getDefaultToolkit().beep();
return false;
}
return true;
}
};
table.setCellEditor(row, NAME_COLUMN, new InputVerifierCellEditor(nameVerifier));
disableSecondaryOptions(row);
}
@Override
public void randomizeSelections(int row) {
table.randomizeSelection(row, TYPE_COLUMN, false);
table.randomizeSelection(row, SHOP_COLUMN);
table.randomizeSelection(row, GAME_COLUMN);
table.randomizeSelection(row, COUNTRY_COLUMN);
table.randomizeSelection(row, REGION_COLUMN);
table.randomizeSelection(row, PHRASE_COLUMN);
table.randomizeSelection(row, DREAMER_COLUMN);
model.setValueAt(findRandomName(row), row, NAME_COLUMN);
}
@Override
public void clearSelections(int row) {
model.setValueAt(null, row, TYPE_COLUMN);
}
@Override
public Image getSelectionIcon(int row) {
String name = row == -1 || getType(row) == null ? "none" : getType(row).name().toLowerCase();
return ImageLoader.getImage("/sprites/trainers/%s.png".formatted(name));
}
public void loadProfile(Player player) {
table.clearSelection();
clearSelections();
int row = 0;
for(AvenueVisitor visitor : player.getAvenueVisitors()) {
model.setValueAt(visitor.type(), row, TYPE_COLUMN);
model.setValueAt(visitor.name(), row, NAME_COLUMN);
model.setValueAt(visitor.shopType(), row, SHOP_COLUMN);
model.setValueAt(visitor.gameVersion(), row, GAME_COLUMN);
Country country = DataManager.getCountry(visitor.countryCode());
model.setValueAt(country, row, COUNTRY_COLUMN);
if(country.hasRegions()) {
model.setValueAt(country.regions().get(visitor.stateProvinceCode() - 1), row, REGION_COLUMN);
}
model.setValueAt(visitor.personality(), row, PHRASE_COLUMN);
model.setValueAt(DataManager.getSpecies(visitor.dreamerSpecies()), row, DREAMER_COLUMN);
row++;
}
}
public void saveProfile(Player player) {
player.setAvenueVisitors(computeSelectionList(i -> new AvenueVisitor(
getName(i), getType(i), getShop(i), getGameVersion(i), getCountry(i).id(), getRegion(i), getPhrase(i), getDreamer(i).id()),
i -> getType(i) != null));
}
private String findRandomName(int row) {
boolean japanese = getGameVersion(row).getLanguageCode() == 1;
String[] table = null;
if(getType(row).isFemale()) {
table = japanese ? JAP_NAMES_F : ENG_NAMES_F;
} else {
table = japanese ? JAP_NAMES_M : ENG_NAMES_M;
}
List<String> existingNames = computeSelectionList(this::getName, i -> getType(row) != null);
List<String> options = Stream.of(table).filter(x -> !existingNames.contains(x)).toList();
return options.get((int)(Math.random() * options.size())); // Just make sure there are enough name options in the tables...
}
private String checkName(String name, int ignoreRow) {
if(name.isBlank()) {
return "Visitor name can't be blank.";
}
if(name.length() > 7) {
return "Visitor name can't exceed 7 characters.";
}
for(int i = 0; i < VISITOR_COUNT; i++) {
if(i != ignoreRow && name.equals(getName(i))) {
return "Visitors can't have the same name.";
}
}
return null;
}
private void disableSecondaryOptions(int row) {
table.disableOption(row, NAME_COLUMN);
table.disableOption(row, SHOP_COLUMN);
table.disableOption(row, GAME_COLUMN);
// table.disableOption(row, COUNTRY_COLUMN);
// table.disableOption(row, REGION_COLUMN);
table.disableOption(row, PHRASE_COLUMN);
table.disableOption(row, DREAMER_COLUMN);
}
private AvenueVisitorType getType(int row) {
return table.getValueAt(row, TYPE_COLUMN, AvenueVisitorType.class, null);
}
private String getName(int row) {
return table.getValueAt(row, NAME_COLUMN, String.class, "");
}
private AvenueShopType getShop(int row) {
return table.getValueAt(row, SHOP_COLUMN, AvenueShopType.class, null);
}
private GameVersion getGameVersion(int row) {
return table.getValueAt(row, GAME_COLUMN, GameVersion.class, null);
}
private Country getCountry(int row) {
return table.getValueAt(row, COUNTRY_COLUMN, Country.class, null);
}
private int getRegion(int row) {
Region region = table.getValueAt(row, REGION_COLUMN, Region.class, null);
return region == null ? 0 : region.id();
}
private int getPhrase(int row) {
return table.getValueAt(row, PHRASE_COLUMN, Integer.class, 0);
}
private PkmnSpecies getDreamer(int row) {
return table.getValueAt(row, DREAMER_COLUMN, PkmnSpecies.class, null);
}
}

View File

@@ -0,0 +1,256 @@
package entralinked.gui.view;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Desktop;
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;
import javax.swing.ImageIcon;
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;
import javax.swing.JTextPane;
import javax.swing.UIManager;
import javax.swing.text.AttributeSet;
import javax.swing.text.BadLocationException;
import javax.swing.text.DefaultCaret;
import javax.swing.text.Document;
import javax.swing.text.SimpleAttributeSet;
import javax.swing.text.StyleConstants;
import javax.swing.text.StyleContext;
import org.apache.logging.log4j.Level;
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;
/**
* Simple Swing user interface.
*/
public class MainView {
public static final Color TEXT_COLOR = Color.WHITE.darker();
public static final Color TEXT_COLOR_WARN = Color.YELLOW.darker();
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"));
UIManager.put("Table.alternateRowColor", ColorFunctions.lighten(UIManager.getColor("Table.background"), 0.05F));
// Create status label
statusLabel = new JLabel("Servers are starting, please wait a bit...", JLabel.CENTER);
// Create footer panel
JPanel footerPanel = new JPanel(new BorderLayout());
footerPanel.add(statusLabel, BorderLayout.CENTER);
footerPanel.setBorder(BorderFactory.createEmptyBorder(2, 0, 5, 0));
// Create console output
JTextPane consoleOutputPane = new JTextPane() {
@Override
public boolean getScrollableTracksViewportWidth() {
return getPreferredSize().width <= getParent().getSize().width;
}
@Override
public Dimension getPreferredSize() {
return getUI().getPreferredSize(this);
};
};
consoleOutputPane.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12));
consoleOutputPane.setEditable(false);
((DefaultCaret)consoleOutputPane.getCaret()).setUpdatePolicy(DefaultCaret.ALWAYS_UPDATE);
// Create console output appender
ConsumerAppender.addConsumer("GuiOutput", message -> {
Document document = consoleOutputPane.getDocument();
Level level = message.level();
Color color = level == Level.ERROR ? TEXT_COLOR_ERROR : level == Level.WARN ? TEXT_COLOR_WARN : TEXT_COLOR;
AttributeSet colorAttribute = styleContext.addAttribute(fontAttribute, StyleConstants.Foreground, color);
try {
consoleOutputPane.getDocument().insertString(document.getLength(), message.formattedMessage(), colorAttribute);
} catch(BadLocationException e) {}
});
// Create console output scroll pane
JScrollPane scrollPane = new JScrollPane();
scrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS);
scrollPane.setViewportView(consoleOutputPane);
// Create main panel
JPanel panel = new JPanel(new BorderLayout());
panel.add(scrollPane, BorderLayout.CENTER);
panel.add(footerPanel, BorderLayout.PAGE_END);
// Create tabbed pane
JTabbedPane tabbedPane = new JTabbedPane();
tabbedPane.addTab("Console", panel);
tabbedPane.addTab("Dashboard", new DashboardPanel(entralinked));
// Create window
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", () -> {
try {
Desktop.getDesktop().browse(new URL("https://github.com/kuroppoi/entralinked").toURI());
} catch(IOException | URISyntaxException e) {
SwingUtility.showExceptionInfo(frame, "Failed to open URL.", e);
}
}));
menuBar.add(helpMenu);
// Set window properties
frame.addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(WindowEvent event) {
// Update status
statusLabel.setText("Servers are shutting down, please wait a bit...");
// Run asynchronously so it doesn't just awkwardly freeze
// Still scuffed but better than nothing I guess
CompletableFuture.runAsync(() -> {
entralinked.stopServers();
System.exit(0);
});
}
});
frame.setIconImages(List.of(
new ImageIcon(getClass().getResource("/icon-64x.png")).getImage(),
new ImageIcon(getClass().getResource("/icon-32x.png")).getImage(),
new ImageIcon(getClass().getResource("/icon-16x.png")).getImage()));
frame.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
frame.setJMenuBar(menuBar);
frame.add(tabbedPane);
frame.getContentPane().setPreferredSize(new Dimension(733, 463));
frame.pack();
frame.setMinimumSize(frame.getSize());
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
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

@@ -1,4 +1,4 @@
package entralinked.gui; package entralinked.gui.view;
import java.awt.GridBagLayout; import java.awt.GridBagLayout;
import java.util.regex.Pattern; import java.util.regex.Pattern;
@@ -60,7 +60,8 @@ public class PidToolDialog {
// Make sure user exists // Make sure user exists
if(user == null) { if(user == null) {
JOptionPane.showMessageDialog(dialog, "This Wi-Fi Connection ID does not exist.", "Attention", JOptionPane.WARNING_MESSAGE); JOptionPane.showMessageDialog(dialog, "This Wi-Fi Connection ID does not exist.\n"
+ "If you haven't attempted to connect yet, please do that first.", "Attention", JOptionPane.WARNING_MESSAGE);
return; return;
} }

View File

@@ -5,11 +5,21 @@ import com.fasterxml.jackson.annotation.JsonEnumDefaultValue;
public enum AvenueShopType { public enum AvenueShopType {
@JsonEnumDefaultValue @JsonEnumDefaultValue
RAFFLE, RAFFLE("Raffle Shop"),
FLORIST, FLORIST("Flower Shop"),
SALON, SALON("Beauty Salon"),
ANTIQUE, ANTIQUE("Antique Shop"),
DOJO, DOJO("Dojo"),
CAFE, CAFE("Café"),
MARKET MARKET("Market");
private final String displayName;
private AvenueShopType(String displayName) {
this.displayName = displayName;
}
public String getDisplayName() {
return displayName;
}
} }

View File

@@ -6,47 +6,53 @@ public enum AvenueVisitorType {
// 0 // 0
@JsonEnumDefaultValue @JsonEnumDefaultValue
YOUNGSTER(0), YOUNGSTER("Youngster", 0),
LASS(0, true), LASS("Lass", 0, true),
// 1 // 1
ACE_TRAINER_MALE(1), ACE_TRAINER_MALE("Ace Trainer♂", 1),
ACE_TRAINER_FEMALE(1, true), ACE_TRAINER_FEMALE("Ace Trainer♀", 1, true),
// 2 // 2
RANGER_MALE(2), RANGER_MALE("Pokémon Ranger♂", 2),
RANGER_FEMALE(2, true), RANGER_FEMALE("Pokémon Ranger♀", 2, true),
// 3 // 3
BREEDER_MALE(3), BREEDER_MALE("Pokémon Breeder♂", 3),
BREEDER_FEMALE(3, true), BREEDER_FEMALE("Pokémon Breeder♀", 3, true),
// 4 // 4
SCIENTIST_MALE(4), SCIENTIST_MALE("Scientist♂", 4),
SCIENTIST_FEMALE(4, true), SCIENTIST_FEMALE("Scientist♀", 4, true),
// 5 // 5
HIKER(5), HIKER("Hiker", 5),
PARASOL_LADY(5, true), PARASOL_LADY("Parasol Lady", 5, true),
// 6 // 6
ROUGHNECK(6), ROUGHNECK("Roughneck", 6),
NURSE(6, true), NURSE("Nurse", 6, true),
// 7 // 7
PRESCHOOLER_MALE(7), PRESCHOOLER_MALE("Preschooler♂", 7),
PRESCHOOLER_FEMALE(7, true); PRESCHOOLER_FEMALE("Preschooler♀", 7, true);
private final String displayName;
private final int clientId; private final int clientId;
private final boolean female; private final boolean female;
private AvenueVisitorType(int clientId, boolean female) { private AvenueVisitorType(String displayName, int clientId, boolean female) {
this.displayName = displayName;
this.clientId = clientId; this.clientId = clientId;
this.female = female; this.female = female;
} }
private AvenueVisitorType(int clientId) { private AvenueVisitorType(String displayName, int clientId) {
this(clientId, false); this(displayName, clientId, false);
}
public String getDisplayName() {
return displayName;
} }
public int getClientId() { public int getClientId() {

View File

@@ -3,6 +3,5 @@ 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,7 +6,6 @@ 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;
@@ -18,9 +17,7 @@ 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();
@@ -30,32 +27,31 @@ public class DlcList {
public DlcList() { public DlcList() {
logger.info("Loading DLC ..."); logger.info("Loading DLC ...");
try(BufferedReader reader = new BufferedReader(new InputStreamReader(getClass().getResourceAsStream("/dlc.paths")))) { // Extract defaults if external DLC directory is not present
reader.lines().forEach(line -> { if(!dataDirectory.exists()) {
String[] segments = line.split("\t"); logger.info("Extracting default DLC files ...");
BufferedReader reader = new BufferedReader(new InputStreamReader(getClass().getResourceAsStream("/dlc.paths")));
if(segments.length != 2) { String line = null;
return;
} try {
while((line = reader.readLine()) != null) {
String path = segments[0]; InputStream resource = getClass().getResourceAsStream(line);
String hash = segments[1]; File outputFile = new File("./%s".formatted(line));
File outputFile = new File("./%s".formatted(path));
// Create parent directories
if(outputFile.getParentFile() != null) { if(outputFile.getParentFile() != null) {
outputFile.getParentFile().mkdirs(); outputFile.getParentFile().mkdirs();
} }
try(InputStream inputStream = getClass().getResourceAsStream(path)){ // Copy resource to destination
if(!outputFile.exists() || !hash.equals(MD5.digest(outputFile))) { if(resource != null) {
Files.copy(inputStream, outputFile.toPath(), StandardCopyOption.REPLACE_EXISTING); Files.copy(resource, outputFile.toPath());
} }
} catch(IOException e) {
logger.error("Couldn't process resource '{}'", path, e);
} }
}); } catch (IOException e) {
} catch(IOException e) { logger.error("Could not extract default DLC files", e);
logger.error("Couldn't extract DLC data", e); return;
}
} }
// Just to be sure... // Just to be sure...
@@ -75,7 +71,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 '{}'", subFile.getName(), file.getName()); logger.warn("Non-directory '{}' in DLC subfolder '{}'", file.getName(), subFile.getName());
continue; continue;
} }

View File

@@ -5,7 +5,17 @@ import com.fasterxml.jackson.annotation.JsonEnumDefaultValue;
public enum PkmnGender { public enum PkmnGender {
@JsonEnumDefaultValue @JsonEnumDefaultValue
MALE, MALE("Male"),
FEMALE, FEMALE("Female"),
GENDERLESS; GENDERLESS("Genderless");
private final String displayName;
private PkmnGender(String displayName) {
this.displayName = displayName;
}
public String getDisplayName() {
return displayName;
}
} }

View File

@@ -63,7 +63,7 @@ public class PkmnInfoReader {
int trainerSecretId = buffer.getShortLE(14) & 0xFFFF; int trainerSecretId = buffer.getShortLE(14) & 0xFFFF;
int level = buffer.getByte(140); int level = buffer.getByte(140);
int ability = buffer.getByte(21) & 0xFF; int ability = buffer.getByte(21) & 0xFF;
int form = (buffer.getByte(64) >> 3) & 0xFF; int form = (buffer.getByte(64) >> 3) & 0x1F;
boolean genderless = ((buffer.getByte(64) >> 2) & 1) == 1; boolean genderless = ((buffer.getByte(64) >> 2) & 1) == 1;
boolean female = ((buffer.getByte(64) >> 1) & 1) == 1; boolean female = ((buffer.getByte(64) >> 1) & 1) == 1;
PkmnGender gender = genderless ? PkmnGender.GENDERLESS : female ? PkmnGender.FEMALE : PkmnGender.MALE; PkmnGender gender = genderless ? PkmnGender.GENDERLESS : female ? PkmnGender.FEMALE : PkmnGender.MALE;

View File

@@ -5,33 +5,44 @@ import com.fasterxml.jackson.annotation.JsonEnumDefaultValue;
public enum PkmnNature { public enum PkmnNature {
@JsonEnumDefaultValue @JsonEnumDefaultValue
HARDY, HARDY("Hardy"),
LONELY, LONELY("Lonely"),
BRAVE, BRAVE("Brave"),
ADAMANT, ADAMANT("Adamant"),
NAUGHTY, NAUGHTY("Naughty"),
BOLD, BOLD("Bold"),
DOCILE, DOCILE("Docile"),
RELAXED, RELAXED("Relaxed"),
IMPISH, IMPISH("Impish"),
LAX, LAX("Lax"),
TIMID, TIMID("Timid"),
HASTY, HASTY("Hasty"),
SERIOUS, SERIOUS("Serious"),
JOLLY, JOLLY("Jolly"),
NAIVE, NAIVE("Naive"),
MODEST, MODEST("Modest"),
MILD, MILD("Mild"),
QUIET, QUIET("Quiet"),
BASHFUL, BASHFUL("Bashful"),
RASH, RASH("Rash"),
CALM, CALM("Calm"),
GENTLE, GENTLE("Gentle"),
SASSY, SASSY("Sassy"),
CAREFUL, CAREFUL("Careful"),
QUIRKY; QUIRKY("Quirky");
private final String displayName;
private PkmnNature(String displayName) {
this.displayName = displayName;
}
public static PkmnNature valueOf(int index) { public static PkmnNature valueOf(int index) {
return index >= 0 && index < values().length ? values()[index] : null; return index >= 0 && index < values().length ? values()[index] : null;
} }
public String getDisplayName() {
return displayName;
}
} }

View File

@@ -8,44 +8,54 @@ public enum DreamAnimation {
* Look around, but stay in the same position. * Look around, but stay in the same position.
*/ */
@JsonEnumDefaultValue @JsonEnumDefaultValue
LOOK_AROUND, LOOK_AROUND("Look around"),
/** /**
* Walk around, but never change direction without moving a step in that direction. * Walk around, but never change direction without moving a step in that direction.
*/ */
WALK_AROUND, WALK_AROUND("Walk around"),
/** /**
* Walk around and occasionally change direction without moving. * Walk around and occasionally change direction without moving.
*/ */
WALK_LOOK_AROUND, WALK_LOOK_AROUND("Walk and look around"),
/** /**
* Only walk up and down. * Only walk up and down.
*/ */
WALK_VERTICALLY, WALK_VERTICALLY("Walk up and down"),
/** /**
* Only walk left and right. * Only walk left and right.
*/ */
WALK_HORIZONTALLY, WALK_HORIZONTALLY("Walk left and right"),
/** /**
* Only walk left and right, and occasionally change direction without moving. * Only walk left and right, and occasionally change direction without moving.
*/ */
WALK_LOOK_HORIZONTALLY, WALK_LOOK_HORIZONTALLY("Walk left and right and look around"),
/** /**
* Continuously spin right. * Continuously spin right.
*/ */
SPIN_RIGHT, SPIN_RIGHT("Spin right"),
/** /**
* Continuously spin left. * Continuously spin left.
*/ */
SPIN_LEFT; SPIN_LEFT("Spin left");
private final String displayName;
private DreamAnimation(String displayName) {
this.displayName = displayName;
}
public static DreamAnimation valueOf(int index) { public static DreamAnimation valueOf(int index) {
return index >= 0 && index < values().length ? values()[index] : null; return index >= 0 && index < values().length ? values()[index] : null;
} }
public String getDisplayName() {
return displayName;
}
} }

View File

@@ -1,7 +1,19 @@
package entralinked.model.player; package entralinked.model.player;
import java.util.List;
import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonProperty;
public record DreamDecor( public record DreamDecor(
@JsonProperty(required = true) int id, @JsonProperty(required = true) int id,
@JsonProperty(required = true) String name) {} @JsonProperty(required = true) String name) {
// TODO names probably differed per language
public static final List<DreamDecor> DEFAULT_DECOR = List.of(
new DreamDecor(1, "Design Table"),
new DreamDecor(2, "Design Stool"),
new DreamDecor(3, "Flower Vase"),
new DreamDecor(4, "Cuddle Rug"),
new DreamDecor(6, "Wall Poster")
);
}

View File

@@ -16,6 +16,7 @@ public class Player {
private final List<DreamEncounter> encounters = new ArrayList<>(); private final List<DreamEncounter> encounters = new ArrayList<>();
private final List<DreamItem> items = new ArrayList<>(); private final List<DreamItem> items = new ArrayList<>();
private final List<AvenueVisitor> avenueVisitors = new ArrayList<>(); private final List<AvenueVisitor> avenueVisitors = new ArrayList<>();
private final List<DreamDecor> decor = new ArrayList<>();
private PlayerStatus status; private PlayerStatus status;
private GameVersion gameVersion; private GameVersion gameVersion;
private PkmnInfo dreamerInfo; private PkmnInfo dreamerInfo;
@@ -25,6 +26,7 @@ public class Player {
private String musical; private String musical;
private String customCGearSkin; private String customCGearSkin;
private String customDexSkin; private String customDexSkin;
private String customMusical;
private File dataDirectory; private File dataDirectory;
public Player(String gameSyncId) { public Player(String gameSyncId) {
@@ -37,6 +39,8 @@ public class Player {
encounters.clear(); encounters.clear();
items.clear(); items.clear();
avenueVisitors.clear(); avenueVisitors.clear();
decor.clear();
decor.addAll(DreamDecor.DEFAULT_DECOR);
levelsGained = 0; levelsGained = 0;
cgearSkin = null; cgearSkin = null;
dexSkin = null; dexSkin = null;
@@ -80,6 +84,17 @@ public class Player {
return Collections.unmodifiableList(avenueVisitors); return Collections.unmodifiableList(avenueVisitors);
} }
public void setDecor(Collection<DreamDecor> decor) {
if(decor.size() <= 5) {
this.decor.clear();
this.decor.addAll(decor);
}
}
public List<DreamDecor> getDecor() {
return Collections.unmodifiableList(decor);
}
public void setStatus(PlayerStatus status) { public void setStatus(PlayerStatus status) {
this.status = status; this.status = status;
} }
@@ -152,6 +167,14 @@ public class Player {
return customDexSkin; return customDexSkin;
} }
public void setCustomMusical(String customMusical) {
this.customMusical = customMusical;
}
public String getCustomMusical() {
return customMusical;
}
// IO stuff // IO stuff
public void setDataDirectory(File dataDirectory) { public void setDataDirectory(File dataDirectory) {
@@ -177,4 +200,8 @@ public class Player {
public File getDexSkinFile() { public File getDexSkinFile() {
return new File(dataDirectory, "zukan.bin"); return new File(dataDirectory, "zukan.bin");
} }
public File getMusicalFile() {
return new File(dataDirectory, "musical.bin");
}
} }

View File

@@ -23,16 +23,18 @@ public record PlayerDto(
String musical, String musical,
String customCGearSkin, String customCGearSkin,
String customDexSkin, String customDexSkin,
String customMusical,
int levelsGained, int levelsGained,
@JsonDeserialize(contentAs = DreamEncounter.class) Collection<DreamEncounter> encounters, @JsonDeserialize(contentAs = DreamEncounter.class) Collection<DreamEncounter> encounters,
@JsonDeserialize(contentAs = DreamItem.class) Collection<DreamItem> items, @JsonDeserialize(contentAs = DreamItem.class) Collection<DreamItem> items,
@JsonDeserialize(contentAs = AvenueVisitor.class) Collection<AvenueVisitor> avenueVisitors) { @JsonDeserialize(contentAs = AvenueVisitor.class) Collection<AvenueVisitor> avenueVisitors,
@JsonDeserialize(contentAs = DreamDecor.class) Collection<DreamDecor> decor) {
public PlayerDto(Player player) { public PlayerDto(Player player) {
this(player.getGameSyncId(), player.getGameVersion(), player.getStatus(), player.getDreamerInfo(), this(player.getGameSyncId(), player.getGameVersion(), player.getStatus(), player.getDreamerInfo(),
player.getCGearSkin(), player.getDexSkin(), player.getMusical(), player.getCustomCGearSkin(), player.getCGearSkin(), player.getDexSkin(), player.getMusical(), player.getCustomCGearSkin(),
player.getCustomDexSkin(), player.getLevelsGained(), player.getEncounters(), player.getItems(), player.getCustomDexSkin(), player.getCustomMusical(), player.getLevelsGained(), player.getEncounters(),
player.getAvenueVisitors()); player.getItems(), player.getAvenueVisitors(), player.getDecor());
} }
/** /**
@@ -48,10 +50,12 @@ public record PlayerDto(
player.setMusical(musical); player.setMusical(musical);
player.setCustomCGearSkin(customCGearSkin); player.setCustomCGearSkin(customCGearSkin);
player.setCustomDexSkin(customDexSkin); player.setCustomDexSkin(customDexSkin);
player.setCustomMusical(customMusical);
player.setLevelsGained(levelsGained); player.setLevelsGained(levelsGained);
player.setEncounters(encounters == null ? Collections.emptyList() : encounters); player.setEncounters(encounters == null ? Collections.emptyList() : encounters);
player.setItems(items == null ? Collections.emptyList() : items); player.setItems(items == null ? Collections.emptyList() : items);
player.setAvenueVisitors(avenueVisitors == null ? Collections.emptyList() : avenueVisitors); player.setAvenueVisitors(avenueVisitors == null ? Collections.emptyList() : avenueVisitors);
player.setDecor(decor == null ? DreamDecor.DEFAULT_DECOR : decor);
return player; return player;
} }
} }

View File

@@ -18,6 +18,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.databind.SerializationFeature;
import entralinked.GameVersion; import entralinked.GameVersion;
import entralinked.utility.GsidUtility;
/** /**
* Manager class for managing {@link Player} information (Global Link users) * Manager class for managing {@link Player} information (Global Link users)
@@ -117,9 +118,14 @@ public class PlayerManager {
Player player = mapper.readValue(inputFile, PlayerDto.class).toPlayer(); Player player = mapper.readValue(inputFile, PlayerDto.class).toPlayer();
String gameSyncId = player.getGameSyncId(); String gameSyncId = player.getGameSyncId();
// Check if Game Sync ID is valid
if(!GsidUtility.isValidGameSyncId(gameSyncId)) {
throw new IOException("Invalid Game Sync ID: %s".formatted(gameSyncId));
}
// Check for duplicate Game Sync ID // Check for duplicate Game Sync ID
if(doesPlayerExist(gameSyncId)) { if(doesPlayerExist(gameSyncId)) {
throw new IOException("Duplicate Game Sync ID %s".formatted(gameSyncId)); throw new IOException("Duplicate Game Sync ID: %s".formatted(gameSyncId));
} }
player.setDataDirectory(inputFile.getParentFile()); player.setDataDirectory(inputFile.getParentFile());
@@ -180,7 +186,13 @@ public class PlayerManager {
public Player registerPlayer(String gameSyncId, GameVersion version) { 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 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; return null;
} }

View File

@@ -1,17 +1,18 @@
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, File> dlcOverrides = new HashMap<>(); private final Map<String, Dlc> 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) {
@@ -58,11 +59,11 @@ public class User {
return Collections.unmodifiableMap(profiles); return Collections.unmodifiableMap(profiles);
} }
public void setDlcOverride(String type, File file) { public void setDlcOverride(String type, Dlc target) {
if(file == null) { if(target == null) {
dlcOverrides.remove(type); dlcOverrides.remove(type);
} else { } else {
dlcOverrides.put(type, file); dlcOverrides.put(type, target);
} }
} }
@@ -74,7 +75,7 @@ public class User {
return dlcOverrides.containsKey(type); return dlcOverrides.containsKey(type);
} }
public File getDlcOverride(String type) { public Dlc getDlcOverride(String type) {
return dlcOverrides.get(type); return dlcOverrides.get(type);
} }

View File

@@ -19,7 +19,6 @@ import javax.imageio.ImageIO;
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
import org.bouncycastle.util.Arrays;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
@@ -35,7 +34,6 @@ import entralinked.model.player.Player;
import entralinked.model.player.PlayerManager; import entralinked.model.player.PlayerManager;
import entralinked.model.player.PlayerStatus; import entralinked.model.player.PlayerStatus;
import entralinked.network.http.HttpHandler; import entralinked.network.http.HttpHandler;
import entralinked.utility.ColorUtility;
import entralinked.utility.Crc16; import entralinked.utility.Crc16;
import entralinked.utility.GsidUtility; import entralinked.utility.GsidUtility;
import entralinked.utility.LEOutputStream; import entralinked.utility.LEOutputStream;
@@ -49,6 +47,8 @@ import io.javalin.json.JavalinJackson;
/** /**
* HTTP handler for requests made to the user dashboard. * HTTP handler for requests made to the user dashboard.
*
* @deprecated
*/ */
public class DashboardHandler implements HttpHandler { public class DashboardHandler implements HttpHandler {
@@ -310,29 +310,8 @@ public class DashboardHandler implements HttpHandler {
previewImage = TiledImageUtility.readCGearSkin(new ByteArrayInputStream(skinBytes), offsetIndices); previewImage = TiledImageUtility.readCGearSkin(new ByteArrayInputStream(skinBytes), offsetIndices);
break; break;
case "ZUKAN": case "ZUKAN":
// Generate some background colors based roughly on what's in the image
int[] backgroundColors = Arrays.copyOf(TiledImageUtility.DEFAULT_DEX_BACKGROUND_COLORS, 64);
int backgroundColor = image.getRGB(0, 0);
int dexColor = image.getRGB(0, 134);
int buttonColor = image.getRGB(128, 134);
// Background colors (58, 59, 60)
for(int i = 0; i < 3; i++) {
backgroundColors[i + 58] = ColorUtility.multiplyColor(backgroundColor, 0.7 + i * 0.15);
}
// Pokédex colors (48, 49, 50, 51, 52)
for(int i = 0; i < 5; i++) {
backgroundColors[i + 48] = ColorUtility.multiplyColor(dexColor, 1.15 - i * 0.15);
}
// Pokédex button colors (53, 54, 55, 56)
for(int i = 0; i < 4; i++) {
backgroundColors[i + 53] = ColorUtility.multiplyColor(buttonColor, 0.55 + i * 0.15);
}
// Process skin data // Process skin data
TiledImageUtility.writeDexSkin(byteOutputStream, image, backgroundColors); TiledImageUtility.writeDexSkin(byteOutputStream, image, TiledImageUtility.generateBackgroundColors(image));
skinBytes = byteOutputStream.toByteArray(); skinBytes = byteOutputStream.toByteArray();
previewImage = TiledImageUtility.readDexSkin(new ByteArrayInputStream(skinBytes), true); previewImage = TiledImageUtility.readDexSkin(new ByteArrayInputStream(skinBytes), true);
break; break;

View File

@@ -8,6 +8,7 @@ import entralinked.model.player.DreamEncounter;
import entralinked.model.player.DreamItem; import entralinked.model.player.DreamItem;
import entralinked.model.player.Player; import entralinked.model.player.Player;
@Deprecated
public record DashboardProfileMessage( public record DashboardProfileMessage(
String gameVersion, String gameVersion,
String dreamerSprite, String dreamerSprite,

View File

@@ -9,6 +9,7 @@ import entralinked.model.avenue.AvenueVisitor;
import entralinked.model.player.DreamEncounter; import entralinked.model.player.DreamEncounter;
import entralinked.model.player.DreamItem; import entralinked.model.player.DreamItem;
@Deprecated
public record DashboardProfileUpdateRequest( public record DashboardProfileUpdateRequest(
@JsonProperty(required = true) @JsonDeserialize(contentAs = DreamEncounter.class) List<DreamEncounter> encounters, @JsonProperty(required = true) @JsonDeserialize(contentAs = DreamEncounter.class) List<DreamEncounter> encounters,
@JsonProperty(required = true) @JsonDeserialize(contentAs = DreamItem.class) List<DreamItem> items, @JsonProperty(required = true) @JsonDeserialize(contentAs = DreamItem.class) List<DreamItem> items,

View File

@@ -1,5 +1,6 @@
package entralinked.network.http.dashboard; package entralinked.network.http.dashboard;
@Deprecated
public record DashboardStatusMessage(String message, boolean error) { public record DashboardStatusMessage(String message, boolean error) {
public DashboardStatusMessage(String message) { public DashboardStatusMessage(String message) {

View File

@@ -1,11 +1,7 @@
package entralinked.network.http.dls; package entralinked.network.http.dls;
import java.io.File; import java.io.FileInputStream;
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;
@@ -14,14 +10,15 @@ 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.GameVersion; import entralinked.model.dlc.Dlc;
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.MysteryGiftUtility; import entralinked.utility.LEOutputStream;
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;
@@ -33,10 +30,11 @@ 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 File rootDirectory = new File("dlc"); private final DlcList dlcList;
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();
} }
@@ -66,7 +64,6 @@ 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());
}; };
@@ -83,48 +80,16 @@ 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 = getDlcType(request.attr1()); String type = getRegionlessDlcType(request.dlcType());
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)) {
files = Arrays.asList(user.getDlcOverride(type)); ctx.result(dlcList.getDlcListString(List.of(user.getDlcOverride(type))));
} else { return;
// Get list of files in DLC directory
File directory = getDlcDirectory(gameCode, type);
files = directory.isDirectory() ? Arrays.asList(directory.listFiles()) : new ArrayList<>();
} }
if(attr2 != null) { // TODO NOTE: I assume that in a conventional implementation, certain DLC attributes may be omitted from the request.
// PGL content attr2 hack ctx.result(dlcList.getDlcListString(dlcList.getDlcList(gameCode, type, request.dlcIndex())));
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());
} }
/** /**
@@ -133,45 +98,33 @@ 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 = getDlcType(request.attr1()); String type = getRegionlessDlcType(request.dlcType());
File file = user.hasDlcOverride(type) ? user.getDlcOverride(type) : type != null Dlc dlc = user.hasDlcOverride(type) ? user.getDlcOverride(type) : dlcList.getDlc(gameCode, type, request.dlcName());
? 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(!file.exists()) { if(dlc == null) {
ctx.status(HttpStatus.NOT_FOUND); ctx.status(HttpStatus.NOT_FOUND);
return; return;
} }
byte[] bytes = Files.readAllBytes(file.toPath()); // Write DLC data
try(FileInputStream inputStream = new FileInputStream(dlc.path())) {
if(type == null) { LEOutputStream outputStream = new LEOutputStream(ctx.outputStream());
// Generation 4 Mystery Gift inputStream.transferTo(outputStream);
bytes = MysteryGiftUtility.createUniversalGiftData4(bytes, file.getName());
} else if(type.equals("MYSTERY")) { // If checksum is not part of the file, manually append it
// Generation 5 Mystery Gift if(!dlc.checksumEmbedded()) {
bytes = MysteryGiftUtility.createUniversalGiftData5(bytes); outputStream.writeShort(dlc.checksum());
}
} }
// 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.substring(0, 3)) { return switch(gameCode) {
case "IRA" -> "IRAO"; // BW & B2W2 case "IRAJ", "IRAK" -> "IRAO";
case "ADA", "CPU", "IPG" -> "ADAE"; // DPPt & HGSS
default -> gameCode; default -> gameCode;
}; };
} }
@@ -179,15 +132,13 @@ 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 getDlcType(String attr1) { private String getRegionlessDlcType(String dlcType) {
if(attr1 == null || !attr1.contains("_")) { return switch(dlcType) {
return attr1; case "CGEAR_E", "CGEAR_F", "CGEAR_I", "CGEAR_G", "CGEAR_S", "CGEAR_J", "CGEAR_K" -> "CGEAR";
} 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";
return attr1.substring(0, attr1.lastIndexOf('_')); case "MUSICAL_E", "MUSICAL_F", "MUSICAL_I", "MUSICAL_G", "MUSICAL_S", "MUSICAL_J", "MUSICAL_K" -> "MUSICAL";
} 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 attr1, // action=list @JsonProperty("attr1") String dlcType, // action=list
@JsonProperty("attr2") String attr2, // action=list @JsonProperty("attr2") int dlcIndex, // 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, attr1=%s, attr2=%s, offset=%s, num=%s]") return ("DlsRequest[gameCode=%s, action=%s, dlcGameCode=%s, dlcName=%s, dlcType=%s, dlcIndex=%s, offset=%s, num=%s]")
.formatted(gameCode, action, dlcGameCode, dlcName, attr1, attr2, offset, num); .formatted(gameCode, action, dlcGameCode, dlcName, dlcType, dlcIndex, offset, num);
} }
} }

View File

@@ -16,7 +16,9 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import entralinked.Configuration; import entralinked.Configuration;
import entralinked.Entralinked; import entralinked.Entralinked;
import entralinked.GameVersion;
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;
@@ -51,12 +53,6 @@ public class PglHandler implements HttpHandler {
private static final String password = "2Phfv9MY"; // Best security in the world private static final String password = "2Phfv9MY"; // Best security in the world
private final ObjectMapper mapper = new ObjectMapper(new UrlEncodedFormFactory() private final ObjectMapper mapper = new ObjectMapper(new UrlEncodedFormFactory()
.disable(UrlEncodedFormParser.Feature.BASE64_DECODE_VALUES)); .disable(UrlEncodedFormParser.Feature.BASE64_DECODE_VALUES));
private final List<DreamDecor> decorList = List.of(
new DreamDecor(1, "+----------+"),
new DreamDecor(2, "Thank you"),
new DreamDecor(3, "for using"),
new DreamDecor(4, "Entralinked!"),
new DreamDecor(5, "+----------+"));
private final Set<Integer> sleepyList = new HashSet<>(); private final Set<Integer> sleepyList = new HashSet<>();
private final Configuration configuration; private final Configuration configuration;
private final DlcList dlcList; private final DlcList dlcList;
@@ -201,48 +197,26 @@ public class PglHandler implements HttpHandler {
return; return;
} }
logger.info("Player {} is downloading save data as user {}", player.getGameSyncId(), user.getRedactedId()); logger.info("Player {} is downloading save data", player.getGameSyncId());
// Write status code // Write status code
writeStatusCode(outputStream, 0); writeStatusCode(outputStream, 0);
// Allow it to wake up anyway, maybe the poor sap is stuck.. // Allow waking up but don't send any data
// Just don't send any other data.
if(player.getStatus() == PlayerStatus.AWAKE) { if(player.getStatus() == PlayerStatus.AWAKE) {
return; return;
} }
GameVersion version = player.getGameVersion();
List<DreamEncounter> encounters = player.getEncounters(); List<DreamEncounter> encounters = player.getEncounters();
List<DreamItem> items = player.getItems(); List<DreamItem> items = player.getItems();
List<DreamDecor> decorList = player.getDecor();
// Prepare DLC information // When waking up a Pokémon, these 4 bytes are written to 0x1D304 in the save file.
String cgearType = player.getGameVersion().isVersion2() ? "CGEAR2" : "CGEAR"; // If the bytes in the game's save file match the new bytes, they will be set to 0x00000000
String cgearSkin = player.getCGearSkin(); // and no content will be downloaded.
String dexSkin = player.getDexSkin(); // Looking at some old save files, this was very likely just a total tuck-in/wake-up counter.
int cgearSkinIndex = 0; // Additionally, waking up sets a flag at 0x1D4A3 (seems to be a "Pokémon is tucked in" flag or something) to 0x0.
int dexSkinIndex = 0;
// Create or remove custom C-Gear skin DLC override
if("custom".equals(cgearSkin)) {
cgearSkinIndex = 1;
user.setDlcOverride(cgearType, player.getCGearSkinFile());
} else {
cgearSkinIndex = dlcList.getDlcIndex("IRAO", cgearType, cgearSkin);
user.removeDlcOverride(cgearType);
}
// Create or remove custom Pokédex skin DLC override
if("custom".equals(dexSkin)) {
dexSkinIndex = 1;
user.setDlcOverride("ZUKAN", player.getDexSkinFile());
} else {
dexSkinIndex = dlcList.getDlcIndex("IRAO", "ZUKAN", dexSkin);
user.removeDlcOverride("ZUKAN");
}
// When waking up a Pokémon, these 4 bytes are written to 0x1D304 in the save file.
// If the bytes in the game's save file match the new bytes, they will be set to 0x00000000
// and no content will be downloaded.
outputStream.writeInt((int)(Math.random() * Integer.MAX_VALUE)); outputStream.writeInt((int)(Math.random() * Integer.MAX_VALUE));
// Write encounter data (max 10) // Write encounter data (max 10)
@@ -261,10 +235,10 @@ public class PglHandler implements HttpHandler {
// Write misc stuff and DLC information // Write misc stuff and DLC information
outputStream.writeShort(player.getLevelsGained()); outputStream.writeShort(player.getLevelsGained());
outputStream.write(0); // Unknown outputStream.write(0); // Unknown
outputStream.write(dlcList.getDlcIndex("IRAO", "MUSICAL", player.getMusical())); outputStream.write(getDlcIndex(user, player.getMusical(), "MUSICAL", player.getMusicalFile()));
outputStream.write(cgearSkinIndex); outputStream.write(getDlcIndex(user, player.getCGearSkin(), version.isVersion2() ? "CGEAR2" : "CGEAR", player.getCGearSkinFile()));
outputStream.write(dexSkinIndex); outputStream.write(getDlcIndex(user, player.getDexSkin(), "ZUKAN", player.getDexSkinFile()));
outputStream.write(decorList.isEmpty() ? 0 : 1); // Seems to be a flag for indicating whether or not decor data is present outputStream.write(decorList.isEmpty() ? 0 : 1); // Decor flag (?) stored at 0x1D4A4
outputStream.write(0); // Must be zero? outputStream.write(0); // Must be zero?
// Write item IDs // Write item IDs
@@ -298,20 +272,23 @@ public class PglHandler implements HttpHandler {
} }
// Write decor padding // Write decor padding
outputStream.writeBytes(0, (5 - decorList.size()) * 26); for(int i = 0; i < (5 - decorList.size()); i++) {
outputStream.writeShort(0x7E); // Just reset to default state
outputStream.writeBytes(0, 24);
}
outputStream.writeShort(0); // ? outputStream.writeShort(0); // ?
// Join Avenue visitor data -- copied in parts to 0x2422C in the save file. // Join Avenue visitor data -- copied in parts to 0x2422C in the save file.
// Black Version 2 and White Version 2 only. // Black Version 2 and White Version 2 only.
if(player.getGameVersion().isVersion2()) { if(version.isVersion2()) {
List<AvenueVisitor> avenueVisitors = player.getAvenueVisitors(); List<AvenueVisitor> avenueVisitors = player.getAvenueVisitors();
for(AvenueVisitor visitor : avenueVisitors) { for(AvenueVisitor visitor : avenueVisitors) {
// Write visitor name + padding. Names cannot be duplicate. // Write visitor name + padding. Names cannot be duplicate.
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, 16 - 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.
@@ -326,7 +303,7 @@ public class PglHandler implements HttpHandler {
outputStream.writeInt(1); // [20] Ignores if 0 outputStream.writeInt(1); // [20] Ignores if 0
outputStream.write(visitor.countryCode()); outputStream.write(visitor.countryCode());
outputStream.write(visitor.stateProvinceCode()); outputStream.write(visitor.stateProvinceCode());
outputStream.write(0); // [26] Ignores if 1 outputStream.write(visitor.gameVersion().getLanguageCode()); // 99% sure this is the lang code because 1 seems to be ignored ONLY if country code isn't Japan (plus it's right above the rom code)
outputStream.write(visitor.gameVersion().getRomCode()); // Affects shop stock outputStream.write(visitor.gameVersion().getRomCode()); // Affects shop stock
outputStream.write(visitor.type().isFemale() ? 1 : 0); outputStream.write(visitor.type().isFemale() ? 1 : 0);
outputStream.write(0); // [29] Does.. something outputStream.write(0); // [29] Does.. something
@@ -344,6 +321,13 @@ public class PglHandler implements HttpHandler {
*/ */
private void handleMemoryLink(PglRequest request, Context ctx) throws IOException { private void handleMemoryLink(PglRequest request, Context ctx) throws IOException {
LEOutputStream outputStream = new LEOutputStream(ctx.outputStream()); LEOutputStream outputStream = new LEOutputStream(ctx.outputStream());
// Check if Game Sync ID is valid
if(!GsidUtility.isValidGameSyncId(request.gameSyncId())) {
writeStatusCode(outputStream, 8); // Invalid Game Sync ID
return;
}
Player player = playerManager.getPlayer(request.gameSyncId()); Player player = playerManager.getPlayer(request.gameSyncId());
User user = ctx.attribute("user"); User user = ctx.attribute("user");
@@ -435,13 +419,9 @@ public class PglHandler implements HttpHandler {
private void handleUploadSaveData(PglRequest request, Context ctx) throws IOException { private void handleUploadSaveData(PglRequest request, Context ctx) throws IOException {
LEOutputStream outputStream = new LEOutputStream(ctx.outputStream()); LEOutputStream outputStream = new LEOutputStream(ctx.outputStream());
Player player = playerManager.getPlayer(request.gameSyncId()); Player player = playerManager.getPlayer(request.gameSyncId());
User user = ctx.attribute("user");
// Check if the player exists, has no Pokémon tucked in already and uses the same game version // Check if the player exists
if(player == null if(player == null) {
|| (!configuration.allowOverwritingPlayerDreamInfo() && player.getStatus() != PlayerStatus.AWAKE)
|| (!configuration.allowPlayerGameVersionMismatch() && player.getGameVersion() != null
&& request.gameVersion() != player.getGameVersion())) {
// Skip everything // Skip everything
ServletInputStream inputStream = ctx.req().getInputStream(); ServletInputStream inputStream = ctx.req().getInputStream();
@@ -454,8 +434,20 @@ public class PglHandler implements HttpHandler {
return; return;
} }
logger.info("Player {} is uploading save data as user {}", player.getGameSyncId(), user.getRedactedId()); logger.info("Player {} is uploading save data", player.getGameSyncId());
// Warn if player's current status is unexpected
if(player.getStatus() != PlayerStatus.AWAKE)
{
logger.warn("Player {} is not AWAKE -- existing dream information will be overwritten!", player.getGameSyncId());
}
// Warn if player's game version changed
if(player.getGameVersion() != null && request.gameVersion() != player.getGameVersion())
{
logger.warn("Player {}'s game version changed from {} to {}", player.getGameSyncId(), player.getGameVersion(), request.gameVersion());
}
// Try to store save data // Try to store save data
if(!playerManager.storePlayerGameSaveFile(player, ctx.bodyInputStream())) { if(!playerManager.storePlayerGameSaveFile(player, ctx.bodyInputStream())) {
writeStatusCode(outputStream, 4); // Game save data IO error writeStatusCode(outputStream, 4); // Game save data IO error
@@ -496,9 +488,9 @@ public class PglHandler implements HttpHandler {
// Prepare response // Prepare response
LEOutputStream outputStream = new LEOutputStream(ctx.outputStream()); LEOutputStream outputStream = new LEOutputStream(ctx.outputStream());
// Make sure Game Sync ID is present // Check if Game Sync ID is valid
if(request.gameSyncId() == null) { if(!GsidUtility.isValidGameSyncId(request.gameSyncId())) {
writeStatusCode(outputStream, 1); // Unauthorized writeStatusCode(outputStream, 8); // Invalid Game Sync ID
return; return;
} }
@@ -535,6 +527,12 @@ public class PglHandler implements HttpHandler {
LEOutputStream outputStream = new LEOutputStream(ctx.outputStream()); LEOutputStream outputStream = new LEOutputStream(ctx.outputStream());
String gameSyncId = GsidUtility.stringifyGameSyncId(Integer.parseInt(ctx.body().replace("\u0000", ""))); // So quirky String gameSyncId = GsidUtility.stringifyGameSyncId(Integer.parseInt(ctx.body().replace("\u0000", ""))); // So quirky
// Check if Game Sync ID is valid
if(!GsidUtility.isValidGameSyncId(request.gameSyncId())) {
writeStatusCode(outputStream, 8); // Invalid Game Sync ID
return;
}
// Check if player doesn't exist already // Check if player doesn't exist already
if(playerManager.doesPlayerExist(gameSyncId)) { if(playerManager.doesPlayerExist(gameSyncId)) {
writeStatusCode(outputStream, 2); // Duplicate Game Sync ID writeStatusCode(outputStream, 2); // Duplicate Game Sync ID
@@ -559,4 +557,17 @@ public class PglHandler implements HttpHandler {
outputStream.writeInt(status); outputStream.writeInt(status);
outputStream.writeBytes(0, 124); outputStream.writeBytes(0, 124);
} }
/**
* Gets the index of the player's chosen DLC for the specified type and prepares DLC overriding if necessary.
*/
private int getDlcIndex(User user, String name, String type, File customFile) {
if("custom".equals(name)) {
user.setDlcOverride(type, new Dlc(customFile.getAbsolutePath(), name, "IRAO", type, 1, (int)customFile.length(), 0, true));
return 1;
} else {
user.removeDlcOverride(type);
return dlcList.getDlcIndex("IRAO", type, name);
}
}
} }

View File

@@ -25,12 +25,6 @@ public class GsidDeserializer extends StdDeserializer<String> {
@Override @Override
public String deserialize(JsonParser parser, DeserializationContext context) throws IOException { public String deserialize(JsonParser parser, DeserializationContext context) throws IOException {
int gsid = parser.getIntValue(); return GsidUtility.stringifyGameSyncId(parser.getValueAsInt(-1));
if(gsid < 0) {
throw new IOException("Game Sync ID cannot be a negative number.");
}
return GsidUtility.stringifyGameSyncId(gsid);
} }
} }

View File

@@ -2,40 +2,87 @@ package entralinked.utility;
import java.util.regex.Pattern; import java.util.regex.Pattern;
/**
* Game Sync ID generation process:
*
* Let's take example PID "1231499195".
* Start by storing both the PID and its checksum (35497) in working variable "ugsid".
* We do this by simply shifting the checksum 32 bits to the left: ugsid = pid | (checksum << 32) = 0x8AA949672FBB
*
* Calculating each character is pretty straightforward.
* We just use the 5 least significant bits of ugsid as the index for the character table.
* Character 1: 0x8AA949672FBB & 0x1F = 27 = '5'
*
* After each character, we shift ugsid 5 bits to the right. Since ugsid contains 48 bits of data,
* taking the 5 least significant bits each time gives us enough indexes for (if we round up) exactly 10 characters.
* If we take a look at the full value of ugsid (0x8AA949672FBB) in binary and split it into sections of 5 bits,
* we'll actually already be able to see the entire Game Sync ID in reverse:
*
* Character: 'E' 'L' 'X' 'F' 'E' 'Y' 'Q' 'M' '7' '5'
* Chartable index: 4 10 21 5 4 22 14 11 29 27
* Binary: XX100 01010 10101 00101 00100 10110 01110 01011 11101 11011
*
* Adding all of the characters together gets us the Game Sync ID "57MQYEFXLE".
*
* Reversing this process to retrieve the PID and checksum is very straightforward.
* Simply go through each character, find the index of it in the character table & left shift the total value 5 bits each time.
* If we do this with the Game Sync ID we just created, then it should give us back the value of ugsid: 0x8AA949672FBB
* To then retrieve the PID, simply do: ugsid & 0xFFFFFFFF = 1231499195
* To retrieve the checksum, simply do: (ugsid >> 32) & 0xFFFF = 35497
* We can then validate the Game Sync ID if we want to by comparing the checksums.
*/
public class GsidUtility { public class GsidUtility {
public static final String GSID_CHARTABLE = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"; public static final String GSID_CHARTABLE = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
public static final Pattern GSID_PATTERN = Pattern.compile("[A-HJ-NP-Z2-9]{10}"); public static final Pattern GSID_PATTERN = Pattern.compile("[A-HJ-NP-Z2-9]{10}");
/** /**
* Stringifies the specified numerical Game Sync ID * Stringifies the specified numerical Game Sync ID.
* *
* Black 2 - {@code sub_21B480C} (overlay #199) * Black 2 - {@code sub_21B480C} (overlay #199)
*/ */
public static String stringifyGameSyncId(int gsid) { public static String stringifyGameSyncId(int gsid) {
char[] output = new char[10]; char[] output = new char[10];
int index = 0; long checksum = Crc16.calc(gsid);
long ugsid = gsid | (checksum << 32);
// v12 = gsid
// v5 = sub_204405C(gsid, 4u)
// v8 = v5 + __CFSHR__(v12, 31) + (v12 >> 31)
// uses unsigned ints for bitshift operations
long ugsid = gsid;
long checksum = Crc16.calc(gsid); // + __CFSHR__(v12, 31) + (v12 >> 31); ??
// do while v4 < 10
for(int i = 0; i < output.length; i++) { for(int i = 0; i < output.length; i++) {
index = (int)((ugsid & 0x1F) & 0x1FFFF); // chartable string is unicode, so normally multiplies by 2 int index = (int)((ugsid >> (5 * i)) & 0x1F);
ugsid = (ugsid >> 5) | (checksum << 27); output[i] = GSID_CHARTABLE.charAt(index);
checksum >>= 5;
output[i] = GSID_CHARTABLE.charAt(index); // sub_2048734(v4, chartable + index)
} }
return new String(output); return new String(output);
} }
/**
* Determines if a Game Sync ID is valid by checking its length, characters & checksum.
*
* @return {@code true} if the Game Sync ID is valid, otherwise {@code false}.
*/
public static boolean isValidGameSyncId(String gsid) { public static boolean isValidGameSyncId(String gsid) {
return GSID_PATTERN.matcher(gsid).matches(); if(gsid == null) {
return false;
}
int length = gsid.length();
long ugsid = 0;
if(length != 10) {
return false;
}
for(int i = 0; i < length; i++) {
int index = GSID_CHARTABLE.indexOf(gsid.charAt(i));
if(index == -1) {
return false;
}
ugsid |= (long)index << (5 * i);
}
int output = (int)(ugsid & 0xFFFFFFFF);
int checksum = (int)((ugsid >> 32) & 0xFFFF);
return output >= 0 && Crc16.calc(output) == checksum;
} }
} }

View File

@@ -48,4 +48,23 @@ public class LEInputStream extends FilterInputStream {
public double readDouble() throws IOException { public double readDouble() throws IOException {
return Double.longBitsToDouble(readLong()); 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);
}
} }

View File

@@ -1,9 +1,6 @@
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;
@@ -21,19 +18,12 @@ public class MD5 {
private static MessageDigest digest; private static MessageDigest digest;
/** /**
* @return A hex-formatted MD5 hash of the specified input string. * @return A hex-formatted MD5 hash of the specified input.
*/ */
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

@@ -1,94 +0,0 @@
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,14 +1,41 @@
package entralinked.utility; package entralinked.utility;
import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.GraphicsEnvironment;
import java.awt.GridBagConstraints; import java.awt.GridBagConstraints;
import java.awt.event.ActionEvent; import java.awt.event.ActionEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.HashSet;
import java.util.Set;
import javax.swing.AbstractAction; import javax.swing.AbstractAction;
import javax.swing.Action; import javax.swing.Action;
import javax.swing.BorderFactory;
import javax.swing.Icon; import javax.swing.Icon;
import javax.swing.JCheckBox;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.JTextField;
import javax.swing.JToggleButton;
import javax.swing.SwingUtilities;
import com.formdev.flatlaf.FlatClientProperties;
import net.miginfocom.swing.MigLayout;
public class SwingUtility { public class SwingUtility {
private static final Set<String> ignoredMessages = new HashSet<>();
@SuppressWarnings("serial") @SuppressWarnings("serial")
public static Action createAction(String name, Icon icon, Runnable handler) { public static Action createAction(String name, Icon icon, Runnable handler) {
AbstractAction action = new AbstractAction(name, icon) { AbstractAction action = new AbstractAction(name, icon) {
@@ -55,4 +82,135 @@ public class SwingUtility {
constraints.ipady = paddingY; constraints.ipady = paddingY;
return constraints; return constraints;
} }
public static void setTextFieldToggle(JTextField textField, boolean selected) {
for(Component component : textField.getComponents()) {
if(component instanceof JToggleButton) {
JToggleButton button = (JToggleButton)component;
if(button.isSelected() != selected) {
button.doClick();
}
}
}
}
// TODO find a good unicode font maybe?
public static Font findSupportingFont(String text, Font def) {
GraphicsEnvironment graphicsEnvironment = GraphicsEnvironment.getLocalGraphicsEnvironment();
for(Font font : graphicsEnvironment.getAllFonts()) {
if(font.canDisplayUpTo(text) == -1) {
return new Font(font.getName(), def.getStyle(), def.getSize());
}
}
return def;
}
public static JLabel createTitleLabel() {
return createTitleLabel("");
}
public static JLabel createTitleLabel(String text) {
return createStyledLabel(text, "font:bold +8");
}
public static JLabel createDescriptionLabel() {
return createDescriptionLabel("");
}
public static JLabel createDescriptionLabel(String text) {
return createStyledLabel(text, "[dark]foreground:darken(@foreground,20%)");
}
public static JLabel createStyledLabel(String text, String style) {
JLabel label = new JLabel(text);
label.putClientProperty(FlatClientProperties.STYLE, style);
return label;
}
public static JLabel createButtonLabel(String text, Runnable actionHandler) {
JLabel label = new JLabel("<html><u>%s</u></html>".formatted(text));
label.putClientProperty(FlatClientProperties.STYLE, "font: -1");
label.addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent event) {
if(SwingUtilities.isLeftMouseButton(event)) {
actionHandler.run();
}
}
});
return label;
}
public static void showIgnorableHint(Component parentComponent, String message, String title, int messageType) {
synchronized(ignoredMessages) {
// Do nothing if message has been ignored
if(ignoredMessages.contains(message)) {
return;
}
// Create ignore checkbox
JCheckBox checkBox = new JCheckBox("Don't show this again");
JOptionPane.showMessageDialog(parentComponent, createIgnorableDialogPanel(message, checkBox), title, messageType);
// Add to ignore list if checkbox is selected
if(checkBox.isSelected()) {
ignoredMessages.add(message);
}
}
}
public static boolean showIgnorableConfirmDialog(Component parentComponent, String message, String title) {
synchronized(ignoredMessages) {
// Do nothing if message has been ignored
if(ignoredMessages.contains(message)) {
return true;
}
JCheckBox checkBox = new JCheckBox("Don't ask this again");
int result = JOptionPane.showConfirmDialog(parentComponent, createIgnorableDialogPanel(message, checkBox), title, JOptionPane.YES_NO_OPTION);
// Add to ignore list if checkbox is selected
if(checkBox.isSelected()) {
ignoredMessages.add(message);
}
return result == JOptionPane.YES_OPTION;
}
}
private static JPanel createIgnorableDialogPanel(String message, JCheckBox checkBox) {
JPanel panel = new JPanel(new MigLayout("insets 0"));
panel.add(new JLabel("<html>%s</html>".formatted(message.replace("\n", "<br/>"))), "wrap"); // TODO no idea how JOptionPane does line breaks
panel.add(checkBox, "gapy 8");
return panel;
}
public static void showExceptionInfo(Component parentComponent, String message, Throwable throwable) {
// Create stacktrace string
StringWriter writer = new StringWriter();
throwable.printStackTrace(new PrintWriter(writer));
// Create text area
JTextArea area = new JTextArea(writer.toString());
area.setBorder(BorderFactory.createEmptyBorder(8, 8, 0, 0));
area.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12));
area.setEditable(false);
// Create scroll pane
int height = Math.min(200, area.getFontMetrics(area.getFont()).getHeight() * area.getLineCount() + 10);
JScrollPane scrollPane = new JScrollPane(area);
scrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS);
scrollPane.setPreferredSize(new Dimension(600, height));
scrollPane.setMaximumSize(scrollPane.getPreferredSize());
// Create dialog
String label = String.format("<html><b>%s</b><br>Exception details:<br><br></html>", message);
JPanel panel = new JPanel(new BorderLayout());
panel.add(new JLabel(label), BorderLayout.PAGE_START);
panel.add(scrollPane);
JOptionPane.showMessageDialog(parentComponent, panel, "An error has occured", JOptionPane.ERROR_MESSAGE);
}
} }

View File

@@ -4,6 +4,7 @@ import java.awt.image.BufferedImage;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
import java.util.Arrays;
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
@@ -363,6 +364,33 @@ public class TiledImageUtility {
} }
} }
/**
* Generates Pokédex skin background colors for the specified image.
*/
public static int[] generateBackgroundColors(BufferedImage image) {
int[] backgroundColors = Arrays.copyOf(DEFAULT_DEX_BACKGROUND_COLORS, 64);
int backgroundColor = image.getRGB(0, 0);
int dexColor = image.getRGB(0, 134);
int buttonColor = image.getRGB(128, 134);
// Background colors (58, 59, 60)
for(int i = 0; i < 3; i++) {
backgroundColors[i + 58] = ColorUtility.multiplyColor(backgroundColor, 0.7 + i * 0.15);
}
// Pokédex colors (48, 49, 50, 51, 52)
for(int i = 0; i < 5; i++) {
backgroundColors[i + 48] = ColorUtility.multiplyColor(dexColor, 1.15 - i * 0.15);
}
// Pokédex button colors (53, 54, 55, 56)
for(int i = 0; i < 4; i++) {
backgroundColors[i + 53] = ColorUtility.multiplyColor(buttonColor, 0.55 + i * 0.15);
}
return backgroundColors;
}
/** /**
* @return The index of the element in the array, or {@code -1} if no such element exists. * @return The index of the element in the array, or {@code -1} if no such element exists.
*/ */

View File

@@ -266,6 +266,30 @@
<option value="WHITE_ENGLISH">White Version</option> <option value="WHITE_ENGLISH">White Version</option>
<option value="BLACK_2_ENGLISH">Black Version 2</option> <option value="BLACK_2_ENGLISH">Black Version 2</option>
<option value="WHITE_2_ENGLISH">White Version 2</option> <option value="WHITE_2_ENGLISH">White Version 2</option>
<option value="BLACK_FRENCH">Version Noire</option>
<option value="WHITE_FRENCH">Version Blanche</option>
<option value="BLACK_2_FRENCH">Version Noire 2</option>
<option value="WHITE_2_FRENCH">Version Blanche 2</option>
<option value="BLACK_ITALIAN">Versione Nera</option>
<option value="WHITE_ITALIAN">Versione Bianca</option>
<option value="BLACK_2_ITALIAN">Versione Nera 2</option>
<option value="WHITE_2_ITALIAN">Versione Bianca 2</option>
<option value="BLACK_GERMAN">Schwarze Edition</option>
<option value="WHITE_GERMAN">Weisse Edition</option>
<option value="BLACK_2_GERMAN">Schwarze Edition 2</option>
<option value="WHITE_2_GERMAN">Weisse Edition 2</option>
<option value="BLACK_SPANISH">Edicion Negra</option>
<option value="WHITE_SPANISH">Edicion Blanca</option>
<option value="BLACK_2_SPANISH">Edicion Negra 2</option>
<option value="WHITE_2_SPANISH">Edicion Blanca 2</option>
<option value="BLACK_JAPANESE">ブラック</option>
<option value="WHITE_JAPANESE">ホワイト</option>
<option value="BLACK_2_JAPANESE">ブラック2</option>
<option value="WHITE_2_JAPANESE">ホワイト2</option>
<option value="BLACK_KOREAN">블랙</option>
<option value="WHITE_KOREAN">화이트</option>
<option value="BLACK_2_KOREAN">블랙2</option>
<option value="WHITE_2_KOREAN">화이트2</option>
</select> </select>
<label for="visitor-form-region">Country</label> <label for="visitor-form-region">Country</label>
<select id="visitor-form-region" name="region" value="1"> <select id="visitor-form-region" name="region" value="1">

View File

@@ -0,0 +1,166 @@
{
"1": "Stench",
"2": "Drizzle",
"3": "Speed Boost",
"4": "Battle Armor",
"5": "Sturdy",
"6": "Damp",
"7": "Limber",
"8": "Sand Veil",
"9": "Static",
"10": "Volt Absorb",
"11": "Water Absorb",
"12": "Oblivious",
"13": "Cloud Nine",
"14": "Compound Eyes",
"15": "Insomnia",
"16": "Color Change",
"17": "Immunity",
"18": "Flash Fire",
"19": "Shield Dust",
"20": "Own Tempo",
"21": "Suction Cups",
"22": "Intimidate",
"23": "Shadow Tag",
"24": "Rough Skin",
"25": "Wonder Guard",
"26": "Levitate",
"27": "Effect Spore",
"28": "Synchronize",
"29": "Clear Body",
"30": "Natural Cure",
"31": "Lightning Rod",
"32": "Serene Grace",
"33": "Swift Swim",
"34": "Chlorophyll",
"35": "Illuminate",
"36": "Trace",
"37": "Huge Power",
"38": "Poison Point",
"39": "Inner Focus",
"40": "Magma Armor",
"41": "Water Veil",
"42": "Magnet Pull",
"43": "Soundproof",
"44": "Rain Dish",
"45": "Sand Stream",
"46": "Pressure",
"47": "Thick Fat",
"48": "Early Bird",
"49": "Flame Body",
"50": "Run Away",
"51": "Keen Eye",
"52": "Hyper Cutter",
"53": "Pickup",
"54": "Truant",
"55": "Hustle",
"56": "Cute Charm",
"57": "Plus",
"58": "Minus",
"59": "Forecast",
"60": "Sticky Hold",
"61": "Shed Skin",
"62": "Guts",
"63": "Marvel Scale",
"64": "Liquid Ooze",
"65": "Overgrow",
"66": "Blaze",
"67": "Torrent",
"68": "Swarm",
"69": "Rock Head",
"70": "Drought",
"71": "Arena Trap",
"72": "Vital Spirit",
"73": "White Smoke",
"74": "Pure Power",
"75": "Shell Armor",
"76": "Air Lock",
"77": "Tangled Feet",
"78": "Motor Drive",
"79": "Rivalry",
"80": "Steadfast",
"81": "Snow Cloak",
"82": "Gluttony",
"83": "Anger Point",
"84": "Unburden",
"85": "Heatproof",
"86": "Simple",
"87": "Dry Skin",
"88": "Download",
"89": "Iron Fist",
"90": "Poison Heal",
"91": "Adaptability",
"92": "Skill Link",
"93": "Hydration",
"94": "Solar Power",
"95": "Quick Feet",
"96": "Normalize",
"97": "Sniper",
"98": "Magic Guard",
"99": "No Guard",
"100": "Stall",
"101": "Technician",
"102": "Leaf Guard",
"103": "Klutz",
"104": "Mold Breaker",
"105": "Super Luck",
"106": "Aftermath",
"107": "Anticipation",
"108": "Forewarn",
"109": "Unaware",
"110": "Tinted Lens",
"111": "Filter",
"112": "Slow Start",
"113": "Scrappy",
"114": "Storm Drain",
"115": "Ice Body",
"116": "Solid Rock",
"117": "Snow Warning",
"118": "Honey Gather",
"119": "Frisk",
"120": "Reckless",
"121": "Multitype",
"122": "Flower Gift",
"123": "Bad Dreams",
"124": "Pickpocket",
"125": "Sheer Force",
"126": "Contrary",
"127": "Unnerve",
"128": "Defiant",
"129": "Defeatist",
"130": "Cursed Body",
"131": "Healer",
"132": "Friend Guard",
"133": "Weak Armor",
"134": "Heavy Metal",
"135": "Light Metal",
"136": "Multiscale",
"137": "Toxic Boost",
"138": "Flare Boost",
"139": "Harvest",
"140": "Telepathy",
"141": "Moody",
"142": "Overcoat",
"143": "Poison Touch",
"144": "Regenerator",
"145": "Big Pecks",
"146": "Sand Rush",
"147": "Wonder Skin",
"148": "Analytic",
"149": "Illusion",
"150": "Imposter",
"151": "Infiltrator",
"152": "Mummy",
"153": "Moxie",
"154": "Justified",
"155": "Rattled",
"156": "Magic Bounce",
"157": "Sap Sipper",
"158": "Prankster",
"159": "Sand Force",
"160": "Iron Barbs",
"161": "Zen Mode",
"162": "Victory Star",
"163": "Turboblaze",
"164": "Teravolt"
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,621 @@
{
"1": "Master Ball",
"2": "Ultra Ball",
"3": "Great Ball",
"4": "Poké Ball",
"5": "Safari Ball",
"6": "Net Ball",
"7": "Dive Ball",
"8": "Nest Ball",
"9": "Repeat Ball",
"10": "Timer Ball",
"11": "Luxury Ball",
"12": "Premier Ball",
"13": "Dusk Ball",
"14": "Heal Ball",
"15": "Quick Ball",
"16": "Cherish Ball",
"17": "Potion",
"18": "Antidote",
"19": "Burn Heal",
"20": "Ice Heal",
"21": "Awakening",
"22": "Paralyze Heal",
"23": "Full Restore",
"24": "Max Potion",
"25": "Hyper Potion",
"26": "Super Potion",
"27": "Full Heal",
"28": "Revive",
"29": "Max Revive",
"30": "Fresh Water",
"31": "Soda Pop",
"32": "Lemonade",
"33": "Moomoo Milk",
"34": "Energy Powder",
"35": "Energy Root",
"36": "Heal Powder",
"37": "Revival Herb",
"38": "Ether",
"39": "Max Ether",
"40": "Elixir",
"41": "Max Elixir",
"42": "Lava Cookie",
"43": "Berry Juice",
"44": "Sacred Ash",
"45": "HP Up",
"46": "Protein",
"47": "Iron",
"48": "Carbos",
"49": "Calcium",
"50": "Rare Candy",
"51": "PP Up",
"52": "Zinc",
"53": "PP Max",
"54": "Old Gateau",
"55": "Guard Spec.",
"56": "Dire Hit",
"57": "X Attack",
"58": "X Defense",
"59": "X Speed",
"60": "X Accuracy",
"61": "X Sp. Atk",
"62": "X Sp. Def",
"63": "Poké Doll",
"64": "Fluffy Tail",
"65": "Blue Flute",
"66": "Yellow Flute",
"67": "Red Flute",
"68": "Black Flute",
"69": "White Flute",
"70": "Shoal Salt",
"71": "Shoal Shell",
"72": "Red Shard",
"73": "Blue Shard",
"74": "Yellow Shard",
"75": "Green Shard",
"76": "Super Repel",
"77": "Max Repel",
"78": "Escape Rope",
"79": "Repel",
"80": "Sun Stone",
"81": "Moon Stone",
"82": "Fire Stone",
"83": "Thunder Stone",
"84": "Water Stone",
"85": "Leaf Stone",
"86": "Tiny Mushroom",
"87": "Big Mushroom",
"88": "Pearl",
"89": "Big Pearl",
"90": "Stardust",
"91": "Star Piece",
"92": "Nugget",
"93": "Heart Scale",
"94": "Honey",
"95": "Growth Mulch",
"96": "Damp Mulch",
"97": "Stable Mulch",
"98": "Gooey Mulch",
"99": "Root Fossil",
"100": "Claw Fossil",
"101": "Helix Fossil",
"102": "Dome Fossil",
"103": "Old Amber",
"104": "Armor Fossil",
"105": "Skull Fossil",
"106": "Rare Bone",
"107": "Shiny Stone",
"108": "Dusk Stone",
"109": "Dawn Stone",
"110": "Oval Stone",
"111": "Odd Keystone",
"112": "Griseous Orb",
"116": "Douse Drive",
"117": "Shock Drive",
"118": "Burn Drive",
"119": "Chill Drive",
"134": "Sweet Heart",
"135": "Adamant Orb",
"136": "Lustrous Orb",
"137": "Greet Mail",
"138": "Favored Mail",
"139": "RSVP Mail",
"140": "Thanks Mail",
"141": "Inquiry Mail",
"142": "Like Mail",
"143": "Reply Mail",
"144": "Bridge Mail S",
"145": "Bridge Mail D",
"146": "Bridge Mail T",
"147": "Bridge Mail V",
"148": "Bridge Mail M",
"149": "Cheri Berry",
"150": "Chesto Berry",
"151": "Pecha Berry",
"152": "Rawst Berry",
"153": "Aspear Berry",
"154": "Leppa Berry",
"155": "Oran Berry",
"156": "Persim Berry",
"157": "Lum Berry",
"158": "Sitrus Berry",
"159": "Figy Berry",
"160": "Wiki Berry",
"161": "Mago Berry",
"162": "Aguav Berry",
"163": "Iapapa Berry",
"164": "Razz Berry",
"165": "Bluk Berry",
"166": "Nanab Berry",
"167": "Wepear Berry",
"168": "Pinap Berry",
"169": "Pomeg Berry",
"170": "Kelpsy Berry",
"171": "Qualot Berry",
"172": "Hondew Berry",
"173": "Grepa Berry",
"174": "Tamato Berry",
"175": "Cornn Berry",
"176": "Magost Berry",
"177": "Rabuta Berry",
"178": "Nomel Berry",
"179": "Spelon Berry",
"180": "Pamtre Berry",
"181": "Watmel Berry",
"182": "Durin Berry",
"183": "Belue Berry",
"184": "Occa Berry",
"185": "Passho Berry",
"186": "Wacan Berry",
"187": "Rindo Berry",
"188": "Yache Berry",
"189": "Chople Berry",
"190": "Kebia Berry",
"191": "Shuca Berry",
"192": "Coba Berry",
"193": "Payapa Berry",
"194": "Tanga Berry",
"195": "Charti Berry",
"196": "Kasib Berry",
"197": "Haban Berry",
"198": "Colbur Berry",
"199": "Babiri Berry",
"200": "Chilan Berry",
"201": "Liechi Berry",
"202": "Ganlon Berry",
"203": "Salac Berry",
"204": "Petaya Berry",
"205": "Apicot Berry",
"206": "Lansat Berry",
"207": "Starf Berry",
"208": "Enigma Berry",
"209": "Micle Berry",
"210": "Custap Berry",
"211": "Jaboca Berry",
"212": "Rowap Berry",
"213": "Bright Powder",
"214": "White Herb",
"215": "Macho Brace",
"216": "Exp. Share",
"217": "Quick Claw",
"218": "Soothe Bell",
"219": "Mental Herb",
"220": "Choice Band",
"221": "King's Rock",
"222": "Silver Powder",
"223": "Amulet Coin",
"224": "Cleanse Tag",
"225": "Soul Dew",
"226": "Deep Sea Tooth",
"227": "Deep Sea Scale",
"228": "Smoke Ball",
"229": "Everstone",
"230": "Focus Band",
"231": "Lucky Egg",
"232": "Scope Lens",
"233": "Metal Coat",
"234": "Leftovers",
"235": "Dragon Scale",
"236": "Light Ball",
"237": "Soft Sand",
"238": "Hard Stone",
"239": "Miracle Seed",
"240": "Black Glasses",
"241": "Black Belt",
"242": "Magnet",
"243": "Mystic Water",
"244": "Sharp Beak",
"245": "Poison Barb",
"246": "Never-Melt Ice",
"247": "Spell Tag",
"248": "Twisted Spoon",
"249": "Charcoal",
"250": "Dragon Fang",
"251": "Silk Scarf",
"252": "Up-Grade",
"253": "Shell Bell",
"254": "Sea Incense",
"255": "Lax Incense",
"256": "Lucky Punch",
"257": "Metal Powder",
"258": "Thick Club",
"259": "Stick",
"260": "Red Scarf",
"261": "Blue Scarf",
"262": "Pink Scarf",
"263": "Green Scarf",
"264": "Yellow Scarf",
"265": "Wide Lens",
"266": "Muscle Band",
"267": "Wise Glasses",
"268": "Expert Belt",
"269": "Light Clay",
"270": "Life Orb",
"271": "Power Herb",
"272": "Toxic Orb",
"273": "Flame Orb",
"274": "Quick Powder",
"275": "Focus Sash",
"276": "Zoom Lens",
"277": "Metronome",
"278": "Iron Ball",
"279": "Lagging Tail",
"280": "Destiny Knot",
"281": "Black Sludge",
"282": "Icy Rock",
"283": "Smooth Rock",
"284": "Heat Rock",
"285": "Damp Rock",
"286": "Grip Claw",
"287": "Choice Scarf",
"288": "Sticky Barb",
"289": "Power Bracer",
"290": "Power Belt",
"291": "Power Lens",
"292": "Power Band",
"293": "Power Anklet",
"294": "Power Weight",
"295": "Shed Shell",
"296": "Big Root",
"297": "Choice Specs",
"298": "Flame Plate",
"299": "Splash Plate",
"300": "Zap Plate",
"301": "Meadow Plate",
"302": "Icicle Plate",
"303": "Fist Plate",
"304": "Toxic Plate",
"305": "Earth Plate",
"306": "Sky Plate",
"307": "Mind Plate",
"308": "Insect Plate",
"309": "Stone Plate",
"310": "Spooky Plate",
"311": "Draco Plate",
"312": "Dread Plate",
"313": "Iron Plate",
"314": "Odd Incense",
"315": "Rock Incense",
"316": "Full Incense",
"317": "Wave Incense",
"318": "Rose Incense",
"319": "Luck Incense",
"320": "Pure Incense",
"321": "Protector",
"322": "Electirizer",
"323": "Magmarizer",
"324": "Dubious Disc",
"325": "Reaper Cloth",
"326": "Razor Claw",
"327": "Razor Fang",
"328": "TM01",
"329": "TM02",
"330": "TM03",
"331": "TM04",
"332": "TM05",
"333": "TM06",
"334": "TM07",
"335": "TM08",
"336": "TM09",
"337": "TM10",
"338": "TM11",
"339": "TM12",
"340": "TM13",
"341": "TM14",
"342": "TM15",
"343": "TM16",
"344": "TM17",
"345": "TM18",
"346": "TM19",
"347": "TM20",
"348": "TM21",
"349": "TM22",
"350": "TM23",
"351": "TM24",
"352": "TM25",
"353": "TM26",
"354": "TM27",
"355": "TM28",
"356": "TM29",
"357": "TM30",
"358": "TM31",
"359": "TM32",
"360": "TM33",
"361": "TM34",
"362": "TM35",
"363": "TM36",
"364": "TM37",
"365": "TM38",
"366": "TM39",
"367": "TM40",
"368": "TM41",
"369": "TM42",
"370": "TM43",
"371": "TM44",
"372": "TM45",
"373": "TM46",
"374": "TM47",
"375": "TM48",
"376": "TM49",
"377": "TM50",
"378": "TM51",
"379": "TM52",
"380": "TM53",
"381": "TM54",
"382": "TM55",
"383": "TM56",
"384": "TM57",
"385": "TM58",
"386": "TM59",
"387": "TM60",
"388": "TM61",
"389": "TM62",
"390": "TM63",
"391": "TM64",
"392": "TM65",
"393": "TM66",
"394": "TM67",
"395": "TM68",
"396": "TM69",
"397": "TM70",
"398": "TM71",
"399": "TM72",
"400": "TM73",
"401": "TM74",
"402": "TM75",
"403": "TM76",
"404": "TM77",
"405": "TM78",
"406": "TM79",
"407": "TM80",
"408": "TM81",
"409": "TM82",
"410": "TM83",
"411": "TM84",
"412": "TM85",
"413": "TM86",
"414": "TM87",
"415": "TM88",
"416": "TM89",
"417": "TM90",
"418": "TM91",
"419": "TM92",
"420": "HM01",
"421": "HM02",
"422": "HM03",
"423": "HM04",
"424": "HM05",
"425": "HM06",
"428": "Explorer Kit",
"429": "Loot Sack",
"430": "Rule Book",
"431": "Poké Radar",
"432": "Point Card",
"433": "Journal",
"434": "Seal Case",
"435": "Fashion Case",
"436": "Seal Bag",
"437": "Pal Pad",
"438": "Works Key",
"439": "Old Charm",
"440": "Galactic Key",
"441": "Red Chain",
"442": "Town Map",
"443": "Vs. Seeker",
"444": "Coin Case",
"445": "Old Rod",
"446": "Good Rod",
"447": "Super Rod",
"448": "Sprayduck",
"449": "Poffin Case",
"450": "Bike",
"451": "Suite Key",
"452": "Oak's Letter",
"453": "Lunar Wing",
"454": "Member Card",
"455": "Azure Flute",
"456": "S.S. Ticket",
"457": "Contest Pass",
"458": "Magma Stone",
"459": "Parcel",
"460": "Coupon 1",
"461": "Coupon 2",
"462": "Coupon 3",
"463": "Storage Key",
"464": "Secret Potion",
"465": "Vs. Recorder",
"466": "Gracidea",
"467": "Secret Key",
"468": "Apricorn Box",
"469": "Unown Report",
"470": "Berry Pots",
"471": "Dowsing Machine",
"472": "Blue Card",
"473": "Slowpoke Tail",
"474": "Clear Bell",
"475": "Card Key",
"476": "Basement Key",
"477": "Squirt Bottle",
"478": "Red Scale",
"479": "Lost Item",
"480": "Pass",
"481": "Machine Part",
"482": "Silver Wing",
"483": "Rainbow Wing",
"484": "Mystery Egg",
"485": "Red Apricorn",
"486": "Blue Apricorn",
"487": "Yellow Apricorn",
"488": "Green Apricorn",
"489": "Pink Apricorn",
"490": "White Apricorn",
"491": "Black Apricorn",
"492": "Fast Ball",
"493": "Level Ball",
"494": "Lure Ball",
"495": "Heavy Ball",
"496": "Love Ball",
"497": "Friend Ball",
"498": "Moon Ball",
"499": "Sport Ball",
"500": "Park Ball",
"501": "Photo Album",
"502": "GB Sounds",
"503": "Tidal Bell",
"504": "RageCandyBar",
"505": "Data Card 01",
"506": "Data Card 02",
"507": "Data Card 03",
"508": "Data Card 04",
"509": "Data Card 05",
"510": "Data Card 06",
"511": "Data Card 07",
"512": "Data Card 08",
"513": "Data Card 09",
"514": "Data Card 10",
"515": "Data Card 11",
"516": "Data Card 12",
"517": "Data Card 13",
"518": "Data Card 14",
"519": "Data Card 15",
"520": "Data Card 16",
"521": "Data Card 17",
"522": "Data Card 18",
"523": "Data Card 19",
"524": "Data Card 20",
"525": "Data Card 21",
"526": "Data Card 22",
"527": "Data Card 23",
"528": "Data Card 24",
"529": "Data Card 25",
"530": "Data Card 26",
"531": "Data Card 27",
"532": "Jade Orb",
"533": "Lock Capsule",
"534": "Red Orb",
"535": "Blue Orb",
"536": "Enigma Stone",
"537": "Prism Scale",
"538": "Eviolite",
"539": "Float Stone",
"540": "Rocky Helmet",
"541": "Air Balloon",
"542": "Red Card",
"543": "Ring Target",
"544": "Binding Band",
"545": "Absorb Bulb",
"546": "Cell Battery",
"547": "Eject Button",
"548": "Fire Gem",
"549": "Water Gem",
"550": "Electric Gem",
"551": "Grass Gem",
"552": "Ice Gem",
"553": "Fighting Gem",
"554": "Poison Gem",
"555": "Ground Gem",
"556": "Flying Gem",
"557": "Psychic Gem",
"558": "Bug Gem",
"559": "Rock Gem",
"560": "Ghost Gem",
"561": "Dragon Gem",
"562": "Dark Gem",
"563": "Steel Gem",
"564": "Normal Gem",
"565": "Health Wing",
"566": "Muscle Wing",
"567": "Resist Wing",
"568": "Genius Wing",
"569": "Clever Wing",
"570": "Swift Wing",
"571": "Pretty Wing",
"572": "Cover Fossil",
"573": "Plume Fossil",
"574": "Liberty Pass",
"575": "Pass Orb",
"576": "Dream Ball",
"577": "Poké Toy",
"578": "Prop Case",
"579": "Dragon Skull",
"580": "Balm Mushroom",
"581": "Big Nugget",
"582": "Pearl String",
"583": "Comet Shard",
"584": "Relic Copper",
"585": "Relic Silver",
"586": "Relic Gold",
"587": "Relic Vase",
"588": "Relic Band",
"589": "Relic Statue",
"590": "Relic Crown",
"591": "Casteliacone",
"592": "Dire Hit 2",
"593": "X Speed 2",
"594": "X Sp. Atk 2",
"595": "X Sp. Def 2",
"596": "X Defense 2",
"597": "X Attack 2",
"598": "X Accuracy 2",
"599": "X Speed 3",
"600": "X Sp. Atk 3",
"601": "X Sp. Def 3",
"602": "X Defense 3",
"603": "X Attack 3",
"604": "X Accuracy 3",
"605": "X Speed 6",
"606": "X Sp. Atk 6",
"607": "X Sp. Def 6",
"608": "X Defense 6",
"609": "X Attack 6",
"610": "X Accuracy 6",
"611": "Ability Urge",
"612": "Item Drop",
"613": "Item Urge",
"614": "Reset Urge",
"615": "Dire Hit 3",
"616": "Light Stone",
"617": "Dark Stone",
"618": "TM93",
"619": "TM94",
"620": "TM95",
"621": "Xtransceiver",
"622": "God Stone",
"623": "Gram 1",
"624": "Gram 2",
"625": "Gram 3",
"626": "Xtransceiver",
"627": "Medal Box",
"628": "DNA Splicers",
"629": "DNA Splicers",
"630": "Permit",
"631": "Oval Charm",
"632": "Shiny Charm",
"633": "Plasma Card",
"634": "Grubby Hanky",
"635": "Colress Machine",
"636": "Dropped Item",
"637": "Dropped Item",
"638": "Reveal Glass"
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,561 @@
{
"1": "Pound",
"2": "Karate Chop",
"3": "Double Slap",
"4": "Comet Punch",
"5": "Mega Punch",
"6": "Pay Day",
"7": "Fire Punch",
"8": "Ice Punch",
"9": "Thunder Punch",
"10": "Scratch",
"11": "Vice Grip",
"12": "Guillotine",
"13": "Razor Wind",
"14": "Swords Dance",
"15": "Cut",
"16": "Gust",
"17": "Wing Attack",
"18": "Whirlwind",
"19": "Fly",
"20": "Bind",
"21": "Slam",
"22": "Vine Whip",
"23": "Stomp",
"24": "Double Kick",
"25": "Mega Kick",
"26": "Jump Kick",
"27": "Rolling Kick",
"28": "Sand Attack",
"29": "Headbutt",
"30": "Horn Attack",
"31": "Fury Attack",
"32": "Horn Drill",
"33": "Tackle",
"34": "Body Slam",
"35": "Wrap",
"36": "Take Down",
"37": "Thrash",
"38": "Double-Edge",
"39": "Tail Whip",
"40": "Poison Sting",
"41": "Twineedle",
"42": "Pin Missile",
"43": "Leer",
"44": "Bite",
"45": "Growl",
"46": "Roar",
"47": "Sing",
"48": "Supersonic",
"49": "Sonic Boom",
"50": "Disable",
"51": "Acid",
"52": "Ember",
"53": "Flamethrower",
"54": "Mist",
"55": "Water Gun",
"56": "Hydro Pump",
"57": "Surf",
"58": "Ice Beam",
"59": "Blizzard",
"60": "Psybeam",
"61": "Bubble Beam",
"62": "Aurora Beam",
"63": "Hyper Beam",
"64": "Peck",
"65": "Drill Peck",
"66": "Submission",
"67": "Low Kick",
"68": "Counter",
"69": "Seismic Toss",
"70": "Strength",
"71": "Absorb",
"72": "Mega Drain",
"73": "Leech Seed",
"74": "Growth",
"75": "Razor Leaf",
"76": "Solar Beam",
"77": "Poison Powder",
"78": "Stun Spore",
"79": "Sleep Powder",
"80": "Petal Dance",
"81": "String Shot",
"82": "Dragon Rage",
"83": "Fire Spin",
"84": "Thunder Shock",
"85": "Thunderbolt",
"86": "Thunder Wave",
"87": "Thunder",
"88": "Rock Throw",
"89": "Earthquake",
"90": "Fissure",
"91": "Dig",
"92": "Toxic",
"93": "Confusion",
"94": "Psychic",
"95": "Hypnosis",
"96": "Meditate",
"97": "Agility",
"98": "Quick Attack",
"99": "Rage",
"100": "Teleport",
"101": "Night Shade",
"102": "Mimic",
"103": "Screech",
"104": "Double Team",
"105": "Recover",
"106": "Harden",
"107": "Minimize",
"108": "Smokescreen",
"109": "Confuse Ray",
"110": "Withdraw",
"111": "Defense Curl",
"112": "Barrier",
"113": "Light Screen",
"114": "Haze",
"115": "Reflect",
"116": "Focus Energy",
"117": "Bide",
"118": "Metronome",
"119": "Mirror Move",
"120": "Self-Destruct",
"121": "Egg Bomb",
"122": "Lick",
"123": "Smog",
"124": "Sludge",
"125": "Bone Club",
"126": "Fire Blast",
"127": "Waterfall",
"128": "Clamp",
"129": "Swift",
"130": "Skull Bash",
"131": "Spike Cannon",
"132": "Constrict",
"133": "Amnesia",
"134": "Kinesis",
"135": "Soft-Boiled",
"136": "High Jump Kick",
"137": "Glare",
"138": "Dream Eater",
"139": "Poison Gas",
"140": "Barrage",
"141": "Leech Life",
"142": "Lovely Kiss",
"143": "Sky Attack",
"144": "Transform",
"145": "Bubble",
"146": "Dizzy Punch",
"147": "Spore",
"148": "Flash",
"149": "Psywave",
"150": "Splash",
"151": "Acid Armor",
"152": "Crabhammer",
"153": "Explosion",
"154": "Fury Swipes",
"155": "Bonemerang",
"156": "Rest",
"157": "Rock Slide",
"158": "Hyper Fang",
"159": "Sharpen",
"160": "Conversion",
"161": "Tri Attack",
"162": "Super Fang",
"163": "Slash",
"164": "Substitute",
"165": "Struggle",
"166": "Sketch",
"167": "Triple Kick",
"168": "Thief",
"169": "Spider Web",
"170": "Mind Reader",
"171": "Nightmare",
"172": "Flame Wheel",
"173": "Snore",
"174": "Curse",
"175": "Flail",
"176": "Conversion 2",
"177": "Aeroblast",
"178": "Cotton Spore",
"179": "Reversal",
"180": "Spite",
"181": "Powder Snow",
"182": "Protect",
"183": "Mach Punch",
"184": "Scary Face",
"185": "Feint Attack",
"186": "Sweet Kiss",
"187": "Belly Drum",
"188": "Sludge Bomb",
"189": "Mud-Slap",
"190": "Octazooka",
"191": "Spikes",
"192": "Zap Cannon",
"193": "Foresight",
"194": "Destiny Bond",
"195": "Perish Song",
"196": "Icy Wind",
"197": "Detect",
"198": "Bone Rush",
"199": "Lock-On",
"200": "Outrage",
"201": "Sandstorm",
"202": "Giga Drain",
"203": "Endure",
"204": "Charm",
"205": "Rollout",
"206": "False Swipe",
"207": "Swagger",
"208": "Milk Drink",
"209": "Spark",
"210": "Fury Cutter",
"211": "Steel Wing",
"212": "Mean Look",
"213": "Attract",
"214": "Sleep Talk",
"215": "Heal Bell",
"216": "Return",
"217": "Present",
"218": "Frustration",
"219": "Safeguard",
"220": "Pain Split",
"221": "Sacred Fire",
"222": "Magnitude",
"223": "Dynamic Punch",
"224": "Megahorn",
"225": "Dragon Breath",
"226": "Baton Pass",
"227": "Encore",
"228": "Pursuit",
"229": "Rapid Spin",
"230": "Sweet Scent",
"231": "Iron Tail",
"232": "Metal Claw",
"233": "Vital Throw",
"234": "Morning Sun",
"235": "Synthesis",
"236": "Moonlight",
"237": "Hidden Power",
"238": "Cross Chop",
"239": "Twister",
"240": "Rain Dance",
"241": "Sunny Day",
"242": "Crunch",
"243": "Mirror Coat",
"244": "Psych Up",
"245": "Extreme Speed",
"246": "Ancient Power",
"247": "Shadow Ball",
"248": "Future Sight",
"249": "Rock Smash",
"250": "Whirlpool",
"251": "Beat Up",
"252": "Fake Out",
"253": "Uproar",
"254": "Stockpile",
"255": "Spit Up",
"256": "Swallow",
"257": "Heat Wave",
"258": "Hail",
"259": "Torment",
"260": "Flatter",
"261": "Will-O-Wisp",
"262": "Memento",
"263": "Facade",
"264": "Focus Punch",
"265": "Smelling Salts",
"266": "Follow Me",
"267": "Nature Power",
"268": "Charge",
"269": "Taunt",
"270": "Helping Hand",
"271": "Trick",
"272": "Role Play",
"273": "Wish",
"274": "Assist",
"275": "Ingrain",
"276": "Superpower",
"277": "Magic Coat",
"278": "Recycle",
"279": "Revenge",
"280": "Brick Break",
"281": "Yawn",
"282": "Knock Off",
"283": "Endeavor",
"284": "Eruption",
"285": "Skill Swap",
"286": "Imprison",
"287": "Refresh",
"288": "Grudge",
"289": "Snatch",
"290": "Secret Power",
"291": "Dive",
"292": "Arm Thrust",
"293": "Camouflage",
"294": "Tail Glow",
"295": "Luster Purge",
"296": "Mist Ball",
"297": "Feather Dance",
"298": "Teeter Dance",
"299": "Blaze Kick",
"300": "Mud Sport",
"301": "Ice Ball",
"302": "Needle Arm",
"303": "Slack Off",
"304": "Hyper Voice",
"305": "Poison Fang",
"306": "Crush Claw",
"307": "Blast Burn",
"308": "Hydro Cannon",
"309": "Meteor Mash",
"310": "Astonish",
"311": "Weather Ball",
"312": "Aromatherapy",
"313": "Fake Tears",
"314": "Air Cutter",
"315": "Overheat",
"316": "Odor Sleuth",
"317": "Rock Tomb",
"318": "Silver Wind",
"319": "Metal Sound",
"320": "Grass Whistle",
"321": "Tickle",
"322": "Cosmic Power",
"323": "Water Spout",
"324": "Signal Beam",
"325": "Shadow Punch",
"326": "Extrasensory",
"327": "Sky Uppercut",
"328": "Sand Tomb",
"329": "Sheer Cold",
"330": "Muddy Water",
"331": "Bullet Seed",
"332": "Aerial Ace",
"333": "Icicle Spear",
"334": "Iron Defense",
"335": "Block",
"336": "Howl",
"337": "Dragon Claw",
"338": "Frenzy Plant",
"339": "Bulk Up",
"340": "Bounce",
"341": "Mud Shot",
"342": "Poison Tail",
"343": "Covet",
"344": "Volt Tackle",
"345": "Magical Leaf",
"346": "Water Sport",
"347": "Calm Mind",
"348": "Leaf Blade",
"349": "Dragon Dance",
"350": "Rock Blast",
"351": "Shock Wave",
"352": "Water Pulse",
"353": "Doom Desire",
"354": "Psycho Boost",
"355": "Roost",
"356": "Gravity",
"357": "Miracle Eye",
"358": "Wake-Up Slap",
"359": "Hammer Arm",
"360": "Gyro Ball",
"361": "Healing Wish",
"362": "Brine",
"363": "Natural Gift",
"364": "Feint",
"365": "Pluck",
"366": "Tailwind",
"367": "Acupressure",
"368": "Metal Burst",
"369": "U-turn",
"370": "Close Combat",
"371": "Payback",
"372": "Assurance",
"373": "Embargo",
"374": "Fling",
"375": "Psycho Shift",
"376": "Trump Card",
"377": "Heal Block",
"378": "Wring Out",
"379": "Power Trick",
"380": "Gastro Acid",
"381": "Lucky Chant",
"382": "Me First",
"383": "Copycat",
"384": "Power Swap",
"385": "Guard Swap",
"386": "Punishment",
"387": "Last Resort",
"388": "Worry Seed",
"389": "Sucker Punch",
"390": "Toxic Spikes",
"391": "Heart Swap",
"392": "Aqua Ring",
"393": "Magnet Rise",
"394": "Flare Blitz",
"395": "Force Palm",
"396": "Aura Sphere",
"397": "Rock Polish",
"398": "Poison Jab",
"399": "Dark Pulse",
"400": "Night Slash",
"401": "Aqua Tail",
"402": "Seed Bomb",
"403": "Air Slash",
"404": "X-Scissor",
"405": "Bug Buzz",
"406": "Dragon Pulse",
"407": "Dragon Rush",
"408": "Power Gem",
"409": "Drain Punch",
"410": "Vacuum Wave",
"411": "Focus Blast",
"412": "Energy Ball",
"413": "Brave Bird",
"414": "Earth Power",
"415": "Switcheroo",
"416": "Giga Impact",
"417": "Nasty Plot",
"418": "Bullet Punch",
"419": "Avalanche",
"420": "Ice Shard",
"421": "Shadow Claw",
"422": "Thunder Fang",
"423": "Ice Fang",
"424": "Fire Fang",
"425": "Shadow Sneak",
"426": "Mud Bomb",
"427": "Psycho Cut",
"428": "Zen Headbutt",
"429": "Mirror Shot",
"430": "Flash Cannon",
"431": "Rock Climb",
"432": "Defog",
"433": "Trick Room",
"434": "Draco Meteor",
"435": "Discharge",
"436": "Lava Plume",
"437": "Leaf Storm",
"438": "Power Whip",
"439": "Rock Wrecker",
"440": "Cross Poison",
"441": "Gunk Shot",
"442": "Iron Head",
"443": "Magnet Bomb",
"444": "Stone Edge",
"445": "Captivate",
"446": "Stealth Rock",
"447": "Grass Knot",
"448": "Chatter",
"449": "Judgment",
"450": "Bug Bite",
"451": "Charge Beam",
"452": "Wood Hammer",
"453": "Aqua Jet",
"454": "Attack Order",
"455": "Defend Order",
"456": "Heal Order",
"457": "Head Smash",
"458": "Double Hit",
"459": "Roar of Time",
"460": "Spacial Rend",
"461": "Lunar Dance",
"462": "Crush Grip",
"463": "Magma Storm",
"464": "Dark Void",
"465": "Seed Flare",
"466": "Ominous Wind",
"467": "Shadow Force",
"468": "Hone Claws",
"469": "Wide Guard",
"470": "Guard Split",
"471": "Power Split",
"472": "Wonder Room",
"473": "Psyshock",
"474": "Venoshock",
"475": "Autotomize",
"476": "Rage Powder",
"477": "Telekinesis",
"478": "Magic Room",
"479": "Smack Down",
"480": "Storm Throw",
"481": "Flame Burst",
"482": "Sludge Wave",
"483": "Quiver Dance",
"484": "Heavy Slam",
"485": "Synchronoise",
"486": "Electro Ball",
"487": "Soak",
"488": "Flame Charge",
"489": "Coil",
"490": "Low Sweep",
"491": "Acid Spray",
"492": "Foul Play",
"493": "Simple Beam",
"494": "Entrainment",
"495": "After You",
"496": "Round",
"497": "Echoed Voice",
"498": "Chip Away",
"499": "Clear Smog",
"500": "Stored Power",
"501": "Quick Guard",
"502": "Ally Switch",
"503": "Scald",
"504": "Shell Smash",
"505": "Heal Pulse",
"506": "Hex",
"507": "Sky Drop",
"508": "Shift Gear",
"509": "Circle Throw",
"510": "Incinerate",
"511": "Quash",
"512": "Acrobatics",
"513": "Reflect Type",
"514": "Retaliate",
"515": "Final Gambit",
"516": "Bestow",
"517": "Inferno",
"518": "Water Pledge",
"519": "Fire Pledge",
"520": "Grass Pledge",
"521": "Volt Switch",
"522": "Struggle Bug",
"523": "Bulldoze",
"524": "Frost Breath",
"525": "Dragon Tail",
"526": "Work Up",
"527": "Electroweb",
"528": "Wild Charge",
"529": "Drill Run",
"530": "Dual Chop",
"531": "Heart Stamp",
"532": "Horn Leech",
"533": "Sacred Sword",
"534": "Razor Shell",
"535": "Heat Crash",
"536": "Leaf Tornado",
"537": "Steamroller",
"538": "Cotton Guard",
"539": "Night Daze",
"540": "Psystrike",
"541": "Tail Slap",
"542": "Hurricane",
"543": "Head Charge",
"544": "Gear Grind",
"545": "Searing Shot",
"546": "Techno Blast",
"547": "Relic Song",
"548": "Secret Sword",
"549": "Glaciate",
"550": "Bolt Strike",
"551": "Blue Flare",
"552": "Fiery Dance",
"553": "Freeze Shock",
"554": "Ice Burn",
"555": "Snarl",
"556": "Icicle Crash",
"557": "V-create",
"558": "Fusion Flare",
"559": "Fusion Bolt"
}

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -31,6 +31,20 @@ public class GsidUtilityTest {
// Illegal length (should be 10) // Illegal length (should be 10)
assertFalse(GsidUtility.isValidGameSyncId("Y67UEN38K")); assertFalse(GsidUtility.isValidGameSyncId("Y67UEN38K"));
assertFalse(GsidUtility.isValidGameSyncId("3ER5K8MBN4C")); assertFalse(GsidUtility.isValidGameSyncId("3ER5K8MBN4C"));
// Invalid checksum
assertFalse(GsidUtility.isValidGameSyncId("VFWM2Q2ADH"));
assertFalse(GsidUtility.isValidGameSyncId("44DAWDA4SH"));
assertFalse(GsidUtility.isValidGameSyncId("J6F55U7FUE"));
assertFalse(GsidUtility.isValidGameSyncId("8FAB4ZF6JF"));
assertFalse(GsidUtility.isValidGameSyncId("HWLNS77HWD"));
// Negative PID
assertFalse(GsidUtility.isValidGameSyncId("VYSBC78999"));
assertFalse(GsidUtility.isValidGameSyncId("2UD7GJ8999"));
assertFalse(GsidUtility.isValidGameSyncId("BTULWN8999"));
assertFalse(GsidUtility.isValidGameSyncId("ZW3JBQ9999"));
assertFalse(GsidUtility.isValidGameSyncId("MNTNWB9999"));
} }
@Test @Test
@@ -38,8 +52,8 @@ public class GsidUtilityTest {
void testValidGameSyncIds() { void testValidGameSyncIds() {
assertTrue(GsidUtility.isValidGameSyncId("VFWM2QAXNF")); assertTrue(GsidUtility.isValidGameSyncId("VFWM2QAXNF"));
assertTrue(GsidUtility.isValidGameSyncId("44DAWDJKJ8")); assertTrue(GsidUtility.isValidGameSyncId("44DAWDJKJ8"));
assertTrue(GsidUtility.isValidGameSyncId("J6F55UB2X9")); assertTrue(GsidUtility.isValidGameSyncId("J6F55UB2XD"));
assertTrue(GsidUtility.isValidGameSyncId("8FAB4Z3EN9")); assertTrue(GsidUtility.isValidGameSyncId("8FAB4Z3END"));
assertTrue(GsidUtility.isValidGameSyncId("HWLNS7BTNB")); assertTrue(GsidUtility.isValidGameSyncId("HWLNS7BTNB"));
} }
} }