Something went wrong. Try again.
Data management tools for playing BattleTech
Something went wrong. Try again.
Java
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361import 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. * * <p>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. * * <p>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: * * <ul> * <li>{@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.</li> * <li>{@code --force "Chassis|Model" ...} writes whether the named units * together qualify, which tests the assembly rules rather than the * requirements.</li> * </ul> * * <p>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 <out.jsonl> [--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<MekSummary> 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<MekSummary> 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<FormationType.Constraint> 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<FormationType.Constraint> 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('"'); }}