Merge pull request 'add dependency-resolving' (#11) from owlsys/dependency-resolving into main
Reviewed-on: #11 Reviewed-by: Ecorous <ecorous@outlook.com>
This commit is contained in:
commit
4aec13abc2
23
.forgejo/workflows/publish.yml
Normal file
23
.forgejo/workflows/publish.yml
Normal file
|
@ -0,0 +1,23 @@
|
|||
name: Publish to snapshot maven
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'main'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: docker
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: https://github.com/actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: 21
|
||||
- uses: https://github.com/gradle/actions/setup-gradle@v3
|
||||
- name: Build
|
||||
run: |
|
||||
chmod +x ./gradlew
|
||||
./gradlew publishMavenJavaPublicationToFrogMCSnapshotsMavenRepository \
|
||||
-PFrogMCSnapshotsMavenUsername=${{ secrets.MAVEN_PUSH_USER }} \
|
||||
-PFrogMCSnapshotsMavenPassword=${{ secrets.MAVEN_PUSH_TOKEN }}
|
|
@ -12,11 +12,14 @@ credits = [
|
|||
|
||||
[frog.dependencies]
|
||||
depends = [
|
||||
{ id = "other_mod", versions = ">=0.2.0" }
|
||||
{ id = "other_mod", name = "Other Mod", versions = ">=0.2.0 <0.5.2 || 0.1.1 || 1.x || 3 || ~5 || ^6.x", link = "https://example.com" }
|
||||
]
|
||||
breaks = [
|
||||
{ id = "old_mod", versions = "*" }
|
||||
]
|
||||
provides = [
|
||||
{ id = "provided_mod", version = "that.version.aa" }
|
||||
]
|
||||
|
||||
[frog.extensions]
|
||||
pre_launch = "dev.frogmc.frogloader.example.ExamplePreLaunchExtension"
|
||||
|
|
|
@ -6,25 +6,29 @@ 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.impl.FrogLoaderImpl;
|
||||
import dev.frogmc.frogloader.impl.plugin.NonsensePlugin;
|
||||
|
||||
public interface FrogLoader {
|
||||
|
||||
static FrogLoader getInstance(){
|
||||
static FrogLoader getInstance() {
|
||||
return FrogLoaderImpl.getInstance();
|
||||
}
|
||||
|
||||
List<NonsensePlugin> getPlugins();
|
||||
List<FrogPlugin> getPlugins();
|
||||
|
||||
Env getEnv();
|
||||
|
||||
Path getGameDir();
|
||||
|
||||
Path getConfigDir();
|
||||
|
||||
Path getModsDir();
|
||||
|
||||
boolean isDevelopment();
|
||||
|
||||
boolean isModLoaded(String id);
|
||||
|
||||
Optional<ModProperties> getModProperties(String id);
|
||||
|
||||
}
|
||||
|
|
|
@ -22,7 +22,7 @@ public final class ModDependencies {
|
|||
for (Type type : Type.values()) {
|
||||
for (Entry entry : getForType(type)) {
|
||||
if (entry.id.equals(id)) {
|
||||
entries.add(new ModEntry(type, entry.range));
|
||||
entries.add(new ModEntry(type, entry.range, entry.link));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -32,10 +32,12 @@ public final class ModDependencies {
|
|||
public static class ModEntry {
|
||||
private final Type type;
|
||||
private final String range;
|
||||
private final String link;
|
||||
|
||||
private ModEntry(Type type, String range) {
|
||||
private ModEntry(Type type, String range, String link) {
|
||||
this.type = type;
|
||||
this.range = range;
|
||||
this.link = link;
|
||||
}
|
||||
|
||||
public Type type() {
|
||||
|
@ -45,9 +47,13 @@ public final class ModDependencies {
|
|||
public String range() {
|
||||
return range;
|
||||
}
|
||||
|
||||
public String link(){
|
||||
return link;
|
||||
}
|
||||
}
|
||||
|
||||
public record Entry(String id, String range) {
|
||||
public record Entry(String id, String range, String link, String name) {
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -3,12 +3,17 @@ package dev.frogmc.frogloader.api.mod;
|
|||
import java.util.Collection;
|
||||
import java.util.Map;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
public interface ModProperties {
|
||||
|
||||
String id();
|
||||
|
||||
String name();
|
||||
|
||||
@Nullable
|
||||
String icon();
|
||||
|
||||
SemVer version();
|
||||
|
||||
String license();
|
||||
|
|
|
@ -6,4 +6,6 @@ public interface SemVer extends Comparable<SemVer> {
|
|||
int patch();
|
||||
String prerelease();
|
||||
String build();
|
||||
|
||||
boolean equals(Object other);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
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;
|
||||
|
||||
public interface FrogPlugin {
|
||||
|
||||
default void run() {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Whether this plugin is applicable to be loaded in the current environment
|
||||
*/
|
||||
default boolean isApplicable() {
|
||||
return false;
|
||||
}
|
||||
|
||||
default void init(FrogLoader loader) throws Exception {
|
||||
}
|
||||
|
||||
default Collection<ModProperties> getMods(){
|
||||
return Collections.emptySet();
|
||||
}
|
||||
}
|
|
@ -12,30 +12,26 @@ import java.util.List;
|
|||
import java.util.function.Predicate;
|
||||
|
||||
public class Discovery {
|
||||
public static Collection<Path> find(Path start, Predicate<Path> directoryFilter, Predicate<Path> fileFilter){
|
||||
public static Collection<Path> find(Path start, Predicate<Path> directoryFilter, Predicate<Path> fileFilter) throws IOException {
|
||||
List<Path> paths = new ArrayList<>();
|
||||
try {
|
||||
Files.walkFileTree(start, new SimpleFileVisitor<>() {
|
||||
Files.walkFileTree(start, new SimpleFileVisitor<>() {
|
||||
|
||||
@Override
|
||||
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
|
||||
if (directoryFilter.test(dir)) {
|
||||
return super.preVisitDirectory(dir, attrs);
|
||||
}
|
||||
return FileVisitResult.SKIP_SUBTREE;
|
||||
@Override
|
||||
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
|
||||
if (directoryFilter.test(dir)) {
|
||||
return super.preVisitDirectory(dir, attrs);
|
||||
}
|
||||
return FileVisitResult.SKIP_SUBTREE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
|
||||
if (fileFilter.test(file)) {
|
||||
paths.add(file);
|
||||
}
|
||||
return super.visitFile(file, attrs);
|
||||
@Override
|
||||
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
|
||||
if (fileFilter.test(file)) {
|
||||
paths.add(file);
|
||||
}
|
||||
});
|
||||
} catch (IOException e) {
|
||||
// TODO error handling.
|
||||
}
|
||||
return super.visitFile(file, attrs);
|
||||
}
|
||||
});
|
||||
return paths;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
package dev.frogmc.frogloader.impl;
|
||||
|
||||
import java.io.*;
|
||||
import java.io.IOException;
|
||||
import java.lang.invoke.MethodHandle;
|
||||
import java.lang.invoke.MethodHandles;
|
||||
import java.lang.invoke.MethodType;
|
||||
|
@ -14,31 +14,30 @@ 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.impl.gui.LoaderGui;
|
||||
import dev.frogmc.frogloader.impl.launch.MixinClassLoader;
|
||||
import dev.frogmc.frogloader.impl.mod.ModUtil;
|
||||
import dev.frogmc.frogloader.impl.plugin.NonsensePlugin;
|
||||
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 {
|
||||
// TODO decide this
|
||||
public static final String MOD_FILE_EXTENSION = ".frogmod";
|
||||
private final boolean DEV_ENV = Boolean.getBoolean("frogmc.development");
|
||||
|
||||
@Getter
|
||||
private static FrogLoaderImpl instance;
|
||||
private final boolean DEV_ENV = Boolean.getBoolean(SystemProperties.DEVELOPMENT);
|
||||
@Getter
|
||||
private final String[] args;
|
||||
@Getter
|
||||
private final Env env;
|
||||
private final Logger LOGGER = LoggerFactory.getLogger("FrogLoader");
|
||||
|
||||
@Getter
|
||||
private static FrogLoaderImpl instance;
|
||||
|
||||
private final Logger LOGGER = LoggerFactory.getLogger("Frogloader");
|
||||
|
||||
@Getter
|
||||
private final List<NonsensePlugin> plugins = new ArrayList<>();
|
||||
private final List<FrogPlugin> plugins = new ArrayList<>();
|
||||
|
||||
@Getter
|
||||
private final Path gameDir, configDir, modsDir;
|
||||
|
@ -49,8 +48,8 @@ public class FrogLoaderImpl implements FrogLoader {
|
|||
@Getter
|
||||
private final Gson gson = new Gson();
|
||||
|
||||
private final Map<String, ModProperties> mods;
|
||||
private final Collection<String> modIds;
|
||||
private Map<String, ModProperties> mods;
|
||||
private Collection<String> modIds;
|
||||
|
||||
|
||||
private FrogLoaderImpl(String[] args, Env env) {
|
||||
|
@ -71,22 +70,16 @@ public class FrogLoaderImpl implements FrogLoader {
|
|||
LOGGER.warn("Failed to create essential directories ", e);
|
||||
}
|
||||
|
||||
discoverPlugins();
|
||||
advanceMixinState();
|
||||
mods = collectMods();
|
||||
modIds = collectModIds();
|
||||
LOGGER.info(ModUtil.getModList(mods.values()));
|
||||
LOGGER.info("Launching...");
|
||||
plugins.forEach(NonsensePlugin::run);
|
||||
}
|
||||
|
||||
private void advanceMixinState(){
|
||||
try {
|
||||
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.DEFAULT);
|
||||
} catch (Throwable e) {
|
||||
throw new RuntimeException(e);
|
||||
discoverPlugins();
|
||||
advanceMixinState();
|
||||
mods = collectMods();
|
||||
modIds = collectModIds();
|
||||
LOGGER.info(ModUtil.getModList(mods.values()));
|
||||
LOGGER.info("Launching...");
|
||||
plugins.forEach(FrogPlugin::run);
|
||||
} catch (Throwable t){
|
||||
LoaderGui.builder().setContent(LoaderGui.ContentType.GENERIC_ERROR, t).addReport(CrashReportGenerator.writeReport(t, collectMods().values())).build().show();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -99,55 +92,46 @@ public class FrogLoaderImpl implements FrogLoader {
|
|||
new FrogLoaderImpl(args, env);
|
||||
}
|
||||
|
||||
private void discoverPlugins() {
|
||||
List<String> classes = new ArrayList<>();
|
||||
this.getClass().getClassLoader().resources("META-INF/services/"+NonsensePlugin.class.getName()).distinct().forEach(url -> {
|
||||
try (InputStream inputStream = url.openStream()) {
|
||||
new BufferedReader(new InputStreamReader(inputStream)).lines().forEach(classes::add);
|
||||
} catch (IOException e) {
|
||||
LOGGER.error("Failed to load plugin: ", e);
|
||||
}
|
||||
});
|
||||
LOGGER.info("Found plugins: \n{}", String.join("\t\n", classes));
|
||||
private void advanceMixinState() {
|
||||
try {
|
||||
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.DEFAULT);
|
||||
} catch (Throwable e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
for (Class<?> c : classes.stream().map((String className) -> {
|
||||
private void discoverPlugins() {
|
||||
ServiceLoader.load(FrogPlugin.class).forEach(plugin -> {
|
||||
try {
|
||||
return classloader.findClass(className);
|
||||
} catch (ClassNotFoundException e) {
|
||||
LOGGER.error("Failed to load plugin: ", e);
|
||||
return null;
|
||||
}
|
||||
}).filter(Objects::nonNull).filter(NonsensePlugin.class::isAssignableFrom).toList()) {
|
||||
try {
|
||||
MethodHandle ctor = MethodHandles.publicLookup().findConstructor(c, MethodType.methodType(void.class));
|
||||
NonsensePlugin plugin = (NonsensePlugin) ctor.invoke();
|
||||
if (plugin.isApplicable()) {
|
||||
plugins.add(plugin);
|
||||
plugin.init(this);
|
||||
plugins.add(plugin);
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
LOGGER.error("Error during plugin initialisation: ", e);
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (plugins.isEmpty()){
|
||||
// TODO display error
|
||||
if (plugins.isEmpty()) {
|
||||
throw new IllegalStateException("No plugin applicable to the current state was found!");
|
||||
}
|
||||
}
|
||||
|
||||
public String getArgument(String name){
|
||||
for (int i=0;i<args.length-1;i+=2){
|
||||
if (args[i].equals("--"+name)){
|
||||
return args[i+1];
|
||||
public String getArgument(String name) {
|
||||
for (int i = 0; i < args.length - 1; i += 2) {
|
||||
if (args[i].equals("--" + name)) {
|
||||
return args[i + 1];
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
public String getArgumentOrElse(String name, String other){
|
||||
public String getArgumentOrElse(String name, String other) {
|
||||
String res = getArgument(name);
|
||||
if (res.isEmpty()){
|
||||
if (res.isEmpty()) {
|
||||
return other;
|
||||
}
|
||||
return res;
|
||||
|
@ -168,15 +152,19 @@ public class FrogLoaderImpl implements FrogLoader {
|
|||
return Optional.ofNullable(mods.get(id));
|
||||
}
|
||||
|
||||
private Map<String, ModProperties> collectMods(){
|
||||
Collection<ModProperties> properties = plugins.stream().map(NonsensePlugin::getMods).reduce(new HashSet<>(), (s1, s2) -> {
|
||||
private Map<String, ModProperties> collectMods() {
|
||||
Collection<ModProperties> properties = plugins.stream().map(FrogPlugin::getMods).reduce(new HashSet<>(), (s1, s2) -> {
|
||||
s1.addAll(s2);
|
||||
return s1;
|
||||
});
|
||||
return properties.stream().collect(Collectors.toMap(ModProperties::id, m -> m));
|
||||
}
|
||||
|
||||
private Collection<String> collectModIds(){
|
||||
private Collection<String> collectModIds() {
|
||||
return mods.keySet();
|
||||
}
|
||||
|
||||
public Collection<ModProperties> getMods(){
|
||||
return mods.values();
|
||||
}
|
||||
}
|
|
@ -4,6 +4,6 @@ import java.io.IOException;
|
|||
|
||||
public class SemVerParseException extends IOException {
|
||||
public SemVerParseException(String message) {
|
||||
super("Failed to parse SemVer: "+message);
|
||||
super("Failed to parse SemVer: " + message);
|
||||
}
|
||||
}
|
||||
|
|
281
src/main/java/dev/frogmc/frogloader/impl/gui/LoaderGui.java
Normal file
281
src/main/java/dev/frogmc/frogloader/impl/gui/LoaderGui.java
Normal file
|
@ -0,0 +1,281 @@
|
|||
package dev.frogmc.frogloader.impl.gui;
|
||||
|
||||
import javax.swing.*;
|
||||
import javax.swing.plaf.basic.BasicBorders;
|
||||
import java.awt.*;
|
||||
import java.awt.event.ActionEvent;
|
||||
import java.io.PrintWriter;
|
||||
import java.io.StringWriter;
|
||||
import java.net.URI;
|
||||
import java.net.URL;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import dev.frogmc.frogloader.impl.FrogLoaderImpl;
|
||||
import dev.frogmc.frogloader.impl.mod.ModDependencyResolver;
|
||||
import dev.frogmc.frogloader.impl.mod.ModUtil;
|
||||
import dev.frogmc.frogloader.impl.util.URLUtil;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.AllArgsConstructor;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
@AllArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public class LoaderGui {
|
||||
|
||||
public static Builder builder(){
|
||||
return new Builder();
|
||||
}
|
||||
|
||||
public static class Builder {
|
||||
|
||||
private Path report;
|
||||
private Consumer<JFrame> contentFunc;
|
||||
private boolean keepRunning = false;
|
||||
|
||||
public <T> Builder setContent(ContentType<T> type, T argument){
|
||||
contentFunc = f -> type.contentSetter.accept(f, argument);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder addReport(Path report){
|
||||
this.report = report;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder keepRunning(){
|
||||
keepRunning = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
public LoaderGui build(){
|
||||
JFrame frame = getFrame(report);
|
||||
if (contentFunc != null) {
|
||||
contentFunc.accept(frame);
|
||||
}
|
||||
return new LoaderGui(frame, keepRunning);
|
||||
}
|
||||
}
|
||||
|
||||
private final JFrame frame;
|
||||
private final boolean keepRunning;
|
||||
|
||||
private static JFrame getFrame(Path report){
|
||||
var frame = new JFrame();
|
||||
frame.setTitle("FrogLoader " + LoaderGui.class.getPackage().getImplementationVersion());
|
||||
frame.setSize(952, 560);
|
||||
frame.setLocationRelativeTo(null);
|
||||
frame.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
|
||||
|
||||
JPanel actions = new JPanel(new FlowLayout(FlowLayout.RIGHT));
|
||||
if (report != null) {
|
||||
actions.add(new JButton(new AbstractAction("Open Report") {
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e) {
|
||||
URLUtil.open(report.toUri());
|
||||
}
|
||||
}));
|
||||
actions.add(new JButton(new AbstractAction("Copy Report") {
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e) {
|
||||
URLUtil.copyStringContent(report.toUri());
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
actions.add(new JButton(new AbstractAction("Join Discord") {
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e) {
|
||||
URLUtil.open(URI.create("https://discord.frogmc.dev"));
|
||||
}
|
||||
}));
|
||||
|
||||
actions.add(new JButton(new AbstractAction("Exit") {
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e) {
|
||||
frame.dispose();
|
||||
}
|
||||
}));
|
||||
frame.add(actions, BorderLayout.SOUTH);
|
||||
return frame;
|
||||
}
|
||||
|
||||
@SuppressWarnings("BusyWait")
|
||||
public void show(){
|
||||
frame.setVisible(true);
|
||||
|
||||
while (frame.isVisible()) {
|
||||
try {
|
||||
Thread.sleep(100);
|
||||
} catch (InterruptedException ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
if (!keepRunning) {
|
||||
System.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
@AllArgsConstructor
|
||||
public static class ContentType<T> {
|
||||
public static final ContentType<ModDependencyResolver.UnfulfilledDependencyException> INFO_UNFULFILLED_DEP = new ContentType<>((frame, ex) -> {
|
||||
JPanel pane = new JPanel(new BorderLayout());
|
||||
JTextPane title = new JTextPane();
|
||||
title.setBackground(pane.getBackground());
|
||||
title.setEditable(false);
|
||||
int size = ex.getDependencies().size();
|
||||
title.setText("Found "+size+" Error"+(size > 1 ? "s:":":"));
|
||||
title.setFont(title.getFont().deriveFont(Font.BOLD, 16f));
|
||||
title.setBorder(BorderFactory.createEmptyBorder(8, 0, 8, 0));
|
||||
pane.setBorder(BorderFactory.createEmptyBorder(4, 4, 4, 4));
|
||||
pane.add(title, BorderLayout.NORTH);
|
||||
|
||||
Box list = Box.createVerticalBox();
|
||||
ex.getDependencies().forEach(e -> {
|
||||
StringBuilder description = new StringBuilder();
|
||||
if (e.presentVersion() != null){
|
||||
description.append("Mod ").append(e.source().id()).append(" (").append(e.source().name()).append(") depends on ");
|
||||
if (e.dependencyName() != null){
|
||||
description.append(e.dependency()).append(" (").append(e.dependencyName()).append(") ");
|
||||
} else {
|
||||
description.append("a Mod with id ").append(e.dependency());
|
||||
}
|
||||
description.append(" with a version matching ").append(printVersionRange(e.range())).append(", but a different version is present or provided: ").append(e.presentVersion());
|
||||
} else {
|
||||
description.append("Mod ").append(e.source().id()).append(" (").append(e.source().name()).append(") depends on ");
|
||||
if (e.dependencyName() != null){
|
||||
description.append(e.dependency()).append(" (").append(e.dependencyName()).append(") ");
|
||||
} else {
|
||||
description.append("a Mod with id ").append(e.dependency());
|
||||
}
|
||||
description.append(" with a version matching ").append(printVersionRange(e.range())).append(". \nNo version is currently available.");
|
||||
}
|
||||
description.append("\nSuggested Solution: Install ")
|
||||
.append(e.range().maxCompatible().or(e.range()::minCompatible).map(Objects::toString)
|
||||
.map(s -> "0.0.0".equals(s) ? "any version" : "version " + s).orElse("<unknown>"))
|
||||
.append(" of ");
|
||||
if (e.dependencyName() != null){
|
||||
description.append(e.dependency()).append(" (").append(e.dependencyName()).append(") ");
|
||||
} else {
|
||||
description.append("Mod with id ").append(e.dependency());
|
||||
}
|
||||
List<JButton> actions = new ArrayList<>();
|
||||
if (e.link() != null) {
|
||||
boolean install = e.link().endsWith(FrogLoaderImpl.MOD_FILE_EXTENSION);
|
||||
String name = install ? "Install" : "Open mod page";
|
||||
JButton urlButton = new JButton(new AbstractAction(name) {
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent event) {
|
||||
if (install){
|
||||
ModUtil.installMod(e.link());
|
||||
} else {
|
||||
URLUtil.open(URI.create(e.link()));
|
||||
}
|
||||
}
|
||||
});
|
||||
actions.add(urlButton);
|
||||
}
|
||||
JPanel entry = getEntry(description.toString(), e.range(), list.getBackground(), e.source().icon(), actions);
|
||||
list.add(entry);
|
||||
});
|
||||
pane.add(new JScrollPane(list));
|
||||
frame.add(pane);
|
||||
});
|
||||
public static final ContentType<ModDependencyResolver.BreakingModException> INFO_BREAKING_DEP = new ContentType<>((frame, ex) -> {
|
||||
JPanel pane = new JPanel(new BorderLayout());
|
||||
JTextPane title = new JTextPane();
|
||||
title.setBackground(pane.getBackground());
|
||||
title.setEditable(false);
|
||||
int size = ex.getBreaks().size();
|
||||
title.setText("Found "+size+" Error"+(size > 1 ? "s:":":"));
|
||||
title.setFont(title.getFont().deriveFont(Font.BOLD, 16f));
|
||||
pane.setBorder(BorderFactory.createEmptyBorder(4, 4, 4, 4));
|
||||
pane.add(title, BorderLayout.NORTH);
|
||||
|
||||
Box list = Box.createVerticalBox();
|
||||
ex.getBreaks().forEach(e -> {
|
||||
String description = "Mod %s (%s) breaks with mod %s (%s) for versions matching %s \n(present: %s)".formatted(e.source().id(), e.source().name(), e.broken().id(), e.broken().name(), printVersionRange(e.range()), e.broken().version()) +
|
||||
"\nSuggested Solution: Install " +
|
||||
e.range().maxCompatible().or(e.range()::minCompatible).map(Objects::toString)
|
||||
.map(s -> "0.0.0".equals(s) ? "any version" : "version " + s).orElse("<unknown>") +
|
||||
" of Mod %s (%s) ".formatted(e.broken().id(), e.broken().name());
|
||||
List<JButton> actions = new ArrayList<>();
|
||||
|
||||
JPanel entry = getEntry(description, e.range(), list.getBackground(), e.source().icon(), actions);
|
||||
entry.setSize(list.getWidth(), entry.getHeight());
|
||||
list.add(entry);
|
||||
});
|
||||
pane.add(new JScrollPane(list));
|
||||
frame.add(pane);
|
||||
});
|
||||
public static final ContentType<Throwable> GENERIC_ERROR = new ContentType<>((frame, throwable) -> {
|
||||
JPanel pane = new JPanel(new BorderLayout());
|
||||
|
||||
JTextPane title = new JTextPane();
|
||||
title.setBackground(pane.getBackground());
|
||||
title.setEditable(false);
|
||||
title.setText("Caught Fatal Error during game startup:");
|
||||
title.setFont(title.getFont().deriveFont(Font.BOLD, 16f));
|
||||
pane.setBorder(BorderFactory.createEmptyBorder(4, 4, 4, 4));
|
||||
pane.add(title, BorderLayout.NORTH);
|
||||
|
||||
JTextPane error = new JTextPane();
|
||||
title.setEditable(false);
|
||||
StringWriter writer = new StringWriter();
|
||||
throwable.printStackTrace(new PrintWriter(writer));
|
||||
error.setText(writer.toString());
|
||||
|
||||
pane.add(error);
|
||||
|
||||
frame.add(pane);
|
||||
});
|
||||
|
||||
private final BiConsumer<JFrame, T> contentSetter;
|
||||
}
|
||||
|
||||
private static String printVersionRange(ModDependencyResolver.VersionRange range){
|
||||
return "range: \n"+ range.toString().replace("||", "or");
|
||||
}
|
||||
|
||||
private static JPanel getEntry(String description, ModDependencyResolver.VersionRange range, Color background, @Nullable String icon, List<JButton> actions){
|
||||
JPanel entry = new JPanel(new BorderLayout());
|
||||
entry.setBorder(BasicBorders.getInternalFrameBorder());
|
||||
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("<", "<").replace("\n", "<br>") + "</html>");
|
||||
JPanel actionPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT));
|
||||
actions.forEach(actionPanel::add);
|
||||
if (icon != null) {
|
||||
URL location = LoaderGui.class.getResource(icon);
|
||||
if (location != null) {
|
||||
entry.add(new JLabel(new ImageIcon(location)), BorderLayout.WEST);
|
||||
}
|
||||
}
|
||||
text.add(desc);
|
||||
JCheckBox showAdvanced = new JCheckBox("Show raw version range");
|
||||
JTextPane advanced = new JTextPane();
|
||||
advanced.setEditable(false);
|
||||
advanced.setBackground(background);
|
||||
advanced.setText(range.toString());
|
||||
advanced.setVisible(false);
|
||||
text.add(advanced);
|
||||
showAdvanced.addActionListener(new AbstractAction() {
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e) {
|
||||
advanced.setVisible(showAdvanced.isSelected());
|
||||
}
|
||||
});
|
||||
entry.add(text);
|
||||
Box bottom = Box.createHorizontalBox();
|
||||
bottom.add(showAdvanced, actionPanel);
|
||||
entry.add(bottom, BorderLayout.SOUTH);
|
||||
return entry;
|
||||
}
|
||||
}
|
|
@ -1,6 +1,8 @@
|
|||
package dev.frogmc.frogloader.impl.launch;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.Arrays;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentSkipListSet;
|
||||
|
||||
|
|
|
@ -14,24 +14,18 @@ import org.spongepowered.asm.service.IPropertyKey;
|
|||
|
||||
public class FrogLauncher {
|
||||
|
||||
@Getter
|
||||
private final MixinClassLoader targetClassLoader;
|
||||
|
||||
@Getter
|
||||
private static FrogLauncher instance;
|
||||
|
||||
@Getter
|
||||
private final MixinClassLoader targetClassLoader;
|
||||
@Getter
|
||||
private final Map<IPropertyKey, Object> globalProperties = new HashMap<>();
|
||||
|
||||
@Getter
|
||||
private final Env env;
|
||||
|
||||
public static void run(String[] args, Env env) {
|
||||
new FrogLauncher(args, env);
|
||||
}
|
||||
|
||||
public FrogLauncher(String[] args, Env env){
|
||||
if (instance != null){
|
||||
public FrogLauncher(String[] args, Env env) {
|
||||
if (instance != null) {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
instance = this;
|
||||
|
@ -57,4 +51,8 @@ public class FrogLauncher {
|
|||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static void run(String[] args, Env env) {
|
||||
new FrogLauncher(args, env);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,9 +1,5 @@
|
|||
package dev.frogmc.frogloader.impl.launch;
|
||||
|
||||
import dev.frogmc.frogloader.impl.mixin.FrogMixinService;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.spongepowered.asm.mixin.MixinEnvironment;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URL;
|
||||
|
@ -13,117 +9,122 @@ import java.util.Collections;
|
|||
import java.util.Enumeration;
|
||||
import java.util.List;
|
||||
|
||||
import dev.frogmc.frogloader.impl.mixin.FrogMixinService;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.spongepowered.asm.mixin.MixinEnvironment;
|
||||
|
||||
public class MixinClassLoader extends URLClassLoader {
|
||||
|
||||
private static final ClassLoader SYSTEM = ClassLoader.getSystemClassLoader();
|
||||
private final List<String> exclusions = new ArrayList<>();
|
||||
private static final ClassLoader SYSTEM = ClassLoader.getSystemClassLoader();
|
||||
private final List<String> exclusions = new ArrayList<>();
|
||||
|
||||
static {
|
||||
registerAsParallelCapable();
|
||||
}
|
||||
static {
|
||||
registerAsParallelCapable();
|
||||
}
|
||||
|
||||
public MixinClassLoader() {
|
||||
super(new URL[0], null);
|
||||
excludePackage("java");
|
||||
excludePackage("com.sun");
|
||||
excludePackage("sun");
|
||||
excludePackage("jdk");
|
||||
}
|
||||
public MixinClassLoader() {
|
||||
super(new URL[0], null);
|
||||
excludePackage("java");
|
||||
excludePackage("com.sun");
|
||||
excludePackage("sun");
|
||||
excludePackage("jdk");
|
||||
excludePackage("javax");
|
||||
}
|
||||
|
||||
public boolean isClassLoaded(String name) {
|
||||
return findLoadedClass(name) != null;
|
||||
}
|
||||
public boolean isClassLoaded(String name) {
|
||||
return findLoadedClass(name) != null;
|
||||
}
|
||||
|
||||
public byte[] getClassBytes(String name) throws IOException {
|
||||
String binName = name.replace('.', '/');
|
||||
String path = binName.concat(".class");
|
||||
public byte[] getClassBytes(String name) throws IOException {
|
||||
String binName = name.replace('.', '/');
|
||||
String path = binName.concat(".class");
|
||||
|
||||
try (InputStream in = getResourceAsStream(path)) {
|
||||
if (in == null)
|
||||
return null;
|
||||
try (InputStream in = getResourceAsStream(path)) {
|
||||
if (in == null)
|
||||
return null;
|
||||
|
||||
return FrogMixinService.getTransformer().transformClass(MixinEnvironment.getCurrentEnvironment(), name, AccessWidener.processClass(in.readAllBytes(), binName));
|
||||
}
|
||||
}
|
||||
return FrogMixinService.getTransformer().transformClass(MixinEnvironment.getCurrentEnvironment(), name, AccessWidener.processClass(in.readAllBytes(), binName));
|
||||
}
|
||||
}
|
||||
|
||||
public void excludePackage(String name) {
|
||||
exclusions.add(name + '.');
|
||||
}
|
||||
public void excludePackage(String name) {
|
||||
exclusions.add(name + '.');
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addURL(URL url) {
|
||||
super.addURL(url);
|
||||
}
|
||||
@Override
|
||||
public void addURL(URL url) {
|
||||
super.addURL(url);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
|
||||
synchronized (getClassLoadingLock(name)) {
|
||||
for (String prefix : exclusions) {
|
||||
if (name.startsWith(prefix)) {
|
||||
return SYSTEM.loadClass(name);
|
||||
}
|
||||
}
|
||||
@Override
|
||||
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
|
||||
synchronized (getClassLoadingLock(name)) {
|
||||
for (String prefix : exclusions) {
|
||||
if (name.startsWith(prefix)) {
|
||||
return SYSTEM.loadClass(name);
|
||||
}
|
||||
}
|
||||
|
||||
Class<?> loaded = findLoadedClass(name);
|
||||
if (loaded != null)
|
||||
return loaded;
|
||||
Class<?> loaded = findLoadedClass(name);
|
||||
if (loaded != null)
|
||||
return loaded;
|
||||
|
||||
Class<?> result = findClass(name);
|
||||
Class<?> result = findClass(name);
|
||||
|
||||
if (resolve)
|
||||
resolveClass(result);
|
||||
if (resolve)
|
||||
resolveClass(result);
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized Class<?> findClass(String name) throws ClassNotFoundException {
|
||||
try {
|
||||
byte[] bytes = getClassBytes(name);
|
||||
if (bytes == null)
|
||||
throw new ClassNotFoundException(name);
|
||||
@Override
|
||||
public synchronized Class<?> findClass(String name) throws ClassNotFoundException {
|
||||
try {
|
||||
byte[] bytes = getClassBytes(name);
|
||||
if (bytes == null)
|
||||
throw new ClassNotFoundException(name);
|
||||
|
||||
return defineClass(name, bytes, 0, bytes.length);
|
||||
} catch (IOException e) {
|
||||
throw new ClassNotFoundException(name, e);
|
||||
}
|
||||
}
|
||||
return defineClass(name, bytes, 0, bytes.length);
|
||||
} catch (IOException e) {
|
||||
throw new ClassNotFoundException(name, e);
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public URL getResource(String name) {
|
||||
URL parentUrl = super.getResource(name);
|
||||
@Nullable
|
||||
@Override
|
||||
public URL getResource(String name) {
|
||||
URL parentUrl = super.getResource(name);
|
||||
|
||||
if (parentUrl != null)
|
||||
return parentUrl;
|
||||
if (parentUrl != null)
|
||||
return parentUrl;
|
||||
|
||||
return SYSTEM.getResource(name);
|
||||
}
|
||||
return SYSTEM.getResource(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Enumeration<URL> getResources(String name) throws IOException {
|
||||
Enumeration<URL> parentResources = super.getResources(name);
|
||||
Enumeration<URL> systemResources = SYSTEM.getResources(name);
|
||||
@Override
|
||||
public Enumeration<URL> getResources(String name) throws IOException {
|
||||
Enumeration<URL> parentResources = super.getResources(name);
|
||||
Enumeration<URL> systemResources = SYSTEM.getResources(name);
|
||||
|
||||
if (parentResources.hasMoreElements() && systemResources.hasMoreElements()) {
|
||||
List<URL> list = new ArrayList<>();
|
||||
if (parentResources.hasMoreElements() && systemResources.hasMoreElements()) {
|
||||
List<URL> list = new ArrayList<>();
|
||||
|
||||
while (parentResources.hasMoreElements())
|
||||
list.add(parentResources.nextElement());
|
||||
while (parentResources.hasMoreElements())
|
||||
list.add(parentResources.nextElement());
|
||||
|
||||
while (systemResources.hasMoreElements())
|
||||
list.add(systemResources.nextElement());
|
||||
while (systemResources.hasMoreElements())
|
||||
list.add(systemResources.nextElement());
|
||||
|
||||
return Collections.enumeration(list);
|
||||
}
|
||||
return Collections.enumeration(list);
|
||||
}
|
||||
|
||||
if (parentResources.hasMoreElements())
|
||||
return parentResources;
|
||||
if (parentResources.hasMoreElements())
|
||||
return parentResources;
|
||||
|
||||
if (systemResources.hasMoreElements())
|
||||
return systemResources;
|
||||
if (systemResources.hasMoreElements())
|
||||
return systemResources;
|
||||
|
||||
return Collections.enumeration(Collections.emptyList());
|
||||
}
|
||||
return Collections.enumeration(Collections.emptyList());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
package dev.frogmc.frogloader.impl.launch.server;
|
||||
|
||||
import dev.frogmc.frogloader.impl.launch.FrogLauncher;
|
||||
import dev.frogmc.frogloader.api.env.Env;
|
||||
import dev.frogmc.frogloader.impl.launch.FrogLauncher;
|
||||
|
||||
public class FrogServer {
|
||||
|
||||
|
|
|
@ -2,7 +2,10 @@ package dev.frogmc.frogloader.impl.mixin;
|
|||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.InputStreamReader;
|
||||
import java.util.*;
|
||||
import java.util.Collection;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentSkipListSet;
|
||||
import java.util.function.Predicate;
|
||||
|
@ -18,7 +21,7 @@ public class AWProcessor {
|
|||
private static final Predicate<String> HEADER = Pattern.compile("accessWidener\\s+v[12]\\s+.*").asMatchPredicate();
|
||||
private static final String SEPARATOR = "[\\t ]+";
|
||||
|
||||
public static void load(Collection<ModProperties> mods){
|
||||
public static void load(Collection<ModProperties> mods) {
|
||||
Map<String, AccessWidener.Entry> classMap = new ConcurrentHashMap<>();
|
||||
Map<String, Map<String, AccessWidener.Entry>> methods = new ConcurrentHashMap<>();
|
||||
Map<String, Map<String, AccessWidener.Entry>> fields = new ConcurrentHashMap<>();
|
||||
|
|
|
@ -12,27 +12,27 @@ import org.spongepowered.asm.logging.LoggerAdapterAbstract;
|
|||
|
||||
public class FrogMixinLogger extends LoggerAdapterAbstract {
|
||||
private static final Map<String, FrogMixinLogger> LOGGERS = new ConcurrentHashMap<>();
|
||||
|
||||
public static ILogger get(String name){
|
||||
return LOGGERS.computeIfAbsent(name, FrogMixinLogger::new);
|
||||
}
|
||||
|
||||
private final Logger log;
|
||||
|
||||
public FrogMixinLogger(String name){
|
||||
public FrogMixinLogger(String name) {
|
||||
super(name);
|
||||
log = LoggerFactory.getLogger("Nonsense Loader/"+name.substring(0, 1).toUpperCase(Locale.ROOT)+name.substring(1));
|
||||
log = LoggerFactory.getLogger("FrogLoader/" + name.substring(0, 1).toUpperCase(Locale.ROOT) + name.substring(1));
|
||||
}
|
||||
|
||||
public static ILogger get(String name) {
|
||||
return LOGGERS.computeIfAbsent(name, FrogMixinLogger::new);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getType() {
|
||||
return "Nonsense Loader/Mixin Logger";
|
||||
return "FrogLoader/Mixin Logger";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void catching(Level level, Throwable t) {
|
||||
log.atLevel(org.slf4j.event.Level.valueOf(level.name())).setCause(t).log();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(Level level, String message, Object... params) {
|
||||
log.atLevel(org.slf4j.event.Level.valueOf(level.name())).log(message, params);
|
||||
|
|
|
@ -29,7 +29,7 @@ public class FrogMixinService implements IMixinService, IClassProvider, IClassBy
|
|||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "NonsenseMixinService";
|
||||
return "FrogMixinService";
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -8,4 +8,5 @@ public class BuiltinExtensions {
|
|||
public final String INCLUDED_JARS = "included_jars";
|
||||
public final String PRE_LAUNCH = "pre_launch";
|
||||
public final String ACCESSWIDENER = "frog_aw";
|
||||
public final String LOADING_TYPE = "loading_type";
|
||||
}
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
package dev.frogmc.frogloader.impl.mod;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
|
||||
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.impl.SemVerParseException;
|
||||
|
||||
public class JavaModProperties {
|
||||
|
||||
private static ModProperties INSTANCE;
|
||||
|
||||
private JavaModProperties() {
|
||||
|
||||
}
|
||||
|
||||
public static ModProperties get() throws SemVerParseException {
|
||||
if (INSTANCE == null) {
|
||||
INSTANCE = new ModPropertiesImpl("java",
|
||||
System.getProperty("java.vm.name"),
|
||||
"/duke.png",
|
||||
SemVerImpl.parse(System.getProperty("java.runtime.version")),
|
||||
"",
|
||||
Map.of(System.getProperty("java.vm.vendor"), Collections.singleton("Vendor")),
|
||||
new ModDependencies(Collections.emptySet(), Collections.emptySet(), Collections.emptySet(), Collections.emptySet()),
|
||||
ModExtensions.of(Collections.emptyMap()));
|
||||
}
|
||||
return INSTANCE;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,564 @@
|
|||
package dev.frogmc.frogloader.impl.mod;
|
||||
|
||||
import java.io.UncheckedIOException;
|
||||
import java.util.*;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import dev.frogmc.frogloader.api.mod.ModDependencies;
|
||||
import dev.frogmc.frogloader.api.mod.ModProperties;
|
||||
import dev.frogmc.frogloader.api.mod.SemVer;
|
||||
import dev.frogmc.frogloader.impl.SemVerParseException;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class ModDependencyResolver {
|
||||
|
||||
private final static Logger LOGGER = LoggerFactory.getLogger(ModDependencyResolver.class);
|
||||
private static final Pattern COMPARATOR = Pattern.compile("(=|>=|<=|>|<)?(\\d.*)");
|
||||
private final Collection<ModProperties> original;
|
||||
private final Map<String, DependencyEntry> dependencies = new HashMap<>();
|
||||
private final Map<String, DependencyEntry> breakings = new HashMap<>();
|
||||
private final Map<String, DependencyEntry> suggests = new HashMap<>();
|
||||
private final Map<String, ProvidedMod> provides = new HashMap<>();
|
||||
private final Map<String, ModProperties> presentMods = new HashMap<>();
|
||||
|
||||
public ModDependencyResolver(Collection<ModProperties> mods) throws ResolverException {
|
||||
this.original = Collections.unmodifiableCollection(mods);
|
||||
load();
|
||||
}
|
||||
|
||||
private void load() throws ResolverException {
|
||||
for (ModProperties props : original) {
|
||||
for (ModDependencies.Entry entry : props.dependencies().getForType(ModDependencies.Type.DEPEND)) {
|
||||
dependencies.put(entry.id(), new DependencyEntry(entry.range(), props, entry.link(), entry.name()));
|
||||
}
|
||||
for (ModDependencies.Entry entry : props.dependencies().getForType(ModDependencies.Type.BREAK)) {
|
||||
breakings.put(entry.id(), new DependencyEntry(entry.range(), props, entry.link(), entry.name()));
|
||||
}
|
||||
for (ModDependencies.Entry entry : props.dependencies().getForType(ModDependencies.Type.SUGGEST)) {
|
||||
suggests.put(entry.id(), new DependencyEntry(entry.range(), props, entry.link(), entry.name()));
|
||||
}
|
||||
props.dependencies().getForType(ModDependencies.Type.PROVIDE).forEach(e -> {
|
||||
try {
|
||||
provides.put(e.id(), new ProvidedMod(e.id(), SemVerImpl.parse(e.range()), props));
|
||||
} catch (SemVerParseException ex) {
|
||||
LOGGER.warn("Version for {} ({}), provided by mod '{}' ({}) does not meet SemVer specifications. Mod will not be provided.",
|
||||
e.id(), e.range(), props.id(), props.name());
|
||||
}
|
||||
});
|
||||
presentMods.put(props.id(), props);
|
||||
}
|
||||
}
|
||||
|
||||
public Collection<ModProperties> solve() throws UnfulfilledDependencyException, BreakingModException {
|
||||
// Step 1: Combine present and provided mods, always use the latest version available
|
||||
Map<String, SemVer> presentOrProvided = new HashMap<>();
|
||||
presentMods.forEach((s, modProperties) -> presentOrProvided.put(s, modProperties.version()));
|
||||
provides.forEach((s, ver) -> {
|
||||
if (presentMods.containsKey(s)) {
|
||||
if (presentMods.get(s).version().compareTo(ver.version()) < 0) {
|
||||
presentOrProvided.replace(s, ver.version());
|
||||
}
|
||||
} else {
|
||||
presentOrProvided.put(s, ver.version());
|
||||
}
|
||||
});
|
||||
|
||||
// Step 2: look for breakage declarations
|
||||
Collection<BreakingModException.Entry> breaks = new HashSet<>();
|
||||
for (Map.Entry<String, DependencyEntry> e : breakings.entrySet()) {
|
||||
String key = e.getKey();
|
||||
DependencyEntry dependencyEntry = e.getValue();
|
||||
if (presentOrProvided.containsKey(key) && dependencyEntry.range.versionMatches(presentOrProvided.get(key))) {
|
||||
breaks.add(new BreakingModException.Entry(dependencyEntry.origin(), Optional.ofNullable(presentMods.get(key)).orElseGet(() -> provides.get(key).source()), dependencyEntry.range));
|
||||
}
|
||||
}
|
||||
|
||||
if (!breaks.isEmpty()) {
|
||||
breaks.forEach(e ->
|
||||
LOGGER.error("Mod {} ({}) breaks with mod {} ({}) for versions matching {}", e.source().id(), e.source().name(), e.broken().id(), e.broken().name(), e.range()));
|
||||
throw new BreakingModException(breaks);
|
||||
}
|
||||
|
||||
|
||||
Set<ModProperties> result = new HashSet<>();
|
||||
|
||||
|
||||
// Step 3: print out information about suggested mods
|
||||
for (Map.Entry<String, DependencyEntry> e : suggests.entrySet()) {
|
||||
String key = e.getKey();
|
||||
DependencyEntry v = e.getValue();
|
||||
if (!presentOrProvided.containsKey(key) || !v.range.versionMatches(presentOrProvided.get(key))) {
|
||||
LOGGER.info("Mod '{}' ({}) suggests range {} of {}, you should install a matching version for the optimal experience!",
|
||||
v.origin().id(), v.origin().name(), v.range(), key);
|
||||
}
|
||||
}
|
||||
|
||||
// Step 4.1: Add all mods to the result that do not depend on any other mods
|
||||
presentMods.forEach((s, modProperties) -> {
|
||||
if (modProperties.dependencies().getForType(ModDependencies.Type.DEPEND).isEmpty()) {
|
||||
result.add(modProperties);
|
||||
}
|
||||
});
|
||||
|
||||
Collection<UnfulfilledDependencyException.Entry> unfulfilled = new HashSet<>();
|
||||
// Step 4.2: Check that all dependencies are satisfied by present or provided mods.
|
||||
for (Map.Entry<String, DependencyEntry> entry : dependencies.entrySet()) {
|
||||
String s = entry.getKey();
|
||||
DependencyEntry value = entry.getValue();
|
||||
if (!(presentMods.containsKey(s) && value.range.versionMatches(presentOrProvided.get(s)))) { // The dependency isn't fulfilled by any present mods
|
||||
if (!(provides.containsKey(s) && value.range.versionMatches(provides.get(s).version()))) { // The dependency also isn't fulfilled by any provided mods
|
||||
if (value.origin.extensions().getOrDefault(BuiltinExtensions.LOADING_TYPE, "required").equals("required")) {
|
||||
unfulfilled.add(new UnfulfilledDependencyException.Entry(value.origin, s, value.range, presentOrProvided.get(s), value.link(), value.name()));
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
// as there hasn't been thrown an exception the dependency must have been fulfilled, add the mod that set the dependency
|
||||
result.add(value.origin());
|
||||
}
|
||||
|
||||
if (!unfulfilled.isEmpty()) {
|
||||
unfulfilled.forEach(e -> {
|
||||
if (e.presentVersion() == null) {
|
||||
LOGGER.error("Mod {} ({}) depends on mod {} with a version matching {} (No version available)", e.source().id(), e.source().name(), e.dependency(), e.range());
|
||||
} else {
|
||||
LOGGER.error("Mod {} ({}) depends on mod {} with a version matching {}, but a different version is present or provided: {}", e.source().id(), e.source().name(), e.dependency(), e.range(), e.presentVersion());
|
||||
}
|
||||
});
|
||||
throw new UnfulfilledDependencyException(unfulfilled);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private record ProvidedMod(String modId, SemVer version, ModProperties source) {
|
||||
}
|
||||
|
||||
@AllArgsConstructor
|
||||
private enum DependencyType {
|
||||
EQ("=", Object::equals),
|
||||
GE(">=", (a, b) -> a.compareTo(b) >= 0),
|
||||
LE("<=", (a, b) -> a.compareTo(b) <= 0),
|
||||
GT(">", (a, b) -> a.compareTo(b) > 0),
|
||||
LT("<", (a, b) -> a.compareTo(b) < 0),
|
||||
|
||||
;
|
||||
private final String prefix;
|
||||
private final TypeComparator comparator;
|
||||
|
||||
private static DependencyType of(String comparator) {
|
||||
return Arrays.stream(values()).filter(t -> t.prefix.equals(comparator)).findFirst().orElse(DependencyType.EQ);
|
||||
}
|
||||
|
||||
private interface TypeComparator {
|
||||
boolean compare(SemVer a, SemVer b);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Getter
|
||||
public static class BreakingModException extends Exception {
|
||||
private final Collection<Entry> breaks;
|
||||
|
||||
public BreakingModException(Collection<Entry> breaks) {
|
||||
super(breaks.stream()
|
||||
.map(e -> "Mod %s (%s) breaks with mod %s (%s) for versions matching %s".formatted(e.source().id(),
|
||||
e.source().name(), e.broken().id(), e.broken().name(), e.range()))
|
||||
.collect(Collectors.joining("\n")));
|
||||
this.breaks = breaks;
|
||||
}
|
||||
|
||||
public record Entry(ModProperties source, ModProperties broken, VersionRange range) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@Getter
|
||||
public static class UnfulfilledDependencyException extends Exception {
|
||||
private final Collection<Entry> dependencies;
|
||||
|
||||
public UnfulfilledDependencyException(Collection<Entry> dependencies) {
|
||||
super(dependencies.stream().map(e -> {
|
||||
if (e.presentVersion() == null) {
|
||||
return "Mod %s (%s) depends on mod %s with a version matching %s (No version available)".formatted(e.source().id(), e.source().name(), e.dependency(), e.range());
|
||||
} else {
|
||||
return "Mod %s (%s) depends on mod %s with a version matching %s, but a different version is present or provided: %s".formatted(e.source().id(), e.source().name(), e.dependency(), e.range(), e.presentVersion());
|
||||
}
|
||||
}).collect(Collectors.joining("\n")));
|
||||
this.dependencies = dependencies;
|
||||
}
|
||||
|
||||
public record Entry(ModProperties source, String dependency, VersionRange range, SemVer presentVersion,
|
||||
@Nullable String link, String dependencyName) {
|
||||
}
|
||||
}
|
||||
|
||||
public static class ResolverException extends Exception {
|
||||
public ResolverException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
private record DependencyEntry(VersionRange range, ModProperties origin, String link, String name) {
|
||||
|
||||
public DependencyEntry(String range, ModProperties origin, @Nullable String link, @Nullable String name) throws ResolverException {
|
||||
this(VersionRange.parse(range), origin, link, name);
|
||||
}
|
||||
}
|
||||
|
||||
private record Comparator(DependencyType type, SemVer version) {
|
||||
|
||||
public static Comparator parse(String comparator) {
|
||||
Matcher matcher = COMPARATOR.matcher(comparator);
|
||||
if (!matcher.find()) {
|
||||
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('.');
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
return new Comparator(type, SemVerImpl.parse(version.toString()));
|
||||
} catch (SemVerParseException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean versionMatches(SemVer version) {
|
||||
return type.comparator.compare(version, this.version);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return type.prefix + version;
|
||||
}
|
||||
|
||||
public SemVer maxCompatible() {
|
||||
return switch (type) {
|
||||
case GT -> null;
|
||||
case LT -> {
|
||||
if (version.patch() > 0) {
|
||||
yield new SemVerImpl(version.major(), version().minor(), version().patch() - 1, version.prerelease(), version.build());
|
||||
}
|
||||
if (version.minor() > 0) {
|
||||
yield new SemVerImpl(version.major(), version().minor() - 1, version().patch(), version.prerelease(), version.build());
|
||||
}
|
||||
yield new SemVerImpl(version.major() - 1, version().minor(), version().patch(), version.prerelease(), version.build());
|
||||
}
|
||||
default -> version;
|
||||
};
|
||||
}
|
||||
|
||||
public SemVer minCompatible() {
|
||||
return switch (type) {
|
||||
case LT -> {
|
||||
if (version.patch() > 0) {
|
||||
yield new SemVerImpl(version.major(), version().minor(), version().patch() - 1, version.prerelease(), version.build());
|
||||
}
|
||||
if (version.minor() > 0) {
|
||||
yield new SemVerImpl(version.major(), version().minor() - 1, version().patch(), version.prerelease(), version.build());
|
||||
}
|
||||
yield new SemVerImpl(version.major() - 1, version().minor(), version().patch(), version.prerelease(), version.build());
|
||||
}
|
||||
case GT -> {
|
||||
if (version.patch() > 0) {
|
||||
yield new SemVerImpl(version.major(), version().minor(), version().patch() + 1, version.prerelease(), version.build());
|
||||
}
|
||||
if (version.minor() > 0) {
|
||||
yield new SemVerImpl(version.major(), version().minor() + 1, version().patch(), version.prerelease(), version.build());
|
||||
}
|
||||
yield new SemVerImpl(version.major() + 1, version().minor(), version().patch(), version.prerelease(), version.build());
|
||||
}
|
||||
default -> version;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private record ComparatorSet(Collection<Comparator> comparators) {
|
||||
|
||||
public static ComparatorSet parse(String set) {
|
||||
String[] comparators = set.split(" ");
|
||||
return new ComparatorSet(Arrays.stream(comparators).map(Comparator::parse).toList());
|
||||
}
|
||||
|
||||
public boolean versionMatches(SemVer version) {
|
||||
return comparators.stream().allMatch(c -> c.versionMatches(version));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return comparators.stream().map(Objects::toString).collect(Collectors.joining(" "));
|
||||
}
|
||||
|
||||
public SemVer maxCompatible() {
|
||||
return comparators.stream().map(Comparator::maxCompatible).filter(Objects::nonNull).max(Comparable::compareTo).orElse(null);
|
||||
}
|
||||
|
||||
public SemVer minCompatible() {
|
||||
return comparators.stream().map(Comparator::minCompatible).filter(Objects::nonNull).min(Comparable::compareTo).orElse(null);
|
||||
}
|
||||
}
|
||||
|
||||
public record VersionRange(Collection<ComparatorSet> sets) {
|
||||
|
||||
private static final Pattern NUMBER_EXTRACTOR = Pattern.compile("[^~]?(\\d+)(?:\\.(?:([\\dxX*]+)(?:\\.([\\dxX*]+)?)?)?)?(.*)");
|
||||
|
||||
public static VersionRange parse(String range) throws ResolverException {
|
||||
|
||||
String[] sets = resolveAdvanced(range).split("\\|\\|");
|
||||
|
||||
return new VersionRange(Arrays.stream(sets).map(ComparatorSet::parse).toList());
|
||||
}
|
||||
|
||||
private static String resolveAdvanced(String range) throws ResolverException {
|
||||
if (range.isEmpty() || range.equals("*")) {
|
||||
return ">=0.0.0";
|
||||
}
|
||||
|
||||
List<String> list = extractRanges(range);
|
||||
|
||||
List<String> ranges = new LinkedList<>();
|
||||
for (int i = 0, listSize = list.size(); i < listSize; i++) {
|
||||
String s = list.get(i);
|
||||
|
||||
if (i < listSize - 1 && list.get(i + 1).equals("-")) {
|
||||
handleHyphenRange(s, ranges, list.get(i + 2));
|
||||
i += 2;
|
||||
} else if (s.startsWith("~")) {
|
||||
handleTilde(s, ranges);
|
||||
} else if (s.startsWith("^")) {
|
||||
handleCaret(s, ranges);
|
||||
} else if (s.contains("x") || s.contains("X") || s.contains("*")) {
|
||||
handleXRanges(s, ranges);
|
||||
} else {
|
||||
ranges.add(s);
|
||||
}
|
||||
}
|
||||
range = String.join("", ranges);
|
||||
|
||||
return range;
|
||||
}
|
||||
|
||||
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('.');
|
||||
}
|
||||
}
|
||||
ranges.add(builder.toString());
|
||||
}
|
||||
ranges.add(" ");
|
||||
|
||||
Matcher matcher = NUMBER_EXTRACTOR.matcher(s);
|
||||
if (!matcher.find()) {
|
||||
throw new ResolverException("Version " + s + " did not match the required pattern to find the numbers within!");
|
||||
}
|
||||
int major = Integer.parseInt(matcher.group(1));
|
||||
int minor = Optional.ofNullable(matcher.group(2)).map(Integer::parseInt).orElse(0);
|
||||
|
||||
if (minor > 0) {
|
||||
ranges.add("<" + major + "." + (minor + 1) + ".0");
|
||||
} else {
|
||||
ranges.add("<" + (major + 1) + ".0.0");
|
||||
}
|
||||
}
|
||||
|
||||
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('.');
|
||||
}
|
||||
}
|
||||
ranges.add(builder.toString());
|
||||
}
|
||||
|
||||
ranges.add(" ");
|
||||
|
||||
if (s.length() < 5) {
|
||||
Matcher matcher = NUMBER_EXTRACTOR.matcher(s);
|
||||
if (!matcher.find()) {
|
||||
throw new ResolverException("Version " + s + " did not match the required pattern to find the numbers within!");
|
||||
}
|
||||
int major = Integer.parseInt(matcher.group(1));
|
||||
int minor = Optional.ofNullable(matcher.group(2)).map(n -> n.equalsIgnoreCase("x") || n.equals("*") ? null : n).map(Integer::parseInt).orElse(0);
|
||||
int patch = Optional.ofNullable(matcher.group(3)).map(n -> n.equalsIgnoreCase("x") || n.equals("*") ? null : n).map(Integer::parseInt).orElse(0);
|
||||
StringBuilder builder = new StringBuilder("<");
|
||||
int[] ints = new int[]{major, minor, patch};
|
||||
for (int j = 0, intsLength = ints.length; j < intsLength; j++) {
|
||||
int x = ints[j];
|
||||
if (x < intsLength - 1 && ints[j + 1] > 0) {
|
||||
builder.append(x);
|
||||
} else {
|
||||
builder.append(x + 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
while (builder.length() < 6) {
|
||||
if (builder.charAt(builder.length() - 1) == '.') {
|
||||
builder.append('0');
|
||||
} else {
|
||||
builder.append('.');
|
||||
}
|
||||
}
|
||||
ranges.add(builder.toString());
|
||||
}
|
||||
}
|
||||
|
||||
private static void handleCaret(String s, List<String> ranges) throws ResolverException {
|
||||
Matcher matcher = NUMBER_EXTRACTOR.matcher(s);
|
||||
if (!matcher.find()) {
|
||||
throw new ResolverException("Version " + s + " did not match the required pattern to find the numbers within!");
|
||||
}
|
||||
int major = Integer.parseInt(matcher.group(1));
|
||||
int minor = Optional.ofNullable(matcher.group(2)).map(n -> n.equalsIgnoreCase("x") || n.equals("*") ? null : n).map(Integer::parseInt).orElse(0);
|
||||
int patch = Optional.ofNullable(matcher.group(3)).map(n -> n.equalsIgnoreCase("x") || n.equals("*") ? null : n).map(Integer::parseInt).orElse(0);
|
||||
String rest = matcher.group(4);
|
||||
|
||||
ranges.add(">=" + major + "." + minor + "." + patch + rest);
|
||||
ranges.add(" ");
|
||||
StringBuilder builder = new StringBuilder("<");
|
||||
for (int x : new int[]{major, minor, patch}) {
|
||||
if (x <= 0) {
|
||||
builder.append(x);
|
||||
} else {
|
||||
builder.append(x + 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
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('.');
|
||||
}
|
||||
}
|
||||
ranges.add(builder.toString());
|
||||
ranges.add(" ");
|
||||
}
|
||||
|
||||
if (end.length() < 5) {
|
||||
Matcher matcher = NUMBER_EXTRACTOR.matcher(end);
|
||||
if (!matcher.find()) {
|
||||
throw new ResolverException("Version " + s + " did not match the required pattern to find the numbers within!");
|
||||
}
|
||||
int major = Integer.parseInt(matcher.group(1));
|
||||
int minor = Optional.ofNullable(matcher.group(2)).map(Integer::parseInt).orElse(0);
|
||||
int patch = Optional.ofNullable(matcher.group(3)).map(Integer::parseInt).orElse(0);
|
||||
StringBuilder builder = new StringBuilder("<");
|
||||
int[] ints = new int[]{major, minor, patch};
|
||||
for (int j = 0, intsLength = ints.length; j < intsLength; j++) {
|
||||
int x = ints[j];
|
||||
if (x < intsLength - 1 && ints[j + 1] > 0) {
|
||||
builder.append(x);
|
||||
} else {
|
||||
builder.append(x + 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
while (builder.length() < 6) {
|
||||
if (builder.charAt(builder.length() - 1) == '.') {
|
||||
builder.append('0');
|
||||
} else {
|
||||
builder.append('.');
|
||||
}
|
||||
}
|
||||
ranges.add(builder.toString());
|
||||
} else {
|
||||
ranges.add("<=" + end);
|
||||
}
|
||||
}
|
||||
|
||||
private static @NotNull List<String> extractRanges(String range) {
|
||||
if (!range.contains(" ") && !range.contains(" - ") && !range.contains("||")) {
|
||||
return List.of(range);
|
||||
}
|
||||
List<String> parts = new ArrayList<>();
|
||||
for (String p : range.split(" - ")) {
|
||||
if (!parts.isEmpty()) {
|
||||
parts.add("-");
|
||||
}
|
||||
parts.add(p.trim());
|
||||
}
|
||||
List<String> moreParts = new ArrayList<>();
|
||||
parts.forEach(s -> {
|
||||
if (!s.contains("||")) {
|
||||
moreParts.add(s);
|
||||
return;
|
||||
}
|
||||
for (String p : s.split("\\|\\|")) {
|
||||
if (!moreParts.isEmpty()) {
|
||||
moreParts.add("||");
|
||||
}
|
||||
moreParts.add(p.trim());
|
||||
}
|
||||
});
|
||||
List<String> list = new ArrayList<>();
|
||||
moreParts.forEach(s -> {
|
||||
if (!s.contains(" ")) {
|
||||
list.add(s);
|
||||
return;
|
||||
}
|
||||
for (String p : s.split(" ")) {
|
||||
if (!list.isEmpty()) {
|
||||
list.add(" ");
|
||||
}
|
||||
list.add(p.trim());
|
||||
}
|
||||
});
|
||||
return list;
|
||||
}
|
||||
|
||||
public boolean versionMatches(SemVer version) {
|
||||
return sets.stream().anyMatch(c -> c.versionMatches(version));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return sets.stream().map(Objects::toString).collect(Collectors.joining(" || "));
|
||||
}
|
||||
|
||||
public Optional<SemVer> maxCompatible() {
|
||||
return sets.stream().map(ComparatorSet::maxCompatible).filter(Objects::nonNull).max(Comparable::compareTo);
|
||||
}
|
||||
|
||||
public Optional<SemVer> minCompatible() {
|
||||
return sets.stream().map(ComparatorSet::minCompatible).filter(Objects::nonNull).min(Comparable::compareTo);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -4,11 +4,11 @@ import java.util.Collection;
|
|||
import java.util.Map;
|
||||
|
||||
import dev.frogmc.frogloader.api.mod.ModDependencies;
|
||||
import dev.frogmc.frogloader.api.mod.SemVer;
|
||||
import dev.frogmc.frogloader.api.mod.ModExtensions;
|
||||
import dev.frogmc.frogloader.api.mod.ModProperties;
|
||||
import dev.frogmc.frogloader.api.mod.SemVer;
|
||||
|
||||
public record ModPropertiesImpl(String id, String name, SemVer version, String license,
|
||||
public record ModPropertiesImpl(String id, String name, String icon, SemVer version, String license,
|
||||
Map<String, Collection<String>> credits, ModDependencies dependencies,
|
||||
ModExtensions extensions) implements ModProperties {
|
||||
}
|
||||
|
|
|
@ -14,16 +14,15 @@ import com.electronwill.nightconfig.core.UnmodifiableConfig;
|
|||
import com.electronwill.nightconfig.core.file.FileNotFoundAction;
|
||||
import com.electronwill.nightconfig.toml.TomlParser;
|
||||
import dev.frogmc.frogloader.api.mod.ModDependencies;
|
||||
import dev.frogmc.frogloader.api.mod.ModExtensions;
|
||||
import dev.frogmc.frogloader.api.mod.ModProperties;
|
||||
import lombok.AllArgsConstructor;
|
||||
import dev.frogmc.frogloader.api.mod.ModExtensions;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class ModPropertiesReader {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(ModPropertiesReader.class);
|
||||
public static final String PROPERTIES_FILE_NAME = "frog.mod.toml";
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(ModPropertiesReader.class);
|
||||
private static final TomlParser PARSER = new TomlParser();
|
||||
|
||||
public static Optional<ModProperties> read(Path mod) {
|
||||
|
@ -60,10 +59,11 @@ public class ModPropertiesReader {
|
|||
V1_0_0("1.0.0", config -> {
|
||||
String id = config.get("frog.mod.id");
|
||||
String name = config.get("frog.mod.name");
|
||||
String icon = config.get("frog.mod.icon");
|
||||
String version = config.get("frog.mod.version");
|
||||
String license = config.get("frog.mod.license");
|
||||
|
||||
if (license == null){
|
||||
if (license == null) {
|
||||
license = "";
|
||||
}
|
||||
|
||||
|
@ -73,21 +73,28 @@ public class ModPropertiesReader {
|
|||
creditsList.forEach(c -> credits.put(c.get("name"), c.get("roles")));
|
||||
}
|
||||
|
||||
Collection<ModDependencies.Entry> depends = config.get("frog.dependencies.depends");
|
||||
if (depends == null){
|
||||
depends = Collections.emptySet();
|
||||
Collection<ModDependencies.Entry> depends = new HashSet<>();
|
||||
List<UnmodifiableConfig> dependsConfig = config.get("frog.dependencies.depends");
|
||||
if (dependsConfig != null) {
|
||||
dependsConfig.forEach(entry -> depends.add(new ModDependencies.Entry(entry.get("id"), entry.get("versions"), entry.get("link"), entry.get("name"))));
|
||||
}
|
||||
Collection<ModDependencies.Entry> breaks = config.get("frog.dependencies.breaks");
|
||||
if (breaks == null){
|
||||
breaks = Collections.emptySet();
|
||||
|
||||
Collection<ModDependencies.Entry> breaks = new HashSet<>();
|
||||
List<UnmodifiableConfig> breaksConfig = config.get("frog.dependencies.breaks");
|
||||
if (breaksConfig != null) {
|
||||
breaksConfig.forEach(entry -> breaks.add(new ModDependencies.Entry(entry.get("id"), entry.get("versions"), entry.get("link"), entry.get("name"))));
|
||||
}
|
||||
Collection<ModDependencies.Entry> suggests = config.get("frog.dependencies.suggests");
|
||||
if (suggests == null){
|
||||
suggests = Collections.emptySet();
|
||||
|
||||
Collection<ModDependencies.Entry> suggests = new HashSet<>();
|
||||
List<UnmodifiableConfig> suggestsConfig = config.get("frog.dependencies.suggests");
|
||||
if (suggestsConfig != null) {
|
||||
suggestsConfig.forEach(entry -> suggests.add(new ModDependencies.Entry(entry.get("id"), entry.get("versions"), entry.get("link"), entry.get("name"))));
|
||||
}
|
||||
Collection<ModDependencies.Entry> provides = config.get("frog.dependencies.provides");
|
||||
if (provides == null){
|
||||
provides = Collections.emptySet();
|
||||
|
||||
Collection<ModDependencies.Entry> provides = new HashSet<>();
|
||||
List<UnmodifiableConfig> providesConfig = config.get("frog.dependencies.provides");
|
||||
if (providesConfig != null) {
|
||||
providesConfig.forEach(entry -> provides.add(new ModDependencies.Entry(entry.get("id"), entry.get("version"), entry.get("link"), entry.get("name"))));
|
||||
}
|
||||
|
||||
UnmodifiableConfig extensionsConfig = config.get("frog.extensions");
|
||||
|
@ -96,15 +103,14 @@ public class ModPropertiesReader {
|
|||
extensionsConfig.entrySet().forEach(entry -> extensions.put(entry.getKey(), entry.getValue()));
|
||||
}
|
||||
|
||||
return new ModPropertiesImpl(id, name, SemVerImpl.parse(version), license, Collections.unmodifiableMap(credits), new ModDependencies(depends, breaks, suggests, provides), ModExtensions.of(extensions));
|
||||
return new ModPropertiesImpl(id, name, icon, SemVerImpl.parse(version), license, Collections.unmodifiableMap(credits), new ModDependencies(depends, breaks, suggests, provides), ModExtensions.of(extensions));
|
||||
});
|
||||
|
||||
private static final Map<String, Parser> versions = Arrays.stream(values()).collect(Collectors.toMap(v -> v.version, v -> v.parser));
|
||||
private final String version;
|
||||
private final Parser parser;
|
||||
|
||||
private static final Map<String, Parser> versions = Arrays.stream(values()).collect(Collectors.toMap(v -> v.version, v -> v.parser));
|
||||
|
||||
public static Parser get(String version){
|
||||
public static Parser get(String version) {
|
||||
return versions.get(version);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,14 +1,26 @@
|
|||
package dev.frogmc.frogloader.impl.mod;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.nio.file.Files;
|
||||
import java.util.Collection;
|
||||
|
||||
import dev.frogmc.frogloader.api.FrogLoader;
|
||||
import dev.frogmc.frogloader.api.mod.ModProperties;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class ModUtil {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(ModUtil.class);
|
||||
|
||||
public static String getModList(Collection<ModProperties> mods){
|
||||
StringBuilder builder = new StringBuilder();
|
||||
int size = mods.size();
|
||||
if (size == 0){
|
||||
return "No mods loaded.";
|
||||
}
|
||||
|
||||
StringBuilder builder = new StringBuilder();
|
||||
builder.append("Loaded ").append(size).append(" mod");
|
||||
if (size > 1){
|
||||
builder.append("s");
|
||||
|
@ -22,9 +34,17 @@ public class ModUtil {
|
|||
} else {
|
||||
builder.append("\\- ");
|
||||
}
|
||||
builder.append(p.name()).append(" (").append(p.id()).append(") ").append(" ").append(p.version());
|
||||
builder.append(p.id()).append(" (").append(p.name()).append(") ").append(" ").append(p.version());
|
||||
i++;
|
||||
}
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
public static void installMod(String url) {
|
||||
try {
|
||||
Files.copy(URI.create(url).toURL().openStream(), FrogLoader.getInstance().getModsDir().resolve(url.substring(url.lastIndexOf("/" + 1))));
|
||||
} catch (IOException e) {
|
||||
LOGGER.error("Failed to install mod:", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -37,10 +37,10 @@ public record SemVerImpl(int major, int minor, int patch, String prerelease, Str
|
|||
public String toString() {
|
||||
StringBuilder b = new StringBuilder();
|
||||
b.append(major).append(".").append(minor).append(".").append(patch);
|
||||
if (prerelease != null){
|
||||
if (prerelease != null) {
|
||||
b.append("-").append(prerelease);
|
||||
}
|
||||
if (build != null){
|
||||
if (build != null) {
|
||||
b.append("+").append(build);
|
||||
}
|
||||
return b.toString();
|
||||
|
@ -74,16 +74,20 @@ public record SemVerImpl(int major, int minor, int patch, String prerelease, Str
|
|||
}
|
||||
}
|
||||
|
||||
if (prerelease == null){
|
||||
return 0;
|
||||
}
|
||||
|
||||
String[] self = prerelease.split("\\.");
|
||||
String[] other = o.prerelease().split("\\.");
|
||||
|
||||
for (int index = 0;index<Math.min(self.length, other.length);index++){
|
||||
for (int index = 0; index < Math.min(self.length, other.length); index++) {
|
||||
boolean selfNumeric = self[index].matches("\\d+");
|
||||
boolean otherNumeric = other[index].matches("\\d+");
|
||||
if (selfNumeric != otherNumeric){
|
||||
if (selfNumeric != otherNumeric) {
|
||||
return selfNumeric ? -1 : 1;
|
||||
} else if (!selfNumeric){
|
||||
if ((i = self[index].compareTo(other[index])) != 0){
|
||||
} else if (!selfNumeric) {
|
||||
if ((i = self[index].compareTo(other[index])) != 0) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,25 +0,0 @@
|
|||
package dev.frogmc.frogloader.impl.plugin;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
import dev.frogmc.frogloader.api.mod.ModProperties;
|
||||
import dev.frogmc.frogloader.impl.FrogLoaderImpl;
|
||||
|
||||
public interface NonsensePlugin extends Runnable {
|
||||
|
||||
default void run() {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Whether this plugin is applicable to be loaded in the current environment
|
||||
*/
|
||||
default boolean isApplicable(){
|
||||
return false;
|
||||
}
|
||||
|
||||
default void init(FrogLoaderImpl loader) throws Exception {
|
||||
}
|
||||
|
||||
Collection<ModProperties> getMods();
|
||||
}
|
|
@ -11,35 +11,36 @@ import java.nio.file.*;
|
|||
import java.util.*;
|
||||
|
||||
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.BuiltinExtensions;
|
||||
import dev.frogmc.frogloader.impl.mod.ModPropertiesImpl;
|
||||
import dev.frogmc.frogloader.impl.mod.ModPropertiesReader;
|
||||
import dev.frogmc.frogloader.impl.plugin.NonsensePlugin;
|
||||
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 NonsensePlugin {
|
||||
public class Minecraft implements FrogPlugin {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger("Plugin/Minecraft");
|
||||
protected static final String[] MINECRAFT_CLASSES = new String[]{
|
||||
"net/minecraft/client/main/Main.class",
|
||||
"net/minecraft/client/MinecraftApplet.class",
|
||||
"net/minecraft/server/Main.class"
|
||||
};
|
||||
|
||||
protected final List<ModProperties> modProperties = new ArrayList<>();
|
||||
private String version;
|
||||
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() {
|
||||
|
@ -48,11 +49,11 @@ public class Minecraft implements NonsensePlugin {
|
|||
}
|
||||
|
||||
@Override
|
||||
public void init(FrogLoaderImpl loader) throws Exception {
|
||||
if (gamePath == null){
|
||||
public void init(FrogLoader loader) throws Exception {
|
||||
if (gamePath == null) {
|
||||
throw new IllegalStateException("Game not found yet!");
|
||||
}
|
||||
Path remappedGamePath = loader.getGameDir().resolve(".nonsense/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())) {
|
||||
try {
|
||||
|
@ -62,8 +63,9 @@ public class Minecraft implements NonsensePlugin {
|
|||
}
|
||||
}
|
||||
|
||||
modProperties.add(new ModPropertiesImpl("minecraft", "Minecraft",
|
||||
new MinecraftSemVerImpl(version), "MC-EULA",
|
||||
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())));
|
||||
|
@ -73,22 +75,29 @@ public class Minecraft implements NonsensePlugin {
|
|||
path.getFileName().toString().endsWith(FrogLoaderImpl.MOD_FILE_EXTENSION));
|
||||
Collection<URL> classpathMods = this.getClass().getClassLoader().resources(ModPropertiesReader.PROPERTIES_FILE_NAME).distinct().toList();
|
||||
|
||||
classpathMods.parallelStream().map(ModPropertiesReader::readFile).forEachOrdered(modProperties::add);
|
||||
classpathMods.stream().map(ModPropertiesReader::readFile).forEachOrdered(modProperties::add);
|
||||
Map<Path, ModProperties> modPaths = new HashMap<>();
|
||||
for (Path mod : new HashSet<>(mods)) {
|
||||
findJiJMods(mod, mods);
|
||||
findJiJMods(mod, mods, modPaths);
|
||||
}
|
||||
|
||||
mods.parallelStream().map(Path::toUri).map(uri -> {
|
||||
try {
|
||||
modProperties.retainAll(new ModDependencyResolver(modProperties).solve());
|
||||
} catch (ModDependencyResolver.BreakingModException e) {
|
||||
LoaderGui.builder().setContent(LoaderGui.ContentType.INFO_BREAKING_DEP, e).addReport(CrashReportGenerator.writeReport(e, modProperties)).build().show();
|
||||
} catch (ModDependencyResolver.UnfulfilledDependencyException e) {
|
||||
LoaderGui.builder().setContent(LoaderGui.ContentType.INFO_UNFULFILLED_DEP, e).addReport(CrashReportGenerator.writeReport(e, modProperties)).build().show();
|
||||
}
|
||||
|
||||
mods.stream().filter(p -> modProperties.contains(modPaths.get(p))).map(Path::toUri).map(uri -> {
|
||||
try {
|
||||
return uri.toURL();
|
||||
} catch (MalformedURLException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}).forEachOrdered(FrogLoaderImpl.getInstance().getClassloader()::addURL);
|
||||
}).forEach(FrogLoaderImpl.getInstance().getClassloader()::addURL);
|
||||
|
||||
// TODO respect mod dependencies and display errors appropriately
|
||||
|
||||
modProperties.parallelStream().forEach(props -> {
|
||||
modProperties.forEach(props -> {
|
||||
String name = props.extensions().get(BuiltinExtensions.MIXIN_CONFIG);
|
||||
if (name != null) {
|
||||
Mixins.addConfiguration(name);
|
||||
|
@ -107,21 +116,22 @@ public class Minecraft implements NonsensePlugin {
|
|||
FrogLoaderImpl.getInstance().getClassloader().addURL(runtimePath.toUri().toURL());
|
||||
}
|
||||
|
||||
protected void findJiJMods(Path mod, Collection<Path> mods) throws IOException {
|
||||
protected void findJiJMods(Path mod, Collection<Path> mods, Map<Path, ModProperties> modPaths) throws IOException {
|
||||
Optional<ModProperties> opt = ModPropertiesReader.read(mod);
|
||||
if (opt.isPresent()) {
|
||||
ModProperties p = opt.get();
|
||||
modProperties.add(p);
|
||||
modPaths.put(mod, p);
|
||||
List<List<Map<String, String>>> entries = p.extensions().getOrDefault(BuiltinExtensions.INCLUDED_JARS, Collections.emptyList());
|
||||
if (entries.isEmpty()){
|
||||
if (entries.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
try (FileSystem fs = FileSystems.newFileSystem(mod)){
|
||||
try (FileSystem fs = FileSystems.newFileSystem(mod)) {
|
||||
for (var jars : entries) {
|
||||
for (Map<String, String> jar : jars) {
|
||||
Path path = fs.getPath(jar.get("path"));
|
||||
Path path = fs.getPath(jar.get("path")).toAbsolutePath();
|
||||
mods.add(path);
|
||||
findJiJMods(path, mods);
|
||||
findJiJMods(path, mods, modPaths);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -135,7 +145,7 @@ public class Minecraft implements NonsensePlugin {
|
|||
|
||||
protected Path findGame() {
|
||||
LOGGER.info("Locating game..");
|
||||
String jar = System.getProperty("nonsense.plugin.minecraft.gameJar");
|
||||
String jar = System.getProperty(SystemProperties.MINECRAFT_GAME_JAR);
|
||||
if (jar != null) {
|
||||
Path p = Paths.get(jar);
|
||||
if (checkLocation(p)) {
|
||||
|
@ -176,7 +186,7 @@ public class Minecraft implements NonsensePlugin {
|
|||
public void run() {
|
||||
try {
|
||||
if (foundMainClass != null) {
|
||||
modProperties.parallelStream().forEach(props ->
|
||||
modProperties.forEach(props ->
|
||||
props.extensions().runIfPresent(PreLaunchExtension.ID,
|
||||
PreLaunchExtension.class, PreLaunchExtension::onPreLaunch));
|
||||
LOGGER.info("Launching main class: {}", foundMainClass);
|
||||
|
|
|
@ -1,28 +1,39 @@
|
|||
package dev.frogmc.frogloader.impl.plugin.game.minecraft;
|
||||
|
||||
import dev.frogmc.frogloader.api.mod.SemVer;
|
||||
import dev.frogmc.frogloader.impl.SemVerParseException;
|
||||
import dev.frogmc.frogloader.impl.mod.SemVerImpl;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
public class MinecraftSemVerImpl implements SemVer {
|
||||
|
||||
private final String version;
|
||||
MinecraftSemVerImpl(String version){
|
||||
|
||||
private MinecraftSemVerImpl(String version) {
|
||||
this.version = version;
|
||||
}
|
||||
|
||||
static SemVer get(String version) {
|
||||
try {
|
||||
return SemVerImpl.parse(version);
|
||||
} catch (SemVerParseException e) {
|
||||
return new MinecraftSemVerImpl(version);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int major() {
|
||||
throw new UnsupportedOperationException("Minecraft versions do not reliably have a major version");
|
||||
throw new UnsupportedOperationException("Minecraft version " + version + " does not represent a semver-compatible version");
|
||||
}
|
||||
|
||||
@Override
|
||||
public int minor() {
|
||||
throw new UnsupportedOperationException("Minecraft versions do not reliably have a minor version");
|
||||
throw new UnsupportedOperationException("Minecraft version " + version + " does not represent a semver-compatible version");
|
||||
}
|
||||
|
||||
@Override
|
||||
public int patch() {
|
||||
throw new UnsupportedOperationException("Minecraft versions do not reliably have a patch version");
|
||||
throw new UnsupportedOperationException("Minecraft version " + version + " does not represent a semver-compatible version");
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -37,7 +48,16 @@ public class MinecraftSemVerImpl implements SemVer {
|
|||
|
||||
@Override
|
||||
public int compareTo(@NotNull SemVer o) {
|
||||
throw new UnsupportedOperationException("Minecraft versions cannot be compared");
|
||||
// Best-effort comparison
|
||||
return version.compareTo(o.toString());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (obj instanceof SemVer) {
|
||||
return obj.toString().equals(version);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -0,0 +1,62 @@
|
|||
package dev.frogmc.frogloader.impl.util;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.PrintWriter;
|
||||
import java.io.StringWriter;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.time.*;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Collection;
|
||||
|
||||
import dev.frogmc.frogloader.api.mod.ModProperties;
|
||||
import dev.frogmc.frogloader.impl.FrogLoaderImpl;
|
||||
import dev.frogmc.frogloader.impl.mod.ModUtil;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class CrashReportGenerator {
|
||||
private static final DateTimeFormatter dateFormat = DateTimeFormatter.ofPattern("yyyy-MM-dd_HH-mm-ss_SSSS");
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(CrashReportGenerator.class);
|
||||
|
||||
public static Path writeReport(Throwable t) {
|
||||
return writeReport(t, FrogLoaderImpl.getInstance().getMods());
|
||||
}
|
||||
|
||||
public static Path writeReport(Throwable t, Collection<ModProperties> mods) {
|
||||
String fileName = "crash-"+dateFormat.format(ZonedDateTime.now())+"_frogloader.log";
|
||||
Path out = FrogLoaderImpl.getInstance().getGameDir().resolve("crash-reports").resolve(fileName);
|
||||
|
||||
try {
|
||||
String report = getForException(t, mods);
|
||||
Files.createDirectories(out.getParent());
|
||||
Files.writeString(out, report);
|
||||
LOGGER.info("Saved crash report to {}!", out.toAbsolutePath().toUri());
|
||||
System.out.println(report);
|
||||
} catch (IOException e) {
|
||||
LOGGER.error("Failed to save crash report!", e);
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
public static String getForException(Throwable t){
|
||||
return getForException(t, FrogLoaderImpl.getInstance().getMods());
|
||||
}
|
||||
|
||||
public static String getForException(Throwable t, Collection<ModProperties> mods) {
|
||||
StringBuilder builder = new StringBuilder("--- FrogLoader Crash Report ---");
|
||||
|
||||
builder.append("\n\n").append("Time: ").append(ZonedDateTime.now()).append(" (").append(ZonedDateTime.now(ZoneOffset.UTC)).append(" UTC)");
|
||||
builder.append("\n");
|
||||
builder.append("\n").append("Exception:");
|
||||
builder.append("\n");
|
||||
StringWriter writer = new StringWriter();
|
||||
t.printStackTrace(new PrintWriter(writer));
|
||||
builder.append(writer);
|
||||
builder.append("\n").append(ModUtil.getModList(mods));
|
||||
builder.append("\n\n").append("--- Report End ---");
|
||||
|
||||
return builder.toString();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
package dev.frogmc.frogloader.impl.util;
|
||||
|
||||
import lombok.experimental.UtilityClass;
|
||||
|
||||
@UtilityClass
|
||||
public class SystemProperties {
|
||||
|
||||
public final String MINECRAFT_GAME_JAR = "frogmc.plugin.minecraft.gameJar";
|
||||
public final String DEVELOPMENT = "frogmc.development";
|
||||
}
|
57
src/main/java/dev/frogmc/frogloader/impl/util/URLUtil.java
Normal file
57
src/main/java/dev/frogmc/frogloader/impl/util/URLUtil.java
Normal file
|
@ -0,0 +1,57 @@
|
|||
package dev.frogmc.frogloader.impl.util;
|
||||
|
||||
import java.awt.*;
|
||||
import java.awt.datatransfer.StringSelection;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.net.URI;
|
||||
import java.util.Locale;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class URLUtil {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(URLUtil.class);
|
||||
|
||||
public static void open(URI url) {
|
||||
|
||||
LOGGER.info("Opening: {}", url);
|
||||
|
||||
try {
|
||||
if (Desktop.getDesktop().isSupported(Desktop.Action.BROWSE)) {
|
||||
Desktop.getDesktop().browse(url);
|
||||
} else {
|
||||
ProcessBuilder builder = new ProcessBuilder("xdg-open", url.toString());
|
||||
builder.start();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
LOGGER.error("Failed to open url: ", e);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static void copyStringContent(URI url) {
|
||||
if (System.getenv().getOrDefault("DESKTOP_SESSION", "").toLowerCase(Locale.ROOT)
|
||||
.contains("wayland")) {
|
||||
try {
|
||||
ProcessBuilder builder = new ProcessBuilder("bash", "-c", "wl-copy < " + url);
|
||||
builder.start();
|
||||
} catch (IOException e) {
|
||||
LOGGER.error("Failed to copy contents of {}:", url, e);
|
||||
}
|
||||
} else {
|
||||
try (InputStream in = url.toURL().openStream();
|
||||
InputStreamReader inReader = new InputStreamReader(in);
|
||||
BufferedReader reader = new BufferedReader(inReader)) {
|
||||
String data = reader.lines().collect(Collectors.joining("\n"));
|
||||
Toolkit.getDefaultToolkit().getSystemClipboard().setContents(new StringSelection(data), null);
|
||||
} catch (IOException e) {
|
||||
LOGGER.error("Failed to copy contents of {}:", url, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
BIN
src/main/resources/duke.png
Normal file
BIN
src/main/resources/duke.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 9.1 KiB |
Loading…
Reference in a new issue