Separate plugin types #10

Merged
owlsys merged 14 commits from ecorous/plugin-types into main 2024-06-16 17:13:54 -04:00
36 changed files with 837 additions and 529 deletions

View file

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

View file

@ -7,7 +7,7 @@ plugins {
}
group = "dev.frogmc"
version = "0.0.1-SNAPSHOT"
version = "0.0.1-alpha.1"
repositories {
maven {

View file

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

View file

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

View file

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

I'm asumming this serves as a code example - in which case it should be moved to the documentation

I'm asumming this serves as a code example - in which case it should be moved to the documentation
Review

The entire minecraft project is a test project - this tests that the discovery works properly

The entire `minecraft` project is a test project - this tests that the discovery works properly
Review

Some of this should be in the documentation as well, but this is fine to stay

Some of this should be in the documentation as well, but this is fine to stay
@Override
public String id() {
return "example_mod:example";
}
}

View file

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

Should we allow more than one modprovider per frogmod?

Should we allow more than one modprovider per frogmod?
Review

Already possible

Already possible

View file

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

View file

@ -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);
}
}
@ -116,9 +119,9 @@ public final class ModExtensions {
}
};
if (value instanceof String s){
if (value instanceof String s) {
c.accept(s);
} else if (value instanceof Collection l){
} else if (value instanceof Collection l) {
((Collection<String>) l).forEach(c);
}
}

View file

@ -79,6 +79,7 @@ public interface ModProperties {
/**
* Get this mod's paths
*
* @return Where this mod is loaded from
*/
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();
Ecorous marked this conversation as resolved
Review

Should/could this be changed to a ResourceLocation?

Should/could this be changed to a ResourceLocation?
Review

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

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

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

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

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

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

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

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

View file

@ -14,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);
}
} catch (Throwable e) {
LOGGER.error("Error during plugin initialisation: ", e);
throw new RuntimeException(e);
}
});
if (plugins.isEmpty()) {
throw new IllegalStateException("No plugin applicable to the current state was found!");
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());
}
gamePlugin = plugin;
}
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());
}
}

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

@ -76,7 +76,7 @@ public class LoaderGui extends JFrame {
public static void execUnfulfilledDep(Path reportPath, ModDependencyResolver.UnfulfilledDependencyException ex, boolean keepRunning) {
exec(gui -> {
int count = ex.getDependencies().size();
gui.setHeader("Found " + count + " problem"+(count > 1 ? "s" : ""));
gui.setHeader("Found " + count + " problem" + (count > 1 ? "s" : ""));
gui.addTab("Info", new UnfulfilledDepPage(ex));
addReport(gui, reportPath);
}, keepRunning);
@ -85,7 +85,7 @@ public class LoaderGui extends JFrame {
public static void execBreakingDep(Path reportPath, ModDependencyResolver.BreakingModException ex, boolean keepRunning) {
exec(gui -> {
int count = ex.getBreaks().size();
gui.setHeader("Found " + count + " problem"+(count > 1 ? "s" : ""));
gui.setHeader("Found " + count + " problem" + (count > 1 ? "s" : ""));
gui.addTab("Info", new BreakingDepPage(ex));
addReport(gui, reportPath);
}, keepRunning);

View file

@ -1,50 +1,64 @@
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;
private final JPanel actions;
public DependencyErrorEntry(String description, ModDependencyResolver.VersionRange range, Color background, @Nullable String icon) {
super(new BorderLayout());
public DependencyErrorEntry(String description, ModDependencyResolver.VersionRange range, Color background, @Nullable String icon) {
super(new BorderLayout());
setBorder(BasicBorders.getInternalFrameBorder());
setBorder(BasicBorders.getInternalFrameBorder());
Box text = Box.createVerticalBox();
text.setBorder(BorderFactory.createEmptyBorder());
Box text = Box.createVerticalBox();
text.setBorder(BorderFactory.createEmptyBorder());
JTextPane desc = new JTextPane();
desc.setContentType("text/html");
desc.setEditable(false);
desc.setBackground(background);
desc.setText("<html>" + description.replace("<", "&lt;").replace("\n", "<br>") + "</html>");
text.add(desc);
JTextPane desc = new JTextPane();
desc.setContentType("text/html");
desc.setEditable(false);
desc.setBackground(background);
desc.setText("<html>" + description.replace("<", "&lt;").replace("\n", "<br>") + "</html>");
text.add(desc);
add(text, BorderLayout.NORTH);
Box top = Box.createHorizontalBox();
add(top, BorderLayout.NORTH);
this.actions = new JPanel(new FlowLayout(FlowLayout.LEFT));
add(this.actions, BorderLayout.SOUTH);
if (icon != null) {
URL location = getClass().getResource("/"+icon);
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) {
}
}
}
if (location != null)
add(new JLabel(new ImageIcon(location)), BorderLayout.WEST);
}
}
top.add(text);
public void addAction(String label, ActionListener listener) {
var button = new JButton(label);
button.addActionListener(listener);
this.actions.add(button);
}
this.actions = new JPanel(new FlowLayout(FlowLayout.LEFT));
add(this.actions, BorderLayout.SOUTH);
}
public void addAction(String label, ActionListener listener) {
var button = new JButton(label);
button.addActionListener(listener);
this.actions.add(button);
}
}

View file

@ -1,54 +1,54 @@
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) {
getHorizontalScrollBar().setUnitIncrement(16);
public BreakingDepPage(ModDependencyResolver.BreakingModException ex) {
getHorizontalScrollBar().setUnitIncrement(16);
getVerticalScrollBar().setUnitIncrement(16);
Box list = Box.createVerticalBox();
ex.getBreaks().forEach(entry -> {
String description =
"""
Mod %s (%s) breaks with mod %s (%s) for versions matching range: %s (present: %s)
Suggested Solution: Install %s of Mod %s (%s)
""";
ex.getBreaks().forEach(entry -> {
String description =
"""
Mod %s (%s) breaks with mod %s (%s) for versions matching range: %s (present: %s)
Suggested Solution: Install %s of Mod %s (%s)
""";
description = description.formatted(
entry.source().id(),
entry.source().name(),
entry.broken().id(),
entry.broken().name(),
entry.range().toString(" or "),
entry.broken().version(),
entry.range()
.maxCompatible()
.or(entry.range()::minCompatible)
.map(Objects::toString)
.map(s -> "0.0.0".equals(s) ? "any version" : "version " + s)
.orElse("<unknown>"),
entry.broken().id(),
entry.broken().name()
entry.source().id(),
entry.source().name(),
entry.broken().id(),
entry.broken().name(),
entry.range().toString(" or "),
entry.broken().version(),
entry.range()
.maxCompatible()
.or(entry.range()::minCompatible)
.map(Objects::toString)
.map(s -> "0.0.0".equals(s) ? "any version" : "version " + s)
.orElse("<unknown>"),
entry.broken().id(),
entry.broken().name()
);
DependencyErrorEntry result = new DependencyErrorEntry(
description,
entry.range(),
list.getBackground(),
entry.source().icon()
description,
entry.range(),
list.getBackground(),
entry.source().icon()
);
list.add(result);
});
setViewportView(list);
list.add(result);
});
setViewportView(list);
SwingUtilities.invokeLater(() -> getViewport().setViewPosition(new Point()));
}
}
}

View file

@ -11,24 +11,24 @@ import java.nio.file.Path;
public class ReportPage extends JScrollPane {
public ReportPage(Path reportPath) {
getHorizontalScrollBar().setUnitIncrement(16);
getVerticalScrollBar().setUnitIncrement(16);
public ReportPage(Path reportPath) {
getHorizontalScrollBar().setUnitIncrement(16);
getVerticalScrollBar().setUnitIncrement(16);
JTextArea text = new JTextArea();
text.setEditable(false);
text.setTabSize(2);
try {
text.setText(Files.readString(reportPath, StandardCharsets.UTF_8));
} catch (IOException e) {
StringWriter writer = new StringWriter();
PrintWriter printer = new PrintWriter(writer);
printer.printf("Could not load contents of %s:%n", reportPath);
e.printStackTrace(printer);
}
text.setBorder(BorderFactory.createEmptyBorder(0, 8, 0, 8));
setViewportView(text);
SwingUtilities.invokeLater(() -> getViewport().setViewPosition(new Point(0, 0)));
}
JTextArea text = new JTextArea();
text.setEditable(false);
text.setTabSize(2);
try {
text.setText(Files.readString(reportPath, StandardCharsets.UTF_8));
} catch (IOException e) {
StringWriter writer = new StringWriter();
PrintWriter printer = new PrintWriter(writer);
printer.printf("Could not load contents of %s:%n", reportPath);
e.printStackTrace(printer);
}
text.setBorder(BorderFactory.createEmptyBorder(0, 8, 0, 8));
setViewportView(text);
SwingUtilities.invokeLater(() -> getViewport().setViewPosition(new Point(0, 0)));
}
}

View file

@ -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) {
@ -39,13 +39,13 @@ public class UnfulfilledDepPage extends JScrollPane {
}
description.append("\nSuggested Solution: Install ")
.append(
entry
.range()
.maxCompatible()
.or(entry.range()::minCompatible)
.map(Objects::toString)
.map(s -> "0.0.0".equals(s) ? "any version" : "version " + s)
.orElse("<unknown>")
entry
.range()
.maxCompatible()
.or(entry.range()::minCompatible)
.map(Objects::toString)
.map(s -> "0.0.0".equals(s) ? "any version" : "version " + s)
.orElse("<unknown>")
)
.append(" of ");
if (entry.dependencyName() != null) {
@ -55,10 +55,10 @@ public class UnfulfilledDepPage extends JScrollPane {
}
DependencyErrorEntry result = new DependencyErrorEntry(
description.toString(),
entry.range(),
list.getBackground(),
entry.source().icon()
description.toString(),
entry.range(),
list.getBackground(),
entry.source().icon()
);
if (entry.link() != null) {

View file

@ -58,15 +58,15 @@ public class FrogLauncher {
new FrogLauncher(args, env);
}
public void putProperty(IPropertyKey key, Object value){
public void putProperty(IPropertyKey key, Object value) {
globalProperties.put(key, value);
}
public Object getProperty(IPropertyKey key){
public Object getProperty(IPropertyKey key) {
return globalProperties.get(key);
}
public Object getProperty(IPropertyKey key, Object defaultValue){
public Object getProperty(IPropertyKey key, Object defaultValue) {
return globalProperties.getOrDefault(key, defaultValue);
}
}

View file

@ -14,15 +14,15 @@ public class FrogGlobalPropertyService implements IGlobalPropertyService {
}
@Override
public boolean equals(Object other){
if (other instanceof IPropertyKey k){
public boolean equals(Object other) {
if (other instanceof IPropertyKey k) {
return name.equals(k.toString());
}
return false;
}
@Override
public int hashCode(){
public int hashCode() {
return name.hashCode();
}
};

View file

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

View file

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

View file

@ -49,8 +49,8 @@ public class ModPropertiesReader {
CommentedConfig props = PARSER.parse(in);
String url = in.toString();
Path source = Path.of(url.substring(url.lastIndexOf(":")+1).split("!")[0]).toAbsolutePath();
if (!source.getFileName().toString().endsWith(".jar")){
Path source = Path.of(url.substring(url.lastIndexOf(":") + 1).split("!")[0]).toAbsolutePath();
if (!source.getFileName().toString().endsWith(".jar")) {
source = source.getParent();
} else {
// TODO will this result in a memory leak?
@ -96,7 +96,7 @@ public class ModPropertiesReader {
if (version == null || version.isEmpty())
badProperties.add("frog.mod.version");
else {
try {
try {
semVer = SemVerImpl.parse(version);
} catch (SemVerParseException e) {
badProperties.add("frog.mod.version");
@ -186,11 +186,11 @@ public class ModPropertiesReader {
public InvalidModPropertiesException(String id, Collection<Path> sources, Collection<String> invalid) {
super(
"Invalid properties for %s (%s) - invalid or missing values for: %s".formatted(
Objects.requireNonNullElse(id, "<unknown>"),
sources.stream().map(Path::toString).collect(Collectors.joining(", ")),
String.join(", ", invalid)
)
"Invalid properties for %s (%s) - invalid or missing values for: %s".formatted(
Objects.requireNonNullElse(id, "<unknown>"),
sources.stream().map(Path::toString).collect(Collectors.joining(", ")),
String.join(", ", invalid)
)
);
this.invalid = invalid;
}

View file

@ -60,7 +60,7 @@ public class ModUtil {
}
}
} 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;
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");

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

@ -41,8 +41,8 @@ public class PlatformUtil {
ProcessBuilder builder = new ProcessBuilder("bash", "-c", "wl-copy < " + path);
builder.start();
} else {
String data = Files.readString(path, StandardCharsets.UTF_8);
Toolkit.getDefaultToolkit().getSystemClipboard().setContents(new StringSelection(data), null);
String data = Files.readString(path, StandardCharsets.UTF_8);
Toolkit.getDefaultToolkit().getSystemClipboard().setContents(new StringSelection(data), null);
}
} catch (IOException e) {
LOGGER.error("Failed to copy contents of {}:", path, e);

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 = [
{ 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