mirror of
https://github.com/kuroppoi/entralinked.git
synced 2026-09-08 08:45:14 -05:00
Compare commits
61 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
306b9e6db6 | ||
|
|
c0cc952cc2 | ||
|
|
2eb8054ba7 | ||
|
|
d61196fea1 | ||
|
|
8726614fce | ||
|
|
76c5d0e1a5 | ||
|
|
86434b2cb1 | ||
|
|
724e30bfcc | ||
|
|
87a06a4c08 | ||
|
|
e06b83c944 | ||
|
|
b890d27bba | ||
|
|
3771cac70a | ||
|
|
2bdb04b88e | ||
|
|
99240e163a | ||
|
|
72d5a10ffc | ||
|
|
6f1853fc50 | ||
|
|
6999f1f976 | ||
|
|
16d1b99f1f | ||
|
|
229cfaf4ca | ||
|
|
639a487530 | ||
|
|
08d354e814 | ||
|
|
adc0c16c68 | ||
|
|
7fa718af48 | ||
|
|
ff0e541fe8 | ||
|
|
4628fee251 | ||
|
|
a6ed715377 | ||
|
|
4e56d2532f | ||
|
|
eeb6048f54 | ||
|
|
875500d302 | ||
|
|
1990519088 | ||
|
|
eb1605dbe7 | ||
|
|
d232de7307 | ||
|
|
d57c7434a0 | ||
|
|
022ee2f1e2 | ||
|
|
4f8a717ffb | ||
|
|
a479c158db | ||
|
|
9a875cdf77 | ||
|
|
0f4160459e | ||
|
|
3dea11ff50 | ||
|
|
b38b26f9ce | ||
|
|
a95e7e2f01 | ||
|
|
53f05acab9 | ||
|
|
49a15a0156 | ||
|
|
329e1de30a | ||
|
|
0e12cb54d7 | ||
|
|
8849265805 | ||
|
|
0d964ec129 | ||
|
|
e4fff87b87 | ||
|
|
aae1abd18f | ||
|
|
038f7f5136 | ||
|
|
f41888ba58 | ||
|
|
328aac45e1 | ||
|
|
806bd5cc17 | ||
|
|
06e388ab89 | ||
|
|
60d6698f6b | ||
|
|
4800f7b6c2 | ||
|
|
a1e8cc97bc | ||
|
|
2aebbac230 | ||
|
|
09ce3a26ce | ||
|
|
e4f7a26f9c | ||
|
|
d90200e4ca |
6
.gitattributes
vendored
6
.gitattributes
vendored
@@ -1,6 +1,2 @@
|
||||
#
|
||||
# https://help.github.com/articles/dealing-with-line-endings/
|
||||
#
|
||||
# These are explicitly windows files and should use crlf
|
||||
*.bat text eol=crlf
|
||||
|
||||
src/main/resources/dashboard/scripts/pokedata.js linguist-vendored
|
||||
|
||||
20
README.md
20
README.md
@@ -1,9 +1,18 @@
|
||||
# Entralinked
|
||||
[](https://github.com/kuroppoi/entralinked/actions)
|
||||
<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/dist-upload-artifact.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 and Musicals\
|
||||
to your game without needing to edit your save file.
|
||||
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
|
||||
|
||||
@@ -27,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 Nintendo WFC Configuration of your DS and try again.\
|
||||
After tucking in a Pokémon, navigate to `http://localhost/dashboard/profile.html` in a web browser to configure Game Sync settings.
|
||||
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 |
Submodule poke-sprites-v updated: 70e5ee79b0...9eebf41346
@@ -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,43 @@ 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) {
|
||||
logger.info("Startup complete! Took a total of {} milliseconds", System.currentTimeMillis() - beginTime);
|
||||
String hostIpAddress = hostAddress.getHostAddress();
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
mainView.setDashboardButtonEnabled(true);
|
||||
mainView.setStatusLabelText("Configure your DS to use the following DNS server: %s".formatted(hostIpAddress));
|
||||
});
|
||||
|
||||
if(mainView == null) {
|
||||
logger.info("Configure your DS to use the following DNS server: {}", hostIpAddress);
|
||||
} else {
|
||||
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 +159,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 +195,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);
|
||||
}
|
||||
|
||||
106
src/main/java/entralinked/gui/PidToolDialog.java
Normal file
106
src/main/java/entralinked/gui/PidToolDialog.java
Normal file
@@ -0,0 +1,106 @@
|
||||
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.model.user.UserManager;
|
||||
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("-", "");
|
||||
String friendCode = friendCodeField.getText().replace("-", "");
|
||||
|
||||
// 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(friendCode).matches()) {
|
||||
JOptionPane.showMessageDialog(dialog, "Please enter a valid Friend Code.", "Attention", JOptionPane.WARNING_MESSAGE);
|
||||
return;
|
||||
}
|
||||
|
||||
UserManager userManager = entralinked.getUserManager();
|
||||
User user = userManager.getUser(userId.substring(0, 13));
|
||||
int profileId = (int)(Long.parseLong(friendCode) & 0x7FFFFFFF);
|
||||
|
||||
// Make sure user exists
|
||||
if(user == null) {
|
||||
JOptionPane.showMessageDialog(dialog, "This Wi-Fi Connection ID does not exist.", "Attention", JOptionPane.WARNING_MESSAGE);
|
||||
return;
|
||||
}
|
||||
|
||||
// Show warning if this user has multiple profiles
|
||||
if(user.getProfiles().size() > 1) {
|
||||
if(JOptionPane.showConfirmDialog(dialog, "Multiple profiles detected. Do you want to update all of them?\n"
|
||||
+ "This may cause error 60000 to occur on your other game cartridges.",
|
||||
"Attention", JOptionPane.YES_NO_OPTION) != JOptionPane.YES_OPTION) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
userManager.updateProfileIdForUser(user, profileId);
|
||||
JOptionPane.showMessageDialog(dialog, "Profile has been updated. Please restart your game and use Game Sync.");
|
||||
});
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
15
src/main/java/entralinked/model/avenue/AvenueShopType.java
Normal file
15
src/main/java/entralinked/model/avenue/AvenueShopType.java
Normal file
@@ -0,0 +1,15 @@
|
||||
package entralinked.model.avenue;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonEnumDefaultValue;
|
||||
|
||||
public enum AvenueShopType {
|
||||
|
||||
@JsonEnumDefaultValue
|
||||
RAFFLE,
|
||||
FLORIST,
|
||||
SALON,
|
||||
ANTIQUE,
|
||||
DOJO,
|
||||
CAFE,
|
||||
MARKET
|
||||
}
|
||||
15
src/main/java/entralinked/model/avenue/AvenueVisitor.java
Normal file
15
src/main/java/entralinked/model/avenue/AvenueVisitor.java
Normal file
@@ -0,0 +1,15 @@
|
||||
package entralinked.model.avenue;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
import entralinked.GameVersion;
|
||||
|
||||
public record AvenueVisitor(
|
||||
@JsonProperty(required = true) String name,
|
||||
@JsonProperty(required = true) AvenueVisitorType type,
|
||||
@JsonProperty(required = true) AvenueShopType shopType,
|
||||
@JsonProperty(required = true) GameVersion gameVersion,
|
||||
@JsonProperty(required = true) int countryCode,
|
||||
@JsonProperty(required = true) int stateProvinceCode,
|
||||
@JsonProperty(required = true) int personality,
|
||||
@JsonProperty(required = true) int dreamerSpecies) {}
|
||||
@@ -0,0 +1,59 @@
|
||||
package entralinked.model.avenue;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonEnumDefaultValue;
|
||||
|
||||
public enum AvenueVisitorType {
|
||||
|
||||
// 0
|
||||
@JsonEnumDefaultValue
|
||||
YOUNGSTER(0),
|
||||
LASS(0, true),
|
||||
|
||||
// 1
|
||||
ACE_TRAINER_MALE(1),
|
||||
ACE_TRAINER_FEMALE(1, true),
|
||||
|
||||
// 2
|
||||
RANGER_MALE(2),
|
||||
RANGER_FEMALE(2, true),
|
||||
|
||||
// 3
|
||||
BREEDER_MALE(3),
|
||||
BREEDER_FEMALE(3, true),
|
||||
|
||||
// 4
|
||||
SCIENTIST_MALE(4),
|
||||
SCIENTIST_FEMALE(4, true),
|
||||
|
||||
// 5
|
||||
HIKER(5),
|
||||
PARASOL_LADY(5, true),
|
||||
|
||||
// 6
|
||||
ROUGHNECK(6),
|
||||
NURSE(6, true),
|
||||
|
||||
// 7
|
||||
PRESCHOOLER_MALE(7),
|
||||
PRESCHOOLER_FEMALE(7, true);
|
||||
|
||||
private final int clientId;
|
||||
private final boolean female;
|
||||
|
||||
private AvenueVisitorType(int clientId, boolean female) {
|
||||
this.clientId = clientId;
|
||||
this.female = female;
|
||||
}
|
||||
|
||||
private AvenueVisitorType(int clientId) {
|
||||
this(clientId, false);
|
||||
}
|
||||
|
||||
public int getClientId() {
|
||||
return clientId;
|
||||
}
|
||||
|
||||
public boolean isFemale() {
|
||||
return female;
|
||||
}
|
||||
}
|
||||
@@ -6,11 +6,10 @@ import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.nio.file.Files;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@@ -22,7 +21,7 @@ import entralinked.utility.Crc16;
|
||||
public class DlcList {
|
||||
|
||||
private static final Logger logger = LogManager.getLogger();
|
||||
private final Map<String, Dlc> dlcMap = new ConcurrentHashMap<>();
|
||||
private final List<Dlc> dlcList = new ArrayList<>();
|
||||
private final File dataDirectory = new File("dlc");
|
||||
|
||||
public DlcList() {
|
||||
@@ -80,29 +79,32 @@ public class DlcList {
|
||||
|
||||
// DLC Content level
|
||||
for(File dlcFile : subFile.listFiles()) {
|
||||
String name = dlcFile.getName();
|
||||
|
||||
// Check if DLC name is reserved as an internal identifier
|
||||
if(name.equals("none") || name.equals("custom")) {
|
||||
logger.warn("DLC '{}/{}/{}' could not be loaded because it uses a reserved name.",
|
||||
file.getName(), subFile.getName(), name);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Load DLC data
|
||||
Dlc dlc = loadDlcFile(file.getName(), subFile.getName(), index, dlcFile);
|
||||
|
||||
// Index DLC object if loading succeeded
|
||||
if(dlc != null) {
|
||||
dlcMap.put(dlc.name(), dlc);
|
||||
dlcList.add(dlc);
|
||||
index++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logger.info("Loaded {} DLC file(s)", dlcMap.size());
|
||||
logger.info("Loaded {} DLC file(s)", dlcList.size());
|
||||
}
|
||||
|
||||
private Dlc loadDlcFile(String gameCode, String type, int index, File dlcFile) {
|
||||
String name = dlcFile.getName();
|
||||
|
||||
if(dlcMap.containsKey(name)) {
|
||||
logger.warn("Duplicate DLC name {}", name);
|
||||
return null;
|
||||
}
|
||||
|
||||
if(dlcFile.isDirectory()) {
|
||||
logger.warn("Directory '{}' in {} DLC folder", name, gameCode);
|
||||
return null;
|
||||
@@ -123,6 +125,7 @@ public class DlcList {
|
||||
int checksumInFile = (bytes[bytes.length - 2] & 0xFF) | ((bytes[bytes.length - 1] & 0xFF) << 8);
|
||||
|
||||
if(checksum != checksumInFile) {
|
||||
logger.warn("Checksum mismatch in DLC '{}'", name);
|
||||
projectedSize += 2;
|
||||
checksum = Crc16.calc(bytes, 0, bytes.length);
|
||||
checksumEmbedded = false;
|
||||
@@ -167,19 +170,18 @@ public class DlcList {
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
public Dlc getDlc(String name) {
|
||||
return dlcMap.get(name);
|
||||
public Dlc getDlc(String gameCode, String type, String name) {
|
||||
List<Dlc> dlcList = getDlcList(gameCode, type).stream()
|
||||
.filter(dlc -> dlc.name().equals(name)).collect(Collectors.toList());
|
||||
return dlcList.isEmpty() ? null : dlcList.get(0);
|
||||
}
|
||||
|
||||
public int getDlcIndex(String name) {
|
||||
return dlcExists(name) ? getDlc(name).index() : 0;
|
||||
}
|
||||
|
||||
public boolean dlcExists(String name) {
|
||||
return name != null && dlcMap.containsKey(name);
|
||||
public int getDlcIndex(String gameCode, String type, String name) {
|
||||
Dlc dlc = getDlc(gameCode, type, name);
|
||||
return dlc == null ? 0 : dlc.index();
|
||||
}
|
||||
|
||||
public Collection<Dlc> getDlc() {
|
||||
return Collections.unmodifiableCollection(dlcMap.values());
|
||||
return Collections.unmodifiableCollection(dlcList);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,17 +7,18 @@ import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
* Record containing information about a Pokémon
|
||||
*/
|
||||
public record PkmnInfo(
|
||||
@JsonProperty(required = true) int personality,
|
||||
@JsonProperty(required = true) int species,
|
||||
@JsonProperty(required = true) int heldItem,
|
||||
@JsonProperty(required = true) int trainerId,
|
||||
@JsonProperty(required = true) int trainerSecretId,
|
||||
@JsonProperty(required = true) int level,
|
||||
@JsonProperty(required = true) int form,
|
||||
@JsonProperty(required = true) PkmnNature nature,
|
||||
@JsonProperty(required = true) PkmnGender gender,
|
||||
@JsonProperty(required = true) String nickname,
|
||||
@JsonProperty(required = true) String trainerName) {
|
||||
@JsonProperty(required = true) String nickname,
|
||||
@JsonProperty(required = true) String trainerName,
|
||||
@JsonProperty(required = true) PkmnNature nature,
|
||||
@JsonProperty(required = true) PkmnGender gender,
|
||||
@JsonProperty(required = true) int species,
|
||||
@JsonProperty(required = true) int personality,
|
||||
@JsonProperty(required = true) int trainerId,
|
||||
@JsonProperty(required = true) int trainerSecretId,
|
||||
@JsonProperty(required = true) int level,
|
||||
@JsonProperty(required = false) int form,
|
||||
@JsonProperty(required = false) int ability,
|
||||
@JsonProperty(required = false) int heldItem) {
|
||||
|
||||
@JsonIgnore
|
||||
public boolean isShiny() {
|
||||
|
||||
@@ -58,7 +58,7 @@ public class PkmnInfoReader {
|
||||
|
||||
// Read Pokémon data
|
||||
int species = buffer.getShortLE(8);
|
||||
int item = buffer.getShortLE(10) & 0xFFFF;
|
||||
int heldItem = buffer.getShortLE(10) & 0xFFFF;
|
||||
int trainerId = buffer.getShortLE(12) & 0xFFFF;
|
||||
int trainerSecretId = buffer.getShortLE(14) & 0xFFFF;
|
||||
int level = buffer.getByte(140);
|
||||
@@ -78,14 +78,14 @@ public class PkmnInfoReader {
|
||||
|
||||
// Loosely verify data
|
||||
if(species < 1 || species > 649) throw new IOException("Invalid species");
|
||||
if(item < 0 || item > 638) throw new IOException("Invalid held item");
|
||||
if(heldItem < 0 || heldItem > 638) throw new IOException("Invalid held item");
|
||||
if(ability < 1 || ability > 164) throw new IOException("Invalid ability");
|
||||
if(level < 1 || level > 100) throw new IOException("Level is out of range");
|
||||
if(nature == null) throw new IOException("Invalid nature");
|
||||
|
||||
// Create record
|
||||
PkmnInfo info = new PkmnInfo(personality, species, item, trainerId, trainerSecretId, level, form, nature, gender, nickname, trainerName);
|
||||
return info;
|
||||
return new PkmnInfo(nickname, trainerName, nature, gender, species, personality,
|
||||
trainerId, trainerSecretId, level, form, ability, heldItem);
|
||||
}
|
||||
|
||||
private static void decryptData(ByteBuf buffer, int offset, int length, int seed) throws IOException {
|
||||
|
||||
7
src/main/java/entralinked/model/player/DreamDecor.java
Normal file
7
src/main/java/entralinked/model/player/DreamDecor.java
Normal file
@@ -0,0 +1,7 @@
|
||||
package entralinked.model.player;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
public record DreamDecor(
|
||||
@JsonProperty(required = true) int id,
|
||||
@JsonProperty(required = true) String name) {}
|
||||
@@ -1,29 +1,34 @@
|
||||
package entralinked.model.player;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import entralinked.GameVersion;
|
||||
import entralinked.model.avenue.AvenueVisitor;
|
||||
import entralinked.model.pkmn.PkmnInfo;
|
||||
|
||||
public class Player {
|
||||
|
||||
private final String gameSyncId;
|
||||
private final GameVersion gameVersion;
|
||||
private final List<DreamEncounter> encounters = new ArrayList<>();
|
||||
private final List<DreamItem> items = new ArrayList<>();
|
||||
private final List<AvenueVisitor> avenueVisitors = new ArrayList<>();
|
||||
private PlayerStatus status;
|
||||
private GameVersion gameVersion;
|
||||
private PkmnInfo dreamerInfo;
|
||||
private int levelsGained;
|
||||
private String cgearSkin;
|
||||
private String dexSkin;
|
||||
private String musical;
|
||||
private String customCGearSkin;
|
||||
private String customDexSkin;
|
||||
private File dataDirectory;
|
||||
|
||||
public Player(String gameSyncId, GameVersion gameVersion) {
|
||||
public Player(String gameSyncId) {
|
||||
this.gameSyncId = gameSyncId;
|
||||
this.gameVersion = gameVersion;
|
||||
}
|
||||
|
||||
public void resetDreamInfo() {
|
||||
@@ -31,6 +36,7 @@ public class Player {
|
||||
dreamerInfo = null;
|
||||
encounters.clear();
|
||||
items.clear();
|
||||
avenueVisitors.clear();
|
||||
levelsGained = 0;
|
||||
cgearSkin = null;
|
||||
dexSkin = null;
|
||||
@@ -41,10 +47,6 @@ public class Player {
|
||||
return gameSyncId;
|
||||
}
|
||||
|
||||
public GameVersion getGameVersion() {
|
||||
return gameVersion;
|
||||
}
|
||||
|
||||
public void setEncounters(Collection<DreamEncounter> encounters) {
|
||||
if(encounters.size() <= 10) {
|
||||
this.encounters.clear();
|
||||
@@ -67,6 +69,17 @@ public class Player {
|
||||
return Collections.unmodifiableList(items);
|
||||
}
|
||||
|
||||
public void setAvenueVisitors(Collection<AvenueVisitor> avenueVisitors) {
|
||||
if(avenueVisitors.size() <= 12) {
|
||||
this.avenueVisitors.clear();
|
||||
this.avenueVisitors.addAll(avenueVisitors);
|
||||
}
|
||||
}
|
||||
|
||||
public List<AvenueVisitor> getAvenueVisitors() {
|
||||
return Collections.unmodifiableList(avenueVisitors);
|
||||
}
|
||||
|
||||
public void setStatus(PlayerStatus status) {
|
||||
this.status = status;
|
||||
}
|
||||
@@ -75,6 +88,14 @@ public class Player {
|
||||
return status;
|
||||
}
|
||||
|
||||
public void setGameVersion(GameVersion gameVersion) {
|
||||
this.gameVersion = gameVersion;
|
||||
}
|
||||
|
||||
public GameVersion getGameVersion() {
|
||||
return gameVersion;
|
||||
}
|
||||
|
||||
public void setDreamerInfo(PkmnInfo dreamerInfo) {
|
||||
this.dreamerInfo = dreamerInfo;
|
||||
}
|
||||
@@ -114,4 +135,46 @@ public class Player {
|
||||
public String getMusical() {
|
||||
return musical;
|
||||
}
|
||||
|
||||
public void setCustomCGearSkin(String customCGearSkin) {
|
||||
this.customCGearSkin = customCGearSkin;
|
||||
}
|
||||
|
||||
public String getCustomCGearSkin() {
|
||||
return customCGearSkin;
|
||||
}
|
||||
|
||||
public void setCustomDexSkin(String customDexSkin) {
|
||||
this.customDexSkin = customDexSkin;
|
||||
}
|
||||
|
||||
public String getCustomDexSkin() {
|
||||
return customDexSkin;
|
||||
}
|
||||
|
||||
// IO stuff
|
||||
|
||||
public void setDataDirectory(File dataDirectory) {
|
||||
this.dataDirectory = dataDirectory;
|
||||
}
|
||||
|
||||
public File getDataDirectory() {
|
||||
return dataDirectory;
|
||||
}
|
||||
|
||||
public File getDataFile() {
|
||||
return new File(dataDirectory, "data.json");
|
||||
}
|
||||
|
||||
public File getSaveFile() {
|
||||
return new File(dataDirectory, "save.bin");
|
||||
}
|
||||
|
||||
public File getCGearSkinFile() {
|
||||
return new File(dataDirectory, "cgear.bin");
|
||||
}
|
||||
|
||||
public File getDexSkinFile() {
|
||||
return new File(dataDirectory, "zukan.bin");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||
|
||||
import entralinked.GameVersion;
|
||||
import entralinked.model.avenue.AvenueVisitor;
|
||||
import entralinked.model.pkmn.PkmnInfo;
|
||||
|
||||
/**
|
||||
@@ -20,28 +21,37 @@ public record PlayerDto(
|
||||
String cgearSkin,
|
||||
String dexSkin,
|
||||
String musical,
|
||||
String customCGearSkin,
|
||||
String customDexSkin,
|
||||
int levelsGained,
|
||||
@JsonDeserialize(contentAs = DreamEncounter.class) Collection<DreamEncounter> encounters,
|
||||
@JsonDeserialize(contentAs = DreamItem.class) Collection<DreamItem> items) {
|
||||
@JsonDeserialize(contentAs = DreamItem.class) Collection<DreamItem> items,
|
||||
@JsonDeserialize(contentAs = AvenueVisitor.class) Collection<AvenueVisitor> avenueVisitors) {
|
||||
|
||||
public PlayerDto(Player player) {
|
||||
this(player.getGameSyncId(), player.getGameVersion(), player.getStatus(), player.getDreamerInfo(), player.getCGearSkin(),
|
||||
player.getDexSkin(), player.getMusical(), player.getLevelsGained(), player.getEncounters(), player.getItems());
|
||||
this(player.getGameSyncId(), player.getGameVersion(), player.getStatus(), player.getDreamerInfo(),
|
||||
player.getCGearSkin(), player.getDexSkin(), player.getMusical(), player.getCustomCGearSkin(),
|
||||
player.getCustomDexSkin(), player.getLevelsGained(), player.getEncounters(), player.getItems(),
|
||||
player.getAvenueVisitors());
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new {@link Player} object using the data in this DTO.
|
||||
*/
|
||||
public Player toPlayer() {
|
||||
Player player = new Player(gameSyncId, gameVersion);
|
||||
Player player = new Player(gameSyncId);
|
||||
player.setStatus(status);
|
||||
player.setGameVersion(gameVersion);
|
||||
player.setDreamerInfo(dreamerInfo);
|
||||
player.setCGearSkin(cgearSkin);
|
||||
player.setDexSkin(dexSkin);
|
||||
player.setMusical(musical);
|
||||
player.setCustomCGearSkin(customCGearSkin);
|
||||
player.setCustomDexSkin(customDexSkin);
|
||||
player.setLevelsGained(levelsGained);
|
||||
player.setEncounters(encounters == null ? Collections.emptyList() : encounters);
|
||||
player.setItems(items == null ? Collections.emptyList() : items);
|
||||
player.setAvenueVisitors(avenueVisitors == null ? Collections.emptyList() : avenueVisitors);
|
||||
return player;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
@@ -16,8 +17,6 @@ import org.apache.logging.log4j.Logger;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.SerializationFeature;
|
||||
|
||||
import entralinked.GameVersion;
|
||||
|
||||
/**
|
||||
* Manager class for managing {@link Player} information (Global Link users)
|
||||
*/
|
||||
@@ -27,7 +26,6 @@ public class PlayerManager {
|
||||
private final ObjectMapper mapper = new ObjectMapper().enable(SerializationFeature.INDENT_OUTPUT);
|
||||
private final Map<String, Player> playerMap = new ConcurrentHashMap<>();
|
||||
private final File dataDirectory = new File("players");
|
||||
private final File gameSaveDirectory = new File(dataDirectory, "savedata");
|
||||
|
||||
public PlayerManager() {
|
||||
logger.info("Loading player data ...");
|
||||
@@ -37,16 +35,76 @@ public class PlayerManager {
|
||||
return;
|
||||
}
|
||||
|
||||
// Migrate player data if necessary
|
||||
migrateLegacyData();
|
||||
|
||||
// Load player data
|
||||
for(File file : dataDirectory.listFiles()) {
|
||||
if(!file.isDirectory()) {
|
||||
loadPlayer(file);
|
||||
if(file.isDirectory()) {
|
||||
loadPlayer(new File(file, "data.json"));
|
||||
}
|
||||
}
|
||||
|
||||
logger.info("Loaded {} player(s)", playerMap.size());
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrates player data from the old storage format.
|
||||
*/
|
||||
private void migrateLegacyData() {
|
||||
// Migrate player configuration files
|
||||
for(File file : dataDirectory.listFiles()) {
|
||||
if(!file.isDirectory()) {
|
||||
try {
|
||||
PlayerDto player = mapper.readValue(file, PlayerDto.class);
|
||||
File outputDirectory = new File(dataDirectory, player.gameSyncId());
|
||||
outputDirectory.mkdirs();
|
||||
Files.copy(file.toPath(), new File(outputDirectory, "data.json").toPath(), StandardCopyOption.REPLACE_EXISTING);
|
||||
file.delete();
|
||||
} catch(IOException e) {
|
||||
logger.error("Could not migrate player data {}", file.getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Migrate game save data files
|
||||
File gameSaveDirectory = new File(dataDirectory, "savedata");
|
||||
|
||||
if(gameSaveDirectory.exists()) {
|
||||
for(File file : gameSaveDirectory.listFiles()) {
|
||||
if(!file.isDirectory()) {
|
||||
String name = file.getName();
|
||||
|
||||
try {
|
||||
// Skip if game save data file has an invalid name
|
||||
if(!name.matches("^SAV-[A-HJ-NP-Z2-9]{10}.bin$")) {
|
||||
logger.warn("Game save data {} will not be migrated because the file name is invalid", name);
|
||||
file.delete();
|
||||
continue;
|
||||
}
|
||||
|
||||
String gameSyncId = name.split("-")[1].replace(".bin", "");
|
||||
File outputDirectory = new File(dataDirectory, gameSyncId);
|
||||
|
||||
// Skip if game save data does not belong to any existing Game Sync data
|
||||
if(!outputDirectory.isDirectory()) {
|
||||
logger.warn("Game save data {} will not be migrated because no Game Sync data exists for it", name);
|
||||
file.delete();
|
||||
continue;
|
||||
}
|
||||
|
||||
Files.copy(file.toPath(), new File(outputDirectory, "save.bin").toPath(), StandardCopyOption.REPLACE_EXISTING);
|
||||
file.delete();
|
||||
} catch(IOException e) {
|
||||
logger.error("Could not migrate game save data {}", name, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
gameSaveDirectory.delete();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a {@link Player} from the specified input file.
|
||||
* The loaded player instance is automatically mapped, unless it has an already-mapped Game Sync ID.
|
||||
@@ -62,6 +120,7 @@ public class PlayerManager {
|
||||
throw new IOException("Duplicate Game Sync ID %s".formatted(gameSyncId));
|
||||
}
|
||||
|
||||
player.setDataDirectory(inputFile.getParentFile());
|
||||
playerMap.put(gameSyncId, player);
|
||||
} catch(IOException e) {
|
||||
logger.error("Could not load player data at {}", inputFile.getAbsolutePath(), e);
|
||||
@@ -77,12 +136,9 @@ public class PlayerManager {
|
||||
|
||||
/**
|
||||
* Saves the data of the specified player to disk, and returns {@code true} if it succeeds.
|
||||
* The output file is generated as follows:
|
||||
*
|
||||
* {@code new File(dataDirectory, "PGL-%s".formatted(gameSyncId))}
|
||||
*/
|
||||
public boolean savePlayer(Player player) {
|
||||
return savePlayer(player, new File(dataDirectory, "PGL-%s.json".formatted(player.getGameSyncId())));
|
||||
return savePlayer(player, player.getDataFile());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -119,16 +175,25 @@ public class PlayerManager {
|
||||
* That is, the specified Game Sync ID wasn't already registered and the player data
|
||||
* was saved without any errors.
|
||||
*/
|
||||
public Player registerPlayer(String gameSyncId, GameVersion gameVersion) {
|
||||
public Player registerPlayer(String gameSyncId) {
|
||||
// Check for duplicate Game Sync ID
|
||||
if(playerMap.containsKey(gameSyncId)) {
|
||||
logger.warn("Attempted to register duplicate player {}", gameSyncId);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Create data directory
|
||||
File playerDataDirectory = new File(dataDirectory, gameSyncId);
|
||||
|
||||
if(playerDataDirectory.exists()) {
|
||||
logger.warn("Can't register player {} because the data directory already exists!", gameSyncId);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Construct player object
|
||||
Player player = new Player(gameSyncId, gameVersion);
|
||||
Player player = new Player(gameSyncId);
|
||||
player.setStatus(PlayerStatus.AWAKE);
|
||||
player.setDataDirectory(playerDataDirectory);
|
||||
|
||||
// Try to save player data
|
||||
if(!savePlayer(player)) {
|
||||
@@ -142,20 +207,11 @@ public class PlayerManager {
|
||||
|
||||
/**
|
||||
* Stores a game save file read from the specified input stream to a file.
|
||||
* The output file is generated as follows:
|
||||
*
|
||||
* {@code new File(gameSaveDirectory, "SAV-%s.bin".formatted(gameSyncId))}
|
||||
*
|
||||
* @return {@code true} if the game save file was written successfully, otherwise {@code false}.
|
||||
*/
|
||||
public boolean storePlayerGameSaveFile(Player player, InputStream inputStream) {
|
||||
// Try create directories
|
||||
if(!gameSaveDirectory.exists() && !gameSaveDirectory.mkdirs()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Write the save data
|
||||
try(FileOutputStream outputStream = new FileOutputStream(getPlayerGameSaveFile(player))) {
|
||||
try(FileOutputStream outputStream = new FileOutputStream(player.getSaveFile())) {
|
||||
inputStream.transferTo(outputStream);
|
||||
return true;
|
||||
} catch(IOException e) {
|
||||
@@ -164,13 +220,6 @@ public class PlayerManager {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The game save data {@link File} object associated with this player.
|
||||
*/
|
||||
public File getPlayerGameSaveFile(Player player) {
|
||||
return new File(gameSaveDirectory, "SAV-%s.bin".formatted(player.getGameSyncId()));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {@code true} if a player with the specified Game Sync ID exists, {@code false} otherwise.
|
||||
*/
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -5,11 +5,14 @@ import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import entralinked.model.dlc.Dlc;
|
||||
|
||||
public class User {
|
||||
|
||||
private final String id;
|
||||
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<>();
|
||||
|
||||
public User(String id, String password) {
|
||||
this.id = id;
|
||||
@@ -47,4 +50,24 @@ public class User {
|
||||
protected Map<String, GameProfile> getProfileMap() {
|
||||
return Collections.unmodifiableMap(profiles);
|
||||
}
|
||||
|
||||
public void setDlcOverride(String type, Dlc target) {
|
||||
if(target == null) {
|
||||
dlcOverrides.remove(type);
|
||||
} else {
|
||||
dlcOverrides.put(type, target);
|
||||
}
|
||||
}
|
||||
|
||||
public void removeDlcOverride(String type) {
|
||||
dlcOverrides.remove(type);
|
||||
}
|
||||
|
||||
public boolean hasDlcOverride(String type) {
|
||||
return dlcOverrides.containsKey(type);
|
||||
}
|
||||
|
||||
public Dlc getDlcOverride(String type) {
|
||||
return dlcOverrides.get(type);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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,22 +230,27 @@ public class UserManager {
|
||||
return null;
|
||||
}
|
||||
|
||||
profiles.put(profileId, profile);
|
||||
return profile;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return A unique random 32-bit profile ID.
|
||||
* This will forcibly set the profile id of all profiles of this user to the specified one.
|
||||
* Potentially a destructive operation; use with caution.
|
||||
*
|
||||
* @return {@code true} if the operation was successful, otherwise {@code false}.
|
||||
*/
|
||||
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();
|
||||
public boolean updateProfileIdForUser(User user, int profileId) {
|
||||
// Set the id of all profiles
|
||||
for(GameProfile profile : user.getProfiles()) {
|
||||
profile.setId(profileId);
|
||||
}
|
||||
|
||||
return profileId;
|
||||
// Try to save user
|
||||
if(!saveUser(user)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,30 @@
|
||||
package entralinked.network.http.dashboard;
|
||||
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.bouncycastle.util.Arrays;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import entralinked.Entralinked;
|
||||
import entralinked.model.avenue.AvenueVisitor;
|
||||
import entralinked.model.dlc.Dlc;
|
||||
import entralinked.model.dlc.DlcList;
|
||||
import entralinked.model.pkmn.PkmnGender;
|
||||
@@ -17,7 +35,11 @@ import entralinked.model.player.Player;
|
||||
import entralinked.model.player.PlayerManager;
|
||||
import entralinked.model.player.PlayerStatus;
|
||||
import entralinked.network.http.HttpHandler;
|
||||
import entralinked.utility.ColorUtility;
|
||||
import entralinked.utility.Crc16;
|
||||
import entralinked.utility.GsidUtility;
|
||||
import entralinked.utility.LEOutputStream;
|
||||
import entralinked.utility.TiledImageUtility;
|
||||
import io.javalin.Javalin;
|
||||
import io.javalin.config.JavalinConfig;
|
||||
import io.javalin.http.Context;
|
||||
@@ -30,22 +52,74 @@ import io.javalin.json.JavalinJackson;
|
||||
*/
|
||||
public class DashboardHandler implements HttpHandler {
|
||||
|
||||
private final Set<Integer> availableBlackAndWhiteSpecies = Set.of(
|
||||
505, 507, 510, 511, 513, 515, 519, 523, 525, 527, 529, 531, 533, 535, 538, 539, 542, 545, 546, 548,
|
||||
550, 553, 556, 558, 559, 561, 564, 569, 572, 575, 578, 580, 583, 587, 588, 594, 596, 600, 605, 607,
|
||||
610, 613, 616, 618, 619, 621, 622, 624, 626, 628, 630, 631, 632);
|
||||
private static final Logger logger = LogManager.getLogger();
|
||||
private final Map<Object, BufferedImage> skinPreviewCache = new HashMap<>();
|
||||
private final DlcList dlcList;
|
||||
private final PlayerManager playerManager;
|
||||
|
||||
public DashboardHandler(Entralinked entralinked) {
|
||||
this.dlcList = entralinked.getDlcList();
|
||||
this.playerManager = entralinked.getPlayerManager();
|
||||
|
||||
// Load & cache skin previews
|
||||
logger.info("Loading C-Gear and Pokédex skin previews ...");
|
||||
List<Dlc> skins = dlcList.getDlcList(dlc -> dlc.type().startsWith("CGEAR") || dlc.type().equals("ZUKAN"));
|
||||
|
||||
// Cache default skins
|
||||
for(Dlc skin : skins) {
|
||||
try(FileInputStream inputStream = new FileInputStream(skin.path())) {
|
||||
BufferedImage image = skin.type().equals("ZUKAN") ? TiledImageUtility.readDexSkin(inputStream, true)
|
||||
: skin.type().equals("CGEAR") ? TiledImageUtility.readCGearSkin(inputStream, true)
|
||||
: TiledImageUtility.readCGearSkin(inputStream, false); // CGEAR2
|
||||
skinPreviewCache.put(skin, image);
|
||||
} catch(IOException | IndexOutOfBoundsException e) {
|
||||
logger.error("Could not load image for skin {} of type {}", skin.name(), skin.type(), e);
|
||||
}
|
||||
}
|
||||
|
||||
// Cache custom skins for each player
|
||||
for(Player player : playerManager.getPlayers()) {
|
||||
File cgearSkinFile = player.getCGearSkinFile();
|
||||
File dexSkinFile = player.getDexSkinFile();
|
||||
|
||||
// Cache custom C-Gear skin preview if it exists
|
||||
if(cgearSkinFile.exists()) {
|
||||
try(FileInputStream inputStream = new FileInputStream(cgearSkinFile)) {
|
||||
boolean version2 = player.getGameVersion().isVersion2();
|
||||
|
||||
// Read the C-Gear skin image
|
||||
BufferedImage image = TiledImageUtility.readCGearSkin(inputStream, !version2);
|
||||
|
||||
// Cache the result
|
||||
skinPreviewCache.put("%s/%s".formatted(player.getGameSyncId(), version2 ? "CGEAR2" : "CGEAR"), image);
|
||||
} catch(IOException | IndexOutOfBoundsException | NullPointerException e) {
|
||||
logger.error("Could not load custom C-Gear skin preview for player {}", player.getGameSyncId(), e);
|
||||
}
|
||||
}
|
||||
|
||||
// Cache custom Pokédex skin preview if it exists
|
||||
if(dexSkinFile.exists()) {
|
||||
try(FileInputStream inputStream = new FileInputStream(dexSkinFile)) {
|
||||
// Read the Pokédex skin image
|
||||
BufferedImage image = TiledImageUtility.readDexSkin(inputStream, true);
|
||||
|
||||
// Cache the result
|
||||
skinPreviewCache.put("%s/ZUKAN".formatted(player.getGameSyncId()), image);
|
||||
} catch(IOException | IndexOutOfBoundsException e) {
|
||||
logger.error("Could not load custom Pokédex skin preview for player {}", player.getGameSyncId(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logger.info("Cached {} skin previews", skinPreviewCache.size());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addHandlers(Javalin javalin) {
|
||||
javalin.get("/dashboard/previewskin", this::handlePreviewSkin);
|
||||
javalin.get("/dashboard/dlc", this::handleRetrieveDlcList);
|
||||
javalin.get("/dashboard/profile", this::handleRetrieveProfile);
|
||||
javalin.post("/dashboard/uploadskin", this::handleUploadSkin);
|
||||
javalin.post("/dashboard/profile", this::handleUpdateProfile);
|
||||
javalin.post("/dashboard/login", this::handleLogin);
|
||||
javalin.post("/dashboard/logout", this::handleLogout);
|
||||
@@ -71,6 +145,54 @@ public class DashboardHandler implements HttpHandler {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* GET request handler for {@code /dashboard/previewskin}
|
||||
*/
|
||||
private void handlePreviewSkin(Context ctx) throws IOException {
|
||||
String type = ctx.queryParam("type");
|
||||
String name = ctx.queryParam("name");
|
||||
|
||||
// Make sure query parameters are present
|
||||
if(type == null || name == null) {
|
||||
ctx.status(HttpStatus.NOT_FOUND);
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle custom skin preview
|
||||
if(name.equals("custom")) {
|
||||
Player player = ctx.sessionAttribute("player");
|
||||
|
||||
// Check if player exists
|
||||
if(player == null) {
|
||||
ctx.status(HttpStatus.NOT_FOUND);
|
||||
return;
|
||||
}
|
||||
|
||||
BufferedImage previewImage = skinPreviewCache.get("%s/%s".formatted(player.getGameSyncId(), type));
|
||||
|
||||
// Check if preview image exists
|
||||
if(previewImage == null) {
|
||||
ctx.status(HttpStatus.NOT_FOUND);
|
||||
return;
|
||||
}
|
||||
|
||||
ImageIO.write(previewImage, "png", ctx.outputStream());
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle DLC skin preview
|
||||
Dlc dlc = dlcList.getDlc("IRAO", type, name);
|
||||
|
||||
// Check if DLC exists
|
||||
if(dlc == null) {
|
||||
ctx.status(HttpStatus.NOT_FOUND);
|
||||
return;
|
||||
}
|
||||
|
||||
// Write cached image data
|
||||
ImageIO.write(skinPreviewCache.get(dlc), "png", ctx.outputStream());
|
||||
}
|
||||
|
||||
/**
|
||||
* GET request handler for {@code /dashboard/dlc}
|
||||
*/
|
||||
@@ -84,7 +206,7 @@ public class DashboardHandler implements HttpHandler {
|
||||
}
|
||||
|
||||
// Send result
|
||||
ctx.json(dlcList.getDlcList("IRAO", type).stream().map(Dlc::name).collect(Collectors.toList()));
|
||||
ctx.json(dlcList.getDlcList("IRAO", type).stream().map(Dlc::name).sorted().collect(Collectors.toList()));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -132,7 +254,124 @@ public class DashboardHandler implements HttpHandler {
|
||||
|
||||
// Store session attribute and send response
|
||||
ctx.sessionAttribute("player", player);
|
||||
ctx.json(new DashboardStatusMessage("ok")); // heh
|
||||
ctx.json(Collections.EMPTY_MAP);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST request handler for {@code /dashboard/uploadskin}
|
||||
*/
|
||||
private void handleUploadSkin(Context ctx) throws IOException {
|
||||
Player player = ctx.sessionAttribute("player");
|
||||
|
||||
// Check if player exists
|
||||
if(player == null) {
|
||||
ctx.json(new DashboardStatusMessage("Unauthorized", true));
|
||||
ctx.status(HttpStatus.UNAUTHORIZED);
|
||||
return;
|
||||
}
|
||||
|
||||
String type = ctx.queryParam("type");
|
||||
String fileName = ctx.queryParam("filename");
|
||||
|
||||
// Check if type & file name are present and are valid
|
||||
if(type == null || fileName == null ||
|
||||
(!type.equals("CGEAR") && !type.equals("CGEAR2") && !type.equals("ZUKAN"))) {
|
||||
ctx.status(HttpStatus.BAD_REQUEST);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
BufferedImage image = ImageIO.read(ctx.bodyInputStream());
|
||||
BufferedImage previewImage = null;
|
||||
|
||||
// Make sure file is a valid image
|
||||
if(image == null) {
|
||||
ctx.json(new DashboardStatusMessage("File is either not an image or uses an unsupported format.", true));
|
||||
return;
|
||||
}
|
||||
|
||||
// Make sure image has the correct dimensions
|
||||
if(image.getWidth() != 256 || image.getHeight() != 192) {
|
||||
ctx.json(new DashboardStatusMessage("Image must be 256 x 192 pixels.", true));
|
||||
return;
|
||||
}
|
||||
|
||||
File outputFile = type.equals("ZUKAN") ? player.getDexSkinFile() : player.getCGearSkinFile();
|
||||
ByteArrayOutputStream byteOutputStream = new ByteArrayOutputStream();
|
||||
byte[] skinBytes = null;
|
||||
|
||||
// Write skin data to buffer and generate a preview image
|
||||
switch(type) {
|
||||
case "CGEAR":
|
||||
case "CGEAR2":
|
||||
boolean offsetIndices = type.equals("CGEAR");
|
||||
TiledImageUtility.writeCGearSkin(byteOutputStream, image, offsetIndices);
|
||||
skinBytes = byteOutputStream.toByteArray();
|
||||
previewImage = TiledImageUtility.readCGearSkin(new ByteArrayInputStream(skinBytes), offsetIndices);
|
||||
break;
|
||||
case "ZUKAN":
|
||||
// Generate some background colors based roughly on what's in the image
|
||||
int[] backgroundColors = Arrays.copyOf(TiledImageUtility.DEFAULT_DEX_BACKGROUND_COLORS, 64);
|
||||
int backgroundColor = image.getRGB(0, 0);
|
||||
int dexColor = image.getRGB(0, 134);
|
||||
int buttonColor = image.getRGB(128, 134);
|
||||
|
||||
// Background colors (58, 59, 60)
|
||||
for(int i = 0; i < 3; i++) {
|
||||
backgroundColors[i + 58] = ColorUtility.multiplyColor(backgroundColor, 0.7 + i * 0.15);
|
||||
}
|
||||
|
||||
// Pokédex colors (48, 49, 50, 51, 52)
|
||||
for(int i = 0; i < 5; i++) {
|
||||
backgroundColors[i + 48] = ColorUtility.multiplyColor(dexColor, 1.15 - i * 0.15);
|
||||
}
|
||||
|
||||
// Pokédex button colors (53, 54, 55, 56)
|
||||
for(int i = 0; i < 4; i++) {
|
||||
backgroundColors[i + 53] = ColorUtility.multiplyColor(buttonColor, 0.55 + i * 0.15);
|
||||
}
|
||||
|
||||
// Process skin data
|
||||
TiledImageUtility.writeDexSkin(byteOutputStream, image, backgroundColors);
|
||||
skinBytes = byteOutputStream.toByteArray();
|
||||
previewImage = TiledImageUtility.readDexSkin(new ByteArrayInputStream(skinBytes), true);
|
||||
break;
|
||||
}
|
||||
|
||||
// Write custom skin to output file
|
||||
try(LEOutputStream outputStream = new LEOutputStream(new FileOutputStream(outputFile))) {
|
||||
outputStream.write(skinBytes);
|
||||
outputStream.writeShort(Crc16.calc(skinBytes));
|
||||
}
|
||||
|
||||
// Cache preview image
|
||||
skinPreviewCache.put("%s/%s".formatted(player.getGameSyncId(), type), previewImage);
|
||||
} catch(IOException e) {
|
||||
logger.error("An error occured while processing custom skin data", e);
|
||||
ctx.status(HttpStatus.INTERNAL_SERVER_ERROR);
|
||||
return;
|
||||
} catch(IllegalArgumentException e) {
|
||||
// Silently discard exception, only send feedback to the client
|
||||
ctx.json(new DashboardStatusMessage(e.getMessage(), true));
|
||||
return;
|
||||
}
|
||||
|
||||
// Update player information
|
||||
if(type.equals("ZUKAN")) {
|
||||
player.setDexSkin("custom");
|
||||
player.setCustomDexSkin(fileName);
|
||||
} else {
|
||||
player.setCGearSkin("custom");
|
||||
player.setCustomCGearSkin(fileName);
|
||||
}
|
||||
|
||||
// Try to save player data
|
||||
if(!playerManager.savePlayer(player)) {
|
||||
ctx.json(new DashboardStatusMessage("Profile data could not be saved due to an error.", true));
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.json(Collections.EMPTY_MAP);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -170,6 +409,7 @@ public class DashboardHandler implements HttpHandler {
|
||||
player.setStatus(PlayerStatus.WAKE_READY);
|
||||
player.setEncounters(request.encounters());
|
||||
player.setItems(request.items());
|
||||
player.setAvenueVisitors(request.avenueVisitors());
|
||||
player.setCGearSkin(request.cgearSkin().equals("none") ? null : request.cgearSkin());
|
||||
player.setDexSkin(request.dexSkin().equals("none") ? null : request.dexSkin());
|
||||
player.setMusical(request.musical().equals("none") ? null : request.musical());
|
||||
@@ -196,15 +436,9 @@ public class DashboardHandler implements HttpHandler {
|
||||
}
|
||||
|
||||
for(DreamEncounter encounter : request.encounters()) {
|
||||
if(encounter.species() < 1) {
|
||||
if(encounter.species() < 1 || encounter.species() > 649) {
|
||||
return "Species is out of range.";
|
||||
} else if(encounter.species() > 493) {
|
||||
if(!player.getGameVersion().isVersion2()) {
|
||||
return "Sorry, Generation V Pokémon are exclusive to Black Version 2 and White Version 2.";
|
||||
} else if(!availableBlackAndWhiteSpecies.contains(encounter.species())) {
|
||||
return "You have selected one or more Pokémon species that cannot be downloaded.";
|
||||
}
|
||||
} else if(encounter.move() < 1 || encounter.move() > 559) {
|
||||
} else if(encounter.move() < 0 || encounter.move() > 559) {
|
||||
return "Move ID is out of range.";
|
||||
} else if(encounter.gender() == null) {
|
||||
return "Gender is undefined.";
|
||||
@@ -223,15 +457,36 @@ public class DashboardHandler implements HttpHandler {
|
||||
for(DreamItem item : request.items()) {
|
||||
if(item.id() < 0 || item.id() > 638) {
|
||||
return "Item ID is out of range";
|
||||
} else if(item.id() > 626 && !player.getGameVersion().isVersion2()) {
|
||||
return "You have selected one or more items that are exclusive to Black Version 2 and White Version 2.";
|
||||
}
|
||||
|
||||
if(item.quantity() < 0 || item.quantity() > 20) {
|
||||
} else if(item.quantity() < 0 || item.quantity() > 20) {
|
||||
return "Item quantity is out of range.";
|
||||
}
|
||||
}
|
||||
|
||||
// Validate Join Avenue visitors
|
||||
Set<String> avenueVisitorNames = new HashSet<>(); // For duplicate checking
|
||||
|
||||
if(request.avenueVisitors().size() > 12) {
|
||||
return "Join Avenue visitor list size exceeds the limit.";
|
||||
}
|
||||
|
||||
for(AvenueVisitor visitor : request.avenueVisitors()) {
|
||||
if(visitor.type() == null) {
|
||||
return "Join Avenue visitor type is undefined.";
|
||||
} else if(visitor.shopType() == null) {
|
||||
return "Join Avenue visitor shop type is undefined.";
|
||||
} else if(visitor.name().isBlank() || visitor.name().length() > 7) {
|
||||
return "Join Avenue visitor name must be between 1 and 7 characters in length.";
|
||||
} else if(avenueVisitorNames.contains(visitor.name())) {
|
||||
return "Join Avenue visitors cannot have the same name!";
|
||||
} else if(visitor.gameVersion() == null) {
|
||||
return "Join Avenue visitor game version is undefined.";
|
||||
} else if(visitor.dreamerSpecies() < 1 || visitor.dreamerSpecies() > 649) {
|
||||
return "Tucked-in Pokémon species of the Join Avenue visitor is out of range.";
|
||||
}
|
||||
|
||||
avenueVisitorNames.add(visitor.name());
|
||||
}
|
||||
|
||||
// Validate gained levels
|
||||
if(request.gainedLevels() < 0 || request.gainedLevels() > 99) {
|
||||
return "Gained levels is out of range.";
|
||||
|
||||
@@ -2,6 +2,7 @@ package entralinked.network.http.dashboard;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
import entralinked.model.avenue.AvenueVisitor;
|
||||
import entralinked.model.pkmn.PkmnInfo;
|
||||
import entralinked.model.player.DreamEncounter;
|
||||
import entralinked.model.player.DreamItem;
|
||||
@@ -14,12 +15,17 @@ public record DashboardProfileMessage(
|
||||
String cgearSkin,
|
||||
String dexSkin,
|
||||
String musical,
|
||||
String customCGearSkin,
|
||||
String customDexSkin,
|
||||
int levelsGained,
|
||||
Collection<DreamEncounter> encounters,
|
||||
Collection<DreamItem> items) {
|
||||
Collection<DreamItem> items,
|
||||
Collection<AvenueVisitor> avenueVisitors) {
|
||||
|
||||
public DashboardProfileMessage(String dreamerSprite, Player player) {
|
||||
this(player.getGameVersion().getDisplayName(), dreamerSprite, player.getDreamerInfo(), player.getCGearSkin(),
|
||||
player.getDexSkin(), player.getMusical(), player.getLevelsGained(), player.getEncounters(), player.getItems());
|
||||
this(player.getGameVersion().getDisplayName(), dreamerSprite, player.getDreamerInfo(),
|
||||
player.getCGearSkin(), player.getDexSkin(), player.getMusical(), player.getCustomCGearSkin(),
|
||||
player.getCustomDexSkin(), player.getLevelsGained(), player.getEncounters(), player.getItems(),
|
||||
player.getAvenueVisitors());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,12 +5,14 @@ import java.util.List;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||
|
||||
import entralinked.model.avenue.AvenueVisitor;
|
||||
import entralinked.model.player.DreamEncounter;
|
||||
import entralinked.model.player.DreamItem;
|
||||
|
||||
public record DashboardProfileUpdateRequest(
|
||||
@JsonProperty(required = true) @JsonDeserialize(contentAs = DreamEncounter.class) List<DreamEncounter> encounters,
|
||||
@JsonProperty(required = true) @JsonDeserialize(contentAs = DreamItem.class) List<DreamItem> items,
|
||||
@JsonProperty(required = true) @JsonDeserialize(contentAs = AvenueVisitor.class) List<AvenueVisitor> avenueVisitors,
|
||||
@JsonProperty(required = true) String cgearSkin,
|
||||
@JsonProperty(required = true) String dexSkin,
|
||||
@JsonProperty(required = true) String musical,
|
||||
|
||||
@@ -2,6 +2,7 @@ package entralinked.network.http.dls;
|
||||
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
@@ -9,6 +10,7 @@ import entralinked.Entralinked;
|
||||
import entralinked.model.dlc.Dlc;
|
||||
import entralinked.model.dlc.DlcList;
|
||||
import entralinked.model.user.ServiceSession;
|
||||
import entralinked.model.user.User;
|
||||
import entralinked.model.user.UserManager;
|
||||
import entralinked.network.http.HttpHandler;
|
||||
import entralinked.network.http.HttpRequestHandler;
|
||||
@@ -59,6 +61,9 @@ public class DlsHandler implements HttpHandler {
|
||||
default -> throw new IllegalArgumentException("Invalid POST request action: " + request.action());
|
||||
};
|
||||
|
||||
// Store user attribute for subsequent handlers
|
||||
ctx.attribute("user", session.user());
|
||||
|
||||
// Handle the request
|
||||
handler.process(request, ctx);
|
||||
}
|
||||
@@ -67,26 +72,30 @@ public class DlsHandler implements HttpHandler {
|
||||
* POST handler for {@code /download action=list}
|
||||
*/
|
||||
private void handleRetrieveDlcList(DlsRequest request, Context ctx) throws IOException {
|
||||
// Map to generic type, I doubt there is a real difference between the language codes anyway.
|
||||
String type = switch(request.dlcType()) {
|
||||
case "CGEAR_E", "CGEAR_F", "CGEAR_I", "CGEAR_G", "CGEAR_S", "CGEAR_J", "CGEAR_K" -> "CGEAR";
|
||||
case "CGEAR2_E", "CGEAR2_F", "CGEAR2_I", "CGEAR2_G", "CGEAR2_S", "CGEAR2_J", "CGEAR2_K" -> "CGEAR2";
|
||||
case "ZUKAN_E", "ZUKAN_F", "ZUKAN_I", "ZUKAN_G", "ZUKAN_S", "ZUKAN_J", "ZUKAN_K" -> "ZUKAN";
|
||||
case "MUSICAL_E", "MUSICAL_F", "MUSICAL_I", "MUSICAL_G", "MUSICAL_S", "MUSICAL_J", "MUSICAL_K" -> "MUSICAL";
|
||||
default -> request.dlcType();
|
||||
};
|
||||
User user = ctx.attribute("user");
|
||||
String gameCode = getDlcGameCode(request.dlcGameCode());
|
||||
String type = getRegionlessDlcType(request.dlcType());
|
||||
|
||||
// If an overriding DLC is present, send the data for that instead.
|
||||
if(user.hasDlcOverride(type)) {
|
||||
ctx.result(dlcList.getDlcListString(List.of(user.getDlcOverride(type))));
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO NOTE: I assume that in a conventional implementation, certain DLC attributes may be omitted from the request.
|
||||
ctx.result(dlcList.getDlcListString(dlcList.getDlcList(request.dlcGameCode(), type, request.dlcIndex())));
|
||||
ctx.result(dlcList.getDlcListString(dlcList.getDlcList(gameCode, type, request.dlcIndex())));
|
||||
}
|
||||
|
||||
/**
|
||||
* POST handler for {@code /download action=contents}
|
||||
*/
|
||||
private void handleRetrieveDlcContent(DlsRequest request, Context ctx) throws IOException {
|
||||
// Check if the requested DLC exists
|
||||
Dlc dlc = dlcList.getDlc(request.dlcName());
|
||||
User user = ctx.attribute("user");
|
||||
String gameCode = getDlcGameCode(request.dlcGameCode());
|
||||
String type = getRegionlessDlcType(request.dlcType());
|
||||
Dlc dlc = user.hasDlcOverride(type) ? user.getDlcOverride(type) : dlcList.getDlc(gameCode, type, request.dlcName());
|
||||
|
||||
// Check if the requested DLC exists
|
||||
if(dlc == null) {
|
||||
ctx.status(HttpStatus.NOT_FOUND);
|
||||
return;
|
||||
@@ -103,4 +112,27 @@ public class DlsHandler implements HttpHandler {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The game serial that should be used for downloading DLC based on the provided input.
|
||||
*/
|
||||
private String getDlcGameCode(String gameCode) {
|
||||
return switch(gameCode) {
|
||||
case "IRAJ" -> "IRAO";
|
||||
default -> gameCode;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The DLC type without the region identifier, or the input if it is an unknown type.
|
||||
*/
|
||||
private String getRegionlessDlcType(String dlcType) {
|
||||
return switch(dlcType) {
|
||||
case "CGEAR_E", "CGEAR_F", "CGEAR_I", "CGEAR_G", "CGEAR_S", "CGEAR_J", "CGEAR_K" -> "CGEAR";
|
||||
case "CGEAR2_E", "CGEAR2_F", "CGEAR2_I", "CGEAR2_G", "CGEAR2_S", "CGEAR2_J", "CGEAR2_K" -> "CGEAR2";
|
||||
case "ZUKAN_E", "ZUKAN_F", "ZUKAN_I", "ZUKAN_G", "ZUKAN_S", "ZUKAN_J", "ZUKAN_K" -> "ZUKAN";
|
||||
case "MUSICAL_E", "MUSICAL_F", "MUSICAL_I", "MUSICAL_G", "MUSICAL_S", "MUSICAL_J", "MUSICAL_K" -> "MUSICAL";
|
||||
default -> dlcType;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,6 +117,14 @@ public class NasHandler implements HttpHandler {
|
||||
* POST handler for {@code /ac action=SVCLOC}
|
||||
*/
|
||||
private void handleRetrieveServiceLocation(NasRequest request, Context ctx) throws IOException {
|
||||
// Authenticate user
|
||||
User user = userManager.authenticateUser(request.userId(), request.password());
|
||||
|
||||
if(user == null) {
|
||||
result(ctx, NasReturnCode.USER_NOT_FOUND);
|
||||
return;
|
||||
}
|
||||
|
||||
// Determine service location from type
|
||||
String service = switch(request.serviceType()) {
|
||||
case "0000" -> "external"; // External game-specific service
|
||||
@@ -125,7 +133,7 @@ public class NasHandler implements HttpHandler {
|
||||
};
|
||||
|
||||
// Prepare user credentials
|
||||
ServiceCredentials credentials = userManager.createServiceSession(null, service, null);
|
||||
ServiceCredentials credentials = userManager.createServiceSession(user, service, null);
|
||||
result(ctx, new NasServiceLocationResponse(true, service, credentials.authToken()));
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ package entralinked.network.http.pgl;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
@@ -14,20 +15,25 @@ import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import entralinked.Configuration;
|
||||
import entralinked.Entralinked;
|
||||
import entralinked.model.avenue.AvenueVisitor;
|
||||
import entralinked.model.dlc.Dlc;
|
||||
import entralinked.model.dlc.DlcList;
|
||||
import entralinked.model.pkmn.PkmnInfo;
|
||||
import entralinked.model.pkmn.PkmnInfoReader;
|
||||
import entralinked.model.player.DreamDecor;
|
||||
import entralinked.model.player.DreamEncounter;
|
||||
import entralinked.model.player.DreamItem;
|
||||
import entralinked.model.player.Player;
|
||||
import entralinked.model.player.PlayerManager;
|
||||
import entralinked.model.player.PlayerStatus;
|
||||
import entralinked.model.user.ServiceSession;
|
||||
import entralinked.model.user.User;
|
||||
import entralinked.model.user.UserManager;
|
||||
import entralinked.network.http.HttpHandler;
|
||||
import entralinked.network.http.HttpRequestHandler;
|
||||
import entralinked.serialization.UrlEncodedFormFactory;
|
||||
import entralinked.serialization.UrlEncodedFormParser;
|
||||
import entralinked.utility.GsidUtility;
|
||||
import entralinked.utility.LEOutputStream;
|
||||
import io.javalin.Javalin;
|
||||
import io.javalin.http.Context;
|
||||
@@ -45,6 +51,12 @@ public class PglHandler implements HttpHandler {
|
||||
private static final String password = "2Phfv9MY"; // Best security in the world
|
||||
private final ObjectMapper mapper = new ObjectMapper(new UrlEncodedFormFactory()
|
||||
.disable(UrlEncodedFormParser.Feature.BASE64_DECODE_VALUES));
|
||||
private final List<DreamDecor> decorList = List.of(
|
||||
new DreamDecor(1, "+----------+"),
|
||||
new DreamDecor(2, "Thank you"),
|
||||
new DreamDecor(3, "for using"),
|
||||
new DreamDecor(4, "Entralinked!"),
|
||||
new DreamDecor(5, "+----------+"));
|
||||
private final Set<Integer> sleepyList = new HashSet<>();
|
||||
private final Configuration configuration;
|
||||
private final DlcList dlcList;
|
||||
@@ -89,13 +101,6 @@ public class PglHandler implements HttpHandler {
|
||||
// Deserialize the request
|
||||
PglRequest request = mapper.readValue(ctx.queryString(), PglRequest.class);
|
||||
|
||||
// Check game version
|
||||
if(request.gameVersion() == null) {
|
||||
ctx.status(HttpStatus.BAD_REQUEST);
|
||||
clearTasks(ctx);
|
||||
return;
|
||||
}
|
||||
|
||||
// Verify the service session token
|
||||
ServiceSession session = userManager.getServiceSession(request.token(), "external");
|
||||
|
||||
@@ -105,8 +110,9 @@ public class PglHandler implements HttpHandler {
|
||||
return;
|
||||
}
|
||||
|
||||
// Store request object for subsequent handlers
|
||||
// Store attributes for subsequent handlers
|
||||
ctx.attribute("request", request);
|
||||
ctx.attribute("user", session.user());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -139,7 +145,7 @@ public class PglHandler implements HttpHandler {
|
||||
writeStatusCode(outputStream, 1); // Unauthorized
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Create bitlist
|
||||
byte[] bitlist = new byte[128]; // TODO pool? maybe just cache
|
||||
|
||||
@@ -184,6 +190,7 @@ public class PglHandler implements HttpHandler {
|
||||
private void handleDownloadSaveData(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) {
|
||||
@@ -204,6 +211,33 @@ public class PglHandler implements HttpHandler {
|
||||
List<DreamEncounter> encounters = player.getEncounters();
|
||||
List<DreamItem> items = player.getItems();
|
||||
|
||||
// Prepare DLC information
|
||||
String cgearType = player.getGameVersion().isVersion2() ? "CGEAR2" : "CGEAR";
|
||||
String cgearSkin = player.getCGearSkin();
|
||||
String dexSkin = player.getDexSkin();
|
||||
int cgearSkinIndex = 0;
|
||||
int dexSkinIndex = 0;
|
||||
|
||||
// Create or remove custom C-Gear skin DLC override
|
||||
if("custom".equals(cgearSkin)) {
|
||||
cgearSkinIndex = 1;
|
||||
user.setDlcOverride(cgearType, new Dlc(player.getCGearSkinFile().getAbsolutePath(),
|
||||
"custom", "IRAO", cgearType, cgearSkinIndex, 9730, 0, true));
|
||||
} else {
|
||||
cgearSkinIndex = dlcList.getDlcIndex("IRAO", cgearType, cgearSkin);
|
||||
user.removeDlcOverride(cgearType);
|
||||
}
|
||||
|
||||
// Create or remove custom Pokédex skin DLC override
|
||||
if("custom".equals(dexSkin)) {
|
||||
dexSkinIndex = 1;
|
||||
user.setDlcOverride("ZUKAN", new Dlc(player.getDexSkinFile().getAbsolutePath(),
|
||||
"custom", "IRAO", "ZUKAN", dexSkinIndex, 25090, 0, true));
|
||||
} else {
|
||||
dexSkinIndex = dlcList.getDlcIndex("IRAO", "ZUKAN", dexSkin);
|
||||
user.removeDlcOverride("ZUKAN");
|
||||
}
|
||||
|
||||
// When waking up a Pokémon, these 4 bytes are written to 0x1D304 in the save file.
|
||||
// If the bytes in the game's save file match the new bytes, they will be set to 0x00000000
|
||||
// and no content will be downloaded.
|
||||
@@ -225,10 +259,10 @@ public class PglHandler implements HttpHandler {
|
||||
// Write misc stuff and DLC information
|
||||
outputStream.writeShort(player.getLevelsGained());
|
||||
outputStream.write(0); // Unknown
|
||||
outputStream.write(dlcList.getDlcIndex(player.getMusical()));
|
||||
outputStream.write(dlcList.getDlcIndex(player.getCGearSkin()));
|
||||
outputStream.write(dlcList.getDlcIndex(player.getDexSkin()));
|
||||
outputStream.write(0); // Unknown
|
||||
outputStream.write(dlcList.getDlcIndex("IRAO", "MUSICAL", player.getMusical()));
|
||||
outputStream.write(cgearSkinIndex);
|
||||
outputStream.write(dexSkinIndex);
|
||||
outputStream.write(decorList.isEmpty() ? 0 : 1); // Seems to be a flag for indicating whether or not decor data is present
|
||||
outputStream.write(0); // Must be zero?
|
||||
|
||||
// Write item IDs
|
||||
@@ -246,6 +280,61 @@ public class PglHandler implements HttpHandler {
|
||||
|
||||
// Write quantity padding
|
||||
outputStream.writeBytes(0, (20 - items.size()));
|
||||
|
||||
// Decor data -- copied to 0x1D420 in the save file
|
||||
// Need to send 5 entries or nothing will happen
|
||||
// After decor is selected in Nacrene City, the *index* of it (default: 0x7F) will be saved to 0x1D4A6 in the save file.
|
||||
for(DreamDecor decor : decorList) {
|
||||
byte[] nameBytes = decor.name().getBytes(StandardCharsets.UTF_16LE);
|
||||
|
||||
// If any ID is 0x7E it will not work. It also appears as the default in the save file.
|
||||
outputStream.writeShort(decor.id());
|
||||
|
||||
// Name can't have more than 12 characters
|
||||
outputStream.write(nameBytes, 0, Math.min(24, nameBytes.length));
|
||||
outputStream.writeBytes(-1, 24 - nameBytes.length);
|
||||
}
|
||||
|
||||
// Write decor padding
|
||||
outputStream.writeBytes(0, (5 - decorList.size()) * 26);
|
||||
outputStream.writeShort(0); // ?
|
||||
|
||||
// Join Avenue visitor data -- copied in parts to 0x2422C in the save file.
|
||||
// Black Version 2 and White Version 2 only.
|
||||
if(player.getGameVersion().isVersion2()) {
|
||||
List<AvenueVisitor> avenueVisitors = player.getAvenueVisitors();
|
||||
|
||||
for(AvenueVisitor visitor : avenueVisitors) {
|
||||
// Write visitor name + padding. Names cannot be duplicate.
|
||||
byte[] nameBytes = visitor.name().getBytes(StandardCharsets.UTF_16LE);
|
||||
outputStream.write(nameBytes, 0, Math.min(14, nameBytes.length));
|
||||
outputStream.writeBytes(-1, 14 - nameBytes.length);
|
||||
|
||||
// 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 '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.
|
||||
int visitorType = visitor.type().getClientId() + visitor.personality() * 8;
|
||||
outputStream.writeShort(-1); // Does nothing, seems to be read as part of the name.
|
||||
outputStream.write(visitorType);
|
||||
outputStream.write(visitor.shopType().ordinal() + (7 - visitorType * 2 % 7));
|
||||
outputStream.writeShort(0); // Does nothing
|
||||
outputStream.writeInt(1); // [20] Ignores if 0
|
||||
outputStream.write(visitor.countryCode());
|
||||
outputStream.write(visitor.stateProvinceCode());
|
||||
outputStream.write(0); // [26] Ignores if 1
|
||||
outputStream.write(visitor.gameVersion().getRomCode()); // Affects shop stock
|
||||
outputStream.write(visitor.type().isFemale() ? 1 : 0);
|
||||
outputStream.write(0); // [29] Does.. something
|
||||
outputStream.writeShort(visitor.dreamerSpecies());
|
||||
}
|
||||
|
||||
// Write visitor padding
|
||||
outputStream.writeBytes(0, (12 - avenueVisitors.size()) * 32);
|
||||
outputStream.writeInt(0); // 672 is the total -- there shouldn't be anything left after this. Hooray!
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -268,7 +357,7 @@ public class PglHandler implements HttpHandler {
|
||||
}
|
||||
|
||||
// Check if the game save data exists
|
||||
File file = playerManager.getPlayerGameSaveFile(player);
|
||||
File file = player.getSaveFile();
|
||||
|
||||
if(!file.exists()) {
|
||||
writeStatusCode(outputStream, 5); // No game save data exists for this Game Sync ID
|
||||
@@ -294,6 +383,7 @@ public class PglHandler implements HttpHandler {
|
||||
case "savedata.upload" -> this::handleUploadSaveData;
|
||||
case "savedata.download.finish" -> this::handleDownloadSaveDataFinish;
|
||||
case "account.create.upload" -> this::handleCreateAccount;
|
||||
case "account.createdata" -> this::handleCreateData;
|
||||
default -> throw new IllegalArgumentException("Invalid POST request type: " + request.type());
|
||||
};
|
||||
|
||||
@@ -335,11 +425,13 @@ public class PglHandler implements HttpHandler {
|
||||
// Prepare response
|
||||
LEOutputStream outputStream = new LEOutputStream(ctx.outputStream());
|
||||
|
||||
// Check if player exists, has the same game version and does not already have a Pokémon tucked in
|
||||
// Check if the player exists, has no Pokémon tucked in already and uses the same game version
|
||||
Player player = playerManager.getPlayer(request.gameSyncId());
|
||||
|
||||
if(player == null || player.getGameVersion() != request.gameVersion()
|
||||
|| (player.getStatus() != PlayerStatus.AWAKE && !configuration.allowOverwritingPlayerDreamInfo())) {
|
||||
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();
|
||||
|
||||
@@ -361,7 +453,7 @@ public class PglHandler implements HttpHandler {
|
||||
// Read save data
|
||||
PkmnInfo dreamerInfo = null;
|
||||
|
||||
try(FileInputStream inputStream = new FileInputStream(playerManager.getPlayerGameSaveFile(player))) {
|
||||
try(FileInputStream inputStream = new FileInputStream(player.getSaveFile())) {
|
||||
inputStream.skip(0x1D300); // Skip to dream world data
|
||||
inputStream.skip(8); // Skip to Pokémon data
|
||||
dreamerInfo = PkmnInfoReader.readPokeInfo(inputStream);
|
||||
@@ -369,6 +461,7 @@ public class PglHandler implements HttpHandler {
|
||||
|
||||
// Update and save player information
|
||||
player.setStatus(PlayerStatus.SLEEPING);
|
||||
player.setGameVersion(request.gameVersion());
|
||||
player.setDreamerInfo(dreamerInfo);
|
||||
|
||||
if(!playerManager.savePlayer(player)) {
|
||||
@@ -394,7 +487,37 @@ public class PglHandler implements HttpHandler {
|
||||
|
||||
// Prepare response
|
||||
LEOutputStream outputStream = new LEOutputStream(ctx.outputStream());
|
||||
String gameSyncId = request.gameSyncId();
|
||||
|
||||
// Make sure Game Sync ID is present
|
||||
if(request.gameSyncId() == null) {
|
||||
writeStatusCode(outputStream, 1); // Unauthorized
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if player doesn't exist already
|
||||
if(playerManager.doesPlayerExist(request.gameSyncId())) {
|
||||
writeStatusCode(outputStream, 2); // Duplicate Game Sync ID
|
||||
return;
|
||||
}
|
||||
|
||||
// Try to register player
|
||||
if(playerManager.registerPlayer(request.gameSyncId()) == null) {
|
||||
writeStatusCode(outputStream, 3); // Registration error
|
||||
return;
|
||||
}
|
||||
|
||||
// Write status code
|
||||
writeStatusCode(outputStream, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST handler for {@code /dsio/gw?p=account.createdata}
|
||||
*
|
||||
* Seems to be a funny Japanese version quirk
|
||||
*/
|
||||
private void handleCreateData(PglRequest request, Context ctx) throws IOException {
|
||||
LEOutputStream outputStream = new LEOutputStream(ctx.outputStream());
|
||||
String gameSyncId = GsidUtility.stringifyGameSyncId(Integer.parseInt(ctx.body().replace("\u0000", ""))); // So quirky
|
||||
|
||||
// Check if player doesn't exist already
|
||||
if(playerManager.doesPlayerExist(gameSyncId)) {
|
||||
@@ -403,7 +526,7 @@ public class PglHandler implements HttpHandler {
|
||||
}
|
||||
|
||||
// Try to register player
|
||||
if(playerManager.registerPlayer(gameSyncId, request.gameVersion()) == null) {
|
||||
if(playerManager.registerPlayer(gameSyncId) == null) {
|
||||
writeStatusCode(outputStream, 3); // Registration error
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -7,12 +7,12 @@ import entralinked.GameVersion;
|
||||
import entralinked.serialization.GsidDeserializer;
|
||||
|
||||
public record PglRequest(
|
||||
@JsonProperty(value = "gsid", required = true) @JsonDeserialize(using = GsidDeserializer.class) String gameSyncId,
|
||||
@JsonProperty(value = "p", required = true) String type,
|
||||
@JsonProperty(value = "rom", required = true) int romCode,
|
||||
@JsonProperty(value = "langcode", required = true) int languageCode,
|
||||
@JsonProperty(value = "dreamw", required = true) int dreamWorld, // Always 1, but what is it for?
|
||||
@JsonProperty(value = "tok", required = true) String token) {
|
||||
@JsonProperty(value = "gsid") @JsonDeserialize(using = GsidDeserializer.class) String gameSyncId,
|
||||
@JsonProperty(value = "p", required = true) String type,
|
||||
@JsonProperty(value = "tok", required = true) String token,
|
||||
@JsonProperty(value = "rom") int romCode,
|
||||
@JsonProperty(value = "langcode") int languageCode,
|
||||
@JsonProperty(value = "dreamw") int dreamWorld) { // Always 1, but what is it for?
|
||||
|
||||
public GameVersion gameVersion() {
|
||||
return GameVersion.lookup(romCode(), languageCode());
|
||||
|
||||
@@ -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"
|
||||
|
||||
48
src/main/java/entralinked/utility/ColorUtility.java
Normal file
48
src/main/java/entralinked/utility/ColorUtility.java
Normal file
@@ -0,0 +1,48 @@
|
||||
package entralinked.utility;
|
||||
|
||||
/**
|
||||
* Utility class for various color-related shenanigans.
|
||||
*/
|
||||
public class ColorUtility {
|
||||
|
||||
/**
|
||||
* Multiplies the red, green and blue components of the specified RGB888 color value by the given factor.
|
||||
*
|
||||
* @param color The color to multiply.
|
||||
* @param factor The factor to use.
|
||||
* @return The multiplied color.
|
||||
*/
|
||||
public static int multiplyColor(int color, double factor) {
|
||||
int red = color >> 16 & 0xFF;
|
||||
int green = color >> 8 & 0xFF;
|
||||
int blue = color & 0xFF;
|
||||
red = Math.max(0, Math.min(255, (int)(red * factor)));
|
||||
green = Math.max(0, Math.min(255, (int)(green * factor)));
|
||||
blue = Math.max(0, Math.min(255, (int)(blue * factor)));
|
||||
return (red << 16) | (green << 8) | blue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the input BGR555 color value to an RGB888 color value.
|
||||
*
|
||||
* @return The RGB888 color value.
|
||||
*/
|
||||
public static int convertBGR555ToRGB888(int color) {
|
||||
int red = (color & 0x1F) << 3;
|
||||
int green = (color >> 5 & 0x1F) << 3;
|
||||
int blue = (color >> 10 & 0x1F) << 3;
|
||||
return ((red | red >> 5) << 16) | ((green | green >> 5) << 8) | (blue | blue >> 5);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the input RGB888 color value to a BGR555 color value.
|
||||
*
|
||||
* @return The BGR555 color value.
|
||||
*/
|
||||
public static int convertRGB888ToBGR555(int color) {
|
||||
int red = color >> 16 & 0xFF;
|
||||
int green = color >> 8 & 0xFF;
|
||||
int blue = color & 0xFF;
|
||||
return (red >> 3) | (green >> 3 << 5) | (blue >> 3 << 10);
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
51
src/main/java/entralinked/utility/LEInputStream.java
Normal file
51
src/main/java/entralinked/utility/LEInputStream.java
Normal file
@@ -0,0 +1,51 @@
|
||||
package entralinked.utility;
|
||||
|
||||
import java.io.FilterInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
/**
|
||||
* Like {@link LEOutputStream}, but {@link InputStream}.
|
||||
*/
|
||||
public class LEInputStream extends FilterInputStream {
|
||||
|
||||
protected final byte[] buffer = new byte[8];
|
||||
|
||||
public LEInputStream(InputStream inputStream) {
|
||||
super(inputStream);
|
||||
}
|
||||
|
||||
public short readShort() throws IOException {
|
||||
in.read(buffer, 0, 2);
|
||||
return (short)(buffer[0] & 0xFF
|
||||
| ((buffer[1] & 0xFF) << 8));
|
||||
}
|
||||
|
||||
public int readInt() throws IOException {
|
||||
in.read(buffer, 0, 4);
|
||||
return buffer[0] & 0xFF
|
||||
| ((buffer[1] & 0xFF) << 8)
|
||||
| ((buffer[2] & 0xFF) << 16)
|
||||
| ((buffer[3] & 0xFF) << 24);
|
||||
}
|
||||
|
||||
public double readFloat() throws IOException {
|
||||
return Float.intBitsToFloat(readInt());
|
||||
}
|
||||
|
||||
public long readLong() throws IOException {
|
||||
in.read(buffer, 0, 8);
|
||||
return buffer[0] & 0xFF
|
||||
| ((buffer[1] & 0xFF) << 8)
|
||||
| ((buffer[2] & 0xFF) << 16)
|
||||
| ((long)(buffer[3] & 0xFF) << 24)
|
||||
| ((long)(buffer[4] & 0xFF) << 32)
|
||||
| ((long)(buffer[5] & 0xFF) << 40)
|
||||
| ((long)(buffer[6] & 0xFF) << 48)
|
||||
| ((long)(buffer[7] & 0xFF) << 56);
|
||||
}
|
||||
|
||||
public double readDouble() throws IOException {
|
||||
return Double.longBitsToDouble(readLong());
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,14 @@ public class NetworkUtility {
|
||||
public static InetAddress getLocalHost() {
|
||||
try(Socket socket = new Socket()){
|
||||
socket.connect(new InetSocketAddress("github.com", 80));
|
||||
return socket.getLocalAddress();
|
||||
InetAddress address = socket.getLocalAddress();
|
||||
|
||||
// Fall back to the network interface method if this is not a private IP address
|
||||
if(!address.isSiteLocalAddress()) {
|
||||
return getLocalHostFromNetworkInterfaces();
|
||||
}
|
||||
|
||||
return address;
|
||||
} catch(IOException e) {
|
||||
logger.error("Couldn't get local host using socket - falling back to the network interface method", e);
|
||||
return getLocalHostFromNetworkInterfaces();
|
||||
@@ -42,8 +49,8 @@ public class NetworkUtility {
|
||||
while(addresses.hasMoreElements()) {
|
||||
InetAddress address = addresses.nextElement();
|
||||
|
||||
// Return if IPv4
|
||||
if(address instanceof Inet4Address) {
|
||||
// Return this address if it is a local IPv4 address
|
||||
if(address.isSiteLocalAddress() && address instanceof Inet4Address) {
|
||||
return address;
|
||||
}
|
||||
}
|
||||
@@ -52,6 +59,7 @@ public class NetworkUtility {
|
||||
logger.error("Could not determine local host - falling back to loopback address", e);
|
||||
}
|
||||
|
||||
logger.warn("No local host candidate could be found - loopback address will be used");
|
||||
return InetAddress.getLoopbackAddress();
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
378
src/main/java/entralinked/utility/TiledImageUtility.java
Normal file
378
src/main/java/entralinked/utility/TiledImageUtility.java
Normal file
@@ -0,0 +1,378 @@
|
||||
package entralinked.utility;
|
||||
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
/**
|
||||
* Utility class for reading/writing tiled images to/from {@link BufferedImage} objects.
|
||||
*/
|
||||
public class TiledImageUtility {
|
||||
|
||||
public static final int[] DEFAULT_DEX_BACKGROUND_COLORS = {
|
||||
0xE8E8E8, 0xE0E0E0, 0xD8D8D8, 0xD0D0D0, 0xD0C8C8, 0xC8C8C8, 0xC0C0C0, 0xB8B8B8, 0xB0B0B0, 0xA8A8A8, 0xA0A0A0,
|
||||
0xA0A098, 0x989898, 0x909090, 0x888888, 0x808080, 0x787878, 0x707070, 0x686868, 0x606060, 0x585858, 0x505050,
|
||||
0x484850, 0x484848, 0x484840, 0x404040, 0xF800F8, 0xF800F8, 0xF800F8, 0xF800F8, 0xF800F8, 0xF800F8, 0x282828,
|
||||
0x202020, 0x181818, 0x101010, 0xF800F8, 0x303030, 0x404040, 0x505050, 0xF800F8, 0xF800F8, 0xF800F8, 0xF800F8,
|
||||
0xF800F8, 0xF800F8, 0xF800F8, 0xF800F8, 0xF870A0, 0xF84878, 0xC03860, 0x802848, 0x482030, 0x505858, 0x909090,
|
||||
0xC8C8C8, 0xE0E8E8, 0xF800F8, 0x404848, 0x707878, 0xA8B0B0, 0xF800F8, 0xF800F8, 0xF800F8}; // Unsafe
|
||||
public static final int TILE_WIDTH = 8;
|
||||
public static final int TILE_HEIGHT = 8;
|
||||
public static final int TILE_SIZE = TILE_WIDTH * TILE_HEIGHT;
|
||||
public static final int COLOR_PALETTE_SIZE = 16;
|
||||
public static final int SCREEN_WIDTH = 256;
|
||||
public static final int SCREEN_HEIGHT = 192;
|
||||
public static final int SCREEN_SIZE = SCREEN_WIDTH * SCREEN_HEIGHT;
|
||||
public static final int SCREEN_TILE_COUNT = SCREEN_SIZE / TILE_SIZE;
|
||||
private static final byte[] dexBackgroundColorIndices = new byte[SCREEN_SIZE];
|
||||
private static final boolean[] dexBackgroundOverrides = new boolean[SCREEN_SIZE];
|
||||
private static final Logger logger = LogManager.getLogger();
|
||||
|
||||
static {
|
||||
// Load Pokédex skin background data
|
||||
try(LEInputStream inputStream = new LEInputStream(TiledImageUtility.class.getResourceAsStream("/zukan.bin"))) {
|
||||
int index = 0;
|
||||
int pairs = inputStream.readShort();
|
||||
|
||||
// Read background color indices
|
||||
for(int i = 0; i < pairs; i++) {
|
||||
int amount = inputStream.read();
|
||||
int value = inputStream.read();
|
||||
|
||||
for(int j = 0; j < amount; j++) {
|
||||
dexBackgroundColorIndices[index++] = (byte)(value & 63);
|
||||
}
|
||||
}
|
||||
|
||||
index = 0;
|
||||
pairs = inputStream.readShort();
|
||||
|
||||
// Read background override bits
|
||||
for(int i = 0; i < pairs; i++) {
|
||||
int amount = inputStream.readShort();
|
||||
int value = inputStream.read();
|
||||
|
||||
for(int j = 0; j < amount; j++) {
|
||||
for(int k = 0; k < 8; k++) {
|
||||
dexBackgroundOverrides[index++] = (value & 1 << k) != 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch(IOException e) {
|
||||
logger.error("Could not load Pokédex background data", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads a C-Gear skin from the specified {@link InputStream}.
|
||||
*
|
||||
* @param inputStream The input stream to read from.
|
||||
* @param normalizeIndices Should be {@code true} if the provided C-Gear skin data is from the original Black & White.
|
||||
* @return A {@link BufferedImage} representing the read C-Gear skin data.
|
||||
*/
|
||||
public static BufferedImage readCGearSkin(InputStream inputStream, boolean normalizeIndices) throws IOException {
|
||||
return readTiledImage(new LEInputStream(inputStream), 255, 0, null, null, normalizeIndices);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads a Pokédex skin from the specified {@link InputStream}.
|
||||
* If the skin in question is an overlay, the Pokédex background will be applied to the resulting image automatically.
|
||||
*
|
||||
* @param inputStream The input stream to read from.
|
||||
* @param applyBackground Whether to apply the Pokédex background to the result image, or read the data as-is.
|
||||
* @return A {@link BufferedImage} representing the read Pokédex skin data.
|
||||
*/
|
||||
public static BufferedImage readDexSkin(InputStream inputStream, boolean applyBackground) throws IOException {
|
||||
return readTiledImage(new LEInputStream(inputStream), 768, 64,
|
||||
applyBackground ? dexBackgroundColorIndices : null, applyBackground ? dexBackgroundOverrides : null, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads tiled image data from the provided {@link InputStream} and returns a {@link BufferedImage} representing the read image data.
|
||||
*
|
||||
* @param inputStream The input stream to read from.
|
||||
* @param tileCount The number of tiles to be read. This should be equal to the maximum number of tiles for this image.
|
||||
* @param backgroundColorCount The number of background colors this image has.
|
||||
* @param backgroundColorIndices The background color indices of the background image. If {@code null}, no background will be used.
|
||||
* @param backgroundOverrides An array of booleans representing pixels that forcibly use the background color. Can be {@code null}.
|
||||
* @param normalizedIndices Indicates that tile indices are not linear (Black & White C-Gear skins) and should be normalized.
|
||||
* @return A {@link BufferedImage} representing the read image data.
|
||||
*/
|
||||
private static BufferedImage readTiledImage(LEInputStream inputStream, int tileCount, int backgroundColorCount,
|
||||
byte[] backgroundColorIndices, boolean[] backgroundOverrides, boolean normalizeIndices) throws IOException {
|
||||
BufferedImage image = new BufferedImage(SCREEN_WIDTH, SCREEN_HEIGHT, BufferedImage.TYPE_INT_RGB); // Result image
|
||||
int[] tileData = new int[tileCount * TILE_SIZE];
|
||||
int[] tileIndices = new int[tileCount]; // Tile index lookup table
|
||||
int[] colorPalette = new int[COLOR_PALETTE_SIZE];
|
||||
int[] backgroundColorPalette = new int[backgroundColorCount];
|
||||
|
||||
// Read tile data.
|
||||
for(int i = 0; i < tileCount; i++) {
|
||||
byte[] rawTileData = inputStream.readNBytes(TILE_SIZE / 2);
|
||||
|
||||
for(int j = 0; j < rawTileData.length; j++) {
|
||||
int paletteIndices = rawTileData[j]; // Contains color palette indices for 2 adjacent pixels.
|
||||
tileData[i * TILE_SIZE + j * 2] = paletteIndices & (COLOR_PALETTE_SIZE - 1);
|
||||
tileData[i * TILE_SIZE + j * 2 + 1] = (paletteIndices >> 4) & (COLOR_PALETTE_SIZE - 1);
|
||||
}
|
||||
|
||||
// Store the index of the tile as it would be in memory so we can look it up later when we're mapping the tiles.
|
||||
tileIndices[i] = normalizeIndices ? i + i / 17 * 15 + 0xA0A0 : i;
|
||||
}
|
||||
|
||||
// Read foreground color data.
|
||||
// In cases where background colors are present, pixels that use the *first* foreground color
|
||||
// will be replaced by the background color at that pixel's location.
|
||||
for(int i = 0; i < COLOR_PALETTE_SIZE; i++) {
|
||||
int color = inputStream.readShort();
|
||||
|
||||
// The game seems to always replace the first color of the foreground with the background,
|
||||
// so let's just set it to black so that C-Gear skin previews are more accurate.
|
||||
// This won't do anything for Pokédex skins, as they use a special background color palette.
|
||||
colorPalette[i] = i == 0 ? 0 : ColorUtility.convertBGR555ToRGB888(color);
|
||||
}
|
||||
|
||||
// Read background color data.
|
||||
// Pokédex skins contain room for 240 extra colors, 64 of which are defined and appear to be used as the
|
||||
// 'background' colors in cases where the skin is only an overlay that is displayed on top of the 'true' Pokédex.
|
||||
for(int i = 0; i < backgroundColorCount; i++) {
|
||||
backgroundColorPalette[i] = ColorUtility.convertBGR555ToRGB888(inputStream.readShort());
|
||||
}
|
||||
|
||||
// Map tiles to the resulting image.
|
||||
// In cases where the tile count is 768 or greater, which is exactly enough tiles to fill the entire screen,
|
||||
// the tiles will be applied in the order they are provided and no additional mapping data will be read.
|
||||
// This is always the case for Pokédex skins, and never the case for C-Gear skins.
|
||||
if(tileCount < SCREEN_TILE_COUNT) {
|
||||
// Not enough tiles -- read additional mapping data to figure out their placement.
|
||||
for(int i = 0; i < SCREEN_TILE_COUNT; i++) {
|
||||
int x = i * TILE_WIDTH % SCREEN_WIDTH;
|
||||
int y = i * TILE_WIDTH / SCREEN_WIDTH * TILE_HEIGHT;
|
||||
int leftBits = inputStream.read();
|
||||
int rightBits = inputStream.read();
|
||||
int memoryIndex = leftBits | (rightBits & ~12) << 8;
|
||||
int flipBits = rightBits & 12;
|
||||
|
||||
// The normalized tile index is the index of the in-memory tile index in the lookup table.
|
||||
int tileIndex = -1;
|
||||
|
||||
for(int k = 0; k < tileIndices.length; k++) {
|
||||
if(memoryIndex == tileIndices[k]) {
|
||||
tileIndex = k;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If no tile could be found, then just skip it.
|
||||
// TODO It is possible that it uses background colors in this case, but just leaving the tile black should be good enough.
|
||||
if(tileIndex == -1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Apply the pixels of this tile to the resulting image.
|
||||
for(int j = 0; j < TILE_SIZE; j++) {
|
||||
// Get the color index for this pixel based on how the tile is flipped.
|
||||
int tilePixelIndex = switch(flipBits) {
|
||||
case 4 -> (TILE_WIDTH * (j / TILE_WIDTH) + TILE_WIDTH) - j % TILE_WIDTH - 1; // Flip horizontally
|
||||
case 8 -> TILE_SIZE - (TILE_WIDTH * (j / TILE_WIDTH) + TILE_WIDTH) + j % TILE_WIDTH; // Flip vertically
|
||||
case 12 -> TILE_SIZE - j - 1; // Flip horizontally & vertically
|
||||
default -> j; // Don't flip
|
||||
};
|
||||
|
||||
int pixelX = x + j % TILE_WIDTH;
|
||||
int pixelY = y + j / TILE_WIDTH;
|
||||
int pixelIndex = pixelY * SCREEN_WIDTH + pixelX;
|
||||
int paletteIndex = tileData[tileIndex * TILE_SIZE + tilePixelIndex];
|
||||
|
||||
// Determine whether the foreground or background color should be used.
|
||||
int color = backgroundColorIndices == null ||
|
||||
(paletteIndex > 0 && (backgroundOverrides == null || !backgroundOverrides[pixelIndex]))
|
||||
? colorPalette[paletteIndex] : backgroundColorPalette[backgroundColorIndices[pixelIndex]];
|
||||
|
||||
// Finally, set the pixel!
|
||||
image.setRGB(pixelX, pixelY, color);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// There are enough tiles to fill up the entire screen, so let's just place them in order.
|
||||
for(int i = 0; i < SCREEN_TILE_COUNT; i++) {
|
||||
int x = i * TILE_WIDTH % SCREEN_WIDTH;
|
||||
int y = i * TILE_WIDTH / SCREEN_WIDTH * TILE_WIDTH;
|
||||
|
||||
for(int j = 0; j < TILE_SIZE; j++) {
|
||||
int pixelX = x + j % TILE_WIDTH;
|
||||
int pixelY = y + j / TILE_WIDTH;
|
||||
int pixelIndex = pixelY * SCREEN_WIDTH + pixelX;
|
||||
int paletteIndex = tileData[i * TILE_SIZE + j];
|
||||
int color = backgroundColorIndices == null ||
|
||||
(paletteIndex > 0 && (backgroundOverrides == null || !backgroundOverrides[pixelIndex]))
|
||||
? colorPalette[paletteIndex] : backgroundColorPalette[backgroundColorIndices[pixelIndex]];
|
||||
image.setRGB(pixelX, pixelY, color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return image;
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes a {@link BufferedImage} to the specified {@link OutputStream} as a C-Gear skin.
|
||||
*
|
||||
* @param outputStream The output stream to write to.
|
||||
* @param image The image to write.
|
||||
* @param offsetIndices Should be {@code true} if the C-Gear skin is intended for use with the original Black & White games.
|
||||
*/
|
||||
public static void writeCGearSkin(OutputStream outputStream, BufferedImage image, boolean offsetIndices) throws IOException {
|
||||
writeTiledImage(new LEOutputStream(outputStream), image, 255, null, offsetIndices);
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes a {@link BufferedImage} to the specified {@link OutputStream} as a Pokédex skin.
|
||||
*
|
||||
* @param outputStream The output stream to write to.
|
||||
* @param image The image to write.
|
||||
* @param backgroundColors The background colors this skin will use in-game. Should contain 64 RGB888 color values.
|
||||
*/
|
||||
public static void writeDexSkin(OutputStream outputStream, BufferedImage image, int[] backgroundColors) throws IOException {
|
||||
if(backgroundColors == null || backgroundColors.length != 64) {
|
||||
throw new IllegalArgumentException("Background color array must contain 64 colors");
|
||||
}
|
||||
|
||||
writeTiledImage(new LEOutputStream(outputStream), image, 768, backgroundColors, false);
|
||||
outputStream.write(new byte[352]); // Extra data that writeTiledImage doesn't write
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes a {@link BufferedImage} to the specified {@link OutputStream} as a tiled image.
|
||||
*
|
||||
* @param outputStream The output stream to write to.
|
||||
* @param image The image to write.
|
||||
* @param tileCount The maximum number of unique tiles that can be used.
|
||||
* @param backgroundColors Additional background colors that the game might use. Can be {@code null}.
|
||||
* @param offsetIndices Indicates that tile indices are not linear (Black & White C-Gear skins) and should be offset.
|
||||
*/
|
||||
private static void writeTiledImage(LEOutputStream outputStream, BufferedImage image,
|
||||
int tileCount, int[] backgroundColors, boolean offsetTileIndices) throws IOException {
|
||||
int[] tileData = new int[TILE_SIZE * tileCount]; // Stores colors, NOT indices!
|
||||
int[] tileMappingData = new int[SCREEN_TILE_COUNT];
|
||||
int[] colorPalette = new int[COLOR_PALETTE_SIZE];
|
||||
int currentTileCount = 0;
|
||||
int currentColorCount = 0;
|
||||
|
||||
// Create tile data
|
||||
for(int i = 0; i < SCREEN_TILE_COUNT; i++) {
|
||||
// If tile count is limited, check if we can replicate this tile by flipping an existing tile.
|
||||
if(tileCount < SCREEN_TILE_COUNT) {
|
||||
boolean duplicateTile = false;
|
||||
|
||||
for(int j = 0; j < currentTileCount; j++) {
|
||||
boolean equal = true;
|
||||
boolean flipX = true;
|
||||
boolean flipY = true;
|
||||
boolean flipXY = true;
|
||||
|
||||
for(int k = 0; k < TILE_SIZE; k++) {
|
||||
int x = i * TILE_WIDTH % SCREEN_WIDTH;
|
||||
int y = i * TILE_WIDTH / SCREEN_WIDTH * TILE_WIDTH;
|
||||
int color = image.getRGB(x + k % TILE_WIDTH, y + k / TILE_WIDTH);
|
||||
equal &= color == tileData[j * TILE_SIZE + k];
|
||||
flipX &= color == tileData[j * TILE_SIZE + ((TILE_WIDTH * (k / TILE_WIDTH) + TILE_WIDTH) - k % TILE_WIDTH - 1)];
|
||||
flipY &= color == tileData[j * TILE_SIZE + (TILE_SIZE - (TILE_WIDTH * (k / TILE_WIDTH) + TILE_WIDTH) + k % TILE_WIDTH)];
|
||||
flipXY &= color == tileData[j * TILE_SIZE + (TILE_SIZE - k - 1)];
|
||||
}
|
||||
|
||||
int flipBits = equal ? 0 : flipX ? 4 : flipY ? 8 : flipXY ? 12 : -1;
|
||||
|
||||
// If this tile is equal to an existing tile or can be replicated by flipping another tile,
|
||||
// create mapping data for it and skip to the next tile.
|
||||
if(flipBits != -1) {
|
||||
tileMappingData[i] = (offsetTileIndices ? (j + (15 * (j / 17)) + 0xA0A0) : j) | flipBits << 8;
|
||||
duplicateTile = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Skip to next tile if it is duplicate.
|
||||
if(duplicateTile) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Throw exception if a new tile needs to be added but we have reached the limit.
|
||||
if(currentTileCount >= tileCount) {
|
||||
throw new IllegalArgumentException(
|
||||
"Image is too complex. Only %s unique tiles (8x8 pixel sections) are permitted.".formatted(tileCount));
|
||||
}
|
||||
|
||||
int x = i * TILE_WIDTH % SCREEN_WIDTH;
|
||||
int y = i * TILE_WIDTH / SCREEN_WIDTH * TILE_WIDTH;
|
||||
|
||||
for(int j = 0; j < TILE_SIZE; j++) {
|
||||
int pixelX = x + j % TILE_WIDTH;
|
||||
int pixelY = y + j / TILE_WIDTH;
|
||||
int color = image.getRGB(pixelX, pixelY);
|
||||
int paletteIndex = indexOf(colorPalette, color);
|
||||
|
||||
// Add color to the palette if it hasn't been already.
|
||||
if(paletteIndex == -1) {
|
||||
// Throw exception if we have already reached the maximum amount of colors.
|
||||
if(currentColorCount >= COLOR_PALETTE_SIZE) {
|
||||
throw new IllegalArgumentException(
|
||||
"Too many unique colors. Only %s unique colors are permitted.".formatted(COLOR_PALETTE_SIZE));
|
||||
}
|
||||
|
||||
paletteIndex = currentColorCount++;
|
||||
colorPalette[paletteIndex] = color;
|
||||
}
|
||||
|
||||
tileData[currentTileCount * TILE_SIZE + j] = color;
|
||||
}
|
||||
|
||||
// Create mapping data for this tile.
|
||||
tileMappingData[i] = offsetTileIndices ? (currentTileCount + currentTileCount / 17 * 15 + 0xA0A0) : currentTileCount;
|
||||
currentTileCount++;
|
||||
}
|
||||
|
||||
// Write tile data.
|
||||
for(int i = 0; i < tileData.length; i += 2) {
|
||||
int paletteIndices = indexOf(colorPalette, tileData[i]) | (indexOf(colorPalette, tileData[i + 1]) << 4);
|
||||
outputStream.write(paletteIndices);
|
||||
}
|
||||
|
||||
// Write foreground color data.
|
||||
for(int color : colorPalette) {
|
||||
outputStream.writeShort(ColorUtility.convertRGB888ToBGR555(color));
|
||||
}
|
||||
|
||||
// Write background color data if it is present.
|
||||
if(backgroundColors != null) {
|
||||
for(int color : backgroundColors) {
|
||||
outputStream.writeShort(ColorUtility.convertRGB888ToBGR555(color));
|
||||
}
|
||||
}
|
||||
|
||||
// Write tile mapping data.
|
||||
if(tileCount < SCREEN_TILE_COUNT) {
|
||||
for(int i = 0; i < SCREEN_TILE_COUNT; i++) {
|
||||
outputStream.writeShort(tileMappingData[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The index of the element in the array, or {@code -1} if no such element exists.
|
||||
*/
|
||||
private static int indexOf(int[] array, int element) {
|
||||
for(int i = 0; i < array.length; i++) {
|
||||
if(element == array[i]) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="stylesheet" href="styles/list.css">
|
||||
</head>
|
||||
<body onload="fillTableWithItems()">
|
||||
<div id="main-container" class="root-container">
|
||||
<label class="big-label">List of items that can be downloaded</label>
|
||||
<label>NOTE: Item IDs greater than 626 are exclusive to Black Version 2 and White Version 2</label>
|
||||
<table id="item-table">
|
||||
<!-- Filled by list.js -->
|
||||
</table>
|
||||
</div>
|
||||
</body>
|
||||
<script src="scripts/list.js"></script>
|
||||
</html>
|
||||
@@ -5,10 +5,13 @@
|
||||
</head>
|
||||
<body>
|
||||
<div class="root-container">
|
||||
<label for="gsid">Game Sync ID</label><br>
|
||||
<input type='text' id="gsid" name='gsid' placeholder='XXXXXXXXXX'>
|
||||
<button id="login" onclick="postLogin()">Login</button>
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
<script src="scripts/utility.js"></script>
|
||||
<script src="scripts/login.js"></script>
|
||||
</html>
|
||||
|
||||
@@ -3,177 +3,309 @@
|
||||
<meta charset="UTF-8">
|
||||
<link rel="stylesheet" href="styles/profile.css">
|
||||
</head>
|
||||
<body onload="fetchProfileData()">
|
||||
<div id="main-container" class="root-container" style="display:none;">
|
||||
<body>
|
||||
<div id="main-container" class="centered-container" style="display:none;">
|
||||
<div>
|
||||
<label id="game-summary" class="header-text"></label>
|
||||
</div>
|
||||
<div>
|
||||
<!-- Dreamer Summary -->
|
||||
<label>Tucked-in Pokémon Summary</label><br>
|
||||
<table id="dreamer-summary" class="dreamer-summary">
|
||||
<tr>
|
||||
<td id="dreamer-sprite" class="dreamer-sprite" rowspan="5">
|
||||
<image src="/sprites/pokemon/normal/0.png"/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Species</th>
|
||||
<td id="dreamer-species"></td>
|
||||
<th>Nature</th>
|
||||
<td id="dreamer-nature"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<td id="dreamer-name"></td>
|
||||
<th>Gender</th>
|
||||
<td id="dreamer-gender"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Trainer</th>
|
||||
<td id="dreamer-trainer"></td>
|
||||
<th>Level</th>
|
||||
<td id="dreamer-level"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Trainer ID</th>
|
||||
<td id="dreamer-trainer-id"></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div>
|
||||
<!-- Entree Forest Encounter Configuration -->
|
||||
<label>Entree Forest Encounters (Max. 10)</label><br>
|
||||
<div>
|
||||
<table id="encounter-table" class="encounter-image-table">
|
||||
<label id="game-summary" class="header-text"></label>
|
||||
</div>
|
||||
<div>
|
||||
<!-- Dreamer Summary -->
|
||||
<label>Tucked-in Pokémon Summary</label><br>
|
||||
<table id="dreamer-summary" class="dreamer-summary">
|
||||
<tr>
|
||||
<td><a id="encounter0" href="#configureEncounter" onclick="configureEncounter(0)"><image src="/sprites/pokemon/normal/0.png"/></a></td>
|
||||
<td><a id="encounter1" href="#configureEncounter" onclick="configureEncounter(1)"><image src="/sprites/pokemon/normal/0.png"/></a></td>
|
||||
<td><a id="encounter2" href="#configureEncounter" onclick="configureEncounter(2)"><image src="/sprites/pokemon/normal/0.png"/></a></td>
|
||||
<td><a id="encounter3" href="#configureEncounter" onclick="configureEncounter(3)"><image src="/sprites/pokemon/normal/0.png"/></a></td>
|
||||
<td><a id="encounter4" href="#configureEncounter" onclick="configureEncounter(4)"><image src="/sprites/pokemon/normal/0.png"/></a></td>
|
||||
<td id="dreamer-sprite" class="dreamer-sprite" rowspan="5">
|
||||
<image src="/sprites/pokemon/normal/0.png"/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a id="encounter5" href="#configureEncounter" onclick="configureEncounter(5)"><image src="/sprites/pokemon/normal/0.png"/></a></td>
|
||||
<td><a id="encounter6" href="#configureEncounter" onclick="configureEncounter(6)"><image src="/sprites/pokemon/normal/0.png"/></a></td>
|
||||
<td><a id="encounter7" href="#configureEncounter" onclick="configureEncounter(7)"><image src="/sprites/pokemon/normal/0.png"/></a></td>
|
||||
<td><a id="encounter8" href="#configureEncounter" onclick="configureEncounter(8)"><image src="/sprites/pokemon/normal/0.png"/></a></td>
|
||||
<td><a id="encounter9" href="#configureEncounter" onclick="configureEncounter(9)"><image src="/sprites/pokemon/normal/0.png"/></a></td>
|
||||
<th>Species</th>
|
||||
<td id="dreamer-species"></td>
|
||||
<th>Ability</th>
|
||||
<td id="dreamer-ability"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<td id="dreamer-name"></td>
|
||||
<th>Nature</th>
|
||||
<td id="dreamer-nature"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Trainer</th>
|
||||
<td id="dreamer-trainer"></td>
|
||||
<th>Gender</th>
|
||||
<td id="dreamer-gender"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Trainer ID</th>
|
||||
<td id="dreamer-trainer-id"></td>
|
||||
<th>Level</th>
|
||||
<td id="dreamer-level"></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<!-- Item Configuration -->
|
||||
<label>Items (Max. 20)</label><br>
|
||||
<div>
|
||||
<table id="item-table" class="item-table">
|
||||
<tr>
|
||||
<td><a id="item0" href="#configureItem" onclick="configureItem(0)"><image src="/sprites/items/0.png"/></a></td>
|
||||
<td><a id="item1" href="#configureItem" onclick="configureItem(1)"><image src="/sprites/items/0.png"/></a></td>
|
||||
<td><a id="item2" href="#configureItem" onclick="configureItem(2)"><image src="/sprites/items/0.png"/></a></td>
|
||||
<td><a id="item3" href="#configureItem" onclick="configureItem(3)"><image src="/sprites/items/0.png"/></a></td>
|
||||
<td><a id="item4" href="#configureItem" onclick="configureItem(4)"><image src="/sprites/items/0.png"/></a></td>
|
||||
<td><a id="item5" href="#configureItem" onclick="configureItem(5)"><image src="/sprites/items/0.png"/></a></td>
|
||||
<td><a id="item6" href="#configureItem" onclick="configureItem(6)"><image src="/sprites/items/0.png"/></a></td>
|
||||
<td><a id="item7" href="#configureItem" onclick="configureItem(7)"><image src="/sprites/items/0.png"/></a></td>
|
||||
<td><a id="item8" href="#configureItem" onclick="configureItem(8)"><image src="/sprites/items/0.png"/></a></td>
|
||||
<td><a id="item9" href="#configureItem" onclick="configureItem(9)"><image src="/sprites/items/0.png"/></a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a id="item10" href="#configureItem" onclick="configureItem(10)"><image src="/sprites/items/0.png"/></a></td>
|
||||
<td><a id="item11" href="#configureItem" onclick="configureItem(11)"><image src="/sprites/items/0.png"/></a></td>
|
||||
<td><a id="item12" href="#configureItem" onclick="configureItem(12)"><image src="/sprites/items/0.png"/></a></td>
|
||||
<td><a id="item13" href="#configureItem" onclick="configureItem(13)"><image src="/sprites/items/0.png"/></a></td>
|
||||
<td><a id="item14" href="#configureItem" onclick="configureItem(14)"><image src="/sprites/items/0.png"/></a></td>
|
||||
<td><a id="item15" href="#configureItem" onclick="configureItem(15)"><image src="/sprites/items/0.png"/></a></td>
|
||||
<td><a id="item16" href="#configureItem" onclick="configureItem(16)"><image src="/sprites/items/0.png"/></a></td>
|
||||
<td><a id="item17" href="#configureItem" onclick="configureItem(17)"><image src="/sprites/items/0.png"/></a></td>
|
||||
<td><a id="item18" href="#configureItem" onclick="configureItem(18)"><image src="/sprites/items/0.png"/></a></td>
|
||||
<td><a id="item19" href="#configureItem" onclick="configureItem(19)"><image src="/sprites/items/0.png"/></a></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<!-- Misc Configurations -->
|
||||
<div class="grid-container">
|
||||
<!-- Entree Forest Encounter Configuration -->
|
||||
<label>Entree Forest Encounters (Max. 10)</label><br>
|
||||
<div>
|
||||
<label>CGear Skin</label>
|
||||
<select id="cgear-skin">
|
||||
<option value="none">Do not change</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label>Pokédex Skin</label>
|
||||
<select id="dex-skin">
|
||||
<option value="none">Do not change</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label>Musical</label>
|
||||
<select id="musical">
|
||||
<option value="none">Do not change</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label>Level Gain</label>
|
||||
<input id="level-gain-input" type="number" value="0" min="0" max="99"/>
|
||||
<table id="encounter-table" class="encounter-image-table">
|
||||
<tr>
|
||||
<td id="encounter0" onclick="configureEncounter(0)"><image src="/sprites/pokemon/normal/0.png"/></td>
|
||||
<td id="encounter1" onclick="configureEncounter(1)"><image src="/sprites/pokemon/normal/0.png"/></td>
|
||||
<td id="encounter2" onclick="configureEncounter(2)"><image src="/sprites/pokemon/normal/0.png"/></td>
|
||||
<td id="encounter3" onclick="configureEncounter(3)"><image src="/sprites/pokemon/normal/0.png"/></td>
|
||||
<td id="encounter4" onclick="configureEncounter(4)"><image src="/sprites/pokemon/normal/0.png"/></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td id="encounter5" onclick="configureEncounter(5)"><image src="/sprites/pokemon/normal/0.png"/></td>
|
||||
<td id="encounter6" onclick="configureEncounter(6)"><image src="/sprites/pokemon/normal/0.png"/></td>
|
||||
<td id="encounter7" onclick="configureEncounter(7)"><image src="/sprites/pokemon/normal/0.png"/></td>
|
||||
<td id="encounter8" onclick="configureEncounter(8)"><image src="/sprites/pokemon/normal/0.png"/></td>
|
||||
<td id="encounter9" onclick="configureEncounter(9)"><image src="/sprites/pokemon/normal/0.png"/></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div id="visitor-table-container" style="display:none;">
|
||||
<!-- Join Avenue Visitor Configuration -->
|
||||
<label>Join Avenue Visitors (Max. 12)</label><br>
|
||||
<div>
|
||||
<table id="visitor-table" class="visitor-table">
|
||||
<tr>
|
||||
<td id="visitor0" onclick="configureVisitor(0)"><image src="/sprites/trainers/none.png"/></td>
|
||||
<td id="visitor1" onclick="configureVisitor(1)"><image src="/sprites/trainers/none.png"/></td>
|
||||
<td id="visitor2" onclick="configureVisitor(2)"><image src="/sprites/trainers/none.png"/></td>
|
||||
<td id="visitor3" onclick="configureVisitor(3)"><image src="/sprites/trainers/none.png"/></td>
|
||||
<td id="visitor4" onclick="configureVisitor(4)"><image src="/sprites/trainers/none.png"/></td>
|
||||
<td id="visitor5" onclick="configureVisitor(5)"><image src="/sprites/trainers/none.png"/></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td id="visitor6" onclick="configureVisitor(6)"><image src="/sprites/trainers/none.png"/></td>
|
||||
<td id="visitor7" onclick="configureVisitor(7)"><image src="/sprites/trainers/none.png"/></td>
|
||||
<td id="visitor8" onclick="configureVisitor(8)"><image src="/sprites/trainers/none.png"/></td>
|
||||
<td id="visitor9" onclick="configureVisitor(9)"><image src="/sprites/trainers/none.png"/></td>
|
||||
<td id="visitor10" onclick="configureVisitor(10)"><image src="/sprites/trainers/none.png"/></td>
|
||||
<td id="visitor11" onclick="configureVisitor(11)"><image src="/sprites/trainers/none.png"/></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<!-- Item Configuration -->
|
||||
<label>Items (Max. 20)</label><br>
|
||||
<div>
|
||||
<table id="item-table" class="item-table">
|
||||
<tr>
|
||||
<td id="item0" onclick="configureItem(0)"><image src="/sprites/items/0.png"/></td>
|
||||
<td id="item1" onclick="configureItem(1)"><image src="/sprites/items/0.png"/></td>
|
||||
<td id="item2" onclick="configureItem(2)"><image src="/sprites/items/0.png"/></td>
|
||||
<td id="item3" onclick="configureItem(3)"><image src="/sprites/items/0.png"/></td>
|
||||
<td id="item4" onclick="configureItem(4)"><image src="/sprites/items/0.png"/></td>
|
||||
<td id="item5" onclick="configureItem(5)"><image src="/sprites/items/0.png"/></td>
|
||||
<td id="item6" onclick="configureItem(6)"><image src="/sprites/items/0.png"/></td>
|
||||
<td id="item7" onclick="configureItem(7)"><image src="/sprites/items/0.png"/></td>
|
||||
<td id="item8" onclick="configureItem(8)"><image src="/sprites/items/0.png"/></td>
|
||||
<td id="item9" onclick="configureItem(9)"><image src="/sprites/items/0.png"/></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td id="item10" onclick="configureItem(10)"><image src="/sprites/items/0.png"/></td>
|
||||
<td id="item11" onclick="configureItem(11)"><image src="/sprites/items/0.png"/></td>
|
||||
<td id="item12" onclick="configureItem(12)"><image src="/sprites/items/0.png"/></td>
|
||||
<td id="item13" onclick="configureItem(13)"><image src="/sprites/items/0.png"/></td>
|
||||
<td id="item14" onclick="configureItem(14)"><image src="/sprites/items/0.png"/></td>
|
||||
<td id="item15" onclick="configureItem(15)"><image src="/sprites/items/0.png"/></td>
|
||||
<td id="item16" onclick="configureItem(16)"><image src="/sprites/items/0.png"/></td>
|
||||
<td id="item17" onclick="configureItem(17)"><image src="/sprites/items/0.png"/></td>
|
||||
<td id="item18" onclick="configureItem(18)"><image src="/sprites/items/0.png"/></td>
|
||||
<td id="item19" onclick="configureItem(19)"><image src="/sprites/items/0.png"/></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<!-- Misc Configurations -->
|
||||
<div class="grid-container">
|
||||
<div>
|
||||
<label>C-Gear Skin | </label><a href="#" onclick="return previewSkin('cgear-skin', 'CGEAR')">Preview</a>
|
||||
<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=".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">
|
||||
<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=".png, .bmp, .jpg, .jpeg" onchange="uploadSkin('ZUKAN', this.files[0])" onclick="this.value = null;" hidden/>
|
||||
</div>
|
||||
<div>
|
||||
<label>Musical Show</label>
|
||||
<select id="musical">
|
||||
<option value="none">Do not change</option>
|
||||
<option disabled>──────────────────────</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label>Level Gain</label>
|
||||
<input id="level-gain-input" type="number" value="0" min="0" max="99"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<button id="save" class="big-button" onclick="postProfileData()">Save Profile</button>
|
||||
<button id="logout" class="big-button" onclick="postLogout()">Log Out</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Entree Forest Encounter Configuration Form -->
|
||||
<div id="configureEncounter" class="popup">
|
||||
<div class="content">
|
||||
<button class="close-button" onclick="closeEncounterForm()">X</button>
|
||||
<form id="encounter-form">
|
||||
<label for="encounter-form-species">Species ID | </label><a href="/dashboard/species.html" target="_blank">View list</a>
|
||||
<input id="encounter-form-species" name="species" type="number" value="1" min="1" max="493"/>
|
||||
<label for="encounter-form-move">Move ID</label>
|
||||
<input id="encounter-form-move" name="move" type="number" value="1" min="1" max="559"/>
|
||||
<label for="encounter-form-form">Forme Index</label>
|
||||
<input id="encounter-form-form" name="form" type="number" value="0" min="0" max="31"/>
|
||||
<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 id="encounter-config" class="popup">
|
||||
<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="configureItem" class="popup">
|
||||
<div class="content">
|
||||
<button class="close-button" onclick="closeEncounterForm()">X</button>
|
||||
<form id="item-form">
|
||||
<label for="item-form-id">Item ID | </label><a href="/dashboard/items.html" target="_blank">View list</a>
|
||||
<input id="item-form-id" name="id" type="number" value="1" min="1" max="626"/>
|
||||
<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 id="item-config" class="popup">
|
||||
<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="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>
|
||||
<script src="scripts/utility.js"></script>
|
||||
<script src="scripts/pokedata.js"></script>
|
||||
<script src="scripts/profile.js"></script>
|
||||
</html>
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
const AVAILABLE_GENERATION_V_POKEMON = new Array(
|
||||
505, 507, 510, 511, 513, 515, 519, 523, 525, 527, 529, 531, 533, 535, 538, 539, 542, 545, 546, 548,
|
||||
550, 553, 556, 558, 559, 561, 564, 569, 572, 575, 578, 580, 583, 587, 588, 594, 596, 600, 605, 607,
|
||||
610, 613, 616, 618, 619, 621, 622, 624, 626, 628, 630, 631, 632);
|
||||
|
||||
const ITEMS_TO_EXCLUDE = new Array(113, 114, 115, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 426, 427);
|
||||
|
||||
function fillTable(tableId, rowSize, elementCount, htmlAppender) {
|
||||
let tableHTML = "";
|
||||
let index = 0;
|
||||
|
||||
for(let value = 1; value <= elementCount; value++) {
|
||||
// Call the provided HTML appender to see what should be appended to the table
|
||||
let toAppend = htmlAppender(value);
|
||||
|
||||
// If there is nothing to append (that is, the function has decided this element should be skipped)
|
||||
// then continue to the next loop.
|
||||
if(!toAppend) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Open a new table row if it should
|
||||
if(index % rowSize == 0) {
|
||||
tableHTML += "<tr>";
|
||||
}
|
||||
|
||||
// Append the HTML data
|
||||
tableHTML += toAppend;
|
||||
|
||||
// Close the table row if it has reached the maximum number of elements
|
||||
if(index % rowSize == rowSize) {
|
||||
tableHTML += "<tr>";
|
||||
}
|
||||
|
||||
index++;
|
||||
}
|
||||
|
||||
// Set the inner HTML
|
||||
document.getElementById(tableId).innerHTML = tableHTML;
|
||||
}
|
||||
|
||||
function fillTableWithSpecies() {
|
||||
fillTable("species-table", 10, 649, (species) => {
|
||||
// Skip this element if species is from Generation V and cannot be downloaded
|
||||
if(species > 493 && !AVAILABLE_GENERATION_V_POKEMON.includes(species)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return "<td style='width:96px;height:96px;'><image src='/sprites/pokemon/normal/" + species + ".png'/><br>#" + species + "</td>";
|
||||
});
|
||||
}
|
||||
|
||||
function fillTableWithItems() {
|
||||
fillTable("item-table", 20, 638, (item) => {
|
||||
if(ITEMS_TO_EXCLUDE.includes(item)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return "<td style='width:48px;height:48px;'><image src='/sprites/items/" + item + ".png'/><br>#" + item + "</td>"
|
||||
});
|
||||
}
|
||||
@@ -1,20 +1,11 @@
|
||||
const ELEMENT_GSID_INPUT = document.getElementById("gsid");
|
||||
|
||||
function postLogin() {
|
||||
let loginData = {
|
||||
fetchData("POST", "/dashboard/login", new URLSearchParams({
|
||||
gsid: ELEMENT_GSID_INPUT.value
|
||||
}
|
||||
|
||||
fetch("/dashboard/login", {
|
||||
method: "POST",
|
||||
body: new URLSearchParams(loginData)
|
||||
}).then((response) => {
|
||||
return response.json();
|
||||
}).then((response) => {
|
||||
console.log(response);
|
||||
|
||||
if(response.error) {
|
||||
alert(response.message);
|
||||
})).then((response) => {
|
||||
if(response.error) {
|
||||
window.alert(response.message);
|
||||
} else {
|
||||
window.location.href = "/dashboard/profile.html";
|
||||
}
|
||||
|
||||
2590
src/main/resources/dashboard/scripts/pokedata.js
vendored
Normal file
2590
src/main/resources/dashboard/scripts/pokedata.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,8 +1,9 @@
|
||||
// HTML document elements
|
||||
const ELEMENT_GAME_SUMMARY = document.getElementById("game-summary");
|
||||
|
||||
// Dreamer elements
|
||||
const ELEMENT_DREAMER_SPRITE = document.getElementById("dreamer-sprite");
|
||||
const ELEMENT_DREAMER_SPECIES = document.getElementById("dreamer-species");
|
||||
const ELEMENT_DREAMER_ABILITY = document.getElementById("dreamer-ability");
|
||||
const ELEMENT_DREAMER_NATURE = document.getElementById("dreamer-nature");
|
||||
const ELEMENT_DREAMER_NAME = document.getElementById("dreamer-name");
|
||||
const ELEMENT_DREAMER_GENDER = document.getElementById("dreamer-gender");
|
||||
@@ -10,128 +11,256 @@ const ELEMENT_DREAMER_TRAINER = document.getElementById("dreamer-trainer");
|
||||
const ELEMENT_DREAMER_TRAINER_ID = document.getElementById("dreamer-trainer-id");
|
||||
const ELEMENT_DREAMER_LEVEL = document.getElementById("dreamer-level");
|
||||
|
||||
// Encounter form elements
|
||||
const ELEMENT_ENCOUNTER_CONFIG = document.getElementById("encounter-config");
|
||||
const ELEMENT_ENCOUNTER_SPECIES = document.getElementById("encounter-form-species");
|
||||
const ELEMENT_ENCOUNTER_MOVE = document.getElementById("encounter-form-move");
|
||||
const ELEMENT_ENCOUNTER_FORM = document.getElementById("encounter-form-form");
|
||||
const ELEMENT_ENCOUNTER_GENDER = document.getElementById("encounter-form-gender");
|
||||
const ELEMENT_ENCOUNTER_ANIMATION = document.getElementById("encounter-form-animation");
|
||||
|
||||
// Item form elements
|
||||
const ELEMENT_ITEM_CONFIG = document.getElementById("item-config");
|
||||
const ELEMENT_ITEM_ID = document.getElementById("item-form-id");
|
||||
const ELEMENT_ITEM_QUANTITY = document.getElementById("item-form-quantity");
|
||||
|
||||
// Misc input elements
|
||||
const ELEMENT_VISITOR_CONFIG = document.getElementById("visitor-config");
|
||||
const ELEMENT_VISITOR_NAME = document.getElementById("visitor-form-name");
|
||||
const ELEMENT_VISITOR_TYPE = document.getElementById("visitor-form-type");
|
||||
const ELEMENT_VISITOR_SHOP_TYPE = document.getElementById("visitor-form-shop-type");
|
||||
const ELEMENT_VISITOR_GAME = document.getElementById("visitor-form-game");
|
||||
const ELEMENT_VISITOR_REGION = document.getElementById("visitor-form-region");
|
||||
const ELEMENT_VISITOR_SUBREGION = document.getElementById("visitor-form-subregion");
|
||||
const ELEMENT_VISITOR_PERSONALITY = document.getElementById("visitor-form-personality");
|
||||
const ELEMENT_VISITOR_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");
|
||||
const ELEMENT_LEVEL_GAIN_INPUT = document.getElementById("level-gain-input");
|
||||
|
||||
// Create event listeners
|
||||
ELEMENT_ENCOUNTER_SPECIES.addEventListener("change", clampValue);
|
||||
ELEMENT_ENCOUNTER_MOVE.addEventListener("change", clampValue);
|
||||
ELEMENT_ITEM_ID.addEventListener("change", clampValue);
|
||||
ELEMENT_ITEM_QUANTITY.addEventListener("change", clampValue);
|
||||
ELEMENT_LEVEL_GAIN_INPUT.addEventListener("change", clampValue);
|
||||
|
||||
function clampValue() {
|
||||
let value = parseInt(this.value);
|
||||
|
||||
if(value < this.min) {
|
||||
this.value = this.min;
|
||||
} else if(value > this.max) {
|
||||
console.log(value);
|
||||
this.value = this.max;
|
||||
}
|
||||
}
|
||||
|
||||
// Other constant stuff
|
||||
const AVAILABLE_GENERATION_V_POKEMON = new Array(
|
||||
505, 507, 510, 511, 513, 515, 519, 523, 525, 527, 529, 531, 533, 535, 538, 539, 542, 545, 546, 548,
|
||||
550, 553, 556, 558, 559, 561, 564, 569, 572, 575, 578, 580, 583, 587, 588, 594, 596, 600, 605, 607,
|
||||
610, 613, 616, 618, 619, 621, 622, 624, 626, 628, 630, 631, 632); // Defining this 3 times is a brilliant idea.
|
||||
|
||||
|
||||
// Local variables
|
||||
var encounterTableIndex = -1;
|
||||
var itemTableIndex = -1;
|
||||
var visitorTableIndex = -1;
|
||||
var profile = {
|
||||
encounters: [],
|
||||
items: []
|
||||
items: [],
|
||||
visitors: []
|
||||
};
|
||||
|
||||
(async function() {
|
||||
// Create event listeners
|
||||
clampOnChange(ELEMENT_ITEM_QUANTITY);
|
||||
clampOnChange(ELEMENT_LEVEL_GAIN_INPUT);
|
||||
clampOnChange(ELEMENT_VISITOR_PERSONALITY);
|
||||
|
||||
// Fetch profile data
|
||||
await fetchData("GET", "/dashboard/profile").then((response) => {
|
||||
// Update game summary
|
||||
profile.gameVersion = response.gameVersion;
|
||||
ELEMENT_GAME_SUMMARY.innerHTML = "Game Card in use: " + profile.gameVersion;
|
||||
|
||||
// Update dreamer summary
|
||||
if(response.dreamerInfo) {
|
||||
let dreamerInfo = response.dreamerInfo;
|
||||
ELEMENT_DREAMER_SPRITE.innerHTML = "<image src='" + response.dreamerSprite + "'/>";
|
||||
ELEMENT_DREAMER_SPECIES.innerHTML = SPECIES_MAP[dreamerInfo.species] ? (SPECIES_MAP[dreamerInfo.species].name) : "N/A";
|
||||
ELEMENT_DREAMER_ABILITY.innerHTML = ABILITY_MAP[dreamerInfo.ability] ? ABILITY_MAP[dreamerInfo.ability].name : "N/A";
|
||||
ELEMENT_DREAMER_NATURE.innerHTML = stringToWord(dreamerInfo.nature);
|
||||
ELEMENT_DREAMER_NAME.innerHTML = dreamerInfo.nickname;
|
||||
ELEMENT_DREAMER_GENDER.innerHTML = stringToWord(dreamerInfo.gender);
|
||||
ELEMENT_DREAMER_TRAINER.innerHTML = dreamerInfo.trainerName;
|
||||
ELEMENT_DREAMER_TRAINER_ID.innerHTML = ("0000" + dreamerInfo.trainerId).slice(-5);
|
||||
ELEMENT_DREAMER_LEVEL.innerHTML = dreamerInfo.level;
|
||||
}
|
||||
|
||||
// Update encounter table
|
||||
if(response.encounters){
|
||||
profile.encounters = response.encounters;
|
||||
updateEncounterTable(0, 10);
|
||||
}
|
||||
|
||||
// Update item table
|
||||
if(response.items){
|
||||
profile.items = response.items;
|
||||
updateItemTable(0, 20);
|
||||
}
|
||||
|
||||
// Update Join Avenue visitor table
|
||||
if(response.avenueVisitors) {
|
||||
profile.visitors = response.avenueVisitors;
|
||||
updateVisitorTable(0, 12);
|
||||
}
|
||||
|
||||
// Update selected DLC
|
||||
profile.cgearSkin = response.cgearSkin ? response.cgearSkin : "none";
|
||||
profile.dexSkin = response.dexSkin ? response.dexSkin : "none";
|
||||
profile.musical = response.musical ? response.musical : "none";
|
||||
|
||||
if(response.customCGearSkin) {
|
||||
profile.customCGearSkin = response.customCGearSkin;
|
||||
ELEMENT_CGEAR_SKIN_INPUT.add(new Option(response.customCGearSkin, "custom"), 1);
|
||||
}
|
||||
|
||||
if(response.customDexSkin) {
|
||||
profile.customDexSkin = response.customDexSkin;
|
||||
ELEMENT_DEX_SKIN_INPUT.add(new Option(response.customDexSkin, "custom"), 1);
|
||||
}
|
||||
|
||||
fetchDlcData();
|
||||
ELEMENT_LEVEL_GAIN_INPUT.value = response.levelsGained;
|
||||
|
||||
// Show Join Avenue visitor table if Black 2 or White 2
|
||||
if(isVersion2()) {
|
||||
document.getElementById("visitor-table-container").style.display = "block";
|
||||
}
|
||||
|
||||
// Show div
|
||||
document.getElementById("main-container").style.display = "flex";
|
||||
});
|
||||
|
||||
// Sort data lists alphabetically
|
||||
let sortedSpecies = [...SPECIES_LIST].sort((a, b) => a.name.localeCompare(b.name));
|
||||
let sortedMoves = [...MOVE_LIST].sort((a, b) => a.name.localeCompare(b.name));
|
||||
let sortedItems = [...ITEM_LIST].sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
// Add species data
|
||||
for(let i in sortedSpecies) {
|
||||
let species = sortedSpecies[i];
|
||||
ELEMENT_VISITOR_DREAMER.options[ELEMENT_VISITOR_DREAMER.options.length] = new Option(species.name, species.id);
|
||||
|
||||
if(species.downloadable && (isVersion2() || species.id <= 493)) {
|
||||
ELEMENT_ENCOUNTER_SPECIES.options[ELEMENT_ENCOUNTER_SPECIES.options.length] = new Option(species.name, species.id);
|
||||
}
|
||||
}
|
||||
|
||||
// Add move data
|
||||
for(let i in sortedMoves) {
|
||||
let move = sortedMoves[i];
|
||||
ELEMENT_ENCOUNTER_MOVE.options[ELEMENT_ENCOUNTER_MOVE.options.length] = new Option(move.name, move.id);
|
||||
}
|
||||
|
||||
// Add item data
|
||||
for(let i in sortedItems) {
|
||||
let item = sortedItems[i];
|
||||
|
||||
if(isVersion2() || item.id <= 626) {
|
||||
ELEMENT_ITEM_ID.options[ELEMENT_ITEM_ID.options.length] = new Option(item.name, item.id);
|
||||
}
|
||||
}
|
||||
|
||||
// Add region data (already sorted alphabetically)
|
||||
for(let i in REGION_LIST) {
|
||||
let region = REGION_LIST[i];
|
||||
ELEMENT_VISITOR_REGION.options[ELEMENT_VISITOR_REGION.options.length] = new Option(region.name, region.id);
|
||||
}
|
||||
|
||||
// Event listener for changing the form & gender selector contents when species changes
|
||||
ELEMENT_ENCOUNTER_SPECIES.addEventListener("change", function() {
|
||||
ELEMENT_ENCOUNTER_FORM.value = updateEncounterFormOptions();
|
||||
ELEMENT_ENCOUNTER_GENDER.value = updateEncounterGenderOptions();
|
||||
});
|
||||
|
||||
// Same thing, but for Join Avenue visitor region & subregion
|
||||
ELEMENT_VISITOR_REGION.addEventListener("change", function() {
|
||||
ELEMENT_VISITOR_SUBREGION.value = updateVisitorSubregionOptions();
|
||||
});
|
||||
})();
|
||||
|
||||
/**
|
||||
* Encounter configuration stuff
|
||||
*/
|
||||
|
||||
function updateEncounterFormOptions() {
|
||||
clearSelectOptions(ELEMENT_ENCOUNTER_FORM);
|
||||
let species = SPECIES_MAP[ELEMENT_ENCOUNTER_SPECIES.value];
|
||||
|
||||
// Update special form options
|
||||
if(species.forms) {
|
||||
for(let i in species.forms) {
|
||||
ELEMENT_ENCOUNTER_FORM.options[ELEMENT_ENCOUNTER_FORM.options.length] = new Option(species.forms[i], i);
|
||||
}
|
||||
} else {
|
||||
ELEMENT_ENCOUNTER_FORM.options[ELEMENT_ENCOUNTER_FORM.options.length] = new Option("N/A", 0);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
function updateEncounterGenderOptions() {
|
||||
clearSelectOptions(ELEMENT_ENCOUNTER_GENDER);
|
||||
let species = SPECIES_MAP[ELEMENT_ENCOUNTER_SPECIES.value];
|
||||
|
||||
// Update gender options
|
||||
if(species.gender) {
|
||||
console.log(species.gender);
|
||||
switch(species.gender) {
|
||||
case "male":
|
||||
ELEMENT_ENCOUNTER_GENDER.options[0] = new Option("Male", "MALE");
|
||||
return "MALE";
|
||||
case "female":
|
||||
ELEMENT_ENCOUNTER_GENDER.options[0] = new Option("Female", "FEMALE");
|
||||
return "FEMALE";
|
||||
case "unknown":
|
||||
ELEMENT_ENCOUNTER_GENDER.options[0] = new Option("N/A", "GENDERLESS");
|
||||
return "GENDERLESS";
|
||||
}
|
||||
}
|
||||
|
||||
ELEMENT_ENCOUNTER_GENDER.options[0] = new Option("Male", "MALE");
|
||||
ELEMENT_ENCOUNTER_GENDER.options[1] = new Option("Female", "FEMALE");
|
||||
ELEMENT_ENCOUNTER_GENDER.options[2] = new Option("Random", "GENDERLESS");
|
||||
return "GENDERLESS";
|
||||
}
|
||||
|
||||
function configureEncounter(index) {
|
||||
encounterTableIndex = Math.min(10, Math.min(index, profile.encounters.length));
|
||||
|
||||
// Load existing settings
|
||||
let encounter = profile.encounters[encounterTableIndex];
|
||||
ELEMENT_ENCOUNTER_SPECIES.value = encounter ? encounter.species : 1;
|
||||
ELEMENT_ENCOUNTER_MOVE.value = encounter ? encounter.move : 1;
|
||||
ELEMENT_ENCOUNTER_FORM.value = encounter ? encounter.form : 0;
|
||||
ELEMENT_ENCOUNTER_GENDER.value = encounter ? encounter.gender : "GENDERLESS";
|
||||
ELEMENT_ENCOUNTER_ANIMATION.value = encounter ? encounter.animation : "WALK_AROUND";
|
||||
ELEMENT_ENCOUNTER_SPECIES.value = encounter ? encounter.species : 493;
|
||||
let form = updateEncounterFormOptions();
|
||||
let gender = updateEncounterGenderOptions();
|
||||
ELEMENT_ENCOUNTER_MOVE.value = encounter ? encounter.move : 0;
|
||||
ELEMENT_ENCOUNTER_FORM.value = encounter ? encounter.form : form;
|
||||
ELEMENT_ENCOUNTER_GENDER.value = encounter ? encounter.gender : gender;
|
||||
ELEMENT_ENCOUNTER_ANIMATION.value = encounter ? encounter.animation : "LOOK_AROUND";
|
||||
ELEMENT_ENCOUNTER_CONFIG.style.display = "block";
|
||||
}
|
||||
|
||||
function saveEncounter() {
|
||||
if(encounterTableIndex < 0) {
|
||||
closeEncounterForm();
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if this species can be downloaded
|
||||
let species = parseInt(ELEMENT_ENCOUNTER_SPECIES.value);
|
||||
|
||||
if(species > 493 && !AVAILABLE_GENERATION_V_POKEMON.includes(species)) {
|
||||
alert("This Pokémon species cannot be downloaded. Click 'View list' in the encounter form to view a list of available Pokémon.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Create encounter data
|
||||
let encounterData = {
|
||||
species: species,
|
||||
profile.encounters[encounterTableIndex] = {
|
||||
species: ELEMENT_ENCOUNTER_SPECIES.value,
|
||||
move: ELEMENT_ENCOUNTER_MOVE.value,
|
||||
form: ELEMENT_ENCOUNTER_FORM.value,
|
||||
gender: ELEMENT_ENCOUNTER_GENDER.value,
|
||||
animation: ELEMENT_ENCOUNTER_ANIMATION.value
|
||||
}
|
||||
|
||||
// Set form to highest form available if it too great
|
||||
let maxForm = 0;
|
||||
|
||||
switch(species) {
|
||||
case 201: maxForm = 27; break; // Unown
|
||||
case 386: maxForm = 3; break; // Deoxys
|
||||
case 412:
|
||||
case 413: maxForm = 2; break; // Burmy & Wormadam
|
||||
case 422:
|
||||
case 423:
|
||||
case 487: maxForm = 1; break; // Shellos, Gastrodon & Giratina
|
||||
case 479: maxForm = 5; break; // Rotom
|
||||
case 493: maxForm = 16; break; // Arceus
|
||||
case 550: maxForm = 1; break; // Basculin
|
||||
}
|
||||
|
||||
if(encounterData.form > maxForm) {
|
||||
encounterData.form = maxForm;
|
||||
}
|
||||
|
||||
profile.encounters[encounterTableIndex] = encounterData;
|
||||
};
|
||||
updateEncounterCell(encounterTableIndex);
|
||||
closeEncounterForm();
|
||||
}
|
||||
|
||||
function removeEncounter() {
|
||||
if(encounterTableIndex < 0) {
|
||||
closeEncounterForm();
|
||||
return;
|
||||
}
|
||||
|
||||
let oldLength = profile.encounters.length;
|
||||
profile.encounters.splice(encounterTableIndex, 1);
|
||||
|
||||
for(let i = encounterTableIndex; i < oldLength; i++) {
|
||||
updateEncounterTable(encounterTableIndex, oldLength);
|
||||
closeEncounterForm();
|
||||
}
|
||||
|
||||
function updateEncounterTable(startIndex, endIndex) {
|
||||
for(let i = startIndex; i < endIndex; i++) {
|
||||
updateEncounterCell(i);
|
||||
}
|
||||
|
||||
closeEncounterForm();
|
||||
}
|
||||
|
||||
function updateEncounterCell(index) {
|
||||
@@ -144,11 +273,19 @@ function updateEncounterCell(index) {
|
||||
spriteImage = spriteBase + encounterData.species + ".png";
|
||||
|
||||
if(encounterData.form > 0) {
|
||||
// Use unique form sprite if it exists
|
||||
let formSpriteImage = spriteBase + encounterData.species + "-" + encounterData.form + ".png";
|
||||
|
||||
if(checkURL(formSpriteImage)){
|
||||
spriteImage = formSpriteImage;
|
||||
}
|
||||
} else if(encounterData.gender == "FEMALE") {
|
||||
// Otherwise, use female sprite if it exists
|
||||
let femaleSpriteImage = spriteBase + "female/" + encounterData.species + ".png";
|
||||
|
||||
if(checkURL(femaleSpriteImage)){
|
||||
spriteImage = femaleSpriteImage;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -157,46 +294,169 @@ function updateEncounterCell(index) {
|
||||
|
||||
function closeEncounterForm() {
|
||||
encounterTableIndex = -1;
|
||||
window.location.href = "#";
|
||||
ELEMENT_ENCOUNTER_CONFIG.style.display = "none";
|
||||
}
|
||||
|
||||
/**
|
||||
* Join Avenue visitor configuration stuff
|
||||
*/
|
||||
|
||||
function updateVisitorSubregionOptions() {
|
||||
clearSelectOptions(ELEMENT_VISITOR_SUBREGION);
|
||||
let region = REGION_MAP[ELEMENT_VISITOR_REGION.value];
|
||||
|
||||
// Update subregion options
|
||||
if(region.subregions) {
|
||||
let sortedSubregions = [...region.subregions].sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
for(let i in sortedSubregions) {
|
||||
let subregion = sortedSubregions[i];
|
||||
ELEMENT_VISITOR_SUBREGION.options[ELEMENT_VISITOR_SUBREGION.options.length] = new Option(subregion.name, subregion.id);
|
||||
}
|
||||
|
||||
return 1;
|
||||
} else {
|
||||
ELEMENT_VISITOR_SUBREGION.options[ELEMENT_VISITOR_SUBREGION.options.length] = new Option("N/A", 0);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
function configureVisitor(index) {
|
||||
visitorTableIndex = Math.min(12, Math.min(index, profile.visitors.length));
|
||||
let visitor = profile.visitors[visitorTableIndex];
|
||||
ELEMENT_VISITOR_NAME.value = visitor ? visitor.name : "";
|
||||
ELEMENT_VISITOR_TYPE.value = visitor ? visitor.type : "ACE_TRAINER_MALE";
|
||||
ELEMENT_VISITOR_SHOP_TYPE.value = visitor ? visitor.shopType : "RAFFLE";
|
||||
ELEMENT_VISITOR_GAME.value = visitor ? visitor.gameVersion : "BLACK_ENGLISH";
|
||||
ELEMENT_VISITOR_REGION.value = visitor ? visitor.countryCode : 1;
|
||||
updateVisitorSubregionOptions();
|
||||
ELEMENT_VISITOR_SUBREGION.value = visitor ? visitor.stateProvinceCode : 0;
|
||||
ELEMENT_VISITOR_PERSONALITY.value = visitor ? visitor.personality : 0;
|
||||
ELEMENT_VISITOR_DREAMER.value = visitor ? visitor.dreamerSpecies : 1;
|
||||
ELEMENT_VISITOR_CONFIG.style.display = "block";
|
||||
}
|
||||
|
||||
function saveVisitor() {
|
||||
if(visitorTableIndex < 0) {
|
||||
closeVisitorForm();
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if name is empty
|
||||
if(ELEMENT_VISITOR_NAME.value == "") {
|
||||
alert("Please enter a name for this visitor.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if name is duplicate
|
||||
for(i in profile.visitors) {
|
||||
if(i != visitorTableIndex) {
|
||||
let visitor = profile.visitors[i];
|
||||
|
||||
if(visitor.name == ELEMENT_VISITOR_NAME.value) {
|
||||
alert("A visitor with this name already exists!")
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
profile.visitors[visitorTableIndex] = {
|
||||
name: ELEMENT_VISITOR_NAME.value,
|
||||
type: ELEMENT_VISITOR_TYPE.value,
|
||||
shopType: ELEMENT_VISITOR_SHOP_TYPE.value,
|
||||
gameVersion: ELEMENT_VISITOR_GAME.value,
|
||||
countryCode: ELEMENT_VISITOR_REGION.value,
|
||||
stateProvinceCode: ELEMENT_VISITOR_SUBREGION.value,
|
||||
personality: ELEMENT_VISITOR_PERSONALITY.value,
|
||||
dreamerSpecies: ELEMENT_VISITOR_DREAMER.value
|
||||
};
|
||||
updateVisitorCell(visitorTableIndex);
|
||||
closeVisitorForm();
|
||||
}
|
||||
|
||||
function removeVisitor() {
|
||||
if(visitorTableIndex < 0) {
|
||||
closeVisitorForm();
|
||||
return;
|
||||
}
|
||||
|
||||
let oldLength = profile.visitors.length;
|
||||
profile.visitors.splice(visitorTableIndex, 1);
|
||||
updateVisitorTable(visitorTableIndex, oldLength);
|
||||
closeVisitorForm();
|
||||
}
|
||||
|
||||
function updateVisitorTable(startIndex, endIndex) {
|
||||
for(let i = startIndex; i < endIndex; i++) {
|
||||
updateVisitorCell(i);
|
||||
}
|
||||
}
|
||||
|
||||
function updateVisitorCell(index) {
|
||||
let cell = document.getElementById("visitor" + index);
|
||||
let visitor = profile.visitors[index];
|
||||
let spriteBase = "/sprites/trainers/";
|
||||
let spriteImage = spriteBase + "none.png";
|
||||
|
||||
if(visitor) {
|
||||
let newSpriteImage = spriteBase + visitor.type.toLowerCase() + ".png";
|
||||
|
||||
if(checkURL(newSpriteImage)){
|
||||
spriteImage = newSpriteImage;
|
||||
}
|
||||
}
|
||||
|
||||
cell.innerHTML = "<img src='" + spriteImage + "'/>";
|
||||
}
|
||||
|
||||
function closeVisitorForm() {
|
||||
visitorTableIndex = -1;
|
||||
ELEMENT_VISITOR_CONFIG.style.display = "none";
|
||||
}
|
||||
|
||||
/**
|
||||
* Item configuration stuff
|
||||
*/
|
||||
|
||||
function configureItem(index) {
|
||||
itemTableIndex = Math.min(20, Math.min(index, profile.items.length));
|
||||
|
||||
// Loadg existing settings
|
||||
let item = profile.items[itemTableIndex];
|
||||
ELEMENT_ITEM_ID.value = item ? item.id : 1;
|
||||
ELEMENT_ITEM_QUANTITY.value = item ? item.quantity : 1;
|
||||
ELEMENT_ITEM_CONFIG.style.display = "block";
|
||||
}
|
||||
|
||||
function saveItem() {
|
||||
if(itemTableIndex < 0) {
|
||||
closeItemForm();
|
||||
return;
|
||||
}
|
||||
|
||||
let itemData = {
|
||||
profile.items[itemTableIndex] = {
|
||||
id: ELEMENT_ITEM_ID.value,
|
||||
quantity: ELEMENT_ITEM_QUANTITY.value
|
||||
}
|
||||
|
||||
profile.items[itemTableIndex] = itemData;
|
||||
};
|
||||
updateItemCell(itemTableIndex);
|
||||
closeItemForm();
|
||||
}
|
||||
|
||||
function removeItem() {
|
||||
if(itemTableIndex < 0) {
|
||||
closeItemForm();
|
||||
return;
|
||||
}
|
||||
|
||||
let oldLength = profile.items.length;
|
||||
profile.items.splice(itemTableIndex, 1);
|
||||
|
||||
for(let i = itemTableIndex; i < oldLength; i++) {
|
||||
updateItemTable(itemTableIndex, oldLength);
|
||||
closeItemForm();
|
||||
}
|
||||
|
||||
function updateItemTable(startIndex, endIndex) {
|
||||
for(let i = startIndex; i < endIndex; i++) {
|
||||
updateItemCell(i);
|
||||
}
|
||||
|
||||
closeItemForm();
|
||||
}
|
||||
|
||||
function updateItemCell(index) {
|
||||
@@ -220,171 +480,134 @@ function updateItemCell(index) {
|
||||
|
||||
function closeItemForm() {
|
||||
itemTableIndex = -1;
|
||||
window.location.href = "#";
|
||||
ELEMENT_ITEM_CONFIG.style.display = "none";
|
||||
}
|
||||
|
||||
async function fetchData(path) {
|
||||
return fetchData(path, "GET", null);
|
||||
/**
|
||||
* Miscellaneous stuff
|
||||
*/
|
||||
|
||||
function openFileChooser(inputDocumentId) {
|
||||
document.getElementById(inputDocumentId).click();
|
||||
}
|
||||
|
||||
async function fetchData(path, method, body) {
|
||||
let response = await fetch(path, {
|
||||
method: method,
|
||||
body: body
|
||||
function uploadSkin(type, file) {
|
||||
fetchData("POST", "/dashboard/uploadskin?type=" + (type == "CGEAR" && isVersion2() ? "CGEAR2" : type)
|
||||
+ "&filename=" + file.name, file).then((response) => {
|
||||
if(!response) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Display message if there is one
|
||||
if(response.message) {
|
||||
window.alert(response.message);
|
||||
}
|
||||
|
||||
// Return if error
|
||||
if(response.error) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Update selector option
|
||||
let selectElement = type == "CGEAR" ? ELEMENT_CGEAR_SKIN_INPUT : ELEMENT_DEX_SKIN_INPUT;
|
||||
|
||||
// Add option for custom skin if one does not already exist
|
||||
// Otherwise, update the existing one
|
||||
if(selectElement.options[1].value == "custom") {
|
||||
selectElement.options[1].innerHTML = file.name;
|
||||
} else{
|
||||
selectElement.add(new Option(file.name, "custom"), 1);
|
||||
}
|
||||
|
||||
selectElement.value = "custom";
|
||||
});
|
||||
}
|
||||
|
||||
function previewSkin(inputElementId, type) {
|
||||
let value = document.getElementById(inputElementId).value;
|
||||
|
||||
// Return to login page if unauthorized
|
||||
if(response.status == 401) {
|
||||
window.location.href = "/dashboard/login.html";
|
||||
return;
|
||||
if(value == "none") {
|
||||
window.alert("Please select a skin to preview it.");
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
return await response.json();
|
||||
} catch(error) {
|
||||
window.alert(error);
|
||||
if(type == "CGEAR" && isVersion2()) {
|
||||
type = "CGEAR2";
|
||||
}
|
||||
|
||||
return null;
|
||||
let revisionParam = value == "custom" ? "&revision=" + new Date().getTime() : ""; // Hack to work around the browser cache
|
||||
ELEMENT_SKIN_PREVIEW_IMAGE.src = "/dashboard/previewskin?type=" + type + "&name=" + value + revisionParam;
|
||||
ELEMENT_SKIN_PREVIEW.style.display = "block";
|
||||
return false;
|
||||
}
|
||||
|
||||
function setSkinPreviewResolution(width, height) {
|
||||
ELEMENT_SKIN_PREVIEW_IMAGE.width = width;
|
||||
ELEMENT_SKIN_PREVIEW_IMAGE.height = height;
|
||||
}
|
||||
|
||||
function closeSkinPreview() {
|
||||
ELEMENT_SKIN_PREVIEW.style.display = "none";
|
||||
}
|
||||
|
||||
function fetchDlcData() {
|
||||
let cgearType = profile.gameVersion.includes("2") ? "CGEAR2" : "CGEAR"; // Not a good way to do this!
|
||||
|
||||
// Fetch CGear skins
|
||||
fetchData("/dashboard/dlc?type=" + cgearType).then((response) => {
|
||||
addValuesToComboBox(ELEMENT_CGEAR_SKIN_INPUT, response);
|
||||
ELEMENT_CGEAR_SKIN_INPUT.value = profile.cgearSkin;
|
||||
// Fetch C-Gear skins
|
||||
fetchData("GET", "/dashboard/dlc?type=" + (isVersion2() ? "CGEAR2" : "CGEAR")).then((response) => {
|
||||
if(response) {
|
||||
addDlcNames(ELEMENT_CGEAR_SKIN_INPUT, response);
|
||||
ELEMENT_CGEAR_SKIN_INPUT.value = profile.cgearSkin == "custom" && profile.customCGearSkin ? "custom"
|
||||
: response.includes(profile.cgearSkin) ? profile.cgearSkin : "none";
|
||||
}
|
||||
});
|
||||
|
||||
// Fetch Dex skins
|
||||
fetchData("/dashboard/dlc?type=ZUKAN").then((response) => {
|
||||
addValuesToComboBox(ELEMENT_DEX_SKIN_INPUT, response);
|
||||
ELEMENT_DEX_SKIN_INPUT.value = profile.dexSkin;
|
||||
fetchData("GET", "/dashboard/dlc?type=ZUKAN").then((response) => {
|
||||
if(response) {
|
||||
addDlcNames(ELEMENT_DEX_SKIN_INPUT, response);
|
||||
ELEMENT_DEX_SKIN_INPUT.value = profile.dexSkin == "custom" && profile.customDexSkin ? "custom"
|
||||
: response.includes(profile.dexSkin) ? profile.dexSkin : "none";
|
||||
}
|
||||
});
|
||||
|
||||
// Fetch musicals
|
||||
fetchData("/dashboard/dlc?type=MUSICAL").then((response) => {
|
||||
addValuesToComboBox(ELEMENT_MUSICAL_INPUT, response);
|
||||
ELEMENT_MUSICAL_INPUT.value = profile.musical;
|
||||
fetchData("GET", "/dashboard/dlc?type=MUSICAL").then((response) => {
|
||||
if(response) {
|
||||
addDlcNames(ELEMENT_MUSICAL_INPUT, response);
|
||||
ELEMENT_MUSICAL_INPUT.value = response.includes(profile.musical) ? profile.musical : "none";
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// TODO
|
||||
function fetchProfileData() {
|
||||
fetchData("/dashboard/profile").then((response) => {
|
||||
let gameVersion = response["gameVersion"];
|
||||
let dreamerSprite = response["dreamerSprite"];
|
||||
let dreamerInfo = response["dreamerInfo"];
|
||||
let encounters = response["encounters"];
|
||||
let items = response["items"];
|
||||
let cgearSkin = response["cgearSkin"];
|
||||
let dexSkin = response["dexSkin"];
|
||||
let musical = response["musical"];
|
||||
let levelsGained = response["levelsGained"];
|
||||
|
||||
// Update game summary
|
||||
profile.gameVersion = gameVersion;
|
||||
ELEMENT_GAME_SUMMARY.innerHTML = "Game Card in use: " + gameVersion;
|
||||
|
||||
// Still don't like this!
|
||||
if(gameVersion.includes("2")) {
|
||||
ELEMENT_ENCOUNTER_SPECIES.max = 649;
|
||||
ELEMENT_ITEM_ID.max = 638;
|
||||
}
|
||||
|
||||
// Update dreamer summary
|
||||
if(dreamerInfo) {
|
||||
let species = dreamerInfo["species"];
|
||||
let nature = dreamerInfo["nature"];
|
||||
let nickname = dreamerInfo["nickname"];
|
||||
let gender = dreamerInfo["gender"];
|
||||
let trainerName = dreamerInfo["trainerName"];
|
||||
let trainerId = dreamerInfo["trainerId"];
|
||||
let level = dreamerInfo["level"];
|
||||
|
||||
// Set element values
|
||||
ELEMENT_DREAMER_SPRITE.innerHTML = "<image src='" + dreamerSprite + "'/>";
|
||||
ELEMENT_DREAMER_SPECIES.innerHTML = "#" + species;
|
||||
ELEMENT_DREAMER_NATURE.innerHTML = stringToWord(nature);
|
||||
ELEMENT_DREAMER_NAME.innerHTML = nickname;
|
||||
ELEMENT_DREAMER_GENDER.innerHTML = stringToWord(gender);
|
||||
ELEMENT_DREAMER_TRAINER.innerHTML = trainerName;
|
||||
ELEMENT_DREAMER_TRAINER_ID.innerHTML = trainerId;
|
||||
ELEMENT_DREAMER_LEVEL.innerHTML = level;
|
||||
}
|
||||
|
||||
// Update encounter table
|
||||
if(encounters){
|
||||
profile.encounters = encounters;
|
||||
|
||||
for(let i = 0; i < 10; i++) {
|
||||
updateEncounterCell(i);
|
||||
}
|
||||
}
|
||||
|
||||
// Update item table
|
||||
if(items){
|
||||
profile.items = items;
|
||||
|
||||
for(let i = 0; i < 20; i++) {
|
||||
updateItemCell(i);
|
||||
}
|
||||
}
|
||||
|
||||
// Update selected DLC
|
||||
profile.cgearSkin = cgearSkin ? cgearSkin : "none";
|
||||
profile.dexSkin = dexSkin ? dexSkin : "none";
|
||||
profile.musical = musical ? musical : "none";
|
||||
fetchDlcData();
|
||||
|
||||
// Update level gain
|
||||
ELEMENT_LEVEL_GAIN_INPUT.value = levelsGained;
|
||||
|
||||
// Show div
|
||||
document.getElementById("main-container").style.display = "grid";
|
||||
});
|
||||
function addDlcNames(selectElement, names) {
|
||||
for(let i in names) {
|
||||
let name = names[i];
|
||||
selectElement.options[selectElement.options.length] = new Option(name.replace(/\.[^/.]+$/, ""), name);
|
||||
}
|
||||
}
|
||||
|
||||
function postProfileData() {
|
||||
// Construct body
|
||||
let profileData = {
|
||||
fetchData("POST", "/dashboard/profile", JSON.stringify({
|
||||
encounters: profile.encounters,
|
||||
items: profile.items,
|
||||
avenueVisitors: profile.visitors,
|
||||
cgearSkin: ELEMENT_CGEAR_SKIN_INPUT.value,
|
||||
dexSkin: ELEMENT_DEX_SKIN_INPUT.value,
|
||||
musical: ELEMENT_MUSICAL_INPUT.value,
|
||||
gainedLevels: ELEMENT_LEVEL_GAIN_INPUT.value
|
||||
}
|
||||
|
||||
// Send data
|
||||
fetchData("/dashboard/profile", "POST", JSON.stringify(profileData)).then((response) => {
|
||||
alert(response.message);
|
||||
})).then((response) => {
|
||||
if(response) {
|
||||
alert(response.message);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function postLogout() {
|
||||
fetchData("/dashboard/logout", "POST", null).then((response) => {
|
||||
fetchData("POST", "/dashboard/logout").then((response) => {
|
||||
// Assume it succeeded
|
||||
window.location.href = "/dashboard/login.html";
|
||||
});
|
||||
}
|
||||
|
||||
// TODO bad
|
||||
function checkURL(url) {
|
||||
var request = new XMLHttpRequest();
|
||||
request.open('HEAD', url, false);
|
||||
request.send();
|
||||
return request.status == 200;
|
||||
}
|
||||
|
||||
function stringToWord(string) {
|
||||
return string.charAt(0).toUpperCase() + string.slice(1).toLowerCase();
|
||||
}
|
||||
|
||||
function addValuesToComboBox(selectorElement, values) {
|
||||
for(i in values) {
|
||||
let value = values[i];
|
||||
selectorElement.options[selectorElement.options.length] = new Option(value, value);
|
||||
}
|
||||
function isVersion2() {
|
||||
return profile.gameVersion.includes("2");
|
||||
}
|
||||
|
||||
59
src/main/resources/dashboard/scripts/utility.js
Normal file
59
src/main/resources/dashboard/scripts/utility.js
Normal file
@@ -0,0 +1,59 @@
|
||||
function clampOnChange(inputElement) {
|
||||
inputElement.addEventListener("change", function() {
|
||||
let value = parseInt(this.value);
|
||||
|
||||
if(value < this.min) {
|
||||
this.value = this.min;
|
||||
} else if(value > this.max) {
|
||||
this.value = this.max;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function clearSelectOptions(selectElement) {
|
||||
let startIndex = selectElement.options.length - 1;
|
||||
|
||||
for(i = startIndex; i >= 0; i--) {
|
||||
selectElement.remove(i);
|
||||
}
|
||||
}
|
||||
|
||||
function stringToWord(string) {
|
||||
return string.charAt(0).toUpperCase() + string.slice(1).toLowerCase();
|
||||
}
|
||||
|
||||
function checkURL(url) {
|
||||
var request = new XMLHttpRequest();
|
||||
request.open('HEAD', url, false);
|
||||
request.send();
|
||||
return request.status == 200;
|
||||
}
|
||||
|
||||
async function fetchData(method, path) {
|
||||
return fetchData(method, path, null);
|
||||
}
|
||||
|
||||
async function fetchData(method, path, body) {
|
||||
let response = await fetch(path, {
|
||||
method: method,
|
||||
body: body
|
||||
});
|
||||
|
||||
if(response.status != 200) {
|
||||
if(response.status == 401) {
|
||||
window.location.href = "/dashboard/login.html"; // TODO not epic idea to put this here
|
||||
return false;
|
||||
}
|
||||
|
||||
window.alert("Server returned status code " + response.status + " while fetching " + path);
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
return await response.json();
|
||||
} catch(error) {
|
||||
window.alert("Could not deserialize JSON response: " + error);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="stylesheet" href="styles/list.css">
|
||||
</head>
|
||||
<body onload="fillTableWithSpecies()">
|
||||
<div id="main-container" class="root-container"">
|
||||
<label class="big-label">List of Pokémon species that can be downloaded</label>
|
||||
<label>NOTE: Generation V Pokémon are exclusive to Black Version 2 and White Version 2</label>
|
||||
<table id="species-table">
|
||||
<!-- Filled by list.js -->
|
||||
</table>
|
||||
</div>
|
||||
</body>
|
||||
<script src="scripts/list.js"></script>
|
||||
</html>
|
||||
@@ -1,33 +0,0 @@
|
||||
body {
|
||||
background-color: #191919;
|
||||
color: white;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
label {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
table {
|
||||
border-spacing: 8px;
|
||||
margin-top: 15px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
td {
|
||||
background-color: #262626;
|
||||
border-radius: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.root-container {
|
||||
display: grid;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translate(-50%, 0%);
|
||||
width: 1136; /* Hack-ish */
|
||||
}
|
||||
|
||||
.big-label {
|
||||
font-size: 36px;
|
||||
}
|
||||
@@ -30,9 +30,8 @@ button:active {
|
||||
}
|
||||
|
||||
.root-container {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 25%;
|
||||
transform: translate(-50%, 0%);
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
@@ -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,23 +42,26 @@ a:link, a:visited {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.root-container {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
padding: 10px;
|
||||
max-width: 600px;
|
||||
.centered-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.centered-container div:first-child {
|
||||
max-width: 532px;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.grid-container {
|
||||
display: grid;
|
||||
grid-auto-columns: 1fr;
|
||||
grid-auto-flow: column;
|
||||
grid-template-columns: auto auto;
|
||||
gap: 5px 10px;
|
||||
}
|
||||
|
||||
.grid-container input {
|
||||
.grid-container input,
|
||||
.grid-container button {
|
||||
width: 100%;
|
||||
margin-top: 4px;
|
||||
margin-bottom: 16px;
|
||||
@@ -65,28 +71,25 @@ a:link, a:visited {
|
||||
}
|
||||
|
||||
.dreamer-summary {
|
||||
width: 100%;
|
||||
/* Idk why it's like this but yeah nonsense like this is why I hate CSS */
|
||||
width: calc(100% + 4px);
|
||||
margin-top: 3px;
|
||||
margin-left: -2px;
|
||||
margin-bottom: 20px;
|
||||
table-layout: fixed;
|
||||
}
|
||||
|
||||
.dreamer-summary th {
|
||||
text-align: left;
|
||||
padding-left: 10px;
|
||||
width: 64px;
|
||||
}
|
||||
|
||||
.dreamer-summary td {
|
||||
width: 96px;
|
||||
background-color: #262626;
|
||||
border-radius: 10px;
|
||||
padding-left: 5px;
|
||||
}
|
||||
|
||||
.dreamer-sprite {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.header-text {
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
@@ -95,43 +98,35 @@ a:link, a:visited {
|
||||
}
|
||||
|
||||
.popup {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
opacity: 0;
|
||||
transition: opacity 200ms;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.popup:target {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.popup .close-button {
|
||||
position: absolute;
|
||||
background-color: #3D3D3D;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
}
|
||||
|
||||
.popup .content {
|
||||
.content {
|
||||
position: relative;
|
||||
background-color: #191919;
|
||||
width: 500px;
|
||||
padding: 20px 20px 0px 20px;
|
||||
}
|
||||
|
||||
.close-button {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 25%;
|
||||
transform: translate(-50%, 0%);
|
||||
padding: 20px;
|
||||
width: 300px;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
background-color: #3D3D3D;
|
||||
}
|
||||
|
||||
.encounter-image-table {
|
||||
width: 100%;
|
||||
width: calc(100% + 16px);
|
||||
border-spacing: 8px;
|
||||
margin-bottom: 20px;
|
||||
margin-left: -8px;
|
||||
margin-top: -4px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.encounter-image-table td {
|
||||
@@ -140,12 +135,15 @@ a:link, a:visited {
|
||||
width: 96px;
|
||||
height: 96px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.item-table {
|
||||
width: 100%;
|
||||
width: calc(100% + 16px);
|
||||
border-spacing: 8px;
|
||||
margin-bottom: 20px;
|
||||
margin-left: -8px;
|
||||
margin-top: -4px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.item-table td {
|
||||
@@ -154,6 +152,7 @@ a:link, a:visited {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.item-table td a {
|
||||
@@ -162,7 +161,45 @@ a:link, a:visited {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.visitor-table {
|
||||
width: calc(100% + 16px);
|
||||
border-spacing: 8px;
|
||||
margin-left: -8px;
|
||||
margin-top: -4px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.visitor-table td {
|
||||
background-color: #262626;
|
||||
border-radius: 10px;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.big-button {
|
||||
font-size: 16px;
|
||||
padding: 16px 32px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
#cgear-skin, #dex-skin {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
#dreamer-sprite {
|
||||
width: 96px;
|
||||
text-align: center;
|
||||
padding-left: 0px;
|
||||
}
|
||||
|
||||
#skin-preview-content {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
#skin-preview-image {
|
||||
box-shadow: 0px 0px 10px 3px #0A0A0A;
|
||||
margin-top: 4px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
@@ -1,44 +1,59 @@
|
||||
/dlc/IRAO/CGEAR/C003_munna_1_en.bin
|
||||
/dlc/IRAO/CGEAR/C004_pikachu_1_e.bin
|
||||
/dlc/IRAO/CGEAR/C005_chirami_1_e.bin
|
||||
/dlc/IRAO/CGEAR/C007_tabunne_1_e.bin
|
||||
/dlc/IRAO/CGEAR/C008_pocchama_1_e.bin
|
||||
/dlc/IRAO/CGEAR/C009_guregguru_1_e.bin
|
||||
/dlc/IRAO/CGEAR/C010_fushigibana_1_e.bin
|
||||
/dlc/IRAO/CGEAR/C011_rizadon_1_e.bin
|
||||
/dlc/IRAO/CGEAR/C012_kamekkusu_1_e.bin
|
||||
/dlc/IRAO/CGEAR/C013_zekurom_1_e_v02.bin
|
||||
/dlc/IRAO/CGEAR/C014_reshiram_1_e.bin
|
||||
/dlc/IRAO/CGEAR/C015_vikutini_1_e.bin
|
||||
/dlc/IRAO/CGEAR/C017_porigonz_0_e.bin
|
||||
/dlc/IRAO/CGEAR/C018_WCS_1_e.bin
|
||||
/dlc/IRAO/CGEAR/C019_shikijikaharu_1_e.bin
|
||||
/dlc/IRAO/CGEAR/C020_shikijikanatu_1_e.bin
|
||||
/dlc/IRAO/CGEAR/C021_shikijikaaki_1_e.bin
|
||||
/dlc/IRAO/CGEAR/C022_shikijikafuyu_1_e.bin
|
||||
/dlc/IRAO/CGEAR/C023_iwaparesu_1_e.bin
|
||||
/dlc/IRAO/CGEAR/C024_zoroark_1_e.bin
|
||||
/dlc/IRAO/CGEAR/C025_giaru_1_e.bin
|
||||
/dlc/IRAO/CGEAR/C030_kerudhio_1_en.bin
|
||||
/dlc/IRAO/CGEAR/C031-1_meroetta_1_e.bin
|
||||
/dlc/IRAO/CGEAR/C032-1_wcs2012_1_e.bin
|
||||
/dlc/IRAO/CGEAR/C100_defo_1_en.bin
|
||||
/dlc/IRAO/CGEAR2/C003-2_munna_1_en.bin
|
||||
/dlc/IRAO/CGEAR2/C013-2_zekrom_1_e.bin
|
||||
/dlc/IRAO/CGEAR2/C014-2_reshiram_1_e.bin
|
||||
/dlc/IRAO/CGEAR2/C015-2_victini_1_e.bin
|
||||
/dlc/IRAO/CGEAR2/C030-2_kerudhio_1_en.bin
|
||||
/dlc/IRAO/CGEAR2/C031-2_meroetta_1_e.bin
|
||||
/dlc/IRAO/CGEAR2/C033_halloween_1_en.bin
|
||||
/dlc/IRAO/CGEAR2/C035_BK_1_e.bin
|
||||
/dlc/IRAO/CGEAR2/C036_WK_1_e.bin
|
||||
/dlc/IRAO/CGEAR2/C100-2_default_1_e.bin
|
||||
/dlc/IRAO/MUSICAL/M010_munna_1_e_02.bin
|
||||
/dlc/IRAO/MUSICAL/M013_meloetta_1_e.bin
|
||||
/dlc/IRAO/ZUKAN/Z003_BWsyokigirl_1_en.bin
|
||||
/dlc/IRAO/ZUKAN/Z004_BWsyokiboy_1_en.bin
|
||||
/dlc/IRAO/ZUKAN/Z007_hyuu_1_en.bin
|
||||
/dlc/IRAO/ZUKAN/Z008_bell_1_e.bin
|
||||
/dlc/IRAO/ZUKAN/Z009_tyeren_1_e.bin
|
||||
/dlc/IRAO/ZUKAN/Z100_defogirl_1_en.bin
|
||||
/dlc/IRAO/ZUKAN/Z101_defoboy_1_en.bin
|
||||
/dlc/IRAO/CGEAR/01 - Default.bin
|
||||
/dlc/IRAO/CGEAR/02 - Meadow Munna.bin
|
||||
/dlc/IRAO/CGEAR/03 - Aim for the Top.bin
|
||||
/dlc/IRAO/CGEAR/04 - Twinkle Minccino.bin
|
||||
/dlc/IRAO/CGEAR/05 - PokeSma Purrloin.bin
|
||||
/dlc/IRAO/CGEAR/06 - Earful Audino.bin
|
||||
/dlc/IRAO/CGEAR/07 - Happy Piplup.bin
|
||||
/dlc/IRAO/CGEAR/08 - Poison Jab Croagunk.bin
|
||||
/dlc/IRAO/CGEAR/09 - Venusaur!.bin
|
||||
/dlc/IRAO/CGEAR/10 - Charizard!.bin
|
||||
/dlc/IRAO/CGEAR/11 - Blastoise!.bin
|
||||
/dlc/IRAO/CGEAR/12 - V for Victory!.bin
|
||||
/dlc/IRAO/CGEAR/13 - Virtual Pokemon.bin
|
||||
/dlc/IRAO/CGEAR/14 - Pokemon Cafe.bin
|
||||
/dlc/IRAO/CGEAR/15 - Hero Reshiram.bin
|
||||
/dlc/IRAO/CGEAR/16 - Hero Zekrom.bin
|
||||
/dlc/IRAO/CGEAR/17 - 2011 Worlds C-Gear.bin
|
||||
/dlc/IRAO/CGEAR/18 - Zoroark!.bin
|
||||
/dlc/IRAO/CGEAR/19 - Spring Deerling.bin
|
||||
/dlc/IRAO/CGEAR/20 - Summer Deerling.bin
|
||||
/dlc/IRAO/CGEAR/21 - Autumn Deerling.bin
|
||||
/dlc/IRAO/CGEAR/22 - Winter Deerling.bin
|
||||
/dlc/IRAO/CGEAR/23 - CRUSTLE!.bin
|
||||
/dlc/IRAO/CGEAR/24 - KLINK!.bin
|
||||
/dlc/IRAO/CGEAR/25 - Ducklett Friends.bin
|
||||
/dlc/IRAO/CGEAR/26 - Guidance Cobalion.bin
|
||||
/dlc/IRAO/CGEAR/27 - Trial Terrakion.bin
|
||||
/dlc/IRAO/CGEAR/28 - Rumination Virizion.bin
|
||||
/dlc/IRAO/CGEAR/29 - Keldeo Ordinary Forme.bin
|
||||
/dlc/IRAO/CGEAR/30 - Aria of the Night Sky.bin
|
||||
/dlc/IRAO/CGEAR/31 - 2012 Worlds C-Gear.bin
|
||||
/dlc/IRAO/CGEAR/32 - Red Genesect.bin
|
||||
/dlc/IRAO/CGEAR2/01 - Default.bin
|
||||
/dlc/IRAO/CGEAR2/02 - Meadow Munna.bin
|
||||
/dlc/IRAO/CGEAR2/03 - Keldeo Resolute Forme.bin
|
||||
/dlc/IRAO/CGEAR2/04 - 2012 Worlds C-Gear.bin
|
||||
/dlc/IRAO/CGEAR2/05 - Pumpkin Pikachu.bin
|
||||
/dlc/IRAO/CGEAR2/06 - V for Victory!.bin
|
||||
/dlc/IRAO/CGEAR2/07 - Hero Reshiram.bin
|
||||
/dlc/IRAO/CGEAR2/08 - Hero Zekrom.bin
|
||||
/dlc/IRAO/CGEAR2/09 - Aria of the Night Sky.bin
|
||||
/dlc/IRAO/CGEAR2/10 - Sleeping Eevee.bin
|
||||
/dlc/IRAO/CGEAR2/11 - Venusaur!.bin
|
||||
/dlc/IRAO/CGEAR2/12 - Charizard!.bin
|
||||
/dlc/IRAO/CGEAR2/13 - Blastoise!.bin
|
||||
/dlc/IRAO/CGEAR2/14 - Red Genesect.bin
|
||||
/dlc/IRAO/CGEAR2/15 - Black Kyurem.bin
|
||||
/dlc/IRAO/CGEAR2/16 - White Kyurem.bin
|
||||
/dlc/IRAO/MUSICAL/01 - Charming Munna.bin
|
||||
/dlc/IRAO/MUSICAL/02 - MELOETTAAA!!!.bin
|
||||
/dlc/IRAO/ZUKAN/01 - Default (Pink).bin
|
||||
/dlc/IRAO/ZUKAN/02 - Default (Red).bin
|
||||
/dlc/IRAO/ZUKAN/03 - Unova Trio (Pink).bin
|
||||
/dlc/IRAO/ZUKAN/04 - Unova Trio (Red).bin
|
||||
/dlc/IRAO/ZUKAN/05 - Kanto Trio (Pink).bin
|
||||
/dlc/IRAO/ZUKAN/06 - Kanto Trio (Red).bin
|
||||
/dlc/IRAO/ZUKAN/07 - Hugh.bin
|
||||
/dlc/IRAO/ZUKAN/08 - Bianca.bin
|
||||
/dlc/IRAO/ZUKAN/09 - Cheren.bin
|
||||
|
||||
BIN
src/main/resources/dlc/IRAO/CGEAR/05 - PokeSma Purrloin.bin
Normal file
BIN
src/main/resources/dlc/IRAO/CGEAR/05 - PokeSma Purrloin.bin
Normal file
Binary file not shown.
BIN
src/main/resources/dlc/IRAO/CGEAR/14 - Pokemon Cafe.bin
Normal file
BIN
src/main/resources/dlc/IRAO/CGEAR/14 - Pokemon Cafe.bin
Normal file
Binary file not shown.
BIN
src/main/resources/dlc/IRAO/CGEAR/25 - Ducklett Friends.bin
Normal file
BIN
src/main/resources/dlc/IRAO/CGEAR/25 - Ducklett Friends.bin
Normal file
Binary file not shown.
BIN
src/main/resources/dlc/IRAO/CGEAR/26 - Guidance Cobalion.bin
Normal file
BIN
src/main/resources/dlc/IRAO/CGEAR/26 - Guidance Cobalion.bin
Normal file
Binary file not shown.
BIN
src/main/resources/dlc/IRAO/CGEAR/27 - Trial Terrakion.bin
Normal file
BIN
src/main/resources/dlc/IRAO/CGEAR/27 - Trial Terrakion.bin
Normal file
Binary file not shown.
BIN
src/main/resources/dlc/IRAO/CGEAR/28 - Rumination Virizion.bin
Normal file
BIN
src/main/resources/dlc/IRAO/CGEAR/28 - Rumination Virizion.bin
Normal file
Binary file not shown.
BIN
src/main/resources/dlc/IRAO/CGEAR/32 - Red Genesect.bin
Normal file
BIN
src/main/resources/dlc/IRAO/CGEAR/32 - Red Genesect.bin
Normal file
Binary file not shown.
File diff suppressed because one or more lines are too long
BIN
src/main/resources/dlc/IRAO/CGEAR2/10 - Sleeping Eevee.bin
Normal file
BIN
src/main/resources/dlc/IRAO/CGEAR2/10 - Sleeping Eevee.bin
Normal file
Binary file not shown.
BIN
src/main/resources/dlc/IRAO/CGEAR2/11 - Venusaur!.bin
Normal file
BIN
src/main/resources/dlc/IRAO/CGEAR2/11 - Venusaur!.bin
Normal file
Binary file not shown.
BIN
src/main/resources/dlc/IRAO/CGEAR2/12 - Charizard!.bin
Normal file
BIN
src/main/resources/dlc/IRAO/CGEAR2/12 - Charizard!.bin
Normal file
Binary file not shown.
BIN
src/main/resources/dlc/IRAO/CGEAR2/13 - Blastoise!.bin
Normal file
BIN
src/main/resources/dlc/IRAO/CGEAR2/13 - Blastoise!.bin
Normal file
Binary file not shown.
BIN
src/main/resources/dlc/IRAO/CGEAR2/14 - Red Genesect.bin
Normal file
BIN
src/main/resources/dlc/IRAO/CGEAR2/14 - Red Genesect.bin
Normal file
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user