33 Commits

Author SHA1 Message Date
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
kuroppoi
abe1b80c2d Store player save & version data on Game Sync data creation (#36) 2024-01-14 22:34:07 +01:00
kuroppoi
8e614e0d27 Don't use config option 2024-01-10 01:39:34 +01:00
kuroppoi
774794f751 Redact user id in logs 2024-01-10 01:16:04 +01:00
kuroppoi
9d288bde09 Exclude a bunch of info from debug logs 2024-01-10 01:13:48 +01:00
kuroppoi
18b86a42e0 Disable loom 2024-01-10 00:20:49 +01:00
kuroppoi
39e0b27cec Update README.md 2023-12-29 22:08:44 +01:00
kuroppoi
1af975ffb6 Testing 2023-12-29 21:57:57 +01:00
kuroppoi
6f2e4a06a0 Preview gif 2023-12-29 21:46:26 +01:00
kuroppoi
68bdb7d47f Fix console logging woes 2023-11-19 15:41:42 +01:00
kuroppoi
163ced2333 Add debug logging 2023-11-19 15:17:21 +01:00
kuroppoi
f3809f4a54 Update README.md 2023-11-10 17:14:18 +01:00
kuroppoi
0cd628ba63 Merge workflows 2023-11-10 16:15:41 +01:00
kuroppoi
3898bd473c Fix browsers not remembering input Game Sync IDs (#22) 2023-09-20 11:40:53 +02:00
kuroppoi
0c4cd69741 Always log DNS server after startup (#15) 2023-08-24 19:04:55 +02:00
kuroppoi
91eb9971f3 Validate friend code checksum 2023-08-11 13:40:43 +02:00
kuroppoi
3224f6312a Only update id of the profile the user uses next when using the pid tool 2023-08-11 05:18:37 +02:00
kuroppoi
306b9e6db6 Add gui tool for fixing error 60000 2023-08-10 21:29:43 +02:00
kuroppoi
c0cc952cc2 Update README.md 2023-08-10 05:19:56 +02:00
kuroppoi
2eb8054ba7 Make Victini not downloadable 2023-07-25 20:13:40 +02:00
kuroppoi
d61196fea1 Minor tweaks 2023-07-24 17:43:50 +02:00
kuroppoi
8726614fce Update icons 2023-07-24 05:04:55 +02:00
kuroppoi
76c5d0e1a5 Update README.md 2023-07-24 03:41:20 +02:00
kuroppoi
86434b2cb1 Add icons 2023-07-24 03:28:04 +02:00
kuroppoi
724e30bfcc Abort as soon as one server fails to start 2023-07-23 04:27:52 +02:00
kuroppoi
87a06a4c08 Swing GUI makeover 2023-07-23 04:25:02 +02:00
kuroppoi
e06b83c944 Add some unit tests 2023-07-22 20:26:12 +02:00
kuroppoi
b890d27bba Fix incorrect animation type 2023-07-22 01:45:00 +02:00
93 changed files with 13328 additions and 321 deletions

View File

@@ -1,34 +1,34 @@
name: Build
on:
workflow_dispatch:
pull_request:
push:
branches: [ master ]
paths:
- poke-sprites-v
- src/**
- build.gradle
jobs:
dist:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
uses: actions/checkout@v4.2.2
- name: Checkout submodules
run: git submodule update --init --recursive
- name: Setup Java 17
uses: actions/setup-java@v3
uses: actions/setup-java@v4.7.0
with:
java-version: 17
distribution: temurin
- name: Make gradlew executable
run: chmod +x ./gradlew
- name: Setup Gradle
uses: gradle/gradle-build-action@v2
uses: gradle/actions/setup-gradle@v3
- name: Run Gradle dist
run: ./gradlew dist
- name: Upload artifact
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4.6.2
with:
name: entralinked
path: build/libs/entralinked.jar

View File

@@ -1,21 +0,0 @@
name: Build on Pull Request
on: pull_request
jobs:
dist:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Checkout submodules
run: git submodule update --init --recursive
- name: Setup Java 17
uses: actions/setup-java@v3
with:
java-version: 17
distribution: temurin
- name: Make gradlew executable
run: chmod +x ./gradlew
- name: Setup Gradle
uses: gradle/gradle-build-action@v2
- name: Run Gradle dist
run: ./gradlew dist

View File

@@ -1,12 +1,21 @@
# Entralinked
[![build](https://github.com/kuroppoi/entralinked/actions/workflows/dist-upload-artifact.yml/badge.svg)](https://github.com/kuroppoi/entralinked/actions)
[![release](https://img.shields.io/github/v/release/kuroppoi/entralinked?labelColor=30373D&label=Release&logoColor=959DA5&logo=github)](https://github.com/kuroppoi/entralinked/releases/latest)
<p align="center">
<img src="https://raw.githubusercontent.com/kuroppoi/entralinked/master/images/icon.png" alt="icon"/>
</p>
<h1 align="center">Entralinked</h1>
<p align="center">
<a href="https://github.com/kuroppoi/entralinked/actions"><img src="https://github.com/kuroppoi/entralinked/actions/workflows/build.yml/badge.svg" alt="build"/></a>
<a href="https://github.com/kuroppoi/entralinked/releases/latest"><img src="https://img.shields.io/github/v/release/kuroppoi/entralinked?labelColor=30373D&label=Release&logoColor=959DA5&logo=github" alt="release"/></a>
</p>
Entralinked is a standalone Game Sync emulator developed for use with Pokémon Black & White and its sequels.\
Its purpose is to serve as a simple utility for downloading Pokémon, Items, C-Gear skins, Pokédex skins, Musicals\
and, in Black 2 & White 2 only, Join Avenue visitors to your game without needing to edit your save file.\
It can also be used to Memory Link with a Black or White save file if you don't have a second DS system.
![preview](https://raw.githubusercontent.com/kuroppoi/entralinked/master/images/preview.gif)
For users: [Quick Setup Guide](https://github.com/kuroppoi/entralinked/wiki/Setup)
## Building
#### Prerequisites
@@ -29,5 +38,4 @@ Entralinked has a built-in DNS server.\
In order for your game to connect, you must configure the DNS settings of your DS.\
By default, Entralinked is configured to automatically use the local host of the system.\
This approach is not always accurate, however, and you may need to manually configure it in `config.json`.\
If you receive error code `60000` when trying to connect, erase the WFC Configuration of your DS and try again.\
After tucking in a Pokémon, navigate to http://localhost/dashboard/profile.html to configure Game Sync settings.

View File

@@ -27,6 +27,11 @@ dependencies {
implementation 'io.netty:netty-all:4.1.79.Final'
implementation 'io.javalin:javalin:5.5.0'
implementation 'org.apache.logging.log4j:log4j-slf4j2-impl:2.20.0'
implementation 'com.formdev:flatlaf:3.1.1'
implementation 'com.formdev:flatlaf-extras: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 {

BIN
images/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

BIN
images/preview-2x.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 356 KiB

BIN
images/preview.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 210 KiB

View File

@@ -14,7 +14,7 @@ import org.apache.logging.log4j.Logger;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import entralinked.gui.MainView;
import entralinked.gui.view.MainView;
import entralinked.model.dlc.DlcList;
import entralinked.model.player.PlayerManager;
import entralinked.model.user.UserManager;
@@ -43,8 +43,11 @@ public class Entralinked {
private final GameSpyServer gameSpyServer;
private final HttpServer httpServer;
private MainView mainView;
private boolean initialized;
public Entralinked(String[] args) {
long beginTime = System.currentTimeMillis();
// Read command line arguments
CommandLineArguments arguments = new CommandLineArguments(args);
@@ -92,16 +95,11 @@ public class Entralinked {
userManager = new UserManager();
playerManager = new PlayerManager();
// Start servers
boolean started = true;
// Create DNS server
dnsServer = new DnsServer(hostAddress);
started &= dnsServer.start();
// Create GameSpy server
gameSpyServer = new GameSpyServer(this);
started &= gameSpyServer.start();
// Create HTTP server
httpServer = new HttpServer(this);
@@ -109,25 +107,41 @@ public class Entralinked {
httpServer.addHandler(new PglHandler(this));
httpServer.addHandler(new DlsHandler(this));
httpServer.addHandler(new DashboardHandler(this));
started &= httpServer.start();
// Handle post-startup GUI stuff
if(mainView != null) {
if(!started) {
SwingUtilities.invokeLater(() -> mainView.setStatusLabelText(
"ERROR: One or more servers failed to start! Please check the logs for info."));
return;
}
// Start servers
boolean started = startServers();
// Post-startup
if(started) {
String hostIpAddress = hostAddress.getHostAddress();
SwingUtilities.invokeLater(() -> {
mainView.setDashboardButtonEnabled(true);
mainView.setStatusLabelText("Configure your DS to use the following DNS server: %s".formatted(hostIpAddress));
});
logger.info("Startup complete! Took a total of {} milliseconds", System.currentTimeMillis() - beginTime);
logger.info("Configure your DS to use the following DNS server: {}", hostIpAddress);
if(mainView != null) {
SwingUtilities.invokeLater(() -> {
mainView.setStatusLabelText("Configure your DS to use the following DNS server: %s".formatted(hostIpAddress));
});
}
} else {
stopServers();
if(mainView != null) {
SwingUtilities.invokeLater(() -> mainView.setStatusLabelText(
"ERROR: Entralinked failed to start. Please check the logs for info."));
}
}
initialized = true;
}
public boolean startServers() {
logger.info("Starting servers ...");
return httpServer.start() && gameSpyServer.start() && dnsServer.start();
}
public void stopServers() {
logger.info("Stopping servers ...");
if(httpServer != null) {
httpServer.stop();
}
@@ -179,4 +193,8 @@ public class Entralinked {
public PlayerManager getPlayerManager() {
return playerManager;
}
public boolean isInitialized() {
return initialized;
}
}

View File

@@ -4,7 +4,7 @@ import java.util.HashMap;
import java.util.Map;
public enum GameVersion {
// ==================================
// Black Version & White Version
// ==================================
@@ -29,21 +29,35 @@ public enum GameVersion {
// Black Version 2 & White Version 2
// ==================================
BLACK_2_JAPANESE(23, 1, "IREJ", "ブラック2", true),
BLACK_2_ENGLISH(23, 2, "IREO", "Black Version 2", true),
BLACK_2_FRENCH(23, 3, "IREF", "Version Noire 2", true),
BLACK_2_ITALIAN(23, 4, "IREI", "Versione Nera 2", true),
BLACK_2_GERMAN(23, 5, "IRED", "Schwarze Edition 2", true),
BLACK_2_SPANISH(23, 7, "IRES", "Edicion Negra 2", true),
BLACK_2_KOREAN(23, 8, "IREK", "블랙2", true),
BLACK_2_JAPANESE(23, 1, "IREJ", "ブラック2"),
BLACK_2_ENGLISH(23, 2, "IREO", "Black Version 2"),
BLACK_2_FRENCH(23, 3, "IREF", "Version Noire 2"),
BLACK_2_ITALIAN(23, 4, "IREI", "Versione Nera 2"),
BLACK_2_GERMAN(23, 5, "IRED", "Schwarze Edition 2"),
BLACK_2_SPANISH(23, 7, "IRES", "Edicion Negra 2"),
BLACK_2_KOREAN(23, 8, "IREK", "블랙2"),
WHITE_2_JAPANESE(22, 1, "IRDJ", "ホワイト2", true),
WHITE_2_ENGLISH(22, 2, "IRDO", "White Version 2", true),
WHITE_2_FRENCH(22, 3, "IRDF", "Version Blanche 2", true),
WHITE_2_ITALIAN(22, 4, "IRDI", "Versione Bianca 2", true),
WHITE_2_GERMAN(22, 5, "IRDD", "Weisse Edition 2", true),
WHITE_2_SPANISH(22, 7, "IRDS", "Edicion Blanca 2", true),
WHITE_2_KOREAN(22, 8, "IRDK", "화이트2", true);
WHITE_2_JAPANESE(22, 1, "IRDJ", "ホワイト2"),
WHITE_2_ENGLISH(22, 2, "IRDO", "White Version 2"),
WHITE_2_FRENCH(22, 3, "IRDF", "Version Blanche 2"),
WHITE_2_ITALIAN(22, 4, "IRDI", "Versione Bianca 2"),
WHITE_2_GERMAN(22, 5, "IRDD", "Weisse Edition 2"),
WHITE_2_SPANISH(22, 7, "IRDS", "Edicion Blanca 2"),
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
private static final Map<String, GameVersion> mapBySerial = new HashMap<>();
@@ -52,7 +66,7 @@ public enum GameVersion {
static {
for(GameVersion version : values()) {
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 String serial;
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.languageCode = languageCode;
this.serial = serial;
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) {
@@ -79,7 +87,11 @@ public enum GameVersion {
}
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() {
@@ -99,6 +111,15 @@ public enum GameVersion {
}
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,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

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

View File

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

View File

@@ -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,19 @@
package entralinked.gui.data;
import java.util.List;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import entralinked.model.pkmn.PkmnGender;
public record Encounter(
@JsonProperty(required = true) int species,
@JsonProperty(required = true) List<Integer> moves,
PkmnGender gender, int versionMask) {
@JsonIgnore
public boolean isGenderLocked() {
return gender != null;
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

@@ -1,6 +1,7 @@
package entralinked.gui;
package entralinked.gui.view;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Desktop;
import java.awt.Dimension;
import java.awt.Font;
@@ -9,16 +10,20 @@ 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.JButton;
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.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTabbedPane;
import javax.swing.JTextPane;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;
import javax.swing.text.AttributeSet;
import javax.swing.text.BadLocationException;
import javax.swing.text.DefaultCaret;
@@ -27,46 +32,41 @@ import javax.swing.text.SimpleAttributeSet;
import javax.swing.text.StyleConstants;
import javax.swing.text.StyleContext;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.Level;
import com.formdev.flatlaf.intellijthemes.FlatOneDarkIJTheme;
import com.formdev.flatlaf.util.ColorFunctions;
import entralinked.Entralinked;
import entralinked.gui.panels.DashboardPanel;
import entralinked.utility.ConsumerAppender;
import entralinked.utility.SwingUtility;
/**
* Simple Swing user interface.
*/
public class MainView {
private static Logger logger = LogManager.getLogger();
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) {
// Try set Look and Feel
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (ReflectiveOperationException | UnsupportedLookAndFeelException e) {
logger.error("Could not set Look and Feel", e);
}
// 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");
});
// 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("Entralinked is starting...", JLabel.CENTER);
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.add(dashboardButton, BorderLayout.LINE_END);
footerPanel.setBorder(BorderFactory.createEmptyBorder(2, 0, 5, 0));
// Create console output
JTextPane consoleOutputPane = new JTextPane() {
@@ -80,15 +80,19 @@ public class MainView {
return getUI().getPreferredSize(this);
};
};
consoleOutputPane.setFont(new Font("Consola", Font.PLAIN, 12));
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, fontAttribute);
consoleOutputPane.getDocument().insertString(document.getLength(), message.formattedMessage(), colorAttribute);
} catch(BadLocationException e) {}
});
@@ -100,16 +104,35 @@ public class MainView {
// Create main panel
JPanel panel = new JPanel(new BorderLayout());
panel.add(scrollPane, BorderLayout.CENTER);
panel.add(footerPanel, BorderLayout.PAGE_END);
panel.add(footerPanel, BorderLayout.PAGE_END);
// Create tabbed pane
JTabbedPane tabbedPane = new JTabbedPane();
tabbedPane.addTab("Console", panel);
tabbedPane.addTab("Dashboard", new DashboardPanel(entralinked));
// Create window
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", () -> {
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
dashboardButton.setEnabled(false);
statusLabel.setText("Entralinked is shutting down ...");
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
@@ -119,32 +142,21 @@ public class MainView {
});
}
});
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.add(panel);
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 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,117 @@
package entralinked.gui.view;
import java.awt.GridBagLayout;
import java.util.regex.Pattern;
import javax.swing.BorderFactory;
import javax.swing.JButton;
import javax.swing.JDialog;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import com.formdev.flatlaf.extras.components.FlatTextField;
import entralinked.Entralinked;
import entralinked.model.user.User;
import entralinked.utility.MD5;
import entralinked.utility.SwingUtility;
public class PidToolDialog {
public static final Pattern WFC_ID_PATTERN = Pattern.compile("[0-9]{16}");
public static final Pattern FRIEND_CODE_PATTERN = Pattern.compile("[0-9]{12}");
public PidToolDialog(Entralinked entralinked, JFrame frame) {
// Create dialog
JDialog dialog = new JDialog(frame, "PID Tool");
// Create input fields
FlatTextField wfcIdField = new FlatTextField();
wfcIdField.setPlaceholderText("XXXX-XXXX-XXXX-XXXX");
FlatTextField friendCodeField = new FlatTextField();
friendCodeField.setPlaceholderText("XXXX-XXXX-XXXX");
// Create logic
JButton updateButton = new JButton("Update");
updateButton.addActionListener(event -> {
if(!entralinked.isInitialized()) {
JOptionPane.showMessageDialog(dialog, "Please wait for Entralinked to finish starting.", "Attention", JOptionPane.WARNING_MESSAGE);
return;
}
String userId = wfcIdField.getText().replace("-", "").replaceAll("\\s+", "");
String friendCodeString = friendCodeField.getText().replace("-", "").replaceAll("\\s+", "");
// Make sure WFC ID is valid
if(!WFC_ID_PATTERN.matcher(userId).matches()) {
JOptionPane.showMessageDialog(dialog, "Please enter a valid Wi-Fi Connection ID.", "Attention", JOptionPane.WARNING_MESSAGE);
return;
}
// Make sure Friend Code is valid
if(!FRIEND_CODE_PATTERN.matcher(friendCodeString).matches()) {
JOptionPane.showMessageDialog(dialog, "Please enter a valid Friend Code.", "Attention", JOptionPane.WARNING_MESSAGE);
return;
}
User user = entralinked.getUserManager().getUser(userId.substring(0, 13));
// Make sure user exists
if(user == null) {
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;
}
long friendCode = Long.parseLong(friendCodeString);
int profileId = (int)(friendCode & 0x7FFFFFFF);
int checksum = (int)(friendCode >> 32);
// Compute friend code checksum
byte[] buffer = {0x00, 0x00, 0x00, 0x00, 0x4A, 0x41, 0x52, 0x49}; // Last 4 bytes is inverted game code (IRAJ)
buffer[0] = (byte)(profileId & 0xFF);
buffer[1] = (byte)(profileId >> 8 & 0xFF);
buffer[2] = (byte)(profileId >> 16 & 0xFF);
buffer[3] = (byte)(profileId >> 24 & 0xFF);
byte[] hash = MD5.digest(buffer);
int computedChecksum = hash[0] >> 1 & 0x7F;
// Compare checksums
if(computedChecksum != checksum) {
JOptionPane.showMessageDialog(dialog, "This is not a valid Friend Code. Please check for typos.", "Attention", JOptionPane.WARNING_MESSAGE);
return;
}
// Everything checks out -- update the profile id!
user.setProfileIdOverride(profileId);
JOptionPane.showMessageDialog(dialog,
"All done! Please restart your game and use Game Sync.\nGame profile data will be updated and saved once you do so.");
});
// Create content panel
JPanel panel = new JPanel(new GridBagLayout());
String infoLabel = """
<html>
Enter the Wi-Fi Connection ID found in the internet settings of your DS<br>
as well as your Friend Code which you can view in-game using the Pal Pad.<br>
Confirm that the input data is correct and press 'Update'<br>
</html>
""";
panel.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
panel.add(new JLabel(infoLabel), SwingUtility.createConstraints(0, 0, 2, 1));
panel.add(new JLabel("Wi-Fi Connection ID"), SwingUtility.createConstraints(0, 1, 1, 1, 0, 1));
panel.add(wfcIdField, SwingUtility.createConstraints(1, 1));
panel.add(new JLabel("Friend Code"), SwingUtility.createConstraints(0, 2, 1, 1, 0, 1));
panel.add(friendCodeField, SwingUtility.createConstraints(1, 2));
panel.add(updateButton, SwingUtility.createConstraints(0, 3, 2, 1));
// Set dialog properties
dialog.setResizable(false);
dialog.add(panel);
dialog.pack();
dialog.setLocationRelativeTo(frame);
dialog.setVisible(true);
}
}

View File

@@ -5,11 +5,21 @@ import com.fasterxml.jackson.annotation.JsonEnumDefaultValue;
public enum AvenueShopType {
@JsonEnumDefaultValue
RAFFLE,
FLORIST,
SALON,
ANTIQUE,
DOJO,
CAFE,
MARKET
RAFFLE("Raffle Shop"),
FLORIST("Flower Shop"),
SALON("Beauty Salon"),
ANTIQUE("Antique Shop"),
DOJO("Dojo"),
CAFE("Café"),
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
@JsonEnumDefaultValue
YOUNGSTER(0),
LASS(0, true),
YOUNGSTER("Youngster", 0),
LASS("Lass", 0, true),
// 1
ACE_TRAINER_MALE(1),
ACE_TRAINER_FEMALE(1, true),
ACE_TRAINER_MALE("Ace Trainer♂", 1),
ACE_TRAINER_FEMALE("Ace Trainer♀", 1, true),
// 2
RANGER_MALE(2),
RANGER_FEMALE(2, true),
RANGER_MALE("Pokémon Ranger♂", 2),
RANGER_FEMALE("Pokémon Ranger♀", 2, true),
// 3
BREEDER_MALE(3),
BREEDER_FEMALE(3, true),
BREEDER_MALE("Pokémon Breeder♂", 3),
BREEDER_FEMALE("Pokémon Breeder♀", 3, true),
// 4
SCIENTIST_MALE(4),
SCIENTIST_FEMALE(4, true),
SCIENTIST_MALE("Scientist♂", 4),
SCIENTIST_FEMALE("Scientist♀", 4, true),
// 5
HIKER(5),
PARASOL_LADY(5, true),
HIKER("Hiker♂", 5),
PARASOL_LADY("Parasol Lady", 5, true),
// 6
ROUGHNECK(6),
NURSE(6, true),
ROUGHNECK("Roughneck", 6),
NURSE("Nurse", 6, true),
// 7
PRESCHOOLER_MALE(7),
PRESCHOOLER_FEMALE(7, true);
PRESCHOOLER_MALE("Preschooler♂", 7),
PRESCHOOLER_FEMALE("Preschooler♀", 7, true);
private final String displayName;
private final int clientId;
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.female = female;
}
private AvenueVisitorType(int clientId) {
this(clientId, false);
private AvenueVisitorType(String displayName, int clientId) {
this(displayName, clientId, false);
}
public String getDisplayName() {
return displayName;
}
public int getClientId() {

View File

@@ -5,7 +5,17 @@ import com.fasterxml.jackson.annotation.JsonEnumDefaultValue;
public enum PkmnGender {
@JsonEnumDefaultValue
MALE,
FEMALE,
GENDERLESS;
MALE("Male"),
FEMALE("Female"),
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 level = buffer.getByte(140);
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 female = ((buffer.getByte(64) >> 1) & 1) == 1;
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 {
@JsonEnumDefaultValue
HARDY,
LONELY,
BRAVE,
ADAMANT,
NAUGHTY,
BOLD,
DOCILE,
RELAXED,
IMPISH,
LAX,
TIMID,
HASTY,
SERIOUS,
JOLLY,
NAIVE,
MODEST,
MILD,
QUIET,
BASHFUL,
RASH,
CALM,
GENTLE,
SASSY,
CAREFUL,
QUIRKY;
HARDY("Hardy"),
LONELY("Lonely"),
BRAVE("Brave"),
ADAMANT("Adamant"),
NAUGHTY("Naughty"),
BOLD("Bold"),
DOCILE("Docile"),
RELAXED("Relaxed"),
IMPISH("Impish"),
LAX("Lax"),
TIMID("Timid"),
HASTY("Hasty"),
SERIOUS("Serious"),
JOLLY("Jolly"),
NAIVE("Naive"),
MODEST("Modest"),
MILD("Mild"),
QUIET("Quiet"),
BASHFUL("Bashful"),
RASH("Rash"),
CALM("Calm"),
GENTLE("Gentle"),
SASSY("Sassy"),
CAREFUL("Careful"),
QUIRKY("Quirky");
private final String displayName;
private PkmnNature(String displayName) {
this.displayName = displayName;
}
public static PkmnNature valueOf(int index) {
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.
*/
@JsonEnumDefaultValue
LOOK_AROUND,
LOOK_AROUND("Look around"),
/**
* 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_LOOK_AROUND,
WALK_LOOK_AROUND("Walk and look around"),
/**
* Only walk up and down.
*/
WALK_VERTICALLY,
WALK_VERTICALLY("Walk up and down"),
/**
* Only walk left and right.
*/
WALK_HORIZONTALLY,
WALK_HORIZONTALLY("Walk left and right"),
/**
* 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.
*/
SPIN_RIGHT,
SPIN_RIGHT("Spin right"),
/**
* 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) {
return index >= 0 && index < values().length ? values()[index] : null;
}
public String getDisplayName() {
return displayName;
}
}

View File

@@ -25,6 +25,7 @@ public class Player {
private String musical;
private String customCGearSkin;
private String customDexSkin;
private String customMusical;
private File dataDirectory;
public Player(String gameSyncId) {
@@ -152,6 +153,14 @@ public class Player {
return customDexSkin;
}
public void setCustomMusical(String customMusical) {
this.customMusical = customMusical;
}
public String getCustomMusical() {
return customMusical;
}
// IO stuff
public void setDataDirectory(File dataDirectory) {
@@ -177,4 +186,8 @@ public class Player {
public File getDexSkinFile() {
return new File(dataDirectory, "zukan.bin");
}
public File getMusicalFile() {
return new File(dataDirectory, "musical.bin");
}
}

View File

@@ -23,16 +23,17 @@ public record PlayerDto(
String musical,
String customCGearSkin,
String customDexSkin,
String customMusical,
int levelsGained,
@JsonDeserialize(contentAs = DreamEncounter.class) Collection<DreamEncounter> encounters,
@JsonDeserialize(contentAs = DreamItem.class) Collection<DreamItem> items,
@JsonDeserialize(contentAs = AvenueVisitor.class) Collection<AvenueVisitor> avenueVisitors) {
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.getCustomDexSkin(), player.getLevelsGained(), player.getEncounters(), player.getItems(),
player.getAvenueVisitors());
player.getCustomDexSkin(), player.getCustomMusical(), player.getLevelsGained(), player.getEncounters(),
player.getItems(), player.getAvenueVisitors());
}
/**
@@ -48,6 +49,7 @@ public record PlayerDto(
player.setMusical(musical);
player.setCustomCGearSkin(customCGearSkin);
player.setCustomDexSkin(customDexSkin);
player.setCustomMusical(customMusical);
player.setLevelsGained(levelsGained);
player.setEncounters(encounters == null ? Collections.emptyList() : encounters);
player.setItems(items == null ? Collections.emptyList() : items);

View File

@@ -17,6 +17,8 @@ import org.apache.logging.log4j.Logger;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import entralinked.GameVersion;
/**
* Manager class for managing {@link Player} information (Global Link users)
*/
@@ -175,7 +177,7 @@ public class PlayerManager {
* That is, the specified Game Sync ID wasn't already registered and the player data
* was saved without any errors.
*/
public Player registerPlayer(String gameSyncId) {
public Player registerPlayer(String gameSyncId, GameVersion version) {
// Check for duplicate Game Sync ID
if(playerMap.containsKey(gameSyncId)) {
logger.warn("Attempted to register duplicate player {}", gameSyncId);
@@ -193,6 +195,7 @@ public class PlayerManager {
// Construct player object
Player player = new Player(gameSyncId);
player.setStatus(PlayerStatus.AWAKE);
player.setGameVersion(version);
player.setDataDirectory(playerDataDirectory);
// Try to save player data

View File

@@ -2,7 +2,7 @@ package entralinked.model.user;
public class GameProfile {
private final int id;
private int id;
private String firstName;
private String lastName;
private String aimName;
@@ -20,6 +20,10 @@ public class GameProfile {
this.zipCode = zipCode;
}
public void setId(int id) {
this.id = id;
}
public int getId() {
return id;
}

View File

@@ -13,6 +13,7 @@ public class User {
private final String password; // I debated hashing it, but.. it's a 3-digit password...
private final Map<String, GameProfile> profiles = new HashMap<>();
private final Map<String, Dlc> dlcOverrides = new HashMap<>();
private int profileIdOverride; // For making it easier for the user to fix error 60000
public User(String id, String password) {
this.id = id;
@@ -23,6 +24,13 @@ public class User {
return "%s000".formatted(id).replaceAll("(.{4})(?!$)", "$1-");
}
/**
* @return The user's id, redacted for logging.
*/
public String getRedactedId() {
return "%s-XXXX-XXXX-XXXX".formatted(id.substring(0, 4));
}
public String getId() {
return id;
}
@@ -70,4 +78,12 @@ public class User {
public Dlc getDlcOverride(String type) {
return dlcOverrides.get(type);
}
public void setProfileIdOverride(int profileIdOverride) {
this.profileIdOverride = profileIdOverride;
}
public int getProfileIdOverride() {
return profileIdOverride;
}
}

View File

@@ -30,7 +30,6 @@ public class UserManager {
private static final Logger logger = LogManager.getLogger();
private final ObjectMapper mapper = new ObjectMapper().enable(SerializationFeature.INDENT_OUTPUT);
private final Map<String, User> users = new ConcurrentHashMap<>();
private final Map<Integer, GameProfile> profiles = new ConcurrentHashMap<>();
private final Map<String, ServiceSession> serviceSessions = new ConcurrentHashMap<>();
private final File dataDirectory = new File("users");
@@ -49,7 +48,7 @@ public class UserManager {
}
}
logger.info("Loaded {} user(s) with a total of {} profile(s)", users.size(), profiles.size());
logger.info("Loaded {} user(s)", users.size());
}
/**
@@ -75,20 +74,8 @@ public class UserManager {
throw new IOException("Duplicate user ID %s".formatted(id));
}
// Check for duplicate profile IDs before indexing anything
Collection<GameProfile> userProfiles = user.getProfiles();
if(userProfiles.stream().map(GameProfile::getId).anyMatch(profiles::containsKey)) {
throw new IOException("Duplicate profile ID in user %s".formatted(id));
}
// Index user
users.put(id, user);
// Index profiles
for(GameProfile profile : userProfiles) {
profiles.put(profile.getId(), profile);
}
} catch(IOException e) {
logger.error("Could not load user data at {}", inputFile.getAbsolutePath(), e);
}
@@ -190,14 +177,14 @@ public class UserManager {
/**
* Registers a user with the given ID and password.
* @return {@code true} if the registration was successful.
* Otherwise, if this user ID has already been registered, or if the user data could not be saved, {@code false} is returned instead.
* @return The {@link User} object of the newly registered user if the registration was successful.
* Otherwise, if this user ID has already been registered, or if the user data could not be saved, {@code null} is returned instead.
*/
public boolean registerUser(String userId, String plainPassword) {
public User registerUser(String userId, String plainPassword) {
// Check if user id already exists
if(users.containsKey(userId)) {
logger.warn("Attempted to register user with duplicate ID: {}", userId);
return false;
return null;
}
// Create user
@@ -205,11 +192,11 @@ public class UserManager {
// Save user data and return null if it fails
if(!saveUser(user)) {
return false;
return null;
}
users.put(userId, user);
return true;
return user;
}
/**
@@ -233,7 +220,7 @@ public class UserManager {
return null;
}
int profileId = nextProfileId();
int profileId = (int)(Math.random() * Integer.MAX_VALUE);
GameProfile profile = new GameProfile(profileId);
user.addProfile(branchCode, profile);
@@ -243,24 +230,9 @@ public class UserManager {
return null;
}
profiles.put(profileId, profile);
return profile;
}
/**
* @return A unique random 32-bit profile ID.
*/
private int nextProfileId() {
int profileId = (int)(Math.random() * Integer.MAX_VALUE);
// I live for that microscopic chance of StackOverflowError
if(profiles.containsKey(profileId)) {
return nextProfileId();
}
return profileId;
}
/**
* @return {@code true} if a user with the specified ID exists, otherwise {@code false}.
*/

View File

@@ -40,7 +40,6 @@ public abstract class NettyServerBase {
public boolean start() {
if(started) {
logger.warn("start() was called while {} server was already running!", name);
return true;
}
@@ -59,7 +58,6 @@ public abstract class NettyServerBase {
public boolean stop() {
if(!started) {
logger.warn("stop() was called while {} server wasn't running!", name);
return true;
}

View File

@@ -28,6 +28,7 @@ public class DnsQueryHandler extends SimpleChannelInboundHandler<DatagramDnsQuer
@Override
protected void channelRead0(ChannelHandlerContext ctx, DatagramDnsQuery query) throws Exception {
DefaultDnsQuestion question = query.recordAt(DnsSection.QUESTION);
logger.debug("Received {}", question);
DnsRecordType type = question.type();
// We only need type A (32 bit IPv4) for the DS

View File

@@ -2,9 +2,6 @@ package entralinked.network.dns;
import java.net.InetAddress;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import entralinked.network.NettyServerBase;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.Channel;
@@ -17,13 +14,11 @@ import io.netty.handler.codec.dns.DatagramDnsResponseEncoder;
public class DnsServer extends NettyServerBase {
private static final Logger logger = LogManager.getLogger();
private InetAddress hostAddress;
public DnsServer(InetAddress hostAddress) {
super("DNS", 53);
this.hostAddress = hostAddress;
logger.info("DNS queries will be resolved to {}", hostAddress.getHostAddress());
}
@Override

View File

@@ -28,6 +28,7 @@ import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.timeout.ReadTimeoutException;
/**
* GameSpy request handler.
@@ -50,6 +51,7 @@ public class GameSpyHandler extends SimpleChannelInboundHandler<GameSpyRequest>
@Override
public void channelActive(ChannelHandlerContext ctx) {
channel = ctx.channel();
logger.debug("Sending GameSpy server challenge");
// Generate random server challenge
serverChallenge = CredentialGenerator.generateChallenge(10);
@@ -60,6 +62,8 @@ public class GameSpyHandler extends SimpleChannelInboundHandler<GameSpyRequest>
@Override
public void channelInactive(ChannelHandlerContext ctx) {
logger.debug("User {} disconnected from GameSpy server", user == null ? null : user.getRedactedId());
// Clear data
serverChallenge = null;
sessionKey = -1;
@@ -69,15 +73,23 @@ public class GameSpyHandler extends SimpleChannelInboundHandler<GameSpyRequest>
@Override
protected void channelRead0(ChannelHandlerContext ctx, GameSpyRequest request) throws Exception {
logger.debug("Received {}", request);
request.process(this);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
if(cause instanceof ClosedChannelException) {
logger.debug("Silently discarded ClosedChannelException");
return; // Ignore this stupid exception.
}
// Handle timeout
if(cause instanceof ReadTimeoutException) {
logger.debug("User {} timed out", user == null ? null : user.getRedactedId());
return;
}
logger.error("Exception caught in GameSpy handler", cause);
// Send error message and close channel afterwards.
@@ -92,6 +104,7 @@ public class GameSpyHandler extends SimpleChannelInboundHandler<GameSpyRequest>
ServiceSession session = userManager.getServiceSession(authToken, "gamespy");
if(session == null) {
logger.debug("Rejecting GameSpy login request because the partner token is invalid");
sendErrorMessage(0x200, "Invalid partner token.", request.sequenceId());
return;
}
@@ -101,6 +114,7 @@ public class GameSpyHandler extends SimpleChannelInboundHandler<GameSpyRequest>
String expectedResponse = createCredentialHash(partnerChallengeHash, authToken, clientChallenge, serverChallenge);
if(!expectedResponse.equals(request.response())) {
logger.debug("Rejecting GameSpy login request because the challenge response is invalid");
sendErrorMessage(0x202, "Invalid response.", request.sequenceId());
return;
}
@@ -119,6 +133,17 @@ public class GameSpyHandler extends SimpleChannelInboundHandler<GameSpyRequest>
}
}
// Update profile id if an override is set
int profileIdOverride = user.getProfileIdOverride();
if(profileIdOverride > 0) {
profile.setId(profileIdOverride);
user.setProfileIdOverride(0);
userManager.saveUser(user); // It's not too big of a deal if this fails for some reason
}
logger.info("User {} logged in with profile {}", user.getRedactedId(), profile.getId());
// Prepare and send response
sessionKey = secureRandom.nextInt(Integer.MAX_VALUE);
String proof = createCredentialHash(partnerChallengeHash, authToken, serverChallenge, clientChallenge);
@@ -171,10 +196,9 @@ public class GameSpyHandler extends SimpleChannelInboundHandler<GameSpyRequest>
passwordHash));
}
public void destroySessionKey(int sessionKey) {
if(validateSessionKey(sessionKey)) {
sessionKey = -1;
}
public void handleLogout() {
logger.info("User {} logged out of profile {}", user.getRedactedId(), profile.getId());
sessionKey = -1; // Is there a point?
}
public boolean validateSessionKey(int sessionKey) {

View File

@@ -26,6 +26,7 @@ import io.netty.channel.ChannelPipeline;
import io.netty.channel.epoll.EpollServerSocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.DelimiterBasedFrameDecoder;
import io.netty.handler.timeout.ReadTimeoutHandler;
import io.netty.util.concurrent.DefaultEventExecutor;
import io.netty.util.concurrent.EventExecutorGroup;
@@ -64,6 +65,7 @@ public class GameSpyServer extends NettyServerBase {
@Override
protected void initChannel(Channel channel) throws Exception {
ChannelPipeline pipeline = channel.pipeline();
pipeline.addLast(new ReadTimeoutHandler(180));
pipeline.addLast(new DelimiterBasedFrameDecoder(512, Unpooled.wrappedBuffer("\\final\\".getBytes())));
pipeline.addLast(new GameSpyRequestDecoder(mapper, requestTypes));
pipeline.addLast(new GameSpyMessageEncoder(mapper));

View File

@@ -1,13 +1,9 @@
package entralinked.network.gamespy.request;
import entralinked.network.gamespy.GameSpyHandler;
import entralinked.network.gamespy.message.GameSpyMessage;
@GameSpyMessage(name = "ka")
public record GameSpyKeepAliveRequest() implements GameSpyRequest {
@Override
public void process(GameSpyHandler handler) {
handler.sendMessage(this); // Pong it back
}
public void process(GameSpyHandler handler) {}
}

View File

@@ -28,4 +28,11 @@ public record GameSpyLoginRequest(
public void process(GameSpyHandler handler) {
handler.handleLoginRequest(this);
}
@Override
public String toString() {
return ("GameSpyLoginRequest[sequenceId=%s, gameName=%s, profileId=%s, namespaceId=%s, partnerId=%s, productId=%s, "
+ "sdkRevision=%s, firewall=%s, port=%s, quiet=%s]")
.formatted(sequenceId, gameName, profileId, namespaceId, partnerId, productId, sdkRevision, firewall, port, quiet);
}
}

View File

@@ -10,6 +10,14 @@ public record GameSpyLogoutRequest(
@Override
public void process(GameSpyHandler handler) {
handler.destroySessionKey(sessionKey);
if(handler.validateSessionKey(sessionKey)) {
handler.handleLogout();
}
}
@Override
public String toString() {
// Exlude session key
return "GameSpyLogoutRequest[]";
}
}

View File

@@ -16,4 +16,10 @@ public record GameSpyProfileRequest(
handler.handleProfileRequest(this);
}
}
@Override
public String toString() {
// Exlude session key
return "GameSpyProfileRequest[sequenceId=%s, profileId=%s]".formatted(sequenceId, profileId);
}
}

View File

@@ -21,4 +21,9 @@ public record GameSpyProfileUpdateRequest(
handler.handleUpdateProfileRequest(this);
}
}
@Override
public String toString() {
return "GameSpyProfileUpdateRequest[partnerId=%s]".formatted(partnerId);
}
}

View File

@@ -14,4 +14,9 @@ public record GameSpyStatusRequest(
public void process(GameSpyHandler handler) {
handler.validateSessionKey(sessionKey);
}
@Override
public String toString() {
return "GameSpyStatusRequest[]";
}
}

View File

@@ -23,6 +23,7 @@ import entralinked.LauncherAgent;
import entralinked.utility.CertificateGenerator;
import io.javalin.Javalin;
import io.javalin.http.HttpStatus;
import io.javalin.util.ConcurrencyUtil;
import io.javalin.util.JavalinException;
public class HttpServer {
@@ -32,7 +33,9 @@ public class HttpServer {
private final Javalin javalin;
private boolean started;
public HttpServer(Entralinked entralinked) {
public HttpServer(Entralinked entralinked) {
ConcurrencyUtil.INSTANCE.setUseLoom(false);
// Create certificate keystore
KeyStore keyStore = null;
@@ -75,7 +78,6 @@ public class HttpServer {
public boolean start() {
if(started) {
logger.warn("start() was called while HTTP server was already running!");
return true;
}
@@ -94,7 +96,6 @@ public class HttpServer {
public boolean stop() {
if(!started) {
logger.warn("stop() was called while HTTP server wasn't running!");
return true;
}

View File

@@ -19,7 +19,6 @@ import javax.imageio.ImageIO;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.bouncycastle.util.Arrays;
import com.fasterxml.jackson.databind.ObjectMapper;
@@ -35,7 +34,6 @@ import entralinked.model.player.Player;
import entralinked.model.player.PlayerManager;
import entralinked.model.player.PlayerStatus;
import entralinked.network.http.HttpHandler;
import entralinked.utility.ColorUtility;
import entralinked.utility.Crc16;
import entralinked.utility.GsidUtility;
import entralinked.utility.LEOutputStream;
@@ -49,6 +47,8 @@ import io.javalin.json.JavalinJackson;
/**
* HTTP handler for requests made to the user dashboard.
*
* @deprecated
*/
public class DashboardHandler implements HttpHandler {
@@ -310,29 +310,8 @@ public class DashboardHandler implements HttpHandler {
previewImage = TiledImageUtility.readCGearSkin(new ByteArrayInputStream(skinBytes), offsetIndices);
break;
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
TiledImageUtility.writeDexSkin(byteOutputStream, image, backgroundColors);
TiledImageUtility.writeDexSkin(byteOutputStream, image, TiledImageUtility.generateBackgroundColors(image));
skinBytes = byteOutputStream.toByteArray();
previewImage = TiledImageUtility.readDexSkin(new ByteArrayInputStream(skinBytes), true);
break;

View File

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

View File

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

View File

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

View File

@@ -4,6 +4,9 @@ import java.io.FileInputStream;
import java.io.IOException;
import java.util.List;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import com.fasterxml.jackson.databind.ObjectMapper;
import entralinked.Entralinked;
@@ -25,6 +28,7 @@ import io.javalin.http.HttpStatus;
*/
public class DlsHandler implements HttpHandler {
private static final Logger logger = LogManager.getLogger();
private final ObjectMapper mapper = new ObjectMapper(new UrlEncodedFormFactory());
private final DlcList dlcList;
private final UserManager userManager;
@@ -45,11 +49,13 @@ public class DlsHandler implements HttpHandler {
private void handleDownloadRequest(Context ctx) throws IOException {
// Deserialize request body
DlsRequest request = mapper.readValue(ctx.body().replace("%2A", "*"), DlsRequest.class);
logger.debug("Received {}", request);
// Check if service session is valid
ServiceSession session = userManager.getServiceSession(request.serviceToken(), "dls1.nintendowifi.net");
if(session == null) {
logger.debug("Rejecting DLS request because the service session has expired");
ctx.status(HttpStatus.UNAUTHORIZED);
return;
}

View File

@@ -21,5 +21,12 @@ public record DlsRequest(
@JsonProperty("contents") String dlcName, // action=contents
@JsonProperty("attr1") String dlcType, // action=list
@JsonProperty("attr2") int dlcIndex, // action=list
@JsonProperty("offset") int offset, // ?
@JsonProperty("num") int num) {} // ?
@JsonProperty("offset") int offset, // Start offset in the list
@JsonProperty("num") int num) { // Number of entries
@Override
public String toString() {
return ("DlsRequest[gameCode=%s, action=%s, dlcGameCode=%s, dlcName=%s, dlcType=%s, dlcIndex=%s, offset=%s, num=%s]")
.formatted(gameCode, action, dlcGameCode, dlcName, dlcType, dlcIndex, offset, num);
}
}

View File

@@ -2,6 +2,9 @@ package entralinked.network.http.nas;
import java.io.IOException;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
@@ -21,6 +24,7 @@ import io.javalin.http.Context;
*/
public class NasHandler implements HttpHandler {
private static final Logger logger = LogManager.getLogger();
private final ObjectMapper mapper = new ObjectMapper(new UrlEncodedFormFactory()).registerModule(new JavaTimeModule());
private final Configuration configuration;
private final UserManager userManager;
@@ -42,6 +46,7 @@ public class NasHandler implements HttpHandler {
private void handleNasRequest(Context ctx) throws IOException {
// Deserialize body into a request object
NasRequest request = mapper.readValue(ctx.body(), NasRequest.class);
logger.debug("Received {}", request);
// Determine handler function based on request action
HttpRequestHandler<NasRequest> handler = switch(request.action()) {
@@ -61,6 +66,7 @@ public class NasHandler implements HttpHandler {
private void handleLogin(NasRequest request, Context ctx) throws IOException {
// Make sure branch code is present
if(request.branchCode() == null) {
logger.debug("Rejecting NAS login request because no branch code is present");
result(ctx, NasReturnCode.BAD_REQUEST);
return;
}
@@ -77,7 +83,7 @@ public class NasHandler implements HttpHandler {
// Try to register, if the configuration allows it
if(!UserManager.isValidUserId(userId) || userManager.doesUserExist(userId)
|| !userManager.registerUser(userId, request.password())) {
|| userManager.registerUser(userId, request.password()) == null) {
// Oh well, try again!
result(ctx, NasReturnCode.USER_NOT_FOUND);
return;
@@ -85,10 +91,12 @@ public class NasHandler implements HttpHandler {
// Should *never* return null in this location
user = userManager.authenticateUser(userId, request.password());
logger.info("Created account for user {}", user.getRedactedId());
}
// Prepare GameSpy server credentials
ServiceCredentials credentials = userManager.createServiceSession(user, "gamespy", request.branchCode());
logger.info("Created GameSpy session for user {}", user.getRedactedId());
result(ctx, new NasLoginResponse("gamespy.com", credentials.authToken(), credentials.challenge()));
}
@@ -105,11 +113,14 @@ public class NasHandler implements HttpHandler {
}
// Try to register user
if(!userManager.registerUser(userId, request.password())) {
User user = userManager.registerUser(userId, request.password());
if(user == null) {
result(ctx, NasReturnCode.INTERNAL_SERVER_ERROR);
return;
}
logger.info("Created account for user {}", user.getRedactedId());
result(ctx, NasReturnCode.REGISTRATION_SUCCESS);
}
@@ -125,15 +136,19 @@ public class NasHandler implements HttpHandler {
return;
}
String type = request.serviceType();
// Determine service location from type
String service = switch(request.serviceType()) {
String service = switch(type) {
case "0000" -> "external"; // External game-specific service
case "9000" -> "dls1.nintendowifi.net"; // Download server
default -> throw new IllegalArgumentException("Invalid service type: " + request.serviceType());
default -> throw new IllegalArgumentException("Invalid service type: " + type);
};
// Prepare user credentials
ServiceCredentials credentials = userManager.createServiceSession(user, service, null);
logger.info("Created {} session for user {}",
type.equals("0000") ? "PGL" : type.equals("9000") ? "DLS1" : "this should never be logged", user.getRedactedId());
result(ctx, new NasServiceLocationResponse(true, service, credentials.authToken()));
}

View File

@@ -29,4 +29,11 @@ public record NasRequest(
// Request-specific info
@JsonProperty(value = "action", required = true) String action,
@JsonProperty("gsbrcd") String branchCode, // action=login
@JsonProperty("svc") String serviceType) {} // action=SVCLOC
@JsonProperty("svc") String serviceType) { // action=SVCLOC
@Override
public String toString() {
return ("NasRequest[gameCode=%s, makerCode=%s, unitCode=%s, sdkVersion=%s, language=%s, action=%s, serviceType=%s]")
.formatted(gameCode, makerCode, unitCode, sdkVersion, language, action, serviceType);
}
}

View File

@@ -1,5 +1,6 @@
package entralinked.network.http.pgl;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
@@ -93,6 +94,7 @@ public class PglHandler implements HttpHandler {
if(credentials == null ||
!username.equals(credentials.getUsername()) || !password.equals(credentials.getPassword())) {
logger.debug("Rejecting PGL request because the auth credentials were incorrect");
ctx.status(HttpStatus.UNAUTHORIZED);
clearTasks(ctx);
return;
@@ -100,11 +102,13 @@ public class PglHandler implements HttpHandler {
// Deserialize the request
PglRequest request = mapper.readValue(ctx.queryString(), PglRequest.class);
logger.debug("Received {}", request);
// Verify the service session token
ServiceSession session = userManager.getServiceSession(request.token(), "external");
if(session == null) {
logger.debug("Rejecting PGL request because the service session has expired");
ctx.status(HttpStatus.UNAUTHORIZED);
clearTasks(ctx);
return;
@@ -131,7 +135,7 @@ public class PglHandler implements HttpHandler {
};
// Handle the request
handler.process(request, ctx);
handler.process(request, ctx);
}
/**
@@ -198,13 +202,14 @@ public class PglHandler implements HttpHandler {
return;
}
logger.info("Player {} is downloading save data as user {}", player.getGameSyncId(), user.getRedactedId());
// Write status code
writeStatusCode(outputStream, 0);
// Allow it to wake up anyway, maybe the poor sap is stuck..
// Just don't send any other data.
if(player.getStatus() == PlayerStatus.AWAKE) {
logger.info("Player {} is downloading save data, but is already awake!", player.getGameSyncId());
return;
}
@@ -215,8 +220,10 @@ public class PglHandler implements HttpHandler {
String cgearType = player.getGameVersion().isVersion2() ? "CGEAR2" : "CGEAR";
String cgearSkin = player.getCGearSkin();
String dexSkin = player.getDexSkin();
String musical = player.getMusical();
int cgearSkinIndex = 0;
int dexSkinIndex = 0;
int musicalIndex = 0;
// Create or remove custom C-Gear skin DLC override
if("custom".equals(cgearSkin)) {
@@ -238,6 +245,17 @@ public class PglHandler implements HttpHandler {
user.removeDlcOverride("ZUKAN");
}
// Create or remove custom musical DLC override
if("custom".equals(musical)) {
musicalIndex = 1;
File file = player.getMusicalFile();
user.setDlcOverride("MUSICAL", new Dlc(file.getAbsolutePath(),
"custom", "IRAO", "MUSICAL", musicalIndex, (int)file.length(), 0, true));
} else {
musicalIndex = dlcList.getDlcIndex("IRAO", "MUSICAL", musical);
user.removeDlcOverride("MUSICAL");
}
// When waking up a Pokémon, these 4 bytes are written to 0x1D304 in the save file.
// 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.
@@ -259,7 +277,7 @@ public class PglHandler implements HttpHandler {
// Write misc stuff and DLC information
outputStream.writeShort(player.getLevelsGained());
outputStream.write(0); // Unknown
outputStream.write(dlcList.getDlcIndex("IRAO", "MUSICAL", player.getMusical()));
outputStream.write(musicalIndex);
outputStream.write(cgearSkinIndex);
outputStream.write(dexSkinIndex);
outputStream.write(decorList.isEmpty() ? 0 : 1); // Seems to be a flag for indicating whether or not decor data is present
@@ -312,7 +330,7 @@ public class PglHandler implements HttpHandler {
// 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.
// The shope type is calculated in such an odd manner because for some reason,
// The shop type is calculated in such an odd manner because for some reason,
// the 'starting' index of the shop type used increases by 2 for each visitor type.
// 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.
@@ -324,7 +342,7 @@ public class PglHandler implements HttpHandler {
outputStream.writeInt(1); // [20] Ignores if 0
outputStream.write(visitor.countryCode());
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.type().isFemale() ? 1 : 0);
outputStream.write(0); // [29] Does.. something
@@ -343,6 +361,7 @@ public class PglHandler implements HttpHandler {
private void handleMemoryLink(PglRequest request, Context ctx) throws IOException {
LEOutputStream outputStream = new LEOutputStream(ctx.outputStream());
Player player = playerManager.getPlayer(request.gameSyncId());
User user = ctx.attribute("user");
// Check if player exists
if(player == null) {
@@ -350,6 +369,12 @@ public class PglHandler implements HttpHandler {
return;
}
// Version null check because this can happen in specific cases
if(player.getGameVersion() == null) {
writeStatusCode(outputStream, 5); // No game save data exists for this Game Sync ID
return;
}
// Check if the save file belongs to Black or White
if(player.getGameVersion().isVersion2()) {
writeStatusCode(outputStream, 10); // Not a Black or White save
@@ -364,6 +389,8 @@ public class PglHandler implements HttpHandler {
return;
}
logger.info("User {} is Memory Linking with player {}", user.getRedactedId(), player.getGameSyncId());
// Send the save data!
try(FileInputStream inputStream = new FileInputStream(file)) {
writeStatusCode(outputStream, 0);
@@ -422,12 +449,11 @@ public class PglHandler implements HttpHandler {
* POST handler for {@code /dsio/gw?p=savedata.upload}
*/
private void handleUploadSaveData(PglRequest request, Context ctx) throws IOException {
// Prepare response
LEOutputStream outputStream = new LEOutputStream(ctx.outputStream());
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
Player player = playerManager.getPlayer(request.gameSyncId());
if(player == null
|| (!configuration.allowOverwritingPlayerDreamInfo() && player.getStatus() != PlayerStatus.AWAKE)
|| (!configuration.allowPlayerGameVersionMismatch() && player.getGameVersion() != null
@@ -444,6 +470,8 @@ public class PglHandler implements HttpHandler {
return;
}
logger.info("Player {} is uploading save data as user {}", player.getGameSyncId(), user.getRedactedId());
// Try to store save data
if(!playerManager.storePlayerGameSaveFile(player, ctx.bodyInputStream())) {
writeStatusCode(outputStream, 4); // Game save data IO error
@@ -477,13 +505,9 @@ public class PglHandler implements HttpHandler {
/**
* POST handler for {@code /dsio/gw?p=account.create.upload}
*/
private void handleCreateAccount(PglRequest request, Context ctx) throws IOException {
// It sends the entire save file, but we just skip through it because we don't need anything from it here
ServletInputStream inputStream = ctx.req().getInputStream();
while(!inputStream.isFinished()) {
inputStream.read();
}
private void handleCreateAccount(PglRequest request, Context ctx) throws IOException {
// Have to read all the bytes first for some reason
byte[] bytes = ctx.bodyAsBytes();
// Prepare response
LEOutputStream outputStream = new LEOutputStream(ctx.outputStream());
@@ -501,11 +525,19 @@ public class PglHandler implements HttpHandler {
}
// Try to register player
if(playerManager.registerPlayer(request.gameSyncId()) == null) {
Player player = playerManager.registerPlayer(request.gameSyncId(), request.gameVersion());
if(player == null) {
writeStatusCode(outputStream, 3); // Registration error
return;
}
// Try to store save data
if(!playerManager.storePlayerGameSaveFile(player, new ByteArrayInputStream(bytes))) {
writeStatusCode(outputStream, 4); // Game save data IO error
return;
}
// Write status code
writeStatusCode(outputStream, 0);
}
@@ -526,7 +558,8 @@ public class PglHandler implements HttpHandler {
}
// Try to register player
if(playerManager.registerPlayer(gameSyncId) == null) {
// Regrettably, this request does not contain game save & version data.
if(playerManager.registerPlayer(gameSyncId, null) == null) {
writeStatusCode(outputStream, 3); // Registration error
return;
}

View File

@@ -17,4 +17,11 @@ public record PglRequest(
public GameVersion gameVersion() {
return GameVersion.lookup(romCode(), languageCode());
}
@Override
public String toString() {
// Exlude token
return "PglRequest[gameSyncId=%s, type=%s, romCode=%s, languageCode=%s, dreamWorld=%s]"
.formatted(gameSyncId, type, romCode, languageCode, dreamWorld);
}
}

View File

@@ -55,7 +55,6 @@ public class CertificateGenerator {
+ "Fw2ewOxmIyw3d4HFGrLx9yvk6Q5HpTlSgvbgO22k/vo0N43dO7utgfn/e4Xo9alIAktnewGqj5nRm75H7iaLEJBo983jJdugeXdp6y5X6sDmaDmyw/p91TKPrKmD"
+ "4ebbTY38D5WT67fONjW/X1OLNHg46GYYZa6etbxQ7671f7GypgwnI=";
private static final String issuerPrivateKeyString =
"eNod0Em2a0AAANAFGRBNYVgK0QalfWaIPproZfX/nH+XcG1dR4mnSxCaUu3JKUe5WLR1rND+Q+gwb3NO3ws5e0YXcydZcUtNV/35votzw9SsDstssI0TPxg5q1XPUq"
+ "EpGgfib4UnATQKiAcZHsBOsEWg2nShyhd7CoLQznBPWW/+iLfW3bMujf723htqG+n0p30h/SClZIRZibH7I5hGgQHRqgvpuJ/IDWpH9HQZelCC0xPK8uXZ87jcwv"

View File

@@ -29,9 +29,9 @@ public class ColorUtility {
*/
public static int convertBGR555ToRGB888(int color) {
int red = (color & 0x1F) << 3;
int green = ((color & 0x3E0) >> 5) << 3;
int blue = ((color & 0x7C00) >> 10) << 3;
return (red << 16) | (green << 8) | blue;
int green = (color >> 5 & 0x1F) << 3;
int blue = (color >> 10 & 0x1F) << 3;
return ((red | red >> 5) << 16) | ((green | green >> 5) << 8) | (blue | blue >> 5);
}
/**

View File

@@ -7,6 +7,7 @@ import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Consumer;
import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.core.Appender;
import org.apache.logging.log4j.core.Core;
import org.apache.logging.log4j.core.Filter;
@@ -25,7 +26,8 @@ import org.apache.logging.log4j.core.config.plugins.PluginFactory;
printObject = true)
public class ConsumerAppender extends AbstractAppender {
protected static final Map<String, List<Consumer<String>>> consumerMap = new ConcurrentHashMap<>();
public record LogMessage(Level level, String rawMessage, String formattedMessage) {}
protected static final Map<String, List<Consumer<LogMessage>>> consumerMap = new ConcurrentHashMap<>();
protected ConsumerAppender(String name, Filter filter, Layout<? extends Serializable> layout,
boolean ignoreExceptions, Property[] properties) {
@@ -41,8 +43,8 @@ public class ConsumerAppender extends AbstractAppender {
return new ConsumerAppender(name, filter, layout, ignoreExceptions, null);
}
public static void addConsumer(String appenderName, Consumer<String> consumer) {
List<Consumer<String>> consumers = consumerMap.getOrDefault(appenderName, new ArrayList<>());
public static void addConsumer(String appenderName, Consumer<LogMessage> consumer) {
List<Consumer<LogMessage>> consumers = consumerMap.getOrDefault(appenderName, new ArrayList<>());
consumers.add(consumer);
consumerMap.putIfAbsent(appenderName, consumers);
}
@@ -50,11 +52,12 @@ public class ConsumerAppender extends AbstractAppender {
@Override
public void append(LogEvent event) {
String formattedMessage = getLayout().toSerializable(event).toString();
List<Consumer<String>> consumers = consumerMap.get(getName());
List<Consumer<LogMessage>> consumers = consumerMap.get(getName());
LogMessage logMessage = new LogMessage(event.getLevel(), event.getMessage().getFormattedMessage(), formattedMessage);
if(consumers != null) {
for(Consumer<String> consumer : consumers) {
consumer.accept(formattedMessage);
for(Consumer<LogMessage> consumer : consumers) {
consumer.accept(logMessage);
}
}
}

View File

@@ -21,6 +21,13 @@ public class MD5 {
* @return A hex-formatted MD5 hash of the specified input.
*/
public static String digest(String string) {
return StringUtil.toHexStringPadded(digest(string.getBytes(StandardCharsets.ISO_8859_1)));
}
/**
* @return An MD5 hash of the specified input.
*/
public static byte[] digest(byte[] bytes) {
if(digest == null) {
try {
digest = MessageDigest.getInstance("MD5");
@@ -29,6 +36,6 @@ public class MD5 {
}
}
return StringUtil.toHexStringPadded(digest.digest(string.getBytes(StandardCharsets.ISO_8859_1)));
return digest.digest(bytes);
}
}

View File

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

View File

@@ -4,6 +4,7 @@ import java.awt.image.BufferedImage;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Arrays;
import org.apache.logging.log4j.LogManager;
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.
*/

View File

@@ -6,9 +6,12 @@
<body>
<div class="root-container">
<div>
<label for="gsid">Game Sync ID</label><br>
<input type='text' id="gsid" name='gsid' placeholder='XXXXXXXXXX'>
<button id="login" onclick="postLogin()">Login</button>
<form onsubmit="return postLogin()">
<label for="gsid">Game Sync ID</label><br>
<input type="text" id="gsid" name="gsid" placeholder="XXXXXXXXXX" autocomplete="on">
<label for="submit" class="big-button">Login</label>
<input type="submit" id="submit" hidden/>
</form>
</div>
</div>
</body>

View File

@@ -195,7 +195,7 @@
<option value="WALK_LOOK_AROUND">Walk and Look Around</option>
<option value="WALK_VERTICALLY">Walk Vertically</option>
<option value="WALK_HORIZONTALLY">Walk Horizontally</option>
<option value="WALK_HORIZONTALLY_LOOK_AROUND">Walk Horizontally and Look Around</option>
<option value="WALK_LOOK_HORIZONTALLY">Walk Horizontally and Look Around</option>
<option value="SPIN_RIGHT">Spin Right</option>
<option value="SPIN_LEFT">Spin Left</option>
</select>
@@ -266,6 +266,30 @@
<option value="WHITE_ENGLISH">White Version</option>
<option value="BLACK_2_ENGLISH">Black 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>
<label for="visitor-form-region">Country</label>
<select id="visitor-form-region" name="region" value="1">

View File

@@ -10,4 +10,6 @@ function postLogin() {
window.location.href = "/dashboard/profile.html";
}
});
return false;
}

View File

@@ -509,7 +509,7 @@ const REGION_LIST = [];
{id: 493, name: "Arceus", downloadable: true, gender: "unknown", forms: [
"Normal", "Fighting", "Flying", "Poison", "Ground", "Rock", "Bug", "Ghost",
"Steel", "Fire", "Water", "Grass", "Electric", "Psychic", "Ice", "Dragon", "Dark"]},
{id: 494, name: "Victini", downloadable: true, gender: "unknown"},
{id: 494, name: "Victini", downloadable: false, gender: "unknown"},
{id: 495, name: "Snivy", downloadable: false},
{id: 496, name: "Servine", downloadable: false},
{id: 497, name: "Serperior", downloadable: false},

View File

@@ -11,21 +11,22 @@ input {
border-radius: 10px;
width: 100%;
margin-top: 4px;
margin-bottom: 16px;
margin-bottom: 24px;
padding: 4px 4px;
font-size: 16px;
}
button {
.big-button {
color: white;
background-color: #3D3D3D;
border: 0px;
border-radius: 10px;
font-size: 16px;
font-family: sans-serif;
padding: 16px 32px;
}
button:active {
.big-button:active {
background-color: #333333;
}

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 720 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@@ -1,25 +1,39 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE xml>
<Configuration status="off" packages="entralinked.utility">
<Configuration status="WARN" packages="entralinked.utility">
<Properties>
<Property name="LOGS_DIR">logs</Property>
<Property name="PATTERN_MINIMAL">%d{yyyy-MM-dd HH:mm:ss.SSS} %5level : %msg%n</Property>
<Property name="PATTERN_DETAILED">%d{yyyy-MM-dd HH:mm:ss.SSS} %5level [%32t] %-48c{1.2*} : %msg%n</Property>
</Properties>
<Appenders>
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss.SSS} %5level [%32t] %-48c{1.2*} : %msg%n"/>
<PatternLayout pattern="${PATTERN_DETAILED}"/>
<ThresholdFilter level="INFO"/>
</Console>
<RollingFile name="LogFile" fileName="logs/latest.log" filePattern="logs/%d{yyyy-MM-dd}-%i.log.gz">
<PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss.SSS} %5level [%16t] %-48c{1.2*} : %msg%n"/>
<RollingFile name="LogFile" fileName="${LOGS_DIR}/latest.log" filePattern="${LOGS_DIR}/%d{yyyy-MM-dd}-%i.log.gz">
<PatternLayout pattern="${PATTERN_DETAILED}"/>
<ThresholdFilter level="INFO"/>
<Policies>
<OnStartupTriggeringPolicy/>
</Policies>
</RollingFile>
<ConsumerAppender name="GuiOutput">
<PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss.SSS} %5level : %msg%n"/>
<PatternLayout pattern="${PATTERN_MINIMAL}"/>
<ThresholdFilter level="INFO"/>
</ConsumerAppender>
<File name="DebugLogFile" fileName="${LOGS_DIR}/debug.log" append="false">
<PatternLayout pattern="${PATTERN_DETAILED}"/>
<ThresholdFilter level="DEBUG"/>
</File>
</Appenders>
<Loggers>
<Root level="info">
<AppenderRef ref="Console"/>
<Logger name="entralinked" additivity="false">
<AppenderRef ref="LogFile"/>
<AppenderRef ref="GuiOutput"/>
</Root>
<AppenderRef ref="DebugLogFile"/>
<AppenderRef ref="Console"/>
</Logger>
<Root level="all"/>
</Loggers>
</Configuration>

View File

@@ -0,0 +1,93 @@
package entralinked.serialization;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import java.io.IOException;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;
import org.junit.jupiter.api.TestInstance.Lifecycle;
import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;
@TestInstance(Lifecycle.PER_CLASS)
public class GameSpyMessageFactoryTest {
protected ObjectMapper mapper;
@BeforeAll
void before() {
mapper = new ObjectMapper(new GameSpyMessageFactory());
}
@Test
@DisplayName("Test if generator writes objects correctly")
void testGeneratorWriteObject() throws IOException {
Map<String, Object> data = new LinkedHashMap<>();
data.put("key", "value");
data.put("emptyValue", "");
data.put("hello", "world");
data.put("numberTest", 123);
assertEquals("\\key\\value\\emptyValue\\\\hello\\world\\numberTest\\123", mapper.writeValueAsString(data));
}
@Test
@DisplayName("Test if generator throws exception when writing a nested object")
void testGeneratorThrowsExceptionWhenWritingNestedObject() {
Map<String, Object> data = Map.of("someKey", "someValue", "nestedObjectKey", Map.of("key", "value"));
JsonMappingException exception = assertThrows(JsonMappingException.class, () -> mapper.writeValueAsString(data));
assertEquals("this format does not support nested objects", exception.getOriginalMessage());
}
@Test
@DisplayName("Test if generator throws exception when writing an array")
void testGeneratorThrowsExceptionWhenWritingArray() {
Map<String, Object> data = Map.of("arrayKey", List.of(1, 2, 3, 4));
JsonMappingException exception = assertThrows(JsonMappingException.class, () -> mapper.writeValueAsString(data));
assertEquals("this format does not support arrays", exception.getOriginalMessage());
}
@Test
@DisplayName("Test if parser creates object from input string correctly")
void testParserReadObject() throws IOException {
String inputString = "\\key\\value\\emptyValue\\\\hello\\world\\numberTest\\123";
Map<String, Object> data = mapper.readValue(inputString, new TypeReference<Map<String, Object>>(){});
assertEquals("value", data.get("key"));
assertEquals("", data.get("emptyValue"));
assertEquals("world", data.get("hello"));
assertEquals("123", data.get("numberTest"));
}
@Test
@DisplayName("Test if parser throws exception when reading an empty key")
void testParserThrowsExceptionWhenReadingEmptyKey() {
String inputString = "\\some\\value\\\\emptyKey";
JsonParseException exception = assertThrows(JsonParseException.class, () -> mapper.readValue(inputString, Map.class));
assertEquals("Unexpected character ('\\' (code 92)): expected field name", exception.getOriginalMessage());
}
@Test
@DisplayName("Test if parser throws exception when input doesn't start with backslash")
void testParserThrowsExceptionWhenFirstCharacterNotBackslash() {
String inputString = "key\\value";
JsonParseException exception = assertThrows(JsonParseException.class, () -> mapper.readValue(inputString, Map.class));
assertEquals("Unexpected character ('k' (code 107)): expected '\\' to open field name", exception.getOriginalMessage());
}
@Test
@DisplayName("Test if parser throws exception when field name isn't closed")
void testParserThrowsExceptionWhenFieldNameNotClosed() {
String inputString = "\\hello\\world\\key";
JsonParseException exception = assertThrows(JsonParseException.class, () -> mapper.readValue(inputString, Map.class));
assertEquals("Unexpected end-of-input in FIELD_NAME", exception.getOriginalMessage());
}
}

View File

@@ -0,0 +1,94 @@
package entralinked.serialization;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import java.io.IOException;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;
import org.junit.jupiter.api.TestInstance.Lifecycle;
import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;
@TestInstance(Lifecycle.PER_CLASS)
public class UrlEncodedFormFactoryTest {
protected ObjectMapper mapper;
protected ObjectMapper mapperNoBase64;
@BeforeAll
void before() {
mapper = new ObjectMapper(new UrlEncodedFormFactory());
mapperNoBase64 = new ObjectMapper(new UrlEncodedFormFactory()
.disable(UrlEncodedFormParser.Feature.BASE64_DECODE_VALUES)
.disable(UrlEncodedFormGenerator.Feature.BASE64_ENCODE_VALUES));
}
@Test
@DisplayName("Test if generator writes objects correctly")
void testGeneratorWriteObject() throws IOException {
Map<String, Object> data = new LinkedHashMap<>();
data.put("hello", "world");
data.put("test", "space test");
data.put("emptyValue", "");
data.put("someNumber", 1234567890);
assertEquals("hello=d29ybGQ*&test=c3BhY2UgdGVzdA**&emptyValue=&someNumber=MTIzNDU2Nzg5MA**", mapper.writeValueAsString(data));
assertEquals("hello=world&test=space+test&emptyValue=&someNumber=1234567890", mapperNoBase64.writeValueAsString(data));
}
@Test
@DisplayName("Test if generator throws exception when writing a nested object")
void testGeneratorThrowsExceptionWhenWritingNestedObject() {
Map<String, Object> data = Map.of("someKey", "someValue", "nestedObjectKey", Map.of("key", "value"));
JsonMappingException exception = assertThrows(JsonMappingException.class, () -> mapper.writeValueAsString(data));
assertEquals("this format does not support nested objects", exception.getOriginalMessage());
}
@Test
@DisplayName("Test if generator throws exception when writing an array")
void testGeneratorThrowsExceptionWhenWritingArray() {
Map<String, Object> data = Map.of("arrayKey", List.of(1, 2, 3, 4));
JsonMappingException exception = assertThrows(JsonMappingException.class, () -> mapper.writeValueAsString(data));
assertEquals("this format does not support arrays", exception.getOriginalMessage());
}
@Test
@DisplayName("Test if parser creates object from input string correctly")
void testParserReadObject() throws IOException {
String inputString = "hello=world&test=space+test&emptyValue=&someNumber=1234567890";
Map<String, Object> data = mapperNoBase64.readValue(inputString, new TypeReference<Map<String, Object>>(){});
assertEquals("world", data.get("hello"));
assertEquals("space test", data.get("test"));
assertEquals("", data.get("emptyValue"));
assertEquals("1234567890", data.get("someNumber"));
// Test Base64
inputString = "hello=d29ybGQ*&test=c3BhY2UgdGVzdA**&emptyValue=&someNumber=MTIzNDU2Nzg5MA**";
assertEquals(data, mapper.readValue(inputString, Map.class));
}
@Test
@DisplayName("Test if parser throws exception when reading an empty key")
void testParserThrowsExceptionWhenReadingEmptyKey() {
String inputString = "someKey=someValue&=emptyKey";
JsonParseException exception = assertThrows(JsonParseException.class, () -> mapperNoBase64.readValue(inputString, Map.class));
assertEquals("Unexpected character ('=' (code 61)): expected field name", exception.getOriginalMessage());
}
@Test
@DisplayName("Test if parser throws exception when field name isn't closed")
void testParserThrowsExceptionWhenFieldNameNotClosed() {
String inputString = "someKey=someValue&notClosed";
JsonParseException exception = assertThrows(JsonParseException.class, () -> mapperNoBase64.readValue(inputString, Map.class));
assertEquals("Unexpected end-of-input in FIELD_NAME", exception.getOriginalMessage());
}
}

View File

@@ -0,0 +1,54 @@
package entralinked.utility;
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
public class ColorUtilityTest {
@Test
@DisplayName("Test if multiplied colors are correct")
void testColorMultiplying() {
assertEquals(0x7F7F7F, ColorUtility.multiplyColor(0xFFFFFF, 0.5));
assertEquals(0xFEFEFE, ColorUtility.multiplyColor(0x7F7F7F, 2.0));
// Test clamping
assertEquals(0xFFFFFF, ColorUtility.multiplyColor(0xFEFEFE, 2.0));
assertEquals(0x000000, ColorUtility.multiplyColor(0x010101, 0.5));
}
@Test
@DisplayName("Test if colors converted from RGB888 to BGR555 are correct")
void testRGB88ToBGR555Converter() {
// Black, white, red, green and blue
assertEquals(0x0000, ColorUtility.convertRGB888ToBGR555(0x000000));
assertEquals(0x7FFF, ColorUtility.convertRGB888ToBGR555(0xFFFFFF));
assertEquals(0x001F, ColorUtility.convertRGB888ToBGR555(0xFF0000));
assertEquals(0x03E0, ColorUtility.convertRGB888ToBGR555(0x00FF00));
assertEquals(0x7C00, ColorUtility.convertRGB888ToBGR555(0x0000FF));
// Random colors
assertEquals(0x07C7, ColorUtility.convertRGB888ToBGR555(0x39F20C));
assertEquals(0x68B3, ColorUtility.convertRGB888ToBGR555(0x9E2BD3));
assertEquals(0x6DFE, ColorUtility.convertRGB888ToBGR555(0xF07CDE));
assertEquals(0x22A4, ColorUtility.convertRGB888ToBGR555(0x26AC44));
}
@Test
@DisplayName("Test if colors converted from BGR555 to RGB888 are correct")
void testBGR555ToRGB888Converter() {
// Black, white, red, green and blue
assertEquals(0x000000, ColorUtility.convertBGR555ToRGB888(0x0000));
assertEquals(0xFFFFFF, ColorUtility.convertBGR555ToRGB888(0xFFFF));
assertEquals(0xFF0000, ColorUtility.convertBGR555ToRGB888(0x001F));
assertEquals(0x00FF00, ColorUtility.convertBGR555ToRGB888(0x03E0));
assertEquals(0x0000FF, ColorUtility.convertBGR555ToRGB888(0x7C00));
// Random colors
assertEquals(0x7BA59C, ColorUtility.convertBGR555ToRGB888(0x4E8F));
assertEquals(0x948CFF, ColorUtility.convertBGR555ToRGB888(0xFE32));
assertEquals(0xBD2121, ColorUtility.convertBGR555ToRGB888(0x1097));
assertEquals(0x5A185A, ColorUtility.convertBGR555ToRGB888(0xAC6B));
}
}

View File

@@ -0,0 +1,34 @@
package entralinked.utility;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
public class LittleEndianStreamTest {
@Test
@DisplayName("Test if LEOutputStream and LEInputStream produce correct values")
void testLittleEndianStreams() throws IOException {
short shortValue = 0x6A84;
int intValue = 0xF827EC80;
long longValue = 0x948EC1AB3F2C88L;
// Test writing
ByteArrayOutputStream byteOutputStream = new ByteArrayOutputStream();
LEOutputStream outputStream = new LEOutputStream(byteOutputStream);
outputStream.writeShort(shortValue);
outputStream.writeInt(intValue);
outputStream.writeLong(longValue);
// Test reading
LEInputStream inputStream = new LEInputStream(new ByteArrayInputStream(byteOutputStream.toByteArray()));
assertEquals(shortValue, inputStream.readShort());
assertEquals(intValue, inputStream.readInt());
assertEquals(longValue, inputStream.readLong());
}
}