Skip to content

Commit 28b1363

Browse files
committed
Add back coremods support.
1 parent 05d9c53 commit 28b1363

File tree

4 files changed

+261
-32
lines changed

4 files changed

+261
-32
lines changed

loader/src/main/java/com/fox2code/foxloader/patching/PreLoader.java

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,11 @@
2727
import com.fox2code.foxloader.dependencies.DependencyHelper;
2828
import com.fox2code.foxloader.launcher.FileInfo;
2929
import com.fox2code.foxloader.launcher.FoxLauncher;
30+
import com.fox2code.foxloader.loader.ModLoaderInit;
3031
import com.fox2code.foxloader.patching.game.GamePatches;
3132
import com.fox2code.foxloader.patching.mixin.MixinModLoader;
3233
import com.fox2code.foxloader.utils.Platform;
34+
import com.fox2code.foxloader.utils.io.IOUtils;
3335
import com.fox2code.rebuild.ClassDataProvider;
3436
import org.objectweb.asm.tree.ClassNode;
3537
import org.spongepowered.asm.mixin.transformer.IMixinTransformer;
@@ -43,15 +45,21 @@
4345
import java.net.URISyntaxException;
4446
import java.net.URL;
4547
import java.net.URLConnection;
48+
import java.nio.charset.StandardCharsets;
4649
import java.nio.file.Files;
50+
import java.util.Arrays;
51+
import java.util.LinkedList;
4752
import java.util.Objects;
4853

4954
public final class PreLoader {
55+
private static final File coremods = new File(FoxLauncher.getGameDir(), "coremods");
5056
private static final File tmpRoot = new File(FoxLauncher.getGameDir(), ".foxloader");
5157
private static final File tmpDir = new File(tmpRoot,
5258
File.separator + "internal" + File.separator + "patched");
5359
private static final File patchedFile = new File(tmpDir,
5460
"ReIndev-v" + BuildConfig.REINDEV_VERSION + "-fl" + BuildConfig.FOXLOADER_VERSION + ".jar");
61+
private static final File patchedHash = new File(tmpDir,
62+
"ReIndev-v" + BuildConfig.REINDEV_VERSION + "-fl" + BuildConfig.FOXLOADER_VERSION + ".hash");
5563
private static File devPatchedFile = null;
5664
// Use reference to allow memory to be freed on demand
5765
private static Reference<ClassDataProvider> classDataProviderReference;
@@ -70,16 +78,55 @@ public static void initializePatching() {
7078
IMixinTransformer mixinTransformer = MixinModLoader.initializeMixin(FoxLauncher.isClient());
7179
FoxLauncher.getFoxClassLoader().installWrappedExtensions(
7280
patchingLoaderExtensions = new PatchingLoaderExtensions(mixinTransformer));
81+
LinkedList<File> coreMods = new LinkedList<>();
82+
if (coremods.isDirectory() || coremods.mkdirs()) {
83+
coreMods.addAll(Arrays.asList(Objects.requireNonNull(coremods.listFiles(
84+
(dir, name) -> name.endsWith(".zip") || name.endsWith(".jar")))));
85+
}
7386
if (!FoxLauncher.DEVELOPING_FOXLOADER && !FoxLauncher.DEV_MODE) {
87+
PreLoaderMetaJarHash preLoaderMetaJarHash = new PreLoaderMetaJarHash();
88+
preLoaderMetaJarHash.addString(BuildConfig.REINDEV_VERSION);
89+
preLoaderMetaJarHash.addString(ModLoaderInit.FOXLOADER_TRUE_SHA_256);
7490
try {
75-
if (!patchedFile.exists()) {
91+
LinkedList<FileInfo> coreModsInfos = new LinkedList<>();
92+
for (File coreMod : coreMods) {
93+
FileInfo fileInfo = new FileInfo(coreMod);
94+
coreModsInfos.add(fileInfo);
95+
preLoaderMetaJarHash.addString(fileInfo.sha256);
96+
}
97+
preLoaderMetaJarHash.freeze();
98+
final String currentHash = preLoaderMetaJarHash.getHash();
99+
String previousHashAndSize = "";
100+
String jarSize = "";
101+
if (patchedFile.exists() && patchedHash.exists()) {
102+
try {
103+
previousHashAndSize = new String(Files.readAllBytes(
104+
patchedHash.toPath()), StandardCharsets.UTF_8);
105+
jarSize = String.format("%08X", patchedFile.length());
106+
} catch (Exception ignored) {}
107+
}
108+
109+
if (jarSize.isEmpty() || !previousHashAndSize.equals(currentHash + jarSize)) {
110+
if (tmpDir.isDirectory()) {
111+
for (File child : Objects.requireNonNull(tmpDir.listFiles())) {
112+
IOUtils.deleteFile(child);
113+
}
114+
}
76115
File file = DependencyHelper.loadDependencyAsFile(DependencyHelper.reIndevDependencySlim);
77-
GamePatches.patchSlimJar(file, patchedFile);
116+
GamePatches.patchSlimJarWithCoreMods(file, coreMods, patchedFile);
117+
jarSize = String.format("%08X", patchedFile.length());
118+
Files.write(patchedHash.toPath(), (currentHash + jarSize).getBytes(StandardCharsets.UTF_8));
78119
}
79120
FoxLauncher.getFoxClassLoader().setPatchedSlimInfo(new FileInfo(patchedFile));
121+
for (FileInfo coreMod : coreModsInfos) {
122+
FoxLauncher.getFoxClassLoader().addFileToClassLoader(coreMod);
123+
}
80124
} catch (IOException e) {
81125
throw new RuntimeException("Failed to patch ReIndev", e);
82126
}
127+
} else if (!coreMods.isEmpty()) {
128+
ModLoaderInit.getModLoaderLogger().warning(
129+
"Core mods are not supported inside a development environment!");
83130
}
84131
}
85132

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
/*
2+
* MIT License
3+
*
4+
* Copyright (c) 2023-2025 Fox2Code
5+
*
6+
* Permission is hereby granted, free of charge, to any person obtaining a copy
7+
* of this software and associated documentation files (the "Software"), to deal
8+
* in the Software without restriction, including without limitation the rights
9+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
* copies of the Software, and to permit persons to whom the Software is
11+
* furnished to do so, subject to the following conditions:
12+
*
13+
* The above copyright notice and this permission notice shall be included in all
14+
* copies or substantial portions of the Software.
15+
*
16+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22+
* SOFTWARE.
23+
*/
24+
package com.fox2code.foxloader.patching;
25+
26+
import java.io.ByteArrayOutputStream;
27+
import java.io.DataOutputStream;
28+
import java.io.IOException;
29+
import java.security.MessageDigest;
30+
import java.security.NoSuchAlgorithmException;
31+
32+
final class PreLoaderMetaJarHash {
33+
private final MessageDigest digest;
34+
private final ByteArrayOutputStream baos;
35+
private final DataOutputStream dos;
36+
private byte[] cache;
37+
38+
public PreLoaderMetaJarHash() {
39+
try {
40+
this.digest = MessageDigest.getInstance("SHA-256");
41+
} catch (NoSuchAlgorithmException e) {
42+
throw new RuntimeException(e);
43+
}
44+
this.baos = new ByteArrayOutputStream();
45+
this.dos = new DataOutputStream(this.baos);
46+
}
47+
48+
public void addString(String text) {
49+
if (this.cache != null)
50+
throw new IllegalStateException("Hash has been frozen");
51+
try {
52+
this.dos.writeUTF(text);
53+
this.dos.writeByte(0);
54+
} catch (IOException e) {
55+
throw new RuntimeException(e);
56+
}
57+
}
58+
59+
public byte[] makeHash() {
60+
if (this.cache != null) return this.cache;
61+
byte[] result = this.digest.digest(this.baos.toByteArray());
62+
if (result.length != 32) {
63+
throw new AssertionError(
64+
"Result hash is not the result hash of a SHA-256 hash " +
65+
"(got " + result.length + ", expected 32)");
66+
}
67+
return result;
68+
}
69+
70+
public String getHash() {
71+
byte[] hash = makeHash();
72+
StringBuilder builder = new StringBuilder();
73+
for (byte b : hash) {
74+
builder.append(String.format("%02X", b));
75+
}
76+
return builder.toString();
77+
}
78+
79+
public void freeze() {
80+
this.cache = this.makeHash();
81+
}
82+
}

patching/src/main/java/com/fox2code/foxloader/patching/game/GamePatches.java

Lines changed: 124 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
package com.fox2code.foxloader.patching.game;
2525

2626
import com.fox2code.foxloader.patching.TransformerUtils;
27+
import com.fox2code.foxloader.utils.io.IOUtils;
2728
import org.objectweb.asm.ClassReader;
2829
import org.objectweb.asm.ClassWriter;
2930
import org.objectweb.asm.tree.ClassNode;
@@ -33,6 +34,7 @@
3334
import java.util.*;
3435
import java.util.function.Function;
3536
import java.util.zip.ZipEntry;
37+
import java.util.zip.ZipFile;
3638
import java.util.zip.ZipInputStream;
3739
import java.util.zip.ZipOutputStream;
3840

@@ -115,7 +117,7 @@ public static void patchSlimJarDev(File slimJar, File patchedJar) throws IOExcep
115117
}
116118
boolean failedRename = false;
117119
try {
118-
patchSlimJarImpl(slimJar, patchedJar, true);
120+
patchSlimJarImpl(slimJar, null, patchedJar, true);
119121
} finally {
120122
if (!patchedJar.renameTo(patchedJar)) {
121123
failedRename = true;
@@ -127,10 +129,20 @@ public static void patchSlimJarDev(File slimJar, File patchedJar) throws IOExcep
127129
}
128130

129131
public static void patchSlimJar(File slimJar, File patchedJar) throws IOException {
130-
patchSlimJarImpl(slimJar, patchedJar, false);
132+
patchSlimJarImpl(slimJar, null, patchedJar, false);
131133
}
132134

133-
private static void patchSlimJarImpl(File slimJar, File patchedJar, boolean check) throws IOException {
135+
public static void patchSlimJarWithCoreMods(File slimJar, List<File> coreMods, File patchedJar) throws IOException {
136+
if (coreMods == null || coreMods.isEmpty()) {
137+
patchSlimJar(slimJar, patchedJar);
138+
return;
139+
}
140+
try (JarSourceSet jarSourceSet = new JarSourceSet(coreMods)) {
141+
patchSlimJarImpl(slimJar, jarSourceSet, patchedJar, false);
142+
}
143+
}
144+
145+
private static void patchSlimJarImpl(File slimJar,JarSourceSet jarSourceSet, File patchedJar, boolean check) throws IOException {
134146
HashSet<String> classesToPatch = new HashSet<>(gameClassPatches.keySet());
135147
try(ZipInputStream zipInputStream = new ZipInputStream(Files.newInputStream(slimJar.toPath()));
136148
ZipOutputStream zipOutputStream = new ZipOutputStream(Files.newOutputStream(patchedJar.toPath()))) {
@@ -139,27 +151,18 @@ private static void patchSlimJarImpl(File slimJar, File patchedJar, boolean chec
139151
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(131072);
140152
while ((zipEntry = zipInputStream.getNextEntry()) != null) {
141153
String path = zipEntry.getName();
142-
if (path.endsWith(".class")) {
143-
byteArrayOutputStream.reset();
144-
copy(zipInputStream, byteArrayOutputStream);
145-
ClassReader classReader = new ClassReader(byteArrayOutputStream.toByteArray());
146-
ClassNode classNode = new ClassNode();
147-
classReader.accept(classNode, ClassReader.SKIP_FRAMES);
148-
classesToPatch.remove(classNode.name);
149-
classNode = patchClassNode(classNode);
150-
if (classNode != null) {
151-
zipOutputStream.putNextEntry(new ZipEntry(zipEntry.getName()));
152-
ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);
153-
classNode.accept(classWriter);
154-
byte[] compiled = classWriter.toByteArray();
155-
zipOutputStream.write(compiled);
156-
zipOutputStream.closeEntry();
157-
if (check) TransformerUtils.checkBytecodeValidity(compiled);
158-
}
159-
} else {
160-
zipOutputStream.putNextEntry(new ZipEntry(zipEntry.getName()));
161-
copy(zipInputStream, zipOutputStream);
162-
zipOutputStream.closeEntry();
154+
InputStream jarSourceInputStream = jarSourceSet != null ?
155+
jarSourceSet.getInputStreamParsed(path) : null;
156+
patchAndInsert(byteArrayOutputStream, classesToPatch, zipOutputStream,
157+
jarSourceInputStream == null ? zipInputStream : jarSourceInputStream,
158+
path, check, jarSourceInputStream != null);
159+
}
160+
if (jarSourceSet != null) {
161+
while ((zipEntry = jarSourceSet.nextExtraZipEntry()) != null) {
162+
String path = zipEntry.getName();
163+
InputStream inputStream = jarSourceSet.getInputStreamOfCurrentEntry();
164+
patchAndInsert(byteArrayOutputStream, classesToPatch, zipOutputStream,
165+
inputStream, path, check, true);
163166
}
164167
}
165168
zipOutputStream.finish();
@@ -169,13 +172,104 @@ private static void patchSlimJarImpl(File slimJar, File patchedJar, boolean chec
169172
}
170173
}
171174

172-
// Utils port for game patches
173-
static void copy(InputStream inputStream, OutputStream outputStream) throws IOException {
174-
byte[] byteChunk = new byte[4096];
175-
int n;
175+
private static void patchAndInsert(
176+
ByteArrayOutputStream byteArrayOutputStream, HashSet<String> classesToPatch,
177+
ZipOutputStream zipOutputStream, InputStream inputStream,
178+
String path, boolean check, boolean closeInput) throws IOException {
179+
if (path.endsWith(".class")) {
180+
byteArrayOutputStream.reset();
181+
IOUtils.copy(inputStream, byteArrayOutputStream);
182+
if (closeInput) {
183+
inputStream.close();
184+
}
185+
ClassReader classReader = new ClassReader(byteArrayOutputStream.toByteArray());
186+
ClassNode classNode = new ClassNode();
187+
classReader.accept(classNode, ClassReader.SKIP_FRAMES);
188+
classesToPatch.remove(classNode.name);
189+
classNode = patchClassNode(classNode);
190+
if (classNode != null) {
191+
zipOutputStream.putNextEntry(new ZipEntry(path));
192+
ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);
193+
classNode.accept(classWriter);
194+
byte[] compiled = classWriter.toByteArray();
195+
zipOutputStream.write(compiled);
196+
zipOutputStream.closeEntry();
197+
if (check) TransformerUtils.checkBytecodeValidity(compiled);
198+
}
199+
} else {
200+
zipOutputStream.putNextEntry(new ZipEntry(path));
201+
IOUtils.copy(inputStream, zipOutputStream);
202+
if (closeInput) {
203+
inputStream.close();
204+
}
205+
zipOutputStream.closeEntry();
206+
}
207+
}
208+
209+
private static final class JarSourceSet implements Closeable {
210+
private final List<ZipFile> zipFiles;
211+
private final HashSet<String> parsedFiles;
212+
private final Iterator<ZipFile> zipFileIterator;
213+
private ZipFile currentZipFile;
214+
private Enumeration<? extends ZipEntry> zipEntryEnumeration;
215+
private ZipEntry currentZipEntry;
176216

177-
while ((n = inputStream.read(byteChunk)) > 0) {
178-
outputStream.write(byteChunk, 0, n);
217+
private JarSourceSet(List<File> files) throws IOException {
218+
this.zipFiles = new ArrayList<>();
219+
try {
220+
for (File file : files) {
221+
this.zipFiles.add(new ZipFile(file));
222+
}
223+
} catch (IOException ioe) {
224+
try {
225+
this.close();
226+
} catch (IOException ignored) {}
227+
throw ioe;
228+
}
229+
this.parsedFiles = new HashSet<>();
230+
this.zipFileIterator = this.zipFiles.iterator();
231+
}
232+
233+
public InputStream getInputStreamParsed(String path) throws IOException {
234+
this.parsedFiles.add(path);
235+
for (ZipFile zipFile : this.zipFiles) {
236+
ZipEntry zipEntry = zipFile.getEntry(path);
237+
if (zipEntry != null) {
238+
return zipFile.getInputStream(zipEntry);
239+
}
240+
}
241+
return null;
242+
}
243+
244+
public ZipEntry nextExtraZipEntry() {
245+
while (true) {
246+
while (this.zipEntryEnumeration == null ||
247+
!this.zipEntryEnumeration.hasMoreElements()) {
248+
if (!this.zipFileIterator.hasNext()) {
249+
return null;
250+
}
251+
this.currentZipFile = this.zipFileIterator.next();
252+
this.zipEntryEnumeration = this.currentZipFile.entries();
253+
}
254+
while (this.zipEntryEnumeration.hasMoreElements()) {
255+
ZipEntry zipEntry = this.zipEntryEnumeration.nextElement();
256+
if (this.parsedFiles.add(zipEntry.getName())) {
257+
return this.currentZipEntry = zipEntry;
258+
}
259+
}
260+
}
261+
}
262+
263+
public InputStream getInputStreamOfCurrentEntry() throws IOException {
264+
return this.currentZipFile.getInputStream(this.currentZipEntry);
265+
}
266+
267+
@Override
268+
public void close() throws IOException {
269+
for (ZipFile zipFile : this.zipFiles) {
270+
zipFile.close();
271+
}
272+
this.zipFiles.clear();
179273
}
180274
}
181275
}

patching/src/main/java/com/fox2code/foxloader/utils/io/IOUtils.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,4 +105,10 @@ public static byte[] sha256Of(String text) {
105105
}
106106
return hash;
107107
}
108+
109+
public static void deleteFile(File file) throws IOException {
110+
if (!file.exists()) return;
111+
if (file.isDirectory()) throw new IOException("\"" + file.getPath() + "\" is a directory");
112+
if (!file.delete()) throw new IOException("Failed to delete \"" + file.getPath() + "\"");
113+
}
108114
}

0 commit comments

Comments
 (0)