import 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.
*
*
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.
*
*
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 ");
System.exit(2);
}
EquipmentType.initializeTypes();
List 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 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 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.
*
* 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.
*
*
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 flagNames(EquipmentType et) {
List 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> flagHolders() {
List> 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('"');
}
}