Compare commits
15 commits
e456f6849c
...
06b28c6e16
Author | SHA1 | Date | |
---|---|---|---|
owlsys | 06b28c6e16 | ||
moehreag | 39ba054fcb | ||
moehreag | b6d904595a | ||
moehreag | a3960b933f | ||
moehreag | 565e4df74e | ||
moehreag | 6dd1bf04ed | ||
moehreag | d58867a92e | ||
moehreag | 0476adadd8 | ||
moehreag | a2d9ac045c | ||
moehreag | f0f742925b | ||
moehreag | a3aab3d87c | ||
moehreag | e9c8ecaaac | ||
moehreag | e222633c13 | ||
Ecorous | e352bb835f | ||
Ecorous | f6715e36a2 |
|
@ -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 {
|
||||||
|
@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"
|
||||||
|
|
|
@ -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();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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) {
|
gamePlugin = plugin;
|
||||||
LOGGER.error("Error during plugin initialisation: ", e);
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
if (plugins.isEmpty()) {
|
private void loadModProviders() throws Exception {
|
||||||
throw new IllegalStateException("No plugin applicable to the current state was found!");
|
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
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,14 +1,16 @@
|
||||||
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;
|
||||||
|
@ -28,17 +30,29 @@ public class DependencyErrorEntry extends JPanel {
|
||||||
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);
|
||||||
|
|
||||||
|
if (icon != null) {
|
||||||
|
URL location = getClass().getResource("/"+icon);
|
||||||
|
|
||||||
|
if (location != null) {
|
||||||
|
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) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
top.add(text);
|
||||||
|
|
||||||
this.actions = new JPanel(new FlowLayout(FlowLayout.LEFT));
|
this.actions = new JPanel(new FlowLayout(FlowLayout.LEFT));
|
||||||
add(this.actions, BorderLayout.SOUTH);
|
add(this.actions, BorderLayout.SOUTH);
|
||||||
|
|
||||||
if (icon != null) {
|
|
||||||
URL location = getClass().getResource(icon);
|
|
||||||
|
|
||||||
if (location != null)
|
|
||||||
add(new JLabel(new ImageIcon(location)), BorderLayout.WEST);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void addAction(String label, ActionListener listener) {
|
public void addAction(String label, ActionListener listener) {
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
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) {
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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?
|
||||||
|
|
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
BIN
src/main/resources/icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 7.3 KiB |
Loading…
Reference in a new issue