Initial push

This commit is contained in:
2026-03-08 15:49:34 +01:00
parent 8da12bb7d1
commit 47127f276d
101 changed files with 13844 additions and 8 deletions

104
lib/minecraft/sync.ts Normal file
View File

@@ -0,0 +1,104 @@
/**
* Syncs Minecraft server data (players, plugins) into the local database
* by parsing server logs and using RCON commands.
*/
import { db } from "@/lib/db";
import { mcPlayers, plugins } from "@/lib/db/schema";
import { rconClient } from "@/lib/minecraft/rcon";
import { eq } from "drizzle-orm";
import { nanoid } from "nanoid";
/** Parse the player list from the RCON "list" command response. */
export async function syncOnlinePlayers(): Promise<void> {
try {
const response = await rconClient.sendCommand("list");
// Response format: "There are X of a max of Y players online: player1, player2"
const match = response.match(/players online: (.*)$/);
if (!match) return;
const onlineNames = match[1]
.split(",")
.map((s) => s.trim())
.filter(Boolean);
// Mark all players as offline first
await db
.update(mcPlayers)
.set({ isOnline: false })
.where(eq(mcPlayers.isOnline, true));
// Mark online players
for (const name of onlineNames) {
const existing = await db
.select()
.from(mcPlayers)
.where(eq(mcPlayers.username, name))
.get();
if (existing) {
await db
.update(mcPlayers)
.set({ isOnline: true, lastSeen: Date.now() })
.where(eq(mcPlayers.username, name));
}
}
} catch {
// RCON might not be connected — ignore
}
}
/** Parse a log line and update player records accordingly. */
export function parseLogLine(
line: string,
onPlayerJoin?: (name: string) => void,
onPlayerLeave?: (name: string) => void,
): void {
// "[HH:MM:SS] [Server thread/INFO]: PlayerName joined the game"
const joinMatch = line.match(/\[.*\]: (\w+) joined the game/);
if (joinMatch) {
const name = joinMatch[1];
upsertPlayer(name, { isOnline: true, lastSeen: Date.now() });
onPlayerJoin?.(name);
return;
}
// "[HH:MM:SS] [Server thread/INFO]: PlayerName left the game"
const leaveMatch = line.match(/\[.*\]: (\w+) left the game/);
if (leaveMatch) {
const name = leaveMatch[1];
upsertPlayer(name, { isOnline: false, lastSeen: Date.now() });
onPlayerLeave?.(name);
return;
}
}
async function upsertPlayer(
username: string,
data: Partial<typeof mcPlayers.$inferInsert>,
): Promise<void> {
const existing = await db
.select()
.from(mcPlayers)
.where(eq(mcPlayers.username, username))
.get();
if (existing) {
await db
.update(mcPlayers)
.set(data as Record<string, unknown>)
.where(eq(mcPlayers.username, username));
} else {
await db.insert(mcPlayers).values({
id: nanoid(),
uuid: (data as { uuid?: string }).uuid ?? nanoid(), // placeholder until real UUID is known
username,
firstSeen: Date.now(),
lastSeen: Date.now(),
isOnline: false,
playTime: 0,
isBanned: false,
...data,
});
}
}