mirror of
https://github.com/kuroppoi/entralinked.git
synced 2026-09-08 08:45:14 -05:00
Compare commits
27 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
@@ -1,13 +1,13 @@
|
||||
name: Build
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
push:
|
||||
branches: [ master ]
|
||||
paths:
|
||||
- poke-sprites-v
|
||||
- src/**
|
||||
- build.gradle
|
||||
|
||||
jobs:
|
||||
dist:
|
||||
runs-on: ubuntu-latest
|
||||
21
.github/workflows/dist-pull-request.yml
vendored
21
.github/workflows/dist-pull-request.yml
vendored
@@ -1,21 +0,0 @@
|
||||
name: Build on Pull Request
|
||||
on: pull_request
|
||||
jobs:
|
||||
dist:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
- name: Checkout submodules
|
||||
run: git submodule update --init --recursive
|
||||
- name: Setup Java 17
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
java-version: 17
|
||||
distribution: temurin
|
||||
- name: Make gradlew executable
|
||||
run: chmod +x ./gradlew
|
||||
- name: Setup Gradle
|
||||
uses: gradle/gradle-build-action@v2
|
||||
- name: Run Gradle dist
|
||||
run: ./gradlew dist
|
||||
14
README.md
14
README.md
@@ -1,12 +1,19 @@
|
||||
# Entralinked
|
||||
[](https://github.com/kuroppoi/entralinked/actions)
|
||||
[](https://github.com/kuroppoi/entralinked/releases/latest)
|
||||
<p align="center">
|
||||
<img src="https://raw.githubusercontent.com/kuroppoi/entralinked/master/images/icon.png" alt="icon"/>
|
||||
</p>
|
||||
<h1 align="center">Entralinked</h1>
|
||||
<p align="center">
|
||||
<a href="https://github.com/kuroppoi/entralinked/actions"><img src="https://github.com/kuroppoi/entralinked/actions/workflows/build.yml/badge.svg" alt="build"/></a>
|
||||
<a href="https://github.com/kuroppoi/entralinked/releases/latest"><img src="https://img.shields.io/github/v/release/kuroppoi/entralinked?labelColor=30373D&label=Release&logoColor=959DA5&logo=github" alt="release"/></a>
|
||||
</p>
|
||||
|
||||
Entralinked is a standalone Game Sync emulator developed for use with Pokémon Black & White and its sequels.\
|
||||
Its purpose is to serve as a simple utility for downloading Pokémon, Items, C-Gear skins, Pokédex skins, Musicals\
|
||||
and, in Black 2 & White 2 only, Join Avenue visitors to your game without needing to edit your save file.\
|
||||
It can also be used to Memory Link with a Black or White save file if you don't have a second DS system.
|
||||
|
||||
For users: [Quick Setup Guide](https://github.com/kuroppoi/entralinked/wiki/Setup)
|
||||
|
||||
## Building
|
||||
|
||||
#### Prerequisites
|
||||
@@ -29,5 +36,4 @@ Entralinked has a built-in DNS server.\
|
||||
In order for your game to connect, you must configure the DNS settings of your DS.\
|
||||
By default, Entralinked is configured to automatically use the local host of the system.\
|
||||
This approach is not always accurate, however, and you may need to manually configure it in `config.json`.\
|
||||
If you receive error code `60000` when trying to connect, erase the WFC Configuration of your DS and try again.\
|
||||
After tucking in a Pokémon, navigate to http://localhost/dashboard/profile.html to configure Game Sync settings.
|
||||
|
||||
@@ -24,9 +24,12 @@ dependencies {
|
||||
implementation 'org.apache.logging.log4j:log4j-api:2.20.0'
|
||||
implementation 'com.fasterxml.jackson.core:jackson-databind: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 'org.apache.logging.log4j:log4j-slf4j2-impl:2.20.0'
|
||||
implementation 'com.formdev:flatlaf:3.1.1'
|
||||
implementation 'com.formdev:flatlaf-extras:3.1.1'
|
||||
implementation 'com.formdev:flatlaf-intellij-themes:3.1.1'
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
|
||||
BIN
images/icon.png
Normal file
BIN
images/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.0 KiB |
@@ -1,14 +1,14 @@
|
||||
package entralinked;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public record Configuration(
|
||||
@JsonProperty(required = true) String hostName,
|
||||
@JsonProperty(required = true) boolean clearPlayerDreamInfoOnWake,
|
||||
@JsonProperty(required = true) boolean allowOverwritingPlayerDreamInfo,
|
||||
@JsonProperty(required = true) boolean allowWfcRegistrationThroughLogin) {
|
||||
String hostName,
|
||||
boolean clearPlayerDreamInfoOnWake,
|
||||
boolean allowOverwritingPlayerDreamInfo,
|
||||
boolean allowPlayerGameVersionMismatch,
|
||||
boolean allowWfcRegistrationThroughLogin) {
|
||||
|
||||
public static final Configuration DEFAULT = new Configuration("local", true, false, true);
|
||||
public static final Configuration DEFAULT = new Configuration("local", true, false, false, true);
|
||||
}
|
||||
|
||||
@@ -43,8 +43,11 @@ public class Entralinked {
|
||||
private final GameSpyServer gameSpyServer;
|
||||
private final HttpServer httpServer;
|
||||
private MainView mainView;
|
||||
private boolean initialized;
|
||||
|
||||
public Entralinked(String[] args) {
|
||||
long beginTime = System.currentTimeMillis();
|
||||
|
||||
// Read command line arguments
|
||||
CommandLineArguments arguments = new CommandLineArguments(args);
|
||||
|
||||
@@ -92,16 +95,11 @@ public class Entralinked {
|
||||
userManager = new UserManager();
|
||||
playerManager = new PlayerManager();
|
||||
|
||||
// Start servers
|
||||
boolean started = true;
|
||||
|
||||
// Create DNS server
|
||||
dnsServer = new DnsServer(hostAddress);
|
||||
started &= dnsServer.start();
|
||||
|
||||
// Create GameSpy server
|
||||
gameSpyServer = new GameSpyServer(this);
|
||||
started &= gameSpyServer.start();
|
||||
|
||||
// Create HTTP server
|
||||
httpServer = new HttpServer(this);
|
||||
@@ -109,25 +107,42 @@ public class Entralinked {
|
||||
httpServer.addHandler(new PglHandler(this));
|
||||
httpServer.addHandler(new DlsHandler(this));
|
||||
httpServer.addHandler(new DashboardHandler(this));
|
||||
started &= httpServer.start();
|
||||
|
||||
// Handle post-startup GUI stuff
|
||||
if(mainView != null) {
|
||||
if(!started) {
|
||||
SwingUtilities.invokeLater(() -> mainView.setStatusLabelText(
|
||||
"ERROR: One or more servers failed to start! Please check the logs for info."));
|
||||
return;
|
||||
}
|
||||
|
||||
// Start servers
|
||||
boolean started = startServers();
|
||||
|
||||
// Post-startup
|
||||
if(started) {
|
||||
String hostIpAddress = hostAddress.getHostAddress();
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
mainView.setDashboardButtonEnabled(true);
|
||||
mainView.setStatusLabelText("Configure your DS to use the following DNS server: %s".formatted(hostIpAddress));
|
||||
});
|
||||
logger.info("Startup complete! Took a total of {} milliseconds", System.currentTimeMillis() - beginTime);
|
||||
logger.info("Configure your DS to use the following DNS server: {}", hostIpAddress);
|
||||
|
||||
if(mainView != null) {
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
mainView.setDashboardButtonEnabled(true);
|
||||
mainView.setStatusLabelText("Configure your DS to use the following DNS server: %s".formatted(hostIpAddress));
|
||||
});
|
||||
}
|
||||
} else {
|
||||
stopServers();
|
||||
|
||||
if(mainView != null) {
|
||||
SwingUtilities.invokeLater(() -> mainView.setStatusLabelText(
|
||||
"ERROR: Entralinked failed to start. Please check the logs for info."));
|
||||
}
|
||||
}
|
||||
|
||||
initialized = true;
|
||||
}
|
||||
|
||||
public boolean startServers() {
|
||||
logger.info("Starting servers ...");
|
||||
return httpServer.start() && gameSpyServer.start() && dnsServer.start();
|
||||
}
|
||||
|
||||
public void stopServers() {
|
||||
logger.info("Stopping servers ...");
|
||||
|
||||
if(httpServer != null) {
|
||||
httpServer.stop();
|
||||
}
|
||||
@@ -143,21 +158,25 @@ public class Entralinked {
|
||||
|
||||
private Configuration loadConfigFile() {
|
||||
logger.info("Loading configuration ...");
|
||||
Configuration configuration = null;
|
||||
|
||||
try {
|
||||
File configFile = new File("config.json");
|
||||
|
||||
if(!configFile.exists()) {
|
||||
logger.info("No configuration file exists - default configuration will be used");
|
||||
mapper.writeValue(configFile, Configuration.DEFAULT);
|
||||
return Configuration.DEFAULT;
|
||||
configuration = Configuration.DEFAULT;
|
||||
} else {
|
||||
return mapper.readValue(configFile, Configuration.class);
|
||||
configuration = mapper.readValue(configFile, Configuration.class);
|
||||
}
|
||||
|
||||
mapper.writeValue(configFile, configuration);
|
||||
} catch(IOException e) {
|
||||
logger.error("Could not load configuration - default configuration will be used", e);
|
||||
return Configuration.DEFAULT;
|
||||
configuration = Configuration.DEFAULT;
|
||||
}
|
||||
|
||||
return configuration;
|
||||
}
|
||||
|
||||
public Configuration getConfiguration() {
|
||||
@@ -175,4 +194,8 @@ public class Entralinked {
|
||||
public PlayerManager getPlayerManager() {
|
||||
return playerManager;
|
||||
}
|
||||
|
||||
public boolean isInitialized() {
|
||||
return initialized;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package entralinked.gui;
|
||||
|
||||
import java.awt.BorderLayout;
|
||||
import java.awt.Color;
|
||||
import java.awt.Desktop;
|
||||
import java.awt.Dimension;
|
||||
import java.awt.Font;
|
||||
@@ -9,16 +10,19 @@ import java.awt.event.WindowEvent;
|
||||
import java.io.IOException;
|
||||
import java.net.URISyntaxException;
|
||||
import java.net.URL;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
import javax.swing.ImageIcon;
|
||||
import javax.swing.JButton;
|
||||
import javax.swing.JFrame;
|
||||
import javax.swing.JLabel;
|
||||
import javax.swing.JMenu;
|
||||
import javax.swing.JMenuBar;
|
||||
import javax.swing.JPanel;
|
||||
import javax.swing.JScrollPane;
|
||||
import javax.swing.JTextPane;
|
||||
import javax.swing.UIManager;
|
||||
import javax.swing.UnsupportedLookAndFeelException;
|
||||
import javax.swing.text.AttributeSet;
|
||||
import javax.swing.text.BadLocationException;
|
||||
import javax.swing.text.DefaultCaret;
|
||||
@@ -27,30 +31,31 @@ import javax.swing.text.SimpleAttributeSet;
|
||||
import javax.swing.text.StyleConstants;
|
||||
import javax.swing.text.StyleContext;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.apache.logging.log4j.Level;
|
||||
|
||||
import com.formdev.flatlaf.intellijthemes.FlatOneDarkIJTheme;
|
||||
|
||||
import entralinked.Entralinked;
|
||||
import entralinked.utility.ConsumerAppender;
|
||||
import entralinked.utility.SwingUtility;
|
||||
|
||||
/**
|
||||
* Simple Swing user interface.
|
||||
*/
|
||||
public class MainView {
|
||||
|
||||
private static Logger logger = LogManager.getLogger();
|
||||
public static final Color TEXT_COLOR = Color.WHITE.darker();
|
||||
public static final Color TEXT_COLOR_WARN = Color.YELLOW.darker();
|
||||
public static final Color TEXT_COLOR_ERROR = Color.RED.darker();
|
||||
private final StyleContext styleContext = StyleContext.getDefaultStyleContext();
|
||||
private final AttributeSet fontAttribute = styleContext.addAttribute(SimpleAttributeSet.EMPTY, StyleConstants.FontFamily, "Consolas");
|
||||
private final JButton dashboardButton;
|
||||
private final JLabel statusLabel;
|
||||
|
||||
public MainView(Entralinked entralinked) {
|
||||
// Try set Look and Feel
|
||||
try {
|
||||
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
|
||||
} catch (ReflectiveOperationException | UnsupportedLookAndFeelException e) {
|
||||
logger.error("Could not set Look and Feel", e);
|
||||
}
|
||||
// Set look and feel
|
||||
FlatOneDarkIJTheme.setup();
|
||||
UIManager.getDefaults().put("Component.focusedBorderColor", UIManager.get("Component.borderColor"));
|
||||
|
||||
// Create dashboard button
|
||||
dashboardButton = new JButton("Open User Dashboard");
|
||||
@@ -87,8 +92,12 @@ public class MainView {
|
||||
// Create console output appender
|
||||
ConsumerAppender.addConsumer("GuiOutput", message -> {
|
||||
Document document = consoleOutputPane.getDocument();
|
||||
Level level = message.level();
|
||||
Color color = level == Level.ERROR ? TEXT_COLOR_ERROR : level == Level.WARN ? TEXT_COLOR_WARN : TEXT_COLOR;
|
||||
AttributeSet colorAttribute = styleContext.addAttribute(fontAttribute, StyleConstants.Foreground, color);
|
||||
|
||||
try {
|
||||
consoleOutputPane.getDocument().insertString(document.getLength(), message, fontAttribute);
|
||||
consoleOutputPane.getDocument().insertString(document.getLength(), message.formattedMessage(), colorAttribute);
|
||||
} catch(BadLocationException e) {}
|
||||
});
|
||||
|
||||
@@ -104,6 +113,15 @@ public class MainView {
|
||||
|
||||
// Create window
|
||||
JFrame frame = new JFrame("Entralinked");
|
||||
|
||||
// Create menu bar
|
||||
JMenuBar menuBar = new JMenuBar();
|
||||
JMenu helpMenu = new JMenu("Help");
|
||||
helpMenu.add(SwingUtility.createAction("Update PID (Error 60000)", () -> new PidToolDialog(entralinked, frame)));
|
||||
helpMenu.add(SwingUtility.createAction("GitHub", () -> openUrl("https://github.com/kuroppoi/entralinked")));
|
||||
menuBar.add(helpMenu);
|
||||
|
||||
// Set window properties
|
||||
frame.addWindowListener(new WindowAdapter() {
|
||||
@Override
|
||||
public void windowClosing(WindowEvent event) {
|
||||
@@ -119,8 +137,13 @@ public class MainView {
|
||||
});
|
||||
}
|
||||
});
|
||||
frame.setIconImages(List.of(
|
||||
new ImageIcon(getClass().getResource("/icon-64x.png")).getImage(),
|
||||
new ImageIcon(getClass().getResource("/icon-32x.png")).getImage(),
|
||||
new ImageIcon(getClass().getResource("/icon-16x.png")).getImage()));
|
||||
frame.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
|
||||
frame.setMinimumSize(new Dimension(512, 288));
|
||||
frame.setJMenuBar(menuBar);
|
||||
frame.add(panel);
|
||||
frame.pack();
|
||||
frame.setLocationRelativeTo(null);
|
||||
@@ -131,7 +154,6 @@ public class MainView {
|
||||
dashboardButton.setEnabled(enabled);
|
||||
}
|
||||
|
||||
|
||||
public void setStatusLabelText(String text) {
|
||||
statusLabel.setText(text);
|
||||
}
|
||||
|
||||
116
src/main/java/entralinked/gui/PidToolDialog.java
Normal file
116
src/main/java/entralinked/gui/PidToolDialog.java
Normal file
@@ -0,0 +1,116 @@
|
||||
package entralinked.gui;
|
||||
|
||||
import java.awt.GridBagLayout;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import javax.swing.BorderFactory;
|
||||
import javax.swing.JButton;
|
||||
import javax.swing.JDialog;
|
||||
import javax.swing.JFrame;
|
||||
import javax.swing.JLabel;
|
||||
import javax.swing.JOptionPane;
|
||||
import javax.swing.JPanel;
|
||||
|
||||
import com.formdev.flatlaf.extras.components.FlatTextField;
|
||||
|
||||
import entralinked.Entralinked;
|
||||
import entralinked.model.user.User;
|
||||
import entralinked.utility.MD5;
|
||||
import entralinked.utility.SwingUtility;
|
||||
|
||||
public class PidToolDialog {
|
||||
|
||||
public static final Pattern WFC_ID_PATTERN = Pattern.compile("[0-9]{16}");
|
||||
public static final Pattern FRIEND_CODE_PATTERN = Pattern.compile("[0-9]{12}");
|
||||
|
||||
public PidToolDialog(Entralinked entralinked, JFrame frame) {
|
||||
// Create dialog
|
||||
JDialog dialog = new JDialog(frame, "PID Tool");
|
||||
|
||||
// Create input fields
|
||||
FlatTextField wfcIdField = new FlatTextField();
|
||||
wfcIdField.setPlaceholderText("XXXX-XXXX-XXXX-XXXX");
|
||||
FlatTextField friendCodeField = new FlatTextField();
|
||||
friendCodeField.setPlaceholderText("XXXX-XXXX-XXXX");
|
||||
|
||||
// Create logic
|
||||
JButton updateButton = new JButton("Update");
|
||||
updateButton.addActionListener(event -> {
|
||||
if(!entralinked.isInitialized()) {
|
||||
JOptionPane.showMessageDialog(dialog, "Please wait for Entralinked to finish starting.", "Attention", JOptionPane.WARNING_MESSAGE);
|
||||
return;
|
||||
}
|
||||
|
||||
String userId = wfcIdField.getText().replace("-", "").replaceAll("\\s+", "");
|
||||
String friendCodeString = friendCodeField.getText().replace("-", "").replaceAll("\\s+", "");
|
||||
|
||||
// Make sure WFC ID is valid
|
||||
if(!WFC_ID_PATTERN.matcher(userId).matches()) {
|
||||
JOptionPane.showMessageDialog(dialog, "Please enter a valid Wi-Fi Connection ID.", "Attention", JOptionPane.WARNING_MESSAGE);
|
||||
return;
|
||||
}
|
||||
|
||||
// Make sure Friend Code is valid
|
||||
if(!FRIEND_CODE_PATTERN.matcher(friendCodeString).matches()) {
|
||||
JOptionPane.showMessageDialog(dialog, "Please enter a valid Friend Code.", "Attention", JOptionPane.WARNING_MESSAGE);
|
||||
return;
|
||||
}
|
||||
|
||||
User user = entralinked.getUserManager().getUser(userId.substring(0, 13));
|
||||
|
||||
// Make sure user exists
|
||||
if(user == null) {
|
||||
JOptionPane.showMessageDialog(dialog, "This Wi-Fi Connection ID does not exist.", "Attention", JOptionPane.WARNING_MESSAGE);
|
||||
return;
|
||||
}
|
||||
|
||||
long friendCode = Long.parseLong(friendCodeString);
|
||||
int profileId = (int)(friendCode & 0x7FFFFFFF);
|
||||
int checksum = (int)(friendCode >> 32);
|
||||
|
||||
// Compute friend code checksum
|
||||
byte[] buffer = {0x00, 0x00, 0x00, 0x00, 0x4A, 0x41, 0x52, 0x49}; // Last 4 bytes is inverted game code (IRAJ)
|
||||
buffer[0] = (byte)(profileId & 0xFF);
|
||||
buffer[1] = (byte)(profileId >> 8 & 0xFF);
|
||||
buffer[2] = (byte)(profileId >> 16 & 0xFF);
|
||||
buffer[3] = (byte)(profileId >> 24 & 0xFF);
|
||||
byte[] hash = MD5.digest(buffer);
|
||||
int computedChecksum = hash[0] >> 1 & 0x7F;
|
||||
|
||||
// Compare checksums
|
||||
if(computedChecksum != checksum) {
|
||||
JOptionPane.showMessageDialog(dialog, "This is not a valid Friend Code. Please check for typos.", "Attention", JOptionPane.WARNING_MESSAGE);
|
||||
return;
|
||||
}
|
||||
|
||||
// Everything checks out -- update the profile id!
|
||||
user.setProfileIdOverride(profileId);
|
||||
JOptionPane.showMessageDialog(dialog,
|
||||
"All done! Please restart your game and use Game Sync.\nGame profile data will be updated and saved once you do so.");
|
||||
});
|
||||
|
||||
// Create content panel
|
||||
JPanel panel = new JPanel(new GridBagLayout());
|
||||
String infoLabel = """
|
||||
<html>
|
||||
Enter the Wi-Fi Connection ID found in the internet settings of your DS<br>
|
||||
as well as your Friend Code which you can view in-game using the Pal Pad.<br>
|
||||
Confirm that the input data is correct and press 'Update'<br>
|
||||
</html>
|
||||
""";
|
||||
panel.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
|
||||
panel.add(new JLabel(infoLabel), SwingUtility.createConstraints(0, 0, 2, 1));
|
||||
panel.add(new JLabel("Wi-Fi Connection ID"), SwingUtility.createConstraints(0, 1, 1, 1, 0, 1));
|
||||
panel.add(wfcIdField, SwingUtility.createConstraints(1, 1));
|
||||
panel.add(new JLabel("Friend Code"), SwingUtility.createConstraints(0, 2, 1, 1, 0, 1));
|
||||
panel.add(friendCodeField, SwingUtility.createConstraints(1, 2));
|
||||
panel.add(updateButton, SwingUtility.createConstraints(0, 3, 2, 1));
|
||||
|
||||
// Set dialog properties
|
||||
dialog.setResizable(false);
|
||||
dialog.add(panel);
|
||||
dialog.pack();
|
||||
dialog.setLocationRelativeTo(frame);
|
||||
dialog.setVisible(true);
|
||||
}
|
||||
}
|
||||
@@ -33,7 +33,6 @@ public class Player {
|
||||
|
||||
public void resetDreamInfo() {
|
||||
status = PlayerStatus.AWAKE;
|
||||
gameVersion = null;
|
||||
dreamerInfo = null;
|
||||
encounters.clear();
|
||||
items.clear();
|
||||
|
||||
@@ -2,7 +2,7 @@ package entralinked.model.user;
|
||||
|
||||
public class GameProfile {
|
||||
|
||||
private final int id;
|
||||
private int id;
|
||||
private String firstName;
|
||||
private String lastName;
|
||||
private String aimName;
|
||||
@@ -20,6 +20,10 @@ public class GameProfile {
|
||||
this.zipCode = zipCode;
|
||||
}
|
||||
|
||||
public void setId(int id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public int getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ public class User {
|
||||
private final String password; // I debated hashing it, but.. it's a 3-digit password...
|
||||
private final Map<String, GameProfile> profiles = new HashMap<>();
|
||||
private final Map<String, Dlc> dlcOverrides = new HashMap<>();
|
||||
private int profileIdOverride; // For making it easier for the user to fix error 60000
|
||||
|
||||
public User(String id, String password) {
|
||||
this.id = id;
|
||||
@@ -70,4 +71,12 @@ public class User {
|
||||
public Dlc getDlcOverride(String type) {
|
||||
return dlcOverrides.get(type);
|
||||
}
|
||||
|
||||
public void setProfileIdOverride(int profileIdOverride) {
|
||||
this.profileIdOverride = profileIdOverride;
|
||||
}
|
||||
|
||||
public int getProfileIdOverride() {
|
||||
return profileIdOverride;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,6 @@ public class UserManager {
|
||||
private static final Logger logger = LogManager.getLogger();
|
||||
private final ObjectMapper mapper = new ObjectMapper().enable(SerializationFeature.INDENT_OUTPUT);
|
||||
private final Map<String, User> users = new ConcurrentHashMap<>();
|
||||
private final Map<Integer, GameProfile> profiles = new ConcurrentHashMap<>();
|
||||
private final Map<String, ServiceSession> serviceSessions = new ConcurrentHashMap<>();
|
||||
private final File dataDirectory = new File("users");
|
||||
|
||||
@@ -49,7 +48,7 @@ public class UserManager {
|
||||
}
|
||||
}
|
||||
|
||||
logger.info("Loaded {} user(s) with a total of {} profile(s)", users.size(), profiles.size());
|
||||
logger.info("Loaded {} user(s)", users.size());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -75,20 +74,8 @@ public class UserManager {
|
||||
throw new IOException("Duplicate user ID %s".formatted(id));
|
||||
}
|
||||
|
||||
// Check for duplicate profile IDs before indexing anything
|
||||
Collection<GameProfile> userProfiles = user.getProfiles();
|
||||
|
||||
if(userProfiles.stream().map(GameProfile::getId).anyMatch(profiles::containsKey)) {
|
||||
throw new IOException("Duplicate profile ID in user %s".formatted(id));
|
||||
}
|
||||
|
||||
// Index user
|
||||
users.put(id, user);
|
||||
|
||||
// Index profiles
|
||||
for(GameProfile profile : userProfiles) {
|
||||
profiles.put(profile.getId(), profile);
|
||||
}
|
||||
} catch(IOException e) {
|
||||
logger.error("Could not load user data at {}", inputFile.getAbsolutePath(), e);
|
||||
}
|
||||
@@ -190,14 +177,14 @@ public class UserManager {
|
||||
/**
|
||||
* Registers a user with the given ID and password.
|
||||
|
||||
* @return {@code true} if the registration was successful.
|
||||
* Otherwise, if this user ID has already been registered, or if the user data could not be saved, {@code false} is returned instead.
|
||||
* @return The {@link User} object of the newly registered user if the registration was successful.
|
||||
* Otherwise, if this user ID has already been registered, or if the user data could not be saved, {@code null} is returned instead.
|
||||
*/
|
||||
public boolean registerUser(String userId, String plainPassword) {
|
||||
public User registerUser(String userId, String plainPassword) {
|
||||
// Check if user id already exists
|
||||
if(users.containsKey(userId)) {
|
||||
logger.warn("Attempted to register user with duplicate ID: {}", userId);
|
||||
return false;
|
||||
return null;
|
||||
}
|
||||
|
||||
// Create user
|
||||
@@ -205,11 +192,11 @@ public class UserManager {
|
||||
|
||||
// Save user data and return null if it fails
|
||||
if(!saveUser(user)) {
|
||||
return false;
|
||||
return null;
|
||||
}
|
||||
|
||||
users.put(userId, user);
|
||||
return true;
|
||||
return user;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -233,7 +220,7 @@ public class UserManager {
|
||||
return null;
|
||||
}
|
||||
|
||||
int profileId = nextProfileId();
|
||||
int profileId = (int)(Math.random() * Integer.MAX_VALUE);
|
||||
GameProfile profile = new GameProfile(profileId);
|
||||
user.addProfile(branchCode, profile);
|
||||
|
||||
@@ -243,24 +230,9 @@ public class UserManager {
|
||||
return null;
|
||||
}
|
||||
|
||||
profiles.put(profileId, profile);
|
||||
return profile;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return A unique random 32-bit profile ID.
|
||||
*/
|
||||
private int nextProfileId() {
|
||||
int profileId = (int)(Math.random() * Integer.MAX_VALUE);
|
||||
|
||||
// I live for that microscopic chance of StackOverflowError
|
||||
if(profiles.containsKey(profileId)) {
|
||||
return nextProfileId();
|
||||
}
|
||||
|
||||
return profileId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {@code true} if a user with the specified ID exists, otherwise {@code false}.
|
||||
*/
|
||||
|
||||
@@ -40,7 +40,6 @@ public abstract class NettyServerBase {
|
||||
|
||||
public boolean start() {
|
||||
if(started) {
|
||||
logger.warn("start() was called while {} server was already running!", name);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -59,7 +58,6 @@ public abstract class NettyServerBase {
|
||||
|
||||
public boolean stop() {
|
||||
if(!started) {
|
||||
logger.warn("stop() was called while {} server wasn't running!", name);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ public class DnsQueryHandler extends SimpleChannelInboundHandler<DatagramDnsQuer
|
||||
@Override
|
||||
protected void channelRead0(ChannelHandlerContext ctx, DatagramDnsQuery query) throws Exception {
|
||||
DefaultDnsQuestion question = query.recordAt(DnsSection.QUESTION);
|
||||
logger.debug("Received {}", question);
|
||||
DnsRecordType type = question.type();
|
||||
|
||||
// We only need type A (32 bit IPv4) for the DS
|
||||
|
||||
@@ -2,9 +2,6 @@ package entralinked.network.dns;
|
||||
|
||||
import java.net.InetAddress;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
import entralinked.network.NettyServerBase;
|
||||
import io.netty.bootstrap.Bootstrap;
|
||||
import io.netty.channel.Channel;
|
||||
@@ -17,13 +14,11 @@ import io.netty.handler.codec.dns.DatagramDnsResponseEncoder;
|
||||
|
||||
public class DnsServer extends NettyServerBase {
|
||||
|
||||
private static final Logger logger = LogManager.getLogger();
|
||||
private InetAddress hostAddress;
|
||||
|
||||
public DnsServer(InetAddress hostAddress) {
|
||||
super("DNS", 53);
|
||||
this.hostAddress = hostAddress;
|
||||
logger.info("DNS queries will be resolved to {}", hostAddress.getHostAddress());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -28,6 +28,7 @@ import io.netty.channel.ChannelFuture;
|
||||
import io.netty.channel.ChannelFutureListener;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.channel.SimpleChannelInboundHandler;
|
||||
import io.netty.handler.timeout.ReadTimeoutException;
|
||||
|
||||
/**
|
||||
* GameSpy request handler.
|
||||
@@ -50,6 +51,7 @@ public class GameSpyHandler extends SimpleChannelInboundHandler<GameSpyRequest>
|
||||
@Override
|
||||
public void channelActive(ChannelHandlerContext ctx) {
|
||||
channel = ctx.channel();
|
||||
logger.debug("Sending GameSpy server challenge");
|
||||
|
||||
// Generate random server challenge
|
||||
serverChallenge = CredentialGenerator.generateChallenge(10);
|
||||
@@ -60,6 +62,8 @@ public class GameSpyHandler extends SimpleChannelInboundHandler<GameSpyRequest>
|
||||
|
||||
@Override
|
||||
public void channelInactive(ChannelHandlerContext ctx) {
|
||||
logger.debug("User {} disconnected from GameSpy server", user == null ? null : user.getFormattedId());
|
||||
|
||||
// Clear data
|
||||
serverChallenge = null;
|
||||
sessionKey = -1;
|
||||
@@ -69,15 +73,23 @@ public class GameSpyHandler extends SimpleChannelInboundHandler<GameSpyRequest>
|
||||
|
||||
@Override
|
||||
protected void channelRead0(ChannelHandlerContext ctx, GameSpyRequest request) throws Exception {
|
||||
logger.debug("Received {}", request);
|
||||
request.process(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
|
||||
if(cause instanceof ClosedChannelException) {
|
||||
logger.debug("Silently discarded ClosedChannelException");
|
||||
return; // Ignore this stupid exception.
|
||||
}
|
||||
|
||||
// Handle timeout
|
||||
if(cause instanceof ReadTimeoutException) {
|
||||
logger.debug("User {} timed out", user == null ? null : user.getFormattedId());
|
||||
return;
|
||||
}
|
||||
|
||||
logger.error("Exception caught in GameSpy handler", cause);
|
||||
|
||||
// Send error message and close channel afterwards.
|
||||
@@ -92,6 +104,7 @@ public class GameSpyHandler extends SimpleChannelInboundHandler<GameSpyRequest>
|
||||
ServiceSession session = userManager.getServiceSession(authToken, "gamespy");
|
||||
|
||||
if(session == null) {
|
||||
logger.debug("Rejecting GameSpy login request because the partner token is invalid");
|
||||
sendErrorMessage(0x200, "Invalid partner token.", request.sequenceId());
|
||||
return;
|
||||
}
|
||||
@@ -101,6 +114,7 @@ public class GameSpyHandler extends SimpleChannelInboundHandler<GameSpyRequest>
|
||||
String expectedResponse = createCredentialHash(partnerChallengeHash, authToken, clientChallenge, serverChallenge);
|
||||
|
||||
if(!expectedResponse.equals(request.response())) {
|
||||
logger.debug("Rejecting GameSpy login request because the challenge response is invalid");
|
||||
sendErrorMessage(0x202, "Invalid response.", request.sequenceId());
|
||||
return;
|
||||
}
|
||||
@@ -119,6 +133,17 @@ public class GameSpyHandler extends SimpleChannelInboundHandler<GameSpyRequest>
|
||||
}
|
||||
}
|
||||
|
||||
// Update profile id if an override is set
|
||||
int profileIdOverride = user.getProfileIdOverride();
|
||||
|
||||
if(profileIdOverride > 0) {
|
||||
profile.setId(profileIdOverride);
|
||||
user.setProfileIdOverride(0);
|
||||
userManager.saveUser(user); // It's not too big of a deal if this fails for some reason
|
||||
}
|
||||
|
||||
logger.info("User {} logged in with profile {}", user.getFormattedId(), profile.getId());
|
||||
|
||||
// Prepare and send response
|
||||
sessionKey = secureRandom.nextInt(Integer.MAX_VALUE);
|
||||
String proof = createCredentialHash(partnerChallengeHash, authToken, serverChallenge, clientChallenge);
|
||||
@@ -171,10 +196,9 @@ public class GameSpyHandler extends SimpleChannelInboundHandler<GameSpyRequest>
|
||||
passwordHash));
|
||||
}
|
||||
|
||||
public void destroySessionKey(int sessionKey) {
|
||||
if(validateSessionKey(sessionKey)) {
|
||||
sessionKey = -1;
|
||||
}
|
||||
public void handleLogout() {
|
||||
logger.info("User {} logged out of profile {}", user.getFormattedId(), profile.getId());
|
||||
sessionKey = -1; // Is there a point?
|
||||
}
|
||||
|
||||
public boolean validateSessionKey(int sessionKey) {
|
||||
|
||||
@@ -26,6 +26,7 @@ import io.netty.channel.ChannelPipeline;
|
||||
import io.netty.channel.epoll.EpollServerSocketChannel;
|
||||
import io.netty.channel.socket.nio.NioServerSocketChannel;
|
||||
import io.netty.handler.codec.DelimiterBasedFrameDecoder;
|
||||
import io.netty.handler.timeout.ReadTimeoutHandler;
|
||||
import io.netty.util.concurrent.DefaultEventExecutor;
|
||||
import io.netty.util.concurrent.EventExecutorGroup;
|
||||
|
||||
@@ -64,6 +65,7 @@ public class GameSpyServer extends NettyServerBase {
|
||||
@Override
|
||||
protected void initChannel(Channel channel) throws Exception {
|
||||
ChannelPipeline pipeline = channel.pipeline();
|
||||
pipeline.addLast(new ReadTimeoutHandler(180));
|
||||
pipeline.addLast(new DelimiterBasedFrameDecoder(512, Unpooled.wrappedBuffer("\\final\\".getBytes())));
|
||||
pipeline.addLast(new GameSpyRequestDecoder(mapper, requestTypes));
|
||||
pipeline.addLast(new GameSpyMessageEncoder(mapper));
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
package entralinked.network.gamespy.request;
|
||||
|
||||
import entralinked.network.gamespy.GameSpyHandler;
|
||||
import entralinked.network.gamespy.message.GameSpyMessage;
|
||||
|
||||
@GameSpyMessage(name = "ka")
|
||||
public record GameSpyKeepAliveRequest() implements GameSpyRequest {
|
||||
|
||||
@Override
|
||||
public void process(GameSpyHandler handler) {
|
||||
handler.sendMessage(this); // Pong it back
|
||||
}
|
||||
public void process(GameSpyHandler handler) {}
|
||||
}
|
||||
|
||||
@@ -28,4 +28,11 @@ public record GameSpyLoginRequest(
|
||||
public void process(GameSpyHandler handler) {
|
||||
handler.handleLoginRequest(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return ("GameSpyLoginRequest[sequenceId=%s, userId=%s, gameName=%s, profileId=%s, namespaceId=%s, partnerId=%s, productId=%s, "
|
||||
+ "sdkRevision=%s, firewall=%s, port=%s, quiet=%s]")
|
||||
.formatted(sequenceId, userId, gameName, profileId, namespaceId, partnerId, productId, sdkRevision, firewall, port, quiet);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,14 @@ public record GameSpyLogoutRequest(
|
||||
|
||||
@Override
|
||||
public void process(GameSpyHandler handler) {
|
||||
handler.destroySessionKey(sessionKey);
|
||||
if(handler.validateSessionKey(sessionKey)) {
|
||||
handler.handleLogout();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
// Exlude session key
|
||||
return "GameSpyLogoutRequest[]";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,4 +16,10 @@ public record GameSpyProfileRequest(
|
||||
handler.handleProfileRequest(this);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
// Exlude session key
|
||||
return "GameSpyProfileRequest[sequenceId=%s, profileId=%s]".formatted(sequenceId, profileId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,4 +21,11 @@ public record GameSpyProfileUpdateRequest(
|
||||
handler.handleUpdateProfileRequest(this);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
// Exlude session key
|
||||
return "GameSpyProfileUpdateRequest[partnerId=%s, firstName=%s, lastName=%s, aimName=%s, zipCode=%s]"
|
||||
.formatted(partnerId, firstName, lastName, aimName, zipCode);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,4 +14,10 @@ public record GameSpyStatusRequest(
|
||||
public void process(GameSpyHandler handler) {
|
||||
handler.validateSessionKey(sessionKey);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
// Exlude session key
|
||||
return "GameSpyStatusRequest[statusString=%s, locationString=%s]".formatted(statusString, locationString);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,7 +75,6 @@ public class HttpServer {
|
||||
|
||||
public boolean start() {
|
||||
if(started) {
|
||||
logger.warn("start() was called while HTTP server was already running!");
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -94,7 +93,6 @@ public class HttpServer {
|
||||
|
||||
public boolean stop() {
|
||||
if(!started) {
|
||||
logger.warn("stop() was called while HTTP server wasn't running!");
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -92,7 +92,7 @@ public class DashboardHandler implements HttpHandler {
|
||||
|
||||
// Cache the result
|
||||
skinPreviewCache.put("%s/%s".formatted(player.getGameSyncId(), version2 ? "CGEAR2" : "CGEAR"), image);
|
||||
} catch(IOException | IndexOutOfBoundsException e) {
|
||||
} catch(IOException | IndexOutOfBoundsException | NullPointerException e) {
|
||||
logger.error("Could not load custom C-Gear skin preview for player {}", player.getGameSyncId(), e);
|
||||
}
|
||||
}
|
||||
@@ -284,6 +284,12 @@ public class DashboardHandler implements HttpHandler {
|
||||
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));
|
||||
|
||||
@@ -4,6 +4,9 @@ import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import entralinked.Entralinked;
|
||||
@@ -25,6 +28,7 @@ import io.javalin.http.HttpStatus;
|
||||
*/
|
||||
public class DlsHandler implements HttpHandler {
|
||||
|
||||
private static final Logger logger = LogManager.getLogger();
|
||||
private final ObjectMapper mapper = new ObjectMapper(new UrlEncodedFormFactory());
|
||||
private final DlcList dlcList;
|
||||
private final UserManager userManager;
|
||||
@@ -45,11 +49,13 @@ public class DlsHandler implements HttpHandler {
|
||||
private void handleDownloadRequest(Context ctx) throws IOException {
|
||||
// Deserialize request body
|
||||
DlsRequest request = mapper.readValue(ctx.body().replace("%2A", "*"), DlsRequest.class);
|
||||
logger.debug("Received {}", request);
|
||||
|
||||
// Check if service session is valid
|
||||
ServiceSession session = userManager.getServiceSession(request.serviceToken(), "dls1.nintendowifi.net");
|
||||
|
||||
if(session == null) {
|
||||
logger.debug("Rejecting DLS request because the service session has expired");
|
||||
ctx.status(HttpStatus.UNAUTHORIZED);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -21,5 +21,13 @@ public record DlsRequest(
|
||||
@JsonProperty("contents") String dlcName, // action=contents
|
||||
@JsonProperty("attr1") String dlcType, // action=list
|
||||
@JsonProperty("attr2") int dlcIndex, // action=list
|
||||
@JsonProperty("offset") int offset, // ?
|
||||
@JsonProperty("num") int num) {} // ?
|
||||
@JsonProperty("offset") int offset, // Start offset in the list
|
||||
@JsonProperty("num") int num) { // Number of entries
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return ("DlsRequest[userId=%s, gameCode=%s, accessPointInfo=%s, action=%s, dlcGameCode=%s, dlcName=%s, dlcType=%s, "
|
||||
+ "dlcIndex=%s, offset=%s, num=%s]")
|
||||
.formatted(userId, gameCode, accessPointInfo, action, dlcGameCode, dlcName, dlcType, dlcIndex, offset, num);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,9 @@ package entralinked.network.http.nas;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
||||
|
||||
@@ -21,6 +24,7 @@ import io.javalin.http.Context;
|
||||
*/
|
||||
public class NasHandler implements HttpHandler {
|
||||
|
||||
private static final Logger logger = LogManager.getLogger();
|
||||
private final ObjectMapper mapper = new ObjectMapper(new UrlEncodedFormFactory()).registerModule(new JavaTimeModule());
|
||||
private final Configuration configuration;
|
||||
private final UserManager userManager;
|
||||
@@ -42,6 +46,7 @@ public class NasHandler implements HttpHandler {
|
||||
private void handleNasRequest(Context ctx) throws IOException {
|
||||
// Deserialize body into a request object
|
||||
NasRequest request = mapper.readValue(ctx.body(), NasRequest.class);
|
||||
logger.debug("Received {}", request);
|
||||
|
||||
// Determine handler function based on request action
|
||||
HttpRequestHandler<NasRequest> handler = switch(request.action()) {
|
||||
@@ -61,6 +66,7 @@ public class NasHandler implements HttpHandler {
|
||||
private void handleLogin(NasRequest request, Context ctx) throws IOException {
|
||||
// Make sure branch code is present
|
||||
if(request.branchCode() == null) {
|
||||
logger.debug("Rejecting NAS login request because no branch code is present");
|
||||
result(ctx, NasReturnCode.BAD_REQUEST);
|
||||
return;
|
||||
}
|
||||
@@ -77,7 +83,7 @@ public class NasHandler implements HttpHandler {
|
||||
|
||||
// Try to register, if the configuration allows it
|
||||
if(!UserManager.isValidUserId(userId) || userManager.doesUserExist(userId)
|
||||
|| !userManager.registerUser(userId, request.password())) {
|
||||
|| userManager.registerUser(userId, request.password()) == null) {
|
||||
// Oh well, try again!
|
||||
result(ctx, NasReturnCode.USER_NOT_FOUND);
|
||||
return;
|
||||
@@ -85,10 +91,12 @@ public class NasHandler implements HttpHandler {
|
||||
|
||||
// Should *never* return null in this location
|
||||
user = userManager.authenticateUser(userId, request.password());
|
||||
logger.info("Created account for user {}", user.getFormattedId());
|
||||
}
|
||||
|
||||
// Prepare GameSpy server credentials
|
||||
ServiceCredentials credentials = userManager.createServiceSession(user, "gamespy", request.branchCode());
|
||||
logger.info("Created GameSpy session for user {}", user.getFormattedId());
|
||||
result(ctx, new NasLoginResponse("gamespy.com", credentials.authToken(), credentials.challenge()));
|
||||
}
|
||||
|
||||
@@ -105,11 +113,14 @@ public class NasHandler implements HttpHandler {
|
||||
}
|
||||
|
||||
// Try to register user
|
||||
if(!userManager.registerUser(userId, request.password())) {
|
||||
User user = userManager.registerUser(userId, request.password());
|
||||
|
||||
if(user == null) {
|
||||
result(ctx, NasReturnCode.INTERNAL_SERVER_ERROR);
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info("Created account for user {}", user.getFormattedId());
|
||||
result(ctx, NasReturnCode.REGISTRATION_SUCCESS);
|
||||
}
|
||||
|
||||
@@ -125,15 +136,19 @@ public class NasHandler implements HttpHandler {
|
||||
return;
|
||||
}
|
||||
|
||||
String type = request.serviceType();
|
||||
|
||||
// Determine service location from type
|
||||
String service = switch(request.serviceType()) {
|
||||
String service = switch(type) {
|
||||
case "0000" -> "external"; // External game-specific service
|
||||
case "9000" -> "dls1.nintendowifi.net"; // Download server
|
||||
default -> throw new IllegalArgumentException("Invalid service type: " + request.serviceType());
|
||||
default -> throw new IllegalArgumentException("Invalid service type: " + type);
|
||||
};
|
||||
|
||||
|
||||
// Prepare user credentials
|
||||
ServiceCredentials credentials = userManager.createServiceSession(user, service, null);
|
||||
logger.info("Created {} session for user {}",
|
||||
type.equals("0000") ? "PGL" : type.equals("9000") ? "DLS1" : "this should never be logged", user.getFormattedId());
|
||||
result(ctx, new NasServiceLocationResponse(true, service, credentials.authToken()));
|
||||
}
|
||||
|
||||
|
||||
@@ -29,4 +29,13 @@ public record NasRequest(
|
||||
// Request-specific info
|
||||
@JsonProperty(value = "action", required = true) String action,
|
||||
@JsonProperty("gsbrcd") String branchCode, // action=login
|
||||
@JsonProperty("svc") String serviceType) {} // action=SVCLOC
|
||||
@JsonProperty("svc") String serviceType) { // action=SVCLOC
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return ("NasRequest[userId=%s, gameCode=%s, makerCode=%s, unitCode=%s, sdkVersion=%s, language=%s, bssid=%s, "
|
||||
+ "accessPointInfo=%s, deviceTime=%s, action=%s, serviceType=%s]")
|
||||
.formatted(userId, gameCode, makerCode, unitCode, sdkVersion, language, bssid,
|
||||
accessPointInfo, deviceTime, action, serviceType);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,6 +93,7 @@ public class PglHandler implements HttpHandler {
|
||||
|
||||
if(credentials == null ||
|
||||
!username.equals(credentials.getUsername()) || !password.equals(credentials.getPassword())) {
|
||||
logger.debug("Rejecting PGL request because the auth credentials were incorrect");
|
||||
ctx.status(HttpStatus.UNAUTHORIZED);
|
||||
clearTasks(ctx);
|
||||
return;
|
||||
@@ -100,11 +101,13 @@ public class PglHandler implements HttpHandler {
|
||||
|
||||
// Deserialize the request
|
||||
PglRequest request = mapper.readValue(ctx.queryString(), PglRequest.class);
|
||||
logger.debug("Received {}", request);
|
||||
|
||||
// Verify the service session token
|
||||
ServiceSession session = userManager.getServiceSession(request.token(), "external");
|
||||
|
||||
if(session == null) {
|
||||
logger.debug("Rejecting PGL request because the service session has expired");
|
||||
ctx.status(HttpStatus.UNAUTHORIZED);
|
||||
clearTasks(ctx);
|
||||
return;
|
||||
@@ -131,7 +134,7 @@ public class PglHandler implements HttpHandler {
|
||||
};
|
||||
|
||||
// Handle the request
|
||||
handler.process(request, ctx);
|
||||
handler.process(request, ctx);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -198,13 +201,14 @@ public class PglHandler implements HttpHandler {
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info("Player {} is downloading save data as user {}", player.getGameSyncId(), user.getFormattedId());
|
||||
|
||||
// Write status code
|
||||
writeStatusCode(outputStream, 0);
|
||||
|
||||
// Allow it to wake up anyway, maybe the poor sap is stuck..
|
||||
// Just don't send any other data.
|
||||
if(player.getStatus() == PlayerStatus.AWAKE) {
|
||||
logger.info("Player {} is downloading save data, but is already awake!", player.getGameSyncId());
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -312,7 +316,7 @@ public class PglHandler implements HttpHandler {
|
||||
|
||||
// Full visitor type consists of a trainer class and what I call a 'personality' index
|
||||
// that, along with the trainer class, determines which phrases the visitor uses.
|
||||
// The shope type is calculated in such an odd manner because for some reason,
|
||||
// The shop type is calculated in such an odd manner because for some reason,
|
||||
// the 'starting' index of the shop type used increases by 2 for each visitor type.
|
||||
// For example, if the visitor type is '0', then shop type '0' would be a raffle.
|
||||
// However, if the visitor type is '2', then shop type '0' results in a dojo instead.
|
||||
@@ -343,6 +347,7 @@ public class PglHandler implements HttpHandler {
|
||||
private void handleMemoryLink(PglRequest request, Context ctx) throws IOException {
|
||||
LEOutputStream outputStream = new LEOutputStream(ctx.outputStream());
|
||||
Player player = playerManager.getPlayer(request.gameSyncId());
|
||||
User user = ctx.attribute("user");
|
||||
|
||||
// Check if player exists
|
||||
if(player == null) {
|
||||
@@ -364,6 +369,8 @@ public class PglHandler implements HttpHandler {
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info("User {} is Memory Linking with player {}", user.getFormattedId(), player.getGameSyncId());
|
||||
|
||||
// Send the save data!
|
||||
try(FileInputStream inputStream = new FileInputStream(file)) {
|
||||
writeStatusCode(outputStream, 0);
|
||||
@@ -422,13 +429,15 @@ public class PglHandler implements HttpHandler {
|
||||
* POST handler for {@code /dsio/gw?p=savedata.upload}
|
||||
*/
|
||||
private void handleUploadSaveData(PglRequest request, Context ctx) throws IOException {
|
||||
// Prepare response
|
||||
LEOutputStream outputStream = new LEOutputStream(ctx.outputStream());
|
||||
|
||||
// Check if the player exists and does not already have a Pokémon tucked in
|
||||
Player player = playerManager.getPlayer(request.gameSyncId());
|
||||
User user = ctx.attribute("user");
|
||||
|
||||
if(player == null || (player.getStatus() != PlayerStatus.AWAKE && !configuration.allowOverwritingPlayerDreamInfo())) {
|
||||
// Check if the player exists, has no Pokémon tucked in already and uses the same game version
|
||||
if(player == null
|
||||
|| (!configuration.allowOverwritingPlayerDreamInfo() && player.getStatus() != PlayerStatus.AWAKE)
|
||||
|| (!configuration.allowPlayerGameVersionMismatch() && player.getGameVersion() != null
|
||||
&& request.gameVersion() != player.getGameVersion())) {
|
||||
// Skip everything
|
||||
ServletInputStream inputStream = ctx.req().getInputStream();
|
||||
|
||||
@@ -441,6 +450,8 @@ public class PglHandler implements HttpHandler {
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info("Player {} is uploading save data as user {}", player.getGameSyncId(), user.getFormattedId());
|
||||
|
||||
// Try to store save data
|
||||
if(!playerManager.storePlayerGameSaveFile(player, ctx.bodyInputStream())) {
|
||||
writeStatusCode(outputStream, 4); // Game save data IO error
|
||||
|
||||
@@ -17,4 +17,11 @@ public record PglRequest(
|
||||
public GameVersion gameVersion() {
|
||||
return GameVersion.lookup(romCode(), languageCode());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
// Exlude token
|
||||
return "PglRequest[gameSyncId=%s, type=%s, romCode=%s, languageCode=%s, dreamWorld=%s]"
|
||||
.formatted(gameSyncId, type, romCode, languageCode, dreamWorld);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,7 +55,6 @@ public class CertificateGenerator {
|
||||
+ "Fw2ewOxmIyw3d4HFGrLx9yvk6Q5HpTlSgvbgO22k/vo0N43dO7utgfn/e4Xo9alIAktnewGqj5nRm75H7iaLEJBo983jJdugeXdp6y5X6sDmaDmyw/p91TKPrKmD"
|
||||
+ "4ebbTY38D5WT67fONjW/X1OLNHg46GYYZa6etbxQ7671f7GypgwnI=";
|
||||
|
||||
|
||||
private static final String issuerPrivateKeyString =
|
||||
"eNod0Em2a0AAANAFGRBNYVgK0QalfWaIPproZfX/nH+XcG1dR4mnSxCaUu3JKUe5WLR1rND+Q+gwb3NO3ws5e0YXcydZcUtNV/35votzw9SsDstssI0TPxg5q1XPUq"
|
||||
+ "EpGgfib4UnATQKiAcZHsBOsEWg2nShyhd7CoLQznBPWW/+iLfW3bMujf723htqG+n0p30h/SClZIRZibH7I5hGgQHRqgvpuJ/IDWpH9HQZelCC0xPK8uXZ87jcwv"
|
||||
|
||||
@@ -29,9 +29,9 @@ public class ColorUtility {
|
||||
*/
|
||||
public static int convertBGR555ToRGB888(int color) {
|
||||
int red = (color & 0x1F) << 3;
|
||||
int green = ((color & 0x3E0) >> 5) << 3;
|
||||
int blue = ((color & 0x7C00) >> 10) << 3;
|
||||
return (red << 16) | (green << 8) | blue;
|
||||
int green = (color >> 5 & 0x1F) << 3;
|
||||
int blue = (color >> 10 & 0x1F) << 3;
|
||||
return ((red | red >> 5) << 16) | ((green | green >> 5) << 8) | (blue | blue >> 5);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -7,6 +7,7 @@ import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.apache.logging.log4j.Level;
|
||||
import org.apache.logging.log4j.core.Appender;
|
||||
import org.apache.logging.log4j.core.Core;
|
||||
import org.apache.logging.log4j.core.Filter;
|
||||
@@ -25,7 +26,8 @@ import org.apache.logging.log4j.core.config.plugins.PluginFactory;
|
||||
printObject = true)
|
||||
public class ConsumerAppender extends AbstractAppender {
|
||||
|
||||
protected static final Map<String, List<Consumer<String>>> consumerMap = new ConcurrentHashMap<>();
|
||||
public record LogMessage(Level level, String rawMessage, String formattedMessage) {}
|
||||
protected static final Map<String, List<Consumer<LogMessage>>> consumerMap = new ConcurrentHashMap<>();
|
||||
|
||||
protected ConsumerAppender(String name, Filter filter, Layout<? extends Serializable> layout,
|
||||
boolean ignoreExceptions, Property[] properties) {
|
||||
@@ -41,8 +43,8 @@ public class ConsumerAppender extends AbstractAppender {
|
||||
return new ConsumerAppender(name, filter, layout, ignoreExceptions, null);
|
||||
}
|
||||
|
||||
public static void addConsumer(String appenderName, Consumer<String> consumer) {
|
||||
List<Consumer<String>> consumers = consumerMap.getOrDefault(appenderName, new ArrayList<>());
|
||||
public static void addConsumer(String appenderName, Consumer<LogMessage> consumer) {
|
||||
List<Consumer<LogMessage>> consumers = consumerMap.getOrDefault(appenderName, new ArrayList<>());
|
||||
consumers.add(consumer);
|
||||
consumerMap.putIfAbsent(appenderName, consumers);
|
||||
}
|
||||
@@ -50,11 +52,12 @@ public class ConsumerAppender extends AbstractAppender {
|
||||
@Override
|
||||
public void append(LogEvent event) {
|
||||
String formattedMessage = getLayout().toSerializable(event).toString();
|
||||
List<Consumer<String>> consumers = consumerMap.get(getName());
|
||||
List<Consumer<LogMessage>> consumers = consumerMap.get(getName());
|
||||
LogMessage logMessage = new LogMessage(event.getLevel(), event.getMessage().getFormattedMessage(), formattedMessage);
|
||||
|
||||
if(consumers != null) {
|
||||
for(Consumer<String> consumer : consumers) {
|
||||
consumer.accept(formattedMessage);
|
||||
for(Consumer<LogMessage> consumer : consumers) {
|
||||
consumer.accept(logMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,13 @@ public class MD5 {
|
||||
* @return A hex-formatted MD5 hash of the specified input.
|
||||
*/
|
||||
public static String digest(String string) {
|
||||
return StringUtil.toHexStringPadded(digest(string.getBytes(StandardCharsets.ISO_8859_1)));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return An MD5 hash of the specified input.
|
||||
*/
|
||||
public static byte[] digest(byte[] bytes) {
|
||||
if(digest == null) {
|
||||
try {
|
||||
digest = MessageDigest.getInstance("MD5");
|
||||
@@ -29,6 +36,6 @@ public class MD5 {
|
||||
}
|
||||
}
|
||||
|
||||
return StringUtil.toHexStringPadded(digest.digest(string.getBytes(StandardCharsets.ISO_8859_1)));
|
||||
return digest.digest(bytes);
|
||||
}
|
||||
}
|
||||
|
||||
58
src/main/java/entralinked/utility/SwingUtility.java
Normal file
58
src/main/java/entralinked/utility/SwingUtility.java
Normal file
@@ -0,0 +1,58 @@
|
||||
package entralinked.utility;
|
||||
|
||||
import java.awt.GridBagConstraints;
|
||||
import java.awt.event.ActionEvent;
|
||||
|
||||
import javax.swing.AbstractAction;
|
||||
import javax.swing.Action;
|
||||
import javax.swing.Icon;
|
||||
|
||||
public class SwingUtility {
|
||||
|
||||
@SuppressWarnings("serial")
|
||||
public static Action createAction(String name, Icon icon, Runnable handler) {
|
||||
AbstractAction action = new AbstractAction(name, icon) {
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent event) {
|
||||
handler.run();
|
||||
}
|
||||
};
|
||||
|
||||
if(icon != null) {
|
||||
action.putValue(Action.SHORT_DESCRIPTION, name);
|
||||
}
|
||||
|
||||
return action;
|
||||
}
|
||||
|
||||
public static Action createAction(String name, Runnable handler) {
|
||||
return createAction(name, null, handler);
|
||||
}
|
||||
|
||||
public static GridBagConstraints createConstraints(int x, int y) {
|
||||
return createConstraints(x, y, 1, 1);
|
||||
}
|
||||
|
||||
public static GridBagConstraints createConstraints(int x, int y, int width, int height) {
|
||||
return createConstraints(x, y, width, height, 1, 1);
|
||||
}
|
||||
|
||||
public static GridBagConstraints createConstraints(int x, int y, int width, int height, double weightX, double weightY) {
|
||||
return createConstraints(x, y, width, height, weightX, weightY, 8, 8);
|
||||
}
|
||||
|
||||
public static GridBagConstraints createConstraints(int x, int y, int width, int height, double weightX, double weightY,
|
||||
int paddingX, int paddingY) {
|
||||
GridBagConstraints constraints = new GridBagConstraints();
|
||||
constraints.fill = GridBagConstraints.BOTH;
|
||||
constraints.gridx = x;
|
||||
constraints.gridy = y;
|
||||
constraints.gridwidth = width;
|
||||
constraints.gridheight = height;
|
||||
constraints.weightx = weightX;
|
||||
constraints.weighty = weightY;
|
||||
constraints.ipadx = paddingX;
|
||||
constraints.ipady = paddingY;
|
||||
return constraints;
|
||||
}
|
||||
}
|
||||
@@ -128,7 +128,12 @@ public class TiledImageUtility {
|
||||
// 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++) {
|
||||
colorPalette[i] = ColorUtility.convertBGR555ToRGB888(inputStream.readShort());
|
||||
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.
|
||||
@@ -153,7 +158,7 @@ public class TiledImageUtility {
|
||||
int flipBits = rightBits & 12;
|
||||
|
||||
// The normalized tile index is the index of the in-memory tile index in the lookup table.
|
||||
int tileIndex = 0;
|
||||
int tileIndex = -1;
|
||||
|
||||
for(int k = 0; k < tileIndices.length; k++) {
|
||||
if(memoryIndex == tileIndices[k]) {
|
||||
@@ -162,6 +167,12 @@ public class TiledImageUtility {
|
||||
}
|
||||
}
|
||||
|
||||
// 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.
|
||||
|
||||
@@ -6,11 +6,14 @@
|
||||
<body>
|
||||
<div class="root-container">
|
||||
<div>
|
||||
<label for="gsid">Game Sync ID</label><br>
|
||||
<input type='text' id="gsid" name='gsid' placeholder='XXXXXXXXXX'>
|
||||
<button id="login" onclick="postLogin()">Login</button>
|
||||
<form onsubmit="return postLogin()">
|
||||
<label for="gsid">Game Sync ID</label><br>
|
||||
<input type="text" id="gsid" name="gsid" placeholder="XXXXXXXXXX" autocomplete="on">
|
||||
<label for="submit" class="big-button">Login</label>
|
||||
<input type="submit" id="submit" hidden/>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
<script src="scripts/utility.js"></script>
|
||||
<script src="scripts/login.js"></script>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<link rel="stylesheet" href="styles/profile.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="main-container" class="root-container" style="display:none;">
|
||||
<div id="main-container" class="centered-container" style="display:none;">
|
||||
<div>
|
||||
<div>
|
||||
<label id="game-summary" class="header-text"></label>
|
||||
@@ -127,21 +127,21 @@
|
||||
<div class="grid-container">
|
||||
<div>
|
||||
<label>C-Gear Skin | </label><a href="#" onclick="return previewSkin('cgear-skin', 'CGEAR')">Preview</a>
|
||||
<select id="cgear-skin" style="margin-bottom:0px;">
|
||||
<select id="cgear-skin">
|
||||
<option value="none">Do not change</option>
|
||||
<option disabled>──────────────────────</option>
|
||||
</select>
|
||||
<button onclick="openFileChooser('cgear-file-input')">Upload Custom</button>
|
||||
<input id="cgear-file-input" type="file" accept="image/*" onchange="uploadSkin('CGEAR', this.files[0])" onclick="this.value = null;" hidden/>
|
||||
<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>
|
||||
<label>Pokédex Skin | </label><a href="#" onclick="return previewSkin('dex-skin', 'ZUKAN')">Preview</a>
|
||||
<select id="dex-skin" style="margin-bottom:0px;">
|
||||
<select id="dex-skin">
|
||||
<option value="none">Do not change</option>
|
||||
<option disabled>──────────────────────</option>
|
||||
</select>
|
||||
<button onclick="openFileChooser('dex-file-input')">Upload Custom</button>
|
||||
<input id="dex-file-input" type="file" accept="image/*" onchange="uploadSkin('ZUKAN', this.files[0])" onclick="this.value = null;" hidden/>
|
||||
<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>
|
||||
<label>Musical Show</label>
|
||||
@@ -164,122 +164,144 @@
|
||||
</div>
|
||||
<!-- Entree Forest Encounter Configuration Form -->
|
||||
<div id="encounter-config" class="popup">
|
||||
<div class="content">
|
||||
<button class="close-button" onclick="closeEncounterForm()">X</button>
|
||||
<form id="encounter-form">
|
||||
<label for="encounter-form-species">Species</label>
|
||||
<select id="encounter-form-species" name="species" value="1">
|
||||
<!-- Filled by profile.js -->
|
||||
</select>
|
||||
<label for="encounter-form-move">Special Move</label>
|
||||
<select id="encounter-form-move" name="move" value="0">
|
||||
<option value="0">None</option>
|
||||
<!-- Filled by profile.js -->
|
||||
</select>
|
||||
<label for="encounter-form-form">Form</label>
|
||||
<select id="encounter-form-form" name="form" value="0">
|
||||
<option value="0">N/A</option>
|
||||
<!-- Filled by profile.js -->
|
||||
</select>
|
||||
<label for="encounter-form-gender">Gender</label>
|
||||
<select id="encounter-form-gender" name="gender" value="GENDERLESS">
|
||||
<option value="MALE">Male</option>
|
||||
<option value="FEMALE">Female</option>
|
||||
<option value="GENDERLESS">Random</option>
|
||||
</select>
|
||||
<label for="encounter-form-animation">Animation</label>
|
||||
<select id="encounter-form-animation" name="animation">
|
||||
<option value="LOOK_AROUND">Look Around</option>
|
||||
<option value="WALK_AROUND">Walk Around</option>
|
||||
<option value="WALK_LOOK_AROUND">Walk and Look Around</option>
|
||||
<option value="WALK_VERTICALLY">Walk Vertically</option>
|
||||
<option value="WALK_HORIZONTALLY">Walk Horizontally</option>
|
||||
<option value="WALK_HORIZONTALLY_LOOK_AROUND">Walk Horizontally and Look Around</option>
|
||||
<option value="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 class="centered-container">
|
||||
<div class="content">
|
||||
<button class="close-button" onclick="closeEncounterForm()">X</button>
|
||||
<form id="encounter-form">
|
||||
<label for="encounter-form-species">Species</label>
|
||||
<select id="encounter-form-species" name="species" value="1">
|
||||
<!-- Filled by profile.js -->
|
||||
</select>
|
||||
<label for="encounter-form-move">Special Move</label>
|
||||
<select id="encounter-form-move" name="move" value="0">
|
||||
<option value="0">None</option>
|
||||
<!-- Filled by profile.js -->
|
||||
</select>
|
||||
<label for="encounter-form-form">Form</label>
|
||||
<select id="encounter-form-form" name="form" value="0">
|
||||
<option value="0">N/A</option>
|
||||
<!-- Filled by profile.js -->
|
||||
</select>
|
||||
<label for="encounter-form-gender">Gender</label>
|
||||
<select id="encounter-form-gender" name="gender" value="GENDERLESS">
|
||||
<option value="MALE">Male</option>
|
||||
<option value="FEMALE">Female</option>
|
||||
<option value="GENDERLESS">Random</option>
|
||||
</select>
|
||||
<label for="encounter-form-animation">Animation</label>
|
||||
<select id="encounter-form-animation" name="animation">
|
||||
<option value="LOOK_AROUND">Look Around</option>
|
||||
<option value="WALK_AROUND">Walk Around</option>
|
||||
<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>
|
||||
<!-- Item Configuration Form -->
|
||||
<div id="item-config" class="popup">
|
||||
<div class="content">
|
||||
<button class="close-button" onclick="closeItemForm()">X</button>
|
||||
<form id="item-form">
|
||||
<label for="item-form-id">Item</label>
|
||||
<select id="item-form-id" name="id" value="1">
|
||||
<!-- Filled by profile.js -->
|
||||
</select>
|
||||
<label for="item-form-quantity">Quantity</label>
|
||||
<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 class="centered-container">
|
||||
<div class="content">
|
||||
<button class="close-button" onclick="closeItemForm()">X</button>
|
||||
<form id="item-form">
|
||||
<label for="item-form-id">Item</label>
|
||||
<select id="item-form-id" name="id" value="1">
|
||||
<!-- Filled by profile.js -->
|
||||
</select>
|
||||
<label for="item-form-quantity">Quantity</label>
|
||||
<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>
|
||||
<!-- Join Avenue Visitor Configuration Form -->
|
||||
<div id="visitor-config" class="popup">
|
||||
<div class="content">
|
||||
<button class="close-button" onclick="closeVisitorForm()">X</button>
|
||||
<form id="visitor-form">
|
||||
<label for="visitor-form-name">Name (Max. 7 characters)</label>
|
||||
<input id="visitor-form-name" name="name" placeholder="Trainer" maxlength="7"/>
|
||||
<label for="visitor-form-type">Trainer Class</label>
|
||||
<select id="visitor-form-type" name="type" value="ACE_TRAINER_MALE">
|
||||
<option value="YOUNGSTER">Youngster</option>
|
||||
<option value="LASS">Lass</option>
|
||||
<option value="ACE_TRAINER_MALE">Ace Trainer (Male)</option>
|
||||
<option value="ACE_TRAINER_FEMALE">Ace Trainer (Female)</option>
|
||||
<option value="RANGER_MALE">Pokémon Ranger (Male)</option>
|
||||
<option value="RANGER_FEMALE">Pokémon Ranger (Female)</option>
|
||||
<option value="BREEDER_MALE">Pokémon Breeder (Male)</option>
|
||||
<option value="BREEDER_FEMALE">Pokémon Breeder (Female)</option>
|
||||
<option value="SCIENTIST_MALE">Scientist (Male)</option>
|
||||
<option value="SCIENTIST_FEMALE">Scientist (Female)</option>
|
||||
<option value="HIKER">Hiker</option>
|
||||
<option value="PARASOL_LADY">Parasol Lady</option>
|
||||
<option value="ROUGHNECK">Roughneck</option>
|
||||
<option value="NURSE">Nurse</option>
|
||||
<option value="PRESCHOOLER_MALE">Preschooler (Male)</option>
|
||||
<option value="PRESCHOOLER_FEMALE">Preschooler (Female)</option>
|
||||
</select>
|
||||
<label for="visitor-form-shop-type">Shop Type</label>
|
||||
<select id="visitor-form-shop-type" name="shopType" value="RAFFLE">
|
||||
<option value="RAFFLE">Raffle Shop</option>
|
||||
<option value="FLORIST">Flower Shop</option>
|
||||
<option value="SALON">Beauty Salon</option>
|
||||
<option value="ANTIQUE">Antique Shop</option>
|
||||
<option value="DOJO">Training Dojo</option>
|
||||
<option value="CAFE">Café</option>
|
||||
<option value="MARKET">Market</option>
|
||||
</select>
|
||||
<label for="visitor-form-game">Game of Origin (Affects which goods/services are sold)</label>
|
||||
<select id="visitor-form-game" name="gameVersion" value="BLACK_ENGLISH">
|
||||
<option value="BLACK_ENGLISH">Black Version</option>
|
||||
<option value="WHITE_ENGLISH">White Version</option>
|
||||
<option value="BLACK_2_ENGLISH">Black Version 2</option>
|
||||
<option value="WHITE_2_ENGLISH">White Version 2</option>
|
||||
</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 class="centered-container">
|
||||
<div class="content">
|
||||
<button class="close-button" onclick="closeVisitorForm()">X</button>
|
||||
<form id="visitor-form">
|
||||
<label for="visitor-form-name">Name (Max. 7 characters)</label>
|
||||
<input id="visitor-form-name" type="text" name="name" placeholder="Trainer" maxlength="7"/>
|
||||
<label for="visitor-form-type">Trainer Class</label>
|
||||
<select id="visitor-form-type" name="type" value="ACE_TRAINER_MALE">
|
||||
<option value="YOUNGSTER">Youngster</option>
|
||||
<option value="LASS">Lass</option>
|
||||
<option value="ACE_TRAINER_MALE">Ace Trainer (Male)</option>
|
||||
<option value="ACE_TRAINER_FEMALE">Ace Trainer (Female)</option>
|
||||
<option value="RANGER_MALE">Pokémon Ranger (Male)</option>
|
||||
<option value="RANGER_FEMALE">Pokémon Ranger (Female)</option>
|
||||
<option value="BREEDER_MALE">Pokémon Breeder (Male)</option>
|
||||
<option value="BREEDER_FEMALE">Pokémon Breeder (Female)</option>
|
||||
<option value="SCIENTIST_MALE">Scientist (Male)</option>
|
||||
<option value="SCIENTIST_FEMALE">Scientist (Female)</option>
|
||||
<option value="HIKER">Hiker</option>
|
||||
<option value="PARASOL_LADY">Parasol Lady</option>
|
||||
<option value="ROUGHNECK">Roughneck</option>
|
||||
<option value="NURSE">Nurse</option>
|
||||
<option value="PRESCHOOLER_MALE">Preschooler (Male)</option>
|
||||
<option value="PRESCHOOLER_FEMALE">Preschooler (Female)</option>
|
||||
</select>
|
||||
<label for="visitor-form-shop-type">Shop Type</label>
|
||||
<select id="visitor-form-shop-type" name="shopType" value="RAFFLE">
|
||||
<option value="RAFFLE">Raffle Shop</option>
|
||||
<option value="FLORIST">Flower Shop</option>
|
||||
<option value="SALON">Beauty Salon</option>
|
||||
<option value="ANTIQUE">Antique Shop</option>
|
||||
<option value="DOJO">Training Dojo</option>
|
||||
<option value="CAFE">Café</option>
|
||||
<option value="MARKET">Market</option>
|
||||
</select>
|
||||
<label for="visitor-form-game">Game of Origin (Affects which goods/services are sold)</label>
|
||||
<select id="visitor-form-game" name="gameVersion" value="BLACK_ENGLISH">
|
||||
<option value="BLACK_ENGLISH">Black Version</option>
|
||||
<option value="WHITE_ENGLISH">White Version</option>
|
||||
<option value="BLACK_2_ENGLISH">Black Version 2</option>
|
||||
<option value="WHITE_2_ENGLISH">White Version 2</option>
|
||||
</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>
|
||||
</body>
|
||||
|
||||
@@ -10,4 +10,6 @@ function postLogin() {
|
||||
window.location.href = "/dashboard/profile.html";
|
||||
}
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -509,7 +509,7 @@ const REGION_LIST = [];
|
||||
{id: 493, name: "Arceus", downloadable: true, gender: "unknown", forms: [
|
||||
"Normal", "Fighting", "Flying", "Poison", "Ground", "Rock", "Bug", "Ghost",
|
||||
"Steel", "Fire", "Water", "Grass", "Electric", "Psychic", "Ice", "Dragon", "Dark"]},
|
||||
{id: 494, name: "Victini", downloadable: true, gender: "unknown"},
|
||||
{id: 494, name: "Victini", downloadable: false, gender: "unknown"},
|
||||
{id: 495, name: "Snivy", downloadable: false},
|
||||
{id: 496, name: "Servine", downloadable: false},
|
||||
{id: 497, name: "Serperior", downloadable: false},
|
||||
|
||||
@@ -32,6 +32,9 @@ const ELEMENT_VISITOR_SUBREGION = document.getElementById("visitor-form-subregio
|
||||
const ELEMENT_VISITOR_PERSONALITY = document.getElementById("visitor-form-personality");
|
||||
const ELEMENT_VISITOR_DREAMER = document.getElementById("visitor-form-dreamer");
|
||||
|
||||
const ELEMENT_SKIN_PREVIEW = document.getElementById("skin-preview");
|
||||
const ELEMENT_SKIN_PREVIEW_IMAGE = document.getElementById("skin-preview-image");
|
||||
|
||||
const ELEMENT_CGEAR_SKIN_INPUT = document.getElementById("cgear-skin");
|
||||
const ELEMENT_DEX_SKIN_INPUT = document.getElementById("dex-skin");
|
||||
const ELEMENT_MUSICAL_INPUT = document.getElementById("musical");
|
||||
@@ -532,10 +535,21 @@ function previewSkin(inputElementId, type) {
|
||||
type = "CGEAR2";
|
||||
}
|
||||
|
||||
window.open("/dashboard/previewskin?type=" + type + "&name=" + value);
|
||||
let revisionParam = value == "custom" ? "&revision=" + new Date().getTime() : ""; // Hack to work around the browser cache
|
||||
ELEMENT_SKIN_PREVIEW_IMAGE.src = "/dashboard/previewskin?type=" + type + "&name=" + value + revisionParam;
|
||||
ELEMENT_SKIN_PREVIEW.style.display = "block";
|
||||
return false;
|
||||
}
|
||||
|
||||
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() {
|
||||
// Fetch C-Gear skins
|
||||
fetchData("GET", "/dashboard/dlc?type=" + (isVersion2() ? "CGEAR2" : "CGEAR")).then((response) => {
|
||||
|
||||
@@ -11,21 +11,22 @@ input {
|
||||
border-radius: 10px;
|
||||
width: 100%;
|
||||
margin-top: 4px;
|
||||
margin-bottom: 16px;
|
||||
margin-bottom: 24px;
|
||||
padding: 4px 4px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
button {
|
||||
.big-button {
|
||||
color: white;
|
||||
background-color: #3D3D3D;
|
||||
border: 0px;
|
||||
border-radius: 10px;
|
||||
font-size: 16px;
|
||||
font-family: sans-serif;
|
||||
padding: 16px 32px;
|
||||
}
|
||||
|
||||
button:active {
|
||||
.big-button:active {
|
||||
background-color: #333333;
|
||||
}
|
||||
|
||||
|
||||
@@ -12,12 +12,15 @@ img {
|
||||
image-rendering: pixelated;
|
||||
}
|
||||
|
||||
input, select {
|
||||
color: white;
|
||||
background-color: #262626;
|
||||
input[type=text], input[type=number], select {
|
||||
border: 0px;
|
||||
border-radius: 10px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
input, select {
|
||||
color: white;
|
||||
background-color: #262626;
|
||||
margin-top: 4px;
|
||||
margin-bottom: 16px;
|
||||
padding: 4px 4px;
|
||||
@@ -39,14 +42,14 @@ a:link, a:visited {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.root-container {
|
||||
.centered-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.root-container div:first-child {
|
||||
.centered-container div:first-child {
|
||||
max-width: 532px;
|
||||
margin: auto;
|
||||
}
|
||||
@@ -87,12 +90,6 @@ a:link, a:visited {
|
||||
padding-left: 5px;
|
||||
}
|
||||
|
||||
.dreamer-summary td#dreamer-sprite {
|
||||
width: 96px;
|
||||
text-align: center;
|
||||
padding-left: 0px;
|
||||
}
|
||||
|
||||
.header-text {
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
@@ -110,21 +107,18 @@ a:link, a:visited {
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
|
||||
.popup .close-button {
|
||||
position: absolute;
|
||||
background-color: #3D3D3D;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
.content {
|
||||
position: relative;
|
||||
background-color: #191919;
|
||||
width: 500px;
|
||||
padding: 20px 20px 0px 20px;
|
||||
}
|
||||
|
||||
.popup .content {
|
||||
background-color: #191919;
|
||||
.close-button {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
padding: 20px;
|
||||
width: 500px;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
background-color: #3D3D3D;
|
||||
}
|
||||
|
||||
.encounter-image-table {
|
||||
@@ -187,5 +181,25 @@ a:link, a:visited {
|
||||
.big-button {
|
||||
font-size: 16px;
|
||||
padding: 16px 32px;
|
||||
margin-bottom: 8px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
#cgear-skin, #dex-skin {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
#dreamer-sprite {
|
||||
width: 96px;
|
||||
text-align: center;
|
||||
padding-left: 0px;
|
||||
}
|
||||
|
||||
#skin-preview-content {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
#skin-preview-image {
|
||||
box-shadow: 0px 0px 10px 3px #0A0A0A;
|
||||
margin-top: 4px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
BIN
src/main/resources/icon-16x.png
Normal file
BIN
src/main/resources/icon-16x.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 720 B |
BIN
src/main/resources/icon-32x.png
Normal file
BIN
src/main/resources/icon-32x.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.4 KiB |
BIN
src/main/resources/icon-64x.png
Normal file
BIN
src/main/resources/icon-64x.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.0 KiB |
@@ -1,25 +1,39 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE xml>
|
||||
<Configuration status="off" packages="entralinked.utility">
|
||||
<Configuration status="WARN" packages="entralinked.utility">
|
||||
<Properties>
|
||||
<Property name="LOGS_DIR">logs</Property>
|
||||
<Property name="PATTERN_MINIMAL">%d{yyyy-MM-dd HH:mm:ss.SSS} %5level : %msg%n</Property>
|
||||
<Property name="PATTERN_DETAILED">%d{yyyy-MM-dd HH:mm:ss.SSS} %5level [%32t] %-48c{1.2*} : %msg%n</Property>
|
||||
</Properties>
|
||||
<Appenders>
|
||||
<Console name="Console" target="SYSTEM_OUT">
|
||||
<PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss.SSS} %5level [%32t] %-48c{1.2*} : %msg%n"/>
|
||||
<PatternLayout pattern="${PATTERN_DETAILED}"/>
|
||||
<ThresholdFilter level="INFO"/>
|
||||
</Console>
|
||||
<RollingFile name="LogFile" fileName="logs/latest.log" filePattern="logs/%d{yyyy-MM-dd}-%i.log.gz">
|
||||
<PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss.SSS} %5level [%16t] %-48c{1.2*} : %msg%n"/>
|
||||
<RollingFile name="LogFile" fileName="${LOGS_DIR}/latest.log" filePattern="${LOGS_DIR}/%d{yyyy-MM-dd}-%i.log.gz">
|
||||
<PatternLayout pattern="${PATTERN_DETAILED}"/>
|
||||
<ThresholdFilter level="INFO"/>
|
||||
<Policies>
|
||||
<OnStartupTriggeringPolicy/>
|
||||
</Policies>
|
||||
</RollingFile>
|
||||
<ConsumerAppender name="GuiOutput">
|
||||
<PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss.SSS} %5level : %msg%n"/>
|
||||
<PatternLayout pattern="${PATTERN_MINIMAL}"/>
|
||||
<ThresholdFilter level="INFO"/>
|
||||
</ConsumerAppender>
|
||||
<File name="DebugLogFile" fileName="${LOGS_DIR}/debug.log" append="false">
|
||||
<PatternLayout pattern="${PATTERN_DETAILED}"/>
|
||||
<ThresholdFilter level="DEBUG"/>
|
||||
</File>
|
||||
</Appenders>
|
||||
<Loggers>
|
||||
<Root level="info">
|
||||
<AppenderRef ref="Console"/>
|
||||
<Logger name="entralinked" additivity="false">
|
||||
<AppenderRef ref="LogFile"/>
|
||||
<AppenderRef ref="GuiOutput"/>
|
||||
</Root>
|
||||
<AppenderRef ref="DebugLogFile"/>
|
||||
<AppenderRef ref="Console"/>
|
||||
</Logger>
|
||||
<Root level="all"/>
|
||||
</Loggers>
|
||||
</Configuration>
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
package entralinked.serialization;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.TestInstance;
|
||||
import org.junit.jupiter.api.TestInstance.Lifecycle;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonParseException;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.JsonMappingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
@TestInstance(Lifecycle.PER_CLASS)
|
||||
public class GameSpyMessageFactoryTest {
|
||||
|
||||
protected ObjectMapper mapper;
|
||||
|
||||
@BeforeAll
|
||||
void before() {
|
||||
mapper = new ObjectMapper(new GameSpyMessageFactory());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Test if generator writes objects correctly")
|
||||
void testGeneratorWriteObject() throws IOException {
|
||||
Map<String, Object> data = new LinkedHashMap<>();
|
||||
data.put("key", "value");
|
||||
data.put("emptyValue", "");
|
||||
data.put("hello", "world");
|
||||
data.put("numberTest", 123);
|
||||
assertEquals("\\key\\value\\emptyValue\\\\hello\\world\\numberTest\\123", mapper.writeValueAsString(data));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Test if generator throws exception when writing a nested object")
|
||||
void testGeneratorThrowsExceptionWhenWritingNestedObject() {
|
||||
Map<String, Object> data = Map.of("someKey", "someValue", "nestedObjectKey", Map.of("key", "value"));
|
||||
JsonMappingException exception = assertThrows(JsonMappingException.class, () -> mapper.writeValueAsString(data));
|
||||
assertEquals("this format does not support nested objects", exception.getOriginalMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Test if generator throws exception when writing an array")
|
||||
void testGeneratorThrowsExceptionWhenWritingArray() {
|
||||
Map<String, Object> data = Map.of("arrayKey", List.of(1, 2, 3, 4));
|
||||
JsonMappingException exception = assertThrows(JsonMappingException.class, () -> mapper.writeValueAsString(data));
|
||||
assertEquals("this format does not support arrays", exception.getOriginalMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Test if parser creates object from input string correctly")
|
||||
void testParserReadObject() throws IOException {
|
||||
String inputString = "\\key\\value\\emptyValue\\\\hello\\world\\numberTest\\123";
|
||||
Map<String, Object> data = mapper.readValue(inputString, new TypeReference<Map<String, Object>>(){});
|
||||
assertEquals("value", data.get("key"));
|
||||
assertEquals("", data.get("emptyValue"));
|
||||
assertEquals("world", data.get("hello"));
|
||||
assertEquals("123", data.get("numberTest"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Test if parser throws exception when reading an empty key")
|
||||
void testParserThrowsExceptionWhenReadingEmptyKey() {
|
||||
String inputString = "\\some\\value\\\\emptyKey";
|
||||
JsonParseException exception = assertThrows(JsonParseException.class, () -> mapper.readValue(inputString, Map.class));
|
||||
assertEquals("Unexpected character ('\\' (code 92)): expected field name", exception.getOriginalMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Test if parser throws exception when input doesn't start with backslash")
|
||||
void testParserThrowsExceptionWhenFirstCharacterNotBackslash() {
|
||||
String inputString = "key\\value";
|
||||
JsonParseException exception = assertThrows(JsonParseException.class, () -> mapper.readValue(inputString, Map.class));
|
||||
assertEquals("Unexpected character ('k' (code 107)): expected '\\' to open field name", exception.getOriginalMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Test if parser throws exception when field name isn't closed")
|
||||
void testParserThrowsExceptionWhenFieldNameNotClosed() {
|
||||
String inputString = "\\hello\\world\\key";
|
||||
JsonParseException exception = assertThrows(JsonParseException.class, () -> mapper.readValue(inputString, Map.class));
|
||||
assertEquals("Unexpected end-of-input in FIELD_NAME", exception.getOriginalMessage());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package entralinked.serialization;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.TestInstance;
|
||||
import org.junit.jupiter.api.TestInstance.Lifecycle;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonParseException;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.JsonMappingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
@TestInstance(Lifecycle.PER_CLASS)
|
||||
public class UrlEncodedFormFactoryTest {
|
||||
|
||||
protected ObjectMapper mapper;
|
||||
protected ObjectMapper mapperNoBase64;
|
||||
|
||||
@BeforeAll
|
||||
void before() {
|
||||
mapper = new ObjectMapper(new UrlEncodedFormFactory());
|
||||
mapperNoBase64 = new ObjectMapper(new UrlEncodedFormFactory()
|
||||
.disable(UrlEncodedFormParser.Feature.BASE64_DECODE_VALUES)
|
||||
.disable(UrlEncodedFormGenerator.Feature.BASE64_ENCODE_VALUES));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Test if generator writes objects correctly")
|
||||
void testGeneratorWriteObject() throws IOException {
|
||||
Map<String, Object> data = new LinkedHashMap<>();
|
||||
data.put("hello", "world");
|
||||
data.put("test", "space test");
|
||||
data.put("emptyValue", "");
|
||||
data.put("someNumber", 1234567890);
|
||||
assertEquals("hello=d29ybGQ*&test=c3BhY2UgdGVzdA**&emptyValue=&someNumber=MTIzNDU2Nzg5MA**", mapper.writeValueAsString(data));
|
||||
assertEquals("hello=world&test=space+test&emptyValue=&someNumber=1234567890", mapperNoBase64.writeValueAsString(data));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Test if generator throws exception when writing a nested object")
|
||||
void testGeneratorThrowsExceptionWhenWritingNestedObject() {
|
||||
Map<String, Object> data = Map.of("someKey", "someValue", "nestedObjectKey", Map.of("key", "value"));
|
||||
JsonMappingException exception = assertThrows(JsonMappingException.class, () -> mapper.writeValueAsString(data));
|
||||
assertEquals("this format does not support nested objects", exception.getOriginalMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Test if generator throws exception when writing an array")
|
||||
void testGeneratorThrowsExceptionWhenWritingArray() {
|
||||
Map<String, Object> data = Map.of("arrayKey", List.of(1, 2, 3, 4));
|
||||
JsonMappingException exception = assertThrows(JsonMappingException.class, () -> mapper.writeValueAsString(data));
|
||||
assertEquals("this format does not support arrays", exception.getOriginalMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Test if parser creates object from input string correctly")
|
||||
void testParserReadObject() throws IOException {
|
||||
String inputString = "hello=world&test=space+test&emptyValue=&someNumber=1234567890";
|
||||
Map<String, Object> data = mapperNoBase64.readValue(inputString, new TypeReference<Map<String, Object>>(){});
|
||||
assertEquals("world", data.get("hello"));
|
||||
assertEquals("space test", data.get("test"));
|
||||
assertEquals("", data.get("emptyValue"));
|
||||
assertEquals("1234567890", data.get("someNumber"));
|
||||
|
||||
// Test Base64
|
||||
inputString = "hello=d29ybGQ*&test=c3BhY2UgdGVzdA**&emptyValue=&someNumber=MTIzNDU2Nzg5MA**";
|
||||
assertEquals(data, mapper.readValue(inputString, Map.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Test if parser throws exception when reading an empty key")
|
||||
void testParserThrowsExceptionWhenReadingEmptyKey() {
|
||||
String inputString = "someKey=someValue&=emptyKey";
|
||||
JsonParseException exception = assertThrows(JsonParseException.class, () -> mapperNoBase64.readValue(inputString, Map.class));
|
||||
assertEquals("Unexpected character ('=' (code 61)): expected field name", exception.getOriginalMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Test if parser throws exception when field name isn't closed")
|
||||
void testParserThrowsExceptionWhenFieldNameNotClosed() {
|
||||
String inputString = "someKey=someValue¬Closed";
|
||||
JsonParseException exception = assertThrows(JsonParseException.class, () -> mapperNoBase64.readValue(inputString, Map.class));
|
||||
assertEquals("Unexpected end-of-input in FIELD_NAME", exception.getOriginalMessage());
|
||||
}
|
||||
}
|
||||
54
src/test/java/entralinked/utility/ColorUtilityTest.java
Normal file
54
src/test/java/entralinked/utility/ColorUtilityTest.java
Normal file
@@ -0,0 +1,54 @@
|
||||
package entralinked.utility;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
public class ColorUtilityTest {
|
||||
|
||||
@Test
|
||||
@DisplayName("Test if multiplied colors are correct")
|
||||
void testColorMultiplying() {
|
||||
assertEquals(0x7F7F7F, ColorUtility.multiplyColor(0xFFFFFF, 0.5));
|
||||
assertEquals(0xFEFEFE, ColorUtility.multiplyColor(0x7F7F7F, 2.0));
|
||||
|
||||
// Test clamping
|
||||
assertEquals(0xFFFFFF, ColorUtility.multiplyColor(0xFEFEFE, 2.0));
|
||||
assertEquals(0x000000, ColorUtility.multiplyColor(0x010101, 0.5));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Test if colors converted from RGB888 to BGR555 are correct")
|
||||
void testRGB88ToBGR555Converter() {
|
||||
// Black, white, red, green and blue
|
||||
assertEquals(0x0000, ColorUtility.convertRGB888ToBGR555(0x000000));
|
||||
assertEquals(0x7FFF, ColorUtility.convertRGB888ToBGR555(0xFFFFFF));
|
||||
assertEquals(0x001F, ColorUtility.convertRGB888ToBGR555(0xFF0000));
|
||||
assertEquals(0x03E0, ColorUtility.convertRGB888ToBGR555(0x00FF00));
|
||||
assertEquals(0x7C00, ColorUtility.convertRGB888ToBGR555(0x0000FF));
|
||||
|
||||
// Random colors
|
||||
assertEquals(0x07C7, ColorUtility.convertRGB888ToBGR555(0x39F20C));
|
||||
assertEquals(0x68B3, ColorUtility.convertRGB888ToBGR555(0x9E2BD3));
|
||||
assertEquals(0x6DFE, ColorUtility.convertRGB888ToBGR555(0xF07CDE));
|
||||
assertEquals(0x22A4, ColorUtility.convertRGB888ToBGR555(0x26AC44));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Test if colors converted from BGR555 to RGB888 are correct")
|
||||
void testBGR555ToRGB888Converter() {
|
||||
// Black, white, red, green and blue
|
||||
assertEquals(0x000000, ColorUtility.convertBGR555ToRGB888(0x0000));
|
||||
assertEquals(0xFFFFFF, ColorUtility.convertBGR555ToRGB888(0xFFFF));
|
||||
assertEquals(0xFF0000, ColorUtility.convertBGR555ToRGB888(0x001F));
|
||||
assertEquals(0x00FF00, ColorUtility.convertBGR555ToRGB888(0x03E0));
|
||||
assertEquals(0x0000FF, ColorUtility.convertBGR555ToRGB888(0x7C00));
|
||||
|
||||
// Random colors
|
||||
assertEquals(0x7BA59C, ColorUtility.convertBGR555ToRGB888(0x4E8F));
|
||||
assertEquals(0x948CFF, ColorUtility.convertBGR555ToRGB888(0xFE32));
|
||||
assertEquals(0xBD2121, ColorUtility.convertBGR555ToRGB888(0x1097));
|
||||
assertEquals(0x5A185A, ColorUtility.convertBGR555ToRGB888(0xAC6B));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package entralinked.utility;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
public class LittleEndianStreamTest {
|
||||
|
||||
@Test
|
||||
@DisplayName("Test if LEOutputStream and LEInputStream produce correct values")
|
||||
void testLittleEndianStreams() throws IOException {
|
||||
short shortValue = 0x6A84;
|
||||
int intValue = 0xF827EC80;
|
||||
long longValue = 0x948EC1AB3F2C88L;
|
||||
|
||||
// Test writing
|
||||
ByteArrayOutputStream byteOutputStream = new ByteArrayOutputStream();
|
||||
LEOutputStream outputStream = new LEOutputStream(byteOutputStream);
|
||||
outputStream.writeShort(shortValue);
|
||||
outputStream.writeInt(intValue);
|
||||
outputStream.writeLong(longValue);
|
||||
|
||||
// Test reading
|
||||
LEInputStream inputStream = new LEInputStream(new ByteArrayInputStream(byteOutputStream.toByteArray()));
|
||||
assertEquals(shortValue, inputStream.readShort());
|
||||
assertEquals(intValue, inputStream.readInt());
|
||||
assertEquals(longValue, inputStream.readLong());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user