Separate plugin types #10

Merged
owlsys merged 14 commits from ecorous/plugin-types into main 2024-06-16 17:13:54 -04:00
14 changed files with 176 additions and 148 deletions
Showing only changes of commit d58867a92e - Show all commits

View file

@ -6,16 +6,15 @@ import java.util.Optional;
import dev.frogmc.frogloader.api.env.Env; import dev.frogmc.frogloader.api.env.Env;
import dev.frogmc.frogloader.api.mod.ModProperties; import dev.frogmc.frogloader.api.mod.ModProperties;
import dev.frogmc.frogloader.api.plugin.FrogGamePlugin; import dev.frogmc.frogloader.api.plugin.GamePlugin;
import dev.frogmc.frogloader.api.plugin.FrogModProvider; import dev.frogmc.frogloader.api.plugin.ModProvider;
import dev.frogmc.frogloader.api.plugin.FrogPlugin;
import dev.frogmc.frogloader.impl.FrogLoaderImpl; import dev.frogmc.frogloader.impl.FrogLoaderImpl;
/** /**
* General API to interact with this loader. * General API to interact with this loader.
* *
* @see ModProperties * @see ModProperties
* @see FrogPlugin * @see ModProvider
* @see Env * @see Env
*/ */
public interface FrogLoader { public interface FrogLoader {
@ -34,14 +33,14 @@ public interface FrogLoader {
* *
* @return The game plugin applicable to the current game * @return The game plugin applicable to the current game
*/ */
FrogGamePlugin getGamePlugin(); GamePlugin getGamePlugin();
/** /**
* Get all loaded mod providers. * Get all loaded mod providers.
* *
* @return A collection of all loaded mod providers * @return A collection of all loaded mod providers
*/ */
Collection<FrogModProvider> getModProviders(); Collection<ModProvider> getModProviders();
/** /**
* Get the current (physical) environment. * Get the current (physical) environment.
@ -103,12 +102,13 @@ public interface FrogLoader {
* *
* @return A collection of all loaded mods * @return A collection of all loaded mods
* @see ModProperties * @see ModProperties
* @see FrogPlugin * @see ModProvider
*/ */
Collection<ModProperties> getMods(); Collection<ModProperties> getMods();
/** /**
* Get the version of the currently loaded game * Get the version of the currently loaded game
*
* @return The current game version * @return The current game version
*/ */
String getGameVersion(); String getGameVersion();

View file

@ -1,22 +0,0 @@
package dev.frogmc.frogloader.api.plugin;
import dev.frogmc.frogloader.api.FrogLoader;
import dev.frogmc.frogloader.api.mod.ModProperties;
public interface FrogGamePlugin {
default void run() {
}
default boolean isApplicable() {
return false;
}
default void init(FrogLoader loader) throws Exception {
}
String queryVersion();
default ModProperties getGameMod() {
return null;
}
}

View file

@ -1,34 +0,0 @@
package dev.frogmc.frogloader.api.plugin;
import java.nio.file.Path;
import java.util.Collection;
import java.util.Collections;
import dev.frogmc.frogloader.api.mod.ModProperties;
public interface FrogModProvider {
String id();
default String loadDirectory() {
return "mods";
}
default boolean isApplicable() {
return false;
}
default boolean isFileApplicable(Path path) {
return false;
}
default boolean isDirectoryApplicable(Path path) {
return false;
}
default void preLaunch(Collection<ModProperties> mods) {
}
default Collection<ModProperties> loadMods(Collection<Path> modFiles) throws Exception {
return Collections.emptySet();
}
}

View file

@ -1,57 +0,0 @@
package dev.frogmc.frogloader.api.plugin;
import java.util.Collection;
import java.util.Collections;
import dev.frogmc.frogloader.api.FrogLoader;
import dev.frogmc.frogloader.api.mod.ModProperties;
/**
* A Plugin that may load mods for a specific game and environment
*
* @see FrogLoader
*/
public interface FrogPlugin {
/**
* General run method of this plugin. This method is run after all plugins have been initialized.
*
* @see FrogPlugin#init(FrogLoader)
* @see FrogPlugin#getMods()
*/
default void run() {
}
/**
* Check whether this plugin is applicable for the current environment
*
* @return Whether this plugin is applicable to be loaded in the current environment
*/
default boolean isApplicable() {
return false;
}
/**
* Initialization method for this plugin. This method will be called after <code>isApplicable()</code>
* if it returns true to initialize is plugin.
*
* @param loader the loader loading this plugin
* @throws Exception This method may throw any exception, it will be handled by the loader.
* @see FrogPlugin#isApplicable()
* @see FrogPlugin#getMods()
*/
default void init(FrogLoader loader) throws Exception {
}
/**
* This method should return all mods loaded by this plugin. It will be queried after <code>init(FrogLoader)</code>
* and before <code>run()</code>.
*
* @return A collection of mods loaded by this plugin.
* @see FrogPlugin#init(FrogLoader)
* @see FrogPlugin#run()
*/
default Collection<ModProperties> getMods() {
return Collections.emptySet();
}
}

View file

@ -0,0 +1,52 @@
package dev.frogmc.frogloader.api.plugin;
import dev.frogmc.frogloader.api.FrogLoader;
import dev.frogmc.frogloader.api.mod.ModProperties;
import org.jetbrains.annotations.NotNull;
/**
* A plugin responsible for loading a game.
* Only one of these may be loaded at a time.
* @see FrogLoader
* @see ModProvider
*/
public interface GamePlugin {
/**
* Method to launch the game.
* This method will be called after mods are located and loaded.
*/
default void run() {
}
/**
* Whether this plugin is applicable to be loaded in the current environment.
* Only one plugin may be applicable at a time.
* @return Whether this plugin is applicable to be loaded
*/
default boolean isApplicable() {
return false;
}
/**
* Initialize this plugin.
* @param loader The loader currently loading this plugin
* @throws Exception If an exception occurs during initialization. It will be handled by the loader.
*/
default void init(FrogLoader loader) throws Exception {
}
/**
* Queries the version of the game of this plugin.
* @return The version of the game loaded by this plugin
*/
@NotNull
String queryVersion();
/**
* Get the mod embodying the game loaded by this plugin.
* @return A mod for the currently loaded game. May be null if no such mod shall be added.
*/
default ModProperties getGameMod() {
return null;
}
}

View file

@ -0,0 +1,85 @@
package dev.frogmc.frogloader.api.plugin;
import java.net.URL;
import java.nio.file.Path;
import java.util.Collection;
import java.util.Collections;
import java.util.function.Consumer;
import dev.frogmc.frogloader.api.mod.ModProperties;
import dev.frogmc.frogloader.api.FrogLoader;
/**
* A provider for mods to be loaded.
* @see FrogLoader
* @see GamePlugin
* @see ModProperties
*/
public interface ModProvider {
/**
* Get the ID of this provider. Mods will be stored separated by their provider ID.
* @return The ID of this provider
*/
String id();
Ecorous marked this conversation as resolved
Review

Should/could this be changed to a ResourceLocation?

Should/could this be changed to a ResourceLocation?
Review

No, thought about it but ResourceLocation is a Minecraft class and therefore may not be available.

No, thought about it but ResourceLocation is a Minecraft class and therefore may not be available.
Review

Should we make a similar wrapper class? It seems strange not to enforce the used format

Should we make a similar wrapper class? It seems strange not to enforce the used format
Review

I don't think so, it doesn't really matter. The only important thing is that the content is unique among all mod providers due to the constraints of java's HashMap. And as we aren't really doing anything with these IDs except for supplying each provider with its mods later on I don't think it's worth requiring any specific format.

I don't think so, it doesn't really matter. The only important thing is that the content is unique among all mod providers due to the constraints of java's HashMap. And as we aren't really doing anything with these IDs except for supplying each provider with its mods later on I don't think it's worth requiring any specific format.
Review

It's something to consider if we ever want to parse it

It's something to consider if we ever want to parse it
/**
* The directory to search in for mods. This will be resolved relative to the game directory.
* @return The name of the mods directory, relative to the game directory
*/
default String loadDirectory() {
return "mods";
}
/**
* Whether this provider is applicable to load in the current environment.
* @return Whether this provider is applicable to load
*/
default boolean isApplicable() {
return false;
}
/**
* Whether a mod file is valid to be loaded by this provider.
* @param path The path to validate
* @return Whether this path is valid to be loaded by this provider
* @see #isDirectoryApplicable(Path)
* @see #loadDirectory()
*/
default boolean isFileApplicable(Path path) {
return false;
}
/**
* Whether a directory (inside the path declared by this provider's mod directory) is valid to be queried for mods
* to be loaded by this provider.
* @param path The directory to be validated
* @return Whether this directory should be traversed further
* @see #loadDirectory()
* @see #isFileApplicable(Path)
*/
default boolean isDirectoryApplicable(Path path) {
return false;
}
/**
* 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
*/
default void preLaunch(Collection<ModProperties> mods) {
}
/**
* This method should load the mods found previously into their runtime representation.
* This method should also load any nested mods
* <p>Note: This method also needs to add mods to the classpath for them to be loaded correctly. For this, a parameter is provided.</p>
*
* @param modFiles The paths to be loaded by this provider
* @param classpathAdder A consumer to easily add URLs to the classpath used to run the game on
* @return Mods loaded by this provider
* @throws Exception If an exception occurs during loading. It will be handled by the loader.
*/
default Collection<ModProperties> loadMods(Collection<Path> modFiles, Consumer<URL> classpathAdder) throws Exception {
return Collections.emptySet();
}
}

View file

@ -14,8 +14,8 @@ import com.google.gson.Gson;
import dev.frogmc.frogloader.api.FrogLoader; import dev.frogmc.frogloader.api.FrogLoader;
import dev.frogmc.frogloader.api.env.Env; import dev.frogmc.frogloader.api.env.Env;
import dev.frogmc.frogloader.api.mod.ModProperties; import dev.frogmc.frogloader.api.mod.ModProperties;
import dev.frogmc.frogloader.api.plugin.FrogGamePlugin; import dev.frogmc.frogloader.api.plugin.GamePlugin;
import dev.frogmc.frogloader.api.plugin.FrogModProvider; 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.launch.MixinClassLoader; import dev.frogmc.frogloader.impl.launch.MixinClassLoader;
import dev.frogmc.frogloader.impl.mod.ModUtil; import dev.frogmc.frogloader.impl.mod.ModUtil;
@ -37,7 +37,7 @@ public class FrogLoaderImpl implements FrogLoader {
@Getter @Getter
private final Env env; private final Env env;
@Getter @Getter
private final Collection<FrogModProvider> modProviders = new ArrayList<>(); private final Collection<ModProvider> modProviders = new ArrayList<>();
@Getter @Getter
private final Path gameDir, configDir; private final Path gameDir, configDir;
@Getter @Getter
@ -48,7 +48,7 @@ public class FrogLoaderImpl implements FrogLoader {
private final Gson gson = new Gson(); private final Gson gson = new Gson();
private final Map<String, Map<String, ModProperties>> mods = new HashMap<>(); private final Map<String, Map<String, ModProperties>> mods = new HashMap<>();
@Getter @Getter
private FrogGamePlugin gamePlugin; private GamePlugin gamePlugin;
@Getter @Getter
private String gameVersion; private String gameVersion;
private Collection<String> modIds = new ArrayList<>(); private Collection<String> modIds = new ArrayList<>();
@ -73,7 +73,7 @@ public class FrogLoaderImpl implements FrogLoader {
try { try {
loadGamePlugin(); loadGamePlugin();
loadModProviders(); loadModProviders();
modProviders.stream().map(FrogModProvider::loadDirectory).map(gameDir::resolve).forEach(modsDirs::add); modProviders.stream().map(ModProvider::loadDirectory).map(gameDir::resolve).forEach(modsDirs::add);
advanceMixinState(); advanceMixinState();
modIds = collectModIds(); modIds = collectModIds();
LOGGER.info(ModUtil.getModList(getMods())); LOGGER.info(ModUtil.getModList(getMods()));
@ -105,7 +105,7 @@ public class FrogLoaderImpl implements FrogLoader {
} }
private void loadGamePlugin() { private void loadGamePlugin() {
FrogGamePlugin plugin = PluginLoader.discoverGamePlugins(); GamePlugin plugin = PluginLoader.discoverGamePlugins();
gameVersion = plugin.queryVersion(); gameVersion = plugin.queryVersion();
ModProperties gameMod = plugin.getGameMod(); ModProperties gameMod = plugin.getGameMod();
if (gameMod != null) { if (gameMod != null) {

View file

@ -9,8 +9,8 @@ import java.util.stream.Collectors;
import dev.frogmc.frogloader.api.FrogLoader; import dev.frogmc.frogloader.api.FrogLoader;
import dev.frogmc.frogloader.api.mod.ModProperties; import dev.frogmc.frogloader.api.mod.ModProperties;
import dev.frogmc.frogloader.api.plugin.FrogGamePlugin; import dev.frogmc.frogloader.api.plugin.GamePlugin;
import dev.frogmc.frogloader.api.plugin.FrogModProvider; 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.BuiltinExtensions;
@ -23,10 +23,10 @@ import org.spongepowered.asm.mixin.Mixins;
public class PluginLoader { public class PluginLoader {
private static final Logger LOGGER = LoggerFactory.getLogger("FrogLoader/Plugins"); private static final Logger LOGGER = LoggerFactory.getLogger("FrogLoader/Plugins");
public static void discoverModProviders(Path gameDir, Map<String, Map<String, ModProperties>> mods, Collection<String> modIds, Collection<FrogModProvider> modProviders) throws ModDependencyResolver.ResolverException { public static void discoverModProviders(Path gameDir, Map<String, Map<String, ModProperties>> mods, Collection<String> modIds, Collection<ModProvider> modProviders) throws ModDependencyResolver.ResolverException {
LOGGER.info("Discovering mod providers..."); LOGGER.info("Discovering mod providers...");
Ecorous marked this conversation as resolved Outdated
Outdated
Review

wouldn't int[] size = new int[] { providers.size() } be better as there's no need for thread safety?

wouldn't `int[] size = new int[] { providers.size() }` be better as there's no need for thread safety?

I believe this works better - but I wouldn't know as I didn't write it

I believe this works better - but I wouldn't know as I didn't write it

The only difference here would be a insignificantly smaller memory footprint - this isn't an issue

The only difference here would be a insignificantly smaller memory footprint - this isn't an issue
Outdated
Review

It's not about performance, i think it's bad practice to use AtomicInteger when you don't need it

It's not about performance, i think it's bad practice to use AtomicInteger when you don't need it

Resolved in 39ba054fcb

Resolved in 39ba054fcbb139134081d9bf776e035c4d893182
for (FrogModProvider plugin : ServiceLoader.load(FrogModProvider.class)) { for (ModProvider plugin : ServiceLoader.load(ModProvider.class)) {
LOGGER.debug("Found mod provider: {}", plugin.getClass().getName()); LOGGER.debug("Found mod provider: {}", plugin.getClass().getName());
if (!plugin.isApplicable()) { if (!plugin.isApplicable()) {
continue; continue;
@ -34,7 +34,7 @@ public class PluginLoader {
try { try {
LOGGER.debug("Initialising mod provider: {}", plugin.id()); LOGGER.debug("Initialising mod provider: {}", plugin.id());
Map<String, ModProperties> modsFromProvider = new HashMap<>(); Map<String, ModProperties> modsFromProvider = new HashMap<>();
Collection<ModProperties> loadedMods = plugin.loadMods(Discovery.find(gameDir.resolve(plugin.loadDirectory()), plugin::isDirectoryApplicable, plugin::isFileApplicable)); Collection<ModProperties> loadedMods = plugin.loadMods(Discovery.find(gameDir.resolve(plugin.loadDirectory()), plugin::isDirectoryApplicable, plugin::isFileApplicable), FrogLoaderImpl.getInstance().getClassloader()::addURL);
initializeModMixins(loadedMods); initializeModMixins(loadedMods);
AWProcessor.load(loadedMods); AWProcessor.load(loadedMods);
@ -72,18 +72,18 @@ public class PluginLoader {
}); });
} }
public static FrogGamePlugin discoverGamePlugins() { public static GamePlugin discoverGamePlugins() {
LOGGER.info("Discovering game plugins..."); LOGGER.info("Discovering game plugins...");
ServiceLoader<FrogGamePlugin> loader = ServiceLoader.load(FrogGamePlugin.class); ServiceLoader<GamePlugin> loader = ServiceLoader.load(GamePlugin.class);
loader.stream().map(ServiceLoader.Provider::get).forEach(p -> LOGGER.info("Found game plugin: {}", p.getClass().getName())); loader.stream().map(ServiceLoader.Provider::get).forEach(p -> LOGGER.info("Found game plugin: {}", p.getClass().getName()));
FrogGamePlugin[] applicablePlugins = ServiceLoader.load(FrogGamePlugin.class).stream().map(ServiceLoader.Provider::get).filter(FrogGamePlugin::isApplicable).toArray(FrogGamePlugin[]::new); GamePlugin[] applicablePlugins = ServiceLoader.load(GamePlugin.class).stream().map(ServiceLoader.Provider::get).filter(GamePlugin::isApplicable).toArray(GamePlugin[]::new);
if (applicablePlugins.length > 1) { if (applicablePlugins.length > 1) {
throw new IllegalStateException("Multiple applicable game plugins found!"); throw new IllegalStateException("Multiple applicable game plugins found!");
} else if (applicablePlugins.length == 0) { } else if (applicablePlugins.length == 0) {
throw new IllegalStateException("No applicable game plugin found!"); throw new IllegalStateException("No applicable game plugin found!");
} }
FrogGamePlugin plugin = applicablePlugins[0]; // we can skip the loop as we always will only have one element GamePlugin plugin = applicablePlugins[0]; // we can skip the loop as we always will only have one element
try { try {
plugin.init(FrogLoader.getInstance()); plugin.init(FrogLoader.getInstance());
return plugin; return plugin;

View file

@ -14,15 +14,16 @@ import dev.frogmc.frogloader.api.FrogLoader;
import dev.frogmc.frogloader.api.mod.ModDependencies; import dev.frogmc.frogloader.api.mod.ModDependencies;
import dev.frogmc.frogloader.api.mod.ModExtensions; 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.FrogGamePlugin; 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.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.Thyroxine; import dev.frogmc.thyroxine.Thyroxine;
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
public class MinecraftGamePlugin implements FrogGamePlugin { public class MinecraftGamePlugin implements GamePlugin {
protected final String[] MINECRAFT_CLASSES = new String[]{ protected final String[] MINECRAFT_CLASSES = new String[]{
"net/minecraft/client/main/Main.class", "net/minecraft/client/main/Main.class",
@ -126,7 +127,7 @@ public class MinecraftGamePlugin implements FrogGamePlugin {
} }
@Override @Override
public String queryVersion() { public @NotNull String queryVersion() {
return version; return version;
} }

View file

@ -2,23 +2,24 @@ package dev.frogmc.frogloader.impl.plugin.mod;
import java.io.IOException; import java.io.IOException;
import java.net.MalformedURLException; import java.net.MalformedURLException;
import java.net.URL;
import java.nio.file.*; import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes; import java.nio.file.attribute.BasicFileAttributes;
import java.util.*; import java.util.*;
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.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.FrogModProvider; import dev.frogmc.frogloader.api.plugin.ModProvider;
import dev.frogmc.frogloader.impl.FrogLoaderImpl;
import dev.frogmc.frogloader.impl.mod.BuiltinExtensions; import dev.frogmc.frogloader.impl.mod.BuiltinExtensions;
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;
@SuppressWarnings("unused") @SuppressWarnings("unused")
public class FrogmodModProvider implements FrogModProvider { public class FrogModProvider implements ModProvider {
public static final String MOD_FILE_EXTENSION = ".frogmod"; public static final String MOD_FILE_EXTENSION = ".frogmod";
@ -53,7 +54,7 @@ public class FrogmodModProvider implements FrogModProvider {
} }
@Override @Override
public Collection<ModProperties> loadMods(Collection<Path> paths) throws Exception { public Collection<ModProperties> loadMods(Collection<Path> paths, Consumer<URL> classpathAdder) throws Exception {
Path jijCache = getJijCacheDir(); Path jijCache = getJijCacheDir();
Map<Path, ModProperties> mods = new HashMap<>(); Map<Path, ModProperties> mods = new HashMap<>();
@ -74,7 +75,7 @@ public class FrogmodModProvider implements FrogModProvider {
LOGGER.warn("Failed to resolve url for {}", path, e); LOGGER.warn("Failed to resolve url for {}", path, e);
return null; return null;
} }
}).filter(Objects::nonNull).forEach(FrogLoaderImpl.getInstance().getClassloader()::addURL); }).filter(Objects::nonNull).forEach(classpathAdder);
loadedMods.addAll(mods.values()); loadedMods.addAll(mods.values());

View file

@ -1,14 +1,16 @@
package dev.frogmc.frogloader.impl.plugin.mod; package dev.frogmc.frogloader.impl.plugin.mod;
import java.net.URL;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.function.Consumer;
import dev.frogmc.frogloader.api.mod.ModProperties; import dev.frogmc.frogloader.api.mod.ModProperties;
import dev.frogmc.frogloader.api.plugin.FrogModProvider; import dev.frogmc.frogloader.api.plugin.ModProvider;
import dev.frogmc.frogloader.impl.mod.JavaModProperties; import dev.frogmc.frogloader.impl.mod.JavaModProperties;
public class JavaModProvider implements FrogModProvider { public class JavaModProvider implements ModProvider {
@Override @Override
public String id() { public String id() {
return "frogloader:integrated/java"; return "frogloader:integrated/java";
@ -20,7 +22,7 @@ public class JavaModProvider implements FrogModProvider {
} }
@Override @Override
public Collection<ModProperties> loadMods(Collection<Path> modFiles) throws Exception { public Collection<ModProperties> loadMods(Collection<Path> modFiles, Consumer<URL> classpathAdder) throws Exception {
return Collections.singleton(JavaModProperties.get()); return Collections.singleton(JavaModProperties.get());
} }
} }

View file

@ -1,2 +0,0 @@
dev.frogmc.frogloader.impl.plugin.mod.JavaModProvider
dev.frogmc.frogloader.impl.plugin.mod.FrogmodModProvider

View file

@ -0,0 +1,2 @@
dev.frogmc.frogloader.impl.plugin.mod.JavaModProvider
dev.frogmc.frogloader.impl.plugin.mod.FrogModProvider