Separate plugin types #10
|
@ -23,4 +23,4 @@ jobs:
|
|||
./gradlew :publishMavenJavaPublicationToFrogMCSnapshotsMavenRepository \
|
||||
-PFrogMCSnapshotsMavenUsername=${{ secrets.MAVEN_PUSH_USER }} \
|
||||
-PFrogMCSnapshotsMavenPassword=${{ secrets.MAVEN_PUSH_TOKEN }} --stacktrace
|
||||
./gradlew :updateMeta
|
||||
./gradlew :updateMeta -PFrogMCMetaKey=${{ secrets.META_SECRET }} --stacktrace
|
||||
|
|
|
@ -7,7 +7,7 @@ plugins {
|
|||
}
|
||||
|
||||
group = "dev.frogmc"
|
||||
version = "0.0.1-SNAPSHOT"
|
||||
version = "0.0.1-alpha.1"
|
||||
|
||||
repositories {
|
||||
maven {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
[versions]
|
||||
|
||||
thyroxine = "0.0.1-alpha.2"
|
||||
thyroxine = "0.0.1-alpha.3"
|
||||
nightconfig = "3.7.2"
|
||||
mixin = "0.14.0+mixin.0.8.6"
|
||||
annotations = "24.1.0"
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
plugins {
|
||||
java
|
||||
id("dev.frogmc.phytotelma") version "0.0.1-alpha.9"
|
||||
id("dev.frogmc.phytotelma") version "0.0.1-alpha.10"
|
||||
}
|
||||
|
||||
repositories {
|
||||
|
@ -20,7 +20,7 @@ dependencies {
|
|||
}
|
||||
|
||||
phytotelma {
|
||||
minecraft("1.20.6")
|
||||
minecraft("1.21", "1.20.6")
|
||||
}
|
||||
|
||||
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"
|
||||
mixin = "example_mod.mixins.json"
|
||||
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.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;
|
||||
|
||||
/**
|
||||
* General API to interact with this loader.
|
||||
*
|
||||
* @see ModProperties
|
||||
* @see FrogPlugin
|
||||
* @see ModProvider
|
||||
* @see Env
|
||||
*/
|
||||
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.
|
||||
|
@ -59,11 +67,11 @@ public interface FrogLoader {
|
|||
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.
|
||||
|
@ -94,7 +102,14 @@ public interface FrogLoader {
|
|||
*
|
||||
* @return A collection of all loaded mods
|
||||
* @see ModProperties
|
||||
* @see FrogPlugin
|
||||
* @see ModProvider
|
||||
*/
|
||||
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.MethodHandles;
|
||||
import java.lang.invoke.MethodType;
|
||||
import java.util.Map;
|
||||
import java.util.Collection;
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
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 <T> The type of the value of this extension
|
||||
*/
|
||||
@SuppressWarnings({"rawtypes", "unchecked"})
|
||||
public <T> void runIfPresent(String key, Consumer<T> action) {
|
||||
T value = get(key);
|
||||
if (value != null) {
|
||||
action.accept(value);
|
||||
Object value = get(key);
|
||||
if (value instanceof Collection c){
|
||||
((Collection<T>)c).forEach(action);
|
||||
} else if (value != null) {
|
||||
action.accept((T)value);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -79,6 +79,7 @@ public interface ModProperties {
|
|||
|
||||
/**
|
||||
* Get this mod's paths
|
||||
*
|
||||
* @return Where this mod is loaded from
|
||||
*/
|
||||
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.env.Env;
|
||||
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.launch.MixinClassLoader;
|
||||
import dev.frogmc.frogloader.impl.mod.ModUtil;
|
||||
|
@ -26,30 +27,31 @@ import org.slf4j.LoggerFactory;
|
|||
import org.spongepowered.asm.mixin.MixinEnvironment;
|
||||
|
||||
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 Logger LOGGER = LoggerFactory.getLogger("FrogLoader");
|
||||
@Getter
|
||||
private static FrogLoaderImpl instance;
|
||||
@Getter
|
||||
private final String[] args;
|
||||
@Getter
|
||||
private final Env env;
|
||||
private final Logger LOGGER = LoggerFactory.getLogger("FrogLoader");
|
||||
|
||||
@Getter
|
||||
private final List<FrogPlugin> plugins = new ArrayList<>();
|
||||
|
||||
private final Collection<ModProvider> modProviders = new ArrayList<>();
|
||||
@Getter
|
||||
private final Path gameDir, configDir, modsDir;
|
||||
|
||||
private final Path gameDir, configDir;
|
||||
@Getter
|
||||
private final Collection<Path> modsDirs = new HashSet<>();
|
||||
@Getter
|
||||
private final MixinClassLoader classloader;
|
||||
|
||||
@Getter
|
||||
private final Gson gson = new Gson();
|
||||
|
||||
private Map<String, ModProperties> mods;
|
||||
private Collection<String> modIds;
|
||||
private final Map<String, Map<String, ModProperties>> mods = new HashMap<>();
|
||||
@Getter
|
||||
private GamePlugin gamePlugin;
|
||||
@Getter
|
||||
private String gameVersion;
|
||||
private Collection<String> modIds = new ArrayList<>();
|
||||
|
||||
|
||||
private FrogLoaderImpl(String[] args, Env env) {
|
||||
|
@ -60,26 +62,26 @@ public class FrogLoaderImpl implements FrogLoader {
|
|||
|
||||
gameDir = Paths.get(getArgumentOrElse("gameDir", "."));
|
||||
configDir = gameDir.resolve("config");
|
||||
modsDir = gameDir.resolve("mods");
|
||||
|
||||
try {
|
||||
Files.createDirectories(gameDir);
|
||||
Files.createDirectories(configDir);
|
||||
Files.createDirectories(modsDir);
|
||||
} catch (IOException e) {
|
||||
LOGGER.warn("Failed to create essential directories ", e);
|
||||
}
|
||||
|
||||
try {
|
||||
discoverPlugins();
|
||||
loadGamePlugin();
|
||||
loadModProviders();
|
||||
modProviders.stream().map(ModProvider::loadDirectory).map(gameDir::resolve).forEach(modsDirs::add);
|
||||
advanceMixinState();
|
||||
mods = collectMods();
|
||||
modIds = collectModIds();
|
||||
LOGGER.info(ModUtil.getModList(mods.values()));
|
||||
LOGGER.info(ModUtil.getModList(getMods()));
|
||||
LOGGER.info("Launching...");
|
||||
plugins.forEach(FrogPlugin::run);
|
||||
modProviders.forEach(plugin -> plugin.preLaunch(mods.get(plugin.id()).values()));
|
||||
gamePlugin.run();
|
||||
} 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() {
|
||||
ServiceLoader.load(FrogPlugin.class).forEach(plugin -> {
|
||||
try {
|
||||
if (plugin.isApplicable()) {
|
||||
plugin.init(this);
|
||||
plugins.add(plugin);
|
||||
private void loadGamePlugin() {
|
||||
GamePlugin plugin = PluginLoader.discoverGamePlugins();
|
||||
gameVersion = plugin.queryVersion();
|
||||
ModProperties gameMod = plugin.getGameMod();
|
||||
if (gameMod != null) {
|
||||
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);
|
||||
gamePlugin = plugin;
|
||||
}
|
||||
});
|
||||
|
||||
if (plugins.isEmpty()) {
|
||||
throw new IllegalStateException("No plugin applicable to the current state was found!");
|
||||
}
|
||||
private void loadModProviders() throws Exception {
|
||||
PluginLoader.discoverModProviders(gameDir, mods, modIds, modProviders);
|
||||
}
|
||||
|
||||
public String getArgument(String name) {
|
||||
|
@ -149,20 +148,15 @@ public class FrogLoaderImpl implements FrogLoader {
|
|||
|
||||
@Override
|
||||
public Optional<ModProperties> getModProperties(String id) {
|
||||
return Optional.ofNullable(mods.get(id));
|
||||
}
|
||||
|
||||
private Map<String, ModProperties> collectMods() {
|
||||
return plugins.stream().map(FrogPlugin::getMods).flatMap(Collection::stream)
|
||||
.collect(Collectors.toMap(ModProperties::id, m -> m));
|
||||
return mods.values().stream().flatMap(m -> m.values().stream()).filter(m -> m.id().equals(id)).findFirst();
|
||||
}
|
||||
|
||||
private Collection<String> collectModIds() {
|
||||
return mods.keySet();
|
||||
return mods.values().stream().flatMap(m -> m.keySet().stream()).collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
@Override
|
||||
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++) {
|
||||
Ecorous marked this conversation as resolved
Outdated
kode
commented
wouldn't wouldn't `int[] size = new int[] { providers.size() }` be better as there's no need for thread safety?
Ecorous
commented
I believe this works better - but I wouldn't know as I didn't write it I believe this works better - but I wouldn't know as I didn't write it
Ecorous
commented
The only difference here would be a insignificantly smaller memory footprint - this isn't an issue The only difference here would be a insignificantly smaller memory footprint - this isn't an issue
kode
commented
It's not about performance, i think it's bad practice to use AtomicInteger when you don't need it It's not about performance, i think it's bad practice to use AtomicInteger when you don't need it
Ecorous
commented
Resolved in Resolved in 39ba054fcbb139134081d9bf776e035c4d893182
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,14 +1,16 @@
|
|||
package dev.frogmc.frogloader.impl.gui.component;
|
||||
|
||||
import dev.frogmc.frogloader.impl.mod.ModDependencyResolver;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import javax.swing.*;
|
||||
import javax.swing.plaf.basic.BasicBorders;
|
||||
import java.awt.*;
|
||||
import java.awt.event.ActionListener;
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
|
||||
import dev.frogmc.frogloader.impl.mod.ModDependencyResolver;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
public class DependencyErrorEntry extends JPanel {
|
||||
|
||||
private final JPanel actions;
|
||||
|
@ -28,17 +30,29 @@ public class DependencyErrorEntry extends JPanel {
|
|||
desc.setText("<html>" + description.replace("<", "<").replace("\n", "<br>") + "</html>");
|
||||
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));
|
||||
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) {
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
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 java.awt.*;
|
||||
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 BreakingDepPage(ModDependencyResolver.BreakingModException ex) {
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
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 java.awt.*;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
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 UnfulfilledDepPage(ModDependencyResolver.UnfulfilledDependencyException ex) {
|
||||
|
|
|
@ -9,4 +9,5 @@ public class BuiltinExtensions {
|
|||
public final String PRE_LAUNCH = "prelaunch";
|
||||
public final String ACCESSWIDENER = "accesswidener";
|
||||
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.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.ModProperties;
|
||||
import dev.frogmc.frogloader.api.mod.SemVer;
|
||||
|
@ -229,18 +230,10 @@ public class ModDependencyResolver {
|
|||
throw new IllegalArgumentException(comparator);
|
||||
}
|
||||
var type = DependencyType.of(matcher.group(1));
|
||||
StringBuilder version = new StringBuilder(matcher.group(2));
|
||||
|
||||
while (version.length() < 5) {
|
||||
if (version.charAt(version.length() - 1) == '.') {
|
||||
version.append('0');
|
||||
} else {
|
||||
version.append('.');
|
||||
}
|
||||
}
|
||||
var version = matcher.group(2);
|
||||
|
||||
try {
|
||||
return new Comparator(type, SemVerImpl.parse(version.toString()));
|
||||
return new Comparator(type, SemVerImpl.parse(version));
|
||||
} catch (SemVerParseException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
|
@ -324,6 +317,7 @@ public class ModDependencyResolver {
|
|||
public record VersionRange(Collection<ComparatorSet> sets) {
|
||||
|
||||
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 {
|
||||
|
||||
|
@ -364,13 +358,7 @@ public class ModDependencyResolver {
|
|||
private static void handleTilde(String s, List<String> ranges) throws ResolverException {
|
||||
{
|
||||
StringBuilder builder = new StringBuilder(">=" + s.substring(1));
|
||||
while (builder.length() < 7) {
|
||||
if (builder.charAt(builder.length() - 1) == '.') {
|
||||
builder.append('0');
|
||||
} else {
|
||||
builder.append('.');
|
||||
}
|
||||
}
|
||||
fillVersion(builder);
|
||||
ranges.add(builder.toString());
|
||||
}
|
||||
ranges.add(" ");
|
||||
|
@ -392,13 +380,7 @@ public class ModDependencyResolver {
|
|||
private static void handleXRanges(String s, List<String> ranges) throws ResolverException {
|
||||
{
|
||||
StringBuilder builder = new StringBuilder(">=" + s.replaceAll("[xX*]", "0"));
|
||||
while (builder.length() < 7) {
|
||||
if (builder.charAt(builder.length() - 1) == '.') {
|
||||
builder.append('0');
|
||||
} else {
|
||||
builder.append('.');
|
||||
}
|
||||
}
|
||||
fillVersion(builder);
|
||||
ranges.add(builder.toString());
|
||||
}
|
||||
|
||||
|
@ -423,13 +405,7 @@ public class ModDependencyResolver {
|
|||
break;
|
||||
}
|
||||
}
|
||||
while (builder.length() < 6) {
|
||||
if (builder.charAt(builder.length() - 1) == '.') {
|
||||
builder.append('0');
|
||||
} else {
|
||||
builder.append('.');
|
||||
}
|
||||
}
|
||||
fillVersion(builder);
|
||||
ranges.add(builder.toString());
|
||||
}
|
||||
}
|
||||
|
@ -455,26 +431,21 @@ public class ModDependencyResolver {
|
|||
break;
|
||||
}
|
||||
}
|
||||
while (builder.length() < 6) {
|
||||
fillVersion(builder);
|
||||
/*while (builder.length() < 6) {
|
||||
if (builder.charAt(builder.length() - 1) == '.') {
|
||||
builder.append('0');
|
||||
} else {
|
||||
builder.append('.');
|
||||
}
|
||||
}
|
||||
}*/
|
||||
ranges.add(builder.toString());
|
||||
}
|
||||
|
||||
private static void handleHyphenRange(String s, List<String> ranges, String end) throws ResolverException {
|
||||
{
|
||||
StringBuilder builder = new StringBuilder(">=" + s);
|
||||
while (builder.length() < 7) {
|
||||
if (builder.charAt(builder.length() - 1) == '.') {
|
||||
builder.append('0');
|
||||
} else {
|
||||
builder.append('.');
|
||||
}
|
||||
}
|
||||
fillVersion(builder);
|
||||
ranges.add(builder.toString());
|
||||
ranges.add(" ");
|
||||
}
|
||||
|
@ -498,19 +469,35 @@ public class ModDependencyResolver {
|
|||
break;
|
||||
}
|
||||
}
|
||||
while (builder.length() < 6) {
|
||||
if (builder.charAt(builder.length() - 1) == '.') {
|
||||
builder.append('0');
|
||||
} else {
|
||||
builder.append('.');
|
||||
}
|
||||
}
|
||||
fillVersion(builder);
|
||||
ranges.add(builder.toString());
|
||||
} else {
|
||||
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) {
|
||||
if (!range.contains(" ") && !range.contains(" - ") && !range.contains("||")) {
|
||||
return List.of(range);
|
||||
|
|
|
@ -60,7 +60,7 @@ public class ModUtil {
|
|||
}
|
||||
}
|
||||
} 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;
|
||||
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import dev.frogmc.frogloader.api.mod.SemVer;
|
||||
import dev.frogmc.frogloader.impl.SemVerParseException;
|
||||
import dev.frogmc.frogloader.impl.mod.SemVerImpl;
|
||||
|
@ -7,6 +10,7 @@ import org.jetbrains.annotations.NotNull;
|
|||
|
||||
public class MinecraftSemVerImpl implements SemVer {
|
||||
|
||||
private static final Pattern FILL_PATTERN = Pattern.compile("(\\d+)\\.?(\\d+)?\\.?(\\d+)?");
|
||||
private final String version;
|
||||
|
||||
private MinecraftSemVerImpl(String version) {
|
||||
|
@ -15,12 +19,36 @@ public class MinecraftSemVerImpl implements SemVer {
|
|||
|
||||
static SemVer get(String version) {
|
||||
try {
|
||||
return SemVerImpl.parse(version);
|
||||
StringBuilder builder = new StringBuilder(version);
|
||||
fillVersion(builder);
|
||||
return SemVerImpl.parse(builder.toString());
|
||||
} catch (SemVerParseException e) {
|
||||
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
|
||||
public int major() {
|
||||
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 = [
|
||||
{ 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