package com.narrowtux.fmm.model; import com.google.gson.Gson; import com.google.gson.JsonArray; import com.google.gson.JsonObject; import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleObjectProperty; import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; import javafx.collections.FXCollections; import javafx.collections.ObservableSet; import org.sat4j.core.VecInt; import org.sat4j.minisat.SolverFactory; import org.sat4j.specs.ContradictionException; import org.sat4j.specs.ISolver; import org.sat4j.tools.ModelIterator; import java.io.FileWriter; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.util.*; import java.util.stream.Collectors; public class Modpack { private static final int RECURSION_LIMIT = 20; private StringProperty name = new SimpleStringProperty(); private ObservableSet<ModReference> mods = FXCollections.observableSet(new LinkedHashSet<ModReference>()); private ObjectProperty<Path> path = new SimpleObjectProperty<>(); public Modpack(String name, Path path) { setName(name); setPath(path); nameProperty().addListener((obs, ov, nv) -> { // sanity check. no slashes allowed if (nv == null || nv.isEmpty() || nv.contains("/")) { setName(ov); return; } Path newPath = getPath().getParent().resolve(nv); try { Files.move(getPath(), newPath); setPath(newPath); for (ModReference mod : getMods()) { String fileName = mod.getMod().getPath().getFileName().toString(); Path modNewPath = mod.getMod().getPath().getParent().getParent().resolve(nv).resolve(fileName); mod.getMod().setPath(modNewPath); } } catch (IOException e) { e.printStackTrace(); setName(ov); } }); } public String getName() { return name.get(); } public void setName(String name) { this.name.set(name); } public StringProperty nameProperty() { return name; } public Path getPath() { return path.get(); } public ObjectProperty<Path> pathProperty() { return path; } public void setPath(Path path) { this.path.set(path); } public ObservableSet<ModReference> getMods() { return mods; } @Override public String toString() { return "Modpack{" + "name=" + getName() + ", mods=" + mods + '}'; } public void writeModList() { writeModList(false); } public void writeModList(boolean writeVersion) { writeModList(getPath().resolve("mod-list.json"), writeVersion, getMods().toArray(new ModReference[0])); } public static void writeModList(Path file, boolean writeVersion, ModReference ... mods) { JsonObject root = new JsonObject(); JsonArray modList = new JsonArray(); JsonObject baseMod = new JsonObject(); baseMod.addProperty("name", "base"); baseMod.addProperty("enabled", true); modList.add(baseMod); for (ModReference mod : mods) { JsonObject modInfo = new JsonObject(); modInfo.addProperty("name", mod.getMod().getName()); modInfo.addProperty("enabled", mod.getEnabled()); if (writeVersion) { modInfo.addProperty("version", mod.getMod().getVersion().toString()); } modList.add(modInfo); } root.add("mods", modList); try { Gson gson = new Gson(); String json = gson.toJson(root); FileWriter writer = new FileWriter(file.toFile()); writer.write(json); writer.close(); } catch (IOException e) { e.printStackTrace(); } } public List<Set<Mod>> resolveDependencies() { Set<Mod> confirmedMods = getMods().stream().map(ModReference::getMod).collect(Collectors.toSet()); try { return resolveDependencies(0, confirmedMods); } catch (Exception e) { e.printStackTrace(); return null; } } public List<Set<Mod>> resolveDependencies(int recursionInterval, Set<Mod> confirmedMods) throws Exception { if (recursionInterval > RECURSION_LIMIT) { throw new Exception("Recursion limit reached"); } recursionInterval ++; List<Set<Mod>> solutions = new LinkedList<>(); List<ModDependency> deps = new LinkedList<>(); confirmedMods.forEach(mod -> deps.addAll(mod.getDependencies().stream().filter(dep -> !dep.getOptional()).collect(Collectors.toList()))); for (Mod mod : confirmedMods) { List<ModDependency> satisfied = new LinkedList<>(); for (ModDependency dep : deps) { if (dep.getDependencyName().equals(mod.getName())) { if (dep.getMatchedVersion() == null || dep.getMatchedVersion().matches(mod.getVersion())) { satisfied.add(dep); } } } deps.removeAll(satisfied); } if (deps.isEmpty()) { solutions.add(confirmedMods); return solutions; } ISolver solver = SolverFactory.newDefault(); solver.setKeepSolverHot(true); Set<String> names = deps.stream().map(ModDependency::getDependencyName).collect(Collectors.toSet()); List<Mod> matchedMods = new LinkedList<>(); Datastore store = Datastore.getInstance(); final boolean[] modNotFound = {false}; deps.stream().forEach(dep -> { final int[] found = {0}; VecInt clause = new VecInt(); store.getMods().values().stream() .filter(mod -> dep.getMatchedVersion() == null || dep.getMatchedVersion().matches(mod.getVersion())) .filter(mod -> dep.getDependencyName().equals(mod.getName())) .forEach(mod -> { found[0]++; if (!matchedMods.contains(mod)) { matchedMods.add(mod); } int literal = matchedMods.indexOf(mod) + 1; if (!clause.contains(literal)) { clause.push(literal); } }); if (found[0] == 0) { modNotFound[0] = true; System.out.println(getName() + ": couldn't find mod for " + dep); } try { solver.addClause(clause); } catch (ContradictionException e) { e.printStackTrace(); } }); if (modNotFound[0]) { System.out.println(getName() + ": one or more mods couldn't be found"); return null; } for (String name : names) { VecInt vector = new VecInt(); matchedMods.stream().filter(mod -> mod.getName().equals(name)).forEach(mod -> { int literal = matchedMods.indexOf(mod) + 1; if (!vector.contains(literal)) { vector.push(literal); } }); try { solver.addExactly(vector, 1); } catch (ContradictionException e) { System.out.println(vector); e.printStackTrace(); return null; } } ModelIterator iter = new ModelIterator(solver); for (int interval = 0; interval < RECURSION_LIMIT; interval++) { if (iter.isSatisfiable()) { Set<Mod> solution = new HashSet<>(confirmedMods); for (int v : iter.model()) { if (v > 0) { Mod mod = matchedMods.get(v - 1); solution.add(mod); } } List<Set<Mod>> result = resolveDependencies(recursionInterval, solution); if (result.size() != 0) { solutions.addAll(result); } } else { return solutions; } } return solutions; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Modpack modpack = (Modpack) o; if (!name.equals(modpack.name)) return false; return path.equals(modpack.path); } @Override public int hashCode() { int result = name.hashCode(); result = 31 * result + path.hashCode(); return result; } }