From 70ca344183c3836a54cd069fd3ecfb41dc354812 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 30 Jul 2026 17:18:47 -0400 Subject: [PATCH] Add Webswing spike: MegaMek Swing client rendered in a browser Stands up the three pieces needed to judge server-side rendering as an alternative to CheerpJ: HostForHuman - MegaMek Server + Princess bot on :8850, leaving one scenario faction as an unclaimed ghost for a human to log into. Bots are deliberately held in the lounge until that slot stops being a ghost; readying them earlier satisfies checkReady() (which skips ghosts) and starts the game without the human. webswing.config - Webswing Lite 26.4.5 launching the stock MegaMek Swing client via Desktop launcher, mainClass megamek.MegaMek, args "-client -server localhost -port 8850 -player_name TraineeA". start.sh - brings both up. Verified so far: the client args are correct and a native JVM client connects and claims the TraineeA slot (host logs "human connected -> readying bots"). Webswing serves /megamek with HTTP 200. NOT yet verified: that Webswing actually renders the Swing client in a browser - that needs a real browser. Webswing Lite requires Java 21+, which MegaMek also needs, so unlike CheerpJ this path needs no backport and no socket-layer workaround. Co-Authored-By: Claude Opus 5 --- .gitignore | 4 + harness/src/spike/HostForHuman.java | 113 ++++++++++++++++++++++++++++ spike-webswing/start.sh | 51 +++++++++++++ spike-webswing/webswing.config | 33 ++++++++ 4 files changed, 201 insertions(+) create mode 100644 harness/src/spike/HostForHuman.java create mode 100755 spike-webswing/start.sh create mode 100644 spike-webswing/webswing.config diff --git a/.gitignore b/.gitignore index d0ae912..3f66ecb 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,7 @@ logs/ *.log MegaMek-patched.jar patches/out/ +webswing-dist/ +slim-jre/ +slim-data/ +slim-home/ diff --git a/harness/src/spike/HostForHuman.java b/harness/src/spike/HostForHuman.java new file mode 100644 index 0000000..67470b4 --- /dev/null +++ b/harness/src/spike/HostForHuman.java @@ -0,0 +1,113 @@ +package spike; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; + +import megamek.client.bot.princess.Princess; +import megamek.common.Player; +import megamek.common.compute.Compute; +import megamek.common.game.IGame; +import megamek.common.loaders.MekSummaryCache; +import megamek.common.scenario.Scenario; +import megamek.common.scenario.ScenarioLoader; +import megamek.server.Server; +import megamek.server.totalWarfare.TWGameManager; + +/** + * Hosts a MegaMek game with Princess on one side and an empty slot for a human. + * + *

Used to drive the Webswing spike: this process owns the server and the bot, and a + * real MegaMek Swing client - rendered into a browser by Webswing - logs into the + * remaining faction. + * + *

The ready-up ordering is the fiddly part. Bots never ready themselves out of the + * lounge, and {@code checkReady()} ignores ghost players. So readying the bot before the + * human has connected would satisfy "all non-ghost players are done" and start the game + * without them. The bot is therefore held until its opponent's slot stops being a ghost. + * + *

Usage: {@code HostForHuman [scenario] [port]} + */ +public final class HostForHuman { + + private static final String LOCALHOST = "127.0.0.1"; + + public static void main(String[] args) throws Exception { + String scenarioPath = args.length > 0 + ? args[0] + : "data/scenarios/TrainingScenarios/1-FirstRun.mms"; + int port = args.length > 1 ? Integer.parseInt(args[1]) : 8850; + + Compute.d6(); + MekSummaryCache cache = MekSummaryCache.getInstance(); + while (!cache.isInitialized()) { + Thread.sleep(250L); + } + + ScenarioLoader loader = new ScenarioLoader(new File(scenarioPath)); + Scenario scenario = loader.load(); + IGame game = scenario.createGame(); + + TWGameManager gameManager = new TWGameManager(); + Server server = new Server("", port, gameManager); + server.setGame(game); + scenario.applyDamage(gameManager); + gameManager.calculatePlayerInitialCounts(); + + List players = new ArrayList<>(game.getPlayersList()); + if (players.size() < 2) { + throw new IllegalStateException("scenario needs at least 2 factions"); + } + Player humanSlot = players.get(0); + List botSlots = players.subList(1, players.size()); + + System.out.println("================================================="); + System.out.printf(" scenario : %s%n", scenarioPath); + System.out.printf(" port : %d%n", port); + System.out.printf(" HUMAN : %s <-- log in as this name%n", humanSlot.getName()); + for (Player p : botSlots) { + System.out.printf(" bot : %s (Princess)%n", p.getName()); + } + System.out.println("================================================="); + + List bots = new ArrayList<>(); + for (Player p : botSlots) { + Princess bot = new Princess(p.getName(), LOCALHOST, port); + bot.startPrecognition(); + bot.connect(); + bots.add(bot); + } + + for (Princess bot : bots) { + long deadline = System.currentTimeMillis() + 30_000L; + while (System.currentTimeMillis() < deadline + && (bot.getLocalPlayer() == null + || bot.getGame().getEntitiesOwnedBy(bot.getLocalPlayer()) == 0)) { + Thread.sleep(100L); + } + } + System.out.println("[host] bots connected; waiting for a human to claim " + + humanSlot.getName() + " ..."); + + // Hold the bots in the lounge until the human slot is actually occupied. + boolean announced = false; + while (true) { + Player current = server.getGame().getPlayer(humanSlot.getId()); + if (current != null && !current.isGhost()) { + if (!announced) { + System.out.println("[host] human connected -> readying bots. " + + "Click 'Done' in the browser to start the game."); + for (Princess bot : bots) { + bot.sendDone(true); + } + announced = true; + } + } else if (announced) { + // Human dropped back out; re-arm so a reconnect works. + System.out.println("[host] human disconnected; waiting again ..."); + announced = false; + } + Thread.sleep(1000L); + } + } +} diff --git a/spike-webswing/start.sh b/spike-webswing/start.sh new file mode 100755 index 0000000..54b4286 --- /dev/null +++ b/spike-webswing/start.sh @@ -0,0 +1,51 @@ +#!/usr/bin/env bash +# Bring up the Webswing spike: MegaMek server + Princess bot + browser-rendered client. +# +# 1. MegaMek Server + Princess (TraineeB) on :8850, TraineeA slot left open +# 2. Webswing on :8080 serving the real MegaMek Swing client at /megamek, +# launched with -client -player_name TraineeA so it claims that slot +# +# Then open http://localhost:8080/megamek/ +set -euo pipefail +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +JAVA="$ROOT/jdk-21.0.12+8/bin/java" +MM="$ROOT/MegaMek-0.51.00" +WS="$ROOT/webswing-dist" +SCENARIO="${1:-data/scenarios/TrainingScenarios/1-FirstRun.mms}" + +mkdir -p "$ROOT/results" "$WS/logs" + +echo "starting MegaMek host (server + Princess)..." +( cd "$MM" && nohup "$JAVA" -Xmx1g \ + -Dlog4j2.configurationFile="$ROOT/harness/log4j2-quiet.xml" \ + --add-opens java.base/java.util=ALL-UNNAMED \ + --add-opens java.base/java.util.concurrent=ALL-UNNAMED \ + -cp "$ROOT/harness/out:$ROOT/MegaMek-patched.jar:$(ls "$MM"/lib/*.jar | tr '\n' ':')" \ + spike.HostForHuman "$SCENARIO" 8850 > "$ROOT/results/host.log" 2>&1 & ) + +echo "starting Webswing..." +( cd "$WS" && env -u DISPLAY -u XAUTHORITY nohup "$JAVA" -server -Xmx2G -Xms256m \ + --add-modules=java.desktop \ + --add-exports=java.desktop/sun.awt=ALL-UNNAMED \ + --add-exports=java.desktop/sun.awt.dnd=ALL-UNNAMED \ + --add-exports=java.desktop/sun.awt.datatransfer=ALL-UNNAMED \ + --add-exports=java.desktop/sun.awt.image=ALL-UNNAMED \ + --add-exports=java.desktop/sun.java2d=ALL-UNNAMED \ + --add-exports=java.desktop/sun.java2d.pipe=ALL-UNNAMED \ + --add-exports=java.desktop/sun.java2d.loops=ALL-UNNAMED \ + --add-exports=java.desktop/sun.font=ALL-UNNAMED \ + --add-exports=java.desktop/sun.print=ALL-UNNAMED \ + --add-exports=java.desktop/java.awt.peer=ALL-UNNAMED \ + --add-exports=java.desktop/java.awt.dnd=ALL-UNNAMED \ + --add-exports=java.base/sun.nio.cs=ALL-UNNAMED \ + --add-opens=java.desktop/sun.awt.image=ALL-UNNAMED \ + -Dwebswing.websocketMessageSizeLimit=1024000 \ + -jar webswing-server-26.4.5.war \ + -h 0.0.0.0 -j jetty.properties -c webswing.config -pf webswing.properties \ + > logs/ws.log 2>&1 & ) + +echo +echo " open http://localhost:8080/megamek/" +echo " host log: $ROOT/results/host.log" +echo " webswing log: $WS/logs/ws.log" +echo " stop: pkill -f HostForHuman; pkill -f webswing-server" diff --git a/spike-webswing/webswing.config b/spike-webswing/webswing.config new file mode 100644 index 0000000..b008d38 --- /dev/null +++ b/spike-webswing/webswing.config @@ -0,0 +1,33 @@ +{ + "/" : { + "path" : "/", + "security" : { + "module" : "NONE" + } + }, + "/megamek" : { + "path" : "/megamek", + "name" : "MegaMek", + "security" : { + "module" : "NONE" + }, + "swingConfig" : { + "launcherType" : "Desktop", + "launcherConfig" : { + "mainClass" : "megamek.MegaMek", + "args" : "-client -server localhost -port 8850 -player_name TraineeA" + }, + "homeDir" : "/home/jmm/Code/mm-spike/MegaMek-0.51.00", + "jreExecutable" : "/home/jmm/Code/mm-spike/jdk-21.0.12+8/bin/java", + "classPathEntries" : [ "MegaMek.jar", "lib/*" ], + "vmArgs" : "-Xmx2G -Dsentry.dsn= --add-opens java.base/java.util=ALL-UNNAMED --add-opens java.base/java.util.concurrent=ALL-UNNAMED", + "directdraw" : true, + "isolatedFs" : false, + "sessionTimeout" : 3600, + "swingSessionTimeout" : 3600, + "timeoutIfInactive" : false, + "allowLocalClipboard" : true, + "debug" : false + } + } +} -- 2.51.2