Compare commits

..

No commits in common. "a8a903d5818b3034c527e9392c7261b9b6e0ae5a" and "7e4d854685f97ec8cd4d0a0cf7f53aa8eb89e9ce" have entirely different histories.

42 changed files with 124 additions and 2252 deletions

2
.gitignore vendored
View File

@ -26,5 +26,3 @@ dist-ssr
.env .env
BearMetal/ BearMetal/
resources/
!**/*/resources/

View File

@ -1,12 +1,9 @@
{ {
"tasks": { "tasks": {
"dev": "deno run -A --node-modules-dir npm:vite & deno run -A --watch ./server/main.ts", "dev": "deno run -A --node-modules-dir npm:vite & deno run --allow-net --allow-read --allow-write --allow-env --watch ./main.ts",
"fdev": "deno run -A --node-modules-dir npm:vite",
"build": "deno run -A --node-modules-dir npm:vite build", "build": "deno run -A --node-modules-dir npm:vite build",
"preview": "deno run -A --node-modules-dir npm:vite preview", "preview": "deno run -A --node-modules-dir npm:vite preview",
"serve": "deno run -A ./server/main.ts", "serve": "deno run --allow-net --allow-read --allow-write --allow-env ./main.ts"
"bdev": "deno run -A --watch ./server/main.ts",
"tmux": "./session.sh"
}, },
"compilerOptions": { "compilerOptions": {
"lib": ["ES2020", "DOM", "DOM.Iterable", "deno.ns"], "lib": ["ES2020", "DOM", "DOM.Iterable", "deno.ns"],
@ -15,25 +12,17 @@
}, },
"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/store": "jsr:@bearmetal/store@^0.0.5", "@bearmetal/store": "jsr:@bearmetal/store@^0.0.4",
"@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",
"@deno/vite-plugin": "npm:@deno/vite-plugin@^1.0.0", "@deno/vite-plugin": "npm:@deno/vite-plugin@^1.0.0",
"@gfx/canvas": "jsr:@gfx/canvas@^0.5.8",
"@preact/preset-vite": "npm:@preact/preset-vite@^2.9.1", "@preact/preset-vite": "npm:@preact/preset-vite@^2.9.1",
"@std/encoding": "jsr:@std/encoding@^1.0.5",
"@std/fs": "jsr:@std/fs@^1.0.4",
"@std/http": "jsr:@std/http@^1.0.8", "@std/http": "jsr:@std/http@^1.0.8",
"@std/path": "jsr:@std/path@^1.0.6",
"@zip.js/zip.js": "npm:@zip.js/zip.js@^2.7.52",
"autoprefixer": "npm:autoprefixer@^10.4.20", "autoprefixer": "npm:autoprefixer@^10.4.20",
"babel-plugin-transform-hook-names": "npm:babel-plugin-transform-hook-names@^1.0.2", "babel-plugin-transform-hook-names": "npm:babel-plugin-transform-hook-names@^1.0.2",
"jotai": "npm:jotai@^2.10.1",
"less": "npm:less@^4.2.0",
"postcss": "npm:postcss@^8.4.47", "postcss": "npm:postcss@^8.4.47",
"preact": "npm:preact@^10.24.3", "preact": "npm:preact@^10.24.3",
"react-router-dom": "npm:react-router-dom@^6.27.0", "react-router-dom": "npm:react-router-dom@^6.27.0",
"swr": "npm:swr@^2.2.5",
"tailwindcss": "npm:tailwindcss@^3.4.13", "tailwindcss": "npm:tailwindcss@^3.4.13",
"vite": "npm:vite@^5.4.8" "vite": "npm:vite@^5.4.8"
} }

213
deno.lock generated
View File

@ -1,125 +1,62 @@
{ {
"version": "4", "version": "4",
"specifiers": { "specifiers": {
"jsr:@bearmetal/store@^0.0.5": "0.0.5", "jsr:@bearmetal/store@^0.0.4": "0.0.4",
"jsr:@denosaurs/plug@1.0.5": "1.0.5",
"jsr:@gfx/canvas@~0.5.8": "0.5.8",
"jsr:@std/assert@0.214": "0.214.0",
"jsr:@std/assert@0.217": "0.217.0",
"jsr:@std/cli@^1.0.6": "1.0.6", "jsr:@std/cli@^1.0.6": "1.0.6",
"jsr:@std/encoding@0.214": "0.214.0",
"jsr:@std/encoding@0.217.0": "0.217.0",
"jsr:@std/encoding@^1.0.5": "1.0.5", "jsr:@std/encoding@^1.0.5": "1.0.5",
"jsr:@std/fmt@0.214": "0.214.0",
"jsr:@std/fmt@^1.0.2": "1.0.2", "jsr:@std/fmt@^1.0.2": "1.0.2",
"jsr:@std/fs@*": "1.0.4", "jsr:@std/fs@*": "1.0.4",
"jsr:@std/fs@0.214": "0.214.0",
"jsr:@std/fs@0.217.0": "0.217.0",
"jsr:@std/fs@^1.0.4": "1.0.4", "jsr:@std/fs@^1.0.4": "1.0.4",
"jsr:@std/http@^1.0.8": "1.0.8", "jsr:@std/http@^1.0.8": "1.0.8",
"jsr:@std/media-types@^1.0.3": "1.0.3", "jsr:@std/media-types@^1.0.3": "1.0.3",
"jsr:@std/net@^1.0.4": "1.0.4", "jsr:@std/net@^1.0.4": "1.0.4",
"jsr:@std/path@0.214": "0.214.0",
"jsr:@std/path@0.217": "0.217.0",
"jsr:@std/path@0.217.0": "0.217.0",
"jsr:@std/path@^1.0.6": "1.0.6", "jsr:@std/path@^1.0.6": "1.0.6",
"jsr:@std/streams@^1.0.7": "1.0.7", "jsr:@std/streams@^1.0.7": "1.0.7",
"npm:@babel/plugin-transform-react-jsx-development@^7.25.7": "7.25.7_@babel+core@7.25.8", "npm:@babel/plugin-transform-react-jsx-development@^7.25.7": "7.25.7_@babel+core@7.25.8",
"npm:@deno/vite-plugin@1": "1.0.0_vite@5.4.9", "npm:@deno/vite-plugin@1": "1.0.0_vite@5.4.9",
"npm:@preact/preset-vite@^2.9.1": "2.9.1_@babel+core@7.25.8_vite@5.4.9_preact@10.24.3", "npm:@preact/preset-vite@^2.9.1": "2.9.1_@babel+core@7.25.8_vite@5.4.9_preact@10.24.3",
"npm:@types/node@*": "22.5.4", "npm:@types/node@*": "22.5.4",
"npm:@zip.js/zip.js@^2.7.52": "2.7.52",
"npm:autoprefixer@^10.4.20": "10.4.20_postcss@8.4.47", "npm:autoprefixer@^10.4.20": "10.4.20_postcss@8.4.47",
"npm:babel-plugin-transform-hook-names@^1.0.2": "1.0.2_@babel+core@7.25.8", "npm:babel-plugin-transform-hook-names@^1.0.2": "1.0.2_@babel+core@7.25.8",
"npm:jotai@^2.10.1": "2.10.1",
"npm:less@^4.2.0": "4.2.0",
"npm:postcss@^8.4.47": "8.4.47", "npm:postcss@^8.4.47": "8.4.47",
"npm:preact@^10.24.3": "10.24.3", "npm:preact@^10.24.3": "10.24.3",
"npm:react-router-dom@^6.27.0": "6.27.0_react@18.3.1_react-dom@18.3.1__react@18.3.1", "npm:react-router-dom@^6.27.0": "6.27.0_react@18.3.1_react-dom@18.3.1__react@18.3.1",
"npm:swr@^2.2.5": "2.2.5_react@18.3.1",
"npm:tailwindcss@*": "3.4.13_postcss@8.4.47", "npm:tailwindcss@*": "3.4.13_postcss@8.4.47",
"npm:tailwindcss@^3.4.13": "3.4.13_postcss@8.4.47", "npm:tailwindcss@^3.4.13": "3.4.13_postcss@8.4.47",
"npm:vite@*": "5.4.9", "npm:vite@*": "5.4.9",
"npm:vite@^5.4.8": "5.4.9" "npm:vite@^5.4.8": "5.4.9"
}, },
"jsr": { "jsr": {
"@bearmetal/store@0.0.5": { "@bearmetal/store@0.0.4": {
"integrity": "d17da24c91bcc05707deb8a55017ebdf5d8eebd2f6293dcb2bbfac57e4e3b395", "integrity": "f5859476184d6f7b3957d18c7c82a37b6b89bb75e18db3186fde94ccb4253dab",
"dependencies": [ "dependencies": [
"jsr:@std/fs@^1.0.4" "jsr:@std/fs@^1.0.4"
] ]
}, },
"@denosaurs/plug@1.0.5": {
"integrity": "04cd988da558adc226202d88c3a434d5fcc08146eaf4baf0cea0c2284b16d2bf",
"dependencies": [
"jsr:@std/encoding@0.214",
"jsr:@std/fmt@0.214",
"jsr:@std/fs@0.214",
"jsr:@std/path@0.214"
]
},
"@gfx/canvas@0.5.8": {
"integrity": "a61c80292528e7433d428556b494a0ea496dd8e6abd4a338b8b25fc04e46ea3e",
"dependencies": [
"jsr:@denosaurs/plug",
"jsr:@std/encoding@0.217.0",
"jsr:@std/fs@0.217.0",
"jsr:@std/path@0.217.0"
]
},
"@std/assert@0.214.0": {
"integrity": "55d398de76a9828fd3b1aa653f4dba3eee4c6985d90c514865d2be9bd082b140"
},
"@std/assert@0.217.0": {
"integrity": "c98e279362ca6982d5285c3b89517b757c1e3477ee9f14eb2fdf80a45aaa9642"
},
"@std/cli@1.0.6": { "@std/cli@1.0.6": {
"integrity": "d22d8b38c66c666d7ad1f2a66c5b122da1704f985d3c47f01129f05abb6c5d3d" "integrity": "d22d8b38c66c666d7ad1f2a66c5b122da1704f985d3c47f01129f05abb6c5d3d"
}, },
"@std/encoding@0.214.0": {
"integrity": "30a8713e1db22986c7e780555ffd2fefd1d4f9374d734bb41f5970f6c3352af5"
},
"@std/encoding@0.217.0": {
"integrity": "b03e8ff94c98d6b6a02c02c5cf8e5d203400155516248964fc4559abc04669dc"
},
"@std/encoding@1.0.5": { "@std/encoding@1.0.5": {
"integrity": "ecf363d4fc25bd85bd915ff6733a7e79b67e0e7806334af15f4645c569fefc04" "integrity": "ecf363d4fc25bd85bd915ff6733a7e79b67e0e7806334af15f4645c569fefc04"
}, },
"@std/fmt@0.214.0": {
"integrity": "40382cff88a0783b347b4d69b94cf931ab8e549a733916718cb866c08efac4d4"
},
"@std/fmt@1.0.2": { "@std/fmt@1.0.2": {
"integrity": "87e9dfcdd3ca7c066e0c3c657c1f987c82888eb8103a3a3baa62684ffeb0f7a7" "integrity": "87e9dfcdd3ca7c066e0c3c657c1f987c82888eb8103a3a3baa62684ffeb0f7a7"
}, },
"@std/fs@0.214.0": {
"integrity": "bc880fea0be120cb1550b1ed7faf92fe071003d83f2456a1e129b39193d85bea",
"dependencies": [
"jsr:@std/assert@0.214",
"jsr:@std/path@0.214"
]
},
"@std/fs@0.217.0": {
"integrity": "0bfff5f3618d68c385b28b4ffbf3a15c98293a0f1186444458b62e0111ce77b2",
"dependencies": [
"jsr:@std/assert@0.217",
"jsr:@std/path@0.217"
]
},
"@std/fs@1.0.4": { "@std/fs@1.0.4": {
"integrity": "2907d32d8d1d9e540588fd5fe0ec21ee638134bd51df327ad4e443aaef07123c", "integrity": "2907d32d8d1d9e540588fd5fe0ec21ee638134bd51df327ad4e443aaef07123c",
"dependencies": [ "dependencies": [
"jsr:@std/path@^1.0.6" "jsr:@std/path"
] ]
}, },
"@std/http@1.0.8": { "@std/http@1.0.8": {
"integrity": "6ea1b2e8d33929967754a3b6d6c6f399ad6647d7bbb5a466c1eaf9b294a6ebcd", "integrity": "6ea1b2e8d33929967754a3b6d6c6f399ad6647d7bbb5a466c1eaf9b294a6ebcd",
"dependencies": [ "dependencies": [
"jsr:@std/cli", "jsr:@std/cli",
"jsr:@std/encoding@^1.0.5", "jsr:@std/encoding",
"jsr:@std/fmt@^1.0.2", "jsr:@std/fmt",
"jsr:@std/media-types", "jsr:@std/media-types",
"jsr:@std/net", "jsr:@std/net",
"jsr:@std/path@^1.0.6", "jsr:@std/path",
"jsr:@std/streams" "jsr:@std/streams"
] ]
}, },
@ -129,18 +66,6 @@
"@std/net@1.0.4": { "@std/net@1.0.4": {
"integrity": "2f403b455ebbccf83d8a027d29c5a9e3a2452fea39bb2da7f2c04af09c8bc852" "integrity": "2f403b455ebbccf83d8a027d29c5a9e3a2452fea39bb2da7f2c04af09c8bc852"
}, },
"@std/path@0.214.0": {
"integrity": "d5577c0b8d66f7e8e3586d864ebdf178bb326145a3611da5a51c961740300285",
"dependencies": [
"jsr:@std/assert@0.214"
]
},
"@std/path@0.217.0": {
"integrity": "1217cc25534bca9a2f672d7fe7c6f356e4027df400c0e85c0ef3e4343bc67d11",
"dependencies": [
"jsr:@std/assert@0.217"
]
},
"@std/path@1.0.6": { "@std/path@1.0.6": {
"integrity": "ab2c55f902b380cf28e0eec501b4906e4c1960d13f00e11cfbcd21de15f18fed" "integrity": "ab2c55f902b380cf28e0eec501b4906e4c1960d13f00e11cfbcd21de15f18fed"
}, },
@ -186,7 +111,7 @@
"debug", "debug",
"gensync", "gensync",
"json5", "json5",
"semver@6.3.1" "semver"
] ]
}, },
"@babel/generator@7.25.7": { "@babel/generator@7.25.7": {
@ -211,7 +136,7 @@
"@babel/helper-validator-option", "@babel/helper-validator-option",
"browserslist", "browserslist",
"lru-cache@5.1.1", "lru-cache@5.1.1",
"semver@6.3.1" "semver"
] ]
}, },
"@babel/helper-module-imports@7.25.7": { "@babel/helper-module-imports@7.25.7": {
@ -469,7 +394,7 @@
"kolorist", "kolorist",
"magic-string", "magic-string",
"node-html-parser", "node-html-parser",
"source-map@0.7.4", "source-map",
"stack-trace", "stack-trace",
"vite" "vite"
] ]
@ -565,9 +490,6 @@
"undici-types" "undici-types"
] ]
}, },
"@zip.js/zip.js@2.7.52": {
"integrity": "sha512-+5g7FQswvrCHwYKNMd/KFxZSObctLSsQOgqBSi0LzwHo3li9Eh1w5cF5ndjQw9Zbr3ajVnd2+XyiX85gAetx1Q=="
},
"ansi-regex@5.0.1": { "ansi-regex@5.0.1": {
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="
}, },
@ -677,9 +599,6 @@
"readdirp" "readdirp"
] ]
}, },
"client-only@0.0.1": {
"integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="
},
"color-convert@1.9.3": { "color-convert@1.9.3": {
"integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
"dependencies": [ "dependencies": [
@ -704,12 +623,6 @@
"convert-source-map@2.0.0": { "convert-source-map@2.0.0": {
"integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==" "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="
}, },
"copy-anything@2.0.6": {
"integrity": "sha512-1j20GZTsvKNkc4BY3NpMOM8tt///wY3FpIzozTOFO2ffuZcV61nojHXVKIy3WM+7ADCy5FVhdZYHYDdgTU0yJw==",
"dependencies": [
"is-what"
]
},
"cross-spawn@7.0.3": { "cross-spawn@7.0.3": {
"integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
"dependencies": [ "dependencies": [
@ -786,12 +699,6 @@
"entities@4.5.0": { "entities@4.5.0": {
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==" "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="
}, },
"errno@0.1.8": {
"integrity": "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==",
"dependencies": [
"prr"
]
},
"esbuild@0.21.5": { "esbuild@0.21.5": {
"integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
"dependencies": [ "dependencies": [
@ -896,9 +803,6 @@
"globals@11.12.0": { "globals@11.12.0": {
"integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==" "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA=="
}, },
"graceful-fs@4.2.11": {
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="
},
"has-flag@3.0.0": { "has-flag@3.0.0": {
"integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==" "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw=="
}, },
@ -911,15 +815,6 @@
"he@1.2.0": { "he@1.2.0": {
"integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==" "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw=="
}, },
"iconv-lite@0.6.3": {
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
"dependencies": [
"safer-buffer"
]
},
"image-size@0.5.5": {
"integrity": "sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ=="
},
"is-binary-path@2.1.0": { "is-binary-path@2.1.0": {
"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
"dependencies": [ "dependencies": [
@ -947,9 +842,6 @@
"is-number@7.0.0": { "is-number@7.0.0": {
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==" "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="
}, },
"is-what@3.14.1": {
"integrity": "sha512-sNxgpk9793nzSs7bA6JQJGeIuRBQhAaNGG77kzYQgMkrID+lS6SlK07K5LaptscDlSaIgH+GPFzf+d75FVxozA=="
},
"isexe@2.0.0": { "isexe@2.0.0": {
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="
}, },
@ -963,9 +855,6 @@
"jiti@1.21.6": { "jiti@1.21.6": {
"integrity": "sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==" "integrity": "sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w=="
}, },
"jotai@2.10.1": {
"integrity": "sha512-4FycO+BOTl2auLyF2Chvi6KTDqdsdDDtpaL/WHQMs8f3KS1E3loiUShQzAzFA/sMU5cJ0hz/RT1xum9YbG/zaA=="
},
"js-tokens@4.0.0": { "js-tokens@4.0.0": {
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
}, },
@ -978,21 +867,6 @@
"kolorist@1.8.0": { "kolorist@1.8.0": {
"integrity": "sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==" "integrity": "sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ=="
}, },
"less@4.2.0": {
"integrity": "sha512-P3b3HJDBtSzsXUl0im2L7gTO5Ubg8mEN6G8qoTS77iXxXX4Hvu4Qj540PZDvQ8V6DmX6iXo98k7Md0Cm1PrLaA==",
"dependencies": [
"copy-anything",
"errno",
"graceful-fs",
"image-size",
"make-dir",
"mime",
"needle",
"parse-node-version",
"source-map@0.6.1",
"tslib"
]
},
"lilconfig@2.1.0": { "lilconfig@2.1.0": {
"integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==" "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ=="
}, },
@ -1023,13 +897,6 @@
"@jridgewell/sourcemap-codec" "@jridgewell/sourcemap-codec"
] ]
}, },
"make-dir@2.1.0": {
"integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==",
"dependencies": [
"pify@4.0.1",
"semver@5.7.2"
]
},
"merge2@1.4.1": { "merge2@1.4.1": {
"integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==" "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="
}, },
@ -1040,9 +907,6 @@
"picomatch" "picomatch"
] ]
}, },
"mime@1.6.0": {
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="
},
"minimatch@9.0.5": { "minimatch@9.0.5": {
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
"dependencies": [ "dependencies": [
@ -1066,13 +930,6 @@
"nanoid@3.3.7": { "nanoid@3.3.7": {
"integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==" "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g=="
}, },
"needle@3.3.1": {
"integrity": "sha512-6k0YULvhpw+RoLNiQCRKOl09Rv1dPLr8hHnVjHqdolKwDrdNyk+Hmrthi4lIGPPz3r39dLx0hsF5s40sZ3Us4Q==",
"dependencies": [
"iconv-lite",
"sax"
]
},
"node-html-parser@6.1.13": { "node-html-parser@6.1.13": {
"integrity": "sha512-qIsTMOY4C/dAa5Q5vsobRpOOvPfC4pB61UVW2uSwZNUp0QU/jCekTal1vMmbO0DgdHeLUJpv/ARmDqErVxA3Sg==", "integrity": "sha512-qIsTMOY4C/dAa5Q5vsobRpOOvPfC4pB61UVW2uSwZNUp0QU/jCekTal1vMmbO0DgdHeLUJpv/ARmDqErVxA3Sg==",
"dependencies": [ "dependencies": [
@ -1104,9 +961,6 @@
"package-json-from-dist@1.0.1": { "package-json-from-dist@1.0.1": {
"integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==" "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="
}, },
"parse-node-version@1.0.1": {
"integrity": "sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA=="
},
"path-key@3.1.1": { "path-key@3.1.1": {
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==" "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="
}, },
@ -1129,9 +983,6 @@
"pify@2.3.0": { "pify@2.3.0": {
"integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==" "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog=="
}, },
"pify@4.0.1": {
"integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g=="
},
"pirates@4.0.6": { "pirates@4.0.6": {
"integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==" "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg=="
}, },
@ -1187,9 +1038,6 @@
"preact@10.24.3": { "preact@10.24.3": {
"integrity": "sha512-Z2dPnBnMUfyQfSQ+GBdsGa16hz35YmLmtTLhM169uW944hYL6xzTYkJjC07j+Wosz733pMWx0fgON3JNw1jJQA==" "integrity": "sha512-Z2dPnBnMUfyQfSQ+GBdsGa16hz35YmLmtTLhM169uW944hYL6xzTYkJjC07j+Wosz733pMWx0fgON3JNw1jJQA=="
}, },
"prr@1.0.1": {
"integrity": "sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw=="
},
"queue-microtask@1.2.3": { "queue-microtask@1.2.3": {
"integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==" "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="
}, },
@ -1226,7 +1074,7 @@
"read-cache@1.0.0": { "read-cache@1.0.0": {
"integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==",
"dependencies": [ "dependencies": [
"pify@2.3.0" "pify"
] ]
}, },
"readdirp@3.6.0": { "readdirp@3.6.0": {
@ -1275,21 +1123,12 @@
"queue-microtask" "queue-microtask"
] ]
}, },
"safer-buffer@2.1.2": {
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
},
"sax@1.4.1": {
"integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg=="
},
"scheduler@0.23.2": { "scheduler@0.23.2": {
"integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
"dependencies": [ "dependencies": [
"loose-envify" "loose-envify"
] ]
}, },
"semver@5.7.2": {
"integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g=="
},
"semver@6.3.1": { "semver@6.3.1": {
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==" "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="
}, },
@ -1308,9 +1147,6 @@
"source-map-js@1.2.1": { "source-map-js@1.2.1": {
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==" "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="
}, },
"source-map@0.6.1": {
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
},
"source-map@0.7.4": { "source-map@0.7.4": {
"integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==" "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA=="
}, },
@ -1366,14 +1202,6 @@
"supports-preserve-symlinks-flag@1.0.0": { "supports-preserve-symlinks-flag@1.0.0": {
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==" "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="
}, },
"swr@2.2.5_react@18.3.1": {
"integrity": "sha512-QtxqyclFeAsxEUeZIYmsaQ0UjimSq1RZ9Un7I68/0ClKK/U3LoyQunwkQfJZr2fc22DfIXLNDc2wFyTEikCUpg==",
"dependencies": [
"client-only",
"react",
"use-sync-external-store"
]
},
"tailwindcss@3.4.13_postcss@8.4.47": { "tailwindcss@3.4.13_postcss@8.4.47": {
"integrity": "sha512-KqjHOJKogOUt5Bs752ykCeiwvi0fKVkr5oqsFNt/8px/tA8scFPIlkygsf6jXrfCqGHz7VflA6+yytWuM+XhFw==", "integrity": "sha512-KqjHOJKogOUt5Bs752ykCeiwvi0fKVkr5oqsFNt/8px/tA8scFPIlkygsf6jXrfCqGHz7VflA6+yytWuM+XhFw==",
"dependencies": [ "dependencies": [
@ -1425,9 +1253,6 @@
"ts-interface-checker@0.1.13": { "ts-interface-checker@0.1.13": {
"integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==" "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA=="
}, },
"tslib@2.8.0": {
"integrity": "sha512-jWVzBLplnCmoaTr13V9dYbiQ99wvZRd0vNWaDRg+aVYRcjDF3nDksxFDE/+fkXnKhpnUUkmx5pK/v8mCtLVqZA=="
},
"undici-types@6.19.8": { "undici-types@6.19.8": {
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==" "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw=="
}, },
@ -1439,12 +1264,6 @@
"picocolors" "picocolors"
] ]
}, },
"use-sync-external-store@1.2.2_react@18.3.1": {
"integrity": "sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw==",
"dependencies": [
"react"
]
},
"util-deprecate@1.0.2": { "util-deprecate@1.0.2": {
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
}, },
@ -1488,24 +1307,16 @@
}, },
"workspace": { "workspace": {
"dependencies": [ "dependencies": [
"jsr:@bearmetal/store@^0.0.5", "jsr:@bearmetal/store@^0.0.4",
"jsr:@gfx/canvas@~0.5.8",
"jsr:@std/encoding@^1.0.5",
"jsr:@std/fs@^1.0.4",
"jsr:@std/http@^1.0.8", "jsr:@std/http@^1.0.8",
"jsr:@std/path@^1.0.6",
"npm:@babel/plugin-transform-react-jsx-development@^7.25.7", "npm:@babel/plugin-transform-react-jsx-development@^7.25.7",
"npm:@deno/vite-plugin@1", "npm:@deno/vite-plugin@1",
"npm:@preact/preset-vite@^2.9.1", "npm:@preact/preset-vite@^2.9.1",
"npm:@zip.js/zip.js@^2.7.52",
"npm:autoprefixer@^10.4.20", "npm:autoprefixer@^10.4.20",
"npm:babel-plugin-transform-hook-names@^1.0.2", "npm:babel-plugin-transform-hook-names@^1.0.2",
"npm:jotai@^2.10.1",
"npm:less@^4.2.0",
"npm:postcss@^8.4.47", "npm:postcss@^8.4.47",
"npm:preact@^10.24.3", "npm:preact@^10.24.3",
"npm:react-router-dom@^6.27.0", "npm:react-router-dom@^6.27.0",
"npm:swr@^2.2.5",
"npm:tailwindcss@^3.4.13", "npm:tailwindcss@^3.4.13",
"npm:vite@^5.4.8" "npm:vite@^5.4.8"
] ]

View File

@ -5,9 +5,6 @@
<link rel="icon" type="image/svg+xml" href="/vite.svg" /> <link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + Preact + TS</title> <title>Vite + Preact + TS</title>
<style lang="less">
@import './src/index.less';
</style>
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>

66
main.ts Normal file
View File

@ -0,0 +1,66 @@
import { SockpuppetPlus } from "@cgg/sockpuppet";
import { serveDir } from "@std/http/file-server";
import { BearMetalStore } from "@bearmetal/store";
const installPath = Deno.env.get("BMP_INSTALL_DIR") || "./";
const sockpuppet = new SockpuppetPlus();
sockpuppet.addHandler((req: Request) => {
if (new URL(req.url).pathname.startsWith("/api")) return;
return serveDir(req, {
fsRoot: installPath + "dist",
});
});
sockpuppet.addHandler(async (req: Request) => {
if (!new URL(req.url).pathname.startsWith("/api")) return;
const store = new BearMetalStore();
console.log("API", req.url);
const API_DIR_ROUTE = new URLPattern({
pathname: "/api/dir",
search: "?list",
});
const match = API_DIR_ROUTE.exec(req.url);
if (!match) return;
switch (req.method) {
case "GET": {
const mcPath = store.get("mcPath") as string;
if (mcPath) {
for await (const file of Deno.readDir(mcPath)) {
if (file.isDirectory && file.name.startsWith("saves")) {
const worlds: string[] = [];
for await (const world of Deno.readDir(mcPath + "/" + file.name)) {
if (world.isDirectory) {
worlds.push(world.name);
}
}
return new Response(JSON.stringify({ worlds, mcPath }), {
status: 200,
});
}
}
}
return new Response(JSON.stringify({ mcPath }), {
status: mcPath ? 200 : 500,
});
}
case "POST": {
const formData = await req.formData();
const dir = formData.get("dir") as string;
if (!dir) return new Response(null, { status: 400 });
Deno.env.set("MC_PATH", dir);
const mcPath = Deno.env.get("MC_PATH");
if (!mcPath) return new Response(null, { status: 500 });
return new Response(null, { status: 200 });
}
default:
return new Response(null, { status: 405 });
}
});

View File

@ -1,63 +0,0 @@
{
"version": "48",
"mcVersion": "^1.21",
"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,251 +0,0 @@
import { SockpuppetPlus } from "@cgg/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 { getPackVersion } from "./util/packVersion.ts";
import { createTagRoutes } from "./tags/routes.ts";
import { createResourcesRoutes } from "./resources/routes.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);
});
// 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 router = new Router();
router.route("/api/dir")
.get(async () => {
using store = new BearMetalStore();
const mcPath = store.get("mcPath") as string;
if (mcPath) {
const worlds: {
world: string;
icon: string;
path: string;
}[] = [];
try {
for await (const file of Deno.readDir(mcPath)) {
if (file.isDirectory && file.name.startsWith("saves")) {
for await (
const world of Deno.readDir(mcPath + "/" + file.name)
) {
if (world.isDirectory) {
for await (
const f of Deno.readDir(
mcPath + "/" + file.name + "/" + world.name,
)
) {
if (f.name.endsWith(".dat")) {
worlds.push({
world: world.name,
icon: Deno.realPathSync(
mcPath + "/" + file.name + "/" + world.name +
"/icon.png",
),
path: Deno.realPathSync(
mcPath + "/" + file.name + "/" + world.name,
),
});
}
}
}
}
}
}
} catch (e: any) {
store.set("mcPath", "");
return new Response(e, { status: 500 });
}
return new Response(JSON.stringify({ worlds, mcPath }), {
status: 200,
});
}
return new Response(JSON.stringify({ mcPath }), {
status: mcPath ? 200 : 500,
});
})
.post(async (req) => {
using store = new BearMetalStore();
const formData = await req.formData();
const dir = formData.get("mcPath") as string;
if (!dir) return new Response(null, { status: 400 });
store.set("mcPath", dir);
if (!store.get("mcPath")) return new Response(null, { status: 500 });
return new Response(null, { status: 200 });
});
router.route("/api/world")
.post(async (req) => {
using store = new BearMetalStore();
const worldPath = await req.text();
if (!worldPath) return new Response(null, { status: 400 });
const mcPath = store.get("mcPath") as string;
if (!mcPath) {
return new Response("Tried to set world, but MC path is not set.", {
status: 500,
});
}
const realWorldPath = Deno.realPathSync(worldPath);
store.set("world", realWorldPath);
return new Response(null, { status: 200 });
})
.get((req) => {
using store = new BearMetalStore();
const worldPath = store.get("world") as string;
if (!worldPath) return new Response(null, { status: 400 });
return new Response(worldPath.split("/").pop() as string, { status: 200 });
});
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/pack/namespaces")
.get(async () => {
using store = new BearMetalStore();
const namespaces = Array.from(Deno.readDirSync(store.get("packlocation")))
.filter((dir) => dir.isDirectory).map((dir) => dir.name);
return new Response(JSON.stringify(namespaces), { status: 200 });
})
.post(async (req) => {
using store = new BearMetalStore();
const namespace = await req.text();
if (!namespace) {
return new Response("Namespace is required", { status: 400 });
}
const namespaceRx = /^[a-zA-Z0-9_\-]+$/;
if (!namespaceRx.test(namespace)) {
return new Response(
"Namespace must only contain letters, numbers, underscores, and dashes",
{ status: 400 },
);
}
await ensureDir(store.get("packlocation") + "/" + namespace);
return new Response(null, { 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 version = getPackVersion(store);
return new Response(version.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 });
});
createTagRoutes(router);
createResourcesRoutes(router);
sockpuppet.addHandler((req: Request) => {
if (new URL(req.url).pathname.startsWith("/api")) return;
return serveDir(req, {
fsRoot: installPath + "dist",
});
});
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

@ -1,359 +0,0 @@
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) => {
const blocks: BlockItem[] =
(await readDirFiles(path + "/assets/minecraft/blockstates"))
.map((b) => ({
name: b.replace(".json", ""),
resourceLocation: "minecraft:" + b.replace(".json", ""),
images: [],
}));
const blockTextures = await readDirFiles(
path + "/assets/minecraft/textures/block",
);
const modelPath = path + "/assets/minecraft/models/block";
for (const block of blocks) {
let name = block.name;
if (
block.name.startsWith("air") ||
block.name.startsWith("void_air") ||
block.name.startsWith("cave_air") ||
block.name.startsWith("end_gateway") ||
block.name.endsWith("_door") ||
block.name.endsWith("_trapdoor") ||
block.name.endsWith("_banner") ||
block.name.endsWith("_fence_gate") ||
block.name.endsWith("_pressure_plate") ||
block.name.endsWith("_button") ||
block.name.endsWith("lever") ||
block.name.endsWith("_sign") ||
block.name.endsWith("torch") ||
block.name.endsWith("_fence") ||
block.name.endsWith("_wall") ||
block.name.endsWith("_skull") ||
block.name.endsWith("_head") ||
block.name.includes("bubble_column") ||
block.name.includes("tripwire_hook") ||
block.name.includes("tripwire") ||
block.name == undefined ||
block.name.includes("chest") ||
block.name.includes("command")
) continue;
if (block.name.includes("water") || block.name.includes("lava")) {
const what = block.name.includes("water") ? "water" : "lava";
const itemTexLoc = path +
`/assets/minecraft/textures/block/${what}_still.png`;
const itemImage = await Deno.readFile(itemTexLoc);
block.images.push("data:image/png;base64," + encodeBase64(itemImage));
continue;
}
if (block.name === "nether_portal") {
const itemTexLoc = path +
`/assets/minecraft/textures/block/nether_portal.png`;
const itemImage = await Deno.readFile(itemTexLoc);
block.images.push("data:image/png;base64," + encodeBase64(itemImage));
continue;
}
if (["fire", "soul_fire"].includes(block.name)) {
const itemTexLoc = path +
`/assets/minecraft/textures/block/${block.name}_0.png`;
const itemImage = await Deno.readFile(itemTexLoc);
block.images.push("data:image/png;base64," + encodeBase64(itemImage));
continue;
}
if (block.name.includes("cake")) {
const itemTexLoc = path +
`/assets/minecraft/textures/item/cake.png`;
const itemImage = await Deno.readFile(itemTexLoc);
block.images.push("data:image/png;base64," + encodeBase64(itemImage));
continue;
}
if (block.name.endsWith("_bed")) {
const itemTexLoc = path + "/assets/minecraft/textures/entity/bed/" +
block.name.replace(/_bed$/, "") + ".png";
const itemImage = await Deno.readFile(itemTexLoc);
block.images.push("data:image/png;base64," + encodeBase64(itemImage));
continue;
}
if (block.name === "nether_portal") {
const itemTexLoc = path +
`/assets/minecraft/textures/block/nether_portal.png`;
const itemImage = await Deno.readFile(itemTexLoc);
block.images.push("data:image/png;base64," + encodeBase64(itemImage));
continue;
}
if (block.name.includes("dripleaf") && !block.name.includes("stem")) {
const itemTexLoc = path +
`/assets/minecraft/textures/block/${block.name}_top.png`;
const itemImage = await Deno.readFile(itemTexLoc);
block.images.push("data:image/png;base64," + encodeBase64(itemImage));
continue;
}
if (
[
"carrots",
"potatoes",
"wheat",
"beetroots",
"grass",
"kelp",
"light",
"bell",
"redstone_wire",
"sweet_berry_bush",
"cocoa",
"nether_wart",
"repeater",
"bamboo",
"pitcher_plant",
"campfire",
"soul_campfire",
].includes(block.name)
) {
const itemTexLoc = path + "/assets/minecraft/textures/item/" +
block.name
.replace("large_fern", "fern")
.replace("tall_seagrass", "seagrass")
.replace(/ss$/, "?")
.replace(/e?s$/, "")
.replace("?", "ss")
.replace("_bush", "")
.replace("berry", "berries")
.replace("cocoa", "cocoa_beans")
.replace("_wire", "") +
".png";
const itemImage = await Deno.readFile(itemTexLoc);
block.images.push("data:image/png;base64," + encodeBase64(itemImage));
continue;
}
if (
[
"melon_stem",
"pumpkin_stem",
"fern",
"large_fern",
"lilac",
"azure_bluet",
"red_tulip",
"orange_tulip",
"pink_tulip",
"white_tulip",
"oxeye_daisy",
"cornflower",
"lily_of_the_valley",
"wither_rose",
"rose_bush",
"peony",
"sunflower",
"torchflower_crop",
"suspicious_sand",
"suspicious_gravel",
"scaffolding",
"respawn_anchor",
"short_grass",
"tall_grass",
"seagrass",
"tall_seagrass",
"frosted_ice",
"pitcher_crop",
"iron_bars",
].includes(block.name)
) {
const textures = blockTextures.filter((t) => t.includes(block.name))
.reverse().filter((t) => !t.endsWith("mcmeta"));
const itemTexLoc = path + "/assets/minecraft/textures/block/" +
textures[0];
const itemImage = await Deno.readFile(itemTexLoc);
block.images.push("data:image/png;base64," + encodeBase64(itemImage));
continue;
}
if (block.name.startsWith("waxed")) {
name = block.name.replace(/^waxed_?/, "");
}
if (block.name.startsWith("infested")) {
name = block.name.replace(/^infested_?/, "");
}
if (block.name.endsWith("_pane")) {
name = block.name.replace(/_pane$/, "");
}
if (block.name === "pink_petals") {
name = "pink_petals_4";
}
if (block.name.includes("candle")) {
const itemTexLoc = path +
`/assets/minecraft/textures/item/${block.name}.png`;
const itemImage = await Deno.readFile(itemTexLoc);
block.images.push("data:image/png;base64," + encodeBase64(itemImage));
continue;
}
if (block.name.includes("cauldron")) {
const textures = blockTextures.filter((t) => t.includes("cauldron"));
for (const texture of textures) {
const texLoc = texture.replace("minecraft:", "");
const image = await Deno.readFile(
path + "/assets/minecraft/textures/block/" + texLoc,
);
const b64 = "data:image/png;base64," + encodeBase64(image);
block.images.push(await createIsometricCube(b64, b64, b64));
}
continue;
}
if (block.name === "snow") {
name = "snow_height2";
}
const data = await Deno.readTextFile(
modelPath + "/" + name + ".json",
);
const modelJson = JSON.parse(data);
if (modelJson.textures && !modelJson.elements) {
if (modelJson.textures.all) {
const texLoc = modelJson.textures.all;
const image = await Deno.readFile(
path + "/assets/minecraft/textures/" + texLoc.replace(
"minecraft:",
"",
) + ".png",
);
const b64 = "data:image/png;base64," + encodeBase64(image);
block.images.push(await createIsometricCube(b64, b64, b64));
continue;
}
if (modelJson.textures.front) {
const frontImage = await Deno.readFile(
path + "/assets/minecraft/textures/" +
modelJson.textures.front.replace(
"minecraft:",
"",
) + ".png",
);
const frontB64 = "data:image/png;base64," + encodeBase64(frontImage);
const topImage = await Deno.readFile(
path + "/assets/minecraft/textures/" +
(modelJson.textures.top ?? modelJson.textures.side).replace(
"minecraft:",
"",
) + ".png",
);
const topB64 = "data:image/png;base64," + encodeBase64(topImage);
const sideImage = await Deno.readFile(
path + "/assets/minecraft/textures/" +
modelJson.textures.side.replace(
"minecraft:",
"",
) + ".png",
);
const sideB64 = "data:image/png;base64," + encodeBase64(sideImage);
block.images.push(await createIsometricCube(frontB64, sideB64, topB64));
continue;
}
if (modelJson.textures.side) {
const sideImage = await Deno.readFile(
path + "/assets/minecraft/textures/" +
modelJson.textures.side.replace(
"minecraft:",
"",
) + ".png",
);
const sideB64 = "data:image/png;base64," + encodeBase64(sideImage);
const topImage = await Deno.readFile(
path + "/assets/minecraft/textures/" +
(modelJson.textures.top ?? modelJson.textures.bottom ??
modelJson.textures.side).replace(
"minecraft:",
"",
) +
".png",
);
const topB64 = "data:image/png;base64," + encodeBase64(topImage);
block.images.push(await createIsometricCube(sideB64, sideB64, topB64));
continue;
}
}
const textures = blockTextures.filter((t) =>
t.includes(block.name) && !t.includes("mcmeta")
);
for (const texture of textures) {
const texLoc = texture.replace("minecraft:", "");
const image = await Deno.readFile(
path + "/assets/minecraft/textures/block/" + texLoc,
);
const b64 = "data:image/png;base64," + encodeBase64(image);
block.images.push(await createIsometricCube(b64, b64, b64));
}
}
return blocks;
};
export const readItems = async (path: string) => {
const items: BlockItem[] =
(await readDirFiles(path + "/assets/minecraft/models/item")).map((i) => ({
name: i.replace(".json", ""),
resourceLocation: "minecraft:" + i.replace(".json", ""),
images: [],
}));
for (const item of items) {
let name = item.name;
if (item.name.startsWith("air")) continue;
if (item.name.startsWith("waxed")) name = item.name.replace(/^waxed_?/, "");
const data = await Deno.readFile(
path + "/assets/minecraft/models/item/" + name + ".json",
);
const json = JSON.parse(new TextDecoder().decode(data));
const texDir = path + "/assets/minecraft/textures/";
if (json.textures) {
const texLoc = json.textures.layer0;
if (texLoc) {
const data = await Deno.readFile(
texDir + texLoc.replace("minecraft:", "") + ".png",
);
item.images.push("data:image/png;base64," + encodeBase64(data));
}
} else if (json.parent) {
const parent = await Deno.readFile(
path + "/assets/minecraft/models/" +
json.parent.replace("minecraft:", "") + ".json",
);
const parentJson = JSON.parse(new TextDecoder().decode(parent));
if (parentJson.textures) {
let texLoc = parentJson.textures.all;
if (!texLoc) texLoc = parentJson.textures.side;
if (!texLoc) texLoc = parentJson.textures.top;
if (!texLoc) texLoc = parentJson.textures.bottom;
if (texLoc) {
const data = await Deno.readFile(
texDir + texLoc.replace("minecraft:", "") + ".png",
);
item.images.push("data:image/png;base64," + encodeBase64(data));
}
}
}
}
return items;
};
if (import.meta.main) {
const path = "./resources/1.21.1";
console.log(await readItems(path));
}

View File

@ -1,81 +0,0 @@
import { createCanvas, Image } from "@gfx/canvas";
function loadImage(src: string): Promise<Image> {
return new Promise((resolve, reject) => {
if (!src.startsWith("data:image/png;base64,")) {
console.log("src", src);
}
const img = new Image();
img.onload = () => resolve(img);
img.onerror = () => {
console.log(src);
reject();
};
img.src = src;
});
}
export async function createIsometricCube(
face1Src: string,
face2Src: string,
face3Src: string,
width: number = 96,
) {
const canvasWidth = width; // Set a size for the canvas
const canvasHeight = Math.ceil(width / .866);
const canvas = createCanvas(canvasWidth, canvasHeight);
const ctx = canvas.getContext("2d");
try {
const [face1, face2, face3] = await Promise.all([
loadImage(face1Src),
loadImage(face2Src),
loadImage(face3Src),
]).catch();
ctx.clearRect(0, 0, canvasWidth, canvasHeight);
ctx.setTransform(
1,
0,
Math.tan(degToRad(30)),
1,
0,
canvasWidth / 3.5,
);
ctx.drawImage(face1, 0, 0, canvasWidth / 2, canvasHeight / 2);
// Face 2
ctx.setTransform(
1,
0,
-Math.tan(degToRad(30)),
1,
canvasWidth / 2,
canvasHeight / 2,
);
ctx.drawImage(face2, 0, 0, canvasWidth / 2, canvasHeight / 2);
// Face 3
ctx.setTransform(
1,
-Math.tan(degToRad(60)),
Math.tan(degToRad(30)),
1,
canvasWidth / 2,
0,
);
ctx.drawImage(face3, 0, 0, canvasWidth / 2, canvasWidth / 3.5);
ctx.setTransform(1, 0, 0, 1, 0, 0);
} catch (e) {
// console.log("error", e);
}
const b64 = canvas.toDataURL("png");
return b64;
}
function degToRad(deg: number) {
return deg * (Math.PI / 180);
}

View File

@ -1,48 +0,0 @@
import type { Router } from "../router.ts";
import { readDirDirs } from "../util/readDir.ts";
import { versionCompat } from "../util/versionCompat.ts";
import { readBlocks } from "./readers.ts";
export const createResourcesRoutes = (router: Router) => {
router.route("/api/resources/:path*")
.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;
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)),
);
}
case "item":
case "items": {
return new Response(
JSON.stringify(await readBlocks(resourcePath)),
);
}
default: {
return new Response("invalid path", { status: 400 });
}
}
}
}
});
};

View File

@ -1,58 +0,0 @@
import { BearMetalStore } from "@bearmetal/store";
import { ZipReader } from "@zip.js/zip.js";
import { ensureFile } from "@std/fs";
export async function unzipResources(mcVersion?: string) {
using store = new BearMetalStore();
mcVersion = mcVersion || await currentVersion(store);
console.log("mcVersion", mcVersion);
if (!mcVersion) return;
const blob = await Deno.open(
store.get("mcPath") + "/versions/" + mcVersion + "/" + mcVersion + ".jar",
);
const zip = new ZipReader(blob);
for (const entry of await zip.getEntries()) {
if (
entry.filename.startsWith("assets/") || entry.filename.startsWith("data/")
) {
// console.log("entry", entry);
await ensureFile(`./resources/${mcVersion}/${entry.filename}`);
const writer = await Deno.open(
`./resources/${mcVersion}/${entry.filename}`,
{ write: true },
);
await entry.getData?.(writer);
}
}
}
async function currentVersion(store: BearMetalStore) {
const mcPath = store.get("mcPath");
if (!mcPath) return;
const versions = Array.from(Deno.readDirSync(mcPath + "/versions")).filter(
(d) => d.isDirectory,
).map((d) => d.name).sort();
let version = versions.pop();
let found = false;
versionC:
while (!found) {
for await (const file of Deno.readDir(mcPath + "/versions/" + version)) {
if (file.name.endsWith(".jar")) {
found = true;
break versionC;
}
}
version = versions.pop();
}
return version;
}
if (import.meta.main) {
unzipResources();
}

View File

@ -1,72 +0,0 @@
export class Router {
private routes: Map<string, Handler[]> = new Map();
public route(route: string) {
const methods: Record<string, Handler> = {
get: () => undefined,
post: () => undefined,
put: () => undefined,
delete: () => undefined,
};
this.routes.set(route, this.routes.get(route) || []);
this.routes.get(route)?.push((r, c) => {
switch (r.method) {
case "GET":
return methods.get?.(r, c);
case "POST":
return methods.post?.(r, c);
case "PUT":
return methods.put?.(r, c);
case "DELETE":
return methods.delete?.(r, c);
default:
return undefined;
}
});
return {
get(handler: Handler) {
methods.get = handler;
return this;
},
post(handler: Handler) {
methods.post = handler;
return this;
},
put(handler: Handler) {
methods.put = handler;
return this;
},
delete(handler: Handler) {
methods.delete = handler;
return this;
},
};
}
public handle = async (req: Request): Promise<Response> => {
const url = new URL(req.url);
for (const [route, handlers] of this.routes.entries()) {
const pattern = new URLPattern({ pathname: route });
const match = pattern.exec(req.url);
if (match) {
let res;
for (const handler of handlers) {
res = await handler(req, {
url,
state: {},
params: match.pathname.groups,
});
if (res) {
return res;
}
}
}
}
return new Response("Not found", { status: 404 });
};
}
export type Handler = (
req: Request,
ctx: Context,
) => Promise<Response | undefined> | Response | undefined;

View File

@ -1,14 +0,0 @@
import type { BearMetalStore } from "@bearmetal/store";
import { getDirName } from "../util/packVersion.ts";
export async function getTagDir(
store: BearMetalStore,
namespace: string,
version: number,
) {
// const versionData = JSON.parse(Deno.readTextFileSync(installPath + "pack_versions/" + version + ".json"));
const tagDir = await getDirName(version, "tags");
return `${store.get("packlocation")}/${namespace}/${tagDir}`;
}

View File

@ -1,135 +0,0 @@
import { BearMetalStore } from "@bearmetal/store";
import { getPackVersion } from "../util/packVersion.ts";
import type { Router } from "../router.ts";
import { getTagDir } from "./getTagDir.ts";
import { ensureDir } from "@std/fs/ensure-dir";
import { ensureFile } from "@std/fs/ensure-file";
export const createTagRoutes = (router: Router) => {
router.route("/api/pack/:namespace/tags")
.get(async (_, ctx) => {
if (!ctx.params.namespace) {
return new Response("somehow hit the tags endpoint without namespace", {
status: 500,
});
}
using store = new BearMetalStore();
const version = getPackVersion(store);
const tagDir = await getTagDir(store, ctx.params.namespace, version);
try {
const tags = Array.from(
Deno.readDirSync(
tagDir,
),
);
return new Response(JSON.stringify(tags), { status: 200 });
} catch {
return new Response("[]", { status: 200 });
}
})
.post(async (req, ctx) => {
if (!ctx.params.namespace) {
return new Response("somehow hit the tags endpoint without namespace", {
status: 500,
});
}
using store = new BearMetalStore();
const version = getPackVersion(store);
const tagDir = await getTagDir(store, ctx.params.namespace, version);
await ensureDir(tagDir);
const { tag, type } = await req.json();
if (!tag || !type) {
return new Response("no tag name provided", { status: 400 });
}
const tagPath = `${tagDir}/${type}/${tag}.json`;
await ensureFile(tagPath);
return new Response(tag, { status: 200 });
});
router.route("/api/pack/:namespace/tags/:type-:tag")
.get(async (_, ctx) => {
if (!ctx.params.namespace) {
return new Response("somehow hit the tags endpoint without namespace", {
status: 500,
});
}
using store = new BearMetalStore();
const version = getPackVersion(store);
const tagDir = await getTagDir(store, ctx.params.namespace, version);
const tag = ctx.params.tag;
const type = ctx.params.type;
if (!tag || !type) {
return new Response("no tag name provided", { status: 400 });
}
try {
const tagFile = Deno.readTextFileSync(`${tagDir}/${type}/${tag}.json`);
return new Response(tagFile, { status: 200 });
} catch {
return new Response("no tag found", { status: 404 });
}
})
.put(async (req, ctx) => {
if (!ctx.params.namespace) {
return new Response("somehow hit the tags endpoint without namespace", {
status: 500,
});
}
using store = new BearMetalStore();
const version = getPackVersion(store);
const tagDir = await getTagDir(store, ctx.params.namespace, version);
const tag = ctx.params.tag;
const type = ctx.params.type;
if (!tag || !type) {
return new Response("no tag name provided", { status: 400 });
}
const tagPath = `${tagDir}/${type}/${tag}.json`;
await ensureFile(tagPath);
await Deno.writeTextFile(tagPath, await req.text());
return new Response(tag, { status: 200 });
})
.delete(async (_, ctx) => {
if (!ctx.params.namespace) {
return new Response("somehow hit the tags endpoint without namespace", {
status: 500,
});
}
using store = new BearMetalStore();
const version = getPackVersion(store);
const tagDir = await getTagDir(store, ctx.params.namespace, version);
const tag = ctx.params.tag;
if (!tag) {
return new Response("no tag name provided", { status: 400 });
}
try {
await Deno.remove(tagDir + "/" + tag + ".json");
return new Response(tag, { status: 200 });
} catch {
return new Response("no tag found", { status: 404 });
}
});
};

View File

@ -1,32 +0,0 @@
import type { BearMetalStore } from "@bearmetal/store";
export function getPackVersion(store: BearMetalStore) {
const packMeta = Deno.readTextFileSync(
store.get("packlocation") + "/pack.mcmeta",
);
const packMetaJson = JSON.parse(packMeta);
return packMetaJson.pack.pack_format as number;
}
export async function getDirName(version: number, path: string) {
const { default: versionData } = await import(
"../../pack_versions/" + version + ".json",
{
with: { type: "json" },
}
);
const singular = makeSingular(path);
const plural = makePlural(path);
return versionData && versionData.schema.data["<namespace>"][singular] ||
versionData.schema.data["<namespace>"][plural];
}
function makeSingular(word: string) {
return word.replace(/s$/, "");
}
function makePlural(word: string) {
return makeSingular(word) + "s";
}

View File

@ -1,20 +0,0 @@
export const readDirFiles = async (path: string) => {
return readDirFiltered(path, (file) => file.isFile);
};
export const readDirDirs = async (path: string) => {
return readDirFiltered(path, (file) => file.isDirectory);
};
export const readDirFiltered = async (
path: string,
filter: (file: Deno.DirEntry) => boolean,
) => {
const files: string[] = [];
for await (const file of Deno.readDir(path)) {
if (filter(file)) {
files.push(file.name);
}
}
return files;
};

View File

@ -1,14 +0,0 @@
export const versionCompat = (version: string, targetVersion: string) => {
if (targetVersion === "*") return true;
if (targetVersion === version) return true;
if (targetVersion.startsWith("^")) {
const versionSplit = version.split(".");
const targetVersionSplit = targetVersion.split(".");
for (let i = 0; i < versionSplit.length; i++) {
if (versionSplit[i] ?? "0" > targetVersionSplit[i] ?? "0") {
return true;
}
}
}
return false;
};

View File

@ -1,16 +0,0 @@
#!/bin/bash
SESSION=BearMetalPacker
tmux new-session -d -s $SESSION
tmux new-window -t $SESSION:1 -n "packer"
tmux select-window -t $SESSION:1
tmux send-keys "deno task bdev" C-m
tmux split-window -h
tmux send-keys "deno task fdev" C-m
tmux split-window -v
tmux attach -t $SESSION

View File

@ -1,17 +1,13 @@
import { BrowserRouter, Route, Routes } from "react-router-dom"; import { BrowserRouter,Routes,Route } from "react-router-dom";
import { Home } from "./views/home.tsx"; import { Home } from "./views/home.tsx";
import { Editor } from "./views/editor.tsx";
import { Provider } from "jotai";
export function App() { export function App() {
return ( return (
<Provider> <BrowserRouter>
<BrowserRouter> <Routes>
<Routes> <Route path="/" Component={Home} />
<Route path="/" Component={Home} /> </Routes>
<Route path="/editor/*" Component={Editor} /> </BrowserRouter>
</Routes> )
</BrowserRouter>
</Provider>
);
} }

View File

@ -1,19 +0,0 @@
import { atomWithStorage } from "jotai/utils";
import { useAtom } from "jotai";
const key = "bmp:namespace";
// const namespaceAtomPrimitive = atom<string>(localStorage.getItem(key) ??"");
// export const namespaceAtom = atom<string>(
// (get) => get(namespaceAtomPrimitive),
// (get, set, newStr) => {
// set(namespaceAtomPrimitive, newStr)
// localStorage.setItem(key, newStr)
// },
// )
export const namespaceAtom = atomWithStorage(key, "");
export const useNamespace = () => {
return useAtom(namespaceAtom);
};

View File

@ -1,62 +0,0 @@
import { useState } from "preact/hooks";
import { fetchJson } from "../../util/fetchJson.ts";
import { Loader } from "../../components/loader.tsx";
import { Modal } from "../../components/modal.tsx";
import useSwr from "swr";
export const NamespaceModal = ({ close }: { close: () => void }) => {
const { data: namespaces, isLoading } = useSwr<string[]>(
"/api/pack/namespaces",
fetchJson,
);
const [namespace, setNamespace] = useState("");
const [invalid, setInvalid] = useState("");
const createNamespace = async (ns?: string) => {
const res = await fetch("/api/pack/namespaces", {
method: "POST",
body: ns ?? namespace,
});
if (res.status === 200) {
return close();
}
setInvalid(await res.text());
};
return (
<Modal>
{isLoading
? <Loader msg="Checking existing namespaces..." />
: (
<div class="flex flex-col gap-2">
<p>Create a new namespace</p>
{!namespaces?.includes("minecraft") &&
(
<button
class="w-full"
onClick={() => createNamespace("minecraft")}
>
Create default minecraft namespace
</button>
)}
<form
onSubmit={(e) => {
e.preventDefault();
createNamespace();
}}
class="flex gap-2"
>
<input
type="text"
value={namespace}
onInput={(e) => setNamespace((e.target as any).value)}
/>
<button type="submit">Create</button>
</form>
</div>
)}
</Modal>
);
};

View File

@ -1,11 +0,0 @@
import { Link } from "react-router-dom";
export const Selector = () => {
return (
<ul class="flex flex-col gap-2">
<li>
<Link to="/editor/tags">Tag Editor</Link>
</li>
</ul>
);
};

View File

@ -1,156 +0,0 @@
import { Link, Route, Routes, useParams } from "react-router-dom";
import useSWR from "swr";
import { fetchJson } from "../../../util/fetchJson.ts";
import { useAtom } from "jotai";
import { namespaceAtom, useNamespace } from "../../../atoms/namespace.ts";
import { Loader } from "../../../components/loader.tsx";
import { useEffect, useState } from "preact/hooks";
import { NewTagModal } from "./newTagModal.tsx";
export const TagRouter = () => {
return (
<Routes>
<Route index Component={TagList} />
<Route path=":typeTag" Component={TagEditor} />
</Routes>
);
};
function TagList() {
const [namespace, _setNamespace] = useAtom(namespaceAtom);
const { data, isLoading } = useSWR<string[]>(
`/api/pack/${namespace}/tags`,
fetchJson,
);
const [showNewTagModal, setShowNewTagModal] = useState(false);
if (isLoading) {
return <Loader msg="Geez, when was the last time you swept?" />;
}
return (
<div>
<ul>
</ul>
<Resources />
{false && (
<ul class="flex flex-col gap-2">
<li>
<button onClick={() => setShowNewTagModal(true)}>New Tag</button>
{showNewTagModal && (
<NewTagModal
close={() => {
setShowNewTagModal(false);
}}
/>
)}
</li>
{data?.map((tag) => (
<li class="flex gap-2">
<Link to={`/editor/tags/${tag}`}>{tag}</Link>
<button>Delete</button>
</li>
))}
</ul>
)}
</div>
);
}
interface Tag {
replace: boolean;
values: (string | { id: string; required: boolean })[];
}
function TagEditor() {
const [namespace, _setNamespace] = useNamespace();
const { typeTag } = useParams();
const { data, isLoading } = useSWR<Tag>(
`/api/pack/${namespace}/tags/${typeTag}`,
fetchJson,
);
const [tag, setTag] = useState<Tag>({
replace: false,
values: [],
});
useEffect(() => {
if (isLoading) {
return;
}
setTag(data || { replace: false, values: [] });
}, [data, isLoading]);
if (isLoading) {
return <Loader msg="Your hard drive is full of... interesting things." />;
}
return (
<div>
<h3 class="text-lg font-bold">
{typeTag}:
<input
type="checkbox"
name="replace"
checked={tag.replace}
onChange={(e) =>
setTag({ ...tag, replace: (e.target as any).checked })}
/>
{
/* {tag.values.map((value, i) => (
<div class="flex gap-2">
<input
type="text"
value={value}
onInput={(e) =>
setTag({
...tag,
values: tag.values.map((v, i) =>
i === i ? e.target.value : v
),
})}
/>
<button
onClick={() =>
setTag({
...tag,
values: tag.values.filter((_, i) => i !== i),
})}
>
Delete
</button>
</div>
))} */
}
{tag.values.length === 0 && (
<button onClick={() => setTag({ ...tag, values: [""] })}>
Add Value
</button>
)}
</h3>
</div>
);
}
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

@ -1,66 +0,0 @@
import { useState } from "preact/hooks";
import { Modal } from "../../modal.tsx";
import { useNavigate } from "react-router-dom";
import { namespaceAtom } from "../../../atoms/namespace.ts";
import { useAtom } from "jotai";
export const NewTagModal = ({ close }: { close: () => void }) => {
const [tagName, setTagName] = useState("");
const [tagType, setTagType] = useState("block");
const nav = useNavigate();
const [namespace, _setNamespace] = useAtom(namespaceAtom);
const createTag = async () => {
const res = await fetch(
`/api/pack/${namespace}/tags`,
{
method: "POST",
body: JSON.stringify({
tag: tagName,
type: tagType,
}),
},
);
if (res.status === 200) {
close();
nav(`/editor/tags/${tagType}-${tagName}`);
}
};
return (
<Modal>
<p class="mb-2">Create a new tag</p>
<form
class="flex flex-col gap-2"
onSubmit={(e) => {
e.preventDefault();
createTag();
}}
>
<label class="flex gap-2 items-center">
Tag Type{" "}
<select
class="flex-1"
name="tag-type"
value={tagType}
onChange={(e) => setTagType((e.target as any).value)}
>
<option value="block">Block</option>
<option value="entity">Entity</option>
<option value="item">Item</option>
<option value="function">Function</option>
</select>
</label>
<div class="flex gap-2">
<input
type="text"
value={tagName}
onInput={(e) => setTagName((e.target as any).value)}
placeholder="Tag name"
/>
<button type="submit">Create</button>
</div>
</form>
</Modal>
);
};

View File

@ -1,15 +0,0 @@
import { useNavigate } from "react-router-dom";
import type { FunctionComponent } from "preact";
export const EditorWrapper: FunctionComponent = ({ children }) => {
const nav = useNavigate();
return (
<div>
<button class="hollow flex items-center gap-2" onClick={() => nav(-1)}>
<span class="text-2xl"></span>
<span class="italic">back</span>
</button>
{children}
</div>
);
};

View File

@ -1,11 +0,0 @@
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

@ -1,18 +0,0 @@
import { classList } from "../util/classes.ts";
interface IProps {
msg?: string;
class?: string;
}
export const Loader = ({ msg, class: className }: IProps) => {
return (
<div
class={classList("flex justify-center items-center gap-4", className)}
>
{!!msg && <p>{msg}</p>}
<div class="animate-spin rounded-full max-w-full h-16 w-16 border-t-2 border-b-2 border-primary-600">
</div>
</div>
);
};

View File

@ -1,14 +0,0 @@
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 relative">
{children}
</div>
</div>
</Portal>
);
};

View File

@ -1,68 +0,0 @@
import { useEffect, useState } from "preact/hooks";
import type { FunctionComponent } from "preact";
import { Loader } from "./loader.tsx";
interface IProps {
packName: string;
}
export const PackInfo: FunctionComponent<IProps> = ({ packName }) => {
const [showVersions, setShowVersions] = useState(false);
const [packVersion, setPackVersion] = useState("");
const [packVersionList, setPackVersionList] = useState<string[]>([]);
const [loading, setLoading] = useState(true);
const [world, setWorld] = useState("");
useEffect(() => {
Promise.all([
fetch("/api/pack/version").then((res) => res.text()).then((text) => {
setPackVersion(text);
}),
fetch("/api/versions").then((res) => res.json()).then((json) => {
setPackVersionList(json);
}),
fetch("/api/world").then((res) => res.text()).then((text) => {
setWorld(text);
}),
]).finally(() => setLoading(false));
}, []);
const updatePackVersion = async (version: string) => {
const res = await fetch("/api/pack/version", {
method: "POST",
body: version,
});
if (res.status === 200) {
setPackVersion(version);
}
};
return (
<>
<h2 class="text-xl">{world} :: {packName}</h2>
<p class="bg-lime-700 rounded-full shadow-md">
{loading ? <Loader class="w-8" /> : (
<>
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>
</>
);
};

View File

@ -1,53 +0,0 @@
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>
);
};

View File

@ -1,26 +0,0 @@
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

@ -1,42 +0,0 @@
import { useEffect } 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>(
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]);
};

18
src/index.css Normal file
View File

@ -0,0 +1,18 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
@apply dark:bg-mixed-600 bg-primary-100 text-dark-600 dark:text-white;
}
* {
box-sizing: border-box;
}
body, #app, html {
@apply h-full;
}
button {
@apply bg-primary-600 text-white rounded-md px-4 py-2 font-bold;
}
}

View File

@ -1,50 +0,0 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@import url('https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap');
@layer base {
:root {
@apply dark:bg-mixed-600 bg-primary-100 text-dark-600 dark:text-white;
}
* {
box-sizing: border-box;
}
body,
#app,
html {
@apply h-full;
}
button {
@apply bg-primary-600 text-white rounded-md px-4 py-2;
}
input,
select {
@apply bg-white text-dark-600 rounded-md px-4 py-2;
&:not(:last-child) {
@apply mr-2;
}
}
}
@layer utilities {
.animate-spin {
animation: spin 1s linear infinite;
}
button.hollow {
@apply bg-transparent p-0 inline;
}
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}

View File

@ -1,4 +1,5 @@
import { render } from "preact"; import { render } from 'preact'
import { App } from "./app.tsx"; import { App } from './app.tsx'
import './index.css'
render(<App />, document.getElementById("app") as HTMLElement); render(<App />, document.getElementById('app') as HTMLElement)

View File

@ -1,3 +0,0 @@
export const classList = (...classes: (string | undefined)[]) => {
return classes.filter(Boolean).join(" ");
};

View File

@ -1,2 +0,0 @@
export const fetchJson = (url: string, init?: RequestInit) =>
fetch(url, init).then((res) => res.json());

View File

@ -1,137 +0,0 @@
import { useEffect, useState } from "preact/hooks";
import { Modal } from "../components/modal.tsx";
import { LabelledHr } from "../components/labelledHr.tsx";
import { PacksList } from "../components/packsList.tsx";
import { PackInfo } from "../components/packInfo.tsx";
import { useAtom } from "jotai";
import { namespaceAtom } from "../atoms/namespace.ts";
import { Outlet, Route, Routes } from "react-router-dom";
import { Selector } from "../components/editor/selector.tsx";
import { NamespaceModal } from "../components/editor/namespaceModal.tsx";
import { EditorWrapper } from "../components/editor/wrapper.tsx";
import { TagRouter } from "../components/editor/tags/editor.tsx";
export const Editor = () => {
const [packName, setPackName] = useState("");
const [loading, setLoading] = useState(true);
const [namespaces, setNamespaces] = useState<string[]>([]);
const [namespace, setNamespace] = useAtom(namespaceAtom);
const fetchNamespaces = async () => {
const res = await fetch("/api/pack/namespaces");
const json = await res.json();
setNamespaces(json);
};
useEffect(() => {
document.title = "BearMetal Packer";
fetch("/api/pack").then((res) => res.json()).then((json) => {
setPackName(json.packName);
setLoading(false);
});
fetchNamespaces();
}, []);
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 [showNamespaceModal, setShowNamespaceModal] = useState(false);
return (
<div class="flex h-full">
<div class="w-1/4 p-4 bg-mixed-400">
<div class="text-center">
<h1 class="text-2xl">BearMetalPacker</h1>
<PackInfo packName={packName} />
</div>
<LabelledHr>Namespaces</LabelledHr>
<div>
<ul class="w-full">
{namespaces.map((namespace) => (
<li
class="text-lg cursor-pointer even:bg-black/5"
onClick={() => setNamespace(namespace)}
>
{namespace}
</li>
))}
<li class="mt-4">
<button onClick={() => setShowNamespaceModal(true)}>
New Namespace
</button>
</li>
</ul>
{showNamespaceModal && (
<NamespaceModal
close={() => {
setShowNamespaceModal(false);
fetchNamespaces();
}}
/>
)}
</div>
</div>
<div class="p-4 w-max">
{!namespace ? <div>No namespace set</div> : (
<>
<h3 class="text-lg font-bold">
Namespace: <span class="italic">{namespace}</span>
</h3>
<Routes>
<Route index element={<Selector />} />
<Route
element={
<EditorWrapper>
<Outlet />
</EditorWrapper>
}
>
<Route
path="tags/*"
element={<TagRouter />}
/>
</Route>
</Routes>
</>
)}
</div>
</div>
);
};

View File

@ -1,99 +1,25 @@
import { useEffect, useState } from "preact/hooks"; import { useEffect, useState } from "preact/hooks";
import { useNavigate } from "react-router-dom";
export function Home() { export function Home() {
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [mcPath, setMcPath] = useState(""); const [mcPath, setMcPath] = useState("");
const [worlds, setWorlds] = useState<{
world: string;
icon: string;
path: string;
}[]>([]);
const fetchWorlds = async () => {
const res = await fetch("/api/dir");
const json = await res.json();
setMcPath(json.mcPath);
setWorlds(json.worlds);
setLoading(false);
};
useEffect(() => { useEffect(() => {
document.title = "BearMetal Packer"; document.title = "BearMetal Packer";
fetchWorlds();
fetch("/api/dir").then(res => res.json()).then(data => {
setMcPath(data);
setLoading(false);
});
}, []); }, []);
const submitMcPath = async (event: SubmitEvent) => {
setLoading(true);
event.preventDefault();
const form = new FormData(event.target as HTMLFormElement);
const res = await fetch("/api/dir", {
method: "POST",
body: form,
});
if (res.status === 200) {
fetchWorlds();
} else setLoading(false);
};
const nav = useNavigate();
const setWorld = async (worldPath: string) => {
setLoading(true);
const res = await fetch("/api/world", {
method: "POST",
body: worldPath,
});
if (res.status === 200) {
nav("/editor");
} else setLoading(false);
};
return ( return (
<div class="grid h-full"> <div class="grid h-full">
<div class="place-self-center text-center max-h-96 bg-primary-400 rounded-lg p-4 overflow-scroll"> <div class="place-self-center text-center">
<h1 class="text-3xl">Welcome BearMetal Packer</h1> <h1 class="text-3xl">Welcome BearMetal Packer</h1>
<p>An all in one toolkit to build datapacks for Minecraft.</p> <p>An all in one toolkit to build datapacks for Minecraft.</p>
{loading ? <p>Hold tight, we're doing some heavy lifting.</p> : ( <p>Hold tight, we're doing some heavy lifting.</p>
<>
{mcPath
? (
<>
<p>Minecraft directory set to {mcPath}</p>
<p>Worlds:</p>
<ul class="w-full even:bg-black/5">
{worlds.map((world) => (
<li
class="flex gap-4 items-center cursor-pointer"
onClick={() => setWorld(world.path)}
>
<img
src={"/images?location=" + world.icon}
class="w-16 h-16"
/>
{world.world}
</li>
))}
</ul>
</>
)
: (
<>
<p>No Minecraft directory set, please set now:</p>
<form method="POST" onSubmit={submitMcPath}>
<input
type="text"
name="mcPath"
placeholder="Minecraft directory"
/>
<button type="submit">Set</button>
</form>
</>
)}
</>
)}
</div> </div>
</div> </div>
); )
} }

View File

@ -6,10 +6,6 @@ module.exports = {
], ],
theme: { theme: {
extend: { extend: {
fontFamily: {
sans: ["Roboto", "sans-serif"],
mono: ["Roboto Mono", "monospace"],
},
backgroundImage: { backgroundImage: {
"gradient-radial": "radial-gradient(var(--tw-gradient-stops))", "gradient-radial": "radial-gradient(var(--tw-gradient-stops))",
"gradient-conic": "gradient-conic":

View File

@ -1,7 +0,0 @@
declare global {
interface Context {
url: URL;
state: Record<string, unknown>;
params: Record<string, string | undefined>;
}
}

View File

@ -16,15 +16,12 @@ export default defineConfig({
"/api": { "/api": {
target: "http://localhost:8000", target: "http://localhost:8000",
changeOrigin: true, changeOrigin: true,
// rewrite: (path) => path.replace(/^\/api/, ""),
}, },
"/puppet": { "/puppet": {
target: "http://localhost:8000", target: "http://localhost:8000",
changeOrigin: true, changeOrigin: true,
ws: true, // rewrite: (path) => path.replace(/^\/puppet/, ""),
},
"/images": {
target: "http://localhost:8000",
changeOrigin: true,
ws: true, ws: true,
}, },
}, },