Separate plugin types #10

Merged
owlsys merged 14 commits from ecorous/plugin-types into main 2024-06-16 17:13:54 -04:00
18 changed files with 451 additions and 451 deletions
Showing only changes of commit e222633c13 - Show all commits

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

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

@ -3,13 +3,13 @@ package dev.frogmc.frogloader.api.plugin;
import dev.frogmc.frogloader.api.FrogLoader; import dev.frogmc.frogloader.api.FrogLoader;
public interface FrogGamePlugin { public interface FrogGamePlugin {
default void run() { default void run() {
} }
default boolean isApplicable() { default boolean isApplicable() {
return false; return false;
} }
default void init(FrogLoader loader) throws Exception { default void init(FrogLoader loader) throws Exception {
} }
} }

View file

@ -1,28 +1,29 @@
package dev.frogmc.frogloader.api.plugin; package dev.frogmc.frogloader.api.plugin;
import dev.frogmc.frogloader.api.mod.ModProperties;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.Collection; import java.util.Collection;
import dev.frogmc.frogloader.api.mod.ModProperties;
public interface FrogModProvider { public interface FrogModProvider {
String id(); String id();
default String loadDirectory() { default String loadDirectory() {
return "mods"; return "mods";
} }
default boolean isApplicable() { default boolean isApplicable() {
return false; return false;
} }
default boolean isFileApplicable(Path path) { default boolean isFileApplicable(Path path) {
return false; return false;
} }
default void initMods(Collection<ModProperties> mods) {}; default void initMods(Collection<ModProperties> mods) {
}
default ModProperties loadMod(Path path) { default ModProperties loadMod(Path path) {
return null; return null;
} }
} }

View file

@ -2,7 +2,6 @@ package dev.frogmc.frogloader.api.plugin;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.List;
import dev.frogmc.frogloader.api.FrogLoader; import dev.frogmc.frogloader.api.FrogLoader;
import dev.frogmc.frogloader.api.mod.ModProperties; import dev.frogmc.frogloader.api.mod.ModProperties;

View file

@ -1,22 +1,5 @@
package dev.frogmc.frogloader.impl; package dev.frogmc.frogloader.impl;
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.FrogGamePlugin;
import dev.frogmc.frogloader.api.plugin.FrogModProvider;
import dev.frogmc.frogloader.api.plugin.FrogPlugin;
import dev.frogmc.frogloader.impl.gui.LoaderGui;
import dev.frogmc.frogloader.impl.launch.MixinClassLoader;
import dev.frogmc.frogloader.impl.mod.ModUtil;
import dev.frogmc.frogloader.impl.util.CrashReportGenerator;
import dev.frogmc.frogloader.impl.util.SystemProperties;
import lombok.Getter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.spongepowered.asm.mixin.MixinEnvironment;
import java.io.IOException; import java.io.IOException;
import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodHandles;
@ -27,145 +10,161 @@ import java.nio.file.Paths;
import java.util.*; import java.util.*;
import java.util.stream.Collectors; import java.util.stream.Collectors;
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.FrogGamePlugin;
import dev.frogmc.frogloader.api.plugin.FrogModProvider;
import dev.frogmc.frogloader.impl.gui.LoaderGui;
import dev.frogmc.frogloader.impl.launch.MixinClassLoader;
import dev.frogmc.frogloader.impl.mod.ModUtil;
import dev.frogmc.frogloader.impl.util.CrashReportGenerator;
import dev.frogmc.frogloader.impl.util.SystemProperties;
import lombok.Getter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.spongepowered.asm.mixin.MixinEnvironment;
public class FrogLoaderImpl implements FrogLoader { public class FrogLoaderImpl implements FrogLoader {
public static final String MOD_FILE_EXTENSION = ".frogmod"; 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);
@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"); private final Logger LOGGER = LoggerFactory.getLogger("FrogLoader");
// @Getter // @Getter
// private final List<FrogPlugin> plugins = new ArrayList<>(); // private final List<FrogPlugin> plugins = new ArrayList<>();
@Getter @Getter
private final Collection<FrogGamePlugin> gamePlugins = new ArrayList<>(); private final Collection<FrogGamePlugin> gamePlugins = new ArrayList<>();
@Getter @Getter
private final Collection<FrogModProvider> modProviders = new ArrayList<>(); private final Collection<FrogModProvider> modProviders = new ArrayList<>();
@Getter @Getter
private final Path gameDir, configDir, modsDir; private final Path gameDir, configDir, modsDir;
@Getter @Getter
private final MixinClassLoader classloader; private final MixinClassLoader classloader;
@Getter @Getter
private final Gson gson = new Gson(); private final Gson gson = new Gson();
// Map<Provider ID, Map<Mod ID, ModProperties>> // Map<Provider ID, Map<Mod ID, ModProperties>>
private Map<String, Map<String, ModProperties>> mods = new HashMap<>(); private final Map<String, Map<String, ModProperties>> mods = new HashMap<>();
// private Map<String, ModProperties> mods; // private Map<String, ModProperties> mods;
private Collection<String> modIds = new ArrayList<>(); private Collection<String> modIds = new ArrayList<>();
private FrogLoaderImpl(String[] args, Env env) { private FrogLoaderImpl(String[] args, Env env) {
instance = this; instance = this;
this.classloader = (MixinClassLoader) this.getClass().getClassLoader(); this.classloader = (MixinClassLoader) this.getClass().getClassLoader();
this.args = args; this.args = args;
this.env = env; this.env = env;
gameDir = Paths.get(getArgumentOrElse("gameDir", ".")); gameDir = Paths.get(getArgumentOrElse("gameDir", "."));
configDir = gameDir.resolve("config"); configDir = gameDir.resolve("config");
modsDir = gameDir.resolve("mods"); modsDir = gameDir.resolve("mods");
try { try {
Files.createDirectories(gameDir); Files.createDirectories(gameDir);
Files.createDirectories(configDir); Files.createDirectories(configDir);
Files.createDirectories(modsDir); 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 {
discoverGamePlugins(); discoverGamePlugins();
discoverModProviders(); discoverModProviders();
advanceMixinState(); advanceMixinState();
modIds = collectModIds(); modIds = collectModIds();
LOGGER.info(ModUtil.getModList(getMods())); LOGGER.info(ModUtil.getModList(getMods()));
LOGGER.info("Launching..."); LOGGER.info("Launching...");
gamePlugins.forEach(FrogGamePlugin::run); gamePlugins.forEach(FrogGamePlugin::run);
modProviders.forEach(m -> m.initMods(mods.get(m.id()).values())); modProviders.forEach(m -> m.initMods(mods.get(m.id()).values()));
} catch (Throwable t) { } catch (Throwable t) {
LoaderGui.execReport(CrashReportGenerator.writeReport(t, getMods()), false); LoaderGui.execReport(CrashReportGenerator.writeReport(t, getMods()), false);
} }
} }
@SuppressWarnings("unused") @SuppressWarnings("unused")
public static void run(String[] args, Env env) { public static void run(String[] args, Env env) {
if (instance != null) { if (instance != null) {
throw new IllegalStateException("Loader was started multiple times!"); throw new IllegalStateException("Loader was started multiple times!");
} }
new FrogLoaderImpl(args, env); new FrogLoaderImpl(args, env);
} }
private void advanceMixinState() { private void advanceMixinState() {
try { try {
MethodHandle m = MethodHandles.privateLookupIn(MixinEnvironment.class, MethodHandles.lookup()).findStatic(MixinEnvironment.class, "gotoPhase", MethodType.methodType(void.class, MixinEnvironment.Phase.class)); MethodHandle m = MethodHandles.privateLookupIn(MixinEnvironment.class, MethodHandles.lookup()).findStatic(MixinEnvironment.class, "gotoPhase", MethodType.methodType(void.class, MixinEnvironment.Phase.class));
m.invoke(MixinEnvironment.Phase.INIT); m.invoke(MixinEnvironment.Phase.INIT);
m.invoke(MixinEnvironment.Phase.DEFAULT); m.invoke(MixinEnvironment.Phase.DEFAULT);
} catch (Throwable e) { } catch (Throwable e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} }
} }
private void discoverModProviders() { private void discoverModProviders() {
LOGGER.info("Discovering mod providers..."); LOGGER.info("Discovering mod providers...");
ServiceLoader<FrogModProvider> loader = ServiceLoader.load(FrogModProvider.class); ServiceLoader<FrogModProvider> loader = ServiceLoader.load(FrogModProvider.class);
loader.stream().map(ServiceLoader.Provider::get).forEach(p -> LOGGER.info("Found mod provider: " + p.getClass().getName())); loader.stream().map(ServiceLoader.Provider::get).forEach(p -> LOGGER.info("Found mod provider: " + p.getClass().getName()));
FrogModProvider[] applicableProviders = ServiceLoader.load(FrogModProvider.class).stream().map(ServiceLoader.Provider::get).filter(FrogModProvider::isApplicable).toArray(FrogModProvider[]::new); FrogModProvider[] applicableProviders = ServiceLoader.load(FrogModProvider.class).stream().map(ServiceLoader.Provider::get).filter(FrogModProvider::isApplicable).toArray(FrogModProvider[]::new);
for (FrogModProvider plugin : applicableProviders) { for (FrogModProvider plugin : applicableProviders) {
try { try {
LOGGER.info("Initialising mod provider: " + plugin.id()); LOGGER.info("Initialising mod provider: " + plugin.id());
Map<String, ModProperties> modsFromProvider = new HashMap<>(); Map<String, ModProperties> modsFromProvider = new HashMap<>();
Collection<Path> paths = Discovery.find(gameDir.resolve(plugin.loadDirectory()), p -> false, plugin::isFileApplicable); Collection<Path> paths = Discovery.find(gameDir.resolve(plugin.loadDirectory()), p -> false, plugin::isFileApplicable);
paths.forEach(p -> { paths.forEach(p -> {
LOGGER.info("Loading mod: " + p); LOGGER.info("Loading mod: " + p);
try { try {
ModProperties mod = plugin.loadMod(p); ModProperties mod = plugin.loadMod(p);
modsFromProvider.put(mod.id(), mod); modsFromProvider.put(mod.id(), mod);
} catch (Throwable e) { } catch (Throwable e) {
LOGGER.error("Error during mod initialisation: ", e); LOGGER.error("Error during mod initialisation: ", e);
throw new RuntimeException(e); throw new RuntimeException(e);
} }
}); });
LOGGER.info("Loaded " + modsFromProvider.size() + " mods from provider: " + plugin.id()); LOGGER.info("Loaded " + modsFromProvider.size() + " mods from provider: " + plugin.id());
mods.put(plugin.id(), modsFromProvider); mods.put(plugin.id(), modsFromProvider);
modIds.addAll(modsFromProvider.keySet()); modIds.addAll(modsFromProvider.keySet());
modProviders.add(plugin); modProviders.add(plugin);
} catch (Throwable e) { } catch (Throwable e) {
LOGGER.error("Error during plugin initialisation: ", e); LOGGER.error("Error during plugin initialisation: ", e);
throw new RuntimeException(e); throw new RuntimeException(e);
} }
} }
} }
private void discoverGamePlugins() { private void discoverGamePlugins() {
LOGGER.info("Discovering game plugins..."); LOGGER.info("Discovering game plugins...");
ServiceLoader<FrogGamePlugin> loader = ServiceLoader.load(FrogGamePlugin.class); ServiceLoader<FrogGamePlugin> loader = ServiceLoader.load(FrogGamePlugin.class);
loader.stream().map(ServiceLoader.Provider::get).forEach(p -> LOGGER.info("Found game plugin: " + p.getClass().getName())); loader.stream().map(ServiceLoader.Provider::get).forEach(p -> LOGGER.info("Found game plugin: " + p.getClass().getName()));
FrogGamePlugin[] applicablePlugins = ServiceLoader.load(FrogGamePlugin.class).stream().map(ServiceLoader.Provider::get).filter(FrogGamePlugin::isApplicable).toArray(FrogGamePlugin[]::new); FrogGamePlugin[] applicablePlugins = ServiceLoader.load(FrogGamePlugin.class).stream().map(ServiceLoader.Provider::get).filter(FrogGamePlugin::isApplicable).toArray(FrogGamePlugin[]::new);
if (applicablePlugins.length > 1) { if (applicablePlugins.length > 1) {
throw new IllegalStateException("Multiple applicable game plugins found!"); throw new IllegalStateException("Multiple applicable game plugins found!");
} else if (applicablePlugins.length == 0) { } else if (applicablePlugins.length == 0) {
throw new IllegalStateException("No applicable game plugin found!"); throw new IllegalStateException("No applicable game plugin found!");
} }
for (FrogGamePlugin plugin : applicablePlugins) { for (FrogGamePlugin plugin : applicablePlugins) {
try { try {
plugin.init(this); plugin.init(this);
gamePlugins.add(plugin); gamePlugins.add(plugin);
} catch (Throwable e) { } catch (Throwable e) {
LOGGER.error("Error during plugin initialisation: ", e); LOGGER.error("Error during plugin initialisation: ", e);
throw new RuntimeException(e); throw new RuntimeException(e);
} }
} }
} }
/*private void discoverPlugins() { /*private void discoverPlugins() {
@ -186,48 +185,48 @@ public class FrogLoaderImpl implements FrogLoader {
} }
}*/ }*/
public String getArgument(String name) { public String getArgument(String name) {
for (int i = 0; i < args.length - 1; i += 2) { for (int i = 0; i < args.length - 1; i += 2) {
if (args[i].equals("--" + name)) { if (args[i].equals("--" + name)) {
return args[i + 1]; return args[i + 1];
} }
} }
return ""; return "";
} }
public String getArgumentOrElse(String name, String other) { public String getArgumentOrElse(String name, String other) {
String res = getArgument(name); String res = getArgument(name);
if (res.isEmpty()) { if (res.isEmpty()) {
return other; return other;
} }
return res; return res;
} }
@Override @Override
public boolean isDevelopment() { public boolean isDevelopment() {
return DEV_ENV; return DEV_ENV;
} }
@Override @Override
public boolean isModLoaded(String id) { public boolean isModLoaded(String id) {
return modIds.contains(id); return modIds.contains(id);
} }
@Override @Override
public Optional<ModProperties> getModProperties(String id) { public Optional<ModProperties> getModProperties(String id) {
return mods.values().stream().flatMap(m -> m.values().stream()).filter(m -> m.id().equals(id)).findFirst(); return mods.values().stream().flatMap(m -> m.values().stream()).filter(m -> m.id().equals(id)).findFirst();
} }
/*private Map<String, ModProperties> collectMods() { /*private Map<String, ModProperties> collectMods() {
return plugins.stream().map(FrogPlugin::getMods).flatMap(Collection::stream).collect(Collectors.toMap(ModProperties::id, m -> m)); 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.values().stream().flatMap(m -> m.keySet().stream()).collect(Collectors.toSet()); 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().stream().map(Map::values).flatMap(Collection::stream).collect(Collectors.toSet()); return mods.values().stream().map(Map::values).flatMap(Collection::stream).collect(Collectors.toSet());
} }
} }

View file

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

View file

@ -1,50 +1,50 @@
package dev.frogmc.frogloader.impl.gui.component; package dev.frogmc.frogloader.impl.gui.component;
import dev.frogmc.frogloader.impl.mod.ModDependencyResolver;
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.net.URL; import java.net.URL;
import dev.frogmc.frogloader.impl.mod.ModDependencyResolver;
import org.jetbrains.annotations.Nullable;
public class DependencyErrorEntry extends JPanel { public class DependencyErrorEntry extends JPanel {
private final JPanel actions; private final JPanel actions;
public DependencyErrorEntry(String description, ModDependencyResolver.VersionRange range, Color background, @Nullable String icon) { public DependencyErrorEntry(String description, ModDependencyResolver.VersionRange range, Color background, @Nullable String icon) {
super(new BorderLayout()); super(new BorderLayout());
setBorder(BasicBorders.getInternalFrameBorder()); setBorder(BasicBorders.getInternalFrameBorder());
Box text = Box.createVerticalBox(); Box text = Box.createVerticalBox();
text.setBorder(BorderFactory.createEmptyBorder()); text.setBorder(BorderFactory.createEmptyBorder());
JTextPane desc = new JTextPane(); JTextPane desc = new JTextPane();
desc.setContentType("text/html"); desc.setContentType("text/html");
desc.setEditable(false); desc.setEditable(false);
desc.setBackground(background); desc.setBackground(background);
desc.setText("<html>" + description.replace("<", "&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); add(text, BorderLayout.NORTH);
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) { if (icon != null) {
URL location = getClass().getResource(icon); URL location = getClass().getResource(icon);
if (location != null) if (location != null)
add(new JLabel(new ImageIcon(location)), BorderLayout.WEST); add(new JLabel(new ImageIcon(location)), BorderLayout.WEST);
} }
} }
public void addAction(String label, ActionListener listener) { public void addAction(String label, ActionListener listener) {
var button = new JButton(label); var button = new JButton(label);
button.addActionListener(listener); button.addActionListener(listener);
this.actions.add(button); this.actions.add(button);
} }
} }

View file

@ -1,54 +1,54 @@
package dev.frogmc.frogloader.impl.gui.page; package dev.frogmc.frogloader.impl.gui.page;
import dev.frogmc.frogloader.impl.gui.component.DependencyErrorEntry;
import dev.frogmc.frogloader.impl.mod.ModDependencyResolver;
import javax.swing.*; import javax.swing.*;
import java.awt.*; import java.awt.*;
import java.util.Objects; import java.util.Objects;
import dev.frogmc.frogloader.impl.gui.component.DependencyErrorEntry;
import dev.frogmc.frogloader.impl.mod.ModDependencyResolver;
public class BreakingDepPage extends JScrollPane { public class BreakingDepPage extends JScrollPane {
public BreakingDepPage(ModDependencyResolver.BreakingModException ex) { public BreakingDepPage(ModDependencyResolver.BreakingModException ex) {
getHorizontalScrollBar().setUnitIncrement(16); getHorizontalScrollBar().setUnitIncrement(16);
getVerticalScrollBar().setUnitIncrement(16); getVerticalScrollBar().setUnitIncrement(16);
Box list = Box.createVerticalBox(); Box list = Box.createVerticalBox();
ex.getBreaks().forEach(entry -> { ex.getBreaks().forEach(entry -> {
String description = String description =
""" """
Mod %s (%s) breaks with mod %s (%s) for versions matching range: %s (present: %s) Mod %s (%s) breaks with mod %s (%s) for versions matching range: %s (present: %s)
Suggested Solution: Install %s of Mod %s (%s) Suggested Solution: Install %s of Mod %s (%s)
"""; """;
description = description.formatted( description = description.formatted(
entry.source().id(), entry.source().id(),
entry.source().name(), entry.source().name(),
entry.broken().id(), entry.broken().id(),
entry.broken().name(), entry.broken().name(),
entry.range().toString(" or "), entry.range().toString(" or "),
entry.broken().version(), entry.broken().version(),
entry.range() entry.range()
.maxCompatible() .maxCompatible()
.or(entry.range()::minCompatible) .or(entry.range()::minCompatible)
.map(Objects::toString) .map(Objects::toString)
.map(s -> "0.0.0".equals(s) ? "any version" : "version " + s) .map(s -> "0.0.0".equals(s) ? "any version" : "version " + s)
.orElse("<unknown>"), .orElse("<unknown>"),
entry.broken().id(), entry.broken().id(),
entry.broken().name() entry.broken().name()
); );
DependencyErrorEntry result = new DependencyErrorEntry( DependencyErrorEntry result = new DependencyErrorEntry(
description, description,
entry.range(), entry.range(),
list.getBackground(), list.getBackground(),
entry.source().icon() entry.source().icon()
); );
list.add(result); list.add(result);
}); });
setViewportView(list); setViewportView(list);
SwingUtilities.invokeLater(() -> getViewport().setViewPosition(new Point())); SwingUtilities.invokeLater(() -> getViewport().setViewPosition(new Point()));
} }
} }

View file

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

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

View file

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

View file

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

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;

View file

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

View file

@ -1,5 +1,12 @@
package dev.frogmc.frogloader.impl.plugin.game.minecraft; 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 com.google.gson.JsonObject; import com.google.gson.JsonObject;
import dev.frogmc.frogloader.api.FrogLoader; import dev.frogmc.frogloader.api.FrogLoader;
import dev.frogmc.frogloader.api.plugin.FrogGamePlugin; import dev.frogmc.frogloader.api.plugin.FrogGamePlugin;
@ -9,113 +16,106 @@ import dev.frogmc.thyroxine.Thyroxine;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
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.*;
public class MinecraftGamePlugin implements FrogGamePlugin { public class MinecraftGamePlugin implements FrogGamePlugin {
protected final String[] MINECRAFT_CLASSES = new String[]{ protected final String[] MINECRAFT_CLASSES = new String[]{
"net/minecraft/client/main/Main.class", "net/minecraft/client/main/Main.class",
"net/minecraft/client/MinecraftApplet.class", "net/minecraft/client/MinecraftApplet.class",
"net/minecraft/server/Main.class" "net/minecraft/server/Main.class"
}; };
private static final Logger LOGGER = LoggerFactory.getLogger("Plugin/Minecraft"); private static final Logger LOGGER = LoggerFactory.getLogger("Plugin/Minecraft");
protected Path gamePath; protected Path gamePath;
protected String foundMainClass; protected String foundMainClass;
private String version; private String version;
protected boolean checkLocation(Path jar) { protected boolean checkLocation(Path jar) {
if (!Files.exists(jar) || Files.isDirectory(jar)) { if (!Files.exists(jar) || Files.isDirectory(jar)) {
return false; return false;
} }
try (FileSystem fs = FileSystems.newFileSystem(jar)) { try (FileSystem fs = FileSystems.newFileSystem(jar)) {
for (String n : MINECRAFT_CLASSES) { for (String n : MINECRAFT_CLASSES) {
if (Files.exists(fs.getPath(n)) && n.contains(FrogLoaderImpl.getInstance().getEnv().getIdentifier())) { if (Files.exists(fs.getPath(n)) && n.contains(FrogLoaderImpl.getInstance().getEnv().getIdentifier())) {
LOGGER.info("Found game: {}", jar); LOGGER.info("Found game: {}", jar);
foundMainClass = n.substring(0, n.length() - 6).replace("/", "."); foundMainClass = n.substring(0, n.length() - 6).replace("/", ".");
try { try {
version = FrogLoaderImpl.getInstance().getGson().fromJson(Files.readString(fs.getPath("version.json")), JsonObject.class).get("id").getAsString(); version = FrogLoaderImpl.getInstance().getGson().fromJson(Files.readString(fs.getPath("version.json")), JsonObject.class).get("id").getAsString();
} catch (Exception e){ } catch (Exception e) {
version = FrogLoaderImpl.getInstance().getArgument("version"); version = FrogLoaderImpl.getInstance().getArgument("version");
} }
return true; return true;
} }
} }
} catch (IOException e) { } catch (IOException e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} }
return false; return false;
} }
protected Path findGame() { protected Path findGame() {
LOGGER.info("Locating game.."); LOGGER.info("Locating game..");
String jar = System.getProperty(SystemProperties.MINECRAFT_GAME_JAR); String jar = System.getProperty(SystemProperties.MINECRAFT_GAME_JAR);
if (jar != null) { if (jar != null) {
Path p = Paths.get(jar); Path p = Paths.get(jar);
if (checkLocation(p)) { if (checkLocation(p)) {
return p; return p;
} }
} }
for (String s : System.getProperty("java.class.path", "").split(File.pathSeparator)) { for (String s : System.getProperty("java.class.path", "").split(File.pathSeparator)) {
Path p = Paths.get(s); Path p = Paths.get(s);
if (checkLocation(p)) { if (checkLocation(p)) {
return p; return p;
} }
} }
LOGGER.warn("Could not locate game!"); LOGGER.warn("Could not locate game!");
return null; return null;
} }
@Override @Override
public boolean isApplicable() { public boolean isApplicable() {
gamePath = findGame(); gamePath = findGame();
return gamePath != null; return gamePath != null;
} }
@Override @Override
public void run() { public void run() {
try { try {
if (foundMainClass != null) { if (foundMainClass != null) {
LOGGER.info("Launching main class: {}", foundMainClass); LOGGER.info("Launching main class: {}", foundMainClass);
Class<?> mainClass = Class.forName(foundMainClass); Class<?> mainClass = Class.forName(foundMainClass);
MethodHandle main = MethodHandles.publicLookup().findStatic(mainClass, "main", MethodType.methodType(void.class, String[].class)); MethodHandle main = MethodHandles.publicLookup().findStatic(mainClass, "main", MethodType.methodType(void.class, String[].class));
main.invoke((Object) FrogLoaderImpl.getInstance().getArgs()); main.invoke((Object) FrogLoaderImpl.getInstance().getArgs());
} else { } else {
LOGGER.warn("Failed to locate main class!"); LOGGER.warn("Failed to locate main class!");
} }
} catch (Throwable e) { } catch (Throwable e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} }
} }
@Override @Override
public void init(FrogLoader loader) throws Exception { public void init(FrogLoader loader) throws Exception {
if (gamePath == null) { if (gamePath == null) {
throw new IllegalStateException("Game not found!"); throw new IllegalStateException("Game not found!");
} }
Path remappedGamePath = loader.getGameDir().resolve(".frogmc/remappedJars").resolve(version).resolve("game-" + version + "-remapped.jar"); Path remappedGamePath = loader.getGameDir().resolve(".frogmc/remappedJars").resolve(version).resolve("game-" + version + "-remapped.jar");
if (!Files.exists(remappedGamePath.getParent())) { if (!Files.exists(remappedGamePath.getParent())) {
try { try {
Files.createDirectories(remappedGamePath.getParent()); Files.createDirectories(remappedGamePath.getParent());
} catch (IOException e) { } catch (IOException e) {
LOGGER.error("Failed to create directory", e); LOGGER.error("Failed to create directory", e);
} }
} }
if (!loader.isDevelopment()) { if (!loader.isDevelopment()) {
if (!Files.exists(remappedGamePath)) { if (!Files.exists(remappedGamePath)) {
Thyroxine.run(version, gamePath, remappedGamePath, true, false); Thyroxine.run(version, gamePath, remappedGamePath, true, false);
} }
} }
var runtimePath = loader.isDevelopment() ? gamePath : remappedGamePath; var runtimePath = loader.isDevelopment() ? gamePath : remappedGamePath;
FrogLoaderImpl.getInstance().getClassloader().addURL(runtimePath.toUri().toURL()); FrogLoaderImpl.getInstance().getClassloader().addURL(runtimePath.toUri().toURL());
} }
} }

View file

@ -1,62 +1,61 @@
package dev.frogmc.frogloader.impl.plugin.mod; package dev.frogmc.frogloader.impl.plugin.mod;
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.FrogModProvider;
import dev.frogmc.frogloader.impl.mod.ModPropertiesReader;
import org.slf4j.Logger;
import java.io.IOException; import java.io.IOException;
import java.nio.file.FileSystem; import java.nio.file.FileSystem;
import java.nio.file.FileSystems; import java.nio.file.FileSystems;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.Collection; import java.util.Collection;
import dev.frogmc.frogloader.api.extensions.PreLaunchExtension;
import dev.frogmc.frogloader.api.mod.ModProperties;
import dev.frogmc.frogloader.api.plugin.FrogModProvider;
import dev.frogmc.frogloader.impl.mod.ModPropertiesReader;
import org.slf4j.Logger;
@SuppressWarnings("unused") @SuppressWarnings("unused")
public class FrogmodModProvider implements FrogModProvider { public class FrogmodModProvider implements FrogModProvider {
Logger LOGGER = org.slf4j.LoggerFactory.getLogger("FrogModProvider"); Logger LOGGER = org.slf4j.LoggerFactory.getLogger("FrogModProvider");
@Override @Override
public String id() { public String id() {
return "frogloader:frogmod"; return "frogloader:frogmod";
} }
@Override @Override
public boolean isApplicable() { public boolean isApplicable() {
return true; return true;
} }
@Override @Override
public boolean isFileApplicable(Path path) { public boolean isFileApplicable(Path path) {
if (!path.toString().endsWith(".frogmod")) { if (!path.toString().endsWith(".frogmod")) {
LOGGER.info("File {} is not a frogmod file", path.toString()); LOGGER.info("File {} is not a frogmod file", path);
return false; return false;
} }
try (FileSystem fs = FileSystems.newFileSystem(path)) { try (FileSystem fs = FileSystems.newFileSystem(path)) {
return fs.getPath("frog.mod.toml").toFile().exists(); return fs.getPath("frog.mod.toml").toFile().exists();
} catch (Exception e) { } catch (Exception e) {
LOGGER.error("Error while checking file {}", path, e); LOGGER.error("Error while checking file {}", path, e);
return false; return false;
} }
} }
@Override @Override
public ModProperties loadMod(Path path) { public ModProperties loadMod(Path path) {
try (FileSystem fs = FileSystems.newFileSystem(path)) { try (FileSystem fs = FileSystems.newFileSystem(path)) {
ModProperties prop = ModPropertiesReader.readFile(fs.getPath("frog.mod.toml").toUri().toURL()).orElseThrow(IOException::new); ModProperties prop = ModPropertiesReader.readFile(fs.getPath("frog.mod.toml").toUri().toURL()).orElseThrow(IOException::new);
// prop.extensions().runIfPresent(PreLaunchExtension.ID, PreLaunchExtension.class, PreLaunchExtension::onPreLaunch); // prop.extensions().runIfPresent(PreLaunchExtension.ID, PreLaunchExtension.class, PreLaunchExtension::onPreLaunch);
return prop; return prop;
} catch (Exception e) { } catch (Exception e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} }
} }
@Override @Override
public void initMods(Collection<ModProperties> mods) { public void initMods(Collection<ModProperties> mods) {
mods.forEach(mod -> { mods.forEach(mod -> {
mod.extensions().runIfPresent(PreLaunchExtension.ID, PreLaunchExtension.class, PreLaunchExtension::onPreLaunch); mod.extensions().runIfPresent(PreLaunchExtension.ID, PreLaunchExtension.class, PreLaunchExtension::onPreLaunch);
}); });
} }
} }

View file

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