2 Commits
v1.4.0 ... dev

Author SHA1 Message Date
kuroppoi
d99608ff11 More Mystery Gift shenanigans 2024-07-07 21:38:46 +02:00
kuroppoi
acbb3d7f95 Crappy support for Mystery Gifts (#42) 2024-07-06 23:44:30 +02:00
62 changed files with 454 additions and 12690 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@v4.2.2 uses: actions/checkout@v3
- 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@v4.7.0 uses: actions/setup-java@v3
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/actions/setup-gradle@v3 uses: gradle/gradle-build-action@v2
- name: Run Gradle dist - name: Run Gradle dist
run: ./gradlew dist run: ./gradlew dist
- name: Upload artifact - name: Upload artifact
uses: actions/upload-artifact@v4.6.2 uses: actions/upload-artifact@v3
with: with:
name: entralinked name: entralinked
path: build/libs/entralinked.jar path: build/libs/entralinked.jar

View File

@@ -30,8 +30,6 @@ 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

@@ -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.view.MainView; import entralinked.gui.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,6 +119,7 @@ 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,35 +29,21 @@ public enum GameVersion {
// Black Version 2 & White Version 2 // Black Version 2 & White Version 2
// ================================== // ==================================
BLACK_2_JAPANESE(23, 1, "IREJ", "ブラック2"), BLACK_2_JAPANESE(23, 1, "IREJ", "ブラック2", true),
BLACK_2_ENGLISH(23, 2, "IREO", "Black Version 2"), BLACK_2_ENGLISH(23, 2, "IREO", "Black Version 2", true),
BLACK_2_FRENCH(23, 3, "IREF", "Version Noire 2"), BLACK_2_FRENCH(23, 3, "IREF", "Version Noire 2", true),
BLACK_2_ITALIAN(23, 4, "IREI", "Versione Nera 2"), BLACK_2_ITALIAN(23, 4, "IREI", "Versione Nera 2", true),
BLACK_2_GERMAN(23, 5, "IRED", "Schwarze Edition 2"), BLACK_2_GERMAN(23, 5, "IRED", "Schwarze Edition 2", true),
BLACK_2_SPANISH(23, 7, "IRES", "Edicion Negra 2"), BLACK_2_SPANISH(23, 7, "IRES", "Edicion Negra 2", true),
BLACK_2_KOREAN(23, 8, "IREK", "블랙2"), BLACK_2_KOREAN(23, 8, "IREK", "블랙2", true),
WHITE_2_JAPANESE(22, 1, "IRDJ", "ホワイト2"), WHITE_2_JAPANESE(22, 1, "IRDJ", "ホワイト2", true),
WHITE_2_ENGLISH(22, 2, "IRDO", "White Version 2"), WHITE_2_ENGLISH(22, 2, "IRDO", "White Version 2", true),
WHITE_2_FRENCH(22, 3, "IRDF", "Version Blanche 2"), WHITE_2_FRENCH(22, 3, "IRDF", "Version Blanche 2", true),
WHITE_2_ITALIAN(22, 4, "IRDI", "Versione Bianca 2"), WHITE_2_ITALIAN(22, 4, "IRDI", "Versione Bianca 2", true),
WHITE_2_GERMAN(22, 5, "IRDD", "Weisse Edition 2"), WHITE_2_GERMAN(22, 5, "IRDD", "Weisse Edition 2", true),
WHITE_2_SPANISH(22, 7, "IRDS", "Edicion Blanca 2"), WHITE_2_SPANISH(22, 7, "IRDS", "Edicion Blanca 2", true),
WHITE_2_KOREAN(22, 8, "IRDK", "화이트2"); WHITE_2_KOREAN(22, 8, "IRDK", "화이트2", true);
// 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<>();
@@ -66,7 +52,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.getBits(), version); mapByCodes.put(version.getRomCode() << version.getLanguageCode(), version);
} }
} }
@@ -74,12 +60,18 @@ 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) { private GameVersion(int romCode, int languageCode, String serial, String displayName, boolean isVersion2) {
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) {
@@ -87,11 +79,7 @@ public enum GameVersion {
} }
public static GameVersion lookup(int romCode, int languageCode) { public static GameVersion lookup(int romCode, int languageCode) {
return mapByCodes.get(getBits(romCode, languageCode)); return mapByCodes.get(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() {
@@ -111,15 +99,6 @@ public enum GameVersion {
} }
public boolean isVersion2() { public boolean isVersion2() {
return checkMask(B2W2_MASK); return isVersion2;
}
public boolean checkMask(int mask) {
int bits = getBits();
return (bits & mask) == bits;
}
public int getBits() {
return getBits(romCode, languageCode);
} }
} }

View File

@@ -1,55 +0,0 @@
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

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

View File

@@ -1,60 +0,0 @@
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

@@ -1,25 +0,0 @@
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,4 +1,4 @@
package entralinked.gui.view; package entralinked.gui;
import java.awt.BorderLayout; import java.awt.BorderLayout;
import java.awt.Color; import java.awt.Color;
@@ -13,15 +13,14 @@ import java.net.URL;
import java.util.List; import java.util.List;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import javax.swing.BorderFactory;
import javax.swing.ImageIcon; import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JFrame; import javax.swing.JFrame;
import javax.swing.JLabel; import javax.swing.JLabel;
import javax.swing.JMenu; import javax.swing.JMenu;
import javax.swing.JMenuBar; import javax.swing.JMenuBar;
import javax.swing.JPanel; import javax.swing.JPanel;
import javax.swing.JScrollPane; import javax.swing.JScrollPane;
import javax.swing.JTabbedPane;
import javax.swing.JTextPane; import javax.swing.JTextPane;
import javax.swing.UIManager; import javax.swing.UIManager;
import javax.swing.text.AttributeSet; import javax.swing.text.AttributeSet;
@@ -35,10 +34,8 @@ import javax.swing.text.StyleContext;
import org.apache.logging.log4j.Level; import org.apache.logging.log4j.Level;
import com.formdev.flatlaf.intellijthemes.FlatOneDarkIJTheme; import com.formdev.flatlaf.intellijthemes.FlatOneDarkIJTheme;
import com.formdev.flatlaf.util.ColorFunctions;
import entralinked.Entralinked; import entralinked.Entralinked;
import entralinked.gui.panels.DashboardPanel;
import entralinked.utility.ConsumerAppender; import entralinked.utility.ConsumerAppender;
import entralinked.utility.SwingUtility; import entralinked.utility.SwingUtility;
@@ -52,21 +49,29 @@ public class MainView {
public static final Color TEXT_COLOR_ERROR = Color.RED.darker(); public static final Color TEXT_COLOR_ERROR = Color.RED.darker();
private final StyleContext styleContext = StyleContext.getDefaultStyleContext(); private final StyleContext styleContext = StyleContext.getDefaultStyleContext();
private final AttributeSet fontAttribute = styleContext.addAttribute(SimpleAttributeSet.EMPTY, StyleConstants.FontFamily, "Consolas"); private final AttributeSet fontAttribute = styleContext.addAttribute(SimpleAttributeSet.EMPTY, StyleConstants.FontFamily, "Consolas");
private final JButton dashboardButton;
private final JLabel statusLabel; private final JLabel statusLabel;
public MainView(Entralinked entralinked) { public MainView(Entralinked entralinked) {
// Set look and feel // Set look and feel
FlatOneDarkIJTheme.setup(); FlatOneDarkIJTheme.setup();
UIManager.getDefaults().put("Component.focusedBorderColor", UIManager.get("Component.borderColor")); UIManager.getDefaults().put("Component.focusedBorderColor", UIManager.get("Component.borderColor"));
UIManager.put("Table.alternateRowColor", ColorFunctions.lighten(UIManager.getColor("Table.background"), 0.05F));
// 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 // Create status label
statusLabel = new JLabel("Servers are starting, please wait a bit...", JLabel.CENTER); statusLabel = new JLabel("Entralinked is starting...", JLabel.CENTER);
// Create footer panel // Create footer panel
JPanel footerPanel = new JPanel(new BorderLayout()); JPanel footerPanel = new JPanel(new BorderLayout());
footerPanel.add(statusLabel, BorderLayout.CENTER); footerPanel.add(statusLabel, BorderLayout.CENTER);
footerPanel.setBorder(BorderFactory.createEmptyBorder(2, 0, 5, 0)); footerPanel.add(dashboardButton, BorderLayout.LINE_END);
// Create console output // Create console output
JTextPane consoleOutputPane = new JTextPane() { JTextPane consoleOutputPane = new JTextPane() {
@@ -80,7 +85,7 @@ public class MainView {
return getUI().getPreferredSize(this); return getUI().getPreferredSize(this);
}; };
}; };
consoleOutputPane.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12)); consoleOutputPane.setFont(new Font("Consola", Font.PLAIN, 12));
consoleOutputPane.setEditable(false); consoleOutputPane.setEditable(false);
((DefaultCaret)consoleOutputPane.getCaret()).setUpdatePolicy(DefaultCaret.ALWAYS_UPDATE); ((DefaultCaret)consoleOutputPane.getCaret()).setUpdatePolicy(DefaultCaret.ALWAYS_UPDATE);
@@ -104,12 +109,7 @@ public class MainView {
// Create main panel // Create main panel
JPanel panel = new JPanel(new BorderLayout()); JPanel panel = new JPanel(new BorderLayout());
panel.add(scrollPane, BorderLayout.CENTER); panel.add(scrollPane, BorderLayout.CENTER);
panel.add(footerPanel, BorderLayout.PAGE_END); 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 // Create window
JFrame frame = new JFrame("Entralinked"); JFrame frame = new JFrame("Entralinked");
@@ -118,13 +118,7 @@ public class MainView {
JMenuBar menuBar = new JMenuBar(); JMenuBar menuBar = new JMenuBar();
JMenu helpMenu = new JMenu("Help"); JMenu helpMenu = new JMenu("Help");
helpMenu.add(SwingUtility.createAction("Update PID (Error 60000)", () -> new PidToolDialog(entralinked, frame))); helpMenu.add(SwingUtility.createAction("Update PID (Error 60000)", () -> new PidToolDialog(entralinked, frame)));
helpMenu.add(SwingUtility.createAction("GitHub", () -> { helpMenu.add(SwingUtility.createAction("GitHub", () -> openUrl("https://github.com/kuroppoi/entralinked")));
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); menuBar.add(helpMenu);
// Set window properties // Set window properties
@@ -132,7 +126,8 @@ public class MainView {
@Override @Override
public void windowClosing(WindowEvent event) { public void windowClosing(WindowEvent event) {
// Update status // Update status
statusLabel.setText("Servers are shutting down, please wait a bit..."); dashboardButton.setEnabled(false);
statusLabel.setText("Entralinked is shutting down ...");
// Run asynchronously so it doesn't just awkwardly freeze // Run asynchronously so it doesn't just awkwardly freeze
// Still scuffed but better than nothing I guess // Still scuffed but better than nothing I guess
@@ -147,16 +142,31 @@ public class MainView {
new ImageIcon(getClass().getResource("/icon-32x.png")).getImage(), new ImageIcon(getClass().getResource("/icon-32x.png")).getImage(),
new ImageIcon(getClass().getResource("/icon-16x.png")).getImage())); new ImageIcon(getClass().getResource("/icon-16x.png")).getImage()));
frame.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE); frame.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
frame.setMinimumSize(new Dimension(512, 288));
frame.setJMenuBar(menuBar); frame.setJMenuBar(menuBar);
frame.add(tabbedPane); frame.add(panel);
frame.getContentPane().setPreferredSize(new Dimension(733, 463));
frame.pack(); frame.pack();
frame.setMinimumSize(frame.getSize());
frame.setLocationRelativeTo(null); frame.setLocationRelativeTo(null);
frame.setVisible(true); frame.setVisible(true);
} }
public void setDashboardButtonEnabled(boolean enabled) {
dashboardButton.setEnabled(enabled);
}
public void setStatusLabelText(String text) { public void setStatusLabelText(String text) {
statusLabel.setText(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

@@ -1,38 +0,0 @@
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

@@ -1,38 +0,0 @@
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

@@ -1,4 +1,4 @@
package entralinked.gui.view; package entralinked.gui;
import java.awt.GridBagLayout; import java.awt.GridBagLayout;
import java.util.regex.Pattern; import java.util.regex.Pattern;
@@ -60,8 +60,7 @@ 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.\n" JOptionPane.showMessageDialog(dialog, "This Wi-Fi Connection ID does not exist.", "Attention", JOptionPane.WARNING_MESSAGE);
+ "If you haven't attempted to connect yet, please do that first.", "Attention", JOptionPane.WARNING_MESSAGE);
return; return;
} }

View File

@@ -1,229 +0,0 @@
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

@@ -1,90 +0,0 @@
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

@@ -1,84 +0,0 @@
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 {
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;
int y = size.height / 2 - height / 2;
graphics.drawImage(shadowImage, x + 4, y + 2, 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);
// TODO shadow can be cut off if pixels touch the border
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, height);
repaint();
}
}

View File

@@ -1,16 +0,0 @@
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

@@ -1,185 +0,0 @@
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 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<Integer> getMoveOptions(GameVersion gameVersion, PkmnSpecies species, PkmnGender gender) {
return getEncounters(gameVersion).stream()
.filter(x -> x.species() == species.id() && (!x.isGenderLocked() || species.gender() == gender))
.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

@@ -1,9 +0,0 @@
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

@@ -1,19 +0,0 @@
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 versionMask) {
@JsonIgnore
public boolean isGenderLocked() {
return gender != null;
}
}

View File

@@ -1,7 +0,0 @@
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

@@ -1,29 +0,0 @@
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

@@ -1,7 +0,0 @@
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

@@ -1,212 +0,0 @@
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 com.formdev.flatlaf.FlatClientProperties;
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.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 MiscPanel miscPanel;
private Player player;
private boolean initialized;
public DashboardPanel(Entralinked entralinked) {
this.entralinked = entralinked;
// Create login labels
JLabel loginTitle = new JLabel("Log in");
loginTitle.putClientProperty(FlatClientProperties.STYLE, "font:bold +8");
JLabel loginDescription = new JLabel("<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>");
loginDescription.putClientProperty(FlatClientProperties.STYLE, "[dark]foreground:darken(@foreground,20%)");
// Create GSID field
FlatTextField gsidTextField = new FlatTextField();
gsidTextField.setPlaceholderText("XXXXXXXXXX");
// 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%"));
loginPanel.add(loginTitle);
loginPanel.add(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);
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);
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();
miscPanel = new MiscPanel(entralinked);
} 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("Miscellaneous", miscPanel);
initialized = true;
return true;
}
}

View File

@@ -1,210 +0,0 @@
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);
PkmnSpecies species = (PkmnSpecies)newValue;
updateGenderOptions(row, species);
updateFormOptions(row, species);
updateMoveOptions(row, species);
optionLock = false;
} else if(column == GENDER_COLUMN) {
if(!isLegalMode()) {
return; // Gender only affects move options if legal mode is enabled
}
if(newValue == null) {
table.disableOption(row, MOVE_COLUMN);
return;
}
updateMoveOptions(row, getSpecies(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, species);
updateFormOptions(i, species);
updateMoveOptions(i, species);
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 updateMoveOptions(int row, PkmnSpecies species) {
setOptions(row, MOVE_COLUMN,
isLegalMode() ? DataManager.getMoveOptions(gameVersion, species, getGender(row)) : DataManager.getMoveIds(),
(a, b) -> DataManager.getMoveName(a).compareTo(DataManager.getMoveName(b)), true);
}
private void updateGenderOptions(int row, PkmnSpecies species) {
setOptions(row, GENDER_COLUMN, isLegalMode() ? DataManager.getGenderOptions(gameVersion, species) : species.getGenders());
}
private void updateFormOptions(int row, PkmnSpecies species) {
setOptions(row, FORM_COLUMN, species.hasForms() ? Arrays.asList(species.forms()) : Collections.emptyList());
}
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

@@ -1,132 +0,0 @@
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

@@ -1,380 +0,0 @@
package entralinked.gui.panels;
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.JSpinner;
import javax.swing.SpinnerNumberModel;
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 MiscPanel 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 BufferedImage EMPTY_IMAGE = new BufferedImage(TiledImageUtility.SCREEN_WIDTH, TiledImageUtility.SCREEN_HEIGHT, BufferedImage.TYPE_INT_RGB);
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 final JSpinner levelSpinner;
private Player player;
private GameVersion gameVersion;
private DlcOption customCGearSkin;
private DlcOption customDexSkin;
private DlcOption customMusical;
public MiscPanel(Entralinked entralinked) {
this.entralinked = entralinked;
setLayout(new MigLayout("align 50% 50%"));
// Create preview labels
JLabel cgearPreviewLabel = new JLabel("", JLabel.CENTER);
cgearPreviewLabel.setBorder(BorderFactory.createTitledBorder("C-Gear Skin Preview"));
JLabel dexPreviewLabel = new JLabel("", JLabel.CENTER);
dexPreviewLabel.setBorder(BorderFactory.createTitledBorder("Pokédex Skin Preview"));
// Create preview image panel
JPanel previewPanel = new JPanel();
previewPanel.add(cgearPreviewLabel);
previewPanel.add(dexPreviewLabel);
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 -> {
cgearPreviewLabel.setIcon(new ImageIcon(getSkinImage((DlcOption)cgearComboBox.getSelectedItem())));
});
zukanComboBox = new JComboBox<>();
zukanComboBox.setMinimumSize(zukanComboBox.getPreferredSize());
zukanComboBox.setRenderer(renderer);
zukanComboBox.addActionListener(event -> {
dexPreviewLabel.setIcon(new ImageIcon(getSkinImage((DlcOption)zukanComboBox.getSelectedItem())));
});
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());
});
});
// Create level spinner
levelSpinner = new JSpinner(new SpinnerNumberModel(0, 0, 99, 1)) ;
optionPanel.add(new JLabel("Level Gain"), "sizegroup label");
optionPanel.add(levelSpinner, "sizegroup option");
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);
levelSpinner.setValue(player.getLevelsGained());
}
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());
player.setLevelsGained((int)levelSpinner.getValue());
}
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 static Image getSkinImage(DlcOption option) {
return option == null ? EMPTY_IMAGE : 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) {
return EMPTY_IMAGE; // TODO show feedback
}
});
}
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) {
e.printStackTrace(); // TODO show feedback
}
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) {
e.printStackTrace(); // TODO show feedback
}
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) {
e.printStackTrace(); // TODO show feedback
}
return false;
}
}

View File

@@ -1,119 +0,0 @@
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 com.formdev.flatlaf.FlatClientProperties;
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 info labels
JLabel titleLabel = new JLabel("Summary");
titleLabel.putClientProperty(FlatClientProperties.STYLE, "font:bold +8");
flavorTextLabel = new JLabel();
flavorTextLabel.putClientProperty(FlatClientProperties.STYLE, "[dark]foreground:darken(@foreground,20%)");
JLabel subLabel = new JLabel("Tucked-in Pokémon info:");
subLabel.putClientProperty(FlatClientProperties.STYLE, "[dark]foreground:darken(@foreground,20%)");
// Create header panel
JPanel headerPanel = new JPanel(new MigLayout("fillx, insets 0"));
headerPanel.add(titleLabel, "wrap");
headerPanel.add(flavorTextLabel, "wrap");
headerPanel.add(subLabel, "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

@@ -1,251 +0,0 @@
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 com.formdev.flatlaf.FlatClientProperties;
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 = new JLabel();
titleLabel.putClientProperty(FlatClientProperties.STYLE, "font:bold +8");
descriptionLabel = new JLabel();
descriptionLabel.putClientProperty(FlatClientProperties.STYLE, "[dark]foreground:darken(@foreground,20%)");
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

@@ -1,272 +0,0 @@
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

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

View File

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

View File

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

View File

@@ -5,17 +5,7 @@ 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) & 0x1F; int form = (buffer.getByte(64) >> 3) & 0xFF;
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,44 +5,33 @@ 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,54 +8,44 @@ 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 and look around"), WALK_LOOK_AROUND,
/** /**
* Only walk up and down. * Only walk up and down.
*/ */
WALK_VERTICALLY("Walk up and down"), WALK_VERTICALLY,
/** /**
* Only walk left and right. * Only walk left and right.
*/ */
WALK_HORIZONTALLY("Walk left and right"), WALK_HORIZONTALLY,
/** /**
* 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 left and right and look around"), WALK_LOOK_HORIZONTALLY,
/** /**
* 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

@@ -25,7 +25,6 @@ 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) {
@@ -153,14 +152,6 @@ 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) {
@@ -186,8 +177,4 @@ 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,17 +23,16 @@ 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) {
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.getCustomMusical(), player.getLevelsGained(), player.getEncounters(), player.getCustomDexSkin(), player.getLevelsGained(), player.getEncounters(), player.getItems(),
player.getItems(), player.getAvenueVisitors()); player.getAvenueVisitors());
} }
/** /**
@@ -49,7 +48,6 @@ 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);

View File

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

View File

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

View File

@@ -17,7 +17,6 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import entralinked.Configuration; import entralinked.Configuration;
import entralinked.Entralinked; import entralinked.Entralinked;
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;
@@ -220,16 +219,13 @@ public class PglHandler implements HttpHandler {
String cgearType = player.getGameVersion().isVersion2() ? "CGEAR2" : "CGEAR"; String cgearType = player.getGameVersion().isVersion2() ? "CGEAR2" : "CGEAR";
String cgearSkin = player.getCGearSkin(); String cgearSkin = player.getCGearSkin();
String dexSkin = player.getDexSkin(); String dexSkin = player.getDexSkin();
String musical = player.getMusical();
int cgearSkinIndex = 0; int cgearSkinIndex = 0;
int dexSkinIndex = 0; int dexSkinIndex = 0;
int musicalIndex = 0;
// Create or remove custom C-Gear skin DLC override // Create or remove custom C-Gear skin DLC override
if("custom".equals(cgearSkin)) { if("custom".equals(cgearSkin)) {
cgearSkinIndex = 1; cgearSkinIndex = 1;
user.setDlcOverride(cgearType, new Dlc(player.getCGearSkinFile().getAbsolutePath(), user.setDlcOverride(cgearType, player.getCGearSkinFile());
"custom", "IRAO", cgearType, cgearSkinIndex, 9730, 0, true));
} else { } else {
cgearSkinIndex = dlcList.getDlcIndex("IRAO", cgearType, cgearSkin); cgearSkinIndex = dlcList.getDlcIndex("IRAO", cgearType, cgearSkin);
user.removeDlcOverride(cgearType); user.removeDlcOverride(cgearType);
@@ -238,24 +234,12 @@ public class PglHandler implements HttpHandler {
// Create or remove custom Pokédex skin DLC override // Create or remove custom Pokédex skin DLC override
if("custom".equals(dexSkin)) { if("custom".equals(dexSkin)) {
dexSkinIndex = 1; dexSkinIndex = 1;
user.setDlcOverride("ZUKAN", new Dlc(player.getDexSkinFile().getAbsolutePath(), user.setDlcOverride("ZUKAN", player.getDexSkinFile());
"custom", "IRAO", "ZUKAN", dexSkinIndex, 25090, 0, true));
} else { } else {
dexSkinIndex = dlcList.getDlcIndex("IRAO", "ZUKAN", dexSkin); dexSkinIndex = dlcList.getDlcIndex("IRAO", "ZUKAN", dexSkin);
user.removeDlcOverride("ZUKAN"); user.removeDlcOverride("ZUKAN");
} }
// Create or remove custom musical DLC override
if("custom".equals(musical)) {
musicalIndex = 1;
File file = player.getMusicalFile();
user.setDlcOverride("MUSICAL", new Dlc(file.getAbsolutePath(),
"custom", "IRAO", "MUSICAL", musicalIndex, (int)file.length(), 0, true));
} else {
musicalIndex = dlcList.getDlcIndex("IRAO", "MUSICAL", musical);
user.removeDlcOverride("MUSICAL");
}
// When waking up a Pokémon, these 4 bytes are written to 0x1D304 in the save file. // 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 // 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. // and no content will be downloaded.
@@ -277,7 +261,7 @@ 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(musicalIndex); outputStream.write(dlcList.getDlcIndex("IRAO", "MUSICAL", player.getMusical()));
outputStream.write(cgearSkinIndex); outputStream.write(cgearSkinIndex);
outputStream.write(dexSkinIndex); outputStream.write(dexSkinIndex);
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); // Seems to be a flag for indicating whether or not decor data is present
@@ -327,6 +311,7 @@ public class PglHandler implements HttpHandler {
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, 14 - 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.
@@ -335,14 +320,13 @@ public class PglHandler implements HttpHandler {
// For example, if the visitor type is '0', then shop type '0' would be a raffle. // For example, if the visitor type is '0', then shop type '0' would be a raffle.
// However, if the visitor type is '2', then shop type '0' results in a dojo instead. // However, if the visitor type is '2', then shop type '0' results in a dojo instead.
int visitorType = visitor.type().getClientId() + visitor.personality() * 8; int visitorType = visitor.type().getClientId() + visitor.personality() * 8;
outputStream.writeShort(-1); // Does nothing, seems to be read as part of the name.
outputStream.write(visitorType); outputStream.write(visitorType);
outputStream.write(visitor.shopType().ordinal() + (7 - visitorType * 2 % 7)); outputStream.write(visitor.shopType().ordinal() + (7 - visitorType * 2 % 7));
outputStream.writeShort(0); // Does nothing outputStream.writeShort(0); // Does nothing
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(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(0); // [26] Ignores if 1
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

View File

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

@@ -0,0 +1,94 @@
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,41 +1,14 @@
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) {
@@ -82,113 +55,4 @@ 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 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,7 +4,6 @@ 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;
@@ -364,33 +363,6 @@ 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,30 +266,6 @@
<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

@@ -1,166 +0,0 @@
{
"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

@@ -1,621 +0,0 @@
{
"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

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