Something went wrong. Try again.
Data management tools for playing BattleTech
Something went wrong. Try again.
Java
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225import 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. * * <p>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. * * <p>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 <out.jsonl>"); 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<Field> 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 "<unnamed>"; } }
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('"'); }}