workign on local server

This commit is contained in:
2026-03-16 19:22:02 -06:00
parent 9209455f02
commit 7cce9b4bbd
4 changed files with 140 additions and 205 deletions

7
.gitignore vendored
View File

@@ -1,4 +1,9 @@
build/ build/
.gradle/ .gradle/
gradle/ gradle/
.gradle_cache/ .gradle_cache/
COBBLEVERSE-1.7.30-CF.zip
.env
modpacks/
minecraft-data/
out.log

3
cobble-api.http Normal file
View File

@@ -0,0 +1,3 @@
@host = http://localhost:8085
GET {{host}}/battles

26
docker-compose.yml Normal file
View File

@@ -0,0 +1,26 @@
services:
minecraft:
image: itzg/minecraft-server:java21
stdin_open: true
tty: true
ports:
- "2222:2222" # Minecraft
- "25576:25576" # RCON
- "8085:8085" # Cobblemon Battle API
environment:
EULA: "true"
TYPE: "AUTO_CURSEFORGE"
CF_SLUG: "cobbleverse-cobblemon"
CF_MODPACK_ZIP: "/modpacks/COBBLEVERSE-1.7.30-CF.zip"
MEMORY: "4G"
SERVER_PORT: "2222"
RCON_PORT: "25576"
CF_OVERRIDES_EXCLUSIONS: |
shaderpacks/**
resourcepacks/**
env_file:
- .env
volumes:
- ./minecraft-data:/data
- ./COBBLEVERSE-1.7.30-CF.zip:/modpacks/COBBLEVERSE-1.7.30-CF.zip:ro
restart: unless-stopped

View File

@@ -15,15 +15,13 @@ import com.cobblemon.mod.common.battles.BattleRegistry;
import com.cobblemon.mod.common.battles.ActiveBattlePokemon; import com.cobblemon.mod.common.battles.ActiveBattlePokemon;
import com.cobblemon.mod.common.battles.actor.PlayerBattleActor; import com.cobblemon.mod.common.battles.actor.PlayerBattleActor;
import com.cobblemon.mod.common.api.storage.party.PlayerPartyStore; import com.cobblemon.mod.common.api.storage.party.PlayerPartyStore;
import com.cobblemon.mod.common.api.storage.pc.PCStore;
import com.cobblemon.mod.common.Cobblemon; import com.cobblemon.mod.common.Cobblemon;
import com.cobblemon.mod.common.pokemon.Pokemon; import com.cobblemon.mod.common.pokemon.Pokemon;
import java.io.IOException; import java.io.IOException;
import java.io.OutputStream; import java.io.OutputStream;
import java.net.InetSocketAddress; import java.net.InetSocketAddress;
import java.util.ArrayList; import java.util.LinkedHashMap;
import java.util.List;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
@@ -31,15 +29,12 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
/** /**
* Fabric server-side mod that exposes Cobblemon battle and Pokémon data * Fabric mod exposing a single HTTP endpoint:
* over a lightweight HTTP API.
* *
* Endpoints: * GET /battles — all active battles; each player actor includes their
* GET /player/<uuid> — is the player in a battle? * playerId, active Pokémon (no moves), and full party
* GET /player/<uuid>/battle — full battle state for that player * with moves. Opponent move sets are only present under
* GET /player/<uuid>/party — player's party Pokémon * that opponent's own actor block.
* GET /player/<uuid>/pc — player's PC Pokémon
* GET /battles — list all active battles
*/ */
public class CobbleBattleApiMod implements ModInitializer { public class CobbleBattleApiMod implements ModInitializer {
@@ -51,12 +46,10 @@ public class CobbleBattleApiMod implements ModInitializer {
@Override @Override
public void onInitialize() { public void onInitialize() {
ServerLifecycleEvents.SERVER_STARTED.register(mc -> { ServerLifecycleEvents.SERVER_STARTED.register(mc -> {
this.server = mc; this.server = mc;
startHttpServer(); startHttpServer();
}); });
ServerLifecycleEvents.SERVER_STOPPING.register(mc -> { ServerLifecycleEvents.SERVER_STOPPING.register(mc -> {
stopHttpServer(); stopHttpServer();
this.server = null; this.server = null;
@@ -68,7 +61,6 @@ public class CobbleBattleApiMod implements ModInitializer {
private void startHttpServer() { private void startHttpServer() {
try { try {
httpServer = HttpServer.create(new InetSocketAddress(PORT), 0); httpServer = HttpServer.create(new InetSocketAddress(PORT), 0);
httpServer.createContext("/player", this::handlePlayer);
httpServer.createContext("/battles", this::handleBattles); httpServer.createContext("/battles", this::handleBattles);
httpServer.start(); httpServer.start();
LOGGER.info("CobbleBattleAPI listening on port {}", PORT); LOGGER.info("CobbleBattleAPI listening on port {}", PORT);
@@ -84,64 +76,13 @@ public class CobbleBattleApiMod implements ModInitializer {
} }
} }
// ─── /player/<uuid>[/battle|/party|/pc] ───────────────────────────── // ─── GET /battles ────────────────────────────────────────────────────
private void handlePlayer(HttpExchange exchange) throws IOException {
if (!"GET".equalsIgnoreCase(exchange.getRequestMethod())) {
send(exchange, 405, json("error", "method not allowed"));
return;
}
String path = exchange.getRequestURI().getPath(); // e.g. /player/<uuid>/battle
String[] parts = path.split("/"); // ["", "player", "<uuid>", ...]
if (parts.length < 3) {
send(exchange, 400, json("error", "missing uuid"));
return;
}
UUID uuid;
try {
uuid = UUID.fromString(parts[2]);
} catch (IllegalArgumentException e) {
send(exchange, 400, json("error", "invalid uuid"));
return;
}
// We need to touch the MC server on the server thread.
String subRoute = parts.length >= 4 ? parts[3] : "";
String response = runOnServerThread(() -> dispatch(uuid, subRoute));
if (response == null) {
send(exchange, 500, json("error", "server unavailable"));
return;
}
send(exchange, 200, response);
}
private String dispatch(UUID uuid, String subRoute) {
ServerPlayerEntity player = server.getPlayerManager().getPlayer(uuid);
if (player == null) {
return json("error", "player not online");
}
return switch (subRoute) {
case "battle" -> buildBattleState(uuid, player);
case "party" -> buildParty(uuid, player);
case "pc" -> buildPc(uuid, player);
default -> buildBasicStatus(uuid, player);
};
}
// ─── /battles ───────────────────────────────────────────────────────
private void handleBattles(HttpExchange exchange) throws IOException { private void handleBattles(HttpExchange exchange) throws IOException {
if (!"GET".equalsIgnoreCase(exchange.getRequestMethod())) { if (!"GET".equalsIgnoreCase(exchange.getRequestMethod())) {
send(exchange, 405, json("error", "method not allowed")); send(exchange, 405, json("error", "method not allowed"));
return; return;
} }
String response = runOnServerThread(this::buildAllBattles); String response = runOnServerThread(this::buildAllBattles);
if (response == null) { if (response == null) {
send(exchange, 500, json("error", "server unavailable")); send(exchange, 500, json("error", "server unavailable"));
@@ -150,120 +91,15 @@ public class CobbleBattleApiMod implements ModInitializer {
send(exchange, 200, response); send(exchange, 200, response);
} }
// ─── Response builders ──────────────────────────────────────────────
/** /**
* GET /player/<uuid> → { uuid, inBattle } * Collects all active battles, deduplicating by battleId.
*/ * Each PlayerBattleActor includes:
private String buildBasicStatus(UUID uuid, ServerPlayerEntity player) { * - playerId, name, type
boolean inBattle = findBattle(uuid) != null; * - activePokemon: species/level/hp only (no moves — hides move info from opponent reads)
StringBuilder sb = new StringBuilder(); * - party: full Pokémon data including moves (keyed to this actor only)
sb.append('{');
jsonField(sb, "uuid", uuid.toString());
sb.append(',');
jsonFieldRaw(sb, "inBattle", String.valueOf(inBattle));
sb.append('}');
return sb.toString();
}
/**
* GET /player/<uuid>/battle → full battle snapshot
*/
private String buildBattleState(UUID uuid, ServerPlayerEntity player) {
PokemonBattle battle = findBattle(uuid);
if (battle == null) {
return json("error", "not in battle");
}
StringBuilder sb = new StringBuilder();
sb.append('{');
jsonField(sb, "battleId", battle.getBattleId().toString());
sb.append(',');
// actors
sb.append("\"actors\":[");
boolean first = true;
for (BattleActor actor : battle.getActors()) {
if (!first) sb.append(',');
first = false;
sb.append('{');
jsonField(sb, "name", actor.getName().getString());
sb.append(',');
String type = (actor instanceof PlayerBattleActor) ? "player" : "npc";
jsonField(sb, "type", type);
sb.append(',');
// active pokémon
sb.append("\"activePokemon\":[");
boolean afirst = true;
for (ActiveBattlePokemon abp : actor.getActivePokemon()) {
if (abp.getBattlePokemon() == null) continue;
if (!afirst) sb.append(',');
afirst = false;
var bp = abp.getBattlePokemon();
sb.append('{');
jsonField(sb, "species", bp.getOriginalPokemon().getSpecies().getName());
sb.append(',');
jsonFieldRaw(sb, "level", String.valueOf(bp.getOriginalPokemon().getLevel()));
sb.append(',');
jsonFieldRaw(sb, "hp", String.valueOf(bp.getHealth()));
sb.append(',');
jsonFieldRaw(sb, "maxHp", String.valueOf(bp.getMaxHealth()));
sb.append('}');
}
sb.append(']');
sb.append('}');
}
sb.append(']');
sb.append('}');
return sb.toString();
}
/**
* GET /player/<uuid>/party → party pokémon list
*/
private String buildParty(UUID uuid, ServerPlayerEntity player) {
PlayerPartyStore party = Cobblemon.INSTANCE.getStorage().getParty(player);
StringBuilder sb = new StringBuilder();
sb.append("{\"uuid\":\"").append(uuid).append("\",\"party\":[");
boolean first = true;
for (Pokemon p : party.toGappyList()) {
if (p == null) continue;
if (!first) sb.append(',');
first = false;
appendPokemonJson(sb, p);
}
sb.append("]}");
return sb.toString();
}
/**
* GET /player/<uuid>/pc → PC pokémon list
*/
private String buildPc(UUID uuid, ServerPlayerEntity player) {
PCStore pc = Cobblemon.INSTANCE.getStorage().getPC(player);
StringBuilder sb = new StringBuilder();
sb.append("{\"uuid\":\"").append(uuid).append("\",\"pc\":[");
boolean first = true;
for (com.cobblemon.mod.common.api.storage.pc.PCBox box : pc.getBoxes()) {
for (Pokemon p : box.getNonEmptySlots().values()) {
if (p == null) continue;
if (!first) sb.append(',');
first = false;
appendPokemonJson(sb, p);
}
}
sb.append("]}");
return sb.toString();
}
/**
* GET /battles → all active battles
*/ */
private String buildAllBattles() { private String buildAllBattles() {
// No getBattles() in this Cobblemon version — collect battles from online players LinkedHashMap<UUID, PokemonBattle> seen = new LinkedHashMap<>();
java.util.LinkedHashMap<UUID, PokemonBattle> seen = new java.util.LinkedHashMap<>();
for (ServerPlayerEntity p : server.getPlayerManager().getPlayerList()) { for (ServerPlayerEntity p : server.getPlayerManager().getPlayerList()) {
PokemonBattle b = BattleRegistry.INSTANCE.getBattleByParticipatingPlayer(p); PokemonBattle b = BattleRegistry.INSTANCE.getBattleByParticipatingPlayer(p);
if (b != null) { if (b != null) {
@@ -273,38 +109,110 @@ public class CobbleBattleApiMod implements ModInitializer {
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
sb.append("{\"battles\":["); sb.append("{\"battles\":[");
boolean first = true; boolean firstBattle = true;
for (PokemonBattle battle : seen.values()) { for (PokemonBattle battle : seen.values()) {
if (!first) sb.append(','); if (!firstBattle) sb.append(',');
first = false; firstBattle = false;
sb.append('{'); sb.append('{');
jsonField(sb, "battleId", battle.getBattleId().toString()); jsonField(sb, "battleId", battle.getBattleId().toString());
sb.append(','); sb.append(",\"actors\":[");
sb.append("\"actors\":[");
boolean afirst = true; boolean firstActor = true;
for (BattleActor actor : battle.getActors()) { for (BattleActor actor : battle.getActors()) {
if (!afirst) sb.append(','); if (!firstActor) sb.append(',');
afirst = false; firstActor = false;
sb.append('{'); sb.append('{');
jsonField(sb, "name", actor.getName().getString()); jsonField(sb, "name", actor.getName().getString());
sb.append(','); sb.append(',');
String type = (actor instanceof PlayerBattleActor) ? "player" : "npc";
jsonField(sb, "type", type); if (actor instanceof PlayerBattleActor playerActor) {
sb.append('}'); jsonField(sb, "type", "player");
sb.append(',');
UUID playerId = playerActor.getUuid();
jsonField(sb, "playerId", playerId.toString());
// Active Pokémon — species/level/hp, intentionally no moves
sb.append(",\"activePokemon\":[");
boolean firstActive = true;
for (ActiveBattlePokemon abp : actor.getActivePokemon()) {
if (abp.getBattlePokemon() == null) continue;
if (!firstActive) sb.append(',');
firstActive = false;
var bp = abp.getBattlePokemon();
var orig = bp.getOriginalPokemon();
sb.append('{');
jsonField(sb, "uuid", orig.getUuid().toString());
sb.append(',');
jsonField(sb, "species", orig.getSpecies().getName());
sb.append(',');
jsonFieldRaw(sb, "level", String.valueOf(orig.getLevel()));
sb.append(',');
jsonFieldRaw(sb, "hp", String.valueOf(bp.getHealth()));
sb.append(',');
jsonFieldRaw(sb, "maxHp", String.valueOf(bp.getMaxHealth()));
sb.append('}');
}
sb.append(']');
// Party — full data including moves, scoped to this player only
ServerPlayerEntity playerEntity = server.getPlayerManager().getPlayer(playerId);
sb.append(",\"party\":[");
if (playerEntity != null) {
PlayerPartyStore party = Cobblemon.INSTANCE.getStorage().getParty(playerEntity);
boolean firstParty = true;
for (Pokemon p : party.toGappyList()) {
if (p == null) continue;
if (!firstParty) sb.append(',');
firstParty = false;
appendPokemonJson(sb, p);
}
}
sb.append(']');
} else {
// NPC actor — name and type only, no party or move data
jsonField(sb, "type", "npc");
sb.append(",\"activePokemon\":[");
boolean firstActive = true;
for (ActiveBattlePokemon abp : actor.getActivePokemon()) {
if (abp.getBattlePokemon() == null) continue;
if (!firstActive) sb.append(',');
firstActive = false;
var bp = abp.getBattlePokemon();
var orig = bp.getOriginalPokemon();
sb.append('{');
jsonField(sb, "uuid", orig.getUuid().toString());
sb.append(',');
jsonField(sb, "species", orig.getSpecies().getName());
sb.append(',');
jsonFieldRaw(sb, "level", String.valueOf(orig.getLevel()));
sb.append(',');
jsonFieldRaw(sb, "hp", String.valueOf(bp.getHealth()));
sb.append(',');
jsonFieldRaw(sb, "maxHp", String.valueOf(bp.getMaxHealth()));
sb.append('}');
}
sb.append(']');
}
sb.append('}'); // end actor
} }
sb.append(']');
sb.append('}'); sb.append("]}"); // end actors + battle
} }
sb.append("]}");
sb.append("]}"); // end battles
return sb.toString(); return sb.toString();
} }
// ─── Helpers ──────────────────────────────────────────────────────── // ─── Helpers ────────────────────────────────────────────────────────
private PokemonBattle findBattle(UUID playerUuid) { /** Full Pokémon snapshot including moves — only used for the owning player's party. */
return BattleRegistry.INSTANCE.getBattleByParticipatingPlayerId(playerUuid);
}
private void appendPokemonJson(StringBuilder sb, Pokemon p) { private void appendPokemonJson(StringBuilder sb, Pokemon p) {
sb.append('{'); sb.append('{');
jsonField(sb, "uuid", p.getUuid().toString()); jsonField(sb, "uuid", p.getUuid().toString());
@@ -322,8 +230,6 @@ public class CobbleBattleApiMod implements ModInitializer {
jsonField(sb, "nature", p.getNature().getName().toString()); jsonField(sb, "nature", p.getNature().getName().toString());
sb.append(','); sb.append(',');
jsonFieldRaw(sb, "shiny", String.valueOf(p.getShiny())); jsonFieldRaw(sb, "shiny", String.valueOf(p.getShiny()));
// moves
sb.append(",\"moves\":["); sb.append(",\"moves\":[");
boolean mfirst = true; boolean mfirst = true;
for (var move : p.getMoveSet()) { for (var move : p.getMoveSet()) {
@@ -337,9 +243,7 @@ public class CobbleBattleApiMod implements ModInitializer {
jsonFieldRaw(sb, "maxPp", String.valueOf(move.getMaxPp())); jsonFieldRaw(sb, "maxPp", String.valueOf(move.getMaxPp()));
sb.append('}'); sb.append('}');
} }
sb.append(']'); sb.append("]}");
sb.append('}');
} }
private static String json(String key, String value) { private static String json(String key, String value) {
@@ -363,10 +267,6 @@ public class CobbleBattleApiMod implements ModInitializer {
.replace("\t", "\\t"); .replace("\t", "\\t");
} }
/**
* Run a task on the MC server thread and wait for the result.
* Returns null if the server is unavailable.
*/
private String runOnServerThread(java.util.function.Supplier<String> task) { private String runOnServerThread(java.util.function.Supplier<String> task) {
MinecraftServer mc = this.server; MinecraftServer mc = this.server;
if (mc == null) return null; if (mc == null) return null;
@@ -398,3 +298,4 @@ public class CobbleBattleApiMod implements ModInitializer {
} }
} }
} }