mirror of
https://github.com/kuroppoi/entralinked.git
synced 2026-09-08 08:45:14 -05:00
Compare commits
55 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d99608ff11 | ||
|
|
acbb3d7f95 | ||
|
|
abe1b80c2d | ||
|
|
8e614e0d27 | ||
|
|
774794f751 | ||
|
|
9d288bde09 | ||
|
|
18b86a42e0 | ||
|
|
39e0b27cec | ||
|
|
1af975ffb6 | ||
|
|
6f2e4a06a0 | ||
|
|
68bdb7d47f | ||
|
|
163ced2333 | ||
|
|
f3809f4a54 | ||
|
|
0cd628ba63 | ||
|
|
3898bd473c | ||
|
|
0c4cd69741 | ||
|
|
91eb9971f3 | ||
|
|
3224f6312a | ||
|
|
306b9e6db6 | ||
|
|
c0cc952cc2 | ||
|
|
2eb8054ba7 | ||
|
|
d61196fea1 | ||
|
|
8726614fce | ||
|
|
76c5d0e1a5 | ||
|
|
86434b2cb1 | ||
|
|
724e30bfcc | ||
|
|
87a06a4c08 | ||
|
|
e06b83c944 | ||
|
|
b890d27bba | ||
|
|
3771cac70a | ||
|
|
2bdb04b88e | ||
|
|
99240e163a | ||
|
|
72d5a10ffc | ||
|
|
6f1853fc50 | ||
|
|
6999f1f976 | ||
|
|
16d1b99f1f | ||
|
|
229cfaf4ca | ||
|
|
639a487530 | ||
|
|
08d354e814 | ||
|
|
adc0c16c68 | ||
|
|
7fa718af48 | ||
|
|
ff0e541fe8 | ||
|
|
4628fee251 | ||
|
|
a6ed715377 | ||
|
|
4e56d2532f | ||
|
|
eeb6048f54 | ||
|
|
875500d302 | ||
|
|
1990519088 | ||
|
|
eb1605dbe7 | ||
|
|
d232de7307 | ||
|
|
d57c7434a0 | ||
|
|
022ee2f1e2 | ||
|
|
4f8a717ffb | ||
|
|
a479c158db | ||
|
|
9a875cdf77 |
6
.gitattributes
vendored
6
.gitattributes
vendored
@@ -1,6 +1,2 @@
|
|||||||
#
|
|
||||||
# https://help.github.com/articles/dealing-with-line-endings/
|
|
||||||
#
|
|
||||||
# These are explicitly windows files and should use crlf
|
|
||||||
*.bat text eol=crlf
|
*.bat text eol=crlf
|
||||||
|
src/main/resources/dashboard/scripts/pokedata.js linguist-vendored
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
name: Build
|
name: Build
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
pull_request:
|
||||||
push:
|
push:
|
||||||
branches: [ master ]
|
branches: [ master ]
|
||||||
paths:
|
paths:
|
||||||
- poke-sprites-v
|
- poke-sprites-v
|
||||||
- src/**
|
- src/**
|
||||||
- build.gradle
|
- build.gradle
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
dist:
|
dist:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
21
.github/workflows/dist-pull-request.yml
vendored
21
.github/workflows/dist-pull-request.yml
vendored
@@ -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
|
|
||||||
17
README.md
17
README.md
@@ -1,11 +1,21 @@
|
|||||||
# Entralinked
|
<p align="center">
|
||||||
[](https://github.com/kuroppoi/entralinked/actions)
|
<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.\
|
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\
|
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.\
|
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.
|
It can also be used to Memory Link with a Black or White save file if you don't have a second DS system.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
For users: [Quick Setup Guide](https://github.com/kuroppoi/entralinked/wiki/Setup)
|
||||||
|
|
||||||
## Building
|
## Building
|
||||||
|
|
||||||
#### Prerequisites
|
#### Prerequisites
|
||||||
@@ -28,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.\
|
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.\
|
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`.\
|
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 Nintendo 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.
|
||||||
After tucking in a Pokémon, navigate to `http://localhost/dashboard/profile.html` in a web browser to configure Game Sync settings.
|
|
||||||
|
|||||||
@@ -24,9 +24,12 @@ dependencies {
|
|||||||
implementation 'org.apache.logging.log4j:log4j-api:2.20.0'
|
implementation 'org.apache.logging.log4j:log4j-api:2.20.0'
|
||||||
implementation 'com.fasterxml.jackson.core:jackson-databind:2.15.2'
|
implementation 'com.fasterxml.jackson.core:jackson-databind:2.15.2'
|
||||||
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.15.2'
|
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.15.2'
|
||||||
implementation 'io.netty:netty-all:4.1.79.Final'
|
implementation 'io.netty:netty-all:4.1.79.Final'
|
||||||
implementation 'io.javalin:javalin:5.5.0'
|
implementation 'io.javalin:javalin:5.5.0'
|
||||||
implementation 'org.apache.logging.log4j:log4j-slf4j2-impl:2.20.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'
|
||||||
}
|
}
|
||||||
|
|
||||||
sourceSets {
|
sourceSets {
|
||||||
|
|||||||
BIN
images/icon.png
Normal file
BIN
images/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.0 KiB |
BIN
images/preview-2x.gif
Normal file
BIN
images/preview-2x.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 356 KiB |
BIN
images/preview.gif
Normal file
BIN
images/preview.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 210 KiB |
Submodule poke-sprites-v updated: 04c697187a...9eebf41346
@@ -1,14 +1,14 @@
|
|||||||
package entralinked;
|
package entralinked;
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
|
||||||
|
|
||||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
public record Configuration(
|
public record Configuration(
|
||||||
@JsonProperty(required = true) String hostName,
|
String hostName,
|
||||||
@JsonProperty(required = true) boolean clearPlayerDreamInfoOnWake,
|
boolean clearPlayerDreamInfoOnWake,
|
||||||
@JsonProperty(required = true) boolean allowOverwritingPlayerDreamInfo,
|
boolean allowOverwritingPlayerDreamInfo,
|
||||||
@JsonProperty(required = true) boolean allowWfcRegistrationThroughLogin) {
|
boolean allowPlayerGameVersionMismatch,
|
||||||
|
boolean allowWfcRegistrationThroughLogin) {
|
||||||
|
|
||||||
public static final Configuration DEFAULT = new Configuration("local", true, false, true);
|
public static final Configuration DEFAULT = new Configuration("local", true, false, false, true);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,8 +43,11 @@ public class Entralinked {
|
|||||||
private final GameSpyServer gameSpyServer;
|
private final GameSpyServer gameSpyServer;
|
||||||
private final HttpServer httpServer;
|
private final HttpServer httpServer;
|
||||||
private MainView mainView;
|
private MainView mainView;
|
||||||
|
private boolean initialized;
|
||||||
|
|
||||||
public Entralinked(String[] args) {
|
public Entralinked(String[] args) {
|
||||||
|
long beginTime = System.currentTimeMillis();
|
||||||
|
|
||||||
// Read command line arguments
|
// Read command line arguments
|
||||||
CommandLineArguments arguments = new CommandLineArguments(args);
|
CommandLineArguments arguments = new CommandLineArguments(args);
|
||||||
|
|
||||||
@@ -92,16 +95,11 @@ public class Entralinked {
|
|||||||
userManager = new UserManager();
|
userManager = new UserManager();
|
||||||
playerManager = new PlayerManager();
|
playerManager = new PlayerManager();
|
||||||
|
|
||||||
// Start servers
|
|
||||||
boolean started = true;
|
|
||||||
|
|
||||||
// Create DNS server
|
// Create DNS server
|
||||||
dnsServer = new DnsServer(hostAddress);
|
dnsServer = new DnsServer(hostAddress);
|
||||||
started &= dnsServer.start();
|
|
||||||
|
|
||||||
// Create GameSpy server
|
// Create GameSpy server
|
||||||
gameSpyServer = new GameSpyServer(this);
|
gameSpyServer = new GameSpyServer(this);
|
||||||
started &= gameSpyServer.start();
|
|
||||||
|
|
||||||
// Create HTTP server
|
// Create HTTP server
|
||||||
httpServer = new HttpServer(this);
|
httpServer = new HttpServer(this);
|
||||||
@@ -109,25 +107,42 @@ public class Entralinked {
|
|||||||
httpServer.addHandler(new PglHandler(this));
|
httpServer.addHandler(new PglHandler(this));
|
||||||
httpServer.addHandler(new DlsHandler(this));
|
httpServer.addHandler(new DlsHandler(this));
|
||||||
httpServer.addHandler(new DashboardHandler(this));
|
httpServer.addHandler(new DashboardHandler(this));
|
||||||
started &= httpServer.start();
|
|
||||||
|
|
||||||
// Handle post-startup GUI stuff
|
// Start servers
|
||||||
if(mainView != null) {
|
boolean started = startServers();
|
||||||
if(!started) {
|
|
||||||
SwingUtilities.invokeLater(() -> mainView.setStatusLabelText(
|
|
||||||
"ERROR: One or more servers failed to start! Please check the logs for info."));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// Post-startup
|
||||||
|
if(started) {
|
||||||
String hostIpAddress = hostAddress.getHostAddress();
|
String hostIpAddress = hostAddress.getHostAddress();
|
||||||
SwingUtilities.invokeLater(() -> {
|
logger.info("Startup complete! Took a total of {} milliseconds", System.currentTimeMillis() - beginTime);
|
||||||
mainView.setDashboardButtonEnabled(true);
|
logger.info("Configure your DS to use the following DNS server: {}", hostIpAddress);
|
||||||
mainView.setStatusLabelText("Configure your DS to use the following DNS server: %s".formatted(hostIpAddress));
|
|
||||||
});
|
if(mainView != null) {
|
||||||
|
SwingUtilities.invokeLater(() -> {
|
||||||
|
mainView.setDashboardButtonEnabled(true);
|
||||||
|
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() {
|
public void stopServers() {
|
||||||
|
logger.info("Stopping servers ...");
|
||||||
|
|
||||||
if(httpServer != null) {
|
if(httpServer != null) {
|
||||||
httpServer.stop();
|
httpServer.stop();
|
||||||
}
|
}
|
||||||
@@ -143,21 +158,25 @@ public class Entralinked {
|
|||||||
|
|
||||||
private Configuration loadConfigFile() {
|
private Configuration loadConfigFile() {
|
||||||
logger.info("Loading configuration ...");
|
logger.info("Loading configuration ...");
|
||||||
|
Configuration configuration = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
File configFile = new File("config.json");
|
File configFile = new File("config.json");
|
||||||
|
|
||||||
if(!configFile.exists()) {
|
if(!configFile.exists()) {
|
||||||
logger.info("No configuration file exists - default configuration will be used");
|
logger.info("No configuration file exists - default configuration will be used");
|
||||||
mapper.writeValue(configFile, Configuration.DEFAULT);
|
configuration = Configuration.DEFAULT;
|
||||||
return Configuration.DEFAULT;
|
|
||||||
} else {
|
} else {
|
||||||
return mapper.readValue(configFile, Configuration.class);
|
configuration = mapper.readValue(configFile, Configuration.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mapper.writeValue(configFile, configuration);
|
||||||
} catch(IOException e) {
|
} catch(IOException e) {
|
||||||
logger.error("Could not load configuration - default configuration will be used", e);
|
logger.error("Could not load configuration - default configuration will be used", e);
|
||||||
return Configuration.DEFAULT;
|
configuration = Configuration.DEFAULT;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return configuration;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Configuration getConfiguration() {
|
public Configuration getConfiguration() {
|
||||||
@@ -175,4 +194,8 @@ public class Entralinked {
|
|||||||
public PlayerManager getPlayerManager() {
|
public PlayerManager getPlayerManager() {
|
||||||
return playerManager;
|
return playerManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isInitialized() {
|
||||||
|
return initialized;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package entralinked.gui;
|
package entralinked.gui;
|
||||||
|
|
||||||
import java.awt.BorderLayout;
|
import java.awt.BorderLayout;
|
||||||
|
import java.awt.Color;
|
||||||
import java.awt.Desktop;
|
import java.awt.Desktop;
|
||||||
import java.awt.Dimension;
|
import java.awt.Dimension;
|
||||||
import java.awt.Font;
|
import java.awt.Font;
|
||||||
@@ -9,16 +10,19 @@ import java.awt.event.WindowEvent;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.URISyntaxException;
|
import java.net.URISyntaxException;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
|
import java.util.List;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
|
||||||
|
import javax.swing.ImageIcon;
|
||||||
import javax.swing.JButton;
|
import javax.swing.JButton;
|
||||||
import javax.swing.JFrame;
|
import javax.swing.JFrame;
|
||||||
import javax.swing.JLabel;
|
import javax.swing.JLabel;
|
||||||
|
import javax.swing.JMenu;
|
||||||
|
import javax.swing.JMenuBar;
|
||||||
import javax.swing.JPanel;
|
import javax.swing.JPanel;
|
||||||
import javax.swing.JScrollPane;
|
import javax.swing.JScrollPane;
|
||||||
import javax.swing.JTextPane;
|
import javax.swing.JTextPane;
|
||||||
import javax.swing.UIManager;
|
import javax.swing.UIManager;
|
||||||
import javax.swing.UnsupportedLookAndFeelException;
|
|
||||||
import javax.swing.text.AttributeSet;
|
import javax.swing.text.AttributeSet;
|
||||||
import javax.swing.text.BadLocationException;
|
import javax.swing.text.BadLocationException;
|
||||||
import javax.swing.text.DefaultCaret;
|
import javax.swing.text.DefaultCaret;
|
||||||
@@ -27,30 +31,31 @@ import javax.swing.text.SimpleAttributeSet;
|
|||||||
import javax.swing.text.StyleConstants;
|
import javax.swing.text.StyleConstants;
|
||||||
import javax.swing.text.StyleContext;
|
import javax.swing.text.StyleContext;
|
||||||
|
|
||||||
import org.apache.logging.log4j.LogManager;
|
import org.apache.logging.log4j.Level;
|
||||||
import org.apache.logging.log4j.Logger;
|
|
||||||
|
import com.formdev.flatlaf.intellijthemes.FlatOneDarkIJTheme;
|
||||||
|
|
||||||
import entralinked.Entralinked;
|
import entralinked.Entralinked;
|
||||||
import entralinked.utility.ConsumerAppender;
|
import entralinked.utility.ConsumerAppender;
|
||||||
|
import entralinked.utility.SwingUtility;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Simple Swing user interface.
|
* Simple Swing user interface.
|
||||||
*/
|
*/
|
||||||
public class MainView {
|
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 StyleContext styleContext = StyleContext.getDefaultStyleContext();
|
||||||
private final AttributeSet fontAttribute = styleContext.addAttribute(SimpleAttributeSet.EMPTY, StyleConstants.FontFamily, "Consolas");
|
private final AttributeSet fontAttribute = styleContext.addAttribute(SimpleAttributeSet.EMPTY, StyleConstants.FontFamily, "Consolas");
|
||||||
private final JButton dashboardButton;
|
private final JButton dashboardButton;
|
||||||
private final JLabel statusLabel;
|
private final JLabel statusLabel;
|
||||||
|
|
||||||
public MainView(Entralinked entralinked) {
|
public MainView(Entralinked entralinked) {
|
||||||
// Try set Look and Feel
|
// Set look and feel
|
||||||
try {
|
FlatOneDarkIJTheme.setup();
|
||||||
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
|
UIManager.getDefaults().put("Component.focusedBorderColor", UIManager.get("Component.borderColor"));
|
||||||
} catch (ReflectiveOperationException | UnsupportedLookAndFeelException e) {
|
|
||||||
logger.error("Could not set Look and Feel", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create dashboard button
|
// Create dashboard button
|
||||||
dashboardButton = new JButton("Open User Dashboard");
|
dashboardButton = new JButton("Open User Dashboard");
|
||||||
@@ -87,8 +92,12 @@ public class MainView {
|
|||||||
// Create console output appender
|
// Create console output appender
|
||||||
ConsumerAppender.addConsumer("GuiOutput", message -> {
|
ConsumerAppender.addConsumer("GuiOutput", message -> {
|
||||||
Document document = consoleOutputPane.getDocument();
|
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 {
|
try {
|
||||||
consoleOutputPane.getDocument().insertString(document.getLength(), message, fontAttribute);
|
consoleOutputPane.getDocument().insertString(document.getLength(), message.formattedMessage(), colorAttribute);
|
||||||
} catch(BadLocationException e) {}
|
} catch(BadLocationException e) {}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -104,6 +113,15 @@ public class MainView {
|
|||||||
|
|
||||||
// Create window
|
// Create window
|
||||||
JFrame frame = new JFrame("Entralinked");
|
JFrame frame = new JFrame("Entralinked");
|
||||||
|
|
||||||
|
// Create menu bar
|
||||||
|
JMenuBar menuBar = new JMenuBar();
|
||||||
|
JMenu helpMenu = new JMenu("Help");
|
||||||
|
helpMenu.add(SwingUtility.createAction("Update PID (Error 60000)", () -> new PidToolDialog(entralinked, frame)));
|
||||||
|
helpMenu.add(SwingUtility.createAction("GitHub", () -> openUrl("https://github.com/kuroppoi/entralinked")));
|
||||||
|
menuBar.add(helpMenu);
|
||||||
|
|
||||||
|
// Set window properties
|
||||||
frame.addWindowListener(new WindowAdapter() {
|
frame.addWindowListener(new WindowAdapter() {
|
||||||
@Override
|
@Override
|
||||||
public void windowClosing(WindowEvent event) {
|
public void windowClosing(WindowEvent event) {
|
||||||
@@ -119,8 +137,13 @@ 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.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
|
||||||
frame.setMinimumSize(new Dimension(512, 288));
|
frame.setMinimumSize(new Dimension(512, 288));
|
||||||
|
frame.setJMenuBar(menuBar);
|
||||||
frame.add(panel);
|
frame.add(panel);
|
||||||
frame.pack();
|
frame.pack();
|
||||||
frame.setLocationRelativeTo(null);
|
frame.setLocationRelativeTo(null);
|
||||||
@@ -131,7 +154,6 @@ public class MainView {
|
|||||||
dashboardButton.setEnabled(enabled);
|
dashboardButton.setEnabled(enabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public void setStatusLabelText(String text) {
|
public void setStatusLabelText(String text) {
|
||||||
statusLabel.setText(text);
|
statusLabel.setText(text);
|
||||||
}
|
}
|
||||||
|
|||||||
116
src/main/java/entralinked/gui/PidToolDialog.java
Normal file
116
src/main/java/entralinked/gui/PidToolDialog.java
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
package entralinked.gui;
|
||||||
|
|
||||||
|
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.", "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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,5 +3,6 @@ package entralinked.model.dlc;
|
|||||||
/**
|
/**
|
||||||
* Simple record for DLC data.
|
* Simple record for DLC data.
|
||||||
*/
|
*/
|
||||||
|
@Deprecated
|
||||||
public record Dlc(String path, String name, String gameCode, String type,
|
public record Dlc(String path, String name, String gameCode, String type,
|
||||||
int index, int projectedSize, int checksum, boolean checksumEmbedded) {}
|
int index, int projectedSize, int checksum, boolean checksumEmbedded) {}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import java.io.IOException;
|
|||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.io.InputStreamReader;
|
import java.io.InputStreamReader;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.StandardCopyOption;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
@@ -17,7 +18,9 @@ import org.apache.logging.log4j.LogManager;
|
|||||||
import org.apache.logging.log4j.Logger;
|
import org.apache.logging.log4j.Logger;
|
||||||
|
|
||||||
import entralinked.utility.Crc16;
|
import entralinked.utility.Crc16;
|
||||||
|
import entralinked.utility.MD5;
|
||||||
|
|
||||||
|
@Deprecated
|
||||||
public class DlcList {
|
public class DlcList {
|
||||||
|
|
||||||
private static final Logger logger = LogManager.getLogger();
|
private static final Logger logger = LogManager.getLogger();
|
||||||
@@ -27,31 +30,32 @@ public class DlcList {
|
|||||||
public DlcList() {
|
public DlcList() {
|
||||||
logger.info("Loading DLC ...");
|
logger.info("Loading DLC ...");
|
||||||
|
|
||||||
// Extract defaults if external DLC directory is not present
|
try(BufferedReader reader = new BufferedReader(new InputStreamReader(getClass().getResourceAsStream("/dlc.paths")))) {
|
||||||
if(!dataDirectory.exists()) {
|
reader.lines().forEach(line -> {
|
||||||
logger.info("Extracting default DLC files ...");
|
String[] segments = line.split("\t");
|
||||||
BufferedReader reader = new BufferedReader(new InputStreamReader(getClass().getResourceAsStream("/dlc.paths")));
|
|
||||||
String line = null;
|
|
||||||
|
|
||||||
try {
|
if(segments.length != 2) {
|
||||||
while((line = reader.readLine()) != null) {
|
return;
|
||||||
InputStream resource = getClass().getResourceAsStream(line);
|
|
||||||
File outputFile = new File("./%s".formatted(line));
|
|
||||||
|
|
||||||
// Create parent directories
|
|
||||||
if(outputFile.getParentFile() != null) {
|
|
||||||
outputFile.getParentFile().mkdirs();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Copy resource to destination
|
|
||||||
if(resource != null) {
|
|
||||||
Files.copy(resource, outputFile.toPath());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (IOException e) {
|
|
||||||
logger.error("Could not extract default DLC files", e);
|
String path = segments[0];
|
||||||
return;
|
String hash = segments[1];
|
||||||
}
|
File outputFile = new File("./%s".formatted(path));
|
||||||
|
|
||||||
|
if(outputFile.getParentFile() != null) {
|
||||||
|
outputFile.getParentFile().mkdirs();
|
||||||
|
}
|
||||||
|
|
||||||
|
try(InputStream inputStream = getClass().getResourceAsStream(path)){
|
||||||
|
if(!outputFile.exists() || !hash.equals(MD5.digest(outputFile))) {
|
||||||
|
Files.copy(inputStream, outputFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
|
||||||
|
}
|
||||||
|
} catch(IOException e) {
|
||||||
|
logger.error("Couldn't process resource '{}'", path, e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch(IOException e) {
|
||||||
|
logger.error("Couldn't extract DLC data", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Just to be sure...
|
// Just to be sure...
|
||||||
@@ -71,7 +75,7 @@ public class DlcList {
|
|||||||
for(File subFile : file.listFiles()) {
|
for(File subFile : file.listFiles()) {
|
||||||
// Check if file is directory
|
// Check if file is directory
|
||||||
if(!subFile.isDirectory()) {
|
if(!subFile.isDirectory()) {
|
||||||
logger.warn("Non-directory '{}' in DLC subfolder '{}'", file.getName(), subFile.getName());
|
logger.warn("Non-directory '{}' in DLC subfolder '{}'", subFile.getName(), file.getName());
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,6 +83,15 @@ public class DlcList {
|
|||||||
|
|
||||||
// DLC Content level
|
// DLC Content level
|
||||||
for(File dlcFile : subFile.listFiles()) {
|
for(File dlcFile : subFile.listFiles()) {
|
||||||
|
String name = dlcFile.getName();
|
||||||
|
|
||||||
|
// Check if DLC name is reserved as an internal identifier
|
||||||
|
if(name.equals("none") || name.equals("custom")) {
|
||||||
|
logger.warn("DLC '{}/{}/{}' could not be loaded because it uses a reserved name.",
|
||||||
|
file.getName(), subFile.getName(), name);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// Load DLC data
|
// Load DLC data
|
||||||
Dlc dlc = loadDlcFile(file.getName(), subFile.getName(), index, dlcFile);
|
Dlc dlc = loadDlcFile(file.getName(), subFile.getName(), index, dlcFile);
|
||||||
|
|
||||||
|
|||||||
@@ -7,17 +7,18 @@ import com.fasterxml.jackson.annotation.JsonProperty;
|
|||||||
* Record containing information about a Pokémon
|
* Record containing information about a Pokémon
|
||||||
*/
|
*/
|
||||||
public record PkmnInfo(
|
public record PkmnInfo(
|
||||||
@JsonProperty(required = true) int personality,
|
@JsonProperty(required = true) String nickname,
|
||||||
@JsonProperty(required = true) int species,
|
@JsonProperty(required = true) String trainerName,
|
||||||
@JsonProperty(required = true) int heldItem,
|
@JsonProperty(required = true) PkmnNature nature,
|
||||||
@JsonProperty(required = true) int trainerId,
|
@JsonProperty(required = true) PkmnGender gender,
|
||||||
@JsonProperty(required = true) int trainerSecretId,
|
@JsonProperty(required = true) int species,
|
||||||
@JsonProperty(required = true) int level,
|
@JsonProperty(required = true) int personality,
|
||||||
@JsonProperty(required = true) int form,
|
@JsonProperty(required = true) int trainerId,
|
||||||
@JsonProperty(required = true) PkmnNature nature,
|
@JsonProperty(required = true) int trainerSecretId,
|
||||||
@JsonProperty(required = true) PkmnGender gender,
|
@JsonProperty(required = true) int level,
|
||||||
@JsonProperty(required = true) String nickname,
|
@JsonProperty(required = false) int form,
|
||||||
@JsonProperty(required = true) String trainerName) {
|
@JsonProperty(required = false) int ability,
|
||||||
|
@JsonProperty(required = false) int heldItem) {
|
||||||
|
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
public boolean isShiny() {
|
public boolean isShiny() {
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ public class PkmnInfoReader {
|
|||||||
|
|
||||||
// Read Pokémon data
|
// Read Pokémon data
|
||||||
int species = buffer.getShortLE(8);
|
int species = buffer.getShortLE(8);
|
||||||
int item = buffer.getShortLE(10) & 0xFFFF;
|
int heldItem = buffer.getShortLE(10) & 0xFFFF;
|
||||||
int trainerId = buffer.getShortLE(12) & 0xFFFF;
|
int trainerId = buffer.getShortLE(12) & 0xFFFF;
|
||||||
int trainerSecretId = buffer.getShortLE(14) & 0xFFFF;
|
int trainerSecretId = buffer.getShortLE(14) & 0xFFFF;
|
||||||
int level = buffer.getByte(140);
|
int level = buffer.getByte(140);
|
||||||
@@ -78,14 +78,14 @@ public class PkmnInfoReader {
|
|||||||
|
|
||||||
// Loosely verify data
|
// Loosely verify data
|
||||||
if(species < 1 || species > 649) throw new IOException("Invalid species");
|
if(species < 1 || species > 649) throw new IOException("Invalid species");
|
||||||
if(item < 0 || item > 638) throw new IOException("Invalid held item");
|
if(heldItem < 0 || heldItem > 638) throw new IOException("Invalid held item");
|
||||||
if(ability < 1 || ability > 164) throw new IOException("Invalid ability");
|
if(ability < 1 || ability > 164) throw new IOException("Invalid ability");
|
||||||
if(level < 1 || level > 100) throw new IOException("Level is out of range");
|
if(level < 1 || level > 100) throw new IOException("Level is out of range");
|
||||||
if(nature == null) throw new IOException("Invalid nature");
|
if(nature == null) throw new IOException("Invalid nature");
|
||||||
|
|
||||||
// Create record
|
// Create record
|
||||||
PkmnInfo info = new PkmnInfo(personality, species, item, trainerId, trainerSecretId, level, form, nature, gender, nickname, trainerName);
|
return new PkmnInfo(nickname, trainerName, nature, gender, species, personality,
|
||||||
return info;
|
trainerId, trainerSecretId, level, form, ability, heldItem);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void decryptData(ByteBuf buffer, int offset, int length, int seed) throws IOException {
|
private static void decryptData(ByteBuf buffer, int offset, int length, int seed) throws IOException {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package entralinked.model.player;
|
package entralinked.model.player;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
@@ -22,6 +23,9 @@ public class Player {
|
|||||||
private String cgearSkin;
|
private String cgearSkin;
|
||||||
private String dexSkin;
|
private String dexSkin;
|
||||||
private String musical;
|
private String musical;
|
||||||
|
private String customCGearSkin;
|
||||||
|
private String customDexSkin;
|
||||||
|
private File dataDirectory;
|
||||||
|
|
||||||
public Player(String gameSyncId) {
|
public Player(String gameSyncId) {
|
||||||
this.gameSyncId = gameSyncId;
|
this.gameSyncId = gameSyncId;
|
||||||
@@ -29,7 +33,6 @@ public class Player {
|
|||||||
|
|
||||||
public void resetDreamInfo() {
|
public void resetDreamInfo() {
|
||||||
status = PlayerStatus.AWAKE;
|
status = PlayerStatus.AWAKE;
|
||||||
gameVersion = null;
|
|
||||||
dreamerInfo = null;
|
dreamerInfo = null;
|
||||||
encounters.clear();
|
encounters.clear();
|
||||||
items.clear();
|
items.clear();
|
||||||
@@ -132,4 +135,46 @@ public class Player {
|
|||||||
public String getMusical() {
|
public String getMusical() {
|
||||||
return musical;
|
return musical;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setCustomCGearSkin(String customCGearSkin) {
|
||||||
|
this.customCGearSkin = customCGearSkin;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getCustomCGearSkin() {
|
||||||
|
return customCGearSkin;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCustomDexSkin(String customDexSkin) {
|
||||||
|
this.customDexSkin = customDexSkin;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getCustomDexSkin() {
|
||||||
|
return customDexSkin;
|
||||||
|
}
|
||||||
|
|
||||||
|
// IO stuff
|
||||||
|
|
||||||
|
public void setDataDirectory(File dataDirectory) {
|
||||||
|
this.dataDirectory = dataDirectory;
|
||||||
|
}
|
||||||
|
|
||||||
|
public File getDataDirectory() {
|
||||||
|
return dataDirectory;
|
||||||
|
}
|
||||||
|
|
||||||
|
public File getDataFile() {
|
||||||
|
return new File(dataDirectory, "data.json");
|
||||||
|
}
|
||||||
|
|
||||||
|
public File getSaveFile() {
|
||||||
|
return new File(dataDirectory, "save.bin");
|
||||||
|
}
|
||||||
|
|
||||||
|
public File getCGearSkinFile() {
|
||||||
|
return new File(dataDirectory, "cgear.bin");
|
||||||
|
}
|
||||||
|
|
||||||
|
public File getDexSkinFile() {
|
||||||
|
return new File(dataDirectory, "zukan.bin");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ public record PlayerDto(
|
|||||||
String cgearSkin,
|
String cgearSkin,
|
||||||
String dexSkin,
|
String dexSkin,
|
||||||
String musical,
|
String musical,
|
||||||
|
String customCGearSkin,
|
||||||
|
String customDexSkin,
|
||||||
int levelsGained,
|
int levelsGained,
|
||||||
@JsonDeserialize(contentAs = DreamEncounter.class) Collection<DreamEncounter> encounters,
|
@JsonDeserialize(contentAs = DreamEncounter.class) Collection<DreamEncounter> encounters,
|
||||||
@JsonDeserialize(contentAs = DreamItem.class) Collection<DreamItem> items,
|
@JsonDeserialize(contentAs = DreamItem.class) Collection<DreamItem> items,
|
||||||
@@ -28,8 +30,9 @@ public record PlayerDto(
|
|||||||
|
|
||||||
public PlayerDto(Player player) {
|
public PlayerDto(Player player) {
|
||||||
this(player.getGameSyncId(), player.getGameVersion(), player.getStatus(), player.getDreamerInfo(),
|
this(player.getGameSyncId(), player.getGameVersion(), player.getStatus(), player.getDreamerInfo(),
|
||||||
player.getCGearSkin(), player.getDexSkin(), player.getMusical(), player.getLevelsGained(),
|
player.getCGearSkin(), player.getDexSkin(), player.getMusical(), player.getCustomCGearSkin(),
|
||||||
player.getEncounters(), player.getItems(),player.getAvenueVisitors());
|
player.getCustomDexSkin(), player.getLevelsGained(), player.getEncounters(), player.getItems(),
|
||||||
|
player.getAvenueVisitors());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -43,6 +46,8 @@ public record PlayerDto(
|
|||||||
player.setCGearSkin(cgearSkin);
|
player.setCGearSkin(cgearSkin);
|
||||||
player.setDexSkin(dexSkin);
|
player.setDexSkin(dexSkin);
|
||||||
player.setMusical(musical);
|
player.setMusical(musical);
|
||||||
|
player.setCustomCGearSkin(customCGearSkin);
|
||||||
|
player.setCustomDexSkin(customDexSkin);
|
||||||
player.setLevelsGained(levelsGained);
|
player.setLevelsGained(levelsGained);
|
||||||
player.setEncounters(encounters == null ? Collections.emptyList() : encounters);
|
player.setEncounters(encounters == null ? Collections.emptyList() : encounters);
|
||||||
player.setItems(items == null ? Collections.emptyList() : items);
|
player.setItems(items == null ? Collections.emptyList() : items);
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import java.io.FileOutputStream;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.StandardCopyOption;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@@ -16,6 +17,8 @@ import org.apache.logging.log4j.Logger;
|
|||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import com.fasterxml.jackson.databind.SerializationFeature;
|
import com.fasterxml.jackson.databind.SerializationFeature;
|
||||||
|
|
||||||
|
import entralinked.GameVersion;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Manager class for managing {@link Player} information (Global Link users)
|
* Manager class for managing {@link Player} information (Global Link users)
|
||||||
*/
|
*/
|
||||||
@@ -25,7 +28,6 @@ public class PlayerManager {
|
|||||||
private final ObjectMapper mapper = new ObjectMapper().enable(SerializationFeature.INDENT_OUTPUT);
|
private final ObjectMapper mapper = new ObjectMapper().enable(SerializationFeature.INDENT_OUTPUT);
|
||||||
private final Map<String, Player> playerMap = new ConcurrentHashMap<>();
|
private final Map<String, Player> playerMap = new ConcurrentHashMap<>();
|
||||||
private final File dataDirectory = new File("players");
|
private final File dataDirectory = new File("players");
|
||||||
private final File gameSaveDirectory = new File(dataDirectory, "savedata");
|
|
||||||
|
|
||||||
public PlayerManager() {
|
public PlayerManager() {
|
||||||
logger.info("Loading player data ...");
|
logger.info("Loading player data ...");
|
||||||
@@ -35,16 +37,76 @@ public class PlayerManager {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Migrate player data if necessary
|
||||||
|
migrateLegacyData();
|
||||||
|
|
||||||
// Load player data
|
// Load player data
|
||||||
for(File file : dataDirectory.listFiles()) {
|
for(File file : dataDirectory.listFiles()) {
|
||||||
if(!file.isDirectory()) {
|
if(file.isDirectory()) {
|
||||||
loadPlayer(file);
|
loadPlayer(new File(file, "data.json"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info("Loaded {} player(s)", playerMap.size());
|
logger.info("Loaded {} player(s)", playerMap.size());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Migrates player data from the old storage format.
|
||||||
|
*/
|
||||||
|
private void migrateLegacyData() {
|
||||||
|
// Migrate player configuration files
|
||||||
|
for(File file : dataDirectory.listFiles()) {
|
||||||
|
if(!file.isDirectory()) {
|
||||||
|
try {
|
||||||
|
PlayerDto player = mapper.readValue(file, PlayerDto.class);
|
||||||
|
File outputDirectory = new File(dataDirectory, player.gameSyncId());
|
||||||
|
outputDirectory.mkdirs();
|
||||||
|
Files.copy(file.toPath(), new File(outputDirectory, "data.json").toPath(), StandardCopyOption.REPLACE_EXISTING);
|
||||||
|
file.delete();
|
||||||
|
} catch(IOException e) {
|
||||||
|
logger.error("Could not migrate player data {}", file.getName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Migrate game save data files
|
||||||
|
File gameSaveDirectory = new File(dataDirectory, "savedata");
|
||||||
|
|
||||||
|
if(gameSaveDirectory.exists()) {
|
||||||
|
for(File file : gameSaveDirectory.listFiles()) {
|
||||||
|
if(!file.isDirectory()) {
|
||||||
|
String name = file.getName();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Skip if game save data file has an invalid name
|
||||||
|
if(!name.matches("^SAV-[A-HJ-NP-Z2-9]{10}.bin$")) {
|
||||||
|
logger.warn("Game save data {} will not be migrated because the file name is invalid", name);
|
||||||
|
file.delete();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
String gameSyncId = name.split("-")[1].replace(".bin", "");
|
||||||
|
File outputDirectory = new File(dataDirectory, gameSyncId);
|
||||||
|
|
||||||
|
// Skip if game save data does not belong to any existing Game Sync data
|
||||||
|
if(!outputDirectory.isDirectory()) {
|
||||||
|
logger.warn("Game save data {} will not be migrated because no Game Sync data exists for it", name);
|
||||||
|
file.delete();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Files.copy(file.toPath(), new File(outputDirectory, "save.bin").toPath(), StandardCopyOption.REPLACE_EXISTING);
|
||||||
|
file.delete();
|
||||||
|
} catch(IOException e) {
|
||||||
|
logger.error("Could not migrate game save data {}", name, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
gameSaveDirectory.delete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Loads a {@link Player} from the specified input file.
|
* Loads a {@link Player} from the specified input file.
|
||||||
* The loaded player instance is automatically mapped, unless it has an already-mapped Game Sync ID.
|
* The loaded player instance is automatically mapped, unless it has an already-mapped Game Sync ID.
|
||||||
@@ -60,6 +122,7 @@ public class PlayerManager {
|
|||||||
throw new IOException("Duplicate Game Sync ID %s".formatted(gameSyncId));
|
throw new IOException("Duplicate Game Sync ID %s".formatted(gameSyncId));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
player.setDataDirectory(inputFile.getParentFile());
|
||||||
playerMap.put(gameSyncId, player);
|
playerMap.put(gameSyncId, player);
|
||||||
} catch(IOException e) {
|
} catch(IOException e) {
|
||||||
logger.error("Could not load player data at {}", inputFile.getAbsolutePath(), e);
|
logger.error("Could not load player data at {}", inputFile.getAbsolutePath(), e);
|
||||||
@@ -75,12 +138,9 @@ public class PlayerManager {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Saves the data of the specified player to disk, and returns {@code true} if it succeeds.
|
* Saves the data of the specified player to disk, and returns {@code true} if it succeeds.
|
||||||
* The output file is generated as follows:
|
|
||||||
*
|
|
||||||
* {@code new File(dataDirectory, "PGL-%s".formatted(gameSyncId))}
|
|
||||||
*/
|
*/
|
||||||
public boolean savePlayer(Player player) {
|
public boolean savePlayer(Player player) {
|
||||||
return savePlayer(player, new File(dataDirectory, "PGL-%s.json".formatted(player.getGameSyncId())));
|
return savePlayer(player, player.getDataFile());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -117,16 +177,26 @@ public class PlayerManager {
|
|||||||
* That is, the specified Game Sync ID wasn't already registered and the player data
|
* That is, the specified Game Sync ID wasn't already registered and the player data
|
||||||
* was saved without any errors.
|
* was saved without any errors.
|
||||||
*/
|
*/
|
||||||
public Player registerPlayer(String gameSyncId) {
|
public Player registerPlayer(String gameSyncId, GameVersion version) {
|
||||||
// Check for duplicate Game Sync ID
|
// Check for duplicate Game Sync ID
|
||||||
if(playerMap.containsKey(gameSyncId)) {
|
if(playerMap.containsKey(gameSyncId)) {
|
||||||
logger.warn("Attempted to register duplicate player {}", gameSyncId);
|
logger.warn("Attempted to register duplicate player {}", gameSyncId);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create data directory
|
||||||
|
File playerDataDirectory = new File(dataDirectory, gameSyncId);
|
||||||
|
|
||||||
|
if(playerDataDirectory.exists()) {
|
||||||
|
logger.warn("Can't register player {} because the data directory already exists!", gameSyncId);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
// Construct player object
|
// Construct player object
|
||||||
Player player = new Player(gameSyncId);
|
Player player = new Player(gameSyncId);
|
||||||
player.setStatus(PlayerStatus.AWAKE);
|
player.setStatus(PlayerStatus.AWAKE);
|
||||||
|
player.setGameVersion(version);
|
||||||
|
player.setDataDirectory(playerDataDirectory);
|
||||||
|
|
||||||
// Try to save player data
|
// Try to save player data
|
||||||
if(!savePlayer(player)) {
|
if(!savePlayer(player)) {
|
||||||
@@ -140,20 +210,11 @@ public class PlayerManager {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Stores a game save file read from the specified input stream to a file.
|
* Stores a game save file read from the specified input stream to a file.
|
||||||
* The output file is generated as follows:
|
|
||||||
*
|
|
||||||
* {@code new File(gameSaveDirectory, "SAV-%s.bin".formatted(gameSyncId))}
|
|
||||||
*
|
*
|
||||||
* @return {@code true} if the game save file was written successfully, otherwise {@code false}.
|
* @return {@code true} if the game save file was written successfully, otherwise {@code false}.
|
||||||
*/
|
*/
|
||||||
public boolean storePlayerGameSaveFile(Player player, InputStream inputStream) {
|
public boolean storePlayerGameSaveFile(Player player, InputStream inputStream) {
|
||||||
// Try create directories
|
try(FileOutputStream outputStream = new FileOutputStream(player.getSaveFile())) {
|
||||||
if(!gameSaveDirectory.exists() && !gameSaveDirectory.mkdirs()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write the save data
|
|
||||||
try(FileOutputStream outputStream = new FileOutputStream(getPlayerGameSaveFile(player))) {
|
|
||||||
inputStream.transferTo(outputStream);
|
inputStream.transferTo(outputStream);
|
||||||
return true;
|
return true;
|
||||||
} catch(IOException e) {
|
} catch(IOException e) {
|
||||||
@@ -162,13 +223,6 @@ public class PlayerManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @return The game save data {@link File} object associated with this player.
|
|
||||||
*/
|
|
||||||
public File getPlayerGameSaveFile(Player player) {
|
|
||||||
return new File(gameSaveDirectory, "SAV-%s.bin".formatted(player.getGameSyncId()));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return {@code true} if a player with the specified Game Sync ID exists, {@code false} otherwise.
|
* @return {@code true} if a player with the specified Game Sync ID exists, {@code false} otherwise.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ package entralinked.model.user;
|
|||||||
|
|
||||||
public class GameProfile {
|
public class GameProfile {
|
||||||
|
|
||||||
private final int id;
|
private int id;
|
||||||
private String firstName;
|
private String firstName;
|
||||||
private String lastName;
|
private String lastName;
|
||||||
private String aimName;
|
private String aimName;
|
||||||
@@ -20,6 +20,10 @@ public class GameProfile {
|
|||||||
this.zipCode = zipCode;
|
this.zipCode = zipCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setId(int id) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
public int getId() {
|
public int getId() {
|
||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package entralinked.model.user;
|
package entralinked.model.user;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
@@ -10,6 +11,8 @@ public class User {
|
|||||||
private final String id;
|
private final String id;
|
||||||
private final String password; // I debated hashing it, but.. it's a 3-digit password...
|
private final String password; // I debated hashing it, but.. it's a 3-digit password...
|
||||||
private final Map<String, GameProfile> profiles = new HashMap<>();
|
private final Map<String, GameProfile> profiles = new HashMap<>();
|
||||||
|
private final Map<String, File> dlcOverrides = new HashMap<>();
|
||||||
|
private int profileIdOverride; // For making it easier for the user to fix error 60000
|
||||||
|
|
||||||
public User(String id, String password) {
|
public User(String id, String password) {
|
||||||
this.id = id;
|
this.id = id;
|
||||||
@@ -20,6 +23,13 @@ public class User {
|
|||||||
return "%s000".formatted(id).replaceAll("(.{4})(?!$)", "$1-");
|
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() {
|
public String getId() {
|
||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
@@ -47,4 +57,32 @@ public class User {
|
|||||||
protected Map<String, GameProfile> getProfileMap() {
|
protected Map<String, GameProfile> getProfileMap() {
|
||||||
return Collections.unmodifiableMap(profiles);
|
return Collections.unmodifiableMap(profiles);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setDlcOverride(String type, File file) {
|
||||||
|
if(file == null) {
|
||||||
|
dlcOverrides.remove(type);
|
||||||
|
} else {
|
||||||
|
dlcOverrides.put(type, file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void removeDlcOverride(String type) {
|
||||||
|
dlcOverrides.remove(type);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasDlcOverride(String type) {
|
||||||
|
return dlcOverrides.containsKey(type);
|
||||||
|
}
|
||||||
|
|
||||||
|
public File getDlcOverride(String type) {
|
||||||
|
return dlcOverrides.get(type);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setProfileIdOverride(int profileIdOverride) {
|
||||||
|
this.profileIdOverride = profileIdOverride;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getProfileIdOverride() {
|
||||||
|
return profileIdOverride;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ public class UserManager {
|
|||||||
private static final Logger logger = LogManager.getLogger();
|
private static final Logger logger = LogManager.getLogger();
|
||||||
private final ObjectMapper mapper = new ObjectMapper().enable(SerializationFeature.INDENT_OUTPUT);
|
private final ObjectMapper mapper = new ObjectMapper().enable(SerializationFeature.INDENT_OUTPUT);
|
||||||
private final Map<String, User> users = new ConcurrentHashMap<>();
|
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 Map<String, ServiceSession> serviceSessions = new ConcurrentHashMap<>();
|
||||||
private final File dataDirectory = new File("users");
|
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));
|
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
|
// Index user
|
||||||
users.put(id, user);
|
users.put(id, user);
|
||||||
|
|
||||||
// Index profiles
|
|
||||||
for(GameProfile profile : userProfiles) {
|
|
||||||
profiles.put(profile.getId(), profile);
|
|
||||||
}
|
|
||||||
} catch(IOException e) {
|
} catch(IOException e) {
|
||||||
logger.error("Could not load user data at {}", inputFile.getAbsolutePath(), 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.
|
* Registers a user with the given ID and password.
|
||||||
|
|
||||||
* @return {@code true} if the registration was successful.
|
* @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 false} is returned instead.
|
* 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
|
// Check if user id already exists
|
||||||
if(users.containsKey(userId)) {
|
if(users.containsKey(userId)) {
|
||||||
logger.warn("Attempted to register user with duplicate ID: {}", userId);
|
logger.warn("Attempted to register user with duplicate ID: {}", userId);
|
||||||
return false;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create user
|
// Create user
|
||||||
@@ -205,11 +192,11 @@ public class UserManager {
|
|||||||
|
|
||||||
// Save user data and return null if it fails
|
// Save user data and return null if it fails
|
||||||
if(!saveUser(user)) {
|
if(!saveUser(user)) {
|
||||||
return false;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
users.put(userId, user);
|
users.put(userId, user);
|
||||||
return true;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -233,7 +220,7 @@ public class UserManager {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
int profileId = nextProfileId();
|
int profileId = (int)(Math.random() * Integer.MAX_VALUE);
|
||||||
GameProfile profile = new GameProfile(profileId);
|
GameProfile profile = new GameProfile(profileId);
|
||||||
user.addProfile(branchCode, profile);
|
user.addProfile(branchCode, profile);
|
||||||
|
|
||||||
@@ -243,24 +230,9 @@ public class UserManager {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
profiles.put(profileId, profile);
|
|
||||||
return 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}.
|
* @return {@code true} if a user with the specified ID exists, otherwise {@code false}.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -40,7 +40,6 @@ public abstract class NettyServerBase {
|
|||||||
|
|
||||||
public boolean start() {
|
public boolean start() {
|
||||||
if(started) {
|
if(started) {
|
||||||
logger.warn("start() was called while {} server was already running!", name);
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,7 +58,6 @@ public abstract class NettyServerBase {
|
|||||||
|
|
||||||
public boolean stop() {
|
public boolean stop() {
|
||||||
if(!started) {
|
if(!started) {
|
||||||
logger.warn("stop() was called while {} server wasn't running!", name);
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ public class DnsQueryHandler extends SimpleChannelInboundHandler<DatagramDnsQuer
|
|||||||
@Override
|
@Override
|
||||||
protected void channelRead0(ChannelHandlerContext ctx, DatagramDnsQuery query) throws Exception {
|
protected void channelRead0(ChannelHandlerContext ctx, DatagramDnsQuery query) throws Exception {
|
||||||
DefaultDnsQuestion question = query.recordAt(DnsSection.QUESTION);
|
DefaultDnsQuestion question = query.recordAt(DnsSection.QUESTION);
|
||||||
|
logger.debug("Received {}", question);
|
||||||
DnsRecordType type = question.type();
|
DnsRecordType type = question.type();
|
||||||
|
|
||||||
// We only need type A (32 bit IPv4) for the DS
|
// We only need type A (32 bit IPv4) for the DS
|
||||||
|
|||||||
@@ -2,9 +2,6 @@ package entralinked.network.dns;
|
|||||||
|
|
||||||
import java.net.InetAddress;
|
import java.net.InetAddress;
|
||||||
|
|
||||||
import org.apache.logging.log4j.LogManager;
|
|
||||||
import org.apache.logging.log4j.Logger;
|
|
||||||
|
|
||||||
import entralinked.network.NettyServerBase;
|
import entralinked.network.NettyServerBase;
|
||||||
import io.netty.bootstrap.Bootstrap;
|
import io.netty.bootstrap.Bootstrap;
|
||||||
import io.netty.channel.Channel;
|
import io.netty.channel.Channel;
|
||||||
@@ -17,13 +14,11 @@ import io.netty.handler.codec.dns.DatagramDnsResponseEncoder;
|
|||||||
|
|
||||||
public class DnsServer extends NettyServerBase {
|
public class DnsServer extends NettyServerBase {
|
||||||
|
|
||||||
private static final Logger logger = LogManager.getLogger();
|
|
||||||
private InetAddress hostAddress;
|
private InetAddress hostAddress;
|
||||||
|
|
||||||
public DnsServer(InetAddress hostAddress) {
|
public DnsServer(InetAddress hostAddress) {
|
||||||
super("DNS", 53);
|
super("DNS", 53);
|
||||||
this.hostAddress = hostAddress;
|
this.hostAddress = hostAddress;
|
||||||
logger.info("DNS queries will be resolved to {}", hostAddress.getHostAddress());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import io.netty.channel.ChannelFuture;
|
|||||||
import io.netty.channel.ChannelFutureListener;
|
import io.netty.channel.ChannelFutureListener;
|
||||||
import io.netty.channel.ChannelHandlerContext;
|
import io.netty.channel.ChannelHandlerContext;
|
||||||
import io.netty.channel.SimpleChannelInboundHandler;
|
import io.netty.channel.SimpleChannelInboundHandler;
|
||||||
|
import io.netty.handler.timeout.ReadTimeoutException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GameSpy request handler.
|
* GameSpy request handler.
|
||||||
@@ -50,6 +51,7 @@ public class GameSpyHandler extends SimpleChannelInboundHandler<GameSpyRequest>
|
|||||||
@Override
|
@Override
|
||||||
public void channelActive(ChannelHandlerContext ctx) {
|
public void channelActive(ChannelHandlerContext ctx) {
|
||||||
channel = ctx.channel();
|
channel = ctx.channel();
|
||||||
|
logger.debug("Sending GameSpy server challenge");
|
||||||
|
|
||||||
// Generate random server challenge
|
// Generate random server challenge
|
||||||
serverChallenge = CredentialGenerator.generateChallenge(10);
|
serverChallenge = CredentialGenerator.generateChallenge(10);
|
||||||
@@ -60,6 +62,8 @@ public class GameSpyHandler extends SimpleChannelInboundHandler<GameSpyRequest>
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void channelInactive(ChannelHandlerContext ctx) {
|
public void channelInactive(ChannelHandlerContext ctx) {
|
||||||
|
logger.debug("User {} disconnected from GameSpy server", user == null ? null : user.getRedactedId());
|
||||||
|
|
||||||
// Clear data
|
// Clear data
|
||||||
serverChallenge = null;
|
serverChallenge = null;
|
||||||
sessionKey = -1;
|
sessionKey = -1;
|
||||||
@@ -69,15 +73,23 @@ public class GameSpyHandler extends SimpleChannelInboundHandler<GameSpyRequest>
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void channelRead0(ChannelHandlerContext ctx, GameSpyRequest request) throws Exception {
|
protected void channelRead0(ChannelHandlerContext ctx, GameSpyRequest request) throws Exception {
|
||||||
|
logger.debug("Received {}", request);
|
||||||
request.process(this);
|
request.process(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
|
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
|
||||||
if(cause instanceof ClosedChannelException) {
|
if(cause instanceof ClosedChannelException) {
|
||||||
|
logger.debug("Silently discarded ClosedChannelException");
|
||||||
return; // Ignore this stupid exception.
|
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);
|
logger.error("Exception caught in GameSpy handler", cause);
|
||||||
|
|
||||||
// Send error message and close channel afterwards.
|
// Send error message and close channel afterwards.
|
||||||
@@ -92,6 +104,7 @@ public class GameSpyHandler extends SimpleChannelInboundHandler<GameSpyRequest>
|
|||||||
ServiceSession session = userManager.getServiceSession(authToken, "gamespy");
|
ServiceSession session = userManager.getServiceSession(authToken, "gamespy");
|
||||||
|
|
||||||
if(session == null) {
|
if(session == null) {
|
||||||
|
logger.debug("Rejecting GameSpy login request because the partner token is invalid");
|
||||||
sendErrorMessage(0x200, "Invalid partner token.", request.sequenceId());
|
sendErrorMessage(0x200, "Invalid partner token.", request.sequenceId());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -101,6 +114,7 @@ public class GameSpyHandler extends SimpleChannelInboundHandler<GameSpyRequest>
|
|||||||
String expectedResponse = createCredentialHash(partnerChallengeHash, authToken, clientChallenge, serverChallenge);
|
String expectedResponse = createCredentialHash(partnerChallengeHash, authToken, clientChallenge, serverChallenge);
|
||||||
|
|
||||||
if(!expectedResponse.equals(request.response())) {
|
if(!expectedResponse.equals(request.response())) {
|
||||||
|
logger.debug("Rejecting GameSpy login request because the challenge response is invalid");
|
||||||
sendErrorMessage(0x202, "Invalid response.", request.sequenceId());
|
sendErrorMessage(0x202, "Invalid response.", request.sequenceId());
|
||||||
return;
|
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
|
// Prepare and send response
|
||||||
sessionKey = secureRandom.nextInt(Integer.MAX_VALUE);
|
sessionKey = secureRandom.nextInt(Integer.MAX_VALUE);
|
||||||
String proof = createCredentialHash(partnerChallengeHash, authToken, serverChallenge, clientChallenge);
|
String proof = createCredentialHash(partnerChallengeHash, authToken, serverChallenge, clientChallenge);
|
||||||
@@ -171,10 +196,9 @@ public class GameSpyHandler extends SimpleChannelInboundHandler<GameSpyRequest>
|
|||||||
passwordHash));
|
passwordHash));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void destroySessionKey(int sessionKey) {
|
public void handleLogout() {
|
||||||
if(validateSessionKey(sessionKey)) {
|
logger.info("User {} logged out of profile {}", user.getRedactedId(), profile.getId());
|
||||||
sessionKey = -1;
|
sessionKey = -1; // Is there a point?
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean validateSessionKey(int sessionKey) {
|
public boolean validateSessionKey(int sessionKey) {
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import io.netty.channel.ChannelPipeline;
|
|||||||
import io.netty.channel.epoll.EpollServerSocketChannel;
|
import io.netty.channel.epoll.EpollServerSocketChannel;
|
||||||
import io.netty.channel.socket.nio.NioServerSocketChannel;
|
import io.netty.channel.socket.nio.NioServerSocketChannel;
|
||||||
import io.netty.handler.codec.DelimiterBasedFrameDecoder;
|
import io.netty.handler.codec.DelimiterBasedFrameDecoder;
|
||||||
|
import io.netty.handler.timeout.ReadTimeoutHandler;
|
||||||
import io.netty.util.concurrent.DefaultEventExecutor;
|
import io.netty.util.concurrent.DefaultEventExecutor;
|
||||||
import io.netty.util.concurrent.EventExecutorGroup;
|
import io.netty.util.concurrent.EventExecutorGroup;
|
||||||
|
|
||||||
@@ -64,6 +65,7 @@ public class GameSpyServer extends NettyServerBase {
|
|||||||
@Override
|
@Override
|
||||||
protected void initChannel(Channel channel) throws Exception {
|
protected void initChannel(Channel channel) throws Exception {
|
||||||
ChannelPipeline pipeline = channel.pipeline();
|
ChannelPipeline pipeline = channel.pipeline();
|
||||||
|
pipeline.addLast(new ReadTimeoutHandler(180));
|
||||||
pipeline.addLast(new DelimiterBasedFrameDecoder(512, Unpooled.wrappedBuffer("\\final\\".getBytes())));
|
pipeline.addLast(new DelimiterBasedFrameDecoder(512, Unpooled.wrappedBuffer("\\final\\".getBytes())));
|
||||||
pipeline.addLast(new GameSpyRequestDecoder(mapper, requestTypes));
|
pipeline.addLast(new GameSpyRequestDecoder(mapper, requestTypes));
|
||||||
pipeline.addLast(new GameSpyMessageEncoder(mapper));
|
pipeline.addLast(new GameSpyMessageEncoder(mapper));
|
||||||
|
|||||||
@@ -1,13 +1,9 @@
|
|||||||
package entralinked.network.gamespy.request;
|
package entralinked.network.gamespy.request;
|
||||||
|
|
||||||
import entralinked.network.gamespy.GameSpyHandler;
|
import entralinked.network.gamespy.GameSpyHandler;
|
||||||
import entralinked.network.gamespy.message.GameSpyMessage;
|
|
||||||
|
|
||||||
@GameSpyMessage(name = "ka")
|
|
||||||
public record GameSpyKeepAliveRequest() implements GameSpyRequest {
|
public record GameSpyKeepAliveRequest() implements GameSpyRequest {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void process(GameSpyHandler handler) {
|
public void process(GameSpyHandler handler) {}
|
||||||
handler.sendMessage(this); // Pong it back
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,4 +28,11 @@ public record GameSpyLoginRequest(
|
|||||||
public void process(GameSpyHandler handler) {
|
public void process(GameSpyHandler handler) {
|
||||||
handler.handleLoginRequest(this);
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,14 @@ public record GameSpyLogoutRequest(
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void process(GameSpyHandler handler) {
|
public void process(GameSpyHandler handler) {
|
||||||
handler.destroySessionKey(sessionKey);
|
if(handler.validateSessionKey(sessionKey)) {
|
||||||
|
handler.handleLogout();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
// Exlude session key
|
||||||
|
return "GameSpyLogoutRequest[]";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,4 +16,10 @@ public record GameSpyProfileRequest(
|
|||||||
handler.handleProfileRequest(this);
|
handler.handleProfileRequest(this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
// Exlude session key
|
||||||
|
return "GameSpyProfileRequest[sequenceId=%s, profileId=%s]".formatted(sequenceId, profileId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,4 +21,9 @@ public record GameSpyProfileUpdateRequest(
|
|||||||
handler.handleUpdateProfileRequest(this);
|
handler.handleUpdateProfileRequest(this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "GameSpyProfileUpdateRequest[partnerId=%s]".formatted(partnerId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,4 +14,9 @@ public record GameSpyStatusRequest(
|
|||||||
public void process(GameSpyHandler handler) {
|
public void process(GameSpyHandler handler) {
|
||||||
handler.validateSessionKey(sessionKey);
|
handler.validateSessionKey(sessionKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "GameSpyStatusRequest[]";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import entralinked.LauncherAgent;
|
|||||||
import entralinked.utility.CertificateGenerator;
|
import entralinked.utility.CertificateGenerator;
|
||||||
import io.javalin.Javalin;
|
import io.javalin.Javalin;
|
||||||
import io.javalin.http.HttpStatus;
|
import io.javalin.http.HttpStatus;
|
||||||
|
import io.javalin.util.ConcurrencyUtil;
|
||||||
import io.javalin.util.JavalinException;
|
import io.javalin.util.JavalinException;
|
||||||
|
|
||||||
public class HttpServer {
|
public class HttpServer {
|
||||||
@@ -33,6 +34,8 @@ public class HttpServer {
|
|||||||
private boolean started;
|
private boolean started;
|
||||||
|
|
||||||
public HttpServer(Entralinked entralinked) {
|
public HttpServer(Entralinked entralinked) {
|
||||||
|
ConcurrencyUtil.INSTANCE.setUseLoom(false);
|
||||||
|
|
||||||
// Create certificate keystore
|
// Create certificate keystore
|
||||||
KeyStore keyStore = null;
|
KeyStore keyStore = null;
|
||||||
|
|
||||||
@@ -75,7 +78,6 @@ public class HttpServer {
|
|||||||
|
|
||||||
public boolean start() {
|
public boolean start() {
|
||||||
if(started) {
|
if(started) {
|
||||||
logger.warn("start() was called while HTTP server was already running!");
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,7 +96,6 @@ public class HttpServer {
|
|||||||
|
|
||||||
public boolean stop() {
|
public boolean stop() {
|
||||||
if(!started) {
|
if(!started) {
|
||||||
logger.warn("stop() was called while HTTP server wasn't running!");
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
package entralinked.network.http.dashboard;
|
package entralinked.network.http.dashboard;
|
||||||
|
|
||||||
import java.awt.image.BufferedImage;
|
import java.awt.image.BufferedImage;
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.File;
|
||||||
import java.io.FileInputStream;
|
import java.io.FileInputStream;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
@@ -15,6 +19,7 @@ import javax.imageio.ImageIO;
|
|||||||
|
|
||||||
import org.apache.logging.log4j.LogManager;
|
import org.apache.logging.log4j.LogManager;
|
||||||
import org.apache.logging.log4j.Logger;
|
import org.apache.logging.log4j.Logger;
|
||||||
|
import org.bouncycastle.util.Arrays;
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
|
||||||
@@ -30,8 +35,11 @@ import entralinked.model.player.Player;
|
|||||||
import entralinked.model.player.PlayerManager;
|
import entralinked.model.player.PlayerManager;
|
||||||
import entralinked.model.player.PlayerStatus;
|
import entralinked.model.player.PlayerStatus;
|
||||||
import entralinked.network.http.HttpHandler;
|
import entralinked.network.http.HttpHandler;
|
||||||
|
import entralinked.utility.ColorUtility;
|
||||||
|
import entralinked.utility.Crc16;
|
||||||
import entralinked.utility.GsidUtility;
|
import entralinked.utility.GsidUtility;
|
||||||
import entralinked.utility.TiledImageReader;
|
import entralinked.utility.LEOutputStream;
|
||||||
|
import entralinked.utility.TiledImageUtility;
|
||||||
import io.javalin.Javalin;
|
import io.javalin.Javalin;
|
||||||
import io.javalin.config.JavalinConfig;
|
import io.javalin.config.JavalinConfig;
|
||||||
import io.javalin.http.Context;
|
import io.javalin.http.Context;
|
||||||
@@ -45,7 +53,7 @@ import io.javalin.json.JavalinJackson;
|
|||||||
public class DashboardHandler implements HttpHandler {
|
public class DashboardHandler implements HttpHandler {
|
||||||
|
|
||||||
private static final Logger logger = LogManager.getLogger();
|
private static final Logger logger = LogManager.getLogger();
|
||||||
private final Map<Dlc, BufferedImage> skinPreviewCache = new HashMap<>();
|
private final Map<Object, BufferedImage> skinPreviewCache = new HashMap<>();
|
||||||
private final DlcList dlcList;
|
private final DlcList dlcList;
|
||||||
private final PlayerManager playerManager;
|
private final PlayerManager playerManager;
|
||||||
|
|
||||||
@@ -57,18 +65,52 @@ public class DashboardHandler implements HttpHandler {
|
|||||||
logger.info("Loading C-Gear and Pokédex skin previews ...");
|
logger.info("Loading C-Gear and Pokédex skin previews ...");
|
||||||
List<Dlc> skins = dlcList.getDlcList(dlc -> dlc.type().startsWith("CGEAR") || dlc.type().equals("ZUKAN"));
|
List<Dlc> skins = dlcList.getDlcList(dlc -> dlc.type().startsWith("CGEAR") || dlc.type().equals("ZUKAN"));
|
||||||
|
|
||||||
|
// Cache default skins
|
||||||
for(Dlc skin : skins) {
|
for(Dlc skin : skins) {
|
||||||
try(FileInputStream inputStream = new FileInputStream(skin.path())) {
|
try(FileInputStream inputStream = new FileInputStream(skin.path())) {
|
||||||
BufferedImage image =
|
BufferedImage image = skin.type().equals("ZUKAN") ? TiledImageUtility.readDexSkin(inputStream, true)
|
||||||
skin.type().equals("ZUKAN") ? TiledImageReader.readDexSkin(inputStream) :
|
: skin.type().equals("CGEAR") ? TiledImageUtility.readCGearSkin(inputStream, true)
|
||||||
skin.type().equals("CGEAR") ? TiledImageReader.readCGearSkin(inputStream, true) :
|
: TiledImageUtility.readCGearSkin(inputStream, false); // CGEAR2
|
||||||
TiledImageReader.readCGearSkin(inputStream, false); // CGEAR2
|
|
||||||
skinPreviewCache.put(skin, image);
|
skinPreviewCache.put(skin, image);
|
||||||
} catch(IOException | IndexOutOfBoundsException e) {
|
} catch(IOException | IndexOutOfBoundsException e) {
|
||||||
logger.error("Could not load image for skin {} of type {}", skin.name(), skin.type(), e);
|
logger.error("Could not load image for skin {} of type {}", skin.name(), skin.type(), e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Cache custom skins for each player
|
||||||
|
for(Player player : playerManager.getPlayers()) {
|
||||||
|
File cgearSkinFile = player.getCGearSkinFile();
|
||||||
|
File dexSkinFile = player.getDexSkinFile();
|
||||||
|
|
||||||
|
// Cache custom C-Gear skin preview if it exists
|
||||||
|
if(cgearSkinFile.exists()) {
|
||||||
|
try(FileInputStream inputStream = new FileInputStream(cgearSkinFile)) {
|
||||||
|
boolean version2 = player.getGameVersion().isVersion2();
|
||||||
|
|
||||||
|
// Read the C-Gear skin image
|
||||||
|
BufferedImage image = TiledImageUtility.readCGearSkin(inputStream, !version2);
|
||||||
|
|
||||||
|
// Cache the result
|
||||||
|
skinPreviewCache.put("%s/%s".formatted(player.getGameSyncId(), version2 ? "CGEAR2" : "CGEAR"), image);
|
||||||
|
} catch(IOException | IndexOutOfBoundsException | NullPointerException e) {
|
||||||
|
logger.error("Could not load custom C-Gear skin preview for player {}", player.getGameSyncId(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache custom Pokédex skin preview if it exists
|
||||||
|
if(dexSkinFile.exists()) {
|
||||||
|
try(FileInputStream inputStream = new FileInputStream(dexSkinFile)) {
|
||||||
|
// Read the Pokédex skin image
|
||||||
|
BufferedImage image = TiledImageUtility.readDexSkin(inputStream, true);
|
||||||
|
|
||||||
|
// Cache the result
|
||||||
|
skinPreviewCache.put("%s/ZUKAN".formatted(player.getGameSyncId()), image);
|
||||||
|
} catch(IOException | IndexOutOfBoundsException e) {
|
||||||
|
logger.error("Could not load custom Pokédex skin preview for player {}", player.getGameSyncId(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
logger.info("Cached {} skin previews", skinPreviewCache.size());
|
logger.info("Cached {} skin previews", skinPreviewCache.size());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,6 +119,7 @@ public class DashboardHandler implements HttpHandler {
|
|||||||
javalin.get("/dashboard/previewskin", this::handlePreviewSkin);
|
javalin.get("/dashboard/previewskin", this::handlePreviewSkin);
|
||||||
javalin.get("/dashboard/dlc", this::handleRetrieveDlcList);
|
javalin.get("/dashboard/dlc", this::handleRetrieveDlcList);
|
||||||
javalin.get("/dashboard/profile", this::handleRetrieveProfile);
|
javalin.get("/dashboard/profile", this::handleRetrieveProfile);
|
||||||
|
javalin.post("/dashboard/uploadskin", this::handleUploadSkin);
|
||||||
javalin.post("/dashboard/profile", this::handleUpdateProfile);
|
javalin.post("/dashboard/profile", this::handleUpdateProfile);
|
||||||
javalin.post("/dashboard/login", this::handleLogin);
|
javalin.post("/dashboard/login", this::handleLogin);
|
||||||
javalin.post("/dashboard/logout", this::handleLogout);
|
javalin.post("/dashboard/logout", this::handleLogout);
|
||||||
@@ -111,15 +154,38 @@ public class DashboardHandler implements HttpHandler {
|
|||||||
|
|
||||||
// Make sure query parameters are present
|
// Make sure query parameters are present
|
||||||
if(type == null || name == null) {
|
if(type == null || name == null) {
|
||||||
ctx.status(404);
|
ctx.status(HttpStatus.NOT_FOUND);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle custom skin preview
|
||||||
|
if(name.equals("custom")) {
|
||||||
|
Player player = ctx.sessionAttribute("player");
|
||||||
|
|
||||||
|
// Check if player exists
|
||||||
|
if(player == null) {
|
||||||
|
ctx.status(HttpStatus.NOT_FOUND);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
BufferedImage previewImage = skinPreviewCache.get("%s/%s".formatted(player.getGameSyncId(), type));
|
||||||
|
|
||||||
|
// Check if preview image exists
|
||||||
|
if(previewImage == null) {
|
||||||
|
ctx.status(HttpStatus.NOT_FOUND);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ImageIO.write(previewImage, "png", ctx.outputStream());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle DLC skin preview
|
||||||
Dlc dlc = dlcList.getDlc("IRAO", type, name);
|
Dlc dlc = dlcList.getDlc("IRAO", type, name);
|
||||||
|
|
||||||
// Check if DLC exists
|
// Check if DLC exists
|
||||||
if(dlc == null) {
|
if(dlc == null) {
|
||||||
ctx.status(404);
|
ctx.status(HttpStatus.NOT_FOUND);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -188,7 +254,124 @@ public class DashboardHandler implements HttpHandler {
|
|||||||
|
|
||||||
// Store session attribute and send response
|
// Store session attribute and send response
|
||||||
ctx.sessionAttribute("player", player);
|
ctx.sessionAttribute("player", player);
|
||||||
ctx.json(new DashboardStatusMessage("ok")); // heh
|
ctx.json(Collections.EMPTY_MAP);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST request handler for {@code /dashboard/uploadskin}
|
||||||
|
*/
|
||||||
|
private void handleUploadSkin(Context ctx) throws IOException {
|
||||||
|
Player player = ctx.sessionAttribute("player");
|
||||||
|
|
||||||
|
// Check if player exists
|
||||||
|
if(player == null) {
|
||||||
|
ctx.json(new DashboardStatusMessage("Unauthorized", true));
|
||||||
|
ctx.status(HttpStatus.UNAUTHORIZED);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String type = ctx.queryParam("type");
|
||||||
|
String fileName = ctx.queryParam("filename");
|
||||||
|
|
||||||
|
// Check if type & file name are present and are valid
|
||||||
|
if(type == null || fileName == null ||
|
||||||
|
(!type.equals("CGEAR") && !type.equals("CGEAR2") && !type.equals("ZUKAN"))) {
|
||||||
|
ctx.status(HttpStatus.BAD_REQUEST);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
BufferedImage image = ImageIO.read(ctx.bodyInputStream());
|
||||||
|
BufferedImage previewImage = null;
|
||||||
|
|
||||||
|
// Make sure file is a valid image
|
||||||
|
if(image == null) {
|
||||||
|
ctx.json(new DashboardStatusMessage("File is either not an image or uses an unsupported format.", true));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure image has the correct dimensions
|
||||||
|
if(image.getWidth() != 256 || image.getHeight() != 192) {
|
||||||
|
ctx.json(new DashboardStatusMessage("Image must be 256 x 192 pixels.", true));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
File outputFile = type.equals("ZUKAN") ? player.getDexSkinFile() : player.getCGearSkinFile();
|
||||||
|
ByteArrayOutputStream byteOutputStream = new ByteArrayOutputStream();
|
||||||
|
byte[] skinBytes = null;
|
||||||
|
|
||||||
|
// Write skin data to buffer and generate a preview image
|
||||||
|
switch(type) {
|
||||||
|
case "CGEAR":
|
||||||
|
case "CGEAR2":
|
||||||
|
boolean offsetIndices = type.equals("CGEAR");
|
||||||
|
TiledImageUtility.writeCGearSkin(byteOutputStream, image, offsetIndices);
|
||||||
|
skinBytes = byteOutputStream.toByteArray();
|
||||||
|
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);
|
||||||
|
skinBytes = byteOutputStream.toByteArray();
|
||||||
|
previewImage = TiledImageUtility.readDexSkin(new ByteArrayInputStream(skinBytes), true);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write custom skin to output file
|
||||||
|
try(LEOutputStream outputStream = new LEOutputStream(new FileOutputStream(outputFile))) {
|
||||||
|
outputStream.write(skinBytes);
|
||||||
|
outputStream.writeShort(Crc16.calc(skinBytes));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache preview image
|
||||||
|
skinPreviewCache.put("%s/%s".formatted(player.getGameSyncId(), type), previewImage);
|
||||||
|
} catch(IOException e) {
|
||||||
|
logger.error("An error occured while processing custom skin data", e);
|
||||||
|
ctx.status(HttpStatus.INTERNAL_SERVER_ERROR);
|
||||||
|
return;
|
||||||
|
} catch(IllegalArgumentException e) {
|
||||||
|
// Silently discard exception, only send feedback to the client
|
||||||
|
ctx.json(new DashboardStatusMessage(e.getMessage(), true));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update player information
|
||||||
|
if(type.equals("ZUKAN")) {
|
||||||
|
player.setDexSkin("custom");
|
||||||
|
player.setCustomDexSkin(fileName);
|
||||||
|
} else {
|
||||||
|
player.setCGearSkin("custom");
|
||||||
|
player.setCustomCGearSkin(fileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to save player data
|
||||||
|
if(!playerManager.savePlayer(player)) {
|
||||||
|
ctx.json(new DashboardStatusMessage("Profile data could not be saved due to an error.", true));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.json(Collections.EMPTY_MAP);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ public record DashboardProfileMessage(
|
|||||||
String cgearSkin,
|
String cgearSkin,
|
||||||
String dexSkin,
|
String dexSkin,
|
||||||
String musical,
|
String musical,
|
||||||
|
String customCGearSkin,
|
||||||
|
String customDexSkin,
|
||||||
int levelsGained,
|
int levelsGained,
|
||||||
Collection<DreamEncounter> encounters,
|
Collection<DreamEncounter> encounters,
|
||||||
Collection<DreamItem> items,
|
Collection<DreamItem> items,
|
||||||
@@ -22,7 +24,8 @@ public record DashboardProfileMessage(
|
|||||||
|
|
||||||
public DashboardProfileMessage(String dreamerSprite, Player player) {
|
public DashboardProfileMessage(String dreamerSprite, Player player) {
|
||||||
this(player.getGameVersion().getDisplayName(), dreamerSprite, player.getDreamerInfo(),
|
this(player.getGameVersion().getDisplayName(), dreamerSprite, player.getDreamerInfo(),
|
||||||
player.getCGearSkin(), player.getDexSkin(), player.getMusical(), player.getLevelsGained(),
|
player.getCGearSkin(), player.getDexSkin(), player.getMusical(), player.getCustomCGearSkin(),
|
||||||
player.getEncounters(), player.getItems(), player.getAvenueVisitors());
|
player.getCustomDexSkin(), player.getLevelsGained(), player.getEncounters(), player.getItems(),
|
||||||
|
player.getAvenueVisitors());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,27 @@
|
|||||||
package entralinked.network.http.dls;
|
package entralinked.network.http.dls;
|
||||||
|
|
||||||
import java.io.FileInputStream;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.apache.logging.log4j.LogManager;
|
||||||
|
import org.apache.logging.log4j.Logger;
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
|
||||||
import entralinked.Entralinked;
|
import entralinked.Entralinked;
|
||||||
import entralinked.model.dlc.Dlc;
|
import entralinked.GameVersion;
|
||||||
import entralinked.model.dlc.DlcList;
|
|
||||||
import entralinked.model.user.ServiceSession;
|
import entralinked.model.user.ServiceSession;
|
||||||
|
import entralinked.model.user.User;
|
||||||
import entralinked.model.user.UserManager;
|
import entralinked.model.user.UserManager;
|
||||||
import entralinked.network.http.HttpHandler;
|
import entralinked.network.http.HttpHandler;
|
||||||
import entralinked.network.http.HttpRequestHandler;
|
import entralinked.network.http.HttpRequestHandler;
|
||||||
import entralinked.serialization.UrlEncodedFormFactory;
|
import entralinked.serialization.UrlEncodedFormFactory;
|
||||||
import entralinked.utility.LEOutputStream;
|
import entralinked.utility.MysteryGiftUtility;
|
||||||
import io.javalin.Javalin;
|
import io.javalin.Javalin;
|
||||||
import io.javalin.http.Context;
|
import io.javalin.http.Context;
|
||||||
import io.javalin.http.HttpStatus;
|
import io.javalin.http.HttpStatus;
|
||||||
@@ -23,12 +31,12 @@ import io.javalin.http.HttpStatus;
|
|||||||
*/
|
*/
|
||||||
public class DlsHandler implements HttpHandler {
|
public class DlsHandler implements HttpHandler {
|
||||||
|
|
||||||
|
private static final Logger logger = LogManager.getLogger();
|
||||||
private final ObjectMapper mapper = new ObjectMapper(new UrlEncodedFormFactory());
|
private final ObjectMapper mapper = new ObjectMapper(new UrlEncodedFormFactory());
|
||||||
private final DlcList dlcList;
|
private final File rootDirectory = new File("dlc");
|
||||||
private final UserManager userManager;
|
private final UserManager userManager;
|
||||||
|
|
||||||
public DlsHandler(Entralinked entralinked) {
|
public DlsHandler(Entralinked entralinked) {
|
||||||
this.dlcList = entralinked.getDlcList();
|
|
||||||
this.userManager = entralinked.getUserManager();
|
this.userManager = entralinked.getUserManager();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,11 +51,13 @@ public class DlsHandler implements HttpHandler {
|
|||||||
private void handleDownloadRequest(Context ctx) throws IOException {
|
private void handleDownloadRequest(Context ctx) throws IOException {
|
||||||
// Deserialize request body
|
// Deserialize request body
|
||||||
DlsRequest request = mapper.readValue(ctx.body().replace("%2A", "*"), DlsRequest.class);
|
DlsRequest request = mapper.readValue(ctx.body().replace("%2A", "*"), DlsRequest.class);
|
||||||
|
logger.debug("Received {}", request);
|
||||||
|
|
||||||
// Check if service session is valid
|
// Check if service session is valid
|
||||||
ServiceSession session = userManager.getServiceSession(request.serviceToken(), "dls1.nintendowifi.net");
|
ServiceSession session = userManager.getServiceSession(request.serviceToken(), "dls1.nintendowifi.net");
|
||||||
|
|
||||||
if(session == null) {
|
if(session == null) {
|
||||||
|
logger.debug("Rejecting DLS request because the service session has expired");
|
||||||
ctx.status(HttpStatus.UNAUTHORIZED);
|
ctx.status(HttpStatus.UNAUTHORIZED);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -56,9 +66,13 @@ public class DlsHandler implements HttpHandler {
|
|||||||
HttpRequestHandler<DlsRequest> handler = switch(request.action()) {
|
HttpRequestHandler<DlsRequest> handler = switch(request.action()) {
|
||||||
case "list" -> this::handleRetrieveDlcList;
|
case "list" -> this::handleRetrieveDlcList;
|
||||||
case "contents" -> this::handleRetrieveDlcContent;
|
case "contents" -> this::handleRetrieveDlcContent;
|
||||||
|
case "count" -> this::handleRetrieveDlcCount;
|
||||||
default -> throw new IllegalArgumentException("Invalid POST request action: " + request.action());
|
default -> throw new IllegalArgumentException("Invalid POST request action: " + request.action());
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Store user attribute for subsequent handlers
|
||||||
|
ctx.attribute("user", session.user());
|
||||||
|
|
||||||
// Handle the request
|
// Handle the request
|
||||||
handler.process(request, ctx);
|
handler.process(request, ctx);
|
||||||
}
|
}
|
||||||
@@ -67,45 +81,97 @@ public class DlsHandler implements HttpHandler {
|
|||||||
* POST handler for {@code /download action=list}
|
* POST handler for {@code /download action=list}
|
||||||
*/
|
*/
|
||||||
private void handleRetrieveDlcList(DlsRequest request, Context ctx) throws IOException {
|
private void handleRetrieveDlcList(DlsRequest request, Context ctx) throws IOException {
|
||||||
|
User user = ctx.attribute("user");
|
||||||
String gameCode = getDlcGameCode(request.dlcGameCode());
|
String gameCode = getDlcGameCode(request.dlcGameCode());
|
||||||
String type = getRegionlessDlcType(request.dlcType());
|
String type = getDlcType(request.attr1());
|
||||||
|
String attr2 = request.attr2();
|
||||||
|
List<File> files = null;
|
||||||
|
|
||||||
// TODO NOTE: I assume that in a conventional implementation, certain DLC attributes may be omitted from the request.
|
if(user.hasDlcOverride(type)) {
|
||||||
ctx.result(dlcList.getDlcListString(dlcList.getDlcList(gameCode, type, request.dlcIndex())));
|
files = Arrays.asList(user.getDlcOverride(type));
|
||||||
|
} else {
|
||||||
|
// Get list of files in DLC directory
|
||||||
|
File directory = getDlcDirectory(gameCode, type);
|
||||||
|
files = directory.isDirectory() ? Arrays.asList(directory.listFiles()) : new ArrayList<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
if(attr2 != null) {
|
||||||
|
// PGL content attr2 hack
|
||||||
|
files = Arrays.asList(files.get(Integer.parseInt(attr2) - 1));
|
||||||
|
} else {
|
||||||
|
// Mystery Gift randomness
|
||||||
|
Collections.shuffle(files);
|
||||||
|
}
|
||||||
|
|
||||||
|
StringBuilder builder = new StringBuilder();
|
||||||
|
int count = Math.min(files.size(), request.num());
|
||||||
|
|
||||||
|
// Create DLC list string
|
||||||
|
for(int i = 0; i < count; i++) {
|
||||||
|
File file = files.get(i);
|
||||||
|
|
||||||
|
if(type == null) {
|
||||||
|
// Generation 4 Mystery Gift
|
||||||
|
builder.append("%s\t\t\t\t\t%s\r\n".formatted(file.getName(), 936));
|
||||||
|
} else if(type.equals("MYSTERY")) {
|
||||||
|
// Generation 5 Mystery Gift
|
||||||
|
String gameFlag = GameVersion.lookup(request.gameCode()).isVersion2() ? "F00000" : "300000";
|
||||||
|
builder.append("%s\t\t%s\t%s\t\t%s\r\n".formatted(file.getName(), type, gameFlag, 720));
|
||||||
|
} else {
|
||||||
|
// PGL content
|
||||||
|
builder.append("%s\t\t%s\t%s\t\t%s\r\n".formatted(file.getName(), type, i + 1, file.length()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send result
|
||||||
|
ctx.result(builder.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* POST handler for {@code /download action=contents}
|
* POST handler for {@code /download action=contents}
|
||||||
*/
|
*/
|
||||||
private void handleRetrieveDlcContent(DlsRequest request, Context ctx) throws IOException {
|
private void handleRetrieveDlcContent(DlsRequest request, Context ctx) throws IOException {
|
||||||
|
User user = ctx.attribute("user");
|
||||||
String gameCode = getDlcGameCode(request.dlcGameCode());
|
String gameCode = getDlcGameCode(request.dlcGameCode());
|
||||||
String type = getRegionlessDlcType(request.dlcType());
|
String type = getDlcType(request.attr1());
|
||||||
Dlc dlc = dlcList.getDlc(gameCode, type, request.dlcName());
|
File file = user.hasDlcOverride(type) ? user.getDlcOverride(type) : type != null
|
||||||
|
? new File(rootDirectory, "%s/%s/%s".formatted(gameCode, type, request.dlcName()))
|
||||||
|
: new File(rootDirectory, "%s/%s".formatted(gameCode, request.dlcName()));
|
||||||
|
|
||||||
// Check if the requested DLC exists
|
// Check if the requested DLC exists
|
||||||
if(dlc == null) {
|
if(!file.exists()) {
|
||||||
ctx.status(HttpStatus.NOT_FOUND);
|
ctx.status(HttpStatus.NOT_FOUND);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write DLC data
|
byte[] bytes = Files.readAllBytes(file.toPath());
|
||||||
try(FileInputStream inputStream = new FileInputStream(dlc.path())) {
|
|
||||||
LEOutputStream outputStream = new LEOutputStream(ctx.outputStream());
|
|
||||||
inputStream.transferTo(outputStream);
|
|
||||||
|
|
||||||
// If checksum is not part of the file, manually append it
|
if(type == null) {
|
||||||
if(!dlc.checksumEmbedded()) {
|
// Generation 4 Mystery Gift
|
||||||
outputStream.writeShort(dlc.checksum());
|
bytes = MysteryGiftUtility.createUniversalGiftData4(bytes, file.getName());
|
||||||
}
|
} else if(type.equals("MYSTERY")) {
|
||||||
|
// Generation 5 Mystery Gift
|
||||||
|
bytes = MysteryGiftUtility.createUniversalGiftData5(bytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Send result
|
||||||
|
ctx.result(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST handler for {@code /download action=count}
|
||||||
|
*/
|
||||||
|
private void handleRetrieveDlcCount(DlsRequest request, Context ctx) throws IOException {
|
||||||
|
ctx.result("1"); // TODO
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return The game serial that should be used for downloading DLC based on the provided input.
|
* @return The game serial that should be used for downloading DLC based on the provided input.
|
||||||
*/
|
*/
|
||||||
private String getDlcGameCode(String gameCode) {
|
private String getDlcGameCode(String gameCode) {
|
||||||
return switch(gameCode) {
|
return switch(gameCode.substring(0, 3)) {
|
||||||
case "IRAJ" -> "IRAO";
|
case "IRA" -> "IRAO"; // BW & B2W2
|
||||||
|
case "ADA", "CPU", "IPG" -> "ADAE"; // DPPt & HGSS
|
||||||
default -> gameCode;
|
default -> gameCode;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -113,13 +179,15 @@ public class DlsHandler implements HttpHandler {
|
|||||||
/**
|
/**
|
||||||
* @return The DLC type without the region identifier, or the input if it is an unknown type.
|
* @return The DLC type without the region identifier, or the input if it is an unknown type.
|
||||||
*/
|
*/
|
||||||
private String getRegionlessDlcType(String dlcType) {
|
private String getDlcType(String attr1) {
|
||||||
return switch(dlcType) {
|
if(attr1 == null || !attr1.contains("_")) {
|
||||||
case "CGEAR_E", "CGEAR_F", "CGEAR_I", "CGEAR_G", "CGEAR_S", "CGEAR_J", "CGEAR_K" -> "CGEAR";
|
return attr1;
|
||||||
case "CGEAR2_E", "CGEAR2_F", "CGEAR2_I", "CGEAR2_G", "CGEAR2_S", "CGEAR2_J", "CGEAR2_K" -> "CGEAR2";
|
}
|
||||||
case "ZUKAN_E", "ZUKAN_F", "ZUKAN_I", "ZUKAN_G", "ZUKAN_S", "ZUKAN_J", "ZUKAN_K" -> "ZUKAN";
|
|
||||||
case "MUSICAL_E", "MUSICAL_F", "MUSICAL_I", "MUSICAL_G", "MUSICAL_S", "MUSICAL_J", "MUSICAL_K" -> "MUSICAL";
|
return attr1.substring(0, attr1.lastIndexOf('_'));
|
||||||
default -> dlcType;
|
}
|
||||||
};
|
|
||||||
|
private File getDlcDirectory(String gameCode, String dlcType) {
|
||||||
|
return dlcType == null ? new File(rootDirectory, gameCode) : new File(rootDirectory, "%s/%s".formatted(gameCode, dlcType));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,14 @@ public record DlsRequest(
|
|||||||
@JsonProperty(value = "action", required = true) String action,
|
@JsonProperty(value = "action", required = true) String action,
|
||||||
@JsonProperty("gamecd") String dlcGameCode,
|
@JsonProperty("gamecd") String dlcGameCode,
|
||||||
@JsonProperty("contents") String dlcName, // action=contents
|
@JsonProperty("contents") String dlcName, // action=contents
|
||||||
@JsonProperty("attr1") String dlcType, // action=list
|
@JsonProperty("attr1") String attr1, // action=list
|
||||||
@JsonProperty("attr2") int dlcIndex, // action=list
|
@JsonProperty("attr2") String attr2, // action=list
|
||||||
@JsonProperty("offset") int offset, // ?
|
@JsonProperty("offset") int offset, // Start offset in the list
|
||||||
@JsonProperty("num") int num) {} // ?
|
@JsonProperty("num") int num) { // Number of entries
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return ("DlsRequest[gameCode=%s, action=%s, dlcGameCode=%s, dlcName=%s, attr1=%s, attr2=%s, offset=%s, num=%s]")
|
||||||
|
.formatted(gameCode, action, dlcGameCode, dlcName, attr1, attr2, offset, num);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,6 +2,9 @@ package entralinked.network.http.nas;
|
|||||||
|
|
||||||
import java.io.IOException;
|
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.databind.ObjectMapper;
|
||||||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
||||||
|
|
||||||
@@ -21,6 +24,7 @@ import io.javalin.http.Context;
|
|||||||
*/
|
*/
|
||||||
public class NasHandler implements HttpHandler {
|
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 ObjectMapper mapper = new ObjectMapper(new UrlEncodedFormFactory()).registerModule(new JavaTimeModule());
|
||||||
private final Configuration configuration;
|
private final Configuration configuration;
|
||||||
private final UserManager userManager;
|
private final UserManager userManager;
|
||||||
@@ -42,6 +46,7 @@ public class NasHandler implements HttpHandler {
|
|||||||
private void handleNasRequest(Context ctx) throws IOException {
|
private void handleNasRequest(Context ctx) throws IOException {
|
||||||
// Deserialize body into a request object
|
// Deserialize body into a request object
|
||||||
NasRequest request = mapper.readValue(ctx.body(), NasRequest.class);
|
NasRequest request = mapper.readValue(ctx.body(), NasRequest.class);
|
||||||
|
logger.debug("Received {}", request);
|
||||||
|
|
||||||
// Determine handler function based on request action
|
// Determine handler function based on request action
|
||||||
HttpRequestHandler<NasRequest> handler = switch(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 {
|
private void handleLogin(NasRequest request, Context ctx) throws IOException {
|
||||||
// Make sure branch code is present
|
// Make sure branch code is present
|
||||||
if(request.branchCode() == null) {
|
if(request.branchCode() == null) {
|
||||||
|
logger.debug("Rejecting NAS login request because no branch code is present");
|
||||||
result(ctx, NasReturnCode.BAD_REQUEST);
|
result(ctx, NasReturnCode.BAD_REQUEST);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -77,7 +83,7 @@ public class NasHandler implements HttpHandler {
|
|||||||
|
|
||||||
// Try to register, if the configuration allows it
|
// Try to register, if the configuration allows it
|
||||||
if(!UserManager.isValidUserId(userId) || userManager.doesUserExist(userId)
|
if(!UserManager.isValidUserId(userId) || userManager.doesUserExist(userId)
|
||||||
|| !userManager.registerUser(userId, request.password())) {
|
|| userManager.registerUser(userId, request.password()) == null) {
|
||||||
// Oh well, try again!
|
// Oh well, try again!
|
||||||
result(ctx, NasReturnCode.USER_NOT_FOUND);
|
result(ctx, NasReturnCode.USER_NOT_FOUND);
|
||||||
return;
|
return;
|
||||||
@@ -85,10 +91,12 @@ public class NasHandler implements HttpHandler {
|
|||||||
|
|
||||||
// Should *never* return null in this location
|
// Should *never* return null in this location
|
||||||
user = userManager.authenticateUser(userId, request.password());
|
user = userManager.authenticateUser(userId, request.password());
|
||||||
|
logger.info("Created account for user {}", user.getRedactedId());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prepare GameSpy server credentials
|
// Prepare GameSpy server credentials
|
||||||
ServiceCredentials credentials = userManager.createServiceSession(user, "gamespy", request.branchCode());
|
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()));
|
result(ctx, new NasLoginResponse("gamespy.com", credentials.authToken(), credentials.challenge()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,11 +113,14 @@ public class NasHandler implements HttpHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Try to register user
|
// 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);
|
result(ctx, NasReturnCode.INTERNAL_SERVER_ERROR);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logger.info("Created account for user {}", user.getRedactedId());
|
||||||
result(ctx, NasReturnCode.REGISTRATION_SUCCESS);
|
result(ctx, NasReturnCode.REGISTRATION_SUCCESS);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,15 +128,27 @@ public class NasHandler implements HttpHandler {
|
|||||||
* POST handler for {@code /ac action=SVCLOC}
|
* POST handler for {@code /ac action=SVCLOC}
|
||||||
*/
|
*/
|
||||||
private void handleRetrieveServiceLocation(NasRequest request, Context ctx) throws IOException {
|
private void handleRetrieveServiceLocation(NasRequest request, Context ctx) throws IOException {
|
||||||
|
// Authenticate user
|
||||||
|
User user = userManager.authenticateUser(request.userId(), request.password());
|
||||||
|
|
||||||
|
if(user == null) {
|
||||||
|
result(ctx, NasReturnCode.USER_NOT_FOUND);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String type = request.serviceType();
|
||||||
|
|
||||||
// Determine service location from type
|
// Determine service location from type
|
||||||
String service = switch(request.serviceType()) {
|
String service = switch(type) {
|
||||||
case "0000" -> "external"; // External game-specific service
|
case "0000" -> "external"; // External game-specific service
|
||||||
case "9000" -> "dls1.nintendowifi.net"; // Download server
|
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
|
// Prepare user credentials
|
||||||
ServiceCredentials credentials = userManager.createServiceSession(null, service, null);
|
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()));
|
result(ctx, new NasServiceLocationResponse(true, service, credentials.authToken()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -29,4 +29,11 @@ public record NasRequest(
|
|||||||
// Request-specific info
|
// Request-specific info
|
||||||
@JsonProperty(value = "action", required = true) String action,
|
@JsonProperty(value = "action", required = true) String action,
|
||||||
@JsonProperty("gsbrcd") String branchCode, // action=login
|
@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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package entralinked.network.http.pgl;
|
package entralinked.network.http.pgl;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileInputStream;
|
import java.io.FileInputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
@@ -26,6 +27,7 @@ import entralinked.model.player.Player;
|
|||||||
import entralinked.model.player.PlayerManager;
|
import entralinked.model.player.PlayerManager;
|
||||||
import entralinked.model.player.PlayerStatus;
|
import entralinked.model.player.PlayerStatus;
|
||||||
import entralinked.model.user.ServiceSession;
|
import entralinked.model.user.ServiceSession;
|
||||||
|
import entralinked.model.user.User;
|
||||||
import entralinked.model.user.UserManager;
|
import entralinked.model.user.UserManager;
|
||||||
import entralinked.network.http.HttpHandler;
|
import entralinked.network.http.HttpHandler;
|
||||||
import entralinked.network.http.HttpRequestHandler;
|
import entralinked.network.http.HttpRequestHandler;
|
||||||
@@ -91,6 +93,7 @@ public class PglHandler implements HttpHandler {
|
|||||||
|
|
||||||
if(credentials == null ||
|
if(credentials == null ||
|
||||||
!username.equals(credentials.getUsername()) || !password.equals(credentials.getPassword())) {
|
!username.equals(credentials.getUsername()) || !password.equals(credentials.getPassword())) {
|
||||||
|
logger.debug("Rejecting PGL request because the auth credentials were incorrect");
|
||||||
ctx.status(HttpStatus.UNAUTHORIZED);
|
ctx.status(HttpStatus.UNAUTHORIZED);
|
||||||
clearTasks(ctx);
|
clearTasks(ctx);
|
||||||
return;
|
return;
|
||||||
@@ -98,18 +101,21 @@ public class PglHandler implements HttpHandler {
|
|||||||
|
|
||||||
// Deserialize the request
|
// Deserialize the request
|
||||||
PglRequest request = mapper.readValue(ctx.queryString(), PglRequest.class);
|
PglRequest request = mapper.readValue(ctx.queryString(), PglRequest.class);
|
||||||
|
logger.debug("Received {}", request);
|
||||||
|
|
||||||
// Verify the service session token
|
// Verify the service session token
|
||||||
ServiceSession session = userManager.getServiceSession(request.token(), "external");
|
ServiceSession session = userManager.getServiceSession(request.token(), "external");
|
||||||
|
|
||||||
if(session == null) {
|
if(session == null) {
|
||||||
|
logger.debug("Rejecting PGL request because the service session has expired");
|
||||||
ctx.status(HttpStatus.UNAUTHORIZED);
|
ctx.status(HttpStatus.UNAUTHORIZED);
|
||||||
clearTasks(ctx);
|
clearTasks(ctx);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store request object for subsequent handlers
|
// Store attributes for subsequent handlers
|
||||||
ctx.attribute("request", request);
|
ctx.attribute("request", request);
|
||||||
|
ctx.attribute("user", session.user());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -187,6 +193,7 @@ public class PglHandler implements HttpHandler {
|
|||||||
private void handleDownloadSaveData(PglRequest request, Context ctx) throws IOException {
|
private void handleDownloadSaveData(PglRequest request, Context ctx) throws IOException {
|
||||||
LEOutputStream outputStream = new LEOutputStream(ctx.outputStream());
|
LEOutputStream outputStream = new LEOutputStream(ctx.outputStream());
|
||||||
Player player = playerManager.getPlayer(request.gameSyncId());
|
Player player = playerManager.getPlayer(request.gameSyncId());
|
||||||
|
User user = ctx.attribute("user");
|
||||||
|
|
||||||
// Check if player exists
|
// Check if player exists
|
||||||
if(player == null) {
|
if(player == null) {
|
||||||
@@ -194,19 +201,45 @@ public class PglHandler implements HttpHandler {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logger.info("Player {} is downloading save data as user {}", player.getGameSyncId(), user.getRedactedId());
|
||||||
|
|
||||||
// Write status code
|
// Write status code
|
||||||
writeStatusCode(outputStream, 0);
|
writeStatusCode(outputStream, 0);
|
||||||
|
|
||||||
// Allow it to wake up anyway, maybe the poor sap is stuck..
|
// Allow it to wake up anyway, maybe the poor sap is stuck..
|
||||||
// Just don't send any other data.
|
// Just don't send any other data.
|
||||||
if(player.getStatus() == PlayerStatus.AWAKE) {
|
if(player.getStatus() == PlayerStatus.AWAKE) {
|
||||||
logger.info("Player {} is downloading save data, but is already awake!", player.getGameSyncId());
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
List<DreamEncounter> encounters = player.getEncounters();
|
List<DreamEncounter> encounters = player.getEncounters();
|
||||||
List<DreamItem> items = player.getItems();
|
List<DreamItem> items = player.getItems();
|
||||||
|
|
||||||
|
// Prepare DLC information
|
||||||
|
String cgearType = player.getGameVersion().isVersion2() ? "CGEAR2" : "CGEAR";
|
||||||
|
String cgearSkin = player.getCGearSkin();
|
||||||
|
String dexSkin = player.getDexSkin();
|
||||||
|
int cgearSkinIndex = 0;
|
||||||
|
int dexSkinIndex = 0;
|
||||||
|
|
||||||
|
// Create or remove custom C-Gear skin DLC override
|
||||||
|
if("custom".equals(cgearSkin)) {
|
||||||
|
cgearSkinIndex = 1;
|
||||||
|
user.setDlcOverride(cgearType, player.getCGearSkinFile());
|
||||||
|
} else {
|
||||||
|
cgearSkinIndex = dlcList.getDlcIndex("IRAO", cgearType, cgearSkin);
|
||||||
|
user.removeDlcOverride(cgearType);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create or remove custom Pokédex skin DLC override
|
||||||
|
if("custom".equals(dexSkin)) {
|
||||||
|
dexSkinIndex = 1;
|
||||||
|
user.setDlcOverride("ZUKAN", player.getDexSkinFile());
|
||||||
|
} else {
|
||||||
|
dexSkinIndex = dlcList.getDlcIndex("IRAO", "ZUKAN", dexSkin);
|
||||||
|
user.removeDlcOverride("ZUKAN");
|
||||||
|
}
|
||||||
|
|
||||||
// When waking up a Pokémon, these 4 bytes are written to 0x1D304 in the save file.
|
// When waking up a Pokémon, these 4 bytes are written to 0x1D304 in the save file.
|
||||||
// If the bytes in the game's save file match the new bytes, they will be set to 0x00000000
|
// If the bytes in the game's save file match the new bytes, they will be set to 0x00000000
|
||||||
// and no content will be downloaded.
|
// and no content will be downloaded.
|
||||||
@@ -229,8 +262,8 @@ public class PglHandler implements HttpHandler {
|
|||||||
outputStream.writeShort(player.getLevelsGained());
|
outputStream.writeShort(player.getLevelsGained());
|
||||||
outputStream.write(0); // Unknown
|
outputStream.write(0); // Unknown
|
||||||
outputStream.write(dlcList.getDlcIndex("IRAO", "MUSICAL", player.getMusical()));
|
outputStream.write(dlcList.getDlcIndex("IRAO", "MUSICAL", player.getMusical()));
|
||||||
outputStream.write(dlcList.getDlcIndex("IRAO", player.getGameVersion().isVersion2() ? "CGEAR2" : "CGEAR", player.getCGearSkin()));
|
outputStream.write(cgearSkinIndex);
|
||||||
outputStream.write(dlcList.getDlcIndex("IRAO", "ZUKAN", player.getDexSkin()));
|
outputStream.write(dexSkinIndex);
|
||||||
outputStream.write(decorList.isEmpty() ? 0 : 1); // Seems to be a flag for indicating whether or not decor data is present
|
outputStream.write(decorList.isEmpty() ? 0 : 1); // Seems to be a flag for indicating whether or not decor data is present
|
||||||
outputStream.write(0); // Must be zero?
|
outputStream.write(0); // Must be zero?
|
||||||
|
|
||||||
@@ -278,15 +311,15 @@ public class PglHandler implements HttpHandler {
|
|||||||
byte[] nameBytes = visitor.name().getBytes(StandardCharsets.UTF_16LE);
|
byte[] nameBytes = visitor.name().getBytes(StandardCharsets.UTF_16LE);
|
||||||
outputStream.write(nameBytes, 0, Math.min(14, nameBytes.length));
|
outputStream.write(nameBytes, 0, Math.min(14, nameBytes.length));
|
||||||
outputStream.writeBytes(-1, 14 - nameBytes.length);
|
outputStream.writeBytes(-1, 14 - nameBytes.length);
|
||||||
|
outputStream.writeShort(0xFF); // Null terminator
|
||||||
|
|
||||||
// Full visitor type consists of a trainer class and what I call a 'personality' index
|
// Full visitor type consists of a trainer class and what I call a 'personality' index
|
||||||
// that, along with the trainer class, determines which phrases the visitor uses.
|
// that, along with the trainer class, determines which phrases the visitor uses.
|
||||||
// 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.
|
// 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.
|
// For example, if the visitor type is '0', then shop type '0' would be a raffle.
|
||||||
// However, if the visitor type is '2', then shop type '0' results in a dojo instead.
|
// However, if the visitor type is '2', then shop type '0' results in a dojo instead.
|
||||||
int visitorType = visitor.type().getClientId() + visitor.personality() * 8;
|
int visitorType = visitor.type().getClientId() + visitor.personality() * 8;
|
||||||
outputStream.writeShort(-1); // Does nothing, seems to be read as part of the name.
|
|
||||||
outputStream.write(visitorType);
|
outputStream.write(visitorType);
|
||||||
outputStream.write(visitor.shopType().ordinal() + (7 - visitorType * 2 % 7));
|
outputStream.write(visitor.shopType().ordinal() + (7 - visitorType * 2 % 7));
|
||||||
outputStream.writeShort(0); // Does nothing
|
outputStream.writeShort(0); // Does nothing
|
||||||
@@ -312,6 +345,7 @@ public class PglHandler implements HttpHandler {
|
|||||||
private void handleMemoryLink(PglRequest request, Context ctx) throws IOException {
|
private void handleMemoryLink(PglRequest request, Context ctx) throws IOException {
|
||||||
LEOutputStream outputStream = new LEOutputStream(ctx.outputStream());
|
LEOutputStream outputStream = new LEOutputStream(ctx.outputStream());
|
||||||
Player player = playerManager.getPlayer(request.gameSyncId());
|
Player player = playerManager.getPlayer(request.gameSyncId());
|
||||||
|
User user = ctx.attribute("user");
|
||||||
|
|
||||||
// Check if player exists
|
// Check if player exists
|
||||||
if(player == null) {
|
if(player == null) {
|
||||||
@@ -319,6 +353,12 @@ public class PglHandler implements HttpHandler {
|
|||||||
return;
|
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
|
// Check if the save file belongs to Black or White
|
||||||
if(player.getGameVersion().isVersion2()) {
|
if(player.getGameVersion().isVersion2()) {
|
||||||
writeStatusCode(outputStream, 10); // Not a Black or White save
|
writeStatusCode(outputStream, 10); // Not a Black or White save
|
||||||
@@ -326,13 +366,15 @@ public class PglHandler implements HttpHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check if the game save data exists
|
// Check if the game save data exists
|
||||||
File file = playerManager.getPlayerGameSaveFile(player);
|
File file = player.getSaveFile();
|
||||||
|
|
||||||
if(!file.exists()) {
|
if(!file.exists()) {
|
||||||
writeStatusCode(outputStream, 5); // No game save data exists for this Game Sync ID
|
writeStatusCode(outputStream, 5); // No game save data exists for this Game Sync ID
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logger.info("User {} is Memory Linking with player {}", user.getRedactedId(), player.getGameSyncId());
|
||||||
|
|
||||||
// Send the save data!
|
// Send the save data!
|
||||||
try(FileInputStream inputStream = new FileInputStream(file)) {
|
try(FileInputStream inputStream = new FileInputStream(file)) {
|
||||||
writeStatusCode(outputStream, 0);
|
writeStatusCode(outputStream, 0);
|
||||||
@@ -391,13 +433,15 @@ public class PglHandler implements HttpHandler {
|
|||||||
* POST handler for {@code /dsio/gw?p=savedata.upload}
|
* POST handler for {@code /dsio/gw?p=savedata.upload}
|
||||||
*/
|
*/
|
||||||
private void handleUploadSaveData(PglRequest request, Context ctx) throws IOException {
|
private void handleUploadSaveData(PglRequest request, Context ctx) throws IOException {
|
||||||
// Prepare response
|
|
||||||
LEOutputStream outputStream = new LEOutputStream(ctx.outputStream());
|
LEOutputStream outputStream = new LEOutputStream(ctx.outputStream());
|
||||||
|
|
||||||
// Check if the player exists and does not already have a Pokémon tucked in
|
|
||||||
Player player = playerManager.getPlayer(request.gameSyncId());
|
Player player = playerManager.getPlayer(request.gameSyncId());
|
||||||
|
User user = ctx.attribute("user");
|
||||||
|
|
||||||
if(player == null || (player.getStatus() != PlayerStatus.AWAKE && !configuration.allowOverwritingPlayerDreamInfo())) {
|
// Check if the player exists, has no Pokémon tucked in already and uses the same game version
|
||||||
|
if(player == null
|
||||||
|
|| (!configuration.allowOverwritingPlayerDreamInfo() && player.getStatus() != PlayerStatus.AWAKE)
|
||||||
|
|| (!configuration.allowPlayerGameVersionMismatch() && player.getGameVersion() != null
|
||||||
|
&& request.gameVersion() != player.getGameVersion())) {
|
||||||
// Skip everything
|
// Skip everything
|
||||||
ServletInputStream inputStream = ctx.req().getInputStream();
|
ServletInputStream inputStream = ctx.req().getInputStream();
|
||||||
|
|
||||||
@@ -410,6 +454,8 @@ public class PglHandler implements HttpHandler {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logger.info("Player {} is uploading save data as user {}", player.getGameSyncId(), user.getRedactedId());
|
||||||
|
|
||||||
// Try to store save data
|
// Try to store save data
|
||||||
if(!playerManager.storePlayerGameSaveFile(player, ctx.bodyInputStream())) {
|
if(!playerManager.storePlayerGameSaveFile(player, ctx.bodyInputStream())) {
|
||||||
writeStatusCode(outputStream, 4); // Game save data IO error
|
writeStatusCode(outputStream, 4); // Game save data IO error
|
||||||
@@ -419,7 +465,7 @@ public class PglHandler implements HttpHandler {
|
|||||||
// Read save data
|
// Read save data
|
||||||
PkmnInfo dreamerInfo = null;
|
PkmnInfo dreamerInfo = null;
|
||||||
|
|
||||||
try(FileInputStream inputStream = new FileInputStream(playerManager.getPlayerGameSaveFile(player))) {
|
try(FileInputStream inputStream = new FileInputStream(player.getSaveFile())) {
|
||||||
inputStream.skip(0x1D300); // Skip to dream world data
|
inputStream.skip(0x1D300); // Skip to dream world data
|
||||||
inputStream.skip(8); // Skip to Pokémon data
|
inputStream.skip(8); // Skip to Pokémon data
|
||||||
dreamerInfo = PkmnInfoReader.readPokeInfo(inputStream);
|
dreamerInfo = PkmnInfoReader.readPokeInfo(inputStream);
|
||||||
@@ -444,12 +490,8 @@ public class PglHandler implements HttpHandler {
|
|||||||
* POST handler for {@code /dsio/gw?p=account.create.upload}
|
* POST handler for {@code /dsio/gw?p=account.create.upload}
|
||||||
*/
|
*/
|
||||||
private void handleCreateAccount(PglRequest request, Context ctx) throws IOException {
|
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
|
// Have to read all the bytes first for some reason
|
||||||
ServletInputStream inputStream = ctx.req().getInputStream();
|
byte[] bytes = ctx.bodyAsBytes();
|
||||||
|
|
||||||
while(!inputStream.isFinished()) {
|
|
||||||
inputStream.read();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prepare response
|
// Prepare response
|
||||||
LEOutputStream outputStream = new LEOutputStream(ctx.outputStream());
|
LEOutputStream outputStream = new LEOutputStream(ctx.outputStream());
|
||||||
@@ -467,11 +509,19 @@ public class PglHandler implements HttpHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Try to register player
|
// 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
|
writeStatusCode(outputStream, 3); // Registration error
|
||||||
return;
|
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
|
// Write status code
|
||||||
writeStatusCode(outputStream, 0);
|
writeStatusCode(outputStream, 0);
|
||||||
}
|
}
|
||||||
@@ -492,7 +542,8 @@ public class PglHandler implements HttpHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Try to register player
|
// 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
|
writeStatusCode(outputStream, 3); // Registration error
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,4 +17,11 @@ public record PglRequest(
|
|||||||
public GameVersion gameVersion() {
|
public GameVersion gameVersion() {
|
||||||
return GameVersion.lookup(romCode(), languageCode());
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,7 +55,6 @@ public class CertificateGenerator {
|
|||||||
+ "Fw2ewOxmIyw3d4HFGrLx9yvk6Q5HpTlSgvbgO22k/vo0N43dO7utgfn/e4Xo9alIAktnewGqj5nRm75H7iaLEJBo983jJdugeXdp6y5X6sDmaDmyw/p91TKPrKmD"
|
+ "Fw2ewOxmIyw3d4HFGrLx9yvk6Q5HpTlSgvbgO22k/vo0N43dO7utgfn/e4Xo9alIAktnewGqj5nRm75H7iaLEJBo983jJdugeXdp6y5X6sDmaDmyw/p91TKPrKmD"
|
||||||
+ "4ebbTY38D5WT67fONjW/X1OLNHg46GYYZa6etbxQ7671f7GypgwnI=";
|
+ "4ebbTY38D5WT67fONjW/X1OLNHg46GYYZa6etbxQ7671f7GypgwnI=";
|
||||||
|
|
||||||
|
|
||||||
private static final String issuerPrivateKeyString =
|
private static final String issuerPrivateKeyString =
|
||||||
"eNod0Em2a0AAANAFGRBNYVgK0QalfWaIPproZfX/nH+XcG1dR4mnSxCaUu3JKUe5WLR1rND+Q+gwb3NO3ws5e0YXcydZcUtNV/35votzw9SsDstssI0TPxg5q1XPUq"
|
"eNod0Em2a0AAANAFGRBNYVgK0QalfWaIPproZfX/nH+XcG1dR4mnSxCaUu3JKUe5WLR1rND+Q+gwb3NO3ws5e0YXcydZcUtNV/35votzw9SsDstssI0TPxg5q1XPUq"
|
||||||
+ "EpGgfib4UnATQKiAcZHsBOsEWg2nShyhd7CoLQznBPWW/+iLfW3bMujf723htqG+n0p30h/SClZIRZibH7I5hGgQHRqgvpuJ/IDWpH9HQZelCC0xPK8uXZ87jcwv"
|
+ "EpGgfib4UnATQKiAcZHsBOsEWg2nShyhd7CoLQznBPWW/+iLfW3bMujf723htqG+n0p30h/SClZIRZibH7I5hGgQHRqgvpuJ/IDWpH9HQZelCC0xPK8uXZ87jcwv"
|
||||||
|
|||||||
48
src/main/java/entralinked/utility/ColorUtility.java
Normal file
48
src/main/java/entralinked/utility/ColorUtility.java
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
package entralinked.utility;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility class for various color-related shenanigans.
|
||||||
|
*/
|
||||||
|
public class ColorUtility {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Multiplies the red, green and blue components of the specified RGB888 color value by the given factor.
|
||||||
|
*
|
||||||
|
* @param color The color to multiply.
|
||||||
|
* @param factor The factor to use.
|
||||||
|
* @return The multiplied color.
|
||||||
|
*/
|
||||||
|
public static int multiplyColor(int color, double factor) {
|
||||||
|
int red = color >> 16 & 0xFF;
|
||||||
|
int green = color >> 8 & 0xFF;
|
||||||
|
int blue = color & 0xFF;
|
||||||
|
red = Math.max(0, Math.min(255, (int)(red * factor)));
|
||||||
|
green = Math.max(0, Math.min(255, (int)(green * factor)));
|
||||||
|
blue = Math.max(0, Math.min(255, (int)(blue * factor)));
|
||||||
|
return (red << 16) | (green << 8) | blue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts the input BGR555 color value to an RGB888 color value.
|
||||||
|
*
|
||||||
|
* @return The RGB888 color value.
|
||||||
|
*/
|
||||||
|
public static int convertBGR555ToRGB888(int color) {
|
||||||
|
int red = (color & 0x1F) << 3;
|
||||||
|
int green = (color >> 5 & 0x1F) << 3;
|
||||||
|
int blue = (color >> 10 & 0x1F) << 3;
|
||||||
|
return ((red | red >> 5) << 16) | ((green | green >> 5) << 8) | (blue | blue >> 5);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts the input RGB888 color value to a BGR555 color value.
|
||||||
|
*
|
||||||
|
* @return The BGR555 color value.
|
||||||
|
*/
|
||||||
|
public static int convertRGB888ToBGR555(int color) {
|
||||||
|
int red = color >> 16 & 0xFF;
|
||||||
|
int green = color >> 8 & 0xFF;
|
||||||
|
int blue = color & 0xFF;
|
||||||
|
return (red >> 3) | (green >> 3 << 5) | (blue >> 3 << 10);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ import java.util.Map;
|
|||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
import org.apache.logging.log4j.Level;
|
||||||
import org.apache.logging.log4j.core.Appender;
|
import org.apache.logging.log4j.core.Appender;
|
||||||
import org.apache.logging.log4j.core.Core;
|
import org.apache.logging.log4j.core.Core;
|
||||||
import org.apache.logging.log4j.core.Filter;
|
import org.apache.logging.log4j.core.Filter;
|
||||||
@@ -25,7 +26,8 @@ import org.apache.logging.log4j.core.config.plugins.PluginFactory;
|
|||||||
printObject = true)
|
printObject = true)
|
||||||
public class ConsumerAppender extends AbstractAppender {
|
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,
|
protected ConsumerAppender(String name, Filter filter, Layout<? extends Serializable> layout,
|
||||||
boolean ignoreExceptions, Property[] properties) {
|
boolean ignoreExceptions, Property[] properties) {
|
||||||
@@ -41,8 +43,8 @@ public class ConsumerAppender extends AbstractAppender {
|
|||||||
return new ConsumerAppender(name, filter, layout, ignoreExceptions, null);
|
return new ConsumerAppender(name, filter, layout, ignoreExceptions, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void addConsumer(String appenderName, Consumer<String> consumer) {
|
public static void addConsumer(String appenderName, Consumer<LogMessage> consumer) {
|
||||||
List<Consumer<String>> consumers = consumerMap.getOrDefault(appenderName, new ArrayList<>());
|
List<Consumer<LogMessage>> consumers = consumerMap.getOrDefault(appenderName, new ArrayList<>());
|
||||||
consumers.add(consumer);
|
consumers.add(consumer);
|
||||||
consumerMap.putIfAbsent(appenderName, consumers);
|
consumerMap.putIfAbsent(appenderName, consumers);
|
||||||
}
|
}
|
||||||
@@ -50,11 +52,12 @@ public class ConsumerAppender extends AbstractAppender {
|
|||||||
@Override
|
@Override
|
||||||
public void append(LogEvent event) {
|
public void append(LogEvent event) {
|
||||||
String formattedMessage = getLayout().toSerializable(event).toString();
|
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) {
|
if(consumers != null) {
|
||||||
for(Consumer<String> consumer : consumers) {
|
for(Consumer<LogMessage> consumer : consumers) {
|
||||||
consumer.accept(formattedMessage);
|
consumer.accept(logMessage);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
51
src/main/java/entralinked/utility/LEInputStream.java
Normal file
51
src/main/java/entralinked/utility/LEInputStream.java
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
package entralinked.utility;
|
||||||
|
|
||||||
|
import java.io.FilterInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Like {@link LEOutputStream}, but {@link InputStream}.
|
||||||
|
*/
|
||||||
|
public class LEInputStream extends FilterInputStream {
|
||||||
|
|
||||||
|
protected final byte[] buffer = new byte[8];
|
||||||
|
|
||||||
|
public LEInputStream(InputStream inputStream) {
|
||||||
|
super(inputStream);
|
||||||
|
}
|
||||||
|
|
||||||
|
public short readShort() throws IOException {
|
||||||
|
in.read(buffer, 0, 2);
|
||||||
|
return (short)(buffer[0] & 0xFF
|
||||||
|
| ((buffer[1] & 0xFF) << 8));
|
||||||
|
}
|
||||||
|
|
||||||
|
public int readInt() throws IOException {
|
||||||
|
in.read(buffer, 0, 4);
|
||||||
|
return buffer[0] & 0xFF
|
||||||
|
| ((buffer[1] & 0xFF) << 8)
|
||||||
|
| ((buffer[2] & 0xFF) << 16)
|
||||||
|
| ((buffer[3] & 0xFF) << 24);
|
||||||
|
}
|
||||||
|
|
||||||
|
public double readFloat() throws IOException {
|
||||||
|
return Float.intBitsToFloat(readInt());
|
||||||
|
}
|
||||||
|
|
||||||
|
public long readLong() throws IOException {
|
||||||
|
in.read(buffer, 0, 8);
|
||||||
|
return buffer[0] & 0xFF
|
||||||
|
| ((buffer[1] & 0xFF) << 8)
|
||||||
|
| ((buffer[2] & 0xFF) << 16)
|
||||||
|
| ((long)(buffer[3] & 0xFF) << 24)
|
||||||
|
| ((long)(buffer[4] & 0xFF) << 32)
|
||||||
|
| ((long)(buffer[5] & 0xFF) << 40)
|
||||||
|
| ((long)(buffer[6] & 0xFF) << 48)
|
||||||
|
| ((long)(buffer[7] & 0xFF) << 56);
|
||||||
|
}
|
||||||
|
|
||||||
|
public double readDouble() throws IOException {
|
||||||
|
return Double.longBitsToDouble(readLong());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,9 @@
|
|||||||
package entralinked.utility;
|
package entralinked.utility;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Files;
|
||||||
import java.security.MessageDigest;
|
import java.security.MessageDigest;
|
||||||
import java.security.NoSuchAlgorithmException;
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
|
||||||
@@ -18,9 +21,23 @@ public class MD5 {
|
|||||||
private static MessageDigest digest;
|
private static MessageDigest digest;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return A hex-formatted MD5 hash of the specified input.
|
* @return A hex-formatted MD5 hash of the specified input string.
|
||||||
*/
|
*/
|
||||||
public static String digest(String string) {
|
public static String digest(String string) {
|
||||||
|
return StringUtil.toHexStringPadded(digest(string.getBytes(StandardCharsets.ISO_8859_1)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return A hex-formatted MD5 hash of the specified input file.
|
||||||
|
*/
|
||||||
|
public static String digest(File file) throws IOException {
|
||||||
|
return StringUtil.toHexStringPadded(digest(Files.readAllBytes(file.toPath())));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return An MD5 hash of the specified input.
|
||||||
|
*/
|
||||||
|
public static byte[] digest(byte[] bytes) {
|
||||||
if(digest == null) {
|
if(digest == null) {
|
||||||
try {
|
try {
|
||||||
digest = MessageDigest.getInstance("MD5");
|
digest = MessageDigest.getInstance("MD5");
|
||||||
@@ -29,6 +46,6 @@ public class MD5 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return StringUtil.toHexStringPadded(digest.digest(string.getBytes(StandardCharsets.ISO_8859_1)));
|
return digest.digest(bytes);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
94
src/main/java/entralinked/utility/MysteryGiftUtility.java
Normal file
94
src/main/java/entralinked/utility/MysteryGiftUtility.java
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
package entralinked.utility;
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
|
||||||
|
import org.bouncycastle.util.Arrays;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Haphazardly thrown-together utility for generating DLS Mystery Gift data & removing version/language locks.
|
||||||
|
*/
|
||||||
|
public class MysteryGiftUtility {
|
||||||
|
|
||||||
|
public static final String DPPGS_CHARTABLE = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿŒœŞşªºááá$¡¿!?,.…·/‘'“”„«»()♂♀+-*#=&~:;♠♣♥♦★◉○□△◇@♪%ááááááááááá ";
|
||||||
|
|
||||||
|
private static byte[] encodeDPPGS(String string) {
|
||||||
|
int length = string.length();
|
||||||
|
byte[] bytes = new byte[length * 2];
|
||||||
|
|
||||||
|
for(int i = 0; i < length; i++) {
|
||||||
|
char character = string.charAt(i);
|
||||||
|
int encoded = character == '\n' ? 0xE000 : DPPGS_CHARTABLE.indexOf(character) + 0x121;
|
||||||
|
|
||||||
|
if(encoded == 0x120) {
|
||||||
|
encoded = 0x1DE;
|
||||||
|
}
|
||||||
|
|
||||||
|
bytes[i * 2] = (byte)(encoded & 0xFF);
|
||||||
|
bytes[i * 2 + 1] = (byte)((encoded >> 8) & 0xFF);
|
||||||
|
}
|
||||||
|
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static byte[] createUniversalGiftData4(byte[] bytes) {
|
||||||
|
return createUniversalGiftData4(bytes, "Here's your Mystery Gift.\nEnjoy!");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static byte[] createUniversalGiftData4(byte[] bytes, String title) {
|
||||||
|
// Check data size
|
||||||
|
if(bytes.length > 936) {
|
||||||
|
throw new IllegalArgumentException("Data too large: %s".formatted(bytes.length));
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] result = new byte[936];
|
||||||
|
|
||||||
|
if(bytes.length <= 856) {
|
||||||
|
// Create gift title data
|
||||||
|
System.arraycopy(bytes, 0x00, result, 0x50, bytes.length);
|
||||||
|
Arrays.fill(result, 0x00, 0x48, (byte)0xFF);
|
||||||
|
byte[] titleBytes = encodeDPPGS(title);
|
||||||
|
System.arraycopy(titleBytes, 0, result, 0, Math.min(0x48, titleBytes.length));
|
||||||
|
|
||||||
|
// Wonder card index (prevents duplicate redemptions)
|
||||||
|
int id = Crc16.calc(result, 0x00, 0x3A8); // Let's just use the checksum for now
|
||||||
|
result[0x4C] = (byte)(id & 0xFF);
|
||||||
|
result[0x4D] = (byte)((id >> 8) & 0xFF);
|
||||||
|
|
||||||
|
// Wonder card present flag?
|
||||||
|
result[0x4E] = (byte)(bytes.length == 0x358 ? 0x0D : 0x00);
|
||||||
|
} else {
|
||||||
|
System.arraycopy(bytes, 0, result, 0, bytes.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear game version
|
||||||
|
result[0x48] = 0;
|
||||||
|
result[0x49] = 0;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static byte[] createUniversalGiftData5(byte[] bytes) {
|
||||||
|
// Check data size
|
||||||
|
if(bytes.length > 720) {
|
||||||
|
throw new IllegalArgumentException("Data too large: %s".formatted(bytes.length));
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] result = new byte[720];
|
||||||
|
System.arraycopy(bytes, 0, result, 0, bytes.length);
|
||||||
|
result[0xCE] = 0; // Version flag
|
||||||
|
result[0x2CB] = 0; // Language code
|
||||||
|
|
||||||
|
// Create standard gift description if there is none
|
||||||
|
if(bytes.length == 204) {
|
||||||
|
Arrays.fill(result, 0xD0, 0x2CA, (byte)0xFF);
|
||||||
|
String description = "No description is available for this gift.";
|
||||||
|
byte[] descriptionBytes = description.replace('\n', '\uFFFE').getBytes(StandardCharsets.UTF_16LE);
|
||||||
|
System.arraycopy(descriptionBytes, 0, result, 0xD0, Math.min(0x1FA, descriptionBytes.length));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recalculate checksum
|
||||||
|
int checksum = Crc16.calc(result, 0, 0x2CE);
|
||||||
|
result[0x2CE] = (byte)(checksum & 0xFF);
|
||||||
|
result[0x2CF] = (byte)((checksum >> 8) & 0xFF);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
58
src/main/java/entralinked/utility/SwingUtility.java
Normal file
58
src/main/java/entralinked/utility/SwingUtility.java
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
package entralinked.utility;
|
||||||
|
|
||||||
|
import java.awt.GridBagConstraints;
|
||||||
|
import java.awt.event.ActionEvent;
|
||||||
|
|
||||||
|
import javax.swing.AbstractAction;
|
||||||
|
import javax.swing.Action;
|
||||||
|
import javax.swing.Icon;
|
||||||
|
|
||||||
|
public class SwingUtility {
|
||||||
|
|
||||||
|
@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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,130 +0,0 @@
|
|||||||
package entralinked.utility;
|
|
||||||
|
|
||||||
import java.awt.image.BufferedImage;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Utility class for reading tiled images (C-Gear & Pokédex skin data) into a usable {@link BufferedImage}.
|
|
||||||
*/
|
|
||||||
public class TiledImageReader {
|
|
||||||
|
|
||||||
public static final int TILE_WIDTH = 8;
|
|
||||||
public static final int TILE_HEIGHT = 8;
|
|
||||||
public static final int TILE_SIZE = TILE_WIDTH * TILE_HEIGHT;
|
|
||||||
public static final int COLOR_PALETTE_SIZE = 16;
|
|
||||||
public static final int SCREEN_WIDTH = 256;
|
|
||||||
public static final int SCREEN_HEIGHT = 192;
|
|
||||||
public static final int SCREEN_TILE_COUNT = SCREEN_WIDTH * SCREEN_HEIGHT / TILE_SIZE;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Calls {@link #readTiledImage(InputStream, int, boolean)} with a tile count of 255.
|
|
||||||
*
|
|
||||||
* @param normalizeIndices Should be {@code true} if the provided C-Gear skin data is from the original Black & White.
|
|
||||||
* @return A {@link BufferedImage} representing the read C-Gear skin data.
|
|
||||||
*/
|
|
||||||
public static BufferedImage readCGearSkin(InputStream inputStream, boolean normalizeIndices) throws IOException {
|
|
||||||
return readTiledImage(inputStream, 255, normalizeIndices);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Calls {@link #readTiledImage(InputStream, int, boolean)} with a tile count of 768 and index normalization disabled.
|
|
||||||
*
|
|
||||||
* @return A {@link BufferedImage} representing the read Pokédex skin data.
|
|
||||||
*/
|
|
||||||
public static BufferedImage readDexSkin(InputStream inputStream) throws IOException {
|
|
||||||
return readTiledImage(inputStream, 768, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reads tiled image data from the provided {@link InputStream} and returns a {@link BufferedImage} representing the read image data.
|
|
||||||
*
|
|
||||||
* @param tileCount The number of tiles to be read. This should be equal to the maximum number of tiles for this image.
|
|
||||||
* @param normalizedIndices Indicates that tile indices are not linear (Black & White C-Gear skins) and should be normalized.
|
|
||||||
* @return A {@link BufferedImage} representing the read image data.
|
|
||||||
*/
|
|
||||||
public static BufferedImage readTiledImage(InputStream inputStream, int tileCount, boolean normalizeIndices) throws IOException {
|
|
||||||
BufferedImage image = new BufferedImage(SCREEN_WIDTH, SCREEN_HEIGHT, BufferedImage.TYPE_INT_RGB); // Result image
|
|
||||||
int[] tileData = new int[tileCount * TILE_SIZE];
|
|
||||||
int[] tileIndices = new int[tileCount]; // Tile index lookup table
|
|
||||||
int[] colorPalette = new int[COLOR_PALETTE_SIZE];
|
|
||||||
|
|
||||||
// Read tile data.
|
|
||||||
for(int i = 0; i < tileCount; i++) {
|
|
||||||
for(int j = 0; j < TILE_SIZE / 2; j++) {
|
|
||||||
int paletteIndices = inputStream.read(); // Contains color palette indices for 2 adjacent pixels.
|
|
||||||
tileData[i * TILE_SIZE + j * 2] = paletteIndices & (COLOR_PALETTE_SIZE - 1);
|
|
||||||
tileData[i * TILE_SIZE + j * 2 + 1] = (paletteIndices >> 4) & (COLOR_PALETTE_SIZE - 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Store the index of the tile as it would be in memory so we can look it up later when we're mapping the tiles.
|
|
||||||
tileIndices[i] = normalizeIndices ? i + i / 17 * 15 + 0xA0A0 : i;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read color data.
|
|
||||||
// Pokédex skins contain room for 240 extra colors, 64 of which are defined and appear to be used as the
|
|
||||||
// 'background' colors in cases where the skin is only an overlay that is displayed on top of the 'true' Pokédex.
|
|
||||||
for(int i = 0; i < COLOR_PALETTE_SIZE; i++) {
|
|
||||||
int color = inputStream.read() | inputStream.read() << 8;
|
|
||||||
|
|
||||||
// Convert BGR555 to RGB888
|
|
||||||
int red = (color & 0x1F) << 3;
|
|
||||||
int green = ((color & 0x3E0) >> 5) << 3;
|
|
||||||
int blue = ((color & 0x7C00) >> 10) << 3;
|
|
||||||
colorPalette[i] = (red << 16) | (green << 8) | blue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Map tiles to the resulting image.
|
|
||||||
// In cases where the tile count is 768 or greater, which is exactly enough tiles to fill the entire screen,
|
|
||||||
// the tiles will be applied in the order they are provided and no additional mapping data will be read.
|
|
||||||
// This is always the case for Pokédex skins, and never the case for C-Gear skins.
|
|
||||||
if(tileCount < SCREEN_TILE_COUNT) {
|
|
||||||
// Not enough tiles -- read additional mapping data to figure out their placement.
|
|
||||||
for(int i = 0; i < SCREEN_TILE_COUNT; i++) {
|
|
||||||
int x = i * TILE_WIDTH % SCREEN_WIDTH;
|
|
||||||
int y = i * TILE_WIDTH / SCREEN_WIDTH * TILE_HEIGHT;
|
|
||||||
int leftBits = inputStream.read();
|
|
||||||
int rightBits = inputStream.read();
|
|
||||||
int memoryIndex = leftBits | (rightBits & ~12) << 8;
|
|
||||||
int flipBits = rightBits & 12;
|
|
||||||
|
|
||||||
// The normalized tile index is the index of the in-memory tile index in the lookup table.
|
|
||||||
int tileIndex = 0;
|
|
||||||
|
|
||||||
for(int k = 0; k < tileIndices.length; k++) {
|
|
||||||
if(memoryIndex == tileIndices[k]) {
|
|
||||||
tileIndex = k;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply the pixels of this tile to the resulting image.
|
|
||||||
for(int j = 0; j < TILE_SIZE; j++) {
|
|
||||||
// Get the color index for this pixel based on how the tile is flipped.
|
|
||||||
int tilePixelIndex = switch(flipBits) {
|
|
||||||
case 4 -> (TILE_WIDTH * (j / TILE_WIDTH) + TILE_WIDTH) - j % TILE_WIDTH - 1; // Flip horizontally
|
|
||||||
case 8 -> TILE_SIZE - (TILE_WIDTH * (j / TILE_WIDTH) + TILE_WIDTH) + j % TILE_WIDTH; // Flip vertically
|
|
||||||
case 12 -> TILE_SIZE - j - 1; // Flip horizontally & vertically
|
|
||||||
default -> j; // Don't flip
|
|
||||||
};
|
|
||||||
|
|
||||||
// Finally, set the pixel!
|
|
||||||
int paletteIndex = tileData[tileIndex * TILE_SIZE + tilePixelIndex];
|
|
||||||
image.setRGB(x + j % TILE_WIDTH, y + j / TILE_WIDTH, colorPalette[paletteIndex]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// There are enough tiles to fill up the entire screen, so let's just place them in order.
|
|
||||||
for(int i = 0; i < SCREEN_TILE_COUNT; i++) {
|
|
||||||
int x = i * TILE_WIDTH % SCREEN_WIDTH;
|
|
||||||
int y = i * TILE_WIDTH / SCREEN_WIDTH * TILE_WIDTH;
|
|
||||||
|
|
||||||
for(int j = 0; j < TILE_SIZE; j++) {
|
|
||||||
image.setRGB(x + j % TILE_WIDTH, y + j / TILE_WIDTH, colorPalette[tileData[i * TILE_SIZE + j]]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return image;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
378
src/main/java/entralinked/utility/TiledImageUtility.java
Normal file
378
src/main/java/entralinked/utility/TiledImageUtility.java
Normal file
@@ -0,0 +1,378 @@
|
|||||||
|
package entralinked.utility;
|
||||||
|
|
||||||
|
import java.awt.image.BufferedImage;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
|
||||||
|
import org.apache.logging.log4j.LogManager;
|
||||||
|
import org.apache.logging.log4j.Logger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility class for reading/writing tiled images to/from {@link BufferedImage} objects.
|
||||||
|
*/
|
||||||
|
public class TiledImageUtility {
|
||||||
|
|
||||||
|
public static final int[] DEFAULT_DEX_BACKGROUND_COLORS = {
|
||||||
|
0xE8E8E8, 0xE0E0E0, 0xD8D8D8, 0xD0D0D0, 0xD0C8C8, 0xC8C8C8, 0xC0C0C0, 0xB8B8B8, 0xB0B0B0, 0xA8A8A8, 0xA0A0A0,
|
||||||
|
0xA0A098, 0x989898, 0x909090, 0x888888, 0x808080, 0x787878, 0x707070, 0x686868, 0x606060, 0x585858, 0x505050,
|
||||||
|
0x484850, 0x484848, 0x484840, 0x404040, 0xF800F8, 0xF800F8, 0xF800F8, 0xF800F8, 0xF800F8, 0xF800F8, 0x282828,
|
||||||
|
0x202020, 0x181818, 0x101010, 0xF800F8, 0x303030, 0x404040, 0x505050, 0xF800F8, 0xF800F8, 0xF800F8, 0xF800F8,
|
||||||
|
0xF800F8, 0xF800F8, 0xF800F8, 0xF800F8, 0xF870A0, 0xF84878, 0xC03860, 0x802848, 0x482030, 0x505858, 0x909090,
|
||||||
|
0xC8C8C8, 0xE0E8E8, 0xF800F8, 0x404848, 0x707878, 0xA8B0B0, 0xF800F8, 0xF800F8, 0xF800F8}; // Unsafe
|
||||||
|
public static final int TILE_WIDTH = 8;
|
||||||
|
public static final int TILE_HEIGHT = 8;
|
||||||
|
public static final int TILE_SIZE = TILE_WIDTH * TILE_HEIGHT;
|
||||||
|
public static final int COLOR_PALETTE_SIZE = 16;
|
||||||
|
public static final int SCREEN_WIDTH = 256;
|
||||||
|
public static final int SCREEN_HEIGHT = 192;
|
||||||
|
public static final int SCREEN_SIZE = SCREEN_WIDTH * SCREEN_HEIGHT;
|
||||||
|
public static final int SCREEN_TILE_COUNT = SCREEN_SIZE / TILE_SIZE;
|
||||||
|
private static final byte[] dexBackgroundColorIndices = new byte[SCREEN_SIZE];
|
||||||
|
private static final boolean[] dexBackgroundOverrides = new boolean[SCREEN_SIZE];
|
||||||
|
private static final Logger logger = LogManager.getLogger();
|
||||||
|
|
||||||
|
static {
|
||||||
|
// Load Pokédex skin background data
|
||||||
|
try(LEInputStream inputStream = new LEInputStream(TiledImageUtility.class.getResourceAsStream("/zukan.bin"))) {
|
||||||
|
int index = 0;
|
||||||
|
int pairs = inputStream.readShort();
|
||||||
|
|
||||||
|
// Read background color indices
|
||||||
|
for(int i = 0; i < pairs; i++) {
|
||||||
|
int amount = inputStream.read();
|
||||||
|
int value = inputStream.read();
|
||||||
|
|
||||||
|
for(int j = 0; j < amount; j++) {
|
||||||
|
dexBackgroundColorIndices[index++] = (byte)(value & 63);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
index = 0;
|
||||||
|
pairs = inputStream.readShort();
|
||||||
|
|
||||||
|
// Read background override bits
|
||||||
|
for(int i = 0; i < pairs; i++) {
|
||||||
|
int amount = inputStream.readShort();
|
||||||
|
int value = inputStream.read();
|
||||||
|
|
||||||
|
for(int j = 0; j < amount; j++) {
|
||||||
|
for(int k = 0; k < 8; k++) {
|
||||||
|
dexBackgroundOverrides[index++] = (value & 1 << k) != 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch(IOException e) {
|
||||||
|
logger.error("Could not load Pokédex background data", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads a C-Gear skin from the specified {@link InputStream}.
|
||||||
|
*
|
||||||
|
* @param inputStream The input stream to read from.
|
||||||
|
* @param normalizeIndices Should be {@code true} if the provided C-Gear skin data is from the original Black & White.
|
||||||
|
* @return A {@link BufferedImage} representing the read C-Gear skin data.
|
||||||
|
*/
|
||||||
|
public static BufferedImage readCGearSkin(InputStream inputStream, boolean normalizeIndices) throws IOException {
|
||||||
|
return readTiledImage(new LEInputStream(inputStream), 255, 0, null, null, normalizeIndices);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads a Pokédex skin from the specified {@link InputStream}.
|
||||||
|
* If the skin in question is an overlay, the Pokédex background will be applied to the resulting image automatically.
|
||||||
|
*
|
||||||
|
* @param inputStream The input stream to read from.
|
||||||
|
* @param applyBackground Whether to apply the Pokédex background to the result image, or read the data as-is.
|
||||||
|
* @return A {@link BufferedImage} representing the read Pokédex skin data.
|
||||||
|
*/
|
||||||
|
public static BufferedImage readDexSkin(InputStream inputStream, boolean applyBackground) throws IOException {
|
||||||
|
return readTiledImage(new LEInputStream(inputStream), 768, 64,
|
||||||
|
applyBackground ? dexBackgroundColorIndices : null, applyBackground ? dexBackgroundOverrides : null, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads tiled image data from the provided {@link InputStream} and returns a {@link BufferedImage} representing the read image data.
|
||||||
|
*
|
||||||
|
* @param inputStream The input stream to read from.
|
||||||
|
* @param tileCount The number of tiles to be read. This should be equal to the maximum number of tiles for this image.
|
||||||
|
* @param backgroundColorCount The number of background colors this image has.
|
||||||
|
* @param backgroundColorIndices The background color indices of the background image. If {@code null}, no background will be used.
|
||||||
|
* @param backgroundOverrides An array of booleans representing pixels that forcibly use the background color. Can be {@code null}.
|
||||||
|
* @param normalizedIndices Indicates that tile indices are not linear (Black & White C-Gear skins) and should be normalized.
|
||||||
|
* @return A {@link BufferedImage} representing the read image data.
|
||||||
|
*/
|
||||||
|
private static BufferedImage readTiledImage(LEInputStream inputStream, int tileCount, int backgroundColorCount,
|
||||||
|
byte[] backgroundColorIndices, boolean[] backgroundOverrides, boolean normalizeIndices) throws IOException {
|
||||||
|
BufferedImage image = new BufferedImage(SCREEN_WIDTH, SCREEN_HEIGHT, BufferedImage.TYPE_INT_RGB); // Result image
|
||||||
|
int[] tileData = new int[tileCount * TILE_SIZE];
|
||||||
|
int[] tileIndices = new int[tileCount]; // Tile index lookup table
|
||||||
|
int[] colorPalette = new int[COLOR_PALETTE_SIZE];
|
||||||
|
int[] backgroundColorPalette = new int[backgroundColorCount];
|
||||||
|
|
||||||
|
// Read tile data.
|
||||||
|
for(int i = 0; i < tileCount; i++) {
|
||||||
|
byte[] rawTileData = inputStream.readNBytes(TILE_SIZE / 2);
|
||||||
|
|
||||||
|
for(int j = 0; j < rawTileData.length; j++) {
|
||||||
|
int paletteIndices = rawTileData[j]; // Contains color palette indices for 2 adjacent pixels.
|
||||||
|
tileData[i * TILE_SIZE + j * 2] = paletteIndices & (COLOR_PALETTE_SIZE - 1);
|
||||||
|
tileData[i * TILE_SIZE + j * 2 + 1] = (paletteIndices >> 4) & (COLOR_PALETTE_SIZE - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store the index of the tile as it would be in memory so we can look it up later when we're mapping the tiles.
|
||||||
|
tileIndices[i] = normalizeIndices ? i + i / 17 * 15 + 0xA0A0 : i;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read foreground color data.
|
||||||
|
// In cases where background colors are present, pixels that use the *first* foreground color
|
||||||
|
// will be replaced by the background color at that pixel's location.
|
||||||
|
for(int i = 0; i < COLOR_PALETTE_SIZE; i++) {
|
||||||
|
int color = inputStream.readShort();
|
||||||
|
|
||||||
|
// The game seems to always replace the first color of the foreground with the background,
|
||||||
|
// so let's just set it to black so that C-Gear skin previews are more accurate.
|
||||||
|
// This won't do anything for Pokédex skins, as they use a special background color palette.
|
||||||
|
colorPalette[i] = i == 0 ? 0 : ColorUtility.convertBGR555ToRGB888(color);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read background color data.
|
||||||
|
// Pokédex skins contain room for 240 extra colors, 64 of which are defined and appear to be used as the
|
||||||
|
// 'background' colors in cases where the skin is only an overlay that is displayed on top of the 'true' Pokédex.
|
||||||
|
for(int i = 0; i < backgroundColorCount; i++) {
|
||||||
|
backgroundColorPalette[i] = ColorUtility.convertBGR555ToRGB888(inputStream.readShort());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map tiles to the resulting image.
|
||||||
|
// In cases where the tile count is 768 or greater, which is exactly enough tiles to fill the entire screen,
|
||||||
|
// the tiles will be applied in the order they are provided and no additional mapping data will be read.
|
||||||
|
// This is always the case for Pokédex skins, and never the case for C-Gear skins.
|
||||||
|
if(tileCount < SCREEN_TILE_COUNT) {
|
||||||
|
// Not enough tiles -- read additional mapping data to figure out their placement.
|
||||||
|
for(int i = 0; i < SCREEN_TILE_COUNT; i++) {
|
||||||
|
int x = i * TILE_WIDTH % SCREEN_WIDTH;
|
||||||
|
int y = i * TILE_WIDTH / SCREEN_WIDTH * TILE_HEIGHT;
|
||||||
|
int leftBits = inputStream.read();
|
||||||
|
int rightBits = inputStream.read();
|
||||||
|
int memoryIndex = leftBits | (rightBits & ~12) << 8;
|
||||||
|
int flipBits = rightBits & 12;
|
||||||
|
|
||||||
|
// The normalized tile index is the index of the in-memory tile index in the lookup table.
|
||||||
|
int tileIndex = -1;
|
||||||
|
|
||||||
|
for(int k = 0; k < tileIndices.length; k++) {
|
||||||
|
if(memoryIndex == tileIndices[k]) {
|
||||||
|
tileIndex = k;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no tile could be found, then just skip it.
|
||||||
|
// TODO It is possible that it uses background colors in this case, but just leaving the tile black should be good enough.
|
||||||
|
if(tileIndex == -1) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply the pixels of this tile to the resulting image.
|
||||||
|
for(int j = 0; j < TILE_SIZE; j++) {
|
||||||
|
// Get the color index for this pixel based on how the tile is flipped.
|
||||||
|
int tilePixelIndex = switch(flipBits) {
|
||||||
|
case 4 -> (TILE_WIDTH * (j / TILE_WIDTH) + TILE_WIDTH) - j % TILE_WIDTH - 1; // Flip horizontally
|
||||||
|
case 8 -> TILE_SIZE - (TILE_WIDTH * (j / TILE_WIDTH) + TILE_WIDTH) + j % TILE_WIDTH; // Flip vertically
|
||||||
|
case 12 -> TILE_SIZE - j - 1; // Flip horizontally & vertically
|
||||||
|
default -> j; // Don't flip
|
||||||
|
};
|
||||||
|
|
||||||
|
int pixelX = x + j % TILE_WIDTH;
|
||||||
|
int pixelY = y + j / TILE_WIDTH;
|
||||||
|
int pixelIndex = pixelY * SCREEN_WIDTH + pixelX;
|
||||||
|
int paletteIndex = tileData[tileIndex * TILE_SIZE + tilePixelIndex];
|
||||||
|
|
||||||
|
// Determine whether the foreground or background color should be used.
|
||||||
|
int color = backgroundColorIndices == null ||
|
||||||
|
(paletteIndex > 0 && (backgroundOverrides == null || !backgroundOverrides[pixelIndex]))
|
||||||
|
? colorPalette[paletteIndex] : backgroundColorPalette[backgroundColorIndices[pixelIndex]];
|
||||||
|
|
||||||
|
// Finally, set the pixel!
|
||||||
|
image.setRGB(pixelX, pixelY, color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// There are enough tiles to fill up the entire screen, so let's just place them in order.
|
||||||
|
for(int i = 0; i < SCREEN_TILE_COUNT; i++) {
|
||||||
|
int x = i * TILE_WIDTH % SCREEN_WIDTH;
|
||||||
|
int y = i * TILE_WIDTH / SCREEN_WIDTH * TILE_WIDTH;
|
||||||
|
|
||||||
|
for(int j = 0; j < TILE_SIZE; j++) {
|
||||||
|
int pixelX = x + j % TILE_WIDTH;
|
||||||
|
int pixelY = y + j / TILE_WIDTH;
|
||||||
|
int pixelIndex = pixelY * SCREEN_WIDTH + pixelX;
|
||||||
|
int paletteIndex = tileData[i * TILE_SIZE + j];
|
||||||
|
int color = backgroundColorIndices == null ||
|
||||||
|
(paletteIndex > 0 && (backgroundOverrides == null || !backgroundOverrides[pixelIndex]))
|
||||||
|
? colorPalette[paletteIndex] : backgroundColorPalette[backgroundColorIndices[pixelIndex]];
|
||||||
|
image.setRGB(pixelX, pixelY, color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return image;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes a {@link BufferedImage} to the specified {@link OutputStream} as a C-Gear skin.
|
||||||
|
*
|
||||||
|
* @param outputStream The output stream to write to.
|
||||||
|
* @param image The image to write.
|
||||||
|
* @param offsetIndices Should be {@code true} if the C-Gear skin is intended for use with the original Black & White games.
|
||||||
|
*/
|
||||||
|
public static void writeCGearSkin(OutputStream outputStream, BufferedImage image, boolean offsetIndices) throws IOException {
|
||||||
|
writeTiledImage(new LEOutputStream(outputStream), image, 255, null, offsetIndices);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes a {@link BufferedImage} to the specified {@link OutputStream} as a Pokédex skin.
|
||||||
|
*
|
||||||
|
* @param outputStream The output stream to write to.
|
||||||
|
* @param image The image to write.
|
||||||
|
* @param backgroundColors The background colors this skin will use in-game. Should contain 64 RGB888 color values.
|
||||||
|
*/
|
||||||
|
public static void writeDexSkin(OutputStream outputStream, BufferedImage image, int[] backgroundColors) throws IOException {
|
||||||
|
if(backgroundColors == null || backgroundColors.length != 64) {
|
||||||
|
throw new IllegalArgumentException("Background color array must contain 64 colors");
|
||||||
|
}
|
||||||
|
|
||||||
|
writeTiledImage(new LEOutputStream(outputStream), image, 768, backgroundColors, false);
|
||||||
|
outputStream.write(new byte[352]); // Extra data that writeTiledImage doesn't write
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes a {@link BufferedImage} to the specified {@link OutputStream} as a tiled image.
|
||||||
|
*
|
||||||
|
* @param outputStream The output stream to write to.
|
||||||
|
* @param image The image to write.
|
||||||
|
* @param tileCount The maximum number of unique tiles that can be used.
|
||||||
|
* @param backgroundColors Additional background colors that the game might use. Can be {@code null}.
|
||||||
|
* @param offsetIndices Indicates that tile indices are not linear (Black & White C-Gear skins) and should be offset.
|
||||||
|
*/
|
||||||
|
private static void writeTiledImage(LEOutputStream outputStream, BufferedImage image,
|
||||||
|
int tileCount, int[] backgroundColors, boolean offsetTileIndices) throws IOException {
|
||||||
|
int[] tileData = new int[TILE_SIZE * tileCount]; // Stores colors, NOT indices!
|
||||||
|
int[] tileMappingData = new int[SCREEN_TILE_COUNT];
|
||||||
|
int[] colorPalette = new int[COLOR_PALETTE_SIZE];
|
||||||
|
int currentTileCount = 0;
|
||||||
|
int currentColorCount = 0;
|
||||||
|
|
||||||
|
// Create tile data
|
||||||
|
for(int i = 0; i < SCREEN_TILE_COUNT; i++) {
|
||||||
|
// If tile count is limited, check if we can replicate this tile by flipping an existing tile.
|
||||||
|
if(tileCount < SCREEN_TILE_COUNT) {
|
||||||
|
boolean duplicateTile = false;
|
||||||
|
|
||||||
|
for(int j = 0; j < currentTileCount; j++) {
|
||||||
|
boolean equal = true;
|
||||||
|
boolean flipX = true;
|
||||||
|
boolean flipY = true;
|
||||||
|
boolean flipXY = true;
|
||||||
|
|
||||||
|
for(int k = 0; k < TILE_SIZE; k++) {
|
||||||
|
int x = i * TILE_WIDTH % SCREEN_WIDTH;
|
||||||
|
int y = i * TILE_WIDTH / SCREEN_WIDTH * TILE_WIDTH;
|
||||||
|
int color = image.getRGB(x + k % TILE_WIDTH, y + k / TILE_WIDTH);
|
||||||
|
equal &= color == tileData[j * TILE_SIZE + k];
|
||||||
|
flipX &= color == tileData[j * TILE_SIZE + ((TILE_WIDTH * (k / TILE_WIDTH) + TILE_WIDTH) - k % TILE_WIDTH - 1)];
|
||||||
|
flipY &= color == tileData[j * TILE_SIZE + (TILE_SIZE - (TILE_WIDTH * (k / TILE_WIDTH) + TILE_WIDTH) + k % TILE_WIDTH)];
|
||||||
|
flipXY &= color == tileData[j * TILE_SIZE + (TILE_SIZE - k - 1)];
|
||||||
|
}
|
||||||
|
|
||||||
|
int flipBits = equal ? 0 : flipX ? 4 : flipY ? 8 : flipXY ? 12 : -1;
|
||||||
|
|
||||||
|
// If this tile is equal to an existing tile or can be replicated by flipping another tile,
|
||||||
|
// create mapping data for it and skip to the next tile.
|
||||||
|
if(flipBits != -1) {
|
||||||
|
tileMappingData[i] = (offsetTileIndices ? (j + (15 * (j / 17)) + 0xA0A0) : j) | flipBits << 8;
|
||||||
|
duplicateTile = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip to next tile if it is duplicate.
|
||||||
|
if(duplicateTile) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Throw exception if a new tile needs to be added but we have reached the limit.
|
||||||
|
if(currentTileCount >= tileCount) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"Image is too complex. Only %s unique tiles (8x8 pixel sections) are permitted.".formatted(tileCount));
|
||||||
|
}
|
||||||
|
|
||||||
|
int x = i * TILE_WIDTH % SCREEN_WIDTH;
|
||||||
|
int y = i * TILE_WIDTH / SCREEN_WIDTH * TILE_WIDTH;
|
||||||
|
|
||||||
|
for(int j = 0; j < TILE_SIZE; j++) {
|
||||||
|
int pixelX = x + j % TILE_WIDTH;
|
||||||
|
int pixelY = y + j / TILE_WIDTH;
|
||||||
|
int color = image.getRGB(pixelX, pixelY);
|
||||||
|
int paletteIndex = indexOf(colorPalette, color);
|
||||||
|
|
||||||
|
// Add color to the palette if it hasn't been already.
|
||||||
|
if(paletteIndex == -1) {
|
||||||
|
// Throw exception if we have already reached the maximum amount of colors.
|
||||||
|
if(currentColorCount >= COLOR_PALETTE_SIZE) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"Too many unique colors. Only %s unique colors are permitted.".formatted(COLOR_PALETTE_SIZE));
|
||||||
|
}
|
||||||
|
|
||||||
|
paletteIndex = currentColorCount++;
|
||||||
|
colorPalette[paletteIndex] = color;
|
||||||
|
}
|
||||||
|
|
||||||
|
tileData[currentTileCount * TILE_SIZE + j] = color;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create mapping data for this tile.
|
||||||
|
tileMappingData[i] = offsetTileIndices ? (currentTileCount + currentTileCount / 17 * 15 + 0xA0A0) : currentTileCount;
|
||||||
|
currentTileCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write tile data.
|
||||||
|
for(int i = 0; i < tileData.length; i += 2) {
|
||||||
|
int paletteIndices = indexOf(colorPalette, tileData[i]) | (indexOf(colorPalette, tileData[i + 1]) << 4);
|
||||||
|
outputStream.write(paletteIndices);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write foreground color data.
|
||||||
|
for(int color : colorPalette) {
|
||||||
|
outputStream.writeShort(ColorUtility.convertRGB888ToBGR555(color));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write background color data if it is present.
|
||||||
|
if(backgroundColors != null) {
|
||||||
|
for(int color : backgroundColors) {
|
||||||
|
outputStream.writeShort(ColorUtility.convertRGB888ToBGR555(color));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write tile mapping data.
|
||||||
|
if(tileCount < SCREEN_TILE_COUNT) {
|
||||||
|
for(int i = 0; i < SCREEN_TILE_COUNT; i++) {
|
||||||
|
outputStream.writeShort(tileMappingData[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return The index of the element in the array, or {@code -1} if no such element exists.
|
||||||
|
*/
|
||||||
|
private static int indexOf(int[] array, int element) {
|
||||||
|
for(int i = 0; i < array.length; i++) {
|
||||||
|
if(element == array[i]) {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,11 +6,14 @@
|
|||||||
<body>
|
<body>
|
||||||
<div class="root-container">
|
<div class="root-container">
|
||||||
<div>
|
<div>
|
||||||
<label for="gsid">Game Sync ID</label><br>
|
<form onsubmit="return postLogin()">
|
||||||
<input type='text' id="gsid" name='gsid' placeholder='XXXXXXXXXX'>
|
<label for="gsid">Game Sync ID</label><br>
|
||||||
<button id="login" onclick="postLogin()">Login</button>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
<script src="scripts/utility.js"></script>
|
<script src="scripts/utility.js"></script>
|
||||||
<script src="scripts/login.js"></script>
|
<script src="scripts/login.js"></script>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<link rel="stylesheet" href="styles/profile.css">
|
<link rel="stylesheet" href="styles/profile.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="main-container" class="root-container" style="display:none;">
|
<div id="main-container" class="centered-container" style="display:none;">
|
||||||
<div>
|
<div>
|
||||||
<div>
|
<div>
|
||||||
<label id="game-summary" class="header-text"></label>
|
<label id="game-summary" class="header-text"></label>
|
||||||
@@ -21,24 +21,26 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<th>Species</th>
|
<th>Species</th>
|
||||||
<td id="dreamer-species"></td>
|
<td id="dreamer-species"></td>
|
||||||
<th>Nature</th>
|
<th>Ability</th>
|
||||||
<td id="dreamer-nature"></td>
|
<td id="dreamer-ability"></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Name</th>
|
<th>Name</th>
|
||||||
<td id="dreamer-name"></td>
|
<td id="dreamer-name"></td>
|
||||||
<th>Gender</th>
|
<th>Nature</th>
|
||||||
<td id="dreamer-gender"></td>
|
<td id="dreamer-nature"></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Trainer</th>
|
<th>Trainer</th>
|
||||||
<td id="dreamer-trainer"></td>
|
<td id="dreamer-trainer"></td>
|
||||||
<th>Level</th>
|
<th>Gender</th>
|
||||||
<td id="dreamer-level"></td>
|
<td id="dreamer-gender"></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Trainer ID</th>
|
<th>Trainer ID</th>
|
||||||
<td id="dreamer-trainer-id"></td>
|
<td id="dreamer-trainer-id"></td>
|
||||||
|
<th>Level</th>
|
||||||
|
<td id="dreamer-level"></td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
@@ -48,18 +50,18 @@
|
|||||||
<div>
|
<div>
|
||||||
<table id="encounter-table" class="encounter-image-table">
|
<table id="encounter-table" class="encounter-image-table">
|
||||||
<tr>
|
<tr>
|
||||||
<td><a id="encounter0" href="#configureEncounter" onclick="configureEncounter(0)"><image src="/sprites/pokemon/normal/0.png"/></a></td>
|
<td id="encounter0" onclick="configureEncounter(0)"><image src="/sprites/pokemon/normal/0.png"/></td>
|
||||||
<td><a id="encounter1" href="#configureEncounter" onclick="configureEncounter(1)"><image src="/sprites/pokemon/normal/0.png"/></a></td>
|
<td id="encounter1" onclick="configureEncounter(1)"><image src="/sprites/pokemon/normal/0.png"/></td>
|
||||||
<td><a id="encounter2" href="#configureEncounter" onclick="configureEncounter(2)"><image src="/sprites/pokemon/normal/0.png"/></a></td>
|
<td id="encounter2" onclick="configureEncounter(2)"><image src="/sprites/pokemon/normal/0.png"/></td>
|
||||||
<td><a id="encounter3" href="#configureEncounter" onclick="configureEncounter(3)"><image src="/sprites/pokemon/normal/0.png"/></a></td>
|
<td id="encounter3" onclick="configureEncounter(3)"><image src="/sprites/pokemon/normal/0.png"/></td>
|
||||||
<td><a id="encounter4" href="#configureEncounter" onclick="configureEncounter(4)"><image src="/sprites/pokemon/normal/0.png"/></a></td>
|
<td id="encounter4" onclick="configureEncounter(4)"><image src="/sprites/pokemon/normal/0.png"/></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><a id="encounter5" href="#configureEncounter" onclick="configureEncounter(5)"><image src="/sprites/pokemon/normal/0.png"/></a></td>
|
<td id="encounter5" onclick="configureEncounter(5)"><image src="/sprites/pokemon/normal/0.png"/></td>
|
||||||
<td><a id="encounter6" href="#configureEncounter" onclick="configureEncounter(6)"><image src="/sprites/pokemon/normal/0.png"/></a></td>
|
<td id="encounter6" onclick="configureEncounter(6)"><image src="/sprites/pokemon/normal/0.png"/></td>
|
||||||
<td><a id="encounter7" href="#configureEncounter" onclick="configureEncounter(7)"><image src="/sprites/pokemon/normal/0.png"/></a></td>
|
<td id="encounter7" onclick="configureEncounter(7)"><image src="/sprites/pokemon/normal/0.png"/></td>
|
||||||
<td><a id="encounter8" href="#configureEncounter" onclick="configureEncounter(8)"><image src="/sprites/pokemon/normal/0.png"/></a></td>
|
<td id="encounter8" onclick="configureEncounter(8)"><image src="/sprites/pokemon/normal/0.png"/></td>
|
||||||
<td><a id="encounter9" href="#configureEncounter" onclick="configureEncounter(9)"><image src="/sprites/pokemon/normal/0.png"/></a></td>
|
<td id="encounter9" onclick="configureEncounter(9)"><image src="/sprites/pokemon/normal/0.png"/></td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
@@ -70,20 +72,20 @@
|
|||||||
<div>
|
<div>
|
||||||
<table id="visitor-table" class="visitor-table">
|
<table id="visitor-table" class="visitor-table">
|
||||||
<tr>
|
<tr>
|
||||||
<td><a id="visitor0" href="#configureVisitor" onclick="configureVisitor(0)"><image src="/sprites/trainers/none.png"/></a></td>
|
<td id="visitor0" onclick="configureVisitor(0)"><image src="/sprites/trainers/none.png"/></td>
|
||||||
<td><a id="visitor1" href="#configureVisitor" onclick="configureVisitor(1)"><image src="/sprites/trainers/none.png"/></a></td>
|
<td id="visitor1" onclick="configureVisitor(1)"><image src="/sprites/trainers/none.png"/></td>
|
||||||
<td><a id="visitor2" href="#configureVisitor" onclick="configureVisitor(2)"><image src="/sprites/trainers/none.png"/></a></td>
|
<td id="visitor2" onclick="configureVisitor(2)"><image src="/sprites/trainers/none.png"/></td>
|
||||||
<td><a id="visitor3" href="#configureVisitor" onclick="configureVisitor(3)"><image src="/sprites/trainers/none.png"/></a></td>
|
<td id="visitor3" onclick="configureVisitor(3)"><image src="/sprites/trainers/none.png"/></td>
|
||||||
<td><a id="visitor4" href="#configureVisitor" onclick="configureVisitor(4)"><image src="/sprites/trainers/none.png"/></a></td>
|
<td id="visitor4" onclick="configureVisitor(4)"><image src="/sprites/trainers/none.png"/></td>
|
||||||
<td><a id="visitor5" href="#configureVisitor" onclick="configureVisitor(5)"><image src="/sprites/trainers/none.png"/></a></td>
|
<td id="visitor5" onclick="configureVisitor(5)"><image src="/sprites/trainers/none.png"/></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><a id="visitor6" href="#configureVisitor" onclick="configureVisitor(6)"><image src="/sprites/trainers/none.png"/></a></td>
|
<td id="visitor6" onclick="configureVisitor(6)"><image src="/sprites/trainers/none.png"/></td>
|
||||||
<td><a id="visitor7" href="#configureVisitor" onclick="configureVisitor(7)"><image src="/sprites/trainers/none.png"/></a></td>
|
<td id="visitor7" onclick="configureVisitor(7)"><image src="/sprites/trainers/none.png"/></td>
|
||||||
<td><a id="visitor8" href="#configureVisitor" onclick="configureVisitor(8)"><image src="/sprites/trainers/none.png"/></a></td>
|
<td id="visitor8" onclick="configureVisitor(8)"><image src="/sprites/trainers/none.png"/></td>
|
||||||
<td><a id="visitor9" href="#configureVisitor" onclick="configureVisitor(9)"><image src="/sprites/trainers/none.png"/></a></td>
|
<td id="visitor9" onclick="configureVisitor(9)"><image src="/sprites/trainers/none.png"/></td>
|
||||||
<td><a id="visitor10" href="#configureVisitor" onclick="configureVisitor(10)"><image src="/sprites/trainers/none.png"/></a></td>
|
<td id="visitor10" onclick="configureVisitor(10)"><image src="/sprites/trainers/none.png"/></td>
|
||||||
<td><a id="visitor11" href="#configureVisitor" onclick="configureVisitor(11)"><image src="/sprites/trainers/none.png"/></a></td>
|
<td id="visitor11" onclick="configureVisitor(11)"><image src="/sprites/trainers/none.png"/></td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
@@ -94,28 +96,28 @@
|
|||||||
<div>
|
<div>
|
||||||
<table id="item-table" class="item-table">
|
<table id="item-table" class="item-table">
|
||||||
<tr>
|
<tr>
|
||||||
<td><a id="item0" href="#configureItem" onclick="configureItem(0)"><image src="/sprites/items/0.png"/></a></td>
|
<td id="item0" onclick="configureItem(0)"><image src="/sprites/items/0.png"/></td>
|
||||||
<td><a id="item1" href="#configureItem" onclick="configureItem(1)"><image src="/sprites/items/0.png"/></a></td>
|
<td id="item1" onclick="configureItem(1)"><image src="/sprites/items/0.png"/></td>
|
||||||
<td><a id="item2" href="#configureItem" onclick="configureItem(2)"><image src="/sprites/items/0.png"/></a></td>
|
<td id="item2" onclick="configureItem(2)"><image src="/sprites/items/0.png"/></td>
|
||||||
<td><a id="item3" href="#configureItem" onclick="configureItem(3)"><image src="/sprites/items/0.png"/></a></td>
|
<td id="item3" onclick="configureItem(3)"><image src="/sprites/items/0.png"/></td>
|
||||||
<td><a id="item4" href="#configureItem" onclick="configureItem(4)"><image src="/sprites/items/0.png"/></a></td>
|
<td id="item4" onclick="configureItem(4)"><image src="/sprites/items/0.png"/></td>
|
||||||
<td><a id="item5" href="#configureItem" onclick="configureItem(5)"><image src="/sprites/items/0.png"/></a></td>
|
<td id="item5" onclick="configureItem(5)"><image src="/sprites/items/0.png"/></td>
|
||||||
<td><a id="item6" href="#configureItem" onclick="configureItem(6)"><image src="/sprites/items/0.png"/></a></td>
|
<td id="item6" onclick="configureItem(6)"><image src="/sprites/items/0.png"/></td>
|
||||||
<td><a id="item7" href="#configureItem" onclick="configureItem(7)"><image src="/sprites/items/0.png"/></a></td>
|
<td id="item7" onclick="configureItem(7)"><image src="/sprites/items/0.png"/></td>
|
||||||
<td><a id="item8" href="#configureItem" onclick="configureItem(8)"><image src="/sprites/items/0.png"/></a></td>
|
<td id="item8" onclick="configureItem(8)"><image src="/sprites/items/0.png"/></td>
|
||||||
<td><a id="item9" href="#configureItem" onclick="configureItem(9)"><image src="/sprites/items/0.png"/></a></td>
|
<td id="item9" onclick="configureItem(9)"><image src="/sprites/items/0.png"/></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><a id="item10" href="#configureItem" onclick="configureItem(10)"><image src="/sprites/items/0.png"/></a></td>
|
<td id="item10" onclick="configureItem(10)"><image src="/sprites/items/0.png"/></td>
|
||||||
<td><a id="item11" href="#configureItem" onclick="configureItem(11)"><image src="/sprites/items/0.png"/></a></td>
|
<td id="item11" onclick="configureItem(11)"><image src="/sprites/items/0.png"/></td>
|
||||||
<td><a id="item12" href="#configureItem" onclick="configureItem(12)"><image src="/sprites/items/0.png"/></a></td>
|
<td id="item12" onclick="configureItem(12)"><image src="/sprites/items/0.png"/></td>
|
||||||
<td><a id="item13" href="#configureItem" onclick="configureItem(13)"><image src="/sprites/items/0.png"/></a></td>
|
<td id="item13" onclick="configureItem(13)"><image src="/sprites/items/0.png"/></td>
|
||||||
<td><a id="item14" href="#configureItem" onclick="configureItem(14)"><image src="/sprites/items/0.png"/></a></td>
|
<td id="item14" onclick="configureItem(14)"><image src="/sprites/items/0.png"/></td>
|
||||||
<td><a id="item15" href="#configureItem" onclick="configureItem(15)"><image src="/sprites/items/0.png"/></a></td>
|
<td id="item15" onclick="configureItem(15)"><image src="/sprites/items/0.png"/></td>
|
||||||
<td><a id="item16" href="#configureItem" onclick="configureItem(16)"><image src="/sprites/items/0.png"/></a></td>
|
<td id="item16" onclick="configureItem(16)"><image src="/sprites/items/0.png"/></td>
|
||||||
<td><a id="item17" href="#configureItem" onclick="configureItem(17)"><image src="/sprites/items/0.png"/></a></td>
|
<td id="item17" onclick="configureItem(17)"><image src="/sprites/items/0.png"/></td>
|
||||||
<td><a id="item18" href="#configureItem" onclick="configureItem(18)"><image src="/sprites/items/0.png"/></a></td>
|
<td id="item18" onclick="configureItem(18)"><image src="/sprites/items/0.png"/></td>
|
||||||
<td><a id="item19" href="#configureItem" onclick="configureItem(19)"><image src="/sprites/items/0.png"/></a></td>
|
<td id="item19" onclick="configureItem(19)"><image src="/sprites/items/0.png"/></td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
@@ -129,6 +131,8 @@
|
|||||||
<option value="none">Do not change</option>
|
<option value="none">Do not change</option>
|
||||||
<option disabled>──────────────────────</option>
|
<option disabled>──────────────────────</option>
|
||||||
</select>
|
</select>
|
||||||
|
<button onclick="openFileChooser('cgear-file-input')">Upload Custom</button>
|
||||||
|
<input id="cgear-file-input" type="file" accept=".png, .bmp, .jpg, .jpeg" onchange="uploadSkin('CGEAR', this.files[0])" onclick="this.value = null;" hidden/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label>Pokédex Skin | </label><a href="#" onclick="return previewSkin('dex-skin', 'ZUKAN')">Preview</a>
|
<label>Pokédex Skin | </label><a href="#" onclick="return previewSkin('dex-skin', 'ZUKAN')">Preview</a>
|
||||||
@@ -136,6 +140,8 @@
|
|||||||
<option value="none">Do not change</option>
|
<option value="none">Do not change</option>
|
||||||
<option disabled>──────────────────────</option>
|
<option disabled>──────────────────────</option>
|
||||||
</select>
|
</select>
|
||||||
|
<button onclick="openFileChooser('dex-file-input')">Upload Custom</button>
|
||||||
|
<input id="dex-file-input" type="file" accept=".png, .bmp, .jpg, .jpeg" onchange="uploadSkin('ZUKAN', this.files[0])" onclick="this.value = null;" hidden/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label>Musical Show</label>
|
<label>Musical Show</label>
|
||||||
@@ -157,123 +163,145 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- Entree Forest Encounter Configuration Form -->
|
<!-- Entree Forest Encounter Configuration Form -->
|
||||||
<div id="configureEncounter" class="popup">
|
<div id="encounter-config" class="popup">
|
||||||
<div class="content">
|
<div class="centered-container">
|
||||||
<button class="close-button" onclick="closeEncounterForm()">X</button>
|
<div class="content">
|
||||||
<form id="encounter-form">
|
<button class="close-button" onclick="closeEncounterForm()">X</button>
|
||||||
<label for="encounter-form-species">Species</label>
|
<form id="encounter-form">
|
||||||
<select id="encounter-form-species" name="species" value="1">
|
<label for="encounter-form-species">Species</label>
|
||||||
<!-- Filled by profile.js -->
|
<select id="encounter-form-species" name="species" value="1">
|
||||||
</select>
|
<!-- Filled by profile.js -->
|
||||||
<label for="encounter-form-move">Special Move</label>
|
</select>
|
||||||
<select id="encounter-form-move" name="move" value="0">
|
<label for="encounter-form-move">Special Move</label>
|
||||||
<option value="0">None</option>
|
<select id="encounter-form-move" name="move" value="0">
|
||||||
<!-- Filled by profile.js -->
|
<option value="0">None</option>
|
||||||
</select>
|
<!-- Filled by profile.js -->
|
||||||
<label for="encounter-form-form">Form</label>
|
</select>
|
||||||
<select id="encounter-form-form" name="form" value="0">
|
<label for="encounter-form-form">Form</label>
|
||||||
<option value="0">N/A</option>
|
<select id="encounter-form-form" name="form" value="0">
|
||||||
<!-- Filled by profile.js -->
|
<option value="0">N/A</option>
|
||||||
</select>
|
<!-- Filled by profile.js -->
|
||||||
<label for="encounter-form-gender">Gender</label>
|
</select>
|
||||||
<select id="encounter-form-gender" name="gender" value="GENDERLESS">
|
<label for="encounter-form-gender">Gender</label>
|
||||||
<option value="MALE">Male</option>
|
<select id="encounter-form-gender" name="gender" value="GENDERLESS">
|
||||||
<option value="FEMALE">Female</option>
|
<option value="MALE">Male</option>
|
||||||
<option value="GENDERLESS">Random</option>
|
<option value="FEMALE">Female</option>
|
||||||
</select>
|
<option value="GENDERLESS">Random</option>
|
||||||
<label for="encounter-form-animation">Animation</label>
|
</select>
|
||||||
<select id="encounter-form-animation" name="animation">
|
<label for="encounter-form-animation">Animation</label>
|
||||||
<option value="LOOK_AROUND">Look Around</option>
|
<select id="encounter-form-animation" name="animation">
|
||||||
<option value="WALK_AROUND">Walk Around</option>
|
<option value="LOOK_AROUND">Look Around</option>
|
||||||
<option value="WALK_LOOK_AROUND">Walk and Look Around</option>
|
<option value="WALK_AROUND">Walk Around</option>
|
||||||
<option value="WALK_VERTICALLY">Walk Vertically</option>
|
<option value="WALK_LOOK_AROUND">Walk and Look Around</option>
|
||||||
<option value="WALK_HORIZONTALLY">Walk Horizontally</option>
|
<option value="WALK_VERTICALLY">Walk Vertically</option>
|
||||||
<option value="WALK_HORIZONTALLY_LOOK_AROUND">Walk Horizontally and Look Around</option>
|
<option value="WALK_HORIZONTALLY">Walk Horizontally</option>
|
||||||
<option value="SPIN_RIGHT">Spin Right</option>
|
<option value="WALK_LOOK_HORIZONTALLY">Walk Horizontally and Look Around</option>
|
||||||
<option value="SPIN_LEFT">Spin Left</option>
|
<option value="SPIN_RIGHT">Spin Right</option>
|
||||||
</select>
|
<option value="SPIN_LEFT">Spin Left</option>
|
||||||
</form>
|
</select>
|
||||||
<button class="big-button" onclick="saveEncounter()">Confirm</button>
|
</form>
|
||||||
<button class="big-button" onclick="removeEncounter()">Remove</button>
|
<button class="big-button" onclick="saveEncounter()">Confirm</button>
|
||||||
|
<button class="big-button" onclick="removeEncounter()">Remove</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- Item Configuration Form -->
|
<!-- Item Configuration Form -->
|
||||||
<div id="configureItem" class="popup">
|
<div id="item-config" class="popup">
|
||||||
<div class="content">
|
<div class="centered-container">
|
||||||
<button class="close-button" onclick="closeEncounterForm()">X</button>
|
<div class="content">
|
||||||
<form id="item-form">
|
<button class="close-button" onclick="closeItemForm()">X</button>
|
||||||
<label for="item-form-id">Item</label>
|
<form id="item-form">
|
||||||
<select id="item-form-id" name="id" value="1">
|
<label for="item-form-id">Item</label>
|
||||||
<!-- Filled by profile.js -->
|
<select id="item-form-id" name="id" value="1">
|
||||||
</select>
|
<!-- Filled by profile.js -->
|
||||||
<label for="item-form-quantity">Quantity</label>
|
</select>
|
||||||
<input id="item-form-quantity" name="quantity" type="number" value="1" min="1" max="20"/>
|
<label for="item-form-quantity">Quantity</label>
|
||||||
</form>
|
<input id="item-form-quantity" name="quantity" type="number" value="1" min="1" max="20"/>
|
||||||
<button class="big-button" onclick="saveItem()">Confirm</button>
|
</form>
|
||||||
<button class="big-button" onclick="removeItem()">Remove</button>
|
<button class="big-button" onclick="saveItem()">Confirm</button>
|
||||||
|
<button class="big-button" onclick="removeItem()">Remove</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- Join Avenue Visitor Configuration Form -->
|
<!-- Join Avenue Visitor Configuration Form -->
|
||||||
<div id="configureVisitor" class="popup">
|
<div id="visitor-config" class="popup">
|
||||||
<div class="content">
|
<div class="centered-container">
|
||||||
<button class="close-button" onclick="closeVisitorForm()">X</button>
|
<div class="content">
|
||||||
<form id="visitor-form">
|
<button class="close-button" onclick="closeVisitorForm()">X</button>
|
||||||
<label for="visitor-form-name">Name (Max. 7 characters)</label>
|
<form id="visitor-form">
|
||||||
<input id="visitor-form-name" name="name" placeholder="Trainer" maxlength="7"/>
|
<label for="visitor-form-name">Name (Max. 7 characters)</label>
|
||||||
<label for="visitor-form-type">Trainer Class</label>
|
<input id="visitor-form-name" type="text" name="name" placeholder="Trainer" maxlength="7"/>
|
||||||
<select id="visitor-form-type" name="type" value="ACE_TRAINER_MALE">
|
<label for="visitor-form-type">Trainer Class</label>
|
||||||
<option value="YOUNGSTER">Youngster</option>
|
<select id="visitor-form-type" name="type" value="ACE_TRAINER_MALE">
|
||||||
<option value="LASS">Lass</option>
|
<option value="YOUNGSTER">Youngster</option>
|
||||||
<option value="ACE_TRAINER_MALE">Ace Trainer (Male)</option>
|
<option value="LASS">Lass</option>
|
||||||
<option value="ACE_TRAINER_FEMALE">Ace Trainer (Female)</option>
|
<option value="ACE_TRAINER_MALE">Ace Trainer (Male)</option>
|
||||||
<option value="RANGER_MALE">Pokémon Ranger (Male)</option>
|
<option value="ACE_TRAINER_FEMALE">Ace Trainer (Female)</option>
|
||||||
<option value="RANGER_FEMALE">Pokémon Ranger (Female)</option>
|
<option value="RANGER_MALE">Pokémon Ranger (Male)</option>
|
||||||
<option value="BREEDER_MALE">Pokémon Breeder (Male)</option>
|
<option value="RANGER_FEMALE">Pokémon Ranger (Female)</option>
|
||||||
<option value="BREEDER_FEMALE">Pokémon Breeder (Female)</option>
|
<option value="BREEDER_MALE">Pokémon Breeder (Male)</option>
|
||||||
<option value="SCIENTIST_MALE">Scientist (Male)</option>
|
<option value="BREEDER_FEMALE">Pokémon Breeder (Female)</option>
|
||||||
<option value="SCIENTIST_FEMALE">Scientist (Female)</option>
|
<option value="SCIENTIST_MALE">Scientist (Male)</option>
|
||||||
<option value="HIKER">Hiker</option>
|
<option value="SCIENTIST_FEMALE">Scientist (Female)</option>
|
||||||
<option value="PARASOL_LADY">Parasol Lady</option>
|
<option value="HIKER">Hiker</option>
|
||||||
<option value="ROUGHNECK">Roughneck</option>
|
<option value="PARASOL_LADY">Parasol Lady</option>
|
||||||
<option value="NURSE">Nurse</option>
|
<option value="ROUGHNECK">Roughneck</option>
|
||||||
<option value="PRESCHOOLER_MALE">Preschooler (Male)</option>
|
<option value="NURSE">Nurse</option>
|
||||||
<option value="PRESCHOOLER_FEMALE">Preschooler (Female)</option>
|
<option value="PRESCHOOLER_MALE">Preschooler (Male)</option>
|
||||||
</select>
|
<option value="PRESCHOOLER_FEMALE">Preschooler (Female)</option>
|
||||||
<label for="visitor-form-shop-type">Shop Type</label>
|
</select>
|
||||||
<select id="visitor-form-shop-type" name="shopType" value="RAFFLE">
|
<label for="visitor-form-shop-type">Shop Type</label>
|
||||||
<option value="RAFFLE">Raffle Shop</option>
|
<select id="visitor-form-shop-type" name="shopType" value="RAFFLE">
|
||||||
<option value="FLORIST">Flower Shop</option>
|
<option value="RAFFLE">Raffle Shop</option>
|
||||||
<option value="SALON">Beauty Salon</option>
|
<option value="FLORIST">Flower Shop</option>
|
||||||
<option value="ANTIQUE">Antique Shop</option>
|
<option value="SALON">Beauty Salon</option>
|
||||||
<option value="DOJO">Training Dojo</option>
|
<option value="ANTIQUE">Antique Shop</option>
|
||||||
<option value="CAFE">Café</option>
|
<option value="DOJO">Training Dojo</option>
|
||||||
<option value="MARKET">Market</option>
|
<option value="CAFE">Café</option>
|
||||||
</select>
|
<option value="MARKET">Market</option>
|
||||||
<label for="visitor-form-game">Game of Origin (Affects which goods/services are sold)</label>
|
</select>
|
||||||
<select id="visitor-form-game" name="gameVersion" value="BLACK_ENGLISH">
|
<label for="visitor-form-game">Game of Origin (Affects which goods/services are sold)</label>
|
||||||
<option value="BLACK_ENGLISH">Black Version</option>
|
<select id="visitor-form-game" name="gameVersion" value="BLACK_ENGLISH">
|
||||||
<option value="WHITE_ENGLISH">White Version</option>
|
<option value="BLACK_ENGLISH">Black Version</option>
|
||||||
<option value="BLACK_2_ENGLISH">Black Version 2</option>
|
<option value="WHITE_ENGLISH">White Version</option>
|
||||||
<option value="WHITE_2_ENGLISH">White Version 2</option>
|
<option value="BLACK_2_ENGLISH">Black Version 2</option>
|
||||||
</select>
|
<option value="WHITE_2_ENGLISH">White Version 2</option>
|
||||||
<label for="visitor-form-region">Country</label>
|
</select>
|
||||||
<select id="visitor-form-region" name="region" value="1">
|
<label for="visitor-form-region">Country</label>
|
||||||
<!-- Filled by profile.js -->
|
<select id="visitor-form-region" name="region" value="1">
|
||||||
</select>
|
<!-- Filled by profile.js -->
|
||||||
<label for="visitor-form-subregion">State/Province</label>
|
</select>
|
||||||
<select id="visitor-form-subregion" name="subregion" value="0">
|
<label for="visitor-form-subregion">State/Province</label>
|
||||||
<option value="0">N/A</option>
|
<select id="visitor-form-subregion" name="subregion" value="0">
|
||||||
<!-- Filled by profile.js -->
|
<option value="0">N/A</option>
|
||||||
</select>
|
<!-- Filled by profile.js -->
|
||||||
<label for="visitor-form-personality">Personality (Affects phrases used)</label>
|
</select>
|
||||||
<input id="visitor-form-personality" name="personality" type="number" value="0" min="0" max="7"/>
|
<label for="visitor-form-personality">Personality (Affects phrases used)</label>
|
||||||
<label for="visitor-form-dreamer">Tucked-in Pokémon Species</label>
|
<input id="visitor-form-personality" name="personality" type="number" value="0" min="0" max="7"/>
|
||||||
<select id="visitor-form-dreamer" name="dreamerSpecies" value="1">
|
<label for="visitor-form-dreamer">Tucked-in Pokémon Species</label>
|
||||||
<!-- Filled by profile.js -->
|
<select id="visitor-form-dreamer" name="dreamerSpecies" value="1">
|
||||||
</select>
|
<!-- Filled by profile.js -->
|
||||||
</form>
|
</select>
|
||||||
<button class="big-button" onclick="saveVisitor()">Confirm</button>
|
</form>
|
||||||
<button class="big-button" onclick="removeVisitor()">Remove</button>
|
<button class="big-button" onclick="saveVisitor()">Confirm</button>
|
||||||
|
<button class="big-button" onclick="removeVisitor()">Remove</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="skin-preview" class="popup">
|
||||||
|
<div class="centered-container">
|
||||||
|
<div id="skin-preview-content" class="content">
|
||||||
|
<button class="close-button" onclick="closeSkinPreview()">X</button>
|
||||||
|
<label>Skin Preview</label><br>
|
||||||
|
<img id="skin-preview-image" width=256 height=192/>
|
||||||
|
<div>
|
||||||
|
<label>Resolution</label><br>
|
||||||
|
<input id="resolution-x1" type="radio" name="resolution" onclick="setSkinPreviewResolution(256, 192);" checked/>
|
||||||
|
<label for="resolution-x1">x1</label>
|
||||||
|
<input id="resolution-x2" type="radio" name="resolution" onclick="setSkinPreviewResolution(512, 384);"/>
|
||||||
|
<label for="resolution-x2">x2</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -10,4 +10,6 @@ function postLogin() {
|
|||||||
window.location.href = "/dashboard/profile.html";
|
window.location.href = "/dashboard/profile.html";
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
410
src/main/resources/dashboard/scripts/pokedata.js
vendored
410
src/main/resources/dashboard/scripts/pokedata.js
vendored
@@ -1,7 +1,9 @@
|
|||||||
const POKE_SPECIES_MAP = {};
|
const SPECIES_MAP = {};
|
||||||
const POKE_SPECIES_LIST = [];
|
const SPECIES_LIST = [];
|
||||||
const POKE_MOVE_MAP = {};
|
const MOVE_MAP = {};
|
||||||
const POKE_MOVE_LIST = [];
|
const MOVE_LIST = [];
|
||||||
|
const ABILITY_MAP = {};
|
||||||
|
const ABILITY_LIST = [];
|
||||||
const ITEM_MAP = {};
|
const ITEM_MAP = {};
|
||||||
const ITEM_LIST = [];
|
const ITEM_LIST = [];
|
||||||
const REGION_MAP = {};
|
const REGION_MAP = {};
|
||||||
@@ -38,12 +40,12 @@ const REGION_LIST = [];
|
|||||||
{id: 26, name: "Raichu", downloadable: true},
|
{id: 26, name: "Raichu", downloadable: true},
|
||||||
{id: 27, name: "Sandshrew", downloadable: true},
|
{id: 27, name: "Sandshrew", downloadable: true},
|
||||||
{id: 28, name: "Sandslash", downloadable: true},
|
{id: 28, name: "Sandslash", downloadable: true},
|
||||||
{id: 29, name: "Nidoran♀", downloadable: true},
|
{id: 29, name: "Nidoran♀", downloadable: true, gender: "female"},
|
||||||
{id: 30, name: "Nidorina", downloadable: true},
|
{id: 30, name: "Nidorina", downloadable: true, gender: "female"},
|
||||||
{id: 31, name: "Nidoqueen", downloadable: true},
|
{id: 31, name: "Nidoqueen", downloadable: true, gender: "female"},
|
||||||
{id: 32, name: "Nidoran♂", downloadable: true},
|
{id: 32, name: "Nidoran♂", downloadable: true, gender: "male"},
|
||||||
{id: 33, name: "Nidorino", downloadable: true},
|
{id: 33, name: "Nidorino", downloadable: true, gender: "male"},
|
||||||
{id: 34, name: "Nidoking", downloadable: true},
|
{id: 34, name: "Nidoking", downloadable: true, gender: "male"},
|
||||||
{id: 35, name: "Clefairy", downloadable: true},
|
{id: 35, name: "Clefairy", downloadable: true},
|
||||||
{id: 36, name: "Clefable", downloadable: true},
|
{id: 36, name: "Clefable", downloadable: true},
|
||||||
{id: 37, name: "Vulpix", downloadable: true},
|
{id: 37, name: "Vulpix", downloadable: true},
|
||||||
@@ -90,8 +92,8 @@ const REGION_LIST = [];
|
|||||||
{id: 78, name: "Rapidash", downloadable: true},
|
{id: 78, name: "Rapidash", downloadable: true},
|
||||||
{id: 79, name: "Slowpoke", downloadable: true},
|
{id: 79, name: "Slowpoke", downloadable: true},
|
||||||
{id: 80, name: "Slowbro", downloadable: true},
|
{id: 80, name: "Slowbro", downloadable: true},
|
||||||
{id: 81, name: "Magnemite", downloadable: true},
|
{id: 81, name: "Magnemite", downloadable: true, gender: "unknown"},
|
||||||
{id: 82, name: "Magneton", downloadable: true},
|
{id: 82, name: "Magneton", downloadable: true, gender: "unknown"},
|
||||||
{id: 83, name: "Farfetch'd", downloadable: true},
|
{id: 83, name: "Farfetch'd", downloadable: true},
|
||||||
{id: 84, name: "Doduo", downloadable: true},
|
{id: 84, name: "Doduo", downloadable: true},
|
||||||
{id: 85, name: "Dodrio", downloadable: true},
|
{id: 85, name: "Dodrio", downloadable: true},
|
||||||
@@ -109,58 +111,58 @@ const REGION_LIST = [];
|
|||||||
{id: 97, name: "Hypno", downloadable: true},
|
{id: 97, name: "Hypno", downloadable: true},
|
||||||
{id: 98, name: "Krabby", downloadable: true},
|
{id: 98, name: "Krabby", downloadable: true},
|
||||||
{id: 99, name: "Kingler", downloadable: true},
|
{id: 99, name: "Kingler", downloadable: true},
|
||||||
{id: 100, name: "Voltorb", downloadable: true},
|
{id: 100, name: "Voltorb", downloadable: true, gender: "unknown"},
|
||||||
{id: 101, name: "Electrode", downloadable: true},
|
{id: 101, name: "Electrode", downloadable: true, gender: "unknown"},
|
||||||
{id: 102, name: "Exeggcute", downloadable: true},
|
{id: 102, name: "Exeggcute", downloadable: true},
|
||||||
{id: 103, name: "Exeggutor", downloadable: true},
|
{id: 103, name: "Exeggutor", downloadable: true},
|
||||||
{id: 104, name: "Cubone", downloadable: true},
|
{id: 104, name: "Cubone", downloadable: true},
|
||||||
{id: 105, name: "Marowak", downloadable: true},
|
{id: 105, name: "Marowak", downloadable: true},
|
||||||
{id: 106, name: "Hitmonlee", downloadable: true},
|
{id: 106, name: "Hitmonlee", downloadable: true, gender: "male"},
|
||||||
{id: 107, name: "Hitmonchan", downloadable: true},
|
{id: 107, name: "Hitmonchan", downloadable: true, gender: "male"},
|
||||||
{id: 108, name: "Lickitung", downloadable: true},
|
{id: 108, name: "Lickitung", downloadable: true},
|
||||||
{id: 109, name: "Koffing", downloadable: true},
|
{id: 109, name: "Koffing", downloadable: true},
|
||||||
{id: 110, name: "Weezing", downloadable: true},
|
{id: 110, name: "Weezing", downloadable: true},
|
||||||
{id: 111, name: "Rhyhorn", downloadable: true},
|
{id: 111, name: "Rhyhorn", downloadable: true},
|
||||||
{id: 112, name: "Rhydon", downloadable: true},
|
{id: 112, name: "Rhydon", downloadable: true},
|
||||||
{id: 113, name: "Chansey", downloadable: true},
|
{id: 113, name: "Chansey", downloadable: true, gender: "female"},
|
||||||
{id: 114, name: "Tangela", downloadable: true},
|
{id: 114, name: "Tangela", downloadable: true},
|
||||||
{id: 115, name: "Kangaskhan", downloadable: true},
|
{id: 115, name: "Kangaskhan", downloadable: true, gender: "female"},
|
||||||
{id: 116, name: "Horsea", downloadable: true},
|
{id: 116, name: "Horsea", downloadable: true},
|
||||||
{id: 117, name: "Seadra", downloadable: true},
|
{id: 117, name: "Seadra", downloadable: true},
|
||||||
{id: 118, name: "Goldeen", downloadable: true},
|
{id: 118, name: "Goldeen", downloadable: true},
|
||||||
{id: 119, name: "Seaking", downloadable: true},
|
{id: 119, name: "Seaking", downloadable: true},
|
||||||
{id: 120, name: "Staryu", downloadable: true},
|
{id: 120, name: "Staryu", downloadable: true, gender: "unknown"},
|
||||||
{id: 121, name: "Starmie", downloadable: true},
|
{id: 121, name: "Starmie", downloadable: true, gender: "unknown"},
|
||||||
{id: 122, name: "Mr. Mime", downloadable: true},
|
{id: 122, name: "Mr. Mime", downloadable: true},
|
||||||
{id: 123, name: "Scyther", downloadable: true},
|
{id: 123, name: "Scyther", downloadable: true},
|
||||||
{id: 124, name: "Jynx", downloadable: true},
|
{id: 124, name: "Jynx", downloadable: true, gender: "female"},
|
||||||
{id: 125, name: "Electabuzz", downloadable: true},
|
{id: 125, name: "Electabuzz", downloadable: true},
|
||||||
{id: 126, name: "Magmar", downloadable: true},
|
{id: 126, name: "Magmar", downloadable: true},
|
||||||
{id: 127, name: "Pinsir", downloadable: true},
|
{id: 127, name: "Pinsir", downloadable: true},
|
||||||
{id: 128, name: "Tauros", downloadable: true},
|
{id: 128, name: "Tauros", downloadable: true, gender: "male"},
|
||||||
{id: 129, name: "Magikarp", downloadable: true},
|
{id: 129, name: "Magikarp", downloadable: true},
|
||||||
{id: 130, name: "Gyarados", downloadable: true},
|
{id: 130, name: "Gyarados", downloadable: true},
|
||||||
{id: 131, name: "Lapras", downloadable: true},
|
{id: 131, name: "Lapras", downloadable: true},
|
||||||
{id: 132, name: "Ditto", downloadable: true},
|
{id: 132, name: "Ditto", downloadable: true, gender: "unknown"},
|
||||||
{id: 133, name: "Eevee", downloadable: true},
|
{id: 133, name: "Eevee", downloadable: true},
|
||||||
{id: 134, name: "Vaporeon", downloadable: true},
|
{id: 134, name: "Vaporeon", downloadable: true},
|
||||||
{id: 135, name: "Jolteon", downloadable: true},
|
{id: 135, name: "Jolteon", downloadable: true},
|
||||||
{id: 136, name: "Flareon", downloadable: true},
|
{id: 136, name: "Flareon", downloadable: true},
|
||||||
{id: 137, name: "Porygon", downloadable: true},
|
{id: 137, name: "Porygon", downloadable: true, gender: "unknown"},
|
||||||
{id: 138, name: "Omanyte", downloadable: true},
|
{id: 138, name: "Omanyte", downloadable: true},
|
||||||
{id: 139, name: "Omastar", downloadable: true},
|
{id: 139, name: "Omastar", downloadable: true},
|
||||||
{id: 140, name: "Kabuto", downloadable: true},
|
{id: 140, name: "Kabuto", downloadable: true},
|
||||||
{id: 141, name: "Kabutops", downloadable: true},
|
{id: 141, name: "Kabutops", downloadable: true},
|
||||||
{id: 142, name: "Aerodactyl", downloadable: true},
|
{id: 142, name: "Aerodactyl", downloadable: true},
|
||||||
{id: 143, name: "Snorlax", downloadable: true},
|
{id: 143, name: "Snorlax", downloadable: true},
|
||||||
{id: 144, name: "Articuno", downloadable: true},
|
{id: 144, name: "Articuno", downloadable: true, gender: "unknown"},
|
||||||
{id: 145, name: "Zapdos", downloadable: true},
|
{id: 145, name: "Zapdos", downloadable: true, gender: "unknown"},
|
||||||
{id: 146, name: "Moltres", downloadable: true},
|
{id: 146, name: "Moltres", downloadable: true, gender: "unknown"},
|
||||||
{id: 147, name: "Dratini", downloadable: true},
|
{id: 147, name: "Dratini", downloadable: true},
|
||||||
{id: 148, name: "Dragonair", downloadable: true},
|
{id: 148, name: "Dragonair", downloadable: true},
|
||||||
{id: 149, name: "Dragonite", downloadable: true},
|
{id: 149, name: "Dragonite", downloadable: true},
|
||||||
{id: 150, name: "Mewtwo", downloadable: true},
|
{id: 150, name: "Mewtwo", downloadable: true, gender: "unknown"},
|
||||||
{id: 151, name: "Mew", downloadable: true},
|
{id: 151, name: "Mew", downloadable: true, gender: "unknown"},
|
||||||
{id: 152, name: "Chikorita", downloadable: true},
|
{id: 152, name: "Chikorita", downloadable: true},
|
||||||
{id: 153, name: "Bayleef", downloadable: true},
|
{id: 153, name: "Bayleef", downloadable: true},
|
||||||
{id: 154, name: "Meganium", downloadable: true},
|
{id: 154, name: "Meganium", downloadable: true},
|
||||||
@@ -210,7 +212,7 @@ const REGION_LIST = [];
|
|||||||
{id: 198, name: "Murkrow", downloadable: true},
|
{id: 198, name: "Murkrow", downloadable: true},
|
||||||
{id: 199, name: "Slowking", downloadable: true},
|
{id: 199, name: "Slowking", downloadable: true},
|
||||||
{id: 200, name: "Misdreavus", downloadable: true},
|
{id: 200, name: "Misdreavus", downloadable: true},
|
||||||
{id: 201, name: "Unown", downloadable: true, forms: [
|
{id: 201, name: "Unown", downloadable: true, gender: "unknown", forms: [
|
||||||
"A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N",
|
"A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N",
|
||||||
"O", "P","Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", "!", "?"]},
|
"O", "P","Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", "!", "?"]},
|
||||||
{id: 202, name: "Wobbuffet", downloadable: true},
|
{id: 202, name: "Wobbuffet", downloadable: true},
|
||||||
@@ -244,25 +246,25 @@ const REGION_LIST = [];
|
|||||||
{id: 230, name: "Kingdra", downloadable: true},
|
{id: 230, name: "Kingdra", downloadable: true},
|
||||||
{id: 231, name: "Phanpy", downloadable: true},
|
{id: 231, name: "Phanpy", downloadable: true},
|
||||||
{id: 232, name: "Donphan", downloadable: true},
|
{id: 232, name: "Donphan", downloadable: true},
|
||||||
{id: 233, name: "Porygon2", downloadable: true},
|
{id: 233, name: "Porygon2", downloadable: true, gender: "unknown"},
|
||||||
{id: 234, name: "Stantler", downloadable: true},
|
{id: 234, name: "Stantler", downloadable: true},
|
||||||
{id: 235, name: "Smeargle", downloadable: true},
|
{id: 235, name: "Smeargle", downloadable: true},
|
||||||
{id: 236, name: "Tyrogue", downloadable: true},
|
{id: 236, name: "Tyrogue", downloadable: true, gender: "male"},
|
||||||
{id: 237, name: "Hitmontop", downloadable: true},
|
{id: 237, name: "Hitmontop", downloadable: true, gender: "male"},
|
||||||
{id: 238, name: "Smoochum", downloadable: true},
|
{id: 238, name: "Smoochum", downloadable: true},
|
||||||
{id: 239, name: "Elekid", downloadable: true},
|
{id: 239, name: "Elekid", downloadable: true},
|
||||||
{id: 240, name: "Magby", downloadable: true},
|
{id: 240, name: "Magby", downloadable: true},
|
||||||
{id: 241, name: "Miltank", downloadable: true},
|
{id: 241, name: "Miltank", downloadable: true, gender: "female"},
|
||||||
{id: 242, name: "Blissey", downloadable: true},
|
{id: 242, name: "Blissey", downloadable: true, gender: "female"},
|
||||||
{id: 243, name: "Raikou", downloadable: true},
|
{id: 243, name: "Raikou", downloadable: true, gender: "unknown"},
|
||||||
{id: 244, name: "Entei", downloadable: true},
|
{id: 244, name: "Entei", downloadable: true, gender: "unknown"},
|
||||||
{id: 245, name: "Suicune", downloadable: true},
|
{id: 245, name: "Suicune", downloadable: true, gender: "unknown"},
|
||||||
{id: 246, name: "Larvitar", downloadable: true},
|
{id: 246, name: "Larvitar", downloadable: true},
|
||||||
{id: 247, name: "Pupitar", downloadable: true},
|
{id: 247, name: "Pupitar", downloadable: true},
|
||||||
{id: 248, name: "Tyranitar", downloadable: true},
|
{id: 248, name: "Tyranitar", downloadable: true},
|
||||||
{id: 249, name: "Lugia", downloadable: true},
|
{id: 249, name: "Lugia", downloadable: true, gender: "unknown"},
|
||||||
{id: 250, name: "Ho-Oh", downloadable: true},
|
{id: 250, name: "Ho-Oh", downloadable: true, gender: "unknown"},
|
||||||
{id: 251, name: "Celebi", downloadable: true},
|
{id: 251, name: "Celebi", downloadable: true, gender: "unknown"},
|
||||||
{id: 252, name: "Treecko", downloadable: true},
|
{id: 252, name: "Treecko", downloadable: true},
|
||||||
{id: 253, name: "Grovyle", downloadable: true},
|
{id: 253, name: "Grovyle", downloadable: true},
|
||||||
{id: 254, name: "Sceptile", downloadable: true},
|
{id: 254, name: "Sceptile", downloadable: true},
|
||||||
@@ -303,7 +305,7 @@ const REGION_LIST = [];
|
|||||||
{id: 289, name: "Slaking", downloadable: true},
|
{id: 289, name: "Slaking", downloadable: true},
|
||||||
{id: 290, name: "Nincada", downloadable: true},
|
{id: 290, name: "Nincada", downloadable: true},
|
||||||
{id: 291, name: "Ninjask", downloadable: true},
|
{id: 291, name: "Ninjask", downloadable: true},
|
||||||
{id: 292, name: "Shedinja", downloadable: true},
|
{id: 292, name: "Shedinja", downloadable: true, gender: "unknown"},
|
||||||
{id: 293, name: "Whismur", downloadable: true},
|
{id: 293, name: "Whismur", downloadable: true},
|
||||||
{id: 294, name: "Loudred", downloadable: true},
|
{id: 294, name: "Loudred", downloadable: true},
|
||||||
{id: 295, name: "Exploud", downloadable: true},
|
{id: 295, name: "Exploud", downloadable: true},
|
||||||
@@ -324,8 +326,8 @@ const REGION_LIST = [];
|
|||||||
{id: 310, name: "Manectric", downloadable: true},
|
{id: 310, name: "Manectric", downloadable: true},
|
||||||
{id: 311, name: "Plusle", downloadable: true},
|
{id: 311, name: "Plusle", downloadable: true},
|
||||||
{id: 312, name: "Minun", downloadable: true},
|
{id: 312, name: "Minun", downloadable: true},
|
||||||
{id: 313, name: "Volbeat", downloadable: true},
|
{id: 313, name: "Volbeat", downloadable: true, gender: "male"},
|
||||||
{id: 314, name: "Illumise", downloadable: true},
|
{id: 314, name: "Illumise", downloadable: true, gender: "female"},
|
||||||
{id: 315, name: "Roselia", downloadable: true},
|
{id: 315, name: "Roselia", downloadable: true},
|
||||||
{id: 316, name: "Gulpin", downloadable: true},
|
{id: 316, name: "Gulpin", downloadable: true},
|
||||||
{id: 317, name: "Swalot", downloadable: true},
|
{id: 317, name: "Swalot", downloadable: true},
|
||||||
@@ -348,14 +350,14 @@ const REGION_LIST = [];
|
|||||||
{id: 334, name: "Altaria", downloadable: true},
|
{id: 334, name: "Altaria", downloadable: true},
|
||||||
{id: 335, name: "Zangoose", downloadable: true},
|
{id: 335, name: "Zangoose", downloadable: true},
|
||||||
{id: 336, name: "Seviper", downloadable: true},
|
{id: 336, name: "Seviper", downloadable: true},
|
||||||
{id: 337, name: "Lunatone", downloadable: true},
|
{id: 337, name: "Lunatone", downloadable: true, gender: "unknown"},
|
||||||
{id: 338, name: "Solrock", downloadable: true},
|
{id: 338, name: "Solrock", downloadable: true, gender: "unknown"},
|
||||||
{id: 339, name: "Barboach", downloadable: true},
|
{id: 339, name: "Barboach", downloadable: true},
|
||||||
{id: 340, name: "Whiscash", downloadable: true},
|
{id: 340, name: "Whiscash", downloadable: true},
|
||||||
{id: 341, name: "Corphish", downloadable: true},
|
{id: 341, name: "Corphish", downloadable: true},
|
||||||
{id: 342, name: "Crawdaunt", downloadable: true},
|
{id: 342, name: "Crawdaunt", downloadable: true},
|
||||||
{id: 343, name: "Baltoy", downloadable: true},
|
{id: 343, name: "Baltoy", downloadable: true, gender: "unknown"},
|
||||||
{id: 344, name: "Claydol", downloadable: true},
|
{id: 344, name: "Claydol", downloadable: true, gender: "unknown"},
|
||||||
{id: 345, name: "Lileep", downloadable: true},
|
{id: 345, name: "Lileep", downloadable: true},
|
||||||
{id: 346, name: "Cradily", downloadable: true},
|
{id: 346, name: "Cradily", downloadable: true},
|
||||||
{id: 347, name: "Anorith", downloadable: true},
|
{id: 347, name: "Anorith", downloadable: true},
|
||||||
@@ -385,19 +387,19 @@ const REGION_LIST = [];
|
|||||||
{id: 371, name: "Bagon", downloadable: true},
|
{id: 371, name: "Bagon", downloadable: true},
|
||||||
{id: 372, name: "Shelgon", downloadable: true},
|
{id: 372, name: "Shelgon", downloadable: true},
|
||||||
{id: 373, name: "Salamence", downloadable: true},
|
{id: 373, name: "Salamence", downloadable: true},
|
||||||
{id: 374, name: "Beldum", downloadable: true},
|
{id: 374, name: "Beldum", downloadable: true, gender: "unknown"},
|
||||||
{id: 375, name: "Metang", downloadable: true},
|
{id: 375, name: "Metang", downloadable: true, gender: "unknown"},
|
||||||
{id: 376, name: "Metagross", downloadable: true},
|
{id: 376, name: "Metagross", downloadable: true, gender: "unknown"},
|
||||||
{id: 377, name: "Regirock", downloadable: true},
|
{id: 377, name: "Regirock", downloadable: true, gender: "unknown"},
|
||||||
{id: 378, name: "Regice", downloadable: true},
|
{id: 378, name: "Regice", downloadable: true, gender: "unknown"},
|
||||||
{id: 379, name: "Registeel", downloadable: true},
|
{id: 379, name: "Registeel", downloadable: true, gender: "unknown"},
|
||||||
{id: 380, name: "Latias", downloadable: true},
|
{id: 380, name: "Latias", downloadable: true, gender: "female"},
|
||||||
{id: 381, name: "Latios", downloadable: true},
|
{id: 381, name: "Latios", downloadable: true, gender: "male"},
|
||||||
{id: 382, name: "Kyogre", downloadable: true},
|
{id: 382, name: "Kyogre", downloadable: true, gender: "unknown"},
|
||||||
{id: 383, name: "Groudon", downloadable: true},
|
{id: 383, name: "Groudon", downloadable: true, gender: "unknown"},
|
||||||
{id: 384, name: "Rayquaza", downloadable: true},
|
{id: 384, name: "Rayquaza", downloadable: true, gender: "unknown"},
|
||||||
{id: 385, name: "Jirachi", downloadable: true},
|
{id: 385, name: "Jirachi", downloadable: true, gender: "unknown"},
|
||||||
{id: 386, name: "Deoxys", downloadable: true, forms: ["Normal", "Attack", "Defense", "Speed"]},
|
{id: 386, name: "Deoxys", downloadable: true, gender: "unknown", forms: ["Normal", "Attack", "Defense", "Speed"]},
|
||||||
{id: 387, name: "Turtwig", downloadable: true},
|
{id: 387, name: "Turtwig", downloadable: true},
|
||||||
{id: 388, name: "Grotle", downloadable: true},
|
{id: 388, name: "Grotle", downloadable: true},
|
||||||
{id: 389, name: "Torterra", downloadable: true},
|
{id: 389, name: "Torterra", downloadable: true},
|
||||||
@@ -424,10 +426,10 @@ const REGION_LIST = [];
|
|||||||
{id: 410, name: "Shieldon", downloadable: true},
|
{id: 410, name: "Shieldon", downloadable: true},
|
||||||
{id: 411, name: "Bastiodon", downloadable: true},
|
{id: 411, name: "Bastiodon", downloadable: true},
|
||||||
{id: 412, name: "Burmy", downloadable: true, forms: ["Plant Cloak", "Sandy Cloak", "Trash Cloak"]},
|
{id: 412, name: "Burmy", downloadable: true, forms: ["Plant Cloak", "Sandy Cloak", "Trash Cloak"]},
|
||||||
{id: 413, name: "Wormadam", downloadable: true, forms: ["Plant Cloak", "Sandy Cloak", "Trash Cloak"]},
|
{id: 413, name: "Wormadam", downloadable: true, gender: "female", forms: ["Plant Cloak", "Sandy Cloak", "Trash Cloak"]},
|
||||||
{id: 414, name: "Mothim", downloadable: true},
|
{id: 414, name: "Mothim", downloadable: true, gender: "male"},
|
||||||
{id: 415, name: "Combee", downloadable: true},
|
{id: 415, name: "Combee", downloadable: true},
|
||||||
{id: 416, name: "Vespiquen", downloadable: true},
|
{id: 416, name: "Vespiquen", downloadable: true, gender: "female"},
|
||||||
{id: 417, name: "Pachirisu", downloadable: true},
|
{id: 417, name: "Pachirisu", downloadable: true},
|
||||||
{id: 418, name: "Buizel", downloadable: true},
|
{id: 418, name: "Buizel", downloadable: true},
|
||||||
{id: 419, name: "Floatzel", downloadable: true},
|
{id: 419, name: "Floatzel", downloadable: true},
|
||||||
@@ -447,8 +449,8 @@ const REGION_LIST = [];
|
|||||||
{id: 433, name: "Chingling", downloadable: true},
|
{id: 433, name: "Chingling", downloadable: true},
|
||||||
{id: 434, name: "Stunky", downloadable: true},
|
{id: 434, name: "Stunky", downloadable: true},
|
||||||
{id: 435, name: "Skuntank", downloadable: true},
|
{id: 435, name: "Skuntank", downloadable: true},
|
||||||
{id: 436, name: "Bronzor", downloadable: true},
|
{id: 436, name: "Bronzor", downloadable: true, gender: "unknown"},
|
||||||
{id: 437, name: "Bronzong", downloadable: true},
|
{id: 437, name: "Bronzong", downloadable: true, gender: "unknown"},
|
||||||
{id: 438, name: "Bonsly", downloadable: true},
|
{id: 438, name: "Bonsly", downloadable: true},
|
||||||
{id: 439, name: "Mime Jr.", downloadable: true},
|
{id: 439, name: "Mime Jr.", downloadable: true},
|
||||||
{id: 440, name: "Happiny", downloadable: true},
|
{id: 440, name: "Happiny", downloadable: true},
|
||||||
@@ -473,7 +475,7 @@ const REGION_LIST = [];
|
|||||||
{id: 459, name: "Snover", downloadable: true},
|
{id: 459, name: "Snover", downloadable: true},
|
||||||
{id: 460, name: "Abomasnow", downloadable: true},
|
{id: 460, name: "Abomasnow", downloadable: true},
|
||||||
{id: 461, name: "Weavile", downloadable: true},
|
{id: 461, name: "Weavile", downloadable: true},
|
||||||
{id: 462, name: "Magnezone", downloadable: true},
|
{id: 462, name: "Magnezone", downloadable: true, gender: "unknown"},
|
||||||
{id: 463, name: "Lickilicky", downloadable: true},
|
{id: 463, name: "Lickilicky", downloadable: true},
|
||||||
{id: 464, name: "Rhyperior", downloadable: true},
|
{id: 464, name: "Rhyperior", downloadable: true},
|
||||||
{id: 465, name: "Tangrowth", downloadable: true},
|
{id: 465, name: "Tangrowth", downloadable: true},
|
||||||
@@ -485,29 +487,29 @@ const REGION_LIST = [];
|
|||||||
{id: 471, name: "Glaceon", downloadable: true},
|
{id: 471, name: "Glaceon", downloadable: true},
|
||||||
{id: 472, name: "Gliscor", downloadable: true},
|
{id: 472, name: "Gliscor", downloadable: true},
|
||||||
{id: 473, name: "Mamoswine", downloadable: true},
|
{id: 473, name: "Mamoswine", downloadable: true},
|
||||||
{id: 474, name: "Porygon-Z", downloadable: true},
|
{id: 474, name: "Porygon-Z", downloadable: true, gender: "unknown"},
|
||||||
{id: 475, name: "Gallade", downloadable: true},
|
{id: 475, name: "Gallade", downloadable: true, gender: "male"},
|
||||||
{id: 476, name: "Probopass", downloadable: true},
|
{id: 476, name: "Probopass", downloadable: true},
|
||||||
{id: 477, name: "Dusknoir", downloadable: true},
|
{id: 477, name: "Dusknoir", downloadable: true},
|
||||||
{id: 478, name: "Froslass", downloadable: true},
|
{id: 478, name: "Froslass", downloadable: true, gender: "female"},
|
||||||
{id: 479, name: "Rotom", downloadable: true, forms: ["Normal", "Heat", "Wash", "Frost", "Fan", "Mow"]},
|
{id: 479, name: "Rotom", downloadable: true, gender: "unknown", forms: ["Normal", "Heat", "Wash", "Frost", "Fan", "Mow"]},
|
||||||
{id: 480, name: "Uxie", downloadable: true},
|
{id: 480, name: "Uxie", downloadable: true, gender: "unknown"},
|
||||||
{id: 481, name: "Mesprit", downloadable: true},
|
{id: 481, name: "Mesprit", downloadable: true, gender: "unknown"},
|
||||||
{id: 482, name: "Azelf", downloadable: true},
|
{id: 482, name: "Azelf", downloadable: true, gender: "unknown"},
|
||||||
{id: 483, name: "Dialga", downloadable: true},
|
{id: 483, name: "Dialga", downloadable: true, gender: "unknown"},
|
||||||
{id: 484, name: "Palkia", downloadable: true},
|
{id: 484, name: "Palkia", downloadable: true, gender: "unknown"},
|
||||||
{id: 485, name: "Heatran", downloadable: true},
|
{id: 485, name: "Heatran", downloadable: true},
|
||||||
{id: 486, name: "Regigigas", downloadable: true},
|
{id: 486, name: "Regigigas", downloadable: true, gender: "unknown"},
|
||||||
{id: 487, name: "Giratina", downloadable: true, forms: ["Altered", "Origin"]},
|
{id: 487, name: "Giratina", downloadable: true, gender: "unknown", forms: ["Altered", "Origin"]},
|
||||||
{id: 488, name: "Cresselia", downloadable: true},
|
{id: 488, name: "Cresselia", downloadable: true, gender: "female"},
|
||||||
{id: 489, name: "Phione", downloadable: true},
|
{id: 489, name: "Phione", downloadable: true, gender: "unknown"},
|
||||||
{id: 490, name: "Manaphy", downloadable: true},
|
{id: 490, name: "Manaphy", downloadable: true, gender: "unknown"},
|
||||||
{id: 491, name: "Darkrai", downloadable: true},
|
{id: 491, name: "Darkrai", downloadable: true, gender: "unknown"},
|
||||||
{id: 492, name: "Shaymin", downloadable: true},
|
{id: 492, name: "Shaymin", downloadable: true, gender: "unknown", forms: ["Land", "Sky"]},
|
||||||
{id: 493, name: "Arceus", downloadable: true, forms: [
|
{id: 493, name: "Arceus", downloadable: true, gender: "unknown", forms: [
|
||||||
"Normal", "Fighting", "Flying", "Poison", "Ground", "Rock", "Bug", "Ghost", "Steel",
|
"Normal", "Fighting", "Flying", "Poison", "Ground", "Rock", "Bug", "Ghost",
|
||||||
"Fire", "Water", "Grass", "Electric", "Psychic", "Ice", "Dragon", "Dark"]},
|
"Steel", "Fire", "Water", "Grass", "Electric", "Psychic", "Ice", "Dragon", "Dark"]},
|
||||||
{id: 494, name: "Victini", downloadable: true},
|
{id: 494, name: "Victini", downloadable: false, gender: "unknown"},
|
||||||
{id: 495, name: "Snivy", downloadable: false},
|
{id: 495, name: "Snivy", downloadable: false},
|
||||||
{id: 496, name: "Servine", downloadable: false},
|
{id: 496, name: "Servine", downloadable: false},
|
||||||
{id: 497, name: "Serperior", downloadable: false},
|
{id: 497, name: "Serperior", downloadable: false},
|
||||||
@@ -551,8 +553,8 @@ const REGION_LIST = [];
|
|||||||
{id: 535, name: "Tympole", downloadable: true},
|
{id: 535, name: "Tympole", downloadable: true},
|
||||||
{id: 536, name: "Palpitoad", downloadable: false},
|
{id: 536, name: "Palpitoad", downloadable: false},
|
||||||
{id: 537, name: "Seismitoad", downloadable: false},
|
{id: 537, name: "Seismitoad", downloadable: false},
|
||||||
{id: 538, name: "Throh", downloadable: true},
|
{id: 538, name: "Throh", downloadable: true, gender: "male"},
|
||||||
{id: 539, name: "Sawk", downloadable: true},
|
{id: 539, name: "Sawk", downloadable: true, gender: "male"},
|
||||||
{id: 540, name: "Sewaddle", downloadable: false},
|
{id: 540, name: "Sewaddle", downloadable: false},
|
||||||
{id: 541, name: "Swadloon", downloadable: false},
|
{id: 541, name: "Swadloon", downloadable: false},
|
||||||
{id: 542, name: "Leavanny", downloadable: true},
|
{id: 542, name: "Leavanny", downloadable: true},
|
||||||
@@ -561,8 +563,8 @@ const REGION_LIST = [];
|
|||||||
{id: 545, name: "Scolipede", downloadable: true},
|
{id: 545, name: "Scolipede", downloadable: true},
|
||||||
{id: 546, name: "Cottonee", downloadable: true},
|
{id: 546, name: "Cottonee", downloadable: true},
|
||||||
{id: 547, name: "Whimsicott", downloadable: false},
|
{id: 547, name: "Whimsicott", downloadable: false},
|
||||||
{id: 548, name: "Petilil", downloadable: true},
|
{id: 548, name: "Petilil", downloadable: true, gender: "female"},
|
||||||
{id: 549, name: "Lilligant", downloadable: false},
|
{id: 549, name: "Lilligant", downloadable: false, gender: "female"},
|
||||||
{id: 550, name: "Basculin", downloadable: true, forms: ["Red-striped", "Blue-striped"]},
|
{id: 550, name: "Basculin", downloadable: true, forms: ["Red-striped", "Blue-striped"]},
|
||||||
{id: 551, name: "Sandile", downloadable: false},
|
{id: 551, name: "Sandile", downloadable: false},
|
||||||
{id: 552, name: "Krokorok", downloadable: false},
|
{id: 552, name: "Krokorok", downloadable: false},
|
||||||
@@ -612,9 +614,9 @@ const REGION_LIST = [];
|
|||||||
{id: 596, name: "Galvantula", downloadable: true},
|
{id: 596, name: "Galvantula", downloadable: true},
|
||||||
{id: 597, name: "Ferroseed", downloadable: false},
|
{id: 597, name: "Ferroseed", downloadable: false},
|
||||||
{id: 598, name: "Ferrothorn", downloadable: false},
|
{id: 598, name: "Ferrothorn", downloadable: false},
|
||||||
{id: 599, name: "Klink", downloadable: false},
|
{id: 599, name: "Klink", downloadable: false, gender: "unknown"},
|
||||||
{id: 600, name: "Klang", downloadable: true},
|
{id: 600, name: "Klang", downloadable: true, gender: "unknown"},
|
||||||
{id: 601, name: "Klinklang", downloadable: false},
|
{id: 601, name: "Klinklang", downloadable: false, gender: "unknown"},
|
||||||
{id: 602, name: "Tynamo", downloadable: false},
|
{id: 602, name: "Tynamo", downloadable: false},
|
||||||
{id: 603, name: "Eelektrik", downloadable: false},
|
{id: 603, name: "Eelektrik", downloadable: false},
|
||||||
{id: 604, name: "Eelektross", downloadable: false},
|
{id: 604, name: "Eelektross", downloadable: false},
|
||||||
@@ -628,22 +630,22 @@ const REGION_LIST = [];
|
|||||||
{id: 612, name: "Haxorus", downloadable: false},
|
{id: 612, name: "Haxorus", downloadable: false},
|
||||||
{id: 613, name: "Cubchoo", downloadable: true},
|
{id: 613, name: "Cubchoo", downloadable: true},
|
||||||
{id: 614, name: "Beartic", downloadable: false},
|
{id: 614, name: "Beartic", downloadable: false},
|
||||||
{id: 615, name: "Cryogonal", downloadable: false},
|
{id: 615, name: "Cryogonal", downloadable: false, gender: "unknown"},
|
||||||
{id: 616, name: "Shelmet", downloadable: true},
|
{id: 616, name: "Shelmet", downloadable: true},
|
||||||
{id: 617, name: "Accelgor", downloadable: false},
|
{id: 617, name: "Accelgor", downloadable: false},
|
||||||
{id: 618, name: "Stunfisk", downloadable: true},
|
{id: 618, name: "Stunfisk", downloadable: true},
|
||||||
{id: 619, name: "Mienfoo", downloadable: true},
|
{id: 619, name: "Mienfoo", downloadable: true},
|
||||||
{id: 620, name: "Mienshao", downloadable: false},
|
{id: 620, name: "Mienshao", downloadable: false},
|
||||||
{id: 621, name: "Druddigon", downloadable: true},
|
{id: 621, name: "Druddigon", downloadable: true},
|
||||||
{id: 622, name: "Golett", downloadable: true},
|
{id: 622, name: "Golett", downloadable: true, gender: "unknown"},
|
||||||
{id: 623, name: "Golurk", downloadable: false},
|
{id: 623, name: "Golurk", downloadable: false, gender: "unknown"},
|
||||||
{id: 624, name: "Pawniard", downloadable: true},
|
{id: 624, name: "Pawniard", downloadable: true},
|
||||||
{id: 625, name: "Bisharp", downloadable: false},
|
{id: 625, name: "Bisharp", downloadable: false},
|
||||||
{id: 626, name: "Bouffalant", downloadable: true},
|
{id: 626, name: "Bouffalant", downloadable: true},
|
||||||
{id: 627, name: "Rufflet", downloadable: false},
|
{id: 627, name: "Rufflet", downloadable: false, gender: "male"},
|
||||||
{id: 628, name: "Braviary", downloadable: true},
|
{id: 628, name: "Braviary", downloadable: true, gender: "male"},
|
||||||
{id: 629, name: "Vullaby", downloadable: false},
|
{id: 629, name: "Vullaby", downloadable: false, gender: "female"},
|
||||||
{id: 630, name: "Mandibuzz", downloadable: true},
|
{id: 630, name: "Mandibuzz", downloadable: true, gender: "female"},
|
||||||
{id: 631, name: "Heatmor", downloadable: true},
|
{id: 631, name: "Heatmor", downloadable: true},
|
||||||
{id: 632, name: "Durant", downloadable: true},
|
{id: 632, name: "Durant", downloadable: true},
|
||||||
{id: 633, name: "Deino", downloadable: false},
|
{id: 633, name: "Deino", downloadable: false},
|
||||||
@@ -651,18 +653,18 @@ const REGION_LIST = [];
|
|||||||
{id: 635, name: "Hydreigon", downloadable: false},
|
{id: 635, name: "Hydreigon", downloadable: false},
|
||||||
{id: 636, name: "Larvesta", downloadable: false},
|
{id: 636, name: "Larvesta", downloadable: false},
|
||||||
{id: 637, name: "Volcarona", downloadable: false},
|
{id: 637, name: "Volcarona", downloadable: false},
|
||||||
{id: 638, name: "Cobalion", downloadable: false},
|
{id: 638, name: "Cobalion", downloadable: false, gender: "unknown"},
|
||||||
{id: 639, name: "Terrakion", downloadable: false},
|
{id: 639, name: "Terrakion", downloadable: false, gender: "unknown"},
|
||||||
{id: 640, name: "Virizion", downloadable: false},
|
{id: 640, name: "Virizion", downloadable: false, gender: "unknown"},
|
||||||
{id: 641, name: "Tornadus", downloadable: false, forms: ["Incarnate", "Therian"]},
|
{id: 641, name: "Tornadus", downloadable: false, gender: "male", forms: ["Incarnate", "Therian"]},
|
||||||
{id: 642, name: "Thundurus", downloadable: false, forms: ["Incarnate", "Therian"]},
|
{id: 642, name: "Thundurus", downloadable: false, gender: "male", forms: ["Incarnate", "Therian"]},
|
||||||
{id: 643, name: "Reshiram", downloadable: false},
|
{id: 643, name: "Reshiram", downloadable: false, gender: "unknown"},
|
||||||
{id: 644, name: "Zekrom", downloadable: false},
|
{id: 644, name: "Zekrom", downloadable: false, gender: "unknown"},
|
||||||
{id: 645, name: "Landorus", downloadable: false, forms: ["Incarnate", "Therian"]},
|
{id: 645, name: "Landorus", downloadable: false, gender: "male", forms: ["Incarnate", "Therian"]},
|
||||||
{id: 646, name: "Kyurem", downloadable: false, forms: ["Normal", "White", "Black"]},
|
{id: 646, name: "Kyurem", downloadable: false, gender: "unknown", forms: ["Normal", "White", "Black"]},
|
||||||
{id: 647, name: "Keldeo", downloadable: false, forms: ["Ordinary", "Resolute"]},
|
{id: 647, name: "Keldeo", downloadable: false, gender: "unknown", forms: ["Ordinary", "Resolute"]},
|
||||||
{id: 648, name: "Meloetta", downloadable: false, forms: ["Aria", "Pirouette"]},
|
{id: 648, name: "Meloetta", downloadable: false, gender: "unknown", forms: ["Aria", "Pirouette"]},
|
||||||
{id: 649, name: "Genesect", downloadable: false, forms: ["Normal", "Douse", "Shock", "Burn", "Chill"]}
|
{id: 649, name: "Genesect", downloadable: false, gender: "unknown", forms: ["Normal", "Douse", "Shock", "Burn", "Chill"]}
|
||||||
];
|
];
|
||||||
|
|
||||||
// Moves
|
// Moves
|
||||||
@@ -1228,6 +1230,174 @@ const REGION_LIST = [];
|
|||||||
{id: 559, name: "Fusion Bolt"}
|
{id: 559, name: "Fusion Bolt"}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Abilities
|
||||||
|
let abilities = [
|
||||||
|
{id: 1, name: "Stench"},
|
||||||
|
{id: 2, name: "Drizzle"},
|
||||||
|
{id: 3, name: "Speed Boost"},
|
||||||
|
{id: 4, name: "Battle Armor"},
|
||||||
|
{id: 5, name: "Sturdy"},
|
||||||
|
{id: 6, name: "Damp"},
|
||||||
|
{id: 7, name: "Limber"},
|
||||||
|
{id: 8, name: "Sand Veil"},
|
||||||
|
{id: 9, name: "Static"},
|
||||||
|
{id: 10, name: "Volt Absorb"},
|
||||||
|
{id: 11, name: "Water Absorb"},
|
||||||
|
{id: 12, name: "Oblivious"},
|
||||||
|
{id: 13, name: "Cloud Nine"},
|
||||||
|
{id: 14, name: "Compound Eyes"},
|
||||||
|
{id: 15, name: "Insomnia"},
|
||||||
|
{id: 16, name: "Color Change"},
|
||||||
|
{id: 17, name: "Immunity"},
|
||||||
|
{id: 18, name: "Flash Fire"},
|
||||||
|
{id: 19, name: "Shield Dust"},
|
||||||
|
{id: 20, name: "Own Tempo"},
|
||||||
|
{id: 21, name: "Suction Cups"},
|
||||||
|
{id: 22, name: "Intimidate"},
|
||||||
|
{id: 23, name: "Shadow Tag"},
|
||||||
|
{id: 24, name: "Rough Skin"},
|
||||||
|
{id: 25, name: "Wonder Guard"},
|
||||||
|
{id: 26, name: "Levitate"},
|
||||||
|
{id: 27, name: "Effect Spore"},
|
||||||
|
{id: 28, name: "Synchronize"},
|
||||||
|
{id: 29, name: "Clear Body"},
|
||||||
|
{id: 30, name: "Natural Cure"},
|
||||||
|
{id: 31, name: "Lightning Rod"},
|
||||||
|
{id: 32, name: "Serene Grace"},
|
||||||
|
{id: 33, name: "Swift Swim"},
|
||||||
|
{id: 34, name: "Chlorophyll"},
|
||||||
|
{id: 35, name: "Illuminate"},
|
||||||
|
{id: 36, name: "Trace"},
|
||||||
|
{id: 37, name: "Huge Power"},
|
||||||
|
{id: 38, name: "Poison Point"},
|
||||||
|
{id: 39, name: "Inner Focus"},
|
||||||
|
{id: 40, name: "Magma Armor"},
|
||||||
|
{id: 41, name: "Water Veil"},
|
||||||
|
{id: 42, name: "Magnet Pull"},
|
||||||
|
{id: 43, name: "Soundproof"},
|
||||||
|
{id: 44, name: "Rain Dish"},
|
||||||
|
{id: 45, name: "Sand Stream"},
|
||||||
|
{id: 46, name: "Pressure"},
|
||||||
|
{id: 47, name: "Thick Fat"},
|
||||||
|
{id: 48, name: "Early Bird"},
|
||||||
|
{id: 49, name: "Flame Body"},
|
||||||
|
{id: 50, name: "Run Away"},
|
||||||
|
{id: 51, name: "Keen Eye"},
|
||||||
|
{id: 52, name: "Hyper Cutter"},
|
||||||
|
{id: 53, name: "Pickup"},
|
||||||
|
{id: 54, name: "Truant"},
|
||||||
|
{id: 55, name: "Hustle"},
|
||||||
|
{id: 56, name: "Cute Charm"},
|
||||||
|
{id: 57, name: "Plus"},
|
||||||
|
{id: 58, name: "Minus"},
|
||||||
|
{id: 59, name: "Forecast"},
|
||||||
|
{id: 60, name: "Sticky Hold"},
|
||||||
|
{id: 61, name: "Shed Skin"},
|
||||||
|
{id: 62, name: "Guts"},
|
||||||
|
{id: 63, name: "Marvel Scale"},
|
||||||
|
{id: 64, name: "Liquid Ooze"},
|
||||||
|
{id: 65, name: "Overgrow"},
|
||||||
|
{id: 66, name: "Blaze"},
|
||||||
|
{id: 67, name: "Torrent"},
|
||||||
|
{id: 68, name: "Swarm"},
|
||||||
|
{id: 69, name: "Rock Head"},
|
||||||
|
{id: 70, name: "Drought"},
|
||||||
|
{id: 71, name: "Arena Trap"},
|
||||||
|
{id: 72, name: "Vital Spirit"},
|
||||||
|
{id: 73, name: "White Smoke"},
|
||||||
|
{id: 74, name: "Pure Power"},
|
||||||
|
{id: 75, name: "Shell Armor"},
|
||||||
|
{id: 76, name: "Air Lock"},
|
||||||
|
{id: 77, name: "Tangled Feet"},
|
||||||
|
{id: 78, name: "Motor Drive"},
|
||||||
|
{id: 79, name: "Rivalry"},
|
||||||
|
{id: 80, name: "Steadfast"},
|
||||||
|
{id: 81, name: "Snow Cloak"},
|
||||||
|
{id: 82, name: "Gluttony"},
|
||||||
|
{id: 83, name: "Anger Point"},
|
||||||
|
{id: 84, name: "Unburden"},
|
||||||
|
{id: 85, name: "Heatproof"},
|
||||||
|
{id: 86, name: "Simple"},
|
||||||
|
{id: 87, name: "Dry Skin"},
|
||||||
|
{id: 88, name: "Download"},
|
||||||
|
{id: 89, name: "Iron Fist"},
|
||||||
|
{id: 90, name: "Poison Heal"},
|
||||||
|
{id: 91, name: "Adaptability"},
|
||||||
|
{id: 92, name: "Skill Link"},
|
||||||
|
{id: 93, name: "Hydration"},
|
||||||
|
{id: 94, name: "Solar Power"},
|
||||||
|
{id: 95, name: "Quick Feet"},
|
||||||
|
{id: 96, name: "Normalize"},
|
||||||
|
{id: 97, name: "Sniper"},
|
||||||
|
{id: 98, name: "Magic Guard"},
|
||||||
|
{id: 99, name: "No Guard"},
|
||||||
|
{id: 100, name: "Stall"},
|
||||||
|
{id: 101, name: "Technician"},
|
||||||
|
{id: 102, name: "Leaf Guard"},
|
||||||
|
{id: 103, name: "Klutz"},
|
||||||
|
{id: 104, name: "Mold Breaker"},
|
||||||
|
{id: 105, name: "Super Luck"},
|
||||||
|
{id: 106, name: "Aftermath"},
|
||||||
|
{id: 107, name: "Anticipation"},
|
||||||
|
{id: 108, name: "Forewarn"},
|
||||||
|
{id: 109, name: "Unaware"},
|
||||||
|
{id: 110, name: "Tinted Lens"},
|
||||||
|
{id: 111, name: "Filter"},
|
||||||
|
{id: 112, name: "Slow Start"},
|
||||||
|
{id: 113, name: "Scrappy"},
|
||||||
|
{id: 114, name: "Storm Drain"},
|
||||||
|
{id: 115, name: "Ice Body"},
|
||||||
|
{id: 116, name: "Solid Rock"},
|
||||||
|
{id: 117, name: "Snow Warning"},
|
||||||
|
{id: 118, name: "Honey Gather"},
|
||||||
|
{id: 119, name: "Frisk"},
|
||||||
|
{id: 120, name: "Reckless"},
|
||||||
|
{id: 121, name: "Multitype"},
|
||||||
|
{id: 122, name: "Flower Gift"},
|
||||||
|
{id: 123, name: "Bad Dreams"},
|
||||||
|
{id: 124, name: "Pickpocket"},
|
||||||
|
{id: 125, name: "Sheer Force"},
|
||||||
|
{id: 126, name: "Contrary"},
|
||||||
|
{id: 127, name: "Unnerve"},
|
||||||
|
{id: 128, name: "Defiant"},
|
||||||
|
{id: 129, name: "Defeatist"},
|
||||||
|
{id: 130, name: "Cursed Body"},
|
||||||
|
{id: 131, name: "Healer"},
|
||||||
|
{id: 132, name: "Friend Guard"},
|
||||||
|
{id: 133, name: "Weak Armor"},
|
||||||
|
{id: 134, name: "Heavy Metal"},
|
||||||
|
{id: 135, name: "Light Metal"},
|
||||||
|
{id: 136, name: "Multiscale"},
|
||||||
|
{id: 137, name: "Toxic Boost"},
|
||||||
|
{id: 138, name: "Flare Boost"},
|
||||||
|
{id: 139, name: "Harvest"},
|
||||||
|
{id: 140, name: "Telepathy"},
|
||||||
|
{id: 141, name: "Moody"},
|
||||||
|
{id: 142, name: "Overcoat"},
|
||||||
|
{id: 143, name: "Poison Touch"},
|
||||||
|
{id: 144, name: "Regenerator"},
|
||||||
|
{id: 145, name: "Big Pecks"},
|
||||||
|
{id: 146, name: "Sand Rush"},
|
||||||
|
{id: 147, name: "Wonder Skin"},
|
||||||
|
{id: 148, name: "Analytic"},
|
||||||
|
{id: 149, name: "Illusion"},
|
||||||
|
{id: 150, name: "Imposter"},
|
||||||
|
{id: 151, name: "Infiltrator"},
|
||||||
|
{id: 152, name: "Mummy"},
|
||||||
|
{id: 153, name: "Moxie"},
|
||||||
|
{id: 154, name: "Justified"},
|
||||||
|
{id: 155, name: "Rattled"},
|
||||||
|
{id: 156, name: "Magic Bounce"},
|
||||||
|
{id: 157, name: "Sap Sipper"},
|
||||||
|
{id: 158, name: "Prankster"},
|
||||||
|
{id: 159, name: "Sand Force"},
|
||||||
|
{id: 160, name: "Iron Barbs"},
|
||||||
|
{id: 161, name: "Zen Mode"},
|
||||||
|
{id: 162, name: "Victory Star"},
|
||||||
|
{id: 163, name: "Turboblaze"},
|
||||||
|
{id: 164, name: "Teravolt"}
|
||||||
|
];
|
||||||
|
|
||||||
// Items
|
// Items
|
||||||
let items = [
|
let items = [
|
||||||
{id: 1, name: "Master Ball"},
|
{id: 1, name: "Master Ball"},
|
||||||
@@ -2390,14 +2560,20 @@ const REGION_LIST = [];
|
|||||||
// Index data
|
// Index data
|
||||||
for(let i in species) {
|
for(let i in species) {
|
||||||
let entry = species[i];
|
let entry = species[i];
|
||||||
POKE_SPECIES_MAP[entry.id] = entry;
|
SPECIES_MAP[entry.id] = entry;
|
||||||
POKE_SPECIES_LIST[i] = entry;
|
SPECIES_LIST[i] = entry;
|
||||||
}
|
}
|
||||||
|
|
||||||
for(let i in moves) {
|
for(let i in moves) {
|
||||||
let move = moves[i];
|
let move = moves[i];
|
||||||
POKE_MOVE_MAP[move.id] = move;
|
MOVE_MAP[move.id] = move;
|
||||||
POKE_MOVE_LIST[i] = move;
|
MOVE_LIST[i] = move;
|
||||||
|
}
|
||||||
|
|
||||||
|
for(let i in abilities) {
|
||||||
|
let ability = abilities[i];
|
||||||
|
ABILITY_MAP[ability.id] = ability;
|
||||||
|
ABILITY_LIST[i] = ability;
|
||||||
}
|
}
|
||||||
|
|
||||||
for(let i in items) {
|
for(let i in items) {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ const ELEMENT_GAME_SUMMARY = document.getElementById("game-summary");
|
|||||||
|
|
||||||
const ELEMENT_DREAMER_SPRITE = document.getElementById("dreamer-sprite");
|
const ELEMENT_DREAMER_SPRITE = document.getElementById("dreamer-sprite");
|
||||||
const ELEMENT_DREAMER_SPECIES = document.getElementById("dreamer-species");
|
const ELEMENT_DREAMER_SPECIES = document.getElementById("dreamer-species");
|
||||||
|
const ELEMENT_DREAMER_ABILITY = document.getElementById("dreamer-ability");
|
||||||
const ELEMENT_DREAMER_NATURE = document.getElementById("dreamer-nature");
|
const ELEMENT_DREAMER_NATURE = document.getElementById("dreamer-nature");
|
||||||
const ELEMENT_DREAMER_NAME = document.getElementById("dreamer-name");
|
const ELEMENT_DREAMER_NAME = document.getElementById("dreamer-name");
|
||||||
const ELEMENT_DREAMER_GENDER = document.getElementById("dreamer-gender");
|
const ELEMENT_DREAMER_GENDER = document.getElementById("dreamer-gender");
|
||||||
@@ -10,15 +11,18 @@ const ELEMENT_DREAMER_TRAINER = document.getElementById("dreamer-trainer");
|
|||||||
const ELEMENT_DREAMER_TRAINER_ID = document.getElementById("dreamer-trainer-id");
|
const ELEMENT_DREAMER_TRAINER_ID = document.getElementById("dreamer-trainer-id");
|
||||||
const ELEMENT_DREAMER_LEVEL = document.getElementById("dreamer-level");
|
const ELEMENT_DREAMER_LEVEL = document.getElementById("dreamer-level");
|
||||||
|
|
||||||
|
const ELEMENT_ENCOUNTER_CONFIG = document.getElementById("encounter-config");
|
||||||
const ELEMENT_ENCOUNTER_SPECIES = document.getElementById("encounter-form-species");
|
const ELEMENT_ENCOUNTER_SPECIES = document.getElementById("encounter-form-species");
|
||||||
const ELEMENT_ENCOUNTER_MOVE = document.getElementById("encounter-form-move");
|
const ELEMENT_ENCOUNTER_MOVE = document.getElementById("encounter-form-move");
|
||||||
const ELEMENT_ENCOUNTER_FORM = document.getElementById("encounter-form-form");
|
const ELEMENT_ENCOUNTER_FORM = document.getElementById("encounter-form-form");
|
||||||
const ELEMENT_ENCOUNTER_GENDER = document.getElementById("encounter-form-gender");
|
const ELEMENT_ENCOUNTER_GENDER = document.getElementById("encounter-form-gender");
|
||||||
const ELEMENT_ENCOUNTER_ANIMATION = document.getElementById("encounter-form-animation");
|
const ELEMENT_ENCOUNTER_ANIMATION = document.getElementById("encounter-form-animation");
|
||||||
|
|
||||||
|
const ELEMENT_ITEM_CONFIG = document.getElementById("item-config");
|
||||||
const ELEMENT_ITEM_ID = document.getElementById("item-form-id");
|
const ELEMENT_ITEM_ID = document.getElementById("item-form-id");
|
||||||
const ELEMENT_ITEM_QUANTITY = document.getElementById("item-form-quantity");
|
const ELEMENT_ITEM_QUANTITY = document.getElementById("item-form-quantity");
|
||||||
|
|
||||||
|
const ELEMENT_VISITOR_CONFIG = document.getElementById("visitor-config");
|
||||||
const ELEMENT_VISITOR_NAME = document.getElementById("visitor-form-name");
|
const ELEMENT_VISITOR_NAME = document.getElementById("visitor-form-name");
|
||||||
const ELEMENT_VISITOR_TYPE = document.getElementById("visitor-form-type");
|
const ELEMENT_VISITOR_TYPE = document.getElementById("visitor-form-type");
|
||||||
const ELEMENT_VISITOR_SHOP_TYPE = document.getElementById("visitor-form-shop-type");
|
const ELEMENT_VISITOR_SHOP_TYPE = document.getElementById("visitor-form-shop-type");
|
||||||
@@ -28,6 +32,9 @@ const ELEMENT_VISITOR_SUBREGION = document.getElementById("visitor-form-subregio
|
|||||||
const ELEMENT_VISITOR_PERSONALITY = document.getElementById("visitor-form-personality");
|
const ELEMENT_VISITOR_PERSONALITY = document.getElementById("visitor-form-personality");
|
||||||
const ELEMENT_VISITOR_DREAMER = document.getElementById("visitor-form-dreamer");
|
const ELEMENT_VISITOR_DREAMER = document.getElementById("visitor-form-dreamer");
|
||||||
|
|
||||||
|
const ELEMENT_SKIN_PREVIEW = document.getElementById("skin-preview");
|
||||||
|
const ELEMENT_SKIN_PREVIEW_IMAGE = document.getElementById("skin-preview-image");
|
||||||
|
|
||||||
const ELEMENT_CGEAR_SKIN_INPUT = document.getElementById("cgear-skin");
|
const ELEMENT_CGEAR_SKIN_INPUT = document.getElementById("cgear-skin");
|
||||||
const ELEMENT_DEX_SKIN_INPUT = document.getElementById("dex-skin");
|
const ELEMENT_DEX_SKIN_INPUT = document.getElementById("dex-skin");
|
||||||
const ELEMENT_MUSICAL_INPUT = document.getElementById("musical");
|
const ELEMENT_MUSICAL_INPUT = document.getElementById("musical");
|
||||||
@@ -59,7 +66,8 @@ var profile = {
|
|||||||
if(response.dreamerInfo) {
|
if(response.dreamerInfo) {
|
||||||
let dreamerInfo = response.dreamerInfo;
|
let dreamerInfo = response.dreamerInfo;
|
||||||
ELEMENT_DREAMER_SPRITE.innerHTML = "<image src='" + response.dreamerSprite + "'/>";
|
ELEMENT_DREAMER_SPRITE.innerHTML = "<image src='" + response.dreamerSprite + "'/>";
|
||||||
ELEMENT_DREAMER_SPECIES.innerHTML = POKE_SPECIES_LIST[dreamerInfo.species - 1].name;
|
ELEMENT_DREAMER_SPECIES.innerHTML = SPECIES_MAP[dreamerInfo.species] ? (SPECIES_MAP[dreamerInfo.species].name) : "N/A";
|
||||||
|
ELEMENT_DREAMER_ABILITY.innerHTML = ABILITY_MAP[dreamerInfo.ability] ? ABILITY_MAP[dreamerInfo.ability].name : "N/A";
|
||||||
ELEMENT_DREAMER_NATURE.innerHTML = stringToWord(dreamerInfo.nature);
|
ELEMENT_DREAMER_NATURE.innerHTML = stringToWord(dreamerInfo.nature);
|
||||||
ELEMENT_DREAMER_NAME.innerHTML = dreamerInfo.nickname;
|
ELEMENT_DREAMER_NAME.innerHTML = dreamerInfo.nickname;
|
||||||
ELEMENT_DREAMER_GENDER.innerHTML = stringToWord(dreamerInfo.gender);
|
ELEMENT_DREAMER_GENDER.innerHTML = stringToWord(dreamerInfo.gender);
|
||||||
@@ -90,6 +98,17 @@ var profile = {
|
|||||||
profile.cgearSkin = response.cgearSkin ? response.cgearSkin : "none";
|
profile.cgearSkin = response.cgearSkin ? response.cgearSkin : "none";
|
||||||
profile.dexSkin = response.dexSkin ? response.dexSkin : "none";
|
profile.dexSkin = response.dexSkin ? response.dexSkin : "none";
|
||||||
profile.musical = response.musical ? response.musical : "none";
|
profile.musical = response.musical ? response.musical : "none";
|
||||||
|
|
||||||
|
if(response.customCGearSkin) {
|
||||||
|
profile.customCGearSkin = response.customCGearSkin;
|
||||||
|
ELEMENT_CGEAR_SKIN_INPUT.add(new Option(response.customCGearSkin, "custom"), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(response.customDexSkin) {
|
||||||
|
profile.customDexSkin = response.customDexSkin;
|
||||||
|
ELEMENT_DEX_SKIN_INPUT.add(new Option(response.customDexSkin, "custom"), 1);
|
||||||
|
}
|
||||||
|
|
||||||
fetchDlcData();
|
fetchDlcData();
|
||||||
ELEMENT_LEVEL_GAIN_INPUT.value = response.levelsGained;
|
ELEMENT_LEVEL_GAIN_INPUT.value = response.levelsGained;
|
||||||
|
|
||||||
@@ -103,8 +122,8 @@ var profile = {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Sort data lists alphabetically
|
// Sort data lists alphabetically
|
||||||
let sortedSpecies = [...POKE_SPECIES_LIST].sort((a, b) => a.name.localeCompare(b.name));
|
let sortedSpecies = [...SPECIES_LIST].sort((a, b) => a.name.localeCompare(b.name));
|
||||||
let sortedMoves = [...POKE_MOVE_LIST].sort((a, b) => a.name.localeCompare(b.name));
|
let sortedMoves = [...MOVE_LIST].sort((a, b) => a.name.localeCompare(b.name));
|
||||||
let sortedItems = [...ITEM_LIST].sort((a, b) => a.name.localeCompare(b.name));
|
let sortedItems = [...ITEM_LIST].sort((a, b) => a.name.localeCompare(b.name));
|
||||||
|
|
||||||
// Add species data
|
// Add species data
|
||||||
@@ -138,10 +157,10 @@ var profile = {
|
|||||||
ELEMENT_VISITOR_REGION.options[ELEMENT_VISITOR_REGION.options.length] = new Option(region.name, region.id);
|
ELEMENT_VISITOR_REGION.options[ELEMENT_VISITOR_REGION.options.length] = new Option(region.name, region.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Event listener for changing the form selector contents when species changes
|
// Event listener for changing the form & gender selector contents when species changes
|
||||||
ELEMENT_ENCOUNTER_SPECIES.addEventListener("change", function() {
|
ELEMENT_ENCOUNTER_SPECIES.addEventListener("change", function() {
|
||||||
updateEncounterFormOptions();
|
ELEMENT_ENCOUNTER_FORM.value = updateEncounterFormOptions();
|
||||||
ELEMENT_ENCOUNTER_FORM.value = 0;
|
ELEMENT_ENCOUNTER_GENDER.value = updateEncounterGenderOptions();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Same thing, but for Join Avenue visitor region & subregion
|
// Same thing, but for Join Avenue visitor region & subregion
|
||||||
@@ -156,7 +175,7 @@ var profile = {
|
|||||||
|
|
||||||
function updateEncounterFormOptions() {
|
function updateEncounterFormOptions() {
|
||||||
clearSelectOptions(ELEMENT_ENCOUNTER_FORM);
|
clearSelectOptions(ELEMENT_ENCOUNTER_FORM);
|
||||||
let species = POKE_SPECIES_MAP[ELEMENT_ENCOUNTER_SPECIES.value];
|
let species = SPECIES_MAP[ELEMENT_ENCOUNTER_SPECIES.value];
|
||||||
|
|
||||||
// Update special form options
|
// Update special form options
|
||||||
if(species.forms) {
|
if(species.forms) {
|
||||||
@@ -166,17 +185,47 @@ function updateEncounterFormOptions() {
|
|||||||
} else {
|
} else {
|
||||||
ELEMENT_ENCOUNTER_FORM.options[ELEMENT_ENCOUNTER_FORM.options.length] = new Option("N/A", 0);
|
ELEMENT_ENCOUNTER_FORM.options[ELEMENT_ENCOUNTER_FORM.options.length] = new Option("N/A", 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateEncounterGenderOptions() {
|
||||||
|
clearSelectOptions(ELEMENT_ENCOUNTER_GENDER);
|
||||||
|
let species = SPECIES_MAP[ELEMENT_ENCOUNTER_SPECIES.value];
|
||||||
|
|
||||||
|
// Update gender options
|
||||||
|
if(species.gender) {
|
||||||
|
console.log(species.gender);
|
||||||
|
switch(species.gender) {
|
||||||
|
case "male":
|
||||||
|
ELEMENT_ENCOUNTER_GENDER.options[0] = new Option("Male", "MALE");
|
||||||
|
return "MALE";
|
||||||
|
case "female":
|
||||||
|
ELEMENT_ENCOUNTER_GENDER.options[0] = new Option("Female", "FEMALE");
|
||||||
|
return "FEMALE";
|
||||||
|
case "unknown":
|
||||||
|
ELEMENT_ENCOUNTER_GENDER.options[0] = new Option("N/A", "GENDERLESS");
|
||||||
|
return "GENDERLESS";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ELEMENT_ENCOUNTER_GENDER.options[0] = new Option("Male", "MALE");
|
||||||
|
ELEMENT_ENCOUNTER_GENDER.options[1] = new Option("Female", "FEMALE");
|
||||||
|
ELEMENT_ENCOUNTER_GENDER.options[2] = new Option("Random", "GENDERLESS");
|
||||||
|
return "GENDERLESS";
|
||||||
}
|
}
|
||||||
|
|
||||||
function configureEncounter(index) {
|
function configureEncounter(index) {
|
||||||
encounterTableIndex = Math.min(10, Math.min(index, profile.encounters.length));
|
encounterTableIndex = Math.min(10, Math.min(index, profile.encounters.length));
|
||||||
let encounter = profile.encounters[encounterTableIndex];
|
let encounter = profile.encounters[encounterTableIndex];
|
||||||
ELEMENT_ENCOUNTER_SPECIES.value = encounter ? encounter.species : 1;
|
ELEMENT_ENCOUNTER_SPECIES.value = encounter ? encounter.species : 493;
|
||||||
updateEncounterFormOptions();
|
let form = updateEncounterFormOptions();
|
||||||
|
let gender = updateEncounterGenderOptions();
|
||||||
ELEMENT_ENCOUNTER_MOVE.value = encounter ? encounter.move : 0;
|
ELEMENT_ENCOUNTER_MOVE.value = encounter ? encounter.move : 0;
|
||||||
ELEMENT_ENCOUNTER_FORM.value = encounter ? encounter.form : 0;
|
ELEMENT_ENCOUNTER_FORM.value = encounter ? encounter.form : form;
|
||||||
ELEMENT_ENCOUNTER_GENDER.value = encounter ? encounter.gender : "GENDERLESS";
|
ELEMENT_ENCOUNTER_GENDER.value = encounter ? encounter.gender : gender;
|
||||||
ELEMENT_ENCOUNTER_ANIMATION.value = encounter ? encounter.animation : "WALK_AROUND";
|
ELEMENT_ENCOUNTER_ANIMATION.value = encounter ? encounter.animation : "LOOK_AROUND";
|
||||||
|
ELEMENT_ENCOUNTER_CONFIG.style.display = "block";
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveEncounter() {
|
function saveEncounter() {
|
||||||
@@ -223,13 +272,20 @@ function updateEncounterCell(index) {
|
|||||||
if(encounterData) {
|
if(encounterData) {
|
||||||
spriteImage = spriteBase + encounterData.species + ".png";
|
spriteImage = spriteBase + encounterData.species + ".png";
|
||||||
|
|
||||||
// Use unique form sprite if it exists
|
|
||||||
if(encounterData.form > 0) {
|
if(encounterData.form > 0) {
|
||||||
|
// Use unique form sprite if it exists
|
||||||
let formSpriteImage = spriteBase + encounterData.species + "-" + encounterData.form + ".png";
|
let formSpriteImage = spriteBase + encounterData.species + "-" + encounterData.form + ".png";
|
||||||
|
|
||||||
if(checkURL(formSpriteImage)){
|
if(checkURL(formSpriteImage)){
|
||||||
spriteImage = formSpriteImage;
|
spriteImage = formSpriteImage;
|
||||||
}
|
}
|
||||||
|
} else if(encounterData.gender == "FEMALE") {
|
||||||
|
// Otherwise, use female sprite if it exists
|
||||||
|
let femaleSpriteImage = spriteBase + "female/" + encounterData.species + ".png";
|
||||||
|
|
||||||
|
if(checkURL(femaleSpriteImage)){
|
||||||
|
spriteImage = femaleSpriteImage;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -238,7 +294,7 @@ function updateEncounterCell(index) {
|
|||||||
|
|
||||||
function closeEncounterForm() {
|
function closeEncounterForm() {
|
||||||
encounterTableIndex = -1;
|
encounterTableIndex = -1;
|
||||||
window.location.href = "#";
|
ELEMENT_ENCOUNTER_CONFIG.style.display = "none";
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -278,6 +334,7 @@ function configureVisitor(index) {
|
|||||||
ELEMENT_VISITOR_SUBREGION.value = visitor ? visitor.stateProvinceCode : 0;
|
ELEMENT_VISITOR_SUBREGION.value = visitor ? visitor.stateProvinceCode : 0;
|
||||||
ELEMENT_VISITOR_PERSONALITY.value = visitor ? visitor.personality : 0;
|
ELEMENT_VISITOR_PERSONALITY.value = visitor ? visitor.personality : 0;
|
||||||
ELEMENT_VISITOR_DREAMER.value = visitor ? visitor.dreamerSpecies : 1;
|
ELEMENT_VISITOR_DREAMER.value = visitor ? visitor.dreamerSpecies : 1;
|
||||||
|
ELEMENT_VISITOR_CONFIG.style.display = "block";
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveVisitor() {
|
function saveVisitor() {
|
||||||
@@ -355,7 +412,7 @@ function updateVisitorCell(index) {
|
|||||||
|
|
||||||
function closeVisitorForm() {
|
function closeVisitorForm() {
|
||||||
visitorTableIndex = -1;
|
visitorTableIndex = -1;
|
||||||
window.location.href = "#";
|
ELEMENT_VISITOR_CONFIG.style.display = "none";
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -367,6 +424,7 @@ function configureItem(index) {
|
|||||||
let item = profile.items[itemTableIndex];
|
let item = profile.items[itemTableIndex];
|
||||||
ELEMENT_ITEM_ID.value = item ? item.id : 1;
|
ELEMENT_ITEM_ID.value = item ? item.id : 1;
|
||||||
ELEMENT_ITEM_QUANTITY.value = item ? item.quantity : 1;
|
ELEMENT_ITEM_QUANTITY.value = item ? item.quantity : 1;
|
||||||
|
ELEMENT_ITEM_CONFIG.style.display = "block";
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveItem() {
|
function saveItem() {
|
||||||
@@ -422,13 +480,49 @@ function updateItemCell(index) {
|
|||||||
|
|
||||||
function closeItemForm() {
|
function closeItemForm() {
|
||||||
itemTableIndex = -1;
|
itemTableIndex = -1;
|
||||||
window.location.href = "#";
|
ELEMENT_ITEM_CONFIG.style.display = "none";
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Miscellaneous stuff
|
* Miscellaneous stuff
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
function openFileChooser(inputDocumentId) {
|
||||||
|
document.getElementById(inputDocumentId).click();
|
||||||
|
}
|
||||||
|
|
||||||
|
function uploadSkin(type, file) {
|
||||||
|
fetchData("POST", "/dashboard/uploadskin?type=" + (type == "CGEAR" && isVersion2() ? "CGEAR2" : type)
|
||||||
|
+ "&filename=" + file.name, file).then((response) => {
|
||||||
|
if(!response) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display message if there is one
|
||||||
|
if(response.message) {
|
||||||
|
window.alert(response.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return if error
|
||||||
|
if(response.error) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update selector option
|
||||||
|
let selectElement = type == "CGEAR" ? ELEMENT_CGEAR_SKIN_INPUT : ELEMENT_DEX_SKIN_INPUT;
|
||||||
|
|
||||||
|
// Add option for custom skin if one does not already exist
|
||||||
|
// Otherwise, update the existing one
|
||||||
|
if(selectElement.options[1].value == "custom") {
|
||||||
|
selectElement.options[1].innerHTML = file.name;
|
||||||
|
} else{
|
||||||
|
selectElement.add(new Option(file.name, "custom"), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
selectElement.value = "custom";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function previewSkin(inputElementId, type) {
|
function previewSkin(inputElementId, type) {
|
||||||
let value = document.getElementById(inputElementId).value;
|
let value = document.getElementById(inputElementId).value;
|
||||||
|
|
||||||
@@ -441,27 +535,46 @@ function previewSkin(inputElementId, type) {
|
|||||||
type = "CGEAR2";
|
type = "CGEAR2";
|
||||||
}
|
}
|
||||||
|
|
||||||
window.open("/dashboard/previewskin?type=" + type + "&name=" + value);
|
let revisionParam = value == "custom" ? "&revision=" + new Date().getTime() : ""; // Hack to work around the browser cache
|
||||||
|
ELEMENT_SKIN_PREVIEW_IMAGE.src = "/dashboard/previewskin?type=" + type + "&name=" + value + revisionParam;
|
||||||
|
ELEMENT_SKIN_PREVIEW.style.display = "block";
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setSkinPreviewResolution(width, height) {
|
||||||
|
ELEMENT_SKIN_PREVIEW_IMAGE.width = width;
|
||||||
|
ELEMENT_SKIN_PREVIEW_IMAGE.height = height;
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeSkinPreview() {
|
||||||
|
ELEMENT_SKIN_PREVIEW.style.display = "none";
|
||||||
|
}
|
||||||
|
|
||||||
function fetchDlcData() {
|
function fetchDlcData() {
|
||||||
// Fetch C-Gear skins
|
// Fetch C-Gear skins
|
||||||
fetchData("GET", "/dashboard/dlc?type=" + (isVersion2() ? "CGEAR2" : "CGEAR")).then((response) => {
|
fetchData("GET", "/dashboard/dlc?type=" + (isVersion2() ? "CGEAR2" : "CGEAR")).then((response) => {
|
||||||
addDlcNames(ELEMENT_CGEAR_SKIN_INPUT, response);
|
if(response) {
|
||||||
ELEMENT_CGEAR_SKIN_INPUT.value = response.includes(profile.cgearSkin) ? profile.cgearSkin : "none";
|
addDlcNames(ELEMENT_CGEAR_SKIN_INPUT, response);
|
||||||
|
ELEMENT_CGEAR_SKIN_INPUT.value = profile.cgearSkin == "custom" && profile.customCGearSkin ? "custom"
|
||||||
|
: response.includes(profile.cgearSkin) ? profile.cgearSkin : "none";
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Fetch Dex skins
|
// Fetch Dex skins
|
||||||
fetchData("GET", "/dashboard/dlc?type=ZUKAN").then((response) => {
|
fetchData("GET", "/dashboard/dlc?type=ZUKAN").then((response) => {
|
||||||
addDlcNames(ELEMENT_DEX_SKIN_INPUT, response);
|
if(response) {
|
||||||
ELEMENT_DEX_SKIN_INPUT.value = response.includes(profile.dexSkin) ? profile.dexSkin : "none";
|
addDlcNames(ELEMENT_DEX_SKIN_INPUT, response);
|
||||||
|
ELEMENT_DEX_SKIN_INPUT.value = profile.dexSkin == "custom" && profile.customDexSkin ? "custom"
|
||||||
|
: response.includes(profile.dexSkin) ? profile.dexSkin : "none";
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Fetch musicals
|
// Fetch musicals
|
||||||
fetchData("GET", "/dashboard/dlc?type=MUSICAL").then((response) => {
|
fetchData("GET", "/dashboard/dlc?type=MUSICAL").then((response) => {
|
||||||
addDlcNames(ELEMENT_MUSICAL_INPUT, response);
|
if(response) {
|
||||||
ELEMENT_MUSICAL_INPUT.value = response.includes(profile.musical) ? profile.musical : "none";
|
addDlcNames(ELEMENT_MUSICAL_INPUT, response);
|
||||||
|
ELEMENT_MUSICAL_INPUT.value = response.includes(profile.musical) ? profile.musical : "none";
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -482,7 +595,9 @@ function postProfileData() {
|
|||||||
musical: ELEMENT_MUSICAL_INPUT.value,
|
musical: ELEMENT_MUSICAL_INPUT.value,
|
||||||
gainedLevels: ELEMENT_LEVEL_GAIN_INPUT.value
|
gainedLevels: ELEMENT_LEVEL_GAIN_INPUT.value
|
||||||
})).then((response) => {
|
})).then((response) => {
|
||||||
alert(response.message);
|
if(response) {
|
||||||
|
alert(response.message);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -42,11 +42,11 @@ async function fetchData(method, path, body) {
|
|||||||
if(response.status != 200) {
|
if(response.status != 200) {
|
||||||
if(response.status == 401) {
|
if(response.status == 401) {
|
||||||
window.location.href = "/dashboard/login.html"; // TODO not epic idea to put this here
|
window.location.href = "/dashboard/login.html"; // TODO not epic idea to put this here
|
||||||
return {};
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
window.alert("Server returned status code " + response.status + " while fetching " + path);
|
window.alert("Server returned status code " + response.status + " while fetching " + path);
|
||||||
return {};
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -55,5 +55,5 @@ async function fetchData(method, path, body) {
|
|||||||
window.alert("Could not deserialize JSON response: " + error);
|
window.alert("Could not deserialize JSON response: " + error);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {};
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,21 +11,22 @@ input {
|
|||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin-top: 4px;
|
margin-top: 4px;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 24px;
|
||||||
padding: 4px 4px;
|
padding: 4px 4px;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
.big-button {
|
||||||
color: white;
|
color: white;
|
||||||
background-color: #3D3D3D;
|
background-color: #3D3D3D;
|
||||||
border: 0px;
|
border: 0px;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
|
font-family: sans-serif;
|
||||||
padding: 16px 32px;
|
padding: 16px 32px;
|
||||||
}
|
}
|
||||||
|
|
||||||
button:active {
|
.big-button:active {
|
||||||
background-color: #333333;
|
background-color: #333333;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,12 +12,15 @@ img {
|
|||||||
image-rendering: pixelated;
|
image-rendering: pixelated;
|
||||||
}
|
}
|
||||||
|
|
||||||
input, select {
|
input[type=text], input[type=number], select {
|
||||||
color: white;
|
|
||||||
background-color: #262626;
|
|
||||||
border: 0px;
|
border: 0px;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
input, select {
|
||||||
|
color: white;
|
||||||
|
background-color: #262626;
|
||||||
margin-top: 4px;
|
margin-top: 4px;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
padding: 4px 4px;
|
padding: 4px 4px;
|
||||||
@@ -39,14 +42,14 @@ a:link, a:visited {
|
|||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.root-container {
|
.centered-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.root-container div:first-child {
|
.centered-container div:first-child {
|
||||||
max-width: 532px;
|
max-width: 532px;
|
||||||
margin: auto;
|
margin: auto;
|
||||||
}
|
}
|
||||||
@@ -57,7 +60,8 @@ a:link, a:visited {
|
|||||||
gap: 5px 10px;
|
gap: 5px 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.grid-container input {
|
.grid-container input,
|
||||||
|
.grid-container button {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin-top: 4px;
|
margin-top: 4px;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
@@ -86,12 +90,6 @@ a:link, a:visited {
|
|||||||
padding-left: 5px;
|
padding-left: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dreamer-summary td#dreamer-sprite {
|
|
||||||
width: 96px;
|
|
||||||
text-align: center;
|
|
||||||
padding-left: 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-text {
|
.header-text {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
@@ -100,37 +98,27 @@ a:link, a:visited {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.popup {
|
.popup {
|
||||||
|
display: none;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
background: rgba(0, 0, 0, 0.6);
|
background: rgba(0, 0, 0, 0.6);
|
||||||
opacity: 0;
|
|
||||||
transition: opacity 200ms;
|
|
||||||
visibility: hidden;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.popup:target {
|
.content {
|
||||||
opacity: 1;
|
position: relative;
|
||||||
visibility: visible;
|
|
||||||
}
|
|
||||||
|
|
||||||
.popup .close-button {
|
|
||||||
position: absolute;
|
|
||||||
background-color: #3D3D3D;
|
|
||||||
top: 10px;
|
|
||||||
right: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.popup .content {
|
|
||||||
background-color: #191919;
|
background-color: #191919;
|
||||||
position: absolute;
|
|
||||||
left: 50%;
|
|
||||||
top: 50%;
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
padding: 20px;
|
|
||||||
width: 500px;
|
width: 500px;
|
||||||
|
padding: 20px 20px 0px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-button {
|
||||||
|
position: absolute;
|
||||||
|
top: 8px;
|
||||||
|
right: 8px;
|
||||||
|
background-color: #3D3D3D;
|
||||||
}
|
}
|
||||||
|
|
||||||
.encounter-image-table {
|
.encounter-image-table {
|
||||||
@@ -147,6 +135,7 @@ a:link, a:visited {
|
|||||||
width: 96px;
|
width: 96px;
|
||||||
height: 96px;
|
height: 96px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.item-table {
|
.item-table {
|
||||||
@@ -163,6 +152,7 @@ a:link, a:visited {
|
|||||||
width: 48px;
|
width: 48px;
|
||||||
height: 48px;
|
height: 48px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.item-table td a {
|
.item-table td a {
|
||||||
@@ -185,10 +175,31 @@ a:link, a:visited {
|
|||||||
width: 80px;
|
width: 80px;
|
||||||
height: 80px;
|
height: 80px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.big-button {
|
.big-button {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
padding: 16px 32px;
|
padding: 16px 32px;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#cgear-skin, #dex-skin {
|
||||||
|
margin-bottom: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#dreamer-sprite {
|
||||||
|
width: 96px;
|
||||||
|
text-align: center;
|
||||||
|
padding-left: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#skin-preview-content {
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
#skin-preview-image {
|
||||||
|
box-shadow: 0px 0px 10px 3px #0A0A0A;
|
||||||
|
margin-top: 4px;
|
||||||
|
margin-bottom: 12px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,59 +1,62 @@
|
|||||||
/dlc/IRAO/CGEAR/01 - Default.bin
|
/dlc/IRAO/CGEAR/01 - Default.bin 19c71995ffb00f4bdc59bfa805eee8fa
|
||||||
/dlc/IRAO/CGEAR/02 - Meadow Munna.bin
|
/dlc/IRAO/CGEAR/02 - Meadow Munna.bin d434decfee8faad9485560fbc9206273
|
||||||
/dlc/IRAO/CGEAR/03 - Aim for the Top.bin
|
/dlc/IRAO/CGEAR/03 - Aim for the Top.bin c93f457ae1310bfe03f7ede622c7f63a
|
||||||
/dlc/IRAO/CGEAR/04 - Twinkle Minccino.bin
|
/dlc/IRAO/CGEAR/04 - Twinkle Minccino.bin 86902dc9ec625e05673697367d492898
|
||||||
/dlc/IRAO/CGEAR/05 - PokeSma Purrloin.bin
|
/dlc/IRAO/CGEAR/05 - PokeSma Purrloin.bin 0efe27a2cf7e48de2273ccba0fda2031
|
||||||
/dlc/IRAO/CGEAR/06 - Earful Audino.bin
|
/dlc/IRAO/CGEAR/06 - Earful Audino.bin ee383864a67e6a04235c7f6a891a9539
|
||||||
/dlc/IRAO/CGEAR/07 - Happy Piplup.bin
|
/dlc/IRAO/CGEAR/07 - Happy Piplup.bin 88d0dfac0dda854486b8b5fb839e0bfd
|
||||||
/dlc/IRAO/CGEAR/08 - Poison Jab Croagunk.bin
|
/dlc/IRAO/CGEAR/08 - Poison Jab Croagunk.bin 7294c3e4db163beb98ea49be10d8eb2a
|
||||||
/dlc/IRAO/CGEAR/09 - Venusaur!.bin
|
/dlc/IRAO/CGEAR/09 - Venusaur!.bin 7c51d55a63117bb166cc507f064f7cca
|
||||||
/dlc/IRAO/CGEAR/10 - Charizard!.bin
|
/dlc/IRAO/CGEAR/10 - Charizard!.bin a8ccb7866393efda3422656aa2d8f292
|
||||||
/dlc/IRAO/CGEAR/11 - Blastoise!.bin
|
/dlc/IRAO/CGEAR/11 - Blastoise!.bin e0a1b44450407af394f390c0b229e7a6
|
||||||
/dlc/IRAO/CGEAR/12 - V for Victory!.bin
|
/dlc/IRAO/CGEAR/12 - V for Victory!.bin 847d6d230fe2513758d50adac52d54b8
|
||||||
/dlc/IRAO/CGEAR/13 - Virtual Pokemon.bin
|
/dlc/IRAO/CGEAR/13 - Virtual Pokemon.bin 002263910ed018ff04922c4f3971b777
|
||||||
/dlc/IRAO/CGEAR/14 - Pokemon Cafe.bin
|
/dlc/IRAO/CGEAR/14 - Pokemon Cafe.bin efd6e6de86ea892dda840bcc6952afc1
|
||||||
/dlc/IRAO/CGEAR/15 - Hero Reshiram.bin
|
/dlc/IRAO/CGEAR/15 - Hero Reshiram.bin d89ceeb9a9e7774fa802c1aa2f38409a
|
||||||
/dlc/IRAO/CGEAR/16 - Hero Zekrom.bin
|
/dlc/IRAO/CGEAR/16 - Hero Zekrom.bin 2548f0ed48e130649ff351ca418e3b7f
|
||||||
/dlc/IRAO/CGEAR/17 - 2011 Worlds C-Gear.bin
|
/dlc/IRAO/CGEAR/17 - 2011 Worlds C-Gear.bin 70119e0a5e67fb4c9dda7b724e157784
|
||||||
/dlc/IRAO/CGEAR/18 - Zoroark!.bin
|
/dlc/IRAO/CGEAR/18 - Zoroark!.bin 81e9241acb3270e2d490dd9966f3ecf1
|
||||||
/dlc/IRAO/CGEAR/19 - Spring Deerling.bin
|
/dlc/IRAO/CGEAR/19 - Spring Deerling.bin ca785e48d49269a182ee0fe0981168a8
|
||||||
/dlc/IRAO/CGEAR/20 - Summer Deerling.bin
|
/dlc/IRAO/CGEAR/20 - Summer Deerling.bin 0e8b6b527a411aeb302cfe07cb3679b2
|
||||||
/dlc/IRAO/CGEAR/21 - Autumn Deerling.bin
|
/dlc/IRAO/CGEAR/21 - Autumn Deerling.bin 7155fca4dc915fc6cb87495fa7a9c743
|
||||||
/dlc/IRAO/CGEAR/22 - Winter Deerling.bin
|
/dlc/IRAO/CGEAR/22 - Winter Deerling.bin 7f9d3448f40d0ea513c1f94e311e77bb
|
||||||
/dlc/IRAO/CGEAR/23 - CRUSTLE!.bin
|
/dlc/IRAO/CGEAR/23 - CRUSTLE!.bin eb1a0cb83e330375c257cb61bbc80e84
|
||||||
/dlc/IRAO/CGEAR/24 - KLINK!.bin
|
/dlc/IRAO/CGEAR/24 - KLINK!.bin f4ebe5cbb0de53c44f0c620215e840b4
|
||||||
/dlc/IRAO/CGEAR/25 - Ducklett Friends.bin
|
/dlc/IRAO/CGEAR/25 - Ducklett Friends.bin bb359435efd5e98f48beba2cf71126d9
|
||||||
/dlc/IRAO/CGEAR/26 - Guidance Cobalion.bin
|
/dlc/IRAO/CGEAR/26 - Guidance Cobalion.bin fcd86f0f98b1d770e28001c02f455c58
|
||||||
/dlc/IRAO/CGEAR/27 - Trial Terrakion.bin
|
/dlc/IRAO/CGEAR/27 - Trial Terrakion.bin 6c57b06edcaa0aeb6b716ecd5d3f2336
|
||||||
/dlc/IRAO/CGEAR/28 - Rumination Virizion.bin
|
/dlc/IRAO/CGEAR/28 - Rumination Virizion.bin b02dce72dfd25a4c8d417b87f3a99329
|
||||||
/dlc/IRAO/CGEAR/29 - Keldeo Ordinary Forme.bin
|
/dlc/IRAO/CGEAR/29 - Keldeo Ordinary Forme.bin a83088c9cf11277049ed472d302b0dd6
|
||||||
/dlc/IRAO/CGEAR/30 - Aria of the Night Sky.bin
|
/dlc/IRAO/CGEAR/30 - Aria of the Night Sky.bin 4306211f215bb0d397c92037812d37ae
|
||||||
/dlc/IRAO/CGEAR/31 - 2012 Worlds C-Gear.bin
|
/dlc/IRAO/CGEAR/31 - 2012 Worlds C-Gear.bin 225505e34dab2ccd300e3b4bd7eda47f
|
||||||
/dlc/IRAO/CGEAR/32 - Red Genesect.bin
|
/dlc/IRAO/CGEAR/32 - Red Genesect.bin 614be39e965dab594a5ffaac6bd5ae82
|
||||||
/dlc/IRAO/CGEAR2/01 - Default.bin
|
/dlc/IRAO/CGEAR/33 - PCN Cup 2024 Summer.bin f51d9e9045b51dcc1d02cc8091c520e7
|
||||||
/dlc/IRAO/CGEAR2/02 - Meadow Munna.bin
|
/dlc/IRAO/CGEAR2/01 - Default.bin d7ab435f5016ac3e6a7f444ccd25f424
|
||||||
/dlc/IRAO/CGEAR2/03 - Keldeo Resolute Forme.bin
|
/dlc/IRAO/CGEAR2/02 - Meadow Munna.bin 98b944531d9206512bbc5f59f8229139
|
||||||
/dlc/IRAO/CGEAR2/04 - 2012 Worlds C-Gear.bin
|
/dlc/IRAO/CGEAR2/03 - Keldeo Resolute Forme.bin e7bddaf878faa5b009caee7d96998954
|
||||||
/dlc/IRAO/CGEAR2/05 - Pumpkin Pikachu.bin
|
/dlc/IRAO/CGEAR2/04 - 2012 Worlds C-Gear.bin 36d5948c310428b251b70ec1dcf0845a
|
||||||
/dlc/IRAO/CGEAR2/06 - V for Victory!.bin
|
/dlc/IRAO/CGEAR2/05 - Pumpkin Pikachu.bin 7fd54cd2bd5310c40139b4329124aa2d
|
||||||
/dlc/IRAO/CGEAR2/07 - Hero Reshiram.bin
|
/dlc/IRAO/CGEAR2/06 - V for Victory!.bin 4f9a1ca435561cfe49d892488b398caa
|
||||||
/dlc/IRAO/CGEAR2/08 - Hero Zekrom.bin
|
/dlc/IRAO/CGEAR2/07 - Hero Reshiram.bin ec233268fb4d04cbb13b8b944552e94b
|
||||||
/dlc/IRAO/CGEAR2/09 - Aria of the Night Sky.bin
|
/dlc/IRAO/CGEAR2/08 - Hero Zekrom.bin 4647c982bbbd3955b4d8e4c9309ff7ef
|
||||||
/dlc/IRAO/CGEAR2/10 - Sleeping Eevee.bin
|
/dlc/IRAO/CGEAR2/09 - Aria of the Night Sky.bin 2f609aeda35b34b6386cfa66d57e0ad5
|
||||||
/dlc/IRAO/CGEAR2/11 - Venusaur!.bin
|
/dlc/IRAO/CGEAR2/10 - Sleeping Eevee.bin 4882e949ae9c6de8cb2735942f0b1624
|
||||||
/dlc/IRAO/CGEAR2/12 - Charizard!.bin
|
/dlc/IRAO/CGEAR2/11 - Venusaur!.bin 21becbbbba091cb1060ed19c4436d98f
|
||||||
/dlc/IRAO/CGEAR2/13 - Blastoise!.bin
|
/dlc/IRAO/CGEAR2/12 - Charizard!.bin 75705d719bae83ac130e03598d85382d
|
||||||
/dlc/IRAO/CGEAR2/14 - Red Genesect.bin
|
/dlc/IRAO/CGEAR2/13 - Blastoise!.bin 4f41e613d79826f48b8f8b9d1aa5747b
|
||||||
/dlc/IRAO/CGEAR2/15 - Black Kyurem.bin
|
/dlc/IRAO/CGEAR2/14 - Red Genesect.bin 0d0951b899bff3d9e243a9dc61aff973
|
||||||
/dlc/IRAO/CGEAR2/16 - White Kyurem.bin
|
/dlc/IRAO/CGEAR2/15 - Black Kyurem.bin ff4356dac514c364557bd685c932f43e
|
||||||
/dlc/IRAO/MUSICAL/01 - Charming Munna.bin
|
/dlc/IRAO/CGEAR2/16 - White Kyurem.bin e51c69805f87a954a7fd563b920cc036
|
||||||
/dlc/IRAO/MUSICAL/02 - MELOETTAAA!!!.bin
|
/dlc/IRAO/CGEAR2/17 - PCN Cup 2024 Summer.bin e925ddca5d9a52c49a3463916f6fcc0a
|
||||||
/dlc/IRAO/ZUKAN/01 - Default (Pink).bin
|
/dlc/IRAO/MUSICAL/01 - Charming Munna.bin dd180c539b51305244c10380266ec2ac
|
||||||
/dlc/IRAO/ZUKAN/02 - Default (Red).bin
|
/dlc/IRAO/MUSICAL/02 - MELOETTAAA!!!.bin f1a18eaf27c60b43020e9d5c0195eaed
|
||||||
/dlc/IRAO/ZUKAN/03 - Unova Trio (Pink).bin
|
/dlc/IRAO/ZUKAN/01 - Default (Pink).bin 7150719a18ffacef993c58b112cfaf52
|
||||||
/dlc/IRAO/ZUKAN/04 - Unova Trio (Red).bin
|
/dlc/IRAO/ZUKAN/02 - Default (Red).bin bc396d6403d9d296c1b4700d31b6eb42
|
||||||
/dlc/IRAO/ZUKAN/05 - Kanto Trio (Pink).bin
|
/dlc/IRAO/ZUKAN/03 - Unova Trio (Pink).bin 2e80bbc0e824c9c17fb0b5480d6bcb7c
|
||||||
/dlc/IRAO/ZUKAN/06 - Kanto Trio (Red).bin
|
/dlc/IRAO/ZUKAN/04 - Unova Trio (Red).bin b3e3eadbad327a8c4b3889768f79fd8e
|
||||||
/dlc/IRAO/ZUKAN/07 - Hugh.bin
|
/dlc/IRAO/ZUKAN/05 - Kanto Trio (Pink).bin abbe82e2ff4c66d0557b9989da3f5c29
|
||||||
/dlc/IRAO/ZUKAN/08 - Bianca.bin
|
/dlc/IRAO/ZUKAN/06 - Kanto Trio (Red).bin 7ba1f19f564e271d2e92810e174cd75e
|
||||||
/dlc/IRAO/ZUKAN/09 - Cheren.bin
|
/dlc/IRAO/ZUKAN/07 - Hugh.bin 2f74e77b34edc1758226778e9f5e91fd
|
||||||
|
/dlc/IRAO/ZUKAN/08 - Bianca.bin f5c57f66af3e096c65850ee47882be90
|
||||||
|
/dlc/IRAO/ZUKAN/09 - Cheren.bin 2dad78cb515505a5de8022bbd8ecd2ca
|
||||||
|
/dlc/IRAO/MYSTERY/PCN Cup 2024 Summer.bin fed0ff82be6903777b2f107ce00e8e4c
|
||||||
|
|||||||
BIN
src/main/resources/dlc/IRAO/CGEAR/33 - PCN Cup 2024 Summer.bin
Normal file
BIN
src/main/resources/dlc/IRAO/CGEAR/33 - PCN Cup 2024 Summer.bin
Normal file
Binary file not shown.
BIN
src/main/resources/dlc/IRAO/CGEAR2/17 - PCN Cup 2024 Summer.bin
Normal file
BIN
src/main/resources/dlc/IRAO/CGEAR2/17 - PCN Cup 2024 Summer.bin
Normal file
Binary file not shown.
BIN
src/main/resources/dlc/IRAO/MYSTERY/PCN Cup 2024 Summer.bin
Normal file
BIN
src/main/resources/dlc/IRAO/MYSTERY/PCN Cup 2024 Summer.bin
Normal file
Binary file not shown.
BIN
src/main/resources/icon-16x.png
Normal file
BIN
src/main/resources/icon-16x.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 720 B |
BIN
src/main/resources/icon-32x.png
Normal file
BIN
src/main/resources/icon-32x.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.4 KiB |
BIN
src/main/resources/icon-64x.png
Normal file
BIN
src/main/resources/icon-64x.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.0 KiB |
@@ -1,25 +1,39 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<!DOCTYPE xml>
|
<!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>
|
<Appenders>
|
||||||
<Console name="Console" target="SYSTEM_OUT">
|
<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>
|
</Console>
|
||||||
<RollingFile name="LogFile" fileName="logs/latest.log" filePattern="logs/%d{yyyy-MM-dd}-%i.log.gz">
|
<RollingFile name="LogFile" fileName="${LOGS_DIR}/latest.log" filePattern="${LOGS_DIR}/%d{yyyy-MM-dd}-%i.log.gz">
|
||||||
<PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss.SSS} %5level [%16t] %-48c{1.2*} : %msg%n"/>
|
<PatternLayout pattern="${PATTERN_DETAILED}"/>
|
||||||
|
<ThresholdFilter level="INFO"/>
|
||||||
<Policies>
|
<Policies>
|
||||||
<OnStartupTriggeringPolicy/>
|
<OnStartupTriggeringPolicy/>
|
||||||
</Policies>
|
</Policies>
|
||||||
</RollingFile>
|
</RollingFile>
|
||||||
<ConsumerAppender name="GuiOutput">
|
<ConsumerAppender name="GuiOutput">
|
||||||
<PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss.SSS} %5level : %msg%n"/>
|
<PatternLayout pattern="${PATTERN_MINIMAL}"/>
|
||||||
|
<ThresholdFilter level="INFO"/>
|
||||||
</ConsumerAppender>
|
</ConsumerAppender>
|
||||||
|
<File name="DebugLogFile" fileName="${LOGS_DIR}/debug.log" append="false">
|
||||||
|
<PatternLayout pattern="${PATTERN_DETAILED}"/>
|
||||||
|
<ThresholdFilter level="DEBUG"/>
|
||||||
|
</File>
|
||||||
</Appenders>
|
</Appenders>
|
||||||
<Loggers>
|
<Loggers>
|
||||||
<Root level="info">
|
<Logger name="entralinked" additivity="false">
|
||||||
<AppenderRef ref="Console"/>
|
|
||||||
<AppenderRef ref="LogFile"/>
|
<AppenderRef ref="LogFile"/>
|
||||||
<AppenderRef ref="GuiOutput"/>
|
<AppenderRef ref="GuiOutput"/>
|
||||||
</Root>
|
<AppenderRef ref="DebugLogFile"/>
|
||||||
|
<AppenderRef ref="Console"/>
|
||||||
|
</Logger>
|
||||||
|
<Root level="all"/>
|
||||||
</Loggers>
|
</Loggers>
|
||||||
</Configuration>
|
</Configuration>
|
||||||
|
|||||||
BIN
src/main/resources/zukan.bin
Normal file
BIN
src/main/resources/zukan.bin
Normal file
Binary file not shown.
@@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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¬Closed";
|
||||||
|
JsonParseException exception = assertThrows(JsonParseException.class, () -> mapperNoBase64.readValue(inputString, Map.class));
|
||||||
|
assertEquals("Unexpected end-of-input in FIELD_NAME", exception.getOriginalMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
54
src/test/java/entralinked/utility/ColorUtilityTest.java
Normal file
54
src/test/java/entralinked/utility/ColorUtilityTest.java
Normal 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user