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

5
.gitignore vendored
View File

@@ -2,3 +2,8 @@ build/
.gradle/
gradle/
.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.actor.PlayerBattleActor;
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.pokemon.Pokemon;
import java.io.IOException;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.util.ArrayList;
import java.util.List;
import java.util.LinkedHashMap;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
@@ -31,15 +29,12 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Fabric server-side mod that exposes Cobblemon battle and Pokémon data
* over a lightweight HTTP API.
* Fabric mod exposing a single HTTP endpoint:
*
* Endpoints:
* GET /player/<uuid> — is the player in a battle?
* GET /player/<uuid>/battle — full battle state for that player
* GET /player/<uuid>/party — player's party Pokémon
* GET /player/<uuid>/pc — player's PC Pokémon
* GET /battles — list all active battles
* GET /battles — all active battles; each player actor includes their
* playerId, active Pokémon (no moves), and full party
* with moves. Opponent move sets are only present under
* that opponent's own actor block.
*/
public class CobbleBattleApiMod implements ModInitializer {
@@ -51,12 +46,10 @@ public class CobbleBattleApiMod implements ModInitializer {
@Override
public void onInitialize() {
ServerLifecycleEvents.SERVER_STARTED.register(mc -> {
this.server = mc;
startHttpServer();
});
ServerLifecycleEvents.SERVER_STOPPING.register(mc -> {
stopHttpServer();
this.server = null;
@@ -68,7 +61,6 @@ public class CobbleBattleApiMod implements ModInitializer {
private void startHttpServer() {
try {
httpServer = HttpServer.create(new InetSocketAddress(PORT), 0);
httpServer.createContext("/player", this::handlePlayer);
httpServer.createContext("/battles", this::handleBattles);
httpServer.start();
LOGGER.info("CobbleBattleAPI listening on port {}", PORT);
@@ -84,64 +76,13 @@ public class CobbleBattleApiMod implements ModInitializer {
}
}
// ─── /player/<uuid>[/battle|/party|/pc] ─────────────────────────────
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 ───────────────────────────────────────────────────────
// ─── GET /battles ────────────────────────────────────────────────────
private void handleBattles(HttpExchange exchange) throws IOException {
if (!"GET".equalsIgnoreCase(exchange.getRequestMethod())) {
send(exchange, 405, json("error", "method not allowed"));
return;
}
String response = runOnServerThread(this::buildAllBattles);
if (response == null) {
send(exchange, 500, json("error", "server unavailable"));
@@ -150,120 +91,15 @@ public class CobbleBattleApiMod implements ModInitializer {
send(exchange, 200, response);
}
// ─── Response builders ──────────────────────────────────────────────
/**
* GET /player/<uuid> → { uuid, inBattle }
*/
private String buildBasicStatus(UUID uuid, ServerPlayerEntity player) {
boolean inBattle = findBattle(uuid) != null;
StringBuilder sb = new StringBuilder();
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
* Collects all active battles, deduplicating by battleId.
* Each PlayerBattleActor includes:
* - playerId, name, type
* - activePokemon: species/level/hp only (no moves — hides move info from opponent reads)
* - party: full Pokémon data including moves (keyed to this actor only)
*/
private String buildAllBattles() {
// No getBattles() in this Cobblemon version — collect battles from online players
java.util.LinkedHashMap<UUID, PokemonBattle> seen = new java.util.LinkedHashMap<>();
LinkedHashMap<UUID, PokemonBattle> seen = new LinkedHashMap<>();
for (ServerPlayerEntity p : server.getPlayerManager().getPlayerList()) {
PokemonBattle b = BattleRegistry.INSTANCE.getBattleByParticipatingPlayer(p);
if (b != null) {
@@ -273,38 +109,110 @@ public class CobbleBattleApiMod implements ModInitializer {
StringBuilder sb = new StringBuilder();
sb.append("{\"battles\":[");
boolean first = true;
boolean firstBattle = true;
for (PokemonBattle battle : seen.values()) {
if (!first) sb.append(',');
first = false;
if (!firstBattle) sb.append(',');
firstBattle = false;
sb.append('{');
jsonField(sb, "battleId", battle.getBattleId().toString());
sb.append(',');
sb.append("\"actors\":[");
boolean afirst = true;
sb.append(",\"actors\":[");
boolean firstActor = true;
for (BattleActor actor : battle.getActors()) {
if (!afirst) sb.append(',');
afirst = false;
if (!firstActor) sb.append(',');
firstActor = false;
sb.append('{');
jsonField(sb, "name", actor.getName().getString());
sb.append(',');
String type = (actor instanceof PlayerBattleActor) ? "player" : "npc";
jsonField(sb, "type", type);
if (actor instanceof PlayerBattleActor playerActor) {
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(']');
}
sb.append('}'); // end actor
}
sb.append("]}"); // end actors + battle
}
sb.append("]}"); // end battles
return sb.toString();
}
// ─── Helpers ────────────────────────────────────────────────────────
private PokemonBattle findBattle(UUID playerUuid) {
return BattleRegistry.INSTANCE.getBattleByParticipatingPlayerId(playerUuid);
}
/** Full Pokémon snapshot including moves — only used for the owning player's party. */
private void appendPokemonJson(StringBuilder sb, Pokemon p) {
sb.append('{');
jsonField(sb, "uuid", p.getUuid().toString());
@@ -322,8 +230,6 @@ public class CobbleBattleApiMod implements ModInitializer {
jsonField(sb, "nature", p.getNature().getName().toString());
sb.append(',');
jsonFieldRaw(sb, "shiny", String.valueOf(p.getShiny()));
// moves
sb.append(",\"moves\":[");
boolean mfirst = true;
for (var move : p.getMoveSet()) {
@@ -337,9 +243,7 @@ public class CobbleBattleApiMod implements ModInitializer {
jsonFieldRaw(sb, "maxPp", String.valueOf(move.getMaxPp()));
sb.append('}');
}
sb.append(']');
sb.append('}');
sb.append("]}");
}
private static String json(String key, String value) {
@@ -363,10 +267,6 @@ public class CobbleBattleApiMod implements ModInitializer {
.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) {
MinecraftServer mc = this.server;
if (mc == null) return null;
@@ -398,3 +298,4 @@ public class CobbleBattleApiMod implements ModInitializer {
}
}
}