mirror of
https://github.com/kuroppoi/entralinked.git
synced 2026-09-08 08:45:14 -05:00
Compare commits
63 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 | ||
|
|
0f4160459e | ||
|
|
3dea11ff50 | ||
|
|
b38b26f9ce | ||
|
|
a95e7e2f01 | ||
|
|
53f05acab9 | ||
|
|
49a15a0156 | ||
|
|
329e1de30a | ||
|
|
0e12cb54d7 |
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(
|
// Post-startup
|
||||||
"ERROR: One or more servers failed to start! Please check the logs for info."));
|
if(started) {
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
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,11 +6,11 @@ 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.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
|
||||||
import java.util.function.Predicate;
|
import java.util.function.Predicate;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
@@ -18,41 +18,44 @@ 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();
|
||||||
private final Map<String, Dlc> dlcMap = new ConcurrentHashMap<>();
|
private final List<Dlc> dlcList = new ArrayList<>();
|
||||||
private final File dataDirectory = new File("dlc");
|
private final File dataDirectory = new File("dlc");
|
||||||
|
|
||||||
public DlcList() {
|
public DlcList() {
|
||||||
logger.info("Loading DLC ...");
|
logger.info("Loading DLC ...");
|
||||||
|
|
||||||
// Extract defaults if external DLC directory is not present
|
try(BufferedReader reader = new BufferedReader(new InputStreamReader(getClass().getResourceAsStream("/dlc.paths")))) {
|
||||||
if(!dataDirectory.exists()) {
|
reader.lines().forEach(line -> {
|
||||||
logger.info("Extracting default DLC files ...");
|
String[] segments = line.split("\t");
|
||||||
BufferedReader reader = new BufferedReader(new InputStreamReader(getClass().getResourceAsStream("/dlc.paths")));
|
|
||||||
String line = null;
|
if(segments.length != 2) {
|
||||||
|
return;
|
||||||
try {
|
|
||||||
while((line = reader.readLine()) != null) {
|
|
||||||
InputStream resource = getClass().getResourceAsStream(line);
|
|
||||||
File outputFile = new File("./%s".formatted(line));
|
|
||||||
|
|
||||||
// Create parent directories
|
|
||||||
if(outputFile.getParentFile() != null) {
|
|
||||||
outputFile.getParentFile().mkdirs();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Copy resource to destination
|
|
||||||
if(resource != null) {
|
|
||||||
Files.copy(resource, outputFile.toPath());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (IOException e) {
|
|
||||||
logger.error("Could not extract default DLC files", e);
|
String path = segments[0];
|
||||||
return;
|
String hash = segments[1];
|
||||||
}
|
File outputFile = new File("./%s".formatted(path));
|
||||||
|
|
||||||
|
if(outputFile.getParentFile() != null) {
|
||||||
|
outputFile.getParentFile().mkdirs();
|
||||||
|
}
|
||||||
|
|
||||||
|
try(InputStream inputStream = getClass().getResourceAsStream(path)){
|
||||||
|
if(!outputFile.exists() || !hash.equals(MD5.digest(outputFile))) {
|
||||||
|
Files.copy(inputStream, outputFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
|
||||||
|
}
|
||||||
|
} catch(IOException e) {
|
||||||
|
logger.error("Couldn't process resource '{}'", path, e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch(IOException e) {
|
||||||
|
logger.error("Couldn't extract DLC data", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Just to be sure...
|
// Just to be sure...
|
||||||
@@ -72,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,29 +83,32 @@ 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);
|
||||||
|
|
||||||
// Index DLC object if loading succeeded
|
|
||||||
if(dlc != null) {
|
if(dlc != null) {
|
||||||
dlcMap.put(dlc.name(), dlc);
|
dlcList.add(dlc);
|
||||||
index++;
|
index++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info("Loaded {} DLC file(s)", dlcMap.size());
|
logger.info("Loaded {} DLC file(s)", dlcList.size());
|
||||||
}
|
}
|
||||||
|
|
||||||
private Dlc loadDlcFile(String gameCode, String type, int index, File dlcFile) {
|
private Dlc loadDlcFile(String gameCode, String type, int index, File dlcFile) {
|
||||||
String name = dlcFile.getName();
|
String name = dlcFile.getName();
|
||||||
|
|
||||||
if(dlcMap.containsKey(name)) {
|
|
||||||
logger.warn("Duplicate DLC name {}", name);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(dlcFile.isDirectory()) {
|
if(dlcFile.isDirectory()) {
|
||||||
logger.warn("Directory '{}' in {} DLC folder", name, gameCode);
|
logger.warn("Directory '{}' in {} DLC folder", name, gameCode);
|
||||||
return null;
|
return null;
|
||||||
@@ -123,6 +129,7 @@ public class DlcList {
|
|||||||
int checksumInFile = (bytes[bytes.length - 2] & 0xFF) | ((bytes[bytes.length - 1] & 0xFF) << 8);
|
int checksumInFile = (bytes[bytes.length - 2] & 0xFF) | ((bytes[bytes.length - 1] & 0xFF) << 8);
|
||||||
|
|
||||||
if(checksum != checksumInFile) {
|
if(checksum != checksumInFile) {
|
||||||
|
logger.warn("Checksum mismatch in DLC '{}'", name);
|
||||||
projectedSize += 2;
|
projectedSize += 2;
|
||||||
checksum = Crc16.calc(bytes, 0, bytes.length);
|
checksum = Crc16.calc(bytes, 0, bytes.length);
|
||||||
checksumEmbedded = false;
|
checksumEmbedded = false;
|
||||||
@@ -167,19 +174,18 @@ public class DlcList {
|
|||||||
return builder.toString();
|
return builder.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
public Dlc getDlc(String name) {
|
public Dlc getDlc(String gameCode, String type, String name) {
|
||||||
return dlcMap.get(name);
|
List<Dlc> dlcList = getDlcList(gameCode, type).stream()
|
||||||
|
.filter(dlc -> dlc.name().equals(name)).collect(Collectors.toList());
|
||||||
|
return dlcList.isEmpty() ? null : dlcList.get(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getDlcIndex(String name) {
|
public int getDlcIndex(String gameCode, String type, String name) {
|
||||||
return dlcExists(name) ? getDlc(name).index() : 0;
|
Dlc dlc = getDlc(gameCode, type, name);
|
||||||
}
|
return dlc == null ? 0 : dlc.index();
|
||||||
|
|
||||||
public boolean dlcExists(String name) {
|
|
||||||
return name != null && dlcMap.containsKey(name);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Collection<Dlc> getDlc() {
|
public Collection<Dlc> getDlc() {
|
||||||
return Collections.unmodifiableCollection(dlcMap.values());
|
return Collections.unmodifiableCollection(dlcList);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
@@ -32,7 +33,9 @@ public class HttpServer {
|
|||||||
private final Javalin javalin;
|
private final Javalin javalin;
|
||||||
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,11 +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 Set<Integer> availableBlackAndWhiteSpecies = Set.of(
|
private final Map<Object, BufferedImage> skinPreviewCache = new HashMap<>();
|
||||||
505, 507, 510, 511, 513, 515, 519, 523, 525, 527, 529, 531, 533, 535, 538, 539, 542, 545, 546, 548,
|
|
||||||
550, 553, 556, 558, 559, 561, 564, 569, 572, 575, 578, 580, 583, 587, 588, 594, 596, 600, 605, 607,
|
|
||||||
610, 613, 616, 618, 619, 621, 622, 624, 626, 628, 630, 631, 632);
|
|
||||||
private final Map<String, BufferedImage> skinPreviewCache = new HashMap<>();
|
|
||||||
private final DlcList dlcList;
|
private final DlcList dlcList;
|
||||||
private final PlayerManager playerManager;
|
private final PlayerManager playerManager;
|
||||||
|
|
||||||
@@ -61,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.name(), 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());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,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);
|
||||||
@@ -110,16 +149,48 @@ public class DashboardHandler implements HttpHandler {
|
|||||||
* GET request handler for {@code /dashboard/previewskin}
|
* GET request handler for {@code /dashboard/previewskin}
|
||||||
*/
|
*/
|
||||||
private void handlePreviewSkin(Context ctx) throws IOException {
|
private void handlePreviewSkin(Context ctx) throws IOException {
|
||||||
// Make sure that the name is present and exists
|
String type = ctx.queryParam("type");
|
||||||
String name = ctx.queryParam("name");
|
String name = ctx.queryParam("name");
|
||||||
|
|
||||||
if(name == null || !skinPreviewCache.containsKey(name)) {
|
// Make sure query parameters are present
|
||||||
ctx.status(404);
|
if(type == null || name == null) {
|
||||||
|
ctx.status(HttpStatus.NOT_FOUND);
|
||||||
|
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);
|
||||||
|
|
||||||
|
// Check if DLC exists
|
||||||
|
if(dlc == null) {
|
||||||
|
ctx.status(HttpStatus.NOT_FOUND);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write cached image data
|
// Write cached image data
|
||||||
ImageIO.write(skinPreviewCache.get(name), "png", ctx.outputStream());
|
ImageIO.write(skinPreviewCache.get(dlc), "png", ctx.outputStream());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -183,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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -248,14 +436,8 @@ public class DashboardHandler implements HttpHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for(DreamEncounter encounter : request.encounters()) {
|
for(DreamEncounter encounter : request.encounters()) {
|
||||||
if(encounter.species() < 1) {
|
if(encounter.species() < 1 || encounter.species() > 649) {
|
||||||
return "Species is out of range.";
|
return "Species is out of range.";
|
||||||
} else if(encounter.species() > 493) {
|
|
||||||
if(!player.getGameVersion().isVersion2()) {
|
|
||||||
return "Sorry, Generation V Pokémon are exclusive to Black Version 2 and White Version 2.";
|
|
||||||
} else if(!availableBlackAndWhiteSpecies.contains(encounter.species())) {
|
|
||||||
return "You have selected one or more Pokémon species that cannot be downloaded.";
|
|
||||||
}
|
|
||||||
} else if(encounter.move() < 0 || encounter.move() > 559) {
|
} else if(encounter.move() < 0 || encounter.move() > 559) {
|
||||||
return "Move ID is out of range.";
|
return "Move ID is out of range.";
|
||||||
} else if(encounter.gender() == null) {
|
} else if(encounter.gender() == null) {
|
||||||
@@ -275,11 +457,7 @@ public class DashboardHandler implements HttpHandler {
|
|||||||
for(DreamItem item : request.items()) {
|
for(DreamItem item : request.items()) {
|
||||||
if(item.id() < 0 || item.id() > 638) {
|
if(item.id() < 0 || item.id() > 638) {
|
||||||
return "Item ID is out of range";
|
return "Item ID is out of range";
|
||||||
} else if(item.id() > 626 && !player.getGameVersion().isVersion2()) {
|
} else if(item.quantity() < 0 || item.quantity() > 20) {
|
||||||
return "You have selected one or more items that are exclusive to Black Version 2 and White Version 2.";
|
|
||||||
}
|
|
||||||
|
|
||||||
if(item.quantity() < 0 || item.quantity() > 20) {
|
|
||||||
return "Item quantity is out of range.";
|
return "Item quantity is out of range.";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,113 @@ 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 {
|
||||||
String gameCode = switch(request.dlcGameCode()) {
|
User user = ctx.attribute("user");
|
||||||
case "IRAJ" -> "IRAO";
|
String gameCode = getDlcGameCode(request.dlcGameCode());
|
||||||
default -> request.dlcGameCode();
|
String type = getDlcType(request.attr1());
|
||||||
};
|
String attr2 = request.attr2();
|
||||||
|
List<File> files = null;
|
||||||
|
|
||||||
// Map to generic type, I doubt there is a real difference between the language codes anyway.
|
if(user.hasDlcOverride(type)) {
|
||||||
String type = switch(request.dlcType()) {
|
files = Arrays.asList(user.getDlcOverride(type));
|
||||||
case "CGEAR_E", "CGEAR_F", "CGEAR_I", "CGEAR_G", "CGEAR_S", "CGEAR_J", "CGEAR_K" -> "CGEAR";
|
} else {
|
||||||
case "CGEAR2_E", "CGEAR2_F", "CGEAR2_I", "CGEAR2_G", "CGEAR2_S", "CGEAR2_J", "CGEAR2_K" -> "CGEAR2";
|
// Get list of files in DLC directory
|
||||||
case "ZUKAN_E", "ZUKAN_F", "ZUKAN_I", "ZUKAN_G", "ZUKAN_S", "ZUKAN_J", "ZUKAN_K" -> "ZUKAN";
|
File directory = getDlcDirectory(gameCode, type);
|
||||||
case "MUSICAL_E", "MUSICAL_F", "MUSICAL_I", "MUSICAL_G", "MUSICAL_S", "MUSICAL_J", "MUSICAL_K" -> "MUSICAL";
|
files = directory.isDirectory() ? Arrays.asList(directory.listFiles()) : new ArrayList<>();
|
||||||
default -> request.dlcType();
|
}
|
||||||
};
|
|
||||||
|
|
||||||
// TODO NOTE: I assume that in a conventional implementation, certain DLC attributes may be omitted from the request.
|
if(attr2 != null) {
|
||||||
ctx.result(dlcList.getDlcListString(dlcList.getDlcList(gameCode, type, request.dlcIndex())));
|
// PGL content attr2 hack
|
||||||
|
files = Arrays.asList(files.get(Integer.parseInt(attr2) - 1));
|
||||||
|
} else {
|
||||||
|
// Mystery Gift randomness
|
||||||
|
Collections.shuffle(files);
|
||||||
|
}
|
||||||
|
|
||||||
|
StringBuilder builder = new StringBuilder();
|
||||||
|
int count = Math.min(files.size(), request.num());
|
||||||
|
|
||||||
|
// Create DLC list string
|
||||||
|
for(int i = 0; i < count; i++) {
|
||||||
|
File file = files.get(i);
|
||||||
|
|
||||||
|
if(type == null) {
|
||||||
|
// Generation 4 Mystery Gift
|
||||||
|
builder.append("%s\t\t\t\t\t%s\r\n".formatted(file.getName(), 936));
|
||||||
|
} else if(type.equals("MYSTERY")) {
|
||||||
|
// Generation 5 Mystery Gift
|
||||||
|
String gameFlag = GameVersion.lookup(request.gameCode()).isVersion2() ? "F00000" : "300000";
|
||||||
|
builder.append("%s\t\t%s\t%s\t\t%s\r\n".formatted(file.getName(), type, gameFlag, 720));
|
||||||
|
} else {
|
||||||
|
// PGL content
|
||||||
|
builder.append("%s\t\t%s\t%s\t\t%s\r\n".formatted(file.getName(), type, i + 1, file.length()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send result
|
||||||
|
ctx.result(builder.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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 {
|
||||||
// Check if the requested DLC exists
|
User user = ctx.attribute("user");
|
||||||
Dlc dlc = dlcList.getDlc(request.dlcName());
|
String gameCode = getDlcGameCode(request.dlcGameCode());
|
||||||
|
String type = getDlcType(request.attr1());
|
||||||
|
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()));
|
||||||
|
|
||||||
if(dlc == null) {
|
// Check if the requested DLC exists
|
||||||
|
if(!file.exists()) {
|
||||||
ctx.status(HttpStatus.NOT_FOUND);
|
ctx.status(HttpStatus.NOT_FOUND);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write DLC data
|
byte[] bytes = Files.readAllBytes(file.toPath());
|
||||||
try(FileInputStream inputStream = new FileInputStream(dlc.path())) {
|
|
||||||
LEOutputStream outputStream = new LEOutputStream(ctx.outputStream());
|
if(type == null) {
|
||||||
inputStream.transferTo(outputStream);
|
// Generation 4 Mystery Gift
|
||||||
|
bytes = MysteryGiftUtility.createUniversalGiftData4(bytes, file.getName());
|
||||||
// If checksum is not part of the file, manually append it
|
} else if(type.equals("MYSTERY")) {
|
||||||
if(!dlc.checksumEmbedded()) {
|
// Generation 5 Mystery Gift
|
||||||
outputStream.writeShort(dlc.checksum());
|
bytes = MysteryGiftUtility.createUniversalGiftData5(bytes);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Send result
|
||||||
|
ctx.result(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST handler for {@code /download action=count}
|
||||||
|
*/
|
||||||
|
private void handleRetrieveDlcCount(DlsRequest request, Context ctx) throws IOException {
|
||||||
|
ctx.result("1"); // TODO
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return The game serial that should be used for downloading DLC based on the provided input.
|
||||||
|
*/
|
||||||
|
private String getDlcGameCode(String gameCode) {
|
||||||
|
return switch(gameCode.substring(0, 3)) {
|
||||||
|
case "IRA" -> "IRAO"; // BW & B2W2
|
||||||
|
case "ADA", "CPU", "IPG" -> "ADAE"; // DPPt & HGSS
|
||||||
|
default -> gameCode;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return The DLC type without the region identifier, or the input if it is an unknown type.
|
||||||
|
*/
|
||||||
|
private String getDlcType(String attr1) {
|
||||||
|
if(attr1 == null || !attr1.contains("_")) {
|
||||||
|
return attr1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return attr1.substring(0, attr1.lastIndexOf('_'));
|
||||||
|
}
|
||||||
|
|
||||||
|
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());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -128,7 +134,7 @@ public class PglHandler implements HttpHandler {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Handle the request
|
// Handle the request
|
||||||
handler.process(request, ctx);
|
handler.process(request, ctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -142,7 +148,7 @@ public class PglHandler implements HttpHandler {
|
|||||||
writeStatusCode(outputStream, 1); // Unauthorized
|
writeStatusCode(outputStream, 1); // Unauthorized
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create bitlist
|
// Create bitlist
|
||||||
byte[] bitlist = new byte[128]; // TODO pool? maybe just cache
|
byte[] bitlist = new byte[128]; // TODO pool? maybe just cache
|
||||||
|
|
||||||
@@ -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.
|
||||||
@@ -228,9 +261,9 @@ public class PglHandler implements HttpHandler {
|
|||||||
// Write misc stuff and DLC information
|
// Write misc stuff and DLC information
|
||||||
outputStream.writeShort(player.getLevelsGained());
|
outputStream.writeShort(player.getLevelsGained());
|
||||||
outputStream.write(0); // Unknown
|
outputStream.write(0); // Unknown
|
||||||
outputStream.write(dlcList.getDlcIndex(player.getMusical()));
|
outputStream.write(dlcList.getDlcIndex("IRAO", "MUSICAL", player.getMusical()));
|
||||||
outputStream.write(dlcList.getDlcIndex(player.getCGearSkin()));
|
outputStream.write(cgearSkinIndex);
|
||||||
outputStream.write(dlcList.getDlcIndex(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);
|
||||||
@@ -443,13 +489,9 @@ 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,7 +18,14 @@ public class NetworkUtility {
|
|||||||
public static InetAddress getLocalHost() {
|
public static InetAddress getLocalHost() {
|
||||||
try(Socket socket = new Socket()){
|
try(Socket socket = new Socket()){
|
||||||
socket.connect(new InetSocketAddress("github.com", 80));
|
socket.connect(new InetSocketAddress("github.com", 80));
|
||||||
return socket.getLocalAddress();
|
InetAddress address = socket.getLocalAddress();
|
||||||
|
|
||||||
|
// Fall back to the network interface method if this is not a private IP address
|
||||||
|
if(!address.isSiteLocalAddress()) {
|
||||||
|
return getLocalHostFromNetworkInterfaces();
|
||||||
|
}
|
||||||
|
|
||||||
|
return address;
|
||||||
} catch(IOException e) {
|
} catch(IOException e) {
|
||||||
logger.error("Couldn't get local host using socket - falling back to the network interface method", e);
|
logger.error("Couldn't get local host using socket - falling back to the network interface method", e);
|
||||||
return getLocalHostFromNetworkInterfaces();
|
return getLocalHostFromNetworkInterfaces();
|
||||||
@@ -42,8 +49,8 @@ public class NetworkUtility {
|
|||||||
while(addresses.hasMoreElements()) {
|
while(addresses.hasMoreElements()) {
|
||||||
InetAddress address = addresses.nextElement();
|
InetAddress address = addresses.nextElement();
|
||||||
|
|
||||||
// Return if IPv4
|
// Return this address if it is a local IPv4 address
|
||||||
if(address instanceof Inet4Address) {
|
if(address.isSiteLocalAddress() && address instanceof Inet4Address) {
|
||||||
return address;
|
return address;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -52,6 +59,7 @@ public class NetworkUtility {
|
|||||||
logger.error("Could not determine local host - falling back to loopback address", e);
|
logger.error("Could not determine local host - falling back to loopback address", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logger.warn("No local host candidate could be found - loopback address will be used");
|
||||||
return InetAddress.getLoopbackAddress();
|
return InetAddress.getLoopbackAddress();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<link rel="stylesheet" href="styles/list.css">
|
|
||||||
</head>
|
|
||||||
<body onload="fillTableWithItems()">
|
|
||||||
<div id="main-container" class="root-container">
|
|
||||||
<label class="big-label">List of items that can be downloaded</label>
|
|
||||||
<label>NOTE: Item IDs greater than 626 are exclusive to Black Version 2 and White Version 2</label>
|
|
||||||
<table id="item-table">
|
|
||||||
<!-- Filled by list.js -->
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
<script src="scripts/list.js"></script>
|
|
||||||
</html>
|
|
||||||
@@ -6,11 +6,15 @@
|
|||||||
<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/login.js"></script>
|
<script src="scripts/login.js"></script>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<link rel="stylesheet" href="styles/profile.css">
|
<link rel="stylesheet" href="styles/profile.css">
|
||||||
</head>
|
</head>
|
||||||
<body onload="fetchProfileData()">
|
<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>
|
||||||
@@ -124,21 +126,28 @@
|
|||||||
<!-- Misc Configurations -->
|
<!-- Misc Configurations -->
|
||||||
<div class="grid-container">
|
<div class="grid-container">
|
||||||
<div>
|
<div>
|
||||||
<label>C-Gear Skin | </label><a href="#" onclick="return previewSkin('cgear-skin')">Preview</a>
|
<label>C-Gear Skin | </label><a href="#" onclick="return previewSkin('cgear-skin', 'CGEAR')">Preview</a>
|
||||||
<select id="cgear-skin">
|
<select id="cgear-skin">
|
||||||
<option value="none">Do not change</option>
|
<option value="none">Do not change</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')">Preview</a>
|
<label>Pokédex Skin | </label><a href="#" onclick="return previewSkin('dex-skin', 'ZUKAN')">Preview</a>
|
||||||
<select id="dex-skin">
|
<select id="dex-skin">
|
||||||
<option value="none">Do not change</option>
|
<option value="none">Do not change</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>
|
||||||
<select id="musical">
|
<select id="musical">
|
||||||
<option value="none">Do not change</option>
|
<option value="none">Do not change</option>
|
||||||
|
<option disabled>──────────────────────</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@@ -152,106 +161,151 @@
|
|||||||
<button id="logout" class="big-button" onclick="postLogout()">Log Out</button>
|
<button id="logout" class="big-button" onclick="postLogout()">Log Out</button>
|
||||||
</div>
|
</div>
|
||||||
</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 ID | </label><a href="/dashboard/species.html" target="_blank">View list</a>
|
<form id="encounter-form">
|
||||||
<input id="encounter-form-species" name="species" type="number" value="1" min="1" max="493"/>
|
<label for="encounter-form-species">Species</label>
|
||||||
<label for="encounter-form-move">Move ID</label>
|
<select id="encounter-form-species" name="species" value="1">
|
||||||
<input id="encounter-form-move" name="move" type="number" value="0" min="0" max="559"/>
|
<!-- Filled by profile.js -->
|
||||||
<label for="encounter-form-form">Forme Index</label>
|
</select>
|
||||||
<input id="encounter-form-form" name="form" type="number" value="0" min="0" max="31"/>
|
<label for="encounter-form-move">Special Move</label>
|
||||||
<label for="encounter-form-gender">Gender</label>
|
<select id="encounter-form-move" name="move" value="0">
|
||||||
<select id="encounter-form-gender" name="gender" value="GENDERLESS">
|
<option value="0">None</option>
|
||||||
<option value="MALE">Male</option>
|
<!-- Filled by profile.js -->
|
||||||
<option value="FEMALE">Female</option>
|
</select>
|
||||||
<option value="GENDERLESS">Random</option>
|
<label for="encounter-form-form">Form</label>
|
||||||
</select>
|
<select id="encounter-form-form" name="form" value="0">
|
||||||
<label for="encounter-form-animation">Animation</label>
|
<option value="0">N/A</option>
|
||||||
<select id="encounter-form-animation" name="animation">
|
<!-- Filled by profile.js -->
|
||||||
<option value="LOOK_AROUND">Look Around</option>
|
</select>
|
||||||
<option value="WALK_AROUND">Walk Around</option>
|
<label for="encounter-form-gender">Gender</label>
|
||||||
<option value="WALK_LOOK_AROUND">Walk and Look Around</option>
|
<select id="encounter-form-gender" name="gender" value="GENDERLESS">
|
||||||
<option value="WALK_VERTICALLY">Walk Vertically</option>
|
<option value="MALE">Male</option>
|
||||||
<option value="WALK_HORIZONTALLY">Walk Horizontally</option>
|
<option value="FEMALE">Female</option>
|
||||||
<option value="WALK_HORIZONTALLY_LOOK_AROUND">Walk Horizontally and Look Around</option>
|
<option value="GENDERLESS">Random</option>
|
||||||
<option value="SPIN_RIGHT">Spin Right</option>
|
</select>
|
||||||
<option value="SPIN_LEFT">Spin Left</option>
|
<label for="encounter-form-animation">Animation</label>
|
||||||
</select>
|
<select id="encounter-form-animation" name="animation">
|
||||||
</form>
|
<option value="LOOK_AROUND">Look Around</option>
|
||||||
<button class="big-button" onclick="saveEncounter()">Confirm</button>
|
<option value="WALK_AROUND">Walk Around</option>
|
||||||
<button class="big-button" onclick="removeEncounter()">Remove</button>
|
<option value="WALK_LOOK_AROUND">Walk and Look Around</option>
|
||||||
|
<option value="WALK_VERTICALLY">Walk Vertically</option>
|
||||||
|
<option value="WALK_HORIZONTALLY">Walk Horizontally</option>
|
||||||
|
<option value="WALK_LOOK_HORIZONTALLY">Walk Horizontally and Look Around</option>
|
||||||
|
<option value="SPIN_RIGHT">Spin Right</option>
|
||||||
|
<option value="SPIN_LEFT">Spin Left</option>
|
||||||
|
</select>
|
||||||
|
</form>
|
||||||
|
<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 ID | </label><a href="/dashboard/items.html" target="_blank">View list</a>
|
<form id="item-form">
|
||||||
<input id="item-form-id" name="id" type="number" value="1" min="1" max="626"/>
|
<label for="item-form-id">Item</label>
|
||||||
<label for="item-form-quantity">Quantity</label>
|
<select id="item-form-id" name="id" value="1">
|
||||||
<input id="item-form-quantity" name="quantity" type="number" value="1" min="1" max="20"/>
|
<!-- Filled by profile.js -->
|
||||||
</form>
|
</select>
|
||||||
<button class="big-button" onclick="saveItem()">Confirm</button>
|
<label for="item-form-quantity">Quantity</label>
|
||||||
<button class="big-button" onclick="removeItem()">Remove</button>
|
<input id="item-form-quantity" name="quantity" type="number" value="1" min="1" max="20"/>
|
||||||
|
</form>
|
||||||
|
<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-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-region">Country</label>
|
||||||
<label for="visitor-form-dreamer">Tucked-in Pokémon Species</label>
|
<select id="visitor-form-region" name="region" value="1">
|
||||||
<input id="visitor-form-dreamer" name="personality" type="number" value="1" min="1" max="649"/>
|
<!-- Filled by profile.js -->
|
||||||
</form>
|
</select>
|
||||||
<button class="big-button" onclick="saveVisitor()">Confirm</button>
|
<label for="visitor-form-subregion">State/Province</label>
|
||||||
<button class="big-button" onclick="removeVisitor()">Remove</button>
|
<select id="visitor-form-subregion" name="subregion" value="0">
|
||||||
|
<option value="0">N/A</option>
|
||||||
|
<!-- Filled by profile.js -->
|
||||||
|
</select>
|
||||||
|
<label for="visitor-form-personality">Personality (Affects phrases used)</label>
|
||||||
|
<input id="visitor-form-personality" name="personality" type="number" value="0" min="0" max="7"/>
|
||||||
|
<label for="visitor-form-dreamer">Tucked-in Pokémon Species</label>
|
||||||
|
<select id="visitor-form-dreamer" name="dreamerSpecies" value="1">
|
||||||
|
<!-- Filled by profile.js -->
|
||||||
|
</select>
|
||||||
|
</form>
|
||||||
|
<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>
|
||||||
|
<script src="scripts/utility.js"></script>
|
||||||
|
<script src="scripts/pokedata.js"></script>
|
||||||
<script src="scripts/profile.js"></script>
|
<script src="scripts/profile.js"></script>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1,61 +0,0 @@
|
|||||||
const AVAILABLE_GENERATION_V_POKEMON = new Array(
|
|
||||||
505, 507, 510, 511, 513, 515, 519, 523, 525, 527, 529, 531, 533, 535, 538, 539, 542, 545, 546, 548,
|
|
||||||
550, 553, 556, 558, 559, 561, 564, 569, 572, 575, 578, 580, 583, 587, 588, 594, 596, 600, 605, 607,
|
|
||||||
610, 613, 616, 618, 619, 621, 622, 624, 626, 628, 630, 631, 632);
|
|
||||||
|
|
||||||
const ITEMS_TO_EXCLUDE = new Array(113, 114, 115, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 426, 427);
|
|
||||||
|
|
||||||
function fillTable(tableId, rowSize, elementCount, htmlAppender) {
|
|
||||||
let tableHTML = "";
|
|
||||||
let index = 0;
|
|
||||||
|
|
||||||
for(let value = 1; value <= elementCount; value++) {
|
|
||||||
// Call the provided HTML appender to see what should be appended to the table
|
|
||||||
let toAppend = htmlAppender(value);
|
|
||||||
|
|
||||||
// If there is nothing to append (that is, the function has decided this element should be skipped)
|
|
||||||
// then continue to the next loop.
|
|
||||||
if(!toAppend) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Open a new table row if it should
|
|
||||||
if(index % rowSize == 0) {
|
|
||||||
tableHTML += "<tr>";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Append the HTML data
|
|
||||||
tableHTML += toAppend;
|
|
||||||
|
|
||||||
// Close the table row if it has reached the maximum number of elements
|
|
||||||
if(index % rowSize == rowSize) {
|
|
||||||
tableHTML += "<tr>";
|
|
||||||
}
|
|
||||||
|
|
||||||
index++;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set the inner HTML
|
|
||||||
document.getElementById(tableId).innerHTML = tableHTML;
|
|
||||||
}
|
|
||||||
|
|
||||||
function fillTableWithSpecies() {
|
|
||||||
fillTable("species-table", 10, 649, (species) => {
|
|
||||||
// Skip this element if species is from Generation V and cannot be downloaded
|
|
||||||
if(species > 493 && !AVAILABLE_GENERATION_V_POKEMON.includes(species)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return "<td style='width:96px;height:96px;'><image src='/sprites/pokemon/normal/" + species + ".png'/><br>#" + species + "</td>";
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function fillTableWithItems() {
|
|
||||||
fillTable("item-table", 20, 638, (item) => {
|
|
||||||
if(ITEMS_TO_EXCLUDE.includes(item)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return "<td style='width:48px;height:48px;'><image src='/sprites/items/" + item + ".png'/><br>#" + item + "</td>"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,22 +1,15 @@
|
|||||||
const ELEMENT_GSID_INPUT = document.getElementById("gsid");
|
const ELEMENT_GSID_INPUT = document.getElementById("gsid");
|
||||||
|
|
||||||
function postLogin() {
|
function postLogin() {
|
||||||
let loginData = {
|
fetchData("POST", "/dashboard/login", new URLSearchParams({
|
||||||
gsid: ELEMENT_GSID_INPUT.value
|
gsid: ELEMENT_GSID_INPUT.value
|
||||||
}
|
})).then((response) => {
|
||||||
|
if(response.error) {
|
||||||
fetch("/dashboard/login", {
|
window.alert(response.message);
|
||||||
method: "POST",
|
|
||||||
body: new URLSearchParams(loginData)
|
|
||||||
}).then((response) => {
|
|
||||||
return response.json();
|
|
||||||
}).then((response) => {
|
|
||||||
console.log(response);
|
|
||||||
|
|
||||||
if(response.error) {
|
|
||||||
alert(response.message);
|
|
||||||
} else {
|
} else {
|
||||||
window.location.href = "/dashboard/profile.html";
|
window.location.href = "/dashboard/profile.html";
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
2590
src/main/resources/dashboard/scripts/pokedata.js
vendored
Normal file
2590
src/main/resources/dashboard/scripts/pokedata.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,8 +1,9 @@
|
|||||||
|
// HTML document elements
|
||||||
const ELEMENT_GAME_SUMMARY = document.getElementById("game-summary");
|
const ELEMENT_GAME_SUMMARY = document.getElementById("game-summary");
|
||||||
|
|
||||||
// Dreamer elements
|
|
||||||
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,58 +11,35 @@ 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");
|
||||||
|
|
||||||
// Encounter form elements
|
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");
|
||||||
|
|
||||||
// Item form elements
|
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");
|
||||||
|
|
||||||
// Join Avenue Visitor form elements
|
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");
|
||||||
const ELEMENT_VISITOR_GAME = document.getElementById("visitor-form-game");
|
const ELEMENT_VISITOR_GAME = document.getElementById("visitor-form-game");
|
||||||
|
const ELEMENT_VISITOR_REGION = document.getElementById("visitor-form-region");
|
||||||
|
const ELEMENT_VISITOR_SUBREGION = document.getElementById("visitor-form-subregion");
|
||||||
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");
|
||||||
|
|
||||||
// Misc input elements
|
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");
|
||||||
const ELEMENT_LEVEL_GAIN_INPUT = document.getElementById("level-gain-input");
|
const ELEMENT_LEVEL_GAIN_INPUT = document.getElementById("level-gain-input");
|
||||||
|
|
||||||
// Create event listeners
|
|
||||||
ELEMENT_ENCOUNTER_SPECIES.addEventListener("change", clampValue);
|
|
||||||
ELEMENT_ENCOUNTER_MOVE.addEventListener("change", clampValue);
|
|
||||||
ELEMENT_ITEM_ID.addEventListener("change", clampValue);
|
|
||||||
ELEMENT_ITEM_QUANTITY.addEventListener("change", clampValue);
|
|
||||||
ELEMENT_LEVEL_GAIN_INPUT.addEventListener("change", clampValue);
|
|
||||||
ELEMENT_VISITOR_PERSONALITY.addEventListener("change", clampValue);
|
|
||||||
ELEMENT_VISITOR_DREAMER.addEventListener("change", clampValue);
|
|
||||||
|
|
||||||
function clampValue() {
|
|
||||||
let value = parseInt(this.value);
|
|
||||||
|
|
||||||
if(value < this.min) {
|
|
||||||
this.value = this.min;
|
|
||||||
} else if(value > this.max) {
|
|
||||||
console.log(value);
|
|
||||||
this.value = this.max;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Other constant stuff
|
|
||||||
const AVAILABLE_GENERATION_V_POKEMON = new Array(
|
|
||||||
505, 507, 510, 511, 513, 515, 519, 523, 525, 527, 529, 531, 533, 535, 538, 539, 542, 545, 546, 548,
|
|
||||||
550, 553, 556, 558, 559, 561, 564, 569, 572, 575, 578, 580, 583, 587, 588, 594, 596, 600, 605, 607,
|
|
||||||
610, 613, 616, 618, 619, 621, 622, 624, 626, 628, 630, 631, 632); // Defining this 3 times is a brilliant idea.
|
|
||||||
|
|
||||||
|
|
||||||
// Local variables
|
// Local variables
|
||||||
var encounterTableIndex = -1;
|
var encounterTableIndex = -1;
|
||||||
var itemTableIndex = -1;
|
var itemTableIndex = -1;
|
||||||
@@ -72,78 +50,217 @@ var profile = {
|
|||||||
visitors: []
|
visitors: []
|
||||||
};
|
};
|
||||||
|
|
||||||
|
(async function() {
|
||||||
|
// Create event listeners
|
||||||
|
clampOnChange(ELEMENT_ITEM_QUANTITY);
|
||||||
|
clampOnChange(ELEMENT_LEVEL_GAIN_INPUT);
|
||||||
|
clampOnChange(ELEMENT_VISITOR_PERSONALITY);
|
||||||
|
|
||||||
|
// Fetch profile data
|
||||||
|
await fetchData("GET", "/dashboard/profile").then((response) => {
|
||||||
|
// Update game summary
|
||||||
|
profile.gameVersion = response.gameVersion;
|
||||||
|
ELEMENT_GAME_SUMMARY.innerHTML = "Game Card in use: " + profile.gameVersion;
|
||||||
|
|
||||||
|
// Update dreamer summary
|
||||||
|
if(response.dreamerInfo) {
|
||||||
|
let dreamerInfo = response.dreamerInfo;
|
||||||
|
ELEMENT_DREAMER_SPRITE.innerHTML = "<image src='" + response.dreamerSprite + "'/>";
|
||||||
|
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_NAME.innerHTML = dreamerInfo.nickname;
|
||||||
|
ELEMENT_DREAMER_GENDER.innerHTML = stringToWord(dreamerInfo.gender);
|
||||||
|
ELEMENT_DREAMER_TRAINER.innerHTML = dreamerInfo.trainerName;
|
||||||
|
ELEMENT_DREAMER_TRAINER_ID.innerHTML = ("0000" + dreamerInfo.trainerId).slice(-5);
|
||||||
|
ELEMENT_DREAMER_LEVEL.innerHTML = dreamerInfo.level;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update encounter table
|
||||||
|
if(response.encounters){
|
||||||
|
profile.encounters = response.encounters;
|
||||||
|
updateEncounterTable(0, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update item table
|
||||||
|
if(response.items){
|
||||||
|
profile.items = response.items;
|
||||||
|
updateItemTable(0, 20);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update Join Avenue visitor table
|
||||||
|
if(response.avenueVisitors) {
|
||||||
|
profile.visitors = response.avenueVisitors;
|
||||||
|
updateVisitorTable(0, 12);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update selected DLC
|
||||||
|
profile.cgearSkin = response.cgearSkin ? response.cgearSkin : "none";
|
||||||
|
profile.dexSkin = response.dexSkin ? response.dexSkin : "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();
|
||||||
|
ELEMENT_LEVEL_GAIN_INPUT.value = response.levelsGained;
|
||||||
|
|
||||||
|
// Show Join Avenue visitor table if Black 2 or White 2
|
||||||
|
if(isVersion2()) {
|
||||||
|
document.getElementById("visitor-table-container").style.display = "block";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show div
|
||||||
|
document.getElementById("main-container").style.display = "flex";
|
||||||
|
});
|
||||||
|
|
||||||
|
// Sort data lists alphabetically
|
||||||
|
let sortedSpecies = [...SPECIES_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));
|
||||||
|
|
||||||
|
// Add species data
|
||||||
|
for(let i in sortedSpecies) {
|
||||||
|
let species = sortedSpecies[i];
|
||||||
|
ELEMENT_VISITOR_DREAMER.options[ELEMENT_VISITOR_DREAMER.options.length] = new Option(species.name, species.id);
|
||||||
|
|
||||||
|
if(species.downloadable && (isVersion2() || species.id <= 493)) {
|
||||||
|
ELEMENT_ENCOUNTER_SPECIES.options[ELEMENT_ENCOUNTER_SPECIES.options.length] = new Option(species.name, species.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add move data
|
||||||
|
for(let i in sortedMoves) {
|
||||||
|
let move = sortedMoves[i];
|
||||||
|
ELEMENT_ENCOUNTER_MOVE.options[ELEMENT_ENCOUNTER_MOVE.options.length] = new Option(move.name, move.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add item data
|
||||||
|
for(let i in sortedItems) {
|
||||||
|
let item = sortedItems[i];
|
||||||
|
|
||||||
|
if(isVersion2() || item.id <= 626) {
|
||||||
|
ELEMENT_ITEM_ID.options[ELEMENT_ITEM_ID.options.length] = new Option(item.name, item.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add region data (already sorted alphabetically)
|
||||||
|
for(let i in REGION_LIST) {
|
||||||
|
let region = REGION_LIST[i];
|
||||||
|
ELEMENT_VISITOR_REGION.options[ELEMENT_VISITOR_REGION.options.length] = new Option(region.name, region.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Event listener for changing the form & gender selector contents when species changes
|
||||||
|
ELEMENT_ENCOUNTER_SPECIES.addEventListener("change", function() {
|
||||||
|
ELEMENT_ENCOUNTER_FORM.value = updateEncounterFormOptions();
|
||||||
|
ELEMENT_ENCOUNTER_GENDER.value = updateEncounterGenderOptions();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Same thing, but for Join Avenue visitor region & subregion
|
||||||
|
ELEMENT_VISITOR_REGION.addEventListener("change", function() {
|
||||||
|
ELEMENT_VISITOR_SUBREGION.value = updateVisitorSubregionOptions();
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encounter configuration stuff
|
||||||
|
*/
|
||||||
|
|
||||||
|
function updateEncounterFormOptions() {
|
||||||
|
clearSelectOptions(ELEMENT_ENCOUNTER_FORM);
|
||||||
|
let species = SPECIES_MAP[ELEMENT_ENCOUNTER_SPECIES.value];
|
||||||
|
|
||||||
|
// Update special form options
|
||||||
|
if(species.forms) {
|
||||||
|
for(let i in species.forms) {
|
||||||
|
ELEMENT_ENCOUNTER_FORM.options[ELEMENT_ENCOUNTER_FORM.options.length] = new Option(species.forms[i], i);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
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));
|
||||||
|
|
||||||
// Load existing settings
|
|
||||||
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;
|
||||||
|
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() {
|
||||||
if(encounterTableIndex < 0) {
|
if(encounterTableIndex < 0) {
|
||||||
|
closeEncounterForm();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if this species can be downloaded
|
profile.encounters[encounterTableIndex] = {
|
||||||
let species = parseInt(ELEMENT_ENCOUNTER_SPECIES.value);
|
species: ELEMENT_ENCOUNTER_SPECIES.value,
|
||||||
|
|
||||||
if(species > 493 && !AVAILABLE_GENERATION_V_POKEMON.includes(species)) {
|
|
||||||
alert("This Pokémon species cannot be downloaded. Click 'View list' in the encounter form to view a list of available Pokémon.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create encounter data
|
|
||||||
let encounterData = {
|
|
||||||
species: species,
|
|
||||||
move: ELEMENT_ENCOUNTER_MOVE.value,
|
move: ELEMENT_ENCOUNTER_MOVE.value,
|
||||||
form: ELEMENT_ENCOUNTER_FORM.value,
|
form: ELEMENT_ENCOUNTER_FORM.value,
|
||||||
gender: ELEMENT_ENCOUNTER_GENDER.value,
|
gender: ELEMENT_ENCOUNTER_GENDER.value,
|
||||||
animation: ELEMENT_ENCOUNTER_ANIMATION.value
|
animation: ELEMENT_ENCOUNTER_ANIMATION.value
|
||||||
}
|
};
|
||||||
|
|
||||||
// Set form to highest form available if it too great
|
|
||||||
let maxForm = 0;
|
|
||||||
|
|
||||||
switch(species) {
|
|
||||||
case 201: maxForm = 27; break; // Unown
|
|
||||||
case 386: maxForm = 3; break; // Deoxys
|
|
||||||
case 412:
|
|
||||||
case 413: maxForm = 2; break; // Burmy & Wormadam
|
|
||||||
case 422:
|
|
||||||
case 423:
|
|
||||||
case 487: maxForm = 1; break; // Shellos, Gastrodon & Giratina
|
|
||||||
case 479: maxForm = 5; break; // Rotom
|
|
||||||
case 493: maxForm = 16; break; // Arceus
|
|
||||||
case 550: maxForm = 1; break; // Basculin
|
|
||||||
}
|
|
||||||
|
|
||||||
if(encounterData.form > maxForm) {
|
|
||||||
encounterData.form = maxForm;
|
|
||||||
}
|
|
||||||
|
|
||||||
profile.encounters[encounterTableIndex] = encounterData;
|
|
||||||
updateEncounterCell(encounterTableIndex);
|
updateEncounterCell(encounterTableIndex);
|
||||||
closeEncounterForm();
|
closeEncounterForm();
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeEncounter() {
|
function removeEncounter() {
|
||||||
if(encounterTableIndex < 0) {
|
if(encounterTableIndex < 0) {
|
||||||
|
closeEncounterForm();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let oldLength = profile.encounters.length;
|
let oldLength = profile.encounters.length;
|
||||||
profile.encounters.splice(encounterTableIndex, 1);
|
profile.encounters.splice(encounterTableIndex, 1);
|
||||||
|
updateEncounterTable(encounterTableIndex, oldLength);
|
||||||
for(let i = encounterTableIndex; i < oldLength; i++) {
|
closeEncounterForm();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateEncounterTable(startIndex, endIndex) {
|
||||||
|
for(let i = startIndex; i < endIndex; i++) {
|
||||||
updateEncounterCell(i);
|
updateEncounterCell(i);
|
||||||
}
|
}
|
||||||
|
|
||||||
closeEncounterForm();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateEncounterCell(index) {
|
function updateEncounterCell(index) {
|
||||||
@@ -156,11 +273,19 @@ function updateEncounterCell(index) {
|
|||||||
spriteImage = spriteBase + encounterData.species + ".png";
|
spriteImage = spriteBase + encounterData.species + ".png";
|
||||||
|
|
||||||
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -169,24 +294,52 @@ function updateEncounterCell(index) {
|
|||||||
|
|
||||||
function closeEncounterForm() {
|
function closeEncounterForm() {
|
||||||
encounterTableIndex = -1;
|
encounterTableIndex = -1;
|
||||||
window.location.href = "#";
|
ELEMENT_ENCOUNTER_CONFIG.style.display = "none";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Join Avenue visitor configuration stuff
|
||||||
|
*/
|
||||||
|
|
||||||
|
function updateVisitorSubregionOptions() {
|
||||||
|
clearSelectOptions(ELEMENT_VISITOR_SUBREGION);
|
||||||
|
let region = REGION_MAP[ELEMENT_VISITOR_REGION.value];
|
||||||
|
|
||||||
|
// Update subregion options
|
||||||
|
if(region.subregions) {
|
||||||
|
let sortedSubregions = [...region.subregions].sort((a, b) => a.name.localeCompare(b.name));
|
||||||
|
|
||||||
|
for(let i in sortedSubregions) {
|
||||||
|
let subregion = sortedSubregions[i];
|
||||||
|
ELEMENT_VISITOR_SUBREGION.options[ELEMENT_VISITOR_SUBREGION.options.length] = new Option(subregion.name, subregion.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
} else {
|
||||||
|
ELEMENT_VISITOR_SUBREGION.options[ELEMENT_VISITOR_SUBREGION.options.length] = new Option("N/A", 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
function configureVisitor(index) {
|
function configureVisitor(index) {
|
||||||
visitorTableIndex = Math.min(12, Math.min(index, profile.visitors.length));
|
visitorTableIndex = Math.min(12, Math.min(index, profile.visitors.length));
|
||||||
|
|
||||||
// Load existing settings
|
|
||||||
let visitor = profile.visitors[visitorTableIndex];
|
let visitor = profile.visitors[visitorTableIndex];
|
||||||
ELEMENT_VISITOR_NAME.value = visitor ? visitor.name : "";
|
ELEMENT_VISITOR_NAME.value = visitor ? visitor.name : "";
|
||||||
ELEMENT_VISITOR_TYPE.value = visitor ? visitor.type : "ACE_TRAINER_MALE";
|
ELEMENT_VISITOR_TYPE.value = visitor ? visitor.type : "ACE_TRAINER_MALE";
|
||||||
ELEMENT_VISITOR_SHOP_TYPE.value = visitor ? visitor.shopType : "RAFFLE";
|
ELEMENT_VISITOR_SHOP_TYPE.value = visitor ? visitor.shopType : "RAFFLE";
|
||||||
ELEMENT_VISITOR_GAME.value = visitor ? visitor.gameVersion : "BLACK_ENGLISH";
|
ELEMENT_VISITOR_GAME.value = visitor ? visitor.gameVersion : "BLACK_ENGLISH";
|
||||||
|
ELEMENT_VISITOR_REGION.value = visitor ? visitor.countryCode : 1;
|
||||||
|
updateVisitorSubregionOptions();
|
||||||
|
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() {
|
||||||
if(visitorTableIndex < 0) {
|
if(visitorTableIndex < 0) {
|
||||||
|
closeVisitorForm();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -208,36 +361,36 @@ function saveVisitor() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// I'll make country codes configurable later... probably
|
profile.visitors[visitorTableIndex] = {
|
||||||
let visitorData = {
|
|
||||||
name: ELEMENT_VISITOR_NAME.value,
|
name: ELEMENT_VISITOR_NAME.value,
|
||||||
type: ELEMENT_VISITOR_TYPE.value,
|
type: ELEMENT_VISITOR_TYPE.value,
|
||||||
shopType: ELEMENT_VISITOR_SHOP_TYPE.value,
|
shopType: ELEMENT_VISITOR_SHOP_TYPE.value,
|
||||||
gameVersion: ELEMENT_VISITOR_GAME.value,
|
gameVersion: ELEMENT_VISITOR_GAME.value,
|
||||||
countryCode: 220, // United States
|
countryCode: ELEMENT_VISITOR_REGION.value,
|
||||||
stateProvinceCode: 48, // Washington, D.C.
|
stateProvinceCode: ELEMENT_VISITOR_SUBREGION.value,
|
||||||
personality: ELEMENT_VISITOR_PERSONALITY.value,
|
personality: ELEMENT_VISITOR_PERSONALITY.value,
|
||||||
dreamerSpecies: ELEMENT_VISITOR_DREAMER.value
|
dreamerSpecies: ELEMENT_VISITOR_DREAMER.value
|
||||||
}
|
};
|
||||||
|
|
||||||
profile.visitors[visitorTableIndex] = visitorData;
|
|
||||||
updateVisitorCell(visitorTableIndex);
|
updateVisitorCell(visitorTableIndex);
|
||||||
closeVisitorForm();
|
closeVisitorForm();
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeVisitor() {
|
function removeVisitor() {
|
||||||
if(visitorTableIndex < 0) {
|
if(visitorTableIndex < 0) {
|
||||||
|
closeVisitorForm();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let oldLength = profile.visitors.length;
|
let oldLength = profile.visitors.length;
|
||||||
profile.visitors.splice(visitorTableIndex, 1);
|
profile.visitors.splice(visitorTableIndex, 1);
|
||||||
|
updateVisitorTable(visitorTableIndex, oldLength);
|
||||||
for(let i = visitorTableIndex; i < oldLength; i++) {
|
closeVisitorForm();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateVisitorTable(startIndex, endIndex) {
|
||||||
|
for(let i = startIndex; i < endIndex; i++) {
|
||||||
updateVisitorCell(i);
|
updateVisitorCell(i);
|
||||||
}
|
}
|
||||||
|
|
||||||
closeVisitorForm();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateVisitorCell(index) {
|
function updateVisitorCell(index) {
|
||||||
@@ -259,46 +412,51 @@ function updateVisitorCell(index) {
|
|||||||
|
|
||||||
function closeVisitorForm() {
|
function closeVisitorForm() {
|
||||||
visitorTableIndex = -1;
|
visitorTableIndex = -1;
|
||||||
window.location.href = "#";
|
ELEMENT_VISITOR_CONFIG.style.display = "none";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Item configuration stuff
|
||||||
|
*/
|
||||||
|
|
||||||
function configureItem(index) {
|
function configureItem(index) {
|
||||||
itemTableIndex = Math.min(20, Math.min(index, profile.items.length));
|
itemTableIndex = Math.min(20, Math.min(index, profile.items.length));
|
||||||
|
|
||||||
// Loadg existing settings
|
|
||||||
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() {
|
||||||
if(itemTableIndex < 0) {
|
if(itemTableIndex < 0) {
|
||||||
|
closeItemForm();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let itemData = {
|
profile.items[itemTableIndex] = {
|
||||||
id: ELEMENT_ITEM_ID.value,
|
id: ELEMENT_ITEM_ID.value,
|
||||||
quantity: ELEMENT_ITEM_QUANTITY.value
|
quantity: ELEMENT_ITEM_QUANTITY.value
|
||||||
}
|
};
|
||||||
|
|
||||||
profile.items[itemTableIndex] = itemData;
|
|
||||||
updateItemCell(itemTableIndex);
|
updateItemCell(itemTableIndex);
|
||||||
closeItemForm();
|
closeItemForm();
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeItem() {
|
function removeItem() {
|
||||||
if(itemTableIndex < 0) {
|
if(itemTableIndex < 0) {
|
||||||
|
closeItemForm();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let oldLength = profile.items.length;
|
let oldLength = profile.items.length;
|
||||||
profile.items.splice(itemTableIndex, 1);
|
profile.items.splice(itemTableIndex, 1);
|
||||||
|
updateItemTable(itemTableIndex, oldLength);
|
||||||
for(let i = itemTableIndex; i < oldLength; i++) {
|
closeItemForm();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateItemTable(startIndex, endIndex) {
|
||||||
|
for(let i = startIndex; i < endIndex; i++) {
|
||||||
updateItemCell(i);
|
updateItemCell(i);
|
||||||
}
|
}
|
||||||
|
|
||||||
closeItemForm();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateItemCell(index) {
|
function updateItemCell(index) {
|
||||||
@@ -322,10 +480,50 @@ function updateItemCell(index) {
|
|||||||
|
|
||||||
function closeItemForm() {
|
function closeItemForm() {
|
||||||
itemTableIndex = -1;
|
itemTableIndex = -1;
|
||||||
window.location.href = "#";
|
ELEMENT_ITEM_CONFIG.style.display = "none";
|
||||||
}
|
}
|
||||||
|
|
||||||
function previewSkin(inputElementId) {
|
/**
|
||||||
|
* 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) {
|
||||||
let value = document.getElementById(inputElementId).value;
|
let value = document.getElementById(inputElementId).value;
|
||||||
|
|
||||||
if(value == "none") {
|
if(value == "none") {
|
||||||
@@ -333,149 +531,62 @@ function previewSkin(inputElementId) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
window.open("/dashboard/previewskin?name=" + value);
|
if(type == "CGEAR" && isVersion2()) {
|
||||||
|
type = "CGEAR2";
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchData(path) {
|
function setSkinPreviewResolution(width, height) {
|
||||||
return fetchData(path, "GET", null);
|
ELEMENT_SKIN_PREVIEW_IMAGE.width = width;
|
||||||
|
ELEMENT_SKIN_PREVIEW_IMAGE.height = height;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchData(path, method, body) {
|
function closeSkinPreview() {
|
||||||
let response = await fetch(path, {
|
ELEMENT_SKIN_PREVIEW.style.display = "none";
|
||||||
method: method,
|
|
||||||
body: body
|
|
||||||
});
|
|
||||||
|
|
||||||
// Return to login page if unauthorized
|
|
||||||
if(response.status == 401) {
|
|
||||||
window.location.href = "/dashboard/login.html";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
return await response.json();
|
|
||||||
} catch(error) {
|
|
||||||
window.alert(error);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function fetchDlcData() {
|
function fetchDlcData() {
|
||||||
let cgearType = profile.gameVersion.includes("2") ? "CGEAR2" : "CGEAR"; // Not a good way to do this!
|
// Fetch C-Gear skins
|
||||||
|
fetchData("GET", "/dashboard/dlc?type=" + (isVersion2() ? "CGEAR2" : "CGEAR")).then((response) => {
|
||||||
// Fetch CGear skins
|
if(response) {
|
||||||
fetchData("/dashboard/dlc?type=" + cgearType).then((response) => {
|
addDlcNames(ELEMENT_CGEAR_SKIN_INPUT, response);
|
||||||
addValuesToComboBox(ELEMENT_CGEAR_SKIN_INPUT, response);
|
ELEMENT_CGEAR_SKIN_INPUT.value = profile.cgearSkin == "custom" && profile.customCGearSkin ? "custom"
|
||||||
ELEMENT_CGEAR_SKIN_INPUT.value = profile.cgearSkin;
|
: response.includes(profile.cgearSkin) ? profile.cgearSkin : "none";
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Fetch Dex skins
|
// Fetch Dex skins
|
||||||
fetchData("/dashboard/dlc?type=ZUKAN").then((response) => {
|
fetchData("GET", "/dashboard/dlc?type=ZUKAN").then((response) => {
|
||||||
addValuesToComboBox(ELEMENT_DEX_SKIN_INPUT, response);
|
if(response) {
|
||||||
ELEMENT_DEX_SKIN_INPUT.value = profile.dexSkin;
|
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("/dashboard/dlc?type=MUSICAL").then((response) => {
|
fetchData("GET", "/dashboard/dlc?type=MUSICAL").then((response) => {
|
||||||
addValuesToComboBox(ELEMENT_MUSICAL_INPUT, response);
|
if(response) {
|
||||||
ELEMENT_MUSICAL_INPUT.value = profile.musical;
|
addDlcNames(ELEMENT_MUSICAL_INPUT, response);
|
||||||
|
ELEMENT_MUSICAL_INPUT.value = response.includes(profile.musical) ? profile.musical : "none";
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO
|
function addDlcNames(selectElement, names) {
|
||||||
function fetchProfileData() {
|
for(let i in names) {
|
||||||
fetchData("/dashboard/profile").then((response) => {
|
let name = names[i];
|
||||||
let gameVersion = response["gameVersion"];
|
selectElement.options[selectElement.options.length] = new Option(name.replace(/\.[^/.]+$/, ""), name);
|
||||||
let dreamerSprite = response["dreamerSprite"];
|
}
|
||||||
let dreamerInfo = response["dreamerInfo"];
|
|
||||||
let encounters = response["encounters"];
|
|
||||||
let items = response["items"];
|
|
||||||
let visitors = response["avenueVisitors"];
|
|
||||||
let cgearSkin = response["cgearSkin"];
|
|
||||||
let dexSkin = response["dexSkin"];
|
|
||||||
let musical = response["musical"];
|
|
||||||
let levelsGained = response["levelsGained"];
|
|
||||||
|
|
||||||
// Update game summary
|
|
||||||
profile.gameVersion = gameVersion;
|
|
||||||
ELEMENT_GAME_SUMMARY.innerHTML = "Game Card in use: " + gameVersion;
|
|
||||||
|
|
||||||
// Still don't like this!
|
|
||||||
if(gameVersion.includes("2")) {
|
|
||||||
ELEMENT_ENCOUNTER_SPECIES.max = 649;
|
|
||||||
ELEMENT_ITEM_ID.max = 638;
|
|
||||||
|
|
||||||
// Show Join Avenue visitor table
|
|
||||||
document.getElementById("visitor-table-container").style.display = "block";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update dreamer summary
|
|
||||||
if(dreamerInfo) {
|
|
||||||
let species = dreamerInfo["species"];
|
|
||||||
let nature = dreamerInfo["nature"];
|
|
||||||
let nickname = dreamerInfo["nickname"];
|
|
||||||
let gender = dreamerInfo["gender"];
|
|
||||||
let trainerName = dreamerInfo["trainerName"];
|
|
||||||
let trainerId = dreamerInfo["trainerId"];
|
|
||||||
let level = dreamerInfo["level"];
|
|
||||||
|
|
||||||
// Set element values
|
|
||||||
ELEMENT_DREAMER_SPRITE.innerHTML = "<image src='" + dreamerSprite + "'/>";
|
|
||||||
ELEMENT_DREAMER_SPECIES.innerHTML = "#" + species;
|
|
||||||
ELEMENT_DREAMER_NATURE.innerHTML = stringToWord(nature);
|
|
||||||
ELEMENT_DREAMER_NAME.innerHTML = nickname;
|
|
||||||
ELEMENT_DREAMER_GENDER.innerHTML = stringToWord(gender);
|
|
||||||
ELEMENT_DREAMER_TRAINER.innerHTML = trainerName;
|
|
||||||
ELEMENT_DREAMER_TRAINER_ID.innerHTML = ("0000" + trainerId).slice(-5);
|
|
||||||
ELEMENT_DREAMER_LEVEL.innerHTML = level;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update encounter table
|
|
||||||
if(encounters){
|
|
||||||
profile.encounters = encounters;
|
|
||||||
|
|
||||||
for(let i = 0; i < 10; i++) {
|
|
||||||
updateEncounterCell(i);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update item table
|
|
||||||
if(items){
|
|
||||||
profile.items = items;
|
|
||||||
|
|
||||||
for(let i = 0; i < 20; i++) {
|
|
||||||
updateItemCell(i);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update Join Avenue visitor table
|
|
||||||
if(visitors) {
|
|
||||||
profile.visitors = visitors;
|
|
||||||
|
|
||||||
for(let i = 0; i < 12; i++) {
|
|
||||||
updateVisitorCell(i);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update selected DLC
|
|
||||||
profile.cgearSkin = cgearSkin ? cgearSkin : "none";
|
|
||||||
profile.dexSkin = dexSkin ? dexSkin : "none";
|
|
||||||
profile.musical = musical ? musical : "none";
|
|
||||||
fetchDlcData();
|
|
||||||
|
|
||||||
// Update level gain
|
|
||||||
ELEMENT_LEVEL_GAIN_INPUT.value = levelsGained;
|
|
||||||
|
|
||||||
// Show div
|
|
||||||
document.getElementById("main-container").style.display = "flex";
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function postProfileData() {
|
function postProfileData() {
|
||||||
// Construct body
|
fetchData("POST", "/dashboard/profile", JSON.stringify({
|
||||||
let profileData = {
|
|
||||||
encounters: profile.encounters,
|
encounters: profile.encounters,
|
||||||
items: profile.items,
|
items: profile.items,
|
||||||
avenueVisitors: profile.visitors,
|
avenueVisitors: profile.visitors,
|
||||||
@@ -483,36 +594,20 @@ function postProfileData() {
|
|||||||
dexSkin: ELEMENT_DEX_SKIN_INPUT.value,
|
dexSkin: ELEMENT_DEX_SKIN_INPUT.value,
|
||||||
musical: ELEMENT_MUSICAL_INPUT.value,
|
musical: ELEMENT_MUSICAL_INPUT.value,
|
||||||
gainedLevels: ELEMENT_LEVEL_GAIN_INPUT.value
|
gainedLevels: ELEMENT_LEVEL_GAIN_INPUT.value
|
||||||
}
|
})).then((response) => {
|
||||||
|
if(response) {
|
||||||
// Send data
|
alert(response.message);
|
||||||
fetchData("/dashboard/profile", "POST", JSON.stringify(profileData)).then((response) => {
|
}
|
||||||
alert(response.message);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function postLogout() {
|
function postLogout() {
|
||||||
fetchData("/dashboard/logout", "POST", null).then((response) => {
|
fetchData("POST", "/dashboard/logout").then((response) => {
|
||||||
// Assume it succeeded
|
// Assume it succeeded
|
||||||
window.location.href = "/dashboard/login.html";
|
window.location.href = "/dashboard/login.html";
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO bad
|
function isVersion2() {
|
||||||
function checkURL(url) {
|
return profile.gameVersion.includes("2");
|
||||||
var request = new XMLHttpRequest();
|
|
||||||
request.open('HEAD', url, false);
|
|
||||||
request.send();
|
|
||||||
return request.status == 200;
|
|
||||||
}
|
|
||||||
|
|
||||||
function stringToWord(string) {
|
|
||||||
return string.charAt(0).toUpperCase() + string.slice(1).toLowerCase();
|
|
||||||
}
|
|
||||||
|
|
||||||
function addValuesToComboBox(selectorElement, values) {
|
|
||||||
for(i in values) {
|
|
||||||
let value = values[i];
|
|
||||||
selectorElement.options[selectorElement.options.length] = new Option(value, value);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
59
src/main/resources/dashboard/scripts/utility.js
Normal file
59
src/main/resources/dashboard/scripts/utility.js
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
function clampOnChange(inputElement) {
|
||||||
|
inputElement.addEventListener("change", function() {
|
||||||
|
let value = parseInt(this.value);
|
||||||
|
|
||||||
|
if(value < this.min) {
|
||||||
|
this.value = this.min;
|
||||||
|
} else if(value > this.max) {
|
||||||
|
this.value = this.max;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearSelectOptions(selectElement) {
|
||||||
|
let startIndex = selectElement.options.length - 1;
|
||||||
|
|
||||||
|
for(i = startIndex; i >= 0; i--) {
|
||||||
|
selectElement.remove(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function stringToWord(string) {
|
||||||
|
return string.charAt(0).toUpperCase() + string.slice(1).toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkURL(url) {
|
||||||
|
var request = new XMLHttpRequest();
|
||||||
|
request.open('HEAD', url, false);
|
||||||
|
request.send();
|
||||||
|
return request.status == 200;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchData(method, path) {
|
||||||
|
return fetchData(method, path, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchData(method, path, body) {
|
||||||
|
let response = await fetch(path, {
|
||||||
|
method: method,
|
||||||
|
body: body
|
||||||
|
});
|
||||||
|
|
||||||
|
if(response.status != 200) {
|
||||||
|
if(response.status == 401) {
|
||||||
|
window.location.href = "/dashboard/login.html"; // TODO not epic idea to put this here
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.alert("Server returned status code " + response.status + " while fetching " + path);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return await response.json();
|
||||||
|
} catch(error) {
|
||||||
|
window.alert("Could not deserialize JSON response: " + error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<link rel="stylesheet" href="styles/list.css">
|
|
||||||
</head>
|
|
||||||
<body onload="fillTableWithSpecies()">
|
|
||||||
<div id="main-container" class="root-container"">
|
|
||||||
<label class="big-label">List of Pokémon species that can be downloaded</label>
|
|
||||||
<label>NOTE: Generation V Pokémon are exclusive to Black Version 2 and White Version 2</label>
|
|
||||||
<table id="species-table">
|
|
||||||
<!-- Filled by list.js -->
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
<script src="scripts/list.js"></script>
|
|
||||||
</html>
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
body {
|
|
||||||
background-color: #191919;
|
|
||||||
color: white;
|
|
||||||
font-size: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
img {
|
|
||||||
image-rendering: pixelated;
|
|
||||||
}
|
|
||||||
|
|
||||||
label {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
table {
|
|
||||||
border-spacing: 8px;
|
|
||||||
margin-top: 15px;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
td {
|
|
||||||
background-color: #262626;
|
|
||||||
border-radius: 10px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.root-container {
|
|
||||||
display: grid;
|
|
||||||
position: absolute;
|
|
||||||
left: 50%;
|
|
||||||
transform: translate(-50%, 0%);
|
|
||||||
width: 1136; /* Hack-ish */
|
|
||||||
}
|
|
||||||
|
|
||||||
.big-label {
|
|
||||||
font-size: 36px;
|
|
||||||
}
|
|
||||||
@@ -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;
|
||||||
@@ -72,27 +76,20 @@ a:link, a:visited {
|
|||||||
margin-top: 3px;
|
margin-top: 3px;
|
||||||
margin-left: -2px;
|
margin-left: -2px;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
|
table-layout: fixed;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dreamer-summary th {
|
.dreamer-summary th {
|
||||||
text-align: left;
|
text-align: left;
|
||||||
padding-left: 10px;
|
padding-left: 10px;
|
||||||
min-width: 76px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.dreamer-summary td {
|
.dreamer-summary td {
|
||||||
width: 50%;
|
|
||||||
background-color: #262626;
|
background-color: #262626;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
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;
|
||||||
@@ -101,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: 25%;
|
|
||||||
transform: translate(-50%, 0%);
|
|
||||||
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 {
|
||||||
@@ -148,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 {
|
||||||
@@ -164,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 {
|
||||||
@@ -186,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,44 +1,62 @@
|
|||||||
/dlc/IRAO/CGEAR/C003_munna_1_en.bin
|
/dlc/IRAO/CGEAR/01 - Default.bin 19c71995ffb00f4bdc59bfa805eee8fa
|
||||||
/dlc/IRAO/CGEAR/C004_pikachu_1_e.bin
|
/dlc/IRAO/CGEAR/02 - Meadow Munna.bin d434decfee8faad9485560fbc9206273
|
||||||
/dlc/IRAO/CGEAR/C005_chirami_1_e.bin
|
/dlc/IRAO/CGEAR/03 - Aim for the Top.bin c93f457ae1310bfe03f7ede622c7f63a
|
||||||
/dlc/IRAO/CGEAR/C007_tabunne_1_e.bin
|
/dlc/IRAO/CGEAR/04 - Twinkle Minccino.bin 86902dc9ec625e05673697367d492898
|
||||||
/dlc/IRAO/CGEAR/C008_pocchama_1_e.bin
|
/dlc/IRAO/CGEAR/05 - PokeSma Purrloin.bin 0efe27a2cf7e48de2273ccba0fda2031
|
||||||
/dlc/IRAO/CGEAR/C009_guregguru_1_e.bin
|
/dlc/IRAO/CGEAR/06 - Earful Audino.bin ee383864a67e6a04235c7f6a891a9539
|
||||||
/dlc/IRAO/CGEAR/C010_fushigibana_1_e.bin
|
/dlc/IRAO/CGEAR/07 - Happy Piplup.bin 88d0dfac0dda854486b8b5fb839e0bfd
|
||||||
/dlc/IRAO/CGEAR/C011_rizadon_1_e.bin
|
/dlc/IRAO/CGEAR/08 - Poison Jab Croagunk.bin 7294c3e4db163beb98ea49be10d8eb2a
|
||||||
/dlc/IRAO/CGEAR/C012_kamekkusu_1_e.bin
|
/dlc/IRAO/CGEAR/09 - Venusaur!.bin 7c51d55a63117bb166cc507f064f7cca
|
||||||
/dlc/IRAO/CGEAR/C013_zekurom_1_e_v02.bin
|
/dlc/IRAO/CGEAR/10 - Charizard!.bin a8ccb7866393efda3422656aa2d8f292
|
||||||
/dlc/IRAO/CGEAR/C014_reshiram_1_e.bin
|
/dlc/IRAO/CGEAR/11 - Blastoise!.bin e0a1b44450407af394f390c0b229e7a6
|
||||||
/dlc/IRAO/CGEAR/C015_vikutini_1_e.bin
|
/dlc/IRAO/CGEAR/12 - V for Victory!.bin 847d6d230fe2513758d50adac52d54b8
|
||||||
/dlc/IRAO/CGEAR/C017_porigonz_0_e.bin
|
/dlc/IRAO/CGEAR/13 - Virtual Pokemon.bin 002263910ed018ff04922c4f3971b777
|
||||||
/dlc/IRAO/CGEAR/C018_WCS_1_e.bin
|
/dlc/IRAO/CGEAR/14 - Pokemon Cafe.bin efd6e6de86ea892dda840bcc6952afc1
|
||||||
/dlc/IRAO/CGEAR/C019_shikijikaharu_1_e.bin
|
/dlc/IRAO/CGEAR/15 - Hero Reshiram.bin d89ceeb9a9e7774fa802c1aa2f38409a
|
||||||
/dlc/IRAO/CGEAR/C020_shikijikanatu_1_e.bin
|
/dlc/IRAO/CGEAR/16 - Hero Zekrom.bin 2548f0ed48e130649ff351ca418e3b7f
|
||||||
/dlc/IRAO/CGEAR/C021_shikijikaaki_1_e.bin
|
/dlc/IRAO/CGEAR/17 - 2011 Worlds C-Gear.bin 70119e0a5e67fb4c9dda7b724e157784
|
||||||
/dlc/IRAO/CGEAR/C022_shikijikafuyu_1_e.bin
|
/dlc/IRAO/CGEAR/18 - Zoroark!.bin 81e9241acb3270e2d490dd9966f3ecf1
|
||||||
/dlc/IRAO/CGEAR/C023_iwaparesu_1_e.bin
|
/dlc/IRAO/CGEAR/19 - Spring Deerling.bin ca785e48d49269a182ee0fe0981168a8
|
||||||
/dlc/IRAO/CGEAR/C024_zoroark_1_e.bin
|
/dlc/IRAO/CGEAR/20 - Summer Deerling.bin 0e8b6b527a411aeb302cfe07cb3679b2
|
||||||
/dlc/IRAO/CGEAR/C025_giaru_1_e.bin
|
/dlc/IRAO/CGEAR/21 - Autumn Deerling.bin 7155fca4dc915fc6cb87495fa7a9c743
|
||||||
/dlc/IRAO/CGEAR/C030_kerudhio_1_en.bin
|
/dlc/IRAO/CGEAR/22 - Winter Deerling.bin 7f9d3448f40d0ea513c1f94e311e77bb
|
||||||
/dlc/IRAO/CGEAR/C031-1_meroetta_1_e.bin
|
/dlc/IRAO/CGEAR/23 - CRUSTLE!.bin eb1a0cb83e330375c257cb61bbc80e84
|
||||||
/dlc/IRAO/CGEAR/C032-1_wcs2012_1_e.bin
|
/dlc/IRAO/CGEAR/24 - KLINK!.bin f4ebe5cbb0de53c44f0c620215e840b4
|
||||||
/dlc/IRAO/CGEAR/C100_defo_1_en.bin
|
/dlc/IRAO/CGEAR/25 - Ducklett Friends.bin bb359435efd5e98f48beba2cf71126d9
|
||||||
/dlc/IRAO/CGEAR2/C003-2_munna_1_en.bin
|
/dlc/IRAO/CGEAR/26 - Guidance Cobalion.bin fcd86f0f98b1d770e28001c02f455c58
|
||||||
/dlc/IRAO/CGEAR2/C013-2_zekrom_1_e.bin
|
/dlc/IRAO/CGEAR/27 - Trial Terrakion.bin 6c57b06edcaa0aeb6b716ecd5d3f2336
|
||||||
/dlc/IRAO/CGEAR2/C014-2_reshiram_1_e.bin
|
/dlc/IRAO/CGEAR/28 - Rumination Virizion.bin b02dce72dfd25a4c8d417b87f3a99329
|
||||||
/dlc/IRAO/CGEAR2/C015-2_victini_1_e.bin
|
/dlc/IRAO/CGEAR/29 - Keldeo Ordinary Forme.bin a83088c9cf11277049ed472d302b0dd6
|
||||||
/dlc/IRAO/CGEAR2/C030-2_kerudhio_1_en.bin
|
/dlc/IRAO/CGEAR/30 - Aria of the Night Sky.bin 4306211f215bb0d397c92037812d37ae
|
||||||
/dlc/IRAO/CGEAR2/C031-2_meroetta_1_e.bin
|
/dlc/IRAO/CGEAR/31 - 2012 Worlds C-Gear.bin 225505e34dab2ccd300e3b4bd7eda47f
|
||||||
/dlc/IRAO/CGEAR2/C033_halloween_1_en.bin
|
/dlc/IRAO/CGEAR/32 - Red Genesect.bin 614be39e965dab594a5ffaac6bd5ae82
|
||||||
/dlc/IRAO/CGEAR2/C035_BK_1_e.bin
|
/dlc/IRAO/CGEAR/33 - PCN Cup 2024 Summer.bin f51d9e9045b51dcc1d02cc8091c520e7
|
||||||
/dlc/IRAO/CGEAR2/C036_WK_1_e.bin
|
/dlc/IRAO/CGEAR2/01 - Default.bin d7ab435f5016ac3e6a7f444ccd25f424
|
||||||
/dlc/IRAO/CGEAR2/C100-2_default_1_e.bin
|
/dlc/IRAO/CGEAR2/02 - Meadow Munna.bin 98b944531d9206512bbc5f59f8229139
|
||||||
/dlc/IRAO/MUSICAL/M010_munna_1_e_02.bin
|
/dlc/IRAO/CGEAR2/03 - Keldeo Resolute Forme.bin e7bddaf878faa5b009caee7d96998954
|
||||||
/dlc/IRAO/MUSICAL/M013_meloetta_1_e.bin
|
/dlc/IRAO/CGEAR2/04 - 2012 Worlds C-Gear.bin 36d5948c310428b251b70ec1dcf0845a
|
||||||
/dlc/IRAO/ZUKAN/Z003_BWsyokigirl_1_en.bin
|
/dlc/IRAO/CGEAR2/05 - Pumpkin Pikachu.bin 7fd54cd2bd5310c40139b4329124aa2d
|
||||||
/dlc/IRAO/ZUKAN/Z004_BWsyokiboy_1_en.bin
|
/dlc/IRAO/CGEAR2/06 - V for Victory!.bin 4f9a1ca435561cfe49d892488b398caa
|
||||||
/dlc/IRAO/ZUKAN/Z007_hyuu_1_en.bin
|
/dlc/IRAO/CGEAR2/07 - Hero Reshiram.bin ec233268fb4d04cbb13b8b944552e94b
|
||||||
/dlc/IRAO/ZUKAN/Z008_bell_1_e.bin
|
/dlc/IRAO/CGEAR2/08 - Hero Zekrom.bin 4647c982bbbd3955b4d8e4c9309ff7ef
|
||||||
/dlc/IRAO/ZUKAN/Z009_tyeren_1_e.bin
|
/dlc/IRAO/CGEAR2/09 - Aria of the Night Sky.bin 2f609aeda35b34b6386cfa66d57e0ad5
|
||||||
/dlc/IRAO/ZUKAN/Z100_defogirl_1_en.bin
|
/dlc/IRAO/CGEAR2/10 - Sleeping Eevee.bin 4882e949ae9c6de8cb2735942f0b1624
|
||||||
/dlc/IRAO/ZUKAN/Z101_defoboy_1_en.bin
|
/dlc/IRAO/CGEAR2/11 - Venusaur!.bin 21becbbbba091cb1060ed19c4436d98f
|
||||||
|
/dlc/IRAO/CGEAR2/12 - Charizard!.bin 75705d719bae83ac130e03598d85382d
|
||||||
|
/dlc/IRAO/CGEAR2/13 - Blastoise!.bin 4f41e613d79826f48b8f8b9d1aa5747b
|
||||||
|
/dlc/IRAO/CGEAR2/14 - Red Genesect.bin 0d0951b899bff3d9e243a9dc61aff973
|
||||||
|
/dlc/IRAO/CGEAR2/15 - Black Kyurem.bin ff4356dac514c364557bd685c932f43e
|
||||||
|
/dlc/IRAO/CGEAR2/16 - White Kyurem.bin e51c69805f87a954a7fd563b920cc036
|
||||||
|
/dlc/IRAO/CGEAR2/17 - PCN Cup 2024 Summer.bin e925ddca5d9a52c49a3463916f6fcc0a
|
||||||
|
/dlc/IRAO/MUSICAL/01 - Charming Munna.bin dd180c539b51305244c10380266ec2ac
|
||||||
|
/dlc/IRAO/MUSICAL/02 - MELOETTAAA!!!.bin f1a18eaf27c60b43020e9d5c0195eaed
|
||||||
|
/dlc/IRAO/ZUKAN/01 - Default (Pink).bin 7150719a18ffacef993c58b112cfaf52
|
||||||
|
/dlc/IRAO/ZUKAN/02 - Default (Red).bin bc396d6403d9d296c1b4700d31b6eb42
|
||||||
|
/dlc/IRAO/ZUKAN/03 - Unova Trio (Pink).bin 2e80bbc0e824c9c17fb0b5480d6bcb7c
|
||||||
|
/dlc/IRAO/ZUKAN/04 - Unova Trio (Red).bin b3e3eadbad327a8c4b3889768f79fd8e
|
||||||
|
/dlc/IRAO/ZUKAN/05 - Kanto Trio (Pink).bin abbe82e2ff4c66d0557b9989da3f5c29
|
||||||
|
/dlc/IRAO/ZUKAN/06 - Kanto Trio (Red).bin 7ba1f19f564e271d2e92810e174cd75e
|
||||||
|
/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/05 - PokeSma Purrloin.bin
Normal file
BIN
src/main/resources/dlc/IRAO/CGEAR/05 - PokeSma Purrloin.bin
Normal file
Binary file not shown.
BIN
src/main/resources/dlc/IRAO/CGEAR/14 - Pokemon Cafe.bin
Normal file
BIN
src/main/resources/dlc/IRAO/CGEAR/14 - Pokemon Cafe.bin
Normal file
Binary file not shown.
BIN
src/main/resources/dlc/IRAO/CGEAR/25 - Ducklett Friends.bin
Normal file
BIN
src/main/resources/dlc/IRAO/CGEAR/25 - Ducklett Friends.bin
Normal file
Binary file not shown.
BIN
src/main/resources/dlc/IRAO/CGEAR/26 - Guidance Cobalion.bin
Normal file
BIN
src/main/resources/dlc/IRAO/CGEAR/26 - Guidance Cobalion.bin
Normal file
Binary file not shown.
BIN
src/main/resources/dlc/IRAO/CGEAR/27 - Trial Terrakion.bin
Normal file
BIN
src/main/resources/dlc/IRAO/CGEAR/27 - Trial Terrakion.bin
Normal file
Binary file not shown.
BIN
src/main/resources/dlc/IRAO/CGEAR/28 - Rumination Virizion.bin
Normal file
BIN
src/main/resources/dlc/IRAO/CGEAR/28 - Rumination Virizion.bin
Normal file
Binary file not shown.
BIN
src/main/resources/dlc/IRAO/CGEAR/32 - Red Genesect.bin
Normal file
BIN
src/main/resources/dlc/IRAO/CGEAR/32 - Red Genesect.bin
Normal file
Binary file not shown.
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.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user