mirror of
https://github.com/kuroppoi/entralinked.git
synced 2026-04-26 00:27:26 -05:00
Crappy support for Mystery Gifts (#42)
This commit is contained in:
parent
abe1b80c2d
commit
acbb3d7f95
|
|
@ -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) {}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import org.apache.logging.log4j.Logger;
|
|||
|
||||
import entralinked.utility.Crc16;
|
||||
|
||||
@Deprecated
|
||||
public class DlcList {
|
||||
|
||||
private static final Logger logger = LogManager.getLogger();
|
||||
|
|
|
|||
|
|
@ -1,18 +1,17 @@
|
|||
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) {
|
||||
|
|
@ -59,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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -75,7 +74,7 @@ public class User {
|
|||
return dlcOverrides.containsKey(type);
|
||||
}
|
||||
|
||||
public Dlc getDlcOverride(String type) {
|
||||
public File getDlcOverride(String type) {
|
||||
return dlcOverrides.get(type);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
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.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
|
|
@ -10,15 +12,14 @@ 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;
|
||||
|
|
@ -30,11 +31,10 @@ 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();
|
||||
}
|
||||
|
||||
|
|
@ -64,6 +64,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());
|
||||
};
|
||||
|
||||
|
|
@ -81,15 +82,43 @@ public class DlsHandler implements HttpHandler {
|
|||
User user = ctx.attribute("user");
|
||||
String gameCode = getDlcGameCode(request.dlcGameCode());
|
||||
String type = getRegionlessDlcType(request.dlcType());
|
||||
String attr2 = request.attr2();
|
||||
List<File> files = user.hasDlcOverride(type) ? Arrays.asList(user.getDlcOverride(type))
|
||||
: Arrays.asList(getDlcDirectory(gameCode, type).listFiles());
|
||||
|
||||
// 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 empty string if no DLC could be found
|
||||
if(files == null) {
|
||||
ctx.result("");
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO NOTE: I assume that in a conventional implementation, certain DLC attributes may be omitted from the request.
|
||||
ctx.result(dlcList.getDlcListString(dlcList.getDlcList(gameCode, type, request.dlcIndex())));
|
||||
// PGL content attr2 hack
|
||||
if(attr2 != null) {
|
||||
files = Arrays.asList(files.get(Integer.parseInt(attr2) - 1));
|
||||
}
|
||||
|
||||
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(), file.length()));
|
||||
} 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());
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -99,32 +128,44 @@ public class DlsHandler implements HttpHandler {
|
|||
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());
|
||||
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);
|
||||
} 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;
|
||||
};
|
||||
}
|
||||
|
|
@ -133,12 +174,21 @@ 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) {
|
||||
if(dlcType == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
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";
|
||||
case "MYSTERY_E", "MYSTERY_F", "MYSTERY_I", "MYSTERY_G", "MYSTERY_S", "MYSTERY_J", "MYSTERY_K" -> "MYSTERY";
|
||||
default -> dlcType;
|
||||
};
|
||||
}
|
||||
|
||||
private File getDlcDirectory(String gameCode, String dlcType) {
|
||||
return dlcType == null ? new File(rootDirectory, gameCode) : new File(rootDirectory, "%s/%s".formatted(gameCode, dlcType));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,13 +20,13 @@ public record DlsRequest(
|
|||
@JsonProperty("gamecd") String dlcGameCode,
|
||||
@JsonProperty("contents") String dlcName, // action=contents
|
||||
@JsonProperty("attr1") String dlcType, // action=list
|
||||
@JsonProperty("attr2") int dlcIndex, // 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, dlcType=%s, dlcIndex=%s, offset=%s, num=%s]")
|
||||
.formatted(gameCode, action, dlcGameCode, dlcName, dlcType, dlcIndex, offset, num);
|
||||
return ("DlsRequest[gameCode=%s, action=%s, dlcGameCode=%s, dlcName=%s, dlcType=%s, attr2=%s, offset=%s, num=%s]")
|
||||
.formatted(gameCode, action, dlcGameCode, dlcName, dlcType, attr2, offset, num);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,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;
|
||||
|
|
@ -226,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);
|
||||
|
|
@ -236,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");
|
||||
|
|
@ -314,6 +311,7 @@ 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.
|
||||
|
|
@ -322,7 +320,6 @@ public class PglHandler implements HttpHandler {
|
|||
// 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
|
||||
|
|
|
|||
55
src/main/java/entralinked/utility/MysteryGiftUtility.java
Normal file
55
src/main/java/entralinked/utility/MysteryGiftUtility.java
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
package entralinked.utility;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
import org.bouncycastle.util.Arrays;
|
||||
|
||||
public class MysteryGiftUtility {
|
||||
|
||||
public static byte[] createUniversalGiftData4(byte[] bytes) {
|
||||
// Check data size
|
||||
if(bytes.length > 936) {
|
||||
throw new IllegalArgumentException("Data too large: %s".formatted(bytes.length));
|
||||
}
|
||||
|
||||
// TODO gift title & wonder card
|
||||
byte[] result = new byte[936];
|
||||
|
||||
if(bytes.length <= 856) {
|
||||
System.arraycopy(bytes, 0, result, 0x50, bytes.length);
|
||||
} 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, descriptionBytes.length);
|
||||
}
|
||||
|
||||
// Recalculate checksum
|
||||
int checksum = Crc16.calc(result, 0, 0x2CE);
|
||||
result[0x2CE] = (byte)(checksum & 0xFF);
|
||||
result[0x2CF] = (byte)((checksum >> 8) & 0xFF);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user