mirror of
https://github.com/kuroppoi/entralinked.git
synced 2026-09-11 10:15:10 -05:00
Compare commits
85 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
df298d8703 | ||
|
|
556eab6b45 | ||
|
|
b831383284 | ||
|
|
b37f8b5624 | ||
|
|
abc32e5051 | ||
|
|
d5c2a57c51 | ||
|
|
afc79c39ad | ||
|
|
15c42165ed | ||
|
|
e663d91559 | ||
|
|
4031d833e8 | ||
|
|
2385398b4b | ||
|
|
840d5e956a | ||
|
|
bf2cc1f6bc | ||
|
|
f3a5df2ff6 | ||
|
|
d8bef2f184 | ||
|
|
31665f3f74 | ||
|
|
356900d591 | ||
|
|
ff8615401e | ||
|
|
90cb874520 | ||
|
|
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 | ||
|
|
8849265805 | ||
|
|
0d964ec129 | ||
|
|
e4fff87b87 | ||
|
|
aae1abd18f | ||
|
|
038f7f5136 |
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,34 +1,34 @@
|
|||||||
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
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4.2.2
|
||||||
- name: Checkout submodules
|
- name: Checkout submodules
|
||||||
run: git submodule update --init --recursive
|
run: git submodule update --init --recursive
|
||||||
- name: Setup Java 17
|
- name: Setup Java 17
|
||||||
uses: actions/setup-java@v3
|
uses: actions/setup-java@v4.7.0
|
||||||
with:
|
with:
|
||||||
java-version: 17
|
java-version: 17
|
||||||
distribution: temurin
|
distribution: temurin
|
||||||
- name: Make gradlew executable
|
- name: Make gradlew executable
|
||||||
run: chmod +x ./gradlew
|
run: chmod +x ./gradlew
|
||||||
- name: Setup Gradle
|
- name: Setup Gradle
|
||||||
uses: gradle/gradle-build-action@v2
|
uses: gradle/actions/setup-gradle@v3
|
||||||
- name: Run Gradle dist
|
- name: Run Gradle dist
|
||||||
run: ./gradlew dist
|
run: ./gradlew dist
|
||||||
- name: Upload artifact
|
- name: Upload artifact
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v4.6.2
|
||||||
with:
|
with:
|
||||||
name: entralinked
|
name: entralinked
|
||||||
path: build/libs/entralinked.jar
|
path: build/libs/entralinked.jar
|
||||||
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
|
|
||||||
22
README.md
22
README.md
@@ -1,9 +1,20 @@
|
|||||||
# 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 and Musicals\
|
Its purpose is to serve as a simple utility for downloading Pokémon, Items, C-Gear skins, Pokédex skins, Musicals\
|
||||||
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.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
For users: [Quick Setup Guide](https://github.com/kuroppoi/entralinked/wiki/Setup)
|
||||||
|
|
||||||
## Building
|
## Building
|
||||||
|
|
||||||
@@ -27,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,14 @@ 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'
|
||||||
|
implementation 'com.miglayout:miglayout-swing:4.2' // Finally, a good fucking layout manager.
|
||||||
|
implementation 'org.swinglabs.swingx:swingx-autocomplete:1.6.5-1'
|
||||||
}
|
}
|
||||||
|
|
||||||
sourceSets {
|
sourceSets {
|
||||||
|
|||||||
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,12 @@
|
|||||||
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 allowWfcRegistrationThroughLogin) {
|
||||||
@JsonProperty(required = true) boolean allowWfcRegistrationThroughLogin) {
|
|
||||||
|
|
||||||
public static final Configuration DEFAULT = new Configuration("local", true, false, true);
|
public static final Configuration DEFAULT = new Configuration("local", true, true);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import org.apache.logging.log4j.Logger;
|
|||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import com.fasterxml.jackson.databind.SerializationFeature;
|
import com.fasterxml.jackson.databind.SerializationFeature;
|
||||||
|
|
||||||
import entralinked.gui.MainView;
|
import entralinked.gui.view.MainView;
|
||||||
import entralinked.model.dlc.DlcList;
|
import entralinked.model.dlc.DlcList;
|
||||||
import entralinked.model.player.PlayerManager;
|
import entralinked.model.player.PlayerManager;
|
||||||
import entralinked.model.user.UserManager;
|
import entralinked.model.user.UserManager;
|
||||||
@@ -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,41 @@ 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.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 +157,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 +193,8 @@ public class Entralinked {
|
|||||||
public PlayerManager getPlayerManager() {
|
public PlayerManager getPlayerManager() {
|
||||||
return playerManager;
|
return playerManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isInitialized() {
|
||||||
|
return initialized;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import java.util.HashMap;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
public enum GameVersion {
|
public enum GameVersion {
|
||||||
|
|
||||||
// ==================================
|
// ==================================
|
||||||
// Black Version & White Version
|
// Black Version & White Version
|
||||||
// ==================================
|
// ==================================
|
||||||
@@ -29,21 +29,35 @@ public enum GameVersion {
|
|||||||
// Black Version 2 & White Version 2
|
// Black Version 2 & White Version 2
|
||||||
// ==================================
|
// ==================================
|
||||||
|
|
||||||
BLACK_2_JAPANESE(23, 1, "IREJ", "ブラック2", true),
|
BLACK_2_JAPANESE(23, 1, "IREJ", "ブラック2"),
|
||||||
BLACK_2_ENGLISH(23, 2, "IREO", "Black Version 2", true),
|
BLACK_2_ENGLISH(23, 2, "IREO", "Black Version 2"),
|
||||||
BLACK_2_FRENCH(23, 3, "IREF", "Version Noire 2", true),
|
BLACK_2_FRENCH(23, 3, "IREF", "Version Noire 2"),
|
||||||
BLACK_2_ITALIAN(23, 4, "IREI", "Versione Nera 2", true),
|
BLACK_2_ITALIAN(23, 4, "IREI", "Versione Nera 2"),
|
||||||
BLACK_2_GERMAN(23, 5, "IRED", "Schwarze Edition 2", true),
|
BLACK_2_GERMAN(23, 5, "IRED", "Schwarze Edition 2"),
|
||||||
BLACK_2_SPANISH(23, 7, "IRES", "Edicion Negra 2", true),
|
BLACK_2_SPANISH(23, 7, "IRES", "Edicion Negra 2"),
|
||||||
BLACK_2_KOREAN(23, 8, "IREK", "블랙2", true),
|
BLACK_2_KOREAN(23, 8, "IREK", "블랙2"),
|
||||||
|
|
||||||
WHITE_2_JAPANESE(22, 1, "IRDJ", "ホワイト2", true),
|
WHITE_2_JAPANESE(22, 1, "IRDJ", "ホワイト2"),
|
||||||
WHITE_2_ENGLISH(22, 2, "IRDO", "White Version 2", true),
|
WHITE_2_ENGLISH(22, 2, "IRDO", "White Version 2"),
|
||||||
WHITE_2_FRENCH(22, 3, "IRDF", "Version Blanche 2", true),
|
WHITE_2_FRENCH(22, 3, "IRDF", "Version Blanche 2"),
|
||||||
WHITE_2_ITALIAN(22, 4, "IRDI", "Versione Bianca 2", true),
|
WHITE_2_ITALIAN(22, 4, "IRDI", "Versione Bianca 2"),
|
||||||
WHITE_2_GERMAN(22, 5, "IRDD", "Weisse Edition 2", true),
|
WHITE_2_GERMAN(22, 5, "IRDD", "Weisse Edition 2"),
|
||||||
WHITE_2_SPANISH(22, 7, "IRDS", "Edicion Blanca 2", true),
|
WHITE_2_SPANISH(22, 7, "IRDS", "Edicion Blanca 2"),
|
||||||
WHITE_2_KOREAN(22, 8, "IRDK", "화이트2", true);
|
WHITE_2_KOREAN(22, 8, "IRDK", "화이트2");
|
||||||
|
|
||||||
|
// Masks
|
||||||
|
public static final int BW_MASK = 0b110011111111;
|
||||||
|
public static final int B2W2_MASK = 0b001111111111;
|
||||||
|
public static final int ALL_MASK = BW_MASK | B2W2_MASK;
|
||||||
|
public static final int JAP_MASK = 0b111100000001;
|
||||||
|
public static final int ENG_MASK = 0b111100000010;
|
||||||
|
public static final int FRE_MASK = 0b111100000100;
|
||||||
|
public static final int ITA_MASK = 0b111100001000;
|
||||||
|
public static final int GER_MASK = 0b111100010000;
|
||||||
|
public static final int SPA_MASK = 0b111101000000;
|
||||||
|
public static final int KOR_MASK = 0b111110000000;
|
||||||
|
public static final int JAP_KOR_MASK = JAP_MASK | KOR_MASK;
|
||||||
|
public static final int NA_EUR_MASK = ENG_MASK | FRE_MASK | ITA_MASK | GER_MASK | SPA_MASK;
|
||||||
|
|
||||||
// Lookup maps
|
// Lookup maps
|
||||||
private static final Map<String, GameVersion> mapBySerial = new HashMap<>();
|
private static final Map<String, GameVersion> mapBySerial = new HashMap<>();
|
||||||
@@ -52,7 +66,7 @@ public enum GameVersion {
|
|||||||
static {
|
static {
|
||||||
for(GameVersion version : values()) {
|
for(GameVersion version : values()) {
|
||||||
mapBySerial.put(version.getSerial(), version);
|
mapBySerial.put(version.getSerial(), version);
|
||||||
mapByCodes.put(version.getRomCode() << version.getLanguageCode(), version);
|
mapByCodes.put(version.getBits(), version);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,18 +74,12 @@ public enum GameVersion {
|
|||||||
private final int languageCode; // Values are not tested
|
private final int languageCode; // Values are not tested
|
||||||
private final String serial;
|
private final String serial;
|
||||||
private final String displayName;
|
private final String displayName;
|
||||||
private final boolean isVersion2;
|
|
||||||
|
|
||||||
private GameVersion(int romCode, int languageCode, String serial, String displayName, boolean isVersion2) {
|
private GameVersion(int romCode, int languageCode, String serial, String displayName) {
|
||||||
this.romCode = romCode;
|
this.romCode = romCode;
|
||||||
this.languageCode = languageCode;
|
this.languageCode = languageCode;
|
||||||
this.serial = serial;
|
this.serial = serial;
|
||||||
this.displayName = displayName;
|
this.displayName = displayName;
|
||||||
this.isVersion2 = isVersion2;
|
|
||||||
}
|
|
||||||
|
|
||||||
private GameVersion(int romCode, int languageCode, String serial, String displayName) {
|
|
||||||
this(romCode, languageCode, serial, displayName, false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static GameVersion lookup(String serial) {
|
public static GameVersion lookup(String serial) {
|
||||||
@@ -79,7 +87,11 @@ public enum GameVersion {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static GameVersion lookup(int romCode, int languageCode) {
|
public static GameVersion lookup(int romCode, int languageCode) {
|
||||||
return mapByCodes.get(romCode << languageCode);
|
return mapByCodes.get(getBits(romCode, languageCode));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int getBits(int romCode, int languageCode) {
|
||||||
|
return (1 << (8 - (romCode - 23))) | (1 << languageCode - 1) & 0b111111111111;
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getRomCode() {
|
public int getRomCode() {
|
||||||
@@ -99,6 +111,15 @@ public enum GameVersion {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public boolean isVersion2() {
|
public boolean isVersion2() {
|
||||||
return isVersion2;
|
return checkMask(B2W2_MASK);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean checkMask(int mask) {
|
||||||
|
int bits = getBits();
|
||||||
|
return (bits & mask) == bits;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getBits() {
|
||||||
|
return getBits(romCode, languageCode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
55
src/main/java/entralinked/gui/FileChooser.java
Normal file
55
src/main/java/entralinked/gui/FileChooser.java
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
package entralinked.gui;
|
||||||
|
|
||||||
|
import java.awt.Component;
|
||||||
|
import java.io.File;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
import java.util.function.Function;
|
||||||
|
|
||||||
|
import javax.swing.JFileChooser;
|
||||||
|
import javax.swing.filechooser.FileFilter;
|
||||||
|
|
||||||
|
public class FileChooser {
|
||||||
|
|
||||||
|
private static final JFileChooser fileChooser = new JFileChooser(".");
|
||||||
|
|
||||||
|
public static void showFileOpenDialog(Component parent, Consumer<FileSelection> handler) {
|
||||||
|
showFileOpenDialog(parent, Collections.emptyList(), handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void showFileOpenDialog(Component parent, FileFilter fileFilter, Consumer<FileSelection> handler) {
|
||||||
|
showFileOpenDialog(parent, List.of(fileFilter), handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void showFileOpenDialog(Component parent, List<FileFilter> fileFilters, Consumer<FileSelection> handler) {
|
||||||
|
showDialog(parent, fileFilters, fileChooser::showOpenDialog, handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void showDialog(Component parent, List<FileFilter> fileFilters, Function<Component, Integer> dialogFunction, Consumer<FileSelection> handler) {
|
||||||
|
FileFilter currentFilter = fileChooser.getFileFilter();
|
||||||
|
fileChooser.resetChoosableFileFilters();
|
||||||
|
fileChooser.setAcceptAllFileFilterUsed(fileFilters.isEmpty());
|
||||||
|
fileFilters.forEach(fileChooser::addChoosableFileFilter);
|
||||||
|
|
||||||
|
if(fileFilters.contains(currentFilter)) {
|
||||||
|
fileChooser.setFileFilter(currentFilter);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(dialogFunction.apply(parent) == JFileChooser.APPROVE_OPTION) {
|
||||||
|
File file = fileChooser.getSelectedFile();
|
||||||
|
handler.accept(new FileSelection(file, fileChooser.getFileFilter(), getFileExtension(file)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getFileExtension(File file) {
|
||||||
|
String name = file.getName();
|
||||||
|
int index = name.lastIndexOf('.');
|
||||||
|
|
||||||
|
if(index == -1 || index + 1 == name.length()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return name.substring(index + 1).toLowerCase();
|
||||||
|
}
|
||||||
|
}
|
||||||
7
src/main/java/entralinked/gui/FileSelection.java
Normal file
7
src/main/java/entralinked/gui/FileSelection.java
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
package entralinked.gui;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
|
||||||
|
import javax.swing.filechooser.FileFilter;
|
||||||
|
|
||||||
|
public record FileSelection(File file, FileFilter filter, String extension) {}
|
||||||
46
src/main/java/entralinked/gui/GsidDocumentFilter.java
Normal file
46
src/main/java/entralinked/gui/GsidDocumentFilter.java
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
package entralinked.gui;
|
||||||
|
|
||||||
|
import java.awt.Toolkit;
|
||||||
|
|
||||||
|
import javax.swing.text.AttributeSet;
|
||||||
|
import javax.swing.text.BadLocationException;
|
||||||
|
|
||||||
|
import entralinked.utility.GsidUtility;
|
||||||
|
|
||||||
|
public class GsidDocumentFilter extends SizeLimitDocumentFilter {
|
||||||
|
|
||||||
|
public GsidDocumentFilter() {
|
||||||
|
super(10);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void insertString(FilterBypass fb, int offset, String text, AttributeSet attrs) throws BadLocationException {
|
||||||
|
replace(fb, offset, 0, text, attrs);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void replace(FilterBypass fb, int offset, int length, String text, AttributeSet attrs) throws BadLocationException {
|
||||||
|
if(text == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
StringBuilder builder = new StringBuilder();
|
||||||
|
boolean shouldBeep = false;
|
||||||
|
|
||||||
|
for(int i = 0; i < text.length(); i++) {
|
||||||
|
char c = Character.toUpperCase(text.charAt(i));
|
||||||
|
|
||||||
|
if(GsidUtility.GSID_CHARTABLE.indexOf(c) != -1) {
|
||||||
|
builder.append(c);
|
||||||
|
} else {
|
||||||
|
shouldBeep = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(shouldBeep) {
|
||||||
|
Toolkit.getDefaultToolkit().beep();
|
||||||
|
}
|
||||||
|
|
||||||
|
super.replace(fb, offset, length, builder.toString(), attrs);
|
||||||
|
}
|
||||||
|
}
|
||||||
60
src/main/java/entralinked/gui/ImageLoader.java
Normal file
60
src/main/java/entralinked/gui/ImageLoader.java
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
package entralinked.gui;
|
||||||
|
|
||||||
|
import java.awt.FontMetrics;
|
||||||
|
import java.awt.Graphics2D;
|
||||||
|
import java.awt.RenderingHints;
|
||||||
|
import java.awt.image.BufferedImage;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import javax.imageio.ImageIO;
|
||||||
|
|
||||||
|
import entralinked.gui.data.DataManager;
|
||||||
|
|
||||||
|
public class ImageLoader {
|
||||||
|
|
||||||
|
private static final Map<String, BufferedImage> cache = new HashMap<>();
|
||||||
|
|
||||||
|
public static BufferedImage getImage(String path) {
|
||||||
|
return cache.computeIfAbsent(path, ImageLoader::loadImage);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static BufferedImage loadImage(String path) {
|
||||||
|
try {
|
||||||
|
return ImageIO.read(DataManager.class.getResource(path));
|
||||||
|
} catch(Exception e) {
|
||||||
|
BufferedImage image = new BufferedImage(128, 128, BufferedImage.TYPE_INT_ARGB);
|
||||||
|
Graphics2D graphics = image.createGraphics();
|
||||||
|
graphics.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
|
||||||
|
graphics.drawString("IMAGE LOAD ERROR", 0, 16);
|
||||||
|
graphics.drawString("Please report this!", 0, 124);
|
||||||
|
drawWrappedString(graphics, path, 0, 32, image.getWidth());
|
||||||
|
drawWrappedString(graphics, e.getMessage(), 0, 80, image.getWidth());
|
||||||
|
graphics.dispose();
|
||||||
|
return image;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void drawWrappedString(Graphics2D graphics, String string, int x, int y, int width) {
|
||||||
|
int length = string.length();
|
||||||
|
FontMetrics metrics = graphics.getFontMetrics();
|
||||||
|
String line = "";
|
||||||
|
int currentY = y;
|
||||||
|
|
||||||
|
for(int i = 0; i < length; i++) {
|
||||||
|
char next = string.charAt(i);
|
||||||
|
|
||||||
|
if((!line.isEmpty() && x + metrics.stringWidth(line + next) >= width)) {
|
||||||
|
graphics.drawString(line, x, currentY);
|
||||||
|
line = "";
|
||||||
|
currentY += metrics.getHeight();
|
||||||
|
}
|
||||||
|
|
||||||
|
line += next;
|
||||||
|
|
||||||
|
if(i + 1 == length) {
|
||||||
|
graphics.drawString(line, x, currentY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
25
src/main/java/entralinked/gui/InputVerifierCellEditor.java
Normal file
25
src/main/java/entralinked/gui/InputVerifierCellEditor.java
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
package entralinked.gui;
|
||||||
|
|
||||||
|
import javax.swing.DefaultCellEditor;
|
||||||
|
import javax.swing.InputVerifier;
|
||||||
|
import javax.swing.JTextField;
|
||||||
|
|
||||||
|
@SuppressWarnings("serial")
|
||||||
|
public class InputVerifierCellEditor extends DefaultCellEditor {
|
||||||
|
|
||||||
|
private final InputVerifier verifier;
|
||||||
|
|
||||||
|
public InputVerifierCellEditor(InputVerifier verifier) {
|
||||||
|
this(verifier, new JTextField());
|
||||||
|
}
|
||||||
|
|
||||||
|
public InputVerifierCellEditor(InputVerifier verifier, JTextField textField) {
|
||||||
|
super(textField);
|
||||||
|
this.verifier = verifier;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean stopCellEditing() {
|
||||||
|
return verifier.verify(editorComponent) && super.stopCellEditing();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,150 +0,0 @@
|
|||||||
package entralinked.gui;
|
|
||||||
|
|
||||||
import java.awt.BorderLayout;
|
|
||||||
import java.awt.Desktop;
|
|
||||||
import java.awt.Dimension;
|
|
||||||
import java.awt.Font;
|
|
||||||
import java.awt.event.WindowAdapter;
|
|
||||||
import java.awt.event.WindowEvent;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.net.URISyntaxException;
|
|
||||||
import java.net.URL;
|
|
||||||
import java.util.concurrent.CompletableFuture;
|
|
||||||
|
|
||||||
import javax.swing.JButton;
|
|
||||||
import javax.swing.JFrame;
|
|
||||||
import javax.swing.JLabel;
|
|
||||||
import javax.swing.JPanel;
|
|
||||||
import javax.swing.JScrollPane;
|
|
||||||
import javax.swing.JTextPane;
|
|
||||||
import javax.swing.UIManager;
|
|
||||||
import javax.swing.UnsupportedLookAndFeelException;
|
|
||||||
import javax.swing.text.AttributeSet;
|
|
||||||
import javax.swing.text.BadLocationException;
|
|
||||||
import javax.swing.text.DefaultCaret;
|
|
||||||
import javax.swing.text.Document;
|
|
||||||
import javax.swing.text.SimpleAttributeSet;
|
|
||||||
import javax.swing.text.StyleConstants;
|
|
||||||
import javax.swing.text.StyleContext;
|
|
||||||
|
|
||||||
import org.apache.logging.log4j.LogManager;
|
|
||||||
import org.apache.logging.log4j.Logger;
|
|
||||||
|
|
||||||
import entralinked.Entralinked;
|
|
||||||
import entralinked.utility.ConsumerAppender;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Simple Swing user interface.
|
|
||||||
*/
|
|
||||||
public class MainView {
|
|
||||||
|
|
||||||
private static Logger logger = LogManager.getLogger();
|
|
||||||
private final StyleContext styleContext = StyleContext.getDefaultStyleContext();
|
|
||||||
private final AttributeSet fontAttribute = styleContext.addAttribute(SimpleAttributeSet.EMPTY, StyleConstants.FontFamily, "Consolas");
|
|
||||||
private final JButton dashboardButton;
|
|
||||||
private final JLabel statusLabel;
|
|
||||||
|
|
||||||
public MainView(Entralinked entralinked) {
|
|
||||||
// Try set Look and Feel
|
|
||||||
try {
|
|
||||||
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
|
|
||||||
} catch (ReflectiveOperationException | UnsupportedLookAndFeelException e) {
|
|
||||||
logger.error("Could not set Look and Feel", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create dashboard button
|
|
||||||
dashboardButton = new JButton("Open User Dashboard");
|
|
||||||
dashboardButton.setEnabled(false);
|
|
||||||
dashboardButton.setFocusable(false);
|
|
||||||
dashboardButton.addActionListener(event -> {
|
|
||||||
openUrl("http://127.0.0.1/dashboard/profile.html");
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create status label
|
|
||||||
statusLabel = new JLabel("Entralinked is starting...", JLabel.CENTER);
|
|
||||||
|
|
||||||
// Create footer panel
|
|
||||||
JPanel footerPanel = new JPanel(new BorderLayout());
|
|
||||||
footerPanel.add(statusLabel, BorderLayout.CENTER);
|
|
||||||
footerPanel.add(dashboardButton, BorderLayout.LINE_END);
|
|
||||||
|
|
||||||
// Create console output
|
|
||||||
JTextPane consoleOutputPane = new JTextPane() {
|
|
||||||
@Override
|
|
||||||
public boolean getScrollableTracksViewportWidth() {
|
|
||||||
return getPreferredSize().width <= getParent().getSize().width;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Dimension getPreferredSize() {
|
|
||||||
return getUI().getPreferredSize(this);
|
|
||||||
};
|
|
||||||
};
|
|
||||||
consoleOutputPane.setFont(new Font("Consola", Font.PLAIN, 12));
|
|
||||||
consoleOutputPane.setEditable(false);
|
|
||||||
((DefaultCaret)consoleOutputPane.getCaret()).setUpdatePolicy(DefaultCaret.ALWAYS_UPDATE);
|
|
||||||
|
|
||||||
// Create console output appender
|
|
||||||
ConsumerAppender.addConsumer("GuiOutput", message -> {
|
|
||||||
Document document = consoleOutputPane.getDocument();
|
|
||||||
try {
|
|
||||||
consoleOutputPane.getDocument().insertString(document.getLength(), message, fontAttribute);
|
|
||||||
} catch(BadLocationException e) {}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create console output scroll pane
|
|
||||||
JScrollPane scrollPane = new JScrollPane();
|
|
||||||
scrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS);
|
|
||||||
scrollPane.setViewportView(consoleOutputPane);
|
|
||||||
|
|
||||||
// Create main panel
|
|
||||||
JPanel panel = new JPanel(new BorderLayout());
|
|
||||||
panel.add(scrollPane, BorderLayout.CENTER);
|
|
||||||
panel.add(footerPanel, BorderLayout.PAGE_END);
|
|
||||||
|
|
||||||
// Create window
|
|
||||||
JFrame frame = new JFrame("Entralinked");
|
|
||||||
frame.addWindowListener(new WindowAdapter() {
|
|
||||||
@Override
|
|
||||||
public void windowClosing(WindowEvent event) {
|
|
||||||
// Update status
|
|
||||||
dashboardButton.setEnabled(false);
|
|
||||||
statusLabel.setText("Entralinked is shutting down ...");
|
|
||||||
|
|
||||||
// Run asynchronously so it doesn't just awkwardly freeze
|
|
||||||
// Still scuffed but better than nothing I guess
|
|
||||||
CompletableFuture.runAsync(() -> {
|
|
||||||
entralinked.stopServers();
|
|
||||||
System.exit(0);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
frame.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
|
|
||||||
frame.setMinimumSize(new Dimension(512, 288));
|
|
||||||
frame.add(panel);
|
|
||||||
frame.pack();
|
|
||||||
frame.setLocationRelativeTo(null);
|
|
||||||
frame.setVisible(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setDashboardButtonEnabled(boolean enabled) {
|
|
||||||
dashboardButton.setEnabled(enabled);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public void setStatusLabelText(String text) {
|
|
||||||
statusLabel.setText(text);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void openUrl(String url) {
|
|
||||||
Desktop desktop = Desktop.getDesktop();
|
|
||||||
|
|
||||||
if(desktop != null && desktop.isSupported(Desktop.Action.BROWSE)) {
|
|
||||||
try {
|
|
||||||
desktop.browse(new URL(url).toURI());
|
|
||||||
} catch(IOException | URISyntaxException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
38
src/main/java/entralinked/gui/ModelListCellRenderer.java
Normal file
38
src/main/java/entralinked/gui/ModelListCellRenderer.java
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
package entralinked.gui;
|
||||||
|
|
||||||
|
import java.awt.Component;
|
||||||
|
import java.awt.Font;
|
||||||
|
import java.util.function.Function;
|
||||||
|
|
||||||
|
import javax.swing.DefaultListCellRenderer;
|
||||||
|
import javax.swing.JList;
|
||||||
|
|
||||||
|
import entralinked.utility.SwingUtility;
|
||||||
|
|
||||||
|
@SuppressWarnings({"serial", "unchecked"})
|
||||||
|
public class ModelListCellRenderer<T> extends DefaultListCellRenderer {
|
||||||
|
|
||||||
|
private final Class<T> type;
|
||||||
|
private final Function<T, String> textSupplier;
|
||||||
|
private final String nullValue;
|
||||||
|
|
||||||
|
public ModelListCellRenderer(Class<T> type, Function<T, String> textSupplier, String nullValue) {
|
||||||
|
this.type = type;
|
||||||
|
this.textSupplier = textSupplier;
|
||||||
|
this.nullValue = nullValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Component getListCellRendererComponent(JList<?> list, Object value, int index, boolean isSelected, boolean cellHasFocus) {
|
||||||
|
Component component = super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
|
||||||
|
setText(value == null ? String.valueOf(nullValue) : type.isAssignableFrom(value.getClass()) ? textSupplier.apply((T)value) : String.valueOf(value));
|
||||||
|
Font font = component.getFont();
|
||||||
|
String text = getText();
|
||||||
|
|
||||||
|
if(font.canDisplayUpTo(text) != -1) {
|
||||||
|
component.setFont(SwingUtility.findSupportingFont(text, font));
|
||||||
|
}
|
||||||
|
|
||||||
|
return component;
|
||||||
|
}
|
||||||
|
}
|
||||||
38
src/main/java/entralinked/gui/ModelTableCellRenderer.java
Normal file
38
src/main/java/entralinked/gui/ModelTableCellRenderer.java
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
package entralinked.gui;
|
||||||
|
|
||||||
|
import java.awt.Component;
|
||||||
|
import java.awt.Font;
|
||||||
|
import java.util.function.Function;
|
||||||
|
|
||||||
|
import javax.swing.JTable;
|
||||||
|
import javax.swing.table.DefaultTableCellRenderer;
|
||||||
|
|
||||||
|
import entralinked.utility.SwingUtility;
|
||||||
|
|
||||||
|
@SuppressWarnings({"serial", "unchecked"})
|
||||||
|
public class ModelTableCellRenderer<T> extends DefaultTableCellRenderer {
|
||||||
|
|
||||||
|
private final Class<T> type;
|
||||||
|
private final Function<T, String> textSupplier;
|
||||||
|
private final String nullValue;
|
||||||
|
|
||||||
|
public ModelTableCellRenderer(Class<T> type, Function<T, String> textSupplier, String nullValue) {
|
||||||
|
this.type = type;
|
||||||
|
this.textSupplier = textSupplier;
|
||||||
|
this.nullValue = nullValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
|
||||||
|
Component component = super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
|
||||||
|
setText(value == null ? String.valueOf(nullValue) : type.isAssignableFrom(value.getClass()) ? textSupplier.apply((T)value) : String.valueOf(value));
|
||||||
|
Font font = component.getFont();
|
||||||
|
String text = getText();
|
||||||
|
|
||||||
|
if(font.canDisplayUpTo(text) != -1) {
|
||||||
|
component.setFont(SwingUtility.findSupportingFont(text, font));
|
||||||
|
}
|
||||||
|
|
||||||
|
return component;
|
||||||
|
}
|
||||||
|
}
|
||||||
40
src/main/java/entralinked/gui/SizeLimitDocumentFilter.java
Normal file
40
src/main/java/entralinked/gui/SizeLimitDocumentFilter.java
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
package entralinked.gui;
|
||||||
|
|
||||||
|
import java.awt.Toolkit;
|
||||||
|
|
||||||
|
import javax.swing.text.AttributeSet;
|
||||||
|
import javax.swing.text.BadLocationException;
|
||||||
|
import javax.swing.text.DocumentFilter;
|
||||||
|
|
||||||
|
public class SizeLimitDocumentFilter extends DocumentFilter {
|
||||||
|
|
||||||
|
private final int limit;
|
||||||
|
|
||||||
|
public SizeLimitDocumentFilter(int limit) {
|
||||||
|
this.limit = limit;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void insertString(FilterBypass fb, int offset, String text, AttributeSet attrs) throws BadLocationException {
|
||||||
|
replace(fb, offset, 0, text, attrs);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void replace(FilterBypass fb, int offset, int length, String text, AttributeSet attrs) throws BadLocationException {
|
||||||
|
if(text == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(limit <= 0) {
|
||||||
|
super.replace(fb, offset, length, text, attrs);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int finalLength = Math.min(text.length(), Math.max(0, limit - fb.getDocument().getLength() + length));
|
||||||
|
super.replace(fb, offset, length, text.substring(0, finalLength), attrs);
|
||||||
|
|
||||||
|
if(finalLength != text.length()) {
|
||||||
|
Toolkit.getDefaultToolkit().beep();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
229
src/main/java/entralinked/gui/component/ConfigTable.java
Normal file
229
src/main/java/entralinked/gui/component/ConfigTable.java
Normal file
@@ -0,0 +1,229 @@
|
|||||||
|
package entralinked.gui.component;
|
||||||
|
|
||||||
|
import java.awt.Component;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Vector;
|
||||||
|
import java.util.function.Function;
|
||||||
|
|
||||||
|
import javax.swing.CellEditor;
|
||||||
|
import javax.swing.DefaultCellEditor;
|
||||||
|
import javax.swing.JComboBox;
|
||||||
|
import javax.swing.JTable;
|
||||||
|
import javax.swing.JTextField;
|
||||||
|
import javax.swing.ListCellRenderer;
|
||||||
|
import javax.swing.table.TableCellEditor;
|
||||||
|
import javax.swing.table.TableCellRenderer;
|
||||||
|
|
||||||
|
import org.jdesktop.swingx.autocomplete.AutoCompleteDecorator;
|
||||||
|
import org.jdesktop.swingx.autocomplete.ComboBoxCellEditor;
|
||||||
|
import org.jdesktop.swingx.autocomplete.ObjectToStringConverter;
|
||||||
|
|
||||||
|
import entralinked.gui.ModelListCellRenderer;
|
||||||
|
import entralinked.gui.ModelTableCellRenderer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TODO look into custom table models because this current implementation is absolutely ass.
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("serial")
|
||||||
|
public class ConfigTable extends JTable {
|
||||||
|
|
||||||
|
private final Map<Integer, TableCellRenderer> tableCellRenderers = new HashMap<>();
|
||||||
|
private final Map<Integer, ListCellRenderer<Object>> listCellRenderers = new HashMap<>();
|
||||||
|
private final Map<Integer, ObjectToStringConverter> converters = new HashMap<>();
|
||||||
|
private TableCellEditor[][] cellEditors;
|
||||||
|
private boolean[][] disabledCells;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public TableCellEditor getCellEditor(int row, int column) {
|
||||||
|
resizeArrays();
|
||||||
|
return cellEditors[row][column];
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public TableCellRenderer getCellRenderer(int row, int column) {
|
||||||
|
return tableCellRenderers.getOrDefault(column, super.getCellRenderer(row, column));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isCellEditable(int row, int column) {
|
||||||
|
resizeArrays();
|
||||||
|
return !disabledCells[row][column] && cellEditors[row][column] != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCellEditable(int row, int column, boolean editable) {
|
||||||
|
resizeArrays();
|
||||||
|
disabledCells[row][column] = !editable;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCellEditor(int row, int column, TableCellEditor editor) {
|
||||||
|
resizeArrays();
|
||||||
|
cellEditors[row][column] = editor;
|
||||||
|
}
|
||||||
|
|
||||||
|
public <T> void setCellRenderers(int column, Class<T> type, Function<T, String> supplier) {
|
||||||
|
setCellRenderers(column, type, supplier, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public <T> void setCellRenderers(int column, Class<T> type, Function<T, String> supplier, String nullValue) {
|
||||||
|
tableCellRenderers.put(column, new ModelTableCellRenderer<T>(type, supplier, nullValue));
|
||||||
|
listCellRenderers.put(column, new ModelListCellRenderer<T>(type, supplier, nullValue));
|
||||||
|
converters.put(column, new ObjectToStringConverter() {
|
||||||
|
@Override
|
||||||
|
public String getPreferredStringForItem(Object item) {
|
||||||
|
return (item == null || !type.isAssignableFrom(item.getClass())) ? null : supplier.apply((T)item);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void enableOption(int row, int column) {
|
||||||
|
if(isCellEditable(row, column)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Object initialValue = "N/A";
|
||||||
|
CellEditor editor = getCellEditor(row, column);
|
||||||
|
|
||||||
|
if(editor instanceof DefaultCellEditor) {
|
||||||
|
Component component = ((DefaultCellEditor)editor).getComponent();
|
||||||
|
|
||||||
|
if(component instanceof JComboBox) {
|
||||||
|
JComboBox<?> comboBox = (JComboBox<?>)component;
|
||||||
|
|
||||||
|
if(comboBox.getItemCount() > 0) {
|
||||||
|
initialValue = comboBox.getItemAt(0);
|
||||||
|
}
|
||||||
|
} else if(component instanceof JTextField) {
|
||||||
|
initialValue = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enableOption(row, column, initialValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void enableOption(int row, int column, Object initialValue) {
|
||||||
|
if(isCellEditable(row, column)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
getModel().setValueAt(initialValue, row, column);
|
||||||
|
setCellEditable(row, column, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void disableOption(int row, int column) {
|
||||||
|
setCellEditable(row, column, false);
|
||||||
|
getModel().setValueAt("", row, column);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOptionsAt(int row, int column, Collection<?> options) {
|
||||||
|
setOptionsAt(row, column, options, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOptionsAt(int row, int column, Collection<?> options, boolean includeNullOption) {
|
||||||
|
if(options.isEmpty()) {
|
||||||
|
setCellEditor(row, column, null);
|
||||||
|
getModel().setValueAt("N/A", row, column);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Vector<?> vector = new Vector<>(options);
|
||||||
|
|
||||||
|
if(includeNullOption) {
|
||||||
|
vector.add(0, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
Object currentValue = getValueAt(row, column);
|
||||||
|
|
||||||
|
if(currentValue == null || !vector.contains(currentValue)) {
|
||||||
|
currentValue = vector.firstElement();
|
||||||
|
getModel().setValueAt(currentValue, row, column);
|
||||||
|
}
|
||||||
|
|
||||||
|
JComboBox<?> comboBox = new JComboBox<>(vector);
|
||||||
|
AutoCompleteDecorator.decorate(comboBox, converters.getOrDefault(column, ObjectToStringConverter.DEFAULT_IMPLEMENTATION));
|
||||||
|
|
||||||
|
if(listCellRenderers.containsKey(column)) {
|
||||||
|
comboBox.setRenderer(listCellRenderers.get(column));
|
||||||
|
}
|
||||||
|
|
||||||
|
setCellEditor(row, column, new ComboBoxCellEditor(comboBox));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void randomizeSelection(int row, int column) {
|
||||||
|
randomizeSelection(row, column, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void randomizeSelection(int row, int column, boolean allowNull) {
|
||||||
|
CellEditor editor = getCellEditor(row, column);
|
||||||
|
|
||||||
|
if(!(editor instanceof DefaultCellEditor)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Component component = ((DefaultCellEditor)editor).getComponent();
|
||||||
|
|
||||||
|
if(component instanceof JComboBox) {
|
||||||
|
JComboBox<?> comboBox = (JComboBox<?>)component;
|
||||||
|
int count = comboBox.getItemCount();
|
||||||
|
|
||||||
|
if(count <= 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int index = (int)(Math.random() * count);
|
||||||
|
Object item = comboBox.getItemAt(index);
|
||||||
|
|
||||||
|
if(!allowNull) {
|
||||||
|
// TODO potential infinite loop
|
||||||
|
while(item == null) {
|
||||||
|
item = comboBox.getItemAt((int)(Math.random() * count));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getModel().setValueAt(item, row, column);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public <T> T getValueAt(int row, int column, Class<T> type) {
|
||||||
|
return getValueAt(row, column, type, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public <T> T getValueAt(int row, int column, Class<T> type, T def) {
|
||||||
|
Object value = getValueAt(row, column);
|
||||||
|
|
||||||
|
if(value != null && type.isAssignableFrom(value.getClass())) {
|
||||||
|
return (T)value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return def;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void resizeArrays() {
|
||||||
|
int rows = getRowCount();
|
||||||
|
int columns = getColumnCount();
|
||||||
|
|
||||||
|
if(cellEditors == null) {
|
||||||
|
cellEditors = new TableCellEditor[rows][columns];
|
||||||
|
disabledCells = new boolean[rows][columns];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check row size
|
||||||
|
if(rows != cellEditors.length) {
|
||||||
|
cellEditors = Arrays.copyOf(cellEditors, rows);
|
||||||
|
disabledCells = Arrays.copyOf(disabledCells, rows);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check column size
|
||||||
|
if(rows > 0 && cellEditors[0].length != columns) {
|
||||||
|
for(int i = 0; i < rows; i++) {
|
||||||
|
cellEditors[i] = Arrays.copyOf(cellEditors[i], columns);
|
||||||
|
disabledCells[i] = Arrays.copyOf(disabledCells[i], columns);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
90
src/main/java/entralinked/gui/component/PropertyDisplay.java
Normal file
90
src/main/java/entralinked/gui/component/PropertyDisplay.java
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
package entralinked.gui.component;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
import javax.swing.JLabel;
|
||||||
|
import javax.swing.JPanel;
|
||||||
|
import javax.swing.JPasswordField;
|
||||||
|
import javax.swing.JTextField;
|
||||||
|
|
||||||
|
import com.formdev.flatlaf.FlatClientProperties;
|
||||||
|
|
||||||
|
import net.miginfocom.swing.MigLayout;
|
||||||
|
|
||||||
|
@SuppressWarnings("serial")
|
||||||
|
public class PropertyDisplay extends JPanel {
|
||||||
|
|
||||||
|
private final Map<Integer, String> keys = new HashMap<>();
|
||||||
|
private final Map<String, JLabel> labels = new HashMap<>();
|
||||||
|
private final Map<String, JTextField> fields = new HashMap<>();
|
||||||
|
|
||||||
|
public PropertyDisplay() {
|
||||||
|
setLayout(new MigLayout("insets 0, fill"));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addProperty(int x, int y, String key, String label) {
|
||||||
|
addProperty(x, y, key, label, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addProperty(int x, int y, String key, String label, boolean hidden) {
|
||||||
|
int coordHash = Objects.hash(x, y);
|
||||||
|
String existingKey = keys.get(coordHash);
|
||||||
|
|
||||||
|
if(existingKey != null) {
|
||||||
|
removeProperty(existingKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
removeProperty(key);
|
||||||
|
String labelConstraints = "cell %s %s, sg proplabel";
|
||||||
|
|
||||||
|
if(x > 0) {
|
||||||
|
labelConstraints += ", gapx 4";
|
||||||
|
}
|
||||||
|
|
||||||
|
JLabel labelComponent = new JLabel(label);
|
||||||
|
labels.put(key, labelComponent);
|
||||||
|
add(labelComponent, labelConstraints.formatted(x * 2, y));
|
||||||
|
add(createValueField(key, hidden), "cell %s %s, sg propfield, width 96".formatted(x * 2 + 1, y));
|
||||||
|
keys.put(coordHash, key);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean removeProperty(String key) {
|
||||||
|
if(!labels.containsKey(key)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
remove(labels.remove(key));
|
||||||
|
remove(fields.remove(key));
|
||||||
|
keys.values().remove(key);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setValue(String key, Object value) {
|
||||||
|
JTextField textField = fields.get(key);
|
||||||
|
|
||||||
|
if(textField != null) {
|
||||||
|
textField.setText(String.valueOf(value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private JTextField createValueField(String key, boolean hidden) {
|
||||||
|
JTextField textField = null;
|
||||||
|
|
||||||
|
if(!hidden) {
|
||||||
|
textField = new JTextField();
|
||||||
|
} else {
|
||||||
|
textField = new JPasswordField();
|
||||||
|
textField.putClientProperty(FlatClientProperties.STYLE, "showRevealButton: true; showCapsLock: false");
|
||||||
|
}
|
||||||
|
|
||||||
|
textField.setEditable(false);
|
||||||
|
fields.put(key, textField);
|
||||||
|
return textField;
|
||||||
|
}
|
||||||
|
|
||||||
|
public JTextField getValueField(String key) {
|
||||||
|
return fields.get(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
85
src/main/java/entralinked/gui/component/ShadowedSprite.java
Normal file
85
src/main/java/entralinked/gui/component/ShadowedSprite.java
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
package entralinked.gui.component;
|
||||||
|
|
||||||
|
import java.awt.Dimension;
|
||||||
|
import java.awt.Graphics;
|
||||||
|
import java.awt.image.BufferedImage;
|
||||||
|
|
||||||
|
import javax.swing.JComponent;
|
||||||
|
|
||||||
|
@SuppressWarnings("serial")
|
||||||
|
public class ShadowedSprite extends JComponent {
|
||||||
|
|
||||||
|
public static final int OFFSET_X = 4;
|
||||||
|
public static final int OFFSET_Y = 2;
|
||||||
|
private BufferedImage image;
|
||||||
|
private BufferedImage shadowImage;
|
||||||
|
private int scale;
|
||||||
|
|
||||||
|
public ShadowedSprite() {
|
||||||
|
setOpaque(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void paintComponent(Graphics graphics) {
|
||||||
|
super.paintComponent(graphics);
|
||||||
|
|
||||||
|
if(image == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Dimension size = getSize();
|
||||||
|
int width = image.getWidth() * scale;
|
||||||
|
int height = image.getHeight() * scale;
|
||||||
|
int x = size.width / 2 - width / 2 - OFFSET_X / 2;
|
||||||
|
int y = size.height / 2 - height / 2 - OFFSET_Y / 2;
|
||||||
|
graphics.drawImage(shadowImage, x + OFFSET_X, y + OFFSET_Y, width, height, null);
|
||||||
|
graphics.drawImage(image, x, y, width, height, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void imageChanged() {
|
||||||
|
if(image == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int width = image.getWidth();
|
||||||
|
int height = image.getHeight();
|
||||||
|
shadowImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
|
||||||
|
|
||||||
|
for(int i = 0; i < image.getWidth(); i++) {
|
||||||
|
for(int j = 0; j < image.getHeight(); j++) {
|
||||||
|
shadowImage.setRGB(i, j, image.getRGB(i, j) & 0x7F000000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateSize(int width, int height) {
|
||||||
|
Dimension oldSize = getPreferredSize();
|
||||||
|
Dimension size = new Dimension(width, height);
|
||||||
|
setMinimumSize(size);
|
||||||
|
setPreferredSize(size);
|
||||||
|
|
||||||
|
if(oldSize.width != width || oldSize.height != height) {
|
||||||
|
revalidate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setImage(BufferedImage image) {
|
||||||
|
setImage(image, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setImage(BufferedImage image, int width, int height) {
|
||||||
|
setImage(image, 1, width, height);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setImage(BufferedImage image, int scale) {
|
||||||
|
setImage(image, scale, image == null ? 0 : image.getWidth(), image == null ? 0 : image.getHeight());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setImage(BufferedImage image, int scale, int width, int height) {
|
||||||
|
this.image = image;
|
||||||
|
this.scale = scale;
|
||||||
|
imageChanged();
|
||||||
|
updateSize(width + OFFSET_X, height + OFFSET_Y);
|
||||||
|
repaint();
|
||||||
|
}
|
||||||
|
}
|
||||||
16
src/main/java/entralinked/gui/data/Country.java
Normal file
16
src/main/java/entralinked/gui/data/Country.java
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
package entralinked.gui.data;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
|
||||||
|
public record Country(
|
||||||
|
@JsonProperty(required = true) int id,
|
||||||
|
@JsonProperty(required = true) String name,
|
||||||
|
List<Region> regions) {
|
||||||
|
|
||||||
|
@JsonProperty
|
||||||
|
public boolean hasRegions() {
|
||||||
|
return regions != null && !regions.isEmpty();
|
||||||
|
}
|
||||||
|
}
|
||||||
194
src/main/java/entralinked/gui/data/DataManager.java
Normal file
194
src/main/java/entralinked/gui/data/DataManager.java
Normal file
@@ -0,0 +1,194 @@
|
|||||||
|
package entralinked.gui.data;
|
||||||
|
|
||||||
|
import java.awt.image.BufferedImage;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.type.TypeReference;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
|
||||||
|
import entralinked.GameVersion;
|
||||||
|
import entralinked.gui.ImageLoader;
|
||||||
|
import entralinked.model.pkmn.PkmnGender;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TODO name is too generic & model management is a bit confusing.
|
||||||
|
*/
|
||||||
|
public class DataManager {
|
||||||
|
|
||||||
|
private static final Map<Integer, String> abilities = new HashMap<>();
|
||||||
|
private static final Map<Integer, String> items = new HashMap<>();
|
||||||
|
private static final Map<Integer, String> moves = new HashMap<>();
|
||||||
|
private static final Map<Integer, PkmnSpecies> species = new HashMap<>();
|
||||||
|
private static final Map<Integer, Country> countries = new HashMap<>();
|
||||||
|
private static final Map<String, DreamWorldArea> dreamWorldAreas = new HashMap<>();
|
||||||
|
private static final Map<GameVersion, List<Encounter>> encounterCache = new HashMap<>();
|
||||||
|
|
||||||
|
public static void clearData() {
|
||||||
|
abilities.clear();
|
||||||
|
items.clear();
|
||||||
|
moves.clear();
|
||||||
|
species.clear();
|
||||||
|
countries.clear();
|
||||||
|
dreamWorldAreas.clear();
|
||||||
|
encounterCache.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void loadData() throws IOException {
|
||||||
|
clearData();
|
||||||
|
ObjectMapper mapper = new ObjectMapper();
|
||||||
|
abilities.putAll(mapper.readValue(DataManager.class.getResource("/data/abilities.json"), new TypeReference<Map<Integer, String>>(){}));
|
||||||
|
items.putAll(mapper.readValue(DataManager.class.getResource("/data/items.json"), new TypeReference<Map<Integer, String>>(){}));
|
||||||
|
moves.putAll(mapper.readValue(DataManager.class.getResource("/data/moves.json"), new TypeReference<Map<Integer, String>>(){}));
|
||||||
|
species.putAll(mapper.readValue(DataManager.class.getResource("/data/species.json"), new TypeReference<Map<Integer, PkmnSpecies>>(){}));
|
||||||
|
countries.putAll(mapper.readValue(DataManager.class.getResource("/data/countries.json"), new TypeReference<Map<Integer, Country>>(){}));
|
||||||
|
dreamWorldAreas.putAll(mapper.readValue(DataManager.class.getResource("/data/legality.json"), new TypeReference<Map<String, DreamWorldArea>>(){}));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static BufferedImage getPokemonSprite(int species) {
|
||||||
|
return getPokemonSprite(0, 0, false, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static BufferedImage getPokemonSprite(PkmnSpecies species, int form, PkmnGender gender, boolean shiny) {
|
||||||
|
return getPokemonSprite(species, form, gender == PkmnGender.FEMALE, shiny);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static BufferedImage getPokemonSprite(int species, int form, PkmnGender gender, boolean shiny) {
|
||||||
|
return getPokemonSprite(species, form, gender == PkmnGender.FEMALE, shiny);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static BufferedImage getPokemonSprite(int species, int form, boolean female, boolean shiny) {
|
||||||
|
return getPokemonSprite(getSpecies(species), form, female, shiny);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static BufferedImage getPokemonSprite(PkmnSpecies species, int form, boolean female, boolean shiny) {
|
||||||
|
String path = "/sprites/pokemon/normal/0.png";
|
||||||
|
|
||||||
|
if(species != null) {
|
||||||
|
path = "/sprites/pokemon/%s/%s%s%s.png".formatted(
|
||||||
|
shiny ? "shiny" : "normal",
|
||||||
|
species.hasFemaleSprite() && female ? "female/" : "",
|
||||||
|
species.id(), form == 0 ? "" : "-" + form);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ImageLoader.getImage(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static BufferedImage getItemSprite(int item) {
|
||||||
|
return ImageLoader.getImage("/sprites/items/%s.png".formatted(item));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getAbilityName(int id) {
|
||||||
|
return abilities.getOrDefault(id, "Unknown (#%s)".formatted(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Set<Integer> getAbilityIds() {
|
||||||
|
return Collections.unmodifiableSet(abilities.keySet());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getItemName(int id) {
|
||||||
|
return items.getOrDefault(id, "Unknown (#%s)".formatted(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Set<Integer> getItemIds() {
|
||||||
|
return Collections.unmodifiableSet(items.keySet());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getMoveName(int id) {
|
||||||
|
return moves.getOrDefault(id, "Unknown (#%s)".formatted(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Set<Integer> getMoveIds() {
|
||||||
|
return Collections.unmodifiableSet(moves.keySet());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static PkmnSpecies getSpecies(int id) {
|
||||||
|
return species.get(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Set<Integer> getSpeciesIds() {
|
||||||
|
return Collections.unmodifiableSet(species.keySet());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Collection<PkmnSpecies> getSpecies() {
|
||||||
|
return Collections.unmodifiableCollection(species.values());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Country getCountry(int id) {
|
||||||
|
return countries.get(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Collection<Country> getCountries() {
|
||||||
|
return Collections.unmodifiableCollection(countries.values());
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO functions might be computationally expensive
|
||||||
|
|
||||||
|
private static List<Encounter> getEncounters(GameVersion gameVersion) {
|
||||||
|
return encounterCache.computeIfAbsent(gameVersion, version -> dreamWorldAreas.values().stream()
|
||||||
|
.map(DreamWorldArea::encounters)
|
||||||
|
.flatMap(List::stream)
|
||||||
|
.filter(x -> x.versionMask() == 0 || version.checkMask(x.versionMask()))
|
||||||
|
.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<PkmnSpecies> getDownloadableSpecies(GameVersion gameVersion) {
|
||||||
|
return species.values().stream()
|
||||||
|
.filter(x -> x.downloadable() && (gameVersion.isVersion2() || x.id() <= 493))
|
||||||
|
.collect(Collectors.toCollection(ArrayList::new));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<PkmnSpecies> getSpeciesOptions(GameVersion gameVersion) {
|
||||||
|
return getEncounters(gameVersion).stream()
|
||||||
|
.map(x -> species.get(x.species()))
|
||||||
|
.distinct()
|
||||||
|
.collect(Collectors.toCollection(ArrayList::new));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<PkmnGender> getGenderOptions(GameVersion gameVersion, PkmnSpecies species) {
|
||||||
|
return getEncounters(gameVersion).stream()
|
||||||
|
.filter(x -> x.species() == species.id())
|
||||||
|
.map(x -> x.isGenderLocked() ? List.of(x.gender()) : species.getGenders())
|
||||||
|
.flatMap(List::stream)
|
||||||
|
.distinct()
|
||||||
|
.sorted((a, b) -> Integer.compare(a.ordinal(), b.ordinal()))
|
||||||
|
.collect(Collectors.toCollection(ArrayList::new));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<PkmnForm> getFormOptions(GameVersion gameVersion, PkmnSpecies species, PkmnGender gender) {
|
||||||
|
return getEncounters(gameVersion).stream()
|
||||||
|
.filter(x -> x.species() == species.id() && (!x.isGenderLocked() || x.gender() == gender))
|
||||||
|
.flatMap(x -> species.hasForms() ? Stream.of(species.forms()).filter(form -> x.hasForm(form.id())) : Stream.empty())
|
||||||
|
.distinct()
|
||||||
|
.collect(Collectors.toCollection(ArrayList::new));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<Integer> getMoveOptions(GameVersion gameVersion, PkmnSpecies species, PkmnGender gender, int form) {
|
||||||
|
return getEncounters(gameVersion).stream()
|
||||||
|
.filter(x -> x.species() == species.id() && (!x.isGenderLocked() || x.gender() == gender) && x.hasForm(form))
|
||||||
|
.map(Encounter::moves)
|
||||||
|
.flatMap(List::stream)
|
||||||
|
.distinct()
|
||||||
|
.collect(Collectors.toCollection(ArrayList::new));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<Integer> getDownloadableItems(GameVersion gameVersion) {
|
||||||
|
return items.keySet().stream().filter(x -> gameVersion.isVersion2() || x <= 626).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<Integer> getItemOptions() {
|
||||||
|
return dreamWorldAreas.values().stream()
|
||||||
|
.map(DreamWorldArea::items)
|
||||||
|
.flatMap(List::stream)
|
||||||
|
.distinct()
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
}
|
||||||
9
src/main/java/entralinked/gui/data/DreamWorldArea.java
Normal file
9
src/main/java/entralinked/gui/data/DreamWorldArea.java
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
package entralinked.gui.data;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
|
||||||
|
public record DreamWorldArea(
|
||||||
|
@JsonProperty(required = true) List<Encounter> encounters,
|
||||||
|
@JsonProperty(required = true) List<Integer> items) {}
|
||||||
30
src/main/java/entralinked/gui/data/Encounter.java
Normal file
30
src/main/java/entralinked/gui/data/Encounter.java
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
package entralinked.gui.data;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
|
||||||
|
import entralinked.model.pkmn.PkmnGender;
|
||||||
|
|
||||||
|
public record Encounter(
|
||||||
|
@JsonProperty(required = true) int species,
|
||||||
|
@JsonProperty(required = true) List<Integer> moves,
|
||||||
|
PkmnGender gender, int formMask, int versionMask) {
|
||||||
|
|
||||||
|
@JsonIgnore
|
||||||
|
public boolean isGenderLocked() {
|
||||||
|
return gender != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonIgnore
|
||||||
|
public boolean isFormLocked() {
|
||||||
|
return formMask != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonIgnore
|
||||||
|
public boolean hasForm(int form) {
|
||||||
|
int bits = 1 << form;
|
||||||
|
return !isFormLocked() || (bits & formMask) == bits;
|
||||||
|
}
|
||||||
|
}
|
||||||
7
src/main/java/entralinked/gui/data/PkmnForm.java
Normal file
7
src/main/java/entralinked/gui/data/PkmnForm.java
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
package entralinked.gui.data;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
|
||||||
|
public record PkmnForm(
|
||||||
|
@JsonProperty(required = true) int id,
|
||||||
|
@JsonProperty(required = true) String name) {}
|
||||||
29
src/main/java/entralinked/gui/data/PkmnSpecies.java
Normal file
29
src/main/java/entralinked/gui/data/PkmnSpecies.java
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
package entralinked.gui.data;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
|
||||||
|
import entralinked.model.pkmn.PkmnGender;
|
||||||
|
|
||||||
|
public record PkmnSpecies(
|
||||||
|
@JsonProperty(required = true) int id,
|
||||||
|
@JsonProperty(required = true) String name,
|
||||||
|
boolean downloadable, boolean hasFemaleSprite, PkmnGender gender, PkmnForm[] forms) {
|
||||||
|
|
||||||
|
@JsonIgnore
|
||||||
|
public boolean isSingleGender() {
|
||||||
|
return gender != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonIgnore
|
||||||
|
public boolean hasForms() {
|
||||||
|
return forms != null && forms.length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonIgnore
|
||||||
|
public List<PkmnGender> getGenders() {
|
||||||
|
return isSingleGender() ? List.of(gender) : List.of(PkmnGender.MALE, PkmnGender.FEMALE);
|
||||||
|
}
|
||||||
|
}
|
||||||
7
src/main/java/entralinked/gui/data/Region.java
Normal file
7
src/main/java/entralinked/gui/data/Region.java
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
package entralinked.gui.data;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
|
||||||
|
public record Region(
|
||||||
|
@JsonProperty(required = true) int id,
|
||||||
|
@JsonProperty(required = true) String name) {}
|
||||||
381
src/main/java/entralinked/gui/panels/CustomizationPanel.java
Normal file
381
src/main/java/entralinked/gui/panels/CustomizationPanel.java
Normal file
@@ -0,0 +1,381 @@
|
|||||||
|
package entralinked.gui.panels;
|
||||||
|
|
||||||
|
import java.awt.Dimension;
|
||||||
|
import java.awt.Image;
|
||||||
|
import java.awt.image.BufferedImage;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Vector;
|
||||||
|
|
||||||
|
import javax.imageio.ImageIO;
|
||||||
|
import javax.swing.BorderFactory;
|
||||||
|
import javax.swing.DefaultComboBoxModel;
|
||||||
|
import javax.swing.ImageIcon;
|
||||||
|
import javax.swing.JButton;
|
||||||
|
import javax.swing.JComboBox;
|
||||||
|
import javax.swing.JLabel;
|
||||||
|
import javax.swing.JOptionPane;
|
||||||
|
import javax.swing.JPanel;
|
||||||
|
import javax.swing.filechooser.FileFilter;
|
||||||
|
import javax.swing.filechooser.FileNameExtensionFilter;
|
||||||
|
|
||||||
|
import entralinked.Entralinked;
|
||||||
|
import entralinked.GameVersion;
|
||||||
|
import entralinked.gui.FileChooser;
|
||||||
|
import entralinked.gui.ModelListCellRenderer;
|
||||||
|
import entralinked.model.player.Player;
|
||||||
|
import entralinked.utility.Crc16;
|
||||||
|
import entralinked.utility.LEOutputStream;
|
||||||
|
import entralinked.utility.SwingUtility;
|
||||||
|
import entralinked.utility.TiledImageUtility;
|
||||||
|
import net.miginfocom.swing.MigLayout;
|
||||||
|
|
||||||
|
@SuppressWarnings("serial")
|
||||||
|
public class CustomizationPanel extends JPanel {
|
||||||
|
|
||||||
|
@FunctionalInterface
|
||||||
|
private static interface SkinWriter {
|
||||||
|
|
||||||
|
public void writeSkin(OutputStream outputStream, BufferedImage image) throws IOException;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal model for combo boxes.
|
||||||
|
*/
|
||||||
|
private static record DlcOption(String type, String name, String path, boolean custom) {
|
||||||
|
|
||||||
|
public DlcOption(String type, String name, String path) {
|
||||||
|
this(type, name, path, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final FileFilter IMAGE_FILE_FILTER = new FileNameExtensionFilter("Image Files (*.png)", "png");
|
||||||
|
private static final FileFilter CGEAR_FILE_FILTER = new FileNameExtensionFilter("C-Gear Skin Files (*.bin, *.cgb, *.psk)", "bin", "cgb", "psk");
|
||||||
|
private static final FileFilter ZUKAN_FILE_FILTER = new FileNameExtensionFilter("Pokédex Skin Files (*.bin, *.pds)", "bin", "pds");
|
||||||
|
private static final byte[] NARC_HEADER = { 0x4E, 0x41, 0x52, 0x43, (byte)0xFE, (byte)0xFF, 0x00, 0x01 };
|
||||||
|
private static final Map<String, Image> skinCache = new HashMap<>();
|
||||||
|
private final Entralinked entralinked;
|
||||||
|
private final JComboBox<DlcOption> cgearComboBox;
|
||||||
|
private final JComboBox<DlcOption> zukanComboBox;
|
||||||
|
private final JComboBox<DlcOption> musicalComboBox;
|
||||||
|
private final JPanel optionPanel;
|
||||||
|
private Player player;
|
||||||
|
private GameVersion gameVersion;
|
||||||
|
private DlcOption customCGearSkin;
|
||||||
|
private DlcOption customDexSkin;
|
||||||
|
private DlcOption customMusical;
|
||||||
|
|
||||||
|
public CustomizationPanel(Entralinked entralinked) {
|
||||||
|
this.entralinked = entralinked;
|
||||||
|
setLayout(new MigLayout("align 50% 50%"));
|
||||||
|
|
||||||
|
// Create preview labels
|
||||||
|
JLabel cgearPreviewLabel = new JLabel("No preview available.", JLabel.CENTER);
|
||||||
|
cgearPreviewLabel.setPreferredSize(new Dimension(TiledImageUtility.SCREEN_WIDTH, TiledImageUtility.SCREEN_HEIGHT));
|
||||||
|
JLabel dexPreviewLabel = new JLabel("No preview available.", JLabel.CENTER);
|
||||||
|
dexPreviewLabel.setPreferredSize(new Dimension(TiledImageUtility.SCREEN_WIDTH, TiledImageUtility.SCREEN_HEIGHT));
|
||||||
|
|
||||||
|
// Create preview image panels
|
||||||
|
// Labels are added to a subpanel first otherwise the preferred size will include the border which causes issues
|
||||||
|
JPanel cgearPreviewPanel = new JPanel(new MigLayout("insets 0"));
|
||||||
|
cgearPreviewPanel.setBorder(BorderFactory.createTitledBorder("C-Gear Skin Preview"));
|
||||||
|
cgearPreviewPanel.add(cgearPreviewLabel);
|
||||||
|
JPanel dexPreviewPanel = new JPanel(new MigLayout("insets 0"));
|
||||||
|
dexPreviewPanel.setBorder(BorderFactory.createTitledBorder("Pokédex Skin Preview"));
|
||||||
|
dexPreviewPanel.add(dexPreviewLabel);
|
||||||
|
JPanel previewPanel = new JPanel();
|
||||||
|
previewPanel.add(cgearPreviewPanel);
|
||||||
|
previewPanel.add(dexPreviewPanel);
|
||||||
|
add(previewPanel, "spanx, align 50%, wrap");
|
||||||
|
|
||||||
|
// Create combo boxes
|
||||||
|
ModelListCellRenderer<DlcOption> renderer = new ModelListCellRenderer<>(DlcOption.class, DlcOption::name, "Do not change");
|
||||||
|
cgearComboBox = new JComboBox<>();
|
||||||
|
cgearComboBox.setMinimumSize(cgearComboBox.getPreferredSize());
|
||||||
|
cgearComboBox.setRenderer(renderer);
|
||||||
|
cgearComboBox.addActionListener(event -> updateSkinPreview(cgearComboBox, cgearPreviewLabel));
|
||||||
|
zukanComboBox = new JComboBox<>();
|
||||||
|
zukanComboBox.setMinimumSize(zukanComboBox.getPreferredSize());
|
||||||
|
zukanComboBox.setRenderer(renderer);
|
||||||
|
zukanComboBox.addActionListener(event -> updateSkinPreview(zukanComboBox, dexPreviewLabel));
|
||||||
|
musicalComboBox = new JComboBox<>();
|
||||||
|
musicalComboBox.setMinimumSize(musicalComboBox.getPreferredSize());
|
||||||
|
musicalComboBox.setRenderer(renderer);
|
||||||
|
|
||||||
|
// Create option panel
|
||||||
|
optionPanel = new JPanel(new MigLayout());
|
||||||
|
|
||||||
|
// Create C-Gear skin selector
|
||||||
|
createDlcOption("C-Gear Skin", cgearComboBox, () -> {
|
||||||
|
FileChooser.showFileOpenDialog(getRootPane(), Arrays.asList(IMAGE_FILE_FILTER, CGEAR_FILE_FILTER), selection -> {
|
||||||
|
File dst = player.getCGearSkinFile();
|
||||||
|
File file = selection.file();
|
||||||
|
FileFilter filter = selection.filter();
|
||||||
|
|
||||||
|
if(filter == IMAGE_FILE_FILTER) {
|
||||||
|
if(!importSkinImage(file, dst, (stream, image) -> TiledImageUtility.writeCGearSkin(stream, image, !gameVersion.isVersion2()))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else if(filter == CGEAR_FILE_FILTER) {
|
||||||
|
if(!importSkinFile(file, dst, 9730)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
DlcOption option = new DlcOption(gameVersion.isVersion2() ? "CGEAR2" : "CGEAR", file.getName(), dst.getAbsolutePath(), true);
|
||||||
|
updateCustomOption(cgearComboBox, customCGearSkin, option);
|
||||||
|
customCGearSkin = option;
|
||||||
|
player.setCustomCGearSkin(customCGearSkin.name());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create Pokédex skin selector
|
||||||
|
createDlcOption("Pokédex Skin", zukanComboBox, () -> {
|
||||||
|
FileChooser.showFileOpenDialog(getRootPane(), Arrays.asList(IMAGE_FILE_FILTER, ZUKAN_FILE_FILTER), selection -> {
|
||||||
|
File dst = player.getDexSkinFile();
|
||||||
|
File file = selection.file();
|
||||||
|
FileFilter filter = selection.filter();
|
||||||
|
|
||||||
|
if(filter == IMAGE_FILE_FILTER) {
|
||||||
|
if(!importSkinImage(file, dst, (stream, image) -> TiledImageUtility.writeDexSkin(stream, image, TiledImageUtility.generateBackgroundColors(image)))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else if(filter == ZUKAN_FILE_FILTER) {
|
||||||
|
if(!importSkinFile(file, dst, 25090)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
DlcOption option = new DlcOption("ZUKAN", file.getName(), dst.getAbsolutePath(), true);
|
||||||
|
updateCustomOption(zukanComboBox, customDexSkin, option);
|
||||||
|
customDexSkin = option;
|
||||||
|
player.setCustomDexSkin(customDexSkin.name());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create musical show selector
|
||||||
|
createDlcOption("Musical Show", musicalComboBox, () -> {
|
||||||
|
SwingUtility.showIgnorableHint(getRootPane(), "Please exercise caution when importing custom musicals.\n"
|
||||||
|
+ "Downloading invalid data might cause game crashes or other issues.", "Attention", JOptionPane.WARNING_MESSAGE);
|
||||||
|
FileChooser.showFileOpenDialog(getRootPane(), selection -> {
|
||||||
|
File dst = player.getMusicalFile();
|
||||||
|
File file = selection.file();
|
||||||
|
|
||||||
|
if(!importNarcFile(file, dst)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
DlcOption option = new DlcOption("MUSICAL", file.getName(), dst.getAbsolutePath(), true);
|
||||||
|
updateCustomOption(musicalComboBox, customMusical, option);
|
||||||
|
customMusical = option;
|
||||||
|
player.setCustomMusical(customMusical.name());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
add(optionPanel, "spanx, align 50%");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void loadProfile(Player player) {
|
||||||
|
this.player = player;
|
||||||
|
gameVersion = player.getGameVersion();
|
||||||
|
String cgearType = player.getGameVersion().isVersion2() ? "CGEAR2" : "CGEAR";
|
||||||
|
customCGearSkin = player.getCustomCGearSkin() == null ? null : new DlcOption(cgearType, player.getCustomCGearSkin(), player.getCGearSkinFile().getAbsolutePath(), true);
|
||||||
|
customDexSkin = player.getCustomDexSkin() == null ? null : new DlcOption("ZUKAN", player.getCustomDexSkin(), player.getDexSkinFile().getAbsolutePath(), true);
|
||||||
|
customMusical = player.getCustomMusical() == null ? null : new DlcOption("MUSICAL", player.getCustomMusical(), player.getMusicalFile().getAbsolutePath(), true);
|
||||||
|
updateDlcOptions(cgearComboBox, cgearType, player.getCGearSkin(), customCGearSkin);
|
||||||
|
updateDlcOptions(zukanComboBox, "ZUKAN", player.getDexSkin(), customDexSkin);
|
||||||
|
updateDlcOptions(musicalComboBox, "MUSICAL", player.getMusical(), customMusical);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void saveProfile(Player player) {
|
||||||
|
DlcOption cgearSkin = (DlcOption)cgearComboBox.getSelectedItem();
|
||||||
|
DlcOption dexSkin = (DlcOption)zukanComboBox.getSelectedItem();
|
||||||
|
DlcOption musical = (DlcOption)musicalComboBox.getSelectedItem();
|
||||||
|
player.setCGearSkin(cgearSkin == null ? null : cgearSkin.custom() ? "custom" : cgearSkin.name());
|
||||||
|
player.setDexSkin(dexSkin == null ? null : dexSkin.custom() ? "custom" : dexSkin.name());
|
||||||
|
player.setMusical(musical == null ? null : musical.custom() ? "custom" : musical.name());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createDlcOption(String label, JComboBox<DlcOption> comboBox, Runnable importListener) {
|
||||||
|
optionPanel.add(new JLabel(label), "sizegroup label");
|
||||||
|
optionPanel.add(comboBox, "sizegroup option");
|
||||||
|
JButton importButton = new JButton("Import");
|
||||||
|
importButton.addActionListener(event -> importListener.run());
|
||||||
|
optionPanel.add(importButton, "wrap");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateDlcOptions(JComboBox<DlcOption> comboBox, String type, String selectedOption, DlcOption customOption) {
|
||||||
|
Vector<DlcOption> options = new Vector<>();
|
||||||
|
|
||||||
|
if(customOption != null) {
|
||||||
|
options.add(customOption);
|
||||||
|
}
|
||||||
|
|
||||||
|
entralinked.getDlcList().getDlcList("IRAO", type).forEach(dlc -> options.add(new DlcOption(type, dlc.name(), dlc.path())));
|
||||||
|
DlcOption selection = selectedOption == null ? null : selectedOption.equals("custom") ? customOption : options.stream().filter(x -> selectedOption.equals(x.name())).findFirst().orElse(null);
|
||||||
|
options.add(0, null); // "Do not change" option
|
||||||
|
comboBox.setModel(new DefaultComboBoxModel<DlcOption>(options));
|
||||||
|
comboBox.setSelectedItem(selection);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateCustomOption(JComboBox<DlcOption> comboBox, DlcOption oldValue, DlcOption newValue) {
|
||||||
|
DefaultComboBoxModel<DlcOption> model = (DefaultComboBoxModel<DlcOption>)comboBox.getModel();
|
||||||
|
|
||||||
|
if(oldValue != null) {
|
||||||
|
model.removeElement(oldValue);
|
||||||
|
skinCache.remove(oldValue.path());
|
||||||
|
}
|
||||||
|
|
||||||
|
model.insertElementAt(newValue, 1);
|
||||||
|
model.setSelectedItem(newValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateSkinPreview(JComboBox<DlcOption> comboBox, JLabel previewLabel) {
|
||||||
|
Image preview = getSkinImage((DlcOption)comboBox.getSelectedItem());
|
||||||
|
previewLabel.setText(preview == null ? "No preview available." : "");
|
||||||
|
previewLabel.setIcon(preview == null ? null : new ImageIcon(preview));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Image getSkinImage(DlcOption option) {
|
||||||
|
return option == null ? null : skinCache.computeIfAbsent(option.path(), path -> {
|
||||||
|
try(FileInputStream inputStream = new FileInputStream(path)) {
|
||||||
|
return switch(option.type()) {
|
||||||
|
case "CGEAR" -> TiledImageUtility.readCGearSkin(inputStream, true);
|
||||||
|
case "CGEAR2" -> TiledImageUtility.readCGearSkin(inputStream, false);
|
||||||
|
case "ZUKAN" -> TiledImageUtility.readDexSkin(inputStream, true);
|
||||||
|
default -> throw new IllegalArgumentException("Invalid type: " + option.type());
|
||||||
|
};
|
||||||
|
} catch(Exception e) {
|
||||||
|
SwingUtility.showExceptionInfo(getRootPane(), "Failed to load skin preview.", e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean importSkinFile(File src, File dst, int expectedSize) {
|
||||||
|
int sizeWithoutChecksum = expectedSize - 2;
|
||||||
|
int length = (int)src.length();
|
||||||
|
|
||||||
|
// Check content length
|
||||||
|
if(length != expectedSize && length != sizeWithoutChecksum) {
|
||||||
|
JOptionPane.showMessageDialog(getRootPane(), "Invalid content length, expected either %s or %s bytes."
|
||||||
|
.formatted(sizeWithoutChecksum, expectedSize), "Attention", JOptionPane.WARNING_MESSAGE);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
byte[] bytes = Files.readAllBytes(src.toPath());
|
||||||
|
boolean writeChecksum = true;
|
||||||
|
|
||||||
|
// Validate checksum
|
||||||
|
if(length == expectedSize) {
|
||||||
|
int checksum = Crc16.calc(bytes, 0, sizeWithoutChecksum);
|
||||||
|
int checksumInFile = (bytes[bytes.length - 2] & 0xFF) | ((bytes[bytes.length - 1] & 0xFF) << 8);
|
||||||
|
|
||||||
|
if(checksum != checksumInFile) {
|
||||||
|
JOptionPane.showMessageDialog(getRootPane(), "File checksum doesn't match.", "Attention", JOptionPane.WARNING_MESSAGE);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
writeChecksum = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write to destination & append checksum if necessary
|
||||||
|
try(LEOutputStream outputStream = new LEOutputStream(new FileOutputStream(dst))) {
|
||||||
|
outputStream.write(bytes);
|
||||||
|
|
||||||
|
if(writeChecksum) {
|
||||||
|
outputStream.writeShort(Crc16.calc(bytes));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch(Exception e) {
|
||||||
|
SwingUtility.showExceptionInfo(getRootPane(), "Failed to import skin.", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean importSkinImage(File src, File dst, SkinWriter writer) {
|
||||||
|
try {
|
||||||
|
BufferedImage image = ImageIO.read(src);
|
||||||
|
int width = TiledImageUtility.SCREEN_WIDTH;
|
||||||
|
int height = TiledImageUtility.SCREEN_HEIGHT;
|
||||||
|
|
||||||
|
if(image.getWidth() != width || image.getHeight() != height) {
|
||||||
|
JOptionPane.showMessageDialog(getRootPane(), "Image size must be %sx%s pixels.".formatted(width, height), "Attention", JOptionPane.WARNING_MESSAGE);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
|
||||||
|
writer.writeSkin(byteStream, image);
|
||||||
|
byte[] bytes = byteStream.toByteArray();
|
||||||
|
|
||||||
|
try(LEOutputStream outputStream = new LEOutputStream(new FileOutputStream(dst))) {
|
||||||
|
outputStream.write(bytes);
|
||||||
|
outputStream.writeShort(Crc16.calc(bytes));
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch(IllegalArgumentException e) {
|
||||||
|
JOptionPane.showMessageDialog(getRootPane(), e.getMessage(), "Attention", JOptionPane.WARNING_MESSAGE);
|
||||||
|
} catch(Exception e) {
|
||||||
|
SwingUtility.showExceptionInfo(getRootPane(), "Failed to import skin image.", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean importNarcFile(File src, File dst) {
|
||||||
|
try {
|
||||||
|
byte[] bytes = Files.readAllBytes(src.toPath());
|
||||||
|
int offset = 0;
|
||||||
|
|
||||||
|
// Check narc header
|
||||||
|
if(!Arrays.equals(bytes, 0, NARC_HEADER.length, NARC_HEADER, 0, NARC_HEADER.length)) {
|
||||||
|
if(bytes.length < 16 || !Arrays.equals(bytes, 16, 16 + NARC_HEADER.length, NARC_HEADER, 0, NARC_HEADER.length)) {
|
||||||
|
JOptionPane.showMessageDialog(getRootPane(), "Invalid or unsupported file.", "Attention", JOptionPane.WARNING_MESSAGE);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
offset = 16;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO utility function
|
||||||
|
int length = ((bytes[offset + NARC_HEADER.length + 3] & 0xFF) << 24)
|
||||||
|
| ((bytes[offset + NARC_HEADER.length + 2] & 0xFF) << 16)
|
||||||
|
| ((bytes[offset + NARC_HEADER.length + 1] & 0xFF) << 8)
|
||||||
|
| (bytes[offset + NARC_HEADER.length] & 0xFF);
|
||||||
|
|
||||||
|
if(offset + length >= bytes.length) {
|
||||||
|
JOptionPane.showMessageDialog(getRootPane(), "File data is malformed or corrupt.", "Attention", JOptionPane.WARNING_MESSAGE);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write to destination
|
||||||
|
try(LEOutputStream outputStream = new LEOutputStream(new FileOutputStream(dst))) {
|
||||||
|
outputStream.write(bytes, offset, length);
|
||||||
|
outputStream.writeShort(Crc16.calc(bytes, offset, length));
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch(Exception e) {
|
||||||
|
SwingUtility.showExceptionInfo(getRootPane(), "Failed to import NARC file.", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
218
src/main/java/entralinked/gui/panels/DashboardPanel.java
Normal file
218
src/main/java/entralinked/gui/panels/DashboardPanel.java
Normal file
@@ -0,0 +1,218 @@
|
|||||||
|
package entralinked.gui.panels;
|
||||||
|
|
||||||
|
import java.awt.BorderLayout;
|
||||||
|
import java.awt.CardLayout;
|
||||||
|
import java.awt.Desktop;
|
||||||
|
import java.awt.GridLayout;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.URISyntaxException;
|
||||||
|
import java.net.URL;
|
||||||
|
|
||||||
|
import javax.swing.JButton;
|
||||||
|
import javax.swing.JLabel;
|
||||||
|
import javax.swing.JOptionPane;
|
||||||
|
import javax.swing.JPanel;
|
||||||
|
import javax.swing.JTabbedPane;
|
||||||
|
import javax.swing.text.AbstractDocument;
|
||||||
|
|
||||||
|
import com.formdev.flatlaf.extras.components.FlatTabbedPane;
|
||||||
|
import com.formdev.flatlaf.extras.components.FlatTabbedPane.TabAreaAlignment;
|
||||||
|
import com.formdev.flatlaf.extras.components.FlatTextField;
|
||||||
|
|
||||||
|
import entralinked.Entralinked;
|
||||||
|
import entralinked.GameVersion;
|
||||||
|
import entralinked.gui.GsidDocumentFilter;
|
||||||
|
import entralinked.gui.data.DataManager;
|
||||||
|
import entralinked.model.player.Player;
|
||||||
|
import entralinked.model.player.PlayerStatus;
|
||||||
|
import entralinked.utility.GsidUtility;
|
||||||
|
import entralinked.utility.SwingUtility;
|
||||||
|
import net.miginfocom.swing.MigLayout;
|
||||||
|
|
||||||
|
@SuppressWarnings("serial")
|
||||||
|
public class DashboardPanel extends JPanel {
|
||||||
|
|
||||||
|
public static final String LOGIN_CARD = "login";
|
||||||
|
public static final String MAIN_CARD = "main";
|
||||||
|
private final Entralinked entralinked;
|
||||||
|
private final JLabel sessionLabel;
|
||||||
|
private final FlatTabbedPane tabbedPane;
|
||||||
|
private CardLayout layout;
|
||||||
|
private SummaryPanel summaryPanel;
|
||||||
|
private EncounterEditorPanel encounterPanel;
|
||||||
|
private ItemEditorPanel itemPanel;
|
||||||
|
private VisitorEditorPanel visitorPanel;
|
||||||
|
private CustomizationPanel customizePanel;
|
||||||
|
private MiscPanel miscPanel;
|
||||||
|
private Player player;
|
||||||
|
private boolean initialized;
|
||||||
|
|
||||||
|
public DashboardPanel(Entralinked entralinked) {
|
||||||
|
this.entralinked = entralinked;
|
||||||
|
|
||||||
|
// Create GSID field
|
||||||
|
FlatTextField gsidTextField = new FlatTextField();
|
||||||
|
gsidTextField.setPlaceholderText("XXXXXXXXXX");
|
||||||
|
((AbstractDocument)gsidTextField.getDocument()).setDocumentFilter(new GsidDocumentFilter());
|
||||||
|
|
||||||
|
// Create login button
|
||||||
|
JButton loginButton = new JButton("Log in");
|
||||||
|
loginButton.addActionListener(event -> login(gsidTextField.getText()));
|
||||||
|
|
||||||
|
// Create browser dashboard button
|
||||||
|
JLabel browserButton = SwingUtility.createButtonLabel("Browser dashboard (Legacy)", () -> {
|
||||||
|
try {
|
||||||
|
Desktop.getDesktop().browse(new URL("http://127.0.0.1/dashboard/profile.html").toURI());
|
||||||
|
} catch(IOException | URISyntaxException e) {
|
||||||
|
SwingUtility.showExceptionInfo(getRootPane(), "Failed to open URL.", e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create login panel
|
||||||
|
JPanel loginPanel = new JPanel(new MigLayout("wrap, align 50% 50%"));
|
||||||
|
String loginDescription = """
|
||||||
|
<html>
|
||||||
|
Tuck in a Pokémon and enter your Game Sync ID to continue.<br/>
|
||||||
|
Your Game Sync ID can be found in 'Game Sync Settings' in the game's main menu.
|
||||||
|
</html>
|
||||||
|
""";
|
||||||
|
loginPanel.add(SwingUtility.createTitleLabel("Log in"));
|
||||||
|
loginPanel.add(SwingUtility.createDescriptionLabel(loginDescription));
|
||||||
|
loginPanel.add(new JLabel("Game Sync ID"), "gapy 8");
|
||||||
|
loginPanel.add(gsidTextField, "growx");
|
||||||
|
loginPanel.add(loginButton, "growx");
|
||||||
|
loginPanel.add(browserButton, "gapy 8, align 100%");
|
||||||
|
|
||||||
|
// Create save button
|
||||||
|
JButton saveButton = new JButton("Save profile");
|
||||||
|
saveButton.addActionListener(event -> {
|
||||||
|
// Try to save data
|
||||||
|
// TODO this is probably not thread-safe!!!
|
||||||
|
try {
|
||||||
|
encounterPanel.saveProfile(player);
|
||||||
|
itemPanel.saveProfile(player);
|
||||||
|
visitorPanel.saveProfile(player);
|
||||||
|
customizePanel.saveProfile(player);
|
||||||
|
miscPanel.saveProfile(player);
|
||||||
|
player.setStatus(PlayerStatus.WAKE_READY);
|
||||||
|
|
||||||
|
if(!entralinked.getPlayerManager().savePlayer(player)) {
|
||||||
|
JOptionPane.showMessageDialog(getRootPane(), "Failed to write player data to disk.", "Attention", JOptionPane.WARNING_MESSAGE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
JOptionPane.showMessageDialog(getRootPane(), "Profile data has been saved successfully!\n"
|
||||||
|
+ "Use Game Sync to wake up your Pokémon and download your selected content.");
|
||||||
|
} catch(Exception e) {
|
||||||
|
SwingUtility.showExceptionInfo(getRootPane(), "Failed to save player data.", e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create log out button
|
||||||
|
JButton logoutButton = new JButton("Log out");
|
||||||
|
logoutButton.addActionListener(event -> {
|
||||||
|
gsidTextField.setText("");
|
||||||
|
layout.show(this, LOGIN_CARD);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create main footer button panel
|
||||||
|
JPanel buttonPanel = new JPanel(new GridLayout(1, 2));
|
||||||
|
buttonPanel.add(saveButton);
|
||||||
|
buttonPanel.add(logoutButton);
|
||||||
|
|
||||||
|
// Create main footer panel
|
||||||
|
sessionLabel = new JLabel("", JLabel.CENTER);
|
||||||
|
JPanel footerPanel = new JPanel(new BorderLayout());
|
||||||
|
footerPanel.add(sessionLabel);
|
||||||
|
footerPanel.add(buttonPanel, BorderLayout.LINE_END);
|
||||||
|
|
||||||
|
// Create main panel
|
||||||
|
tabbedPane = new FlatTabbedPane();
|
||||||
|
tabbedPane.setTabPlacement(JTabbedPane.LEFT);
|
||||||
|
tabbedPane.setTabAreaAlignment(TabAreaAlignment.center);
|
||||||
|
JPanel mainPanel = new JPanel(new BorderLayout());
|
||||||
|
mainPanel.add(tabbedPane);
|
||||||
|
mainPanel.add(footerPanel, BorderLayout.PAGE_END);
|
||||||
|
|
||||||
|
// Create layout
|
||||||
|
layout = new CardLayout();
|
||||||
|
setLayout(layout);
|
||||||
|
add(LOGIN_CARD, loginPanel);
|
||||||
|
add(MAIN_CARD, mainPanel);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void login(String gameSyncId) {
|
||||||
|
// Check if GSID is valid
|
||||||
|
if(!GsidUtility.isValidGameSyncId(gameSyncId)) {
|
||||||
|
JOptionPane.showMessageDialog(this, "Please enter a valid Game Sync ID.", "Attention", JOptionPane.WARNING_MESSAGE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Player player = entralinked.getPlayerManager().getPlayer(gameSyncId);
|
||||||
|
|
||||||
|
// Check if player exists
|
||||||
|
if(player == null) {
|
||||||
|
JOptionPane.showMessageDialog(this, "This Game Sync ID does not exist.\n"
|
||||||
|
+ "If you haven't already, please tuck in a Pokémon first.", "Attention", JOptionPane.WARNING_MESSAGE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
GameVersion version = player.getGameVersion();
|
||||||
|
|
||||||
|
// Check if necessary data is present
|
||||||
|
if(player.getDreamerInfo() == null || version == null) {
|
||||||
|
JOptionPane.showMessageDialog(this, "Please use Game Sync to tuck in a Pokémon first.", "Attention", JOptionPane.INFORMATION_MESSAGE);
|
||||||
|
player = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to initialize dashboard
|
||||||
|
if(!initialized && !initializeDashboard()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load player data
|
||||||
|
sessionLabel.setText("Game Version: %s, Game Sync ID: %s".formatted(version.getDisplayName(), gameSyncId));
|
||||||
|
tabbedPane.setEnabledAt(3, version.isVersion2());
|
||||||
|
|
||||||
|
try {
|
||||||
|
summaryPanel.loadProfile(player);
|
||||||
|
encounterPanel.loadProfile(player);
|
||||||
|
itemPanel.loadProfile(player);
|
||||||
|
visitorPanel.loadProfile(player);
|
||||||
|
customizePanel.loadProfile(player);
|
||||||
|
miscPanel.loadProfile(player);
|
||||||
|
} catch(Exception e) {
|
||||||
|
SwingUtility.showExceptionInfo(getRootPane(), "Failed to load player data.", e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.player = player;
|
||||||
|
tabbedPane.setSelectedIndex(0);
|
||||||
|
layout.show(this, MAIN_CARD);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean initializeDashboard() {
|
||||||
|
try {
|
||||||
|
DataManager.loadData();
|
||||||
|
summaryPanel = new SummaryPanel();
|
||||||
|
encounterPanel = new EncounterEditorPanel();
|
||||||
|
itemPanel = new ItemEditorPanel();
|
||||||
|
visitorPanel = new VisitorEditorPanel();
|
||||||
|
customizePanel = new CustomizationPanel(entralinked);
|
||||||
|
miscPanel = new MiscPanel();
|
||||||
|
} catch(Exception e) {
|
||||||
|
SwingUtility.showExceptionInfo(getRootPane(), "Failed to initialize dashboard.", e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
tabbedPane.add("Summary", summaryPanel);
|
||||||
|
tabbedPane.add("Entree Forest", encounterPanel);
|
||||||
|
tabbedPane.add("Dream Remnants", itemPanel);
|
||||||
|
tabbedPane.add("Join Avenue", visitorPanel);
|
||||||
|
tabbedPane.add("Customization", customizePanel);
|
||||||
|
tabbedPane.add("Miscellaneous", miscPanel);
|
||||||
|
initialized = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
227
src/main/java/entralinked/gui/panels/EncounterEditorPanel.java
Normal file
227
src/main/java/entralinked/gui/panels/EncounterEditorPanel.java
Normal file
@@ -0,0 +1,227 @@
|
|||||||
|
package entralinked.gui.panels;
|
||||||
|
|
||||||
|
import java.awt.Image;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
|
||||||
|
import entralinked.GameVersion;
|
||||||
|
import entralinked.gui.data.DataManager;
|
||||||
|
import entralinked.gui.data.PkmnForm;
|
||||||
|
import entralinked.gui.data.PkmnSpecies;
|
||||||
|
import entralinked.model.pkmn.PkmnGender;
|
||||||
|
import entralinked.model.player.DreamAnimation;
|
||||||
|
import entralinked.model.player.DreamEncounter;
|
||||||
|
import entralinked.model.player.Player;
|
||||||
|
|
||||||
|
@SuppressWarnings("serial")
|
||||||
|
public class EncounterEditorPanel extends TableEditorPanel {
|
||||||
|
|
||||||
|
public static final int ENCOUNTER_COUNT = 10;
|
||||||
|
public static final int SPECIES_COLUMN = 1;
|
||||||
|
public static final int MOVE_COLUMN = 2;
|
||||||
|
public static final int FORM_COLUMN = 3;
|
||||||
|
public static final int GENDER_COLUMN = 4;
|
||||||
|
public static final int ANIMATION_COLUMN = 5;
|
||||||
|
private static final String[] COLUMN_NAMES = { "Species", "Move", "Form", "Gender", "Animation" };
|
||||||
|
private static final Class<?>[] COLUMN_TYPES = { PkmnSpecies.class, Integer.class, PkmnForm.class, PkmnGender.class, DreamAnimation.class };
|
||||||
|
private GameVersion gameVersion;
|
||||||
|
private boolean optionLock; // Used to prevent double option updates
|
||||||
|
|
||||||
|
public EncounterEditorPanel() {
|
||||||
|
super(ENCOUNTER_COUNT, COLUMN_NAMES, COLUMN_TYPES);
|
||||||
|
setTitle("Entree Forest");
|
||||||
|
setDescription("""
|
||||||
|
<html>
|
||||||
|
Use the table below to configure Entree Forest encounters.<br/>
|
||||||
|
Up to 10 different encounters can be configured at a time.<br/>
|
||||||
|
Please be aware that Unova Pokémon are exclusive to B2W2.
|
||||||
|
</html>
|
||||||
|
""");
|
||||||
|
table.setCellRenderers(SPECIES_COLUMN, PkmnSpecies.class, PkmnSpecies::name, "— — — — —");
|
||||||
|
table.setCellRenderers(MOVE_COLUMN, Integer.class, DataManager::getMoveName, "None");
|
||||||
|
table.setCellRenderers(FORM_COLUMN, PkmnForm.class, PkmnForm::name);
|
||||||
|
table.setCellRenderers(GENDER_COLUMN, PkmnGender.class, PkmnGender::getDisplayName);
|
||||||
|
table.setCellRenderers(ANIMATION_COLUMN, DreamAnimation.class, DreamAnimation::getDisplayName);
|
||||||
|
initializeOptions();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void dataChanged(int row, int column, Object oldValue, Object newValue) {
|
||||||
|
if(optionLock) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(column == SPECIES_COLUMN) {
|
||||||
|
if(newValue == null) {
|
||||||
|
disableSecondaryOptions(row);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
optionLock = true;
|
||||||
|
table.enableOption(row, GENDER_COLUMN);
|
||||||
|
table.enableOption(row, MOVE_COLUMN);
|
||||||
|
table.enableOption(row, FORM_COLUMN);
|
||||||
|
table.enableOption(row, ANIMATION_COLUMN);
|
||||||
|
updateGenderOptions(row);
|
||||||
|
updateFormOptions(row);
|
||||||
|
updateMoveOptions(row);
|
||||||
|
optionLock = false;
|
||||||
|
} else if(column == GENDER_COLUMN) {
|
||||||
|
if(!isLegalMode()) {
|
||||||
|
return; // Gender only affects form options if legal mode is enabled
|
||||||
|
}
|
||||||
|
|
||||||
|
if(newValue == null) {
|
||||||
|
table.disableOption(row, FORM_COLUMN);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateFormOptions(row);
|
||||||
|
|
||||||
|
if(oldValue == null) {
|
||||||
|
table.enableOption(row, FORM_COLUMN);
|
||||||
|
}
|
||||||
|
} else if(column == FORM_COLUMN) {
|
||||||
|
if(!isLegalMode()) {
|
||||||
|
return; // Form only affects move options if legal mode is enabled
|
||||||
|
}
|
||||||
|
|
||||||
|
if(newValue == null) {
|
||||||
|
table.disableOption(row, MOVE_COLUMN);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateMoveOptions(row);
|
||||||
|
|
||||||
|
if(oldValue == null) {
|
||||||
|
table.enableOption(row, MOVE_COLUMN);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void legalModeChanged() {
|
||||||
|
for(int i = 0; i < ENCOUNTER_COUNT; i++) {
|
||||||
|
updateSpeciesOptions(i);
|
||||||
|
PkmnSpecies species = getSpecies(i);
|
||||||
|
|
||||||
|
if(species != null) {
|
||||||
|
optionLock = true;
|
||||||
|
updateGenderOptions(i);
|
||||||
|
updateFormOptions(i);
|
||||||
|
updateMoveOptions(i);
|
||||||
|
optionLock = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void initializeOptions(int row) {
|
||||||
|
setOptions(row, ANIMATION_COLUMN, Arrays.asList(DreamAnimation.values()));
|
||||||
|
disableSecondaryOptions(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void randomizeSelections(int row) {
|
||||||
|
table.randomizeSelection(row, SPECIES_COLUMN, false);
|
||||||
|
table.randomizeSelection(row, GENDER_COLUMN);
|
||||||
|
table.randomizeSelection(row, FORM_COLUMN);
|
||||||
|
table.randomizeSelection(row, MOVE_COLUMN);
|
||||||
|
table.randomizeSelection(row, ANIMATION_COLUMN);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void clearSelections(int row) {
|
||||||
|
model.setValueAt(null, row, SPECIES_COLUMN);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Image getSelectionIcon(int row) {
|
||||||
|
return row == -1 ? DataManager.getPokemonSprite(0) : DataManager.getPokemonSprite(getSpecies(row), getForm(row), getGender(row), false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean shouldUpdateSelectionIcon(int column) {
|
||||||
|
return column == SPECIES_COLUMN || column == GENDER_COLUMN || column == FORM_COLUMN;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateSpeciesOptions(int row) {
|
||||||
|
setOptions(row, SPECIES_COLUMN,
|
||||||
|
isLegalMode() ? DataManager.getSpeciesOptions(gameVersion) : DataManager.getDownloadableSpecies(gameVersion),
|
||||||
|
(a, b) -> a.name().compareTo(b.name()), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateGenderOptions(int row) {
|
||||||
|
PkmnSpecies species = getSpecies(row);
|
||||||
|
setOptions(row, GENDER_COLUMN, isLegalMode() ? DataManager.getGenderOptions(gameVersion, species) : species.getGenders());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateFormOptions(int row) {
|
||||||
|
PkmnSpecies species = getSpecies(row);
|
||||||
|
setOptions(row, FORM_COLUMN, species.hasForms() ? (isLegalMode() ? DataManager.getFormOptions(gameVersion, species, getGender(row))
|
||||||
|
: Arrays.asList(species.forms())) : Collections.emptyList());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateMoveOptions(int row) {
|
||||||
|
setOptions(row, MOVE_COLUMN,
|
||||||
|
isLegalMode() ? DataManager.getMoveOptions(gameVersion, getSpecies(row), getGender(row), getForm(row)) : DataManager.getMoveIds(),
|
||||||
|
(a, b) -> DataManager.getMoveName(a).compareTo(DataManager.getMoveName(b)), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void loadProfile(Player player) {
|
||||||
|
gameVersion = player.getGameVersion();
|
||||||
|
table.clearSelection();
|
||||||
|
clearSelections();
|
||||||
|
legalModeToggle.setSelected(false);
|
||||||
|
legalModeChanged();
|
||||||
|
int row = 0;
|
||||||
|
|
||||||
|
for(DreamEncounter encounter : player.getEncounters()) {
|
||||||
|
PkmnSpecies species = DataManager.getSpecies(encounter.species());
|
||||||
|
model.setValueAt(species, row, SPECIES_COLUMN);
|
||||||
|
model.setValueAt(encounter.move() == 0 ? null : encounter.move(), row, MOVE_COLUMN);
|
||||||
|
model.setValueAt(encounter.gender(), row, GENDER_COLUMN);
|
||||||
|
model.setValueAt(encounter.animation(), row, ANIMATION_COLUMN);
|
||||||
|
|
||||||
|
if(species.hasForms()) {
|
||||||
|
model.setValueAt(species.forms()[encounter.form()], row, FORM_COLUMN);
|
||||||
|
}
|
||||||
|
|
||||||
|
row++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void saveProfile(Player player) {
|
||||||
|
player.setEncounters(computeSelectionList(row ->
|
||||||
|
new DreamEncounter(getSpecies(row).id(), getMove(row), getForm(row), getGender(row), getAnimation(row)),
|
||||||
|
row -> getSpecies(row) != null));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void disableSecondaryOptions(int row) {
|
||||||
|
table.disableOption(row, MOVE_COLUMN);
|
||||||
|
table.disableOption(row, FORM_COLUMN);
|
||||||
|
table.disableOption(row, GENDER_COLUMN);
|
||||||
|
table.disableOption(row, ANIMATION_COLUMN);
|
||||||
|
}
|
||||||
|
|
||||||
|
private PkmnSpecies getSpecies(int row) {
|
||||||
|
return table.getValueAt(row, SPECIES_COLUMN, PkmnSpecies.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
private int getMove(int row) {
|
||||||
|
return table.getValueAt(row, MOVE_COLUMN, Integer.class, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private int getForm(int row) {
|
||||||
|
PkmnForm form = table.getValueAt(row, FORM_COLUMN, PkmnForm.class);
|
||||||
|
return form == null ? 0 : form.id();
|
||||||
|
}
|
||||||
|
|
||||||
|
private PkmnGender getGender(int row) {
|
||||||
|
return table.getValueAt(row, GENDER_COLUMN, PkmnGender.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
private DreamAnimation getAnimation(int row) {
|
||||||
|
return table.getValueAt(row, ANIMATION_COLUMN, DreamAnimation.class);
|
||||||
|
}
|
||||||
|
}
|
||||||
132
src/main/java/entralinked/gui/panels/ItemEditorPanel.java
Normal file
132
src/main/java/entralinked/gui/panels/ItemEditorPanel.java
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
package entralinked.gui.panels;
|
||||||
|
|
||||||
|
import java.awt.Image;
|
||||||
|
import java.util.stream.IntStream;
|
||||||
|
|
||||||
|
import entralinked.GameVersion;
|
||||||
|
import entralinked.gui.data.DataManager;
|
||||||
|
import entralinked.model.player.DreamItem;
|
||||||
|
import entralinked.model.player.Player;
|
||||||
|
|
||||||
|
@SuppressWarnings("serial")
|
||||||
|
public class ItemEditorPanel extends TableEditorPanel {
|
||||||
|
|
||||||
|
public static final int ITEM_COUNT = 20;
|
||||||
|
public static final int MAX_ITEM_QUANTITY = 20;
|
||||||
|
public static final int ITEM_COLUMN = 1;
|
||||||
|
public static final int QUANTITY_COLUMN = 2;
|
||||||
|
private static final String[] COLUMN_NAMES = { "Item", "Quantity" };
|
||||||
|
private static final Class<?>[] COLUMN_TYPES = { Integer.class, Integer.class };
|
||||||
|
private GameVersion gameVersion;
|
||||||
|
|
||||||
|
public ItemEditorPanel() {
|
||||||
|
super(ITEM_COUNT, COLUMN_NAMES, COLUMN_TYPES);
|
||||||
|
setTitle("Dream Remnants");
|
||||||
|
setDescription("""
|
||||||
|
<html>
|
||||||
|
Use the table below to select the items you want to receive.<br/>
|
||||||
|
After waking up your Pokémon, talk to the boy near the<br/>
|
||||||
|
Entree Forest entrance in the Entralink to receive your items.
|
||||||
|
</html>
|
||||||
|
""");
|
||||||
|
table.setCellRenderers(ITEM_COLUMN, Integer.class, DataManager::getItemName, "— — — — —");
|
||||||
|
initializeOptions();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void dataChanged(int row, int column, Object oldValue, Object newValue) {
|
||||||
|
if(column != ITEM_COLUMN) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(newValue != null) {
|
||||||
|
if(oldValue != null) {
|
||||||
|
return; // Do nothing if new and old values are both non-null
|
||||||
|
}
|
||||||
|
|
||||||
|
table.enableOption(row, QUANTITY_COLUMN);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.disableOption(row, QUANTITY_COLUMN);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void legalModeChanged() {
|
||||||
|
for(int i = 0; i < ITEM_COUNT; i++) {
|
||||||
|
updateItemOptions(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void initializeOptions(int row) {
|
||||||
|
setOptions(row, QUANTITY_COLUMN, IntStream.rangeClosed(1, ITEM_COUNT).boxed().toList());
|
||||||
|
table.disableOption(row, QUANTITY_COLUMN);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void randomizeSelections(int row) {
|
||||||
|
table.randomizeSelection(row, ITEM_COLUMN, false);
|
||||||
|
int quantity = 1;
|
||||||
|
|
||||||
|
while(quantity < MAX_ITEM_QUANTITY && Math.random() < 0.5) {
|
||||||
|
quantity++;
|
||||||
|
}
|
||||||
|
|
||||||
|
model.setValueAt(quantity, row, QUANTITY_COLUMN);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void clearSelections(int row) {
|
||||||
|
model.setValueAt(null, row, ITEM_COLUMN);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Image getSelectionIcon(int row) {
|
||||||
|
int item = row == -1 ? 0 : getItem(row);
|
||||||
|
return DataManager.getItemSprite(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean shouldUpdateSelectionIcon(int column) {
|
||||||
|
return column == ITEM_COLUMN;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getSelectionIconScale() {
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void loadProfile(Player player) {
|
||||||
|
gameVersion = player.getGameVersion();
|
||||||
|
table.clearSelection();
|
||||||
|
clearSelections();
|
||||||
|
legalModeToggle.setSelected(false);
|
||||||
|
legalModeChanged();
|
||||||
|
int row = 0;
|
||||||
|
|
||||||
|
for(DreamItem item : player.getItems()) {
|
||||||
|
model.setValueAt(item.id(), row, ITEM_COLUMN);
|
||||||
|
model.setValueAt(item.quantity(), row, QUANTITY_COLUMN);
|
||||||
|
row++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void saveProfile(Player player) {
|
||||||
|
player.setItems(computeSelectionList(row -> new DreamItem(getItem(row), getQuantity(row)), row -> getItem(row) != 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateItemOptions(int row) {
|
||||||
|
setOptions(row, ITEM_COLUMN,
|
||||||
|
isLegalMode() ? DataManager.getItemOptions() : DataManager.getDownloadableItems(gameVersion),
|
||||||
|
(a, b) -> DataManager.getItemName(a).compareTo(DataManager.getItemName(b)), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private int getItem(int row) {
|
||||||
|
return table.getValueAt(row, ITEM_COLUMN, Integer.class, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private int getQuantity(int row) {
|
||||||
|
return table.getValueAt(row, QUANTITY_COLUMN, Integer.class, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
152
src/main/java/entralinked/gui/panels/MiscPanel.java
Normal file
152
src/main/java/entralinked/gui/panels/MiscPanel.java
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
package entralinked.gui.panels;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import javax.swing.JButton;
|
||||||
|
import javax.swing.JCheckBox;
|
||||||
|
import javax.swing.JPanel;
|
||||||
|
import javax.swing.JSpinner;
|
||||||
|
import javax.swing.JTextField;
|
||||||
|
import javax.swing.SpinnerNumberModel;
|
||||||
|
import javax.swing.text.PlainDocument;
|
||||||
|
|
||||||
|
import entralinked.gui.SizeLimitDocumentFilter;
|
||||||
|
import entralinked.model.player.DreamDecor;
|
||||||
|
import entralinked.model.player.Player;
|
||||||
|
import entralinked.utility.SwingUtility;
|
||||||
|
import net.miginfocom.swing.MigLayout;
|
||||||
|
|
||||||
|
@SuppressWarnings("serial")
|
||||||
|
public class MiscPanel extends JPanel {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Container for keeping track of Décor options.
|
||||||
|
*/
|
||||||
|
private static class DecorOption {
|
||||||
|
|
||||||
|
private final JCheckBox checkBox;
|
||||||
|
private final JSpinner spinner;
|
||||||
|
private final JTextField nameField;
|
||||||
|
|
||||||
|
public DecorOption(JPanel parent, String label) {
|
||||||
|
spinner = new JSpinner(new SpinnerNumberModel(0, 0, 127, 1));
|
||||||
|
nameField = new JTextField();
|
||||||
|
((PlainDocument)nameField.getDocument()).setDocumentFilter(new SizeLimitDocumentFilter(12));
|
||||||
|
checkBox = new JCheckBox(label);
|
||||||
|
checkBox.addActionListener(event -> {
|
||||||
|
boolean active = checkBox.isSelected();
|
||||||
|
spinner.setEnabled(active);
|
||||||
|
nameField.setEnabled(active);
|
||||||
|
});
|
||||||
|
parent.add(checkBox);
|
||||||
|
parent.add(spinner);
|
||||||
|
parent.add(nameField, "growx");
|
||||||
|
setActive(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setActive(boolean active) {
|
||||||
|
checkBox.setSelected(active);
|
||||||
|
spinner.setEnabled(active);
|
||||||
|
nameField.setEnabled(active);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isActive() {
|
||||||
|
return checkBox.isSelected();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setId(int id) {
|
||||||
|
spinner.setValue(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getId() {
|
||||||
|
return (int)spinner.getValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setName(String name) {
|
||||||
|
nameField.setText(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getName() {
|
||||||
|
return nameField.getText();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final int DECOR_COUNT = 5;
|
||||||
|
private final List<DecorOption> decorOptions = new ArrayList<>();
|
||||||
|
private final JSpinner levelSpinner;
|
||||||
|
|
||||||
|
public MiscPanel() {
|
||||||
|
setLayout(new MigLayout("align 50% 50%, insets 0, wrap", "", "[]0[]"));
|
||||||
|
|
||||||
|
// Create decor panel
|
||||||
|
JPanel decorPanel = new JPanel(new MigLayout("insets 0, wrap 3, fill", "[][][grow]"));
|
||||||
|
String decorDescription = """
|
||||||
|
<html>
|
||||||
|
Configure Décor options to appear in Loblolly's studio.<br/>
|
||||||
|
Due to the closure of the Dream World, this function serves no real purpose<br/>
|
||||||
|
and is mostly meant for people who want to test or just play around with it.
|
||||||
|
</html>
|
||||||
|
""";
|
||||||
|
decorPanel.add(SwingUtility.createTitleLabel("Dream Décor"), "spanx, wrap");
|
||||||
|
decorPanel.add(SwingUtility.createDescriptionLabel(decorDescription), "spanx, wrap");
|
||||||
|
|
||||||
|
for(int i = 0; i < DECOR_COUNT; i++) {
|
||||||
|
decorOptions.add(new DecorOption(decorPanel, "Decor %s".formatted(i + 1)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create decor option buttons
|
||||||
|
JButton resetButton = new JButton("Default");
|
||||||
|
resetButton.addActionListener(event -> {
|
||||||
|
for(int i = 0; i < DECOR_COUNT; i++) {
|
||||||
|
DecorOption option = decorOptions.get(i);
|
||||||
|
DreamDecor decor = DreamDecor.DEFAULT_DECOR.get(i);
|
||||||
|
option.setActive(true);
|
||||||
|
option.setId(decor.id());
|
||||||
|
option.setName(decor.name());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
JButton clearButton = new JButton("Clear");
|
||||||
|
clearButton.addActionListener(event -> {
|
||||||
|
for(DecorOption option : decorOptions) {
|
||||||
|
option.setActive(false);
|
||||||
|
option.setId(0);
|
||||||
|
option.setName("");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create decor button panel
|
||||||
|
JPanel buttonPanel = new JPanel(new MigLayout("insets 0", "0[]"));
|
||||||
|
buttonPanel.add(resetButton);
|
||||||
|
buttonPanel.add(clearButton);
|
||||||
|
decorPanel.add(buttonPanel, "spanx, align right");
|
||||||
|
add(decorPanel, "spanx, growx");
|
||||||
|
|
||||||
|
// Create level panel
|
||||||
|
levelSpinner = new JSpinner(new SpinnerNumberModel(0, 0, 99, 1));
|
||||||
|
JPanel levelPanel = new JPanel(new MigLayout("insets 0, wrap"));
|
||||||
|
levelPanel.add(SwingUtility.createTitleLabel("Level Gain"));
|
||||||
|
levelPanel.add(SwingUtility.createDescriptionLabel("Amount of levels to gain on waking up."));
|
||||||
|
levelPanel.add(levelSpinner, "grow");
|
||||||
|
add(levelPanel, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void loadProfile(Player player) {
|
||||||
|
levelSpinner.setValue(player.getLevelsGained());
|
||||||
|
List<DreamDecor> decorList = player.getDecor();
|
||||||
|
|
||||||
|
for(int i = 0; i < DECOR_COUNT; i++) {
|
||||||
|
DecorOption option = decorOptions.get(i);
|
||||||
|
DreamDecor decor = i < decorList.size() ? decorList.get(i) : null;
|
||||||
|
option.setActive(decor != null);
|
||||||
|
option.setId(decor == null ? 0 : decor.id());
|
||||||
|
option.setName(decor == null ? "" : decor.name());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void saveProfile(Player player) {
|
||||||
|
player.setDecor(decorOptions.stream().filter(DecorOption::isActive).map(x -> new DreamDecor(x.getId(), x.getName())).toList());
|
||||||
|
player.setLevelsGained((int)levelSpinner.getValue());
|
||||||
|
}
|
||||||
|
}
|
||||||
110
src/main/java/entralinked/gui/panels/SummaryPanel.java
Normal file
110
src/main/java/entralinked/gui/panels/SummaryPanel.java
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
package entralinked.gui.panels;
|
||||||
|
|
||||||
|
import java.awt.Desktop;
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import javax.swing.JLabel;
|
||||||
|
import javax.swing.JOptionPane;
|
||||||
|
import javax.swing.JPanel;
|
||||||
|
|
||||||
|
import entralinked.gui.component.PropertyDisplay;
|
||||||
|
import entralinked.gui.component.ShadowedSprite;
|
||||||
|
import entralinked.gui.data.DataManager;
|
||||||
|
import entralinked.model.pkmn.PkmnInfo;
|
||||||
|
import entralinked.model.player.Player;
|
||||||
|
import entralinked.utility.SwingUtility;
|
||||||
|
import net.miginfocom.swing.MigLayout;
|
||||||
|
|
||||||
|
@SuppressWarnings("serial")
|
||||||
|
public class SummaryPanel extends JPanel {
|
||||||
|
|
||||||
|
public static final String KEY_NAME = "name";
|
||||||
|
public static final String KEY_OT = "trainer";
|
||||||
|
public static final String KEY_ID = "tid";
|
||||||
|
public static final String KEY_SID = "sid";
|
||||||
|
public static final String KEY_PID = "pid";
|
||||||
|
public static final String KEY_ABILITY = "ability";
|
||||||
|
public static final String KEY_NATURE = "nature";
|
||||||
|
public static final String KEY_GENDER = "gender";
|
||||||
|
public static final String KEY_LEVEL = "level";
|
||||||
|
public static final String KEY_ITEM = "item";
|
||||||
|
private static final String[] FLAVOR_TEXT = {
|
||||||
|
"*pokemon* is trying to grow berries in a nearby garden.",
|
||||||
|
"*pokemon* is itching to earn some Dream Points.",
|
||||||
|
"*pokemon* is looking at the Tree of Dreams from afar.",
|
||||||
|
"*pokemon* is playing a fun minigame (and you're not allowed to join, ha!)",
|
||||||
|
"*pokemon* is checking out the Friend Board.",
|
||||||
|
"*pokemon* is dreaming about when they first met *trainer*.",
|
||||||
|
"*pokemon* left a berry at the Tree of Dreams.",
|
||||||
|
"*pokemon* is trying to come up with more of these."
|
||||||
|
}; // TODO I think it would be fun to grab various bits of data from the save file and use them here
|
||||||
|
private final ShadowedSprite icon;
|
||||||
|
private final JLabel flavorTextLabel;
|
||||||
|
private final PropertyDisplay infoDisplay;
|
||||||
|
private Player player;
|
||||||
|
|
||||||
|
public SummaryPanel() {
|
||||||
|
setLayout(new MigLayout("align 50% 50%, gapy 0, insets 0"));
|
||||||
|
|
||||||
|
// Create header panel
|
||||||
|
flavorTextLabel = SwingUtility.createDescriptionLabel();
|
||||||
|
JPanel headerPanel = new JPanel(new MigLayout("fillx, insets 0"));
|
||||||
|
headerPanel.add(SwingUtility.createTitleLabel("Summary"), "wrap");
|
||||||
|
headerPanel.add(flavorTextLabel, "wrap");
|
||||||
|
headerPanel.add(SwingUtility.createDescriptionLabel("Tucked-in Pokémon info:"), "wrap");
|
||||||
|
add(headerPanel, "spanx");
|
||||||
|
|
||||||
|
// Create icon label
|
||||||
|
icon = new ShadowedSprite();
|
||||||
|
add(icon, "gapright 4");
|
||||||
|
|
||||||
|
// Create property display
|
||||||
|
infoDisplay = new PropertyDisplay();
|
||||||
|
infoDisplay.addProperty(0, 0, KEY_NAME, "Name");
|
||||||
|
infoDisplay.addProperty(0, 1, KEY_OT, "OT");
|
||||||
|
infoDisplay.addProperty(0, 2, KEY_ID, "ID No.");
|
||||||
|
infoDisplay.addProperty(0, 3, KEY_SID, "SID No.", true);
|
||||||
|
infoDisplay.addProperty(0, 4, KEY_PID, "PID", true);
|
||||||
|
infoDisplay.addProperty(1, 0, KEY_ABILITY, "Ability");
|
||||||
|
infoDisplay.addProperty(1, 1, KEY_NATURE, "Nature");
|
||||||
|
infoDisplay.addProperty(1, 2, KEY_GENDER, "Gender");
|
||||||
|
infoDisplay.addProperty(1, 3, KEY_LEVEL, "Level");
|
||||||
|
infoDisplay.addProperty(1, 4, KEY_ITEM, "Held Item");
|
||||||
|
add(infoDisplay, "wrap");
|
||||||
|
|
||||||
|
// Create save file location button
|
||||||
|
JLabel openSaveButton = SwingUtility.createButtonLabel("Open data directory", () -> {
|
||||||
|
SwingUtility.showIgnorableHint(getRootPane(), "The game sends incomplete/corrupt save files to the server.\n"
|
||||||
|
+ "Please do not rely on Game Sync to back up your save files.", "Attention", JOptionPane.WARNING_MESSAGE);
|
||||||
|
|
||||||
|
try {
|
||||||
|
Desktop.getDesktop().open(player.getDataDirectory());
|
||||||
|
} catch(IOException e) {
|
||||||
|
SwingUtility.showExceptionInfo(getRootPane(), "Couldn't open data directory.", e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
add(openSaveButton, "spanx, align 100%, gapy 8");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void loadProfile(Player player) {
|
||||||
|
this.player = player;
|
||||||
|
PkmnInfo info = player.getDreamerInfo();
|
||||||
|
icon.setImage(DataManager.getPokemonSprite(info.species(), info.form(), info.gender(), info.isShiny()));
|
||||||
|
infoDisplay.setValue(KEY_NAME, info.nickname());
|
||||||
|
infoDisplay.setValue(KEY_OT, info.trainerName());
|
||||||
|
infoDisplay.setValue(KEY_ID, "%05d".formatted(info.trainerId()));
|
||||||
|
infoDisplay.setValue(KEY_SID, "%05d".formatted(info.trainerSecretId()));
|
||||||
|
infoDisplay.setValue(KEY_PID, "%08X".formatted(info.personality()));
|
||||||
|
infoDisplay.setValue(KEY_ABILITY, DataManager.getAbilityName(info.ability()));
|
||||||
|
infoDisplay.setValue(KEY_NATURE, info.nature().getDisplayName());
|
||||||
|
infoDisplay.setValue(KEY_GENDER, info.gender().getDisplayName());
|
||||||
|
infoDisplay.setValue(KEY_LEVEL, info.level());
|
||||||
|
infoDisplay.setValue(KEY_ITEM, info.heldItem() == 0 ? "None" : DataManager.getItemName(info.heldItem()));
|
||||||
|
SwingUtility.setTextFieldToggle(infoDisplay.getValueField(KEY_SID), false);
|
||||||
|
SwingUtility.setTextFieldToggle(infoDisplay.getValueField(KEY_PID), false);
|
||||||
|
String flavorText = FLAVOR_TEXT[(int)(Math.random() * FLAVOR_TEXT.length)]
|
||||||
|
.replace("*pokemon*", info.nickname())
|
||||||
|
.replace("*trainer*", info.trainerName());
|
||||||
|
flavorTextLabel.setText(flavorText);
|
||||||
|
}
|
||||||
|
}
|
||||||
247
src/main/java/entralinked/gui/panels/TableEditorPanel.java
Normal file
247
src/main/java/entralinked/gui/panels/TableEditorPanel.java
Normal file
@@ -0,0 +1,247 @@
|
|||||||
|
package entralinked.gui.panels;
|
||||||
|
|
||||||
|
import java.awt.Dimension;
|
||||||
|
import java.awt.Image;
|
||||||
|
import java.awt.image.BufferedImage;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.function.Function;
|
||||||
|
|
||||||
|
import javax.swing.JButton;
|
||||||
|
import javax.swing.JCheckBox;
|
||||||
|
import javax.swing.JLabel;
|
||||||
|
import javax.swing.JPanel;
|
||||||
|
import javax.swing.JScrollPane;
|
||||||
|
import javax.swing.ListSelectionModel;
|
||||||
|
import javax.swing.table.DefaultTableModel;
|
||||||
|
import javax.swing.table.TableModel;
|
||||||
|
|
||||||
|
import entralinked.gui.component.ConfigTable;
|
||||||
|
import entralinked.gui.component.ShadowedSprite;
|
||||||
|
import entralinked.utility.SwingUtility;
|
||||||
|
import net.miginfocom.swing.MigLayout;
|
||||||
|
|
||||||
|
@SuppressWarnings("serial")
|
||||||
|
public abstract class TableEditorPanel extends JPanel {
|
||||||
|
|
||||||
|
protected final Map<Integer, Object> oldValues = new HashMap<>();
|
||||||
|
protected final int rowCount;
|
||||||
|
protected final ConfigTable table;
|
||||||
|
protected final TableModel model;
|
||||||
|
protected final JLabel titleLabel;
|
||||||
|
protected final JLabel descriptionLabel;
|
||||||
|
protected final ShadowedSprite selectionIcon;
|
||||||
|
protected JButton randomizeButton;
|
||||||
|
protected JButton clearAllButton;
|
||||||
|
protected JCheckBox legalModeToggle;
|
||||||
|
|
||||||
|
public TableEditorPanel(int rowCount, String[] columnNames, Class<?>[] columnTypes) {
|
||||||
|
this(rowCount, true, columnNames, columnTypes);
|
||||||
|
}
|
||||||
|
|
||||||
|
public TableEditorPanel(int rowCount, boolean incluceLegalMode, String[] columnNames, Class<?>[] columnTypes) {
|
||||||
|
if(columnNames.length != columnTypes.length) {
|
||||||
|
throw new IllegalArgumentException("Length mismatch between column names and column types");
|
||||||
|
}
|
||||||
|
|
||||||
|
this.rowCount = rowCount;
|
||||||
|
String[] columns = new String[columnNames.length + 1];
|
||||||
|
columns[0] = "No.";
|
||||||
|
System.arraycopy(columnNames, 0, columns, 1, columnNames.length);
|
||||||
|
|
||||||
|
// Create labels
|
||||||
|
titleLabel = SwingUtility.createTitleLabel();
|
||||||
|
descriptionLabel = SwingUtility.createDescriptionLabel();
|
||||||
|
selectionIcon = new ShadowedSprite();
|
||||||
|
|
||||||
|
// Create buttons & toggles
|
||||||
|
randomizeButton = new JButton("Random");
|
||||||
|
randomizeButton.addActionListener(event -> randomizeSelections());
|
||||||
|
clearAllButton = new JButton("Clear all");
|
||||||
|
clearAllButton.addActionListener(event -> clearSelections());
|
||||||
|
legalModeToggle = new JCheckBox("Legal mode");
|
||||||
|
legalModeToggle.addActionListener(event -> {
|
||||||
|
boolean selected = legalModeToggle.isSelected();
|
||||||
|
|
||||||
|
// Since we're clearing illegal settings, ask for confirmation first.
|
||||||
|
if(selected && !SwingUtility.showIgnorableConfirmDialog(getRootPane(), "Enable legal mode? Illegal selections will be cleared.", "Attention")) {
|
||||||
|
legalModeToggle.setSelected(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
legalModeChanged();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create layout
|
||||||
|
setLayout(new MigLayout("align 50% 50%, gapy 0, insets 0 8 0 8"));
|
||||||
|
|
||||||
|
// Create header panels
|
||||||
|
JPanel labelPanel = new JPanel(new MigLayout("insets 0, fill"));
|
||||||
|
labelPanel.add(titleLabel, "wrap");
|
||||||
|
labelPanel.add(descriptionLabel, "wrap");
|
||||||
|
|
||||||
|
JPanel headerPanel = new JPanel(new MigLayout("insets n n 0 n", "[][grow]"));
|
||||||
|
headerPanel.add(labelPanel);
|
||||||
|
headerPanel.add(selectionIcon, "align 100%");
|
||||||
|
add(headerPanel, "wrap, grow");
|
||||||
|
|
||||||
|
// Create table
|
||||||
|
model = new DefaultTableModel(columns, rowCount);
|
||||||
|
model.addTableModelListener(event -> {
|
||||||
|
int column = event.getColumn();
|
||||||
|
|
||||||
|
if(column == 0) {
|
||||||
|
return; // Ignore number column
|
||||||
|
}
|
||||||
|
|
||||||
|
if(shouldUpdateSelectionIcon(column)) {
|
||||||
|
updateSelectionIcon();
|
||||||
|
}
|
||||||
|
|
||||||
|
for(int i = event.getFirstRow(); i <= event.getLastRow(); i++) {
|
||||||
|
int index = i * rowCount + column;
|
||||||
|
Object oldValue = oldValues.get(index);
|
||||||
|
Object newValue = model.getValueAt(i, column);
|
||||||
|
Class<?> type = columnTypes[column - 1];
|
||||||
|
|
||||||
|
// Set to null if types don't match
|
||||||
|
if(oldValue != null && !type.isAssignableFrom(oldValue.getClass())) {
|
||||||
|
oldValue = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(newValue != null && !type.isAssignableFrom(newValue.getClass())) {
|
||||||
|
newValue = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fire data changed if new value is different from old value
|
||||||
|
if(newValue != oldValue) {
|
||||||
|
dataChanged(i, column, oldValue, newValue);
|
||||||
|
oldValues.put(index, model.getValueAt(i, column)); // We use getValueAt again because dataChanged might have updated the value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
table = new ConfigTable();
|
||||||
|
table.putClientProperty("terminateEditOnFocusLost", Boolean.TRUE);
|
||||||
|
table.getTableHeader().setReorderingAllowed(false);
|
||||||
|
table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
|
||||||
|
table.setModel(model);
|
||||||
|
table.getSelectionModel().addListSelectionListener(event -> updateSelectionIcon());
|
||||||
|
int width = Math.max(table.getPreferredSize().width, table.getPreferredScrollableViewportSize().width);
|
||||||
|
table.setPreferredScrollableViewportSize(new Dimension(width, table.getRowHeight() * rowCount));
|
||||||
|
table.getColumnModel().getColumn(0).setResizable(false);
|
||||||
|
table.getColumnModel().getColumn(0).setMinWidth(32);
|
||||||
|
table.getColumnModel().getColumn(0).setMaxWidth(32);
|
||||||
|
add(new JScrollPane(table), "spanx, grow");
|
||||||
|
updateSelectionIcon();
|
||||||
|
|
||||||
|
// Initialize rows
|
||||||
|
for(int i = 0; i < rowCount; i++) {
|
||||||
|
model.setValueAt(i + 1, i, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create footer panel
|
||||||
|
JPanel footerPanel = new JPanel(new MigLayout("insets 0", "0[]"));
|
||||||
|
footerPanel.add(randomizeButton);
|
||||||
|
footerPanel.add(clearAllButton);
|
||||||
|
|
||||||
|
if(incluceLegalMode) {
|
||||||
|
footerPanel.add(legalModeToggle);
|
||||||
|
}
|
||||||
|
|
||||||
|
add(footerPanel, "spanx, gapy 4");
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract void initializeOptions(int row);
|
||||||
|
public abstract void randomizeSelections(int row);
|
||||||
|
public abstract void clearSelections(int row);
|
||||||
|
public abstract Image getSelectionIcon(int row);
|
||||||
|
|
||||||
|
public void dataChanged(int row, int column, Object oldValue, Object newValue) {
|
||||||
|
// Override
|
||||||
|
}
|
||||||
|
|
||||||
|
public void legalModeChanged() {
|
||||||
|
// Override
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean shouldUpdateSelectionIcon(int column) {
|
||||||
|
return true; // Override
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getSelectionIconScale() {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void initializeOptions() {
|
||||||
|
for(int i = 0; i < rowCount; i++) {
|
||||||
|
initializeOptions(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void randomizeSelections() {
|
||||||
|
for(int i = 0; i < rowCount; i++) {
|
||||||
|
randomizeSelections(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void clearSelections() {
|
||||||
|
for(int i = 0; i < rowCount; i++) {
|
||||||
|
clearSelections(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTitle(String text) {
|
||||||
|
titleLabel.setText(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDescription(String text) {
|
||||||
|
descriptionLabel.setText(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isLegalMode() {
|
||||||
|
return legalModeToggle.isSelected();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateSelectionIcon() {
|
||||||
|
BufferedImage image = (BufferedImage)getSelectionIcon(table.getSelectedRow());
|
||||||
|
selectionIcon.setImage(image, getSelectionIconScale(), 96, 96);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected <T> void setOptions(int row, int column, Collection<T> options) {
|
||||||
|
setOptions(row, column, options, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected <T> void setOptions(int row, int column, Collection<T> options, Comparator<T> sorter) {
|
||||||
|
setOptions(row, column, options, sorter, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected <T> void setOptions(int row, int column, Collection<T> options, boolean includeNullOption) {
|
||||||
|
setOptions(row, column, options, null, includeNullOption);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected <T> void setOptions(int row, int column, Collection<T> options, Comparator<T> sorter, boolean includeNullOption) {
|
||||||
|
List<T> list = new ArrayList<>(options);
|
||||||
|
|
||||||
|
if(sorter != null) {
|
||||||
|
list.sort(sorter);
|
||||||
|
}
|
||||||
|
|
||||||
|
table.setOptionsAt(row, column, list, includeNullOption);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected <T> List<T> computeSelectionList(Function<Integer, T> supplier, Function<Integer, Boolean> rowFilter) {
|
||||||
|
List<T> list = new ArrayList<>();
|
||||||
|
|
||||||
|
for(int i = 0; i < rowCount; i++) {
|
||||||
|
if(rowFilter.apply(i)) {
|
||||||
|
list.add(supplier.apply(i));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
}
|
||||||
272
src/main/java/entralinked/gui/panels/VisitorEditorPanel.java
Normal file
272
src/main/java/entralinked/gui/panels/VisitorEditorPanel.java
Normal file
@@ -0,0 +1,272 @@
|
|||||||
|
package entralinked.gui.panels;
|
||||||
|
|
||||||
|
import java.awt.Image;
|
||||||
|
import java.awt.Toolkit;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.IntStream;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
import javax.swing.InputVerifier;
|
||||||
|
import javax.swing.JComponent;
|
||||||
|
import javax.swing.JTextField;
|
||||||
|
|
||||||
|
import entralinked.GameVersion;
|
||||||
|
import entralinked.gui.ImageLoader;
|
||||||
|
import entralinked.gui.InputVerifierCellEditor;
|
||||||
|
import entralinked.gui.data.Country;
|
||||||
|
import entralinked.gui.data.DataManager;
|
||||||
|
import entralinked.gui.data.PkmnSpecies;
|
||||||
|
import entralinked.gui.data.Region;
|
||||||
|
import entralinked.model.avenue.AvenueShopType;
|
||||||
|
import entralinked.model.avenue.AvenueVisitor;
|
||||||
|
import entralinked.model.avenue.AvenueVisitorType;
|
||||||
|
import entralinked.model.player.Player;
|
||||||
|
|
||||||
|
@SuppressWarnings("serial")
|
||||||
|
public class VisitorEditorPanel extends TableEditorPanel {
|
||||||
|
|
||||||
|
public static final int VISITOR_COUNT = 12;
|
||||||
|
public static final int TYPE_COLUMN = 1;
|
||||||
|
public static final int NAME_COLUMN = 2;
|
||||||
|
public static final int SHOP_COLUMN = 3;
|
||||||
|
public static final int GAME_COLUMN = 4;
|
||||||
|
public static final int COUNTRY_COLUMN = 5;
|
||||||
|
public static final int REGION_COLUMN = 6;
|
||||||
|
public static final int PHRASE_COLUMN = 7;
|
||||||
|
public static final int DREAMER_COLUMN = 8;
|
||||||
|
private static final String[] COLUMN_NAMES = { "Type", "Name", "Shop", "Game", "Country", "Region", "Phrase", "Pokémon" };
|
||||||
|
private static final Class<?>[] COLUMN_TYPES = { AvenueVisitorType.class, String.class, AvenueShopType.class, GameVersion.class, Country.class, Region.class, Integer.class, PkmnSpecies.class };
|
||||||
|
|
||||||
|
// Name tables for randomization
|
||||||
|
// TODO more languages would be neat but isn't really necessary
|
||||||
|
private static final String[] ENG_NAMES_M = { "Aleron", "Alpi", "Amando", "Anselmi", "Anton", "Arhippa", "Arpo", "Artos", "Assar", "Atilio", "Axel", "Azzo", "Jasper", "Jaylen", "Jephew", "Jimbo", "Joakim", "Joelle", "Jonas", "Jule", "Julian", "Julio", "Justan" };
|
||||||
|
private static final String[] ENG_NAMES_F = { "Agata", "Ainikki", "Alena", "Alibena", "Alwyn", "Anelma", "Anneli", "Annetta", "Antonie", "Armina", "Assunta", "Asta", "Jaclyn", "Jane", "Janette", "Jannis", "Jeanne", "Jenna", "Jess", "Joan", "Jocelyn", "Josie", "Judith", "Julie" };
|
||||||
|
private static final String[] JAP_NAMES_M = { "アートス", "アーポ", "アクセル", "アッサール", "アッツォ", "アッラン", "アティリオ", "アマンド", "アルピ", "アルヒッパ", "アンセルミ", "アントン", "ジェヒュー", "ジェリー", "ジェローム", "ジャコブ", "ジャスパー", "ジャレッド", "ジャン", "ジュール", "ジョエル", "ジョシュア", "ジョナス" };
|
||||||
|
private static final String[] JAP_NAMES_F = { "アーム", "アイニッキ", "アガタ", "アスタ", "アッスンタ", "アネルマ", "アラベッラ", "アリビーナ", "アレーナ", "アントニナ", "アンネッタ", "アンネリ", "ジェーン", "ジェシー", "ジェナ", "ジャサント", "ジャニス", "ジャンヌ", "ジュディス", "ジュリー", "ジョアン", "ジョイス", "ジョスリン", "ジョゼ" };
|
||||||
|
|
||||||
|
public VisitorEditorPanel() {
|
||||||
|
super(VISITOR_COUNT, false, COLUMN_NAMES, COLUMN_TYPES);
|
||||||
|
setTitle("Join Avenue");
|
||||||
|
setDescription("""
|
||||||
|
<html>
|
||||||
|
Use the table below to configure Join Avenue visitors.<br/>
|
||||||
|
Please be aware that new visitors will not appear if you have<br/>
|
||||||
|
already reached the max amount of concurrent visitors.
|
||||||
|
</html>
|
||||||
|
""");
|
||||||
|
table.setCellRenderers(TYPE_COLUMN, AvenueVisitorType.class, AvenueVisitorType::getDisplayName, "— — — — —");
|
||||||
|
table.setCellRenderers(SHOP_COLUMN, AvenueShopType.class, AvenueShopType::getDisplayName);
|
||||||
|
table.setCellRenderers(GAME_COLUMN, GameVersion.class, GameVersion::getDisplayName);
|
||||||
|
table.setCellRenderers(COUNTRY_COLUMN, Country.class, Country::name);
|
||||||
|
table.setCellRenderers(REGION_COLUMN, Region.class, Region::name);
|
||||||
|
table.setCellRenderers(DREAMER_COLUMN, PkmnSpecies.class, PkmnSpecies::name);
|
||||||
|
initializeOptions();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void dataChanged(int row, int column, Object oldValue, Object newValue) {
|
||||||
|
if(column == TYPE_COLUMN) {
|
||||||
|
if(newValue != null) {
|
||||||
|
if(oldValue != null) {
|
||||||
|
return; // Do nothing if new and old values are both non-null
|
||||||
|
}
|
||||||
|
|
||||||
|
table.enableOption(row, SHOP_COLUMN);
|
||||||
|
table.enableOption(row, GAME_COLUMN, GameVersion.BLACK_2_ENGLISH); // Updates country -> region
|
||||||
|
table.enableOption(row, PHRASE_COLUMN);
|
||||||
|
table.enableOption(row, DREAMER_COLUMN);
|
||||||
|
table.enableOption(row, NAME_COLUMN, findRandomName(row));
|
||||||
|
model.setValueAt(findRandomName(row), row, NAME_COLUMN);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
disableSecondaryOptions(row);
|
||||||
|
} else if(column == GAME_COLUMN) {
|
||||||
|
if(newValue == null) {
|
||||||
|
table.disableOption(row, COUNTRY_COLUMN);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
GameVersion newVersion = (GameVersion)newValue;
|
||||||
|
boolean shouldUpdate = oldValue == null; // Update by default if previous value was null
|
||||||
|
|
||||||
|
// Check if language code changed from or to Japanese
|
||||||
|
if(oldValue != null) {
|
||||||
|
GameVersion oldVersion = (GameVersion)oldValue;
|
||||||
|
shouldUpdate |= (oldVersion.getLanguageCode() == 1 && newVersion.getLanguageCode() != 1)
|
||||||
|
|| (oldVersion.getLanguageCode() != 1 && newVersion.getLanguageCode() == 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(shouldUpdate) {
|
||||||
|
// Japanese language visitors seem to only be able to appear if their set country is Japan
|
||||||
|
setOptions(row, COUNTRY_COLUMN, newVersion.getLanguageCode() == 1 ? Arrays.asList(DataManager.getCountry(105)) : DataManager.getCountries());
|
||||||
|
table.enableOption(row, COUNTRY_COLUMN);
|
||||||
|
}
|
||||||
|
} else if(column == COUNTRY_COLUMN) {
|
||||||
|
if(newValue == null) {
|
||||||
|
table.disableOption(row, REGION_COLUMN);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Country country = (Country)newValue;
|
||||||
|
setOptions(row, REGION_COLUMN, country.hasRegions() ? country.regions() : Collections.emptyList(), (a, b) -> a.name().compareTo(b.name()));
|
||||||
|
table.enableOption(row, REGION_COLUMN);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void initializeOptions(int row) {
|
||||||
|
setOptions(row, TYPE_COLUMN, Arrays.asList(AvenueVisitorType.values()), true);
|
||||||
|
setOptions(row, SHOP_COLUMN, Arrays.asList(AvenueShopType.values()));
|
||||||
|
setOptions(row, GAME_COLUMN, Arrays.asList(GameVersion.values()), (a, b) -> Integer.compare(a.getLanguageCode(), b.getLanguageCode()));
|
||||||
|
setOptions(row, PHRASE_COLUMN, IntStream.rangeClosed(0, 7).boxed().toList());
|
||||||
|
setOptions(row, DREAMER_COLUMN, DataManager.getSpecies(), (a, b) -> a.name().compareTo(b.name()));
|
||||||
|
InputVerifier nameVerifier = new InputVerifier() {
|
||||||
|
@Override
|
||||||
|
public boolean verify(JComponent input) {
|
||||||
|
String text = ((JTextField)input).getText();
|
||||||
|
String error = checkName(text, row);
|
||||||
|
|
||||||
|
if(error != null) {
|
||||||
|
// TODO show hint
|
||||||
|
Toolkit.getDefaultToolkit().beep();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
table.setCellEditor(row, NAME_COLUMN, new InputVerifierCellEditor(nameVerifier));
|
||||||
|
disableSecondaryOptions(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void randomizeSelections(int row) {
|
||||||
|
table.randomizeSelection(row, TYPE_COLUMN, false);
|
||||||
|
table.randomizeSelection(row, SHOP_COLUMN);
|
||||||
|
table.randomizeSelection(row, GAME_COLUMN);
|
||||||
|
table.randomizeSelection(row, COUNTRY_COLUMN);
|
||||||
|
table.randomizeSelection(row, REGION_COLUMN);
|
||||||
|
table.randomizeSelection(row, PHRASE_COLUMN);
|
||||||
|
table.randomizeSelection(row, DREAMER_COLUMN);
|
||||||
|
model.setValueAt(findRandomName(row), row, NAME_COLUMN);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void clearSelections(int row) {
|
||||||
|
model.setValueAt(null, row, TYPE_COLUMN);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Image getSelectionIcon(int row) {
|
||||||
|
String name = row == -1 || getType(row) == null ? "none" : getType(row).name().toLowerCase();
|
||||||
|
return ImageLoader.getImage("/sprites/trainers/%s.png".formatted(name));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void loadProfile(Player player) {
|
||||||
|
table.clearSelection();
|
||||||
|
clearSelections();
|
||||||
|
int row = 0;
|
||||||
|
|
||||||
|
for(AvenueVisitor visitor : player.getAvenueVisitors()) {
|
||||||
|
model.setValueAt(visitor.type(), row, TYPE_COLUMN);
|
||||||
|
model.setValueAt(visitor.name(), row, NAME_COLUMN);
|
||||||
|
model.setValueAt(visitor.shopType(), row, SHOP_COLUMN);
|
||||||
|
model.setValueAt(visitor.gameVersion(), row, GAME_COLUMN);
|
||||||
|
Country country = DataManager.getCountry(visitor.countryCode());
|
||||||
|
model.setValueAt(country, row, COUNTRY_COLUMN);
|
||||||
|
|
||||||
|
if(country.hasRegions()) {
|
||||||
|
model.setValueAt(country.regions().get(visitor.stateProvinceCode() - 1), row, REGION_COLUMN);
|
||||||
|
}
|
||||||
|
|
||||||
|
model.setValueAt(visitor.personality(), row, PHRASE_COLUMN);
|
||||||
|
model.setValueAt(DataManager.getSpecies(visitor.dreamerSpecies()), row, DREAMER_COLUMN);
|
||||||
|
row++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void saveProfile(Player player) {
|
||||||
|
player.setAvenueVisitors(computeSelectionList(i -> new AvenueVisitor(
|
||||||
|
getName(i), getType(i), getShop(i), getGameVersion(i), getCountry(i).id(), getRegion(i), getPhrase(i), getDreamer(i).id()),
|
||||||
|
i -> getType(i) != null));
|
||||||
|
}
|
||||||
|
|
||||||
|
private String findRandomName(int row) {
|
||||||
|
boolean japanese = getGameVersion(row).getLanguageCode() == 1;
|
||||||
|
String[] table = null;
|
||||||
|
|
||||||
|
if(getType(row).isFemale()) {
|
||||||
|
table = japanese ? JAP_NAMES_F : ENG_NAMES_F;
|
||||||
|
} else {
|
||||||
|
table = japanese ? JAP_NAMES_M : ENG_NAMES_M;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> existingNames = computeSelectionList(this::getName, i -> getType(row) != null);
|
||||||
|
List<String> options = Stream.of(table).filter(x -> !existingNames.contains(x)).toList();
|
||||||
|
return options.get((int)(Math.random() * options.size())); // Just make sure there are enough name options in the tables...
|
||||||
|
}
|
||||||
|
|
||||||
|
private String checkName(String name, int ignoreRow) {
|
||||||
|
if(name.isBlank()) {
|
||||||
|
return "Visitor name can't be blank.";
|
||||||
|
}
|
||||||
|
|
||||||
|
if(name.length() > 7) {
|
||||||
|
return "Visitor name can't exceed 7 characters.";
|
||||||
|
}
|
||||||
|
|
||||||
|
for(int i = 0; i < VISITOR_COUNT; i++) {
|
||||||
|
if(i != ignoreRow && name.equals(getName(i))) {
|
||||||
|
return "Visitors can't have the same name.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void disableSecondaryOptions(int row) {
|
||||||
|
table.disableOption(row, NAME_COLUMN);
|
||||||
|
table.disableOption(row, SHOP_COLUMN);
|
||||||
|
table.disableOption(row, GAME_COLUMN);
|
||||||
|
// table.disableOption(row, COUNTRY_COLUMN);
|
||||||
|
// table.disableOption(row, REGION_COLUMN);
|
||||||
|
table.disableOption(row, PHRASE_COLUMN);
|
||||||
|
table.disableOption(row, DREAMER_COLUMN);
|
||||||
|
}
|
||||||
|
|
||||||
|
private AvenueVisitorType getType(int row) {
|
||||||
|
return table.getValueAt(row, TYPE_COLUMN, AvenueVisitorType.class, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getName(int row) {
|
||||||
|
return table.getValueAt(row, NAME_COLUMN, String.class, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
private AvenueShopType getShop(int row) {
|
||||||
|
return table.getValueAt(row, SHOP_COLUMN, AvenueShopType.class, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private GameVersion getGameVersion(int row) {
|
||||||
|
return table.getValueAt(row, GAME_COLUMN, GameVersion.class, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Country getCountry(int row) {
|
||||||
|
return table.getValueAt(row, COUNTRY_COLUMN, Country.class, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private int getRegion(int row) {
|
||||||
|
Region region = table.getValueAt(row, REGION_COLUMN, Region.class, null);
|
||||||
|
return region == null ? 0 : region.id();
|
||||||
|
}
|
||||||
|
|
||||||
|
private int getPhrase(int row) {
|
||||||
|
return table.getValueAt(row, PHRASE_COLUMN, Integer.class, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private PkmnSpecies getDreamer(int row) {
|
||||||
|
return table.getValueAt(row, DREAMER_COLUMN, PkmnSpecies.class, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
256
src/main/java/entralinked/gui/view/MainView.java
Normal file
256
src/main/java/entralinked/gui/view/MainView.java
Normal file
@@ -0,0 +1,256 @@
|
|||||||
|
package entralinked.gui.view;
|
||||||
|
|
||||||
|
import java.awt.BorderLayout;
|
||||||
|
import java.awt.Color;
|
||||||
|
import java.awt.Desktop;
|
||||||
|
import java.awt.Dimension;
|
||||||
|
import java.awt.Font;
|
||||||
|
import java.awt.event.WindowAdapter;
|
||||||
|
import java.awt.event.WindowEvent;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.URISyntaxException;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.StandardCopyOption;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
|
||||||
|
import javax.swing.BorderFactory;
|
||||||
|
import javax.swing.ImageIcon;
|
||||||
|
import javax.swing.JFrame;
|
||||||
|
import javax.swing.JLabel;
|
||||||
|
import javax.swing.JMenu;
|
||||||
|
import javax.swing.JMenuBar;
|
||||||
|
import javax.swing.JOptionPane;
|
||||||
|
import javax.swing.JPanel;
|
||||||
|
import javax.swing.JScrollPane;
|
||||||
|
import javax.swing.JTabbedPane;
|
||||||
|
import javax.swing.JTextPane;
|
||||||
|
import javax.swing.UIManager;
|
||||||
|
import javax.swing.text.AttributeSet;
|
||||||
|
import javax.swing.text.BadLocationException;
|
||||||
|
import javax.swing.text.DefaultCaret;
|
||||||
|
import javax.swing.text.Document;
|
||||||
|
import javax.swing.text.SimpleAttributeSet;
|
||||||
|
import javax.swing.text.StyleConstants;
|
||||||
|
import javax.swing.text.StyleContext;
|
||||||
|
|
||||||
|
import org.apache.logging.log4j.Level;
|
||||||
|
|
||||||
|
import com.formdev.flatlaf.intellijthemes.FlatOneDarkIJTheme;
|
||||||
|
import com.formdev.flatlaf.util.ColorFunctions;
|
||||||
|
|
||||||
|
import entralinked.Entralinked;
|
||||||
|
import entralinked.GameVersion;
|
||||||
|
import entralinked.gui.FileChooser;
|
||||||
|
import entralinked.gui.panels.DashboardPanel;
|
||||||
|
import entralinked.model.player.Player;
|
||||||
|
import entralinked.model.player.PlayerManager;
|
||||||
|
import entralinked.utility.ConsumerAppender;
|
||||||
|
import entralinked.utility.GsidUtility;
|
||||||
|
import entralinked.utility.LEInputStream;
|
||||||
|
import entralinked.utility.SwingUtility;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simple Swing user interface.
|
||||||
|
*/
|
||||||
|
public class MainView {
|
||||||
|
|
||||||
|
public static final Color TEXT_COLOR = Color.WHITE.darker();
|
||||||
|
public static final Color TEXT_COLOR_WARN = Color.YELLOW.darker();
|
||||||
|
public static final Color TEXT_COLOR_ERROR = Color.RED.darker();
|
||||||
|
private final StyleContext styleContext = StyleContext.getDefaultStyleContext();
|
||||||
|
private final AttributeSet fontAttribute = styleContext.addAttribute(SimpleAttributeSet.EMPTY, StyleConstants.FontFamily, "Consolas");
|
||||||
|
private final Entralinked entralinked;
|
||||||
|
private final JFrame frame;
|
||||||
|
private final JLabel statusLabel;
|
||||||
|
|
||||||
|
public MainView(Entralinked entralinked) {
|
||||||
|
this.entralinked = entralinked;
|
||||||
|
|
||||||
|
// Set look and feel
|
||||||
|
FlatOneDarkIJTheme.setup();
|
||||||
|
UIManager.getDefaults().put("Component.focusedBorderColor", UIManager.get("Component.borderColor"));
|
||||||
|
UIManager.put("Table.alternateRowColor", ColorFunctions.lighten(UIManager.getColor("Table.background"), 0.05F));
|
||||||
|
|
||||||
|
// Create status label
|
||||||
|
statusLabel = new JLabel("Servers are starting, please wait a bit...", JLabel.CENTER);
|
||||||
|
|
||||||
|
// Create footer panel
|
||||||
|
JPanel footerPanel = new JPanel(new BorderLayout());
|
||||||
|
footerPanel.add(statusLabel, BorderLayout.CENTER);
|
||||||
|
footerPanel.setBorder(BorderFactory.createEmptyBorder(2, 0, 5, 0));
|
||||||
|
|
||||||
|
// Create console output
|
||||||
|
JTextPane consoleOutputPane = new JTextPane() {
|
||||||
|
@Override
|
||||||
|
public boolean getScrollableTracksViewportWidth() {
|
||||||
|
return getPreferredSize().width <= getParent().getSize().width;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Dimension getPreferredSize() {
|
||||||
|
return getUI().getPreferredSize(this);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
consoleOutputPane.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12));
|
||||||
|
consoleOutputPane.setEditable(false);
|
||||||
|
((DefaultCaret)consoleOutputPane.getCaret()).setUpdatePolicy(DefaultCaret.ALWAYS_UPDATE);
|
||||||
|
|
||||||
|
// Create console output appender
|
||||||
|
ConsumerAppender.addConsumer("GuiOutput", message -> {
|
||||||
|
Document document = consoleOutputPane.getDocument();
|
||||||
|
Level level = message.level();
|
||||||
|
Color color = level == Level.ERROR ? TEXT_COLOR_ERROR : level == Level.WARN ? TEXT_COLOR_WARN : TEXT_COLOR;
|
||||||
|
AttributeSet colorAttribute = styleContext.addAttribute(fontAttribute, StyleConstants.Foreground, color);
|
||||||
|
|
||||||
|
try {
|
||||||
|
consoleOutputPane.getDocument().insertString(document.getLength(), message.formattedMessage(), colorAttribute);
|
||||||
|
} catch(BadLocationException e) {}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create console output scroll pane
|
||||||
|
JScrollPane scrollPane = new JScrollPane();
|
||||||
|
scrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS);
|
||||||
|
scrollPane.setViewportView(consoleOutputPane);
|
||||||
|
|
||||||
|
// Create main panel
|
||||||
|
JPanel panel = new JPanel(new BorderLayout());
|
||||||
|
panel.add(scrollPane, BorderLayout.CENTER);
|
||||||
|
panel.add(footerPanel, BorderLayout.PAGE_END);
|
||||||
|
|
||||||
|
// Create tabbed pane
|
||||||
|
JTabbedPane tabbedPane = new JTabbedPane();
|
||||||
|
tabbedPane.addTab("Console", panel);
|
||||||
|
tabbedPane.addTab("Dashboard", new DashboardPanel(entralinked));
|
||||||
|
|
||||||
|
// Create window
|
||||||
|
frame = new JFrame("Entralinked");
|
||||||
|
|
||||||
|
// Create menu bar
|
||||||
|
JMenuBar menuBar = new JMenuBar();
|
||||||
|
JMenu toolsMenu = new JMenu("Tools");
|
||||||
|
toolsMenu.add(SwingUtility.createAction("Import save file (Memory Link)", () -> FileChooser.showFileOpenDialog(frame, selection -> {
|
||||||
|
importSaveFile(selection.file());
|
||||||
|
})));
|
||||||
|
menuBar.add(toolsMenu);
|
||||||
|
JMenu helpMenu = new JMenu("Help");
|
||||||
|
helpMenu.add(SwingUtility.createAction("Update PID (Error 60000)", () -> new PidToolDialog(entralinked, frame)));
|
||||||
|
helpMenu.add(SwingUtility.createAction("GitHub", () -> {
|
||||||
|
try {
|
||||||
|
Desktop.getDesktop().browse(new URL("https://github.com/kuroppoi/entralinked").toURI());
|
||||||
|
} catch(IOException | URISyntaxException e) {
|
||||||
|
SwingUtility.showExceptionInfo(frame, "Failed to open URL.", e);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
menuBar.add(helpMenu);
|
||||||
|
|
||||||
|
// Set window properties
|
||||||
|
frame.addWindowListener(new WindowAdapter() {
|
||||||
|
@Override
|
||||||
|
public void windowClosing(WindowEvent event) {
|
||||||
|
// Update status
|
||||||
|
statusLabel.setText("Servers are shutting down, please wait a bit...");
|
||||||
|
|
||||||
|
// Run asynchronously so it doesn't just awkwardly freeze
|
||||||
|
// Still scuffed but better than nothing I guess
|
||||||
|
CompletableFuture.runAsync(() -> {
|
||||||
|
entralinked.stopServers();
|
||||||
|
System.exit(0);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
frame.setIconImages(List.of(
|
||||||
|
new ImageIcon(getClass().getResource("/icon-64x.png")).getImage(),
|
||||||
|
new ImageIcon(getClass().getResource("/icon-32x.png")).getImage(),
|
||||||
|
new ImageIcon(getClass().getResource("/icon-16x.png")).getImage()));
|
||||||
|
frame.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
|
||||||
|
frame.setJMenuBar(menuBar);
|
||||||
|
frame.add(tabbedPane);
|
||||||
|
frame.getContentPane().setPreferredSize(new Dimension(733, 463));
|
||||||
|
frame.pack();
|
||||||
|
frame.setMinimumSize(frame.getSize());
|
||||||
|
frame.setLocationRelativeTo(null);
|
||||||
|
frame.setVisible(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setStatusLabelText(String text) {
|
||||||
|
statusLabel.setText(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void importSaveFile(File file) {
|
||||||
|
// Check file size
|
||||||
|
if(file.length() != 524288) {
|
||||||
|
JOptionPane.showMessageDialog(frame, "Invalid file length.\n"
|
||||||
|
+ "Expected 524288 bytes, got %s.".formatted(file.length()), "Attention", JOptionPane.WARNING_MESSAGE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
PlayerManager playerManager = entralinked.getPlayerManager();
|
||||||
|
Player player = null;
|
||||||
|
GameVersion version = null;
|
||||||
|
|
||||||
|
try(LEInputStream inputStream = new LEInputStream(new FileInputStream(file))) {
|
||||||
|
inputStream.skipNBytes(0x19400); // Skip to trainer info
|
||||||
|
inputStream.skipNBytes(0x4);
|
||||||
|
String name = inputStream.readUTF16(7);
|
||||||
|
inputStream.skipNBytes(0x2);
|
||||||
|
int trainerId = inputStream.readInt();
|
||||||
|
int profileId = inputStream.readInt();
|
||||||
|
inputStream.skipNBytes(0x2);
|
||||||
|
int language = inputStream.read();
|
||||||
|
int romCode = inputStream.read();
|
||||||
|
version = GameVersion.lookup(romCode, language);
|
||||||
|
|
||||||
|
// Check game version
|
||||||
|
if(version == null || version.isVersion2()) {
|
||||||
|
JOptionPane.showMessageDialog(frame, "This is not a Black & White save file.", "Attention", JOptionPane.WARNING_MESSAGE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String gameSyncId = GsidUtility.stringifyGameSyncId(profileId == 0 ? Objects.hash(name, trainerId) & 0x7FFFFFFF : profileId);
|
||||||
|
player = playerManager.doesPlayerExist(gameSyncId) ? playerManager.getPlayer(gameSyncId) : playerManager.registerPlayer(gameSyncId, version);
|
||||||
|
} catch(Exception e) {
|
||||||
|
SwingUtility.showExceptionInfo(frame, "Failed to read save data.", e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if player exists
|
||||||
|
if(player == null) {
|
||||||
|
JOptionPane.showMessageDialog(frame, "Failed to create player data.", "Attention", JOptionPane.WARNING_MESSAGE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check version mismatch
|
||||||
|
if(player.getGameVersion() != version) {
|
||||||
|
if(!SwingUtility.showIgnorableConfirmDialog(frame,
|
||||||
|
"The game version stored in the profile data does not match.\n"
|
||||||
|
+ "Do you want to overwrite it and import the save file anyway?", "Attention")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
player.setGameVersion(version);
|
||||||
|
|
||||||
|
// Try to save player data
|
||||||
|
if(!playerManager.savePlayer(player)) {
|
||||||
|
JOptionPane.showMessageDialog(frame, "Failed to save player data.", "Attention", JOptionPane.WARNING_MESSAGE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy save file
|
||||||
|
try {
|
||||||
|
Files.copy(file.toPath(), player.getSaveFile().toPath(), StandardCopyOption.REPLACE_EXISTING);
|
||||||
|
} catch(Exception e) {
|
||||||
|
SwingUtility.showExceptionInfo(frame, "Failed to import save data.", e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
JOptionPane.showMessageDialog(frame, "Save file has been imported successfully!\n"
|
||||||
|
+ "You can now use Memory Link with the following Game Sync ID:\n\n%s".formatted(player.getGameSyncId()),
|
||||||
|
"Attention", JOptionPane.INFORMATION_MESSAGE);
|
||||||
|
}
|
||||||
|
}
|
||||||
117
src/main/java/entralinked/gui/view/PidToolDialog.java
Normal file
117
src/main/java/entralinked/gui/view/PidToolDialog.java
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
package entralinked.gui.view;
|
||||||
|
|
||||||
|
import java.awt.GridBagLayout;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
import javax.swing.BorderFactory;
|
||||||
|
import javax.swing.JButton;
|
||||||
|
import javax.swing.JDialog;
|
||||||
|
import javax.swing.JFrame;
|
||||||
|
import javax.swing.JLabel;
|
||||||
|
import javax.swing.JOptionPane;
|
||||||
|
import javax.swing.JPanel;
|
||||||
|
|
||||||
|
import com.formdev.flatlaf.extras.components.FlatTextField;
|
||||||
|
|
||||||
|
import entralinked.Entralinked;
|
||||||
|
import entralinked.model.user.User;
|
||||||
|
import entralinked.utility.MD5;
|
||||||
|
import entralinked.utility.SwingUtility;
|
||||||
|
|
||||||
|
public class PidToolDialog {
|
||||||
|
|
||||||
|
public static final Pattern WFC_ID_PATTERN = Pattern.compile("[0-9]{16}");
|
||||||
|
public static final Pattern FRIEND_CODE_PATTERN = Pattern.compile("[0-9]{12}");
|
||||||
|
|
||||||
|
public PidToolDialog(Entralinked entralinked, JFrame frame) {
|
||||||
|
// Create dialog
|
||||||
|
JDialog dialog = new JDialog(frame, "PID Tool");
|
||||||
|
|
||||||
|
// Create input fields
|
||||||
|
FlatTextField wfcIdField = new FlatTextField();
|
||||||
|
wfcIdField.setPlaceholderText("XXXX-XXXX-XXXX-XXXX");
|
||||||
|
FlatTextField friendCodeField = new FlatTextField();
|
||||||
|
friendCodeField.setPlaceholderText("XXXX-XXXX-XXXX");
|
||||||
|
|
||||||
|
// Create logic
|
||||||
|
JButton updateButton = new JButton("Update");
|
||||||
|
updateButton.addActionListener(event -> {
|
||||||
|
if(!entralinked.isInitialized()) {
|
||||||
|
JOptionPane.showMessageDialog(dialog, "Please wait for Entralinked to finish starting.", "Attention", JOptionPane.WARNING_MESSAGE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String userId = wfcIdField.getText().replace("-", "").replaceAll("\\s+", "");
|
||||||
|
String friendCodeString = friendCodeField.getText().replace("-", "").replaceAll("\\s+", "");
|
||||||
|
|
||||||
|
// Make sure WFC ID is valid
|
||||||
|
if(!WFC_ID_PATTERN.matcher(userId).matches()) {
|
||||||
|
JOptionPane.showMessageDialog(dialog, "Please enter a valid Wi-Fi Connection ID.", "Attention", JOptionPane.WARNING_MESSAGE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure Friend Code is valid
|
||||||
|
if(!FRIEND_CODE_PATTERN.matcher(friendCodeString).matches()) {
|
||||||
|
JOptionPane.showMessageDialog(dialog, "Please enter a valid Friend Code.", "Attention", JOptionPane.WARNING_MESSAGE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
User user = entralinked.getUserManager().getUser(userId.substring(0, 13));
|
||||||
|
|
||||||
|
// Make sure user exists
|
||||||
|
if(user == null) {
|
||||||
|
JOptionPane.showMessageDialog(dialog, "This Wi-Fi Connection ID does not exist.\n"
|
||||||
|
+ "If you haven't attempted to connect yet, please do that first.", "Attention", JOptionPane.WARNING_MESSAGE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
long friendCode = Long.parseLong(friendCodeString);
|
||||||
|
int profileId = (int)(friendCode & 0x7FFFFFFF);
|
||||||
|
int checksum = (int)(friendCode >> 32);
|
||||||
|
|
||||||
|
// Compute friend code checksum
|
||||||
|
byte[] buffer = {0x00, 0x00, 0x00, 0x00, 0x4A, 0x41, 0x52, 0x49}; // Last 4 bytes is inverted game code (IRAJ)
|
||||||
|
buffer[0] = (byte)(profileId & 0xFF);
|
||||||
|
buffer[1] = (byte)(profileId >> 8 & 0xFF);
|
||||||
|
buffer[2] = (byte)(profileId >> 16 & 0xFF);
|
||||||
|
buffer[3] = (byte)(profileId >> 24 & 0xFF);
|
||||||
|
byte[] hash = MD5.digest(buffer);
|
||||||
|
int computedChecksum = hash[0] >> 1 & 0x7F;
|
||||||
|
|
||||||
|
// Compare checksums
|
||||||
|
if(computedChecksum != checksum) {
|
||||||
|
JOptionPane.showMessageDialog(dialog, "This is not a valid Friend Code. Please check for typos.", "Attention", JOptionPane.WARNING_MESSAGE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Everything checks out -- update the profile id!
|
||||||
|
user.setProfileIdOverride(profileId);
|
||||||
|
JOptionPane.showMessageDialog(dialog,
|
||||||
|
"All done! Please restart your game and use Game Sync.\nGame profile data will be updated and saved once you do so.");
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create content panel
|
||||||
|
JPanel panel = new JPanel(new GridBagLayout());
|
||||||
|
String infoLabel = """
|
||||||
|
<html>
|
||||||
|
Enter the Wi-Fi Connection ID found in the internet settings of your DS<br>
|
||||||
|
as well as your Friend Code which you can view in-game using the Pal Pad.<br>
|
||||||
|
Confirm that the input data is correct and press 'Update'<br>
|
||||||
|
</html>
|
||||||
|
""";
|
||||||
|
panel.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
|
||||||
|
panel.add(new JLabel(infoLabel), SwingUtility.createConstraints(0, 0, 2, 1));
|
||||||
|
panel.add(new JLabel("Wi-Fi Connection ID"), SwingUtility.createConstraints(0, 1, 1, 1, 0, 1));
|
||||||
|
panel.add(wfcIdField, SwingUtility.createConstraints(1, 1));
|
||||||
|
panel.add(new JLabel("Friend Code"), SwingUtility.createConstraints(0, 2, 1, 1, 0, 1));
|
||||||
|
panel.add(friendCodeField, SwingUtility.createConstraints(1, 2));
|
||||||
|
panel.add(updateButton, SwingUtility.createConstraints(0, 3, 2, 1));
|
||||||
|
|
||||||
|
// Set dialog properties
|
||||||
|
dialog.setResizable(false);
|
||||||
|
dialog.add(panel);
|
||||||
|
dialog.pack();
|
||||||
|
dialog.setLocationRelativeTo(frame);
|
||||||
|
dialog.setVisible(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,11 +5,21 @@ import com.fasterxml.jackson.annotation.JsonEnumDefaultValue;
|
|||||||
public enum AvenueShopType {
|
public enum AvenueShopType {
|
||||||
|
|
||||||
@JsonEnumDefaultValue
|
@JsonEnumDefaultValue
|
||||||
RAFFLE,
|
RAFFLE("Raffle Shop"),
|
||||||
FLORIST,
|
FLORIST("Flower Shop"),
|
||||||
SALON,
|
SALON("Beauty Salon"),
|
||||||
ANTIQUE,
|
ANTIQUE("Antique Shop"),
|
||||||
DOJO,
|
DOJO("Dojo"),
|
||||||
CAFE,
|
CAFE("Café"),
|
||||||
MARKET
|
MARKET("Market");
|
||||||
|
|
||||||
|
private final String displayName;
|
||||||
|
|
||||||
|
private AvenueShopType(String displayName) {
|
||||||
|
this.displayName = displayName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDisplayName() {
|
||||||
|
return displayName;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,47 +6,53 @@ public enum AvenueVisitorType {
|
|||||||
|
|
||||||
// 0
|
// 0
|
||||||
@JsonEnumDefaultValue
|
@JsonEnumDefaultValue
|
||||||
YOUNGSTER(0),
|
YOUNGSTER("Youngster", 0),
|
||||||
LASS(0, true),
|
LASS("Lass", 0, true),
|
||||||
|
|
||||||
// 1
|
// 1
|
||||||
ACE_TRAINER_MALE(1),
|
ACE_TRAINER_MALE("Ace Trainer♂", 1),
|
||||||
ACE_TRAINER_FEMALE(1, true),
|
ACE_TRAINER_FEMALE("Ace Trainer♀", 1, true),
|
||||||
|
|
||||||
// 2
|
// 2
|
||||||
RANGER_MALE(2),
|
RANGER_MALE("Pokémon Ranger♂", 2),
|
||||||
RANGER_FEMALE(2, true),
|
RANGER_FEMALE("Pokémon Ranger♀", 2, true),
|
||||||
|
|
||||||
// 3
|
// 3
|
||||||
BREEDER_MALE(3),
|
BREEDER_MALE("Pokémon Breeder♂", 3),
|
||||||
BREEDER_FEMALE(3, true),
|
BREEDER_FEMALE("Pokémon Breeder♀", 3, true),
|
||||||
|
|
||||||
// 4
|
// 4
|
||||||
SCIENTIST_MALE(4),
|
SCIENTIST_MALE("Scientist♂", 4),
|
||||||
SCIENTIST_FEMALE(4, true),
|
SCIENTIST_FEMALE("Scientist♀", 4, true),
|
||||||
|
|
||||||
// 5
|
// 5
|
||||||
HIKER(5),
|
HIKER("Hiker", 5),
|
||||||
PARASOL_LADY(5, true),
|
PARASOL_LADY("Parasol Lady", 5, true),
|
||||||
|
|
||||||
// 6
|
// 6
|
||||||
ROUGHNECK(6),
|
ROUGHNECK("Roughneck", 6),
|
||||||
NURSE(6, true),
|
NURSE("Nurse", 6, true),
|
||||||
|
|
||||||
// 7
|
// 7
|
||||||
PRESCHOOLER_MALE(7),
|
PRESCHOOLER_MALE("Preschooler♂", 7),
|
||||||
PRESCHOOLER_FEMALE(7, true);
|
PRESCHOOLER_FEMALE("Preschooler♀", 7, true);
|
||||||
|
|
||||||
|
private final String displayName;
|
||||||
private final int clientId;
|
private final int clientId;
|
||||||
private final boolean female;
|
private final boolean female;
|
||||||
|
|
||||||
private AvenueVisitorType(int clientId, boolean female) {
|
private AvenueVisitorType(String displayName, int clientId, boolean female) {
|
||||||
|
this.displayName = displayName;
|
||||||
this.clientId = clientId;
|
this.clientId = clientId;
|
||||||
this.female = female;
|
this.female = female;
|
||||||
}
|
}
|
||||||
|
|
||||||
private AvenueVisitorType(int clientId) {
|
private AvenueVisitorType(String displayName, int clientId) {
|
||||||
this(clientId, false);
|
this(displayName, clientId, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDisplayName() {
|
||||||
|
return displayName;
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getClientId() {
|
public int getClientId() {
|
||||||
|
|||||||
@@ -6,11 +6,10 @@ 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.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;
|
||||||
|
|
||||||
@@ -22,7 +21,7 @@ import entralinked.utility.Crc16;
|
|||||||
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() {
|
||||||
@@ -80,29 +79,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 +125,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 +170,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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,17 @@ import com.fasterxml.jackson.annotation.JsonEnumDefaultValue;
|
|||||||
public enum PkmnGender {
|
public enum PkmnGender {
|
||||||
|
|
||||||
@JsonEnumDefaultValue
|
@JsonEnumDefaultValue
|
||||||
MALE,
|
MALE("Male"),
|
||||||
FEMALE,
|
FEMALE("Female"),
|
||||||
GENDERLESS;
|
GENDERLESS("Genderless");
|
||||||
|
|
||||||
|
private final String displayName;
|
||||||
|
|
||||||
|
private PkmnGender(String displayName) {
|
||||||
|
this.displayName = displayName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDisplayName() {
|
||||||
|
return displayName;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,12 +58,12 @@ 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);
|
||||||
int ability = buffer.getByte(21) & 0xFF;
|
int ability = buffer.getByte(21) & 0xFF;
|
||||||
int form = (buffer.getByte(64) >> 3) & 0xFF;
|
int form = (buffer.getByte(64) >> 3) & 0x1F;
|
||||||
boolean genderless = ((buffer.getByte(64) >> 2) & 1) == 1;
|
boolean genderless = ((buffer.getByte(64) >> 2) & 1) == 1;
|
||||||
boolean female = ((buffer.getByte(64) >> 1) & 1) == 1;
|
boolean female = ((buffer.getByte(64) >> 1) & 1) == 1;
|
||||||
PkmnGender gender = genderless ? PkmnGender.GENDERLESS : female ? PkmnGender.FEMALE : PkmnGender.MALE;
|
PkmnGender gender = genderless ? PkmnGender.GENDERLESS : female ? PkmnGender.FEMALE : PkmnGender.MALE;
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -5,33 +5,44 @@ import com.fasterxml.jackson.annotation.JsonEnumDefaultValue;
|
|||||||
public enum PkmnNature {
|
public enum PkmnNature {
|
||||||
|
|
||||||
@JsonEnumDefaultValue
|
@JsonEnumDefaultValue
|
||||||
HARDY,
|
HARDY("Hardy"),
|
||||||
LONELY,
|
LONELY("Lonely"),
|
||||||
BRAVE,
|
BRAVE("Brave"),
|
||||||
ADAMANT,
|
ADAMANT("Adamant"),
|
||||||
NAUGHTY,
|
NAUGHTY("Naughty"),
|
||||||
BOLD,
|
BOLD("Bold"),
|
||||||
DOCILE,
|
DOCILE("Docile"),
|
||||||
RELAXED,
|
RELAXED("Relaxed"),
|
||||||
IMPISH,
|
IMPISH("Impish"),
|
||||||
LAX,
|
LAX("Lax"),
|
||||||
TIMID,
|
TIMID("Timid"),
|
||||||
HASTY,
|
HASTY("Hasty"),
|
||||||
SERIOUS,
|
SERIOUS("Serious"),
|
||||||
JOLLY,
|
JOLLY("Jolly"),
|
||||||
NAIVE,
|
NAIVE("Naive"),
|
||||||
MODEST,
|
MODEST("Modest"),
|
||||||
MILD,
|
MILD("Mild"),
|
||||||
QUIET,
|
QUIET("Quiet"),
|
||||||
BASHFUL,
|
BASHFUL("Bashful"),
|
||||||
RASH,
|
RASH("Rash"),
|
||||||
CALM,
|
CALM("Calm"),
|
||||||
GENTLE,
|
GENTLE("Gentle"),
|
||||||
SASSY,
|
SASSY("Sassy"),
|
||||||
CAREFUL,
|
CAREFUL("Careful"),
|
||||||
QUIRKY;
|
QUIRKY("Quirky");
|
||||||
|
|
||||||
|
private final String displayName;
|
||||||
|
|
||||||
|
private PkmnNature(String displayName) {
|
||||||
|
this.displayName = displayName;
|
||||||
|
}
|
||||||
|
|
||||||
public static PkmnNature valueOf(int index) {
|
public static PkmnNature valueOf(int index) {
|
||||||
return index >= 0 && index < values().length ? values()[index] : null;
|
return index >= 0 && index < values().length ? values()[index] : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getDisplayName() {
|
||||||
|
return displayName;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,44 +8,54 @@ public enum DreamAnimation {
|
|||||||
* Look around, but stay in the same position.
|
* Look around, but stay in the same position.
|
||||||
*/
|
*/
|
||||||
@JsonEnumDefaultValue
|
@JsonEnumDefaultValue
|
||||||
LOOK_AROUND,
|
LOOK_AROUND("Look around"),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Walk around, but never change direction without moving a step in that direction.
|
* Walk around, but never change direction without moving a step in that direction.
|
||||||
*/
|
*/
|
||||||
WALK_AROUND,
|
WALK_AROUND("Walk around"),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Walk around and occasionally change direction without moving.
|
* Walk around and occasionally change direction without moving.
|
||||||
*/
|
*/
|
||||||
WALK_LOOK_AROUND,
|
WALK_LOOK_AROUND("Walk and look around"),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Only walk up and down.
|
* Only walk up and down.
|
||||||
*/
|
*/
|
||||||
WALK_VERTICALLY,
|
WALK_VERTICALLY("Walk up and down"),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Only walk left and right.
|
* Only walk left and right.
|
||||||
*/
|
*/
|
||||||
WALK_HORIZONTALLY,
|
WALK_HORIZONTALLY("Walk left and right"),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Only walk left and right, and occasionally change direction without moving.
|
* Only walk left and right, and occasionally change direction without moving.
|
||||||
*/
|
*/
|
||||||
WALK_LOOK_HORIZONTALLY,
|
WALK_LOOK_HORIZONTALLY("Walk left and right and look around"),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Continuously spin right.
|
* Continuously spin right.
|
||||||
*/
|
*/
|
||||||
SPIN_RIGHT,
|
SPIN_RIGHT("Spin right"),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Continuously spin left.
|
* Continuously spin left.
|
||||||
*/
|
*/
|
||||||
SPIN_LEFT;
|
SPIN_LEFT("Spin left");
|
||||||
|
|
||||||
|
private final String displayName;
|
||||||
|
|
||||||
|
private DreamAnimation(String displayName) {
|
||||||
|
this.displayName = displayName;
|
||||||
|
}
|
||||||
|
|
||||||
public static DreamAnimation valueOf(int index) {
|
public static DreamAnimation valueOf(int index) {
|
||||||
return index >= 0 && index < values().length ? values()[index] : null;
|
return index >= 0 && index < values().length ? values()[index] : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getDisplayName() {
|
||||||
|
return displayName;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,19 @@
|
|||||||
package entralinked.model.player;
|
package entralinked.model.player;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
|
||||||
public record DreamDecor(
|
public record DreamDecor(
|
||||||
@JsonProperty(required = true) int id,
|
@JsonProperty(required = true) int id,
|
||||||
@JsonProperty(required = true) String name) {}
|
@JsonProperty(required = true) String name) {
|
||||||
|
|
||||||
|
// TODO names probably differed per language
|
||||||
|
public static final List<DreamDecor> DEFAULT_DECOR = List.of(
|
||||||
|
new DreamDecor(1, "Design Table"),
|
||||||
|
new DreamDecor(2, "Design Stool"),
|
||||||
|
new DreamDecor(3, "Flower Vase"),
|
||||||
|
new DreamDecor(4, "Cuddle Rug"),
|
||||||
|
new DreamDecor(6, "Wall Poster")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -15,6 +16,7 @@ public class Player {
|
|||||||
private final List<DreamEncounter> encounters = new ArrayList<>();
|
private final List<DreamEncounter> encounters = new ArrayList<>();
|
||||||
private final List<DreamItem> items = new ArrayList<>();
|
private final List<DreamItem> items = new ArrayList<>();
|
||||||
private final List<AvenueVisitor> avenueVisitors = new ArrayList<>();
|
private final List<AvenueVisitor> avenueVisitors = new ArrayList<>();
|
||||||
|
private final List<DreamDecor> decor = new ArrayList<>();
|
||||||
private PlayerStatus status;
|
private PlayerStatus status;
|
||||||
private GameVersion gameVersion;
|
private GameVersion gameVersion;
|
||||||
private PkmnInfo dreamerInfo;
|
private PkmnInfo dreamerInfo;
|
||||||
@@ -22,6 +24,10 @@ 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 String customMusical;
|
||||||
|
private File dataDirectory;
|
||||||
|
|
||||||
public Player(String gameSyncId) {
|
public Player(String gameSyncId) {
|
||||||
this.gameSyncId = gameSyncId;
|
this.gameSyncId = gameSyncId;
|
||||||
@@ -29,11 +35,12 @@ 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();
|
||||||
avenueVisitors.clear();
|
avenueVisitors.clear();
|
||||||
|
decor.clear();
|
||||||
|
decor.addAll(DreamDecor.DEFAULT_DECOR);
|
||||||
levelsGained = 0;
|
levelsGained = 0;
|
||||||
cgearSkin = null;
|
cgearSkin = null;
|
||||||
dexSkin = null;
|
dexSkin = null;
|
||||||
@@ -77,6 +84,17 @@ public class Player {
|
|||||||
return Collections.unmodifiableList(avenueVisitors);
|
return Collections.unmodifiableList(avenueVisitors);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setDecor(Collection<DreamDecor> decor) {
|
||||||
|
if(decor.size() <= 5) {
|
||||||
|
this.decor.clear();
|
||||||
|
this.decor.addAll(decor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<DreamDecor> getDecor() {
|
||||||
|
return Collections.unmodifiableList(decor);
|
||||||
|
}
|
||||||
|
|
||||||
public void setStatus(PlayerStatus status) {
|
public void setStatus(PlayerStatus status) {
|
||||||
this.status = status;
|
this.status = status;
|
||||||
}
|
}
|
||||||
@@ -132,4 +150,58 @@ 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCustomMusical(String customMusical) {
|
||||||
|
this.customMusical = customMusical;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getCustomMusical() {
|
||||||
|
return customMusical;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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");
|
||||||
|
}
|
||||||
|
|
||||||
|
public File getMusicalFile() {
|
||||||
|
return new File(dataDirectory, "musical.bin");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,15 +21,20 @@ public record PlayerDto(
|
|||||||
String cgearSkin,
|
String cgearSkin,
|
||||||
String dexSkin,
|
String dexSkin,
|
||||||
String musical,
|
String musical,
|
||||||
|
String customCGearSkin,
|
||||||
|
String customDexSkin,
|
||||||
|
String customMusical,
|
||||||
int levelsGained,
|
int levelsGained,
|
||||||
@JsonDeserialize(contentAs = DreamEncounter.class) Collection<DreamEncounter> encounters,
|
@JsonDeserialize(contentAs = DreamEncounter.class) Collection<DreamEncounter> encounters,
|
||||||
@JsonDeserialize(contentAs = DreamItem.class) Collection<DreamItem> items,
|
@JsonDeserialize(contentAs = DreamItem.class) Collection<DreamItem> items,
|
||||||
@JsonDeserialize(contentAs = AvenueVisitor.class) Collection<AvenueVisitor> avenueVisitors) {
|
@JsonDeserialize(contentAs = AvenueVisitor.class) Collection<AvenueVisitor> avenueVisitors,
|
||||||
|
@JsonDeserialize(contentAs = DreamDecor.class) Collection<DreamDecor> decor) {
|
||||||
|
|
||||||
public PlayerDto(Player player) {
|
public PlayerDto(Player player) {
|
||||||
this(player.getGameSyncId(), player.getGameVersion(), player.getStatus(), player.getDreamerInfo(),
|
this(player.getGameSyncId(), player.getGameVersion(), player.getStatus(), player.getDreamerInfo(),
|
||||||
player.getCGearSkin(), player.getDexSkin(), player.getMusical(), player.getLevelsGained(),
|
player.getCGearSkin(), player.getDexSkin(), player.getMusical(), player.getCustomCGearSkin(),
|
||||||
player.getEncounters(), player.getItems(),player.getAvenueVisitors());
|
player.getCustomDexSkin(), player.getCustomMusical(), player.getLevelsGained(), player.getEncounters(),
|
||||||
|
player.getItems(), player.getAvenueVisitors(), player.getDecor());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -43,10 +48,14 @@ 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.setCustomMusical(customMusical);
|
||||||
player.setLevelsGained(levelsGained);
|
player.setLevelsGained(levelsGained);
|
||||||
player.setEncounters(encounters == null ? Collections.emptyList() : encounters);
|
player.setEncounters(encounters == null ? Collections.emptyList() : encounters);
|
||||||
player.setItems(items == null ? Collections.emptyList() : items);
|
player.setItems(items == null ? Collections.emptyList() : items);
|
||||||
player.setAvenueVisitors(avenueVisitors == null ? Collections.emptyList() : avenueVisitors);
|
player.setAvenueVisitors(avenueVisitors == null ? Collections.emptyList() : avenueVisitors);
|
||||||
|
player.setDecor(decor == null ? DreamDecor.DEFAULT_DECOR : decor);
|
||||||
return player;
|
return player;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,9 @@ 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;
|
||||||
|
import entralinked.utility.GsidUtility;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Manager class for managing {@link Player} information (Global Link users)
|
* Manager class for managing {@link Player} information (Global Link users)
|
||||||
*/
|
*/
|
||||||
@@ -25,7 +29,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 +38,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.
|
||||||
@@ -55,11 +118,17 @@ public class PlayerManager {
|
|||||||
Player player = mapper.readValue(inputFile, PlayerDto.class).toPlayer();
|
Player player = mapper.readValue(inputFile, PlayerDto.class).toPlayer();
|
||||||
String gameSyncId = player.getGameSyncId();
|
String gameSyncId = player.getGameSyncId();
|
||||||
|
|
||||||
// Check for duplicate Game Sync ID
|
// Check if Game Sync ID is valid
|
||||||
if(doesPlayerExist(gameSyncId)) {
|
if(!GsidUtility.isValidGameSyncId(gameSyncId)) {
|
||||||
throw new IOException("Duplicate Game Sync ID %s".formatted(gameSyncId));
|
throw new IOException("Invalid Game Sync ID: %s".formatted(gameSyncId));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check for duplicate Game Sync ID
|
||||||
|
if(doesPlayerExist(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 +144,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 +183,32 @@ 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 Game Sync ID: {}", gameSyncId);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if Game Sync ID is valid
|
||||||
|
if(!GsidUtility.isValidGameSyncId(gameSyncId)) {
|
||||||
|
logger.warn("Attempted to register invalid Game Sync ID: {}", gameSyncId);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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;
|
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 +222,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 +235,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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,11 +5,15 @@ import java.util.Collections;
|
|||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
|
import entralinked.model.dlc.Dlc;
|
||||||
|
|
||||||
public class User {
|
public class User {
|
||||||
|
|
||||||
private final String id;
|
private final String id;
|
||||||
private final String password; // I debated hashing it, but.. it's a 3-digit password...
|
private final String password; // I debated hashing it, but.. it's a 3-digit password...
|
||||||
private final Map<String, GameProfile> profiles = new HashMap<>();
|
private final Map<String, GameProfile> profiles = new HashMap<>();
|
||||||
|
private final Map<String, Dlc> dlcOverrides = new HashMap<>();
|
||||||
|
private 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 +24,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 +58,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, Dlc target) {
|
||||||
|
if(target == null) {
|
||||||
|
dlcOverrides.remove(type);
|
||||||
|
} else {
|
||||||
|
dlcOverrides.put(type, target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void removeDlcOverride(String type) {
|
||||||
|
dlcOverrides.remove(type);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasDlcOverride(String type) {
|
||||||
|
return dlcOverrides.containsKey(type);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Dlc 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,10 +1,25 @@
|
|||||||
package entralinked.network.http.dashboard;
|
package entralinked.network.http.dashboard;
|
||||||
|
|
||||||
|
import java.awt.image.BufferedImage;
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import javax.imageio.ImageIO;
|
||||||
|
|
||||||
|
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;
|
||||||
@@ -19,7 +34,10 @@ 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.Crc16;
|
||||||
import entralinked.utility.GsidUtility;
|
import entralinked.utility.GsidUtility;
|
||||||
|
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;
|
||||||
@@ -29,25 +47,79 @@ import io.javalin.json.JavalinJackson;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* HTTP handler for requests made to the user dashboard.
|
* HTTP handler for requests made to the user dashboard.
|
||||||
|
*
|
||||||
|
* @deprecated
|
||||||
*/
|
*/
|
||||||
public class DashboardHandler implements HttpHandler {
|
public class DashboardHandler implements HttpHandler {
|
||||||
|
|
||||||
private final Set<Integer> availableBlackAndWhiteSpecies = Set.of(
|
private static final Logger logger = LogManager.getLogger();
|
||||||
505, 507, 510, 511, 513, 515, 519, 523, 525, 527, 529, 531, 533, 535, 538, 539, 542, 545, 546, 548,
|
private final Map<Object, BufferedImage> skinPreviewCache = new HashMap<>();
|
||||||
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 DlcList dlcList;
|
private final DlcList dlcList;
|
||||||
private final PlayerManager playerManager;
|
private final PlayerManager playerManager;
|
||||||
|
|
||||||
public DashboardHandler(Entralinked entralinked) {
|
public DashboardHandler(Entralinked entralinked) {
|
||||||
this.dlcList = entralinked.getDlcList();
|
this.dlcList = entralinked.getDlcList();
|
||||||
this.playerManager = entralinked.getPlayerManager();
|
this.playerManager = entralinked.getPlayerManager();
|
||||||
|
|
||||||
|
// Load & cache 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"));
|
||||||
|
|
||||||
|
// Cache default skins
|
||||||
|
for(Dlc skin : skins) {
|
||||||
|
try(FileInputStream inputStream = new FileInputStream(skin.path())) {
|
||||||
|
BufferedImage image = skin.type().equals("ZUKAN") ? TiledImageUtility.readDexSkin(inputStream, true)
|
||||||
|
: skin.type().equals("CGEAR") ? TiledImageUtility.readCGearSkin(inputStream, true)
|
||||||
|
: TiledImageUtility.readCGearSkin(inputStream, false); // CGEAR2
|
||||||
|
skinPreviewCache.put(skin, image);
|
||||||
|
} catch(IOException | IndexOutOfBoundsException 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());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void addHandlers(Javalin javalin) {
|
public void addHandlers(Javalin javalin) {
|
||||||
|
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);
|
||||||
@@ -73,6 +145,54 @@ public class DashboardHandler implements HttpHandler {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET request handler for {@code /dashboard/previewskin}
|
||||||
|
*/
|
||||||
|
private void handlePreviewSkin(Context ctx) throws IOException {
|
||||||
|
String type = ctx.queryParam("type");
|
||||||
|
String name = ctx.queryParam("name");
|
||||||
|
|
||||||
|
// Make sure query parameters are present
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write cached image data
|
||||||
|
ImageIO.write(skinPreviewCache.get(dlc), "png", ctx.outputStream());
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GET request handler for {@code /dashboard/dlc}
|
* GET request handler for {@code /dashboard/dlc}
|
||||||
*/
|
*/
|
||||||
@@ -86,7 +206,7 @@ public class DashboardHandler implements HttpHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Send result
|
// Send result
|
||||||
ctx.json(dlcList.getDlcList("IRAO", type).stream().map(Dlc::name).collect(Collectors.toList()));
|
ctx.json(dlcList.getDlcList("IRAO", type).stream().map(Dlc::name).sorted().collect(Collectors.toList()));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -134,7 +254,103 @@ 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":
|
||||||
|
// Process skin data
|
||||||
|
TiledImageUtility.writeDexSkin(byteOutputStream, image, TiledImageUtility.generateBackgroundColors(image));
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -199,14 +415,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) {
|
||||||
@@ -226,11 +436,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.";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import entralinked.model.player.DreamEncounter;
|
|||||||
import entralinked.model.player.DreamItem;
|
import entralinked.model.player.DreamItem;
|
||||||
import entralinked.model.player.Player;
|
import entralinked.model.player.Player;
|
||||||
|
|
||||||
|
@Deprecated
|
||||||
public record DashboardProfileMessage(
|
public record DashboardProfileMessage(
|
||||||
String gameVersion,
|
String gameVersion,
|
||||||
String dreamerSprite,
|
String dreamerSprite,
|
||||||
@@ -15,6 +16,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 +25,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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import entralinked.model.avenue.AvenueVisitor;
|
|||||||
import entralinked.model.player.DreamEncounter;
|
import entralinked.model.player.DreamEncounter;
|
||||||
import entralinked.model.player.DreamItem;
|
import entralinked.model.player.DreamItem;
|
||||||
|
|
||||||
|
@Deprecated
|
||||||
public record DashboardProfileUpdateRequest(
|
public record DashboardProfileUpdateRequest(
|
||||||
@JsonProperty(required = true) @JsonDeserialize(contentAs = DreamEncounter.class) List<DreamEncounter> encounters,
|
@JsonProperty(required = true) @JsonDeserialize(contentAs = DreamEncounter.class) List<DreamEncounter> encounters,
|
||||||
@JsonProperty(required = true) @JsonDeserialize(contentAs = DreamItem.class) List<DreamItem> items,
|
@JsonProperty(required = true) @JsonDeserialize(contentAs = DreamItem.class) List<DreamItem> items,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package entralinked.network.http.dashboard;
|
package entralinked.network.http.dashboard;
|
||||||
|
|
||||||
|
@Deprecated
|
||||||
public record DashboardStatusMessage(String message, boolean error) {
|
public record DashboardStatusMessage(String message, boolean error) {
|
||||||
|
|
||||||
public DashboardStatusMessage(String message) {
|
public DashboardStatusMessage(String message) {
|
||||||
|
|||||||
@@ -2,6 +2,10 @@ package entralinked.network.http.dls;
|
|||||||
|
|
||||||
import java.io.FileInputStream;
|
import java.io.FileInputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.apache.logging.log4j.LogManager;
|
||||||
|
import org.apache.logging.log4j.Logger;
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
|
||||||
@@ -9,6 +13,7 @@ import entralinked.Entralinked;
|
|||||||
import entralinked.model.dlc.Dlc;
|
import entralinked.model.dlc.Dlc;
|
||||||
import entralinked.model.dlc.DlcList;
|
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;
|
||||||
@@ -23,6 +28,7 @@ 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 DlcList dlcList;
|
||||||
private final UserManager userManager;
|
private final UserManager userManager;
|
||||||
@@ -43,11 +49,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;
|
||||||
}
|
}
|
||||||
@@ -59,6 +67,9 @@ public class DlsHandler implements HttpHandler {
|
|||||||
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,19 +78,15 @@ 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 = getRegionlessDlcType(request.dlcType());
|
||||||
};
|
|
||||||
|
|
||||||
// Map to generic type, I doubt there is a real difference between the language codes anyway.
|
// If an overriding DLC is present, send the data for that instead.
|
||||||
String type = switch(request.dlcType()) {
|
if(user.hasDlcOverride(type)) {
|
||||||
case "CGEAR_E", "CGEAR_F", "CGEAR_I", "CGEAR_G", "CGEAR_S", "CGEAR_J", "CGEAR_K" -> "CGEAR";
|
ctx.result(dlcList.getDlcListString(List.of(user.getDlcOverride(type))));
|
||||||
case "CGEAR2_E", "CGEAR2_F", "CGEAR2_I", "CGEAR2_G", "CGEAR2_S", "CGEAR2_J", "CGEAR2_K" -> "CGEAR2";
|
return;
|
||||||
case "ZUKAN_E", "ZUKAN_F", "ZUKAN_I", "ZUKAN_G", "ZUKAN_S", "ZUKAN_J", "ZUKAN_K" -> "ZUKAN";
|
}
|
||||||
case "MUSICAL_E", "MUSICAL_F", "MUSICAL_I", "MUSICAL_G", "MUSICAL_S", "MUSICAL_J", "MUSICAL_K" -> "MUSICAL";
|
|
||||||
default -> request.dlcType();
|
|
||||||
};
|
|
||||||
|
|
||||||
// TODO NOTE: I assume that in a conventional implementation, certain DLC attributes may be omitted from the request.
|
// TODO NOTE: I assume that in a conventional implementation, certain DLC attributes may be omitted from the request.
|
||||||
ctx.result(dlcList.getDlcListString(dlcList.getDlcList(gameCode, type, request.dlcIndex())));
|
ctx.result(dlcList.getDlcListString(dlcList.getDlcList(gameCode, type, request.dlcIndex())));
|
||||||
@@ -89,9 +96,12 @@ public class DlsHandler implements HttpHandler {
|
|||||||
* 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 = getRegionlessDlcType(request.dlcType());
|
||||||
|
Dlc dlc = user.hasDlcOverride(type) ? user.getDlcOverride(type) : dlcList.getDlc(gameCode, type, request.dlcName());
|
||||||
|
|
||||||
|
// Check if the requested DLC exists
|
||||||
if(dlc == null) {
|
if(dlc == null) {
|
||||||
ctx.status(HttpStatus.NOT_FOUND);
|
ctx.status(HttpStatus.NOT_FOUND);
|
||||||
return;
|
return;
|
||||||
@@ -108,4 +118,27 @@ public class DlsHandler implements HttpHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return The game serial that should be used for downloading DLC based on the provided input.
|
||||||
|
*/
|
||||||
|
private String getDlcGameCode(String gameCode) {
|
||||||
|
return switch(gameCode) {
|
||||||
|
case "IRAJ", "IRAK" -> "IRAO";
|
||||||
|
default -> gameCode;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return The DLC type without the region identifier, or the input if it is an unknown type.
|
||||||
|
*/
|
||||||
|
private String getRegionlessDlcType(String dlcType) {
|
||||||
|
return switch(dlcType) {
|
||||||
|
case "CGEAR_E", "CGEAR_F", "CGEAR_I", "CGEAR_G", "CGEAR_S", "CGEAR_J", "CGEAR_K" -> "CGEAR";
|
||||||
|
case "CGEAR2_E", "CGEAR2_F", "CGEAR2_I", "CGEAR2_G", "CGEAR2_S", "CGEAR2_J", "CGEAR2_K" -> "CGEAR2";
|
||||||
|
case "ZUKAN_E", "ZUKAN_F", "ZUKAN_I", "ZUKAN_G", "ZUKAN_S", "ZUKAN_J", "ZUKAN_K" -> "ZUKAN";
|
||||||
|
case "MUSICAL_E", "MUSICAL_F", "MUSICAL_I", "MUSICAL_G", "MUSICAL_S", "MUSICAL_J", "MUSICAL_K" -> "MUSICAL";
|
||||||
|
default -> dlcType;
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,5 +21,12 @@ public record DlsRequest(
|
|||||||
@JsonProperty("contents") String dlcName, // action=contents
|
@JsonProperty("contents") String dlcName, // action=contents
|
||||||
@JsonProperty("attr1") String dlcType, // action=list
|
@JsonProperty("attr1") String dlcType, // action=list
|
||||||
@JsonProperty("attr2") int dlcIndex, // action=list
|
@JsonProperty("attr2") int dlcIndex, // 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, dlcType=%s, dlcIndex=%s, offset=%s, num=%s]")
|
||||||
|
.formatted(gameCode, action, dlcGameCode, dlcName, dlcType, dlcIndex, 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;
|
||||||
@@ -15,7 +16,9 @@ import com.fasterxml.jackson.databind.ObjectMapper;
|
|||||||
|
|
||||||
import entralinked.Configuration;
|
import entralinked.Configuration;
|
||||||
import entralinked.Entralinked;
|
import entralinked.Entralinked;
|
||||||
|
import entralinked.GameVersion;
|
||||||
import entralinked.model.avenue.AvenueVisitor;
|
import entralinked.model.avenue.AvenueVisitor;
|
||||||
|
import entralinked.model.dlc.Dlc;
|
||||||
import entralinked.model.dlc.DlcList;
|
import entralinked.model.dlc.DlcList;
|
||||||
import entralinked.model.pkmn.PkmnInfo;
|
import entralinked.model.pkmn.PkmnInfo;
|
||||||
import entralinked.model.pkmn.PkmnInfoReader;
|
import entralinked.model.pkmn.PkmnInfoReader;
|
||||||
@@ -26,6 +29,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;
|
||||||
@@ -49,12 +53,6 @@ public class PglHandler implements HttpHandler {
|
|||||||
private static final String password = "2Phfv9MY"; // Best security in the world
|
private static final String password = "2Phfv9MY"; // Best security in the world
|
||||||
private final ObjectMapper mapper = new ObjectMapper(new UrlEncodedFormFactory()
|
private final ObjectMapper mapper = new ObjectMapper(new UrlEncodedFormFactory()
|
||||||
.disable(UrlEncodedFormParser.Feature.BASE64_DECODE_VALUES));
|
.disable(UrlEncodedFormParser.Feature.BASE64_DECODE_VALUES));
|
||||||
private final List<DreamDecor> decorList = List.of(
|
|
||||||
new DreamDecor(1, "+----------+"),
|
|
||||||
new DreamDecor(2, "Thank you"),
|
|
||||||
new DreamDecor(3, "for using"),
|
|
||||||
new DreamDecor(4, "Entralinked!"),
|
|
||||||
new DreamDecor(5, "+----------+"));
|
|
||||||
private final Set<Integer> sleepyList = new HashSet<>();
|
private final Set<Integer> sleepyList = new HashSet<>();
|
||||||
private final Configuration configuration;
|
private final Configuration configuration;
|
||||||
private final DlcList dlcList;
|
private final DlcList dlcList;
|
||||||
@@ -91,6 +89,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 +97,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 +130,7 @@ public class PglHandler implements HttpHandler {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Handle the request
|
// Handle the request
|
||||||
handler.process(request, ctx);
|
handler.process(request, ctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -142,7 +144,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 +189,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,22 +197,26 @@ public class PglHandler implements HttpHandler {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logger.info("Player {} is downloading save data", player.getGameSyncId());
|
||||||
|
|
||||||
// Write status code
|
// Write status code
|
||||||
writeStatusCode(outputStream, 0);
|
writeStatusCode(outputStream, 0);
|
||||||
|
|
||||||
// Allow it to wake up anyway, maybe the poor sap is stuck..
|
// Allow waking up but don't send any data
|
||||||
// Just don't send any other data.
|
|
||||||
if(player.getStatus() == PlayerStatus.AWAKE) {
|
if(player.getStatus() == PlayerStatus.AWAKE) {
|
||||||
logger.info("Player {} is downloading save data, but is already awake!", player.getGameSyncId());
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
GameVersion version = player.getGameVersion();
|
||||||
List<DreamEncounter> encounters = player.getEncounters();
|
List<DreamEncounter> encounters = player.getEncounters();
|
||||||
List<DreamItem> items = player.getItems();
|
List<DreamItem> items = player.getItems();
|
||||||
|
List<DreamDecor> decorList = player.getDecor();
|
||||||
|
|
||||||
// When waking up a Pokémon, these 4 bytes are written to 0x1D304 in the save file.
|
// When waking up a Pokémon, these 4 bytes are written to 0x1D304 in the save file.
|
||||||
// If the bytes in the game's save file match the new bytes, they will be set to 0x00000000
|
// If the bytes in the game's save file match the new bytes, they will be set to 0x00000000
|
||||||
// and no content will be downloaded.
|
// and no content will be downloaded.
|
||||||
|
// Looking at some old save files, this was very likely just a total tuck-in/wake-up counter.
|
||||||
|
// Additionally, waking up sets a flag at 0x1D4A3 (seems to be a "Pokémon is tucked in" flag or something) to 0x0.
|
||||||
outputStream.writeInt((int)(Math.random() * Integer.MAX_VALUE));
|
outputStream.writeInt((int)(Math.random() * Integer.MAX_VALUE));
|
||||||
|
|
||||||
// Write encounter data (max 10)
|
// Write encounter data (max 10)
|
||||||
@@ -228,10 +235,10 @@ public class PglHandler implements HttpHandler {
|
|||||||
// Write misc stuff and DLC information
|
// Write misc stuff and DLC information
|
||||||
outputStream.writeShort(player.getLevelsGained());
|
outputStream.writeShort(player.getLevelsGained());
|
||||||
outputStream.write(0); // Unknown
|
outputStream.write(0); // Unknown
|
||||||
outputStream.write(dlcList.getDlcIndex(player.getMusical()));
|
outputStream.write(getDlcIndex(user, player.getMusical(), "MUSICAL", player.getMusicalFile()));
|
||||||
outputStream.write(dlcList.getDlcIndex(player.getCGearSkin()));
|
outputStream.write(getDlcIndex(user, player.getCGearSkin(), version.isVersion2() ? "CGEAR2" : "CGEAR", player.getCGearSkinFile()));
|
||||||
outputStream.write(dlcList.getDlcIndex(player.getDexSkin()));
|
outputStream.write(getDlcIndex(user, player.getDexSkin(), "ZUKAN", player.getDexSkinFile()));
|
||||||
outputStream.write(decorList.isEmpty() ? 0 : 1); // Seems to be a flag for indicating whether or not decor data is present
|
outputStream.write(decorList.isEmpty() ? 0 : 1); // Decor flag (?) stored at 0x1D4A4
|
||||||
outputStream.write(0); // Must be zero?
|
outputStream.write(0); // Must be zero?
|
||||||
|
|
||||||
// Write item IDs
|
// Write item IDs
|
||||||
@@ -265,35 +272,38 @@ public class PglHandler implements HttpHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Write decor padding
|
// Write decor padding
|
||||||
outputStream.writeBytes(0, (5 - decorList.size()) * 26);
|
for(int i = 0; i < (5 - decorList.size()); i++) {
|
||||||
|
outputStream.writeShort(0x7E); // Just reset to default state
|
||||||
|
outputStream.writeBytes(0, 24);
|
||||||
|
}
|
||||||
|
|
||||||
outputStream.writeShort(0); // ?
|
outputStream.writeShort(0); // ?
|
||||||
|
|
||||||
// Join Avenue visitor data -- copied in parts to 0x2422C in the save file.
|
// Join Avenue visitor data -- copied in parts to 0x2422C in the save file.
|
||||||
// Black Version 2 and White Version 2 only.
|
// Black Version 2 and White Version 2 only.
|
||||||
if(player.getGameVersion().isVersion2()) {
|
if(version.isVersion2()) {
|
||||||
List<AvenueVisitor> avenueVisitors = player.getAvenueVisitors();
|
List<AvenueVisitor> avenueVisitors = player.getAvenueVisitors();
|
||||||
|
|
||||||
for(AvenueVisitor visitor : avenueVisitors) {
|
for(AvenueVisitor visitor : avenueVisitors) {
|
||||||
// Write visitor name + padding. Names cannot be duplicate.
|
// Write visitor name + padding. Names cannot be duplicate.
|
||||||
byte[] nameBytes = visitor.name().getBytes(StandardCharsets.UTF_16LE);
|
byte[] nameBytes = visitor.name().getBytes(StandardCharsets.UTF_16LE);
|
||||||
outputStream.write(nameBytes, 0, Math.min(14, nameBytes.length));
|
outputStream.write(nameBytes, 0, Math.min(14, nameBytes.length));
|
||||||
outputStream.writeBytes(-1, 14 - nameBytes.length);
|
outputStream.writeBytes(-1, 16 - nameBytes.length);
|
||||||
|
|
||||||
// Full visitor type consists of a trainer class and what I call a 'personality' index
|
// Full visitor type consists of a trainer class and what I call a 'personality' index
|
||||||
// that, along with the trainer class, determines which phrases the visitor uses.
|
// that, along with the trainer class, determines which phrases the visitor uses.
|
||||||
// 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
|
||||||
outputStream.writeInt(1); // [20] Ignores if 0
|
outputStream.writeInt(1); // [20] Ignores if 0
|
||||||
outputStream.write(visitor.countryCode());
|
outputStream.write(visitor.countryCode());
|
||||||
outputStream.write(visitor.stateProvinceCode());
|
outputStream.write(visitor.stateProvinceCode());
|
||||||
outputStream.write(0); // [26] Ignores if 1
|
outputStream.write(visitor.gameVersion().getLanguageCode()); // 99% sure this is the lang code because 1 seems to be ignored ONLY if country code isn't Japan (plus it's right above the rom code)
|
||||||
outputStream.write(visitor.gameVersion().getRomCode()); // Affects shop stock
|
outputStream.write(visitor.gameVersion().getRomCode()); // Affects shop stock
|
||||||
outputStream.write(visitor.type().isFemale() ? 1 : 0);
|
outputStream.write(visitor.type().isFemale() ? 1 : 0);
|
||||||
outputStream.write(0); // [29] Does.. something
|
outputStream.write(0); // [29] Does.. something
|
||||||
@@ -311,7 +321,15 @@ public class PglHandler implements HttpHandler {
|
|||||||
*/
|
*/
|
||||||
private void handleMemoryLink(PglRequest request, Context ctx) throws IOException {
|
private void handleMemoryLink(PglRequest request, Context ctx) throws IOException {
|
||||||
LEOutputStream outputStream = new LEOutputStream(ctx.outputStream());
|
LEOutputStream outputStream = new LEOutputStream(ctx.outputStream());
|
||||||
|
|
||||||
|
// Check if Game Sync ID is valid
|
||||||
|
if(!GsidUtility.isValidGameSyncId(request.gameSyncId())) {
|
||||||
|
writeStatusCode(outputStream, 8); // Invalid Game Sync ID
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
Player player = playerManager.getPlayer(request.gameSyncId());
|
Player player = playerManager.getPlayer(request.gameSyncId());
|
||||||
|
User user = ctx.attribute("user");
|
||||||
|
|
||||||
// Check if player exists
|
// Check if player exists
|
||||||
if(player == null) {
|
if(player == null) {
|
||||||
@@ -319,6 +337,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 +350,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 +417,11 @@ 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());
|
||||||
|
|
||||||
if(player == null || (player.getStatus() != PlayerStatus.AWAKE && !configuration.allowOverwritingPlayerDreamInfo())) {
|
// Check if the player exists
|
||||||
|
if(player == null) {
|
||||||
// Skip everything
|
// Skip everything
|
||||||
ServletInputStream inputStream = ctx.req().getInputStream();
|
ServletInputStream inputStream = ctx.req().getInputStream();
|
||||||
|
|
||||||
@@ -410,6 +434,20 @@ public class PglHandler implements HttpHandler {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logger.info("Player {} is uploading save data", player.getGameSyncId());
|
||||||
|
|
||||||
|
// Warn if player's current status is unexpected
|
||||||
|
if(player.getStatus() != PlayerStatus.AWAKE)
|
||||||
|
{
|
||||||
|
logger.warn("Player {} is not AWAKE -- existing dream information will be overwritten!", player.getGameSyncId());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Warn if player's game version changed
|
||||||
|
if(player.getGameVersion() != null && request.gameVersion() != player.getGameVersion())
|
||||||
|
{
|
||||||
|
logger.warn("Player {}'s game version changed from {} to {}", player.getGameSyncId(), player.getGameVersion(), request.gameVersion());
|
||||||
|
}
|
||||||
|
|
||||||
// Try to store save data
|
// Try to store save data
|
||||||
if(!playerManager.storePlayerGameSaveFile(player, ctx.bodyInputStream())) {
|
if(!playerManager.storePlayerGameSaveFile(player, ctx.bodyInputStream())) {
|
||||||
writeStatusCode(outputStream, 4); // Game save data IO error
|
writeStatusCode(outputStream, 4); // Game save data IO error
|
||||||
@@ -419,7 +457,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,20 +481,16 @@ 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());
|
||||||
|
|
||||||
// Make sure Game Sync ID is present
|
// Check if Game Sync ID is valid
|
||||||
if(request.gameSyncId() == null) {
|
if(!GsidUtility.isValidGameSyncId(request.gameSyncId())) {
|
||||||
writeStatusCode(outputStream, 1); // Unauthorized
|
writeStatusCode(outputStream, 8); // Invalid Game Sync ID
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -467,11 +501,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);
|
||||||
}
|
}
|
||||||
@@ -485,6 +527,13 @@ public class PglHandler implements HttpHandler {
|
|||||||
LEOutputStream outputStream = new LEOutputStream(ctx.outputStream());
|
LEOutputStream outputStream = new LEOutputStream(ctx.outputStream());
|
||||||
String gameSyncId = GsidUtility.stringifyGameSyncId(Integer.parseInt(ctx.body().replace("\u0000", ""))); // So quirky
|
String gameSyncId = GsidUtility.stringifyGameSyncId(Integer.parseInt(ctx.body().replace("\u0000", ""))); // So quirky
|
||||||
|
|
||||||
|
// Check if Game Sync ID is valid
|
||||||
|
if(!GsidUtility.isValidGameSyncId(gameSyncId)) {
|
||||||
|
logger.debug("[account.createdata] Rejecting invalid Game Sync ID: {} ({})", gameSyncId, ctx.body());
|
||||||
|
writeStatusCode(outputStream, 8); // Invalid Game Sync ID
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Check if player doesn't exist already
|
// Check if player doesn't exist already
|
||||||
if(playerManager.doesPlayerExist(gameSyncId)) {
|
if(playerManager.doesPlayerExist(gameSyncId)) {
|
||||||
writeStatusCode(outputStream, 2); // Duplicate Game Sync ID
|
writeStatusCode(outputStream, 2); // Duplicate Game Sync ID
|
||||||
@@ -492,7 +541,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;
|
||||||
}
|
}
|
||||||
@@ -508,4 +558,17 @@ public class PglHandler implements HttpHandler {
|
|||||||
outputStream.writeInt(status);
|
outputStream.writeInt(status);
|
||||||
outputStream.writeBytes(0, 124);
|
outputStream.writeBytes(0, 124);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the index of the player's chosen DLC for the specified type and prepares DLC overriding if necessary.
|
||||||
|
*/
|
||||||
|
private int getDlcIndex(User user, String name, String type, File customFile) {
|
||||||
|
if("custom".equals(name)) {
|
||||||
|
user.setDlcOverride(type, new Dlc(customFile.getAbsolutePath(), name, "IRAO", type, 1, (int)customFile.length(), 0, true));
|
||||||
|
return 1;
|
||||||
|
} else {
|
||||||
|
user.removeDlcOverride(type);
|
||||||
|
return dlcList.getDlcIndex("IRAO", type, name);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,12 +25,6 @@ public class GsidDeserializer extends StdDeserializer<String> {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String deserialize(JsonParser parser, DeserializationContext context) throws IOException {
|
public String deserialize(JsonParser parser, DeserializationContext context) throws IOException {
|
||||||
int gsid = parser.getIntValue();
|
return GsidUtility.stringifyGameSyncId(parser.getValueAsInt(-1));
|
||||||
|
|
||||||
if(gsid < 0) {
|
|
||||||
throw new IOException("Game Sync ID cannot be a negative number.");
|
|
||||||
}
|
|
||||||
|
|
||||||
return GsidUtility.stringifyGameSyncId(gsid);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,40 +2,87 @@ package entralinked.utility;
|
|||||||
|
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Game Sync ID generation process:
|
||||||
|
*
|
||||||
|
* Let's take example PID "1231499195".
|
||||||
|
* Start by storing both the PID and its checksum (35497) in working variable "ugsid".
|
||||||
|
* We do this by simply shifting the checksum 32 bits to the left: ugsid = pid | (checksum << 32) = 0x8AA949672FBB
|
||||||
|
*
|
||||||
|
* Calculating each character is pretty straightforward.
|
||||||
|
* We just use the 5 least significant bits of ugsid as the index for the character table.
|
||||||
|
* Character 1: 0x8AA949672FBB & 0x1F = 27 = '5'
|
||||||
|
*
|
||||||
|
* After each character, we shift ugsid 5 bits to the right. Since ugsid contains 48 bits of data,
|
||||||
|
* taking the 5 least significant bits each time gives us enough indexes for (if we round up) exactly 10 characters.
|
||||||
|
* If we take a look at the full value of ugsid (0x8AA949672FBB) in binary and split it into sections of 5 bits,
|
||||||
|
* we'll actually already be able to see the entire Game Sync ID in reverse:
|
||||||
|
*
|
||||||
|
* Character: 'E' 'L' 'X' 'F' 'E' 'Y' 'Q' 'M' '7' '5'
|
||||||
|
* Chartable index: 4 10 21 5 4 22 14 11 29 27
|
||||||
|
* Binary: XX100 01010 10101 00101 00100 10110 01110 01011 11101 11011
|
||||||
|
*
|
||||||
|
* Adding all of the characters together gets us the Game Sync ID "57MQYEFXLE".
|
||||||
|
*
|
||||||
|
* Reversing this process to retrieve the PID and checksum is very straightforward.
|
||||||
|
* Simply go through each character, find the index of it in the character table & left shift the total value 5 bits each time.
|
||||||
|
* If we do this with the Game Sync ID we just created, then it should give us back the value of ugsid: 0x8AA949672FBB
|
||||||
|
* To then retrieve the PID, simply do: ugsid & 0xFFFFFFFF = 1231499195
|
||||||
|
* To retrieve the checksum, simply do: (ugsid >> 32) & 0xFFFF = 35497
|
||||||
|
* We can then validate the Game Sync ID if we want to by comparing the checksums.
|
||||||
|
*/
|
||||||
public class GsidUtility {
|
public class GsidUtility {
|
||||||
|
|
||||||
public static final String GSID_CHARTABLE = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
|
public static final String GSID_CHARTABLE = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
|
||||||
public static final Pattern GSID_PATTERN = Pattern.compile("[A-HJ-NP-Z2-9]{10}");
|
public static final Pattern GSID_PATTERN = Pattern.compile("[A-HJ-NP-Z2-9]{10}");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stringifies the specified numerical Game Sync ID
|
* Stringifies the specified numerical Game Sync ID.
|
||||||
*
|
*
|
||||||
* Black 2 - {@code sub_21B480C} (overlay #199)
|
* Black 2 - {@code sub_21B480C} (overlay #199)
|
||||||
*/
|
*/
|
||||||
public static String stringifyGameSyncId(int gsid) {
|
public static String stringifyGameSyncId(int gsid) {
|
||||||
char[] output = new char[10];
|
char[] output = new char[10];
|
||||||
int index = 0;
|
long checksum = Crc16.calc(gsid);
|
||||||
|
long ugsid = gsid | (checksum << 32);
|
||||||
|
|
||||||
// v12 = gsid
|
|
||||||
// v5 = sub_204405C(gsid, 4u)
|
|
||||||
// v8 = v5 + __CFSHR__(v12, 31) + (v12 >> 31)
|
|
||||||
|
|
||||||
// uses unsigned ints for bitshift operations
|
|
||||||
long ugsid = gsid;
|
|
||||||
long checksum = Crc16.calc(gsid); // + __CFSHR__(v12, 31) + (v12 >> 31); ??
|
|
||||||
|
|
||||||
// do while v4 < 10
|
|
||||||
for(int i = 0; i < output.length; i++) {
|
for(int i = 0; i < output.length; i++) {
|
||||||
index = (int)((ugsid & 0x1F) & 0x1FFFF); // chartable string is unicode, so normally multiplies by 2
|
int index = (int)((ugsid >> (5 * i)) & 0x1F);
|
||||||
ugsid = (ugsid >> 5) | (checksum << 27);
|
output[i] = GSID_CHARTABLE.charAt(index);
|
||||||
checksum >>= 5;
|
|
||||||
output[i] = GSID_CHARTABLE.charAt(index); // sub_2048734(v4, chartable + index)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return new String(output);
|
return new String(output);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines if a Game Sync ID is valid by checking its length, characters & checksum.
|
||||||
|
*
|
||||||
|
* @return {@code true} if the Game Sync ID is valid, otherwise {@code false}.
|
||||||
|
*/
|
||||||
public static boolean isValidGameSyncId(String gsid) {
|
public static boolean isValidGameSyncId(String gsid) {
|
||||||
return GSID_PATTERN.matcher(gsid).matches();
|
if(gsid == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
int length = gsid.length();
|
||||||
|
long ugsid = 0;
|
||||||
|
|
||||||
|
if(length != 10) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for(int i = 0; i < length; i++) {
|
||||||
|
int index = GSID_CHARTABLE.indexOf(gsid.charAt(i));
|
||||||
|
|
||||||
|
if(index == -1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ugsid |= (long)index << (5 * i);
|
||||||
|
}
|
||||||
|
|
||||||
|
int output = (int)(ugsid & 0xFFFFFFFF);
|
||||||
|
int checksum = (int)((ugsid >> 32) & 0xFFFF);
|
||||||
|
return output >= 0 && Crc16.calc(output) == checksum;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
70
src/main/java/entralinked/utility/LEInputStream.java
Normal file
70
src/main/java/entralinked/utility/LEInputStream.java
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
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());
|
||||||
|
}
|
||||||
|
|
||||||
|
public String readUTF16(int length) throws IOException {
|
||||||
|
char[] charBuffer = new char[length];
|
||||||
|
int read = 0;
|
||||||
|
|
||||||
|
for(int i = 0; i < charBuffer.length; i++) {
|
||||||
|
int c = readShort() & 0xFFFF;
|
||||||
|
|
||||||
|
if(c == 0xFFFF) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
charBuffer[i] = (char)c;
|
||||||
|
read++;
|
||||||
|
}
|
||||||
|
|
||||||
|
skipNBytes((length - (read + 1)) * 2);
|
||||||
|
return new String(charBuffer, 0, read);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,6 +21,13 @@ public class MD5 {
|
|||||||
* @return A hex-formatted MD5 hash of the specified input.
|
* @return A hex-formatted MD5 hash of the specified input.
|
||||||
*/
|
*/
|
||||||
public static String digest(String string) {
|
public static String digest(String string) {
|
||||||
|
return StringUtil.toHexStringPadded(digest(string.getBytes(StandardCharsets.ISO_8859_1)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return An MD5 hash of the specified input.
|
||||||
|
*/
|
||||||
|
public static byte[] digest(byte[] bytes) {
|
||||||
if(digest == null) {
|
if(digest == null) {
|
||||||
try {
|
try {
|
||||||
digest = MessageDigest.getInstance("MD5");
|
digest = MessageDigest.getInstance("MD5");
|
||||||
@@ -29,6 +36,6 @@ public class MD5 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return StringUtil.toHexStringPadded(digest.digest(string.getBytes(StandardCharsets.ISO_8859_1)));
|
return digest.digest(bytes);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
216
src/main/java/entralinked/utility/SwingUtility.java
Normal file
216
src/main/java/entralinked/utility/SwingUtility.java
Normal file
@@ -0,0 +1,216 @@
|
|||||||
|
package entralinked.utility;
|
||||||
|
|
||||||
|
import java.awt.BorderLayout;
|
||||||
|
import java.awt.Component;
|
||||||
|
import java.awt.Dimension;
|
||||||
|
import java.awt.Font;
|
||||||
|
import java.awt.GraphicsEnvironment;
|
||||||
|
import java.awt.GridBagConstraints;
|
||||||
|
import java.awt.event.ActionEvent;
|
||||||
|
import java.awt.event.MouseAdapter;
|
||||||
|
import java.awt.event.MouseEvent;
|
||||||
|
import java.io.PrintWriter;
|
||||||
|
import java.io.StringWriter;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import javax.swing.AbstractAction;
|
||||||
|
import javax.swing.Action;
|
||||||
|
import javax.swing.BorderFactory;
|
||||||
|
import javax.swing.Icon;
|
||||||
|
import javax.swing.JCheckBox;
|
||||||
|
import javax.swing.JLabel;
|
||||||
|
import javax.swing.JOptionPane;
|
||||||
|
import javax.swing.JPanel;
|
||||||
|
import javax.swing.JScrollPane;
|
||||||
|
import javax.swing.JTextArea;
|
||||||
|
import javax.swing.JTextField;
|
||||||
|
import javax.swing.JToggleButton;
|
||||||
|
import javax.swing.SwingUtilities;
|
||||||
|
|
||||||
|
import com.formdev.flatlaf.FlatClientProperties;
|
||||||
|
|
||||||
|
import net.miginfocom.swing.MigLayout;
|
||||||
|
|
||||||
|
public class SwingUtility {
|
||||||
|
|
||||||
|
private static final Set<String> ignoredMessages = new HashSet<>();
|
||||||
|
|
||||||
|
@SuppressWarnings("serial")
|
||||||
|
public static Action createAction(String name, Icon icon, Runnable handler) {
|
||||||
|
AbstractAction action = new AbstractAction(name, icon) {
|
||||||
|
@Override
|
||||||
|
public void actionPerformed(ActionEvent event) {
|
||||||
|
handler.run();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if(icon != null) {
|
||||||
|
action.putValue(Action.SHORT_DESCRIPTION, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
return action;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Action createAction(String name, Runnable handler) {
|
||||||
|
return createAction(name, null, handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static GridBagConstraints createConstraints(int x, int y) {
|
||||||
|
return createConstraints(x, y, 1, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static GridBagConstraints createConstraints(int x, int y, int width, int height) {
|
||||||
|
return createConstraints(x, y, width, height, 1, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static GridBagConstraints createConstraints(int x, int y, int width, int height, double weightX, double weightY) {
|
||||||
|
return createConstraints(x, y, width, height, weightX, weightY, 8, 8);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static GridBagConstraints createConstraints(int x, int y, int width, int height, double weightX, double weightY,
|
||||||
|
int paddingX, int paddingY) {
|
||||||
|
GridBagConstraints constraints = new GridBagConstraints();
|
||||||
|
constraints.fill = GridBagConstraints.BOTH;
|
||||||
|
constraints.gridx = x;
|
||||||
|
constraints.gridy = y;
|
||||||
|
constraints.gridwidth = width;
|
||||||
|
constraints.gridheight = height;
|
||||||
|
constraints.weightx = weightX;
|
||||||
|
constraints.weighty = weightY;
|
||||||
|
constraints.ipadx = paddingX;
|
||||||
|
constraints.ipady = paddingY;
|
||||||
|
return constraints;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void setTextFieldToggle(JTextField textField, boolean selected) {
|
||||||
|
for(Component component : textField.getComponents()) {
|
||||||
|
if(component instanceof JToggleButton) {
|
||||||
|
JToggleButton button = (JToggleButton)component;
|
||||||
|
|
||||||
|
if(button.isSelected() != selected) {
|
||||||
|
button.doClick();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO find a good unicode font maybe?
|
||||||
|
public static Font findSupportingFont(String text, Font def) {
|
||||||
|
GraphicsEnvironment graphicsEnvironment = GraphicsEnvironment.getLocalGraphicsEnvironment();
|
||||||
|
|
||||||
|
for(Font font : graphicsEnvironment.getAllFonts()) {
|
||||||
|
if(font.canDisplayUpTo(text) == -1) {
|
||||||
|
return new Font(font.getName(), def.getStyle(), def.getSize());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return def;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static JLabel createTitleLabel() {
|
||||||
|
return createTitleLabel("");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static JLabel createTitleLabel(String text) {
|
||||||
|
return createStyledLabel(text, "font:bold +8");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static JLabel createDescriptionLabel() {
|
||||||
|
return createDescriptionLabel("");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static JLabel createDescriptionLabel(String text) {
|
||||||
|
return createStyledLabel(text, "[dark]foreground:darken(@foreground,20%)");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static JLabel createStyledLabel(String text, String style) {
|
||||||
|
JLabel label = new JLabel(text);
|
||||||
|
label.putClientProperty(FlatClientProperties.STYLE, style);
|
||||||
|
return label;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static JLabel createButtonLabel(String text, Runnable actionHandler) {
|
||||||
|
JLabel label = new JLabel("<html><u>%s</u></html>".formatted(text));
|
||||||
|
label.putClientProperty(FlatClientProperties.STYLE, "font: -1");
|
||||||
|
label.addMouseListener(new MouseAdapter() {
|
||||||
|
@Override
|
||||||
|
public void mouseClicked(MouseEvent event) {
|
||||||
|
if(SwingUtilities.isLeftMouseButton(event)) {
|
||||||
|
actionHandler.run();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return label;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void showIgnorableHint(Component parentComponent, String message, String title, int messageType) {
|
||||||
|
synchronized(ignoredMessages) {
|
||||||
|
// Do nothing if message has been ignored
|
||||||
|
if(ignoredMessages.contains(message)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create ignore checkbox
|
||||||
|
JCheckBox checkBox = new JCheckBox("Don't show this again");
|
||||||
|
JOptionPane.showMessageDialog(parentComponent, createIgnorableDialogPanel(message, checkBox), title, messageType);
|
||||||
|
|
||||||
|
// Add to ignore list if checkbox is selected
|
||||||
|
if(checkBox.isSelected()) {
|
||||||
|
ignoredMessages.add(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean showIgnorableConfirmDialog(Component parentComponent, String message, String title) {
|
||||||
|
synchronized(ignoredMessages) {
|
||||||
|
// Do nothing if message has been ignored
|
||||||
|
if(ignoredMessages.contains(message)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
JCheckBox checkBox = new JCheckBox("Don't ask this again");
|
||||||
|
int result = JOptionPane.showConfirmDialog(parentComponent, createIgnorableDialogPanel(message, checkBox), title, JOptionPane.YES_NO_OPTION);
|
||||||
|
|
||||||
|
// Add to ignore list if checkbox is selected
|
||||||
|
if(checkBox.isSelected()) {
|
||||||
|
ignoredMessages.add(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result == JOptionPane.YES_OPTION;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static JPanel createIgnorableDialogPanel(String message, JCheckBox checkBox) {
|
||||||
|
JPanel panel = new JPanel(new MigLayout("insets 0"));
|
||||||
|
panel.add(new JLabel("<html>%s</html>".formatted(message.replace("\n", "<br/>"))), "wrap"); // TODO no idea how JOptionPane does line breaks
|
||||||
|
panel.add(checkBox, "gapy 8");
|
||||||
|
return panel;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void showExceptionInfo(Component parentComponent, String message, Throwable throwable) {
|
||||||
|
// Create stacktrace string
|
||||||
|
StringWriter writer = new StringWriter();
|
||||||
|
throwable.printStackTrace(new PrintWriter(writer));
|
||||||
|
|
||||||
|
// Create text area
|
||||||
|
JTextArea area = new JTextArea(writer.toString());
|
||||||
|
area.setBorder(BorderFactory.createEmptyBorder(8, 8, 0, 0));
|
||||||
|
area.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12));
|
||||||
|
area.setEditable(false);
|
||||||
|
|
||||||
|
// Create scroll pane
|
||||||
|
int height = Math.min(200, area.getFontMetrics(area.getFont()).getHeight() * area.getLineCount() + 10);
|
||||||
|
JScrollPane scrollPane = new JScrollPane(area);
|
||||||
|
scrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS);
|
||||||
|
scrollPane.setPreferredSize(new Dimension(600, height));
|
||||||
|
scrollPane.setMaximumSize(scrollPane.getPreferredSize());
|
||||||
|
|
||||||
|
// Create dialog
|
||||||
|
String label = String.format("<html><b>%s</b><br>Exception details:<br><br></html>", message);
|
||||||
|
JPanel panel = new JPanel(new BorderLayout());
|
||||||
|
panel.add(new JLabel(label), BorderLayout.PAGE_START);
|
||||||
|
panel.add(scrollPane);
|
||||||
|
JOptionPane.showMessageDialog(parentComponent, panel, "An error has occured", JOptionPane.ERROR_MESSAGE);
|
||||||
|
}
|
||||||
|
}
|
||||||
406
src/main/java/entralinked/utility/TiledImageUtility.java
Normal file
406
src/main/java/entralinked/utility/TiledImageUtility.java
Normal file
@@ -0,0 +1,406 @@
|
|||||||
|
package entralinked.utility;
|
||||||
|
|
||||||
|
import java.awt.image.BufferedImage;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
import org.apache.logging.log4j.LogManager;
|
||||||
|
import org.apache.logging.log4j.Logger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates Pokédex skin background colors for the specified image.
|
||||||
|
*/
|
||||||
|
public static int[] generateBackgroundColors(BufferedImage image) {
|
||||||
|
int[] backgroundColors = Arrays.copyOf(DEFAULT_DEX_BACKGROUND_COLORS, 64);
|
||||||
|
int backgroundColor = image.getRGB(0, 0);
|
||||||
|
int dexColor = image.getRGB(0, 134);
|
||||||
|
int buttonColor = image.getRGB(128, 134);
|
||||||
|
|
||||||
|
// Background colors (58, 59, 60)
|
||||||
|
for(int i = 0; i < 3; i++) {
|
||||||
|
backgroundColors[i + 58] = ColorUtility.multiplyColor(backgroundColor, 0.7 + i * 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pokédex colors (48, 49, 50, 51, 52)
|
||||||
|
for(int i = 0; i < 5; i++) {
|
||||||
|
backgroundColors[i + 48] = ColorUtility.multiplyColor(dexColor, 1.15 - i * 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pokédex button colors (53, 54, 55, 56)
|
||||||
|
for(int i = 0; i < 4; i++) {
|
||||||
|
backgroundColors[i + 53] = ColorUtility.multiplyColor(buttonColor, 0.55 + i * 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
return backgroundColors;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return The index of the element in the array, or {@code -1} if no such element exists.
|
||||||
|
*/
|
||||||
|
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,50 +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>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<!-- Item Configuration -->
|
|
||||||
<label>Items (Max. 20)</label><br>
|
|
||||||
<div>
|
|
||||||
<table id="item-table" class="item-table">
|
|
||||||
<tr>
|
|
||||||
<td><a id="item0" href="#configureItem" onclick="configureItem(0)"><image src="/sprites/items/0.png"/></a></td>
|
|
||||||
<td><a id="item1" href="#configureItem" onclick="configureItem(1)"><image src="/sprites/items/0.png"/></a></td>
|
|
||||||
<td><a id="item2" href="#configureItem" onclick="configureItem(2)"><image src="/sprites/items/0.png"/></a></td>
|
|
||||||
<td><a id="item3" href="#configureItem" onclick="configureItem(3)"><image src="/sprites/items/0.png"/></a></td>
|
|
||||||
<td><a id="item4" href="#configureItem" onclick="configureItem(4)"><image src="/sprites/items/0.png"/></a></td>
|
|
||||||
<td><a id="item5" href="#configureItem" onclick="configureItem(5)"><image src="/sprites/items/0.png"/></a></td>
|
|
||||||
<td><a id="item6" href="#configureItem" onclick="configureItem(6)"><image src="/sprites/items/0.png"/></a></td>
|
|
||||||
<td><a id="item7" href="#configureItem" onclick="configureItem(7)"><image src="/sprites/items/0.png"/></a></td>
|
|
||||||
<td><a id="item8" href="#configureItem" onclick="configureItem(8)"><image src="/sprites/items/0.png"/></a></td>
|
|
||||||
<td><a id="item9" href="#configureItem" onclick="configureItem(9)"><image src="/sprites/items/0.png"/></a></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td><a id="item10" href="#configureItem" onclick="configureItem(10)"><image src="/sprites/items/0.png"/></a></td>
|
|
||||||
<td><a id="item11" href="#configureItem" onclick="configureItem(11)"><image src="/sprites/items/0.png"/></a></td>
|
|
||||||
<td><a id="item12" href="#configureItem" onclick="configureItem(12)"><image src="/sprites/items/0.png"/></a></td>
|
|
||||||
<td><a id="item13" href="#configureItem" onclick="configureItem(13)"><image src="/sprites/items/0.png"/></a></td>
|
|
||||||
<td><a id="item14" href="#configureItem" onclick="configureItem(14)"><image src="/sprites/items/0.png"/></a></td>
|
|
||||||
<td><a id="item15" href="#configureItem" onclick="configureItem(15)"><image src="/sprites/items/0.png"/></a></td>
|
|
||||||
<td><a id="item16" href="#configureItem" onclick="configureItem(16)"><image src="/sprites/items/0.png"/></a></td>
|
|
||||||
<td><a id="item17" href="#configureItem" onclick="configureItem(17)"><image src="/sprites/items/0.png"/></a></td>
|
|
||||||
<td><a id="item18" href="#configureItem" onclick="configureItem(18)"><image src="/sprites/items/0.png"/></a></td>
|
|
||||||
<td><a id="item19" href="#configureItem" onclick="configureItem(19)"><image src="/sprites/items/0.png"/></a></td>
|
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
@@ -102,20 +72,52 @@
|
|||||||
<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>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<!-- Item Configuration -->
|
||||||
|
<label>Items (Max. 20)</label><br>
|
||||||
|
<div>
|
||||||
|
<table id="item-table" class="item-table">
|
||||||
|
<tr>
|
||||||
|
<td id="item0" onclick="configureItem(0)"><image src="/sprites/items/0.png"/></td>
|
||||||
|
<td id="item1" onclick="configureItem(1)"><image src="/sprites/items/0.png"/></td>
|
||||||
|
<td id="item2" onclick="configureItem(2)"><image src="/sprites/items/0.png"/></td>
|
||||||
|
<td id="item3" onclick="configureItem(3)"><image src="/sprites/items/0.png"/></td>
|
||||||
|
<td id="item4" onclick="configureItem(4)"><image src="/sprites/items/0.png"/></td>
|
||||||
|
<td id="item5" onclick="configureItem(5)"><image src="/sprites/items/0.png"/></td>
|
||||||
|
<td id="item6" onclick="configureItem(6)"><image src="/sprites/items/0.png"/></td>
|
||||||
|
<td id="item7" onclick="configureItem(7)"><image src="/sprites/items/0.png"/></td>
|
||||||
|
<td id="item8" onclick="configureItem(8)"><image src="/sprites/items/0.png"/></td>
|
||||||
|
<td id="item9" onclick="configureItem(9)"><image src="/sprites/items/0.png"/></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td id="item10" onclick="configureItem(10)"><image src="/sprites/items/0.png"/></td>
|
||||||
|
<td id="item11" onclick="configureItem(11)"><image src="/sprites/items/0.png"/></td>
|
||||||
|
<td id="item12" onclick="configureItem(12)"><image src="/sprites/items/0.png"/></td>
|
||||||
|
<td id="item13" onclick="configureItem(13)"><image src="/sprites/items/0.png"/></td>
|
||||||
|
<td id="item14" onclick="configureItem(14)"><image src="/sprites/items/0.png"/></td>
|
||||||
|
<td id="item15" onclick="configureItem(15)"><image src="/sprites/items/0.png"/></td>
|
||||||
|
<td id="item16" onclick="configureItem(16)"><image src="/sprites/items/0.png"/></td>
|
||||||
|
<td id="item17" onclick="configureItem(17)"><image src="/sprites/items/0.png"/></td>
|
||||||
|
<td id="item18" onclick="configureItem(18)"><image src="/sprites/items/0.png"/></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>
|
<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>
|
<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,175 @@
|
|||||||
<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>
|
<option value="BLACK_FRENCH">Version Noire</option>
|
||||||
<input id="visitor-form-personality" name="personality" type="number" value="0" min="0" max="7"/>
|
<option value="WHITE_FRENCH">Version Blanche</option>
|
||||||
<label for="visitor-form-dreamer">Tucked-in Pokémon Species</label>
|
<option value="BLACK_2_FRENCH">Version Noire 2</option>
|
||||||
<input id="visitor-form-dreamer" name="personality" type="number" value="1" min="1" max="649"/>
|
<option value="WHITE_2_FRENCH">Version Blanche 2</option>
|
||||||
</form>
|
<option value="BLACK_ITALIAN">Versione Nera</option>
|
||||||
<button class="big-button" onclick="saveVisitor()">Confirm</button>
|
<option value="WHITE_ITALIAN">Versione Bianca</option>
|
||||||
<button class="big-button" onclick="removeVisitor()">Remove</button>
|
<option value="BLACK_2_ITALIAN">Versione Nera 2</option>
|
||||||
|
<option value="WHITE_2_ITALIAN">Versione Bianca 2</option>
|
||||||
|
<option value="BLACK_GERMAN">Schwarze Edition</option>
|
||||||
|
<option value="WHITE_GERMAN">Weisse Edition</option>
|
||||||
|
<option value="BLACK_2_GERMAN">Schwarze Edition 2</option>
|
||||||
|
<option value="WHITE_2_GERMAN">Weisse Edition 2</option>
|
||||||
|
<option value="BLACK_SPANISH">Edicion Negra</option>
|
||||||
|
<option value="WHITE_SPANISH">Edicion Blanca</option>
|
||||||
|
<option value="BLACK_2_SPANISH">Edicion Negra 2</option>
|
||||||
|
<option value="WHITE_2_SPANISH">Edicion Blanca 2</option>
|
||||||
|
<option value="BLACK_JAPANESE">ブラック</option>
|
||||||
|
<option value="WHITE_JAPANESE">ホワイト</option>
|
||||||
|
<option value="BLACK_2_JAPANESE">ブラック2</option>
|
||||||
|
<option value="WHITE_2_JAPANESE">ホワイト2</option>
|
||||||
|
<option value="BLACK_KOREAN">블랙</option>
|
||||||
|
<option value="WHITE_KOREAN">화이트</option>
|
||||||
|
<option value="BLACK_2_KOREAN">블랙2</option>
|
||||||
|
<option value="WHITE_2_KOREAN">화이트2</option>
|
||||||
|
</select>
|
||||||
|
<label for="visitor-form-region">Country</label>
|
||||||
|
<select id="visitor-form-region" name="region" value="1">
|
||||||
|
<!-- Filled by profile.js -->
|
||||||
|
</select>
|
||||||
|
<label for="visitor-form-subregion">State/Province</label>
|
||||||
|
<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;
|
||||||
ELEMENT_ENCOUNTER_MOVE.value = encounter ? encounter.move : 1;
|
let form = updateEncounterFormOptions();
|
||||||
ELEMENT_ENCOUNTER_FORM.value = encounter ? encounter.form : 0;
|
let gender = updateEncounterGenderOptions();
|
||||||
ELEMENT_ENCOUNTER_GENDER.value = encounter ? encounter.gender : "GENDERLESS";
|
ELEMENT_ENCOUNTER_MOVE.value = encounter ? encounter.move : 0;
|
||||||
ELEMENT_ENCOUNTER_ANIMATION.value = encounter ? encounter.animation : "WALK_AROUND";
|
ELEMENT_ENCOUNTER_FORM.value = encounter ? encounter.form : form;
|
||||||
|
ELEMENT_ENCOUNTER_GENDER.value = encounter ? encounter.gender : gender;
|
||||||
|
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,148 +480,113 @@ function updateItemCell(index) {
|
|||||||
|
|
||||||
function closeItemForm() {
|
function closeItemForm() {
|
||||||
itemTableIndex = -1;
|
itemTableIndex = -1;
|
||||||
window.location.href = "#";
|
ELEMENT_ITEM_CONFIG.style.display = "none";
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchData(path) {
|
/**
|
||||||
return fetchData(path, "GET", null);
|
* Miscellaneous stuff
|
||||||
|
*/
|
||||||
|
|
||||||
|
function openFileChooser(inputDocumentId) {
|
||||||
|
document.getElementById(inputDocumentId).click();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchData(path, method, body) {
|
function uploadSkin(type, file) {
|
||||||
let response = await fetch(path, {
|
fetchData("POST", "/dashboard/uploadskin?type=" + (type == "CGEAR" && isVersion2() ? "CGEAR2" : type)
|
||||||
method: method,
|
+ "&filename=" + file.name, file).then((response) => {
|
||||||
body: body
|
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;
|
||||||
|
|
||||||
// Return to login page if unauthorized
|
if(value == "none") {
|
||||||
if(response.status == 401) {
|
window.alert("Please select a skin to preview it.");
|
||||||
window.location.href = "/dashboard/login.html";
|
return false;
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
if(type == "CGEAR" && isVersion2()) {
|
||||||
return await response.json();
|
type = "CGEAR2";
|
||||||
} catch(error) {
|
|
||||||
window.alert(error);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setSkinPreviewResolution(width, height) {
|
||||||
|
ELEMENT_SKIN_PREVIEW_IMAGE.width = width;
|
||||||
|
ELEMENT_SKIN_PREVIEW_IMAGE.height = height;
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeSkinPreview() {
|
||||||
|
ELEMENT_SKIN_PREVIEW.style.display = "none";
|
||||||
}
|
}
|
||||||
|
|
||||||
function fetchDlcData() {
|
function fetchDlcData() {
|
||||||
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,
|
||||||
@@ -471,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 {
|
.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,9 +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: 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;
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user