import megamek.client.ratgenerator.FormationType;
import megamek.client.ratgenerator.MissionRole;
import megamek.common.loaders.MekSummary;
import megamek.common.loaders.MekSummaryCache;
import megamek.common.units.UnitType;
import java.io.BufferedWriter;
import java.io.FileOutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
/**
* Dump the formation blueprints MegaMek carries, and its verdict on a unit.
*
*
Campaign Operations defines what makes a lance a Fire Lance rather than
* four Meks: an ideal role, weight bounds, and requirements like "at least
* three units with the Sniper or Missile Boat role". MegaMek holds all of it in
* {@code FormationType}, so helm reads the blueprints off the shelf the way it
* reads the unit library - nothing here is committed.
*
*
The predicates themselves are lambdas and cannot be read out, only their
* display strings. helm reimplements them from those strings and is measured
* against MegaMek's own answers, which is the arrangement battle value already
* uses. Two oracle modes serve that:
*
*
* - {@code --matches N} writes, for the first N designs in the cache, whether
* each blueprint's main criterion and each of its requirements matches that one
* design. This is the per-unit oracle, and it is what tells a mis-parsed
* requirement from a correct one.
* - {@code --force "Chassis|Model" ...} writes whether the named units
* together qualify, which tests the assembly rules rather than the
* requirements.
*
*
* Usage: DumpFormations out.jsonl [--matches N | --force "Chassis|Model" ...]
*/
public final class DumpFormations {
/** The range bands the damage requirements ask about. */
private static final int[] RANGES = { 6, 7, 9, 18 };
/** The bands asked about for one weapon rather than a total. */
private static final int[] SINGLE_RANGES = { 15, 18 };
public static void main(String[] args) throws Exception {
if (args.length < 1) {
System.err.println("usage: DumpFormations [--matches N]"
+ " [--force \"Chassis|Model\" ...]");
System.exit(2);
}
MekSummaryCache cache = MekSummaryCache.getInstance();
long deadline = System.currentTimeMillis() + 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);
}
FormationType.createFormationTypes();
List force = new ArrayList<>();
boolean wantForce = false;
int sample = 0;
for (int i = 1; i < args.length; i++) {
if (args[i].equals("--force")) {
wantForce = true;
continue;
}
if (args[i].equals("--matches")) {
sample = Integer.parseInt(args[++i]);
continue;
}
force.add(lookUp(cache, args[i]));
}
int written;
try (PrintWriter out = new PrintWriter(new BufferedWriter(
new OutputStreamWriter(new FileOutputStream(args[0]), StandardCharsets.UTF_8)))) {
written = sample > 0
? writeMatches(out, cache, sample)
: writeBlueprints(out, force, wantForce);
}
System.err.println("wrote " + written + " rows");
}
private static MekSummary lookUp(MekSummaryCache cache, String arg) {
String[] parts = arg.split("\\|", 2);
String name = parts.length > 1 && !parts[1].isBlank()
? parts[0] + " " + parts[1]
: parts[0];
MekSummary ms = cache.getMek(name);
if (ms == null) {
System.err.println("no such design: " + name);
System.exit(1);
}
return ms;
}
/** One row per blueprint: everything a validator has to reproduce. */
private static int writeBlueprints(PrintWriter out, List force, boolean wantForce) {
int written = 0;
for (FormationType type : FormationType.getAllFormations()) {
StringBuilder sb = new StringBuilder(1024);
sb.append('{');
field(sb, "name", type.getName());
sb.append(',');
field(sb, "category", type.getCategory());
sb.append(",\"ground\":").append(type.isGround());
sb.append(",\"minWeightClass\":").append(type.getMinWeightClass());
sb.append(",\"maxWeightClass\":").append(type.getMaxWeightClass());
sb.append(',');
field(sb, "idealRole", String.valueOf(type.getIdealRole()));
sb.append(',');
field(sb, "mainDescription", type.getMainDescription());
sb.append(",\"unitTypes\":[");
boolean firstType = true;
for (int t = 0; t < UnitType.SIZE; t++) {
if (!type.isAllowedUnitType(t)) {
continue;
}
if (!firstType) {
sb.append(',');
}
firstType = false;
quote(sb, UnitType.getTypeName(t));
}
sb.append(']');
sb.append(",\"missionRoles\":[");
boolean firstRole = true;
for (MissionRole role : type.getMissionRoles()) {
if (!firstRole) {
sb.append(',');
}
firstRole = false;
quote(sb, role.toString());
}
sb.append(']');
sb.append(",\"criteria\":[");
Iterator it = type.getOtherCriteria();
boolean first = true;
while (it.hasNext()) {
if (!first) {
sb.append(',');
}
first = false;
constraint(sb, it.next());
}
sb.append(']');
// The grouping requirement, where there is one: "two units of the
// same chassis" and its kin. Held apart from the others upstream
// because it is a rule about pairs rather than about a unit.
FormationType.GroupingConstraint grouping = type.getGroupingCriteria();
sb.append(",\"grouping\":");
if (grouping == null) {
sb.append("null");
} else {
constraint(sb, grouping);
sb.setLength(sb.length() - 1);
sb.append(",\"groupSize\":").append(grouping.getGroupSize());
sb.append(",\"numGroups\":").append(grouping.getNumGroups());
sb.append('}');
}
if (wantForce) {
// Upstream can throw here rather than answer - see UPSTREAM.md
// - and one blueprint that cannot make up its mind must not
// cost the answer for the other forty.
Boolean verdict = null;
String report = null;
String error = null;
try {
verdict = type.qualifies(force);
report = type.qualificationReport(force);
} catch (Throwable t) {
error = t.toString();
}
// Which units the grouping requirement looks at, and which
// pairs it accepts. Neither is visible in the verdict, and a
// reimplementation that gets the verdict right by accident is
// worth nothing.
if (grouping != null) {
sb.append(",\"groupMatches\":[");
for (int u = 0; u < force.size(); u++) {
if (u > 0) {
sb.append(',');
}
sb.append(grouping.matches(force.get(u)));
}
sb.append("],\"groupPairs\":[");
boolean firstPair = true;
for (int a = 0; a < force.size(); a++) {
for (int b = a + 1; b < force.size(); b++) {
if (!firstPair) {
sb.append(',');
}
firstPair = false;
sb.append('[').append(a).append(',').append(b).append(',')
.append(grouping.matches(force.get(a), force.get(b)))
.append(']');
}
}
sb.append(']');
}
sb.append(",\"qualifies\":").append(verdict);
sb.append(',');
if (error == null) {
field(sb, "report", report);
} else {
field(sb, "error", error);
}
}
sb.append('}');
out.println(sb);
written++;
}
return written;
}
/**
* The minimum each requirement asks of a force of 1 through 20. Dumped as
* the whole curve rather than a percentage, because the two kinds of
* requirement - a fixed count and a proportion - are private fields, and a
* curve says what either of them means without reading one.
*/
private static void constraint(StringBuilder sb, FormationType.Constraint c) {
sb.append('{');
field(sb, "description", c.getDescription());
sb.append(",\"pairedWithNext\":").append(c.isPairedWithNext());
sb.append(",\"pairedWithPrevious\":").append(c.isPairedWithPrevious());
sb.append(",\"minimums\":[");
for (int size = 1; size <= 20; size++) {
if (size > 1) {
sb.append(',');
}
sb.append(c.getMinimum(size));
}
sb.append("]}");
}
/** One row per design: which requirements MegaMek says that design meets. */
private static int writeMatches(PrintWriter out, MekSummaryCache cache, int sample) {
MekSummary[] all = cache.getAllMeks();
// Strided rather than the first N, so a sample of 400 spans the whole
// library instead of whatever the file walk happened to reach first.
int step = Math.max(1, all.length / Math.max(1, sample));
int written = 0;
for (int i = 0; i < all.length && written < sample; i += step) {
MekSummary ms = all[i];
StringBuilder sb = new StringBuilder(2048);
sb.append('{');
field(sb, "chassis", ms.getChassis());
sb.append(',');
field(sb, "model", ms.getModel());
sb.append(',');
field(sb, "unitType", ms.getUnitType());
// The figures the damage requirements are worked out from, so a
// disagreement can be read as "helm says 9, MegaMek says 40"
// rather than "some requirement about damage".
sb.append(",\"damage\":{");
for (int r = 0; r < RANGES.length; r++) {
if (r > 0) {
sb.append(',');
}
quote(sb, String.valueOf(RANGES[r]));
sb.append(':').append(damageAt("getDamageAtRange", ms, RANGES[r]));
}
sb.append("},\"single\":{");
for (int r = 0; r < SINGLE_RANGES.length; r++) {
if (r > 0) {
sb.append(',');
}
quote(sb, String.valueOf(SINGLE_RANGES[r]));
sb.append(':')
.append(damageAt("getSingleWeaponDamageAtRange", ms, SINGLE_RANGES[r]));
}
sb.append('}');
sb.append(",\"formations\":{");
boolean firstType = true;
for (FormationType type : FormationType.getAllFormations()) {
if (!firstType) {
sb.append(',');
}
firstType = false;
quote(sb, type.getName());
sb.append(":{\"main\":").append(type.getMainCriteria().test(ms));
sb.append(",\"criteria\":[");
Iterator it = type.getOtherCriteria();
boolean first = true;
while (it.hasNext()) {
if (!first) {
sb.append(',');
}
first = false;
sb.append(it.next().matches(ms));
}
sb.append("]}");
}
sb.append("}}");
out.println(sb);
written++;
}
return written;
}
/**
* Call one of {@code FormationType}'s private damage helpers.
*
* Reflection rather than a reimplementation on purpose: the figure has to
* be MegaMek's to be worth anything here, and copying the calculation into
* this file would make the oracle agree with helm by construction.
*/
private static long damageAt(String method, MekSummary ms, int range) {
try {
java.lang.reflect.Method m = FormationType.class
.getDeclaredMethod(method, MekSummary.class, int.class);
m.setAccessible(true);
return (long) m.invoke(null, ms, range);
} catch (ReflectiveOperationException e) {
throw new IllegalStateException("no " + method + " to call: " + e, e);
}
}
private static void field(StringBuilder sb, String key, String value) {
quote(sb, key);
sb.append(':');
quote(sb, value == null ? "" : value);
}
private static void quote(StringBuilder sb, String value) {
sb.append('"');
for (int i = 0; i < value.length(); i++) {
char c = value.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('"');
}
}