package net.krazyweb.starmodmanager.data;
import java.io.File;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.SeekableByteChannel;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.sql.SQLException;
import java.util.Comparator;
import java.util.HashSet;
import java.util.Set;
import net.krazyweb.helpers.Archive;
import net.krazyweb.helpers.ArchiveFile;
import net.krazyweb.helpers.FileHelper;
import net.krazyweb.helpers.JSONHelper;
import net.krazyweb.stardb.databases.AssetDatabase;
import net.krazyweb.stardb.exceptions.StarDBException;
import net.krazyweb.starmodmanager.dialogue.MessageDialogue;
import net.krazyweb.starmodmanager.dialogue.MessageDialogue.MessageType;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import com.eclipsesource.json.JsonArray;
import com.eclipsesource.json.JsonObject;
import com.eclipsesource.json.ParseException;
public class Mod implements Observable {
private static final Logger log = LogManager.getLogger(Mod.class);
private static final String NO_DESCRIPTION = "StarboundModManager___NO_DESCRIPTION_FOR_MOD";
private static final String NO_AUTHOR = "StarboundModManager___NO_AUTHOR_FOR_MOD";
private static final String NO_VERSION = "StarboundModManager___NO_VERSION_FOR_MOD";
private String internalName;
private String displayName;
private String modVersion;
private String gameVersion;
private String author;
private String description;
private String url;
private String archiveName;
private String imageName;
private long checksum;
private int order = -1;
private boolean hidden = false;
private boolean installed = false;
private boolean hasImage = false;
private Set<String> dependencies;
private Set<ModFile> files; //All files that the mod alters
private SettingsModelInterface settings;
private static LocalizerModelInterface localizer;
private Set<Observer> observers;
protected static class ModOrderComparator implements Comparator<Mod> {
@Override
public int compare(Mod mod1, Mod mod2) {
return mod1.order - mod2.order;
}
}
protected Mod(final LocalizerModelFactory localizerFactory, final SettingsModelFactory settingsFactory) {
observers = new HashSet<>();
localizer = localizerFactory.getInstance();
settings = settingsFactory.getInstance();
}
public static Set<Mod> load(final Path path, final int order, final SettingsModelFactory settingsFactory, final DatabaseModelFactory databaseFactory, final LocalizerModelFactory localizerFactory) {
log.debug("Loading mod: {}", path);
Set<Mod> mods = new HashSet<>();
//TODO Count all mods and send back progress info
Set<Archive> archives = null;
try {
archives = processModFile(path, settingsFactory);
} catch (IOException | StarDBException | ParseException e) {
//TODO Error Dialogue
log.error("", e);
return new HashSet<Mod>();
}
for (Archive archive : archives) {
Mod mod = new Mod(localizerFactory, settingsFactory);
mod.setOrder(order);
mod.files = new HashSet<>();
mod.setArchiveName(archive.getFileName());
//Get the modinfo file and parse it
JsonObject obj = JsonObject.readFrom(new String(archive.getFile(".modinfo").getData()));
mod.setInternalName(obj.get("name").asString());
if (obj.get("version") != null) {
mod.setGameVersion(obj.get("version").asString());
} else {
mod.setGameVersion("Field Empty");
}
Set<String> dependencies = new HashSet<>();
Set<String> ignoredFileNames = new HashSet<>();
if (obj.get("dependencies") != null) {
JsonArray arr = obj.get("dependencies").asArray();
for (int i = 0; i < arr.size(); i++) {
dependencies.add(arr.get(i).asString());
}
}
mod.setDependencies(dependencies);
if (obj.get("metadata") != null) {
JsonObject metadata = obj.get("metadata").asObject();
mod.setDisplayName(JSONHelper.getString(metadata, "displayname", mod.getInternalName()));
mod.setAuthor(JSONHelper.getString(metadata, "author", NO_AUTHOR));
mod.setDescription(JSONHelper.getString(metadata, "description", NO_DESCRIPTION));
mod.setURL(JSONHelper.getString(metadata, "support_url", ""));
mod.setModVersion(JSONHelper.getString(metadata, "version", NO_VERSION));
if (obj.get("ignoredfiles") != null) {
JsonArray arr = obj.get("ignoredfiles").asArray();
for (int i = 0; i < arr.size(); i++) {
ignoredFileNames.add(arr.get(i).asString());
}
}
} else {
mod.setDisplayName(mod.getInternalName());
mod.setAuthor(NO_AUTHOR);
mod.setDescription(NO_DESCRIPTION);
mod.setURL("");
mod.setModVersion(NO_VERSION);
}
try {
mod.setChecksum(FileHelper.getChecksum(new File(settingsFactory.getInstance().getPropertyString("modsdir") + File.separator + mod.archiveName).toPath())); //TODO Better Path manipulation
} catch (IOException e) {
log.error("Setting Checksum", e);
}
for (ArchiveFile archiveFile : archive.getFiles()) {
ModFile modFile = new ModFile();
modFile.setPath(archiveFile.getPath());
//Find and list all ignored files
for (String ignored : ignoredFileNames) {
if (archiveFile.getPath().endsWith(ignored) || archiveFile.getPath().endsWith(".txt")) {
modFile.setIgnored(true);
}
}
//Scan all json files and find those with mergeability
if (!archiveFile.isFolder() && FileHelper.isJSON(archiveFile.getPath())) {
modFile.setJson(true);
String fileContents = new String(archiveFile.getData());
if (fileContents.contains("__merge")) {
modFile.setAutoMerged(true);
}
}
if (!archiveFile.isFolder()) {
mod.files.add(modFile);
}
}
try {
databaseFactory.getInstance().updateMod(mod);
} catch (SQLException e) {
log.error("", e);
MessageDialogue dialogue = new MessageDialogue(localizer.getMessage("mod.dbconnectionerror"), localizer.getMessage("mod.dbconnectionerror.title"), MessageType.ERROR, new LocalizerFactory());
dialogue.getResult();
return new HashSet<Mod>();
}
mods.add(mod);
}
return mods;
}
private static Set<Archive> processModFile(final Path path, final SettingsModelFactory settingsFactory) throws IOException, StarDBException, ParseException {
Set<Archive> archives = new HashSet<>();
if (FileHelper.identifyType(path, false).equals("pak")) {
processPakFile(path, archives, settingsFactory);
} else {
processArchive(path, archives, settingsFactory);
}
return archives;
}
private static void processPakFile(final Path path, final Set<Archive> output, final SettingsModelFactory settingsFactory) throws IOException, StarDBException {
SettingsModelInterface settings = settingsFactory.getInstance();
AssetDatabase database = AssetDatabase.open(path);
log.debug(database.getFileList());
byte[] modinfoFile = database.getAsset("/pak.modinfo");
String[] modinfoContents = new String(modinfoFile).split("\n");
String modName = "";
String assetsPath = "";
//TODO Use actual JSON parser
for (String line : modinfoContents) {
if (line.contains("\"name\"")) {
modName = line.trim().split(":")[1];
modName = modName.substring(modName.indexOf("\"") + 1, modName.lastIndexOf("\""));
}
if (line.contains("\"path\"")) {
assetsPath = line.trim().split(":")[1];
assetsPath = assetsPath.substring(assetsPath.indexOf("\"") + 1, assetsPath.lastIndexOf("\""));
}
}
if (assetsPath.startsWith(".")) {
assetsPath = assetsPath.replace(".", "");
}
if (assetsPath.startsWith("/")) {
assetsPath = assetsPath.replace("/", "");
}
if (assetsPath.startsWith("./")) {
assetsPath = assetsPath.replace("./", "");
}
Archive modArchive = new Archive(settings.getPropertyPath("modsdir").resolve(modName + ".zip"));
modArchive.addFile(new ArchiveFile(modinfoFile, Paths.get(modName + ".modinfo"), false));
ArchiveFile modinfo = modArchive.getFile(".modinfo");
JsonObject o2 = JsonObject.readFrom(new String(modinfo.getData()));
o2.set("path", "assets");
modinfo.setData(o2.toString().getBytes());
assetsPath = "/" + assetsPath + "/";
log.debug("Assets path is '{}'", assetsPath);
for (String file : database.getFileList()) {
log.trace("{} is in {}", file, path);
if (file.endsWith(".modinfo") || file.endsWith("desktop.ini") || file.endsWith("thumbs.db")) {
continue;
}
if (assetsPath.isEmpty() || !file.startsWith(assetsPath)) {
modArchive.addFile(new ArchiveFile(database.getAsset(file), Paths.get("assets/" + file.substring(1)), false));
} else {
log.debug("{} --- {}", assetsPath, file.substring(assetsPath.length()));
modArchive.addFile(new ArchiveFile(database.getAsset(file), Paths.get("assets/" + file.substring(assetsPath.length())), false));
}
}
Files.deleteIfExists(settings.getPropertyPath("modsdir").resolve(path.subpath(path.getNameCount() - 1, path.getNameCount())));
modArchive.writeToFile();
output.add(modArchive);
}
private static void processArchive(final Path path, final Set<Archive> output, final SettingsModelFactory settingsFactory) throws IOException, StarDBException {
Archive originalArchive = new Archive(path);
if (!originalArchive.extract()) {
throw new IOException("Could not extract archive.");
}
Set<ArchiveFile> usedPaks = new HashSet<>();
for (ArchiveFile file : originalArchive.getFiles()) {
if (FileHelper.getExtension(file.getPath()).equals("modinfo")) {
JsonObject o = JsonObject.readFrom(new String(file.getData()));
String preformattedPath = o.get("path").asString().replaceAll("\"", "");
if (preformattedPath.startsWith("./")) {
preformattedPath = preformattedPath.replace("./", "");
}
if (preformattedPath.startsWith(".")) {
preformattedPath = preformattedPath.replace(".", "");
}
if (preformattedPath.startsWith("/")) {
preformattedPath = preformattedPath.replace("/", "");
}
Path modinfoPath = file.getPath();
if (modinfoPath.getNameCount() > 2) {
modinfoPath = modinfoPath.subpath(0, modinfoPath.getNameCount() - 1);
} else if (modinfoPath.getNameCount() > 1){
modinfoPath = modinfoPath.getName(0);
} else {
modinfoPath = Paths.get("");
}
Archive outputArchive = new Archive(settingsFactory.getInstance().getPropertyPath("modsdir").resolve(Paths.get(o.get("name").asString() + ".zip")));
if (file.getPath().getNameCount() == 1) {
outputArchive.addFile(new ArchiveFile(file.getData(), file.getPath(), false));
log.debug("'{}' -> '{}' relativized to '{}'", modinfoPath, file.getPath(), file.getPath());
} else {
outputArchive.addFile(new ArchiveFile(file.getData(), modinfoPath.relativize(file.getPath()).normalize(), false));
log.debug("'{}' -> '{}' relativized to '{}'", modinfoPath, file.getPath(), modinfoPath.relativize(file.getPath()).normalize());
}
Path assetsPath = modinfoPath.resolve(Paths.get(preformattedPath));
log.debug("Assets path for modinfo '{}': {}", file.getPath(), assetsPath);
if (originalArchive.getFile(assetsPath) != null && !assetsPath.toString().isEmpty() && !originalArchive.getFile(assetsPath).isFolder()) {
if (FileHelper.identifyType(originalArchive.getFile(assetsPath).getData()).equals("pak")) {
log.debug("Assets for mod '{}' identified as .pak file: {}", modinfoPath, assetsPath);
usedPaks.add(originalArchive.getFile(assetsPath));
ArchiveFile modinfo = outputArchive.getFile(".modinfo");
JsonObject o2 = JsonObject.readFrom(new String(modinfo.getData()));
o2.set("path", "assets");
modinfo.setData(o2.toString().getBytes());
//TODO Update StarDB to let me pass in a byte array instead of needing to open a file
Path tempPath = Paths.get("tempPak" + System.nanoTime());
SeekableByteChannel tempPakFile = Files.newByteChannel(tempPath, StandardOpenOption.CREATE, StandardOpenOption.WRITE);
tempPakFile.write(ByteBuffer.wrap(originalArchive.getFile(assetsPath).getData()));
tempPakFile.close();
AssetDatabase database = AssetDatabase.open(tempPath);
for (String assetFile : database.getFileList()) {
outputArchive.addFile(new ArchiveFile(database.getAsset(assetFile), Paths.get("assets/" + assetFile), false));
log.trace("Asset extracted: {} | {}", assetFile, Paths.get("assets/" + assetFile));
}
Files.deleteIfExists(tempPath);
}
} else {
ArchiveFile modinfo = outputArchive.getFile(".modinfo");
JsonObject o2 = JsonObject.readFrom(new String(modinfo.getData()));
o2.set("path", "assets");
modinfo.setData(o2.toString().getBytes());
log.debug("Assets for mod '{}' is a standard assets folder: {}", modinfoPath, assetsPath);
for (ArchiveFile f2 : originalArchive.getFiles()) {
if ((assetsPath.toString().isEmpty() || f2.getPath().startsWith(assetsPath)) && !f2.isFolder() && !f2.getPath().toString().endsWith(".modinfo")) {
if (modinfoPath.toString().isEmpty()) {
if (f2.getPath().getNameCount() == 1) {
if (!f2.getPath().startsWith(assetsPath)) {
outputArchive.addFile(new ArchiveFile(f2.getData(), Paths.get("assets/").resolve(f2.getPath()).normalize(), false));
log.trace("'{}' -> '{}' relativized to '{}'", modinfoPath, f2.getPath(), Paths.get("assets/").resolve(f2.getPath()).normalize());
} else {
outputArchive.addFile(new ArchiveFile(f2.getData(), f2.getPath(), false));
log.trace("'{}' -> '{}' relativized to '{}'", modinfoPath, f2.getPath(), f2.getPath());
}
} else {
if (!f2.getPath().startsWith(assetsPath)) {
outputArchive.addFile(new ArchiveFile(f2.getData(), Paths.get("assets/").resolve(f2.getPath()).normalize(), false));
log.trace("'{}' -> '{}' relativized to '{}'", modinfoPath, f2.getPath(), Paths.get("assets/").resolve(f2.getPath()).normalize());
} else {
outputArchive.addFile(new ArchiveFile(f2.getData(), f2.getPath(), false));
log.trace("'{}' -> '{}' relativized to '{}'", modinfoPath, f2.getPath(), f2.getPath());
}
}
} else {
if (f2.getPath().getNameCount() == 1) {
if (!modinfoPath.relativize(f2.getPath()).startsWith("assets")) {
outputArchive.addFile(new ArchiveFile(f2.getData(), Paths.get("assets/").resolve(f2.getPath()), false));
log.trace("'{}' -> '{}' relativized to '{}'", modinfoPath, f2.getPath(), Paths.get("assets/").resolve(f2.getPath()));
} else {
outputArchive.addFile(new ArchiveFile(f2.getData(), f2.getPath(), false));
log.trace("'{}' -> '{}' relativized to '{}'", modinfoPath, f2.getPath(), f2.getPath());
}
} else {
if (!modinfoPath.relativize(f2.getPath()).startsWith("assets")) {
outputArchive.addFile(new ArchiveFile(f2.getData(), Paths.get("assets/").resolve(modinfoPath.relativize(f2.getPath()).normalize()), false));
log.trace("'{}' -> '{}' relativized to '{}'", modinfoPath, f2.getPath(), Paths.get("assets/").resolve(modinfoPath.relativize(f2.getPath()).normalize()));
} else {
outputArchive.addFile(new ArchiveFile(f2.getData(), modinfoPath.relativize(f2.getPath()).normalize(), false));
log.trace("'{}' -> '{}' relativized to '{}'", modinfoPath, f2.getPath(), modinfoPath.relativize(f2.getPath()).normalize());
}
}
}
}
}
}
outputArchive.writeToFile(settingsFactory.getInstance().getPropertyPath("modsdir").resolve(Paths.get(o.get("name").asString() + ".zip")).toFile()); //TODO
output.add(outputArchive);
}
}
for (ArchiveFile file : originalArchive.getFiles()) {
if (!usedPaks.contains(file) && !file.isFolder() && FileHelper.identifyType(file.getData()).equals("pak")) {
log.debug("Additional .pak identified: {}", file.getPath());
Path tempPath = Paths.get("tempPak" + System.nanoTime());
SeekableByteChannel tempPakFile = Files.newByteChannel(tempPath, StandardOpenOption.CREATE, StandardOpenOption.WRITE);
tempPakFile.write(ByteBuffer.wrap(file.getData()));
tempPakFile.close();
AssetDatabase database = AssetDatabase.open(tempPath);
log.debug(database.getFileList());
if (database.getAsset("/pak.modinfo") != null) {
log.debug("{} has a .modinfo file, parsing into mod.", file.getPath());
processPakFile(tempPath, output, settingsFactory);
} else {
log.debug("{} does not contain a .modinfo file, skipping.", file.getPath());
}
Files.deleteIfExists(tempPath);
}
}
}
public boolean conflictsWith(final Mod mod) {
for (ModFile file : files) {
if (file.isAutoMerged() || file.isIgnored()) {
continue;
}
for (ModFile otherFile : mod.files) {
if (otherFile.isAutoMerged() || otherFile.isIgnored()) {
continue;
}
if (file.getPath().equals(otherFile.getPath())) {
return true;
}
}
}
return false;
}
public String getInternalName() {
return internalName;
}
protected void setInternalName(final String internalName) {
this.internalName = internalName;
}
public String getDisplayName() {
return displayName;
}
protected void setDisplayName(final String displayName) {
this.displayName = displayName;
}
public String getModVersion() {
if (modVersion.equals(NO_VERSION)) {
return localizer.getMessage("mod.unknownversion");
}
return modVersion;
}
protected void setModVersion(final String version) {
this.modVersion = version;
}
public String getGameVersion() {
return gameVersion;
}
protected void setGameVersion(final String gameVersion) {
this.gameVersion = gameVersion;
}
public String getAuthor() {
if (author.equals(NO_AUTHOR)) {
return localizer.getMessage("mod.unknownauthor");
}
return author;
}
protected void setAuthor(final String author) {
this.author = author;
}
public String getDescription() {
if (description.equals(NO_DESCRIPTION)) {
return localizer.getMessage("mod.nodescription");
}
return description;
}
protected void setDescription(final String description) {
this.description = description;
}
public String getURL() {
return url;
}
protected void setURL(final String url) {
this.url = url;
}
public String getArchiveName() {
return archiveName;
}
protected void setArchiveName(final String file) {
this.archiveName = file;
}
protected long getChecksum() {
return checksum;
}
protected void setChecksum(final long checksum) {
this.checksum = checksum;
}
public boolean isHidden() {
return hidden;
}
protected void setHidden(final boolean hidden) {
this.hidden = hidden;
}
public boolean isInstalled() {
return installed;
}
protected void setInstalled(final boolean installed) {
this.installed = installed;
notifyObservers("installstatuschanged");
}
public boolean hasImage() {
return hasImage;
}
public String getImageLocation() {
return settings.getPropertyPath("modsdir").resolve(settings.getPropertyPath("modsimagedir")).resolve(Paths.get(imageName)).toAbsolutePath().toString();
}
protected void setImage() {
//TODO
}
public Set<String> getDependencies() {
return dependencies;
}
protected void setDependencies(final Set<String> dependencies2) {
this.dependencies = dependencies2;
}
public Set<ModFile> getFiles() {
return files;
}
protected void setFiles(final Set<ModFile> files) {
this.files = files;
}
public int getOrder() {
return order;
}
protected void setOrder(final int order) {
this.order = order;
}
@Override
public void addObserver(final Observer observer) {
observers.add(observer);
}
@Override
public void removeObserver(final Observer observer) {
observers.remove(observer);
}
private final void notifyObservers(final Object message) {
for (final Observer o : observers) {
o.update(this, message);
}
}
}