import megamek.client.ui.clientGUI.calculationReport.TextCalculationReport; import megamek.common.battleValue.BVCalculator; import megamek.common.loaders.MekSummary; import megamek.common.loaders.MekSummaryCache; import megamek.common.units.Entity; 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.LinkedHashMap; import java.util.Map; /** * Dump the working of MegaMek's battle value calculation, one object per unit. * *

DumpUnits already carries the answer - MekSummary.bv - and an answer alone * makes a poor oracle for a reimplementation. Battle value is a defensive * rating and an offensive rating combined, and a total that is 40 points light * says nothing about which half is wrong. This runs the calculation and records * its intermediate state, so each part of helm-bv can be checked on its own * rather than only once all of them exist. * *

Fields are walked reflectively up the calculator's class hierarchy, the * way DumpEquipment walks equipment: MekBVCalculator carries its own state on * top of BVCalculator's, and a MegaMek bump that moves a field should change * the output columns rather than fail to compile. * *

Unlike DumpUnits this has to load every Entity rather than read the * summary cache, because the calculator needs a real unit. That is the whole * cost of the program. * * java -cp "MegaMek.jar:lib/*" DumpBv.java out.jsonl [unitType ...] [--report] * * Must run with MegaMek's install dir as the working directory. */ public final class DumpBv { public static void main(String[] args) throws Exception { if (args.length < 1) { System.err.println("usage: DumpBv [unitType]"); System.exit(2); } // Several types may be named. A combat vehicle is scored by the // calculator every unit falls back to, and MekSummary files it under // whichever of Tank, VTOL and Naval it looks like - so asking for // "vehicles" is asking for three names. java.util.Set only = new java.util.LinkedHashSet<>(); boolean wantReport = false; for (int i = 1; i < args.length; i++) { if (args[i].equals("--report")) { wantReport = true; } else { only.add(args[i].toLowerCase(java.util.Locale.ROOT)); } } 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); } 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 : cache.getAllMeks()) { String name = safeName(ms); try { if (!only.isEmpty() && !only.contains(ms.getUnitType().toLowerCase(java.util.Locale.ROOT))) { continue; } Entity entity = ms.loadEntity(); if (entity == null) { skipped++; continue; } BVCalculator calc = BVCalculator.getBVCalculator(entity); // ignoreC3 false and ignoreSkill false is what MekSummary.bv // records, so the total here is comparable with the one // already in units.jsonl. A stock unit's crew is the regular // 4/5 whose skill multiplier is exactly 1. // MegaMek writes its own working into a report as it goes // - every weapon with the heat it cost and what it counted // for, every multiplier with the figure before and after - // and that is a far better oracle than the total. Off by // default because it is most of the file's size. TextCalculationReport report = wantReport ? new TextCalculationReport() : null; int bv = report == null ? calc.calculateBV(false, false) : calc.calculateBV(false, false, report); Map row = new LinkedHashMap<>(); row.put("name", name); row.put("unitType", ms.getUnitType()); row.put("bv", bv); collectFields(calc, row); if (report != null) { row.put("report", report.toString()); } try { row.put("offensiveSpeedFactorMultiplier", calc.getOffensiveSpeedFactorMultiplier()); } catch (Throwable ignored) { // Not every subclass answers this; absence is not failure. } StringBuilder sb = new StringBuilder(1024); sb.append('{'); boolean first = true; for (Map.Entry e : row.entrySet()) { if (!first) { sb.append(','); } first = false; str(sb, e.getKey()); sb.append(':'); val(sb, e.getValue()); } sb.append('}'); out.println(sb); written++; } catch (Throwable t) { skipped++; System.err.println("skipped " + name + ": " + t); } } } System.out.printf("dumped %d units in %.1fs (%d skipped) -> %s%n", written, (System.currentTimeMillis() - start) / 1000.0, skipped, args[0]); // MegaMek leaves non-daemon threads behind; nothing left to do but leave. System.exit(0); } /** * Every non-static field on the calculator, subclass first so that a name * a subclass redeclares wins - that is the value the calculation used. */ private static void collectFields(BVCalculator calc, Map row) { for (Class c = calc.getClass(); c != null && c != Object.class; c = c.getSuperclass()) { for (Field f : c.getDeclaredFields()) { if (Modifier.isStatic(f.getModifiers()) || row.containsKey(f.getName())) { continue; } Object v; try { f.setAccessible(true); v = f.get(calc); } catch (Throwable t) { continue; } // The report and the entity are the calculation's scaffolding, // not its working, and both are huge object graphs. if (v == null || v instanceof Number || v instanceof Boolean || v instanceof CharSequence) { row.put(f.getName(), v); } } } } private static void val(StringBuilder sb, Object v) { if (v == null) { sb.append("null"); } else if (v instanceof Number n) { double d = n.doubleValue(); sb.append(Double.isFinite(d) ? n.toString() : "null"); } else if (v instanceof Boolean b) { sb.append(b.toString()); } else { str(sb, String.valueOf(v)); } } 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('"'); } private static String safeName(MekSummary ms) { try { return ms.getName(); } catch (Throwable t) { return ""; } } private DumpBv() { } }