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/ BearMetal/
resources/ resources/
!**/*/resources/ !**/*/resources/
test.ts

View File

@@ -15,6 +15,8 @@
}, },
"imports": { "imports": {
"@babel/plugin-transform-react-jsx-development": "npm:@babel/plugin-transform-react-jsx-development@^7.25.7", "@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", "@bearmetal/store": "jsr:@bearmetal/store@^0.0.5",
"@cgg/sockpuppet": "../sockpuppet.ts/server/mod.ts", "@cgg/sockpuppet": "../sockpuppet.ts/server/mod.ts",
"@cgg/sockpuppet/client": "../sockpuppet.ts/client/mod.ts", "@cgg/sockpuppet/client": "../sockpuppet.ts/client/mod.ts",

10
deno.lock generated
View File

@@ -1,6 +1,8 @@
{ {
"version": "4", "version": "4",
"specifiers": { "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:@bearmetal/store@^0.0.5": "0.0.5",
"jsr:@denosaurs/plug@1.0.5": "1.0.5", "jsr:@denosaurs/plug@1.0.5": "1.0.5",
"jsr:@gfx/canvas@~0.5.8": "0.5.8", "jsr:@gfx/canvas@~0.5.8": "0.5.8",
@@ -43,6 +45,12 @@
"npm:vite@^5.4.8": "5.4.9" "npm:vite@^5.4.8": "5.4.9"
}, },
"jsr": { "jsr": {
"@bearmetal/router@0.1.1": {
"integrity": "cd526ac6b4a6426ac171f3868f83ec1727ebf2de21aad4e1f128c2605345f19d"
},
"@bearmetal/sockpuppet@1.0.0": {
"integrity": "e6dc133b2a5c9e7f1fb99309f592c24b0b8ffd4fc43ee3d61313f5c517ddfb8c"
},
"@bearmetal/store@0.0.5": { "@bearmetal/store@0.0.5": {
"integrity": "d17da24c91bcc05707deb8a55017ebdf5d8eebd2f6293dcb2bbfac57e4e3b395", "integrity": "d17da24c91bcc05707deb8a55017ebdf5d8eebd2f6293dcb2bbfac57e4e3b395",
"dependencies": [ "dependencies": [
@@ -1488,6 +1496,8 @@
}, },
"workspace": { "workspace": {
"dependencies": [ "dependencies": [
"jsr:@bearmetal/router@~0.1.1",
"jsr:@bearmetal/sockpuppet@1",
"jsr:@bearmetal/store@^0.0.5", "jsr:@bearmetal/store@^0.0.5",
"jsr:@gfx/canvas@~0.5.8", "jsr:@gfx/canvas@~0.5.8",
"jsr:@std/encoding@^1.0.5", "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 { serveDir, serveFile } from "@std/http/file-server";
import { BearMetalStore } from "@bearmetal/store"; import { BearMetalStore } from "@bearmetal/store";
import { ensureDir, ensureFile, exists } from "@std/fs"; import { ensureDir, ensureFile, exists } from "@std/fs";
import { Router } from "./router.ts"; import { Router } from "@bearmetal/router";
import { getPackVersion } from "./util/packVersion.ts"; import { getPackVersion } from "./util/packVersion.ts";
import { createTagRoutes } from "./tags/routes.ts"; import { createTagRoutes } from "./tags/routes.ts";
import { createResourcesRoutes } from "./resources/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 installPath = Deno.env.get("BMP_INSTALL_DIR") || "./";
const sockpuppet = new SockpuppetPlus(); const sockpuppet = new Sockpuppet();
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);
});
// sockpuppet.addHandler((req: Request) => { // sockpuppet.addHandler((req: Request) => {
// const method = req.method; // const url = new URL(req.url);
// if (method === "OPTIONS") { // if (!url.pathname.startsWith("/images")) return;
// return new Response(null, {
// status: 200, // return serveFile(req, url.searchParams.get("location") as string);
// headers: {
// "Access-Control-Allow-Origin": "*",
// "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
// },
// });
// }
// }); // });
const router = new Router(); const router = new Router();
router.route("/images").get((req,ctx) => serveFile(req,ctx.url.searchParams.get("location") as string));
router.route("/api/dir") router.route("/api/dir")
.get(async () => { .get(async () => {
console.log("hitting dir");
using store = new BearMetalStore(); using store = new BearMetalStore();
const mcPath = store.get("mcPath") as string; const mcPath = store.get("mcPath") as string;
if (mcPath) { if (mcPath) {
@@ -208,6 +201,15 @@ router.route("/api/pack/version")
store.get("packlocation") + "/pack.mcmeta", store.get("packlocation") + "/pack.mcmeta",
JSON.stringify(packMetaJson), 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) { } catch (e: any) {
return new Response(e, { status: 500 }); return new Response(e, { status: 500 });
} }
@@ -226,15 +228,38 @@ router.route("/api/versions")
createTagRoutes(router); createTagRoutes(router);
createResourcesRoutes(router); createResourcesRoutes(router);
sockpuppet.addHandler((req: Request) => { router.route("/api/stream/test")
if (new URL(req.url).pathname.startsWith("/api")) return; .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, { // sockpuppet.addHandler((req: Request) => {
fsRoot: installPath + "dist", // 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) { async function createPack(store: BearMetalStore, packName: string) {
const realWorldPath = store.get("world"); const realWorldPath = store.get("world");

View File

@@ -2,13 +2,7 @@ import { encodeBase64 } from "@std/encoding/base64";
import { readDirFiles } from "../util/readDir.ts"; import { readDirFiles } from "../util/readDir.ts";
import { createIsometricCube } from "./renderer.ts"; import { createIsometricCube } from "./renderer.ts";
interface BlockItem { export const readBlocks = async (path: string, read?: (b: BlockItem, i: number) => void) => {
name: string;
resourceLocation: string;
images: string[];
}
export const readBlocks = async (path: string) => {
const blocks: BlockItem[] = const blocks: BlockItem[] =
(await readDirFiles(path + "/assets/minecraft/blockstates")) (await readDirFiles(path + "/assets/minecraft/blockstates"))
.map((b) => ({ .map((b) => ({
@@ -300,10 +294,16 @@ export const readBlocks = async (path: string) => {
block.images.push(await createIsometricCube(b64, b64, b64)); block.images.push(await createIsometricCube(b64, b64, b64));
} }
} }
let i = 0;
for (const block of blocks) {
read?.(block, i);
i++;
}
return blocks; return blocks;
}; };
export const readItems = async (path: string) => { export const readItems = async (path: string, read?: (b: BlockItem) => void) => {
const items: BlockItem[] = const items: BlockItem[] =
(await readDirFiles(path + "/assets/minecraft/models/item")).map((i) => ({ (await readDirFiles(path + "/assets/minecraft/models/item")).map((i) => ({
name: i.replace(".json", ""), name: i.replace(".json", ""),
@@ -348,6 +348,7 @@ export const readItems = async (path: string) => {
} }
} }
} }
read?.(item);
} }
return items; 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 { readDirDirs } from "../util/readDir.ts";
import { versionCompat } from "../util/versionCompat.ts"; import { versionCompat } from "../util/versionCompat.ts";
import { readBlocks } from "./readers.ts"; import { readBlocks, readItems } from "./readers.ts";
export const createResourcesRoutes = (router: Router) => { export const createResourcesRoutes = (router: Router) => {
router.route("/api/resources/:path*") router.route("/api/resources/:path*")
.get(async (req, ctx) => { .get(async (_req, ctx) => {
const path = ctx.params.path; const path = ctx.params.path;
if (!path) { if (!path) {
return new Response("no path provided", { status: 400 }); return new Response("no path provided", { status: 400 });
} }
const format = ctx.url.searchParams.get("format"); const format = ctx.url.searchParams.get("format");
if (!format) { if (!format) {
return new Response("no format provided", { status: 400 }); return new Response("no format provided", { status: 400 });
} }
const packVersion = await Deno.readTextFile( const packVersion = await Deno.readTextFile(
"./pack_versions/" + format + ".json", "./pack_versions/" + format + ".json",
); );
const packVersionJson = JSON.parse(packVersion); const packVersionJson = JSON.parse(packVersion);
const mcVersion = packVersionJson.mcVersion; const mcVersion = packVersionJson.mcVersion;
await ensureDir("./resources");
const resourceVersions = await readDirDirs("./resources"); const resourceVersions = await readDirDirs("./resources");
console.log("resourceVersions", resourceVersions);
for (const resourceVersion of resourceVersions) { for (const resourceVersion of resourceVersions) {
if (versionCompat(resourceVersion, mcVersion)) { if (versionCompat(resourceVersion, mcVersion)) {
const resourcePath = "./resources/" + resourceVersion; const resourcePath = "./resources/" + resourceVersion;
const splitPath = path.split("/"); let items: BlockItem[] = [];
switch (splitPath[0]) {
case "block": const batch = (
case "blocks": { controller: ReadableStreamDefaultController,
return new Response( res: BlockItem,
JSON.stringify(await readBlocks(resourcePath)), ) => {
items.push(res);
if (items.length > 4) {
controller.enqueue(
new TextEncoder().encode(JSON.stringify(items) + "\n"),
); );
items = [];
} }
case "item": };
case "items": { const body = new ReadableStream({
return new Response( async start(controller) {
JSON.stringify(await readBlocks(resourcePath)), switch (path) {
); case "block":
} case "blocks": {
default: { await readBlocks(resourcePath, (res, i) => {
return new Response("invalid path", { status: 400 }); 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 { BearMetalStore } from "@bearmetal/store";
import { getPackVersion } from "../util/packVersion.ts"; import { getPackVersion } from "../util/packVersion.ts";
import type { Router } from "../router.ts"; import type { Router } from "@bearmetal/router";
import { getTagDir } from "./getTagDir.ts"; import { getTagDir } from "./getTagDir.ts";
import { ensureDir } from "@std/fs/ensure-dir"; import { ensureDir } from "@std/fs/ensure-dir";
import { ensureFile } from "@std/fs/ensure-file"; 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 { Loader } from "../../../components/loader.tsx";
import { useEffect, useState } from "preact/hooks"; import { useEffect, useState } from "preact/hooks";
import { NewTagModal } from "./newTagModal.tsx"; import { NewTagModal } from "./newTagModal.tsx";
import { ResourceList } from "../../resourceList.tsx";
export const TagRouter = () => { export const TagRouter = () => {
return ( return (
@@ -33,7 +34,7 @@ function TagList() {
<div> <div>
<ul> <ul>
</ul> </ul>
<Resources /> <ResourceList />
{false && ( {false && (
<ul class="flex flex-col gap-2"> <ul class="flex flex-col gap-2">
<li> <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>( export const useRemoteStream = <T = string>(url: string, transformer?: (e: string) => T) : [T[], boolean, string | null] => {
stream: ReadableStream<T>, const [items, setItems] = useState<T[]>([]);
onData: (data: T) => void, const [isLoading, setIsLoading] = useState(true);
onError: (err: Error) => void, const [error, setError] = useState(null);
) => {
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>(
url: string,
onData: (data: T) => void,
onError: (err: Error) => void,
) => {
useEffect(() => { useEffect(() => {
const stream = new ReadableStream({ const abortController = new AbortController();
async start(controller) {
const res = await fetch(url); async function fetchStream() {
res.body?.pipeThrough(new TextDecoderStream()) try {
.pipeTo( const response = await fetch(url, {
new WritableStream({ signal: abortController.signal
write(chunk) { });
controller.enqueue(chunk);
}, 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();
useStream(stream, onData, onError);
}, [url]); 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; url: URL;
state: Record<string, unknown>; state: Record<string, unknown>;
params: Record<string, string | undefined>; 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": { "/api": {
target: "http://localhost:8000", target: "http://localhost:8000",
changeOrigin: true, changeOrigin: true,
ws: true,
}, },
"/puppet": { "/puppet": {
target: "http://localhost:8000", target: "http://localhost:8000",
@@ -29,4 +30,11 @@ export default defineConfig({
}, },
}, },
}, },
build: {
rollupOptions: {
external: [
"server",
],
},
},
}); });