Something went wrong. Try again.
Data management tools for playing BattleTech
Something went wrong. Try again.
Java
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221import 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. * * <p>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. * * <p>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. * * <p>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 <out.jsonl> [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<String> 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<String, Object> 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<String, Object> 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<String, Object> 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 "<unnamed>"; } }
private DumpBv() { }}