Active player list
This commit is contained in:
parent
4a4563ba85
commit
b63db82a99
1
.gitignore
vendored
1
.gitignore
vendored
@ -17,3 +17,4 @@ server/
|
||||
|
||||
# Config file for MCGRIZZ
|
||||
mcgrizz.json
|
||||
players.cache.json
|
@ -18,8 +18,13 @@ const colors = {
|
||||
text: "text-white",
|
||||
},
|
||||
fire: {
|
||||
bg: "bg-grape",
|
||||
border: "border-grape-800",
|
||||
bg: "bg-fire",
|
||||
border: "border-fire-800",
|
||||
text: "text-white",
|
||||
},
|
||||
wasabi: {
|
||||
bg: "bg-wasabi-600",
|
||||
border: "border-wasabi-800",
|
||||
text: "text-white",
|
||||
},
|
||||
};
|
||||
@ -38,6 +43,7 @@ export function Button(
|
||||
${colors[color].text}
|
||||
hover:bg-smoke-500
|
||||
transition-colors
|
||||
disabled:opacity-30
|
||||
${props.class || ""}`;
|
||||
return (
|
||||
<button
|
||||
|
@ -2,7 +2,7 @@ import { VNode } from "preact";
|
||||
|
||||
export function Content(props: { children?: VNode | VNode[] }) {
|
||||
return (
|
||||
<div class="mt-16 bg-smoke-200 dark:bg-smoke-900 border-4 border-licorice-800 p-8 rounded-3xl">
|
||||
<div class="bg-smoke-200 dark:bg-smoke-900 border-4 border-licorice-800 p-8 rounded-3xl">
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
|
28
components/properties.tsx
Normal file
28
components/properties.tsx
Normal file
@ -0,0 +1,28 @@
|
||||
import { Button } from "./Button.tsx";
|
||||
import { Content } from "./Content.tsx";
|
||||
|
||||
export function MCProperties(
|
||||
{ properties }: { properties: Map<string, string> },
|
||||
) {
|
||||
return (
|
||||
<div className="container p-8">
|
||||
<Content>
|
||||
<form method="POST" action="/properties">
|
||||
<div class="grid grid-cols-3 gap-y-4 gap-x-16">
|
||||
{Array.from(properties.entries()).sort(([a],[b]) => a > b ? 1 : -1).map(([k, v]) => (
|
||||
<div>
|
||||
<label class="block font-pixel text-lg" htmlFor={k}>{k}</label>
|
||||
<input class="w-full" type="text" name={k} value={v} autocomplete="off" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div class="mt-8">
|
||||
<Button type="submit">
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Content>
|
||||
</div>
|
||||
);
|
||||
}
|
38
fresh.gen.ts
38
fresh.gen.ts
@ -5,30 +5,40 @@
|
||||
import * as $0 from "./routes/_404.tsx";
|
||||
import * as $1 from "./routes/_app.tsx";
|
||||
import * as $2 from "./routes/api/fabric/index.ts";
|
||||
import * as $3 from "./routes/api/terminal.ts";
|
||||
import * as $4 from "./routes/index.tsx";
|
||||
import * as $5 from "./routes/setup/eula.tsx";
|
||||
import * as $6 from "./routes/setup/fabric.tsx";
|
||||
import * as $7 from "./routes/setup/index.tsx";
|
||||
import * as $8 from "./routes/setup/start.tsx";
|
||||
import * as $3 from "./routes/api/manage.ts";
|
||||
import * as $4 from "./routes/api/players.ts";
|
||||
import * as $5 from "./routes/index.tsx";
|
||||
import * as $6 from "./routes/players.tsx";
|
||||
import * as $7 from "./routes/properties.tsx";
|
||||
import * as $8 from "./routes/setup/eula.tsx";
|
||||
import * as $9 from "./routes/setup/fabric.tsx";
|
||||
import * as $10 from "./routes/setup/index.tsx";
|
||||
import * as $11 from "./routes/terminal.tsx";
|
||||
import * as $$0 from "./islands/fabricVersions.tsx";
|
||||
import * as $$1 from "./islands/terminal.tsx";
|
||||
import * as $$1 from "./islands/players.tsx";
|
||||
import * as $$2 from "./islands/statusManager.tsx";
|
||||
import * as $$3 from "./islands/terminal.tsx";
|
||||
|
||||
const manifest = {
|
||||
routes: {
|
||||
"./routes/_404.tsx": $0,
|
||||
"./routes/_app.tsx": $1,
|
||||
"./routes/api/fabric/index.ts": $2,
|
||||
"./routes/api/terminal.ts": $3,
|
||||
"./routes/index.tsx": $4,
|
||||
"./routes/setup/eula.tsx": $5,
|
||||
"./routes/setup/fabric.tsx": $6,
|
||||
"./routes/setup/index.tsx": $7,
|
||||
"./routes/setup/start.tsx": $8,
|
||||
"./routes/api/manage.ts": $3,
|
||||
"./routes/api/players.ts": $4,
|
||||
"./routes/index.tsx": $5,
|
||||
"./routes/players.tsx": $6,
|
||||
"./routes/properties.tsx": $7,
|
||||
"./routes/setup/eula.tsx": $8,
|
||||
"./routes/setup/fabric.tsx": $9,
|
||||
"./routes/setup/index.tsx": $10,
|
||||
"./routes/terminal.tsx": $11,
|
||||
},
|
||||
islands: {
|
||||
"./islands/fabricVersions.tsx": $$0,
|
||||
"./islands/terminal.tsx": $$1,
|
||||
"./islands/players.tsx": $$1,
|
||||
"./islands/statusManager.tsx": $$2,
|
||||
"./islands/terminal.tsx": $$3,
|
||||
},
|
||||
baseUrl: import.meta.url,
|
||||
};
|
||||
|
55
islands/players.tsx
Normal file
55
islands/players.tsx
Normal file
@ -0,0 +1,55 @@
|
||||
import { useEffect, useRef, useState } from "preact/hooks";
|
||||
import { StatusManager } from "./statusManager.tsx";
|
||||
import { IS_BROWSER } from "$fresh/runtime.ts";
|
||||
import { PlayerData } from "../util/players.ts";
|
||||
import { Button } from "../components/Button.tsx";
|
||||
|
||||
export function ActivePlayerList() {
|
||||
const [players, setPlayers] = useState<PlayerData[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!IS_BROWSER) return;
|
||||
configureEventSource();
|
||||
}, []);
|
||||
|
||||
const eventSource = useRef<EventSource>();
|
||||
|
||||
const configureEventSource = () => {
|
||||
if (eventSource.current?.OPEN) eventSource.current.close();
|
||||
eventSource.current = new EventSource("/api/players");
|
||||
eventSource.current.addEventListener("players", (e) => {
|
||||
setPlayers(JSON.parse(e.data));
|
||||
});
|
||||
};
|
||||
|
||||
const followStatusManager = (action: string) => {
|
||||
if (action === "started" || action === "restarted") {
|
||||
configureEventSource();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<StatusManager onAction={followStatusManager} />
|
||||
<div class="grid grid-cols-2 p-8">
|
||||
<h2 class="col-span-2 font-pixel text-xl">Active Players</h2>
|
||||
{players.map((p) => <PlayerCard player={p} />)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function PlayerCard({ player }: { player: PlayerData }) {
|
||||
return (
|
||||
<div class="flex gap-4">
|
||||
<img class="w-16" src={player.avatar} alt={`${player.username}'s avatar`} />
|
||||
<div>
|
||||
<h3>{player.username}</h3>
|
||||
<small class="opacity-50">UUID: {player.id}</small>
|
||||
<div>
|
||||
Did you know that bats are the only mammal that can fly?
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
51
islands/statusManager.tsx
Normal file
51
islands/statusManager.tsx
Normal file
@ -0,0 +1,51 @@
|
||||
import { JSX } from "preact";
|
||||
import { Button } from "../components/Button.tsx";
|
||||
import { ManageAction } from "../routes/api/manage.ts";
|
||||
import { useEffect, useState } from "preact/hooks";
|
||||
import { IS_BROWSER } from "$fresh/runtime.ts";
|
||||
|
||||
export function StatusManager(
|
||||
props: JSX.HTMLAttributes<HTMLDivElement> & {
|
||||
onAction?: (res: string) => void;
|
||||
},
|
||||
) {
|
||||
const sendCommand = async (action: ManageAction) => {
|
||||
console.log(action);
|
||||
const res = await fetch("/api/manage", {
|
||||
method: "POST",
|
||||
body: action,
|
||||
});
|
||||
const body = await res.text();
|
||||
props.onAction && props.onAction(body);
|
||||
};
|
||||
|
||||
const [status, setStatus] = useState('');
|
||||
|
||||
const getStatus = async () => {
|
||||
const res = await fetch('/api/manage');
|
||||
const body = await res.text();
|
||||
|
||||
setStatus(body);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (IS_BROWSER) getStatus();
|
||||
},[])
|
||||
|
||||
return (
|
||||
<div {...props}>
|
||||
{!!status && <small>Server is {status}</small>}
|
||||
<div class="flex gap-4">
|
||||
<Button color="wasabi" disabled={!status || status === 'running'} onClick={() => sendCommand(ManageAction.start)}>
|
||||
Start
|
||||
</Button>
|
||||
<Button color="fire" disabled={!status || status === 'stopped'} onClick={() => sendCommand(ManageAction.stop)}>
|
||||
Stop
|
||||
</Button>
|
||||
<Button color="sky" disabled={!status} onClick={() => sendCommand(ManageAction.restart)}>
|
||||
Restart
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,19 +1,40 @@
|
||||
import { IS_BROWSER } from "$fresh/runtime.ts";
|
||||
import { useEffect, useRef, useState } from "preact/hooks";
|
||||
import { JSX } from "preact/jsx-runtime";
|
||||
import { Sockpuppet } from "puppet/client";
|
||||
|
||||
|
||||
export function Terminal(props: { channelId: string }) {
|
||||
const puppet = useRef(new Sockpuppet("ws://sockpuppet.cyborggrizzly.com"));
|
||||
const [lines, setLines] = useState<string[]>([]);
|
||||
const divRef = useRef<HTMLDivElement>(null);
|
||||
const storeKey = 'commandHistory';
|
||||
const [commandHistory, setCommandHistory] = useState<string[]>(JSON.parse(localStorage.getItem(storeKey) || '[]'));
|
||||
const [historyIndex, setHistoryIndex] = useState(commandHistory.length);
|
||||
|
||||
const [command, setCommand] = useState("");
|
||||
|
||||
const changeHistoryIndex = (by: number) => setHistoryIndex(i => i + by);
|
||||
|
||||
useEffect(() => {
|
||||
if (!IS_BROWSER) return;
|
||||
puppet.current.joinChannel(props.channelId, (line) => {
|
||||
setLines((l) => [...l, line]);
|
||||
});
|
||||
|
||||
document.addEventListener('keyup', (e) => {
|
||||
console.log(e.key)
|
||||
switch (e.key) {
|
||||
case 'Up':
|
||||
case 'ArrowUp':
|
||||
changeHistoryIndex(-1);
|
||||
break;
|
||||
case 'Down':
|
||||
case 'ArrowDown':
|
||||
changeHistoryIndex(1);
|
||||
break;
|
||||
}
|
||||
})
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
@ -24,22 +45,38 @@ export function Terminal(props: { channelId: string }) {
|
||||
|
||||
const sendCommand = (e: Event) => {
|
||||
e.preventDefault();
|
||||
puppet.current.getChannel(props.channelId)?.send(command);
|
||||
puppet.current.getChannel(props.channelId)?.send(historyIndex === commandHistory.length ? command : commandHistory[historyIndex]);
|
||||
setCommandHistory(c => [...c, command]);
|
||||
setHistoryIndex(commandHistory.length + 1)
|
||||
setCommand("");
|
||||
};
|
||||
|
||||
const handleCommandUpdate = (e: JSX.TargetedEvent<HTMLInputElement, Event>) => {
|
||||
const value = e.currentTarget.value;
|
||||
|
||||
if (historyIndex !== commandHistory.length) {
|
||||
setHistoryIndex(commandHistory.length);
|
||||
}
|
||||
|
||||
setCommand(value || '')
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.setItem(storeKey, JSON.stringify(commandHistory.slice(0,100)));
|
||||
}, [commandHistory])
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div ref={divRef} class="flex flex-col h-[600px] w-full overflow-auto">
|
||||
{lines.map((l) => <div class="font-mono w-full">{l}</div>)}
|
||||
{lines.map((l) => <pre class="font-mono w-full">{l}</pre>)}
|
||||
</div>
|
||||
<div>
|
||||
<div class="mt-4">
|
||||
<form onSubmit={sendCommand}>
|
||||
<input
|
||||
type="text"
|
||||
class="w-full bg-smoke-600 text-white"
|
||||
value={command}
|
||||
onInput={(e) => setCommand((e.target as HTMLInputElement).value)}
|
||||
value={commandHistory[historyIndex] || command}
|
||||
onInput={handleCommandUpdate}
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
|
31
routes/api/manage.ts
Normal file
31
routes/api/manage.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import { Handlers } from "$fresh/server.ts";
|
||||
import { SERVER_STATE } from "../../state/serverState.ts";
|
||||
|
||||
export enum ManageAction {
|
||||
start = "start",
|
||||
stop = "stop",
|
||||
restart = "restart",
|
||||
}
|
||||
|
||||
export const handler: Handlers = {
|
||||
async POST(req) {
|
||||
const body: ManageAction & string = await req.text() as ManageAction;
|
||||
switch (body) {
|
||||
case ManageAction.start:
|
||||
SERVER_STATE.startMCServer();
|
||||
return new Response("started");
|
||||
case ManageAction.stop:
|
||||
SERVER_STATE.gracefullyStopMCServer();
|
||||
return new Response("stopped");
|
||||
case ManageAction.restart:
|
||||
SERVER_STATE.restartMCServer();
|
||||
return new Response("restarted");
|
||||
default:
|
||||
SERVER_STATE.sendStdIn(body);
|
||||
return new Response("action done");
|
||||
}
|
||||
},
|
||||
GET() {
|
||||
return new Response(SERVER_STATE.status);
|
||||
}
|
||||
};
|
44
routes/api/players.ts
Normal file
44
routes/api/players.ts
Normal file
@ -0,0 +1,44 @@
|
||||
import { Handlers } from "$fresh/server.ts";
|
||||
import { SERVER_STATE } from "../../state/serverState.ts";
|
||||
import { getActivePlayers } from "../../util/players.ts";
|
||||
|
||||
export const handler: Handlers = {
|
||||
GET(_req, _ctx) {
|
||||
let listener: (e: CustomEvent) => void;
|
||||
const body = new ReadableStream({
|
||||
async start(controller){
|
||||
console.log('did the thing')
|
||||
if (SERVER_STATE.status !== 'running') return;
|
||||
const players = await getActivePlayers();
|
||||
|
||||
const event = `event:players\ndata:${JSON.stringify(players)}\n\n`
|
||||
|
||||
controller.enqueue(event);
|
||||
console.log('sent the thing')
|
||||
|
||||
listener = async (e: CustomEvent<string>) => {
|
||||
console.log('message received')
|
||||
if (e.detail.includes('joined the game') || e.detail.includes('lost connection')) {
|
||||
console.log('connection change')
|
||||
const players = await getActivePlayers();
|
||||
const event = `event: players\ndata: ${JSON.stringify(players)}\n\n`
|
||||
|
||||
controller.enqueue(event);
|
||||
}
|
||||
}
|
||||
|
||||
globalThis.addEventListener('stdoutmsg' as any, listener);
|
||||
console.log('listened the thing')
|
||||
},
|
||||
cancel() {
|
||||
console.log('cancelled')
|
||||
globalThis.removeEventListener('stdoutmsg' as any, listener);
|
||||
}
|
||||
})
|
||||
|
||||
return new Response(body.pipeThrough(new TextEncoderStream()), { headers: {
|
||||
"Content-Type": "text/event-stream",
|
||||
"cache-control": "no-cache"
|
||||
}});
|
||||
}
|
||||
}
|
@ -1 +0,0 @@
|
||||
// const
|
12
routes/players.tsx
Normal file
12
routes/players.tsx
Normal file
@ -0,0 +1,12 @@
|
||||
import { Content } from "../components/Content.tsx";
|
||||
import { ActivePlayerList } from "../islands/players.tsx";
|
||||
|
||||
export default function PlayerManger() {
|
||||
return (
|
||||
<div className="container p-8">
|
||||
<Content>
|
||||
<ActivePlayerList />
|
||||
</Content>
|
||||
</div>
|
||||
)
|
||||
}
|
32
routes/properties.tsx
Normal file
32
routes/properties.tsx
Normal file
@ -0,0 +1,32 @@
|
||||
import { Handlers } from "$fresh/server.ts";
|
||||
import { MCProperties } from "../components/properties.tsx";
|
||||
import {
|
||||
deserializeMCProperties,
|
||||
serializeMCProperties,
|
||||
} from "../util/mcProperties.ts";
|
||||
|
||||
export const handler: Handlers = {
|
||||
async POST(req, ctx) {
|
||||
const filePath = `./server/server.properties`;
|
||||
const formData = await req.formData();
|
||||
const text = await Deno.readTextFile(filePath);
|
||||
const properties = deserializeMCProperties(text);
|
||||
|
||||
for (const [key, value] of formData.entries()) {
|
||||
properties.set(key, value as string);
|
||||
}
|
||||
|
||||
const serialized = serializeMCProperties(properties);
|
||||
|
||||
await Deno.writeTextFile(filePath, serialized);
|
||||
|
||||
return ctx.render();
|
||||
},
|
||||
};
|
||||
|
||||
export default async function Properties() {
|
||||
const text = await Deno.readTextFile(`./server/server.properties`);
|
||||
const properties = await deserializeMCProperties(text);
|
||||
|
||||
return <MCProperties properties={properties} />;
|
||||
}
|
@ -4,7 +4,7 @@ import { Content } from "../../components/Content.tsx";
|
||||
import { SERVER_STATE } from "../../state/serverState.ts";
|
||||
|
||||
export const handler: Handlers = {
|
||||
async POST(req, _ctx) {
|
||||
POST(req, _ctx) {
|
||||
const url = new URL(req.url);
|
||||
SERVER_STATE.acceptEULA();
|
||||
return Response.redirect(url.origin + '/setup/start');
|
||||
|
@ -1,27 +0,0 @@
|
||||
import { Handlers } from "$fresh/server.ts";
|
||||
import { Content } from "../../components/Content.tsx";
|
||||
import { Terminal } from "../../islands/terminal.tsx";
|
||||
import { SERVER_STATE } from "../../state/serverState.ts";
|
||||
|
||||
export const handler: Handlers = {
|
||||
async GET(req,ctx) {
|
||||
if (!SERVER_STATE.eulaAccepted) {
|
||||
const url = new URL(req.url);
|
||||
return Response.redirect(`${url.origin}/setup/eula`)
|
||||
}
|
||||
|
||||
const res = await ctx.render();
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
export default function StartFabric() {
|
||||
SERVER_STATE.startMCServer();
|
||||
return (
|
||||
<div class="container p-8">
|
||||
<Content>
|
||||
<Terminal channelId={SERVER_STATE.channelId} />
|
||||
</Content>
|
||||
</div>
|
||||
);
|
||||
}
|
18
routes/terminal.tsx
Normal file
18
routes/terminal.tsx
Normal file
@ -0,0 +1,18 @@
|
||||
import { Content } from "../components/Content.tsx";
|
||||
import { StatusManager } from "../islands/statusManager.tsx";
|
||||
import { Terminal } from "../islands/terminal.tsx";
|
||||
import { SERVER_STATE } from "../state/serverState.ts";
|
||||
|
||||
export default function TerminalPage() {
|
||||
const chId = SERVER_STATE.channelId;
|
||||
return (
|
||||
<div class="container p-8">
|
||||
<Content>
|
||||
<div>
|
||||
<StatusManager class="ml-auto" />
|
||||
</div>
|
||||
<Terminal channelId={chId} />
|
||||
</Content>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,49 +1,67 @@
|
||||
import { Tail } from "npm:tail";
|
||||
import { Sockpuppet } from "puppet/client";
|
||||
import { acceptEULA, checkEULA } from "../util/EULA.ts";
|
||||
import { Loader } from "../types/mcgrizzconf.ts";
|
||||
import { updateConfFile } from "../util/confFile.ts";
|
||||
import { IS_BROWSER } from "$fresh/runtime.ts";
|
||||
|
||||
type MCServerEvent = 'message';
|
||||
type MCServerEventCallback = (msg: string) => void;
|
||||
|
||||
class ServerState {
|
||||
private status: "running" | "stopped" = "stopped";
|
||||
private _status: "running" | "stopped" = "stopped";
|
||||
public get status() {
|
||||
return this._status;
|
||||
}
|
||||
|
||||
private command!: Deno.Command;
|
||||
private process!: Deno.ChildProcess;
|
||||
|
||||
private tail: Tail;
|
||||
|
||||
private _eulaAccepted: boolean;
|
||||
|
||||
private sockpuppet!: Sockpuppet;
|
||||
private _channelId = "blanaba";
|
||||
public get channelId () {
|
||||
public get channelId() {
|
||||
return this._channelId;
|
||||
}
|
||||
public get eulaAccepted() {
|
||||
return this._eulaAccepted;
|
||||
}
|
||||
private stdin!: WritableStreamDefaultWriter;
|
||||
|
||||
private _serverType: Loader = 'unset';
|
||||
public get serverType(): Loader {
|
||||
return this.serverType;
|
||||
}
|
||||
public set serverType(loader: Loader) {
|
||||
updateConfFile({loader});
|
||||
this._serverType = loader;
|
||||
}
|
||||
|
||||
constructor() {
|
||||
this._eulaAccepted = checkEULA();
|
||||
this.sockpuppet = new Sockpuppet(
|
||||
"ws://sockpuppet.cyborggrizzly.com",
|
||||
() => {
|
||||
this.sockpuppet.joinChannel(this.channelId, (msg) => {this.sendStdIn(msg)});
|
||||
this.sockpuppet.joinChannel(this.channelId, (msg) => {
|
||||
this.sendStdIn(msg);
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
public get stdin() {
|
||||
return this.process.stdin;
|
||||
}
|
||||
|
||||
public get channel() {
|
||||
return this.sockpuppet.getChannel(this.channelId)
|
||||
return this.sockpuppet.getChannel(this.channelId);
|
||||
}
|
||||
|
||||
public sendStdIn(message: string) {
|
||||
const msg = new TextEncoder().encode(message);
|
||||
this.process.stdin.getWriter().write(msg);
|
||||
public async sendStdIn(message: string) {
|
||||
if (IS_BROWSER || !this.stdin) return;
|
||||
|
||||
const msg = new TextEncoder().encode(message + "\n");
|
||||
|
||||
await this.stdin.write(msg);
|
||||
}
|
||||
|
||||
|
||||
// "instance" should be moved to a private member once multi-instance support is implemented
|
||||
public startMCServer(instance = "server") {
|
||||
this.command = new Deno.Command("java", {
|
||||
@ -60,6 +78,11 @@ class ServerState {
|
||||
|
||||
this.process = this.command.spawn();
|
||||
|
||||
const {readable, writable} = new TransformStream();
|
||||
readable.pipeTo(this.process.stdin);
|
||||
this.stdin = writable.getWriter();
|
||||
|
||||
this._status = "running";
|
||||
this.startStream();
|
||||
}
|
||||
|
||||
@ -68,14 +91,39 @@ class ServerState {
|
||||
const decoder = new TextDecoder();
|
||||
|
||||
while (true) {
|
||||
const {done, value} = await stream.read();
|
||||
const { done, value } = await stream.read();
|
||||
if (value) {
|
||||
this.channel?.send(decoder.decode(value));
|
||||
const line = decoder.decode(value);
|
||||
this.channel?.send(line);
|
||||
const stdoutMsg = new CustomEvent('stdoutmsg', {
|
||||
detail: line
|
||||
})
|
||||
globalThis.dispatchEvent(stdoutMsg);
|
||||
}
|
||||
if (done) break;
|
||||
}
|
||||
}
|
||||
|
||||
public gracefullyStopMCServer() {
|
||||
this._status = "stopped";
|
||||
this.sendStdIn("stop");
|
||||
}
|
||||
|
||||
public forceStopMCServer() {
|
||||
this._status = "stopped";
|
||||
this.process.kill();
|
||||
}
|
||||
|
||||
public async restartMCServer() {
|
||||
if (this.status === "running") {
|
||||
await this.sendStdIn("stop");
|
||||
// while (true) {
|
||||
await this.process.status;
|
||||
// }
|
||||
}
|
||||
this.startMCServer();
|
||||
}
|
||||
|
||||
public acceptEULA() {
|
||||
this._eulaAccepted = true;
|
||||
acceptEULA();
|
||||
|
File diff suppressed because one or more lines are too long
18
util/EULA.ts
18
util/EULA.ts
@ -1,11 +1,13 @@
|
||||
import { IS_BROWSER } from "$fresh/runtime.ts";
|
||||
|
||||
const eulaRegex = /(eula=false)/;
|
||||
export const checkEULA = (instance = 'server') =>
|
||||
!eulaRegex.test(Deno.readTextFileSync(`./${instance}/eula.txt`));
|
||||
export const checkEULA = (instance = "server") =>
|
||||
!IS_BROWSER && !eulaRegex.test(Deno.readTextFileSync(`./${instance}/eula.txt`));
|
||||
// true;
|
||||
|
||||
|
||||
export const acceptEULA = (instance = 'server') => {
|
||||
export const acceptEULA = (instance = "server") => {
|
||||
const eula = Deno.readTextFileSync(`./${instance}/eula.txt`);
|
||||
const mod = eula.replace(eulaRegex, 'eula=true');
|
||||
console.log(mod)
|
||||
Deno.writeTextFileSync(`./${instance}/eula.txt`, mod);
|
||||
}
|
||||
const mod = eula.replace(eulaRegex, "eula=true");
|
||||
console.log(mod);
|
||||
!IS_BROWSER && Deno.writeTextFileSync(`./${instance}/eula.txt`, mod);
|
||||
};
|
||||
|
30
util/confFile.ts
Normal file
30
util/confFile.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import { MCGrizzConf } from "../types/mcgrizzconf.ts";
|
||||
|
||||
const defaultConf: MCGrizzConf = {
|
||||
loader: 'unset',
|
||||
}
|
||||
|
||||
const confPath = 'mcgrizz.json'
|
||||
|
||||
export function makeConfFile(): MCGrizzConf {
|
||||
|
||||
Deno.writeTextFileSync(confPath, JSON.stringify(defaultConf, null, 2), {create: true});
|
||||
|
||||
return defaultConf;
|
||||
}
|
||||
|
||||
export function getConfFile(): MCGrizzConf {
|
||||
const conf = JSON.parse(Deno.readTextFileSync(confPath));
|
||||
|
||||
if (!conf) {
|
||||
return makeConfFile();
|
||||
}
|
||||
|
||||
return conf;
|
||||
}
|
||||
|
||||
export async function updateConfFile(newConf: Partial<MCGrizzConf>) {
|
||||
const conf = {...getConfFile(), newConf};
|
||||
|
||||
await Deno.writeTextFile(confPath, JSON.stringify(conf, null, 2));
|
||||
}
|
1
util/filters.ts
Normal file
1
util/filters.ts
Normal file
@ -0,0 +1 @@
|
||||
export const filterTruthy = (a: unknown) => !!a
|
@ -1,6 +1,6 @@
|
||||
import { MCGrizzConf } from "../types/mcgrizzconf.ts";
|
||||
import { NavItem } from "../types/nav.ts";
|
||||
import { makeConfFile } from "./makeConfFile.ts";
|
||||
import { makeConfFile } from "./confFile.ts";
|
||||
|
||||
/**
|
||||
* @description Determines the state of setup and returns the nav items for that state
|
||||
@ -8,7 +8,7 @@ import { makeConfFile } from "./makeConfFile.ts";
|
||||
export function getNavItems(): NavItem[] {
|
||||
let conf: MCGrizzConf;
|
||||
try {
|
||||
conf = JSON.parse(Deno.readTextFileSync('mcgrizz.json'));
|
||||
conf = JSON.parse(Deno.readTextFileSync("mcgrizz.json"));
|
||||
} catch {
|
||||
conf = makeConfFile();
|
||||
}
|
||||
@ -16,12 +16,25 @@ export function getNavItems(): NavItem[] {
|
||||
switch (conf.loader) {
|
||||
case "unset":
|
||||
return [{
|
||||
title: 'Setup',
|
||||
href: '/',
|
||||
}]
|
||||
title: "Setup",
|
||||
href: "/",
|
||||
}];
|
||||
case "forge":
|
||||
case "fabric":
|
||||
case "vanilla":
|
||||
return [];
|
||||
return [
|
||||
{
|
||||
title: "Server Terminal",
|
||||
href: "/terminal",
|
||||
},
|
||||
{
|
||||
title: "Players",
|
||||
href: "/players",
|
||||
},
|
||||
{
|
||||
title: "Server Properties",
|
||||
href: "/properties",
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@ -1,11 +0,0 @@
|
||||
import { MCGrizzConf } from "../types/mcgrizzconf.ts";
|
||||
|
||||
export function makeConfFile(): MCGrizzConf {
|
||||
const conf: MCGrizzConf = {
|
||||
loader: 'unset',
|
||||
}
|
||||
|
||||
Deno.writeTextFileSync('mcgrizz.json', JSON.stringify(conf), {create: true});
|
||||
|
||||
return conf;
|
||||
}
|
24
util/mcProperties.ts
Normal file
24
util/mcProperties.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import { filterTruthy } from "./filters.ts";
|
||||
|
||||
export const deserializeMCProperties = (serialized: string) => {
|
||||
const propertiesMap = new Map<string,string>();
|
||||
|
||||
const commentRegex = /#.+\r?\n/g;
|
||||
serialized.replace(commentRegex, '')
|
||||
.split(/\r?\n/)
|
||||
.filter(filterTruthy)
|
||||
.forEach(prop => {
|
||||
const [key, value] = prop.split('=');
|
||||
propertiesMap.set(key, value ?? '');
|
||||
});
|
||||
|
||||
return propertiesMap;
|
||||
}
|
||||
|
||||
export const serializeMCProperties = (deserialized: Map<string,string>) => {
|
||||
let text = '';
|
||||
for (const [key,value] of deserialized.entries()) {
|
||||
text += `${key}=${value}\n`;
|
||||
}
|
||||
return text;
|
||||
}
|
46
util/players.ts
Normal file
46
util/players.ts
Normal file
@ -0,0 +1,46 @@
|
||||
import { SERVER_STATE } from "../state/serverState.ts";
|
||||
import { filterTruthy } from "./filters.ts";
|
||||
|
||||
const playerListRegex= /There are [0-9] of a max of [0-9]+ players online:/
|
||||
|
||||
export const getActivePlayers = (): Promise<PlayerData[]> => new Promise(res => {
|
||||
const listener = async (e: CustomEvent<string>) => {
|
||||
if (playerListRegex.test(e.detail)) {
|
||||
const players: PlayerData[] = []
|
||||
const [_, playerString] = e.detail.split(playerListRegex);
|
||||
for (const playerName of playerString.split(', ')) {
|
||||
players.push(await getPlayerData(playerName));
|
||||
}
|
||||
res(players.filter(filterTruthy));
|
||||
globalThis.removeEventListener('stdoutmsg' as any, listener);
|
||||
}
|
||||
}
|
||||
globalThis.addEventListener('stdoutmsg' as any, listener);
|
||||
SERVER_STATE.sendStdIn('list');
|
||||
})
|
||||
|
||||
export type PlayerData = {
|
||||
username: string;
|
||||
id: string;
|
||||
avatar: string;
|
||||
}
|
||||
|
||||
const getPlayerData = async (username: string) => {
|
||||
username = username.trim();
|
||||
if (!username) return;
|
||||
const cacheFile = 'players.cache.json'
|
||||
await Deno.create(cacheFile);
|
||||
const cache = JSON.parse(await Deno.readTextFile(cacheFile) || '{}');
|
||||
if (!cache[username]) {
|
||||
const req = await fetch('https://playerdb.co/api/player/minecraft/' + username, {
|
||||
headers: {
|
||||
"User-Agent": "MCGRIZZ/0.1 emma@cyborggrizzly.com"
|
||||
}
|
||||
});
|
||||
const json = await req.json();
|
||||
cache[username] = json.data.player;
|
||||
await Deno.writeTextFile(cacheFile, JSON.stringify(cache, null, 2), {create: true});
|
||||
}
|
||||
|
||||
return cache[username]
|
||||
}
|
@ -1,17 +0,0 @@
|
||||
import { startTail } from "./startTail.ts";
|
||||
|
||||
export async function startMCServer(instance = "server") {
|
||||
const start = new Deno.Command("java", {
|
||||
args: [
|
||||
"-Xmx2G",
|
||||
"-jar",
|
||||
"./server.jar",
|
||||
"nogui",
|
||||
],
|
||||
cwd: "./" + instance,
|
||||
});
|
||||
|
||||
start.output();
|
||||
|
||||
return await startTail(instance);
|
||||
}
|
@ -1,25 +0,0 @@
|
||||
import { Sockpuppet } from "puppet/client";
|
||||
import { Tail } from "npm:tail";
|
||||
|
||||
export function startTail(instance = "server") {
|
||||
return new Promise<[Tail, string]>((resolve) => {
|
||||
const puppet = new Sockpuppet("ws://sockpuppet.cyborggrizzly.com", () => {
|
||||
const channelId = crypto.randomUUID();
|
||||
puppet.createChannel(channelId);
|
||||
puppet.joinChannel(channelId, (line) => {
|
||||
console.log(line);
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
const path = `./${instance}/logs/latest.log`;
|
||||
const tail = new Tail(path, { follow: true });
|
||||
|
||||
tail.on("line", (line: string) => {
|
||||
puppet.getChannel(channelId)?.send(line);
|
||||
console.log(line);
|
||||
});
|
||||
resolve([tail, channelId]);
|
||||
}, 1000);
|
||||
});
|
||||
});
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user