server state and stdio streaming
This commit is contained in:
parent
dc4c8efeb2
commit
4a4563ba85
1
.gitignore
vendored
1
.gitignore
vendored
@ -13,6 +13,7 @@ _fresh/
|
|||||||
fabric/
|
fabric/
|
||||||
forge/
|
forge/
|
||||||
vanilla/
|
vanilla/
|
||||||
|
server/
|
||||||
|
|
||||||
# Config file for MCGRIZZ
|
# Config file for MCGRIZZ
|
||||||
mcgrizz.json
|
mcgrizz.json
|
@ -42,7 +42,7 @@ export function Button(
|
|||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
{...props}
|
{...props}
|
||||||
disabled={(!IS_BROWSER && !props.href) || props.disabled}
|
disabled={(!IS_BROWSER && !props.href && props.type !== 'submit') || props.disabled}
|
||||||
class={classes}
|
class={classes}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
10
components/Loader.tsx
Normal file
10
components/Loader.tsx
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
export function Loader() {
|
||||||
|
return (
|
||||||
|
<div class="grid grid-cols-2 grid-rows-2 w-20 h-20 loader m-auto">
|
||||||
|
<div class="loader-ball w-5 h-5 rounded-full"></div>
|
||||||
|
<div class="loader-ball w-5 h-5 rounded-full" style={{'--loader-delay': '-1s'}}></div>
|
||||||
|
<div class="loader-ball w-5 h-5 rounded-full" style={{'--loader-delay': '-1.7s'}}></div>
|
||||||
|
<div class="loader-ball w-5 h-5 rounded-full" style={{'--loader-delay': '-2.6s'}}></div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
@ -30,7 +30,9 @@
|
|||||||
"preact-render-to-string": "https://esm.sh/*preact-render-to-string@6.2.1",
|
"preact-render-to-string": "https://esm.sh/*preact-render-to-string@6.2.1",
|
||||||
"@preact/signals": "https://esm.sh/*@preact/signals@1.1.3",
|
"@preact/signals": "https://esm.sh/*@preact/signals@1.1.3",
|
||||||
"@preact/signals-core": "https://esm.sh/*@preact/signals-core@1.2.3",
|
"@preact/signals-core": "https://esm.sh/*@preact/signals-core@1.2.3",
|
||||||
"$std/": "https://deno.land/std@0.193.0/"
|
"$std/": "https://deno.land/std@0.193.0/",
|
||||||
|
"puppet": "https://deno.land/x/sockpuppet@0.6.0/mod.ts",
|
||||||
|
"puppet/client": "https://deno.land/x/sockpuppet@0.6.0/client/mod.ts"
|
||||||
},
|
},
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"jsx": "react-jsx",
|
"jsx": "react-jsx",
|
||||||
|
26
fresh.gen.ts
26
fresh.gen.ts
@ -4,21 +4,31 @@
|
|||||||
|
|
||||||
import * as $0 from "./routes/_404.tsx";
|
import * as $0 from "./routes/_404.tsx";
|
||||||
import * as $1 from "./routes/_app.tsx";
|
import * as $1 from "./routes/_app.tsx";
|
||||||
import * as $2 from "./routes/api/joke.ts";
|
import * as $2 from "./routes/api/fabric/index.ts";
|
||||||
import * as $3 from "./routes/index.tsx";
|
import * as $3 from "./routes/api/terminal.ts";
|
||||||
import * as $4 from "./routes/setup/index.tsx";
|
import * as $4 from "./routes/index.tsx";
|
||||||
import * as $$0 from "./islands/Counter.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 $$0 from "./islands/fabricVersions.tsx";
|
||||||
|
import * as $$1 from "./islands/terminal.tsx";
|
||||||
|
|
||||||
const manifest = {
|
const manifest = {
|
||||||
routes: {
|
routes: {
|
||||||
"./routes/_404.tsx": $0,
|
"./routes/_404.tsx": $0,
|
||||||
"./routes/_app.tsx": $1,
|
"./routes/_app.tsx": $1,
|
||||||
"./routes/api/joke.ts": $2,
|
"./routes/api/fabric/index.ts": $2,
|
||||||
"./routes/index.tsx": $3,
|
"./routes/api/terminal.ts": $3,
|
||||||
"./routes/setup/index.tsx": $4,
|
"./routes/index.tsx": $4,
|
||||||
|
"./routes/setup/eula.tsx": $5,
|
||||||
|
"./routes/setup/fabric.tsx": $6,
|
||||||
|
"./routes/setup/index.tsx": $7,
|
||||||
|
"./routes/setup/start.tsx": $8,
|
||||||
},
|
},
|
||||||
islands: {
|
islands: {
|
||||||
"./islands/Counter.tsx": $$0,
|
"./islands/fabricVersions.tsx": $$0,
|
||||||
|
"./islands/terminal.tsx": $$1,
|
||||||
},
|
},
|
||||||
baseUrl: import.meta.url,
|
baseUrl: import.meta.url,
|
||||||
};
|
};
|
||||||
|
@ -1,16 +0,0 @@
|
|||||||
import type { Signal } from "@preact/signals";
|
|
||||||
import { Button } from "../components/Button.tsx";
|
|
||||||
|
|
||||||
interface CounterProps {
|
|
||||||
count: Signal<number>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Counter(props: CounterProps) {
|
|
||||||
return (
|
|
||||||
<div class="flex gap-8 py-6">
|
|
||||||
<Button color="fire" onClick={() => props.count.value -= 1}>-1</Button>
|
|
||||||
<p class="text-3xl">{props.count}</p>
|
|
||||||
<Button color="sky" onClick={() => props.count.value += 1}>+1</Button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
79
islands/fabricVersions.tsx
Normal file
79
islands/fabricVersions.tsx
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
import { useEffect, useState } from "preact/hooks";
|
||||||
|
import { FabricGame, FabricInstaller, FabricLoader } from "../types/fabric.ts";
|
||||||
|
import { Loader } from "../components/Loader.tsx";
|
||||||
|
import { Button } from "../components/Button.tsx";
|
||||||
|
|
||||||
|
export function FabricVersions() {
|
||||||
|
const [game, setGame] = useState<FabricGame[]>([]);
|
||||||
|
const [installer, setInstaller] = useState<FabricInstaller[]>([]);
|
||||||
|
const [loader, setLoader] = useState<FabricLoader[]>([]);
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
|
||||||
|
const [includeSnapshots, setIncludeSnapshots] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let poll: number;
|
||||||
|
if (isLoading) {
|
||||||
|
poll = setInterval(async () => {
|
||||||
|
const res = await fetch("/api/fabric");
|
||||||
|
if (res.status !== 200) return;
|
||||||
|
const json: {
|
||||||
|
gameVersions: FabricGame[];
|
||||||
|
installerVersions: FabricInstaller[];
|
||||||
|
loaderVersions: FabricLoader[];
|
||||||
|
} = await res.json();
|
||||||
|
setGame(json.gameVersions);
|
||||||
|
setInstaller(json.installerVersions);
|
||||||
|
setLoader(json.loaderVersions);
|
||||||
|
setIsLoading(false);
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => clearInterval(poll);
|
||||||
|
}, [isLoading]);
|
||||||
|
|
||||||
|
return isLoading ? <Loader /> : (
|
||||||
|
<form action="/api/fabric" method="POST">
|
||||||
|
<div class="grid grid-cols-3 gap-8">
|
||||||
|
<div>
|
||||||
|
<label class="block" htmlFor="game">Game Version</label>
|
||||||
|
<select name="game">
|
||||||
|
{game.filter((g) => includeSnapshots || g.stable).map((g) => (
|
||||||
|
<option>{g.version}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
<br />
|
||||||
|
<label>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={includeSnapshots}
|
||||||
|
onChange={() => setIncludeSnapshots(!includeSnapshots)}
|
||||||
|
/>{" "}
|
||||||
|
Include Snapshots?
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block" htmlFor="loader">Loader Version</label>
|
||||||
|
<select name="loader">
|
||||||
|
{loader.map((g) => <option>{g.version}</option>)}
|
||||||
|
</select>
|
||||||
|
<small class="block">
|
||||||
|
You probably don't need to change this one.
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block" htmlFor="installer">Installer Version</label>
|
||||||
|
<select name="installer">
|
||||||
|
{installer.map((g) => <option>{g.version}</option>)}
|
||||||
|
</select>
|
||||||
|
<small class="block">
|
||||||
|
You probably don't need to change this one.
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="w-full">
|
||||||
|
<Button class="ml-auto text-right text-lg font-pixel block mt-8" type="submit">Let's Go!</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
48
islands/terminal.tsx
Normal file
48
islands/terminal.tsx
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import { IS_BROWSER } from "$fresh/runtime.ts";
|
||||||
|
import { useEffect, useRef, useState } from "preact/hooks";
|
||||||
|
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 [command, setCommand] = useState("");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!IS_BROWSER) return;
|
||||||
|
puppet.current.joinChannel(props.channelId, (line) => {
|
||||||
|
setLines((l) => [...l, line]);
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (divRef.current) {
|
||||||
|
divRef.current.scrollTop = divRef.current.scrollHeight;
|
||||||
|
}
|
||||||
|
}, [lines]);
|
||||||
|
|
||||||
|
const sendCommand = (e: Event) => {
|
||||||
|
e.preventDefault();
|
||||||
|
puppet.current.getChannel(props.channelId)?.send(command);
|
||||||
|
setCommand("");
|
||||||
|
};
|
||||||
|
|
||||||
|
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>)}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<form onSubmit={sendCommand}>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="w-full bg-smoke-600 text-white"
|
||||||
|
value={command}
|
||||||
|
onInput={(e) => setCommand((e.target as HTMLInputElement).value)}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -9,7 +9,7 @@ export default function Error404() {
|
|||||||
<div class="container w-full">
|
<div class="container w-full">
|
||||||
<div class="max-w-screen-md mx-auto flex flex-col items-center justify-center">
|
<div class="max-w-screen-md mx-auto flex flex-col items-center justify-center">
|
||||||
<img
|
<img
|
||||||
class="my-6"
|
class="my-6 rounded-3xl"
|
||||||
src="/bearcam.gif"
|
src="/bearcam.gif"
|
||||||
alt="a gif of a bear playing with the camera"
|
alt="a gif of a bear playing with the camera"
|
||||||
/>
|
/>
|
||||||
|
@ -7,7 +7,7 @@ export default function App({ Component }: AppProps) {
|
|||||||
<head>
|
<head>
|
||||||
<meta charSet="utf-8" />
|
<meta charSet="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<link rel="shortcut icon" href="cyborggrizzly.svg" type="image/svg" />
|
<link rel="shortcut icon" href="/cyborggrizzly.svg" type="image/svg" />
|
||||||
<title>MC GRIZZ</title>
|
<title>MC GRIZZ</title>
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
<link
|
<link
|
||||||
@ -24,9 +24,9 @@ export default function App({ Component }: AppProps) {
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="flex h-[100vh]">
|
<div class="flex h-[100vh]">
|
||||||
<div class="h-full min-w-[400px] bg-licorice overflow-y-auto border-r-2 p-4 dark:border-sky-950 border-sky text-white flex flex-col">
|
<div class="relative h-full min-w-[400px] bg-licorice overflow-y-auto border-r-2 p-4 dark:border-sky-950 border-sky text-white flex flex-col">
|
||||||
<div class="flex items-center justify-center p-4 gap-4 mb-8 rounded-3xl bg-smoke-900 border border-sky-950">
|
<div class="flex items-center justify-center p-4 gap-4 mb-8 rounded-3xl bg-smoke-900 border border-sky-950">
|
||||||
<img src="cyborggrizzly.svg" alt="" height={100} width={100} />
|
<img src="/cyborggrizzly.svg" alt="" height={100} width={100} />
|
||||||
<div>
|
<div>
|
||||||
<h1 class="text-4xl font-pixel">MC Grizz</h1>
|
<h1 class="text-4xl font-pixel">MC Grizz</h1>
|
||||||
<hr class="color-sky" />
|
<hr class="color-sky" />
|
||||||
@ -36,11 +36,11 @@ export default function App({ Component }: AppProps) {
|
|||||||
|
|
||||||
<Nav />
|
<Nav />
|
||||||
|
|
||||||
<small class="mt-auto text-center">
|
<div class="w-full text-center absolute bottom-4 left-0 whitespace-nowrap">
|
||||||
Made with love by Emma@Cyborggrizzly 🏳️⚧️❤️
|
<small>Made with love by Emma@Cyborggrizzly 🏳️⚧️❤️</small>
|
||||||
</small>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="max-h-full flex-1">
|
<div class="max-h-full flex-1 overflow-auto">
|
||||||
<Component />
|
<Component />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,21 +0,0 @@
|
|||||||
import { HandlerContext } from "$fresh/server.ts";
|
|
||||||
|
|
||||||
// Jokes courtesy of https://punsandoneliners.com/randomness/programmer-jokes/
|
|
||||||
const JOKES = [
|
|
||||||
"Why do Java developers often wear glasses? They can't C#.",
|
|
||||||
"A SQL query walks into a bar, goes up to two tables and says “can I join you?”",
|
|
||||||
"Wasn't hard to crack Forrest Gump's password. 1forrest1.",
|
|
||||||
"I love pressing the F5 key. It's refreshing.",
|
|
||||||
"Called IT support and a chap from Australia came to fix my network connection. I asked “Do you come from a LAN down under?”",
|
|
||||||
"There are 10 types of people in the world. Those who understand binary and those who don't.",
|
|
||||||
"Why are assembly programmers often wet? They work below C level.",
|
|
||||||
"My favourite computer based band is the Black IPs.",
|
|
||||||
"What programme do you use to predict the music tastes of former US presidential candidates? An Al Gore Rhythm.",
|
|
||||||
"An SEO expert walked into a bar, pub, inn, tavern, hostelry, public house.",
|
|
||||||
];
|
|
||||||
|
|
||||||
export const handler = (_req: Request, _ctx: HandlerContext): Response => {
|
|
||||||
const randomIndex = Math.floor(Math.random() * JOKES.length);
|
|
||||||
const body = JOKES[randomIndex];
|
|
||||||
return new Response(body);
|
|
||||||
};
|
|
1
routes/api/terminal.ts
Normal file
1
routes/api/terminal.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
// const
|
48
routes/setup/eula.tsx
Normal file
48
routes/setup/eula.tsx
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import { Handlers, PageProps } from "$fresh/server.ts";
|
||||||
|
import { Button } from "../../components/Button.tsx";
|
||||||
|
import { Content } from "../../components/Content.tsx";
|
||||||
|
import { SERVER_STATE } from "../../state/serverState.ts";
|
||||||
|
|
||||||
|
export const handler: Handlers = {
|
||||||
|
async POST(req, _ctx) {
|
||||||
|
const url = new URL(req.url);
|
||||||
|
SERVER_STATE.acceptEULA();
|
||||||
|
return Response.redirect(url.origin + '/setup/start');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default async function EULA({url}: PageProps) {
|
||||||
|
// TODO: this does not respect instances and needs to once they are supported
|
||||||
|
const eulaText = await Deno.readTextFileSync("./server/eula.txt");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class="container p-8">
|
||||||
|
<Content>
|
||||||
|
<h2 class="text-2xl font-pixel">Minecraft EULA</h2>
|
||||||
|
<p>
|
||||||
|
This page is provided as a courtesy. The text below is the contents of
|
||||||
|
"eula.txt," a file that is generated by the server the first time it
|
||||||
|
runs. In order to run the server, you must agree to the EULA by
|
||||||
|
changing "false" to "true."
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
To make this easier, there is a button below. The one and only action
|
||||||
|
this button takes is to change the value in the text file to true,
|
||||||
|
this is not an agreement between you and CyborgGrizzly Games.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
If you are unsure of the contents of the EULA, you are encouraged to
|
||||||
|
read it by following the link in the text file.
|
||||||
|
</p>
|
||||||
|
<pre class="p-2 bg-smoke-500 rounded-lg">{eulaText}</pre>
|
||||||
|
<div class="mt-8 w-full">
|
||||||
|
<form action={url.pathname} method="post">
|
||||||
|
<Button class="ml-auto text-right block" type="submit">
|
||||||
|
Accept EULA
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</Content>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
22
routes/setup/fabric.tsx
Normal file
22
routes/setup/fabric.tsx
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { Content } from "../../components/Content.tsx";
|
||||||
|
import { FabricVersions } from "../../islands/fabricVersions.tsx";
|
||||||
|
import { initFabric } from "../../util/initFabric.ts";
|
||||||
|
|
||||||
|
export default function FabricSetup() {
|
||||||
|
try {
|
||||||
|
const vers = Deno.readDirSync("./fabric/versions");
|
||||||
|
if (Array.from(vers).length !== 3) throw "";
|
||||||
|
} catch {
|
||||||
|
initFabric();
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class="container p-8">
|
||||||
|
<Content>
|
||||||
|
<h2 class="font-pixel text-2xl">Fabric Setup</h2>
|
||||||
|
<p>Select the game version you wish to create a server for. If you know you need a different loader and installer version, you can change it here.</p>
|
||||||
|
<FabricVersions />
|
||||||
|
</Content>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -10,13 +10,13 @@ export default function Setup() {
|
|||||||
|
|
||||||
<div class="m-auto flex gap-4 mt-4">
|
<div class="m-auto flex gap-4 mt-4">
|
||||||
<a href="/setup/vanilla">
|
<a href="/setup/vanilla">
|
||||||
<Button>Vanilla</Button>
|
<Button href="/setup/vanilla">Vanilla</Button>
|
||||||
</a>
|
</a>
|
||||||
<a href="/setup/fabric">
|
<a href="/setup/fabric">
|
||||||
<Button>Fabric</Button>
|
<Button href="/setup/fabric">Fabric</Button>
|
||||||
</a>
|
</a>
|
||||||
<a href="/setup/forge">
|
<a href="/setup/forge">
|
||||||
<Button>Forge</Button>
|
<Button href="/setup/forge">Forge</Button>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</Content>
|
</Content>
|
||||||
|
27
routes/setup/start.tsx
Normal file
27
routes/setup/start.tsx
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
85
state/serverState.ts
Normal file
85
state/serverState.ts
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
import { Tail } from "npm:tail";
|
||||||
|
import { Sockpuppet } from "puppet/client";
|
||||||
|
import { acceptEULA, checkEULA } from "../util/EULA.ts";
|
||||||
|
|
||||||
|
class ServerState {
|
||||||
|
private status: "running" | "stopped" = "stopped";
|
||||||
|
|
||||||
|
private command!: Deno.Command;
|
||||||
|
private process!: Deno.ChildProcess;
|
||||||
|
|
||||||
|
private tail: Tail;
|
||||||
|
|
||||||
|
private _eulaAccepted: boolean;
|
||||||
|
|
||||||
|
private sockpuppet!: Sockpuppet;
|
||||||
|
private _channelId = "blanaba";
|
||||||
|
public get channelId () {
|
||||||
|
return this._channelId;
|
||||||
|
}
|
||||||
|
public get eulaAccepted() {
|
||||||
|
return this._eulaAccepted;
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this._eulaAccepted = checkEULA();
|
||||||
|
this.sockpuppet = new Sockpuppet(
|
||||||
|
"ws://sockpuppet.cyborggrizzly.com",
|
||||||
|
() => {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
public sendStdIn(message: string) {
|
||||||
|
const msg = new TextEncoder().encode(message);
|
||||||
|
this.process.stdin.getWriter().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", {
|
||||||
|
args: [
|
||||||
|
"-Xmx2G",
|
||||||
|
"-jar",
|
||||||
|
"./server.jar",
|
||||||
|
"nogui",
|
||||||
|
],
|
||||||
|
cwd: "./" + instance,
|
||||||
|
stdin: "piped",
|
||||||
|
stdout: "piped",
|
||||||
|
});
|
||||||
|
|
||||||
|
this.process = this.command.spawn();
|
||||||
|
|
||||||
|
this.startStream();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async startStream() {
|
||||||
|
const stream = this.process.stdout.getReader();
|
||||||
|
const decoder = new TextDecoder();
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const {done, value} = await stream.read();
|
||||||
|
if (value) {
|
||||||
|
this.channel?.send(decoder.decode(value));
|
||||||
|
}
|
||||||
|
if (done) break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public acceptEULA() {
|
||||||
|
this._eulaAccepted = true;
|
||||||
|
acceptEULA();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SERVER_STATE = new ServerState();
|
@ -30,3 +30,51 @@
|
|||||||
* {
|
* {
|
||||||
font-family: 'Titillium Web', sans-serif;
|
font-family: 'Titillium Web', sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@layer components {
|
||||||
|
select, input {
|
||||||
|
@apply text-black p-2 rounded-xl
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer utilities {
|
||||||
|
.loader-ball {
|
||||||
|
animation: color-cycle infinite 3s linear;
|
||||||
|
animation-delay: var(--loader-delay);
|
||||||
|
@apply m-auto
|
||||||
|
}
|
||||||
|
|
||||||
|
.loader {
|
||||||
|
animation: spin 5s ease-in-out;
|
||||||
|
animation-iteration-count: infinite;
|
||||||
|
animation-direction: alternate;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
0% {
|
||||||
|
transform: rotate(0deg)
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: rotate(1440deg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes color-cycle {
|
||||||
|
0% {
|
||||||
|
@apply bg-fire;
|
||||||
|
}
|
||||||
|
25% {
|
||||||
|
@apply bg-grape;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
@apply bg-sky;
|
||||||
|
}
|
||||||
|
75% {
|
||||||
|
@apply bg-wasabi;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
@apply bg-fire;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
File diff suppressed because one or more lines are too long
@ -1,21 +0,0 @@
|
|||||||
const fabricHost = 'https://meta.fabricmc.net/v2'
|
|
||||||
|
|
||||||
const gameVersionSlug = '/versions/game'
|
|
||||||
const loaderVersionSlug = '/versions/loader'
|
|
||||||
const installerVersionSlug = '/versions/installer'
|
|
||||||
const versionSlugs = [gameVersionSlug,loaderVersionSlug,installerVersionSlug];
|
|
||||||
|
|
||||||
for (const slug of versionSlugs) {
|
|
||||||
const uri = fabricHost + slug;
|
|
||||||
|
|
||||||
const req = await fetch(uri);
|
|
||||||
const text = await req.text();
|
|
||||||
|
|
||||||
const path = `./fabric/${slug}.json`;
|
|
||||||
|
|
||||||
await Deno.mkdir(path.split('/').slice(0,-1).join('/'), {recursive: true});
|
|
||||||
|
|
||||||
await Deno.writeTextFile(path, text, {create: true});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
19
types/fabric.ts
Normal file
19
types/fabric.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
export type FabricGame = {
|
||||||
|
version: string;
|
||||||
|
stable: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type FabricInstaller = {
|
||||||
|
url: string;
|
||||||
|
maven: string;
|
||||||
|
version: string;
|
||||||
|
stable: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type FabricLoader = {
|
||||||
|
separator: string;
|
||||||
|
build: number;
|
||||||
|
maven: string;
|
||||||
|
version: string;
|
||||||
|
stable: boolean;
|
||||||
|
};
|
11
util/EULA.ts
Normal file
11
util/EULA.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
const eulaRegex = /(eula=false)/;
|
||||||
|
export const checkEULA = (instance = 'server') =>
|
||||||
|
!eulaRegex.test(Deno.readTextFileSync(`./${instance}/eula.txt`));
|
||||||
|
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
27
util/initFabric.ts
Normal file
27
util/initFabric.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
export async function initFabric() {
|
||||||
|
const fabricHost = "https://meta.fabricmc.net/v2";
|
||||||
|
|
||||||
|
const gameVersionSlug = "/versions/game";
|
||||||
|
const loaderVersionSlug = "/versions/loader";
|
||||||
|
const installerVersionSlug = "/versions/installer";
|
||||||
|
const versionSlugs = [
|
||||||
|
gameVersionSlug,
|
||||||
|
loaderVersionSlug,
|
||||||
|
installerVersionSlug,
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const slug of versionSlugs) {
|
||||||
|
const uri = fabricHost + slug;
|
||||||
|
|
||||||
|
const req = await fetch(uri);
|
||||||
|
const text = await req.text();
|
||||||
|
|
||||||
|
const path = `./fabric/${slug}.json`;
|
||||||
|
|
||||||
|
await Deno.mkdir(path.split("/").slice(0, -1).join("/"), {
|
||||||
|
recursive: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
await Deno.writeTextFile(path, text, { create: true });
|
||||||
|
}
|
||||||
|
}
|
17
util/startServer.ts
Normal file
17
util/startServer.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
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);
|
||||||
|
}
|
25
util/startTail.ts
Normal file
25
util/startTail.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
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