Compare commits

...

2 Commits

Author SHA1 Message Date
762baa37e7 Convert to bearmetal router, updated sockpuppet 2024-11-11 13:39:30 -07:00
1f306485a8 fixing some chunking issues 2024-10-29 00:14:21 -06:00
13 changed files with 311 additions and 118 deletions

3
.gitignore vendored
View File

@@ -27,4 +27,5 @@ dist-ssr
BearMetal/
resources/
!**/*/resources/
!**/*/resources/
test.ts

View File

@@ -15,6 +15,8 @@
},
"imports": {
"@babel/plugin-transform-react-jsx-development": "npm:@babel/plugin-transform-react-jsx-development@^7.25.7",
"@bearmetal/router": "jsr:@bearmetal/router@^0.1.1",
"@bearmetal/sockpuppet": "jsr:@bearmetal/sockpuppet@^1.0.0",
"@bearmetal/store": "jsr:@bearmetal/store@^0.0.5",
"@cgg/sockpuppet": "../sockpuppet.ts/server/mod.ts",
"@cgg/sockpuppet/client": "../sockpuppet.ts/client/mod.ts",

10
deno.lock generated
View File

@@ -1,6 +1,8 @@
{
"version": "4",
"specifiers": {
"jsr:@bearmetal/router@~0.1.1": "0.1.1",
"jsr:@bearmetal/sockpuppet@1": "1.0.0",
"jsr:@bearmetal/store@^0.0.5": "0.0.5",
"jsr:@denosaurs/plug@1.0.5": "1.0.5",
"jsr:@gfx/canvas@~0.5.8": "0.5.8",
@@ -43,6 +45,12 @@
"npm:vite@^5.4.8": "5.4.9"
},
"jsr": {
"@bearmetal/router@0.1.1": {
"integrity": "cd526ac6b4a6426ac171f3868f83ec1727ebf2de21aad4e1f128c2605345f19d"
},
"@bearmetal/sockpuppet@1.0.0": {
"integrity": "e6dc133b2a5c9e7f1fb99309f592c24b0b8ffd4fc43ee3d61313f5c517ddfb8c"
},
"@bearmetal/store@0.0.5": {
"integrity": "d17da24c91bcc05707deb8a55017ebdf5d8eebd2f6293dcb2bbfac57e4e3b395",
"dependencies": [
@@ -1488,6 +1496,8 @@
},
"workspace": {
"dependencies": [
"jsr:@bearmetal/router@~0.1.1",
"jsr:@bearmetal/sockpuppet@1",
"jsr:@bearmetal/store@^0.0.5",
"jsr:@gfx/canvas@~0.5.8",
"jsr:@std/encoding@^1.0.5",

63
pack_versions/57.json Normal file
View File

@@ -0,0 +1,63 @@
{
"version": "48",
"mcVersion": "^1.21.3",
"schema": {
"pack.mcmeta": "json",
"pack.png": "image/png",
"data": {
"<namespace>": {
"function": "function",
"structure": {
"DataVersion": "int",
"size": ["int", "int", "int"],
"palette": [{
"name": "blockId",
"properties": ["string"]
}],
"palettes": [
[{
"name": "blockId",
"properties": ["string"]
}]
],
"blocks": [{
"state": "int",
"pos": ["int", "int", "int"],
"nbt": "nbt"
}],
"entities": [{
"pos": ["double", "double", "double"],
"blockPos": ["int", "int", "int"],
"nbt": "nbt"
}]
},
"tags": "tags",
"advancment": {
"parent": "advancment",
"display": {
"icon": {
"id": "itemId",
"count": "int",
"components": ["itemComponent"]
},
"title": "jsonString",
"description": "jsonString",
"frame": "frame",
"background": "resource",
"show_toast": "bool",
"announce_to_chat": "bool",
"hidden": "bool"
},
"criteria": "criteria",
"requirements": ["criterion_name"],
"rewards": {
"experience": "int",
"function": "function",
"loot": ["loot_table"],
"recipes": ["recipe"]
}
}
}
}
}
}

View File

@@ -1,38 +1,31 @@
import { SockpuppetPlus } from "@cgg/sockpuppet";
import { Sockpuppet } from "@bearmetal/sockpuppet";
import { serveDir, serveFile } from "@std/http/file-server";
import { BearMetalStore } from "@bearmetal/store";
import { ensureDir, ensureFile, exists } from "@std/fs";
import { Router } from "./router.ts";
import { Router } from "@bearmetal/router";
import { getPackVersion } from "./util/packVersion.ts";
import { createTagRoutes } from "./tags/routes.ts";
import { createResourcesRoutes } from "./resources/routes.ts";
import { readDirDirs } from "./util/readDir.ts";
import { unzipResources } from "./resources/unzip.ts";
const installPath = Deno.env.get("BMP_INSTALL_DIR") || "./";
const sockpuppet = new SockpuppetPlus();
sockpuppet.addHandler((req: Request) => {
const url = new URL(req.url);
if (!url.pathname.startsWith("/images")) return;
return serveFile(req, url.searchParams.get("location") as string);
});
const sockpuppet = new Sockpuppet();
// sockpuppet.addHandler((req: Request) => {
// const method = req.method;
// if (method === "OPTIONS") {
// return new Response(null, {
// status: 200,
// headers: {
// "Access-Control-Allow-Origin": "*",
// "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
// },
// });
// }
// const url = new URL(req.url);
// if (!url.pathname.startsWith("/images")) return;
// return serveFile(req, url.searchParams.get("location") as string);
// });
const router = new Router();
router.route("/images").get((req,ctx) => serveFile(req,ctx.url.searchParams.get("location") as string));
router.route("/api/dir")
.get(async () => {
console.log("hitting dir");
using store = new BearMetalStore();
const mcPath = store.get("mcPath") as string;
if (mcPath) {
@@ -208,6 +201,15 @@ router.route("/api/pack/version")
store.get("packlocation") + "/pack.mcmeta",
JSON.stringify(packMetaJson),
);
const packVersionSchema = Deno.readTextFileSync(
installPath + "pack_versions/" + version + ".json",
);
const packVersionSchemaJson = JSON.parse(packVersionSchema);
packVersionSchemaJson.mcVersion = store.get("mcVersion");
const versionResourceDir = await readDirDirs(installPath + "resources/");
if (!versionResourceDir.includes(version)) {
unzipResources();
}
} catch (e: any) {
return new Response(e, { status: 500 });
}
@@ -226,15 +228,38 @@ router.route("/api/versions")
createTagRoutes(router);
createResourcesRoutes(router);
sockpuppet.addHandler((req: Request) => {
if (new URL(req.url).pathname.startsWith("/api")) return;
router.route("/api/stream/test")
.get(() => {
const stream = new ReadableStream({
async start(controller) {
const enc = new TextEncoder();
controller.enqueue(enc.encode("Hello"));
controller.enqueue(enc.encode(", "));
controller.enqueue(enc.encode("World!"));
controller.close();
},
});
return new Response(stream, {
headers: {
"content-type": "text/plain",
"x-content-type-options": "nosniff",
},
});
})
return serveDir(req, {
fsRoot: installPath + "dist",
});
});
// sockpuppet.addHandler((req: Request) => {
// if (new URL(req.url).pathname.startsWith("/api")) return;
sockpuppet.addHandler(router.handle);
// return serveDir(req, {
// fsRoot: installPath + "dist",
// });
// });
router.route("/").get((req) => serveDir(req, {fsRoot:installPath + "dist"}));
router.route("/ws").get(sockpuppet.handler)
Deno.serve(router.handle);
async function createPack(store: BearMetalStore, packName: string) {
const realWorldPath = store.get("world");

View File

@@ -2,13 +2,7 @@ import { encodeBase64 } from "@std/encoding/base64";
import { readDirFiles } from "../util/readDir.ts";
import { createIsometricCube } from "./renderer.ts";
interface BlockItem {
name: string;
resourceLocation: string;
images: string[];
}
export const readBlocks = async (path: string) => {
export const readBlocks = async (path: string, read?: (b: BlockItem, i: number) => void) => {
const blocks: BlockItem[] =
(await readDirFiles(path + "/assets/minecraft/blockstates"))
.map((b) => ({
@@ -300,10 +294,16 @@ export const readBlocks = async (path: string) => {
block.images.push(await createIsometricCube(b64, b64, b64));
}
}
let i = 0;
for (const block of blocks) {
read?.(block, i);
i++;
}
return blocks;
};
export const readItems = async (path: string) => {
export const readItems = async (path: string, read?: (b: BlockItem) => void) => {
const items: BlockItem[] =
(await readDirFiles(path + "/assets/minecraft/models/item")).map((i) => ({
name: i.replace(".json", ""),
@@ -348,6 +348,7 @@ export const readItems = async (path: string) => {
}
}
}
read?.(item);
}
return items;

View File

@@ -1,48 +1,104 @@
import type { Router } from "../router.ts";
import { ensureDir } from "@std/fs/ensure-dir";
import type { Router } from "@bearmetal/router";
import { readDirDirs } from "../util/readDir.ts";
import { versionCompat } from "../util/versionCompat.ts";
import { readBlocks } from "./readers.ts";
import { readBlocks, readItems } from "./readers.ts";
export const createResourcesRoutes = (router: Router) => {
router.route("/api/resources/:path*")
.get(async (req, ctx) => {
.get(async (_req, ctx) => {
const path = ctx.params.path;
if (!path) {
return new Response("no path provided", { status: 400 });
}
const format = ctx.url.searchParams.get("format");
if (!format) {
return new Response("no format provided", { status: 400 });
}
const packVersion = await Deno.readTextFile(
"./pack_versions/" + format + ".json",
);
const packVersionJson = JSON.parse(packVersion);
const mcVersion = packVersionJson.mcVersion;
await ensureDir("./resources");
const resourceVersions = await readDirDirs("./resources");
console.log("resourceVersions", resourceVersions);
for (const resourceVersion of resourceVersions) {
if (versionCompat(resourceVersion, mcVersion)) {
const resourcePath = "./resources/" + resourceVersion;
const splitPath = path.split("/");
switch (splitPath[0]) {
case "block":
case "blocks": {
return new Response(
JSON.stringify(await readBlocks(resourcePath)),
let items: BlockItem[] = [];
const batch = (
controller: ReadableStreamDefaultController,
res: BlockItem,
) => {
items.push(res);
if (items.length > 4) {
controller.enqueue(
new TextEncoder().encode(JSON.stringify(items) + "\n"),
);
items = [];
}
case "item":
case "items": {
return new Response(
JSON.stringify(await readBlocks(resourcePath)),
);
}
default: {
return new Response("invalid path", { status: 400 });
}
}
};
const body = new ReadableStream({
async start(controller) {
switch (path) {
case "block":
case "blocks": {
await readBlocks(resourcePath, (res, i) => {
console.log(i);
batch(controller, res);
});
controller.close();
break;
}
case "item":
case "items": {
await readItems(resourcePath, (res) => {
batch(controller, res);
});
controller.close();
break;
}
default: {
controller.close();
break;
}
}
},
});
return new Response(body, {
headers: {
"content-type": "application/json",
"access-control-allow-origin": "*",
},
});
// const resourcePath = "./resources/" + resourceVersion;
// const splitPath = path.split("/");
// switch (splitPath[0]) {
// case "block":
// case "blocks": {
// return new Response(
// JSON.stringify(await readBlocks(resourcePath)),
// );
// }
// case "item":
// case "items": {
// return new Response(
// JSON.stringify(await readBlocks(resourcePath)),
// );
// }
// default: {
// return new Response("invalid path", { status: 400 });
// }
// }
}
}
return new Response("no path provided", { status: 400 });
});
};

View File

@@ -1,6 +1,6 @@
import { BearMetalStore } from "@bearmetal/store";
import { getPackVersion } from "../util/packVersion.ts";
import type { Router } from "../router.ts";
import type { Router } from "@bearmetal/router";
import { getTagDir } from "./getTagDir.ts";
import { ensureDir } from "@std/fs/ensure-dir";
import { ensureFile } from "@std/fs/ensure-file";

View File

@@ -6,6 +6,7 @@ import { namespaceAtom, useNamespace } from "../../../atoms/namespace.ts";
import { Loader } from "../../../components/loader.tsx";
import { useEffect, useState } from "preact/hooks";
import { NewTagModal } from "./newTagModal.tsx";
import { ResourceList } from "../../resourceList.tsx";
export const TagRouter = () => {
return (
@@ -33,7 +34,7 @@ function TagList() {
<div>
<ul>
</ul>
<Resources />
<ResourceList />
{false && (
<ul class="flex flex-col gap-2">
<li>
@@ -133,24 +134,3 @@ function TagEditor() {
);
}
function Resources() {
const { data, isLoading } = useSWR<{ name: string; images: string[] }[]>(
`/api/resources/blocks?format=48`,
fetchJson,
);
if (isLoading || !data) {
return <Loader msg="Your hard drive is full of... interesting things." />;
}
return (
<div>
<ul>
{data.map((resource) => (
<li>
{resource.name}
<img class="min-w-12 max-h-16" src={resource.images[0]} />
</li>
))}
</ul>
</div>
);
}

View File

@@ -0,0 +1,33 @@
import { useState } from "preact/hooks";
import { useRemoteStream } from "../hooks/useStream.ts";
export const ResourceList = () => {
// const [resources, setResources] = useState<BlockItem[]>([]);
// // useRemoteStream("/api/stream/test", (e) => {
// useRemoteStream("http://localhost:8000/api/resources/blocks?format=57", (e: string) => {
// try {
// JSON.parse(e);
// } catch (err) {
// return console.log(e);
// }
// setResources((resources) => [...resources, ...JSON.parse(e) as BlockItem[]]);
// }, (e) => console.log(e));
const [resources, isLoading, error] = useRemoteStream<BlockItem>('http://localhost:8000/api/resources/blocks?format=57');
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
return (
<div>
<ul>
{resources?.map((resource) => (
<li>
{resource.name}
<img class="min-w-6 max-w-8" src={resource.images[0]} />
</li>
))}
</ul>
</div>
);
};

View File

@@ -1,42 +1,49 @@
import { useEffect } from "preact/hooks";
import { useEffect, useState } from "preact/hooks";
export const useStream = <T>(
stream: ReadableStream<T>,
onData: (data: T) => void,
onError: (err: Error) => void,
) => {
const reader = stream.getReader();
const read = async () => {
const { done, value } = await reader.read();
if (done) {
reader.releaseLock();
return;
}
onData(value);
read();
};
read();
};
export const useRemoteStream = <T = string>(url: string, transformer?: (e: string) => T) : [T[], boolean, string | null] => {
const [items, setItems] = useState<T[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null);
export const useRemoteStream = <T>(
url: string,
onData: (data: T) => void,
onError: (err: Error) => void,
) => {
useEffect(() => {
const stream = new ReadableStream({
async start(controller) {
const res = await fetch(url);
res.body?.pipeThrough(new TextDecoderStream())
.pipeTo(
new WritableStream({
write(chunk) {
controller.enqueue(chunk);
},
}),
);
},
});
useStream(stream, onData, onError);
}, [url]);
};
const abortController = new AbortController();
async function fetchStream() {
try {
const response = await fetch(url, {
signal: abortController.signal
});
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
if (!response.body) throw new Error('ReadableStream not supported');
const reader = response.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value);
const batchItems = chunk.trim().split('\n')
.filter(line => line.length > 0)
.map(line => transformer?.(line) ?? JSON.parse(line))
.flat();
setItems(prev => [...prev, ...batchItems]);
}
} catch (err:any) {
if (err?.name === 'AbortError') return;
setError(err.message);
} finally {
setIsLoading(false);
}
}
fetchStream();
return () => abortController.abort();
}, [])
return [items, isLoading, error];
}

View File

@@ -3,5 +3,12 @@ declare global {
url: URL;
state: Record<string, unknown>;
params: Record<string, string | undefined>;
headers?: Headers,
}
interface BlockItem {
name: string;
resourceLocation: string;
images: string[];
}
}

View File

@@ -16,6 +16,7 @@ export default defineConfig({
"/api": {
target: "http://localhost:8000",
changeOrigin: true,
ws: true,
},
"/puppet": {
target: "http://localhost:8000",
@@ -29,4 +30,11 @@ export default defineConfig({
},
},
},
build: {
rollupOptions: {
external: [
"server",
],
},
},
});