This repository was archived by the owner on Apr 28, 2020. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 55
Expand file tree
/
Copy pathRiftLoader.java
More file actions
358 lines (310 loc) · 15.4 KB
/
RiftLoader.java
File metadata and controls
358 lines (310 loc) · 15.4 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
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
package org.dimdev.riftloader;
import com.google.gson.Gson;
import com.google.gson.JsonParseException;
import com.vdurmont.semver4j.Semver;
import com.vdurmont.semver4j.SemverException;
import net.minecraft.launchwrapper.Launch;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.dimdev.accesstransform.AccessTransformationSet;
import org.dimdev.accesstransform.AccessTransformer;
import org.dimdev.riftloader.listener.InitializationListener;
import org.dimdev.riftloader.listener.Instantiator;
import org.dimdev.utils.InstanceListMap;
import org.dimdev.utils.InstanceMap;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.*;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.zip.ZipException;
public class RiftLoader {
public static final RiftLoader instance = new RiftLoader();
private static final Logger log = LogManager.getLogger("RiftLoader");
private static final Gson GSON = new Gson();
public final File modsDir = new File(Launch.minecraftHome, "mods");
public final File configDir = new File(Launch.minecraftHome, "config");
public AccessTransformer accessTransformer;
private Map<String, ModInfo> modInfoMap = new HashMap<>();
private List<Class<?>> listenerClasses = new ArrayList<>();
private InstanceMap listenerInstanceMap = new InstanceMap();
private InstanceListMap listeners = new InstanceListMap();
private InstanceListMap customListenerInstances = new InstanceListMap();
public void load() {
findMods(modsDir);
sortMods();
initMods();
initAccessTransformer();
}
/**
* Looks for Rift mods (jars containing a 'riftmod.json' at their root) in
* the 'modsDir' directory (creating it if it doesn't exist) and loads them
* into the 'modInfoMap'.
**/
public void findMods(File modsDir) {
// Load jar mods
log.info("Searching for mods in " + modsDir);
modsDir.mkdirs();
for (File file : modsDir.listFiles()) {
if (!file.getName().endsWith(".jar")) continue;
try (JarFile jar = new JarFile(file)) {
// Check if the file contains a 'riftmod.json'
if (!file.isFile()) continue; // Inside try since there may be a SecurityException
JarEntry entry = jar.getJarEntry("riftmod.json");
if (entry != null) {
loadModFromJson(jar.getInputStream(entry), file);
continue;
}
if (jar.getJarEntry("optifine/OptiFineClassTransformer.class") != null) {
ModInfo mod = new ModInfo();
mod.source = file;
mod.id = "optifine";
mod.name = "Optifine";
mod.version = "1.13-alpha8";
mod.authors.add("sp614x");
mod.listeners.add("org.dimdev.riftloader.OptifineLoader");
modInfoMap.put("optifine", mod);
}
log.debug("Skipping " + file + " since it does not contain riftmod.json");
} catch (ZipException e) {
log.error("Could not read file " + file + " as a jar file", e);
} catch (Throwable t) {
log.error("Exception while checking if file " + file + " is a mod", t);
}
}
// Load classpath mods
log.info("Searching mods on classpath");
try {
Enumeration<URL> urls = ClassLoader.getSystemResources("riftmod.json");
while (urls.hasMoreElements()) {
URL url = urls.nextElement();
InputStream in = url.openStream();
// Convert jar utls to file urls (from JarUrlConnection.parseSpecs)
if (url.getProtocol().equals("jar")) {
String spec = url.getFile();
int separator = spec.indexOf("!/");
if (separator == -1) {
throw new MalformedURLException("no !/ found in url spec:" + spec);
}
url = new URL(spec.substring(0, separator));
loadModFromJson(in, new File(url.toURI()));
} else if (url.getProtocol().equals("file")) {
loadModFromJson(in, new File(url.toURI()).getParentFile());
} else {
throw new RuntimeException("Unsupported protocol: " + url);
}
}
} catch (IOException | URISyntaxException e) {
throw new RuntimeException(e);
}
log.info("Loaded " + modInfoMap.size() + " mods");
}
private void loadModFromJson(InputStream in, File source) {
try {
// Parse the 'riftmod.json' and make a ModInfo
ModInfo modInfo = GSON.fromJson(new InputStreamReader(in), ModInfo.class);
modInfo.source = source;
// Make sure the id isn't null and there aren't any duplicates
if (modInfo.id == null) {
log.error("Mod file " + modInfo.source + "'s riftmod.json is missing a 'id' field");
return;
} else if (modInfoMap.containsKey(modInfo.id)) {
throw new ModConflictException("Duplicate mod '" + modInfo.id + "': " + modInfoMap.get(modInfo.id).source + ", " + modInfo.source);
}
// Make sure the version is proper SemVer
if (modInfo.version == null) {
log.warn("Mod file " + modInfo.source + "'s riftmod.json is missing a 'version' field. This may affect dependencies!");
} else {
try {
Semver version = new Semver(modInfo.version, Semver.SemverType.LOOSE);
} catch (SemverException t) {
log.warn("Mod file " + modInfo.source + "'s riftmod.json has a malformed 'version' field. This may affect dependencies!");
log.warn("SemVer error: " + t.getMessage());
}
}
// Add the mod to the 'id -> mod info' map
modInfoMap.put(modInfo.id, modInfo);
log.info("Loaded mod '" + modInfo.id + "'");
} catch (JsonParseException e) {
throw new RuntimeException("Could not read riftmod.json in " + source, e);
}
}
public void sortMods() {
log.debug("Sorting mods"); // TODO
}
public void initMods() {
log.info("Initializing mods");
// Check dependencies
for (ModInfo modInfo : modInfoMap.values()) {
if (!modInfo.dependencies.isEmpty()) {
log.info("Found dependency list for " + modInfo.id);
for (Dependency dependency : modInfo.dependencies) {
log.info(dependency.id);
log.info(dependency.minVersion);
log.info(dependency.load);
if (!modInfoMap.containsKey(dependency.id)) {
if (dependency.type.equals("hard")) throw new MissingDependencyException("Mod " + modInfo.source + " is missing hard dependency " + dependency.id + ":" + dependency.minVersion);
else log.error("Mod " + modInfo.source + " is missing soft dependency " + dependency.id + ":" + dependency.minVersion + ". This may impact gameplay!");
}
ModInfo depInfo = modInfoMap.get(dependency.id);
Semver trueVersion = new Semver(depInfo.version, Semver.SemverType.LOOSE);
try {
Semver lowestVersion = new Semver(dependency.minVersion, Semver.SemverType.LOOSE);
if (trueVersion.isLowerThan(lowestVersion)) {
if (dependency.type.equals("hard")) throw new MissingDependencyException("Mod " + modInfo.source + " has outdated hard dependency: " + dependency.id + " must be at least " + lowestVersion);
else log.error("Mod " + modInfo.source + " has outdated soft dependency: " + dependency.id + " must be at least " + lowestVersion);
}
if (dependency.maxVersion != null) {
Semver highestVersion = new Semver(dependency.maxVersion, Semver.SemverType.LOOSE);
if (trueVersion.isGreaterThan(highestVersion)) {
if (dependency.type.equals("hard")) throw new MissingDependencyException("Mod " + modInfo.source + " has outdated hard dependency: " + dependency.id + " must be at most " + highestVersion);
else log.error("Mod " + modInfo.source + " has outdated soft dependency: " + dependency.id + " must be at most " + highestVersion); }
}
} catch (SemverException t) {
if (dependency.type.equals("hard")) throw new MissingDependencyException("Mod " + modInfo.source + " has malformed hard dependency in " + dependency.id + ": SemVer error " + t.getMessage());
else log.error("Mod " + modInfo.source + " has malformed soft dependency in " + dependency.id + ": SemVer error " + t.getMessage());
}
}
} else {
log.info("Found no dependencies for " + modInfo.id);
}
}
// Load all the mod jars
for (ModInfo modInfo : modInfoMap.values()) {
try {
addURLToClasspath(modInfo.source.toURI().toURL());
} catch (MalformedURLException e) {
throw new RuntimeException(e);
}
}
// Load the listener classes
for (ModInfo modInfo : modInfoMap.values()) {
if (modInfo.listeners != null) {
for (String listenerClassName : modInfo.listeners) {
Class<?> listenerClass;
try {
listenerClass = Launch.classLoader.findClass(listenerClassName);
listenerClasses.add(listenerClass);
} catch (ReflectiveOperationException e) {
throw new RuntimeException("Failed to find listener class " + listenerClassName, e);
}
}
}
}
for (InitializationListener listener : getListeners(InitializationListener.class)) {
listener.onInitialization();
}
log.info("Done initializing mods");
}
private static void addURLToClasspath(URL url) {
try {
Method method = URLClassLoader.class.getDeclaredMethod("addURL", URL.class);
method.setAccessible(true);
method.invoke(ClassLoader.getSystemClassLoader(), url);
Launch.classLoader.addURL(url);
} catch (ReflectiveOperationException e) {
throw new RuntimeException(e);
}
}
public void initAccessTransformer() {
try {
AccessTransformationSet transformations = new AccessTransformationSet();
Enumeration<URL> urls = Launch.classLoader.getResources("access_transformations.at");
while (urls.hasMoreElements()) {
URL url = urls.nextElement();
try (Scanner scanner = new Scanner(url.openStream())) {
while (scanner.hasNextLine()) {
transformations.addMinimumAccessLevel(scanner.nextLine());
}
}
}
accessTransformer = new AccessTransformer(transformations);
Launch.classLoader.registerTransformer("org.dimdev.riftloader.RiftAccessTransformer");
} catch (Throwable t) {
throw new RuntimeException("Failed to initialize access transformers", t);
}
}
public void addMod(ModInfo mod) {
modInfoMap.put(mod.id, mod);
}
public Collection<ModInfo> getMods() {
return modInfoMap.values();
}
public <T> List<T> getListeners(Class<T> listenerInterface) {
List<T> listenerInstances = listeners.get(listenerInterface);
if (listenerInstances == null) {
loadListeners(listenerInterface);
listenerInstances = listeners.get(listenerInterface);
}
return listenerInstances;
}
public <T> void loadListeners(Class<T> listenerInterface) {
List<T> listenerInstances = new ArrayList<>();
listeners.put(listenerInterface, listenerInstances);
for (Class<?> listenerClass : listenerClasses) {
if (listenerInterface.isAssignableFrom(listenerClass)) {
// Get the instance of that class, or create a new one if it wasn't instantiated yet
T listenerInstance = listenerInterface.cast(listenerInstanceMap.get(listenerClass));
if (listenerInstance == null) {
try {
listenerInstance = listenerInterface.cast(newInstanceOfClass(listenerClass));
listenerInstanceMap.castAndPut(listenerClass, listenerInstance);
} catch (ReflectiveOperationException e) {
throw new RuntimeException("Failed to create listener instance", e);
}
}
listenerInstances.add(listenerInstance);
}
}
List<T> customInstances = customListenerInstances.get(listenerInterface);
if (customInstances != null) {
listenerInstances.addAll(customInstances);
}
}
public <T> T newInstanceOfClass(Class<T> listenerClass) throws ReflectiveOperationException {
for (Constructor<?> constructor : listenerClass.getConstructors()) {
if (constructor.getParameterCount() == 0) {
return listenerClass.cast(constructor.newInstance());
}
}
// No no-args constructor found, ask mod instantiators to build an instance
for (Instantiator instantiator : getListeners(Instantiator.class)) {
T instance = instantiator.newInstance(listenerClass);
if (instance != null) {
return instance;
}
}
throw new InstantiationException("Class has no public no-args constructor, and no instantiator handled it either");
}
/**
* Register a custom instance of a class for Rift to use rather than creating
* one by invoking its public no-args constructor.
*/
public <T> void setInstanceForListenerClass(Class<T> listenerClass, T instance) {
listenerInstanceMap.put(listenerClass, instance);
}
/**
* Adds a listener for a particular listener interface. This is an alternative
* to registering the interface class in riftmod.json.
*/
public <T> void addListener(Class<T> listenerInterface, T listener) {
List<T> customInstances = customListenerInstances.get(listenerInterface);
if (customInstances == null) {
customInstances = new ArrayList<>();
customListenerInstances.put(listenerInterface, customInstances);
}
customInstances.add(listener);
List<T> loadedInstances = listeners.get(listenerInterface);
if (loadedInstances != null) {
loadedInstances.add(listener);
}
}
}