18 Commits
dev ... v1.4.2

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

View File

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

View File

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

View File

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

View File

@@ -14,7 +14,7 @@ import org.apache.logging.log4j.Logger;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.databind.SerializationFeature;
import entralinked.gui.MainView; import entralinked.gui.view.MainView;
import entralinked.model.dlc.DlcList; import entralinked.model.dlc.DlcList;
import entralinked.model.player.PlayerManager; import entralinked.model.player.PlayerManager;
import entralinked.model.user.UserManager; import entralinked.model.user.UserManager;
@@ -119,7 +119,6 @@ public class Entralinked {
if(mainView != null) { if(mainView != null) {
SwingUtilities.invokeLater(() -> { SwingUtilities.invokeLater(() -> {
mainView.setDashboardButtonEnabled(true);
mainView.setStatusLabelText("Configure your DS to use the following DNS server: %s".formatted(hostIpAddress)); mainView.setStatusLabelText("Configure your DS to use the following DNS server: %s".formatted(hostIpAddress));
}); });
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,256 @@
package entralinked.gui.view;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Desktop;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.StandardCopyOption;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import javax.swing.BorderFactory;
import javax.swing.ImageIcon;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTabbedPane;
import javax.swing.JTextPane;
import javax.swing.UIManager;
import javax.swing.text.AttributeSet;
import javax.swing.text.BadLocationException;
import javax.swing.text.DefaultCaret;
import javax.swing.text.Document;
import javax.swing.text.SimpleAttributeSet;
import javax.swing.text.StyleConstants;
import javax.swing.text.StyleContext;
import org.apache.logging.log4j.Level;
import com.formdev.flatlaf.intellijthemes.FlatOneDarkIJTheme;
import com.formdev.flatlaf.util.ColorFunctions;
import entralinked.Entralinked;
import entralinked.GameVersion;
import entralinked.gui.FileChooser;
import entralinked.gui.panels.DashboardPanel;
import entralinked.model.player.Player;
import entralinked.model.player.PlayerManager;
import entralinked.utility.ConsumerAppender;
import entralinked.utility.GsidUtility;
import entralinked.utility.LEInputStream;
import entralinked.utility.SwingUtility;
/**
* Simple Swing user interface.
*/
public class MainView {
public static final Color TEXT_COLOR = Color.WHITE.darker();
public static final Color TEXT_COLOR_WARN = Color.YELLOW.darker();
public static final Color TEXT_COLOR_ERROR = Color.RED.darker();
private final StyleContext styleContext = StyleContext.getDefaultStyleContext();
private final AttributeSet fontAttribute = styleContext.addAttribute(SimpleAttributeSet.EMPTY, StyleConstants.FontFamily, "Consolas");
private final Entralinked entralinked;
private final JFrame frame;
private final JLabel statusLabel;
public MainView(Entralinked entralinked) {
this.entralinked = entralinked;
// Set look and feel
FlatOneDarkIJTheme.setup();
UIManager.getDefaults().put("Component.focusedBorderColor", UIManager.get("Component.borderColor"));
UIManager.put("Table.alternateRowColor", ColorFunctions.lighten(UIManager.getColor("Table.background"), 0.05F));
// Create status label
statusLabel = new JLabel("Servers are starting, please wait a bit...", JLabel.CENTER);
// Create footer panel
JPanel footerPanel = new JPanel(new BorderLayout());
footerPanel.add(statusLabel, BorderLayout.CENTER);
footerPanel.setBorder(BorderFactory.createEmptyBorder(2, 0, 5, 0));
// Create console output
JTextPane consoleOutputPane = new JTextPane() {
@Override
public boolean getScrollableTracksViewportWidth() {
return getPreferredSize().width <= getParent().getSize().width;
}
@Override
public Dimension getPreferredSize() {
return getUI().getPreferredSize(this);
};
};
consoleOutputPane.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12));
consoleOutputPane.setEditable(false);
((DefaultCaret)consoleOutputPane.getCaret()).setUpdatePolicy(DefaultCaret.ALWAYS_UPDATE);
// Create console output appender
ConsumerAppender.addConsumer("GuiOutput", message -> {
Document document = consoleOutputPane.getDocument();
Level level = message.level();
Color color = level == Level.ERROR ? TEXT_COLOR_ERROR : level == Level.WARN ? TEXT_COLOR_WARN : TEXT_COLOR;
AttributeSet colorAttribute = styleContext.addAttribute(fontAttribute, StyleConstants.Foreground, color);
try {
consoleOutputPane.getDocument().insertString(document.getLength(), message.formattedMessage(), colorAttribute);
} catch(BadLocationException e) {}
});
// Create console output scroll pane
JScrollPane scrollPane = new JScrollPane();
scrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS);
scrollPane.setViewportView(consoleOutputPane);
// Create main panel
JPanel panel = new JPanel(new BorderLayout());
panel.add(scrollPane, BorderLayout.CENTER);
panel.add(footerPanel, BorderLayout.PAGE_END);
// Create tabbed pane
JTabbedPane tabbedPane = new JTabbedPane();
tabbedPane.addTab("Console", panel);
tabbedPane.addTab("Dashboard", new DashboardPanel(entralinked));
// Create window
frame = new JFrame("Entralinked");
// Create menu bar
JMenuBar menuBar = new JMenuBar();
JMenu toolsMenu = new JMenu("Tools");
toolsMenu.add(SwingUtility.createAction("Import save file (Memory Link)", () -> FileChooser.showFileOpenDialog(frame, selection -> {
importSaveFile(selection.file());
})));
menuBar.add(toolsMenu);
JMenu helpMenu = new JMenu("Help");
helpMenu.add(SwingUtility.createAction("Update PID (Error 60000)", () -> new PidToolDialog(entralinked, frame)));
helpMenu.add(SwingUtility.createAction("GitHub", () -> {
try {
Desktop.getDesktop().browse(new URL("https://github.com/kuroppoi/entralinked").toURI());
} catch(IOException | URISyntaxException e) {
SwingUtility.showExceptionInfo(frame, "Failed to open URL.", e);
}
}));
menuBar.add(helpMenu);
// Set window properties
frame.addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(WindowEvent event) {
// Update status
statusLabel.setText("Servers are shutting down, please wait a bit...");
// Run asynchronously so it doesn't just awkwardly freeze
// Still scuffed but better than nothing I guess
CompletableFuture.runAsync(() -> {
entralinked.stopServers();
System.exit(0);
});
}
});
frame.setIconImages(List.of(
new ImageIcon(getClass().getResource("/icon-64x.png")).getImage(),
new ImageIcon(getClass().getResource("/icon-32x.png")).getImage(),
new ImageIcon(getClass().getResource("/icon-16x.png")).getImage()));
frame.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
frame.setJMenuBar(menuBar);
frame.add(tabbedPane);
frame.getContentPane().setPreferredSize(new Dimension(733, 463));
frame.pack();
frame.setMinimumSize(frame.getSize());
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
public void setStatusLabelText(String text) {
statusLabel.setText(text);
}
private void importSaveFile(File file) {
// Check file size
if(file.length() != 524288) {
JOptionPane.showMessageDialog(frame, "Invalid file length.\n"
+ "Expected 524288 bytes, got %s.".formatted(file.length()), "Attention", JOptionPane.WARNING_MESSAGE);
return;
}
PlayerManager playerManager = entralinked.getPlayerManager();
Player player = null;
GameVersion version = null;
try(LEInputStream inputStream = new LEInputStream(new FileInputStream(file))) {
inputStream.skipNBytes(0x19400); // Skip to trainer info
inputStream.skipNBytes(0x4);
String name = inputStream.readUTF16(7);
inputStream.skipNBytes(0x2);
int trainerId = inputStream.readInt();
int profileId = inputStream.readInt();
inputStream.skipNBytes(0x2);
int language = inputStream.read();
int romCode = inputStream.read();
version = GameVersion.lookup(romCode, language);
// Check game version
if(version == null || version.isVersion2()) {
JOptionPane.showMessageDialog(frame, "This is not a Black & White save file.", "Attention", JOptionPane.WARNING_MESSAGE);
return;
}
String gameSyncId = GsidUtility.stringifyGameSyncId(profileId == 0 ? Objects.hash(name, trainerId) & 0x7FFFFFFF : profileId);
player = playerManager.doesPlayerExist(gameSyncId) ? playerManager.getPlayer(gameSyncId) : playerManager.registerPlayer(gameSyncId, version);
} catch(Exception e) {
SwingUtility.showExceptionInfo(frame, "Failed to read save data.", e);
return;
}
// Check if player exists
if(player == null) {
JOptionPane.showMessageDialog(frame, "Failed to create player data.", "Attention", JOptionPane.WARNING_MESSAGE);
return;
}
// Check version mismatch
if(player.getGameVersion() != version) {
if(!SwingUtility.showIgnorableConfirmDialog(frame,
"The game version stored in the profile data does not match.\n"
+ "Do you want to overwrite it and import the save file anyway?", "Attention")) {
return;
}
player.setGameVersion(version);
// Try to save player data
if(!playerManager.savePlayer(player)) {
JOptionPane.showMessageDialog(frame, "Failed to save player data.", "Attention", JOptionPane.WARNING_MESSAGE);
return;
}
}
// Copy save file
try {
Files.copy(file.toPath(), player.getSaveFile().toPath(), StandardCopyOption.REPLACE_EXISTING);
} catch(Exception e) {
SwingUtility.showExceptionInfo(frame, "Failed to import save data.", e);
return;
}
JOptionPane.showMessageDialog(frame, "Save file has been imported successfully!\n"
+ "You can now use Memory Link with the following Game Sync ID:\n\n%s".formatted(player.getGameSyncId()),
"Attention", JOptionPane.INFORMATION_MESSAGE);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -124,7 +124,7 @@ public class DlsHandler implements HttpHandler {
*/ */
private String getDlcGameCode(String gameCode) { private String getDlcGameCode(String gameCode) {
return switch(gameCode) { return switch(gameCode) {
case "IRAJ" -> "IRAO"; case "IRAJ", "IRAK" -> "IRAO";
default -> gameCode; default -> gameCode;
}; };
} }

View File

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

View File

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

View File

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

View File

@@ -48,4 +48,23 @@ public class LEInputStream extends FilterInputStream {
public double readDouble() throws IOException { public double readDouble() throws IOException {
return Double.longBitsToDouble(readLong()); return Double.longBitsToDouble(readLong());
} }
public String readUTF16(int length) throws IOException {
char[] charBuffer = new char[length];
int read = 0;
for(int i = 0; i < charBuffer.length; i++) {
int c = readShort() & 0xFFFF;
if(c == 0xFFFF) {
break;
}
charBuffer[i] = (char)c;
read++;
}
skipNBytes((length - (read + 1)) * 2);
return new String(charBuffer, 0, read);
}
} }

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

File diff suppressed because it is too large Load Diff

View File

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

File diff suppressed because it is too large Load Diff

View File

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