workflow for creating pack,

changeable pack version
This commit is contained in:
Emmaline Autumn 2024-10-19 01:08:54 -06:00
parent 70b489213c
commit e03e8809b7
11 changed files with 402 additions and 4 deletions

View File

@ -19,6 +19,7 @@
"@preact/preset-vite": "npm:@preact/preset-vite@^2.9.1",
"@std/fs": "jsr:@std/fs@^1.0.4",
"@std/http": "jsr:@std/http@^1.0.8",
"@std/path": "jsr:@std/path@^1.0.6",
"autoprefixer": "npm:autoprefixer@^10.4.20",
"babel-plugin-transform-hook-names": "npm:babel-plugin-transform-hook-names@^1.0.2",
"less": "npm:less@^4.2.0",

1
deno.lock generated
View File

@ -1394,6 +1394,7 @@
"jsr:@bearmetal/store@^0.0.5",
"jsr:@std/fs@^1.0.4",
"jsr:@std/http@^1.0.8",
"jsr:@std/path@^1.0.6",
"npm:@babel/plugin-transform-react-jsx-development@^7.25.7",
"npm:@deno/vite-plugin@1",
"npm:@preact/preset-vite@^2.9.1",

62
pack_versions/48.json Normal file
View File

@ -0,0 +1,62 @@
{
"version": "48",
"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,7 +1,7 @@
import { SockpuppetPlus } from "@cgg/sockpuppet";
import { serveDir, serveFile } from "@std/http/file-server";
import { BearMetalStore } from "@bearmetal/store";
import { ensureDir } from "@std/fs";
import { ensureDir, ensureFile, exists } from "@std/fs";
import { Router } from "./router.ts";
const installPath = Deno.env.get("BMP_INSTALL_DIR") || "./";
@ -92,8 +92,6 @@ router.route("/api/world")
}
const realWorldPath = Deno.realPathSync(worldPath);
store.set("world", realWorldPath);
store.set("packlocation", realWorldPath + "/datapacks/bmp_dev");
await ensureDir(store.get("packlocation") as string);
return new Response(null, { status: 200 });
})
.get((req) => {
@ -103,6 +101,88 @@ router.route("/api/world")
return serveFile(req, worldPath);
});
router.route("/api/pack")
.get(() => {
using store = new BearMetalStore();
return new Response(
JSON.stringify({ packName: store.get("packname") as string }),
{ status: 200 },
);
})
.post(async (req) => {
using store = new BearMetalStore();
const formData = await req.formData();
const packName = formData.get("packName") as string;
if (!packName) return new Response(null, { status: 400 });
createPack(store, packName);
store.set("packname", packName);
return new Response(JSON.stringify({ packName }), { status: 200 });
});
router.route("/api/packs")
.get(async () => {
using store = new BearMetalStore();
const packs: { name: string; path: string }[] = [];
const world = store.get("world") as string;
for await (const pack of Deno.readDir(world + "/datapacks")) {
if (
pack.isDirectory &&
await exists(world + "/datapacks/" + pack.name + "/bmp_dev")
) {
packs.push({
name: pack.name,
path: Deno.realPathSync(world + "/datapacks/" + pack.name),
});
}
}
return new Response("No BMP packs found", { status: 400 });
});
router.route("/api/pack/version")
.get(() => {
using store = new BearMetalStore();
const packMeta = Deno.readTextFileSync(
store.get("packlocation") + "/pack.mcmeta",
);
const packMetaJson = JSON.parse(packMeta);
return new Response(packMetaJson.pack.pack_format.toString(), {
status: 200,
});
})
.post(async (req) => {
using store = new BearMetalStore();
const version = await req.text();
if (!version) return new Response(null, { status: 400 });
try {
store.set("version", version);
const packMeta = Deno.readTextFileSync(
store.get("packlocation") + "/pack.mcmeta",
);
const packMetaJson = JSON.parse(packMeta);
packMetaJson.pack.pack_format = parseInt(version);
await Deno.writeTextFile(
store.get("packlocation") + "/pack.mcmeta",
JSON.stringify(packMetaJson),
);
} catch (e: any) {
return new Response(e, { status: 500 });
}
return new Response(null, { status: 200 });
});
router.route("/api/versions")
.get(() => {
const versions = Array.from(Deno.readDirSync(installPath + "pack_versions"))
.filter((v) => v.isFile).map((version) =>
version.name.replace(".json", "")
);
return new Response(JSON.stringify(versions), { status: 200 });
});
sockpuppet.addHandler((req: Request) => {
if (new URL(req.url).pathname.startsWith("/api")) return;
@ -112,3 +192,17 @@ sockpuppet.addHandler((req: Request) => {
});
sockpuppet.addHandler(router.handle);
async function createPack(store: BearMetalStore, packName: string) {
const realWorldPath = store.get("world");
store.set("packlocation", realWorldPath + "/datapacks/" + packName);
await ensureDir(store.get("packlocation"));
await ensureFile(store.get("packlocation") + "/bmp_dev");
await ensureFile(store.get("packlocation") + "/pack.mcmeta");
if (!Deno.readTextFileSync(store.get("packlocation") + "/pack.mcmeta")) {
await Deno.writeTextFile(
store.get("packlocation") + "/pack.mcmeta",
`{"pack":{"pack_format":48,"description":"${packName}"}}`,
);
}
}

View File

@ -0,0 +1,11 @@
import type { FunctionComponent } from "preact";
export const LabelledHr: FunctionComponent = ({ children }) => {
return (
<div class="flex gap-4 items-center my-4">
<hr class="w-full" />
<label>{children}</label>
<hr class="w-full" />
</div>
);
};

View File

@ -0,0 +1,9 @@
export const Loader = ({ msg }: { msg?: string }) => {
return (
<div class="flex justify-center items-center gap-4">
{!!msg && <p>{msg}</p>}
<div class="animate-spin rounded-full h-16 w-16 border-t-2 border-b-2 border-primary-600">
</div>
</div>
);
};

14
src/components/modal.tsx Normal file
View File

@ -0,0 +1,14 @@
import type { FunctionComponent } from "preact";
import { Portal } from "./portal.tsx";
export const Modal: FunctionComponent = ({ children }) => {
return (
<Portal>
<div class="fixed inset-0 z-10 overflow-y-auto bg-black/50 grid">
<div class="place-self-center bg-white dark:bg-mixed-400 w-min min-w-64 rounded-lg p-4 overflow-scroll">
{children}
</div>
</div>
</Portal>
);
};

View File

@ -0,0 +1,53 @@
import {
type Dispatch,
type StateUpdater,
useEffect,
useState,
} from "preact/hooks";
import { Loader } from "./loader.tsx";
export const PacksList = (
{ setPackName }: { setPackName: Dispatch<StateUpdater<string>> },
) => {
const [packs, setPacks] = useState<{ name: string; path: string }[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch("/api/packs").then((res) => res.json()).then((json) => {
setPacks(json);
}).finally(() => setLoading(false));
}, []);
const setPackNameThing = async (name: string) => {
setLoading(true);
const body = new FormData();
body.set("packName", name);
const res = await fetch("/api/pack", {
method: "POST",
body,
});
const json = await res.json();
if (res.status === 200) {
document.title = json.packName;
setPackName(json.packName);
}
setLoading(false);
};
return loading ? <Loader /> : (
<ul>
{packs.length
? packs.map((pack) => (
<li
class="cursor-pointer even:bg-black/5"
onClick={() => setPackNameThing(pack.name)}
>
{pack.name}
</li>
))
: <li>No packs found</li>}
</ul>
);
};

26
src/components/portal.tsx Normal file
View File

@ -0,0 +1,26 @@
import { useEffect, useState } from "preact/hooks";
import { createPortal } from "preact/compat";
import type { FunctionComponent } from "preact";
interface IProps {
className?: string;
el?: string;
}
export const Portal: FunctionComponent<IProps> = (
{ children, className = "root-portal", el = "div" },
) => {
const [container, setContainer] = useState<HTMLElement>();
useEffect(() => {
const container = document.createElement(el);
container.classList.add(className);
document.body.appendChild(container);
setContainer(container);
return () => {
document.body.removeChild(container);
};
}, [className, el]);
return container ? createPortal(children, container) : <></>;
};

View File

@ -30,3 +30,15 @@
}
}
}
@layer utilities {
.animate-spin {
animation: spin 1s linear infinite;
}
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}

View File

@ -1,3 +1,118 @@
import { useEffect, useState } from "preact/hooks";
import { Modal } from "../components/modal.tsx";
import { LabelledHr } from "../components/labelledHr.tsx";
import { PacksList } from "../components/packsList.tsx";
export const Editor = () => {
return <div>Editor</div>;
const [packName, setPackName] = useState("");
const [loading, setLoading] = useState(true);
const [namespaces, setNamespaces] = useState<string[]>([]);
const [packVersion, setPackVersion] = useState("");
const [packVersionList, setPackVersionList] = useState<string[]>([]);
useEffect(() => {
document.title = "BearMetal Packer";
fetch("/api/pack").then((res) => res.json()).then((json) => {
setPackName(json.packName);
setLoading(false);
});
fetch("/api/pack/version").then((res) => res.text()).then((text) => {
setPackVersion(text);
});
fetch("/api/versions").then((res) => res.json()).then((json) => {
setPackVersionList(json);
});
}, []);
const setPackNameThing = async (event: SubmitEvent) => {
setLoading(true);
const res = await fetch("/api/pack", {
method: "POST",
body: new FormData(event.target as HTMLFormElement),
});
if (res.status === 200) {
document.title = packName;
setPackName(packName);
}
setLoading(false);
};
if (!packName) {
return (
<Modal>
{loading
? <p>Just a sec, trying to put the cats back in the bag.</p>
: (
<>
<form
method="POST"
onSubmit={setPackNameThing}
>
<label class="w-full">Set pack name</label>
<div class="flex gap-2">
<input type="text" name="packName" />
<button type="submit">Set</button>
</div>
</form>
<LabelledHr>OR</LabelledHr>
<PacksList setPackName={setPackName} />
</>
)}
</Modal>
);
}
const [showVersions, setShowVersions] = useState(false);
const updatePackVersion = async (version: string) => {
const res = await fetch("/api/pack/version", {
method: "POST",
body: version,
});
if (res.status === 200) {
setPackVersion(version);
}
};
return (
<div class="flex h-full">
<div class="w-1/4 p-4 bg-primary-600">
<div class="text-center">
<h1 class="text-2xl">BearMetalPacker</h1>
<h2 class="text-xl">{packName}</h2>
<p class="bg-lime-700 rounded-full shadow-md">
Datapack Version:{" "}
<span
class="relative cursor-pointer rounded-full bg-lime-200 text-black px-2 inline-block"
onClick={() => setShowVersions(!showVersions)}
>
{packVersion}
{showVersions && (
<ul class="absolute top-full left-0 bg-lime-50 rounded-md shadow-md p-2 min-w 12">
{packVersionList.map((version) => (
<li onClick={() => updatePackVersion(version)}>
{version}
</li>
))}
</ul>
)}
</span>
</p>
</div>
<LabelledHr>Namespaces</LabelledHr>
<div>
<ul class="w-full">
{namespaces.map((namespace) => (
<li class="cursor-pointer even:bg-black/5" // onClick={() => setNamespace(namespace)}
>
{namespace}
</li>
))}
</ul>
</div>
</div>
</div>
);
};