Skip to content

Commit

Permalink
First commit.
Browse files Browse the repository at this point in the history
  • Loading branch information
Lapis256 committed Oct 27, 2023
0 parents commit 786a945
Show file tree
Hide file tree
Showing 6 changed files with 280 additions and 0 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
bds.exe
8 changes: 8 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"deno.enable": true,
"deno.lint": true,
"deno.unstable": false,
"deno.config": "./deno.json",
"editor.formatOnSave": true,
"editor.tabSize": 2
}
181 changes: 181 additions & 0 deletions cli.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
import { LogDelimiterStream } from "./src/logDelimiterStream.ts";

function buildCommand(os: string, cwd: string) {
if (os !== "linux" && os !== "windows") {
throw Error(`Unsupported platform: ${os}`);
}

Deno.chdir(cwd);
return new Deno.Command("./bedrock_server", {
env: os === "linux" ? { LD_LIBRARY_PATH: "." } : undefined,
stdin: "piped",
stdout: "piped",
cwd,
});
}

const encoder = new TextEncoder();
function encodeString(string: string) {
return encoder.encode(string);
}

enum Color {
Red = "\u001b[91m",
Yellow = "\u001b[93m",
Reset = "\u001b[0m",
}

const regExpBase =
"\\[[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}:[0-9]{3}";

const regExpError = new RegExp(`${regExpBase} ERROR\\] `);
const regExpWarn = new RegExp(`${regExpBase} WARN\\] `);
const regExpActionMessage = new RegExp(`.*\\[Scripting\\] bds_enhancer:(.*)?`);
const regExpConnected = new RegExp(
`${regExpBase} INFO\\] Player connected: (.*)?, xuid: (.*)?`
);
const regExpInfo = new RegExp(`${regExpBase} INFO\\] `);

interface ActionRequest<T extends string, P> {
action: T;
payload: P;
}

type EmptyPayload = Record<never, never>;

interface TransferPayload {
player: string;
host: string;
port: number;
}

interface KickByIdPayload {
playerId: string;
reason: string;
}

type ReloadAction = ActionRequest<"reload", EmptyPayload>;
type StopAction = ActionRequest<"stop", EmptyPayload>;
type SaveAction = ActionRequest<"save", EmptyPayload>;
type TransferAction = ActionRequest<"transfer", TransferPayload>;
type KickByIdAction = ActionRequest<"kick", KickByIdPayload>;

type Action =
| ReloadAction
| StopAction
| SaveAction
| TransferAction
| KickByIdAction;

function createStringWriter(): [
() => Promise<void>,
(color: string, string: string) => Promise<void>
] {
const stdoutWriter = Deno.stdout.writable.getWriter();

return [
stdoutWriter.close,
(color: string, string: string) =>
stdoutWriter.write(encodeString(color + string + Color.Reset)),
];
}

async function handleStdin(
stdinWriter: WritableStreamDefaultWriter<Uint8Array>
) {
for await (const chunk of Deno.stdin.readable) {
await stdinWriter.write(chunk);
}
}

async function handleStdout(
child: Deno.ChildProcess,
executeCommand: (command: string) => Promise<void>
) {
const [stdoutClose, write] = createStringWriter();

const stringStream = child.stdout
.pipeThrough(new TextDecoderStream())
.pipeThrough(new LogDelimiterStream());

for await (const string of stringStream) {
const includeError = regExpError.test(string);
const includeWarn = regExpWarn.test(string);
const includeInfo = regExpInfo.test(string);

const color = includeError ? Color.Red : includeWarn ? Color.Yellow : "";

if (includeWarn) {
const match = string.match(regExpActionMessage);
if (match) {
const actionData = match[1];
const data = JSON.parse(actionData) as Action;

switch (data.action) {
case "reload":
await executeCommand("reload");
break;
case "stop":
await executeCommand("stop");
break;
case "save":
await executeCommand("save hold");
break;
case "transfer":
await executeCommand(
`transfer ${data.payload.player} ${data.payload.host} ${data.payload.port}`
);
break;
case "kick":
await executeCommand(
`kick ${data.payload.playerId} ${data.payload.reason}`
);
break;
}
continue;
}
}

if (includeInfo) {
if (string.includes("Running AutoCompaction...")) {
continue;
}

const match = string.match(regExpConnected);
if (match) {
const playerName = match[1];
const playerId = match[2];
await executeCommand(
`scriptevent bds_enhancer:joinPlayer ${{ playerName, playerId }}}`
);
}
}

await write(color, string);
}

await child.status;
await stdoutClose();
}

function main() {
const command = buildCommand(Deno.build.os, Deno.args[0] ?? ".");
const child = command.spawn();

const stdinWriter = child.stdin.getWriter();
handleStdin(stdinWriter);
handleStdout(
child,
async (cmd: string) => await stdinWriter.write(encodeString(cmd + "\n"))
);

(async function () {
const { code } = await child.status;
await stdinWriter.close();
Deno.exit(code);
})();
}

if (import.meta.main) {
main();
}
9 changes: 9 additions & 0 deletions deno.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"tasks": {
"run": "deno run -A ./cli.ts",
"compile": "deno compile -A -o bds.exe ./cli.ts"
},
"imports": {
"std/": "https://deno.land/std@0.197.0/"
}
}
34 changes: 34 additions & 0 deletions deno.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

47 changes: 47 additions & 0 deletions src/logDelimiterStream.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { TextDelimiterStream } from "std/streams/mod.ts";

export class LogDelimiterStream extends TransformStream<string, string> {
readable: ReadableStream<string>;
writable: WritableStream<string>;

#buffer = "";

constructor() {
super({});

const textLineDelimiter = new TextDelimiterStream("\n", {
disposition: "suffix",
});
const logDelimiter = new TransformStream<string, string>({
transform: (chunk, controller) => this.#handleLine(chunk, controller),
flush: (controller) => controller.enqueue(this.#buffer),
});

this.writable = textLineDelimiter.writable;
this.readable = logDelimiter.readable;

textLineDelimiter.readable.pipeTo(logDelimiter.writable);
}

#handleLine(
chunk: string,
controller: TransformStreamDefaultController<string>
) {
const logHead = "[" + new Date().getFullYear();
if (chunk.startsWith(logHead) && this.#buffer !== "") {
controller.enqueue(this.#buffer);
this.#buffer = chunk;
} else {
this.#buffer += chunk;
}

setTimeout(() => {
if (!(this.readable.locked && this.writable.locked)) {
return;
}

controller.enqueue(this.#buffer);
this.#buffer = "";
}, 50);
}
}

0 comments on commit 786a945

Please sign in to comment.