feat: implement new, flow-based MDS
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
ci/woodpecker/push/docs Pipeline failed

This commit is contained in:
Johannes Frohnmeyer 2024-06-22 19:58:01 +02:00
parent 082a4f3b9c
commit 4945381030
Signed by: Johannes
GPG Key ID: E76429612C2929F4
37 changed files with 601 additions and 58 deletions

View File

@ -11,7 +11,7 @@ import java.util.LinkedList;
import java.util.List;
public class GList {
public static <T, TEx extends Exception, Reader extends SerializeReader<TEx, Reader>> List<T> read(Reader reader, ThrowingFunction<Reader, T, TEx> read) throws TEx {
public static <T, TEx extends Exception, Reader extends SerializeReader<TEx, ?>> List<T> read(Reader reader, ThrowingFunction<Reader, T, TEx> read) throws TEx {
if (reader.isLenient() && reader.peek() != Token.BEGIN_ARRAY) return List.of(read.apply(reader));
reader.beginArray();
List<T> res = new LinkedList<>();

View File

@ -9,6 +9,7 @@ import java.io.IOException;
public class InceptumEnvironmentInitializer {
public static void initialize() throws IOException {
HttpClient.scheduleOnlineCheck();
HotswapLoggerFinder.updateAllStrategies(InceptumEnvironmentInitializer::defaultFactory);
HttpClient.setUserAgent("jfmods/inceptum/" + BuildMetadata.VERSION);
InceptumConfig.load();

View File

@ -5,8 +5,7 @@ import io.gitlab.jfronny.commons.io.JFiles;
import io.gitlab.jfronny.commons.logger.SystemLoggerPlus;
import java.awt.*;
import java.io.File;
import java.io.IOException;
import java.io.*;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.file.FileSystem;
@ -46,8 +45,12 @@ public class Utils {
}
}
public static FileSystem openZipFile(Path zip, boolean create) throws IOException, URISyntaxException {
return JFiles.openZipFile(zip, create, SYSTEM_LOADER);
public static FileSystem openZipFile(Path zip, boolean create) throws IOException {
try {
return JFiles.openZipFile(zip, create, SYSTEM_LOADER);
} catch (URISyntaxException e) {
throw new IOException("Could not access file system", e);
}
}
@SuppressWarnings("unused") // Called through reflection from wrapper

View File

@ -1,5 +1,5 @@
[versions]
jf-commons = "1.7-SNAPSHOT"
jf-commons = "2.0.0-SNAPSHOT"
annotations = "24.0.1"
lwjgl = "3.3.2"
imgui = "1.86.10"
@ -34,6 +34,8 @@ javagi-adw = { module = "io.github.jwharm.javagi:adw", version.ref = "javagi" }
commons = { module = "io.gitlab.jfronny:commons", version.ref = "jf-commons" }
commons-http-client = { module = "io.gitlab.jfronny:commons-http-client", version.ref = "jf-commons" }
commons-http-server = { module = "io.gitlab.jfronny:commons-http-server", version.ref = "jf-commons" }
commons-flow = { module = "io.gitlab.jfronny:commons-flow", version.ref = "jf-commons" }
commons-flow-backend-unsafe = { module = "io.gitlab.jfronny:commons-flow-backend-unsafe", version.ref = "jf-commons" }
commons-io = { module = "io.gitlab.jfronny:commons-io", version.ref = "jf-commons" }
commons-logger = { module = "io.gitlab.jfronny:commons-logger", version.ref = "jf-commons" }
commons-serialize-json = { module = "io.gitlab.jfronny:commons-serialize-json", version.ref = "jf-commons" }

View File

@ -50,7 +50,7 @@ public class ModCommand extends Command {
return;
}
System.out.println("Scanning installed mods, this might take a while");
instance.mds().runOnce(ScanStage.UPDATECHECK, (path, mod) -> {
instance.mds().runOnce(ScanStage.ALL, (path, mod) -> {
boolean hasSources = !mod.getMetadata().sources().isEmpty();
boolean updatable = hasSources && mod.getMetadata().sources().values().stream().anyMatch(Optional::isPresent);
if (filterUpdatable && !updatable) return;

View File

@ -6,8 +6,13 @@ import org.gnome.gtk.Application
class InstanceSettingsWindow(val app: Application?, val instance: Instance) : SettingsWindow(app) {
init {
val claim = instance.mds.focus()
addTab(GeneralTab(this), "instance.settings.general", "preferences-other-symbolic")
addTab(ModsTab(this), "instance.settings.mods", "package-x-generic-symbolic")
addTab(ExportTab(this), "instance.settings.export", "send-to-symbolic")
onCloseRequest {
claim.close()
false
}
}
}

View File

@ -85,7 +85,7 @@ public class AddModWindow extends Window {
ImGui.text(mod.description());
ImGui.tableNextColumn();
boolean alreadyPresent = false;
for (Mod mdsMod : instance.getMods(ScanStage.CROSSREFERENCE)) {
for (Mod mdsMod : instance.mds().getMods()) {
alreadyPresent = mdsMod.getMetadata().sources().keySet().stream()
.anyMatch(s -> s instanceof ModrinthModSource ms && ms.getModId().equals(projectId));
if (alreadyPresent)

View File

@ -1,16 +1,20 @@
package io.gitlab.jfronny.inceptum.imgui.window.edit;
import imgui.ImGui;
import io.gitlab.jfronny.inceptum.common.Utils;
import io.gitlab.jfronny.inceptum.imgui.control.Tab;
import io.gitlab.jfronny.inceptum.imgui.window.GuiUtil;
import io.gitlab.jfronny.inceptum.imgui.window.Window;
import io.gitlab.jfronny.inceptum.launcher.LauncherEnv;
import io.gitlab.jfronny.inceptum.launcher.system.instance.Instance;
import java.io.Closeable;
import java.io.IOException;
import java.util.List;
public class InstanceEditWindow extends Window {
protected final Instance instance;
protected final Closeable focus;
private final List<Tab> tabs;
protected boolean reDownload = false;
protected boolean lastTabWasMods = false;
@ -19,6 +23,7 @@ public class InstanceEditWindow extends Window {
super(instance.getName() + " - Edit");
this.instance = instance;
this.instance.mds().start();
this.focus = instance.mds().focus();
this.tabs = List.of(
new GeneralTab(this),
new ArgumentsTab(this),
@ -46,6 +51,11 @@ public class InstanceEditWindow extends Window {
@Override
public void close() {
super.close();
try {
focus.close();
} catch (IOException e) {
Utils.LOGGER.error("Could not release focus on MDS", e);
}
if (reDownload) {
GuiUtil.reload(instance);
}

View File

@ -6,4 +6,6 @@ plugins {
dependencies {
api(projects.common)
api(libs.commons.http.server) // required for launcher-gtk for some reason
api(libs.commons.flow)
api(libs.commons.flow.backend.unsafe)
}

View File

@ -6,11 +6,11 @@ import io.gitlab.jfronny.commons.serialize.SerializeWriter;
import io.gitlab.jfronny.inceptum.launcher.api.account.MicrosoftAccount;
public class MicrosoftAccountAdapter {
public static <TEx extends Exception, Writer extends SerializeWriter<TEx, Writer>> void serialize(MicrosoftAccount value, Writer writer) throws TEx, MalformedDataException {
public static <TEx extends Exception, Writer extends SerializeWriter<TEx, ?>> void serialize(MicrosoftAccount value, Writer writer) throws TEx, MalformedDataException {
GC_MicrosoftAccountMeta.serialize(value == null ? null : value.toMeta(), writer);
}
public static <TEx extends Exception, Reader extends SerializeReader<TEx, Reader>> MicrosoftAccount deserialize(Reader reader) throws TEx, MalformedDataException {
public static <TEx extends Exception, Reader extends SerializeReader<TEx, ?>> MicrosoftAccount deserialize(Reader reader) throws TEx, MalformedDataException {
MicrosoftAccountMeta meta = GC_MicrosoftAccountMeta.deserialize(reader);
return meta == null ? null : new MicrosoftAccount(meta);
}

View File

@ -11,11 +11,11 @@ import java.util.List;
import java.util.Set;
public class MinecraftArgumentAdapter {
public static <TEx extends Exception, Writer extends SerializeWriter<TEx, Writer>> void serialize(MinecraftArgument rules, Writer writer) throws TEx {
public static <TEx extends Exception, Writer extends SerializeWriter<TEx, ?>> void serialize(MinecraftArgument rules, Writer writer) throws TEx {
throw new UnsupportedOperationException();
}
public static <TEx extends Exception, Reader extends SerializeReader<TEx, Reader>> MinecraftArgument deserialize(Reader reader) throws TEx, MalformedDataException {
public static <TEx extends Exception, Reader extends SerializeReader<TEx, ?>> MinecraftArgument deserialize(Reader reader) throws TEx, MalformedDataException {
if (reader.peek() == Token.STRING) return new MinecraftArgument(Set.of(reader.nextString()));
Rules rules = null;
List<String> value = null;

View File

@ -10,13 +10,13 @@ import io.gitlab.jfronny.inceptum.launcher.system.source.ModSource;
import java.util.Optional;
public class ModMetaSourcesAdapter {
public static <TEx extends Exception, Writer extends SerializeWriter<TEx, Writer>> void serialize(Sources value, Writer writer) throws TEx, MalformedDataException {
public static <TEx extends Exception, Writer extends SerializeWriter<TEx, ?>> void serialize(Sources value, Writer writer) throws TEx, MalformedDataException {
writer.beginArray();
for (ModSource source : value.keySet()) GC_ModSource.serialize(source, writer);
writer.endArray();
}
public static <TEx extends Exception, Reader extends SerializeReader<TEx, Reader>> Sources deserialize(Reader reader) throws TEx, MalformedDataException {
public static <TEx extends Exception, Reader extends SerializeReader<TEx, ?>> Sources deserialize(Reader reader) throws TEx, MalformedDataException {
reader.beginArray();
Sources sources = new Sources();
while (reader.hasNext()) {

View File

@ -10,7 +10,7 @@ import java.util.LinkedHashSet;
import java.util.Set;
public class ModSourceAdapter {
public static <TEx extends Exception, Writer extends SerializeWriter<TEx, Writer>> void serialize(ModSource src, Writer writer) throws TEx {
public static <TEx extends Exception, Writer extends SerializeWriter<TEx, ?>> void serialize(ModSource src, Writer writer) throws TEx {
writer.beginObject();
switch (src) {
case ModrinthModSource mo -> writer.name("type").value("modrinth")
@ -41,7 +41,7 @@ public class ModSourceAdapter {
writer.endObject();
}
public static <TEx extends Exception, Reader extends SerializeReader<TEx, Reader>> ModSource deserialize(Reader reader) throws TEx, MalformedDataException {
public static <TEx extends Exception, Reader extends SerializeReader<TEx, ?>> ModSource deserialize(Reader reader) throws TEx, MalformedDataException {
String type = null;
String mr$id = null;

View File

@ -7,11 +7,11 @@ import io.gitlab.jfronny.commons.serialize.SerializeWriter;
import io.gitlab.jfronny.inceptum.launcher.model.mojang.Rules;
public class RulesAdapter {
public static <TEx extends Exception, Writer extends SerializeWriter<TEx, Writer>> void serialize(Rules rules, Writer writer) throws TEx {
public static <TEx extends Exception, Writer extends SerializeWriter<TEx, ?>> void serialize(Rules rules, Writer writer) throws TEx {
throw new UnsupportedOperationException();
}
public static <TEx extends Exception, Reader extends SerializeReader<TEx, Reader>> Rules deserialize(Reader reader) throws TEx, MalformedDataException {
public static <TEx extends Exception, Reader extends SerializeReader<TEx, ?>> Rules deserialize(Reader reader) throws TEx, MalformedDataException {
boolean valid = true;
reader.beginArray();
while (reader.hasNext()) {
@ -49,7 +49,7 @@ public class RulesAdapter {
throw new MalformedDataException("Unexpected action in argument: " + actionType);
}
if (hasFeatures) valid = false;
if (osName != null && !OSUtils.TYPE.getMojName().equals(osName)) valid = false;
if (osName != null && !OSUtils.TYPE.mojName.equals(osName)) valid = false;
if (osVersion != null && !System.getProperty("os.version").matches(osVersion)) valid = false;
if (actionType.equals("disallow")) valid = !valid;
}

View File

@ -98,18 +98,17 @@ public record ModMeta(
return res;
}
public boolean updateCheck(String gameVersion) {
public boolean crossReference() {
boolean modrinth = false;
boolean curseforge = false;
for (ModSource source : sources.keySet().toArray(ModSource[]::new)) {
if (source instanceof ModrinthModSource) modrinth = true;
if (source instanceof CurseforgeModSource) curseforge = true;
checkAndAddSource(source, gameVersion);
}
boolean changed = false;
if (!modrinth) {
try {
checkAndAddSource(new ModrinthModSource(ModrinthApi.getVersionByHash(sha1).id()), gameVersion);
sources.put(new ModrinthModSource(ModrinthApi.getVersionByHash(sha1).id()), Optional.empty());
changed = true;
} catch (IOException e) {
// not found
@ -120,7 +119,7 @@ public record ModMeta(
FingerprintMatchesResponse.Result cf = CurseforgeApi.checkFingerprint(murmur2);
if (!cf.exactMatches().isEmpty()) {
FingerprintMatchesResponse.Result.Match f = cf.exactMatches().getFirst();
checkAndAddSource(new CurseforgeModSource(f.id(), f.file().id()), gameVersion);
sources.put(new CurseforgeModSource(f.id(), f.file().id()), Optional.empty());
changed = true;
}
} catch (IOException | URISyntaxException e) {
@ -130,6 +129,12 @@ public record ModMeta(
return changed;
}
public void updateCheck(String gameVersion) {
for (ModSource source : sources.keySet().toArray(ModSource[]::new)) {
checkAndAddSource(source, gameVersion);
}
}
private void checkAndAddSource(ModSource source, String gameVersion) {
try {
sources.put(source, source.getUpdate(gameVersion));

View File

@ -9,7 +9,6 @@ import io.gitlab.jfronny.inceptum.launcher.util.ProcessState;
import io.gitlab.jfronny.inceptum.launcher.util.gitignore.IgnoringWalk;
import java.io.IOException;
import java.net.URISyntaxException;
import java.nio.file.*;
import java.util.Objects;
@ -37,7 +36,7 @@ public abstract class Exporter<Manifest> {
Manifest manifest = generateManifests(root, instance, instance.meta().instanceVersion);
if (instance.isFabric()) {
state.incrementStep("Adding mods");
addMods(root, instance, instance.getMods(ScanStage.CROSSREFERENCE).stream().filter(mod -> {
addMods(root, instance, instance.completeModsScan(ScanStage.CROSSREFERENCE).stream().filter(mod -> {
if (!mod.isEnabled()) return false;
state.updateStep(mod.getName());
return true;
@ -58,9 +57,6 @@ public abstract class Exporter<Manifest> {
}
state.incrementStep("Cleaning up");
Files.walkFileTree(root, new CleanupFileVisitor());
} catch (URISyntaxException se) {
// Can only be thrown by openZipFile, the target file therefore cannot exist
throw new IOException("Could not open export path", se);
} catch (Throwable t) {
if (Files.exists(exportPath)) Files.delete(exportPath);
throw t;

View File

@ -28,8 +28,6 @@ public class Importers {
return importer.importPack(fs.getPath("."), state);
}
}
} catch (URISyntaxException e) {
throw new IOException("Could not open zip", e);
}
throw new IOException("Could not import pack: unsupported format");
}

View File

@ -75,8 +75,10 @@ public record Instance(String id, Path path, InstanceMeta meta, ModsDirScanner m
return path.resolve("config");
}
public Set<Mod> getMods(ScanStage stage) throws IOException {
mds.runOnce(stage, R::nop);
public Set<Mod> completeModsScan(ScanStage stage) throws IOException {
try (var focus = mds.focus()) {
mds.runOnce(stage, R::nop);
}
return mds.getMods();
}

View File

@ -19,7 +19,6 @@ import io.gitlab.jfronny.inceptum.launcher.util.VersionInfoLibraryResolver;
import java.io.File;
import java.io.IOException;
import java.net.URISyntaxException;
import java.nio.file.*;
import java.util.*;
import java.util.concurrent.atomic.AtomicReference;
@ -101,7 +100,7 @@ public class InstanceLauncher {
// Fabric imods
if (instance.isFabric()) {
StringBuilder fabricAddMods = new StringBuilder("-Dfabric.addMods=");
for (Mod mod : instance.getMods(ScanStage.DOWNLOAD)) {
for (Mod mod : instance.completeModsScan(ScanStage.DOWNLOAD)) {
if (mod.isEnabled() && mod.getNeedsInject()) {
fabricAddMods.append(mod.getJarPath());
fabricAddMods.append(File.pathSeparatorChar);
@ -197,7 +196,7 @@ public class InstanceLauncher {
return line.substring(linePrefix.length());
}
}
} catch (IOException | URISyntaxException e) {
} catch (IOException e) {
throw new LaunchException("IO Exception while trying to identify entrypoint", e);
}
throw new LaunchException("Could not identify entrypoint");

View File

@ -21,29 +21,31 @@ import java.util.stream.Collectors;
public class MdsMod extends Mod {
private final ProtoInstance instance;
private final Path imodPath;
private final Path initialJarPath;
private final ModMeta meta;
private @NotNull ScanStage scanStage = ScanStage.DISCOVER;
private @Nullable DownloadAux downloadAux = null;
public MdsMod(ProtoInstance instance, Path imodPath, ModMeta meta) {
public MdsMod(ProtoInstance instance, Path imodPath, Path initialJarPath, ModMeta meta) {
this.instance = instance;
this.imodPath = imodPath;
this.initialJarPath = initialJarPath;
this.meta = meta;
}
private record DownloadAux(Path jarPath, boolean managedJar, @Nullable FabricModJson fmj) {}
private record DownloadAux(Path imodPath, Path jarPath, boolean managedJar, @Nullable FabricModJson fmj) {}
public void markDownloaded(Path jarPath, boolean managedJar, @Nullable FabricModJson fmj) {
this.downloadAux = new DownloadAux(jarPath, managedJar, fmj);
public void markDownloaded(Path imodPath, Path jarPath, boolean managedJar, @Nullable FabricModJson fmj) {
this.downloadAux = new DownloadAux(imodPath, jarPath, managedJar, fmj);
this.scanStage = ScanStage.DOWNLOAD;
}
public void markCrossReferenced() {
this.scanStage = ScanStage.CROSSREFERENCE;
if (!this.scanStage.contains(ScanStage.CROSSREFERENCE)) this.scanStage = ScanStage.CROSSREFERENCE;
}
public void markUpdateChecked() {
this.scanStage = ScanStage.UPDATECHECK;
if (!this.scanStage.contains(ScanStage.UPDATECHECK)) this.scanStage = ScanStage.UPDATECHECK;
}
private Optional<FabricModJson> getFmj() {
@ -80,12 +82,12 @@ public class MdsMod extends Mod {
@Override
public Path getJarPath() {
return requireDownload().jarPath;
return downloadAux == null ? initialJarPath : requireDownload().jarPath;
}
@Override
public Path getMetadataPath() {
return imodPath;
return downloadAux == null ? imodPath : requireDownload().imodPath;
}
@Override

View File

@ -1,8 +1,8 @@
package io.gitlab.jfronny.inceptum.launcher.system.mds;
import io.gitlab.jfronny.inceptum.launcher.model.inceptum.InstanceMeta;
import io.gitlab.jfronny.inceptum.launcher.system.mds.flow.FlowMds;
import io.gitlab.jfronny.inceptum.launcher.system.mds.noop.NoopMds;
import io.gitlab.jfronny.inceptum.launcher.system.mds.threaded.ThreadedMds;
import io.gitlab.jfronny.inceptum.launcher.util.GameVersionParser;
import java.io.Closeable;
@ -14,18 +14,20 @@ import java.util.function.BiConsumer;
public interface ModsDirScanner extends Closeable {
static ModsDirScanner get(Path modsDir, InstanceMeta meta) throws IOException {
if (Files.exists(modsDir)) return ThreadedMds.get(modsDir, meta);
if (Files.exists(modsDir)) return FlowMds.get(modsDir, meta);//ThreadedMds.get(modsDir, meta);
return new NoopMds(GameVersionParser.getGameVersion(meta.gameVersion));
}
static void closeAll() {
ThreadedMds.closeAll();
FlowMds.closeAll();//ThreadedMds.closeAll();
}
boolean isComplete(ScanStage stage);
void start();
Closeable focus();
String getGameVersion();
Set<Mod> getMods() throws IOException;

View File

@ -20,9 +20,13 @@ public enum ScanStage implements Comparable<ScanStage> {
/**
* The mod(s) have been checked for updates
*/
UPDATECHECK;
UPDATECHECK,
/**
* The mod(s) have been scanned in all stages
*/
ALL;
public boolean isComplete(ScanStage stage) {
public boolean contains(ScanStage stage) {
return ordinal() >= stage.ordinal();
}

View File

@ -0,0 +1,195 @@
package io.gitlab.jfronny.inceptum.launcher.system.mds.flow;
import io.gitlab.jfronny.commons.io.JFiles;
import io.gitlab.jfronny.commons.ref.R;
import io.gitlab.jfronny.commons.tuple.Tuple;
import io.gitlab.jfronny.inceptum.common.Utils;
import io.gitlab.jfronny.inceptum.launcher.model.inceptum.InstanceMeta;
import io.gitlab.jfronny.inceptum.launcher.system.instance.ModPath;
import io.gitlab.jfronny.inceptum.launcher.system.mds.*;
import io.gitlab.jfronny.inceptum.launcher.system.mds.noop.NoopMod;
import io.gitlab.jfronny.inceptum.launcher.util.GameVersionParser;
import io.gitlab.jfronny.inceptum.launcher.util.VoidClaimPool;
import java.io.IOException;
import java.nio.file.*;
import java.util.*;
import java.util.concurrent.*;
import java.util.function.BiConsumer;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static java.nio.file.StandardWatchEventKinds.*;
public class FlowMds implements ModsDirScanner {
private static final Map<Path, FlowMds> SCANNERS = new HashMap<>();
private boolean disposed = false;
private final MdsThreadFactory factory = new MdsThreadFactory("mds");
private final ProtoInstance instance;
private final WatchService service;
private final Thread th;
private final Map<Path, Mod> descriptions = new HashMap<>();
private final Set<Path> scannedPaths = new HashSet<>();
private FlowMds(Path modsDir, InstanceMeta instance) throws IOException {
this.instance = new ProtoInstance(modsDir, this, instance);
this.th = factory.newThread(this::scanTaskInternal, modsDir.getParent().getFileName().toString());
this.service = FileSystems.getDefault().newWatchService();
modsDir.register(service, ENTRY_MODIFY, ENTRY_CREATE, ENTRY_DELETE);
}
public static FlowMds get(Path modsDir, InstanceMeta instance) throws IOException {
if (SCANNERS.containsKey(modsDir)) {
FlowMds mds = SCANNERS.get(modsDir);
if (mds.instance.meta().equals(instance)) return mds;
mds.close();
}
FlowMds mds = new FlowMds(modsDir, instance);
SCANNERS.put(modsDir, mds);
return mds;
}
@Override
public boolean isComplete(ScanStage stage) {
if (!Files.isDirectory(instance.modsDir())) return true;
try {
for (Path path : JFiles.list(instance.modsDir())) {
Mod mod = descriptions.get(path);
if (mod == null || mod.getScanStage().ordinal() < stage.ordinal()) return false;
}
} catch (IOException e) {
Utils.LOGGER.error("Could not list files in mods dir", e);
}
return true;
}
@Override
public void start() {
if (!th.isAlive()) th.start();
}
@Override
public VoidClaimPool.Claim focus() {
return factory.focusClaim.claim();
}
@Override
public String getGameVersion() {
return GameVersionParser.getGameVersion(instance.meta().gameVersion);
}
@Override
public Set<Mod> getMods() throws IOException {
Set<Mod> mods = new TreeSet<>();
if (Files.isDirectory(instance.modsDir())) {
for (Path path : JFiles.list(instance.modsDir())) {
if (ModPath.isImod(path) && Files.exists(ModPath.trimImod(path)))
continue;
mods.add(get(path));
}
}
return mods;
}
@Override
public Mod get(Path path) {
if (!Files.isRegularFile(path)) return null;
if (!descriptions.containsKey(path)) return new NoopMod(path); // not yet scanned
return descriptions.get(path);
}
@Override
public void invalidate(Path path) {
descriptions.remove(path);
scannedPaths.remove(path);
}
@Override
public boolean hasScanned(Path path) {
return scannedPaths.contains(path) || scannedPaths.contains(ModPath.appendImod(path));
}
private void scanTaskInternal() {
while (!disposed) {
runOnce(ScanStage.ALL, R::nop);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
private Set<Path> getToScan() throws IOException {
if (descriptions.isEmpty()) return Set.copyOf(JFiles.list(instance.modsDir()));
Set<Path> toScan = new HashSet<>();
WatchKey key = service.poll();
if (key != null) {
for (WatchEvent<?> event : key.pollEvents()) {
if (event.context() instanceof Path p) {
toScan.add(instance.modsDir().resolve(p));
}
}
if (!key.reset()) Utils.LOGGER.error("Could not reset watch key");
}
JFiles.listTo(instance.modsDir(), path -> {
if (!descriptions.containsKey(path)) toScan.add(path);
});
return toScan;
}
@Override
public void runOnce(ScanStage targetStage, BiConsumer<Path, Mod> discovered) {
try {
if (!Files.isDirectory(instance.modsDir())) {
return;
}
MdsPipeline pipeline = new MdsPipeline();
if (targetStage.contains(ScanStage.DOWNLOAD)) {
pipeline.addTask(ScanStage.DOWNLOAD, new MdsDownloadTask(instance));
}
if (targetStage.contains(ScanStage.CROSSREFERENCE)) {
pipeline.addTask(ScanStage.CROSSREFERENCE, new MdsCrossReferenceTask(instance));
}
if (targetStage.contains(ScanStage.UPDATECHECK)) {
pipeline.addTask(ScanStage.UPDATECHECK, new MdsUpdateTask(instance, getGameVersion()));
}
pipeline.addTask(ScanStage.NONE, (path, mod) -> {
scannedPaths.add(path);
descriptions.put(path, mod);
if (mod != null) discovered.accept(path, mod);
});
var futures1 = pipeline.run(factory, getToScan(), new MdsDiscoverTask(instance, getGameVersion()));
var futures2 = pipeline.run(factory, descriptions.entrySet().stream().map(Tuple::from).collect(Collectors.toSet()));
for (CompletableFuture<Void> future : Stream.concat(futures1.stream(), futures2.stream()).toList()) {
try {
future.get();
} catch (InterruptedException | ExecutionException e) {
Utils.LOGGER.error("Could not scan file for mod info", e);
}
}
} catch (IOException e) {
Utils.LOGGER.error("Could not scan file for mod info", e);
}
}
public static void closeAll() {
for (FlowMds value : SCANNERS.values().toArray(FlowMds[]::new)) {
try {
value.close();
} catch (IOException e) {
Utils.LOGGER.error("Could not close MDS", e);
}
}
MdsThreadFactory.scheduler.shutdown();
}
@Override
public void close() throws IOException {
disposed = true;
service.close();
SCANNERS.remove(instance.modsDir());
}
}

View File

@ -0,0 +1,22 @@
package io.gitlab.jfronny.inceptum.launcher.system.mds.flow;
import io.gitlab.jfronny.commons.http.client.HttpClient;
import io.gitlab.jfronny.commons.throwable.ThrowingConsumer;
import io.gitlab.jfronny.inceptum.common.GsonPreset;
import io.gitlab.jfronny.inceptum.launcher.model.inceptum.GC_ModMeta;
import io.gitlab.jfronny.inceptum.launcher.system.mds.*;
import java.io.IOException;
public record MdsCrossReferenceTask(ProtoInstance instance) implements ThrowingConsumer<MdsMod, IOException> {
@Override
public void accept(MdsMod mod) throws IOException {
if (mod.getScanStage().contains(ScanStage.CROSSREFERENCE)) return;
if (HttpClient.wasOnline()) {
if (mod.getMetadata().crossReference()) {
GC_ModMeta.serialize(mod.getMetadata(), mod.getMetadataPath(), GsonPreset.CONFIG);
}
mod.markCrossReferenced();
}
}
}

View File

@ -0,0 +1,34 @@
package io.gitlab.jfronny.inceptum.launcher.system.mds.flow;
import io.gitlab.jfronny.commons.throwable.ThrowingFunction;
import io.gitlab.jfronny.inceptum.common.GsonPreset;
import io.gitlab.jfronny.inceptum.launcher.model.inceptum.GC_ModMeta;
import io.gitlab.jfronny.inceptum.launcher.model.inceptum.ModMeta;
import io.gitlab.jfronny.inceptum.launcher.system.instance.ModPath;
import io.gitlab.jfronny.inceptum.launcher.system.mds.*;
import io.gitlab.jfronny.inceptum.launcher.system.mds.noop.NoopMod;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
public record MdsDiscoverTask(ProtoInstance instance, String gameVersion) implements ThrowingFunction<Path, Mod, IOException> {
@Override
public Mod apply(Path file) throws IOException {
if (!Files.exists(file)) return null;
if (Files.isDirectory(file)) return null; // Directories are not supported
if (ModPath.isJar(file)) return discover(file, ModPath.appendImod(file));
else if (ModPath.isImod(file)) return discover(ModPath.trimImod(file), file);
else return new NoopMod(file);
}
private Mod discover(Path jarPath, Path imodPath) throws IOException {
ModMeta meta;
if (Files.exists(imodPath)) meta = GC_ModMeta.deserialize(imodPath, GsonPreset.CONFIG);
else {
meta = ModMeta.fromJar(jarPath);
GC_ModMeta.serialize(meta, imodPath, GsonPreset.CONFIG);
}
return new MdsMod(instance, imodPath, jarPath, meta);
}
}

View File

@ -0,0 +1,53 @@
package io.gitlab.jfronny.inceptum.launcher.system.mds.flow;
import io.gitlab.jfronny.commons.http.client.HttpClient;
import io.gitlab.jfronny.commons.throwable.ThrowingConsumer;
import io.gitlab.jfronny.inceptum.common.GsonPreset;
import io.gitlab.jfronny.inceptum.common.Utils;
import io.gitlab.jfronny.inceptum.launcher.model.fabric.FabricModJson;
import io.gitlab.jfronny.inceptum.launcher.model.fabric.GC_FabricModJson;
import io.gitlab.jfronny.inceptum.launcher.system.instance.ModPath;
import io.gitlab.jfronny.inceptum.launcher.system.mds.*;
import io.gitlab.jfronny.inceptum.launcher.system.source.ModSource;
import java.io.IOException;
import java.nio.file.*;
public record MdsDownloadTask(ProtoInstance instance) implements ThrowingConsumer<MdsMod, IOException> {
@Override
public void accept(MdsMod mod) throws IOException {
if (mod.getScanStage().contains(ScanStage.DOWNLOAD)) return;
ModSource selectedSource = null;
for (ModSource source : mod.getMetadata().sources().keySet()) {
if (!Files.exists(source.getJarPath()) && HttpClient.wasOnline()) source.download();
selectedSource = source;
}
Path imodPath = mod.getMetadataPath();
Path jarPath = mod.getJarPath();
boolean managed = false;
if (selectedSource != null) {
if (jarPath.startsWith(instance.modsDir()) && Files.exists(jarPath)) {
if (Files.exists(selectedSource.getJarPath())) {
Files.delete(jarPath);
Path newImod = imodPath.getParent().resolve(selectedSource.getShortName() + ModPath.EXT_IMOD);
Files.move(imodPath, newImod);
imodPath = newImod;
jarPath = selectedSource.getJarPath();
managed = true;
}
} else {
jarPath = selectedSource.getJarPath();
managed = true;
}
} else if (!Files.exists(jarPath)) throw new IOException("Mod has no jar and no sources");
FabricModJson fmj;
try (FileSystem fs = Utils.openZipFile(jarPath, false)) {
Path fmjPath = fs.getPath("fabric.mod.json");
if (Files.exists(fmjPath)) fmj = GC_FabricModJson.deserialize(fmjPath, GsonPreset.API);
else fmj = null;
}
mod.markDownloaded(imodPath, jarPath, managed, fmj);
}
}

View File

@ -0,0 +1,68 @@
package io.gitlab.jfronny.inceptum.launcher.system.mds.flow;
import io.gitlab.jfronny.commons.throwable.*;
import io.gitlab.jfronny.commons.tuple.Tuple;
import io.gitlab.jfronny.inceptum.launcher.system.mds.*;
import java.io.IOException;
import java.nio.file.Path;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.function.Consumer;
public class MdsPipeline {
private final Set<Tuple<ScanStage, ThrowingConsumer<Tuple<Path, Mod>, IOException>>> functions = new HashSet<>();
public void addTask(ScanStage stage, ThrowingConsumer<MdsMod, IOException> task) {
functions.add(Tuple.of(stage, tuple -> {
if (tuple.right() instanceof MdsMod mmod) task.accept(mmod);
}));
}
public void addTask(ScanStage stage, ThrowingBiConsumer<Path, MdsMod, IOException> task) {
functions.add(Tuple.of(stage, tuple -> {
if (tuple.right() instanceof MdsMod mmod) task.accept(tuple.left(), mmod);
}));
}
private void work(MdsThreadFactory factory, Tuple<Path, Mod> tuple, Queue<Tuple<ScanStage, ThrowingConsumer<Tuple<Path, Mod>, IOException>>> tasks, Consumer<Throwable> fail) {
if (tasks.isEmpty()) return;
Tuple<ScanStage, ThrowingConsumer<Tuple<Path, Mod>, IOException>> task = tasks.poll();
factory.newThread(MdsThreadFactory.prioritize(
task.right()
.compose(() -> tuple)
.andThen(() -> {
work(factory, tuple, tasks, fail);
}).addHandler(fail),
task.left()
)).start();
}
public List<CompletableFuture<Void>> run(MdsThreadFactory factory, Set<Path> paths, ThrowingFunction<Path, Mod, IOException> seed) {
return paths.stream().map(s -> {
CompletableFuture<Void> finished = new CompletableFuture<>();
Queue<Tuple<ScanStage, ThrowingConsumer<Tuple<Path, Mod>, IOException>>> taskQueue = new LinkedList<>(functions);
taskQueue.add(Tuple.of(ScanStage.NONE, tuple -> finished.complete(null)));
Consumer<Throwable> fail = finished::completeExceptionally;
factory.newThread(MdsThreadFactory.prioritize(() -> {
seed.andThen(mod -> {
work(factory, Tuple.of(s, mod), taskQueue, fail);
}).addHandler(fail).accept(s);
}, ScanStage.DISCOVER)).start();
return finished;
}).toList();
}
public List<CompletableFuture<Void>> run(MdsThreadFactory factory, Set<Tuple<Path, Mod>> mods) {
return mods.stream().map(s -> {
CompletableFuture<Void> finished = new CompletableFuture<>();
Queue<Tuple<ScanStage, ThrowingConsumer<Tuple<Path, Mod>, IOException>>> taskQueue = new LinkedList<>(functions);
taskQueue.add(Tuple.of(ScanStage.NONE, tuple -> finished.complete(null)));
Consumer<Throwable> fail = finished::completeExceptionally;
factory.newThread(MdsThreadFactory.prioritize(() -> {
work(factory, s, taskQueue, fail);
}, ScanStage.DISCOVER)).start();
return finished;
}).toList();
}
}

View File

@ -0,0 +1,69 @@
package io.gitlab.jfronny.inceptum.launcher.system.mds.flow;
import io.gitlab.jfronny.commons.flow.*;
import io.gitlab.jfronny.inceptum.launcher.system.mds.ScanStage;
import io.gitlab.jfronny.inceptum.launcher.util.VoidClaimPool;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Set;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
public class MdsThreadFactory implements ThreadFactory {
protected static final ThreadPoolExecutor scheduler = DefaultSchedulers.createPriorityScheduler();
private static final AtomicInteger poolNumber = new AtomicInteger(1);
public final VoidClaimPool focusClaim = new VoidClaimPool(this::focus, this::defocus);
private final AtomicInteger threadNumber = new AtomicInteger(1);
private final String namePrefix;
private final Set<OwnedThread> ownedRunnables = ConcurrentHashMap.newKeySet();
public MdsThreadFactory(String name) {
this.namePrefix = name + "-" + poolNumber.getAndIncrement() + "-";
}
public static Runnable prioritize(Runnable runnable, ScanStage stage) {
return PrioritizedRunnable.of(runnable, 5 - stage.ordinal());
}
@Override
public Thread newThread(@NotNull Runnable runnable) {
return newThread(runnable, null);
}
public Thread newThread(@NotNull Runnable runnable, @Nullable String name) {
int priority = runnable instanceof PrioritizedRunnable pr ? pr.getPriority() : 0;
OwnedThread ownedRunnable = new OwnedThread(runnable, new Thread[1], priority, this);
Thread t = ScheduledVirtualThreadBuilder.ofVirtual(scheduler)
.name(this.namePrefix + this.threadNumber.getAndIncrement() + (name == null ? "": "-" + name))
.unstarted(ownedRunnable);
ownedRunnable.pfThread[0] = t;
ownedRunnables.add(ownedRunnable);
ScheduledVirtualThreadBuilder.setPriority(t, priority);
return t;
}
record OwnedThread(Runnable runnable, Thread[] pfThread, int basePriority, MdsThreadFactory pool) implements Runnable {
int priority(boolean focus) {
return focus ? basePriority + 5 : basePriority;
}
@Override
public void run() {
try {
runnable.run();
} finally {
pool.ownedRunnables.remove(this);
}
}
}
private void focus() {
ownedRunnables.forEach(r -> ScheduledVirtualThreadBuilder.setPriority(r.pfThread[0], r.priority(true)));
}
private void defocus() {
ownedRunnables.forEach(r -> ScheduledVirtualThreadBuilder.setPriority(r.pfThread[0], r.priority(false)));
}
}

View File

@ -0,0 +1,18 @@
package io.gitlab.jfronny.inceptum.launcher.system.mds.flow;
import io.gitlab.jfronny.commons.http.client.HttpClient;
import io.gitlab.jfronny.commons.throwable.ThrowingConsumer;
import io.gitlab.jfronny.inceptum.launcher.system.mds.*;
import java.io.IOException;
public record MdsUpdateTask(ProtoInstance instance, String gameVersion) implements ThrowingConsumer<MdsMod, IOException> {
@Override
public void accept(MdsMod mod) throws IOException {
if (mod.getScanStage().contains(ScanStage.UPDATECHECK)) return;
if (HttpClient.wasOnline()) {
mod.getMetadata().updateCheck(gameVersion);
mod.markUpdateChecked();
}
}
}

View File

@ -1,7 +1,10 @@
package io.gitlab.jfronny.inceptum.launcher.system.mds.noop;
import io.gitlab.jfronny.commons.ref.R;
import io.gitlab.jfronny.inceptum.launcher.system.mds.*;
import io.gitlab.jfronny.inceptum.launcher.util.VoidClaimPool;
import java.io.Closeable;
import java.io.IOException;
import java.nio.file.Path;
import java.util.Set;
@ -21,6 +24,11 @@ public record NoopMds(String gameVersion) implements ModsDirScanner {
public void start() {
}
@Override
public Closeable focus() {
return R::nop;
}
@Override
public String getGameVersion() {
return gameVersion;

View File

@ -48,7 +48,7 @@ public class NoopMod extends Mod {
@Override
public ScanStage getScanStage() {
return ScanStage.UPDATECHECK;
return ScanStage.DISCOVER;
}
@Override

View File

@ -33,19 +33,20 @@ public record FileScanTask(ProtoInstance instance, Path file, BiConsumer<Path, M
private void discover(Path jarPath, Path imodPath) throws IOException, URISyntaxException {
boolean managed = false;
ModMeta meta;
Path initialJarPath = jarPath;
if (Files.exists(imodPath)) meta = GC_ModMeta.deserialize(imodPath, GsonPreset.CONFIG);
else {
meta = ModMeta.fromJar(jarPath);
GC_ModMeta.serialize(meta, imodPath, GsonPreset.CONFIG);
}
boolean modified = false;
if (meta.updateCheck(gameVersion)) {
if (meta.crossReference()) {
GC_ModMeta.serialize(meta, imodPath, GsonPreset.CONFIG);
modified = true;
}
meta.updateCheck(gameVersion);
ModSource selectedSource = null;
for (ModSource source : meta.sources().keySet()) {
source.getUpdate(gameVersion);
if (!Files.exists(source.getJarPath())) source.download();
selectedSource = source;
}
@ -69,8 +70,9 @@ public record FileScanTask(ProtoInstance instance, Path file, BiConsumer<Path, M
else fmj = null;
}
MdsMod result = new MdsMod(instance, imodPath, meta);
result.markDownloaded(jarPath, managed, fmj);
MdsMod result = new MdsMod(instance, imodPath, initialJarPath, meta);
result.markDownloaded(imodPath, jarPath, managed, fmj);
result.markCrossReferenced();
result.markUpdateChecked();
discovered.accept(imodPath, result);
}

View File

@ -9,6 +9,7 @@ import io.gitlab.jfronny.inceptum.launcher.system.instance.ModPath;
import io.gitlab.jfronny.inceptum.launcher.system.mds.noop.NoopMod;
import io.gitlab.jfronny.inceptum.launcher.util.GameVersionParser;
import java.io.Closeable;
import java.io.IOException;
import java.nio.file.*;
import java.util.*;
@ -63,6 +64,11 @@ public class ThreadedMds implements ModsDirScanner {
if (!th.isAlive()) th.start();
}
@Override
public Closeable focus() {
return R::nop;
}
@Override
public String getGameVersion() {
return GameVersionParser.getGameVersion(instance.meta().gameVersion);

View File

@ -37,9 +37,6 @@ public class DownloadClientStep implements Step {
try (FileSystem fs = Utils.openZipFile(p, false)) {
Files.copy(fs.getPath("META-INF", "versions", minecraftVersion, "server-" + minecraftVersion + ".jar"),
serverPath);
} catch (URISyntaxException e) {
Files.delete(p);
throw new IOException("Could not open bundler zip", e);
}
Files.delete(p);
} else {

View File

@ -13,8 +13,8 @@ public class VersionInfoLibraryResolver {
Set<ArtifactInfo> artifacts = new LinkedHashSet<>();
for (VersionInfo.Library library : version.libraries) {
if (library.rules() != null && !library.rules().allow()) continue;
if (library.downloads().classifiers() != null && library.natives() != null && library.natives().containsKey(OSUtils.TYPE.getMojName())) {
artifacts.add(new ArtifactInfo(library.downloads().classifiers().get(library.natives().get(OSUtils.TYPE.getMojName())), true));
if (library.downloads().classifiers() != null && library.natives() != null && library.natives().containsKey(OSUtils.TYPE.mojName)) {
artifacts.add(new ArtifactInfo(library.downloads().classifiers().get(library.natives().get(OSUtils.TYPE.mojName)), true));
}
if (library.downloads().artifact() == null) {
Utils.LOGGER.info("Null library artifact @ " + library.name());

View File

@ -0,0 +1,39 @@
package io.gitlab.jfronny.inceptum.launcher.util;
import java.io.Closeable;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
public class VoidClaimPool {
private final AtomicInteger content = new AtomicInteger(0);
private final Runnable onClaim;
private final Runnable onRelease;
public VoidClaimPool(Runnable onClaim, Runnable onRelease) {
this.onClaim = onClaim;
this.onRelease = onRelease;
}
public Claim claim() {
return new Claim();
}
public boolean isEmpty() {
return content.get() == 0;
}
public class Claim implements Closeable {
private final AtomicBoolean active = new AtomicBoolean(true);
private Claim() {
if (content.getAndIncrement() == 0) onClaim.run();
}
@Override
public void close() {
if (!active.getAndSet(false))
throw new UnsupportedOperationException("Cannot release claim that is already released");
if (content.decrementAndGet() == 0) onRelease.run();
}
}
}

View File

@ -30,4 +30,5 @@ module io.gitlab.jfronny.inceptum.launcher {
requires static org.jetbrains.annotations;
requires static io.gitlab.jfronny.commons.serialize.generator.annotations;
requires io.gitlab.jfronny.commons.serialize;
requires io.gitlab.jfronny.commons.flow;
}