mirror of
https://github.com/kuroppoi/entralinked.git
synced 2026-09-08 08:45:14 -05:00
Compare commits
19 Commits
v1.3.3
...
dls-rework
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d51c69ac70 | ||
|
|
d99608ff11 | ||
|
|
acbb3d7f95 | ||
|
|
abe1b80c2d | ||
|
|
8e614e0d27 | ||
|
|
774794f751 | ||
|
|
9d288bde09 | ||
|
|
18b86a42e0 | ||
|
|
39e0b27cec | ||
|
|
1af975ffb6 | ||
|
|
6f2e4a06a0 | ||
|
|
68bdb7d47f | ||
|
|
163ced2333 | ||
|
|
f3809f4a54 | ||
|
|
0cd628ba63 | ||
|
|
3898bd473c | ||
|
|
0c4cd69741 | ||
|
|
91eb9971f3 | ||
|
|
3224f6312a |
@@ -1,13 +1,13 @@
|
||||
name: Build
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
push:
|
||||
branches: [ master ]
|
||||
paths:
|
||||
- poke-sprites-v
|
||||
- src/**
|
||||
- build.gradle
|
||||
|
||||
jobs:
|
||||
dist:
|
||||
runs-on: ubuntu-latest
|
||||
21
.github/workflows/dist-pull-request.yml
vendored
21
.github/workflows/dist-pull-request.yml
vendored
@@ -1,21 +0,0 @@
|
||||
name: Build on Pull Request
|
||||
on: pull_request
|
||||
jobs:
|
||||
dist:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
- name: Checkout submodules
|
||||
run: git submodule update --init --recursive
|
||||
- name: Setup Java 17
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
java-version: 17
|
||||
distribution: temurin
|
||||
- name: Make gradlew executable
|
||||
run: chmod +x ./gradlew
|
||||
- name: Setup Gradle
|
||||
uses: gradle/gradle-build-action@v2
|
||||
- name: Run Gradle dist
|
||||
run: ./gradlew dist
|
||||
@@ -3,7 +3,7 @@
|
||||
</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/actions"><img src="https://github.com/kuroppoi/entralinked/actions/workflows/build.yml/badge.svg" alt="build"/></a>
|
||||
<a href="https://github.com/kuroppoi/entralinked/releases/latest"><img src="https://img.shields.io/github/v/release/kuroppoi/entralinked?labelColor=30373D&label=Release&logoColor=959DA5&logo=github" alt="release"/></a>
|
||||
</p>
|
||||
|
||||
@@ -12,6 +12,8 @@ Its purpose is to serve as a simple utility for downloading Pokémon, Items, C-G
|
||||
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
|
||||
|
||||
BIN
images/preview-2x.gif
Normal file
BIN
images/preview-2x.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 356 KiB |
BIN
images/preview.gif
Normal file
BIN
images/preview.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 210 KiB |
@@ -113,12 +113,11 @@ public class Entralinked {
|
||||
|
||||
// Post-startup
|
||||
if(started) {
|
||||
logger.info("Startup complete! Took a total of {} milliseconds", System.currentTimeMillis() - beginTime);
|
||||
String hostIpAddress = hostAddress.getHostAddress();
|
||||
logger.info("Startup complete! Took a total of {} milliseconds", System.currentTimeMillis() - beginTime);
|
||||
logger.info("Configure your DS to use the following DNS server: {}", hostIpAddress);
|
||||
|
||||
if(mainView == null) {
|
||||
logger.info("Configure your DS to use the following DNS server: {}", hostIpAddress);
|
||||
} else {
|
||||
if(mainView != null) {
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
mainView.setDashboardButtonEnabled(true);
|
||||
mainView.setStatusLabelText("Configure your DS to use the following DNS server: %s".formatted(hostIpAddress));
|
||||
|
||||
@@ -15,7 +15,7 @@ import com.formdev.flatlaf.extras.components.FlatTextField;
|
||||
|
||||
import entralinked.Entralinked;
|
||||
import entralinked.model.user.User;
|
||||
import entralinked.model.user.UserManager;
|
||||
import entralinked.utility.MD5;
|
||||
import entralinked.utility.SwingUtility;
|
||||
|
||||
public class PidToolDialog {
|
||||
@@ -41,8 +41,8 @@ public class PidToolDialog {
|
||||
return;
|
||||
}
|
||||
|
||||
String userId = wfcIdField.getText().replace("-", "");
|
||||
String friendCode = friendCodeField.getText().replace("-", "");
|
||||
String userId = wfcIdField.getText().replace("-", "").replaceAll("\\s+", "");
|
||||
String friendCodeString = friendCodeField.getText().replace("-", "").replaceAll("\\s+", "");
|
||||
|
||||
// Make sure WFC ID is valid
|
||||
if(!WFC_ID_PATTERN.matcher(userId).matches()) {
|
||||
@@ -51,14 +51,12 @@ public class PidToolDialog {
|
||||
}
|
||||
|
||||
// Make sure Friend Code is valid
|
||||
if(!FRIEND_CODE_PATTERN.matcher(friendCode).matches()) {
|
||||
if(!FRIEND_CODE_PATTERN.matcher(friendCodeString).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);
|
||||
User user = entralinked.getUserManager().getUser(userId.substring(0, 13));
|
||||
|
||||
// Make sure user exists
|
||||
if(user == null) {
|
||||
@@ -66,17 +64,29 @@ public class PidToolDialog {
|
||||
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;
|
||||
}
|
||||
long friendCode = Long.parseLong(friendCodeString);
|
||||
int profileId = (int)(friendCode & 0x7FFFFFFF);
|
||||
int checksum = (int)(friendCode >> 32);
|
||||
|
||||
// Compute friend code checksum
|
||||
byte[] buffer = {0x00, 0x00, 0x00, 0x00, 0x4A, 0x41, 0x52, 0x49}; // Last 4 bytes is inverted game code (IRAJ)
|
||||
buffer[0] = (byte)(profileId & 0xFF);
|
||||
buffer[1] = (byte)(profileId >> 8 & 0xFF);
|
||||
buffer[2] = (byte)(profileId >> 16 & 0xFF);
|
||||
buffer[3] = (byte)(profileId >> 24 & 0xFF);
|
||||
byte[] hash = MD5.digest(buffer);
|
||||
int computedChecksum = hash[0] >> 1 & 0x7F;
|
||||
|
||||
// Compare checksums
|
||||
if(computedChecksum != checksum) {
|
||||
JOptionPane.showMessageDialog(dialog, "This is not a valid Friend Code. Please check for typos.", "Attention", JOptionPane.WARNING_MESSAGE);
|
||||
return;
|
||||
}
|
||||
|
||||
userManager.updateProfileIdForUser(user, profileId);
|
||||
JOptionPane.showMessageDialog(dialog, "Profile has been updated. Please restart your game and use Game Sync.");
|
||||
// Everything checks out -- update the profile id!
|
||||
user.setProfileIdOverride(profileId);
|
||||
JOptionPane.showMessageDialog(dialog,
|
||||
"All done! Please restart your game and use Game Sync.\nGame profile data will be updated and saved once you do so.");
|
||||
});
|
||||
|
||||
// Create content panel
|
||||
|
||||
@@ -3,5 +3,6 @@ package entralinked.model.dlc;
|
||||
/**
|
||||
* Simple record for DLC data.
|
||||
*/
|
||||
@Deprecated
|
||||
public record Dlc(String path, String name, String gameCode, String type,
|
||||
int index, int projectedSize, int checksum, boolean checksumEmbedded) {}
|
||||
|
||||
@@ -6,6 +6,7 @@ import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
@@ -17,7 +18,9 @@ import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
import entralinked.utility.Crc16;
|
||||
import entralinked.utility.MD5;
|
||||
|
||||
@Deprecated
|
||||
public class DlcList {
|
||||
|
||||
private static final Logger logger = LogManager.getLogger();
|
||||
@@ -27,31 +30,32 @@ public class DlcList {
|
||||
public DlcList() {
|
||||
logger.info("Loading DLC ...");
|
||||
|
||||
// Extract defaults if external DLC directory is not present
|
||||
if(!dataDirectory.exists()) {
|
||||
logger.info("Extracting default DLC files ...");
|
||||
BufferedReader reader = new BufferedReader(new InputStreamReader(getClass().getResourceAsStream("/dlc.paths")));
|
||||
String line = null;
|
||||
|
||||
try {
|
||||
while((line = reader.readLine()) != null) {
|
||||
InputStream resource = getClass().getResourceAsStream(line);
|
||||
File outputFile = new File("./%s".formatted(line));
|
||||
|
||||
// Create parent directories
|
||||
if(outputFile.getParentFile() != null) {
|
||||
outputFile.getParentFile().mkdirs();
|
||||
}
|
||||
|
||||
// Copy resource to destination
|
||||
if(resource != null) {
|
||||
Files.copy(resource, outputFile.toPath());
|
||||
}
|
||||
try(BufferedReader reader = new BufferedReader(new InputStreamReader(getClass().getResourceAsStream("/dlc.paths")))) {
|
||||
reader.lines().forEach(line -> {
|
||||
String[] segments = line.split("\t");
|
||||
|
||||
if(segments.length != 2) {
|
||||
return;
|
||||
}
|
||||
} catch (IOException e) {
|
||||
logger.error("Could not extract default DLC files", e);
|
||||
return;
|
||||
}
|
||||
|
||||
String path = segments[0];
|
||||
String hash = segments[1];
|
||||
File outputFile = new File("./%s".formatted(path));
|
||||
|
||||
if(outputFile.getParentFile() != null) {
|
||||
outputFile.getParentFile().mkdirs();
|
||||
}
|
||||
|
||||
try(InputStream inputStream = getClass().getResourceAsStream(path)){
|
||||
if(!outputFile.exists() || !hash.equals(MD5.digest(outputFile))) {
|
||||
Files.copy(inputStream, outputFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
|
||||
}
|
||||
} catch(IOException e) {
|
||||
logger.error("Couldn't process resource '{}'", path, e);
|
||||
}
|
||||
});
|
||||
} catch(IOException e) {
|
||||
logger.error("Couldn't extract DLC data", e);
|
||||
}
|
||||
|
||||
// Just to be sure...
|
||||
@@ -71,7 +75,7 @@ public class DlcList {
|
||||
for(File subFile : file.listFiles()) {
|
||||
// Check if file is directory
|
||||
if(!subFile.isDirectory()) {
|
||||
logger.warn("Non-directory '{}' in DLC subfolder '{}'", file.getName(), subFile.getName());
|
||||
logger.warn("Non-directory '{}' in DLC subfolder '{}'", subFile.getName(), file.getName());
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,8 @@ 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)
|
||||
*/
|
||||
@@ -175,7 +177,7 @@ 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) {
|
||||
public Player registerPlayer(String gameSyncId, GameVersion version) {
|
||||
// Check for duplicate Game Sync ID
|
||||
if(playerMap.containsKey(gameSyncId)) {
|
||||
logger.warn("Attempted to register duplicate player {}", gameSyncId);
|
||||
@@ -193,6 +195,7 @@ public class PlayerManager {
|
||||
// Construct player object
|
||||
Player player = new Player(gameSyncId);
|
||||
player.setStatus(PlayerStatus.AWAKE);
|
||||
player.setGameVersion(version);
|
||||
player.setDataDirectory(playerDataDirectory);
|
||||
|
||||
// Try to save player data
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
package entralinked.model.user;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.Collection;
|
||||
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<>();
|
||||
private final Map<String, File> dlcOverrides = new HashMap<>();
|
||||
private int profileIdOverride; // For making it easier for the user to fix error 60000
|
||||
|
||||
public User(String id, String password) {
|
||||
this.id = id;
|
||||
@@ -23,6 +23,13 @@ public class User {
|
||||
return "%s000".formatted(id).replaceAll("(.{4})(?!$)", "$1-");
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The user's id, redacted for logging.
|
||||
*/
|
||||
public String getRedactedId() {
|
||||
return "%s-XXXX-XXXX-XXXX".formatted(id.substring(0, 4));
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
@@ -51,11 +58,11 @@ public class User {
|
||||
return Collections.unmodifiableMap(profiles);
|
||||
}
|
||||
|
||||
public void setDlcOverride(String type, Dlc target) {
|
||||
if(target == null) {
|
||||
public void setDlcOverride(String type, File file) {
|
||||
if(file == null) {
|
||||
dlcOverrides.remove(type);
|
||||
} else {
|
||||
dlcOverrides.put(type, target);
|
||||
dlcOverrides.put(type, file);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,7 +74,15 @@ public class User {
|
||||
return dlcOverrides.containsKey(type);
|
||||
}
|
||||
|
||||
public Dlc getDlcOverride(String type) {
|
||||
public File getDlcOverride(String type) {
|
||||
return dlcOverrides.get(type);
|
||||
}
|
||||
|
||||
public void setProfileIdOverride(int profileIdOverride) {
|
||||
this.profileIdOverride = profileIdOverride;
|
||||
}
|
||||
|
||||
public int getProfileIdOverride() {
|
||||
return profileIdOverride;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -177,14 +177,14 @@ public class UserManager {
|
||||
/**
|
||||
* Registers a user with the given ID and password.
|
||||
|
||||
* @return {@code true} if the registration was successful.
|
||||
* Otherwise, if this user ID has already been registered, or if the user data could not be saved, {@code false} is returned instead.
|
||||
* @return The {@link User} object of the newly registered user if the registration was successful.
|
||||
* Otherwise, if this user ID has already been registered, or if the user data could not be saved, {@code null} is returned instead.
|
||||
*/
|
||||
public boolean registerUser(String userId, String plainPassword) {
|
||||
public User registerUser(String userId, String plainPassword) {
|
||||
// Check if user id already exists
|
||||
if(users.containsKey(userId)) {
|
||||
logger.warn("Attempted to register user with duplicate ID: {}", userId);
|
||||
return false;
|
||||
return null;
|
||||
}
|
||||
|
||||
// Create user
|
||||
@@ -192,11 +192,11 @@ public class UserManager {
|
||||
|
||||
// Save user data and return null if it fails
|
||||
if(!saveUser(user)) {
|
||||
return false;
|
||||
return null;
|
||||
}
|
||||
|
||||
users.put(userId, user);
|
||||
return true;
|
||||
return user;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -233,26 +233,6 @@ public class UserManager {
|
||||
return profile;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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}.
|
||||
*/
|
||||
public boolean updateProfileIdForUser(User user, int profileId) {
|
||||
// Set the id of all profiles
|
||||
for(GameProfile profile : user.getProfiles()) {
|
||||
profile.setId(profileId);
|
||||
}
|
||||
|
||||
// Try to save user
|
||||
if(!saveUser(user)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {@code true} if a user with the specified ID exists, otherwise {@code false}.
|
||||
*/
|
||||
|
||||
@@ -28,6 +28,7 @@ public class DnsQueryHandler extends SimpleChannelInboundHandler<DatagramDnsQuer
|
||||
@Override
|
||||
protected void channelRead0(ChannelHandlerContext ctx, DatagramDnsQuery query) throws Exception {
|
||||
DefaultDnsQuestion question = query.recordAt(DnsSection.QUESTION);
|
||||
logger.debug("Received {}", question);
|
||||
DnsRecordType type = question.type();
|
||||
|
||||
// We only need type A (32 bit IPv4) for the DS
|
||||
|
||||
@@ -28,6 +28,7 @@ import io.netty.channel.ChannelFuture;
|
||||
import io.netty.channel.ChannelFutureListener;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.channel.SimpleChannelInboundHandler;
|
||||
import io.netty.handler.timeout.ReadTimeoutException;
|
||||
|
||||
/**
|
||||
* GameSpy request handler.
|
||||
@@ -50,6 +51,7 @@ public class GameSpyHandler extends SimpleChannelInboundHandler<GameSpyRequest>
|
||||
@Override
|
||||
public void channelActive(ChannelHandlerContext ctx) {
|
||||
channel = ctx.channel();
|
||||
logger.debug("Sending GameSpy server challenge");
|
||||
|
||||
// Generate random server challenge
|
||||
serverChallenge = CredentialGenerator.generateChallenge(10);
|
||||
@@ -60,6 +62,8 @@ public class GameSpyHandler extends SimpleChannelInboundHandler<GameSpyRequest>
|
||||
|
||||
@Override
|
||||
public void channelInactive(ChannelHandlerContext ctx) {
|
||||
logger.debug("User {} disconnected from GameSpy server", user == null ? null : user.getRedactedId());
|
||||
|
||||
// Clear data
|
||||
serverChallenge = null;
|
||||
sessionKey = -1;
|
||||
@@ -69,15 +73,23 @@ public class GameSpyHandler extends SimpleChannelInboundHandler<GameSpyRequest>
|
||||
|
||||
@Override
|
||||
protected void channelRead0(ChannelHandlerContext ctx, GameSpyRequest request) throws Exception {
|
||||
logger.debug("Received {}", request);
|
||||
request.process(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
|
||||
if(cause instanceof ClosedChannelException) {
|
||||
logger.debug("Silently discarded ClosedChannelException");
|
||||
return; // Ignore this stupid exception.
|
||||
}
|
||||
|
||||
// Handle timeout
|
||||
if(cause instanceof ReadTimeoutException) {
|
||||
logger.debug("User {} timed out", user == null ? null : user.getRedactedId());
|
||||
return;
|
||||
}
|
||||
|
||||
logger.error("Exception caught in GameSpy handler", cause);
|
||||
|
||||
// Send error message and close channel afterwards.
|
||||
@@ -92,6 +104,7 @@ public class GameSpyHandler extends SimpleChannelInboundHandler<GameSpyRequest>
|
||||
ServiceSession session = userManager.getServiceSession(authToken, "gamespy");
|
||||
|
||||
if(session == null) {
|
||||
logger.debug("Rejecting GameSpy login request because the partner token is invalid");
|
||||
sendErrorMessage(0x200, "Invalid partner token.", request.sequenceId());
|
||||
return;
|
||||
}
|
||||
@@ -101,6 +114,7 @@ public class GameSpyHandler extends SimpleChannelInboundHandler<GameSpyRequest>
|
||||
String expectedResponse = createCredentialHash(partnerChallengeHash, authToken, clientChallenge, serverChallenge);
|
||||
|
||||
if(!expectedResponse.equals(request.response())) {
|
||||
logger.debug("Rejecting GameSpy login request because the challenge response is invalid");
|
||||
sendErrorMessage(0x202, "Invalid response.", request.sequenceId());
|
||||
return;
|
||||
}
|
||||
@@ -119,6 +133,17 @@ public class GameSpyHandler extends SimpleChannelInboundHandler<GameSpyRequest>
|
||||
}
|
||||
}
|
||||
|
||||
// Update profile id if an override is set
|
||||
int profileIdOverride = user.getProfileIdOverride();
|
||||
|
||||
if(profileIdOverride > 0) {
|
||||
profile.setId(profileIdOverride);
|
||||
user.setProfileIdOverride(0);
|
||||
userManager.saveUser(user); // It's not too big of a deal if this fails for some reason
|
||||
}
|
||||
|
||||
logger.info("User {} logged in with profile {}", user.getRedactedId(), profile.getId());
|
||||
|
||||
// Prepare and send response
|
||||
sessionKey = secureRandom.nextInt(Integer.MAX_VALUE);
|
||||
String proof = createCredentialHash(partnerChallengeHash, authToken, serverChallenge, clientChallenge);
|
||||
@@ -171,10 +196,9 @@ public class GameSpyHandler extends SimpleChannelInboundHandler<GameSpyRequest>
|
||||
passwordHash));
|
||||
}
|
||||
|
||||
public void destroySessionKey(int sessionKey) {
|
||||
if(validateSessionKey(sessionKey)) {
|
||||
sessionKey = -1;
|
||||
}
|
||||
public void handleLogout() {
|
||||
logger.info("User {} logged out of profile {}", user.getRedactedId(), profile.getId());
|
||||
sessionKey = -1; // Is there a point?
|
||||
}
|
||||
|
||||
public boolean validateSessionKey(int sessionKey) {
|
||||
|
||||
@@ -26,6 +26,7 @@ import io.netty.channel.ChannelPipeline;
|
||||
import io.netty.channel.epoll.EpollServerSocketChannel;
|
||||
import io.netty.channel.socket.nio.NioServerSocketChannel;
|
||||
import io.netty.handler.codec.DelimiterBasedFrameDecoder;
|
||||
import io.netty.handler.timeout.ReadTimeoutHandler;
|
||||
import io.netty.util.concurrent.DefaultEventExecutor;
|
||||
import io.netty.util.concurrent.EventExecutorGroup;
|
||||
|
||||
@@ -64,6 +65,7 @@ public class GameSpyServer extends NettyServerBase {
|
||||
@Override
|
||||
protected void initChannel(Channel channel) throws Exception {
|
||||
ChannelPipeline pipeline = channel.pipeline();
|
||||
pipeline.addLast(new ReadTimeoutHandler(180));
|
||||
pipeline.addLast(new DelimiterBasedFrameDecoder(512, Unpooled.wrappedBuffer("\\final\\".getBytes())));
|
||||
pipeline.addLast(new GameSpyRequestDecoder(mapper, requestTypes));
|
||||
pipeline.addLast(new GameSpyMessageEncoder(mapper));
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
package entralinked.network.gamespy.request;
|
||||
|
||||
import entralinked.network.gamespy.GameSpyHandler;
|
||||
import entralinked.network.gamespy.message.GameSpyMessage;
|
||||
|
||||
@GameSpyMessage(name = "ka")
|
||||
public record GameSpyKeepAliveRequest() implements GameSpyRequest {
|
||||
|
||||
@Override
|
||||
public void process(GameSpyHandler handler) {
|
||||
handler.sendMessage(this); // Pong it back
|
||||
}
|
||||
public void process(GameSpyHandler handler) {}
|
||||
}
|
||||
|
||||
@@ -28,4 +28,11 @@ public record GameSpyLoginRequest(
|
||||
public void process(GameSpyHandler handler) {
|
||||
handler.handleLoginRequest(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return ("GameSpyLoginRequest[sequenceId=%s, gameName=%s, profileId=%s, namespaceId=%s, partnerId=%s, productId=%s, "
|
||||
+ "sdkRevision=%s, firewall=%s, port=%s, quiet=%s]")
|
||||
.formatted(sequenceId, gameName, profileId, namespaceId, partnerId, productId, sdkRevision, firewall, port, quiet);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,14 @@ public record GameSpyLogoutRequest(
|
||||
|
||||
@Override
|
||||
public void process(GameSpyHandler handler) {
|
||||
handler.destroySessionKey(sessionKey);
|
||||
if(handler.validateSessionKey(sessionKey)) {
|
||||
handler.handleLogout();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
// Exlude session key
|
||||
return "GameSpyLogoutRequest[]";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,4 +16,10 @@ public record GameSpyProfileRequest(
|
||||
handler.handleProfileRequest(this);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
// Exlude session key
|
||||
return "GameSpyProfileRequest[sequenceId=%s, profileId=%s]".formatted(sequenceId, profileId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,4 +21,9 @@ public record GameSpyProfileUpdateRequest(
|
||||
handler.handleUpdateProfileRequest(this);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "GameSpyProfileUpdateRequest[partnerId=%s]".formatted(partnerId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,4 +14,9 @@ public record GameSpyStatusRequest(
|
||||
public void process(GameSpyHandler handler) {
|
||||
handler.validateSessionKey(sessionKey);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "GameSpyStatusRequest[]";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ import entralinked.LauncherAgent;
|
||||
import entralinked.utility.CertificateGenerator;
|
||||
import io.javalin.Javalin;
|
||||
import io.javalin.http.HttpStatus;
|
||||
import io.javalin.util.ConcurrencyUtil;
|
||||
import io.javalin.util.JavalinException;
|
||||
|
||||
public class HttpServer {
|
||||
@@ -32,7 +33,9 @@ public class HttpServer {
|
||||
private final Javalin javalin;
|
||||
private boolean started;
|
||||
|
||||
public HttpServer(Entralinked entralinked) {
|
||||
public HttpServer(Entralinked entralinked) {
|
||||
ConcurrencyUtil.INSTANCE.setUseLoom(false);
|
||||
|
||||
// Create certificate keystore
|
||||
KeyStore keyStore = null;
|
||||
|
||||
|
||||
@@ -1,21 +1,27 @@
|
||||
package entralinked.network.http.dls;
|
||||
|
||||
import java.io.FileInputStream;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import entralinked.Entralinked;
|
||||
import entralinked.model.dlc.Dlc;
|
||||
import entralinked.model.dlc.DlcList;
|
||||
import entralinked.GameVersion;
|
||||
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.utility.LEOutputStream;
|
||||
import entralinked.utility.MysteryGiftUtility;
|
||||
import io.javalin.Javalin;
|
||||
import io.javalin.http.Context;
|
||||
import io.javalin.http.HttpStatus;
|
||||
@@ -25,12 +31,12 @@ import io.javalin.http.HttpStatus;
|
||||
*/
|
||||
public class DlsHandler implements HttpHandler {
|
||||
|
||||
private static final Logger logger = LogManager.getLogger();
|
||||
private final ObjectMapper mapper = new ObjectMapper(new UrlEncodedFormFactory());
|
||||
private final DlcList dlcList;
|
||||
private final File rootDirectory = new File("dlc");
|
||||
private final UserManager userManager;
|
||||
|
||||
public DlsHandler(Entralinked entralinked) {
|
||||
this.dlcList = entralinked.getDlcList();
|
||||
this.userManager = entralinked.getUserManager();
|
||||
}
|
||||
|
||||
@@ -45,11 +51,13 @@ public class DlsHandler implements HttpHandler {
|
||||
private void handleDownloadRequest(Context ctx) throws IOException {
|
||||
// Deserialize request body
|
||||
DlsRequest request = mapper.readValue(ctx.body().replace("%2A", "*"), DlsRequest.class);
|
||||
logger.debug("Received {}", request);
|
||||
|
||||
// Check if service session is valid
|
||||
ServiceSession session = userManager.getServiceSession(request.serviceToken(), "dls1.nintendowifi.net");
|
||||
|
||||
if(session == null) {
|
||||
logger.debug("Rejecting DLS request because the service session has expired");
|
||||
ctx.status(HttpStatus.UNAUTHORIZED);
|
||||
return;
|
||||
}
|
||||
@@ -58,6 +66,7 @@ public class DlsHandler implements HttpHandler {
|
||||
HttpRequestHandler<DlsRequest> handler = switch(request.action()) {
|
||||
case "list" -> this::handleRetrieveDlcList;
|
||||
case "contents" -> this::handleRetrieveDlcContent;
|
||||
case "count" -> this::handleRetrieveDlcCount;
|
||||
default -> throw new IllegalArgumentException("Invalid POST request action: " + request.action());
|
||||
};
|
||||
|
||||
@@ -74,16 +83,48 @@ public class DlsHandler implements HttpHandler {
|
||||
private void handleRetrieveDlcList(DlsRequest request, Context ctx) throws IOException {
|
||||
User user = ctx.attribute("user");
|
||||
String gameCode = getDlcGameCode(request.dlcGameCode());
|
||||
String type = getRegionlessDlcType(request.dlcType());
|
||||
String type = getDlcType(request.attr1());
|
||||
String attr2 = request.attr2();
|
||||
List<File> files = null;
|
||||
|
||||
// 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;
|
||||
files = Arrays.asList(user.getDlcOverride(type));
|
||||
} else {
|
||||
// Get list of files in DLC directory
|
||||
File directory = getDlcDirectory(gameCode, type);
|
||||
files = directory.isDirectory() ? Arrays.asList(directory.listFiles()) : new ArrayList<>();
|
||||
}
|
||||
|
||||
// TODO NOTE: I assume that in a conventional implementation, certain DLC attributes may be omitted from the request.
|
||||
ctx.result(dlcList.getDlcListString(dlcList.getDlcList(gameCode, type, request.dlcIndex())));
|
||||
if(attr2 != null) {
|
||||
// PGL content attr2 hack
|
||||
files = Arrays.asList(files.get(Integer.parseInt(attr2) - 1));
|
||||
} else {
|
||||
// Mystery Gift randomness
|
||||
Collections.shuffle(files);
|
||||
}
|
||||
|
||||
StringBuilder builder = new StringBuilder();
|
||||
int count = Math.min(files.size(), request.num());
|
||||
|
||||
// Create DLC list string
|
||||
for(int i = 0; i < count; i++) {
|
||||
File file = files.get(i);
|
||||
|
||||
if(type == null) {
|
||||
// Generation 4 Mystery Gift
|
||||
builder.append("%s\t\t\t\t\t%s\r\n".formatted(file.getName(), 936));
|
||||
} else if(type.equals("MYSTERY")) {
|
||||
// Generation 5 Mystery Gift
|
||||
String gameFlag = GameVersion.lookup(request.gameCode()).isVersion2() ? "F00000" : "300000";
|
||||
builder.append("%s\t\t%s\t%s\t\t%s\r\n".formatted(file.getName(), type, gameFlag, 720));
|
||||
} else {
|
||||
// PGL content
|
||||
builder.append("%s\t\t%s\t%s\t\t%s\r\n".formatted(file.getName(), type, i + 1, file.length()));
|
||||
}
|
||||
}
|
||||
|
||||
// Send result
|
||||
ctx.result(builder.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -92,33 +133,45 @@ public class DlsHandler implements HttpHandler {
|
||||
private void handleRetrieveDlcContent(DlsRequest request, Context ctx) throws IOException {
|
||||
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());
|
||||
String type = getDlcType(request.attr1());
|
||||
File file = user.hasDlcOverride(type) ? user.getDlcOverride(type) : type != null
|
||||
? new File(rootDirectory, "%s/%s/%s".formatted(gameCode, type, request.dlcName()))
|
||||
: new File(rootDirectory, "%s/%s".formatted(gameCode, request.dlcName()));
|
||||
|
||||
// Check if the requested DLC exists
|
||||
if(dlc == null) {
|
||||
if(!file.exists()) {
|
||||
ctx.status(HttpStatus.NOT_FOUND);
|
||||
return;
|
||||
}
|
||||
|
||||
// Write DLC data
|
||||
try(FileInputStream inputStream = new FileInputStream(dlc.path())) {
|
||||
LEOutputStream outputStream = new LEOutputStream(ctx.outputStream());
|
||||
inputStream.transferTo(outputStream);
|
||||
|
||||
// If checksum is not part of the file, manually append it
|
||||
if(!dlc.checksumEmbedded()) {
|
||||
outputStream.writeShort(dlc.checksum());
|
||||
}
|
||||
byte[] bytes = Files.readAllBytes(file.toPath());
|
||||
|
||||
if(type == null) {
|
||||
// Generation 4 Mystery Gift
|
||||
bytes = MysteryGiftUtility.createUniversalGiftData4(bytes, file.getName());
|
||||
} else if(type.equals("MYSTERY")) {
|
||||
// Generation 5 Mystery Gift
|
||||
bytes = MysteryGiftUtility.createUniversalGiftData5(bytes);
|
||||
}
|
||||
|
||||
// Send result
|
||||
ctx.result(bytes);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST handler for {@code /download action=count}
|
||||
*/
|
||||
private void handleRetrieveDlcCount(DlsRequest request, Context ctx) throws IOException {
|
||||
ctx.result("1"); // TODO
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The game serial that should be used for downloading DLC based on the provided input.
|
||||
*/
|
||||
private String getDlcGameCode(String gameCode) {
|
||||
return switch(gameCode) {
|
||||
case "IRAJ" -> "IRAO";
|
||||
return switch(gameCode.substring(0, 3)) {
|
||||
case "IRA" -> "IRAO"; // BW & B2W2
|
||||
case "ADA", "CPU", "IPG" -> "ADAE"; // DPPt & HGSS
|
||||
default -> gameCode;
|
||||
};
|
||||
}
|
||||
@@ -126,13 +179,15 @@ public class DlsHandler implements HttpHandler {
|
||||
/**
|
||||
* @return The DLC type without the region identifier, or the input if it is an unknown type.
|
||||
*/
|
||||
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;
|
||||
};
|
||||
private String getDlcType(String attr1) {
|
||||
if(attr1 == null || !attr1.contains("_")) {
|
||||
return attr1;
|
||||
}
|
||||
|
||||
return attr1.substring(0, attr1.lastIndexOf('_'));
|
||||
}
|
||||
|
||||
private File getDlcDirectory(String gameCode, String dlcType) {
|
||||
return dlcType == null ? new File(rootDirectory, gameCode) : new File(rootDirectory, "%s/%s".formatted(gameCode, dlcType));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,14 @@ public record DlsRequest(
|
||||
@JsonProperty(value = "action", required = true) String action,
|
||||
@JsonProperty("gamecd") String dlcGameCode,
|
||||
@JsonProperty("contents") String dlcName, // action=contents
|
||||
@JsonProperty("attr1") String dlcType, // action=list
|
||||
@JsonProperty("attr2") int dlcIndex, // action=list
|
||||
@JsonProperty("offset") int offset, // ?
|
||||
@JsonProperty("num") int num) {} // ?
|
||||
@JsonProperty("attr1") String attr1, // action=list
|
||||
@JsonProperty("attr2") String attr2, // action=list
|
||||
@JsonProperty("offset") int offset, // Start offset in the list
|
||||
@JsonProperty("num") int num) { // Number of entries
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return ("DlsRequest[gameCode=%s, action=%s, dlcGameCode=%s, dlcName=%s, attr1=%s, attr2=%s, offset=%s, num=%s]")
|
||||
.formatted(gameCode, action, dlcGameCode, dlcName, attr1, attr2, offset, num);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,9 @@ package entralinked.network.http.nas;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
||||
|
||||
@@ -21,6 +24,7 @@ import io.javalin.http.Context;
|
||||
*/
|
||||
public class NasHandler implements HttpHandler {
|
||||
|
||||
private static final Logger logger = LogManager.getLogger();
|
||||
private final ObjectMapper mapper = new ObjectMapper(new UrlEncodedFormFactory()).registerModule(new JavaTimeModule());
|
||||
private final Configuration configuration;
|
||||
private final UserManager userManager;
|
||||
@@ -42,6 +46,7 @@ public class NasHandler implements HttpHandler {
|
||||
private void handleNasRequest(Context ctx) throws IOException {
|
||||
// Deserialize body into a request object
|
||||
NasRequest request = mapper.readValue(ctx.body(), NasRequest.class);
|
||||
logger.debug("Received {}", request);
|
||||
|
||||
// Determine handler function based on request action
|
||||
HttpRequestHandler<NasRequest> handler = switch(request.action()) {
|
||||
@@ -61,6 +66,7 @@ public class NasHandler implements HttpHandler {
|
||||
private void handleLogin(NasRequest request, Context ctx) throws IOException {
|
||||
// Make sure branch code is present
|
||||
if(request.branchCode() == null) {
|
||||
logger.debug("Rejecting NAS login request because no branch code is present");
|
||||
result(ctx, NasReturnCode.BAD_REQUEST);
|
||||
return;
|
||||
}
|
||||
@@ -77,7 +83,7 @@ public class NasHandler implements HttpHandler {
|
||||
|
||||
// Try to register, if the configuration allows it
|
||||
if(!UserManager.isValidUserId(userId) || userManager.doesUserExist(userId)
|
||||
|| !userManager.registerUser(userId, request.password())) {
|
||||
|| userManager.registerUser(userId, request.password()) == null) {
|
||||
// Oh well, try again!
|
||||
result(ctx, NasReturnCode.USER_NOT_FOUND);
|
||||
return;
|
||||
@@ -85,10 +91,12 @@ public class NasHandler implements HttpHandler {
|
||||
|
||||
// Should *never* return null in this location
|
||||
user = userManager.authenticateUser(userId, request.password());
|
||||
logger.info("Created account for user {}", user.getRedactedId());
|
||||
}
|
||||
|
||||
// Prepare GameSpy server credentials
|
||||
ServiceCredentials credentials = userManager.createServiceSession(user, "gamespy", request.branchCode());
|
||||
logger.info("Created GameSpy session for user {}", user.getRedactedId());
|
||||
result(ctx, new NasLoginResponse("gamespy.com", credentials.authToken(), credentials.challenge()));
|
||||
}
|
||||
|
||||
@@ -105,11 +113,14 @@ public class NasHandler implements HttpHandler {
|
||||
}
|
||||
|
||||
// Try to register user
|
||||
if(!userManager.registerUser(userId, request.password())) {
|
||||
User user = userManager.registerUser(userId, request.password());
|
||||
|
||||
if(user == null) {
|
||||
result(ctx, NasReturnCode.INTERNAL_SERVER_ERROR);
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info("Created account for user {}", user.getRedactedId());
|
||||
result(ctx, NasReturnCode.REGISTRATION_SUCCESS);
|
||||
}
|
||||
|
||||
@@ -125,15 +136,19 @@ public class NasHandler implements HttpHandler {
|
||||
return;
|
||||
}
|
||||
|
||||
String type = request.serviceType();
|
||||
|
||||
// Determine service location from type
|
||||
String service = switch(request.serviceType()) {
|
||||
String service = switch(type) {
|
||||
case "0000" -> "external"; // External game-specific service
|
||||
case "9000" -> "dls1.nintendowifi.net"; // Download server
|
||||
default -> throw new IllegalArgumentException("Invalid service type: " + request.serviceType());
|
||||
default -> throw new IllegalArgumentException("Invalid service type: " + type);
|
||||
};
|
||||
|
||||
|
||||
// Prepare user credentials
|
||||
ServiceCredentials credentials = userManager.createServiceSession(user, service, null);
|
||||
logger.info("Created {} session for user {}",
|
||||
type.equals("0000") ? "PGL" : type.equals("9000") ? "DLS1" : "this should never be logged", user.getRedactedId());
|
||||
result(ctx, new NasServiceLocationResponse(true, service, credentials.authToken()));
|
||||
}
|
||||
|
||||
|
||||
@@ -29,4 +29,11 @@ public record NasRequest(
|
||||
// Request-specific info
|
||||
@JsonProperty(value = "action", required = true) String action,
|
||||
@JsonProperty("gsbrcd") String branchCode, // action=login
|
||||
@JsonProperty("svc") String serviceType) {} // action=SVCLOC
|
||||
@JsonProperty("svc") String serviceType) { // action=SVCLOC
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return ("NasRequest[gameCode=%s, makerCode=%s, unitCode=%s, sdkVersion=%s, language=%s, action=%s, serviceType=%s]")
|
||||
.formatted(gameCode, makerCode, unitCode, sdkVersion, language, action, serviceType);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package entralinked.network.http.pgl;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
@@ -16,7 +17,6 @@ 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;
|
||||
@@ -93,6 +93,7 @@ public class PglHandler implements HttpHandler {
|
||||
|
||||
if(credentials == null ||
|
||||
!username.equals(credentials.getUsername()) || !password.equals(credentials.getPassword())) {
|
||||
logger.debug("Rejecting PGL request because the auth credentials were incorrect");
|
||||
ctx.status(HttpStatus.UNAUTHORIZED);
|
||||
clearTasks(ctx);
|
||||
return;
|
||||
@@ -100,11 +101,13 @@ public class PglHandler implements HttpHandler {
|
||||
|
||||
// Deserialize the request
|
||||
PglRequest request = mapper.readValue(ctx.queryString(), PglRequest.class);
|
||||
logger.debug("Received {}", request);
|
||||
|
||||
// Verify the service session token
|
||||
ServiceSession session = userManager.getServiceSession(request.token(), "external");
|
||||
|
||||
if(session == null) {
|
||||
logger.debug("Rejecting PGL request because the service session has expired");
|
||||
ctx.status(HttpStatus.UNAUTHORIZED);
|
||||
clearTasks(ctx);
|
||||
return;
|
||||
@@ -131,7 +134,7 @@ public class PglHandler implements HttpHandler {
|
||||
};
|
||||
|
||||
// Handle the request
|
||||
handler.process(request, ctx);
|
||||
handler.process(request, ctx);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -198,13 +201,14 @@ public class PglHandler implements HttpHandler {
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info("Player {} is downloading save data as user {}", player.getGameSyncId(), user.getRedactedId());
|
||||
|
||||
// Write status code
|
||||
writeStatusCode(outputStream, 0);
|
||||
|
||||
// Allow it to wake up anyway, maybe the poor sap is stuck..
|
||||
// Just don't send any other data.
|
||||
if(player.getStatus() == PlayerStatus.AWAKE) {
|
||||
logger.info("Player {} is downloading save data, but is already awake!", player.getGameSyncId());
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -221,8 +225,7 @@ public class PglHandler implements HttpHandler {
|
||||
// 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));
|
||||
user.setDlcOverride(cgearType, player.getCGearSkinFile());
|
||||
} else {
|
||||
cgearSkinIndex = dlcList.getDlcIndex("IRAO", cgearType, cgearSkin);
|
||||
user.removeDlcOverride(cgearType);
|
||||
@@ -231,8 +234,7 @@ public class PglHandler implements HttpHandler {
|
||||
// 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));
|
||||
user.setDlcOverride("ZUKAN", player.getDexSkinFile());
|
||||
} else {
|
||||
dexSkinIndex = dlcList.getDlcIndex("IRAO", "ZUKAN", dexSkin);
|
||||
user.removeDlcOverride("ZUKAN");
|
||||
@@ -309,15 +311,15 @@ public class PglHandler implements HttpHandler {
|
||||
byte[] nameBytes = visitor.name().getBytes(StandardCharsets.UTF_16LE);
|
||||
outputStream.write(nameBytes, 0, Math.min(14, nameBytes.length));
|
||||
outputStream.writeBytes(-1, 14 - nameBytes.length);
|
||||
outputStream.writeShort(0xFF); // Null terminator
|
||||
|
||||
// Full visitor type consists of a trainer class and what I call a 'personality' index
|
||||
// that, along with the trainer class, determines which phrases the visitor uses.
|
||||
// The shope type is calculated in such an odd manner because for some reason,
|
||||
// The shop type is calculated in such an odd manner because for some reason,
|
||||
// the 'starting' index of the shop type used increases by 2 for each visitor type.
|
||||
// For example, if the visitor type is '0', then shop type '0' would be a raffle.
|
||||
// However, if the visitor type is '2', then shop type '0' results in a dojo instead.
|
||||
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
|
||||
@@ -343,6 +345,7 @@ public class PglHandler implements HttpHandler {
|
||||
private void handleMemoryLink(PglRequest request, Context ctx) throws IOException {
|
||||
LEOutputStream outputStream = new LEOutputStream(ctx.outputStream());
|
||||
Player player = playerManager.getPlayer(request.gameSyncId());
|
||||
User user = ctx.attribute("user");
|
||||
|
||||
// Check if player exists
|
||||
if(player == null) {
|
||||
@@ -350,6 +353,12 @@ public class PglHandler implements HttpHandler {
|
||||
return;
|
||||
}
|
||||
|
||||
// Version null check because this can happen in specific cases
|
||||
if(player.getGameVersion() == null) {
|
||||
writeStatusCode(outputStream, 5); // No game save data exists for this Game Sync ID
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the save file belongs to Black or White
|
||||
if(player.getGameVersion().isVersion2()) {
|
||||
writeStatusCode(outputStream, 10); // Not a Black or White save
|
||||
@@ -364,6 +373,8 @@ public class PglHandler implements HttpHandler {
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info("User {} is Memory Linking with player {}", user.getRedactedId(), player.getGameSyncId());
|
||||
|
||||
// Send the save data!
|
||||
try(FileInputStream inputStream = new FileInputStream(file)) {
|
||||
writeStatusCode(outputStream, 0);
|
||||
@@ -422,12 +433,11 @@ public class PglHandler implements HttpHandler {
|
||||
* POST handler for {@code /dsio/gw?p=savedata.upload}
|
||||
*/
|
||||
private void handleUploadSaveData(PglRequest request, Context ctx) throws IOException {
|
||||
// Prepare response
|
||||
LEOutputStream outputStream = new LEOutputStream(ctx.outputStream());
|
||||
Player player = playerManager.getPlayer(request.gameSyncId());
|
||||
User user = ctx.attribute("user");
|
||||
|
||||
// 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
|
||||
|| (!configuration.allowOverwritingPlayerDreamInfo() && player.getStatus() != PlayerStatus.AWAKE)
|
||||
|| (!configuration.allowPlayerGameVersionMismatch() && player.getGameVersion() != null
|
||||
@@ -444,6 +454,8 @@ public class PglHandler implements HttpHandler {
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info("Player {} is uploading save data as user {}", player.getGameSyncId(), user.getRedactedId());
|
||||
|
||||
// Try to store save data
|
||||
if(!playerManager.storePlayerGameSaveFile(player, ctx.bodyInputStream())) {
|
||||
writeStatusCode(outputStream, 4); // Game save data IO error
|
||||
@@ -477,13 +489,9 @@ public class PglHandler implements HttpHandler {
|
||||
/**
|
||||
* POST handler for {@code /dsio/gw?p=account.create.upload}
|
||||
*/
|
||||
private void handleCreateAccount(PglRequest request, Context ctx) throws IOException {
|
||||
// It sends the entire save file, but we just skip through it because we don't need anything from it here
|
||||
ServletInputStream inputStream = ctx.req().getInputStream();
|
||||
|
||||
while(!inputStream.isFinished()) {
|
||||
inputStream.read();
|
||||
}
|
||||
private void handleCreateAccount(PglRequest request, Context ctx) throws IOException {
|
||||
// Have to read all the bytes first for some reason
|
||||
byte[] bytes = ctx.bodyAsBytes();
|
||||
|
||||
// Prepare response
|
||||
LEOutputStream outputStream = new LEOutputStream(ctx.outputStream());
|
||||
@@ -501,11 +509,19 @@ public class PglHandler implements HttpHandler {
|
||||
}
|
||||
|
||||
// Try to register player
|
||||
if(playerManager.registerPlayer(request.gameSyncId()) == null) {
|
||||
Player player = playerManager.registerPlayer(request.gameSyncId(), request.gameVersion());
|
||||
|
||||
if(player == null) {
|
||||
writeStatusCode(outputStream, 3); // Registration error
|
||||
return;
|
||||
}
|
||||
|
||||
// Try to store save data
|
||||
if(!playerManager.storePlayerGameSaveFile(player, new ByteArrayInputStream(bytes))) {
|
||||
writeStatusCode(outputStream, 4); // Game save data IO error
|
||||
return;
|
||||
}
|
||||
|
||||
// Write status code
|
||||
writeStatusCode(outputStream, 0);
|
||||
}
|
||||
@@ -526,7 +542,8 @@ public class PglHandler implements HttpHandler {
|
||||
}
|
||||
|
||||
// Try to register player
|
||||
if(playerManager.registerPlayer(gameSyncId) == null) {
|
||||
// Regrettably, this request does not contain game save & version data.
|
||||
if(playerManager.registerPlayer(gameSyncId, null) == null) {
|
||||
writeStatusCode(outputStream, 3); // Registration error
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -17,4 +17,11 @@ public record PglRequest(
|
||||
public GameVersion gameVersion() {
|
||||
return GameVersion.lookup(romCode(), languageCode());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
// Exlude token
|
||||
return "PglRequest[gameSyncId=%s, type=%s, romCode=%s, languageCode=%s, dreamWorld=%s]"
|
||||
.formatted(gameSyncId, type, romCode, languageCode, dreamWorld);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package entralinked.utility;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
|
||||
@@ -18,9 +21,23 @@ public class MD5 {
|
||||
private static MessageDigest digest;
|
||||
|
||||
/**
|
||||
* @return A hex-formatted MD5 hash of the specified input.
|
||||
* @return A hex-formatted MD5 hash of the specified input string.
|
||||
*/
|
||||
public static String digest(String string) {
|
||||
return StringUtil.toHexStringPadded(digest(string.getBytes(StandardCharsets.ISO_8859_1)));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return A hex-formatted MD5 hash of the specified input file.
|
||||
*/
|
||||
public static String digest(File file) throws IOException {
|
||||
return StringUtil.toHexStringPadded(digest(Files.readAllBytes(file.toPath())));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return An MD5 hash of the specified input.
|
||||
*/
|
||||
public static byte[] digest(byte[] bytes) {
|
||||
if(digest == null) {
|
||||
try {
|
||||
digest = MessageDigest.getInstance("MD5");
|
||||
@@ -29,6 +46,6 @@ public class MD5 {
|
||||
}
|
||||
}
|
||||
|
||||
return StringUtil.toHexStringPadded(digest.digest(string.getBytes(StandardCharsets.ISO_8859_1)));
|
||||
return digest.digest(bytes);
|
||||
}
|
||||
}
|
||||
|
||||
94
src/main/java/entralinked/utility/MysteryGiftUtility.java
Normal file
94
src/main/java/entralinked/utility/MysteryGiftUtility.java
Normal file
@@ -0,0 +1,94 @@
|
||||
package entralinked.utility;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
import org.bouncycastle.util.Arrays;
|
||||
|
||||
/**
|
||||
* Haphazardly thrown-together utility for generating DLS Mystery Gift data & removing version/language locks.
|
||||
*/
|
||||
public class MysteryGiftUtility {
|
||||
|
||||
public static final String DPPGS_CHARTABLE = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖרÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿŒœŞşªºááá$¡¿!?,.…·/‘'“”„«»()♂♀+-*#=&~:;♠♣♥♦★◉○□△◇@♪%ááááááááááá ";
|
||||
|
||||
private static byte[] encodeDPPGS(String string) {
|
||||
int length = string.length();
|
||||
byte[] bytes = new byte[length * 2];
|
||||
|
||||
for(int i = 0; i < length; i++) {
|
||||
char character = string.charAt(i);
|
||||
int encoded = character == '\n' ? 0xE000 : DPPGS_CHARTABLE.indexOf(character) + 0x121;
|
||||
|
||||
if(encoded == 0x120) {
|
||||
encoded = 0x1DE;
|
||||
}
|
||||
|
||||
bytes[i * 2] = (byte)(encoded & 0xFF);
|
||||
bytes[i * 2 + 1] = (byte)((encoded >> 8) & 0xFF);
|
||||
}
|
||||
|
||||
return bytes;
|
||||
}
|
||||
|
||||
public static byte[] createUniversalGiftData4(byte[] bytes) {
|
||||
return createUniversalGiftData4(bytes, "Here's your Mystery Gift.\nEnjoy!");
|
||||
}
|
||||
|
||||
public static byte[] createUniversalGiftData4(byte[] bytes, String title) {
|
||||
// Check data size
|
||||
if(bytes.length > 936) {
|
||||
throw new IllegalArgumentException("Data too large: %s".formatted(bytes.length));
|
||||
}
|
||||
|
||||
byte[] result = new byte[936];
|
||||
|
||||
if(bytes.length <= 856) {
|
||||
// Create gift title data
|
||||
System.arraycopy(bytes, 0x00, result, 0x50, bytes.length);
|
||||
Arrays.fill(result, 0x00, 0x48, (byte)0xFF);
|
||||
byte[] titleBytes = encodeDPPGS(title);
|
||||
System.arraycopy(titleBytes, 0, result, 0, Math.min(0x48, titleBytes.length));
|
||||
|
||||
// Wonder card index (prevents duplicate redemptions)
|
||||
int id = Crc16.calc(result, 0x00, 0x3A8); // Let's just use the checksum for now
|
||||
result[0x4C] = (byte)(id & 0xFF);
|
||||
result[0x4D] = (byte)((id >> 8) & 0xFF);
|
||||
|
||||
// Wonder card present flag?
|
||||
result[0x4E] = (byte)(bytes.length == 0x358 ? 0x0D : 0x00);
|
||||
} else {
|
||||
System.arraycopy(bytes, 0, result, 0, bytes.length);
|
||||
}
|
||||
|
||||
// Clear game version
|
||||
result[0x48] = 0;
|
||||
result[0x49] = 0;
|
||||
return result;
|
||||
}
|
||||
|
||||
public static byte[] createUniversalGiftData5(byte[] bytes) {
|
||||
// Check data size
|
||||
if(bytes.length > 720) {
|
||||
throw new IllegalArgumentException("Data too large: %s".formatted(bytes.length));
|
||||
}
|
||||
|
||||
byte[] result = new byte[720];
|
||||
System.arraycopy(bytes, 0, result, 0, bytes.length);
|
||||
result[0xCE] = 0; // Version flag
|
||||
result[0x2CB] = 0; // Language code
|
||||
|
||||
// Create standard gift description if there is none
|
||||
if(bytes.length == 204) {
|
||||
Arrays.fill(result, 0xD0, 0x2CA, (byte)0xFF);
|
||||
String description = "No description is available for this gift.";
|
||||
byte[] descriptionBytes = description.replace('\n', '\uFFFE').getBytes(StandardCharsets.UTF_16LE);
|
||||
System.arraycopy(descriptionBytes, 0, result, 0xD0, Math.min(0x1FA, descriptionBytes.length));
|
||||
}
|
||||
|
||||
// Recalculate checksum
|
||||
int checksum = Crc16.calc(result, 0, 0x2CE);
|
||||
result[0x2CE] = (byte)(checksum & 0xFF);
|
||||
result[0x2CF] = (byte)((checksum >> 8) & 0xFF);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -6,9 +6,12 @@
|
||||
<body>
|
||||
<div class="root-container">
|
||||
<div>
|
||||
<label for="gsid">Game Sync ID</label><br>
|
||||
<input type='text' id="gsid" name='gsid' placeholder='XXXXXXXXXX'>
|
||||
<button id="login" onclick="postLogin()">Login</button>
|
||||
<form onsubmit="return postLogin()">
|
||||
<label for="gsid">Game Sync ID</label><br>
|
||||
<input type="text" id="gsid" name="gsid" placeholder="XXXXXXXXXX" autocomplete="on">
|
||||
<label for="submit" class="big-button">Login</label>
|
||||
<input type="submit" id="submit" hidden/>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
@@ -10,4 +10,6 @@ function postLogin() {
|
||||
window.location.href = "/dashboard/profile.html";
|
||||
}
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -11,21 +11,22 @@ input {
|
||||
border-radius: 10px;
|
||||
width: 100%;
|
||||
margin-top: 4px;
|
||||
margin-bottom: 16px;
|
||||
margin-bottom: 24px;
|
||||
padding: 4px 4px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
button {
|
||||
.big-button {
|
||||
color: white;
|
||||
background-color: #3D3D3D;
|
||||
border: 0px;
|
||||
border-radius: 10px;
|
||||
font-size: 16px;
|
||||
font-family: sans-serif;
|
||||
padding: 16px 32px;
|
||||
}
|
||||
|
||||
button:active {
|
||||
.big-button:active {
|
||||
background-color: #333333;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,59 +1,62 @@
|
||||
/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
|
||||
/dlc/IRAO/CGEAR/01 - Default.bin 19c71995ffb00f4bdc59bfa805eee8fa
|
||||
/dlc/IRAO/CGEAR/02 - Meadow Munna.bin d434decfee8faad9485560fbc9206273
|
||||
/dlc/IRAO/CGEAR/03 - Aim for the Top.bin c93f457ae1310bfe03f7ede622c7f63a
|
||||
/dlc/IRAO/CGEAR/04 - Twinkle Minccino.bin 86902dc9ec625e05673697367d492898
|
||||
/dlc/IRAO/CGEAR/05 - PokeSma Purrloin.bin 0efe27a2cf7e48de2273ccba0fda2031
|
||||
/dlc/IRAO/CGEAR/06 - Earful Audino.bin ee383864a67e6a04235c7f6a891a9539
|
||||
/dlc/IRAO/CGEAR/07 - Happy Piplup.bin 88d0dfac0dda854486b8b5fb839e0bfd
|
||||
/dlc/IRAO/CGEAR/08 - Poison Jab Croagunk.bin 7294c3e4db163beb98ea49be10d8eb2a
|
||||
/dlc/IRAO/CGEAR/09 - Venusaur!.bin 7c51d55a63117bb166cc507f064f7cca
|
||||
/dlc/IRAO/CGEAR/10 - Charizard!.bin a8ccb7866393efda3422656aa2d8f292
|
||||
/dlc/IRAO/CGEAR/11 - Blastoise!.bin e0a1b44450407af394f390c0b229e7a6
|
||||
/dlc/IRAO/CGEAR/12 - V for Victory!.bin 847d6d230fe2513758d50adac52d54b8
|
||||
/dlc/IRAO/CGEAR/13 - Virtual Pokemon.bin 002263910ed018ff04922c4f3971b777
|
||||
/dlc/IRAO/CGEAR/14 - Pokemon Cafe.bin efd6e6de86ea892dda840bcc6952afc1
|
||||
/dlc/IRAO/CGEAR/15 - Hero Reshiram.bin d89ceeb9a9e7774fa802c1aa2f38409a
|
||||
/dlc/IRAO/CGEAR/16 - Hero Zekrom.bin 2548f0ed48e130649ff351ca418e3b7f
|
||||
/dlc/IRAO/CGEAR/17 - 2011 Worlds C-Gear.bin 70119e0a5e67fb4c9dda7b724e157784
|
||||
/dlc/IRAO/CGEAR/18 - Zoroark!.bin 81e9241acb3270e2d490dd9966f3ecf1
|
||||
/dlc/IRAO/CGEAR/19 - Spring Deerling.bin ca785e48d49269a182ee0fe0981168a8
|
||||
/dlc/IRAO/CGEAR/20 - Summer Deerling.bin 0e8b6b527a411aeb302cfe07cb3679b2
|
||||
/dlc/IRAO/CGEAR/21 - Autumn Deerling.bin 7155fca4dc915fc6cb87495fa7a9c743
|
||||
/dlc/IRAO/CGEAR/22 - Winter Deerling.bin 7f9d3448f40d0ea513c1f94e311e77bb
|
||||
/dlc/IRAO/CGEAR/23 - CRUSTLE!.bin eb1a0cb83e330375c257cb61bbc80e84
|
||||
/dlc/IRAO/CGEAR/24 - KLINK!.bin f4ebe5cbb0de53c44f0c620215e840b4
|
||||
/dlc/IRAO/CGEAR/25 - Ducklett Friends.bin bb359435efd5e98f48beba2cf71126d9
|
||||
/dlc/IRAO/CGEAR/26 - Guidance Cobalion.bin fcd86f0f98b1d770e28001c02f455c58
|
||||
/dlc/IRAO/CGEAR/27 - Trial Terrakion.bin 6c57b06edcaa0aeb6b716ecd5d3f2336
|
||||
/dlc/IRAO/CGEAR/28 - Rumination Virizion.bin b02dce72dfd25a4c8d417b87f3a99329
|
||||
/dlc/IRAO/CGEAR/29 - Keldeo Ordinary Forme.bin a83088c9cf11277049ed472d302b0dd6
|
||||
/dlc/IRAO/CGEAR/30 - Aria of the Night Sky.bin 4306211f215bb0d397c92037812d37ae
|
||||
/dlc/IRAO/CGEAR/31 - 2012 Worlds C-Gear.bin 225505e34dab2ccd300e3b4bd7eda47f
|
||||
/dlc/IRAO/CGEAR/32 - Red Genesect.bin 614be39e965dab594a5ffaac6bd5ae82
|
||||
/dlc/IRAO/CGEAR/33 - PCN Cup 2024 Summer.bin 017f64854e984a08d5fa5194f1a492f6
|
||||
/dlc/IRAO/CGEAR2/01 - Default.bin d7ab435f5016ac3e6a7f444ccd25f424
|
||||
/dlc/IRAO/CGEAR2/02 - Meadow Munna.bin 98b944531d9206512bbc5f59f8229139
|
||||
/dlc/IRAO/CGEAR2/03 - Keldeo Resolute Forme.bin e7bddaf878faa5b009caee7d96998954
|
||||
/dlc/IRAO/CGEAR2/04 - 2012 Worlds C-Gear.bin 36d5948c310428b251b70ec1dcf0845a
|
||||
/dlc/IRAO/CGEAR2/05 - Pumpkin Pikachu.bin 7fd54cd2bd5310c40139b4329124aa2d
|
||||
/dlc/IRAO/CGEAR2/06 - V for Victory!.bin 4f9a1ca435561cfe49d892488b398caa
|
||||
/dlc/IRAO/CGEAR2/07 - Hero Reshiram.bin ec233268fb4d04cbb13b8b944552e94b
|
||||
/dlc/IRAO/CGEAR2/08 - Hero Zekrom.bin 4647c982bbbd3955b4d8e4c9309ff7ef
|
||||
/dlc/IRAO/CGEAR2/09 - Aria of the Night Sky.bin 2f609aeda35b34b6386cfa66d57e0ad5
|
||||
/dlc/IRAO/CGEAR2/10 - Sleeping Eevee.bin 4882e949ae9c6de8cb2735942f0b1624
|
||||
/dlc/IRAO/CGEAR2/11 - Venusaur!.bin 21becbbbba091cb1060ed19c4436d98f
|
||||
/dlc/IRAO/CGEAR2/12 - Charizard!.bin 75705d719bae83ac130e03598d85382d
|
||||
/dlc/IRAO/CGEAR2/13 - Blastoise!.bin 4f41e613d79826f48b8f8b9d1aa5747b
|
||||
/dlc/IRAO/CGEAR2/14 - Red Genesect.bin 0d0951b899bff3d9e243a9dc61aff973
|
||||
/dlc/IRAO/CGEAR2/15 - Black Kyurem.bin ff4356dac514c364557bd685c932f43e
|
||||
/dlc/IRAO/CGEAR2/16 - White Kyurem.bin e51c69805f87a954a7fd563b920cc036
|
||||
/dlc/IRAO/CGEAR2/17 - PCN Cup 2024 Summer.bin 3c17d7bedeeb89a43daddac8c865f1e1
|
||||
/dlc/IRAO/MUSICAL/01 - Charming Munna.bin dd180c539b51305244c10380266ec2ac
|
||||
/dlc/IRAO/MUSICAL/02 - MELOETTAAA!!!.bin f1a18eaf27c60b43020e9d5c0195eaed
|
||||
/dlc/IRAO/ZUKAN/01 - Default (Pink).bin 7150719a18ffacef993c58b112cfaf52
|
||||
/dlc/IRAO/ZUKAN/02 - Default (Red).bin bc396d6403d9d296c1b4700d31b6eb42
|
||||
/dlc/IRAO/ZUKAN/03 - Unova Trio (Pink).bin 2e80bbc0e824c9c17fb0b5480d6bcb7c
|
||||
/dlc/IRAO/ZUKAN/04 - Unova Trio (Red).bin b3e3eadbad327a8c4b3889768f79fd8e
|
||||
/dlc/IRAO/ZUKAN/05 - Kanto Trio (Pink).bin abbe82e2ff4c66d0557b9989da3f5c29
|
||||
/dlc/IRAO/ZUKAN/06 - Kanto Trio (Red).bin 7ba1f19f564e271d2e92810e174cd75e
|
||||
/dlc/IRAO/ZUKAN/07 - Hugh.bin 2f74e77b34edc1758226778e9f5e91fd
|
||||
/dlc/IRAO/ZUKAN/08 - Bianca.bin f5c57f66af3e096c65850ee47882be90
|
||||
/dlc/IRAO/ZUKAN/09 - Cheren.bin 2dad78cb515505a5de8022bbd8ecd2ca
|
||||
/dlc/IRAO/MYSTERY/PCN Cup 2024 Summer.bin fed0ff82be6903777b2f107ce00e8e4c
|
||||
|
||||
BIN
src/main/resources/dlc/IRAO/CGEAR/33 - PCN Cup 2024 Summer.bin
Normal file
BIN
src/main/resources/dlc/IRAO/CGEAR/33 - PCN Cup 2024 Summer.bin
Normal file
Binary file not shown.
BIN
src/main/resources/dlc/IRAO/CGEAR2/17 - PCN Cup 2024 Summer.bin
Normal file
BIN
src/main/resources/dlc/IRAO/CGEAR2/17 - PCN Cup 2024 Summer.bin
Normal file
Binary file not shown.
BIN
src/main/resources/dlc/IRAO/MYSTERY/PCN Cup 2024 Summer.bin
Normal file
BIN
src/main/resources/dlc/IRAO/MYSTERY/PCN Cup 2024 Summer.bin
Normal file
Binary file not shown.
@@ -1,25 +1,39 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE xml>
|
||||
<Configuration status="off" packages="entralinked.utility">
|
||||
<Configuration status="WARN" packages="entralinked.utility">
|
||||
<Properties>
|
||||
<Property name="LOGS_DIR">logs</Property>
|
||||
<Property name="PATTERN_MINIMAL">%d{yyyy-MM-dd HH:mm:ss.SSS} %5level : %msg%n</Property>
|
||||
<Property name="PATTERN_DETAILED">%d{yyyy-MM-dd HH:mm:ss.SSS} %5level [%32t] %-48c{1.2*} : %msg%n</Property>
|
||||
</Properties>
|
||||
<Appenders>
|
||||
<Console name="Console" target="SYSTEM_OUT">
|
||||
<PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss.SSS} %5level [%32t] %-48c{1.2*} : %msg%n"/>
|
||||
<PatternLayout pattern="${PATTERN_DETAILED}"/>
|
||||
<ThresholdFilter level="INFO"/>
|
||||
</Console>
|
||||
<RollingFile name="LogFile" fileName="logs/latest.log" filePattern="logs/%d{yyyy-MM-dd}-%i.log.gz">
|
||||
<PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss.SSS} %5level [%16t] %-48c{1.2*} : %msg%n"/>
|
||||
<RollingFile name="LogFile" fileName="${LOGS_DIR}/latest.log" filePattern="${LOGS_DIR}/%d{yyyy-MM-dd}-%i.log.gz">
|
||||
<PatternLayout pattern="${PATTERN_DETAILED}"/>
|
||||
<ThresholdFilter level="INFO"/>
|
||||
<Policies>
|
||||
<OnStartupTriggeringPolicy/>
|
||||
</Policies>
|
||||
</RollingFile>
|
||||
<ConsumerAppender name="GuiOutput">
|
||||
<PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss.SSS} %5level : %msg%n"/>
|
||||
<PatternLayout pattern="${PATTERN_MINIMAL}"/>
|
||||
<ThresholdFilter level="INFO"/>
|
||||
</ConsumerAppender>
|
||||
<File name="DebugLogFile" fileName="${LOGS_DIR}/debug.log" append="false">
|
||||
<PatternLayout pattern="${PATTERN_DETAILED}"/>
|
||||
<ThresholdFilter level="DEBUG"/>
|
||||
</File>
|
||||
</Appenders>
|
||||
<Loggers>
|
||||
<Root level="info">
|
||||
<AppenderRef ref="Console"/>
|
||||
<Logger name="entralinked" additivity="false">
|
||||
<AppenderRef ref="LogFile"/>
|
||||
<AppenderRef ref="GuiOutput"/>
|
||||
</Root>
|
||||
<AppenderRef ref="DebugLogFile"/>
|
||||
<AppenderRef ref="Console"/>
|
||||
</Logger>
|
||||
<Root level="all"/>
|
||||
</Loggers>
|
||||
</Configuration>
|
||||
|
||||
Reference in New Issue
Block a user