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() {
}
}