import megamek.common.loaders.MekSummary;
import megamek.common.loaders.MekSummaryCache;
import java.io.BufferedWriter;
import java.io.FileOutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
/**
* Dump every unit MegaMek knows about as JSON Lines, one object per unit.
*
*
Reads MekSummaryCache, which is MegaMek's own index over data/mekfiles.
* Going through the cache rather than parsing .mtf/.blk by hand is the whole
* point: battle value, cost and the Alpha Strike conversion are computed at
* load time and appear in no unit file, so a hand-rolled parser cannot produce
* them without reimplementing the construction rules.
*
*
Fields are walked reflectively rather than through ~120 named getters, so
* a MegaMek bump that adds or renames a field changes the output columns
* instead of failing to compile.
*
* java -cp "MegaMek.jar:lib/*" DumpUnits.java out.jsonl
*
* Must run with MegaMek's install dir as the working directory - MegaMek
* resolves data/ relative to cwd.
*/
public final class DumpUnits {
public static void main(String[] args) throws Exception {
if (args.length < 1) {
System.err.println("usage: DumpUnits ");
System.exit(2);
}
long start = System.currentTimeMillis();
MekSummaryCache cache = MekSummaryCache.getInstance();
long deadline = start + 600_000L;
while (!cache.isInitialized()) {
if (System.currentTimeMillis() > deadline) {
System.err.println("units.cache did not initialise within 600s");
System.exit(1);
}
Thread.sleep(250L);
}
MekSummary[] all = cache.getAllMeks();
List fields = new ArrayList<>();
for (Field f : MekSummary.class.getDeclaredFields()) {
if (Modifier.isStatic(f.getModifiers())) {
continue;
}
try {
f.setAccessible(true);
fields.add(f);
} catch (RuntimeException e) {
System.err.println("skipping field " + f.getName() + ": " + e);
}
}
int written = 0;
int skipped = 0;
try (PrintWriter out = new PrintWriter(new BufferedWriter(
new OutputStreamWriter(new FileOutputStream(args[0]), StandardCharsets.UTF_8)))) {
for (MekSummary ms : all) {
try {
StringBuilder sb = new StringBuilder(4096);
sb.append('{');
boolean first = true;
for (Field f : fields) {
Object v;
try {
v = f.get(ms);
} catch (IllegalAccessException e) {
continue;
}
if (!first) {
sb.append(',');
}
first = false;
str(sb, f.getName());
sb.append(':');
val(sb, v, 0);
}
sb.append('}');
out.println(sb);
written++;
} catch (Throwable t) {
// A unit that cannot be serialised at all is named and skipped,
// so the run reports what is missing instead of silently short.
skipped++;
System.err.println("skipped " + safeName(ms) + ": " + t);
}
}
}
System.out.printf("dumped %d units in %.1fs (%d skipped, %d unrenderable fields) -> %s%n",
written, (System.currentTimeMillis() - start) / 1000.0, skipped, FAILED_FIELDS, args[0]);
// MegaMek leaves non-daemon threads behind; nothing left to do but leave.
System.exit(0);
}
/**
* Serialise one value. Anything MegaMek models as its own type (Alpha Strike
* damage vectors, arcs, special-ability collections, enums) has a toString
* that is the game's own notation, so it is kept as that string rather than
* exploded - the SQL layer can parse it if it ever needs to.
*/
private static void val(StringBuilder sb, Object v, int depth) {
if (v == null) {
sb.append("null");
} else if (v instanceof Number n) {
double d = n.doubleValue();
// NaN and infinities are not JSON; they show up in unfinished designs.
sb.append(Double.isFinite(d) ? n.toString() : "null");
} else if (v instanceof Boolean b) {
sb.append(b.toString());
} else if (v instanceof CharSequence) {
str(sb, v.toString());
} else if (depth > 3) {
// Guard against a cyclic object graph; nothing real nests this deep.
safeToString(sb, v);
} else if (v instanceof Map, ?> m) {
sb.append('{');
boolean first = true;
for (Map.Entry, ?> e : m.entrySet()) {
if (!first) {
sb.append(',');
}
first = false;
str(sb, String.valueOf(e.getKey()));
sb.append(':');
val(sb, e.getValue(), depth + 1);
}
sb.append('}');
} else if (v instanceof Collection> c) {
sb.append('[');
boolean first = true;
for (Object o : c) {
if (!first) {
sb.append(',');
}
first = false;
val(sb, o, depth + 1);
}
sb.append(']');
} else if (v.getClass().isArray()) {
sb.append('[');
int n = java.lang.reflect.Array.getLength(v);
for (int i = 0; i < n; i++) {
if (i > 0) {
sb.append(',');
}
val(sb, java.lang.reflect.Array.get(v, i), depth + 1);
}
sb.append(']');
} else {
safeToString(sb, v);
}
}
/**
* toString() on MegaMek's own value types is the game's notation, but some
* of them throw on incomplete data - ASTurretSummary NPEs formatting the
* specials of certain turreted units, for one. A field that cannot render
* becomes null rather than taking the whole run down. The string is built
* before anything is appended so a throw cannot leave a half-written value
* in the buffer.
*/
private static void safeToString(StringBuilder sb, Object v) {
String s;
try {
s = v.toString();
} catch (Throwable t) {
FAILED_FIELDS++;
sb.append("null");
return;
}
if (s == null) {
sb.append("null");
} else {
str(sb, s);
}
}
private static int FAILED_FIELDS = 0;
private static String safeName(MekSummary ms) {
try {
return ms.getName();
} catch (Throwable t) {
return "";
}
}
private static void str(StringBuilder sb, String s) {
sb.append('"');
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
switch (c) {
case '"' -> sb.append("\\\"");
case '\\' -> sb.append("\\\\");
case '\n' -> sb.append("\\n");
case '\r' -> sb.append("\\r");
case '\t' -> sb.append("\\t");
default -> {
if (c < 0x20) {
sb.append(String.format("\\u%04x", (int) c));
} else {
sb.append(c);
}
}
}
}
sb.append('"');
}
}