Compare commits

...

15 commits

Author SHA1 Message Date
owlsys 06b28c6e16 Merge pull request 'Separate plugin types' (#10) from ecorous/plugin-types into main
Some checks failed
Publish to snapshot maven / build (push) Failing after 38s
Reviewed-on: #10
Reviewed-by: ender <ender@noreply.localhost>
2024-06-16 17:13:54 -04:00
moehreag 39ba054fcb replace AtomicInteger usage with single-element array 2024-06-16 22:45:37 +02:00
moehreag b6d904595a add icon, fix icon positioning 2024-06-16 19:24:34 +02:00
moehreag a3960b933f update dependencies 2024-06-16 14:54:51 +02:00
moehreag 565e4df74e add ability for mods to provide further mod providers 2024-06-16 12:25:35 +02:00
moehreag 6dd1bf04ed add missing stacktrace arg to meta update command 2024-06-15 22:48:10 +02:00
moehreag d58867a92e rename files, add javadocs, remove unused class 2024-06-15 21:13:18 +02:00
moehreag 0476adadd8 bump version, add meta secret to action 2024-06-15 20:40:45 +02:00
moehreag a2d9ac045c Merge branch 'refs/heads/main' into ecorous/plugin-types
# Conflicts:
#	minecraft/build.gradle.kts
2024-06-15 20:35:07 +02:00
moehreag f0f742925b fix dependency resolving, fix minecraft version parsing
(since 1.21 isn't valid semver but we can expand it)
2024-06-15 19:59:43 +02:00
moehreag a3aab3d87c clean up a bit, update example mod to 1.21 2024-06-15 16:58:11 +02:00
moehreag e9c8ecaaac fix loading (restore previous functionality) 2024-06-15 15:16:47 +02:00
moehreag e222633c13 format 2024-06-15 13:17:51 +02:00
Ecorous e352bb835f
progress 2024-06-14 14:58:33 +01:00
Ecorous f6715e36a2
initial work on separating plugin types 2024-06-13 22:08:43 +01:00
36 changed files with 837 additions and 529 deletions

View file

@ -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

View file

@ -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 {

View file

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

View file

@ -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 {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,85 @@
package dev.frogmc.frogloader.api.plugin;
import java.net.URL;
import java.nio.file.Path;
import java.util.Collection;
import java.util.Collections;
import java.util.function.Consumer;
import dev.frogmc.frogloader.api.mod.ModProperties;
import dev.frogmc.frogloader.api.FrogLoader;
/**
* A provider for mods to be loaded.
* @see FrogLoader
* @see GamePlugin
* @see ModProperties
*/
public interface ModProvider {
/**
* Get the ID of this provider. Mods will be stored separated by their provider ID.
* @return The ID of this provider
*/
String id();
/**
* The directory to search in for mods. This will be resolved relative to the game directory.
* @return The name of the mods directory, relative to the game directory
*/
default String loadDirectory() {
return "mods";
}
/**
* Whether this provider is applicable to load in the current environment.
* @return Whether this provider is applicable to load
*/
default boolean isApplicable() {
return false;
}
/**
* Whether a mod file is valid to be loaded by this provider.
* @param path The path to validate
* @return Whether this path is valid to be loaded by this provider
* @see #isDirectoryApplicable(Path)
* @see #loadDirectory()
*/
default boolean isFileApplicable(Path path) {
return false;
}
/**
* Whether a directory (inside the path declared by this provider's mod directory) is valid to be queried for mods
* to be loaded by this provider.
* @param path The directory to be validated
* @return Whether this directory should be traversed further
* @see #loadDirectory()
* @see #isFileApplicable(Path)
*/
default boolean isDirectoryApplicable(Path path) {
return false;
}
/**
* This method will be invoked just before the game is launched. It may be used for a pre-launch entrypoint for mods.
* @param mods The mods loaded by this provider
*/
default void preLaunch(Collection<ModProperties> mods) {
}
/**
* This method should load the mods found previously into their runtime representation.
* This method should also load any nested mods
* <p>Note: This method also needs to add mods to the classpath for them to be loaded correctly. For this, a parameter is provided.</p>
*
* @param modFiles The paths to be loaded by this provider
* @param classpathAdder A consumer to easily add URLs to the classpath used to run the game on
* @return Mods loaded by this provider
* @throws Exception If an exception occurs during loading. It will be handled by the loader.
*/
default Collection<ModProperties> loadMods(Collection<Path> modFiles, Consumer<URL> classpathAdder) throws Exception {
return Collections.emptySet();
}
}

View file

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

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

View file

@ -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("<", "&lt;").replace("\n", "<br>") + "</html>"); desc.setText("<html>" + description.replace("<", "&lt;").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) {

View file

@ -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) {

View file

@ -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) {

View file

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

View file

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

View file

@ -60,7 +60,7 @@ public class ModUtil {
} }
} }
} else { } else {
children.computeIfAbsent(mod, m -> new HashSet<>()); children.putIfAbsent(mod, Collections.emptySet());
} }
} }

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1 +0,0 @@
dev.frogmc.frogloader.impl.plugin.game.minecraft.Minecraft

View file

@ -0,0 +1 @@
dev.frogmc.frogloader.impl.plugin.game.minecraft.MinecraftGamePlugin

View file

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

View file

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB