Compare commits
10 Commits
7e4d854685
...
a8a903d581
Author | SHA1 | Date | |
---|---|---|---|
a8a903d581 | |||
44c1862869 | |||
3d9b877661 | |||
f6ce166b11 | |||
0517e7c2e2 | |||
0941690f91 | |||
9498f16c28 | |||
e03e8809b7 | |||
70b489213c | |||
89502213c4 |
4
.gitignore
vendored
4
.gitignore
vendored
@ -25,4 +25,6 @@ dist-ssr
|
||||
*.sw?
|
||||
.env
|
||||
|
||||
BearMetal/
|
||||
BearMetal/
|
||||
resources/
|
||||
!**/*/resources/
|
17
deno.json
17
deno.json
@ -1,9 +1,12 @@
|
||||
{
|
||||
"tasks": {
|
||||
"dev": "deno run -A --node-modules-dir npm:vite & deno run --allow-net --allow-read --allow-write --allow-env --watch ./main.ts",
|
||||
"dev": "deno run -A --node-modules-dir npm:vite & deno run -A --watch ./server/main.ts",
|
||||
"fdev": "deno run -A --node-modules-dir npm:vite",
|
||||
"build": "deno run -A --node-modules-dir npm:vite build",
|
||||
"preview": "deno run -A --node-modules-dir npm:vite preview",
|
||||
"serve": "deno run --allow-net --allow-read --allow-write --allow-env ./main.ts"
|
||||
"serve": "deno run -A ./server/main.ts",
|
||||
"bdev": "deno run -A --watch ./server/main.ts",
|
||||
"tmux": "./session.sh"
|
||||
},
|
||||
"compilerOptions": {
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable", "deno.ns"],
|
||||
@ -12,17 +15,25 @@
|
||||
},
|
||||
"imports": {
|
||||
"@babel/plugin-transform-react-jsx-development": "npm:@babel/plugin-transform-react-jsx-development@^7.25.7",
|
||||
"@bearmetal/store": "jsr:@bearmetal/store@^0.0.4",
|
||||
"@bearmetal/store": "jsr:@bearmetal/store@^0.0.5",
|
||||
"@cgg/sockpuppet": "../sockpuppet.ts/server/mod.ts",
|
||||
"@cgg/sockpuppet/client": "../sockpuppet.ts/client/mod.ts",
|
||||
"@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",
|
||||
"@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/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",
|
||||
"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",
|
||||
"preact": "npm:preact@^10.24.3",
|
||||
"react-router-dom": "npm:react-router-dom@^6.27.0",
|
||||
"swr": "npm:swr@^2.2.5",
|
||||
"tailwindcss": "npm:tailwindcss@^3.4.13",
|
||||
"vite": "npm:vite@^5.4.8"
|
||||
}
|
||||
|
213
deno.lock
generated
213
deno.lock
generated
@ -1,62 +1,125 @@
|
||||
{
|
||||
"version": "4",
|
||||
"specifiers": {
|
||||
"jsr:@bearmetal/store@^0.0.4": "0.0.4",
|
||||
"jsr:@bearmetal/store@^0.0.5": "0.0.5",
|
||||
"jsr:@denosaurs/plug@1.0.5": "1.0.5",
|
||||
"jsr:@gfx/canvas@~0.5.8": "0.5.8",
|
||||
"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/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/fmt@0.214": "0.214.0",
|
||||
"jsr:@std/fmt@^1.0.2": "1.0.2",
|
||||
"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/http@^1.0.8": "1.0.8",
|
||||
"jsr:@std/media-types@^1.0.3": "1.0.3",
|
||||
"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/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:@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:@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: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: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: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": "3.4.13_postcss@8.4.47",
|
||||
"npm:vite@*": "5.4.9",
|
||||
"npm:vite@^5.4.8": "5.4.9"
|
||||
},
|
||||
"jsr": {
|
||||
"@bearmetal/store@0.0.4": {
|
||||
"integrity": "f5859476184d6f7b3957d18c7c82a37b6b89bb75e18db3186fde94ccb4253dab",
|
||||
"@bearmetal/store@0.0.5": {
|
||||
"integrity": "d17da24c91bcc05707deb8a55017ebdf5d8eebd2f6293dcb2bbfac57e4e3b395",
|
||||
"dependencies": [
|
||||
"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": {
|
||||
"integrity": "d22d8b38c66c666d7ad1f2a66c5b122da1704f985d3c47f01129f05abb6c5d3d"
|
||||
},
|
||||
"@std/encoding@0.214.0": {
|
||||
"integrity": "30a8713e1db22986c7e780555ffd2fefd1d4f9374d734bb41f5970f6c3352af5"
|
||||
},
|
||||
"@std/encoding@0.217.0": {
|
||||
"integrity": "b03e8ff94c98d6b6a02c02c5cf8e5d203400155516248964fc4559abc04669dc"
|
||||
},
|
||||
"@std/encoding@1.0.5": {
|
||||
"integrity": "ecf363d4fc25bd85bd915ff6733a7e79b67e0e7806334af15f4645c569fefc04"
|
||||
},
|
||||
"@std/fmt@0.214.0": {
|
||||
"integrity": "40382cff88a0783b347b4d69b94cf931ab8e549a733916718cb866c08efac4d4"
|
||||
},
|
||||
"@std/fmt@1.0.2": {
|
||||
"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": {
|
||||
"integrity": "2907d32d8d1d9e540588fd5fe0ec21ee638134bd51df327ad4e443aaef07123c",
|
||||
"dependencies": [
|
||||
"jsr:@std/path"
|
||||
"jsr:@std/path@^1.0.6"
|
||||
]
|
||||
},
|
||||
"@std/http@1.0.8": {
|
||||
"integrity": "6ea1b2e8d33929967754a3b6d6c6f399ad6647d7bbb5a466c1eaf9b294a6ebcd",
|
||||
"dependencies": [
|
||||
"jsr:@std/cli",
|
||||
"jsr:@std/encoding",
|
||||
"jsr:@std/fmt",
|
||||
"jsr:@std/encoding@^1.0.5",
|
||||
"jsr:@std/fmt@^1.0.2",
|
||||
"jsr:@std/media-types",
|
||||
"jsr:@std/net",
|
||||
"jsr:@std/path",
|
||||
"jsr:@std/path@^1.0.6",
|
||||
"jsr:@std/streams"
|
||||
]
|
||||
},
|
||||
@ -66,6 +129,18 @@
|
||||
"@std/net@1.0.4": {
|
||||
"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": {
|
||||
"integrity": "ab2c55f902b380cf28e0eec501b4906e4c1960d13f00e11cfbcd21de15f18fed"
|
||||
},
|
||||
@ -111,7 +186,7 @@
|
||||
"debug",
|
||||
"gensync",
|
||||
"json5",
|
||||
"semver"
|
||||
"semver@6.3.1"
|
||||
]
|
||||
},
|
||||
"@babel/generator@7.25.7": {
|
||||
@ -136,7 +211,7 @@
|
||||
"@babel/helper-validator-option",
|
||||
"browserslist",
|
||||
"lru-cache@5.1.1",
|
||||
"semver"
|
||||
"semver@6.3.1"
|
||||
]
|
||||
},
|
||||
"@babel/helper-module-imports@7.25.7": {
|
||||
@ -394,7 +469,7 @@
|
||||
"kolorist",
|
||||
"magic-string",
|
||||
"node-html-parser",
|
||||
"source-map",
|
||||
"source-map@0.7.4",
|
||||
"stack-trace",
|
||||
"vite"
|
||||
]
|
||||
@ -490,6 +565,9 @@
|
||||
"undici-types"
|
||||
]
|
||||
},
|
||||
"@zip.js/zip.js@2.7.52": {
|
||||
"integrity": "sha512-+5g7FQswvrCHwYKNMd/KFxZSObctLSsQOgqBSi0LzwHo3li9Eh1w5cF5ndjQw9Zbr3ajVnd2+XyiX85gAetx1Q=="
|
||||
},
|
||||
"ansi-regex@5.0.1": {
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="
|
||||
},
|
||||
@ -599,6 +677,9 @@
|
||||
"readdirp"
|
||||
]
|
||||
},
|
||||
"client-only@0.0.1": {
|
||||
"integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="
|
||||
},
|
||||
"color-convert@1.9.3": {
|
||||
"integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
|
||||
"dependencies": [
|
||||
@ -623,6 +704,12 @@
|
||||
"convert-source-map@2.0.0": {
|
||||
"integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="
|
||||
},
|
||||
"copy-anything@2.0.6": {
|
||||
"integrity": "sha512-1j20GZTsvKNkc4BY3NpMOM8tt///wY3FpIzozTOFO2ffuZcV61nojHXVKIy3WM+7ADCy5FVhdZYHYDdgTU0yJw==",
|
||||
"dependencies": [
|
||||
"is-what"
|
||||
]
|
||||
},
|
||||
"cross-spawn@7.0.3": {
|
||||
"integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
|
||||
"dependencies": [
|
||||
@ -699,6 +786,12 @@
|
||||
"entities@4.5.0": {
|
||||
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="
|
||||
},
|
||||
"errno@0.1.8": {
|
||||
"integrity": "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==",
|
||||
"dependencies": [
|
||||
"prr"
|
||||
]
|
||||
},
|
||||
"esbuild@0.21.5": {
|
||||
"integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
|
||||
"dependencies": [
|
||||
@ -803,6 +896,9 @@
|
||||
"globals@11.12.0": {
|
||||
"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": {
|
||||
"integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw=="
|
||||
},
|
||||
@ -815,6 +911,15 @@
|
||||
"he@1.2.0": {
|
||||
"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": {
|
||||
"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
|
||||
"dependencies": [
|
||||
@ -842,6 +947,9 @@
|
||||
"is-number@7.0.0": {
|
||||
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="
|
||||
},
|
||||
"is-what@3.14.1": {
|
||||
"integrity": "sha512-sNxgpk9793nzSs7bA6JQJGeIuRBQhAaNGG77kzYQgMkrID+lS6SlK07K5LaptscDlSaIgH+GPFzf+d75FVxozA=="
|
||||
},
|
||||
"isexe@2.0.0": {
|
||||
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="
|
||||
},
|
||||
@ -855,6 +963,9 @@
|
||||
"jiti@1.21.6": {
|
||||
"integrity": "sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w=="
|
||||
},
|
||||
"jotai@2.10.1": {
|
||||
"integrity": "sha512-4FycO+BOTl2auLyF2Chvi6KTDqdsdDDtpaL/WHQMs8f3KS1E3loiUShQzAzFA/sMU5cJ0hz/RT1xum9YbG/zaA=="
|
||||
},
|
||||
"js-tokens@4.0.0": {
|
||||
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
|
||||
},
|
||||
@ -867,6 +978,21 @@
|
||||
"kolorist@1.8.0": {
|
||||
"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": {
|
||||
"integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ=="
|
||||
},
|
||||
@ -897,6 +1023,13 @@
|
||||
"@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": {
|
||||
"integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="
|
||||
},
|
||||
@ -907,6 +1040,9 @@
|
||||
"picomatch"
|
||||
]
|
||||
},
|
||||
"mime@1.6.0": {
|
||||
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="
|
||||
},
|
||||
"minimatch@9.0.5": {
|
||||
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
|
||||
"dependencies": [
|
||||
@ -930,6 +1066,13 @@
|
||||
"nanoid@3.3.7": {
|
||||
"integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g=="
|
||||
},
|
||||
"needle@3.3.1": {
|
||||
"integrity": "sha512-6k0YULvhpw+RoLNiQCRKOl09Rv1dPLr8hHnVjHqdolKwDrdNyk+Hmrthi4lIGPPz3r39dLx0hsF5s40sZ3Us4Q==",
|
||||
"dependencies": [
|
||||
"iconv-lite",
|
||||
"sax"
|
||||
]
|
||||
},
|
||||
"node-html-parser@6.1.13": {
|
||||
"integrity": "sha512-qIsTMOY4C/dAa5Q5vsobRpOOvPfC4pB61UVW2uSwZNUp0QU/jCekTal1vMmbO0DgdHeLUJpv/ARmDqErVxA3Sg==",
|
||||
"dependencies": [
|
||||
@ -961,6 +1104,9 @@
|
||||
"package-json-from-dist@1.0.1": {
|
||||
"integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="
|
||||
},
|
||||
"parse-node-version@1.0.1": {
|
||||
"integrity": "sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA=="
|
||||
},
|
||||
"path-key@3.1.1": {
|
||||
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="
|
||||
},
|
||||
@ -983,6 +1129,9 @@
|
||||
"pify@2.3.0": {
|
||||
"integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog=="
|
||||
},
|
||||
"pify@4.0.1": {
|
||||
"integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g=="
|
||||
},
|
||||
"pirates@4.0.6": {
|
||||
"integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg=="
|
||||
},
|
||||
@ -1038,6 +1187,9 @@
|
||||
"preact@10.24.3": {
|
||||
"integrity": "sha512-Z2dPnBnMUfyQfSQ+GBdsGa16hz35YmLmtTLhM169uW944hYL6xzTYkJjC07j+Wosz733pMWx0fgON3JNw1jJQA=="
|
||||
},
|
||||
"prr@1.0.1": {
|
||||
"integrity": "sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw=="
|
||||
},
|
||||
"queue-microtask@1.2.3": {
|
||||
"integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="
|
||||
},
|
||||
@ -1074,7 +1226,7 @@
|
||||
"read-cache@1.0.0": {
|
||||
"integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==",
|
||||
"dependencies": [
|
||||
"pify"
|
||||
"pify@2.3.0"
|
||||
]
|
||||
},
|
||||
"readdirp@3.6.0": {
|
||||
@ -1123,12 +1275,21 @@
|
||||
"queue-microtask"
|
||||
]
|
||||
},
|
||||
"safer-buffer@2.1.2": {
|
||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
|
||||
},
|
||||
"sax@1.4.1": {
|
||||
"integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg=="
|
||||
},
|
||||
"scheduler@0.23.2": {
|
||||
"integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
|
||||
"dependencies": [
|
||||
"loose-envify"
|
||||
]
|
||||
},
|
||||
"semver@5.7.2": {
|
||||
"integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g=="
|
||||
},
|
||||
"semver@6.3.1": {
|
||||
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="
|
||||
},
|
||||
@ -1147,6 +1308,9 @@
|
||||
"source-map-js@1.2.1": {
|
||||
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="
|
||||
},
|
||||
"source-map@0.6.1": {
|
||||
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
|
||||
},
|
||||
"source-map@0.7.4": {
|
||||
"integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA=="
|
||||
},
|
||||
@ -1202,6 +1366,14 @@
|
||||
"supports-preserve-symlinks-flag@1.0.0": {
|
||||
"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": {
|
||||
"integrity": "sha512-KqjHOJKogOUt5Bs752ykCeiwvi0fKVkr5oqsFNt/8px/tA8scFPIlkygsf6jXrfCqGHz7VflA6+yytWuM+XhFw==",
|
||||
"dependencies": [
|
||||
@ -1253,6 +1425,9 @@
|
||||
"ts-interface-checker@0.1.13": {
|
||||
"integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA=="
|
||||
},
|
||||
"tslib@2.8.0": {
|
||||
"integrity": "sha512-jWVzBLplnCmoaTr13V9dYbiQ99wvZRd0vNWaDRg+aVYRcjDF3nDksxFDE/+fkXnKhpnUUkmx5pK/v8mCtLVqZA=="
|
||||
},
|
||||
"undici-types@6.19.8": {
|
||||
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw=="
|
||||
},
|
||||
@ -1264,6 +1439,12 @@
|
||||
"picocolors"
|
||||
]
|
||||
},
|
||||
"use-sync-external-store@1.2.2_react@18.3.1": {
|
||||
"integrity": "sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw==",
|
||||
"dependencies": [
|
||||
"react"
|
||||
]
|
||||
},
|
||||
"util-deprecate@1.0.2": {
|
||||
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
|
||||
},
|
||||
@ -1307,16 +1488,24 @@
|
||||
},
|
||||
"workspace": {
|
||||
"dependencies": [
|
||||
"jsr:@bearmetal/store@^0.0.4",
|
||||
"jsr:@bearmetal/store@^0.0.5",
|
||||
"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/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",
|
||||
"npm:@zip.js/zip.js@^2.7.52",
|
||||
"npm:autoprefixer@^10.4.20",
|
||||
"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:preact@^10.24.3",
|
||||
"npm:react-router-dom@^6.27.0",
|
||||
"npm:swr@^2.2.5",
|
||||
"npm:tailwindcss@^3.4.13",
|
||||
"npm:vite@^5.4.8"
|
||||
]
|
||||
|
@ -5,6 +5,9 @@
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Vite + Preact + TS</title>
|
||||
<style lang="less">
|
||||
@import './src/index.less';
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
66
main.ts
66
main.ts
@ -1,66 +0,0 @@
|
||||
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 });
|
||||
}
|
||||
});
|
63
pack_versions/48.json
Normal file
63
pack_versions/48.json
Normal file
@ -0,0 +1,63 @@
|
||||
{
|
||||
"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"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
251
server/main.ts
Normal file
251
server/main.ts
Normal file
@ -0,0 +1,251 @@
|
||||
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}"}}`,
|
||||
);
|
||||
}
|
||||
}
|
359
server/resources/readers.ts
Normal file
359
server/resources/readers.ts
Normal file
@ -0,0 +1,359 @@
|
||||
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));
|
||||
}
|
81
server/resources/renderer.ts
Normal file
81
server/resources/renderer.ts
Normal file
@ -0,0 +1,81 @@
|
||||
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);
|
||||
}
|
48
server/resources/routes.ts
Normal file
48
server/resources/routes.ts
Normal file
@ -0,0 +1,48 @@
|
||||
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 });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
58
server/resources/unzip.ts
Normal file
58
server/resources/unzip.ts
Normal file
@ -0,0 +1,58 @@
|
||||
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();
|
||||
}
|
72
server/router.ts
Normal file
72
server/router.ts
Normal file
@ -0,0 +1,72 @@
|
||||
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;
|
14
server/tags/getTagDir.ts
Normal file
14
server/tags/getTagDir.ts
Normal file
@ -0,0 +1,14 @@
|
||||
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}`;
|
||||
}
|
135
server/tags/routes.ts
Normal file
135
server/tags/routes.ts
Normal file
@ -0,0 +1,135 @@
|
||||
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 });
|
||||
}
|
||||
});
|
||||
};
|
32
server/util/packVersion.ts
Normal file
32
server/util/packVersion.ts
Normal file
@ -0,0 +1,32 @@
|
||||
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";
|
||||
}
|
20
server/util/readDir.ts
Normal file
20
server/util/readDir.ts
Normal file
@ -0,0 +1,20 @@
|
||||
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;
|
||||
};
|
14
server/util/versionCompat.ts
Normal file
14
server/util/versionCompat.ts
Normal file
@ -0,0 +1,14 @@
|
||||
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;
|
||||
};
|
16
session.sh
Executable file
16
session.sh
Executable file
@ -0,0 +1,16 @@
|
||||
#!/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
|
20
src/app.tsx
20
src/app.tsx
@ -1,13 +1,17 @@
|
||||
import { BrowserRouter,Routes,Route } from "react-router-dom";
|
||||
import { BrowserRouter, Route, Routes } from "react-router-dom";
|
||||
import { Home } from "./views/home.tsx";
|
||||
import { Editor } from "./views/editor.tsx";
|
||||
import { Provider } from "jotai";
|
||||
|
||||
export function App() {
|
||||
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
<Route path="/" Component={Home} />
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
)
|
||||
<Provider>
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
<Route path="/" Component={Home} />
|
||||
<Route path="/editor/*" Component={Editor} />
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
</Provider>
|
||||
);
|
||||
}
|
||||
|
19
src/atoms/namespace.ts
Normal file
19
src/atoms/namespace.ts
Normal file
@ -0,0 +1,19 @@
|
||||
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);
|
||||
};
|
62
src/components/editor/namespaceModal.tsx
Normal file
62
src/components/editor/namespaceModal.tsx
Normal file
@ -0,0 +1,62 @@
|
||||
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>
|
||||
);
|
||||
};
|
11
src/components/editor/selector.tsx
Normal file
11
src/components/editor/selector.tsx
Normal file
@ -0,0 +1,11 @@
|
||||
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>
|
||||
);
|
||||
};
|
156
src/components/editor/tags/editor.tsx
Normal file
156
src/components/editor/tags/editor.tsx
Normal file
@ -0,0 +1,156 @@
|
||||
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>
|
||||
);
|
||||
}
|
66
src/components/editor/tags/newTagModal.tsx
Normal file
66
src/components/editor/tags/newTagModal.tsx
Normal file
@ -0,0 +1,66 @@
|
||||
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>
|
||||
);
|
||||
};
|
15
src/components/editor/wrapper.tsx
Normal file
15
src/components/editor/wrapper.tsx
Normal file
@ -0,0 +1,15 @@
|
||||
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>
|
||||
);
|
||||
};
|
11
src/components/labelledHr.tsx
Normal file
11
src/components/labelledHr.tsx
Normal 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>
|
||||
);
|
||||
};
|
18
src/components/loader.tsx
Normal file
18
src/components/loader.tsx
Normal file
@ -0,0 +1,18 @@
|
||||
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>
|
||||
);
|
||||
};
|
14
src/components/modal.tsx
Normal file
14
src/components/modal.tsx
Normal 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 relative">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</Portal>
|
||||
);
|
||||
};
|
68
src/components/packInfo.tsx
Normal file
68
src/components/packInfo.tsx
Normal file
@ -0,0 +1,68 @@
|
||||
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>
|
||||
</>
|
||||
);
|
||||
};
|
53
src/components/packsList.tsx
Normal file
53
src/components/packsList.tsx
Normal 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
26
src/components/portal.tsx
Normal 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) : <></>;
|
||||
};
|
42
src/hooks/useStream.ts
Normal file
42
src/hooks/useStream.ts
Normal file
@ -0,0 +1,42 @@
|
||||
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]);
|
||||
};
|
@ -1,18 +0,0 @@
|
||||
@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;
|
||||
}
|
||||
}
|
50
src/index.less
Normal file
50
src/index.less
Normal file
@ -0,0 +1,50 @@
|
||||
@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);
|
||||
}
|
||||
}
|
@ -1,5 +1,4 @@
|
||||
import { render } from 'preact'
|
||||
import { App } from './app.tsx'
|
||||
import './index.css'
|
||||
import { render } from "preact";
|
||||
import { App } from "./app.tsx";
|
||||
|
||||
render(<App />, document.getElementById('app') as HTMLElement)
|
||||
render(<App />, document.getElementById("app") as HTMLElement);
|
||||
|
3
src/util/classes.ts
Normal file
3
src/util/classes.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export const classList = (...classes: (string | undefined)[]) => {
|
||||
return classes.filter(Boolean).join(" ");
|
||||
};
|
2
src/util/fetchJson.ts
Normal file
2
src/util/fetchJson.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export const fetchJson = (url: string, init?: RequestInit) =>
|
||||
fetch(url, init).then((res) => res.json());
|
137
src/views/editor.tsx
Normal file
137
src/views/editor.tsx
Normal file
@ -0,0 +1,137 @@
|
||||
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>
|
||||
);
|
||||
};
|
@ -1,25 +1,99 @@
|
||||
import { useEffect, useState } from "preact/hooks";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
export function Home() {
|
||||
const [loading, setLoading] = useState(true);
|
||||
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(() => {
|
||||
document.title = "BearMetal Packer";
|
||||
|
||||
fetch("/api/dir").then(res => res.json()).then(data => {
|
||||
setMcPath(data);
|
||||
setLoading(false);
|
||||
});
|
||||
fetchWorlds();
|
||||
}, []);
|
||||
|
||||
|
||||
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 (
|
||||
<div class="grid h-full">
|
||||
<div class="place-self-center text-center">
|
||||
<div class="place-self-center text-center max-h-96 bg-primary-400 rounded-lg p-4 overflow-scroll">
|
||||
<h1 class="text-3xl">Welcome BearMetal Packer</h1>
|
||||
<p>An all in one toolkit to build datapacks for Minecraft.</p>
|
||||
<p>Hold tight, we're doing some heavy lifting.</p>
|
||||
{loading ? <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>
|
||||
)
|
||||
}
|
||||
);
|
||||
}
|
||||
|
@ -6,6 +6,10 @@ module.exports = {
|
||||
],
|
||||
theme: {
|
||||
extend: {
|
||||
fontFamily: {
|
||||
sans: ["Roboto", "sans-serif"],
|
||||
mono: ["Roboto Mono", "monospace"],
|
||||
},
|
||||
backgroundImage: {
|
||||
"gradient-radial": "radial-gradient(var(--tw-gradient-stops))",
|
||||
"gradient-conic":
|
||||
|
7
types.ts
Normal file
7
types.ts
Normal file
@ -0,0 +1,7 @@
|
||||
declare global {
|
||||
interface Context {
|
||||
url: URL;
|
||||
state: Record<string, unknown>;
|
||||
params: Record<string, string | undefined>;
|
||||
}
|
||||
}
|
@ -16,12 +16,15 @@ export default defineConfig({
|
||||
"/api": {
|
||||
target: "http://localhost:8000",
|
||||
changeOrigin: true,
|
||||
// rewrite: (path) => path.replace(/^\/api/, ""),
|
||||
},
|
||||
"/puppet": {
|
||||
target: "http://localhost:8000",
|
||||
changeOrigin: true,
|
||||
// rewrite: (path) => path.replace(/^\/puppet/, ""),
|
||||
ws: true,
|
||||
},
|
||||
"/images": {
|
||||
target: "http://localhost:8000",
|
||||
changeOrigin: true,
|
||||
ws: true,
|
||||
},
|
||||
},
|
||||
|
Loading…
x
Reference in New Issue
Block a user