Skip to content

Commit 99a2c33

Browse files
authored
Merge pull request #2 from padraigfl/level0mod
Alternative modding system to avoid full database refresh
2 parents 5b7b3d3 + 47771b1 commit 99a2c33

27 files changed

Lines changed: 837 additions & 434 deletions

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
*.apk
33
*.idsig
44
*.split194
5+
*.split3
6+
*.split4
57
public
68
decompiled
79
dataconsult

readme.md

Lines changed: 33 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,18 @@ adb is also baked in to simplify the process of connecting to an Android device;
2929

3030
## Setup:
3131

32-
1. Add a key and certificate to the same directory as you run the jar from for signing the new APK (I may include a dummy one to remove this step)
3332
1. Run `java -jar [app path]/DropmixModdingTool.jar` (depending on your device you may only need to click on the icon in your Finder/Explorer)
3433
1. Add a valid Dropmix APK file (must be v1.9.0) and verify it
3534
1. (if desired) connect Android device (will fail if more than one available)
3635
1. Go to playlists tab and select the playlists you wish to swap
37-
1. Build the modded version of the APK
36+
1. Build the modded version of the APK (Safe Swap recommended; see Swap info below)
3837
1. Either install it directly to you ADB connected device or save it to your hard drive
38+
1. Once you have all card data downloaded, you should only run the app with wifi and data disabled to prevent constant server data refreshes
39+
40+
### Recommendations:
41+
42+
- As modifying the apk requires the app to be be signed with a new security key, there will be issues with any data you already have. With this in mind it's recommended to use the "Build Re-Signed APK" function to have a straight copy of the app unmodified but with the same signature as other mods
43+
- The Safe Swap modification process isn't perfect but as it doesn't break the data integrity within the app (requiring all card data to be redownloaded), it's a better long term option
3944

4045
## Troubleshooting
4146

@@ -45,13 +50,13 @@ I need to know the version of Java you're using and the output of the log panel
4550
- what version of java is it?
4651
- do you have multiple adb devices conneccted to your computer?
4752
- is the Dropmix APK file definitely 1.9.0
53+
- download all card data via the app and proceed to never use the app while connected to the internet again
4854

4955
### Known Issues:
5056

51-
1. A fresh install of all card data is required after each install currently [I'm guessing this is some kind of auto refresh function baked into the app]
57+
1. Full card swap process triggers redownload of all card assets
5258
1. I raced through this so the UI is janky as hell. I expect frequent restarts. Some of it looks pretty bad with the Java 1.8 compilation settings but it'll do
53-
1. Logs aren't updating correctly; I think I need to add some multithreading functionality
54-
1. UI in general has weird lags tbh
59+
1. UI in general has weird lags tbh; it may need a rethink in general
5560
1. Need to figure out the licensing stuff fully. My understanding is that all the libraries I depend on here use Apache 2.0 so I will use that too
5661
1. The included keys for signing the APK currently have had less than zero thought put into them. This is a rough and insecure project. If you want to improve anything around the security of this project I'd love the help!
5762
1. No data is saved between instances of the program so you need to readd the apk each time
@@ -60,14 +65,30 @@ I need to know the version of Java you're using and the output of the log panel
6065
## Current functionality:
6166

6267
1. Parse app's key card database
63-
1. Generates a modified version of the application with playlists swapped
68+
1. Generates a modified version of the application with playlists swapped (two processes available)
6469
1. Directly installs modified APK onto connected android device (requires ADB server instance)
6570

71+
### Modification Options
72+
73+
#### Full swap
74+
75+
This simply swaps the card IDs on the top level data tables (found in sharedassets), meaning a card will behave exactly like its swapped counterpart.
76+
77+
Unfortunately this means the card's have data which does not match the app data and a fresh download of assets will be triggered before the game can be played; meaning limited long term value
78+
79+
#### Safe Swap
80+
81+
This swaps the IDs in the game level data (found in level0), mostly behaving exactly the same however "Power" appears to come from the top level database.
82+
83+
#### Future Options
84+
85+
As I've had to update the game level data for the safe swap, I've largely implemented the parsing and writing of the database which would allow custom cards along with more extensive game modifications (e.g. FX cards rules appear to be set here too)
86+
6687
## Future work:
6788

68-
1. Alternative swap process using `level0` data (will not require fresh data downloads so future-proof but won't have 100% accurate card behaviour)
69-
1. Include associated baffler cards in playlist swaps
70-
1. Mod iOS app on M1 devices
71-
1. Verify working on Windows
72-
1. Tools for straightforward installing of APK without mods
73-
1. Mayyyyybe custom cards? This involves modifying the level0 assets file instead and injecting custom data files post-install so it's not so easy
89+
This is the work I intend to do rather than stretch goals and bolder things
90+
91+
1. Output APK alongside CSV file outlining changes from the main one
92+
1. Verify working on Windows (partially checked, ADB not)
93+
1. Tools for straightforward installing of APK without mods (i.e. install APK to device, copy data files over)
94+
1. Mod iOS app on M1 devices

src/AdbWinApi.dll

106 KB
Binary file not shown.

src/AdbWinUsbApi.dll

71.6 KB
Binary file not shown.

src/level0.split3

1 MB
Binary file not shown.

src/level0.split4

572 KB
Binary file not shown.
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package model;
2+
3+
import util.Helpers;
4+
5+
/*
6+
Can be used for:
7+
- shared assets card data bases (each season is one record)
8+
- level0 in-game card data (each row in table is one record)
9+
- probably other locations
10+
*/
11+
public abstract class AbstractDropmixDataRecord {
12+
int startIdx;
13+
int dataStartIdx;
14+
int dataLength;
15+
int dataSpace; // the space occupied by the data (does not include the specifier before the data, does include whitespace at end)
16+
byte[] raw;// everything including the 32 bit length specifier and whitespace
17+
byte[] recordData; //
18+
boolean iOS;
19+
boolean modified;
20+
public AbstractDropmixDataRecord(byte[] data, int startIdx, boolean iOS) {
21+
this.startIdx = startIdx;
22+
this.dataStartIdx = startIdx + 4;
23+
this.dataLength = Helpers.intFromByteArray(Helpers.getNRange(data, startIdx, 4));
24+
int remainder = (dataLength) % 4;
25+
this.dataSpace = dataLength + (remainder == 0 ? 0 : 4 - remainder);
26+
this.raw = Helpers.getNRange(data, startIdx, dataSpace + 4);
27+
this.recordData = Helpers.getNRange(data, startIdx + 4, dataLength);
28+
this.iOS = iOS;
29+
}
30+
31+
public String toString() {
32+
return (this.modified ? "[MODIFIED]" : "") + Helpers.byteArrayToString(Helpers.getNRange(raw, 4, dataLength));
33+
}
34+
35+
public static int getStartIndex(byte[] rawData, byte[] startSequence) {
36+
outer:
37+
for (int i = 0; i < rawData.length; i++) {
38+
if (rawData[i] == startSequence[0]) {
39+
for (int j = 1; j < startSequence.length && (j+i) < rawData.length; j++ ) {
40+
if (rawData[i + j] != startSequence[j]) {
41+
continue outer;
42+
}
43+
}
44+
return i + startSequence.length;
45+
}
46+
}
47+
return -1;
48+
}
49+
}

src/model/AppState.java

Lines changed: 44 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -2,28 +2,25 @@
22

33
import se.vidstige.jadb.JadbDevice;
44
import ui.UIMain;
5+
import util.Helpers;
56

6-
import javax.swing.*;
77
import java.io.File;
8-
import java.io.IOException;
9-
import java.nio.file.Files;
10-
import java.nio.file.Path;
11-
import java.nio.file.Paths;
128
import java.util.*;
139

1410
public class AppState {
1511
public static AppState instance = new AppState();
1612
public File apkFile;
1713
public File dataZip;
18-
public byte[] rawData;
19-
public TreeMap<String, String> swapOptions = new TreeMap<String, String>();;
14+
public TreeMap<String, String> swapOptions = new TreeMap<>();
2015
public TreeMap<String, String> playlistSwap = new TreeMap<>();
2116
public JadbDevice adbDevice;
2217
public DropmixSharedAssets assetsHandler;
18+
public DropmixLevel0 level0Handler;
2319
public LogOptions logState = LogOptions.ERROR;
2420
public Process currentProcess = Process.NONE;
2521
public UIMain appFrame; // for forcing refreshes
2622
public boolean isNestedLog;
23+
public boolean iOS = false;
2724
private AppState() {
2825
}
2926
public static AppState getInstance() {
@@ -32,7 +29,7 @@ public static AppState getInstance() {
3229
public static AppState getInstance(boolean isTest) {
3330
AppState instance = getInstance();
3431
if (isTest) {
35-
instance.setData(instance.loadFile());
32+
instance.setData(Helpers.loadFile("sharedassets0.assets.split194"), Helpers.loadFile("level0.split3"));
3633
}
3734
return instance;
3835
}
@@ -41,43 +38,43 @@ public static AppState getInstance(boolean isTest, UIMain appFrame) {
4138
instance.appFrame = appFrame;
4239
return instance;
4340
}
44-
public void setData(byte[] fileData) {
45-
this.assetsHandler = new DropmixSharedAssets(fileData);
41+
public void setData(byte[] sharedAssets, byte[] level0) {
42+
this.assetsHandler = new DropmixSharedAssets(sharedAssets);
43+
this.level0Handler = new DropmixLevel0(level0);
4644
}
4745

48-
public CardDetail[] getCards() {
49-
ArrayList<CardDetail> cards = new ArrayList<CardDetail>();
46+
public DropmixSharedAssetsCard[] getCards() {
47+
ArrayList<DropmixSharedAssetsCard> cards = new ArrayList<>();
5048
try {
5149
int seasonIdx = 0;
52-
SeasonTable season = this.assetsHandler.seasons.get(seasonIdx++);
50+
DropmixSharedAssetsSeason season = this.assetsHandler.seasons.get(seasonIdx++);
5351
while (season != null) {
5452
cards.addAll(Arrays.asList(season.cards));
5553
season = this.assetsHandler.seasons.get(seasonIdx++);
5654
}
57-
return cards.toArray(new CardDetail[0]);
55+
return cards.toArray(new DropmixSharedAssetsCard[0]);
5856
} catch (Exception e) {
5957
e.printStackTrace();
60-
return new CardDetail[0];
58+
return new DropmixSharedAssetsCard[0];
6159
}
6260
}
63-
public PlaylistDetail[] getPlaylists() {
64-
Set<String> playlistNames = new HashSet<String>();
65-
for (CardDetail c: AppState.getInstance().getCards()) {
66-
playlistNames.add(c.cardData.get(CardDetail.SeriesIcon));
61+
public DropmixSharedAssetsPlaylist[] getPlaylists() {
62+
Set<String> playlistNames = new HashSet<>();
63+
for (DropmixSharedAssetsCard c: AppState.getInstance().getCards()) {
64+
playlistNames.add(c.data.get(DropmixSharedAssetsCard.SeriesIcon));
6765
}
6866
String[] playlistNamesArray = playlistNames.toArray(new String[0]);
6967

7068

71-
ArrayList<PlaylistDetail> seasons = new ArrayList<>();
69+
ArrayList<DropmixSharedAssetsPlaylist> seasons = new ArrayList<>();
7270

7371
for (int i=0; i < playlistNames.size(); i++) {
74-
PlaylistDetail playlist = new PlaylistDetail(playlistNamesArray[i]);
72+
DropmixSharedAssetsPlaylist playlist = new DropmixSharedAssetsPlaylist(playlistNamesArray[i]);
7573
seasons.add(playlist);
7674
}
7775
// this is required to sort the playlists in the common order
78-
Collections.sort(seasons, new Comparator<PlaylistDetail>(){
79-
public int compare(PlaylistDetail o1, PlaylistDetail o2)
80-
{
76+
seasons.sort(new Comparator<DropmixSharedAssetsPlaylist>() {
77+
public int compare(DropmixSharedAssetsPlaylist o1, DropmixSharedAssetsPlaylist o2) {
8178
int val = o1.season.compareTo(o2.season);
8279
if (val == 0) {
8380
// baffler and promo are both empty
@@ -89,26 +86,15 @@ public int compare(PlaylistDetail o1, PlaylistDetail o2)
8986
}
9087
int card1 = Integer.parseInt(o1.cardId);
9188
int card2 = Integer.parseInt(o2.cardId);
89+
if (card1 == card2) {
90+
return 0;
91+
}
9292
return card1 > card2 ? 1 : -1;
9393
}
9494
return val;
9595
}
9696
});
97-
return seasons.toArray(new PlaylistDetail[0]);
98-
}
99-
byte[] loadFile() {
100-
if (rawData != null) {
101-
return rawData;
102-
}
103-
ClassLoader classLoader = getClass().getClassLoader();
104-
105-
try {
106-
String fileByteArrayPathString = classLoader.getResource("sharedassets0.assets.split194").getFile();
107-
rawData = Files.readAllBytes(Paths.get(fileByteArrayPathString));
108-
return rawData;
109-
} catch (IOException | NullPointerException e) {
110-
throw new Error(e);
111-
}
97+
return seasons.toArray(new DropmixSharedAssetsPlaylist[0]);
11298
}
11399
public void removePlaylistSwap(String p1) {
114100
String p2 = this.playlistSwap.get(p1);
@@ -128,8 +114,8 @@ public void setPlaylistSwap(String p1, String p2) throws Exception {
128114
if (this.playlistSwap.containsValue(p1) || this.playlistSwap.containsValue(p2)) {
129115
throw new Exception("value-in-use");
130116
}
131-
for(PlaylistDetail pl : this.getPlaylists()) {
132-
if (pl.name == p1 || pl.name == p2) {
117+
for(DropmixSharedAssetsPlaylist pl : this.getPlaylists()) {
118+
if (pl.name.equals(p1) || pl.name.equals(p2)) {
133119
if (pl.playlistCount != 15) {
134120
throw new Exception("invalid-playlist");
135121
}
@@ -195,25 +181,25 @@ public void reset() {
195181
this.appFrame.addPlaceholders();
196182
}
197183
// builds a card based swap from the playlist swap; may require more careful refinement as assumptions about order persistence exist
198-
public static TreeMap<String, String> getCardSwapFromPlaylist(TreeMap<String, String> plSwap) {
184+
public static TreeMap<String, String> getCardSwapFromPlaylist(TreeMap<String, String> plSwap, boolean includeBafflers) {
199185
for (String key: plSwap.values()) {
200186
String value = plSwap.get(key);
201187
String validator = plSwap.get(value);
202-
if (value == null || validator == null || !key.equals(validator)) {
188+
if (value == null || !key.equals(validator)) {
203189
throw new RuntimeException("playlist-swap-sync-issue");
204190
}
205191
}
206192
String[] playlistNames = plSwap.keySet().toArray(new String[0]);
207-
PlaylistDetail[] playlists = getInstance().getPlaylists();
208-
TreeMap<String, PlaylistDetail> playlistDetailTreeMap = new TreeMap<String, PlaylistDetail>();
193+
DropmixSharedAssetsPlaylist[] playlists = getInstance().getPlaylists();
194+
TreeMap<String, DropmixSharedAssetsPlaylist> dropmixSharedAssetsPlaylistTreeMap = new TreeMap<>();
209195
TreeMap<String, String> generatedCardSwap = new TreeMap<>();
210196
Set<String> alreadySwappedPlaylists = new HashSet<>();
211-
for (PlaylistDetail pl: playlists) {
212-
playlistDetailTreeMap.put(pl.name, pl);
197+
for (DropmixSharedAssetsPlaylist pl: playlists) {
198+
dropmixSharedAssetsPlaylistTreeMap.put(pl.name, pl);
213199
}
214200
for (String playlist: playlistNames) {
215-
PlaylistDetail srcPl = playlistDetailTreeMap.get(playlist);
216-
PlaylistDetail swapPl = playlistDetailTreeMap.get(plSwap.get(playlist));
201+
DropmixSharedAssetsPlaylist srcPl = dropmixSharedAssetsPlaylistTreeMap.get(playlist);
202+
DropmixSharedAssetsPlaylist swapPl = dropmixSharedAssetsPlaylistTreeMap.get(plSwap.get(playlist));
217203
if (alreadySwappedPlaylists.contains(swapPl.name)) {
218204
continue;
219205
}
@@ -225,7 +211,15 @@ public static TreeMap<String, String> getCardSwapFromPlaylist(TreeMap<String, St
225211
generatedCardSwap.put(srcPl.cards[i], swapPl.cards[i]);
226212
generatedCardSwap.put(swapPl.cards[i], srcPl.cards[i]);
227213
} catch (Exception e) {
228-
continue;
214+
e.printStackTrace();
215+
}
216+
}
217+
if (includeBafflers) {
218+
String srcBaffler = srcPl.getBaffler();
219+
String swapBaffler = swapPl.getBaffler();
220+
if (srcBaffler != null && swapBaffler != null) {
221+
generatedCardSwap.put(srcBaffler, swapBaffler);
222+
generatedCardSwap.put(swapBaffler, srcBaffler);
229223
}
230224
}
231225
alreadySwappedPlaylists.add(playlist);

src/model/AppStateTest.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ public void getCardSwapFromPlaylist() {
1414
plSwap.put("highness", "sweets");
1515
plSwap.put("sweets", "highness");
1616

17-
TreeMap<String, String> cardSwap = AppState.getCardSwapFromPlaylist(plSwap);
17+
TreeMap<String, String> cardSwap = AppState.getCardSwapFromPlaylist(plSwap, false);
1818
Assertions.assertEquals(cardSwap.size(), 30);
1919
Assertions.assertTrue(cardSwap.containsKey("LIC_0058_Wild"));
2020
Assertions.assertTrue(cardSwap.containsValue("LIC_0058_Wild"));
@@ -25,7 +25,7 @@ public void getCardSwapFromPlaylist() {
2525
plSwap.clear();
2626
plSwap.put("baffler", "promo");
2727
plSwap.put("promo", "baffler");
28-
cardSwap = AppState.getCardSwapFromPlaylist(plSwap);
28+
AppState.getCardSwapFromPlaylist(plSwap, false);
2929
} catch (RuntimeException e) {
3030
errorMessage = e.getMessage();
3131
}
@@ -37,7 +37,7 @@ public void getCardSwapFromPlaylist() {
3737
plSwap.put("instinct", "bomb");
3838
plSwap.put("chiller", "instinct");
3939
plSwap.put("bomb", "chiller");
40-
cardSwap = AppState.getCardSwapFromPlaylist(plSwap);
40+
AppState.getCardSwapFromPlaylist(plSwap, false);
4141
} catch (RuntimeException e) {
4242
errorMessage = e.getMessage();
4343
}

0 commit comments

Comments
 (0)