28 Commits

Author SHA1 Message Date
kuroppoi
b38b26f9ce Refactor dashboard stuff & ditch magic numbers for drop-down lists (#9) 2023-07-08 18:22:15 +02:00
kuroppoi
a95e7e2f01 Switcheroo 2023-07-08 15:09:45 +02:00
kuroppoi
53f05acab9 Center popup forms 2023-07-08 02:35:11 +02:00
kuroppoi
49a15a0156 Change DLC names to be clearer & add missing skins 2023-07-08 02:13:17 +02:00
kuroppoi
329e1de30a Rely on DLC game code & type instead of names not being duplicate 2023-07-08 02:09:05 +02:00
kuroppoi
0e12cb54d7 Check if address is actually a local address before returning it 2023-07-07 03:34:44 +02:00
kuroppoi
8849265805 Add option to preview C-Gear & Pokédex skins 2023-07-06 22:52:42 +02:00
kuroppoi
0d964ec129 Sort DLC names alphabetically 2023-07-06 20:28:46 +02:00
kuroppoi
e4fff87b87 Empty default encounter move 2023-07-03 23:21:36 +02:00
kuroppoi
aae1abd18f Swap places of visitor configuration and item configuration 2023-07-03 22:16:22 +02:00
kuroppoi
038f7f5136 Update README.md 2023-07-02 22:44:13 +02:00
kuroppoi
f41888ba58 Fix scroll issue 2023-07-02 03:17:39 +02:00
kuroppoi
328aac45e1 Join Avenue visitor stuff (#7) 2023-07-02 02:56:33 +02:00
kuroppoi
806bd5cc17 Use a different centering strategy 2023-07-01 20:14:09 +02:00
kuroppoi
06e388ab89 Make user dashboard a bit more consistent 2023-06-30 20:49:50 +02:00
kuroppoi
60d6698f6b Add pixel image filter to list style 2023-06-30 20:46:59 +02:00
kuroppoi
4800f7b6c2 Send mock decor data in savedata.download (#6) 2023-06-30 19:52:13 +02:00
kuroppoi
a1e8cc97bc Change a couple labels 2023-06-30 15:48:35 +02:00
kuroppoi
2aebbac230 Update submodule 2023-06-30 15:40:22 +02:00
kuroppoi
09ce3a26ce Pad Trainer ID with leading zeroes 2023-06-30 15:38:52 +02:00
kuroppoi
e4f7a26f9c Add option to not specify a special move for encounters 2023-06-30 15:34:28 +02:00
kuroppoi
d90200e4ca Fix compatibility issue with Japanese Black & White versions 2023-06-30 00:25:30 +02:00
kuroppoi
0249ff8040 Update submodule 2023-06-29 17:09:35 +02:00
kuroppoi
1133a44360 Exclude unknown items from list view 2023-06-29 17:06:48 +02:00
kuroppoi
71cf227630 Deserialize birth date as string instead of MonthDay object (#4) 2023-06-29 12:46:21 +02:00
kuroppoi
2d346ab9b6 Add visual item list to dashboard 2023-06-29 03:18:41 +02:00
kuroppoi
1ff56e2be0 Add species list view + ability to download available gen V species 2023-06-28 18:20:41 +02:00
kuroppoi
26dbfd0edb Don't URL decode/encode values if base64 decoding/encoding is enabled 2023-06-28 03:47:44 +02:00
89 changed files with 3101 additions and 483 deletions

View File

@@ -2,8 +2,9 @@
[![build](https://github.com/kuroppoi/entralinked/actions/workflows/dist-upload-artifact.yml/badge.svg)](https://github.com/kuroppoi/entralinked/actions)
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.
## Building

View 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
}

View 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) {}

View File

@@ -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;
}
}

View File

@@ -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() {
@@ -83,26 +82,20 @@ public class DlcList {
// 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 +116,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 +161,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);
}
}

View 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) {}

View File

@@ -6,31 +6,34 @@ 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;
public Player(String gameSyncId, GameVersion gameVersion) {
public Player(String gameSyncId) {
this.gameSyncId = gameSyncId;
this.gameVersion = gameVersion;
}
public void resetDreamInfo() {
status = PlayerStatus.AWAKE;
gameVersion = null;
dreamerInfo = null;
encounters.clear();
items.clear();
avenueVisitors.clear();
levelsGained = 0;
cgearSkin = null;
dexSkin = null;
@@ -41,10 +44,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 +66,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 +85,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;
}

View File

@@ -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;
/**
@@ -22,19 +23,22 @@ public record PlayerDto(
String musical,
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.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);
@@ -42,6 +46,7 @@ public record PlayerDto(
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;
}
}

View File

@@ -16,8 +16,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)
*/
@@ -119,7 +117,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, 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);
@@ -127,7 +125,7 @@ public class PlayerManager {
}
// Construct player object
Player player = new Player(gameSyncId, gameVersion);
Player player = new Player(gameSyncId);
player.setStatus(PlayerStatus.AWAKE);
// Try to save player data

View File

@@ -1,11 +1,25 @@
package entralinked.network.http.dashboard;
import java.awt.image.BufferedImage;
import java.io.FileInputStream;
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 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,6 +31,7 @@ import entralinked.model.player.PlayerManager;
import entralinked.model.player.PlayerStatus;
import entralinked.network.http.HttpHandler;
import entralinked.utility.GsidUtility;
import entralinked.utility.TiledImageReader;
import io.javalin.Javalin;
import io.javalin.config.JavalinConfig;
import io.javalin.http.Context;
@@ -29,16 +44,37 @@ import io.javalin.json.JavalinJackson;
*/
public class DashboardHandler implements HttpHandler {
private static final Logger logger = LogManager.getLogger();
private final Map<Dlc, 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"));
for(Dlc skin : skins) {
try(FileInputStream inputStream = new FileInputStream(skin.path())) {
BufferedImage image =
skin.type().equals("ZUKAN") ? TiledImageReader.readDexSkin(inputStream) :
skin.type().equals("CGEAR") ? TiledImageReader.readCGearSkin(inputStream, true) :
TiledImageReader.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);
}
}
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/profile", this::handleUpdateProfile);
@@ -66,6 +102,31 @@ 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(404);
return;
}
Dlc dlc = dlcList.getDlc("IRAO", type, name);
// Check if DLC exists
if(dlc == null) {
ctx.status(404);
return;
}
// Write cached image data
ImageIO.write(skinPreviewCache.get(dlc), "png", ctx.outputStream());
}
/**
* GET request handler for {@code /dashboard/dlc}
*/
@@ -79,7 +140,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()));
}
/**
@@ -165,6 +226,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());
@@ -191,9 +253,9 @@ public class DashboardHandler implements HttpHandler {
}
for(DreamEncounter encounter : request.encounters()) {
if(encounter.species() < 1 || encounter.species() > 493) {
if(encounter.species() < 1 || encounter.species() > 649) {
return "Species is out of range.";
} 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.";
@@ -212,15 +274,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.";

View File

@@ -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;
@@ -16,10 +17,12 @@ public record DashboardProfileMessage(
String musical,
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.getLevelsGained(),
player.getEncounters(), player.getItems(), player.getAvenueVisitors());
}
}

View File

@@ -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,

View File

@@ -67,26 +67,22 @@ 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();
};
String gameCode = getDlcGameCode(request.dlcGameCode());
String type = getRegionlessDlcType(request.dlcType());
// 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());
String gameCode = getDlcGameCode(request.dlcGameCode());
String type = getRegionlessDlcType(request.dlcType());
Dlc dlc = dlcList.getDlc(gameCode, type, request.dlcName());
// Check if the requested DLC exists
if(dlc == null) {
ctx.status(HttpStatus.NOT_FOUND);
return;
@@ -103,4 +99,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;
};
}
}

View File

@@ -1,7 +1,6 @@
package entralinked.network.http.nas;
import java.time.LocalDateTime;
import java.time.MonthDay;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonFormat.Shape;
@@ -24,8 +23,8 @@ public record NasRequest(
@JsonProperty("bssid") String bssid,
@JsonProperty("apinfo") String accessPointInfo,
@JsonProperty("devname") String deviceName,
@JsonProperty("birth") String birthDate, // Hex, apparently
@JsonProperty("devtime") @JsonFormat(shape = Shape.STRING, pattern = "yyMMddHHmmss") LocalDateTime deviceTime,
@JsonProperty("birth") @JsonFormat(shape = Shape.STRING, pattern = "MMdd") MonthDay birthDate,
// Request-specific info
@JsonProperty(value = "action", required = true) String action,

View File

@@ -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,9 +15,11 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import entralinked.Configuration;
import entralinked.Entralinked;
import entralinked.model.avenue.AvenueVisitor;
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;
@@ -28,6 +31,7 @@ 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 +49,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 +99,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");
@@ -225,10 +228,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(dlcList.getDlcIndex("IRAO", player.getGameVersion().isVersion2() ? "CGEAR2" : "CGEAR", player.getCGearSkin()));
outputStream.write(dlcList.getDlcIndex("IRAO", "ZUKAN", player.getDexSkin()));
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 +249,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!
}
}
/**
@@ -294,6 +352,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 +394,10 @@ 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 and does not already have a Pokémon tucked in
Player player = playerManager.getPlayer(request.gameSyncId());
if(player == null || player.getGameVersion() != request.gameVersion()
|| (player.getStatus() != PlayerStatus.AWAKE && !configuration.allowOverwritingPlayerDreamInfo())) {
if(player == null || (player.getStatus() != PlayerStatus.AWAKE && !configuration.allowOverwritingPlayerDreamInfo())) {
// Skip everything
ServletInputStream inputStream = ctx.req().getInputStream();
@@ -369,6 +427,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 +453,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 +492,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;
}

View File

@@ -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());

View File

@@ -98,15 +98,14 @@ public class UrlEncodedFormGenerator extends SimpleGeneratorBase {
writer.write('=');
}
String value = text;
// Encode value as base64 if feature is enabled
// Otherwise, encode using URLEncoder.
if(Feature.BASE64_ENCODE_VALUES.enabledIn(formatFeatures)) {
value = Base64.getEncoder().encodeToString(text.getBytes(StandardCharsets.ISO_8859_1))
.replace('=', '*').replace('+', '.').replace('/', '-');
writer.write(Base64.getEncoder().encodeToString(text.getBytes(StandardCharsets.ISO_8859_1))
.replace('=', '*').replace('+', '.').replace('/', '-'));
} else {
writer.write(URLEncoder.encode(text, StandardCharsets.UTF_8));
}
writer.write(URLEncoder.encode(value, StandardCharsets.UTF_8));
}
@Override

View File

@@ -101,13 +101,14 @@ public class UrlEncodedFormParser extends SimpleParserBase {
_currToken = JsonToken.VALUE_STRING;
// Decode base64 if feature is enabled
// Otherwise, decode using URLDecoder.
if(Feature.BASE64_DECODE_VALUES.enabledIn(formatFeatures)) {
parsedString = new String(Base64.getDecoder().decode(
parsedString.replace('*', '=').replace('.', '+').replace('-', '/')), StandardCharsets.ISO_8859_1);
context.setCurrentValue(new String(Base64.getDecoder().decode(
parsedString.replace('*', '=').replace('.', '+').replace('-', '/')), StandardCharsets.ISO_8859_1));
} else {
context.setCurrentValue(URLDecoder.decode(parsedString, StandardCharsets.UTF_8));
}
context.setCurrentValue(URLDecoder.decode(parsedString, StandardCharsets.UTF_8));
if(i != '&') {
if(i != -1) {
_reportUnexpectedChar(i, "expected '&' to mark end of value and start of new key");

View File

@@ -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();
}
}

View File

@@ -0,0 +1,130 @@
package entralinked.utility;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.io.InputStream;
/**
* Utility class for reading tiled images (C-Gear & Pokédex skin data) into a usable {@link BufferedImage}.
*/
public class TiledImageReader {
public static final int TILE_WIDTH = 8;
public static final int TILE_HEIGHT = 8;
public static final int TILE_SIZE = TILE_WIDTH * TILE_HEIGHT;
public static final int COLOR_PALETTE_SIZE = 16;
public static final int SCREEN_WIDTH = 256;
public static final int SCREEN_HEIGHT = 192;
public static final int SCREEN_TILE_COUNT = SCREEN_WIDTH * SCREEN_HEIGHT / TILE_SIZE;
/**
* Calls {@link #readTiledImage(InputStream, int, boolean)} with a tile count of 255.
*
* @param normalizeIndices Should be {@code true} if the provided C-Gear skin data is from the original Black & White.
* @return A {@link BufferedImage} representing the read C-Gear skin data.
*/
public static BufferedImage readCGearSkin(InputStream inputStream, boolean normalizeIndices) throws IOException {
return readTiledImage(inputStream, 255, normalizeIndices);
}
/**
* Calls {@link #readTiledImage(InputStream, int, boolean)} with a tile count of 768 and index normalization disabled.
*
* @return A {@link BufferedImage} representing the read Pokédex skin data.
*/
public static BufferedImage readDexSkin(InputStream inputStream) throws IOException {
return readTiledImage(inputStream, 768, false);
}
/**
* Reads tiled image data from the provided {@link InputStream} and returns a {@link BufferedImage} representing the read image data.
*
* @param tileCount The number of tiles to be read. This should be equal to the maximum number of tiles for this image.
* @param normalizedIndices Indicates that tile indices are not linear (Black & White C-Gear skins) and should be normalized.
* @return A {@link BufferedImage} representing the read image data.
*/
public static BufferedImage readTiledImage(InputStream inputStream, int tileCount, boolean normalizeIndices) throws IOException {
BufferedImage image = new BufferedImage(SCREEN_WIDTH, SCREEN_HEIGHT, BufferedImage.TYPE_INT_RGB); // Result image
int[] tileData = new int[tileCount * TILE_SIZE];
int[] tileIndices = new int[tileCount]; // Tile index lookup table
int[] colorPalette = new int[COLOR_PALETTE_SIZE];
// Read tile data.
for(int i = 0; i < tileCount; i++) {
for(int j = 0; j < TILE_SIZE / 2; j++) {
int paletteIndices = inputStream.read(); // Contains color palette indices for 2 adjacent pixels.
tileData[i * TILE_SIZE + j * 2] = paletteIndices & (COLOR_PALETTE_SIZE - 1);
tileData[i * TILE_SIZE + j * 2 + 1] = (paletteIndices >> 4) & (COLOR_PALETTE_SIZE - 1);
}
// Store the index of the tile as it would be in memory so we can look it up later when we're mapping the tiles.
tileIndices[i] = normalizeIndices ? i + i / 17 * 15 + 0xA0A0 : i;
}
// Read color data.
// Pokédex skins contain room for 240 extra colors, 64 of which are defined and appear to be used as the
// 'background' colors in cases where the skin is only an overlay that is displayed on top of the 'true' Pokédex.
for(int i = 0; i < COLOR_PALETTE_SIZE; i++) {
int color = inputStream.read() | inputStream.read() << 8;
// Convert BGR555 to RGB888
int red = (color & 0x1F) << 3;
int green = ((color & 0x3E0) >> 5) << 3;
int blue = ((color & 0x7C00) >> 10) << 3;
colorPalette[i] = (red << 16) | (green << 8) | blue;
}
// Map tiles to the resulting image.
// In cases where the tile count is 768 or greater, which is exactly enough tiles to fill the entire screen,
// the tiles will be applied in the order they are provided and no additional mapping data will be read.
// This is always the case for Pokédex skins, and never the case for C-Gear skins.
if(tileCount < SCREEN_TILE_COUNT) {
// Not enough tiles -- read additional mapping data to figure out their placement.
for(int i = 0; i < SCREEN_TILE_COUNT; i++) {
int x = i * TILE_WIDTH % SCREEN_WIDTH;
int y = i * TILE_WIDTH / SCREEN_WIDTH * TILE_HEIGHT;
int leftBits = inputStream.read();
int rightBits = inputStream.read();
int memoryIndex = leftBits | (rightBits & ~12) << 8;
int flipBits = rightBits & 12;
// The normalized tile index is the index of the in-memory tile index in the lookup table.
int tileIndex = 0;
for(int k = 0; k < tileIndices.length; k++) {
if(memoryIndex == tileIndices[k]) {
tileIndex = k;
break;
}
}
// Apply the pixels of this tile to the resulting image.
for(int j = 0; j < TILE_SIZE; j++) {
// Get the color index for this pixel based on how the tile is flipped.
int tilePixelIndex = switch(flipBits) {
case 4 -> (TILE_WIDTH * (j / TILE_WIDTH) + TILE_WIDTH) - j % TILE_WIDTH - 1; // Flip horizontally
case 8 -> TILE_SIZE - (TILE_WIDTH * (j / TILE_WIDTH) + TILE_WIDTH) + j % TILE_WIDTH; // Flip vertically
case 12 -> TILE_SIZE - j - 1; // Flip horizontally & vertically
default -> j; // Don't flip
};
// Finally, set the pixel!
int paletteIndex = tileData[tileIndex * TILE_SIZE + tilePixelIndex];
image.setRGB(x + j % TILE_WIDTH, y + j / TILE_WIDTH, colorPalette[paletteIndex]);
}
}
} else {
// There are enough tiles to fill up the entire screen, so let's just place them in order.
for(int i = 0; i < SCREEN_TILE_COUNT; i++) {
int x = i * TILE_WIDTH % SCREEN_WIDTH;
int y = i * TILE_WIDTH / SCREEN_WIDTH * TILE_WIDTH;
for(int j = 0; j < TILE_SIZE; j++) {
image.setRGB(x + j % TILE_WIDTH, y + j / TILE_WIDTH, colorPalette[tileData[i * TILE_SIZE + j]]);
}
}
}
return image;
}
}

View File

@@ -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>

View File

@@ -3,141 +3,178 @@
<meta charset="UTF-8">
<link rel="stylesheet" href="styles/profile.css">
</head>
<body onload="fetchProfileData()">
<body>
<div id="main-container" class="root-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>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>
<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><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>
</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>
</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><a id="visitor0" href="#configureVisitor" onclick="configureVisitor(0)"><image src="/sprites/trainers/none.png"/></a></td>
<td><a id="visitor1" href="#configureVisitor" onclick="configureVisitor(1)"><image src="/sprites/trainers/none.png"/></a></td>
<td><a id="visitor2" href="#configureVisitor" onclick="configureVisitor(2)"><image src="/sprites/trainers/none.png"/></a></td>
<td><a id="visitor3" href="#configureVisitor" onclick="configureVisitor(3)"><image src="/sprites/trainers/none.png"/></a></td>
<td><a id="visitor4" href="#configureVisitor" onclick="configureVisitor(4)"><image src="/sprites/trainers/none.png"/></a></td>
<td><a id="visitor5" href="#configureVisitor" onclick="configureVisitor(5)"><image src="/sprites/trainers/none.png"/></a></td>
</tr>
<tr>
<td><a id="visitor6" href="#configureVisitor" onclick="configureVisitor(6)"><image src="/sprites/trainers/none.png"/></a></td>
<td><a id="visitor7" href="#configureVisitor" onclick="configureVisitor(7)"><image src="/sprites/trainers/none.png"/></a></td>
<td><a id="visitor8" href="#configureVisitor" onclick="configureVisitor(8)"><image src="/sprites/trainers/none.png"/></a></td>
<td><a id="visitor9" href="#configureVisitor" onclick="configureVisitor(9)"><image src="/sprites/trainers/none.png"/></a></td>
<td><a id="visitor10" href="#configureVisitor" onclick="configureVisitor(10)"><image src="/sprites/trainers/none.png"/></a></td>
<td><a id="visitor11" href="#configureVisitor" onclick="configureVisitor(11)"><image src="/sprites/trainers/none.png"/></a></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">
<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>
</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>
</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>
<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-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">Normal</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>
@@ -165,8 +202,10 @@
<div class="content">
<button class="close-button" onclick="closeEncounterForm()">X</button>
<form id="item-form">
<label for="item-form-id">Item ID</label>
<input id="item-form-id" name="id" type="number" value="1" min="1" max="638"/>
<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>
@@ -174,6 +213,62 @@
<button class="big-button" onclick="removeItem()">Remove</button>
</div>
</div>
<!-- Join Avenue Visitor Configuration Form -->
<div id="configureVisitor" class="popup">
<div class="content">
<button class="close-button" onclick="closeVisitorForm()">X</button>
<form id="visitor-form">
<label for="visitor-form-name">Name (Max. 7 characters)</label>
<input id="visitor-form-name" name="name" placeholder="Trainer" maxlength="7"/>
<label for="visitor-form-type">Trainer Class</label>
<select id="visitor-form-type" name="type" value="ACE_TRAINER_MALE">
<option value="YOUNGSTER">Youngster</option>
<option value="LASS">Lass</option>
<option value="ACE_TRAINER_MALE">Ace Trainer (Male)</option>
<option value="ACE_TRAINER_FEMALE">Ace Trainer (Female)</option>
<option value="RANGER_MALE">Pokémon Ranger (Male)</option>
<option value="RANGER_FEMALE">Pokémon Ranger (Female)</option>
<option value="BREEDER_MALE">Pokémon Breeder (Male)</option>
<option value="BREEDER_FEMALE">Pokémon Breeder (Female)</option>
<option value="SCIENTIST_MALE">Scientist (Male)</option>
<option value="SCIENTIST_FEMALE">Scientist (Female)</option>
<option value="HIKER">Hiker</option>
<option value="PARASOL_LADY">Parasol Lady</option>
<option value="ROUGHNECK">Roughneck</option>
<option value="NURSE">Nurse</option>
<option value="PRESCHOOLER_MALE">Preschooler (Male)</option>
<option value="PRESCHOOLER_FEMALE">Preschooler (Female)</option>
</select>
<label for="visitor-form-shop-type">Shop Type</label>
<select id="visitor-form-shop-type" name="shopType" value="RAFFLE">
<option value="RAFFLE">Raffle Shop</option>
<option value="FLORIST">Flower Shop</option>
<option value="SALON">Beauty Salon</option>
<option value="ANTIQUE">Antique Shop</option>
<option value="DOJO">Training Dojo</option>
<option value="CAFE">Café</option>
<option value="MARKET">Market</option>
</select>
<label for="visitor-form-game">Game of Origin (Affects which goods/services are sold)</label>
<select id="visitor-form-game" name="gameVersion" value="BLACK_ENGLISH">
<option value="BLACK_ENGLISH">Black Version</option>
<option value="WHITE_ENGLISH">White Version</option>
<option value="BLACK_2_ENGLISH">Black Version 2</option>
<option value="WHITE_2_ENGLISH">White Version 2</option>
</select>
<label for="visitor-form-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>
</body>
<script src="scripts/utility.js"></script>
<script src="scripts/pokedata.js"></script>
<script src="scripts/profile.js"></script>
</html>

View File

@@ -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";
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
// 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_NATURE = document.getElementById("dreamer-nature");
@@ -10,56 +10,155 @@ 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_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_ID = document.getElementById("item-form-id");
const ELEMENT_ITEM_QUANTITY = document.getElementById("item-form-quantity");
// Misc input elements
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_PERSONALITY = document.getElementById("visitor-form-personality");
const ELEMENT_VISITOR_DREAMER = document.getElementById("visitor-form-dreamer");
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;
}
}
// 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 = POKE_SPECIES_LIST[dreamerInfo.species - 1].name;
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";
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";
});
// Add species data
for(let i in POKE_SPECIES_LIST) {
let species = POKE_SPECIES_LIST[i];
let formattedName = "#" + ("00" + species.id).slice(-3) + " - " + species.name;
ELEMENT_VISITOR_DREAMER.options[ELEMENT_VISITOR_DREAMER.options.length] = new Option(formattedName, species.id);
if(species.downloadable && (isVersion2() || species.id <= 493)) {
ELEMENT_ENCOUNTER_SPECIES.options[ELEMENT_ENCOUNTER_SPECIES.options.length] = new Option(formattedName, species.id);
}
}
// Add move data
for(let i in POKE_MOVE_LIST) {
let move = POKE_MOVE_LIST[i];
let formattedName = "#" + ("00" + move.id).slice(-3) + " - " + move.name;
ELEMENT_ENCOUNTER_MOVE.options[ELEMENT_ENCOUNTER_MOVE.options.length] = new Option(formattedName, move.id);
}
// Add item data
for(let i in ITEM_LIST) {
let item = ITEM_LIST[i];
if(isVersion2() || item.id <= 626) {
let formattedName = "#" + ("00" + item.id).slice(-3) + " - " + item.name;
ELEMENT_ITEM_ID.options[ELEMENT_ITEM_ID.options.length] = new Option(formattedName, item.id);
}
}
// Event listener for changing the form selector contents when species changes
ELEMENT_ENCOUNTER_SPECIES.addEventListener("change", function() {
updateEncounterFormOptions();
ELEMENT_ENCOUNTER_FORM.value = 0;
});
})();
/**
* Encounter configuration stuff
*/
function updateEncounterFormOptions() {
clearSelectOptions(ELEMENT_ENCOUNTER_FORM);
let species = POKE_SPECIES_LIST[ELEMENT_ENCOUNTER_SPECIES.value - 1];
// 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("Normal", 0);
}
}
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;
updateEncounterFormOptions();
ELEMENT_ENCOUNTER_MOVE.value = encounter ? encounter.move : 0;
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";
@@ -67,55 +166,37 @@ function configureEncounter(index) {
function saveEncounter() {
if(encounterTableIndex < 0) {
closeEncounterForm();
return;
}
// Create encounter data
let encounterData = {
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(encounterData.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
}
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) {
@@ -127,6 +208,7 @@ function updateEncounterCell(index) {
if(encounterData) {
spriteImage = spriteBase + encounterData.species + ".png";
// Use unique form sprite if it exists
if(encounterData.form > 0) {
let formSpriteImage = spriteBase + encounterData.species + "-" + encounterData.form + ".png";
@@ -144,10 +226,107 @@ function closeEncounterForm() {
window.location.href = "#";
}
/**
* Join Avenue visitor configuration stuff
*/
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_PERSONALITY.value = visitor ? visitor.personality : 0;
ELEMENT_VISITOR_DREAMER.value = visitor ? visitor.dreamerSpecies : 1;
}
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;
}
}
}
// I'll make country codes configurable later... probably
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: 220, // United States
stateProvinceCode: 48, // Washington, D.C.
personality: ELEMENT_VISITOR_PERSONALITY.value,
dreamerSpecies: ELEMENT_VISITOR_DREAMER.value
};
console.log(profile.visitors[visitorTableIndex]);
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;
window.location.href = "#";
}
/**
* 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;
@@ -155,32 +334,34 @@ function configureItem(index) {
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) {
@@ -207,162 +388,74 @@ function closeItemForm() {
window.location.href = "#";
}
async function fetchData(path) {
return fetchData(path, "GET", null);
}
/**
* Miscellaneous stuff
*/
async function fetchData(path, method, body) {
let response = await fetch(path, {
method: method,
body: body
});
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;
window.open("/dashboard/previewskin?type=" + type + "&name=" + value);
return false;
}
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) => {
addDlcNames(ELEMENT_CGEAR_SKIN_INPUT, response);
ELEMENT_CGEAR_SKIN_INPUT.value = 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) => {
addDlcNames(ELEMENT_DEX_SKIN_INPUT, response);
ELEMENT_DEX_SKIN_INPUT.value = 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) => {
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;
// 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) => {
})).then((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");
}

View 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 {};
}
window.alert("Server returned status code " + response.status + " while fetching " + path);
return {};
}
try {
return await response.json();
} catch(error) {
window.alert("Could not deserialize JSON response: " + error);
}
return {};
}

View File

@@ -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%;
}

View File

@@ -35,19 +35,25 @@ button:active {
background-color: #333333;
}
a:link, a:visited {
color: white;
}
.root-container {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
padding: 10px;
max-width: 600px;
display: flex;
align-items: center;
justify-content: center;
height: 100%;
}
.root-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;
}
@@ -61,26 +67,29 @@ button:active {
}
.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 {
.dreamer-summary td#dreamer-sprite {
width: 96px;
text-align: center;
padding-left: 0px;
}
.header-text {
@@ -118,16 +127,18 @@ button:active {
background-color: #191919;
position: absolute;
left: 50%;
top: 25%;
transform: translate(-50%, 0%);
top: 50%;
transform: translate(-50%, -50%);
padding: 20px;
width: 300px;
width: 500px;
}
.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 {
@@ -139,9 +150,11 @@ button:active {
}
.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 {
@@ -158,7 +171,24 @@ button:active {
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;
}
.big-button {
font-size: 16px;
padding: 16px 32px;
margin-bottom: 8px;
}

View File

@@ -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

File diff suppressed because one or more lines are too long

Binary file not shown.

Binary file not shown.

Binary file not shown.