Require extensions to be namespaced

This commit is contained in:
TheKodeToad 2024-09-01 14:09:22 +01:00
parent ac3e0d86df
commit 12f9d823b4
18 changed files with 241 additions and 156 deletions

View file

@ -10,8 +10,8 @@ credits = [
{ name = "You", roles = ["author", "other_role"] }
]
[frog.extensions]
prelaunch = "dev.frogmc.frogloader.example.ExamplePreLaunchExtension"
[frog.extensions.frogloader]
prelaunch = "dev.frogmc.frogloader.example.ExamplePreLaunchExtension::onPreLaunch"
mixin = "example_mod.mixins.json"
accesswidener = "example_mod.accesswidener"
modprovider = "dev.frogmc.frogloader.example.ExampleModProvider"

View file

@ -10,6 +10,6 @@ credits = [
{ name = "You", roles = ["author", "other_role"] }
]
[frog.extensions]
[frog.extensions.frogloader]
prelaunch="dev.frogmc.example.ExampleTestMod"

View file

@ -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);
}
}

View file

@ -1,7 +1,7 @@
package dev.frogmc.frogloader.api.extensions;
import dev.frogmc.frogloader.api.mod.ModExtensions;
import dev.frogmc.frogloader.impl.mod.BuiltinExtensions;
import dev.frogmc.frogloader.impl.Constants;
/**
* 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.
*/
String ID = BuiltinExtensions.PRE_LAUNCH;
String ID = Constants.EXTENSION_PRE_LAUNCH;
/**
* The initializer. This method will be invoked when this extension is run.

View file

@ -1,150 +1,74 @@
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 org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import dev.frogmc.frogloader.api.exception.ModExtensionResolutionException;
/**
* 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>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>
* <p>
* An extension is a simple key-value mapping of a string name to any value.
* </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
*/
public final class 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);
}
public interface ModExtensions {
/**
* 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 <T> The type of the value of this extension
*
* @return The value of the extension, or null
*/
@SuppressWarnings("unchecked")
public <T> T get(String key) {
return (T) extensions.get(key);
}
<T> T get(String modId, String key);
/**
* Get the value of the provided extension name.
*
* @param key The extension name to query
* @param defaultValue a default value
* @param <T> The type of the value of this extension
* @param modId The mod ID this extension is stored under
* @param key The extension name to query
* @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
*/
@SuppressWarnings("unchecked")
public <T> T getOrDefault(String key, T defaultValue) {
return (T) extensions.getOrDefault(key, defaultValue);
}
<T> T getOrDefault(String modId, String key, T defaultValue);
/**
* Run the given action on this extension if it is present.
*
* @param key the extension name to query
* @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
* @param modId The mod ID this extension is stored under
* @param key The extension name to query
* @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"})
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);
}
}
<T> void runIfPresent(String modId, String key, Consumer<T> action);
/**
* Run the given action on this extension if it is present.
* <p>This method simplifies handling references to classes or methods.</p>
* 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.
* <p>
* This method simplifies handling references to classes or methods.
* </p>
* 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 type The class type of the extension (The class the extension class is extending/implementing)
* @param modId The mod ID this extension is stored under
* @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 <T> The type of the class
* @param <T> The type of the class
*
* @throws ModExtensionResolutionException If the reference could not be resolved
*/
@SuppressWarnings({"unchecked"})
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() + "!");
}
<T> void runIfPresent(String modId, String key, Class<T> type, Consumer<T> action) throws ModExtensionResolutionException;
}

View file

@ -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.
* @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 {
}
/**

View 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";
}

View file

@ -78,7 +78,8 @@ public class FrogLoaderImpl implements FrogLoader {
modIds = collectModIds();
LOGGER.info(ModUtil.getModList(getMods()));
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();
} catch (Throwable t) {
LoaderGui.execReport(CrashReportGenerator.writeReport(t, getMods()), false);

View file

@ -11,7 +11,6 @@ import dev.frogmc.frogloader.api.plugin.GamePlugin;
import dev.frogmc.frogloader.api.plugin.ModProvider;
import dev.frogmc.frogloader.impl.gui.LoaderGui;
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.util.CrashReportGenerator;
import org.slf4j.Logger;
@ -47,10 +46,12 @@ public class PluginLoader {
properties.put(plugin.id(), loadedMods);
LOGGER.debug("Loaded {} mod(s) from provider: {}", loadedMods.size(), plugin.id());
loadedMods.forEach(p -> p.extensions().runIfPresent(BuiltinExtensions.MOD_PROVIDER, ModProvider.class, provider -> {
providers.add(provider);
size[0]++;
}));
for (ModProperties p : loadedMods) {
p.extensions().runIfPresent(Constants.MOD_ID, Constants.EXTENSION_MOD_PROVIDER, ModProvider.class, provider -> {
providers.add(provider);
size[0]++;
});
}
modProviders.add(plugin);
} catch (Throwable e) {
LOGGER.error("Error during plugin initialisation: ", e);
@ -78,7 +79,7 @@ public class PluginLoader {
private static void initializeModMixins(Collection<ModProperties> loadedMods) {
Map<String, ModProperties> configs = new HashMap<>();
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) {
configs.put(name, props);
Mixins.addConfiguration(name);

View file

@ -14,12 +14,11 @@ import java.util.regex.Pattern;
import java.util.stream.Stream;
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.mod.BuiltinExtensions;
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 String SEPARATOR = "[\\t ]+";
@ -30,7 +29,7 @@ public class AWProcessor {
Map<String, Map<String, AccessWidener.Entry>> mutations = new ConcurrentHashMap<>();
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) {
return c.stream().map(Object::toString);
}

View file

@ -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";
}

View file

@ -25,7 +25,7 @@ public class JavaModProperties {
"",
Map.of(System.getProperty("java.vm.vendor"), Collections.singleton("Vendor")),
new ModDependencies(Collections.emptySet(), Collections.emptySet(), Collections.emptySet(), Collections.emptySet()),
ModExtensions.of(Collections.emptyMap()), Collections.emptySet());
new ModExtensionsImpl(Collections.emptyMap()), Collections.emptySet());
}
return INSTANCE;
}

View file

@ -11,6 +11,7 @@ import com.google.common.collect.MultimapBuilder;
import dev.frogmc.frogloader.api.mod.ModDependencies;
import dev.frogmc.frogloader.api.mod.ModProperties;
import dev.frogmc.frogloader.api.mod.SemVer;
import dev.frogmc.frogloader.impl.Constants;
import dev.frogmc.frogloader.impl.SemVerParseException;
import lombok.AllArgsConstructor;
import lombok.Getter;
@ -121,7 +122,7 @@ public class ModDependencyResolver {
DependencyEntry value = entry.getValue();
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 (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()));
} else {
LOGGER.debug("Skipping optional mod: {} ({})", value.origin().id(), value.origin().name());

View file

@ -0,0 +1,129 @@
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);
found.setAccessible(true);
return found.get(null);
} catch (Exception ignored) {
}
Method found = null;
for (Method method : owner.getDeclaredMethods()) {
if (method.getName().equals(name)) {
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 IllegalArgumentException("Could not find either a static field or a method named '" + name + "' in class " + owner.getName() + "!");
}
public record Key(String modId, String key) {
}
}

View file

@ -153,16 +153,23 @@ public class ModPropertiesReader {
var suggests = parseDependencies.apply("suggests");
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())
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),
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));

View file

@ -6,6 +6,7 @@ import java.util.stream.Collectors;
import com.electronwill.nightconfig.core.UnmodifiableConfig;
import dev.frogmc.frogloader.api.mod.ModProperties;
import dev.frogmc.frogloader.impl.Constants;
public class ModUtil {
@ -66,7 +67,7 @@ public class ModUtil {
private static Map<ModProperties, Collection<String>> getParentMods(Collection<ModProperties> mods) {
Map<ModProperties, Collection<String>> children = new HashMap<>();
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) {
for (var jar : entries) {
String id = jar.get("id");

View file

@ -21,6 +21,7 @@ import dev.frogmc.frogloader.api.mod.ModExtensions;
import dev.frogmc.frogloader.api.mod.ModProperties;
import dev.frogmc.frogloader.api.plugin.GamePlugin;
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.util.SystemProperties;
import dev.frogmc.thyroxine.HttpHelper;
@ -204,6 +205,6 @@ public class MinecraftGamePlugin implements GamePlugin {
MinecraftSemVerImpl.get(version), "MC-EULA",
Map.of("Mojang AB", Collections.singleton("Author")),
new ModDependencies(Collections.emptySet(), Collections.emptySet(), Collections.emptySet(), Collections.emptySet()),
ModExtensions.of(Collections.emptyMap()), Collections.emptySet());
new ModExtensionsImpl(Collections.emptyMap()), Collections.emptySet());
}
}

View file

@ -10,10 +10,11 @@ import java.util.function.Consumer;
import com.electronwill.nightconfig.core.UnmodifiableConfig;
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.mod.ModProperties;
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 org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -115,7 +116,7 @@ public class FrogModProvider implements ModProvider {
if (opt.isPresent()) {
ModProperties p = opt.get();
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()) {
return;
}
@ -135,7 +136,8 @@ public class FrogModProvider implements ModProvider {
}
@Override
public void preLaunch(Collection<ModProperties> mods) {
mods.forEach(mod -> mod.extensions().runIfPresent(PreLaunchExtension.ID, PreLaunchExtension.class, PreLaunchExtension::onPreLaunch));
public void preLaunch(Collection<ModProperties> mods) throws ModExtensionResolutionException {
for (ModProperties mod : mods)
mod.extensions().runIfPresent(Constants.MOD_ID, Constants.EXTENSION_PRE_LAUNCH, PreLaunchExtension.class, PreLaunchExtension::onPreLaunch);
}
}