add crash report generation, add error handling, separate system properties
This commit is contained in:
parent
e688242ef5
commit
98848bafee
|
@ -15,8 +15,11 @@ import dev.frogmc.frogloader.api.FrogLoader;
|
||||||
import dev.frogmc.frogloader.api.env.Env;
|
import dev.frogmc.frogloader.api.env.Env;
|
||||||
import dev.frogmc.frogloader.api.mod.ModProperties;
|
import dev.frogmc.frogloader.api.mod.ModProperties;
|
||||||
import dev.frogmc.frogloader.api.plugin.NonsensePlugin;
|
import dev.frogmc.frogloader.api.plugin.NonsensePlugin;
|
||||||
|
import dev.frogmc.frogloader.impl.gui.LoaderGui;
|
||||||
import dev.frogmc.frogloader.impl.launch.MixinClassLoader;
|
import dev.frogmc.frogloader.impl.launch.MixinClassLoader;
|
||||||
import dev.frogmc.frogloader.impl.mod.ModUtil;
|
import dev.frogmc.frogloader.impl.mod.ModUtil;
|
||||||
|
import dev.frogmc.frogloader.impl.util.CrashReportGenerator;
|
||||||
|
import dev.frogmc.frogloader.impl.util.SystemProperties;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
@ -26,7 +29,7 @@ public class FrogLoaderImpl implements FrogLoader {
|
||||||
public static final String MOD_FILE_EXTENSION = ".frogmod";
|
public static final String MOD_FILE_EXTENSION = ".frogmod";
|
||||||
@Getter
|
@Getter
|
||||||
private static FrogLoaderImpl instance;
|
private static FrogLoaderImpl instance;
|
||||||
private final boolean DEV_ENV = Boolean.getBoolean("frogmc.development");
|
private final boolean DEV_ENV = Boolean.getBoolean(SystemProperties.DEVELOPMENT);
|
||||||
@Getter
|
@Getter
|
||||||
private final String[] args;
|
private final String[] args;
|
||||||
@Getter
|
@Getter
|
||||||
|
@ -45,8 +48,8 @@ public class FrogLoaderImpl implements FrogLoader {
|
||||||
@Getter
|
@Getter
|
||||||
private final Gson gson = new Gson();
|
private final Gson gson = new Gson();
|
||||||
|
|
||||||
private final Map<String, ModProperties> mods;
|
private Map<String, ModProperties> mods;
|
||||||
private final Collection<String> modIds;
|
private Collection<String> modIds;
|
||||||
|
|
||||||
|
|
||||||
private FrogLoaderImpl(String[] args, Env env) {
|
private FrogLoaderImpl(String[] args, Env env) {
|
||||||
|
@ -67,6 +70,7 @@ public class FrogLoaderImpl implements FrogLoader {
|
||||||
LOGGER.warn("Failed to create essential directories ", e);
|
LOGGER.warn("Failed to create essential directories ", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
discoverPlugins();
|
discoverPlugins();
|
||||||
advanceMixinState();
|
advanceMixinState();
|
||||||
mods = collectMods();
|
mods = collectMods();
|
||||||
|
@ -74,6 +78,9 @@ public class FrogLoaderImpl implements FrogLoader {
|
||||||
LOGGER.info(ModUtil.getModList(mods.values()));
|
LOGGER.info(ModUtil.getModList(mods.values()));
|
||||||
LOGGER.info("Launching...");
|
LOGGER.info("Launching...");
|
||||||
plugins.forEach(NonsensePlugin::run);
|
plugins.forEach(NonsensePlugin::run);
|
||||||
|
} catch (Throwable t){
|
||||||
|
LoaderGui.builder().setContent(LoaderGui.ContentType.GENERIC_ERROR, t).addReport(CrashReportGenerator.writeReport(t, collectMods().values())).build().show();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
|
@ -157,4 +164,8 @@ public class FrogLoaderImpl implements FrogLoader {
|
||||||
private Collection<String> collectModIds() {
|
private Collection<String> collectModIds() {
|
||||||
return mods.keySet();
|
return mods.keySet();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Collection<ModProperties> getMods(){
|
||||||
|
return mods.values();
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -4,17 +4,24 @@ 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.ActionEvent;
|
import java.awt.event.ActionEvent;
|
||||||
|
import java.io.PrintWriter;
|
||||||
|
import java.io.StringWriter;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
|
import java.nio.file.Path;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
import java.util.function.BiConsumer;
|
import java.util.function.BiConsumer;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
import dev.frogmc.frogloader.impl.FrogLoaderImpl;
|
import dev.frogmc.frogloader.impl.FrogLoaderImpl;
|
||||||
import dev.frogmc.frogloader.impl.mod.ModDependencyResolver;
|
import dev.frogmc.frogloader.impl.mod.ModDependencyResolver;
|
||||||
import dev.frogmc.frogloader.impl.mod.ModUtil;
|
import dev.frogmc.frogloader.impl.mod.ModUtil;
|
||||||
import dev.frogmc.frogloader.impl.util.URLUtil;
|
import dev.frogmc.frogloader.impl.util.URLUtil;
|
||||||
|
import lombok.AccessLevel;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
|
|
||||||
|
@AllArgsConstructor(access = AccessLevel.PRIVATE)
|
||||||
public class LoaderGui {
|
public class LoaderGui {
|
||||||
|
|
||||||
public static Builder builder(){
|
public static Builder builder(){
|
||||||
|
@ -23,39 +30,96 @@ public class LoaderGui {
|
||||||
|
|
||||||
public static class Builder {
|
public static class Builder {
|
||||||
|
|
||||||
private final JFrame frame = getFrame();
|
private Path report;
|
||||||
|
private Consumer<JFrame> contentFunc;
|
||||||
|
private boolean keepRunning = false;
|
||||||
|
|
||||||
public <T> Builder setContent(ContentType<T> type, T argument){
|
public <T> Builder setContent(ContentType<T> type, T argument){
|
||||||
type.contentSetter.accept(frame, 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;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public LoaderGui build(){
|
public LoaderGui build(){
|
||||||
return new LoaderGui(frame);
|
JFrame frame = getFrame(report);
|
||||||
|
if (contentFunc != null) {
|
||||||
|
contentFunc.accept(frame);
|
||||||
|
}
|
||||||
|
return new LoaderGui(frame, keepRunning);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private final JFrame frame;
|
private final JFrame frame;
|
||||||
private LoaderGui(JFrame frame){
|
private final boolean keepRunning;
|
||||||
this.frame = frame;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static JFrame getFrame(){
|
private static JFrame getFrame(Path report){
|
||||||
var frame = new JFrame();
|
var frame = new JFrame();
|
||||||
frame.setTitle("FrogLoader " + LoaderGui.class.getPackage().getImplementationVersion());
|
frame.setTitle("FrogLoader " + LoaderGui.class.getPackage().getImplementationVersion());
|
||||||
frame.setSize(952, 560);
|
frame.setSize(952, 560);
|
||||||
frame.setLocationRelativeTo(null);
|
frame.setLocationRelativeTo(null);
|
||||||
frame.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
|
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) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
actions.add(new JButton(new AbstractAction("Exit") {
|
||||||
|
@Override
|
||||||
|
public void actionPerformed(ActionEvent e) {
|
||||||
|
frame.dispose();
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
frame.add(actions, BorderLayout.SOUTH);
|
||||||
return frame;
|
return frame;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("BusyWait")
|
||||||
public void show(){
|
public void show(){
|
||||||
frame.setVisible(true);
|
frame.setVisible(true);
|
||||||
|
|
||||||
|
while (frame.isVisible()) {
|
||||||
|
try {
|
||||||
|
Thread.sleep(100);
|
||||||
|
} catch (InterruptedException ignored) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!keepRunning) {
|
||||||
|
System.exit(0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
public static class ContentType<T> {
|
public static class ContentType<T> {
|
||||||
public static ContentType<ModDependencyResolver.UnfulfilledDependencyException> INFO_UNFULFILLED_DEP = new ContentType<>((frame, ex) -> {
|
public static final ContentType<ModDependencyResolver.UnfulfilledDependencyException> INFO_UNFULFILLED_DEP = new ContentType<>((frame, ex) -> {
|
||||||
JPanel pane = new JPanel(new BorderLayout());
|
JPanel pane = new JPanel(new BorderLayout());
|
||||||
JTextPane title = new JTextPane();
|
JTextPane title = new JTextPane();
|
||||||
title.setContentType("text/html");
|
title.setContentType("text/html");
|
||||||
|
@ -67,13 +131,18 @@ public class LoaderGui {
|
||||||
pane.add(title, BorderLayout.NORTH);
|
pane.add(title, BorderLayout.NORTH);
|
||||||
|
|
||||||
JPanel list = new JPanel();
|
JPanel list = new JPanel();
|
||||||
|
list.setLayout(new BoxLayout(list, BoxLayout.Y_AXIS));
|
||||||
ex.getDependencies().forEach(e -> {
|
ex.getDependencies().forEach(e -> {
|
||||||
String description;
|
StringBuilder description = new StringBuilder();
|
||||||
if (e.presentVersion() != null){
|
if (e.presentVersion() != null){
|
||||||
description = String.format("Mod %s (%s) depends on a Mod with id %s with a version matching %s, but a different version is present or provided: %s", e.source().id(), e.source().name(), e.dependency(), printVersionRange(e.range()), e.presentVersion());
|
description.append("Mod %s (%s) depends on a Mod with id %s with a version matching %s, but a different version is present or provided: %s".formatted(e.source().id(), e.source().name(), e.dependency(), printVersionRange(e.range()), e.presentVersion()));
|
||||||
} else {
|
} else {
|
||||||
description = String.format("Mod %s (%s) depends on a Mod with id %s with a version matching %s. \nNo version is currently available.", e.source().id(), e.source().name(), e.dependency(), printVersionRange(e.range()));
|
description.append(String.format("Mod %s (%s) depends on a Mod with id %s with a version matching %s. \nNo version is currently available.", e.source().id(), e.source().name(), e.dependency(), printVersionRange(e.range())));
|
||||||
}
|
}
|
||||||
|
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 Mod with id ").append(e.dependency());
|
||||||
List<JButton> actions = new ArrayList<>();
|
List<JButton> actions = new ArrayList<>();
|
||||||
if (e.link() != null) {
|
if (e.link() != null) {
|
||||||
boolean install = e.link().endsWith(FrogLoaderImpl.MOD_FILE_EXTENSION);
|
boolean install = e.link().endsWith(FrogLoaderImpl.MOD_FILE_EXTENSION);
|
||||||
|
@ -90,14 +159,13 @@ public class LoaderGui {
|
||||||
});
|
});
|
||||||
actions.add(urlButton);
|
actions.add(urlButton);
|
||||||
}
|
}
|
||||||
JPanel entry = getEntry(description, list.getBackground(), actions);
|
JPanel entry = getEntry(description.toString(), list.getBackground(), actions);
|
||||||
entry.setSize(list.getWidth(), entry.getHeight());
|
|
||||||
list.add(entry);
|
list.add(entry);
|
||||||
});
|
});
|
||||||
pane.add(new JScrollPane(list));
|
pane.add(new JScrollPane(list));
|
||||||
frame.add(pane);
|
frame.add(pane);
|
||||||
});
|
});
|
||||||
public static ContentType<ModDependencyResolver.BreakingModException> INFO_BREAKING_DEP = new ContentType<>((frame, ex) -> {
|
public static final ContentType<ModDependencyResolver.BreakingModException> INFO_BREAKING_DEP = new ContentType<>((frame, ex) -> {
|
||||||
JPanel pane = new JPanel(new BorderLayout());
|
JPanel pane = new JPanel(new BorderLayout());
|
||||||
JTextPane title = new JTextPane();
|
JTextPane title = new JTextPane();
|
||||||
title.setContentType("text/html");
|
title.setContentType("text/html");
|
||||||
|
@ -109,8 +177,13 @@ public class LoaderGui {
|
||||||
pane.add(title, BorderLayout.NORTH);
|
pane.add(title, BorderLayout.NORTH);
|
||||||
|
|
||||||
JPanel list = new JPanel();
|
JPanel list = new JPanel();
|
||||||
|
list.setLayout(new BoxLayout(list, BoxLayout.Y_AXIS));
|
||||||
ex.getBreaks().forEach(e -> {
|
ex.getBreaks().forEach(e -> {
|
||||||
String description = String.format("Mod %s (%s) breaks with mod %s (%s) for versions matching %s \n(present: %s)", e.source().id(), e.source().name(), e.broken().id(), e.broken().name(), printVersionRange(e.range()), e.broken().version());
|
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<>();
|
List<JButton> actions = new ArrayList<>();
|
||||||
|
|
||||||
JPanel entry = getEntry(description, list.getBackground(), actions);
|
JPanel entry = getEntry(description, list.getBackground(), actions);
|
||||||
|
@ -120,6 +193,27 @@ public class LoaderGui {
|
||||||
pane.add(new JScrollPane(list));
|
pane.add(new JScrollPane(list));
|
||||||
frame.add(pane);
|
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.setContentType("text/html");
|
||||||
|
title.setBackground(pane.getBackground());
|
||||||
|
title.setEditable(false);
|
||||||
|
title.setText("<html><h3>Caught Fatal Error during game startup:</h3></html>");
|
||||||
|
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 final BiConsumer<JFrame, T> contentSetter;
|
||||||
}
|
}
|
||||||
|
@ -135,7 +229,7 @@ public class LoaderGui {
|
||||||
text.setContentType("text/html");
|
text.setContentType("text/html");
|
||||||
text.setEditable(false);
|
text.setEditable(false);
|
||||||
text.setBackground(background);
|
text.setBackground(background);
|
||||||
text.setText("<html>" + description.replace("\n", "<br>") + "</html>");
|
text.setText("<html>" + description.replace("<", "<").replace("\n", "<br>") + "</html>");
|
||||||
JPanel actionPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT));
|
JPanel actionPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT));
|
||||||
actions.forEach(actionPanel::add);
|
actions.forEach(actionPanel::add);
|
||||||
entry.add(text);
|
entry.add(text);
|
||||||
|
|
|
@ -139,7 +139,8 @@ public class ModDependencyResolver {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private record ProvidedMod(String modId, SemVer version, ModProperties source){}
|
private record ProvidedMod(String modId, SemVer version, ModProperties source) {
|
||||||
|
}
|
||||||
|
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
private enum DependencyType {
|
private enum DependencyType {
|
||||||
|
@ -162,22 +163,42 @@ public class ModDependencyResolver {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@AllArgsConstructor
|
|
||||||
@Getter
|
@Getter
|
||||||
public static class BreakingModException extends Exception {
|
public static class BreakingModException extends Exception {
|
||||||
private final Collection<Entry> breaks;
|
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) {
|
public record Entry(ModProperties source, ModProperties broken, VersionRange range) {
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@AllArgsConstructor
|
|
||||||
@Getter
|
@Getter
|
||||||
public static class UnfulfilledDependencyException extends Exception {
|
public static class UnfulfilledDependencyException extends Exception {
|
||||||
private final Collection<Entry> dependencies;
|
private final Collection<Entry> dependencies;
|
||||||
|
|
||||||
public record Entry(ModProperties source, String dependency, VersionRange range, SemVer presentVersion, @Nullable String link){}
|
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) {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class ResolverException extends Exception {
|
public static class ResolverException extends Exception {
|
||||||
|
@ -226,6 +247,46 @@ public class ModDependencyResolver {
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return type.prefix + version;
|
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) {
|
private record ComparatorSet(Collection<Comparator> comparators) {
|
||||||
|
@ -243,6 +304,14 @@ public class ModDependencyResolver {
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return comparators.stream().map(Objects::toString).collect(Collectors.joining(" "));
|
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) {
|
public record VersionRange(Collection<ComparatorSet> sets) {
|
||||||
|
@ -483,5 +552,13 @@ public class ModDependencyResolver {
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return sets.stream().map(Objects::toString).collect(Collectors.joining(" || "));
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,12 +7,20 @@ import java.util.Collection;
|
||||||
|
|
||||||
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;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
public class ModUtil {
|
public class ModUtil {
|
||||||
|
|
||||||
|
private static final Logger LOGGER = LoggerFactory.getLogger(ModUtil.class);
|
||||||
|
|
||||||
public static String getModList(Collection<ModProperties> mods){
|
public static String getModList(Collection<ModProperties> mods){
|
||||||
StringBuilder builder = new StringBuilder();
|
|
||||||
int size = mods.size();
|
int size = mods.size();
|
||||||
|
if (size == 0){
|
||||||
|
return "No mods loaded.";
|
||||||
|
}
|
||||||
|
|
||||||
|
StringBuilder builder = new StringBuilder();
|
||||||
builder.append("Loaded ").append(size).append(" mod");
|
builder.append("Loaded ").append(size).append(" mod");
|
||||||
if (size > 1){
|
if (size > 1){
|
||||||
builder.append("s");
|
builder.append("s");
|
||||||
|
@ -36,8 +44,7 @@ public class ModUtil {
|
||||||
try {
|
try {
|
||||||
Files.copy(URI.create(url).toURL().openStream(), FrogLoader.getInstance().getModsDir().resolve(url.substring(url.lastIndexOf("/" + 1))));
|
Files.copy(URI.create(url).toURL().openStream(), FrogLoader.getInstance().getModsDir().resolve(url.substring(url.lastIndexOf("/" + 1))));
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
e.printStackTrace();
|
LOGGER.error("Failed to install mod:", e);
|
||||||
// TODO handle better than this
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -74,6 +74,10 @@ public record SemVerImpl(int major, int minor, int patch, String prerelease, Str
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (prerelease == null){
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
String[] self = prerelease.split("\\.");
|
String[] self = prerelease.split("\\.");
|
||||||
String[] other = o.prerelease().split("\\.");
|
String[] other = o.prerelease().split("\\.");
|
||||||
|
|
||||||
|
|
|
@ -22,6 +22,8 @@ import dev.frogmc.frogloader.impl.FrogLoaderImpl;
|
||||||
import dev.frogmc.frogloader.impl.gui.LoaderGui;
|
import dev.frogmc.frogloader.impl.gui.LoaderGui;
|
||||||
import dev.frogmc.frogloader.impl.mixin.AWProcessor;
|
import dev.frogmc.frogloader.impl.mixin.AWProcessor;
|
||||||
import dev.frogmc.frogloader.impl.mod.*;
|
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 dev.frogmc.thyroxine.Thyroxine;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
@ -82,13 +84,9 @@ public class Minecraft implements NonsensePlugin {
|
||||||
try {
|
try {
|
||||||
modProperties.retainAll(new ModDependencyResolver(modProperties).solve());
|
modProperties.retainAll(new ModDependencyResolver(modProperties).solve());
|
||||||
} catch (ModDependencyResolver.BreakingModException e) {
|
} catch (ModDependencyResolver.BreakingModException e) {
|
||||||
LoaderGui.builder().setContent(LoaderGui.ContentType.INFO_BREAKING_DEP, e).build().show();
|
LoaderGui.builder().setContent(LoaderGui.ContentType.INFO_BREAKING_DEP, e).addReport(CrashReportGenerator.writeReport(e, modProperties)).build().show();
|
||||||
throw e;
|
|
||||||
// TODO handle
|
|
||||||
} catch (ModDependencyResolver.UnfulfilledDependencyException e) {
|
} catch (ModDependencyResolver.UnfulfilledDependencyException e) {
|
||||||
LoaderGui.builder().setContent(LoaderGui.ContentType.INFO_UNFULFILLED_DEP, e).build().show();
|
LoaderGui.builder().setContent(LoaderGui.ContentType.INFO_UNFULFILLED_DEP, e).addReport(CrashReportGenerator.writeReport(e, modProperties)).build().show();
|
||||||
throw e;
|
|
||||||
// TODO handle (and display)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
mods.stream().filter(p -> modProperties.contains(modPaths.get(p))).map(Path::toUri).map(uri -> {
|
mods.stream().filter(p -> modProperties.contains(modPaths.get(p))).map(Path::toUri).map(uri -> {
|
||||||
|
@ -147,7 +145,7 @@ public class Minecraft implements NonsensePlugin {
|
||||||
|
|
||||||
protected Path findGame() {
|
protected Path findGame() {
|
||||||
LOGGER.info("Locating game..");
|
LOGGER.info("Locating game..");
|
||||||
String jar = System.getProperty("nonsense.plugin.minecraft.gameJar");
|
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)) {
|
||||||
|
|
|
@ -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";
|
||||||
|
}
|
|
@ -1,8 +1,14 @@
|
||||||
package dev.frogmc.frogloader.impl.util;
|
package dev.frogmc.frogloader.impl.util;
|
||||||
|
|
||||||
import java.awt.*;
|
import java.awt.*;
|
||||||
|
import java.awt.datatransfer.StringSelection;
|
||||||
|
import java.io.BufferedReader;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
@ -19,17 +25,33 @@ public class URLUtil {
|
||||||
if (Desktop.getDesktop().isSupported(Desktop.Action.BROWSE)) {
|
if (Desktop.getDesktop().isSupported(Desktop.Action.BROWSE)) {
|
||||||
Desktop.getDesktop().browse(url);
|
Desktop.getDesktop().browse(url);
|
||||||
} else {
|
} else {
|
||||||
throw new UnsupportedOperationException("Browse action is not supported");
|
|
||||||
}
|
|
||||||
} catch (IOException | UnsupportedOperationException e) {
|
|
||||||
ProcessBuilder builder = new ProcessBuilder("xdg-open", url.toString());
|
ProcessBuilder builder = new ProcessBuilder("xdg-open", url.toString());
|
||||||
try {
|
|
||||||
builder.start();
|
builder.start();
|
||||||
} catch (IOException ex) {
|
|
||||||
ex.addSuppressed(e);
|
|
||||||
LOGGER.error("Failed to open url: ", ex);
|
|
||||||
}
|
}
|
||||||
|
} 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue