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/
|
||||
forge/
|
||||
vanilla/
|
||||
server/
|
||||
|
||||
# Config file for MCGRIZZ
|
||||
mcgrizz.json
|
@ -42,7 +42,7 @@ export function Button(
|
||||
return (
|
||||
<button
|
||||
{...props}
|
||||
disabled={(!IS_BROWSER && !props.href) || props.disabled}
|
||||
disabled={(!IS_BROWSER && !props.href && props.type !== 'submit') || props.disabled}
|
||||
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/signals": "https://esm.sh/*@preact/signals@1.1.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": {
|
||||
"jsx": "react-jsx",
|
||||
|
26
fresh.gen.ts
26
fresh.gen.ts
@ -4,21 +4,31 @@
|
||||
|
||||
import * as $0 from "./routes/_404.tsx";
|
||||
import * as $1 from "./routes/_app.tsx";
|
||||
import * as $2 from "./routes/api/joke.ts";
|
||||
import * as $3 from "./routes/index.tsx";
|
||||
import * as $4 from "./routes/setup/index.tsx";
|
||||
import * as $$0 from "./islands/Counter.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 $$0 from "./islands/fabricVersions.tsx";
|
||||
import * as $$1 from "./islands/terminal.tsx";
|
||||
|
||||
const manifest = {
|
||||
routes: {
|
||||
"./routes/_404.tsx": $0,
|
||||
"./routes/_app.tsx": $1,
|
||||
"./routes/api/joke.ts": $2,
|
||||
"./routes/index.tsx": $3,
|
||||
"./routes/setup/index.tsx": $4,
|
||||
"./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,
|
||||
},
|
||||
islands: {
|
||||
"./islands/Counter.tsx": $$0,
|
||||
"./islands/fabricVersions.tsx": $$0,
|
||||
"./islands/terminal.tsx": $$1,
|
||||
},
|
||||
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="max-w-screen-md mx-auto flex flex-col items-center justify-center">
|
||||
<img
|
||||
class="my-6"
|
||||
class="my-6 rounded-3xl"
|
||||
src="/bearcam.gif"
|
||||
alt="a gif of a bear playing with the camera"
|
||||
/>
|
||||
|
@ -7,7 +7,7 @@ export default function App({ Component }: AppProps) {
|
||||
<head>
|
||||
<meta charSet="utf-8" />
|
||||
<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>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link
|
||||
@ -24,9 +24,9 @@ export default function App({ Component }: AppProps) {
|
||||
</head>
|
||||
<body>
|
||||
<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">
|
||||
<img src="cyborggrizzly.svg" alt="" height={100} width={100} />
|
||||
<img src="/cyborggrizzly.svg" alt="" height={100} width={100} />
|
||||
<div>
|
||||
<h1 class="text-4xl font-pixel">MC Grizz</h1>
|
||||
<hr class="color-sky" />
|
||||
@ -36,11 +36,11 @@ export default function App({ Component }: AppProps) {
|
||||
|
||||
<Nav />
|
||||
|
||||
<small class="mt-auto text-center">
|
||||
Made with love by Emma@Cyborggrizzly 🏳️⚧️❤️
|
||||
</small>
|
||||
<div class="w-full text-center absolute bottom-4 left-0 whitespace-nowrap">
|
||||
<small>Made with love by Emma@Cyborggrizzly 🏳️⚧️❤️</small>
|
||||
</div>
|
||||
<div class="max-h-full flex-1">
|
||||
</div>
|
||||
<div class="max-h-full flex-1 overflow-auto">
|
||||
<Component />
|
||||
</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">
|
||||
<a href="/setup/vanilla">
|
||||
<Button>Vanilla</Button>
|
||||
<Button href="/setup/vanilla">Vanilla</Button>
|
||||
</a>
|
||||
<a href="/setup/fabric">
|
||||
<Button>Fabric</Button>
|
||||
<Button href="/setup/fabric">Fabric</Button>
|
||||
</a>
|
||||
<a href="/setup/forge">
|
||||
<Button>Forge</Button>
|
||||
<Button href="/setup/forge">Forge</Button>
|
||||
</a>
|
||||
</div>
|
||||
</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;
|
||||
}
|
||||
|
||||
@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