Skip to content

Commit acbb3d7

Browse files
committed
Crappy support for Mystery Gifts (#42)
1 parent abe1b80 commit acbb3d7

File tree

7 files changed

+143
-40
lines changed

7 files changed

+143
-40
lines changed

src/main/java/entralinked/model/dlc/Dlc.java

+1
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,6 @@
33
/**
44
* Simple record for DLC data.
55
*/
6+
@Deprecated
67
public record Dlc(String path, String name, String gameCode, String type,
78
int index, int projectedSize, int checksum, boolean checksumEmbedded) {}

src/main/java/entralinked/model/dlc/DlcList.java

+1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import entralinked.utility.Crc16;
2020

21+
@Deprecated
2122
public class DlcList {
2223

2324
private static final Logger logger = LogManager.getLogger();

src/main/java/entralinked/model/user/User.java

+6-7
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,17 @@
11
package entralinked.model.user;
22

3+
import java.io.File;
34
import java.util.Collection;
45
import java.util.Collections;
56
import java.util.HashMap;
67
import java.util.Map;
78

8-
import entralinked.model.dlc.Dlc;
9-
109
public class User {
1110

1211
private final String id;
1312
private final String password; // I debated hashing it, but.. it's a 3-digit password...
1413
private final Map<String, GameProfile> profiles = new HashMap<>();
15-
private final Map<String, Dlc> dlcOverrides = new HashMap<>();
14+
private final Map<String, File> dlcOverrides = new HashMap<>();
1615
private int profileIdOverride; // For making it easier for the user to fix error 60000
1716

1817
public User(String id, String password) {
@@ -59,11 +58,11 @@ protected Map<String, GameProfile> getProfileMap() {
5958
return Collections.unmodifiableMap(profiles);
6059
}
6160

62-
public void setDlcOverride(String type, Dlc target) {
63-
if(target == null) {
61+
public void setDlcOverride(String type, File file) {
62+
if(file == null) {
6463
dlcOverrides.remove(type);
6564
} else {
66-
dlcOverrides.put(type, target);
65+
dlcOverrides.put(type, file);
6766
}
6867
}
6968

@@ -75,7 +74,7 @@ public boolean hasDlcOverride(String type) {
7574
return dlcOverrides.containsKey(type);
7675
}
7776

78-
public Dlc getDlcOverride(String type) {
77+
public File getDlcOverride(String type) {
7978
return dlcOverrides.get(type);
8079
}
8180

src/main/java/entralinked/network/http/dls/DlsHandler.java

+74-24
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
package entralinked.network.http.dls;
22

3-
import java.io.FileInputStream;
3+
import java.io.File;
44
import java.io.IOException;
5+
import java.nio.file.Files;
6+
import java.util.Arrays;
57
import java.util.List;
68

79
import org.apache.logging.log4j.LogManager;
@@ -10,15 +12,14 @@
1012
import com.fasterxml.jackson.databind.ObjectMapper;
1113

1214
import entralinked.Entralinked;
13-
import entralinked.model.dlc.Dlc;
14-
import entralinked.model.dlc.DlcList;
15+
import entralinked.GameVersion;
1516
import entralinked.model.user.ServiceSession;
1617
import entralinked.model.user.User;
1718
import entralinked.model.user.UserManager;
1819
import entralinked.network.http.HttpHandler;
1920
import entralinked.network.http.HttpRequestHandler;
2021
import entralinked.serialization.UrlEncodedFormFactory;
21-
import entralinked.utility.LEOutputStream;
22+
import entralinked.utility.MysteryGiftUtility;
2223
import io.javalin.Javalin;
2324
import io.javalin.http.Context;
2425
import io.javalin.http.HttpStatus;
@@ -30,11 +31,10 @@ public class DlsHandler implements HttpHandler {
3031

3132
private static final Logger logger = LogManager.getLogger();
3233
private final ObjectMapper mapper = new ObjectMapper(new UrlEncodedFormFactory());
33-
private final DlcList dlcList;
34+
private final File rootDirectory = new File("dlc");
3435
private final UserManager userManager;
3536

3637
public DlsHandler(Entralinked entralinked) {
37-
this.dlcList = entralinked.getDlcList();
3838
this.userManager = entralinked.getUserManager();
3939
}
4040

@@ -64,6 +64,7 @@ private void handleDownloadRequest(Context ctx) throws IOException {
6464
HttpRequestHandler<DlsRequest> handler = switch(request.action()) {
6565
case "list" -> this::handleRetrieveDlcList;
6666
case "contents" -> this::handleRetrieveDlcContent;
67+
case "count" -> this::handleRetrieveDlcCount;
6768
default -> throw new IllegalArgumentException("Invalid POST request action: " + request.action());
6869
};
6970

@@ -81,15 +82,43 @@ private void handleRetrieveDlcList(DlsRequest request, Context ctx) throws IOExc
8182
User user = ctx.attribute("user");
8283
String gameCode = getDlcGameCode(request.dlcGameCode());
8384
String type = getRegionlessDlcType(request.dlcType());
85+
String attr2 = request.attr2();
86+
List<File> files = user.hasDlcOverride(type) ? Arrays.asList(user.getDlcOverride(type))
87+
: Arrays.asList(getDlcDirectory(gameCode, type).listFiles());
8488

85-
// If an overriding DLC is present, send the data for that instead.
86-
if(user.hasDlcOverride(type)) {
87-
ctx.result(dlcList.getDlcListString(List.of(user.getDlcOverride(type))));
89+
// Return empty string if no DLC could be found
90+
if(files == null) {
91+
ctx.result("");
8892
return;
8993
}
9094

91-
// TODO NOTE: I assume that in a conventional implementation, certain DLC attributes may be omitted from the request.
92-
ctx.result(dlcList.getDlcListString(dlcList.getDlcList(gameCode, type, request.dlcIndex())));
95+
// PGL content attr2 hack
96+
if(attr2 != null) {
97+
files = Arrays.asList(files.get(Integer.parseInt(attr2) - 1));
98+
}
99+
100+
StringBuilder builder = new StringBuilder();
101+
int count = Math.min(files.size(), request.num());
102+
103+
// Create DLC list string
104+
for(int i = 0; i < count; i++) {
105+
File file = files.get(i);
106+
107+
if(type == null) {
108+
// Generation 4 Mystery Gift
109+
builder.append("%s\t\t\t\t\t%s\r\n".formatted(file.getName(), file.length()));
110+
} else if(type.equals("MYSTERY")) {
111+
// Generation 5 Mystery Gift
112+
String gameFlag = GameVersion.lookup(request.gameCode()).isVersion2() ? "F00000" : "300000";
113+
builder.append("%s\t\t%s\t%s\t\t%s\r\n".formatted(file.getName(), type, gameFlag, 720));
114+
} else {
115+
// PGL content
116+
builder.append("%s\t\t%s\t%s\t\t%s\r\n".formatted(file.getName(), type, i + 1, file.length()));
117+
}
118+
}
119+
120+
// Send result
121+
ctx.result(builder.toString());
93122
}
94123

95124
/**
@@ -99,32 +128,44 @@ private void handleRetrieveDlcContent(DlsRequest request, Context ctx) throws IO
99128
User user = ctx.attribute("user");
100129
String gameCode = getDlcGameCode(request.dlcGameCode());
101130
String type = getRegionlessDlcType(request.dlcType());
102-
Dlc dlc = user.hasDlcOverride(type) ? user.getDlcOverride(type) : dlcList.getDlc(gameCode, type, request.dlcName());
131+
File file = user.hasDlcOverride(type) ? user.getDlcOverride(type) : type != null
132+
? new File(rootDirectory, "%s/%s/%s".formatted(gameCode, type, request.dlcName()))
133+
: new File(rootDirectory, "%s/%s".formatted(gameCode, request.dlcName()));
103134

104135
// Check if the requested DLC exists
105-
if(dlc == null) {
136+
if(!file.exists()) {
106137
ctx.status(HttpStatus.NOT_FOUND);
107138
return;
108139
}
109140

110-
// Write DLC data
111-
try(FileInputStream inputStream = new FileInputStream(dlc.path())) {
112-
LEOutputStream outputStream = new LEOutputStream(ctx.outputStream());
113-
inputStream.transferTo(outputStream);
114-
115-
// If checksum is not part of the file, manually append it
116-
if(!dlc.checksumEmbedded()) {
117-
outputStream.writeShort(dlc.checksum());
118-
}
141+
byte[] bytes = Files.readAllBytes(file.toPath());
142+
143+
if(type == null) {
144+
// Generation 4 Mystery Gift
145+
bytes = MysteryGiftUtility.createUniversalGiftData4(bytes);
146+
} else if(type.equals("MYSTERY")) {
147+
// Generation 5 Mystery Gift
148+
bytes = MysteryGiftUtility.createUniversalGiftData5(bytes);
119149
}
150+
151+
// Send result
152+
ctx.result(bytes);
153+
}
154+
155+
/**
156+
* POST handler for {@code /download action=count}
157+
*/
158+
private void handleRetrieveDlcCount(DlsRequest request, Context ctx) throws IOException {
159+
ctx.result("1"); // TODO
120160
}
121161

122162
/**
123163
* @return The game serial that should be used for downloading DLC based on the provided input.
124164
*/
125165
private String getDlcGameCode(String gameCode) {
126-
return switch(gameCode) {
127-
case "IRAJ" -> "IRAO";
166+
return switch(gameCode.substring(0, 3)) {
167+
case "IRA" -> "IRAO"; // BW & B2W2
168+
case "ADA", "CPU", "IPG" -> "ADAE"; // DPPt & HGSS
128169
default -> gameCode;
129170
};
130171
}
@@ -133,12 +174,21 @@ private String getDlcGameCode(String gameCode) {
133174
* @return The DLC type without the region identifier, or the input if it is an unknown type.
134175
*/
135176
private String getRegionlessDlcType(String dlcType) {
177+
if(dlcType == null) {
178+
return null;
179+
}
180+
136181
return switch(dlcType) {
137182
case "CGEAR_E", "CGEAR_F", "CGEAR_I", "CGEAR_G", "CGEAR_S", "CGEAR_J", "CGEAR_K" -> "CGEAR";
138183
case "CGEAR2_E", "CGEAR2_F", "CGEAR2_I", "CGEAR2_G", "CGEAR2_S", "CGEAR2_J", "CGEAR2_K" -> "CGEAR2";
139184
case "ZUKAN_E", "ZUKAN_F", "ZUKAN_I", "ZUKAN_G", "ZUKAN_S", "ZUKAN_J", "ZUKAN_K" -> "ZUKAN";
140185
case "MUSICAL_E", "MUSICAL_F", "MUSICAL_I", "MUSICAL_G", "MUSICAL_S", "MUSICAL_J", "MUSICAL_K" -> "MUSICAL";
186+
case "MYSTERY_E", "MYSTERY_F", "MYSTERY_I", "MYSTERY_G", "MYSTERY_S", "MYSTERY_J", "MYSTERY_K" -> "MYSTERY";
141187
default -> dlcType;
142188
};
143189
}
190+
191+
private File getDlcDirectory(String gameCode, String dlcType) {
192+
return dlcType == null ? new File(rootDirectory, gameCode) : new File(rootDirectory, "%s/%s".formatted(gameCode, dlcType));
193+
}
144194
}

src/main/java/entralinked/network/http/dls/DlsRequest.java

+3-3
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,13 @@ public record DlsRequest(
2020
@JsonProperty("gamecd") String dlcGameCode,
2121
@JsonProperty("contents") String dlcName, // action=contents
2222
@JsonProperty("attr1") String dlcType, // action=list
23-
@JsonProperty("attr2") int dlcIndex, // action=list
23+
@JsonProperty("attr2") String attr2, // action=list
2424
@JsonProperty("offset") int offset, // Start offset in the list
2525
@JsonProperty("num") int num) { // Number of entries
2626

2727
@Override
2828
public String toString() {
29-
return ("DlsRequest[gameCode=%s, action=%s, dlcGameCode=%s, dlcName=%s, dlcType=%s, dlcIndex=%s, offset=%s, num=%s]")
30-
.formatted(gameCode, action, dlcGameCode, dlcName, dlcType, dlcIndex, offset, num);
29+
return ("DlsRequest[gameCode=%s, action=%s, dlcGameCode=%s, dlcName=%s, dlcType=%s, attr2=%s, offset=%s, num=%s]")
30+
.formatted(gameCode, action, dlcGameCode, dlcName, dlcType, attr2, offset, num);
3131
}
3232
}

src/main/java/entralinked/network/http/pgl/PglHandler.java

+3-6
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
import entralinked.Configuration;
1818
import entralinked.Entralinked;
1919
import entralinked.model.avenue.AvenueVisitor;
20-
import entralinked.model.dlc.Dlc;
2120
import entralinked.model.dlc.DlcList;
2221
import entralinked.model.pkmn.PkmnInfo;
2322
import entralinked.model.pkmn.PkmnInfoReader;
@@ -226,8 +225,7 @@ private void handleDownloadSaveData(PglRequest request, Context ctx) throws IOEx
226225
// Create or remove custom C-Gear skin DLC override
227226
if("custom".equals(cgearSkin)) {
228227
cgearSkinIndex = 1;
229-
user.setDlcOverride(cgearType, new Dlc(player.getCGearSkinFile().getAbsolutePath(),
230-
"custom", "IRAO", cgearType, cgearSkinIndex, 9730, 0, true));
228+
user.setDlcOverride(cgearType, player.getCGearSkinFile());
231229
} else {
232230
cgearSkinIndex = dlcList.getDlcIndex("IRAO", cgearType, cgearSkin);
233231
user.removeDlcOverride(cgearType);
@@ -236,8 +234,7 @@ private void handleDownloadSaveData(PglRequest request, Context ctx) throws IOEx
236234
// Create or remove custom Pokédex skin DLC override
237235
if("custom".equals(dexSkin)) {
238236
dexSkinIndex = 1;
239-
user.setDlcOverride("ZUKAN", new Dlc(player.getDexSkinFile().getAbsolutePath(),
240-
"custom", "IRAO", "ZUKAN", dexSkinIndex, 25090, 0, true));
237+
user.setDlcOverride("ZUKAN", player.getDexSkinFile());
241238
} else {
242239
dexSkinIndex = dlcList.getDlcIndex("IRAO", "ZUKAN", dexSkin);
243240
user.removeDlcOverride("ZUKAN");
@@ -314,6 +311,7 @@ private void handleDownloadSaveData(PglRequest request, Context ctx) throws IOEx
314311
byte[] nameBytes = visitor.name().getBytes(StandardCharsets.UTF_16LE);
315312
outputStream.write(nameBytes, 0, Math.min(14, nameBytes.length));
316313
outputStream.writeBytes(-1, 14 - nameBytes.length);
314+
outputStream.writeShort(0xFF); // Null terminator
317315

318316
// Full visitor type consists of a trainer class and what I call a 'personality' index
319317
// that, along with the trainer class, determines which phrases the visitor uses.
@@ -322,7 +320,6 @@ private void handleDownloadSaveData(PglRequest request, Context ctx) throws IOEx
322320
// For example, if the visitor type is '0', then shop type '0' would be a raffle.
323321
// However, if the visitor type is '2', then shop type '0' results in a dojo instead.
324322
int visitorType = visitor.type().getClientId() + visitor.personality() * 8;
325-
outputStream.writeShort(-1); // Does nothing, seems to be read as part of the name.
326323
outputStream.write(visitorType);
327324
outputStream.write(visitor.shopType().ordinal() + (7 - visitorType * 2 % 7));
328325
outputStream.writeShort(0); // Does nothing
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package entralinked.utility;
2+
3+
import java.nio.charset.StandardCharsets;
4+
5+
import org.bouncycastle.util.Arrays;
6+
7+
public class MysteryGiftUtility {
8+
9+
public static byte[] createUniversalGiftData4(byte[] bytes) {
10+
// Check data size
11+
if(bytes.length > 936) {
12+
throw new IllegalArgumentException("Data too large: %s".formatted(bytes.length));
13+
}
14+
15+
// TODO gift title & wonder card
16+
byte[] result = new byte[936];
17+
18+
if(bytes.length <= 856) {
19+
System.arraycopy(bytes, 0, result, 0x50, bytes.length);
20+
} else {
21+
System.arraycopy(bytes, 0, result, 0, bytes.length);
22+
}
23+
24+
// Clear game version
25+
result[0x48] = 0;
26+
result[0x49] = 0;
27+
return result;
28+
}
29+
30+
public static byte[] createUniversalGiftData5(byte[] bytes) {
31+
// Check data size
32+
if(bytes.length > 720) {
33+
throw new IllegalArgumentException("Data too large: %s".formatted(bytes.length));
34+
}
35+
36+
byte[] result = new byte[720];
37+
System.arraycopy(bytes, 0, result, 0, bytes.length);
38+
result[0xCE] = 0; // Version flag
39+
result[0x2CB] = 0; // Language code
40+
41+
// Create standard gift description if there is none
42+
if(bytes.length == 204) {
43+
Arrays.fill(result, 0xD0, 0x2CA, (byte)0xFF);
44+
String description = "No description is available for this gift.";
45+
byte[] descriptionBytes = description.replace('\n', '\uFFFE').getBytes(StandardCharsets.UTF_16LE);
46+
System.arraycopy(descriptionBytes, 0, result, 0xD0, descriptionBytes.length);
47+
}
48+
49+
// Recalculate checksum
50+
int checksum = Crc16.calc(result, 0, 0x2CE);
51+
result[0x2CE] = (byte)(checksum & 0xFF);
52+
result[0x2CF] = (byte)((checksum >> 8) & 0xFF);
53+
return result;
54+
}
55+
}

0 commit comments

Comments
 (0)