Use thread pool for MDS file scanning

This commit is contained in:
Johannes Frohnmeyer 2022-01-23 17:10:19 +01:00
parent 76301dc765
commit 3814da2013
Signed by: Johannes
GPG Key ID: E76429612C2929F4
13 changed files with 357 additions and 300 deletions

View File

@ -11,7 +11,7 @@ import io.gitlab.jfronny.inceptum.model.mojang.MinecraftArgument;
import io.gitlab.jfronny.inceptum.model.mojang.Rules;
import io.gitlab.jfronny.inceptum.util.ConfigHolder;
import io.gitlab.jfronny.inceptum.util.MetaHolder;
import io.gitlab.jfronny.inceptum.util.ModsDirScanner;
import io.gitlab.jfronny.inceptum.util.mds.ModsDirScanner;
import io.gitlab.jfronny.inceptum.util.Utils;
import io.gitlab.jfronny.inceptum.util.api.McApi;

View File

@ -9,7 +9,7 @@ import io.gitlab.jfronny.inceptum.frontend.cli.CommandArgs;
import io.gitlab.jfronny.inceptum.model.inceptum.InstanceMeta;
import io.gitlab.jfronny.inceptum.util.ConfigHolder;
import io.gitlab.jfronny.inceptum.util.MetaHolder;
import io.gitlab.jfronny.inceptum.util.ModsDirScanner;
import io.gitlab.jfronny.inceptum.util.mds.ModsDirScanner;
import io.gitlab.jfronny.inceptum.util.Utils;
import io.gitlab.jfronny.inceptum.util.api.account.AccountManager;
import io.gitlab.jfronny.inceptum.frontend.gui.Window;

View File

@ -4,9 +4,10 @@ import io.gitlab.jfronny.inceptum.frontend.cli.BaseInstanceCommand;
import io.gitlab.jfronny.inceptum.frontend.cli.Command;
import io.gitlab.jfronny.inceptum.frontend.cli.CommandArgs;
import io.gitlab.jfronny.inceptum.model.inceptum.InstanceMeta;
import io.gitlab.jfronny.inceptum.util.mds.IWModDescription;
import io.gitlab.jfronny.inceptum.util.source.ModSource;
import io.gitlab.jfronny.inceptum.util.ModManager;
import io.gitlab.jfronny.inceptum.util.ModsDirScanner;
import io.gitlab.jfronny.inceptum.util.mds.ModsDirScanner;
import io.gitlab.jfronny.inceptum.util.Utils;
import java.io.IOException;
@ -111,7 +112,7 @@ public class ModCommand extends Command {
}
for (Path mod : mods) {
try {
ModManager.delete(new ModsDirScanner.IWModDescription(mod), instancePath.resolve("mods"), mds);
ModManager.delete(new IWModDescription(mod), instancePath.resolve("mods"), mds);
} catch (IOException e) {
Utils.LOGGER.error("Could not delete " + mod, e);
return;

View File

@ -8,13 +8,14 @@ import io.gitlab.jfronny.inceptum.model.curseforge.CurseforgeMod;
import io.gitlab.jfronny.inceptum.model.inceptum.InstanceMeta;
import io.gitlab.jfronny.inceptum.model.inceptum.ModDescription;
import io.gitlab.jfronny.inceptum.util.PathUtil;
import io.gitlab.jfronny.inceptum.util.mds.IWModDescription;
import io.gitlab.jfronny.inceptum.util.source.CurseforgeModSource;
import io.gitlab.jfronny.inceptum.util.source.ModDownload;
import io.gitlab.jfronny.inceptum.util.source.ModSource;
import io.gitlab.jfronny.inceptum.util.source.ModrinthModSource;
import io.gitlab.jfronny.inceptum.model.modrinth.ModrinthSearchResult;
import io.gitlab.jfronny.inceptum.model.modrinth.ModrinthVersion;
import io.gitlab.jfronny.inceptum.util.ModsDirScanner;
import io.gitlab.jfronny.inceptum.util.mds.ModsDirScanner;
import io.gitlab.jfronny.inceptum.util.Utils;
import io.gitlab.jfronny.inceptum.util.api.CurseforgeApi;
import io.gitlab.jfronny.inceptum.util.api.ModrinthApi;
@ -95,7 +96,7 @@ public class AddModWindow extends Window {
ImGui.text(mod.description);
ImGui.tableNextColumn();
boolean alreadyPresent = false;
for (ModsDirScanner.IWModDescription mdsMod : mds.getMods()) {
for (IWModDescription mdsMod : mds.getMods()) {
alreadyPresent = mdsMod.mod().isPresent()
&& mdsMod.mod().get().sources.keySet().stream()
.anyMatch(s -> s instanceof ModrinthModSource ms
@ -173,7 +174,7 @@ public class AddModWindow extends Window {
ImGui.text(mod.summary);
ImGui.tableNextColumn();
boolean alreadyPresent = false;
for (ModsDirScanner.IWModDescription mdsMod : mds.getMods()) {
for (IWModDescription mdsMod : mds.getMods()) {
alreadyPresent = mdsMod.mod().isPresent()
&& mdsMod.mod().get().sources.keySet().stream()
.anyMatch(s -> s instanceof CurseforgeModSource ms
@ -224,7 +225,7 @@ public class AddModWindow extends Window {
}
public static DownloadMeta download(ModSource ms, Path metaFile, ModsDirScanner mds) throws IOException {
for (ModsDirScanner.IWModDescription value : mds.getMods()) {
for (IWModDescription value : mds.getMods()) {
if (value.mod().isEmpty()) continue;
for (ModSource source : value.mod().get().sources.keySet()) {
if (ms.equals(source)) {

View File

@ -11,6 +11,8 @@ import io.gitlab.jfronny.inceptum.frontend.gui.dialog.ProcessStateWatcherWindow;
import io.gitlab.jfronny.inceptum.model.inceptum.Config;
import io.gitlab.jfronny.inceptum.model.inceptum.InstanceMeta;
import io.gitlab.jfronny.inceptum.util.*;
import io.gitlab.jfronny.inceptum.util.mds.IWModDescription;
import io.gitlab.jfronny.inceptum.util.mds.ModsDirScanner;
import io.gitlab.jfronny.inceptum.util.source.ModSource;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.Status;
@ -152,11 +154,11 @@ public class InstanceEditWindow extends Window {
Utils.openFile(path.resolve("config").toFile());
}
try {
Set<ModsDirScanner.IWModDescription> modSet = mds.getMods();
Set<IWModDescription> modSet = mds.getMods();
boolean updatesFound = false;
float scannedPercentage = 0;
boolean hasUnScanned = false;
for (ModsDirScanner.IWModDescription mod : modSet) {
for (IWModDescription mod : modSet) {
if (mds.hasScanned(mod.path())) scannedPercentage++;
else hasUnScanned = true;
if (mod.mod().isEmpty()) continue;
@ -176,7 +178,7 @@ public class InstanceEditWindow extends Window {
else
filterUpdates.set(false);
ImGui.separator();
for (ModsDirScanner.IWModDescription mod : modSet) {
for (IWModDescription mod : modSet) {
updatesFound = false;
if (mod.mod().isPresent()) {
for (Optional<ModSource> value : mod.mod().get().sources.values()) {
@ -190,7 +192,7 @@ public class InstanceEditWindow extends Window {
try {
Files.move(mod.path(), newSel);
if (mod.path().equals(selected)) selected = newSel;
mod = new ModsDirScanner.IWModDescription(newSel, mod);
mod = new IWModDescription(newSel, mod);
} catch (IOException e) {
Inceptum.showError("Could not change disabled state", e);
}
@ -207,7 +209,7 @@ public class InstanceEditWindow extends Window {
if (selected == null) {
ImGui.text("Select a mod to view settings");
} else if (mds.hasScanned(selected)) {
ModsDirScanner.IWModDescription md = mds.get(selected);
IWModDescription md = mds.get(selected);
ImGui.text(md.getName());
ImGui.separator();
for (String s : md.getDescription()) {
@ -379,7 +381,7 @@ public class InstanceEditWindow extends Window {
}
}
private void delete(ModsDirScanner.IWModDescription md) throws IOException {
private void delete(IWModDescription md) throws IOException {
ModManager.delete(md, path.resolve("mods"), mds);
}

View File

@ -2,6 +2,8 @@ package io.gitlab.jfronny.inceptum.util;
import io.gitlab.jfronny.inceptum.model.curseforge.CurseforgeModpackManifest;
import io.gitlab.jfronny.inceptum.model.inceptum.InstanceMeta;
import io.gitlab.jfronny.inceptum.util.mds.IWModDescription;
import io.gitlab.jfronny.inceptum.util.mds.ModsDirScanner;
import io.gitlab.jfronny.inceptum.util.source.CurseforgeModSource;
import io.gitlab.jfronny.inceptum.util.source.ModSource;
import org.eclipse.jgit.api.Git;
@ -49,7 +51,7 @@ public class InstanceExporter {
state.incrementStep("Adding mods");
manifest.files = new LinkedHashSet<>();
if (!mds.isComplete()) throw new IOException("Mods dir scan is not yet completed");
modsLoop: for (ModsDirScanner.IWModDescription mod : mds.getMods()) {
modsLoop: for (IWModDescription mod : mds.getMods()) {
if (!PathUtil.isEnabled(mod.path())) return;
state.updateStep(mod.path().toString());
if (PathUtil.isImod(mod.path())) {

View File

@ -1,17 +1,20 @@
package io.gitlab.jfronny.inceptum.util;
import io.gitlab.jfronny.inceptum.util.mds.IWModDescription;
import io.gitlab.jfronny.inceptum.util.mds.ModsDirScanner;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
public class ModManager {
public static void delete(ModsDirScanner.IWModDescription md, Path modsDirectory, ModsDirScanner mds) throws IOException {
public static void delete(IWModDescription md, Path modsDirectory, ModsDirScanner mds) throws IOException {
Utils.deleteRecursive(md.path());
if (md.imod().isPresent() && Files.exists(md.imod().get())) Files.delete(md.imod().get());
if (md.mod().isPresent()) {
for (String dependency : md.mod().get().dependencies) {
Path dep = modsDirectory.resolve(dependency);
ModsDirScanner.IWModDescription dmd = mds.get(dep);
IWModDescription dmd = mds.get(dep);
if (dmd.mod().isPresent()) {
dmd.mod().get().dependencies.remove(md.path().getFileName().toString());
if (dmd.mod().get().dependencies.isEmpty()) delete(dmd, modsDirectory, mds);

View File

@ -1,269 +0,0 @@
package io.gitlab.jfronny.inceptum.util;
import com.google.gson.JsonParseException;
import io.gitlab.jfronny.inceptum.model.fabric.FabricModJson;
import io.gitlab.jfronny.inceptum.model.inceptum.InstanceMeta;
import io.gitlab.jfronny.inceptum.model.inceptum.ModDescription;
import io.gitlab.jfronny.inceptum.util.source.ModSource;
import io.gitlab.jfronny.inceptum.util.lambda.ThrowingConsumer;
import java.io.Closeable;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.URISyntaxException;
import java.nio.file.FileSystem;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.function.BiConsumer;
public class ModsDirScanner implements Closeable {
private static final Map<Path, ModsDirScanner> scanners = new HashMap<>();
private final Map<Path, IWModDescription> descriptions = new HashMap<>();
private final Set<Path> scannedPaths = new HashSet<>();
private final Thread th;
private final Path modsDir;
private final InstanceMeta instance;
private boolean disposed = false;
private ModsDirScanner(Path modsDir, InstanceMeta instance) {
this.modsDir = modsDir;
this.instance = instance;
this.th = new Thread(this::scanTaskInternal);
}
public static ModsDirScanner get(Path modsDir, InstanceMeta instance) {
if (scanners.containsKey(modsDir)) {
ModsDirScanner mds = scanners.get(modsDir);
if (mds.instance.equals(instance))
return mds;
mds.close();
}
ModsDirScanner mds = new ModsDirScanner(modsDir, instance);
scanners.put(modsDir, mds);
return mds;
}
public boolean isComplete() {
if (!Files.isDirectory(modsDir)) return true;
try {
for (Path path : Utils.ls(modsDir)) {
if (!descriptions.containsKey(path)) return false;
}
} catch (IOException e) {
e.printStackTrace();
}
return true;
}
public void start() {
if (!th.isAlive())
th.start();
}
public String getGameVersion() {
return instance.getMinecraftVersion();
}
public Set<IWModDescription> getMods() throws IOException {
Set<IWModDescription> mods = new TreeSet<>();
if (Files.isDirectory(modsDir)) {
for (Path path : Utils.ls(modsDir)) {
if (PathUtil.isImod(path) && Files.exists(PathUtil.trimImod(path)))
continue;
mods.add(get(path));
}
}
return mods;
}
public IWModDescription get(Path path) {
if (!descriptions.containsKey(path)) {
// not yet scanned
descriptions.put(path, new IWModDescription(path));
}
return descriptions.get(path);
}
public void invalidate(Path path) {
descriptions.remove(path);
scannedPaths.remove(path);
}
public boolean hasScanned(Path path) {
return scannedPaths.contains(path);
}
private void scanTaskInternal() {
while (!disposed) {
runOnce(descriptions::put);
}
}
public void runOnce(BiConsumer<Path, IWModDescription> discovered) {
try {
if (!Files.isDirectory(modsDir)) {
Thread.sleep(5000);
return;
}
for (Path mods : Utils.ls(modsDir)) {
if (disposed) return;
scanFile(mods, discovered);
}
Thread.sleep(100);
} catch (IOException | InterruptedException e) {
Utils.LOGGER.error("Could not list mods", e);
}
}
private void scanFile(Path file, BiConsumer<Path, IWModDescription> discovered) {
if (!Files.exists(file)) return;
if (Files.isDirectory(file)) {
discovered.accept(file, new IWModDescription(file));
scannedPaths.add(file);
return;
}
try {
if (PathUtil.isJar(file)) {
final var imod = new Object() {
Path i = file.getParent().resolve(file.getFileName() + PathUtil.EXT_IMOD);
ModDescription md;
};
// mod description
if (!Files.exists(imod.i)) {
Utils.writeObject(imod.i, ModDescription.of(file));
}
imod.md = Utils.loadObject(imod.i, ModDescription.class);
evaluateSources(imod.md, file, imod.i, newImod -> {
imod.i = newImod;
imod.md = Utils.loadObject(imod.i, ModDescription.class);
});
discovered.accept(imod.i, new IWModDescription(file, Optional.of(imod.md), getFmj(file, imod.md), Optional.of(imod.i)));
scannedPaths.add(file);
return;
}
if (PathUtil.isImod(file)) {
String fn = file.getFileName().toString();
Path modFile = file.getParent().resolve(fn.substring(0, fn.length() - 5));
final var imod = new Object() {
Path i = file;
ModDescription md = Utils.loadObject(file, ModDescription.class);
};
evaluateSources(imod.md, modFile, imod.i, newImod -> {
imod.i = newImod;
imod.md = Utils.loadObject(imod.i, ModDescription.class);
});
discovered.accept(imod.i, new IWModDescription(imod.i, Optional.of(imod.md), getFmj(modFile, imod.md), Optional.of(imod.i)));
scannedPaths.add(file);
return;
}
discovered.accept(file, new IWModDescription(file));
scannedPaths.add(file);
} catch (IOException | URISyntaxException | JsonParseException e) {
Utils.LOGGER.error("Could not scan file for mod info", e);
}
}
private <TEx extends Throwable> void evaluateSources(ModDescription md, Path modFile, Path imodFile, ThrowingConsumer<Path, TEx> imodModified) throws IOException, TEx {
boolean modified = false;
if (md.initialize(getGameVersion())) {
Utils.writeObject(imodFile, md);
modified = true;
}
ModSource selectedSource = null;
for (ModSource source : md.sources.keySet()) {
source.getUpdate(getGameVersion());
if (!Files.exists(source.getJarPath())) source.download();
selectedSource = source;
}
if (selectedSource != null && Files.exists(modFile)) {
Files.delete(modFile);
Path newImod = imodFile.getParent().resolve(selectedSource.getShortName() + PathUtil.EXT_IMOD);
Files.move(imodFile, newImod);
imodFile = newImod;
modified = true;
}
if (modified) imodModified.consume(imodFile);
}
private Optional<FabricModJson> getFmj(Path modJarDefault, ModDescription md) throws IOException, URISyntaxException {
if (!Files.exists(modJarDefault)) {
if (md.sources.isEmpty()) {
throw new FileNotFoundException("Mod " + modJarDefault.getFileName().toString() + " doesn't specify a source and has no file");
}
modJarDefault = List.copyOf(md.sources.keySet()).get(0).getJarPath();
}
try (FileSystem fs = Utils.openZipFile(modJarDefault, false)) {
return Files.exists(fs.getPath("fabric.mod.json"))
? Optional.of(Utils.loadObject(fs.getPath("fabric.mod.json"), FabricModJson.class))
: Optional.empty();
}
}
public static void closeAll() {
for (ModsDirScanner value : scanners.values().toArray(ModsDirScanner[]::new)) {
value.close();
}
}
@Override
public void close() {
disposed = true;
scanners.remove(modsDir);
}
public record IWModDescription(Path path, Optional<ModDescription> mod, Optional<FabricModJson> fmj, Optional<Path> imod) implements Comparable<IWModDescription> {
public String getName() {
if (fmj.isEmpty()) return path.getFileName().toString();
String base;
if (fmj.get().name != null) base = fmj.get().name;
else if (fmj.get().id != null) base = fmj.get().id;
else {
base = path.getFileName().toString();
if (Files.isDirectory(path))
base += '/';
}
if (fmj.get().version != null) base += ' ' + fmj.get().version;
return base;
}
public String[] getDescription() {
try {
if (fmj.isPresent() && fmj.get().description != null) {
return fmj.get().description.split("\n");
}
else if (Files.isDirectory(path)) {
return Utils.lsVi(path);
}
} catch (IOException e) {
Utils.LOGGER.error("Could not get description", e);
}
return new String[] {"No description is available", "This mod may have been added manually"};
}
/**
* Generates a description with only the path and no additional information
* @param path The path of the mod entry
*/
public IWModDescription(Path path) {
this(path,
Files.isDirectory(path) ? Optional.empty() : Optional.of(ModDescription.of(path)),
Optional.empty(),
Files.exists(PathUtil.appendImod(path)) ? Optional.of(PathUtil.appendImod(path)) : Optional.empty());
}
/**
* Generates a new mod description based on an existing one with an adjusted path
* @param path The new path to use
* @param base The mod description to copy
*/
public IWModDescription(Path path, IWModDescription base) {
this(path, base.mod, base.fmj, base.imod);
}
@Override
public int compareTo(IWModDescription o) {
return getName().compareTo(o.getName());
}
}
}

View File

@ -1,12 +1,10 @@
package io.gitlab.jfronny.inceptum.util.install;
import io.gitlab.jfronny.inceptum.Inceptum;
import io.gitlab.jfronny.inceptum.model.inceptum.InstanceMeta;
import io.gitlab.jfronny.inceptum.model.inceptum.LoaderInfo;
import io.gitlab.jfronny.inceptum.model.mojang.VersionInfo;
import io.gitlab.jfronny.inceptum.model.mojang.VersionsListInfo;
import io.gitlab.jfronny.inceptum.util.InstanceLock;
import io.gitlab.jfronny.inceptum.util.MetaHolder;
import io.gitlab.jfronny.inceptum.util.ProcessState;
import io.gitlab.jfronny.inceptum.util.Utils;
import io.gitlab.jfronny.inceptum.util.api.FabricMetaApi;
@ -27,23 +25,14 @@ public class Steps {
new DownloadClientStep(),
new DownloadLibrariesStep(),
new DownloadAssetsStep(),
new WriteMetadataStep())
);
new WriteMetadataStep(),
new RunMdsStep()
));
public static ProcessState createProcessState() {
return new ProcessState(STEPS.size(), "Initializing");
}
public static void reDownload(ProcessState state) throws IOException {
Utils.ls(MetaHolder.INSTANCE_DIR, p -> {
try {
reDownload(p, state);
} catch (IOException e) {
Inceptum.showError("Could not execute refresh task", e);
}
});
}
public static void reDownload(Path instance, ProcessState state) throws IOException {
if (InstanceLock.isLocked(instance)) return;
InstanceMeta im = Utils.loadObject(instance.resolve("instance.json"), InstanceMeta.class);

View File

@ -0,0 +1,24 @@
package io.gitlab.jfronny.inceptum.util.install.steps;
import io.gitlab.jfronny.inceptum.model.inceptum.InstanceMeta;
import io.gitlab.jfronny.inceptum.util.MetaHolder;
import io.gitlab.jfronny.inceptum.util.Utils;
import io.gitlab.jfronny.inceptum.util.install.SetupStepInfo;
import io.gitlab.jfronny.inceptum.util.install.Step;
import io.gitlab.jfronny.inceptum.util.mds.ModsDirScanner;
import java.io.IOException;
import java.nio.file.Path;
import java.util.concurrent.atomic.AtomicBoolean;
public class RunMdsStep implements Step {
@Override
public void execute(SetupStepInfo info, AtomicBoolean stopThread) throws IOException {
info.setState("Running MDS");
Path instance = MetaHolder.INSTANCE_DIR.resolve(info.name());
ModsDirScanner.get(instance.resolve("mods"), Utils.loadObject(instance.resolve("instance.json"), InstanceMeta.class))
.runOnce((path, iwModDescription) -> {
info.setState("Scanned " + path);
});
}
}

View File

@ -0,0 +1,102 @@
package io.gitlab.jfronny.inceptum.util.mds;
import com.google.gson.JsonParseException;
import io.gitlab.jfronny.inceptum.model.fabric.FabricModJson;
import io.gitlab.jfronny.inceptum.model.inceptum.ModDescription;
import io.gitlab.jfronny.inceptum.util.PathUtil;
import io.gitlab.jfronny.inceptum.util.Utils;
import io.gitlab.jfronny.inceptum.util.lambda.ThrowingConsumer;
import io.gitlab.jfronny.inceptum.util.source.ModSource;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.URISyntaxException;
import java.nio.file.FileSystem;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.Optional;
import java.util.function.BiConsumer;
public record FileScanTask(Path file, BiConsumer<Path, IWModDescription> discovered, String gameVersion) implements Runnable {
@Override
public void run() {
if (!Files.exists(file)) return;
if (Files.isDirectory(file)) {
discovered.accept(file, new IWModDescription(file));
return;
}
try {
if (PathUtil.isJar(file)) {
final var imod = new Object() {
Path i = file.getParent().resolve(file.getFileName() + PathUtil.EXT_IMOD);
ModDescription md;
};
// mod description
if (!Files.exists(imod.i)) {
Utils.writeObject(imod.i, ModDescription.of(file));
}
imod.md = Utils.loadObject(imod.i, ModDescription.class);
evaluateSources(imod.md, file, imod.i, newImod -> {
imod.i = newImod;
imod.md = Utils.loadObject(imod.i, ModDescription.class);
});
discovered.accept(imod.i, new IWModDescription(file, Optional.of(imod.md), getFmj(file, imod.md), Optional.of(imod.i)));
return;
}
if (PathUtil.isImod(file)) {
String fn = file.getFileName().toString();
Path modFile = file.getParent().resolve(fn.substring(0, fn.length() - 5));
final var imod = new Object() {
Path i = file;
ModDescription md = Utils.loadObject(file, ModDescription.class);
};
evaluateSources(imod.md, modFile, imod.i, newImod -> {
imod.i = newImod;
imod.md = Utils.loadObject(imod.i, ModDescription.class);
});
discovered.accept(imod.i, new IWModDescription(imod.i, Optional.of(imod.md), getFmj(modFile, imod.md), Optional.of(imod.i)));
return;
}
discovered.accept(file, new IWModDescription(file));
} catch (IOException | URISyntaxException | JsonParseException e) {
Utils.LOGGER.error("Could not scan file for mod info", e);
}
}
private <TEx extends Throwable> void evaluateSources(ModDescription md, Path modFile, Path imodFile, ThrowingConsumer<Path, TEx> imodModified) throws IOException, TEx {
boolean modified = false;
if (md.initialize(gameVersion)) {
Utils.writeObject(imodFile, md);
modified = true;
}
ModSource selectedSource = null;
for (ModSource source : md.sources.keySet()) {
source.getUpdate(gameVersion);
if (!Files.exists(source.getJarPath())) source.download();
selectedSource = source;
}
if (selectedSource != null && Files.exists(modFile)) {
Files.delete(modFile);
Path newImod = imodFile.getParent().resolve(selectedSource.getShortName() + PathUtil.EXT_IMOD);
Files.move(imodFile, newImod);
imodFile = newImod;
modified = true;
}
if (modified) imodModified.consume(imodFile);
}
private Optional<FabricModJson> getFmj(Path modJarDefault, ModDescription md) throws IOException, URISyntaxException {
if (!Files.exists(modJarDefault)) {
if (md.sources.isEmpty()) {
throw new FileNotFoundException("Mod " + modJarDefault.getFileName().toString() + " doesn't specify a source and has no file");
}
modJarDefault = List.copyOf(md.sources.keySet()).get(0).getJarPath();
}
try (FileSystem fs = Utils.openZipFile(modJarDefault, false)) {
return Files.exists(fs.getPath("fabric.mod.json"))
? Optional.of(Utils.loadObject(fs.getPath("fabric.mod.json"), FabricModJson.class))
: Optional.empty();
}
}
}

View File

@ -0,0 +1,67 @@
package io.gitlab.jfronny.inceptum.util.mds;
import io.gitlab.jfronny.inceptum.model.fabric.FabricModJson;
import io.gitlab.jfronny.inceptum.model.inceptum.ModDescription;
import io.gitlab.jfronny.inceptum.util.PathUtil;
import io.gitlab.jfronny.inceptum.util.Utils;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Optional;
public record IWModDescription(Path path, Optional<ModDescription> mod, Optional<FabricModJson> fmj, Optional<Path> imod) implements Comparable<IWModDescription> {
public String getName() {
if (fmj.isEmpty()) return path.getFileName().toString();
String base;
if (fmj.get().name != null) base = fmj.get().name;
else if (fmj.get().id != null) base = fmj.get().id;
else {
base = path.getFileName().toString();
if (Files.isDirectory(path))
base += '/';
}
if (fmj.get().version != null) base += ' ' + fmj.get().version;
return base;
}
public String[] getDescription() {
try {
if (fmj.isPresent() && fmj.get().description != null) {
return fmj.get().description.split("\n");
} else if (Files.isDirectory(path)) {
return Utils.lsVi(path);
}
} catch (IOException e) {
Utils.LOGGER.error("Could not get description", e);
}
return new String[]{"No description is available", "This mod may have been added manually"};
}
/**
* Generates a description with only the path and no additional information
*
* @param path The path of the mod entry
*/
public IWModDescription(Path path) {
this(path,
Files.isDirectory(path) ? Optional.empty() : Optional.of(ModDescription.of(path)),
Optional.empty(),
Files.exists(PathUtil.appendImod(path)) ? Optional.of(PathUtil.appendImod(path)) : Optional.empty());
}
/**
* Generates a new mod description based on an existing one with an adjusted path
*
* @param path The new path to use
* @param base The mod description to copy
*/
public IWModDescription(Path path, IWModDescription base) {
this(path, base.mod, base.fmj, base.imod);
}
@Override
public int compareTo(IWModDescription o) {
return getName().compareTo(o.getName());
}
}

View File

@ -0,0 +1,135 @@
package io.gitlab.jfronny.inceptum.util.mds;
import io.gitlab.jfronny.inceptum.model.inceptum.InstanceMeta;
import io.gitlab.jfronny.inceptum.util.PathUtil;
import io.gitlab.jfronny.inceptum.util.Utils;
import java.io.Closeable;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.concurrent.*;
import java.util.function.BiConsumer;
public class ModsDirScanner implements Closeable {
private static final Map<Path, ModsDirScanner> SCANNERS = new HashMap<>();
private static final ExecutorService POOL = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors() * 2);
private final Map<Path, IWModDescription> descriptions = new HashMap<>();
private final Set<Path> scannedPaths = new HashSet<>();
private final Thread th;
private final Path modsDir;
private final InstanceMeta instance;
private boolean disposed = false;
private ModsDirScanner(Path modsDir, InstanceMeta instance) {
this.modsDir = modsDir;
this.instance = instance;
this.th = new Thread(this::scanTaskInternal);
}
public static ModsDirScanner get(Path modsDir, InstanceMeta instance) {
if (SCANNERS.containsKey(modsDir)) {
ModsDirScanner mds = SCANNERS.get(modsDir);
if (mds.instance.equals(instance))
return mds;
mds.close();
}
ModsDirScanner mds = new ModsDirScanner(modsDir, instance);
SCANNERS.put(modsDir, mds);
return mds;
}
public boolean isComplete() {
if (!Files.isDirectory(modsDir)) return true;
try {
for (Path path : Utils.ls(modsDir)) {
if (!descriptions.containsKey(path)) return false;
}
} catch (IOException e) {
e.printStackTrace();
}
return true;
}
public void start() {
if (!th.isAlive())
th.start();
}
public String getGameVersion() {
return instance.getMinecraftVersion();
}
public Set<IWModDescription> getMods() throws IOException {
Set<IWModDescription> mods = new TreeSet<>();
if (Files.isDirectory(modsDir)) {
for (Path path : Utils.ls(modsDir)) {
if (PathUtil.isImod(path) && Files.exists(PathUtil.trimImod(path)))
continue;
mods.add(get(path));
}
}
return mods;
}
public IWModDescription get(Path path) {
if (!descriptions.containsKey(path)) {
// not yet scanned
descriptions.put(path, new IWModDescription(path));
}
return descriptions.get(path);
}
public void invalidate(Path path) {
descriptions.remove(path);
scannedPaths.remove(path);
}
public boolean hasScanned(Path path) {
return scannedPaths.contains(path);
}
private void scanTaskInternal() {
while (!disposed) {
runOnce(descriptions::put);
}
}
public void runOnce(BiConsumer<Path, IWModDescription> discovered) {
try {
if (!Files.isDirectory(modsDir)) {
Thread.sleep(5000);
return;
}
Set<Future<?>> tasks = new HashSet<>();
for (Path mods : Utils.ls(modsDir)) {
if (disposed) return;
tasks.add(POOL.submit(new FileScanTask(mods, discovered.andThen((path, iwModDescription) -> scannedPaths.add(path)), getGameVersion())));
}
for (Future<?> task : tasks) {
try {
task.get();
} catch (ExecutionException e) {
Utils.LOGGER.error("Failed to execute ScanTask", e);
}
}
Thread.sleep(100);
} catch (IOException | InterruptedException e) {
Utils.LOGGER.error("Could not list mods", e);
}
}
public static void closeAll() {
for (ModsDirScanner value : SCANNERS.values().toArray(ModsDirScanner[]::new)) {
value.close();
}
POOL.shutdown();
}
@Override
public void close() {
disposed = true;
SCANNERS.remove(modsDir);
}
}