Require extensions to be namespaced #14
|
@ -10,7 +10,7 @@ credits = [
|
||||||
{ name = "You", roles = ["author", "other_role"] }
|
{ name = "You", roles = ["author", "other_role"] }
|
||||||
]
|
]
|
||||||
|
|
||||||
[frog.extensions]
|
[frog.extensions.frogloader]
|
||||||
prelaunch = "dev.frogmc.frogloader.example.ExamplePreLaunchExtension"
|
prelaunch = "dev.frogmc.frogloader.example.ExamplePreLaunchExtension"
|
||||||
mixin = "example_mod.mixins.json"
|
mixin = "example_mod.mixins.json"
|
||||||
accesswidener = "example_mod.accesswidener"
|
accesswidener = "example_mod.accesswidener"
|
||||||
|
|
|
@ -10,6 +10,6 @@ credits = [
|
||||||
{ name = "You", roles = ["author", "other_role"] }
|
{ name = "You", roles = ["author", "other_role"] }
|
||||||
]
|
]
|
||||||
|
|
||||||
[frog.extensions]
|
[frog.extensions.frogloader]
|
||||||
prelaunch="dev.frogmc.example.ExampleTestMod"
|
prelaunch="dev.frogmc.example.ExampleTestMod"
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
package dev.frogmc.frogloader.api.exception;
|
||||||
|
|
||||||
|
public class ModExtensionResolutionException extends Exception {
|
||||||
|
|
||||||
|
public ModExtensionResolutionException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ModExtensionResolutionException(Throwable cause) {
|
||||||
|
super(cause);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ModExtensionResolutionException(String message, Throwable cause) {
|
||||||
|
super(message, cause);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -1,7 +1,7 @@
|
||||||
package dev.frogmc.frogloader.api.extensions;
|
package dev.frogmc.frogloader.api.extensions;
|
||||||
|
|
||||||
import dev.frogmc.frogloader.api.mod.ModExtensions;
|
import dev.frogmc.frogloader.api.mod.ModExtensions;
|
||||||
import dev.frogmc.frogloader.impl.mod.BuiltinExtensions;
|
import dev.frogmc.frogloader.impl.Constants;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The Pre-Launch Extension.
|
* The Pre-Launch Extension.
|
||||||
|
@ -13,7 +13,7 @@ public interface PreLaunchExtension {
|
||||||
/**
|
/**
|
||||||
* This extension's id. This is the key to use in your frog.mod.toml.
|
* This extension's id. This is the key to use in your frog.mod.toml.
|
||||||
*/
|
*/
|
||||||
String ID = BuiltinExtensions.PRE_LAUNCH;
|
String ID = Constants.EXTENSION_PRE_LAUNCH;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The initializer. This method will be invoked when this extension is run.
|
* The initializer. This method will be invoked when this extension is run.
|
||||||
|
|
|
@ -1,150 +1,74 @@
|
||||||
package dev.frogmc.frogloader.api.mod;
|
package dev.frogmc.frogloader.api.mod;
|
||||||
|
|
||||||
import java.lang.invoke.MethodHandle;
|
|
||||||
import java.lang.invoke.MethodHandleProxies;
|
|
||||||
import java.lang.invoke.MethodHandles;
|
|
||||||
import java.lang.invoke.MethodType;
|
|
||||||
import java.lang.reflect.Field;
|
|
||||||
import java.lang.reflect.Method;
|
|
||||||
import java.lang.reflect.Modifier;
|
|
||||||
import java.util.Collection;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
import dev.frogmc.frogloader.api.exception.ModExtensionResolutionException;
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This class stores a mod's extensions.
|
* This class stores a mod's extensions.
|
||||||
* <p>An extension is a simple key-value mapping of a string name to any value.</p>
|
* <p>
|
||||||
* <p>This class further provides utility methods to easily work with extension values of various types,
|
* An extension is a simple key-value mapping of a string name to any value.
|
||||||
* especially for extensions providing some form of class or method reference.</p>
|
* </p>
|
||||||
|
* <p>
|
||||||
|
* This class further provides utility methods to easily work with extension
|
||||||
|
* values of various types, especially for extensions providing some form of
|
||||||
|
* class or method reference.
|
||||||
|
* </p>
|
||||||
*
|
*
|
||||||
* @see ModProperties
|
* @see ModProperties
|
||||||
*/
|
*/
|
||||||
public final class ModExtensions {
|
public interface ModExtensions {
|
||||||
|
|
||||||
private static final Logger LOGGER = LoggerFactory.getLogger(ModExtensions.class);
|
|
||||||
private final Map<String, Object> extensions;
|
|
||||||
|
|
||||||
private ModExtensions(Map<String, Object> entries) {
|
|
||||||
extensions = entries;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieve a new instance of this class.
|
|
||||||
* <p><strong>Internal use only.</strong></p>
|
|
||||||
*
|
|
||||||
* @param entries the entries read from the mod's properties file
|
|
||||||
* @return an instance of this class
|
|
||||||
*/
|
|
||||||
public static ModExtensions of(Map<String, Object> entries) {
|
|
||||||
return new ModExtensions(entries);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the value of the provided extension name.
|
* Get the value of the provided extension name.
|
||||||
*
|
*
|
||||||
|
* @param modId The mod ID this extension is stored under
|
||||||
* @param key The extension name to query
|
* @param key The extension name to query
|
||||||
* @param <T> The type of the value of this extension
|
* @param <T> The type of the value of this extension
|
||||||
|
*
|
||||||
* @return The value of the extension, or null
|
* @return The value of the extension, or null
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings("unchecked")
|
<T> T get(String modId, String key);
|
||||||
public <T> T get(String key) {
|
|
||||||
return (T) extensions.get(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the value of the provided extension name.
|
* Get the value of the provided extension name.
|
||||||
*
|
*
|
||||||
* @param key The extension name to query
|
* @param modId The mod ID this extension is stored under
|
||||||
* @param defaultValue a default value
|
* @param key The extension name to query
|
||||||
* @param <T> The type of the value of this extension
|
* @param defaultValue A default value
|
||||||
|
* @param <T> The type of the value of this extension
|
||||||
|
*
|
||||||
* @return The value of the extension, or the default value if it isn't present
|
* @return The value of the extension, or the default value if it isn't present
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings("unchecked")
|
<T> T getOrDefault(String modId, String key, T defaultValue);
|
||||||
public <T> T getOrDefault(String key, T defaultValue) {
|
|
||||||
return (T) extensions.getOrDefault(key, defaultValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Run the given action on this extension if it is present.
|
* Run the given action on this extension if it is present.
|
||||||
*
|
*
|
||||||
* @param key the extension name to query
|
* @param modId The mod ID this extension is stored under
|
||||||
* @param action the action to run on the value of the extension if it is present
|
* @param key The extension name to query
|
||||||
* @param <T> The type of the value of this extension
|
* @param action The action to run on the value of the extension if it is present
|
||||||
|
* @param <T> The type of the value of this extension
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings({"rawtypes", "unchecked"})
|
<T> void runIfPresent(String modId, String key, Consumer<T> action);
|
||||||
public <T> void runIfPresent(String key, Consumer<T> action) {
|
|
||||||
Object value = get(key);
|
|
||||||
if (value == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (value instanceof Collection c) {
|
|
||||||
((Collection<T>) c).forEach(action);
|
|
||||||
} else {
|
|
||||||
action.accept((T) value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Run the given action on this extension if it is present.
|
* Run the given action on this extension if it is present.
|
||||||
* <p>This method simplifies handling references to classes or methods.</p>
|
* <p>
|
||||||
* It will first query the string value of the extension and, if it is found, create a new instance of the referenced class
|
* This method simplifies handling references to classes or methods.
|
||||||
* or invoke the referenced method to retrieve an instance of the provided class. Then the provided action is run on this
|
* </p>
|
||||||
* object.
|
* It will first query the string value of the extension and, if it is found,
|
||||||
|
* create a new instance of the referenced class or invoke the referenced method
|
||||||
|
* to retrieve an instance of the provided class. Then the provided action is
|
||||||
|
* run on this object.
|
||||||
*
|
*
|
||||||
* @param key The name of the extension
|
* @param modId The mod ID this extension is stored under
|
||||||
* @param type The class type of the extension (The class the extension class is extending/implementing)
|
* @param key The name of the extension
|
||||||
|
* @param type The class type of the extension (The class the extension class is extending/implementing)
|
||||||
* @param action The action to run on the newly retrieved instance of the provided class
|
* @param action The action to run on the newly retrieved instance of the provided class
|
||||||
* @param <T> The type of the class
|
* @param <T> The type of the class
|
||||||
|
*
|
||||||
|
* @throws ModExtensionResolutionException If the reference could not be resolved
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings({"unchecked"})
|
<T> void runIfPresent(String modId, String key, Class<T> type, Consumer<T> action) throws ModExtensionResolutionException;
|
||||||
public <T> void runIfPresent(String key, Class<T> type, Consumer<T> action) {
|
|
||||||
runIfPresent(key, (Consumer<String>) s -> {
|
|
||||||
try {
|
|
||||||
T object;
|
|
||||||
if (s.contains("::")) {
|
|
||||||
String[] parts = s.split("::");
|
|
||||||
Class<?> extension = Class.forName(parts[0]);
|
|
||||||
object = (T) handleReference(extension, type, parts[1]);
|
|
||||||
} else {
|
|
||||||
object = (T) MethodHandles.lookup().findConstructor(Class.forName(s), MethodType.methodType(void.class)).invoke();
|
|
||||||
}
|
|
||||||
if (object != null) {
|
|
||||||
action.accept(object);
|
|
||||||
}
|
|
||||||
} catch (Throwable e) {
|
|
||||||
LOGGER.warn("Failed to instantiate Extension: ", e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private Object handleReference(Class<?> clazz, Class<?> type, String ref) throws Throwable {
|
|
||||||
try {
|
|
||||||
Field found = clazz.getDeclaredField(ref);
|
|
||||||
found.setAccessible(true);
|
|
||||||
return found.get(null);
|
|
||||||
} catch (Exception ignored) {
|
|
||||||
}
|
|
||||||
|
|
||||||
Method found = null;
|
|
||||||
for (Method method : clazz.getDeclaredMethods()) {
|
|
||||||
if (method.getName().equals(ref)) {
|
|
||||||
if (found != null) {
|
|
||||||
throw new IllegalArgumentException("Ambiguous method reference: "+ref+" in "+clazz.getName());
|
|
||||||
}
|
|
||||||
found = method;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (found != null) {
|
|
||||||
MethodHandle method = MethodHandles.lookup().unreflect(found);
|
|
||||||
if (!Modifier.isStatic(found.getModifiers())) {
|
|
||||||
method = method.bindTo(MethodHandles.lookup().findConstructor(clazz, MethodType.methodType(void.class)).invoke());
|
|
||||||
}
|
|
||||||
return MethodHandleProxies.asInterfaceInstance(type, method);
|
|
||||||
}
|
|
||||||
throw new IllegalArgumentException("Could not find either a static field or a method named '" + ref + "' in class " + clazz.getName() + "!");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -65,8 +65,9 @@ public interface ModProvider {
|
||||||
/**
|
/**
|
||||||
* This method will be invoked just before the game is launched. It may be used for a pre-launch entrypoint for mods.
|
* This method will be invoked just before the game is launched. It may be used for a pre-launch entrypoint for mods.
|
||||||
* @param mods The mods loaded by this provider
|
* @param mods The mods loaded by this provider
|
||||||
|
* @throws Exception If an exception occurs during pre-launch. It will be handled by the loader.
|
||||||
*/
|
*/
|
||||||
default void preLaunch(Collection<ModProperties> mods) {
|
default void preLaunch(Collection<ModProperties> mods) throws Exception {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
14
src/main/java/dev/frogmc/frogloader/impl/Constants.java
Normal file
14
src/main/java/dev/frogmc/frogloader/impl/Constants.java
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
package dev.frogmc.frogloader.impl;
|
||||||
|
|
||||||
|
import lombok.experimental.UtilityClass;
|
||||||
|
|
||||||
|
@UtilityClass
|
||||||
|
public class Constants {
|
||||||
|
public final String MOD_ID = "frogloader";
|
||||||
|
public final String EXTENSION_MIXIN_CONFIG = "mixin";
|
||||||
|
public final String EXTENSION_INCLUDED_JARS = "included_jars";
|
||||||
|
public final String EXTENSION_PRE_LAUNCH = "prelaunch";
|
||||||
|
public final String EXTENSION_ACCESSWIDENER = "accesswidener";
|
||||||
|
public final String EXTENSION_LOADING_TYPE = "loading_type";
|
||||||
|
public final String EXTENSION_MOD_PROVIDER = "modprovider";
|
||||||
|
}
|
|
@ -78,7 +78,8 @@ public class FrogLoaderImpl implements FrogLoader {
|
||||||
modIds = collectModIds();
|
modIds = collectModIds();
|
||||||
LOGGER.info(ModUtil.getModList(getMods()));
|
LOGGER.info(ModUtil.getModList(getMods()));
|
||||||
LOGGER.info("Launching...");
|
LOGGER.info("Launching...");
|
||||||
modProviders.forEach(plugin -> plugin.preLaunch(mods.get(plugin.id()).values()));
|
for (ModProvider plugin : modProviders)
|
||||||
|
plugin.preLaunch(mods.get(plugin.id()).values());
|
||||||
gamePlugin.run();
|
gamePlugin.run();
|
||||||
} catch (Throwable t) {
|
} catch (Throwable t) {
|
||||||
LoaderGui.execReport(CrashReportGenerator.writeReport(t, getMods()), false);
|
LoaderGui.execReport(CrashReportGenerator.writeReport(t, getMods()), false);
|
||||||
|
|
|
@ -11,7 +11,6 @@ import dev.frogmc.frogloader.api.plugin.GamePlugin;
|
||||||
import dev.frogmc.frogloader.api.plugin.ModProvider;
|
import dev.frogmc.frogloader.api.plugin.ModProvider;
|
||||||
import dev.frogmc.frogloader.impl.gui.LoaderGui;
|
import dev.frogmc.frogloader.impl.gui.LoaderGui;
|
||||||
import dev.frogmc.frogloader.impl.mixin.AWProcessor;
|
import dev.frogmc.frogloader.impl.mixin.AWProcessor;
|
||||||
import dev.frogmc.frogloader.impl.mod.BuiltinExtensions;
|
|
||||||
import dev.frogmc.frogloader.impl.mod.ModDependencyResolver;
|
import dev.frogmc.frogloader.impl.mod.ModDependencyResolver;
|
||||||
import dev.frogmc.frogloader.impl.util.CrashReportGenerator;
|
import dev.frogmc.frogloader.impl.util.CrashReportGenerator;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
|
@ -47,10 +46,12 @@ public class PluginLoader {
|
||||||
properties.put(plugin.id(), loadedMods);
|
properties.put(plugin.id(), loadedMods);
|
||||||
|
|
||||||
LOGGER.debug("Loaded {} mod(s) from provider: {}", loadedMods.size(), plugin.id());
|
LOGGER.debug("Loaded {} mod(s) from provider: {}", loadedMods.size(), plugin.id());
|
||||||
loadedMods.forEach(p -> p.extensions().runIfPresent(BuiltinExtensions.MOD_PROVIDER, ModProvider.class, provider -> {
|
for (ModProperties p : loadedMods) {
|
||||||
providers.add(provider);
|
p.extensions().runIfPresent(Constants.MOD_ID, Constants.EXTENSION_MOD_PROVIDER, ModProvider.class, provider -> {
|
||||||
size[0]++;
|
providers.add(provider);
|
||||||
}));
|
size[0]++;
|
||||||
owlsys marked this conversation as resolved
|
|||||||
|
});
|
||||||
|
}
|
||||||
modProviders.add(plugin);
|
modProviders.add(plugin);
|
||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
LOGGER.error("Error during plugin initialisation: ", e);
|
LOGGER.error("Error during plugin initialisation: ", e);
|
||||||
|
@ -78,7 +79,7 @@ public class PluginLoader {
|
||||||
private static void initializeModMixins(Collection<ModProperties> loadedMods) {
|
private static void initializeModMixins(Collection<ModProperties> loadedMods) {
|
||||||
Map<String, ModProperties> configs = new HashMap<>();
|
Map<String, ModProperties> configs = new HashMap<>();
|
||||||
loadedMods.forEach(props -> {
|
loadedMods.forEach(props -> {
|
||||||
Object o = props.extensions().get(BuiltinExtensions.MIXIN_CONFIG);
|
Object o = props.extensions().get(Constants.MOD_ID, Constants.EXTENSION_MIXIN_CONFIG);
|
||||||
if (o instanceof String name) {
|
if (o instanceof String name) {
|
||||||
configs.put(name, props);
|
configs.put(name, props);
|
||||||
Mixins.addConfiguration(name);
|
Mixins.addConfiguration(name);
|
||||||
|
|
|
@ -14,12 +14,11 @@ import java.util.regex.Pattern;
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
import dev.frogmc.frogloader.api.mod.ModProperties;
|
import dev.frogmc.frogloader.api.mod.ModProperties;
|
||||||
|
import dev.frogmc.frogloader.impl.Constants;
|
||||||
import dev.frogmc.frogloader.impl.launch.transformer.AccessWidener;
|
import dev.frogmc.frogloader.impl.launch.transformer.AccessWidener;
|
||||||
import dev.frogmc.frogloader.impl.mod.BuiltinExtensions;
|
|
||||||
|
|
||||||
public class AWProcessor {
|
public class AWProcessor {
|
||||||
|
|
||||||
private static final String AW_EXTENSION_NAME = BuiltinExtensions.ACCESSWIDENER;
|
|
||||||
private static final Predicate<String> HEADER = Pattern.compile("accessWidener\\s+v[12]\\s+.*").asMatchPredicate();
|
private static final Predicate<String> HEADER = Pattern.compile("accessWidener\\s+v[12]\\s+.*").asMatchPredicate();
|
||||||
private static final String SEPARATOR = "[\\t ]+";
|
private static final String SEPARATOR = "[\\t ]+";
|
||||||
|
|
||||||
|
@ -30,7 +29,7 @@ public class AWProcessor {
|
||||||
Map<String, Map<String, AccessWidener.Entry>> mutations = new ConcurrentHashMap<>();
|
Map<String, Map<String, AccessWidener.Entry>> mutations = new ConcurrentHashMap<>();
|
||||||
Set<String> classNames = new ConcurrentSkipListSet<>();
|
Set<String> classNames = new ConcurrentSkipListSet<>();
|
||||||
|
|
||||||
mods.stream().map(ModProperties::extensions).map(e -> e.get(AW_EXTENSION_NAME)).flatMap(o -> {
|
mods.stream().map(ModProperties::extensions).map(e -> e.get(Constants.MOD_ID, Constants.EXTENSION_ACCESSWIDENER)).flatMap(o -> {
|
||||||
if (o instanceof Collection<?> c) {
|
if (o instanceof Collection<?> c) {
|
||||||
return c.stream().map(Object::toString);
|
return c.stream().map(Object::toString);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +0,0 @@
|
||||||
package dev.frogmc.frogloader.impl.mod;
|
|
||||||
|
|
||||||
import lombok.experimental.UtilityClass;
|
|
||||||
|
|
||||||
@UtilityClass
|
|
||||||
public class BuiltinExtensions {
|
|
||||||
public final String MIXIN_CONFIG = "mixin";
|
|
||||||
public final String INCLUDED_JARS = "included_jars";
|
|
||||||
public final String PRE_LAUNCH = "prelaunch";
|
|
||||||
public final String ACCESSWIDENER = "accesswidener";
|
|
||||||
public final String LOADING_TYPE = "loading_type";
|
|
||||||
public final String MOD_PROVIDER = "modprovider";
|
|
||||||
}
|
|
|
@ -25,7 +25,7 @@ public class JavaModProperties {
|
||||||
"",
|
"",
|
||||||
Map.of(System.getProperty("java.vm.vendor"), Collections.singleton("Vendor")),
|
Map.of(System.getProperty("java.vm.vendor"), Collections.singleton("Vendor")),
|
||||||
new ModDependencies(Collections.emptySet(), Collections.emptySet(), Collections.emptySet(), Collections.emptySet()),
|
new ModDependencies(Collections.emptySet(), Collections.emptySet(), Collections.emptySet(), Collections.emptySet()),
|
||||||
ModExtensions.of(Collections.emptyMap()), Collections.emptySet());
|
new ModExtensionsImpl(Collections.emptyMap()), Collections.emptySet());
|
||||||
}
|
}
|
||||||
return INSTANCE;
|
return INSTANCE;
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,7 @@ import com.google.common.collect.MultimapBuilder;
|
||||||
import dev.frogmc.frogloader.api.mod.ModDependencies;
|
import dev.frogmc.frogloader.api.mod.ModDependencies;
|
||||||
import dev.frogmc.frogloader.api.mod.ModProperties;
|
import dev.frogmc.frogloader.api.mod.ModProperties;
|
||||||
import dev.frogmc.frogloader.api.mod.SemVer;
|
import dev.frogmc.frogloader.api.mod.SemVer;
|
||||||
|
import dev.frogmc.frogloader.impl.Constants;
|
||||||
import dev.frogmc.frogloader.impl.SemVerParseException;
|
import dev.frogmc.frogloader.impl.SemVerParseException;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
|
@ -121,7 +122,7 @@ public class ModDependencyResolver {
|
||||||
DependencyEntry value = entry.getValue();
|
DependencyEntry value = entry.getValue();
|
||||||
if (!(presentMods.containsKey(s) && value.range.versionMatches(presentOrProvided.get(s)))) { // The dependency isn't fulfilled by any present mods
|
if (!(presentMods.containsKey(s) && value.range.versionMatches(presentOrProvided.get(s)))) { // The dependency isn't fulfilled by any present mods
|
||||||
if (!(provides.containsKey(s) && provides.get(s).stream().map(ProvidedMod::version).allMatch(value.range::versionMatches))) { // The dependency also isn't fulfilled by any provided mods
|
if (!(provides.containsKey(s) && provides.get(s).stream().map(ProvidedMod::version).allMatch(value.range::versionMatches))) { // The dependency also isn't fulfilled by any provided mods
|
||||||
if (value.origin.extensions().getOrDefault(BuiltinExtensions.LOADING_TYPE, "required").equals("required")) {
|
if (value.origin.extensions().getOrDefault(Constants.MOD_ID, Constants.EXTENSION_LOADING_TYPE, "required").equals("required")) {
|
||||||
unfulfilled.add(new UnfulfilledDependencyException.Entry(value.origin, s, value.range, presentOrProvided.get(s), value.link(), value.name()));
|
unfulfilled.add(new UnfulfilledDependencyException.Entry(value.origin, s, value.range, presentOrProvided.get(s), value.link(), value.name()));
|
||||||
} else {
|
} else {
|
||||||
LOGGER.debug("Skipping optional mod: {} ({})", value.origin().id(), value.origin().name());
|
LOGGER.debug("Skipping optional mod: {} ({})", value.origin().id(), value.origin().name());
|
||||||
|
|
|
@ -0,0 +1,136 @@
|
||||||
|
package dev.frogmc.frogloader.impl.mod;
|
||||||
|
|
||||||
|
import java.lang.invoke.MethodHandle;
|
||||||
|
import java.lang.invoke.MethodHandleProxies;
|
||||||
|
import java.lang.invoke.MethodHandles;
|
||||||
|
import java.lang.invoke.MethodType;
|
||||||
|
import java.lang.invoke.WrongMethodTypeException;
|
||||||
|
import java.lang.reflect.AccessibleObject;
|
||||||
|
import java.lang.reflect.Field;
|
||||||
|
import java.lang.reflect.Member;
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
import java.lang.reflect.Modifier;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
import dev.frogmc.frogloader.api.exception.ModExtensionResolutionException;
|
||||||
|
import dev.frogmc.frogloader.api.mod.ModExtensions;
|
||||||
|
|
||||||
|
public class ModExtensionsImpl implements ModExtensions {
|
||||||
|
|
||||||
|
private final Map<Key, Object> map;
|
||||||
|
|
||||||
|
public ModExtensionsImpl(Map<Key, Object> map) {
|
||||||
|
this.map = map;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public <T> T get(String modId, String key) {
|
||||||
|
return (T) map.get(new Key(modId, key));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public <T> T getOrDefault(String modId, String key, T defaultValue) {
|
||||||
|
return (T) map.getOrDefault(new Key(modId, key), defaultValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public <T> void runIfPresent(String modId, String key, Consumer<T> action) {
|
||||||
|
Object value = get(modId, key);
|
||||||
|
|
||||||
|
if (value == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (value instanceof Collection)
|
||||||
|
((Collection<T>) value).forEach(action);
|
||||||
|
else
|
||||||
|
action.accept((T) value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <T> void runIfPresent(String modId, String key, Class<T> type, Consumer<T> action)
|
||||||
|
throws ModExtensionResolutionException {
|
||||||
|
String s = get(modId, key);
|
||||||
|
|
||||||
|
if (s == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
T object;
|
||||||
|
if (s.contains("::")) {
|
||||||
|
String[] parts = s.split("::", 2);
|
||||||
|
try {
|
||||||
|
Class<?> extension = Class.forName(parts[0]);
|
||||||
|
object = (T) handleReference(extension, type, parts[1]);
|
||||||
|
} catch (ModExtensionResolutionException e) {
|
||||||
|
throw e;
|
||||||
|
} catch (Throwable e) {
|
||||||
|
throw new ModExtensionResolutionException(e);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
Class<?> extension = Class.forName(s);
|
||||||
|
|
||||||
|
if (!type.isAssignableFrom(extension))
|
||||||
|
throw new ModExtensionResolutionException(extension + " does not inherit from " + type);
|
||||||
|
|
||||||
|
object = (T) MethodHandles.lookup().findConstructor(extension, MethodType.methodType(void.class))
|
||||||
|
.invoke();
|
||||||
|
} catch (ModExtensionResolutionException e) {
|
||||||
|
throw e;
|
||||||
|
} catch (Throwable e) {
|
||||||
|
throw new ModExtensionResolutionException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (object != null) {
|
||||||
|
action.accept(object);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Object handleReference(Class<?> owner, Class<?> type, String name)
|
||||||
|
throws ModExtensionResolutionException, IllegalAccessException {
|
||||||
|
try {
|
||||||
|
Field found = owner.getDeclaredField(name);
|
||||||
|
|
||||||
|
if (Modifier.isStatic(found.getModifiers())) {
|
||||||
|
found.setAccessible(true);
|
||||||
|
return found.get(null);
|
||||||
|
}
|
||||||
|
} catch (NoSuchFieldException ignored) {
|
||||||
|
}
|
||||||
|
|
||||||
|
Method found = null;
|
||||||
|
for (Method method : owner.getDeclaredMethods()) {
|
||||||
|
if (!Modifier.isStatic(method.getModifiers()))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (!method.getName().equals(name))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (found != null)
|
||||||
|
throw new ModExtensionResolutionException("Ambiguous method reference: " + name + " in " + owner.getName());
|
||||||
|
|
||||||
|
found = method;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (found != null) {
|
||||||
|
found.setAccessible(true);
|
||||||
|
|
||||||
|
MethodHandle method = MethodHandles.lookup().unreflect(found);
|
||||||
|
return MethodHandleProxies.asInterfaceInstance(type, method);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new ModExtensionResolutionException("Could not find a static field or method named '" + name + "' in class " + owner.getName() + "!");
|
||||||
|
}
|
||||||
|
|
||||||
|
public record Key(String modId, String key) {
|
||||||
|
}
|
||||||
|
}
|
|
@ -153,16 +153,23 @@ public class ModPropertiesReader {
|
||||||
var suggests = parseDependencies.apply("suggests");
|
var suggests = parseDependencies.apply("suggests");
|
||||||
var provides = parseDependencies.apply("provides");
|
var provides = parseDependencies.apply("provides");
|
||||||
|
|
||||||
|
UnmodifiableConfig extensionsConfig = config.get("frog.extensions");
|
||||||
|
Map<ModExtensionsImpl.Key, Object> extensions = new HashMap<>();
|
||||||
|
if (extensionsConfig != null) {
|
||||||
|
for (var modEntry : extensionsConfig.entrySet()) {
|
||||||
|
UnmodifiableConfig modExtensionsConig = modEntry.getValue();
|
||||||
|
|
||||||
|
for (var entry : modExtensionsConig.entrySet())
|
||||||
|
extensions.put(new ModExtensionsImpl.Key(modEntry.getKey(), entry.getKey()), entry.getValue());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!badProperties.isEmpty())
|
if (!badProperties.isEmpty())
|
||||||
throw new InvalidModPropertiesException(id, sources, badProperties);
|
throw new InvalidModPropertiesException(id, sources, badProperties);
|
||||||
|
|
||||||
UnmodifiableConfig extensionsConfig = config.get("frog.extensions");
|
|
||||||
Map<String, Object> extensions = new HashMap<>();
|
|
||||||
if (extensionsConfig != null)
|
|
||||||
extensionsConfig.entrySet().forEach(entry -> extensions.put(entry.getKey(), entry.getValue()));
|
|
||||||
|
|
||||||
return new ModPropertiesImpl(id, name, icon, semVer, license, Collections.unmodifiableMap(credits),
|
return new ModPropertiesImpl(id, name, icon, semVer, license, Collections.unmodifiableMap(credits),
|
||||||
new ModDependencies(depends, breaks, suggests, provides), ModExtensions.of(extensions), sources);
|
new ModDependencies(depends, breaks, suggests, provides), new ModExtensionsImpl(extensions), sources);
|
||||||
});
|
});
|
||||||
|
|
||||||
private static final Map<String, Parser> versions = Arrays.stream(values()).collect(Collectors.toMap(v -> v.version, v -> v.parser));
|
private static final Map<String, Parser> versions = Arrays.stream(values()).collect(Collectors.toMap(v -> v.version, v -> v.parser));
|
||||||
|
|
|
@ -6,6 +6,7 @@ import java.util.stream.Collectors;
|
||||||
|
|
||||||
import com.electronwill.nightconfig.core.UnmodifiableConfig;
|
import com.electronwill.nightconfig.core.UnmodifiableConfig;
|
||||||
import dev.frogmc.frogloader.api.mod.ModProperties;
|
import dev.frogmc.frogloader.api.mod.ModProperties;
|
||||||
|
import dev.frogmc.frogloader.impl.Constants;
|
||||||
|
|
||||||
public class ModUtil {
|
public class ModUtil {
|
||||||
|
|
||||||
|
@ -66,7 +67,7 @@ public class ModUtil {
|
||||||
private static Map<ModProperties, Collection<String>> getParentMods(Collection<ModProperties> mods) {
|
private static Map<ModProperties, Collection<String>> getParentMods(Collection<ModProperties> mods) {
|
||||||
Map<ModProperties, Collection<String>> children = new HashMap<>();
|
Map<ModProperties, Collection<String>> children = new HashMap<>();
|
||||||
for (ModProperties mod : mods) {
|
for (ModProperties mod : mods) {
|
||||||
List<UnmodifiableConfig> entries = mod.extensions().get(BuiltinExtensions.INCLUDED_JARS);
|
List<UnmodifiableConfig> entries = mod.extensions().get(Constants.MOD_ID, Constants.EXTENSION_INCLUDED_JARS);
|
||||||
if (entries != null) {
|
if (entries != null) {
|
||||||
for (var jar : entries) {
|
for (var jar : entries) {
|
||||||
String id = jar.get("id");
|
String id = jar.get("id");
|
||||||
|
|
|
@ -21,6 +21,7 @@ import dev.frogmc.frogloader.api.mod.ModExtensions;
|
||||||
import dev.frogmc.frogloader.api.mod.ModProperties;
|
import dev.frogmc.frogloader.api.mod.ModProperties;
|
||||||
import dev.frogmc.frogloader.api.plugin.GamePlugin;
|
import dev.frogmc.frogloader.api.plugin.GamePlugin;
|
||||||
import dev.frogmc.frogloader.impl.FrogLoaderImpl;
|
import dev.frogmc.frogloader.impl.FrogLoaderImpl;
|
||||||
|
import dev.frogmc.frogloader.impl.mod.ModExtensionsImpl;
|
||||||
import dev.frogmc.frogloader.impl.mod.ModPropertiesImpl;
|
import dev.frogmc.frogloader.impl.mod.ModPropertiesImpl;
|
||||||
import dev.frogmc.frogloader.impl.util.SystemProperties;
|
import dev.frogmc.frogloader.impl.util.SystemProperties;
|
||||||
import dev.frogmc.thyroxine.HttpHelper;
|
import dev.frogmc.thyroxine.HttpHelper;
|
||||||
|
@ -203,6 +204,6 @@ public class MinecraftGamePlugin implements GamePlugin {
|
||||||
MinecraftSemVerImpl.get(version), "MC-EULA",
|
MinecraftSemVerImpl.get(version), "MC-EULA",
|
||||||
Map.of("Mojang AB", Collections.singleton("Author")),
|
Map.of("Mojang AB", Collections.singleton("Author")),
|
||||||
new ModDependencies(Collections.emptySet(), Collections.emptySet(), Collections.emptySet(), Collections.emptySet()),
|
new ModDependencies(Collections.emptySet(), Collections.emptySet(), Collections.emptySet(), Collections.emptySet()),
|
||||||
ModExtensions.of(Collections.emptyMap()), Collections.emptySet());
|
new ModExtensionsImpl(Collections.emptyMap()), Collections.emptySet());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,10 +10,11 @@ import java.util.function.Consumer;
|
||||||
|
|
||||||
import com.electronwill.nightconfig.core.UnmodifiableConfig;
|
import com.electronwill.nightconfig.core.UnmodifiableConfig;
|
||||||
import dev.frogmc.frogloader.api.FrogLoader;
|
import dev.frogmc.frogloader.api.FrogLoader;
|
||||||
|
import dev.frogmc.frogloader.api.exception.ModExtensionResolutionException;
|
||||||
import dev.frogmc.frogloader.api.extensions.PreLaunchExtension;
|
import dev.frogmc.frogloader.api.extensions.PreLaunchExtension;
|
||||||
import dev.frogmc.frogloader.api.mod.ModProperties;
|
import dev.frogmc.frogloader.api.mod.ModProperties;
|
||||||
import dev.frogmc.frogloader.api.plugin.ModProvider;
|
import dev.frogmc.frogloader.api.plugin.ModProvider;
|
||||||
import dev.frogmc.frogloader.impl.mod.BuiltinExtensions;
|
import dev.frogmc.frogloader.impl.Constants;
|
||||||
import dev.frogmc.frogloader.impl.mod.ModPropertiesReader;
|
import dev.frogmc.frogloader.impl.mod.ModPropertiesReader;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
@ -115,7 +116,7 @@ public class FrogModProvider implements ModProvider {
|
||||||
if (opt.isPresent()) {
|
if (opt.isPresent()) {
|
||||||
ModProperties p = opt.get();
|
ModProperties p = opt.get();
|
||||||
modPaths.put(mod, p);
|
modPaths.put(mod, p);
|
||||||
List<UnmodifiableConfig> entries = p.extensions().getOrDefault(BuiltinExtensions.INCLUDED_JARS, Collections.emptyList());
|
List<UnmodifiableConfig> entries = p.extensions().getOrDefault(Constants.MOD_ID, Constants.EXTENSION_INCLUDED_JARS, Collections.emptyList());
|
||||||
if (entries.isEmpty()) {
|
if (entries.isEmpty()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -135,7 +136,8 @@ public class FrogModProvider implements ModProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void preLaunch(Collection<ModProperties> mods) {
|
public void preLaunch(Collection<ModProperties> mods) throws ModExtensionResolutionException {
|
||||||
owlsys marked this conversation as resolved
owlsys
commented
the exception should probably be handled inside the loop to just skip the faulty mod instead of breaking and propagating the error upwards the exception should probably be handled inside the loop to just skip the faulty mod instead of breaking and propagating the error upwards
kode
commented
If a mod wants to handle an error without crashing, shouldn't it have a try catch block in its prelaunch handler? If a mod wants to handle an error without crashing, shouldn't it have a try catch block in its prelaunch handler?
owlsys
commented
it may still be okay like this considering mod devs should definitely know when their stuff is malformed or throwing errors it may still be okay like this considering mod devs should definitely know when their stuff is malformed or throwing errors
|
|||||||
mods.forEach(mod -> mod.extensions().runIfPresent(PreLaunchExtension.ID, PreLaunchExtension.class, PreLaunchExtension::onPreLaunch));
|
for (ModProperties mod : mods)
|
||||||
|
mod.extensions().runIfPresent(Constants.MOD_ID, Constants.EXTENSION_PRE_LAUNCH, PreLaunchExtension.class, PreLaunchExtension::onPreLaunch);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue
this single-element array could be replaced with a normal int since it doesn't need to be used anymore (since the lambda was replaced with a loop)
I think it is still needed as it is a callback to runIfPresent
I tried to look for other usages already but it's fairly possible I missed something in the diff view.