Something went wrong. Try again.
Data management tools for playing BattleTech
Something went wrong. Try again.
Java
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327import 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(']');
// 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); } } }
System.out.printf("dumped %d equipment types (%d skipped, %d unrenderable fields) -> %s%n", written, skipped, failedFields, args[0]); System.exit(0); }
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('"'); }}