Fix gamerule toggles for 26.x renames; add map/skins/pregen + admin favors
Minecraft 26.x renamed every gamerule to snake_case (keepInventory → keep_inventory, doDaylightCycle → advance_time, ...), so the panel's queries hit unknown rules: reads returned nothing, toggles always started from 'off', and sets silently failed. Rules are now defined by candidate names resolved against the live server (works on old and new versions), sets are verified by re-query, and failures surface the server's actual reply. Toggles render only for rules the server reports. Creative additions: - Featured mods on the Mods page: BlueMap (3D web map), SkinRestorer (real skins in offline mode), Chunky (pregen) — verified for 26.2 - BlueMap plumbing: MAP_PORT mapping in compose, in-network detection, 'Open the live world map' link on the Overview - Player modal favors: heal, teleport-to-player, give item - Broadcast box gained a full-screen title mode Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
deeeaf0042
commit
b2ff88b255
9 files changed
+344
-50
No files matched your search
@@ -8,6 +8,10 @@ import {
|
||||
getGamerules,
|
||||
setGamerule,
|
||||
say,
|
||||
broadcastTitle,
|
||||
giveItem,
|
||||
teleportToPlayer,
|
||||
healPlayer,
|
||||
} from "@/server/mc";
|
||||
import { logActivity } from "@/server/db";
|
||||
import { ok, fail, readJson, handle } from "@/server/api";
|
||||
@@ -28,7 +32,10 @@ type Body =
|
||||
| { op: "weather"; value: "clear" | "rain" | "thunder" }
|
||||
| { op: "difficulty"; value: string }
|
||||
| { op: "gamerule"; rule: string; value: boolean }
|
||||
| { op: "broadcast"; message: string };
|
||||
| { op: "broadcast"; message: string; style?: "chat" | "title" }
|
||||
| { op: "give"; name: string; item: string; count?: number }
|
||||
| { op: "teleport"; name: string; target: string }
|
||||
| { op: "heal"; name: string };
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
return handle(async () => {
|
||||
@@ -56,13 +63,33 @@ export async function POST(req: NextRequest) {
|
||||
return ok({ output: out });
|
||||
}
|
||||
case "gamerule": {
|
||||
const out = await setGamerule(body.rule, body.value);
|
||||
const applied = await setGamerule(body.rule, body.value);
|
||||
logActivity("admin", null, `gamerule ${body.rule} ${body.value}`);
|
||||
return ok({ output: out });
|
||||
return ok({ value: applied });
|
||||
}
|
||||
case "broadcast": {
|
||||
if (!body.message?.trim()) return fail("Empty message");
|
||||
await say(body.message.trim().slice(0, 256));
|
||||
const message = body.message.trim().slice(0, 256);
|
||||
if (body.style === "title") {
|
||||
await broadcastTitle(message);
|
||||
} else {
|
||||
await say(message);
|
||||
}
|
||||
return ok({});
|
||||
}
|
||||
case "give": {
|
||||
const out = await giveItem(body.name, body.item, body.count ?? 1);
|
||||
logActivity("admin", body.name, `give ${body.item} ×${body.count ?? 1}`);
|
||||
return ok({ output: out });
|
||||
}
|
||||
case "teleport": {
|
||||
const out = await teleportToPlayer(body.name, body.target);
|
||||
logActivity("admin", body.name, `tp → ${body.target}`);
|
||||
return ok({ output: out });
|
||||
}
|
||||
case "heal": {
|
||||
await healPlayer(body.name);
|
||||
logActivity("admin", body.name, "heal");
|
||||
return ok({});
|
||||
}
|
||||
default:
|
||||
|
||||
@@ -4,10 +4,26 @@ import { db } from "@/server/db";
|
||||
import { env } from "@/server/env";
|
||||
import { getWorldSizeBytes } from "@/server/sampler";
|
||||
import { readWorldInfo } from "@/server/playerstats";
|
||||
import { readEnvFile } from "@/server/envfile";
|
||||
import { ok, handle } from "@/server/api";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
// BlueMap detection (in-network probe of the mc container), cached 60 s.
|
||||
let mapCheck: { at: number; available: boolean } = { at: 0, available: false };
|
||||
|
||||
async function bluemapAvailable(running: boolean): Promise<boolean> {
|
||||
if (!running) return false;
|
||||
if (Date.now() - mapCheck.at < 60_000) return mapCheck.available;
|
||||
let available = false;
|
||||
try {
|
||||
const res = await fetch(`http://${env.mcContainer}:8100/`, { signal: AbortSignal.timeout(1500) });
|
||||
available = res.ok;
|
||||
} catch {}
|
||||
mapCheck = { at: Date.now(), available };
|
||||
return available;
|
||||
}
|
||||
|
||||
export async function GET() {
|
||||
return handle(async () => {
|
||||
let container;
|
||||
@@ -49,6 +65,10 @@ export async function GET() {
|
||||
serverType,
|
||||
metrics: latest ?? null,
|
||||
pendingApprovals: pendingCount,
|
||||
map: {
|
||||
available: await bluemapAvailable(container.running),
|
||||
port: Number(readEnvFile()["MAP_PORT"]) || 8100,
|
||||
},
|
||||
world: readWorldInfo(getWorldSizeBytes()),
|
||||
uptimeMs:
|
||||
container.running && "startedAt" in container && container.startedAt
|
||||
|
||||
Reference in new issue
Block a user