workign on local server
This commit is contained in:
5
.gitignore
vendored
5
.gitignore
vendored
@@ -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
3
cobble-api.http
Normal file
@@ -0,0 +1,3 @@
|
||||
@host = http://localhost:8085
|
||||
|
||||
GET {{host}}/battles
|
||||
26
docker-compose.yml
Normal file
26
docker-compose.yml
Normal 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
|
||||
@@ -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 {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user