package io.gitlab.jfronny.inceptum.launcher.system.mds; import io.gitlab.jfronny.commons.io.JFiles; import io.gitlab.jfronny.commons.ref.R; import io.gitlab.jfronny.inceptum.common.Utils; import io.gitlab.jfronny.inceptum.launcher.model.inceptum.InstanceMeta; import io.gitlab.jfronny.inceptum.launcher.system.instance.Mod; 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.IOException; import java.nio.file.*; import java.util.*; import java.util.concurrent.*; import java.util.function.BiConsumer; import static java.nio.file.StandardWatchEventKinds.*; class ModsDirScannerImpl implements ModsDirScanner { private static final Map SCANNERS = new HashMap<>(); private static final ExecutorService POOL = Executors.newFixedThreadPool(Runtime.runtime.availableProcessors(), new NamedThreadFactory("mds")); private final Map descriptions = new HashMap<>(); private final Set scannedPaths = new HashSet<>(); private final Thread th; private final ProtoInstance instance; private final WatchService service; private boolean disposed = false; private ModsDirScannerImpl(Path modsDir, InstanceMeta instance) throws IOException { this.instance = new ProtoInstance(modsDir, this, instance); this.th = new Thread(this::scanTaskInternal, "mds-" + modsDir.parent.fileName); this.service = FileSystems.getDefault().newWatchService(); modsDir.register(service, ENTRY_MODIFY, ENTRY_CREATE, ENTRY_DELETE); } protected static ModsDirScannerImpl get(Path modsDir, InstanceMeta instance) throws IOException { if (SCANNERS.containsKey(modsDir)) { ModsDirScannerImpl mds = SCANNERS[modsDir]; if (mds.instance.meta.equals(instance)) return mds; mds.close(); } ModsDirScannerImpl mds = new ModsDirScannerImpl(modsDir, instance); SCANNERS[modsDir] = mds; return mds; } @Override public boolean isComplete() { if (!Files.isDirectory(instance.modsDir)) return true; try { for (Path path : JFiles.list(instance.modsDir)) { if (!descriptions.containsKey(path)) 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 String getGameVersion() { return GameVersionParser.getGameVersion(instance.meta.version); } @Override public Set getMods() throws IOException { Set 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[path]; } @Override public void invalidate(Path path) { descriptions.remove(path); scannedPaths.remove(path); } public boolean hasScanned(Path path) { return scannedPaths.contains(path) || scannedPaths.contains(path.parent.resolve(path.fileName.toString() + ".imod")); } private void scanTaskInternal() { while (!disposed) { runOnce(R::nop); } } @Override public void runOnce(BiConsumer discovered) { try { if (!Files.isDirectory(instance.modsDir)) { return; } Set> tasks = new HashSet<>(); discovered = discovered.andThen((path, iwModDescription) -> { scannedPaths.add(path); descriptions[path] = iwModDescription; }); Set toScan; if (descriptions.isEmpty) { toScan = Set.copyOf(JFiles.list(instance.modsDir)); } else { 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.warn("Could not reset config watch key"); } JFiles.listTo(instance.modsDir, path -> { if (!descriptions.containsKey(path)) toScan.add(path); }); } for (Path p : toScan) { tasks.add(POOL.submit(new FileScanTask(instance, p, discovered, gameVersion))); } for (Future task : tasks) { try { task.get(); } catch (ExecutionException e) { Utils.LOGGER.error("Failed to execute ScanTask", e); } } } catch (IOException | InterruptedException e) { Utils.LOGGER.error("Could not list mods", e); } } public static void closeAll() { for (ModsDirScannerImpl value : SCANNERS.values().toArray(ModsDirScannerImpl[]::new)) { try { value.close(); } catch (IOException e) { Utils.LOGGER.error("Could not close MDS", e); } } POOL.shutdown(); } @Override public void close() throws IOException { disposed = true; service.close(); SCANNERS.remove(instance.modsDir); } }