feat: add Unity/Godot WebSocket server (Phase 1 stub)
- pom.xml: add Java-WebSocket 1.5.6 dependency - bridge/GameWsServer.java: WS server on ws://localhost:7654 - bridge/GameSession.java: Phase 1 stub game loop with human-input queue - cli/LabCli.java: add ws-server subcommand via picocli
This commit is contained in:
1 parent
aa5e381c59
commit
4e8a999ebd
4 files changed
+508
No files matched your search
@@ -67,6 +67,13 @@
|
|||||||
<version>3.45.1.0</version>
|
<version>3.45.1.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<!-- WebSocket server for Unity/Godot client -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.java-websocket</groupId>
|
||||||
|
<artifactId>Java-WebSocket</artifactId>
|
||||||
|
<version>1.5.6</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<!-- Testing -->
|
<!-- Testing -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.junit.jupiter</groupId>
|
<groupId>org.junit.jupiter</groupId>
|
||||||
|
|||||||
@@ -0,0 +1,235 @@
|
|||||||
|
package commanderailab.bridge;
|
||||||
|
|
||||||
|
import com.google.gson.Gson;
|
||||||
|
import com.google.gson.GsonBuilder;
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
|
import com.google.gson.JsonParser;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.concurrent.BlockingQueue;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GameSession — holds state for one live interactive Commander game.
|
||||||
|
*
|
||||||
|
* PHASE 1 STUB: generates synthetic game state so you can verify the full
|
||||||
|
* WebSocket contract round-trips correctly before wiring in real Forge IPC.
|
||||||
|
*
|
||||||
|
* Replace run() in Phase 2 once the contract is verified end-to-end.
|
||||||
|
*/
|
||||||
|
public class GameSession {
|
||||||
|
|
||||||
|
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
|
||||||
|
|
||||||
|
private final List<String> deckNames;
|
||||||
|
private final Long seed;
|
||||||
|
private final String forgeJarPath;
|
||||||
|
private final String forgeWorkDir;
|
||||||
|
private final BlockingQueue<String> humanActionQueue;
|
||||||
|
|
||||||
|
private volatile boolean running = false;
|
||||||
|
private volatile int turnNumber = 1;
|
||||||
|
|
||||||
|
// Mutable game state
|
||||||
|
private String phase = "UNTAP";
|
||||||
|
private int activePlayer = 0;
|
||||||
|
private int priorityPlayer = 0;
|
||||||
|
private final int[] life;
|
||||||
|
private final int[] poison;
|
||||||
|
private final List<String>[] hands;
|
||||||
|
private final List<String>[] battlefields;
|
||||||
|
private final List<String>[] graveyards;
|
||||||
|
private final List<String>[] commandZones;
|
||||||
|
private final int[] commanderTax;
|
||||||
|
private boolean awaitingHumanInput = false;
|
||||||
|
|
||||||
|
private static final String[] PHASES = {
|
||||||
|
"UNTAP", "UPKEEP", "DRAW",
|
||||||
|
"MAIN1", "BEGIN_COMBAT", "DECLARE_ATTACKERS",
|
||||||
|
"DECLARE_BLOCKERS", "DAMAGE", "END_COMBAT",
|
||||||
|
"MAIN2", "END", "CLEANUP"
|
||||||
|
};
|
||||||
|
private int phaseIndex = 0;
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public GameSession(List<String> deckNames, Long seed,
|
||||||
|
String forgeJarPath, String forgeWorkDir,
|
||||||
|
BlockingQueue<String> humanActionQueue) {
|
||||||
|
this.deckNames = deckNames;
|
||||||
|
this.seed = seed;
|
||||||
|
this.forgeJarPath = forgeJarPath;
|
||||||
|
this.forgeWorkDir = forgeWorkDir;
|
||||||
|
this.humanActionQueue = humanActionQueue;
|
||||||
|
|
||||||
|
int seats = Math.min(deckNames.size(), 4);
|
||||||
|
life = new int[seats];
|
||||||
|
poison = new int[seats];
|
||||||
|
hands = new List[seats];
|
||||||
|
battlefields = new List[seats];
|
||||||
|
graveyards = new List[seats];
|
||||||
|
commandZones = new List[seats];
|
||||||
|
commanderTax = new int[seats];
|
||||||
|
|
||||||
|
for (int i = 0; i < seats; i++) {
|
||||||
|
life[i] = 40;
|
||||||
|
hands[i] = new ArrayList<>(List.of(
|
||||||
|
"card_stub_1", "card_stub_2", "card_stub_3",
|
||||||
|
"card_stub_4", "card_stub_5", "card_stub_6", "card_stub_7"
|
||||||
|
));
|
||||||
|
battlefields[i] = new ArrayList<>();
|
||||||
|
graveyards[i] = new ArrayList<>();
|
||||||
|
commandZones[i] = new ArrayList<>(List.of(deckNames.get(i) + "_commander"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isRunning() { return running; }
|
||||||
|
public int getTurnNumber() { return turnNumber; }
|
||||||
|
|
||||||
|
public void stop() {
|
||||||
|
running = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run the game loop. Calls onStateChange after every action resolves.
|
||||||
|
* Blocks until the game ends or stop() is called.
|
||||||
|
*
|
||||||
|
* PHASE 1 STUB: advances through phases automatically; on MAIN1 of seat 0's
|
||||||
|
* turn, pauses and waits for the human client to send an action.
|
||||||
|
*/
|
||||||
|
public void run(Consumer<Object> onStateChange) throws InterruptedException {
|
||||||
|
running = true;
|
||||||
|
Random rng = seed != null ? new Random(seed) : new Random();
|
||||||
|
System.out.println("[Session] Game started. Decks: " + deckNames);
|
||||||
|
|
||||||
|
while (running && !isGameOver()) {
|
||||||
|
String currentPhase = PHASES[phaseIndex];
|
||||||
|
phase = currentPhase;
|
||||||
|
activePlayer = (turnNumber - 1) % deckNames.size();
|
||||||
|
|
||||||
|
boolean isHumanTurn = (activePlayer == 0);
|
||||||
|
boolean isInteractivePhase = currentPhase.equals("MAIN1") || currentPhase.equals("MAIN2");
|
||||||
|
|
||||||
|
if (isHumanTurn && isInteractivePhase) {
|
||||||
|
awaitingHumanInput = true;
|
||||||
|
priorityPlayer = 0;
|
||||||
|
onStateChange.accept(buildStateSnapshot());
|
||||||
|
|
||||||
|
System.out.println("[Session] Awaiting human action for " + currentPhase + "...");
|
||||||
|
String actionJson = humanActionQueue.poll(120, TimeUnit.SECONDS);
|
||||||
|
|
||||||
|
if (actionJson == null) {
|
||||||
|
System.out.println("[Session] Human timed out — auto passing.");
|
||||||
|
} else {
|
||||||
|
applyAction(actionJson);
|
||||||
|
System.out.println("[Session] Human action applied: " + actionJson);
|
||||||
|
}
|
||||||
|
awaitingHumanInput = false;
|
||||||
|
} else {
|
||||||
|
// AI turn — small delay for readability
|
||||||
|
Thread.sleep(300);
|
||||||
|
// Simulate AI occasionally dealing damage to human
|
||||||
|
if (!isHumanTurn && currentPhase.equals("DAMAGE") && life[0] > 5) {
|
||||||
|
life[0] -= rng.nextInt(4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Advance phase
|
||||||
|
phaseIndex++;
|
||||||
|
if (phaseIndex >= PHASES.length) {
|
||||||
|
phaseIndex = 0;
|
||||||
|
turnNumber++;
|
||||||
|
}
|
||||||
|
|
||||||
|
onStateChange.accept(buildStateSnapshot());
|
||||||
|
}
|
||||||
|
|
||||||
|
running = false;
|
||||||
|
System.out.println("[Session] Game ended after " + turnNumber + " turns.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applyAction(String actionJson) {
|
||||||
|
try {
|
||||||
|
JsonObject action = JsonParser.parseString(actionJson).getAsJsonObject();
|
||||||
|
String type = action.has("type") ? action.get("type").getAsString() : "PASS_PRIORITY";
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case "PLAY_LAND", "CAST_SPELL" -> {
|
||||||
|
String cardId = action.has("cardId") ? action.get("cardId").getAsString() : null;
|
||||||
|
if (cardId != null && hands[0].contains(cardId)) {
|
||||||
|
hands[0].remove(cardId);
|
||||||
|
battlefields[0].add(cardId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "PASS_PRIORITY" -> { /* no-op, phase advances naturally */ }
|
||||||
|
default -> System.out.println("[Session] Unknown action type: " + type);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
System.err.println("[Session] Failed to apply action: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isGameOver() {
|
||||||
|
int alive = 0;
|
||||||
|
for (int lp : life) if (lp > 0) alive++;
|
||||||
|
// Phase 1 stub: end after 50 turns or only 1 player alive
|
||||||
|
return alive <= 1 || turnNumber > 50;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------
|
||||||
|
// State serialization
|
||||||
|
// ---------------------------------------------------------
|
||||||
|
|
||||||
|
/** Returns a full { type: "STATE", state: {...} } JSON string. */
|
||||||
|
public String buildStateMessage() {
|
||||||
|
Map<String, Object> msg = new LinkedHashMap<>();
|
||||||
|
msg.put("type", "STATE");
|
||||||
|
msg.put("state", buildStateSnapshot());
|
||||||
|
return GSON.toJson(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns the raw state snapshot map (serialized by caller). */
|
||||||
|
public Map<String, Object> buildStateSnapshot() {
|
||||||
|
Map<String, Object> state = new LinkedHashMap<>();
|
||||||
|
state.put("schema", "1.0.0");
|
||||||
|
state.put("phase", phase);
|
||||||
|
state.put("turnNumber", turnNumber);
|
||||||
|
state.put("activePlayer", activePlayer);
|
||||||
|
state.put("priorityPlayer", priorityPlayer);
|
||||||
|
state.put("awaitingInput", awaitingHumanInput);
|
||||||
|
state.put("inputPrompt", awaitingHumanInput ? "Choose an action or pass priority" : null);
|
||||||
|
|
||||||
|
List<Map<String, Object>> players = new ArrayList<>();
|
||||||
|
for (int i = 0; i < deckNames.size(); i++) {
|
||||||
|
Map<String, Object> p = new LinkedHashMap<>();
|
||||||
|
p.put("seat", i);
|
||||||
|
p.put("name", i == 0 ? "Human" : "AI-" + i);
|
||||||
|
p.put("isAI", i != 0);
|
||||||
|
p.put("deckName", deckNames.get(i));
|
||||||
|
p.put("life", life[i]);
|
||||||
|
p.put("poison", poison[i]);
|
||||||
|
p.put("commanderTax", commanderTax[i]);
|
||||||
|
p.put("handCount", hands[i].size());
|
||||||
|
p.put("hand", i == 0 ? hands[i] : List.of()); // hide opponent hands
|
||||||
|
p.put("battlefield", battlefields[i]);
|
||||||
|
p.put("graveyard", graveyards[i]);
|
||||||
|
p.put("commandZone", commandZones[i]);
|
||||||
|
p.put("manaPool", Map.of("W",0,"U",0,"B",0,"R",0,"G",0,"C",0));
|
||||||
|
players.add(p);
|
||||||
|
}
|
||||||
|
state.put("players", players);
|
||||||
|
state.put("stack", List.of());
|
||||||
|
|
||||||
|
// Legal actions — only populated when it's the human's turn
|
||||||
|
List<Map<String, Object>> legalActions = new ArrayList<>();
|
||||||
|
if (awaitingHumanInput) {
|
||||||
|
legalActions.add(Map.of("type", "PASS_PRIORITY", "label", "Pass Priority"));
|
||||||
|
for (String card : hands[0]) {
|
||||||
|
legalActions.add(Map.of("type", "CAST_SPELL", "cardId", card, "label", "Cast " + card));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
state.put("legalActions", legalActions);
|
||||||
|
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,204 @@
|
|||||||
|
package commanderailab.bridge;
|
||||||
|
|
||||||
|
import com.google.gson.Gson;
|
||||||
|
import com.google.gson.GsonBuilder;
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
|
import com.google.gson.JsonParser;
|
||||||
|
import org.java_websocket.WebSocket;
|
||||||
|
import org.java_websocket.handshake.ClientHandshake;
|
||||||
|
import org.java_websocket.server.WebSocketServer;
|
||||||
|
|
||||||
|
import java.net.InetSocketAddress;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.concurrent.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GameWsServer — WebSocket server that exposes the Forge engine to a Unity/Godot client.
|
||||||
|
*
|
||||||
|
* Connect at: ws://localhost:7654
|
||||||
|
*
|
||||||
|
* Protocol:
|
||||||
|
* ENGINE -> CLIENT: GameStateSnapshot JSON (pushed after every action resolves)
|
||||||
|
* CLIENT -> ENGINE: PlayerAction JSON
|
||||||
|
*
|
||||||
|
* Message types (CLIENT -> ENGINE):
|
||||||
|
* { "type": "START_GAME", "decks": ["DeckA","DeckB","DeckC","DeckD"], "seed": 12345 }
|
||||||
|
* { "type": "PLAYER_ACTION", "action": { ... } }
|
||||||
|
* { "type": "PING" }
|
||||||
|
*
|
||||||
|
* Message types (ENGINE -> CLIENT):
|
||||||
|
* { "type": "STATE", "state": { ... GameStateSnapshot ... } }
|
||||||
|
* { "type": "ERROR", "message": "..." }
|
||||||
|
* { "type": "PONG" }
|
||||||
|
*/
|
||||||
|
public class GameWsServer extends WebSocketServer {
|
||||||
|
|
||||||
|
private static final int DEFAULT_PORT = 7654;
|
||||||
|
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
|
||||||
|
|
||||||
|
private final String forgeJarPath;
|
||||||
|
private final String forgeWorkDir;
|
||||||
|
|
||||||
|
// Active game session (one game at a time for Phase 1)
|
||||||
|
private volatile GameSession activeSession;
|
||||||
|
|
||||||
|
// Waiting-for-human-input: the session parks here until Unity sends an action
|
||||||
|
private final BlockingQueue<String> humanActionQueue = new LinkedBlockingQueue<>();
|
||||||
|
|
||||||
|
public GameWsServer(String forgeJarPath, String forgeWorkDir) {
|
||||||
|
super(new InetSocketAddress(DEFAULT_PORT));
|
||||||
|
this.forgeJarPath = forgeJarPath;
|
||||||
|
this.forgeWorkDir = forgeWorkDir;
|
||||||
|
setReuseAddr(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public GameWsServer(int port, String forgeJarPath, String forgeWorkDir) {
|
||||||
|
super(new InetSocketAddress(port));
|
||||||
|
this.forgeJarPath = forgeJarPath;
|
||||||
|
this.forgeWorkDir = forgeWorkDir;
|
||||||
|
setReuseAddr(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------
|
||||||
|
// WebSocket lifecycle
|
||||||
|
// ---------------------------------------------------------
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onOpen(WebSocket conn, ClientHandshake handshake) {
|
||||||
|
System.out.println("[WS] Client connected: " + conn.getRemoteSocketAddress());
|
||||||
|
// Send current state if a game is in progress
|
||||||
|
if (activeSession != null) {
|
||||||
|
conn.send(activeSession.buildStateMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onClose(WebSocket conn, int code, String reason, boolean remote) {
|
||||||
|
System.out.println("[WS] Client disconnected: " + conn.getRemoteSocketAddress());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onMessage(WebSocket conn, String message) {
|
||||||
|
try {
|
||||||
|
JsonObject msg = JsonParser.parseString(message).getAsJsonObject();
|
||||||
|
String type = msg.get("type").getAsString();
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case "PING" -> conn.send(buildPong());
|
||||||
|
case "START_GAME" -> handleStartGame(conn, msg);
|
||||||
|
case "PLAYER_ACTION" -> handlePlayerAction(conn, msg);
|
||||||
|
case "STOP_GAME" -> {
|
||||||
|
if (activeSession != null) activeSession.stop();
|
||||||
|
activeSession = null;
|
||||||
|
broadcast(buildInfo("Game stopped."));
|
||||||
|
}
|
||||||
|
default -> sendError(conn, "Unknown message type: " + type);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
sendError(conn, "Parse error: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onError(WebSocket conn, Exception ex) {
|
||||||
|
System.err.println("[WS] Error: " + ex.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onStart() {
|
||||||
|
System.out.println("[WS] GameWsServer started on port " + getPort());
|
||||||
|
System.out.println("[WS] Connect Unity/Godot to: ws://localhost:" + getPort());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------
|
||||||
|
// Message handlers
|
||||||
|
// ---------------------------------------------------------
|
||||||
|
|
||||||
|
private void handleStartGame(WebSocket conn, JsonObject msg) {
|
||||||
|
if (activeSession != null && activeSession.isRunning()) {
|
||||||
|
sendError(conn, "A game is already in progress. Send { type: 'STOP_GAME' } first.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> decks = new ArrayList<>();
|
||||||
|
if (msg.has("decks")) {
|
||||||
|
msg.getAsJsonArray("decks").forEach(d -> decks.add(d.getAsString()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (decks.size() < 3) {
|
||||||
|
sendError(conn, "Need at least 3 deck names in 'decks' array.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Long seed = msg.has("seed") ? msg.get("seed").getAsLong() : null;
|
||||||
|
|
||||||
|
System.out.println("[WS] Starting game: " + decks + " seed=" + seed);
|
||||||
|
|
||||||
|
// Clear any stale actions from previous game
|
||||||
|
humanActionQueue.clear();
|
||||||
|
|
||||||
|
activeSession = new GameSession(decks, seed, forgeJarPath, forgeWorkDir, humanActionQueue);
|
||||||
|
|
||||||
|
// Push initial state immediately
|
||||||
|
broadcast(activeSession.buildStateMessage());
|
||||||
|
|
||||||
|
// Run the game loop on a background thread so WebSocket thread stays free
|
||||||
|
Thread gameThread = new Thread(() -> {
|
||||||
|
try {
|
||||||
|
activeSession.run(state -> broadcast(GSON.toJson(state)));
|
||||||
|
broadcast(buildInfo("Game over after " + activeSession.getTurnNumber() + " turns."));
|
||||||
|
} catch (Exception e) {
|
||||||
|
broadcastError("Game error: " + e.getMessage());
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}, "game-loop");
|
||||||
|
gameThread.setDaemon(true);
|
||||||
|
gameThread.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handlePlayerAction(WebSocket conn, JsonObject msg) {
|
||||||
|
if (activeSession == null || !activeSession.isRunning()) {
|
||||||
|
sendError(conn, "No active game. Send START_GAME first.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!msg.has("action")) {
|
||||||
|
sendError(conn, "Missing 'action' field.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Put the raw action JSON string onto the queue -- GameSession polls this
|
||||||
|
humanActionQueue.offer(msg.get("action").toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------
|
||||||
|
// Broadcast helpers
|
||||||
|
// ---------------------------------------------------------
|
||||||
|
|
||||||
|
private void broadcastError(String message) {
|
||||||
|
broadcast(buildError(message));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sendError(WebSocket conn, String message) {
|
||||||
|
conn.send(buildError(message));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String buildError(String message) {
|
||||||
|
JsonObject obj = new JsonObject();
|
||||||
|
obj.addProperty("type", "ERROR");
|
||||||
|
obj.addProperty("message", message);
|
||||||
|
return obj.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String buildInfo(String message) {
|
||||||
|
JsonObject obj = new JsonObject();
|
||||||
|
obj.addProperty("type", "INFO");
|
||||||
|
obj.addProperty("message", message);
|
||||||
|
return obj.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String buildPong() {
|
||||||
|
JsonObject obj = new JsonObject();
|
||||||
|
obj.addProperty("type", "PONG");
|
||||||
|
obj.addProperty("ts", System.currentTimeMillis());
|
||||||
|
return obj.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
package commanderailab.cli;
|
||||||
|
|
||||||
|
import commanderailab.bridge.GameWsServer;
|
||||||
|
import picocli.CommandLine.Command;
|
||||||
|
import picocli.CommandLine.Option;
|
||||||
|
|
||||||
|
import java.util.concurrent.Callable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* WsServerCommand — picocli subcommand that starts the interactive game WebSocket server.
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* java -jar commander-ai-lab.jar ws-server \
|
||||||
|
* --forge-jar path/to/forge.jar \
|
||||||
|
* --forge-dir path/to/forge-gui \
|
||||||
|
* --port 7654
|
||||||
|
*
|
||||||
|
* Then connect Unity/Godot to ws://localhost:7654 and send:
|
||||||
|
* { "type": "START_GAME", "decks": ["DeckA","DeckB","DeckC","DeckD"], "seed": 42 }
|
||||||
|
*/
|
||||||
|
@Command(
|
||||||
|
name = "ws-server",
|
||||||
|
mixinStandardHelpOptions = true,
|
||||||
|
description = "Start the interactive game WebSocket server for Unity/Godot clients."
|
||||||
|
)
|
||||||
|
public class WsServerCommand implements Callable<Integer> {
|
||||||
|
|
||||||
|
@Option(names = {"--forge-jar", "-F"}, required = true,
|
||||||
|
description = "Path to Forge desktop JAR (forge-gui-desktop-XXX-jar-with-dependencies.jar)")
|
||||||
|
private String forgeJarPath;
|
||||||
|
|
||||||
|
@Option(names = {"--forge-dir", "-W"}, required = true,
|
||||||
|
description = "Forge working directory (folder containing res/)")
|
||||||
|
private String forgeWorkDir;
|
||||||
|
|
||||||
|
@Option(names = {"--port", "-p"}, defaultValue = "7654",
|
||||||
|
description = "WebSocket port (default: 7654)")
|
||||||
|
private int port;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Integer call() throws Exception {
|
||||||
|
System.out.println("╔══════════════════════════════════════════════════╗");
|
||||||
|
System.out.println("║ Commander AI Lab — Interactive WS Server ║");
|
||||||
|
System.out.println("╚══════════════════════════════════════════════════╝");
|
||||||
|
System.out.println();
|
||||||
|
System.out.println(" Forge JAR : " + forgeJarPath);
|
||||||
|
System.out.println(" Forge Dir : " + forgeWorkDir);
|
||||||
|
System.out.println(" WS Port : " + port);
|
||||||
|
System.out.println();
|
||||||
|
|
||||||
|
GameWsServer server = new GameWsServer(port, forgeJarPath, forgeWorkDir);
|
||||||
|
server.start();
|
||||||
|
|
||||||
|
System.out.println(" Ready — connect Unity/Godot to ws://localhost:" + port);
|
||||||
|
System.out.println(" Press Ctrl+C to stop.");
|
||||||
|
System.out.println();
|
||||||
|
|
||||||
|
// Block until killed
|
||||||
|
Thread.currentThread().join();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user