-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathDynamicModLoader.java
More file actions
294 lines (269 loc) · 13.7 KB
/
Copy pathDynamicModLoader.java
File metadata and controls
294 lines (269 loc) · 13.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
package ru.mymod;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import net.fabricmc.api.EnvType;
import net.fabricmc.loader.api.LanguageAdapter;
import net.fabricmc.loader.api.Version;
import net.fabricmc.loader.api.metadata.ContactInformation;
import net.fabricmc.loader.api.metadata.ModEnvironment;
import net.fabricmc.loader.api.metadata.Person;
import net.fabricmc.loader.impl.FabricLoaderImpl;
import net.fabricmc.loader.impl.ModContainerImpl;
import net.fabricmc.loader.impl.entrypoint.EntrypointStorage;
import net.fabricmc.loader.impl.launch.FabricLauncherBase;
import net.fabricmc.loader.impl.metadata.EntrypointMetadata;
import net.fabricmc.loader.impl.metadata.LoaderModMetadata;
import net.fabricmc.loader.impl.metadata.NestedJarEntry;
import org.spongepowered.asm.mixin.Mixins;
import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.util.*;
import java.util.jar.JarFile;
import java.util.zip.ZipEntry;
public class DynamicModLoader {
public static int compareVersions(String v1, String v2) {
String[] a = v1.split("\\.");
String[] b = v2.split("\\.");
int len = Math.max(a.length, b.length);
for (int i = 0; i < len; i++) {
int num1 = i < a.length ? Integer.parseInt(a[i]) : 0;
int num2 = i < b.length ? Integer.parseInt(b[i]) : 0;
if (num1 != num2) return Integer.compare(num1, num2);
}
return 0;
}
public void registerMod(File jarFile) throws Exception {
// 1. Read fabric.mod.json from JAR
JsonObject modJson = readModJson(jarFile);
// 2. Add JAR to classpath
Path modPath = jarFile.toPath();
FabricLauncherBase.getLauncher().addToClassPath(modPath);
// 3. Parse metadata
LoaderModMetadata metadata = parseMetadata(jarFile, modJson);
// 4. Create ModCandidateImpl
String vers = (String) FabricLoaderImpl.class.getField("VERSION").get(null);
System.out.println("[Loader] Fabric version: " + vers);
//compareVersions("0.16.0", "0.16.1"); // -1 (меньше)
//compareVersions("0.18.3", "0.16.2"); // 1 (больше)
//compareVersions("0.14.6", "0.14.6"); // 0 (равно)
int version = compareVersions("0.16.0", vers);
Class<?> clazz;
if (version == -1) {
// > 0.16.0
clazz = Class.forName("net.fabricmc.loader.impl.discovery.ModCandidateImpl");
} else {
clazz = Class.forName("net.fabricmc.loader.impl.discovery.ModCandidate");
}
Method method = clazz.getDeclaredMethod("createPlain", List.class, LoaderModMetadata.class, boolean.class, Collection.class);
method.setAccessible(true);
Object candidate = method.invoke(null,
Collections.singletonList(modPath),
metadata,
false,
Collections.emptyList()
);
Constructor<ModContainerImpl> constructor = ModContainerImpl.class.getDeclaredConstructor(clazz);
constructor.setAccessible(true);
// 5. Create ModContainerImpl
ModContainerImpl container = constructor.newInstance(candidate);
// 6. Add to FabricLoaderImpl via reflection
FabricLoaderImpl loader = FabricLoaderImpl.INSTANCE;
Field modsField = FabricLoaderImpl.class.getDeclaredField("mods");
modsField.setAccessible(true);
@SuppressWarnings("unchecked")
List<ModContainerImpl> mods = (List<ModContainerImpl>) modsField.get(loader);
mods.add(container);
Field modMapField = FabricLoaderImpl.class.getDeclaredField("modMap");
modMapField.setAccessible(true);
@SuppressWarnings("unchecked")
Map<String, ModContainerImpl> modMap = (Map<String, ModContainerImpl>) modMapField.get(loader);
modMap.put(metadata.getId(), container);
// 7. Register mixin configs
for (String mixinConfig : metadata.getMixinConfigs(EnvType.CLIENT)) {
Mixins.addConfiguration(mixinConfig);
}
// 8. Register entrypoints
registerEntrypoints(loader, container, metadata);
System.out.println("[Loader] Dynamic mod registered: " + metadata.getId());
}
private JsonObject readModJson(File jarFile) throws IOException {
try (JarFile jar = new JarFile(jarFile)) {
ZipEntry entry = jar.getEntry("fabric.mod.json");
if (entry == null) {
throw new FileNotFoundException("No fabric.mod.json in " + jarFile);
}
try (InputStream in = jar.getInputStream(entry)) {
return new Gson().fromJson(new InputStreamReader(in), JsonObject.class);
}
}
}
private LoaderModMetadata parseMetadata(File jarFile, JsonObject json) throws Exception {
// Build metadata using V1ModMetadata constructor via reflection
// since it's package-private
String id = json.get("id").getAsString();
String versionStr = json.has("version") ? json.get("version").getAsString() : "1.0.0";
Version version = net.fabricmc.loader.impl.util.version.VersionParser.parseSemantic(versionStr);
Collection<String> provides = Collections.emptyList();
if (json.has("provides")) {
provides = new ArrayList<>();
for (JsonElement e : json.getAsJsonArray("provides")) {
provides.add(e.getAsString());
}
}
ModEnvironment environment = ModEnvironment.UNIVERSAL;
if (json.has("environment")) {
environment = ModEnvironment.valueOf(json.get("environment").getAsString().toUpperCase());
}
Map<String, List<EntrypointMetadata>> entrypoints = parseEntrypoints(json);
Collection<Object> mixins = parseMixins(json);
Collection<NestedJarEntry> jars = Collections.emptyList();
if (json.has("jars")) {
jars = new ArrayList<>();
for (JsonElement e : json.getAsJsonArray("jars")) {
jars.add(new NestedJarEntry() {
final String str = e.getAsJsonObject().get("file").getAsString();
@Override
public String getFile() {
return str;
}
});
}
try (JarFile jar = new JarFile(jarFile)) {
for (NestedJarEntry entry : jars) {
String filePath = entry.getFile();
ZipEntry nestedEntry = jar.getEntry(filePath);
if (nestedEntry == null) continue;
// Извлекаем во временную папку
Path nestedPath = jarFile.toPath().getParent()
.resolve("libs")
.resolve(Paths.get(filePath).getFileName());
Files.createDirectories(nestedPath.getParent());
try (InputStream is = jar.getInputStream(nestedEntry)) {
Files.copy(is, nestedPath, StandardCopyOption.REPLACE_EXISTING);
}
FabricLauncherBase.getLauncher().addToClassPath(nestedPath);
}
}
}
String name = json.has("name") ? json.get("name").getAsString() : null;
String description = json.has("description") ? json.get("description").getAsString() : "";
Collection<Person> authors = Collections.emptyList();
Collection<Person> contributors = Collections.emptyList();
ContactInformation contact = ContactInformation.EMPTY;
Collection<String> license = Collections.emptyList();
Map<String, String> languageAdapters = Collections.emptyMap();
// Use V1ModMetadata constructor via reflection
Class<?> mixinEntryClass = Class.forName("net.fabricmc.loader.impl.metadata.V1ModMetadata$MixinEntry");
Class<?> clazz = Class.forName("net.fabricmc.loader.impl.metadata.V1ModMetadata");
//Map<String, List<EntrypointMetadata>> entrypoints, Collection<NestedJarEntry> jars,
// Collection<MixinEntry> mixins, /* @Nullable */ String classTweaker,
// Collection<ModDependency> dependencies, boolean hasRequires,
// /* @Nullable */ String name, /* @Nullable */String description,
// Collection<Person> authors, Collection<Person> contributors, /* @Nullable */ContactInformation contact, Collection<String> license, IconEntry icon,
// Map<String, String> languageAdapters,
// Map<String, CustomValue> customValues
Class<?> clazz2 = Class.forName("net.fabricmc.loader.impl.metadata.V1ModMetadata$IconEntry");
Constructor<?> constructor = clazz.getDeclaredConstructor(
String.class, Version.class, Collection.class, ModEnvironment.class,
Map.class, Collection.class, Collection.class, String.class,
Collection.class, boolean.class, String.class, String.class,
Collection.class, Collection.class, ContactInformation.class,
Collection.class, clazz2, Map.class, Map.class
);
constructor.setAccessible(true);
return (LoaderModMetadata) constructor.newInstance(
id, version, provides, environment,
entrypoints, jars, mixins, null,
Collections.emptyList(), false, name, description,
authors, contributors, contact, license,
null, languageAdapters, Collections.emptyMap()
);
}
private Map<String, List<EntrypointMetadata>> parseEntrypoints(JsonObject json) {
Map<String, List<EntrypointMetadata>> result = new HashMap<>();
if (!json.has("entrypoints")) return result;
JsonObject entrypointsObj = json.getAsJsonObject("entrypoints");
for (Map.Entry<String, JsonElement> entry : entrypointsObj.entrySet()) {
String key = entry.getKey();
List<EntrypointMetadata> list = new ArrayList<>();
for (JsonElement e : entry.getValue().getAsJsonArray()) {
String value;
String adapter = "default";
if (e.isJsonObject()) {
JsonObject obj = e.getAsJsonObject();
value = obj.get("value").getAsString();
if (obj.has("adapter")) {
adapter = obj.get("adapter").getAsString();
}
} else {
value = e.getAsString();
}
String adapterFinal = adapter;
String valueFinal = value;
EntrypointMetadata metadata = new EntrypointMetadata() {
@Override
public String getAdapter() {
return adapterFinal;
}
@Override
public String getValue() {
return valueFinal;
}
};
list.add(metadata);
}
result.put(key, list);
}
return result;
}
private Collection<Object> parseMixins(JsonObject json) throws Exception {
List<Object> result = new ArrayList<>();
if (!json.has("mixins")) return result;
Class<?> mixinEntryClass = Class.forName("net.fabricmc.loader.impl.metadata.V1ModMetadata$MixinEntry");
Constructor<?> constructor = mixinEntryClass.getDeclaredConstructor(String.class, ModEnvironment.class);
constructor.setAccessible(true);
for (JsonElement e : json.getAsJsonArray("mixins")) {
String config;
String environment = null;
if (e.isJsonObject()) {
JsonObject obj = e.getAsJsonObject();
config = obj.get("config").getAsString();
if (obj.has("environment")) {
environment = obj.get("environment").getAsString();
}
} else {
config = e.getAsString();
}
result.add(constructor.newInstance(config, ModEnvironment.UNIVERSAL)); // TODO parse from envirmoment string
}
return result;
}
private void registerEntrypoints(FabricLoaderImpl loader, ModContainerImpl container, LoaderModMetadata metadata) throws Exception {
// Get entrypointStorage via reflection
Field storageField = FabricLoaderImpl.class.getDeclaredField("entrypointStorage");
storageField.setAccessible(true);
EntrypointStorage storage = (EntrypointStorage) storageField.get(loader);
// Get adapterMap via reflection
Field adapterMapField = FabricLoaderImpl.class.getDeclaredField("adapterMap");
adapterMapField.setAccessible(true);
@SuppressWarnings("unchecked")
Map<String, LanguageAdapter> adapterMap = (Map<String, LanguageAdapter>) adapterMapField.get(loader);
// Register each entrypoint
Method addMethod = EntrypointStorage.class.getDeclaredMethod(
"add", ModContainerImpl.class, String.class, EntrypointMetadata.class, Map.class
);
addMethod.setAccessible(true);
for (String key : metadata.getEntrypointKeys()) {
for (EntrypointMetadata entry : metadata.getEntrypoints(key)) {
addMethod.invoke(storage, container, key, entry, adapterMap);
}
}
}
}