Fractal Softworks Forum

Please login or register.

Login with username, password and session length

Author Topic: Save modlist extractor  (Read 769 times)

Histidine

  • Admiral
  • *****
  • Posts: 4682
    • View Profile
    • GitHub profile
Save modlist extractor
« on: September 07, 2020, 09:09:18 PM »

I made a tool to convert a save's descriptor.xml to an enabled_mods.json file. It prints warnings to console if any of the mods are not found in the current install, too.

How to use (Windows): run the .exe, browse to descriptor.xml, browse to where you want to save the file (by default this is the /mods folder corresponding to the /saves folder from the previous step), done.

Download

Source (too lazy to GitHub a one-off project)
Spoiler
Code: java
package org.histidine.modlistextractor;

import java.awt.FileDialog;
import java.awt.Frame;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.Writer;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

public class main {

public static void main(String[] args) {
FileDialog loadDialog = new FileDialog((Frame)null, "Select save descriptor.xml");
loadDialog.setMode(FileDialog.LOAD);
//dialog.setFilenameFilter(new XMLFileFilter());
loadDialog.setFile("descriptor.xml");
loadDialog.setVisible(true);

if (loadDialog.getFile() == null) {
System.out.println("No file selected");
System.exit(0);
return;
}

Path path = Paths.get(loadDialog.getDirectory(), loadDialog.getFile());
System.out.println("Loading file " + path);
List<ModEntry> mods;

mods = getModList(path.toString());


String str = createEnabledModsJsonAsString(mods);
//System.out.println(str);

FileDialog saveDialog = new FileDialog((Frame)null, "Select enabled_mods.json");
saveDialog.setMode(FileDialog.SAVE);
String modPath = Paths.get(loadDialog.getDirectory(), "..", "..", "mods").toString();
saveDialog.setDirectory(modPath);
saveDialog.setFile("enabled_mods.json");
saveDialog.setVisible(true);

if (saveDialog.getFile() == null) {
System.exit(0);
return;
}

path = Paths.get(saveDialog.getDirectory(), saveDialog.getFile());
File json = path.toFile();

try (Writer output = new BufferedWriter(new FileWriter(json))) {
output.write(str);
output.close();
} catch (Exception ex) {
throw new RuntimeException(ex);
}

System.out.println("Done, press Enter to exit");
try {
System.in.read();
} catch (IOException ex) {}

System.exit(0);
}

/**
* Gets the mod list from a Starsector save's descriptor.xml.
* @param path
* @return
*/
public static List<ModEntry> getModList(String path)
{
Map<Integer, ModEntry> allMods = new HashMap<>();
List<ModEntry> modlist = new ArrayList<>();
try {
File xml = new File(path);

DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
DocumentBuilder dBuilder = dbFactory.newDocumentBuilder();
Document doc = dBuilder.parse(xml);
doc.getDocumentElement().normalize();

// Load mod data from all mods ever enabled
Node everMods = doc.getElementsByTagName("allModsEverEnabled").item(0);
NodeList nlEverMods = everMods.getChildNodes();
for (int i=0; i<nlEverMods.getLength(); i++) {
Node node = nlEverMods.item(i);
String nodeName = node.getNodeName();
if (!nodeName.equals("com.fs.starfarer.campaign.ModAndPluginData_-EnabledModData"))
continue;
Element el = (Element)node;

int xmlId = Integer.parseInt(((Element)getElement(el, "spec")).getAttribute("z"));
String id = getElement(el, "id").getTextContent();
String name = getElement(el, "name").getTextContent();
String versionStr = "";
NodeList version = el.getElementsByTagName("versionInfo");
for (int j=0; j<version.getLength(); j++) {
Node versionNode = version.item(j);
if (versionNode.getNodeName().equals("string")) {
versionStr = versionNode.getTextContent();
}
}


// Store only the mod's folder name, not the full path
// This is because someone else's saves may come from a Starsector copy installed to a different directory
String modPath = getElement(el, "path").getTextContent();
String folderName = FileSystems.getDefault().getPath(modPath).getFileName().toString();

ModEntry entry = new ModEntry(id, name, versionStr, folderName);
allMods.put(xmlId, entry);
}

// Process mods last saved with
// Go up three levels: save folder -> saves/ -> starsector root
Path modFolderPath = FileSystems.getDefault().getPath(path, "..", "..", "..", "mods").normalize();
Node lastMods = doc.getElementsByTagName("enabledMods").item(0);
NodeList nlLastMods = lastMods.getChildNodes();
for (int i=0; i<nlEverMods.getLength(); i++) {
Node node = nlLastMods.item(i);
if (node == null) {
continue;
}
String nodeName = node.getNodeName();
if (!nodeName.equals("com.fs.starfarer.campaign.ModAndPluginData_-EnabledModData"))
continue;
Element el = (Element)node;

int xmlId = Integer.parseInt(((Element)getElement(el, "spec")).getAttribute("ref"));
ModEntry entry = allMods.get(xmlId);

Path modPath = FileSystems.getDefault().getPath(modFolderPath.toString(), entry.folderName);
//System.out.println(modPath.toString() + ", " + entry.folderName);
if (!Files.exists(modPath)) {
System.err.println("Warning, mod not found: " + entry.name + " " +
entry.version + " (" + modPath.toString() + ")");
}

//System.out.println(entry);
modlist.add(entry);
}
Collections.sort(modlist);
} catch (Exception ex) {
throw new RuntimeException(ex);
}

return modlist;
}

/**
* Creates a string ready to write to {@code enabled_mods.json}.
* @param mods
* @return
*/
public static String createEnabledModsJsonAsString(List<ModEntry> mods) {
StringBuilder sb = new StringBuilder();
sb.append("{\"enabledMods\": [\r\n");
for (ModEntry mod : mods) {
sb.append("  \"").append(mod.id).append("\",\r\n");
}
sb.append("]}");

return sb.toString();
}

public static Node getElement(Element el, String id) {
return el.getElementsByTagName(id).item(0);
}

public static class ModEntry implements Comparable<ModEntry> {
public String id;
public String name;
public String version;
public String folderName;

public ModEntry(String id, String name, String version, String folderName) {
this.id = id;
this.name = name;
this.version = version;
this.folderName = folderName;
}

@Override
public String toString() {
String str = String.format("%s v%s (id %s, path %s)", name, version, id, folderName);
return str;
}

@Override
public int compareTo(ModEntry other) {
return this.name.compareTo(other.name);
}
}
}
[close]

EDIT 2021-08-05: Updated source code for Starsector 0.95 (but not the linked application)
« Last Edit: August 05, 2021, 05:52:46 AM by Histidine »
Logged