Separate plugin types #10
|
@ -23,4 +23,4 @@ jobs:
|
||||||
./gradlew :publishMavenJavaPublicationToFrogMCSnapshotsMavenRepository \
|
./gradlew :publishMavenJavaPublicationToFrogMCSnapshotsMavenRepository \
|
||||||
-PFrogMCSnapshotsMavenUsername=${{ secrets.MAVEN_PUSH_USER }} \
|
-PFrogMCSnapshotsMavenUsername=${{ secrets.MAVEN_PUSH_USER }} \
|
||||||
-PFrogMCSnapshotsMavenPassword=${{ secrets.MAVEN_PUSH_TOKEN }} --stacktrace
|
-PFrogMCSnapshotsMavenPassword=${{ secrets.MAVEN_PUSH_TOKEN }} --stacktrace
|
||||||
./gradlew :updateMeta
|
./gradlew :updateMeta -PFrogMCMetaKey=${{ secrets.META_SECRET }} --stacktrace
|
||||||
|
|
|
@ -7,7 +7,7 @@ plugins {
|
||||||
}
|
}
|
||||||
|
|
||||||
group = "dev.frogmc"
|
group = "dev.frogmc"
|
||||||
version = "0.0.1-SNAPSHOT"
|
version = "0.0.1-alpha.1"
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
maven {
|
maven {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[versions]
|
[versions]
|
||||||
|
|
||||||
thyroxine = "0.0.1-alpha.2"
|
thyroxine = "0.0.1-alpha.3"
|
||||||
nightconfig = "3.7.2"
|
nightconfig = "3.7.2"
|
||||||
mixin = "0.14.0+mixin.0.8.6"
|
mixin = "0.14.0+mixin.0.8.6"
|
||||||
annotations = "24.1.0"
|
annotations = "24.1.0"
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
plugins {
|
plugins {
|
||||||
java
|
java
|
||||||
id("dev.frogmc.phytotelma") version "0.0.1-alpha.9"
|
id("dev.frogmc.phytotelma") version "0.0.1-alpha.10"
|
||||||
}
|
}
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
|
@ -20,7 +20,7 @@ dependencies {
|
||||||
}
|
}
|
||||||
|
|
||||||
phytotelma {
|
phytotelma {
|
||||||
minecraft("1.20.6")
|
minecraft("1.21", "1.20.6")
|
||||||
}
|
}
|
||||||
|
|
||||||
java {
|
java {
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
package dev.frogmc.frogloader.example;
|
||||||
|
|
||||||
|
import dev.frogmc.frogloader.api.plugin.ModProvider;
|
||||||
|
|
||||||
|
public class ExampleModProvider implements ModProvider {
|
||||||
Ecorous marked this conversation as resolved
|
|||||||
|
@Override
|
||||||
|
public String id() {
|
||||||
|
return "example_mod:example";
|
||||||
|
}
|
||||||
|
}
|
|
@ -14,4 +14,4 @@ credits = [
|
||||||
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"
|
||||||
|
modprovider = "dev.frogmc.frogloader.example.ExampleModProvider"
|
||||||
Ecorous marked this conversation as resolved
Ecorous
commented
Should we allow more than one modprovider per frogmod? Should we allow more than one modprovider per frogmod?
owlsys
commented
Already possible Already possible
|
|||||||
|
|
|
@ -6,14 +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.FrogPlugin;
|
import dev.frogmc.frogloader.api.plugin.GamePlugin;
|
||||||
|
import dev.frogmc.frogloader.api.plugin.ModProvider;
|
||||||
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 {
|
||||||
|
@ -28,11 +29,18 @@ public interface FrogLoader {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all loaded plugins.
|
* Get the currently loaded game plugin.
|
||||||
*
|
*
|
||||||
* @return A collection of all loaded plugins
|
* @return The game plugin applicable to the current game
|
||||||
*/
|
*/
|
||||||
Collection<FrogPlugin> getPlugins();
|
GamePlugin getGamePlugin();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all loaded mod providers.
|
||||||
|
*
|
||||||
|
* @return A collection of all loaded mod providers
|
||||||
|
*/
|
||||||
|
Collection<ModProvider> getModProviders();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the current (physical) environment.
|
* Get the current (physical) environment.
|
||||||
|
@ -59,11 +67,11 @@ public interface FrogLoader {
|
||||||
Path getConfigDir();
|
Path getConfigDir();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the current mods directory.
|
* Get the current mods directories.
|
||||||
*
|
*
|
||||||
* @return The current mods directory
|
* @return The current mods directories
|
||||||
*/
|
*/
|
||||||
Path getModsDir();
|
Collection<Path> getModsDirs();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Query whether this loader is currently running in a development environment.
|
* Query whether this loader is currently running in a development environment.
|
||||||
|
@ -94,7 +102,14 @@ 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
|
||||||
|
*
|
||||||
|
* @return The current game version
|
||||||
|
*/
|
||||||
|
String getGameVersion();
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,8 +3,8 @@ package dev.frogmc.frogloader.api.mod;
|
||||||
import java.lang.invoke.MethodHandle;
|
import java.lang.invoke.MethodHandle;
|
||||||
import java.lang.invoke.MethodHandles;
|
import java.lang.invoke.MethodHandles;
|
||||||
import java.lang.invoke.MethodType;
|
import java.lang.invoke.MethodType;
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
|
@ -70,10 +70,13 @@ public final class ModExtensions {
|
||||||
* @param action the action to run on the value of the extension if it is present
|
* @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 <T> The type of the value of this extension
|
||||||
*/
|
*/
|
||||||
|
@SuppressWarnings({"rawtypes", "unchecked"})
|
||||||
public <T> void runIfPresent(String key, Consumer<T> action) {
|
public <T> void runIfPresent(String key, Consumer<T> action) {
|
||||||
T value = get(key);
|
Object value = get(key);
|
||||||
if (value != null) {
|
if (value instanceof Collection c){
|
||||||
action.accept(value);
|
((Collection<T>)c).forEach(action);
|
||||||
|
} else if (value != null) {
|
||||||
|
action.accept((T)value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -116,9 +119,9 @@ public final class ModExtensions {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (value instanceof String s){
|
if (value instanceof String s) {
|
||||||
c.accept(s);
|
c.accept(s);
|
||||||
} else if (value instanceof Collection l){
|
} else if (value instanceof Collection l) {
|
||||||
((Collection<String>) l).forEach(c);
|
((Collection<String>) l).forEach(c);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -79,6 +79,7 @@ public interface ModProperties {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get this mod's paths
|
* Get this mod's paths
|
||||||
|
*
|
||||||
* @return Where this mod is loaded from
|
* @return Where this mod is loaded from
|
||||||
*/
|
*/
|
||||||
Collection<Path> paths();
|
Collection<Path> paths();
|
||||||
|
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
Ecorous
commented
Should/could this be changed to a ResourceLocation? Should/could this be changed to a ResourceLocation?
owlsys
commented
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.
Ecorous
commented
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
owlsys
commented
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.
Ecorous
commented
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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -14,7 +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.FrogPlugin;
|
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.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;
|
||||||
|
@ -26,30 +27,31 @@ import org.slf4j.LoggerFactory;
|
||||||
import org.spongepowered.asm.mixin.MixinEnvironment;
|
import org.spongepowered.asm.mixin.MixinEnvironment;
|
||||||
|
|
||||||
public class FrogLoaderImpl implements FrogLoader {
|
public class FrogLoaderImpl implements FrogLoader {
|
||||||
public static final String MOD_FILE_EXTENSION = ".frogmod";
|
|
||||||
private static final boolean DEV_ENV = Boolean.getBoolean(SystemProperties.DEVELOPMENT);
|
private static final boolean DEV_ENV = Boolean.getBoolean(SystemProperties.DEVELOPMENT);
|
||||||
|
private static final Logger LOGGER = LoggerFactory.getLogger("FrogLoader");
|
||||||
@Getter
|
@Getter
|
||||||
private static FrogLoaderImpl instance;
|
private static FrogLoaderImpl instance;
|
||||||
@Getter
|
@Getter
|
||||||
private final String[] args;
|
private final String[] args;
|
||||||
@Getter
|
@Getter
|
||||||
private final Env env;
|
private final Env env;
|
||||||
private final Logger LOGGER = LoggerFactory.getLogger("FrogLoader");
|
|
||||||
|
|
||||||
@Getter
|
@Getter
|
||||||
private final List<FrogPlugin> plugins = new ArrayList<>();
|
private final Collection<ModProvider> modProviders = new ArrayList<>();
|
||||||
|
|
||||||
@Getter
|
@Getter
|
||||||
private final Path gameDir, configDir, modsDir;
|
private final Path gameDir, configDir;
|
||||||
|
@Getter
|
||||||
|
private final Collection<Path> modsDirs = new HashSet<>();
|
||||||
@Getter
|
@Getter
|
||||||
private final MixinClassLoader classloader;
|
private final MixinClassLoader classloader;
|
||||||
|
|
||||||
@Getter
|
@Getter
|
||||||
private final Gson gson = new Gson();
|
private final Gson gson = new Gson();
|
||||||
|
private final Map<String, Map<String, ModProperties>> mods = new HashMap<>();
|
||||||
private Map<String, ModProperties> mods;
|
@Getter
|
||||||
private Collection<String> modIds;
|
private GamePlugin gamePlugin;
|
||||||
|
@Getter
|
||||||
|
private String gameVersion;
|
||||||
|
private Collection<String> modIds = new ArrayList<>();
|
||||||
|
|
||||||
|
|
||||||
private FrogLoaderImpl(String[] args, Env env) {
|
private FrogLoaderImpl(String[] args, Env env) {
|
||||||
|
@ -60,26 +62,26 @@ public class FrogLoaderImpl implements FrogLoader {
|
||||||
|
|
||||||
gameDir = Paths.get(getArgumentOrElse("gameDir", "."));
|
gameDir = Paths.get(getArgumentOrElse("gameDir", "."));
|
||||||
configDir = gameDir.resolve("config");
|
configDir = gameDir.resolve("config");
|
||||||
modsDir = gameDir.resolve("mods");
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Files.createDirectories(gameDir);
|
Files.createDirectories(gameDir);
|
||||||
Files.createDirectories(configDir);
|
Files.createDirectories(configDir);
|
||||||
Files.createDirectories(modsDir);
|
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
LOGGER.warn("Failed to create essential directories ", e);
|
LOGGER.warn("Failed to create essential directories ", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
discoverPlugins();
|
loadGamePlugin();
|
||||||
|
loadModProviders();
|
||||||
|
modProviders.stream().map(ModProvider::loadDirectory).map(gameDir::resolve).forEach(modsDirs::add);
|
||||||
advanceMixinState();
|
advanceMixinState();
|
||||||
mods = collectMods();
|
|
||||||
modIds = collectModIds();
|
modIds = collectModIds();
|
||||||
LOGGER.info(ModUtil.getModList(mods.values()));
|
LOGGER.info(ModUtil.getModList(getMods()));
|
||||||
LOGGER.info("Launching...");
|
LOGGER.info("Launching...");
|
||||||
plugins.forEach(FrogPlugin::run);
|
modProviders.forEach(plugin -> plugin.preLaunch(mods.get(plugin.id()).values()));
|
||||||
|
gamePlugin.run();
|
||||||
} catch (Throwable t) {
|
} catch (Throwable t) {
|
||||||
LoaderGui.execReport(CrashReportGenerator.writeReport(t, collectMods().values()), false);
|
LoaderGui.execReport(CrashReportGenerator.writeReport(t, getMods()), false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -102,22 +104,19 @@ public class FrogLoaderImpl implements FrogLoader {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void discoverPlugins() {
|
private void loadGamePlugin() {
|
||||||
ServiceLoader.load(FrogPlugin.class).forEach(plugin -> {
|
GamePlugin plugin = PluginLoader.discoverGamePlugins();
|
||||||
try {
|
gameVersion = plugin.queryVersion();
|
||||||
if (plugin.isApplicable()) {
|
ModProperties gameMod = plugin.getGameMod();
|
||||||
plugin.init(this);
|
if (gameMod != null) {
|
||||||
plugins.add(plugin);
|
mods.put("integrated", Map.of(gameMod.id(), gameMod));
|
||||||
}
|
modIds.add(gameMod.id());
|
||||||
} catch (Throwable e) {
|
|
||||||
LOGGER.error("Error during plugin initialisation: ", e);
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (plugins.isEmpty()) {
|
|
||||||
throw new IllegalStateException("No plugin applicable to the current state was found!");
|
|
||||||
}
|
}
|
||||||
|
gamePlugin = plugin;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void loadModProviders() throws Exception {
|
||||||
|
PluginLoader.discoverModProviders(gameDir, mods, modIds, modProviders);
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getArgument(String name) {
|
public String getArgument(String name) {
|
||||||
|
@ -149,20 +148,15 @@ public class FrogLoaderImpl implements FrogLoader {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Optional<ModProperties> getModProperties(String id) {
|
public Optional<ModProperties> getModProperties(String id) {
|
||||||
return Optional.ofNullable(mods.get(id));
|
return mods.values().stream().flatMap(m -> m.values().stream()).filter(m -> m.id().equals(id)).findFirst();
|
||||||
}
|
|
||||||
|
|
||||||
private Map<String, ModProperties> collectMods() {
|
|
||||||
return plugins.stream().map(FrogPlugin::getMods).flatMap(Collection::stream)
|
|
||||||
.collect(Collectors.toMap(ModProperties::id, m -> m));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private Collection<String> collectModIds() {
|
private Collection<String> collectModIds() {
|
||||||
return mods.keySet();
|
return mods.values().stream().flatMap(m -> m.keySet().stream()).collect(Collectors.toSet());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Collection<ModProperties> getMods() {
|
public Collection<ModProperties> getMods() {
|
||||||
return mods.values();
|
return mods.values().stream().map(Map::values).flatMap(Collection::stream).collect(Collectors.toSet());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
104
src/main/java/dev/frogmc/frogloader/impl/PluginLoader.java
Normal file
|
@ -0,0 +1,104 @@
|
||||||
|
package dev.frogmc.frogloader.impl;
|
||||||
|
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
import dev.frogmc.frogloader.api.FrogLoader;
|
||||||
|
import dev.frogmc.frogloader.api.mod.ModProperties;
|
||||||
|
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;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.spongepowered.asm.mixin.Mixins;
|
||||||
|
|
||||||
|
public class PluginLoader {
|
||||||
|
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<ModProvider> modProviders) throws ModDependencyResolver.ResolverException {
|
||||||
|
LOGGER.info("Discovering mod providers...");
|
||||||
|
|
||||||
|
List<ModProvider> providers = new ArrayList<>(ServiceLoader.load(ModProvider.class).stream().map(ServiceLoader.Provider::get).toList()); // we need mutability & random access
|
||||||
|
Map<String, Collection<ModProperties>> properties = new HashMap<>();
|
||||||
|
int[] size = new int[providers.size()]; // use a separate size variable to not have to iterate over the list over and over again
|
||||||
|
for (int i = 0; i < size[0]; i++) {
|
||||||
|
ModProvider plugin = providers.get(i); // use random access to work around concurrent access (through modifications during iteration)
|
||||||
|
LOGGER.debug("Found mod provider: {}", plugin.getClass().getName());
|
||||||
|
if (!plugin.isApplicable()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
LOGGER.debug("Initialising mod provider: {}", plugin.id());
|
||||||
|
Collection<ModProperties> loadedMods = plugin.loadMods(Discovery.find(gameDir.resolve(plugin.loadDirectory()),
|
||||||
|
plugin::isDirectoryApplicable,
|
||||||
|
plugin::isFileApplicable),
|
||||||
|
FrogLoaderImpl.getInstance().getClassloader()::addURL);
|
||||||
|
initializeModMixins(loadedMods);
|
||||||
|
AWProcessor.load(loadedMods);
|
||||||
|
|
||||||
|
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]++;
|
||||||
|
}));
|
||||||
|
} catch (Throwable e) {
|
||||||
|
LOGGER.error("Error during plugin initialisation: ", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Collection<ModProperties> flattened = properties.values().stream().flatMap(Collection::stream).toList();
|
||||||
|
try {
|
||||||
|
Collection<ModProperties> solved = new ModDependencyResolver(flattened).solve();
|
||||||
|
properties.forEach((s, c) -> c.retainAll(solved));
|
||||||
|
properties.forEach((s, c) -> {
|
||||||
|
Map<String, ModProperties> map = mods.computeIfAbsent(s, u -> new HashMap<>());
|
||||||
|
c.forEach(p -> map.put(p.id(), p));
|
||||||
|
});
|
||||||
|
modIds.addAll(solved.stream().map(ModProperties::id).toList());
|
||||||
|
modProviders.addAll(providers);
|
||||||
|
} catch (ModDependencyResolver.UnfulfilledDependencyException e) {
|
||||||
|
LoaderGui.execUnfulfilledDep(CrashReportGenerator.writeReport(e, flattened), e, false);
|
||||||
|
} catch (ModDependencyResolver.BreakingModException e) {
|
||||||
|
LoaderGui.execBreakingDep(CrashReportGenerator.writeReport(e, flattened), e, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings({"rawtypes", "unchecked"})
|
||||||
|
private static void initializeModMixins(Collection<ModProperties> loadedMods) {
|
||||||
|
loadedMods.forEach(props -> {
|
||||||
|
Object o = props.extensions().get(BuiltinExtensions.MIXIN_CONFIG);
|
||||||
|
if (o instanceof String name) {
|
||||||
|
Mixins.addConfiguration(name);
|
||||||
|
} else if (o instanceof Collection l) {
|
||||||
|
((Collection<String>) l).forEach(Mixins::addConfiguration);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public static GamePlugin discoverGamePlugins() {
|
||||||
|
LOGGER.info("Discovering game plugins...");
|
||||||
|
GamePlugin[] applicablePlugins = ServiceLoader.load(GamePlugin.class).stream().map(ServiceLoader.Provider::get)
|
||||||
|
.peek(p -> LOGGER.debug("Found game plugin: {}", p.getClass().getName()))
|
||||||
|
.filter(GamePlugin::isApplicable).toArray(GamePlugin[]::new);
|
||||||
|
if (applicablePlugins.length > 1) {
|
||||||
|
throw new IllegalStateException("Multiple applicable game plugins found!");
|
||||||
|
} else if (applicablePlugins.length == 0) {
|
||||||
|
throw new IllegalStateException("No applicable game plugin found!");
|
||||||
|
}
|
||||||
|
|
||||||
|
GamePlugin plugin = applicablePlugins[0]; // we can skip the loop as we always will only have one element
|
||||||
|
try {
|
||||||
|
plugin.init(FrogLoader.getInstance());
|
||||||
|
return plugin;
|
||||||
|
} catch (Throwable e) {
|
||||||
|
LOGGER.error("Error during plugin initialisation: ", e);
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -76,7 +76,7 @@ public class LoaderGui extends JFrame {
|
||||||
public static void execUnfulfilledDep(Path reportPath, ModDependencyResolver.UnfulfilledDependencyException ex, boolean keepRunning) {
|
public static void execUnfulfilledDep(Path reportPath, ModDependencyResolver.UnfulfilledDependencyException ex, boolean keepRunning) {
|
||||||
exec(gui -> {
|
exec(gui -> {
|
||||||
int count = ex.getDependencies().size();
|
int count = ex.getDependencies().size();
|
||||||
gui.setHeader("Found " + count + " problem"+(count > 1 ? "s" : ""));
|
gui.setHeader("Found " + count + " problem" + (count > 1 ? "s" : ""));
|
||||||
gui.addTab("Info", new UnfulfilledDepPage(ex));
|
gui.addTab("Info", new UnfulfilledDepPage(ex));
|
||||||
addReport(gui, reportPath);
|
addReport(gui, reportPath);
|
||||||
}, keepRunning);
|
}, keepRunning);
|
||||||
|
@ -85,7 +85,7 @@ public class LoaderGui extends JFrame {
|
||||||
public static void execBreakingDep(Path reportPath, ModDependencyResolver.BreakingModException ex, boolean keepRunning) {
|
public static void execBreakingDep(Path reportPath, ModDependencyResolver.BreakingModException ex, boolean keepRunning) {
|
||||||
exec(gui -> {
|
exec(gui -> {
|
||||||
int count = ex.getBreaks().size();
|
int count = ex.getBreaks().size();
|
||||||
gui.setHeader("Found " + count + " problem"+(count > 1 ? "s" : ""));
|
gui.setHeader("Found " + count + " problem" + (count > 1 ? "s" : ""));
|
||||||
gui.addTab("Info", new BreakingDepPage(ex));
|
gui.addTab("Info", new BreakingDepPage(ex));
|
||||||
addReport(gui, reportPath);
|
addReport(gui, reportPath);
|
||||||
}, keepRunning);
|
}, keepRunning);
|
||||||
|
|
|
@ -1,50 +1,64 @@
|
||||||
package dev.frogmc.frogloader.impl.gui.component;
|
package dev.frogmc.frogloader.impl.gui.component;
|
||||||
|
|
||||||
import dev.frogmc.frogloader.impl.mod.ModDependencyResolver;
|
import javax.imageio.ImageIO;
|
||||||
import org.jetbrains.annotations.Nullable;
|
|
||||||
|
|
||||||
import javax.swing.*;
|
import javax.swing.*;
|
||||||
import javax.swing.plaf.basic.BasicBorders;
|
import javax.swing.plaf.basic.BasicBorders;
|
||||||
import java.awt.*;
|
import java.awt.*;
|
||||||
import java.awt.event.ActionListener;
|
import java.awt.event.ActionListener;
|
||||||
|
import java.io.IOException;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
|
|
||||||
|
import dev.frogmc.frogloader.impl.mod.ModDependencyResolver;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
public class DependencyErrorEntry extends JPanel {
|
public class DependencyErrorEntry extends JPanel {
|
||||||
|
|
||||||
private final JPanel actions;
|
private final JPanel actions;
|
||||||
|
|
||||||
public DependencyErrorEntry(String description, ModDependencyResolver.VersionRange range, Color background, @Nullable String icon) {
|
public DependencyErrorEntry(String description, ModDependencyResolver.VersionRange range, Color background, @Nullable String icon) {
|
||||||
super(new BorderLayout());
|
super(new BorderLayout());
|
||||||
|
|
||||||
setBorder(BasicBorders.getInternalFrameBorder());
|
setBorder(BasicBorders.getInternalFrameBorder());
|
||||||
|
|
||||||
Box text = Box.createVerticalBox();
|
Box text = Box.createVerticalBox();
|
||||||
text.setBorder(BorderFactory.createEmptyBorder());
|
text.setBorder(BorderFactory.createEmptyBorder());
|
||||||
|
|
||||||
JTextPane desc = new JTextPane();
|
JTextPane desc = new JTextPane();
|
||||||
desc.setContentType("text/html");
|
desc.setContentType("text/html");
|
||||||
desc.setEditable(false);
|
desc.setEditable(false);
|
||||||
desc.setBackground(background);
|
desc.setBackground(background);
|
||||||
desc.setText("<html>" + description.replace("<", "<").replace("\n", "<br>") + "</html>");
|
desc.setText("<html>" + description.replace("<", "<").replace("\n", "<br>") + "</html>");
|
||||||
text.add(desc);
|
text.add(desc);
|
||||||
|
|
||||||
add(text, BorderLayout.NORTH);
|
Box top = Box.createHorizontalBox();
|
||||||
|
add(top, BorderLayout.NORTH);
|
||||||
|
|
||||||
this.actions = new JPanel(new FlowLayout(FlowLayout.LEFT));
|
if (icon != null) {
|
||||||
add(this.actions, BorderLayout.SOUTH);
|
URL location = getClass().getResource("/"+icon);
|
||||||
|
|
||||||
if (icon != null) {
|
if (location != null) {
|
||||||
URL location = getClass().getResource(icon);
|
try {
|
||||||
|
int size = 100;
|
||||||
|
Image image = ImageIO.read(location).getScaledInstance(size, size, Image.SCALE_SMOOTH);
|
||||||
|
Box container = Box.createVerticalBox();
|
||||||
|
container.add(new JLabel(new ImageIcon(image)));
|
||||||
|
container.add(Box.createVerticalGlue());
|
||||||
|
top.add(container);
|
||||||
|
} catch (IOException ignored) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (location != null)
|
top.add(text);
|
||||||
add(new JLabel(new ImageIcon(location)), BorderLayout.WEST);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void addAction(String label, ActionListener listener) {
|
this.actions = new JPanel(new FlowLayout(FlowLayout.LEFT));
|
||||||
var button = new JButton(label);
|
add(this.actions, BorderLayout.SOUTH);
|
||||||
button.addActionListener(listener);
|
}
|
||||||
this.actions.add(button);
|
|
||||||
}
|
public void addAction(String label, ActionListener listener) {
|
||||||
|
var button = new JButton(label);
|
||||||
|
button.addActionListener(listener);
|
||||||
|
this.actions.add(button);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,54 +1,54 @@
|
||||||
package dev.frogmc.frogloader.impl.gui.page;
|
package dev.frogmc.frogloader.impl.gui.page;
|
||||||
|
|
||||||
import dev.frogmc.frogloader.impl.gui.component.DependencyErrorEntry;
|
|
||||||
import dev.frogmc.frogloader.impl.mod.ModDependencyResolver;
|
|
||||||
|
|
||||||
import javax.swing.*;
|
import javax.swing.*;
|
||||||
import java.awt.*;
|
import java.awt.*;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
||||||
|
import dev.frogmc.frogloader.impl.gui.component.DependencyErrorEntry;
|
||||||
|
import dev.frogmc.frogloader.impl.mod.ModDependencyResolver;
|
||||||
|
|
||||||
public class BreakingDepPage extends JScrollPane {
|
public class BreakingDepPage extends JScrollPane {
|
||||||
|
|
||||||
public BreakingDepPage(ModDependencyResolver.BreakingModException ex) {
|
public BreakingDepPage(ModDependencyResolver.BreakingModException ex) {
|
||||||
getHorizontalScrollBar().setUnitIncrement(16);
|
getHorizontalScrollBar().setUnitIncrement(16);
|
||||||
getVerticalScrollBar().setUnitIncrement(16);
|
getVerticalScrollBar().setUnitIncrement(16);
|
||||||
|
|
||||||
Box list = Box.createVerticalBox();
|
Box list = Box.createVerticalBox();
|
||||||
|
|
||||||
ex.getBreaks().forEach(entry -> {
|
ex.getBreaks().forEach(entry -> {
|
||||||
String description =
|
String description =
|
||||||
"""
|
"""
|
||||||
Mod %s (%s) breaks with mod %s (%s) for versions matching range: %s (present: %s)
|
Mod %s (%s) breaks with mod %s (%s) for versions matching range: %s (present: %s)
|
||||||
Suggested Solution: Install %s of Mod %s (%s)
|
Suggested Solution: Install %s of Mod %s (%s)
|
||||||
""";
|
""";
|
||||||
|
|
||||||
description = description.formatted(
|
description = description.formatted(
|
||||||
entry.source().id(),
|
entry.source().id(),
|
||||||
entry.source().name(),
|
entry.source().name(),
|
||||||
entry.broken().id(),
|
entry.broken().id(),
|
||||||
entry.broken().name(),
|
entry.broken().name(),
|
||||||
entry.range().toString(" or "),
|
entry.range().toString(" or "),
|
||||||
entry.broken().version(),
|
entry.broken().version(),
|
||||||
entry.range()
|
entry.range()
|
||||||
.maxCompatible()
|
.maxCompatible()
|
||||||
.or(entry.range()::minCompatible)
|
.or(entry.range()::minCompatible)
|
||||||
.map(Objects::toString)
|
.map(Objects::toString)
|
||||||
.map(s -> "0.0.0".equals(s) ? "any version" : "version " + s)
|
.map(s -> "0.0.0".equals(s) ? "any version" : "version " + s)
|
||||||
.orElse("<unknown>"),
|
.orElse("<unknown>"),
|
||||||
entry.broken().id(),
|
entry.broken().id(),
|
||||||
entry.broken().name()
|
entry.broken().name()
|
||||||
);
|
);
|
||||||
|
|
||||||
DependencyErrorEntry result = new DependencyErrorEntry(
|
DependencyErrorEntry result = new DependencyErrorEntry(
|
||||||
description,
|
description,
|
||||||
entry.range(),
|
entry.range(),
|
||||||
list.getBackground(),
|
list.getBackground(),
|
||||||
entry.source().icon()
|
entry.source().icon()
|
||||||
);
|
);
|
||||||
list.add(result);
|
list.add(result);
|
||||||
});
|
});
|
||||||
setViewportView(list);
|
setViewportView(list);
|
||||||
SwingUtilities.invokeLater(() -> getViewport().setViewPosition(new Point()));
|
SwingUtilities.invokeLater(() -> getViewport().setViewPosition(new Point()));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,24 +11,24 @@ import java.nio.file.Path;
|
||||||
|
|
||||||
public class ReportPage extends JScrollPane {
|
public class ReportPage extends JScrollPane {
|
||||||
|
|
||||||
public ReportPage(Path reportPath) {
|
public ReportPage(Path reportPath) {
|
||||||
getHorizontalScrollBar().setUnitIncrement(16);
|
getHorizontalScrollBar().setUnitIncrement(16);
|
||||||
getVerticalScrollBar().setUnitIncrement(16);
|
getVerticalScrollBar().setUnitIncrement(16);
|
||||||
|
|
||||||
JTextArea text = new JTextArea();
|
JTextArea text = new JTextArea();
|
||||||
text.setEditable(false);
|
text.setEditable(false);
|
||||||
text.setTabSize(2);
|
text.setTabSize(2);
|
||||||
try {
|
try {
|
||||||
text.setText(Files.readString(reportPath, StandardCharsets.UTF_8));
|
text.setText(Files.readString(reportPath, StandardCharsets.UTF_8));
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
StringWriter writer = new StringWriter();
|
StringWriter writer = new StringWriter();
|
||||||
PrintWriter printer = new PrintWriter(writer);
|
PrintWriter printer = new PrintWriter(writer);
|
||||||
printer.printf("Could not load contents of %s:%n", reportPath);
|
printer.printf("Could not load contents of %s:%n", reportPath);
|
||||||
e.printStackTrace(printer);
|
e.printStackTrace(printer);
|
||||||
}
|
}
|
||||||
text.setBorder(BorderFactory.createEmptyBorder(0, 8, 0, 8));
|
text.setBorder(BorderFactory.createEmptyBorder(0, 8, 0, 8));
|
||||||
setViewportView(text);
|
setViewportView(text);
|
||||||
SwingUtilities.invokeLater(() -> getViewport().setViewPosition(new Point(0, 0)));
|
SwingUtilities.invokeLater(() -> getViewport().setViewPosition(new Point(0, 0)));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,15 +1,15 @@
|
||||||
package dev.frogmc.frogloader.impl.gui.page;
|
package dev.frogmc.frogloader.impl.gui.page;
|
||||||
|
|
||||||
import dev.frogmc.frogloader.impl.gui.component.DependencyErrorEntry;
|
|
||||||
import dev.frogmc.frogloader.impl.mod.ModDependencyResolver;
|
|
||||||
import dev.frogmc.frogloader.impl.util.PlatformUtil;
|
|
||||||
|
|
||||||
import javax.swing.*;
|
import javax.swing.*;
|
||||||
import java.awt.*;
|
import java.awt.*;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.net.URISyntaxException;
|
import java.net.URISyntaxException;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
||||||
|
import dev.frogmc.frogloader.impl.gui.component.DependencyErrorEntry;
|
||||||
|
import dev.frogmc.frogloader.impl.mod.ModDependencyResolver;
|
||||||
|
import dev.frogmc.frogloader.impl.util.PlatformUtil;
|
||||||
|
|
||||||
public class UnfulfilledDepPage extends JScrollPane {
|
public class UnfulfilledDepPage extends JScrollPane {
|
||||||
|
|
||||||
public UnfulfilledDepPage(ModDependencyResolver.UnfulfilledDependencyException ex) {
|
public UnfulfilledDepPage(ModDependencyResolver.UnfulfilledDependencyException ex) {
|
||||||
|
@ -39,13 +39,13 @@ public class UnfulfilledDepPage extends JScrollPane {
|
||||||
}
|
}
|
||||||
description.append("\nSuggested Solution: Install ")
|
description.append("\nSuggested Solution: Install ")
|
||||||
.append(
|
.append(
|
||||||
entry
|
entry
|
||||||
.range()
|
.range()
|
||||||
.maxCompatible()
|
.maxCompatible()
|
||||||
.or(entry.range()::minCompatible)
|
.or(entry.range()::minCompatible)
|
||||||
.map(Objects::toString)
|
.map(Objects::toString)
|
||||||
.map(s -> "0.0.0".equals(s) ? "any version" : "version " + s)
|
.map(s -> "0.0.0".equals(s) ? "any version" : "version " + s)
|
||||||
.orElse("<unknown>")
|
.orElse("<unknown>")
|
||||||
)
|
)
|
||||||
.append(" of ");
|
.append(" of ");
|
||||||
if (entry.dependencyName() != null) {
|
if (entry.dependencyName() != null) {
|
||||||
|
@ -55,10 +55,10 @@ public class UnfulfilledDepPage extends JScrollPane {
|
||||||
}
|
}
|
||||||
|
|
||||||
DependencyErrorEntry result = new DependencyErrorEntry(
|
DependencyErrorEntry result = new DependencyErrorEntry(
|
||||||
description.toString(),
|
description.toString(),
|
||||||
entry.range(),
|
entry.range(),
|
||||||
list.getBackground(),
|
list.getBackground(),
|
||||||
entry.source().icon()
|
entry.source().icon()
|
||||||
);
|
);
|
||||||
|
|
||||||
if (entry.link() != null) {
|
if (entry.link() != null) {
|
||||||
|
|
|
@ -58,15 +58,15 @@ public class FrogLauncher {
|
||||||
new FrogLauncher(args, env);
|
new FrogLauncher(args, env);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void putProperty(IPropertyKey key, Object value){
|
public void putProperty(IPropertyKey key, Object value) {
|
||||||
globalProperties.put(key, value);
|
globalProperties.put(key, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Object getProperty(IPropertyKey key){
|
public Object getProperty(IPropertyKey key) {
|
||||||
return globalProperties.get(key);
|
return globalProperties.get(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Object getProperty(IPropertyKey key, Object defaultValue){
|
public Object getProperty(IPropertyKey key, Object defaultValue) {
|
||||||
return globalProperties.getOrDefault(key, defaultValue);
|
return globalProperties.getOrDefault(key, defaultValue);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,15 +14,15 @@ public class FrogGlobalPropertyService implements IGlobalPropertyService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean equals(Object other){
|
public boolean equals(Object other) {
|
||||||
if (other instanceof IPropertyKey k){
|
if (other instanceof IPropertyKey k) {
|
||||||
return name.equals(k.toString());
|
return name.equals(k.toString());
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int hashCode(){
|
public int hashCode() {
|
||||||
return name.hashCode();
|
return name.hashCode();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -9,4 +9,5 @@ public class BuiltinExtensions {
|
||||||
public final String PRE_LAUNCH = "prelaunch";
|
public final String PRE_LAUNCH = "prelaunch";
|
||||||
public final String ACCESSWIDENER = "accesswidener";
|
public final String ACCESSWIDENER = "accesswidener";
|
||||||
public final String LOADING_TYPE = "loading_type";
|
public final String LOADING_TYPE = "loading_type";
|
||||||
|
public final String MOD_PROVIDER = "modprovider";
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,8 @@ import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import com.google.common.collect.*;
|
import com.google.common.collect.Multimap;
|
||||||
|
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;
|
||||||
|
@ -229,18 +230,10 @@ public class ModDependencyResolver {
|
||||||
throw new IllegalArgumentException(comparator);
|
throw new IllegalArgumentException(comparator);
|
||||||
}
|
}
|
||||||
var type = DependencyType.of(matcher.group(1));
|
var type = DependencyType.of(matcher.group(1));
|
||||||
StringBuilder version = new StringBuilder(matcher.group(2));
|
var version = matcher.group(2);
|
||||||
|
|
||||||
while (version.length() < 5) {
|
|
||||||
if (version.charAt(version.length() - 1) == '.') {
|
|
||||||
version.append('0');
|
|
||||||
} else {
|
|
||||||
version.append('.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return new Comparator(type, SemVerImpl.parse(version.toString()));
|
return new Comparator(type, SemVerImpl.parse(version));
|
||||||
} catch (SemVerParseException e) {
|
} catch (SemVerParseException e) {
|
||||||
throw new UncheckedIOException(e);
|
throw new UncheckedIOException(e);
|
||||||
}
|
}
|
||||||
|
@ -324,6 +317,7 @@ public class ModDependencyResolver {
|
||||||
public record VersionRange(Collection<ComparatorSet> sets) {
|
public record VersionRange(Collection<ComparatorSet> sets) {
|
||||||
|
|
||||||
private static final Pattern NUMBER_EXTRACTOR = Pattern.compile("[^~]?(\\d+)(?:\\.(?:([\\dxX*]+)(?:\\.([\\dxX*]+)?)?)?)?(.*)");
|
private static final Pattern NUMBER_EXTRACTOR = Pattern.compile("[^~]?(\\d+)(?:\\.(?:([\\dxX*]+)(?:\\.([\\dxX*]+)?)?)?)?(.*)");
|
||||||
|
private static final Pattern FILL_PATTERN = Pattern.compile("[><=]+(\\d+)\\.?(\\d+)?\\.?(\\d+)?");
|
||||||
|
|
||||||
public static VersionRange parse(String range) throws ResolverException {
|
public static VersionRange parse(String range) throws ResolverException {
|
||||||
|
|
||||||
|
@ -364,13 +358,7 @@ public class ModDependencyResolver {
|
||||||
private static void handleTilde(String s, List<String> ranges) throws ResolverException {
|
private static void handleTilde(String s, List<String> ranges) throws ResolverException {
|
||||||
{
|
{
|
||||||
StringBuilder builder = new StringBuilder(">=" + s.substring(1));
|
StringBuilder builder = new StringBuilder(">=" + s.substring(1));
|
||||||
while (builder.length() < 7) {
|
fillVersion(builder);
|
||||||
if (builder.charAt(builder.length() - 1) == '.') {
|
|
||||||
builder.append('0');
|
|
||||||
} else {
|
|
||||||
builder.append('.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ranges.add(builder.toString());
|
ranges.add(builder.toString());
|
||||||
}
|
}
|
||||||
ranges.add(" ");
|
ranges.add(" ");
|
||||||
|
@ -392,13 +380,7 @@ public class ModDependencyResolver {
|
||||||
private static void handleXRanges(String s, List<String> ranges) throws ResolverException {
|
private static void handleXRanges(String s, List<String> ranges) throws ResolverException {
|
||||||
{
|
{
|
||||||
StringBuilder builder = new StringBuilder(">=" + s.replaceAll("[xX*]", "0"));
|
StringBuilder builder = new StringBuilder(">=" + s.replaceAll("[xX*]", "0"));
|
||||||
while (builder.length() < 7) {
|
fillVersion(builder);
|
||||||
if (builder.charAt(builder.length() - 1) == '.') {
|
|
||||||
builder.append('0');
|
|
||||||
} else {
|
|
||||||
builder.append('.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ranges.add(builder.toString());
|
ranges.add(builder.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -423,13 +405,7 @@ public class ModDependencyResolver {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
while (builder.length() < 6) {
|
fillVersion(builder);
|
||||||
if (builder.charAt(builder.length() - 1) == '.') {
|
|
||||||
builder.append('0');
|
|
||||||
} else {
|
|
||||||
builder.append('.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ranges.add(builder.toString());
|
ranges.add(builder.toString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -455,26 +431,21 @@ public class ModDependencyResolver {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
while (builder.length() < 6) {
|
fillVersion(builder);
|
||||||
|
/*while (builder.length() < 6) {
|
||||||
if (builder.charAt(builder.length() - 1) == '.') {
|
if (builder.charAt(builder.length() - 1) == '.') {
|
||||||
builder.append('0');
|
builder.append('0');
|
||||||
} else {
|
} else {
|
||||||
builder.append('.');
|
builder.append('.');
|
||||||
}
|
}
|
||||||
}
|
}*/
|
||||||
ranges.add(builder.toString());
|
ranges.add(builder.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void handleHyphenRange(String s, List<String> ranges, String end) throws ResolverException {
|
private static void handleHyphenRange(String s, List<String> ranges, String end) throws ResolverException {
|
||||||
{
|
{
|
||||||
StringBuilder builder = new StringBuilder(">=" + s);
|
StringBuilder builder = new StringBuilder(">=" + s);
|
||||||
while (builder.length() < 7) {
|
fillVersion(builder);
|
||||||
if (builder.charAt(builder.length() - 1) == '.') {
|
|
||||||
builder.append('0');
|
|
||||||
} else {
|
|
||||||
builder.append('.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ranges.add(builder.toString());
|
ranges.add(builder.toString());
|
||||||
ranges.add(" ");
|
ranges.add(" ");
|
||||||
}
|
}
|
||||||
|
@ -498,19 +469,35 @@ public class ModDependencyResolver {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
while (builder.length() < 6) {
|
fillVersion(builder);
|
||||||
if (builder.charAt(builder.length() - 1) == '.') {
|
|
||||||
builder.append('0');
|
|
||||||
} else {
|
|
||||||
builder.append('.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ranges.add(builder.toString());
|
ranges.add(builder.toString());
|
||||||
} else {
|
} else {
|
||||||
ranges.add("<=" + end);
|
ranges.add("<=" + end);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void fillVersion(StringBuilder builder) {
|
||||||
|
Matcher matcher = FILL_PATTERN.matcher(builder);
|
||||||
|
if (!matcher.find()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (matcher.group(3) != null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (builder.charAt(builder.length() - 1) != '.') {
|
||||||
|
builder.append(".");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (matcher.group(2) == null) {
|
||||||
|
builder.append("0.0");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.append("0");
|
||||||
|
}
|
||||||
|
|
||||||
private static @NotNull List<String> extractRanges(String range) {
|
private static @NotNull List<String> extractRanges(String range) {
|
||||||
if (!range.contains(" ") && !range.contains(" - ") && !range.contains("||")) {
|
if (!range.contains(" ") && !range.contains(" - ") && !range.contains("||")) {
|
||||||
return List.of(range);
|
return List.of(range);
|
||||||
|
|
|
@ -49,8 +49,8 @@ public class ModPropertiesReader {
|
||||||
CommentedConfig props = PARSER.parse(in);
|
CommentedConfig props = PARSER.parse(in);
|
||||||
|
|
||||||
String url = in.toString();
|
String url = in.toString();
|
||||||
Path source = Path.of(url.substring(url.lastIndexOf(":")+1).split("!")[0]).toAbsolutePath();
|
Path source = Path.of(url.substring(url.lastIndexOf(":") + 1).split("!")[0]).toAbsolutePath();
|
||||||
if (!source.getFileName().toString().endsWith(".jar")){
|
if (!source.getFileName().toString().endsWith(".jar")) {
|
||||||
source = source.getParent();
|
source = source.getParent();
|
||||||
} else {
|
} else {
|
||||||
// TODO will this result in a memory leak?
|
// TODO will this result in a memory leak?
|
||||||
|
@ -96,7 +96,7 @@ public class ModPropertiesReader {
|
||||||
if (version == null || version.isEmpty())
|
if (version == null || version.isEmpty())
|
||||||
badProperties.add("frog.mod.version");
|
badProperties.add("frog.mod.version");
|
||||||
else {
|
else {
|
||||||
try {
|
try {
|
||||||
semVer = SemVerImpl.parse(version);
|
semVer = SemVerImpl.parse(version);
|
||||||
} catch (SemVerParseException e) {
|
} catch (SemVerParseException e) {
|
||||||
badProperties.add("frog.mod.version");
|
badProperties.add("frog.mod.version");
|
||||||
|
@ -186,11 +186,11 @@ public class ModPropertiesReader {
|
||||||
|
|
||||||
public InvalidModPropertiesException(String id, Collection<Path> sources, Collection<String> invalid) {
|
public InvalidModPropertiesException(String id, Collection<Path> sources, Collection<String> invalid) {
|
||||||
super(
|
super(
|
||||||
"Invalid properties for %s (%s) - invalid or missing values for: %s".formatted(
|
"Invalid properties for %s (%s) - invalid or missing values for: %s".formatted(
|
||||||
Objects.requireNonNullElse(id, "<unknown>"),
|
Objects.requireNonNullElse(id, "<unknown>"),
|
||||||
sources.stream().map(Path::toString).collect(Collectors.joining(", ")),
|
sources.stream().map(Path::toString).collect(Collectors.joining(", ")),
|
||||||
String.join(", ", invalid)
|
String.join(", ", invalid)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
this.invalid = invalid;
|
this.invalid = invalid;
|
||||||
}
|
}
|
||||||
|
|
|
@ -60,7 +60,7 @@ public class ModUtil {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
children.computeIfAbsent(mod, m -> new HashSet<>());
|
children.putIfAbsent(mod, Collections.emptySet());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,244 +0,0 @@
|
||||||
package dev.frogmc.frogloader.impl.plugin.game.minecraft;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.lang.invoke.MethodHandle;
|
|
||||||
import java.lang.invoke.MethodHandles;
|
|
||||||
import java.lang.invoke.MethodType;
|
|
||||||
import java.net.MalformedURLException;
|
|
||||||
import java.nio.file.*;
|
|
||||||
import java.nio.file.attribute.BasicFileAttributes;
|
|
||||||
import java.util.*;
|
|
||||||
|
|
||||||
import com.electronwill.nightconfig.core.UnmodifiableConfig;
|
|
||||||
import com.google.gson.JsonObject;
|
|
||||||
import dev.frogmc.frogloader.api.FrogLoader;
|
|
||||||
import dev.frogmc.frogloader.api.extensions.PreLaunchExtension;
|
|
||||||
import dev.frogmc.frogloader.api.mod.ModDependencies;
|
|
||||||
import dev.frogmc.frogloader.api.mod.ModExtensions;
|
|
||||||
import dev.frogmc.frogloader.api.mod.ModProperties;
|
|
||||||
import dev.frogmc.frogloader.api.plugin.FrogPlugin;
|
|
||||||
import dev.frogmc.frogloader.impl.Discovery;
|
|
||||||
import dev.frogmc.frogloader.impl.FrogLoaderImpl;
|
|
||||||
import dev.frogmc.frogloader.impl.gui.LoaderGui;
|
|
||||||
import dev.frogmc.frogloader.impl.mixin.AWProcessor;
|
|
||||||
import dev.frogmc.frogloader.impl.mod.*;
|
|
||||||
import dev.frogmc.frogloader.impl.util.CrashReportGenerator;
|
|
||||||
import dev.frogmc.frogloader.impl.util.SystemProperties;
|
|
||||||
import dev.frogmc.thyroxine.Thyroxine;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
import org.spongepowered.asm.mixin.Mixins;
|
|
||||||
|
|
||||||
public class Minecraft implements FrogPlugin {
|
|
||||||
|
|
||||||
protected static final String[] MINECRAFT_CLASSES = new String[]{
|
|
||||||
"net/minecraft/client/main/Main.class",
|
|
||||||
"net/minecraft/client/MinecraftApplet.class",
|
|
||||||
"net/minecraft/server/Main.class"
|
|
||||||
};
|
|
||||||
private static final Logger LOGGER = LoggerFactory.getLogger("Plugin/Minecraft");
|
|
||||||
protected final Collection<ModProperties> modProperties = new ArrayList<>();
|
|
||||||
protected Path gamePath;
|
|
||||||
protected String foundMainClass;
|
|
||||||
private String version;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isApplicable() {
|
|
||||||
gamePath = findGame();
|
|
||||||
return gamePath != null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings({"rawtypes", "unchecked"})
|
|
||||||
@Override
|
|
||||||
public void init(FrogLoader loader) throws Exception {
|
|
||||||
if (gamePath == null) {
|
|
||||||
throw new IllegalStateException("Game not found yet!");
|
|
||||||
}
|
|
||||||
Path remappedGamePath = loader.getGameDir().resolve(".frogmc/remappedJars").resolve(version).resolve("game-" + version + "-remapped.jar");
|
|
||||||
|
|
||||||
if (!Files.exists(remappedGamePath.getParent())) {
|
|
||||||
try {
|
|
||||||
Files.createDirectories(remappedGamePath.getParent());
|
|
||||||
} catch (IOException e) {
|
|
||||||
LOGGER.error("Failed to create directory", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
modProperties.add(JavaModProperties.get());
|
|
||||||
modProperties.add(new ModPropertiesImpl("minecraft", "Minecraft", "/assets/minecraft/textures/block/grass_block_side.png",
|
|
||||||
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()));
|
|
||||||
|
|
||||||
Collection<Path> mods = Discovery.find(loader.getModsDir(), path ->
|
|
||||||
version.equals(path.getFileName().toString()), path ->
|
|
||||||
path.getFileName().toString().endsWith(FrogLoaderImpl.MOD_FILE_EXTENSION));
|
|
||||||
this.getClass().getClassLoader().resources(ModPropertiesReader.PROPERTIES_FILE_NAME).map(ModPropertiesReader::readFile)
|
|
||||||
.map(o -> o.orElse(null)).filter(Objects::nonNull).forEach(modProperties::add);
|
|
||||||
|
|
||||||
Map<Path, ModProperties> modPaths = new HashMap<>();
|
|
||||||
Path jijCache = getJijCacheDir();
|
|
||||||
for (Path mod : new HashSet<>(mods)) {
|
|
||||||
findJiJMods(mod, mods, modPaths, jijCache);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
modProperties.retainAll(new ModDependencyResolver(modProperties).solve());
|
|
||||||
} catch (ModDependencyResolver.BreakingModException e) {
|
|
||||||
LoaderGui.execBreakingDep(CrashReportGenerator.writeReport(e, modProperties), e, false);
|
|
||||||
} catch (ModDependencyResolver.UnfulfilledDependencyException e) {
|
|
||||||
LoaderGui.execUnfulfilledDep(CrashReportGenerator.writeReport(e, modProperties), e, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
mods.stream().filter(p -> modProperties.contains(modPaths.get(p))).map(path -> {
|
|
||||||
try {
|
|
||||||
return path.toUri().toURL();
|
|
||||||
} catch (MalformedURLException e) {
|
|
||||||
LOGGER.warn("Failed to resolve url for {}", path, e);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}).filter(Objects::nonNull).forEach(FrogLoaderImpl.getInstance().getClassloader()::addURL);
|
|
||||||
|
|
||||||
modProperties.forEach(props -> {
|
|
||||||
Object o = props.extensions().get(BuiltinExtensions.MIXIN_CONFIG);
|
|
||||||
if (o instanceof String name) {
|
|
||||||
Mixins.addConfiguration(name);
|
|
||||||
} else if (o instanceof Collection l) {
|
|
||||||
((Collection<String>) l).forEach(Mixins::addConfiguration);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!loader.isDevelopment()) {
|
|
||||||
if (!Files.exists(remappedGamePath)) {
|
|
||||||
Thyroxine.run(version, gamePath, remappedGamePath, true, false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
AWProcessor.load(modProperties);
|
|
||||||
|
|
||||||
var runtimePath = loader.isDevelopment() ? gamePath : remappedGamePath;
|
|
||||||
FrogLoaderImpl.getInstance().getClassloader().addURL(runtimePath.toUri().toURL());
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void findJiJMods(Path mod, Collection<Path> mods, Map<Path, ModProperties> modPaths, Path jijCache) throws IOException {
|
|
||||||
Optional<ModProperties> opt = ModPropertiesReader.read(mod);
|
|
||||||
if (opt.isPresent()) {
|
|
||||||
ModProperties p = opt.get();
|
|
||||||
modProperties.add(p);
|
|
||||||
modPaths.put(mod, p);
|
|
||||||
List<List<UnmodifiableConfig>> entries = p.extensions().getOrDefault(BuiltinExtensions.INCLUDED_JARS, Collections.emptyList());
|
|
||||||
if (entries.isEmpty()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try (FileSystem fs = FileSystems.newFileSystem(mod)) {
|
|
||||||
for (List<UnmodifiableConfig> jars : entries) {
|
|
||||||
for (UnmodifiableConfig jar : jars) {
|
|
||||||
Path path = fs.getPath(jar.get("path")).toAbsolutePath();
|
|
||||||
Path extracted = jijCache.resolve((String) jar.get("id"));
|
|
||||||
if (!Files.exists(extracted)){
|
|
||||||
Files.createDirectories(jijCache);
|
|
||||||
Files.copy(path, extracted);
|
|
||||||
}
|
|
||||||
mods.add(extracted);
|
|
||||||
findJiJMods(extracted, mods, modPaths, jijCache);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private Path getJijCacheDir(){
|
|
||||||
Path dir = FrogLoader.getInstance().getGameDir().resolve(".frogmc").resolve("jijcache");
|
|
||||||
|
|
||||||
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
|
|
||||||
try {
|
|
||||||
Files.walkFileTree(dir, new SimpleFileVisitor<>(){
|
|
||||||
@Override
|
|
||||||
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
|
|
||||||
Files.delete(file);
|
|
||||||
return super.visitFile(file, attrs);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
|
|
||||||
Files.delete(dir);
|
|
||||||
return super.postVisitDirectory(dir, exc);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (IOException e) {
|
|
||||||
LOGGER.error("Failed to clear extracted jij mods!", e);
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
return dir;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Collection<ModProperties> getMods() {
|
|
||||||
return modProperties;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected Path findGame() {
|
|
||||||
LOGGER.info("Locating game..");
|
|
||||||
String jar = System.getProperty(SystemProperties.MINECRAFT_GAME_JAR);
|
|
||||||
if (jar != null) {
|
|
||||||
Path p = Paths.get(jar);
|
|
||||||
if (checkLocation(p)) {
|
|
||||||
return p;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (String s : System.getProperty("java.class.path", "").split(File.pathSeparator)) {
|
|
||||||
Path p = Paths.get(s);
|
|
||||||
if (checkLocation(p)) {
|
|
||||||
return p;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
LOGGER.warn("Could not locate game!");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected boolean checkLocation(Path jar) {
|
|
||||||
if (!Files.exists(jar) || Files.isDirectory(jar)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
try (FileSystem fs = FileSystems.newFileSystem(jar)) {
|
|
||||||
for (String n : MINECRAFT_CLASSES) {
|
|
||||||
if (Files.exists(fs.getPath(n)) && n.contains(FrogLoaderImpl.getInstance().getEnv().getIdentifier())) {
|
|
||||||
LOGGER.info("Found game: {}", jar);
|
|
||||||
foundMainClass = n.substring(0, n.length() - 6).replace("/", ".");
|
|
||||||
try {
|
|
||||||
version = FrogLoaderImpl.getInstance().getGson().fromJson(Files.readString(fs.getPath("version.json")), JsonObject.class).get("id").getAsString();
|
|
||||||
} catch (Exception e){
|
|
||||||
version = FrogLoaderImpl.getInstance().getArgument("version");
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
try {
|
|
||||||
if (foundMainClass != null) {
|
|
||||||
modProperties.forEach(props ->
|
|
||||||
props.extensions().runIfPresent(PreLaunchExtension.ID,
|
|
||||||
PreLaunchExtension.class, PreLaunchExtension::onPreLaunch));
|
|
||||||
LOGGER.info("Launching main class: {}", foundMainClass);
|
|
||||||
Class<?> mainClass = Class.forName(foundMainClass);
|
|
||||||
MethodHandle main = MethodHandles.publicLookup().findStatic(mainClass, "main", MethodType.methodType(void.class, String[].class));
|
|
||||||
main.invoke((Object) FrogLoaderImpl.getInstance().getArgs());
|
|
||||||
} else {
|
|
||||||
LOGGER.warn("Failed to locate main class!");
|
|
||||||
}
|
|
||||||
} catch (Throwable e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,141 @@
|
||||||
|
package dev.frogmc.frogloader.impl.plugin.game.minecraft;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.lang.invoke.MethodHandle;
|
||||||
|
import java.lang.invoke.MethodHandles;
|
||||||
|
import java.lang.invoke.MethodType;
|
||||||
|
import java.nio.file.*;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
|
import dev.frogmc.frogloader.api.FrogLoader;
|
||||||
|
import dev.frogmc.frogloader.api.mod.ModDependencies;
|
||||||
|
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.ModPropertiesImpl;
|
||||||
|
import dev.frogmc.frogloader.impl.util.SystemProperties;
|
||||||
|
import dev.frogmc.thyroxine.Thyroxine;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
public class MinecraftGamePlugin implements GamePlugin {
|
||||||
|
|
||||||
|
protected final String[] MINECRAFT_CLASSES = new String[]{
|
||||||
|
"net/minecraft/client/main/Main.class",
|
||||||
|
"net/minecraft/client/MinecraftApplet.class",
|
||||||
|
"net/minecraft/server/Main.class"
|
||||||
|
};
|
||||||
|
private static final Logger LOGGER = LoggerFactory.getLogger("Plugin/Minecraft");
|
||||||
|
protected Path gamePath;
|
||||||
|
protected String foundMainClass;
|
||||||
|
private String version;
|
||||||
|
|
||||||
|
protected boolean checkLocation(Path jar) {
|
||||||
|
if (!Files.exists(jar) || Files.isDirectory(jar)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try (FileSystem fs = FileSystems.newFileSystem(jar)) {
|
||||||
|
for (String n : MINECRAFT_CLASSES) {
|
||||||
|
if (Files.exists(fs.getPath(n)) && n.contains(FrogLoaderImpl.getInstance().getEnv().getIdentifier())) {
|
||||||
|
LOGGER.info("Found game: {}", jar);
|
||||||
|
foundMainClass = n.substring(0, n.length() - 6).replace("/", ".");
|
||||||
|
try {
|
||||||
|
version = FrogLoaderImpl.getInstance().getGson().fromJson(Files.readString(fs.getPath("version.json")), JsonObject.class).get("id").getAsString();
|
||||||
|
} catch (Exception e) {
|
||||||
|
version = FrogLoaderImpl.getInstance().getArgument("version");
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
protected Path findGame() {
|
||||||
|
LOGGER.info("Locating game..");
|
||||||
|
String jar = System.getProperty(SystemProperties.MINECRAFT_GAME_JAR);
|
||||||
|
if (jar != null) {
|
||||||
|
Path p = Paths.get(jar);
|
||||||
|
if (checkLocation(p)) {
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (String s : System.getProperty("java.class.path", "").split(File.pathSeparator)) {
|
||||||
|
Path p = Paths.get(s);
|
||||||
|
if (checkLocation(p)) {
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LOGGER.warn("Could not locate game!");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isApplicable() {
|
||||||
|
gamePath = findGame();
|
||||||
|
return gamePath != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
try {
|
||||||
|
if (foundMainClass != null) {
|
||||||
|
LOGGER.info("Launching main class: {}", foundMainClass);
|
||||||
|
Class<?> mainClass = Class.forName(foundMainClass);
|
||||||
|
MethodHandle main = MethodHandles.publicLookup().findStatic(mainClass, "main", MethodType.methodType(void.class, String[].class));
|
||||||
|
main.invoke((Object) FrogLoaderImpl.getInstance().getArgs());
|
||||||
|
} else {
|
||||||
|
LOGGER.warn("Failed to locate main class!");
|
||||||
|
}
|
||||||
|
} catch (Throwable e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void init(FrogLoader loader) throws Exception {
|
||||||
|
if (gamePath == null) {
|
||||||
|
throw new IllegalStateException("Game not found!");
|
||||||
|
}
|
||||||
|
Path remappedGamePath = loader.getGameDir().resolve(".frogmc/remappedJars").resolve(version).resolve("game-" + version + "-remapped.jar");
|
||||||
|
|
||||||
|
if (!Files.exists(remappedGamePath.getParent())) {
|
||||||
|
try {
|
||||||
|
Files.createDirectories(remappedGamePath.getParent());
|
||||||
|
} catch (IOException e) {
|
||||||
|
LOGGER.error("Failed to create directory", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!loader.isDevelopment()) {
|
||||||
|
if (!Files.exists(remappedGamePath)) {
|
||||||
|
Thyroxine.run(version, gamePath, remappedGamePath, true, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var runtimePath = loader.isDevelopment() ? gamePath : remappedGamePath;
|
||||||
|
FrogLoaderImpl.getInstance().getClassloader().addURL(runtimePath.toUri().toURL());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NotNull String queryVersion() {
|
||||||
|
return version;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ModProperties getGameMod() {
|
||||||
|
return new ModPropertiesImpl("minecraft", "Minecraft", "/assets/minecraft/textures/block/grass_block_side.png",
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,8 @@
|
||||||
package dev.frogmc.frogloader.impl.plugin.game.minecraft;
|
package dev.frogmc.frogloader.impl.plugin.game.minecraft;
|
||||||
|
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
import dev.frogmc.frogloader.api.mod.SemVer;
|
import dev.frogmc.frogloader.api.mod.SemVer;
|
||||||
import dev.frogmc.frogloader.impl.SemVerParseException;
|
import dev.frogmc.frogloader.impl.SemVerParseException;
|
||||||
import dev.frogmc.frogloader.impl.mod.SemVerImpl;
|
import dev.frogmc.frogloader.impl.mod.SemVerImpl;
|
||||||
|
@ -7,6 +10,7 @@ import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
public class MinecraftSemVerImpl implements SemVer {
|
public class MinecraftSemVerImpl implements SemVer {
|
||||||
|
|
||||||
|
private static final Pattern FILL_PATTERN = Pattern.compile("(\\d+)\\.?(\\d+)?\\.?(\\d+)?");
|
||||||
private final String version;
|
private final String version;
|
||||||
|
|
||||||
private MinecraftSemVerImpl(String version) {
|
private MinecraftSemVerImpl(String version) {
|
||||||
|
@ -15,12 +19,36 @@ public class MinecraftSemVerImpl implements SemVer {
|
||||||
|
|
||||||
static SemVer get(String version) {
|
static SemVer get(String version) {
|
||||||
try {
|
try {
|
||||||
return SemVerImpl.parse(version);
|
StringBuilder builder = new StringBuilder(version);
|
||||||
|
fillVersion(builder);
|
||||||
|
return SemVerImpl.parse(builder.toString());
|
||||||
} catch (SemVerParseException e) {
|
} catch (SemVerParseException e) {
|
||||||
return new MinecraftSemVerImpl(version);
|
return new MinecraftSemVerImpl(version);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void fillVersion(StringBuilder builder) {
|
||||||
|
Matcher matcher = FILL_PATTERN.matcher(builder);
|
||||||
|
if (!matcher.find()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (matcher.group(3) != null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (builder.charAt(builder.length() - 1) != '.') {
|
||||||
|
builder.append(".");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (matcher.group(2) == null) {
|
||||||
|
builder.append("0.0");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.append("0");
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int major() {
|
public int major() {
|
||||||
throw new UnsupportedOperationException("Minecraft version " + version + " does not represent a semver-compatible version");
|
throw new UnsupportedOperationException("Minecraft version " + version + " does not represent a semver-compatible version");
|
||||||
|
|
|
@ -0,0 +1,143 @@
|
||||||
|
package dev.frogmc.frogloader.impl.plugin.mod;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.MalformedURLException;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.nio.file.*;
|
||||||
|
import java.nio.file.attribute.BasicFileAttributes;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
import com.electronwill.nightconfig.core.UnmodifiableConfig;
|
||||||
|
import dev.frogmc.frogloader.api.FrogLoader;
|
||||||
|
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.mod.ModPropertiesReader;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
@SuppressWarnings("unused")
|
||||||
|
public class FrogModProvider implements ModProvider {
|
||||||
|
|
||||||
|
public static final String MOD_FILE_EXTENSION = ".frogmod";
|
||||||
|
|
||||||
|
Logger LOGGER = LoggerFactory.getLogger("FrogModProvider");
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String id() {
|
||||||
|
return "frogloader:frogmod";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isApplicable() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isFileApplicable(Path path) {
|
||||||
|
if (!path.toString().endsWith(MOD_FILE_EXTENSION)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try (FileSystem fs = FileSystems.newFileSystem(path)) {
|
||||||
|
return Files.exists(fs.getPath("frog.mod.toml"));
|
||||||
|
} catch (Exception e) {
|
||||||
|
LOGGER.error("Error while checking file {}", path, e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isDirectoryApplicable(Path path) {
|
||||||
|
return path.getFileName().toString().equals(FrogLoader.getInstance().getGameVersion());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Collection<ModProperties> loadMods(Collection<Path> paths, Consumer<URL> classpathAdder) throws Exception {
|
||||||
|
Path jijCache = getJijCacheDir();
|
||||||
|
|
||||||
|
Map<Path, ModProperties> mods = new HashMap<>();
|
||||||
|
Collection<Path> set = new HashSet<>(paths);
|
||||||
|
Collection<ModProperties> loadedMods = new HashSet<>();
|
||||||
|
|
||||||
|
this.getClass().getClassLoader().resources(ModPropertiesReader.PROPERTIES_FILE_NAME).map(ModPropertiesReader::readFile)
|
||||||
|
.map(o -> o.orElse(null)).filter(Objects::nonNull).forEach(loadedMods::add);
|
||||||
|
|
||||||
|
for (Path p : paths) {
|
||||||
|
findJiJMods(p, set, mods, jijCache);
|
||||||
|
}
|
||||||
|
|
||||||
|
set.stream().filter(mods::containsKey).map(path -> {
|
||||||
|
try {
|
||||||
|
return path.toUri().toURL();
|
||||||
|
} catch (MalformedURLException e) {
|
||||||
|
LOGGER.warn("Failed to resolve url for {}", path, e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}).filter(Objects::nonNull).forEach(classpathAdder);
|
||||||
|
|
||||||
|
loadedMods.addAll(mods.values());
|
||||||
|
|
||||||
|
return loadedMods;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Path getJijCacheDir(){
|
||||||
|
Path dir = FrogLoader.getInstance().getGameDir().resolve(".frogmc").resolve("jijcache");
|
||||||
|
|
||||||
|
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
|
||||||
|
try {
|
||||||
|
if (Files.exists(dir)) {
|
||||||
|
Files.walkFileTree(dir, new SimpleFileVisitor<>() {
|
||||||
|
@Override
|
||||||
|
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
|
||||||
|
Files.delete(file);
|
||||||
|
return super.visitFile(file, attrs);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
|
||||||
|
Files.delete(dir);
|
||||||
|
return super.postVisitDirectory(dir, exc);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
LOGGER.error("Failed to clear extracted jij mods!", e);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
return dir;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void findJiJMods(Path mod, Collection<Path> mods, Map<Path, ModProperties> modPaths, Path jijCache) throws IOException {
|
||||||
|
Optional<ModProperties> opt = ModPropertiesReader.read(mod);
|
||||||
|
if (opt.isPresent()) {
|
||||||
|
ModProperties p = opt.get();
|
||||||
|
modPaths.put(mod, p);
|
||||||
|
List<List<UnmodifiableConfig>> entries = p.extensions().getOrDefault(BuiltinExtensions.INCLUDED_JARS, Collections.emptyList());
|
||||||
|
if (entries.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try (FileSystem fs = FileSystems.newFileSystem(mod)) {
|
||||||
|
for (List<UnmodifiableConfig> jars : entries) {
|
||||||
|
for (UnmodifiableConfig jar : jars) {
|
||||||
|
Path path = fs.getPath(jar.get("path")).toAbsolutePath();
|
||||||
|
Path extracted = jijCache.resolve((String) jar.get("id"));
|
||||||
|
if (!Files.exists(extracted)){
|
||||||
|
Files.createDirectories(jijCache);
|
||||||
|
Files.copy(path, extracted);
|
||||||
|
}
|
||||||
|
mods.add(extracted);
|
||||||
|
findJiJMods(extracted, mods, modPaths, jijCache);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void preLaunch(Collection<ModProperties> mods) {
|
||||||
|
mods.forEach(mod -> mod.extensions().runIfPresent(PreLaunchExtension.ID, PreLaunchExtension.class, PreLaunchExtension::onPreLaunch));
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,28 @@
|
||||||
|
package dev.frogmc.frogloader.impl.plugin.mod;
|
||||||
|
|
||||||
|
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.plugin.ModProvider;
|
||||||
|
import dev.frogmc.frogloader.impl.mod.JavaModProperties;
|
||||||
|
|
||||||
|
public class JavaModProvider implements ModProvider {
|
||||||
|
@Override
|
||||||
|
public String id() {
|
||||||
|
return "frogloader:integrated/java";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isApplicable() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Collection<ModProperties> loadMods(Collection<Path> modFiles, Consumer<URL> classpathAdder) throws Exception {
|
||||||
|
return Collections.singleton(JavaModProperties.get());
|
||||||
|
}
|
||||||
|
}
|
|
@ -41,8 +41,8 @@ public class PlatformUtil {
|
||||||
ProcessBuilder builder = new ProcessBuilder("bash", "-c", "wl-copy < " + path);
|
ProcessBuilder builder = new ProcessBuilder("bash", "-c", "wl-copy < " + path);
|
||||||
builder.start();
|
builder.start();
|
||||||
} else {
|
} else {
|
||||||
String data = Files.readString(path, StandardCharsets.UTF_8);
|
String data = Files.readString(path, StandardCharsets.UTF_8);
|
||||||
Toolkit.getDefaultToolkit().getSystemClipboard().setContents(new StringSelection(data), null);
|
Toolkit.getDefaultToolkit().getSystemClipboard().setContents(new StringSelection(data), null);
|
||||||
}
|
}
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
LOGGER.error("Failed to copy contents of {}:", path, e);
|
LOGGER.error("Failed to copy contents of {}:", path, e);
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
dev.frogmc.frogloader.impl.plugin.game.minecraft.Minecraft
|
|
|
@ -0,0 +1 @@
|
||||||
|
dev.frogmc.frogloader.impl.plugin.game.minecraft.MinecraftGamePlugin
|
|
@ -0,0 +1,2 @@
|
||||||
|
dev.frogmc.frogloader.impl.plugin.mod.JavaModProvider
|
||||||
|
dev.frogmc.frogloader.impl.plugin.mod.FrogModProvider
|
|
@ -9,4 +9,5 @@ license = "Apache-2.0"
|
||||||
credits = [
|
credits = [
|
||||||
{ name = "FrogMC Team", roles = ["author"] }
|
{ name = "FrogMC Team", roles = ["author"] }
|
||||||
]
|
]
|
||||||
|
icon = "icon.png"
|
||||||
|
|
||||||
|
|
BIN
src/main/resources/icon.png
Normal file
After Width: | Height: | Size: 7.3 KiB |
I'm asumming this serves as a code example - in which case it should be moved to the documentation
The entire
minecraft
project is a test project - this tests that the discovery works properlySome of this should be in the documentation as well, but this is fine to stay