initial
This commit is contained in:
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
build/
|
||||
.gradle/
|
||||
gradle/
|
||||
.gradle_cache/
|
||||
33
build.gradle
Normal file
33
build.gradle
Normal file
@@ -0,0 +1,33 @@
|
||||
plugins {
|
||||
id 'fabric-loom' version '1.7-SNAPSHOT'
|
||||
id 'maven-publish'
|
||||
}
|
||||
|
||||
group = 'com.example'
|
||||
version = '1.0.0'
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
maven { url = "https://maven.fabricmc.net/" }
|
||||
maven { url = "https://maven.architectury.dev/" }
|
||||
maven { url = "https://maven.minecraftforge.net/" }
|
||||
maven { url = "https://dl.cloudsmith.io/public/geckolib3/geckolib/maven/" }
|
||||
maven { url = "https://maven.impactdev.net/repository/development/" }
|
||||
}
|
||||
|
||||
dependencies {
|
||||
minecraft "com.mojang:minecraft:1.21.1"
|
||||
mappings "net.fabricmc:yarn:1.21.1+build.3:v2"
|
||||
modImplementation "net.fabricmc:fabric-loader:0.16.5"
|
||||
|
||||
modImplementation "net.fabricmc.fabric-api:fabric-api:0.106.1+1.21.1"
|
||||
|
||||
// Cobblemon
|
||||
modImplementation "com.cobblemon:fabric:1.7.3+1.21.1"
|
||||
}
|
||||
|
||||
java {
|
||||
withSourcesJar()
|
||||
sourceCompatibility = JavaVersion.VERSION_21
|
||||
targetCompatibility = JavaVersion.VERSION_21
|
||||
}
|
||||
24
build.sh
Executable file
24
build.sh
Executable file
@@ -0,0 +1,24 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
IMAGE="gradle:8.7-jdk21"
|
||||
|
||||
echo "=== Building with Docker ($IMAGE) ==="
|
||||
echo "Project dir: $(pwd)"
|
||||
echo ""
|
||||
|
||||
docker run --rm \
|
||||
-v "$PWD":/project \
|
||||
-v "$PWD/.gradle_cache:/home/gradle/.gradle" \
|
||||
-w /project \
|
||||
"$IMAGE" \
|
||||
gradle build "$@"
|
||||
|
||||
echo ""
|
||||
echo "=== Build complete ==="
|
||||
ls -lh build/libs/*.jar 2>/dev/null || echo "No jars found in build/libs/"
|
||||
|
||||
|
||||
echo ""
|
||||
echo "put the build/libs/cobblemon-battle-api-1.0.0.jar in the mods folder of your minecraft instance to use it"
|
||||
1
gradle.properties
Normal file
1
gradle.properties
Normal file
@@ -0,0 +1 @@
|
||||
org.gradle.jvmargs=-Xmx2G
|
||||
14
jar.sh
Executable file
14
jar.sh
Executable file
@@ -0,0 +1,14 @@
|
||||
#!/usr/bin/env nix-shell
|
||||
#!nix-shell -i bash -p jdk17
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
if [ $# -eq 0 ]; then
|
||||
echo "Usage: ./jar.sh <jar-args...>"
|
||||
echo "Examples:"
|
||||
echo " ./jar.sh tf some.jar # list contents"
|
||||
echo " ./jar.sh tf some.jar | grep ClassName # find a class"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
jar "$@"
|
||||
12
javap.sh
Executable file
12
javap.sh
Executable file
@@ -0,0 +1,12 @@
|
||||
#!/usr/bin/env nix-shell
|
||||
#!nix-shell -i bash -p jdk17
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
if [ $# -eq 0 ]; then
|
||||
echo "Usage: ./javap.sh <class-name> <jar-file>"
|
||||
echo "Example: ./javap.sh com.cobblemon.mod.common.battles.BattleRegistry some.jar"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
javap -classpath "$2" "$1"
|
||||
8
settings.gradle
Normal file
8
settings.gradle
Normal file
@@ -0,0 +1,8 @@
|
||||
pluginManagement {
|
||||
repositories {
|
||||
maven { url = "https://maven.fabricmc.net/" }
|
||||
gradlePluginPortal()
|
||||
}
|
||||
}
|
||||
|
||||
rootProject.name = "cobblemon-battle-api"
|
||||
406
src/main/java/com/example/cobbleapi/CobbleBattleApiMod.java
Normal file
406
src/main/java/com/example/cobbleapi/CobbleBattleApiMod.java
Normal file
@@ -0,0 +1,406 @@
|
||||
package com.example.cobbleapi;
|
||||
|
||||
import com.sun.net.httpserver.HttpServer;
|
||||
import com.sun.net.httpserver.HttpExchange;
|
||||
|
||||
import net.fabricmc.api.ModInitializer;
|
||||
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents;
|
||||
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.network.ServerPlayerEntity;
|
||||
|
||||
import com.cobblemon.mod.common.api.battles.model.PokemonBattle;
|
||||
import com.cobblemon.mod.common.api.battles.model.actor.BattleActor;
|
||||
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.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
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.
|
||||
*
|
||||
* 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
|
||||
*/
|
||||
public class CobbleBattleApiMod implements ModInitializer {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger("CobbleBattleAPI");
|
||||
private static final int PORT = 8085;
|
||||
|
||||
private volatile MinecraftServer server;
|
||||
private HttpServer httpServer;
|
||||
|
||||
@Override
|
||||
public void onInitialize() {
|
||||
|
||||
ServerLifecycleEvents.SERVER_STARTED.register(mc -> {
|
||||
this.server = mc;
|
||||
startHttpServer();
|
||||
});
|
||||
|
||||
ServerLifecycleEvents.SERVER_STOPPING.register(mc -> {
|
||||
stopHttpServer();
|
||||
this.server = null;
|
||||
});
|
||||
}
|
||||
|
||||
// ─── HTTP server lifecycle ──────────────────────────────────────────
|
||||
|
||||
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);
|
||||
} catch (IOException e) {
|
||||
LOGGER.error("Failed to start CobbleBattleAPI HTTP server", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void stopHttpServer() {
|
||||
if (httpServer != null) {
|
||||
httpServer.stop(0);
|
||||
LOGGER.info("CobbleBattleAPI HTTP server stopped");
|
||||
}
|
||||
}
|
||||
|
||||
// ─── /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);
|
||||
case "pc" -> buildPc(uuid);
|
||||
default -> buildBasicStatus(uuid, player);
|
||||
};
|
||||
}
|
||||
|
||||
// ─── /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"));
|
||||
return;
|
||||
}
|
||||
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) {
|
||||
PlayerPartyStore party;
|
||||
try {
|
||||
party = Cobblemon.INSTANCE.getStorage().getParty(uuid);
|
||||
} catch (Exception e) {
|
||||
return json("error", "could not load party");
|
||||
}
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("{\"uuid\":\"").append(uuid).append("\",\"party\":[");
|
||||
boolean first = true;
|
||||
for (Pokemon p : party) {
|
||||
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) {
|
||||
PCStore pc;
|
||||
try {
|
||||
pc = Cobblemon.INSTANCE.getStorage().getPC(uuid);
|
||||
} catch (Exception e) {
|
||||
return json("error", "could not load pc");
|
||||
}
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("{\"uuid\":\"").append(uuid).append("\",\"pc\":[");
|
||||
boolean first = true;
|
||||
for (Pokemon p : pc) {
|
||||
if (!first) sb.append(',');
|
||||
first = false;
|
||||
appendPokemonJson(sb, p);
|
||||
}
|
||||
sb.append("]}");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /battles → all active battles
|
||||
*/
|
||||
private String buildAllBattles() {
|
||||
// No getBattles() in this Cobblemon version — collect battles from online players
|
||||
java.util.LinkedHashMap<UUID, PokemonBattle> seen = new java.util.LinkedHashMap<>();
|
||||
for (ServerPlayerEntity p : server.getPlayerManager().getPlayerList()) {
|
||||
PokemonBattle b = BattleRegistry.INSTANCE.getBattleByParticipatingPlayer(p);
|
||||
if (b != null) {
|
||||
seen.putIfAbsent(b.getBattleId(), b);
|
||||
}
|
||||
}
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("{\"battles\":[");
|
||||
boolean first = true;
|
||||
for (PokemonBattle battle : seen.values()) {
|
||||
if (!first) sb.append(',');
|
||||
first = false;
|
||||
sb.append('{');
|
||||
jsonField(sb, "battleId", battle.getBattleId().toString());
|
||||
sb.append(',');
|
||||
sb.append("\"actors\":[");
|
||||
boolean afirst = true;
|
||||
for (BattleActor actor : battle.getActors()) {
|
||||
if (!afirst) sb.append(',');
|
||||
afirst = false;
|
||||
sb.append('{');
|
||||
jsonField(sb, "name", actor.getName().getString());
|
||||
sb.append(',');
|
||||
String type = (actor instanceof PlayerBattleActor) ? "player" : "npc";
|
||||
jsonField(sb, "type", type);
|
||||
sb.append('}');
|
||||
}
|
||||
sb.append(']');
|
||||
sb.append('}');
|
||||
}
|
||||
sb.append("]}");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
// ─── Helpers ────────────────────────────────────────────────────────
|
||||
|
||||
private PokemonBattle findBattle(UUID playerUuid) {
|
||||
return BattleRegistry.INSTANCE.getBattleByParticipatingPlayerId(playerUuid);
|
||||
}
|
||||
|
||||
private void appendPokemonJson(StringBuilder sb, Pokemon p) {
|
||||
sb.append('{');
|
||||
jsonField(sb, "uuid", p.getUuid().toString());
|
||||
sb.append(',');
|
||||
jsonField(sb, "species", p.getSpecies().getName());
|
||||
sb.append(',');
|
||||
jsonFieldRaw(sb, "level", String.valueOf(p.getLevel()));
|
||||
sb.append(',');
|
||||
jsonFieldRaw(sb, "hp", String.valueOf(p.getCurrentHealth()));
|
||||
sb.append(',');
|
||||
jsonFieldRaw(sb, "maxHp", String.valueOf(p.getHp()));
|
||||
sb.append(',');
|
||||
jsonField(sb, "ability", p.getAbility().getName());
|
||||
sb.append(',');
|
||||
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()) {
|
||||
if (!mfirst) sb.append(',');
|
||||
mfirst = false;
|
||||
sb.append('{');
|
||||
jsonField(sb, "name", move.getName());
|
||||
sb.append(',');
|
||||
jsonFieldRaw(sb, "pp", String.valueOf(move.getCurrentPp()));
|
||||
sb.append(',');
|
||||
jsonFieldRaw(sb, "maxPp", String.valueOf(move.getMaxPp()));
|
||||
sb.append('}');
|
||||
}
|
||||
sb.append(']');
|
||||
|
||||
sb.append('}');
|
||||
}
|
||||
|
||||
private static String json(String key, String value) {
|
||||
return "{\"" + escapeJson(key) + "\":\"" + escapeJson(value) + "\"}";
|
||||
}
|
||||
|
||||
private static void jsonField(StringBuilder sb, String key, String value) {
|
||||
sb.append('"').append(escapeJson(key)).append("\":\"").append(escapeJson(value)).append('"');
|
||||
}
|
||||
|
||||
private static void jsonFieldRaw(StringBuilder sb, String key, String rawValue) {
|
||||
sb.append('"').append(escapeJson(key)).append("\":").append(rawValue);
|
||||
}
|
||||
|
||||
private static String escapeJson(String s) {
|
||||
if (s == null) return "";
|
||||
return s.replace("\\", "\\\\")
|
||||
.replace("\"", "\\\"")
|
||||
.replace("\n", "\\n")
|
||||
.replace("\r", "\\r")
|
||||
.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;
|
||||
|
||||
CompletableFuture<String> future = new CompletableFuture<>();
|
||||
mc.execute(() -> {
|
||||
try {
|
||||
future.complete(task.get());
|
||||
} catch (Exception e) {
|
||||
LOGGER.error("Error processing API request", e);
|
||||
future.complete(json("error", "internal error"));
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
return future.get(5, java.util.concurrent.TimeUnit.SECONDS);
|
||||
} catch (Exception e) {
|
||||
LOGGER.error("Timeout waiting for server thread", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private void send(HttpExchange exchange, int code, String body) throws IOException {
|
||||
byte[] bytes = body.getBytes(java.nio.charset.StandardCharsets.UTF_8);
|
||||
exchange.getResponseHeaders().set("Content-Type", "application/json; charset=utf-8");
|
||||
exchange.sendResponseHeaders(code, bytes.length);
|
||||
try (OutputStream os = exchange.getResponseBody()) {
|
||||
os.write(bytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
18
src/main/resources/fabric.mod.json
Normal file
18
src/main/resources/fabric.mod.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"id": "cobblebattleapi",
|
||||
"version": "1.0.0",
|
||||
"name": "Cobblemon Battle API",
|
||||
"description": "HTTP API exposing Cobblemon battle state",
|
||||
"authors": ["you"],
|
||||
"environment": "server",
|
||||
"entrypoints": {
|
||||
"main": ["com.example.cobbleapi.CobbleBattleApiMod"]
|
||||
},
|
||||
"depends": {
|
||||
"fabricloader": ">=0.16",
|
||||
"minecraft": "1.21.1",
|
||||
"fabric-api": "*",
|
||||
"cobblemon": "*"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user