#!/usr/bin/env bash # Run the Java bridge: dump MegaMek's computed unit and equipment data as JSON # Lines for `helm build --bridge-dir` to read. # # ./bridge/dump.sh [out-dir] # # Why this exists at all: battle value, C-bill cost and the Alpha Strike # conversion are computed when MegaMek loads a design and appear in no .mtf or # .blk file. Everything else helm needs is read natively by helm-unitfile. # When those calculations are reimplemented in Rust, this directory goes away. # # bv.jsonl is the exception to that: it is not data helm needs, it is the # oracle helm-bv is measured against, and it earns its keep for exactly as long # as the reimplementation is unfinished. # # The JDK runs in a container so no Java has to be installed. Set JDK_IMAGE to # use a different one, or JAVA_HOME to run against a local JDK instead. set -euo pipefail HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" MM_HOME="${1:?usage: dump.sh [out-dir]}" OUT_DIR="${2:-$PWD}" JDK_IMAGE="${JDK_IMAGE:-eclipse-temurin:21-jdk}" MM_HOME="$(cd "$MM_HOME" && pwd)" mkdir -p "$OUT_DIR" OUT_DIR="$(cd "$OUT_DIR" && pwd)" [ -f "$MM_HOME/MegaMek.jar" ] || { echo "ERROR: no MegaMek.jar in $MM_HOME" >&2; exit 1; } # MegaMek writes preferences and temp images into its own install. An install # copied out of a container image can have these as symlinks to a runtime path # that does not exist here, and MegaMek fails on the dangling link rather than # recreating it. for p in userdata data/images/temp; do if [ -L "$MM_HOME/$p" ] && [ ! -e "$MM_HOME/$p" ]; then echo "note: replacing dangling symlink $p" rm "$MM_HOME/$p"; mkdir -p "$MM_HOME/$p" fi done mkdir -p "$MM_HOME/logs" "$MM_HOME/mmconf" # --add-opens: MekSummaryCache deserialises a prebuilt units.cache through # java.util collections. # cwd must be the install - MegaMek resolves data/ relative to it. run() { local src="$1" out="$2" shift 2 if [ -n "${JAVA_HOME:-}" ]; then ( cd "$MM_HOME" && "$JAVA_HOME/bin/java" --source 21 \ -Dlog4j2.configurationFile="$HERE/log4j2-quiet.xml" \ -Dsentry.dsn= -Djava.awt.headless=true \ --add-opens java.base/java.util=ALL-UNNAMED \ --add-opens java.base/java.util.concurrent=ALL-UNNAMED \ -cp "MegaMek.jar:lib/*" "$HERE/$src" "$OUT_DIR/$out" "$@" ) else docker run --rm \ --user "$(id -u):$(id -g)" \ -v "$HERE:/bridge:ro" -v "$MM_HOME:/mm" -v "$OUT_DIR:/out" \ -w /mm "$JDK_IMAGE" \ java --source 21 \ -Dlog4j2.configurationFile=/bridge/log4j2-quiet.xml \ -Dsentry.dsn= -Djava.awt.headless=true \ --add-opens java.base/java.util=ALL-UNNAMED \ --add-opens java.base/java.util.concurrent=ALL-UNNAMED \ -cp "MegaMek.jar:lib/*" "/bridge/$src" "/out/$out" "$@" fi } # Building units.cache from scratch walks ~11k files and takes about 20s; # an install that already has one loads it in about 3s. echo "dumping units ..." run DumpUnits.java units.jsonl echo "dumping equipment ..." run DumpEquipment.java equipment.jsonl # The working of the battle value calculation, not just its answer. This one # loads every Entity rather than reading the summary cache, because the # calculator needs a real unit, so it costs about as much again as the units # dump. # # `--report` asks for MegaMek's own working as it goes: every weapon with the # heat it cost and what it counted for, every multiplier with the figure either # side of it. A total that is 40 points light says nothing about which half is # wrong; a report says which line. echo "dumping battle value working ..." run DumpBv.java bv.jsonl Mek --report # The same for combat vehicles, which are the next thing helm-bv scores. Much # cheaper than the Meks: there are fewer of them and a tank has no heat budget # to walk. # Three names for one calculator: everything MegaMek does not recognise as # something else is scored as a combat vehicle, and MekSummary files those # under whichever of Tank, VTOL and Naval the design looks like. echo "dumping battle value working for vehicles ..." run DumpBv.java bv-vehicle.jsonl Tank VTOL Naval --report # The same oracle for designs that have been shot at. A .mtf describes a design # as it leaves the factory and nothing else, so what a Mek is worth once its # plate is gone or its magazines are empty has to be asked of MegaMek directly. # A handful of designs is enough: it is the rules that are being checked, not # the library. echo "dumping damaged designs ..." run DumpDamaged.java damaged.jsonl # And the units in a `.mul`, which is the only oracle for a unit somebody # varied rather than designed: a magazine whose `type` names inferno rounds is # loaded with inferno rounds and scored as such. The fixtures are this # repository's own, because MegaMek ships no file that varies a unit. # # Copied into the output directory first: the container is given `bridge/`, # MegaMek and the output directory, and the fixtures are in none of those. echo "dumping the .mul fixtures ..." FIXTURES="$HERE/../crates/helm-bv/tests/mul" if compgen -G "$FIXTURES/*.mul" > /dev/null; then mkdir -p "$OUT_DIR/muls" cp "$FIXTURES"/*.mul "$OUT_DIR/muls/" muls=() for f in "$OUT_DIR"/muls/*.mul; do # The container sees the output directory as /out; a local JDK sees it # where it is. if [ -n "${JAVA_HOME:-}" ]; then muls+=("$f"); else muls+=("/out/muls/$(basename "$f")"); fi done run DumpMul.java mul.jsonl "${muls[@]}" else echo "no .mul fixtures to dump" fi # The formation blueprints: what makes four Meks a Fire Lance rather than four # Meks. These are Campaign Operations rules that MegaMek holds in code, so they # are read off the shelf here rather than copied. Cheap - no unit is loaded. echo "dumping formations ..." run DumpFormations.java formations.jsonl # Whether a design meets each requirement, asked of MegaMek one design at a # time. This is what says helm read a requirement's label the way the lambda # behind it behaves; a thousand-odd designs is plenty, since it is the reading # being measured and not the library. run DumpFormations.java matches.jsonl --matches 1200 # And MegaMek's verdict on whole forces, which tests the assembly rules rather # than the requirements - the ideal-role shortcut, the weight bounds, and the # requirements that are alternatives to one another. LANCES="$HERE/../crates/helm-force/tests/lances.txt" if [ -f "$LANCES" ]; then : > "$OUT_DIR/formation_forces.jsonl" while IFS= read -r lance; do case "$lance" in ''|'#'*) continue ;; esac units=() while IFS= read -r unit; do [ -n "$unit" ] && units+=("$unit") done <<< "${lance//|/$'\n'}" run DumpFormations.java one_force.jsonl --force "${units[@]}" # One row per formation per force, with the force written in so a test can # tell them apart. while IFS= read -r row; do printf '{"force":"%s",%s\n' "$lance" "${row#\{}" >> "$OUT_DIR/formation_forces.jsonl" done < "$OUT_DIR/one_force.jsonl" done < "$LANCES" rm -f "$OUT_DIR/one_force.jsonl" fi echo "wrote $OUT_DIR/units.jsonl, $OUT_DIR/equipment.jsonl, $OUT_DIR/bv.jsonl, $OUT_DIR/bv-vehicle.jsonl," echo " $OUT_DIR/damaged.jsonl, $OUT_DIR/mul.jsonl, $OUT_DIR/formations.jsonl," echo " $OUT_DIR/matches.jsonl and $OUT_DIR/formation_forces.jsonl"