diff --git a/.gitignore b/.gitignore index 82434ef..3d1956f 100644 --- a/.gitignore +++ b/.gitignore @@ -27,4 +27,5 @@ dist-ssr BearMetal/ resources/ -!**/*/resources/ \ No newline at end of file +!**/*/resources/ +test.ts \ No newline at end of file diff --git a/pack_versions/57.json b/pack_versions/57.json new file mode 100644 index 0000000..1bfeb62 --- /dev/null +++ b/pack_versions/57.json @@ -0,0 +1,63 @@ +{ + "version": "48", + "mcVersion": "^1.21.3", + "schema": { + "pack.mcmeta": "json", + "pack.png": "image/png", + "data": { + "": { + "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"] + } + } + } + } + } +} diff --git a/server/main.ts b/server/main.ts index b612c96..fef72f1 100644 --- a/server/main.ts +++ b/server/main.ts @@ -6,6 +6,8 @@ import { Router } from "./router.ts"; 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") || "./"; @@ -31,6 +33,7 @@ sockpuppet.addHandler((req: Request) => { // }); const router = new Router(); + router.route("/api/dir") .get(async () => { using store = new BearMetalStore(); @@ -208,6 +211,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,6 +238,25 @@ router.route("/api/versions") createTagRoutes(router); createResourcesRoutes(router); +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", + }, + }); + }) + sockpuppet.addHandler((req: Request) => { if (new URL(req.url).pathname.startsWith("/api")) return; diff --git a/server/resources/readers.ts b/server/resources/readers.ts index 8d2436f..a441612 100644 --- a/server/resources/readers.ts +++ b/server/resources/readers.ts @@ -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; diff --git a/server/resources/routes.ts b/server/resources/routes.ts index 1594763..35675bb 100644 --- a/server/resources/routes.ts +++ b/server/resources/routes.ts @@ -1,7 +1,8 @@ +import { ensureDir } from "@std/fs/ensure-dir"; import type { Router } from "../router.ts"; 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*") @@ -10,38 +11,92 @@ export const createResourcesRoutes = (router: Router) => { 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 }); + // } + // } } } }); diff --git a/src/components/editor/tags/editor.tsx b/src/components/editor/tags/editor.tsx index 508b00a..93396bb 100644 --- a/src/components/editor/tags/editor.tsx +++ b/src/components/editor/tags/editor.tsx @@ -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() {
- + {false && (
  • @@ -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 ; - } - return ( -
    -
      - {data.map((resource) => ( -
    • - {resource.name} - -
    • - ))} -
    -
    - ); -} diff --git a/src/components/resourceList.tsx b/src/components/resourceList.tsx new file mode 100644 index 0000000..7c86441 --- /dev/null +++ b/src/components/resourceList.tsx @@ -0,0 +1,33 @@ +import { useState } from "preact/hooks"; +import { useRemoteStream } from "../hooks/useStream.ts"; + +export const ResourceList = () => { + // const [resources, setResources] = useState([]); + // // 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('http://localhost:8000/api/resources/blocks?format=57'); + + if (isLoading) return
    Loading...
    ; + if (error) return
    Error: {error}
    ; + + return ( +
    +
      + {resources?.map((resource) => ( +
    • + {resource.name} + +
    • + ))} +
    +
    + ); +}; diff --git a/src/hooks/useStream.ts b/src/hooks/useStream.ts index b979d38..af21b94 100644 --- a/src/hooks/useStream.ts +++ b/src/hooks/useStream.ts @@ -1,42 +1,49 @@ -import { useEffect } from "preact/hooks"; +import { useEffect, useState } from "preact/hooks"; -export const useStream = ( - stream: ReadableStream, - 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 = (url: string, transformer?: (e: string) => T) : [T[], boolean, string | null] => { + const [items, setItems] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); -export const useRemoteStream = ( - 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]; +} + diff --git a/types.ts b/types.ts index 63d60b0..6d51e18 100644 --- a/types.ts +++ b/types.ts @@ -3,5 +3,12 @@ declare global { url: URL; state: Record; params: Record; + headers?: Headers, + } + + interface BlockItem { + name: string; + resourceLocation: string; + images: string[]; } } diff --git a/vite.config.ts b/vite.config.ts index 0ffc5a1..c37f250 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -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", + ], + }, + }, });