Something went wrong. Try again.
Data management tools for playing BattleTech
Something went wrong. Try again.
Java
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425import megamek.common.equipment.EquipmentType;
import megamek.common.equipment.MiscType;import megamek.common.equipment.WeaponType;
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.Method;import java.lang.reflect.Modifier;import java.nio.charset.StandardCharsets;import java.util.ArrayList;import java.util.Collection;import java.util.LinkedHashMap;import java.util.List;import java.util.Map;
/** * Dump MegaMek's equipment catalogue as JSON Lines, one object per type. * * <p>MekSummary records a unit's loadout as internal lookup names - ISGaussRifle, * CLERMediumLaser - which do not match what anyone types. This resolves those * keys to display names and to the weapon statistics (heat, damage, ranges, * rack size) that a procgen filter needs, giving unit_equipment something to * join against. * * <p>Fields are walked up the class hierarchy, so WeaponType and AmmoType * subclass data comes out alongside the EquipmentType base fields. * * java -cp "MegaMek.jar:lib/*" DumpEquipment.java out.jsonl */public final class DumpEquipment {
private static int failedFields = 0;
public static void main(String[] args) throws Exception { if (args.length < 1) { System.err.println("usage: DumpEquipment <out.jsonl>"); System.exit(2); }
EquipmentType.initializeTypes(); List<EquipmentType> all = EquipmentType.allTypes();
int written = 0; int skipped = 0; try (PrintWriter out = new PrintWriter(new BufferedWriter( new OutputStreamWriter(new FileOutputStream(args[0]), StandardCharsets.UTF_8)))) { for (EquipmentType et : all) { try { StringBuilder sb = new StringBuilder(2048); sb.append('{');
str(sb, "_class"); sb.append(':'); str(sb, et.getClass().getSimpleName()); sb.append(','); str(sb, "_name"); sb.append(':'); safeToString(sb, callName(et, true)); sb.append(','); str(sb, "_internalName"); sb.append(':'); safeToString(sb, callName(et, false));
// The flags by name. MegaMek's own rules are written against // these - "counts as defensive equipment" is a list of them, // and so is "explodes when hit" - and the field itself // serialises as opaque bit indices, which nothing outside // this jar can read. Asked rather than decoded, so a release // that renumbers them changes nothing here. // The class hierarchy, not just the leaf. MegaMek's rules // ask `instanceof GaussWeapon` and `instanceof PPCWeapon`, // which no leaf class name answers - ISGaussRifle and // CLGaussRifle share only their parent. sb.append(','); str(sb, "_classes"); sb.append(":["); boolean firstClass = true; for (Class<?> c = et.getClass(); c != null && c != Object.class; c = c.getSuperclass()) { if (!firstClass) { sb.append(','); } firstClass = false; str(sb, c.getSimpleName()); } sb.append(']');
sb.append(','); str(sb, "_flags"); sb.append(':'); sb.append('['); boolean firstFlag = true; for (String flag : flagNames(et)) { if (!firstFlag) { sb.append(','); } firstFlag = false; str(sb, flag); } sb.append(']');
// The bay a weapon is grouped into aboard a large // aerospace unit. MegaMek makes one bay Mounted per // group while it reads the design, and the grouping is // in the weapon rather than in the file. Object bay = bayType(et); if (bay != null) { sb.append(','); str(sb, "bayType"); sb.append(':'); str(sb, String.valueOf(bay)); }
// Later (more derived) classes win on a name clash, which is // what a caller reading "damage" expects to see. Map<String, Object> vals = new LinkedHashMap<>(); for (Class<?> c = et.getClass(); c != null && c != Object.class; c = c.getSuperclass()) { for (Field f : c.getDeclaredFields()) { if (Modifier.isStatic(f.getModifiers()) || vals.containsKey(f.getName())) { continue; } try { f.setAccessible(true); vals.put(f.getName(), f.get(et)); } catch (Throwable t) { // inaccessible field, nothing to record } } } for (Map.Entry<String, Object> e : vals.entrySet()) { sb.append(','); str(sb, e.getKey()); sb.append(':'); val(sb, e.getValue(), 0); }
sb.append('}'); out.println(sb); written++; } catch (Throwable t) { skipped++; System.err.println("skipped equipment: " + t); } } written += extras(out); }
System.out.printf("dumped %d equipment types (%d skipped, %d unrenderable fields) -> %s%n", written, skipped, failedFields, args[0]); System.exit(0); }
/** * Two tables MegaMek keeps in code rather than in `data/`, written into * the same file as the equipment. * * <p>Neither is an EquipmentType, and both are needed to score a * conventional infantry platoon: an animal carries the platoon and lends * it the animal's own protection, and a prosthetic limb changes what a * trooper does climbing a Mek. A `.blk` names them and says nothing about * them - `motion_type: Beast:Camel` is the whole of it - so the figures * have to be read off the shelf here. * * <p>They share the equipment file because they share its whole * pipeline: one artifact is built, trimmed and shipped to a browser, and * a second one would have to be threaded through all of that to be worth * the same. `_class` tells them apart on the way back in. */ private static int extras(PrintWriter out) { int written = 0; for (megamek.common.units.InfantryMount mount : megamek.common.units.InfantryMount.sampleMounts) { // Keyed the way a `.blk` names it, so nothing has to know the // prefix on the way back in. out.println(record(mount, "InfantryMount", "Beast:" + mount.name(), mount.name())); written++; } for (megamek.common.enums.ProstheticEnhancementType type : megamek.common.enums.ProstheticEnhancementType.values()) { out.println(record(type, "ProstheticEnhancementType", type.name(), type.getDisplayName())); written++; } return written; }
/** One non-equipment record, with its own fields walked the same way. */ private static String record(Object value, String className, String key, String name) { StringBuilder sb = new StringBuilder(512); sb.append('{'); str(sb, "_class"); sb.append(':'); str(sb, className); sb.append(','); str(sb, "_internalName"); sb.append(':'); str(sb, key); sb.append(','); str(sb, "_name"); sb.append(':'); str(sb, name); for (Class<?> c = value.getClass(); c != null && c != Object.class; c = c.getSuperclass()) { for (Field f : c.getDeclaredFields()) { if (Modifier.isStatic(f.getModifiers())) { continue; } try { f.setAccessible(true); sb.append(','); str(sb, f.getName()); sb.append(':'); val(sb, f.get(value), 0); } catch (Throwable t) { // inaccessible field, nothing to record } } } sb.append('}'); return sb.toString(); }
/** * The bay this weapon belongs to, by its lookup name, or null for * anything that is not a weapon or is a bay itself. */ private static Object bayType(EquipmentType et) { if (!(et instanceof WeaponType weapon)) { return null; } try { EquipmentType bay = weapon.getBayType(); return bay == null || bay == et ? null : bay.getInternalName(); } catch (Throwable t) { return null; } }
private static Object callName(EquipmentType et, boolean display) { try { return display ? et.getName() : et.getInternalName(); } catch (Throwable t) { return null; } }
/** * Every `F_*` flag and `S_*` subtype this equipment carries, by name. * * MegaMek's own rules are written against these - "counts as defensive * equipment" is a list of them, and so is "explodes when hit". The field * itself serialises as opaque bit indices that nothing outside the jar can * read, so each constant is asked of the equipment in turn. Reflective for * the same reason the field walk is: a release that adds or renumbers a * flag should change a column rather than fail to compile. */ private static List<String> flagNames(EquipmentType et) { List<String> out = new ArrayList<>(); // MiscType and WeaponType hold the F_ constants; the subtypes that tell // a hatchet from a shield live on their own enums, and a club carries // both kinds. Missing the enums makes every club look identical. for (Class<?> holder : flagHolders()) { if (holder.isEnum()) { for (Object flag : holder.getEnumConstants()) { if (asks(et, flag)) { out.add(holder.getSimpleName() + "." + flag); } } continue; } for (Field f : holder.getDeclaredFields()) { // S_ constants as well as F_: MegaMek stores an item's subtype // in the same bit set, and that is the only thing telling a // shield apart from a hatchet - both are F_CLUB. if (!Modifier.isStatic(f.getModifiers()) || !(f.getName().startsWith("F_") || f.getName().startsWith("S_"))) { continue; } try { f.setAccessible(true); Object flag = f.get(null); if (flag == null) { continue; } if (asks(et, flag)) { out.add(holder.getSimpleName() + "." + f.getName()); } } catch (Throwable ignored) { // A flag that cannot be asked is one this equipment does not // have, as far as anything downstream is concerned. } } } return out; }
/** The classes carrying flag constants, skipping any a release has moved. */ private static List<Class<?>> flagHolders() { List<Class<?>> out = new ArrayList<>(); out.add(MiscType.class); out.add(WeaponType.class); for (String name : new String[] { "megamek.common.equipment.enums.MiscTypeFlag", "megamek.common.equipment.WeaponTypeFlag" }) { try { out.add(Class.forName(name)); } catch (Throwable ignored) { // A release that moves or drops a flag holder drops its column // rather than failing the run. } } return out; }
/** Whether the equipment carries this flag, whatever type the flag is. */ private static boolean asks(EquipmentType et, Object flag) { for (Method m : EquipmentType.class.getMethods()) { if (m.getName().equals("hasFlag") && m.getParameterCount() == 1 && m.getParameterTypes()[0].isInstance(flag)) { try { return Boolean.TRUE.equals(m.invoke(et, flag)); } catch (Throwable ignored) { return false; } } } return false; }
private static void val(StringBuilder sb, Object v, int depth) { 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 if (v instanceof CharSequence) { str(sb, v.toString()); } else if (depth > 3) { safeToString(sb, v); } else if (v instanceof Map<?, ?> m) { sb.append('{'); boolean first = true; for (Map.Entry<?, ?> e : m.entrySet()) { if (!first) { sb.append(','); } first = false; str(sb, String.valueOf(e.getKey())); sb.append(':'); val(sb, e.getValue(), depth + 1); } sb.append('}'); } else if (v instanceof Collection<?> c) { sb.append('['); boolean first = true; for (Object o : c) { if (!first) { sb.append(','); } first = false; val(sb, o, depth + 1); } sb.append(']'); } else if (v.getClass().isArray()) { sb.append('['); int n = java.lang.reflect.Array.getLength(v); for (int i = 0; i < n; i++) { if (i > 0) { sb.append(','); } val(sb, java.lang.reflect.Array.get(v, i), depth + 1); } sb.append(']'); } else { safeToString(sb, v); } }
private static void safeToString(StringBuilder sb, Object v) { if (v == null) { sb.append("null"); return; } String s; try { s = v.toString(); } catch (Throwable t) { failedFields++; sb.append("null"); return; } if (s == null) { sb.append("null"); } else { str(sb, s); } }
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('"'); }}