next release preparing (#10)
This commit is contained in:
commit
abb6b06b53
37 changed files with 661 additions and 207 deletions
1
.github/workflows/ci.yml
vendored
1
.github/workflows/ci.yml
vendored
|
|
@ -21,4 +21,3 @@ jobs:
|
|||
with:
|
||||
name: cypress-images
|
||||
path: cypress/integration/__image_snapshots__/
|
||||
if-no-files-found: ignore
|
||||
|
|
|
|||
11
.github/workflows/publish.yml
vendored
11
.github/workflows/publish.yml
vendored
|
|
@ -13,8 +13,15 @@ jobs:
|
|||
run: npm i -g pnpm
|
||||
- run: pnpm install
|
||||
- run: pnpm build
|
||||
# todo use nohup and official action?
|
||||
# - run: pnpm prod-start & pnpm test:cypress
|
||||
- uses: cypress-io/github-action@v5
|
||||
with:
|
||||
install: false
|
||||
start: pnpm prod-start
|
||||
- uses: actions/upload-artifact@v3
|
||||
if: failure()
|
||||
with:
|
||||
name: cypress-images
|
||||
path: cypress/integration/__image_snapshots__/
|
||||
- uses: peaceiris/actions-gh-pages@v3
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
|
|
|||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -6,6 +6,7 @@ public
|
|||
.env.local
|
||||
Thumbs.db
|
||||
build
|
||||
localSettings.mjs
|
||||
dist
|
||||
.DS_Store
|
||||
.idea/
|
||||
|
|
|
|||
8
.vscode/launch.json
vendored
8
.vscode/launch.json
vendored
|
|
@ -7,6 +7,7 @@
|
|||
"address": "localhost",
|
||||
"name": "Attach Chrome",
|
||||
"request": "attach",
|
||||
// comment if using webpack
|
||||
"pathMapping": {
|
||||
"/": "${workspaceFolder}/dist"
|
||||
},
|
||||
|
|
@ -24,10 +25,13 @@
|
|||
},
|
||||
{
|
||||
// not recommended as in most cases it will slower as it launches from extension host so it slows down extension host, not sure why
|
||||
"type": "msedge",
|
||||
"name": "Launch Edge",
|
||||
"type": "chrome",
|
||||
"name": "Launch Chrome",
|
||||
"request": "launch",
|
||||
"url": "http://localhost:8080/",
|
||||
"pathMapping": {
|
||||
"/": "${workspaceFolder}/dist"
|
||||
},
|
||||
"outFiles": [
|
||||
"${workspaceFolder}/dist/**/*.js",
|
||||
// "!${workspaceFolder}/dist/**/*vendors*",
|
||||
|
|
|
|||
|
|
@ -3,15 +3,18 @@
|
|||
it('Loads & renders singleplayer', () => {
|
||||
// todo use <button match text selectors
|
||||
cy.visit('/')
|
||||
window.localStorage.clear()
|
||||
window.localStorage.cypress = 'true'
|
||||
cy.window().then((win) => {
|
||||
})
|
||||
window.localStorage.server = 'localhost'
|
||||
window.localStorage.setItem('renderDistance', '2')
|
||||
window.localStorage.setItem('localServerOptions', JSON.stringify({
|
||||
generation: {
|
||||
name: 'superflat',
|
||||
options: { seed: 250869072 }
|
||||
window.localStorage.setItem('options', JSON.stringify({
|
||||
localServerOptions: {
|
||||
generation: {
|
||||
name: 'superflat',
|
||||
options: { seed: 250869072 }
|
||||
}
|
||||
}
|
||||
}))
|
||||
// todo replace with data-test
|
||||
|
|
|
|||
36
esbuild.mjs
36
esbuild.mjs
|
|
@ -3,7 +3,6 @@ import * as esbuild from 'esbuild'
|
|||
import fs from 'fs'
|
||||
// import htmlPlugin from '@chialab/esbuild-plugin-html'
|
||||
import server from './server.js'
|
||||
import { analyzeMetafile } from 'esbuild'
|
||||
import { clients, plugins } from './scripts/esbuildPlugins.mjs'
|
||||
import { generateSW } from 'workbox-build'
|
||||
import { getSwAdditionalEntries } from './scripts/build.js'
|
||||
|
|
@ -32,23 +31,33 @@ let baseConfig = {}
|
|||
// outdir: undefined,
|
||||
// }
|
||||
|
||||
try {
|
||||
await import('./localSettings.mjs')
|
||||
} catch { }
|
||||
|
||||
fs.copyFileSync('index.html', 'dist/index.html')
|
||||
fs.writeFileSync('dist/index.html', fs.readFileSync('dist/index.html', 'utf8').replace('<!-- inject script -->', '<script src="index.js"></script>'), 'utf8')
|
||||
|
||||
const watch = process.argv.includes('--watch') || process.argv.includes('-w')
|
||||
const prod = process.argv.includes('--prod')
|
||||
const dev = !prod
|
||||
|
||||
const banner = [
|
||||
'window.global = globalThis;',
|
||||
// report reload time
|
||||
dev && 'if (sessionStorage.lastReload) { const [rebuild, reloadStart] = sessionStorage.lastReload.split(","); const now = Date.now(); console.log(`rebuild + reload:`, +rebuild, "+", now - reloadStart, "=", ((+rebuild + (now - reloadStart)) / 1000).toFixed(1) + "s");sessionStorage.lastReload = ""; }',
|
||||
// auto-reload
|
||||
'(() => new EventSource("/esbuild").onmessage = ({ data: _data }) => {const data = JSON.parse(_data);if (!data.update)return;sessionStorage.lastReload = JSON.stringify({buildTime:data.update.time, reloadStart:Date.now()});location.reload()})();'
|
||||
]
|
||||
dev && ';(() => new EventSource("/esbuild").onmessage = ({ data: _data }) => { if (!_data) return; const data = JSON.parse(_data); if (!data.update) return; sessionStorage.lastReload = `${data.update.time},${Date.now()}`; location.reload() })();'
|
||||
].filter(Boolean)
|
||||
|
||||
const buildingVersion = new Date().toISOString().split(':')[0]
|
||||
|
||||
const dev = process.argv.includes('--watch') || process.argv.includes('-w')
|
||||
const prod = process.argv.includes('--prod')
|
||||
|
||||
const ctx = await esbuild.context({
|
||||
bundle: true,
|
||||
entryPoints: ['src/index.js'],
|
||||
target: ['es2020'],
|
||||
jsx: 'automatic',
|
||||
jsxDev: dev,
|
||||
// logLevel: 'debug',
|
||||
logLevel: 'info',
|
||||
platform: 'browser',
|
||||
|
|
@ -60,7 +69,7 @@ const ctx = await esbuild.context({
|
|||
keepNames: true,
|
||||
...baseConfig,
|
||||
banner: {
|
||||
js: banner.join('\n')
|
||||
js: banner.join('\n'),
|
||||
},
|
||||
alias: {
|
||||
events: 'events', // make explicit
|
||||
|
|
@ -83,14 +92,21 @@ const ctx = await esbuild.context({
|
|||
],
|
||||
minify: process.argv.includes('--minify'),
|
||||
define: {
|
||||
'process.env.NODE_ENV': JSON.stringify(dev ? 'development' : 'production'),
|
||||
'process.env.BUILD_VERSION': JSON.stringify(!dev ? buildingVersion : 'undefined'),
|
||||
'process.env.GITHUB_URL':
|
||||
JSON.stringify(`https://github.com/${process.env.GITHUB_REPOSITORY || `${process.env.VERCEL_GIT_REPO_OWNER}/${process.env.VERCEL_GIT_REPO_SLUG}`}`)
|
||||
},
|
||||
// chunkNames: '[name]',
|
||||
loader: {
|
||||
// todo use external or resolve issues with duplicating
|
||||
'.png': 'dataurl'
|
||||
},
|
||||
write: false,
|
||||
// todo would be better to enable?
|
||||
// preserveSymlinks: true,
|
||||
})
|
||||
|
||||
if (dev) {
|
||||
if (watch) {
|
||||
await ctx.watch()
|
||||
server.app.get('/esbuild', (req, res, next) => {
|
||||
res.writeHead(200, {
|
||||
|
|
@ -115,7 +131,7 @@ if (dev) {
|
|||
})
|
||||
} else {
|
||||
const result = await ctx.rebuild()
|
||||
// console.log(await analyzeMetafile(result.metafile))
|
||||
// console.log(await esbuild.analyzeMetafile(result.metafile))
|
||||
|
||||
if (prod) {
|
||||
fs.writeFileSync('dist/version.txt', buildingVersion, 'utf-8')
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import mcServer from 'space-squid'
|
|||
const serverOptions = {
|
||||
'motd': 'A Minecraft Server \nRunning flying-squid',
|
||||
// host: '',
|
||||
customPackets: true,
|
||||
'port': 25565,
|
||||
'max-players': 10,
|
||||
'online-mode': false,
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@
|
|||
"start-watch-script": "nodemon -w esbuild.mjs esbuild.mjs",
|
||||
"build": "node scripts/build.js copyFiles && node esbuild.mjs --minify --prod",
|
||||
"watch": "node scripts/build.js copyFilesDev && webpack serve --config webpack.dev.js --progress",
|
||||
"postinstall": "node scripts/patchPackages.js",
|
||||
"test:cypress": "cypress run",
|
||||
"test:e2e": "start-test http-get://localhost:8080 test:cypress",
|
||||
"prod-start": "node server.js",
|
||||
|
|
@ -39,6 +38,7 @@
|
|||
"express": "^4.18.2",
|
||||
"fs-extra": "^11.1.1",
|
||||
"iconify-icon": "^1.0.8",
|
||||
"jszip": "^3.10.1",
|
||||
"lit": "^2.8.0",
|
||||
"minecraft-data": "^3.0.0",
|
||||
"net-browserify": "github:PrismarineJS/net-browserify",
|
||||
|
|
@ -73,7 +73,7 @@
|
|||
"https-browserify": "^1.0.0",
|
||||
"lodash-webpack-plugin": "^0.11.6",
|
||||
"memfs": "^3.5.3",
|
||||
"mineflayer": "^4.11.0",
|
||||
"mineflayer": "github:zardoy/mineflayer#custom",
|
||||
"mineflayer-pathfinder": "^2.4.4",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"os-browserify": "^0.3.0",
|
||||
|
|
@ -97,6 +97,7 @@
|
|||
"pnpm": {
|
||||
"overrides": {
|
||||
"minecraft-data": "latest",
|
||||
"prismarine-provider-anvil": "github:zardoy/prismarine-provider-anvil#chunk-impl-require-fix",
|
||||
"minecraft-protocol": "github:zardoy/minecraft-protocol#custom-client-extra"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ const filesAlwaysToCopy = [
|
|||
// these files could be copied at build time eg with copy plugin, but copy plugin slows down the config (2x in my testing, sometimes with too many open files error) is slow so we also copy them there
|
||||
const webpackFilesToCopy = [
|
||||
{ from: './node_modules/prismarine-viewer2/public/blocksStates/', to: 'dist/blocksStates/' },
|
||||
{ from: './node_modules/prismarine-viewer2/public/textures/', to: 'dist/textures/' },
|
||||
// { from: './node_modules/prismarine-viewer2/public/textures/', to: 'dist/textures/' },
|
||||
{ from: './node_modules/prismarine-viewer2/public/worker.js', to: 'dist/worker.js' },
|
||||
{ from: './node_modules/prismarine-viewer2/public/supportedVersions.json', to: 'dist/supportedVersions.json' },
|
||||
{ from: './assets/', to: './dist/' },
|
||||
|
|
@ -24,6 +24,13 @@ exports.copyFiles = (isDev = false) => {
|
|||
[...filesAlwaysToCopy, ...webpackFilesToCopy].forEach(file => {
|
||||
fsExtra.copySync(file.from, file.to)
|
||||
})
|
||||
const cwd = './node_modules/prismarine-viewer2/public/textures/'
|
||||
const files = glob.sync('{*/entity/**,*.png}', { cwd: cwd, nodir: true, })
|
||||
for (const file of files) {
|
||||
const copyDest = path.join('dist/textures/', file)
|
||||
fs.mkdirSync(path.dirname(copyDest), { recursive: true, })
|
||||
fs.copyFileSync(path.join(cwd, file), copyDest)
|
||||
}
|
||||
|
||||
console.timeEnd('copy files')
|
||||
}
|
||||
|
|
@ -48,15 +55,8 @@ exports.getSwAdditionalEntries = () => {
|
|||
'*.png',
|
||||
'*.woff',
|
||||
'worker.js',
|
||||
// todo add gui textures (1.17.1)
|
||||
// todo if we uncomment it it will spam the server with requests for textures on initial page load
|
||||
// we need to put all textures into on file instead!
|
||||
// `textures/${singlePlayerVersion}/**`,
|
||||
`textures/${singlePlayerVersion}/blocks/destroy_stage_0.png.png`,
|
||||
`textures/${singlePlayerVersion}/blocks/destroy_stage_1.png.png`,
|
||||
// todo-low preload entity atlas?
|
||||
`textures/${singlePlayerVersion}.png`,
|
||||
`textures/1.16.4/gui/widgets.png`,
|
||||
`textures/1.16.4/gui/icons.png`,
|
||||
`textures/1.16.4/entity/squid.png`,
|
||||
]
|
||||
const filesNeedsCacheKey = [
|
||||
|
|
|
|||
|
|
@ -5,7 +5,8 @@ import { join, dirname } from 'path'
|
|||
import * as fs from 'fs'
|
||||
import { filesize } from 'filesize'
|
||||
|
||||
let clients = []
|
||||
const prod = process.argv.includes('--prod')
|
||||
let connectedClients = []
|
||||
|
||||
/** @type {import('esbuild').Plugin[]} */
|
||||
const plugins = [
|
||||
|
|
@ -62,12 +63,20 @@ const plugins = [
|
|||
}
|
||||
},
|
||||
{
|
||||
name: 'assets-resolve',
|
||||
name: 'data-assets',
|
||||
setup (build) {
|
||||
const customMcDataNs = 'custom-mc-data'
|
||||
|
||||
build.onResolve({
|
||||
filter: /.*/,
|
||||
}, async ({ path, ...rest }) => {
|
||||
if (['.woff', '.woff2', '.ttf', '.png', '.jpg', '.jpeg', '.gif', '.svg'].some(ext => path.endsWith(ext))) {
|
||||
if (join(rest.resolveDir, path).replaceAll('\\', '/').endsWith('minecraft-data/data.js')) {
|
||||
return {
|
||||
namespace: customMcDataNs,
|
||||
path
|
||||
}
|
||||
}
|
||||
if (['.woff', '.woff2', '.ttf'].some(ext => path.endsWith(ext))) {
|
||||
return {
|
||||
path,
|
||||
namespace: 'assets',
|
||||
|
|
@ -75,11 +84,79 @@ const plugins = [
|
|||
}
|
||||
}
|
||||
})
|
||||
build.onEnd(({ metafile, outputFiles }) => {
|
||||
// top 5 biggest deps
|
||||
//@ts-ignore
|
||||
|
||||
build.onLoad({
|
||||
filter: /.*/,
|
||||
namespace: customMcDataNs,
|
||||
}, async ({ path, ...rest }) => {
|
||||
const resolvedPath = await build.resolve('minecraft-data/minecraft-data/data/dataPaths.json', { kind: 'require-call', resolveDir: process.cwd() })
|
||||
const dataPaths = JSON.parse(await fs.promises.readFile(resolvedPath.path, 'utf8'))
|
||||
|
||||
// bedrock unsupported
|
||||
delete dataPaths.bedrock
|
||||
|
||||
const allowOnlyList = process.env.ONLY_MC_DATA?.split(',') ?? []
|
||||
|
||||
// skip data for 0.30c, snapshots and pre-releases
|
||||
const ignoredVersionsRegex = /(^0\.30c$)|w|-pre|-rc/
|
||||
|
||||
const includedVersions = []
|
||||
let contents = 'module.exports =\n{\n'
|
||||
for (const platform of Object.keys(dataPaths)) {
|
||||
contents += ` '${platform}': {\n`
|
||||
for (const version of Object.keys(dataPaths[platform])) {
|
||||
if (allowOnlyList.length && !allowOnlyList.includes(version)) continue
|
||||
if (ignoredVersionsRegex.test(version)) continue
|
||||
|
||||
includedVersions.push(version)
|
||||
contents += ` '${version}': {\n`
|
||||
for (const dataType of Object.keys(dataPaths[platform][version])) {
|
||||
const loc = `minecraft-data/data/${dataPaths[platform][version][dataType]}/`
|
||||
contents += ` get ${dataType} () { return require("./${loc}${dataType}.json") },\n`
|
||||
}
|
||||
contents += ' },\n'
|
||||
}
|
||||
contents += ' },\n'
|
||||
}
|
||||
contents += '}\n'
|
||||
|
||||
if (prod) {
|
||||
console.log('Included mc-data versions:', includedVersions)
|
||||
}
|
||||
return {
|
||||
contents,
|
||||
loader: 'js',
|
||||
resolveDir: join(dirname(resolvedPath.path), '../..'),
|
||||
}
|
||||
})
|
||||
|
||||
build.onEnd(async ({ metafile, outputFiles }) => {
|
||||
// write outputFiles
|
||||
for (const file of outputFiles) {
|
||||
// if (file.path.endsWith('index.js.map')) {
|
||||
// const map = JSON.parse(file.text)
|
||||
// map.sourcesContent = map.sourcesContent.map((c, index) => {
|
||||
// if (map.sources[index].endsWith('.json')) return ''
|
||||
// return c
|
||||
// })
|
||||
// // data.sources = data.sources.filter(source => !source.endsWith('.json'))
|
||||
// await fs.promises.writeFile(file.path, JSON.stringify(map), 'utf8')
|
||||
// } else {
|
||||
await fs.promises.writeFile(file.path, file.contents)
|
||||
// }
|
||||
}
|
||||
if (!prod) return
|
||||
// const deps = Object.entries(metafile.inputs).sort(([, a], [, b]) => b.bytes - a.bytes).map(([x, { bytes }]) => [x, filesize(bytes)]).slice(0, 5)
|
||||
// console.log(deps)
|
||||
//@ts-ignore
|
||||
const sizeByExt = {}
|
||||
//@ts-ignore
|
||||
Object.entries(metafile.inputs).sort(([, a], [, b]) => b.bytes - a.bytes).forEach(([x, { bytes }]) => {
|
||||
const ext = x.slice(x.lastIndexOf('.'))
|
||||
sizeByExt[ext] ??= 0
|
||||
sizeByExt[ext] += bytes
|
||||
})
|
||||
console.log('Input size by ext:')
|
||||
console.log(Object.fromEntries(Object.entries(sizeByExt).map(x => [x[0], filesize(x[1])])))
|
||||
})
|
||||
},
|
||||
},
|
||||
|
|
@ -107,22 +184,24 @@ const plugins = [
|
|||
})
|
||||
build.onEnd(({ errors, outputFiles, metafile, warnings }) => {
|
||||
const elapsed = Date.now() - time
|
||||
// write metafile to disk if needed
|
||||
// fs.writeFileSync('dist/meta.json', JSON.stringify(metafile, null, 2))
|
||||
console.log(`Done in ${elapsed}ms`)
|
||||
if (count++ === 0) {
|
||||
return
|
||||
}
|
||||
if (errors.length) {
|
||||
clients.forEach((res) => {
|
||||
connectedClients.forEach((res) => {
|
||||
res.write(`data: ${JSON.stringify({ errors: errors.map(error => error.text) })}\n\n`)
|
||||
res.flush()
|
||||
})
|
||||
return
|
||||
}
|
||||
clients.forEach((res) => {
|
||||
connectedClients.forEach((res) => {
|
||||
res.write(`data: ${JSON.stringify({ update: { time: elapsed } })}\n\n`)
|
||||
res.flush()
|
||||
})
|
||||
clients.length = 0
|
||||
connectedClients.length = 0
|
||||
})
|
||||
}
|
||||
},
|
||||
|
|
@ -173,6 +252,7 @@ const plugins = [
|
|||
namespace: 'esbuild-import-glob',
|
||||
}, async ({ pluginData, path }) => {
|
||||
const { resolveDir } = pluginData
|
||||
//@ts-ignore
|
||||
const [, userPath, skipFiles] = /^esbuild-import-glob\(path:(.+),skipFiles:(.+)\)+$/g.exec(path)
|
||||
const files = (await fs.promises.readdir(join(resolveDir, userPath))).filter(f => !skipFiles.includes(f))
|
||||
return {
|
||||
|
|
@ -226,4 +306,4 @@ const plugins = [
|
|||
})
|
||||
]
|
||||
|
||||
export { plugins, clients }
|
||||
export { plugins, connectedClients as clients }
|
||||
|
|
|
|||
|
|
@ -1,27 +0,0 @@
|
|||
//@ts-check
|
||||
const path = require('path')
|
||||
const dataPath = path.join(require.resolve('minecraft-data'), '../data.js')
|
||||
|
||||
const fs = require('fs')
|
||||
|
||||
const lines = fs.readFileSync(dataPath, 'utf8').split('\n')
|
||||
if (lines[0] === '//patched') {
|
||||
console.log('Already patched')
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
function removeLinesBetween (start, end) {
|
||||
let startIndex = lines.findIndex(line => line === start)
|
||||
if (startIndex === -1) return
|
||||
let endIndex = startIndex + lines.slice(startIndex).findIndex(line => line === end)
|
||||
// insert block comments
|
||||
lines.splice(startIndex, 0, `/*`)
|
||||
lines.splice(endIndex + 2, 0, `*/`)
|
||||
}
|
||||
|
||||
// todo removing bedrock support for now, will optiimze in future instead
|
||||
removeLinesBetween(" 'bedrock': {", ' }')
|
||||
|
||||
lines.unshift('//patched')
|
||||
// fs.writeFileSync(path.join(dataPath, '../dataGlobal.js'), newContents, 'utf8')
|
||||
fs.writeFileSync(dataPath, lines.join('\n'), 'utf8')
|
||||
|
|
@ -88,7 +88,7 @@ document.addEventListener('keydown', (e) => {
|
|||
switch (km.defaultKey) {
|
||||
case 'KeyE':
|
||||
// todo reenable
|
||||
// showModal({ reactType: 'inventory', })
|
||||
showModal({ reactType: 'inventory', })
|
||||
// todo seems to be workaround
|
||||
// avoid calling inner keybinding listener, but should be handled there
|
||||
e.stopImmediatePropagation()
|
||||
|
|
@ -128,7 +128,7 @@ document.addEventListener('keydown', (e) => {
|
|||
})
|
||||
|
||||
document.addEventListener('keyup', (e) => {
|
||||
if (!isGameActive(true)) return
|
||||
// if (!isGameActive(true)) return
|
||||
|
||||
keyBindScrn.keymaps.forEach(km => {
|
||||
if (e.code === km.key) {
|
||||
|
|
|
|||
140
src/browserfs.js
140
src/browserfs.js
|
|
@ -1,9 +1,12 @@
|
|||
//@ts-check
|
||||
import { fsState, loadFolder } from './loadFolder'
|
||||
import { oneOf } from '@zardoy/utils'
|
||||
import JSZip from 'jszip'
|
||||
import { join } from 'path'
|
||||
|
||||
const { promisify } = require('util')
|
||||
const browserfs = require('browserfs')
|
||||
const _fs = require('fs')
|
||||
const fs = require('fs')
|
||||
|
||||
browserfs.install(window)
|
||||
// todo migrate to StorageManager API for localsave as localstorage has only 5mb limit, when localstorage is fallback test limit warning on 4mb
|
||||
|
|
@ -16,8 +19,11 @@ browserfs.configure({
|
|||
}, (e) => {
|
||||
if (e) throw e
|
||||
})
|
||||
|
||||
export const forceCachedDataPaths = {}
|
||||
|
||||
//@ts-ignore
|
||||
_fs.promises = new Proxy(Object.fromEntries(['readFile', 'writeFile', 'stat', 'mkdir', 'rename', /* 'copyFile', */'readdir'].map(key => [key, promisify(_fs[key])])), {
|
||||
fs.promises = new Proxy(Object.fromEntries(['readFile', 'writeFile', 'stat', 'mkdir', 'rename', /* 'copyFile', */'readdir'].map(key => [key, promisify(fs[key])])), {
|
||||
get (target, p, receiver) {
|
||||
//@ts-ignore
|
||||
if (!target[p]) throw new Error(`Not implemented fs.promises.${p}`)
|
||||
|
|
@ -26,24 +32,42 @@ _fs.promises = new Proxy(Object.fromEntries(['readFile', 'writeFile', 'stat', 'm
|
|||
if (typeof args[0] === 'string' && !args[0].startsWith('/')) args[0] = '/' + args[0]
|
||||
// Write methods
|
||||
// todo issue one-time warning (in chat I guess)
|
||||
if (oneOf(p, 'writeFile', 'mkdir', 'rename') && fsState.isReadonly) return
|
||||
if (fsState.isReadonly) {
|
||||
if (oneOf(p, 'readFile', 'writeFile') && forceCachedDataPaths[args[0]]) {
|
||||
if (p === 'readFile') {
|
||||
return Promise.resolve(forceCachedDataPaths[args[0]])
|
||||
} else if (p === 'writeFile') {
|
||||
forceCachedDataPaths[args[0]] = args[1]
|
||||
return Promise.resolve()
|
||||
}
|
||||
}
|
||||
if (oneOf(p, 'writeFile', 'mkdir', 'rename')) return
|
||||
}
|
||||
if (p === 'open' && fsState.isReadonly) {
|
||||
args[1] = 'r' // read-only, zipfs throw otherwise
|
||||
}
|
||||
//@ts-ignore
|
||||
return target[p](...args)
|
||||
}
|
||||
}
|
||||
})
|
||||
//@ts-ignore
|
||||
_fs.promises.open = async (...args) => {
|
||||
const fd = await promisify(_fs.open)(...args)
|
||||
fs.promises.open = async (...args) => {
|
||||
const fd = await promisify(fs.open)(...args)
|
||||
return {
|
||||
...Object.fromEntries(['read', 'write', 'close'].map(x => [x, async (...args) => {
|
||||
return await new Promise(resolve => {
|
||||
_fs[x](fd, ...args, (err, bytesRead, buffer) => {
|
||||
// todo it results in world corruption on interactions eg block placements
|
||||
if (x === 'write' && fsState.isReadonly) {
|
||||
return resolve({ buffer: Buffer.from([]), bytesRead: 0 })
|
||||
}
|
||||
|
||||
fs[x](fd, ...args, (err, bytesRead, buffer) => {
|
||||
if (err) throw err
|
||||
// todo if readonly probably there is no need to open at all (return some mocked version - check reload)?
|
||||
if (x === 'write' && !fsState.isReadonly && fsState.syncFs) {
|
||||
// flush data, though alternatively we can rely on close in unload
|
||||
_fs.fsync(fd, () => { })
|
||||
fs.fsync(fd, () => { })
|
||||
}
|
||||
resolve({ buffer, bytesRead })
|
||||
})
|
||||
|
|
@ -54,7 +78,7 @@ _fs.promises.open = async (...args) => {
|
|||
filename: args[0],
|
||||
close: () => {
|
||||
return new Promise(resolve => {
|
||||
_fs.close(fd, (err) => {
|
||||
fs.close(fd, (err) => {
|
||||
if (err) {
|
||||
throw err
|
||||
} else {
|
||||
|
|
@ -66,16 +90,37 @@ _fs.promises.open = async (...args) => {
|
|||
}
|
||||
}
|
||||
|
||||
// for testing purposes, todo move it to core patch
|
||||
const removeFileRecursiveSync = (path) => {
|
||||
fs.readdirSync(path).forEach((file) => {
|
||||
const curPath = join(path, file)
|
||||
if (fs.lstatSync(curPath).isDirectory()) {
|
||||
// recurse
|
||||
removeFileRecursiveSync(curPath)
|
||||
fs.rmdirSync(curPath)
|
||||
} else {
|
||||
// delete file
|
||||
fs.unlinkSync(curPath)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
window.removeFileRecursiveSync = removeFileRecursiveSync
|
||||
|
||||
const SUPPORT_WRITE = true
|
||||
|
||||
export const openWorldDirectory = async (dragndropData) => {
|
||||
export const openWorldDirectory = async (/** @type {FileSystemDirectoryHandle?} */dragndropHandle = undefined) => {
|
||||
/** @type {FileSystemDirectoryHandle} */
|
||||
let _directoryHandle
|
||||
try {
|
||||
_directoryHandle = await window.showDirectoryPicker()
|
||||
} catch (err) {
|
||||
if (err instanceof DOMException && err.name === 'AbortError') return
|
||||
throw err
|
||||
if (dragndropHandle) {
|
||||
_directoryHandle = dragndropHandle
|
||||
} else {
|
||||
try {
|
||||
_directoryHandle = await window.showDirectoryPicker()
|
||||
} catch (err) {
|
||||
if (err instanceof DOMException && err.name === 'AbortError') return
|
||||
throw err
|
||||
}
|
||||
}
|
||||
const directoryHandle = _directoryHandle
|
||||
|
||||
|
|
@ -106,3 +151,70 @@ export const openWorldDirectory = async (dragndropData) => {
|
|||
fsState.syncFs = false
|
||||
loadFolder()
|
||||
}
|
||||
|
||||
export const openWorldZip = async (/** @type {File} */file) => {
|
||||
await new Promise(async resolve => {
|
||||
browserfs.configure({
|
||||
// todo
|
||||
fs: 'MountableFileSystem',
|
||||
options: {
|
||||
"/world": {
|
||||
fs: "ZipFS",
|
||||
options: {
|
||||
zipData: Buffer.from(await file.arrayBuffer()),
|
||||
name: file.name
|
||||
}
|
||||
}
|
||||
},
|
||||
}, (e) => {
|
||||
if (e) throw e
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
|
||||
fsState.isReadonly = true
|
||||
fsState.syncFs = true
|
||||
|
||||
if (fs.existsSync('/world/level.dat')) {
|
||||
loadFolder()
|
||||
} else {
|
||||
const dirs = fs.readdirSync('/world')
|
||||
let availableWorlds = []
|
||||
for (const dir of dirs) {
|
||||
if (fs.existsSync(`/world/${dir}/level.dat`)) {
|
||||
availableWorlds.push(dir)
|
||||
}
|
||||
}
|
||||
|
||||
if (availableWorlds.length === 0) {
|
||||
alert('No worlds found in the zip')
|
||||
return
|
||||
}
|
||||
|
||||
if (availableWorlds.length === 1) {
|
||||
loadFolder(`/world/${availableWorlds[0]}`)
|
||||
}
|
||||
|
||||
alert(`Many (${availableWorlds.length}) worlds found in the zip!`)
|
||||
// todo prompt picker
|
||||
// const selectWorld
|
||||
}
|
||||
}
|
||||
|
||||
export async function generateZipAndWorld () {
|
||||
const zip = new JSZip()
|
||||
|
||||
zip.folder('world')
|
||||
|
||||
// Generate the ZIP archive content
|
||||
const zipContent = await zip.generateAsync({ type: "blob" })
|
||||
|
||||
// Create a download link and trigger the download
|
||||
const downloadLink = document.createElement("a")
|
||||
downloadLink.href = URL.createObjectURL(zipContent)
|
||||
downloadLink.download = "prismarine-world.zip"
|
||||
downloadLink.click()
|
||||
|
||||
// Clean up the URL object after download
|
||||
URL.revokeObjectURL(downloadLink.href)
|
||||
}
|
||||
|
|
|
|||
101
src/builtinCommands.ts
Normal file
101
src/builtinCommands.ts
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
import JSZip from 'jszip'
|
||||
import fs from 'fs'
|
||||
import { join } from 'path'
|
||||
import { fsState } from './loadFolder'
|
||||
|
||||
const notImplemented = () => {
|
||||
return 'Not implemented yet'
|
||||
}
|
||||
|
||||
async function addFolderToZip(folderPath, zip, relativePath) {
|
||||
const entries = await fs.promises.readdir(folderPath)
|
||||
|
||||
for (const entry of entries) {
|
||||
const entryPath = join(folderPath, entry)
|
||||
const stats = await fs.promises.stat(entryPath)
|
||||
|
||||
const zipEntryPath = join(relativePath, entry)
|
||||
|
||||
if (stats.isDirectory()) {
|
||||
const subZip = zip.folder(zipEntryPath)
|
||||
await addFolderToZip(entryPath, subZip, zipEntryPath)
|
||||
} else {
|
||||
const fileData = await fs.promises.readFile(entryPath)
|
||||
zip.file(entry, fileData)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// todo include in help
|
||||
const exportWorld = async () => {
|
||||
// todo issue into chat warning if fs is writable!
|
||||
const zip = new JSZip()
|
||||
let worldFolder: string = singlePlayerServer.options.worldFolder
|
||||
if (!worldFolder.startsWith('/')) worldFolder = `/${worldFolder}`
|
||||
await addFolderToZip(worldFolder, zip, '')
|
||||
|
||||
// Generate the ZIP archive content
|
||||
const zipContent = await zip.generateAsync({ type: "blob" })
|
||||
|
||||
// Create a download link and trigger the download
|
||||
const downloadLink = document.createElement("a")
|
||||
downloadLink.href = URL.createObjectURL(zipContent)
|
||||
downloadLink.download = "world-exported.zip"
|
||||
downloadLink.click()
|
||||
|
||||
// Clean up the URL object after download
|
||||
URL.revokeObjectURL(downloadLink.href)
|
||||
}
|
||||
|
||||
window.exportWorld = exportWorld
|
||||
|
||||
const commands = [
|
||||
{
|
||||
command: ['/download', '/export'],
|
||||
invoke: exportWorld
|
||||
},
|
||||
{
|
||||
command: ['/publish'],
|
||||
// todo
|
||||
invoke: notImplemented
|
||||
},
|
||||
{
|
||||
command: '/reset-world -y',
|
||||
invoke: async () => {
|
||||
if (fsState.isReadonly || !fsState.syncFs) return
|
||||
// todo for testing purposes
|
||||
sessionStorage.oldData = localStorage
|
||||
singlePlayerServer.quit()
|
||||
// todo browserfs bug
|
||||
fs.rmdirSync(singlePlayerServer.options.worldFolder, { recursive: true })
|
||||
}
|
||||
},
|
||||
{
|
||||
command: ['/save'],
|
||||
invoke: () => {
|
||||
saveWorld()
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
export const tryHandleBuiltinCommand = (message) => {
|
||||
if (!singlePlayerServer) return
|
||||
|
||||
for (const command of commands) {
|
||||
if (command.command.includes(message)) {
|
||||
command.invoke()
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const saveWorld = async () => {
|
||||
for (const player of window.singlePlayerServer.players) {
|
||||
await player.save()
|
||||
}
|
||||
const worlds = [singlePlayerServer.overworld]
|
||||
for (const world of worlds) {
|
||||
await world.storageProvider.close()
|
||||
}
|
||||
}
|
||||
35
src/chat.js
35
src/chat.js
|
|
@ -5,6 +5,8 @@ const { activeModalStack, hideCurrentModal, showModal, miscUiState } = require('
|
|||
import { repeat } from 'lit/directives/repeat.js'
|
||||
import { classMap } from 'lit/directives/class-map.js'
|
||||
import { isCypress } from './utils'
|
||||
import { tryHandleBuiltinCommand } from './builtinCommands'
|
||||
import { notification } from './menus/notification'
|
||||
|
||||
const styles = {
|
||||
black: 'color:#000000',
|
||||
|
|
@ -71,6 +73,7 @@ class ChatBox extends LitElement {
|
|||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
background-color: rgba(0, 0, 0, 0);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.input-mobile {
|
||||
|
|
@ -91,6 +94,10 @@ class ChatBox extends LitElement {
|
|||
font-family: mojangles, minecraft, monospace;
|
||||
width: 100%;
|
||||
max-height: var(--chatHeight);
|
||||
pointer-events: none;
|
||||
}
|
||||
.chat.opened {
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
input[type=text], #chatinput {
|
||||
|
|
@ -98,6 +105,7 @@ class ChatBox extends LitElement {
|
|||
border: 1px solid rgba(0, 0, 0, 0);
|
||||
display: none;
|
||||
outline: none;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
#chatinput:focus {
|
||||
|
|
@ -161,18 +169,21 @@ class ChatBox extends LitElement {
|
|||
}
|
||||
|
||||
enableChat (initialText = '') {
|
||||
if (this.inChat) {
|
||||
hideCurrentModal()
|
||||
return
|
||||
}
|
||||
|
||||
notification.show = false
|
||||
const chat = this.shadowRoot.getElementById('chat-messages')
|
||||
/** @type {HTMLInputElement} */
|
||||
// @ts-ignore
|
||||
const chatInput = this.shadowRoot.getElementById('chatinput')
|
||||
|
||||
this.shadowRoot.getElementById('chat-wrapper2').classList.toggle('input-mobile', miscUiState.currentTouch)
|
||||
this.shadowRoot.getElementById('chat-wrapper').classList.toggle('display-mobile', miscUiState.currentTouch)
|
||||
|
||||
showModal(this)
|
||||
|
||||
// Exit the pointer lock
|
||||
document.exitPointerLock()
|
||||
document.exitPointerLock?.()
|
||||
// Show chat input
|
||||
chatInput.style.display = 'block'
|
||||
// Show extended chat history
|
||||
|
|
@ -242,9 +253,15 @@ class ChatBox extends LitElement {
|
|||
e.stopPropagation()
|
||||
|
||||
if (e.code === 'Enter') {
|
||||
this.chatHistory.push(chatInput.value)
|
||||
window.sessionStorage.chatHistory = JSON.stringify(this.chatHistory)
|
||||
client.write('chat', { message: chatInput.value })
|
||||
const message = chatInput.value
|
||||
if (message) {
|
||||
this.chatHistory.push(message)
|
||||
window.sessionStorage.chatHistory = JSON.stringify(this.chatHistory)
|
||||
const builtinHandled = tryHandleBuiltinCommand(message)
|
||||
if (!builtinHandled) {
|
||||
client.write('chat', { message })
|
||||
}
|
||||
}
|
||||
hideCurrentModal()
|
||||
}
|
||||
})
|
||||
|
|
@ -403,13 +420,13 @@ class ChatBox extends LitElement {
|
|||
render () {
|
||||
|
||||
return html`
|
||||
<div id="chat-wrapper" class="chat-wrapper chat-messages-wrapper">
|
||||
<div class="chat-wrapper chat-messages-wrapper ${miscUiState.currentTouch ? 'display-mobile' : ''}">
|
||||
<div class="chat ${this.inChat ? 'opened' : ''}" id="chat-messages">
|
||||
<!-- its to hide player joined at random timings, todo add chat tests as well -->
|
||||
${repeat(isCypress() ? [] : this.messages, (m) => m.id, (m) => this.renderMessage(m))}
|
||||
</div>
|
||||
</div>
|
||||
<div id="chat-wrapper2" class="chat-wrapper chat-input-wrapper">
|
||||
<div class="chat-wrapper chat-input-wrapper ${miscUiState.currentTouch ? 'input-mobile' : ''}">
|
||||
<div class="chat" id="chat-input">
|
||||
<input type="text" class="chat" id="chatinput" spellcheck="false" autocomplete="off"></input>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -6,3 +6,7 @@ export const startLocalServer = () => {
|
|||
const server = mcServer.createMCServer({ ...serverOptions, Server: LocalServer })
|
||||
return server
|
||||
}
|
||||
|
||||
// features that flying-squid doesn't support at all
|
||||
// todo move & generate in flying-squid
|
||||
export const unsupportedLocalServerFeatures = ['transactionPacketExists', 'teleportUsesOwnPacket']
|
||||
|
|
|
|||
|
|
@ -1,7 +1,20 @@
|
|||
//@ts-check
|
||||
/* global THREE performance */
|
||||
|
||||
const { Vec3 } = require('vec3')
|
||||
const { isGameActive } = require('./globalState')
|
||||
// wouldn't better to create atlas instead?
|
||||
import destroyStage0 from 'minecraft-assets/minecraft-assets/data/1.10/blocks/destroy_stage_0.png'
|
||||
import destroyStage1 from 'minecraft-assets/minecraft-assets/data/1.10/blocks/destroy_stage_1.png'
|
||||
import destroyStage2 from 'minecraft-assets/minecraft-assets/data/1.10/blocks/destroy_stage_2.png'
|
||||
import destroyStage3 from 'minecraft-assets/minecraft-assets/data/1.10/blocks/destroy_stage_3.png'
|
||||
import destroyStage4 from 'minecraft-assets/minecraft-assets/data/1.10/blocks/destroy_stage_4.png'
|
||||
import destroyStage5 from 'minecraft-assets/minecraft-assets/data/1.10/blocks/destroy_stage_5.png'
|
||||
import destroyStage6 from 'minecraft-assets/minecraft-assets/data/1.10/blocks/destroy_stage_6.png'
|
||||
import destroyStage7 from 'minecraft-assets/minecraft-assets/data/1.10/blocks/destroy_stage_7.png'
|
||||
import destroyStage8 from 'minecraft-assets/minecraft-assets/data/1.10/blocks/destroy_stage_8.png'
|
||||
import destroyStage9 from 'minecraft-assets/minecraft-assets/data/1.10/blocks/destroy_stage_9.png'
|
||||
|
||||
import { Vec3 } from 'vec3'
|
||||
import { isGameActive } from './globalState'
|
||||
|
||||
function getViewDirection (pitch, yaw) {
|
||||
const csPitch = Math.cos(pitch)
|
||||
|
|
@ -27,8 +40,20 @@ class Cursor {
|
|||
|
||||
const loader = new THREE.TextureLoader()
|
||||
this.breakTextures = []
|
||||
const destroyStagesImages = [
|
||||
destroyStage0,
|
||||
destroyStage1,
|
||||
destroyStage2,
|
||||
destroyStage3,
|
||||
destroyStage4,
|
||||
destroyStage5,
|
||||
destroyStage6,
|
||||
destroyStage7,
|
||||
destroyStage8,
|
||||
destroyStage9
|
||||
]
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const texture = loader.load('textures/' + viewer.version + '/blocks/destroy_stage_' + i + '.png')
|
||||
const texture = loader.load(destroyStagesImages[i])
|
||||
texture.magFilter = THREE.NearestFilter
|
||||
texture.minFilter = THREE.NearestFilter
|
||||
this.breakTextures.push(texture)
|
||||
|
|
@ -96,8 +121,10 @@ class Cursor {
|
|||
// Place
|
||||
if (cursorBlock && this.buttons[2] && (!this.lastButtons[2] || cursorChanged) && this.lastBlockPlaced >= 4) {
|
||||
const vecArray = [new Vec3(0, -1, 0), new Vec3(0, 1, 0), new Vec3(0, 0, -1), new Vec3(0, 0, 1), new Vec3(-1, 0, 0), new Vec3(1, 0, 0)]
|
||||
//@ts-ignore
|
||||
const delta = cursorBlock.intersect.minus(cursorBlock.position)
|
||||
// check instead?
|
||||
//@ts-ignore
|
||||
bot._placeBlockWithOptions(cursorBlock, vecArray[cursorBlock.face], { delta, forceLook: 'ignore' }).catch(console.warn)
|
||||
// this.lastBlockPlaced = 0
|
||||
}
|
||||
|
|
@ -148,4 +175,4 @@ class Cursor {
|
|||
}
|
||||
}
|
||||
|
||||
module.exports = Cursor
|
||||
export default Cursor
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
module.exports = {
|
||||
'motd': 'A Minecraft Server \nRunning flying-squid',
|
||||
// host: '',
|
||||
customPackets: true,
|
||||
'port': 25565,
|
||||
'max-players': 10,
|
||||
'online-mode': false,
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
import * as nbt from 'prismarine-nbt'
|
||||
import { promisify } from 'util'
|
||||
import { showNotification } from './menus/notification'
|
||||
import { openWorldDirectory, openWorldZip } from './browserfs'
|
||||
import { isGameActive } from './globalState'
|
||||
|
||||
const parseNbt = promisify(nbt.parse);
|
||||
const parseNbt = promisify(nbt.parse)
|
||||
window.nbt = nbt;
|
||||
|
||||
// todo display drop zone
|
||||
|
|
@ -18,13 +20,29 @@ window.nbt = nbt;
|
|||
window.addEventListener("drop", async e => {
|
||||
if (!e.dataTransfer?.files.length) return
|
||||
// todo support drop save folder
|
||||
const { files } = e.dataTransfer
|
||||
const file = files.item(0)!
|
||||
const buffer = await file.arrayBuffer()
|
||||
const parsed = await parseNbt(Buffer.from(buffer))
|
||||
showNotification({
|
||||
message: `${file.name} data available in browser console`,
|
||||
})
|
||||
console.log('raw', parsed)
|
||||
console.log('simplified', nbt.simplify(parsed).Data)
|
||||
const { items } = e.dataTransfer
|
||||
const item = items[0]
|
||||
const filehandle = await item.getAsFileSystemHandle() as FileSystemFileHandle | FileSystemDirectoryHandle
|
||||
if (filehandle.kind === 'file') {
|
||||
const file = await filehandle.getFile()
|
||||
|
||||
if (file.name.endsWith('.zip')) {
|
||||
openWorldZip(file)
|
||||
return
|
||||
}
|
||||
|
||||
const buffer = await file.arrayBuffer()
|
||||
const parsed = await parseNbt(Buffer.from(buffer))
|
||||
showNotification({
|
||||
message: `${file.name} data available in browser console`,
|
||||
})
|
||||
console.log('raw', parsed)
|
||||
console.log('simplified', nbt.simplify(parsed))
|
||||
} else {
|
||||
if (isGameActive(false)) {
|
||||
alert('Exit current world first, before loading a new one.')
|
||||
return
|
||||
}
|
||||
await openWorldDirectory(filehandle as FileSystemDirectoryHandle)
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -112,7 +112,9 @@ export const miscUiState = proxy({
|
|||
singleplayer: false
|
||||
})
|
||||
|
||||
// state that is not possible to get via bot
|
||||
window.miscUiState = miscUiState
|
||||
|
||||
// state that is not possible to get via bot and in-game specific
|
||||
export const gameAdditionalState = proxy({
|
||||
isFlying: false,
|
||||
isSprinting: false,
|
||||
|
|
@ -120,17 +122,6 @@ export const gameAdditionalState = proxy({
|
|||
|
||||
window.gameAdditionalState = gameAdditionalState
|
||||
|
||||
// todo thats weird workaround, probably we can do better?
|
||||
let forceDisableLeaveWarning = false
|
||||
const info = console.info
|
||||
console.info = (...args) => {
|
||||
const message = args[0]
|
||||
if (message === '[webpack-dev-server] App updated. Recompiling...') {
|
||||
forceDisableLeaveWarning = true
|
||||
}
|
||||
info.apply(console, args)
|
||||
}
|
||||
|
||||
const savePlayers = () => {
|
||||
if (!window.singlePlayerServer) return
|
||||
for (const player of window.singlePlayerServer.players) {
|
||||
|
|
@ -141,17 +132,19 @@ const savePlayers = () => {
|
|||
setInterval(() => {
|
||||
savePlayers()
|
||||
// todo investigate unload failures instead
|
||||
}, 1000)
|
||||
}, 2000)
|
||||
|
||||
window.addEventListener('unload', (e) => {
|
||||
savePlayers()
|
||||
})
|
||||
|
||||
window.inspectPlayer = () => require('fs').promises.readFile('/world/playerdata/9e487d23-2ffc-365a-b1f8-f38203f59233.dat').then(window.nbt.parse).then(console.log)
|
||||
|
||||
// todo move from global state
|
||||
window.addEventListener('beforeunload', (event) => {
|
||||
// todo-low maybe exclude chat?
|
||||
if (!isGameActive(true) && activeModalStack.at(-1)?.elem.id !== 'chat') return
|
||||
if (forceDisableLeaveWarning && options.preventDevReloadWhilePlaying === false) return
|
||||
if (sessionStorage.lastReload && options.preventDevReloadWhilePlaying === false) return
|
||||
|
||||
// For major browsers doning only this is enough
|
||||
event.preventDefault()
|
||||
|
|
@ -159,6 +152,4 @@ window.addEventListener('beforeunload', (event) => {
|
|||
// Display a confirmation prompt
|
||||
event.returnValue = '' // Required for some browsers
|
||||
return 'The game is running. Are you sure you want to close this page?'
|
||||
});
|
||||
|
||||
window.miscUiState = miscUiState
|
||||
})
|
||||
|
|
|
|||
9
src/globals.d.ts
vendored
9
src/globals.d.ts
vendored
|
|
@ -1,6 +1,6 @@
|
|||
/// <reference types="wicg-file-system-access" />
|
||||
|
||||
declare const THREE: typeof import('three');
|
||||
declare const THREE: typeof import('three')
|
||||
// todo
|
||||
declare const bot: import('mineflayer').Bot
|
||||
declare const singlePlayerServer: any
|
||||
|
|
@ -30,9 +30,10 @@ interface ObjectConstructor {
|
|||
}
|
||||
|
||||
declare module '*.css' {
|
||||
/**
|
||||
* @deprecated Use `import style from './style.css?inline'` instead.
|
||||
*/
|
||||
const css: string
|
||||
export default css
|
||||
}
|
||||
declare module '*.png' {
|
||||
const png: string
|
||||
export default png
|
||||
}
|
||||
|
|
|
|||
45
src/index.js
45
src/index.js
|
|
@ -10,7 +10,7 @@ require('iconify-icon')
|
|||
require('./chat')
|
||||
|
||||
// workaround for mineflayer
|
||||
process.versions.node = '14.0.0'
|
||||
process.versions.node = '18.0.0'
|
||||
|
||||
require('./menus/components/button')
|
||||
require('./menus/components/edit_box')
|
||||
|
|
@ -48,7 +48,7 @@ const nbt = require('prismarine-nbt')
|
|||
const pathfinder = require('mineflayer-pathfinder')
|
||||
const { Vec3 } = require('vec3')
|
||||
|
||||
const Cursor = require('./cursor')
|
||||
const Cursor = require('./cursor').default
|
||||
//@ts-ignore
|
||||
global.THREE = require('three')
|
||||
const { initVR } = require('./vr')
|
||||
|
|
@ -56,7 +56,7 @@ const { activeModalStack, showModal, hideModal, hideCurrentModal, activeModalSta
|
|||
const { pointerLock, goFullscreen, toNumber, isCypress } = require('./utils')
|
||||
const { notification } = require('./menus/notification')
|
||||
const { removePanorama, addPanoramaCubeMap, initPanoramaOptions } = require('./panorama')
|
||||
const { startLocalServer } = require('./createLocalServer')
|
||||
const { startLocalServer, unsupportedLocalServerFeatures } = require('./createLocalServer')
|
||||
const serverOptions = require('./defaultLocalServerOptions')
|
||||
const { customCommunication } = require('./customServer')
|
||||
const { default: updateTime } = require('./updateTime')
|
||||
|
|
@ -211,7 +211,7 @@ async function main () {
|
|||
})
|
||||
const connectSingleplayer = (serverOverrides = {}) => {
|
||||
// todo clean
|
||||
connect({ server: '', port: '', proxy: '', singleplayer: true, username: 'wanderer', password: '', serverOverrides })
|
||||
connect({ server: '', port: '', proxy: '', singleplayer: true, username: options.localUsername, password: '', serverOverrides })
|
||||
}
|
||||
document.querySelector('#title-screen').addEventListener('singleplayer', (e) => {
|
||||
//@ts-ignore
|
||||
|
|
@ -357,8 +357,7 @@ async function connect (connectOptions) {
|
|||
if (singeplayer) {
|
||||
window.serverDataChannel ??= {}
|
||||
window.worldLoaded = false
|
||||
//@ts-ignore TODO
|
||||
Object.assign(serverOptions, _.defaultsDeep(JSON.parse(localStorage.localServerOptions || '{}'), connectOptions.serverOverrides, serverOptions))
|
||||
Object.assign(serverOptions, _.defaultsDeep({}, options.localServerOptions, connectOptions.serverOverrides, serverOptions))
|
||||
singlePlayerServer = window.singlePlayerServer = startLocalServer()
|
||||
// todo need just to call quit if started
|
||||
// loadingScreen.maybeRecoverable = false
|
||||
|
|
@ -388,6 +387,14 @@ async function connect (connectOptions) {
|
|||
closeTimeout: 240 * 1000
|
||||
})
|
||||
if (singeplayer) {
|
||||
const _supportFeature = bot.supportFeature
|
||||
bot.supportFeature = (feature) => {
|
||||
if (unsupportedLocalServerFeatures.includes(feature)) {
|
||||
return false
|
||||
}
|
||||
return _supportFeature(feature)
|
||||
}
|
||||
|
||||
bot.emit('inject_allowed')
|
||||
bot._client.emit('connect')
|
||||
}
|
||||
|
|
@ -442,9 +449,7 @@ async function connect (connectOptions) {
|
|||
const d = subscribeKey(options, 'renderDistance', () => {
|
||||
singlePlayerServer.options['view-distance'] = options.renderDistance
|
||||
worldView.viewDistance = options.renderDistance
|
||||
if (miscUiState.singleplayer) {
|
||||
window.onPlayerChangeRenderDistance?.(options.renderDistance)
|
||||
}
|
||||
window.onPlayerChangeRenderDistance?.(options.renderDistance)
|
||||
})
|
||||
disposables.push(d)
|
||||
}
|
||||
|
|
@ -532,14 +537,14 @@ async function connect (connectOptions) {
|
|||
const cameraControlEl = hud
|
||||
|
||||
// after what time of holding the finger start breaking the block
|
||||
const touchBreakBlockMs = 500
|
||||
const touchStartBreakingBlockMs = 500
|
||||
let virtualClickActive = false
|
||||
let virtualClickTimeout
|
||||
/** @type {{id,x,y,sourceX,sourceY,jittered,time}?} */
|
||||
/** @type {{id,x,y,sourceX,sourceY,activateCameraMove,time}?} */
|
||||
let capturedPointer
|
||||
registerListener(document, 'pointerdown', (e) => {
|
||||
const clickedEl = e.composedPath()[0]
|
||||
if (!isGameActive(true) || !miscUiState.currentTouch || clickedEl !== cameraControlEl || capturedPointer) {
|
||||
if (!isGameActive(true) || !miscUiState.currentTouch || clickedEl !== cameraControlEl || capturedPointer || e.pointerId === undefined) {
|
||||
return
|
||||
}
|
||||
cameraControlEl.setPointerCapture(e.pointerId)
|
||||
|
|
@ -549,18 +554,16 @@ async function connect (connectOptions) {
|
|||
y: e.clientY,
|
||||
sourceX: e.clientX,
|
||||
sourceY: e.clientY,
|
||||
jittered: false,
|
||||
activateCameraMove: false,
|
||||
time: new Date()
|
||||
}
|
||||
virtualClickTimeout ??= setTimeout(() => {
|
||||
virtualClickActive = true
|
||||
document.dispatchEvent(new MouseEvent('mousedown', { button: 0 }))
|
||||
}, touchBreakBlockMs)
|
||||
}, {
|
||||
|
||||
}, touchStartBreakingBlockMs)
|
||||
})
|
||||
registerListener(document, 'pointermove', (e) => {
|
||||
if (e.pointerId !== capturedPointer?.id) return
|
||||
if (e.pointerId === undefined || e.pointerId !== capturedPointer?.id) return
|
||||
window.scrollTo(0, 0)
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
|
|
@ -569,8 +572,8 @@ async function connect (connectOptions) {
|
|||
// todo support .pressure (3d touch)
|
||||
const xDiff = Math.abs(e.pageX - capturedPointer.sourceX) > allowedJitter
|
||||
const yDiff = Math.abs(e.pageY - capturedPointer.sourceY) > allowedJitter
|
||||
if (!capturedPointer.jittered && (xDiff || yDiff)) capturedPointer.jittered = true
|
||||
if (capturedPointer.jittered) {
|
||||
if (!capturedPointer.activateCameraMove && (xDiff || yDiff)) capturedPointer.activateCameraMove = true
|
||||
if (capturedPointer.activateCameraMove) {
|
||||
clearTimeout(virtualClickTimeout)
|
||||
}
|
||||
onMouseMove({ movementX: e.pageX - capturedPointer.x, movementY: e.pageY - capturedPointer.y, type: 'touchmove' })
|
||||
|
|
@ -579,7 +582,7 @@ async function connect (connectOptions) {
|
|||
}, { passive: false })
|
||||
|
||||
registerListener(document, 'lostpointercapture', (e) => {
|
||||
if (e.pointerId !== capturedPointer?.id) return
|
||||
if (e.pointerId === undefined || e.pointerId !== capturedPointer?.id) return
|
||||
clearTimeout(virtualClickTimeout)
|
||||
virtualClickTimeout = undefined
|
||||
|
||||
|
|
@ -587,7 +590,7 @@ async function connect (connectOptions) {
|
|||
// button 0 is left click
|
||||
document.dispatchEvent(new MouseEvent('mouseup', { button: 0 }))
|
||||
virtualClickActive = false
|
||||
} else if (!capturedPointer.jittered && (Date.now() - capturedPointer.time < touchBreakBlockMs)) {
|
||||
} else if (!capturedPointer.activateCameraMove && (Date.now() - capturedPointer.time < touchStartBreakingBlockMs)) {
|
||||
document.dispatchEvent(new MouseEvent('mousedown', { button: 2 }))
|
||||
nextFrameFn.push(() => {
|
||||
document.dispatchEvent(new MouseEvent('mouseup', { button: 2 }))
|
||||
|
|
|
|||
|
|
@ -4,6 +4,9 @@ import * as nbt from 'prismarine-nbt'
|
|||
import { promisify } from 'util'
|
||||
import { options } from './optionsStorage'
|
||||
import { proxy } from 'valtio'
|
||||
import { nameToMcOfflineUUID } from './utils'
|
||||
import { forceCachedDataPaths } from './browserfs'
|
||||
import { gzip } from 'node-gzip'
|
||||
|
||||
const parseNbt = promisify(nbt.parse)
|
||||
|
||||
|
|
@ -15,12 +18,17 @@ export const fsState = proxy({
|
|||
|
||||
const PROPOSE_BACKUP = true
|
||||
|
||||
export const loadFolder = async () => {
|
||||
// todo-low cache reading
|
||||
export const loadFolder = async (root = '/world') => {
|
||||
// todo do it in singleplayer as well
|
||||
for (const key in forceCachedDataPaths) {
|
||||
delete forceCachedDataPaths[key]
|
||||
}
|
||||
|
||||
const warnings: string[] = []
|
||||
let levelDatContent
|
||||
try {
|
||||
levelDatContent = await fs.promises.readFile('/world/level.dat')
|
||||
// todo-low cache reading
|
||||
levelDatContent = await fs.promises.readFile(`${root}/level.dat`)
|
||||
} catch (err) {
|
||||
if (err.code === 'ENOENT') {
|
||||
if (!fsState.isReadonly) {
|
||||
|
|
@ -33,11 +41,12 @@ export const loadFolder = async () => {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
let version: string | undefined
|
||||
let isFlat = false
|
||||
if (levelDatContent) {
|
||||
const parsedRaw = await parseNbt(Buffer.from(levelDatContent))
|
||||
const levelDat: import('./mcTypes').LevelDat = nbt.simplify(parsedRaw).Data
|
||||
|
||||
version = levelDat.Version?.Name
|
||||
if (!version) {
|
||||
const newVersion = prompt(`In 1.8 and before world save doesn\'t contain version info, please enter version you want to use to load the world.\nSupported versions ${supportedVersions.join(', ')}`, '1.8.8')
|
||||
|
|
@ -48,7 +57,6 @@ export const loadFolder = async () => {
|
|||
warnings.push(`Version ${version} is not supported, supported versions ${supportedVersions.join(', ')}, 1.16.1 will be used`)
|
||||
version = '1.16.1'
|
||||
}
|
||||
let isFlat = false
|
||||
if (levelDat.WorldGenSettings) {
|
||||
for (const [key, value] of Object.entries(levelDat.WorldGenSettings.dimensions)) {
|
||||
if (key.slice(10) === 'overworld') {
|
||||
|
|
@ -64,6 +72,20 @@ export const loadFolder = async () => {
|
|||
if (!isFlat) {
|
||||
warnings.push(`Generator ${levelDat.generatorName} is not supported yet`)
|
||||
}
|
||||
|
||||
const playerUuid = nameToMcOfflineUUID(options.localUsername)
|
||||
const playerDatPath = `${root}/playerdata/${playerUuid}.dat`
|
||||
try {
|
||||
await fs.promises.stat(playerDatPath)
|
||||
} catch (err) {
|
||||
const playerDat = await gzip(nbt.writeUncompressed({ name: '', ...(parsedRaw.value.Data.value as Record<string, any>).Player }))
|
||||
if (fsState.isReadonly) {
|
||||
forceCachedDataPaths[playerDatPath] = playerDat
|
||||
} else {
|
||||
await fs.promises.writeFile(playerDatPath, playerDat)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (warnings.length) {
|
||||
|
|
@ -97,7 +119,15 @@ export const loadFolder = async () => {
|
|||
document.querySelector('#title-screen').dispatchEvent(new CustomEvent('singleplayer', {
|
||||
// todo check gamemode level.dat data etc
|
||||
detail: {
|
||||
version
|
||||
version,
|
||||
...isFlat ? {
|
||||
generation: {
|
||||
name: 'superflat'
|
||||
}
|
||||
} : {},
|
||||
...root !== '/world' ? {
|
||||
'worldFolder': root
|
||||
} : {}
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
const { LitElement, html, css } = require('lit')
|
||||
const { LitElement, html, css, unsafeCSS } = require('lit')
|
||||
const { guiIcons1_17_1 } = require('../hud')
|
||||
|
||||
class BreathBar extends LitElement {
|
||||
static get styles () {
|
||||
|
|
@ -22,13 +23,13 @@ class BreathBar extends LitElement {
|
|||
}
|
||||
|
||||
.breath.full {
|
||||
background-image: url('textures/1.17.1/gui/icons.png');
|
||||
background-image: url('${unsafeCSS(guiIcons1_17_1)}');
|
||||
background-size: 256px;
|
||||
background-position: var(--offset) var(--bg-y);
|
||||
}
|
||||
|
||||
.breath.half {
|
||||
background-image: url('textures/1.17.1/gui/icons.png');
|
||||
background-image: url('${unsafeCSS(guiIcons1_17_1)}');
|
||||
background-size: 256px;
|
||||
background-position: calc(var(--offset) - 9) var(--bg-y);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
const { LitElement, html, css } = require('lit')
|
||||
//@ts-check
|
||||
const widgetsGui = require('minecraft-assets/minecraft-assets/data/1.17.1/gui/widgets.png')
|
||||
const { LitElement, html, css, unsafeCSS } = require('lit')
|
||||
|
||||
const audioContext = new window.AudioContext()
|
||||
const sounds = {}
|
||||
|
|
@ -74,7 +76,7 @@ class Button extends LitElement {
|
|||
left: 0;
|
||||
width: calc(50% + 1px);
|
||||
height: 20px;
|
||||
background: url('textures/1.17.1/gui/widgets.png');
|
||||
background: url('${unsafeCSS(widgetsGui)}');
|
||||
background-size: 256px;
|
||||
background-position-y: calc(var(--txrV) * -1);
|
||||
z-index: -1;
|
||||
|
|
@ -88,7 +90,7 @@ class Button extends LitElement {
|
|||
left: 50%;
|
||||
width: 50%;
|
||||
height: 20px;
|
||||
background: url('textures/1.17.1/gui/widgets.png');
|
||||
background: url('${unsafeCSS(widgetsGui)}');
|
||||
background-size: 256px;
|
||||
background-position-x: calc(-200px + 100%);
|
||||
background-position-y: calc(var(--txrV) * -1);
|
||||
|
|
@ -131,6 +133,7 @@ class Button extends LitElement {
|
|||
constructor () {
|
||||
super()
|
||||
this.label = ''
|
||||
this.icon = undefined
|
||||
this.disabled = false
|
||||
this.width = '200px'
|
||||
this.onPress = () => { }
|
||||
|
|
@ -150,9 +153,9 @@ class Button extends LitElement {
|
|||
</button>`
|
||||
}
|
||||
|
||||
onBtnClick () {
|
||||
onBtnClick (e) {
|
||||
playSound('click_stereo.mp3')
|
||||
this.dispatchEvent(new window.CustomEvent('pmui-click'))
|
||||
this.dispatchEvent(new window.CustomEvent('pmui-click', { detail: e, }))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
const { LitElement, html, css } = require('lit')
|
||||
const { LitElement, html, css, unsafeCSS } = require('lit')
|
||||
const { guiIcons1_17_1 } = require('../hud')
|
||||
|
||||
class FoodBar extends LitElement {
|
||||
static get styles () {
|
||||
|
|
@ -19,7 +20,7 @@ class FoodBar extends LitElement {
|
|||
.food {
|
||||
width: 9px;
|
||||
height: 9px;
|
||||
background-image: url('textures/1.17.1/gui/icons.png'), url('textures/1.17.1/gui/icons.png');
|
||||
background-image: url('${unsafeCSS(guiIcons1_17_1)}'), url('${unsafeCSS(guiIcons1_17_1)}');
|
||||
background-size: 256px, 256px;
|
||||
background-position: var(--bg-x) var(--bg-y), var(--bg-x) var(--bg-y);
|
||||
margin-left: -1px;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
const { LitElement, html, css } = require('lit')
|
||||
const { LitElement, html, css, unsafeCSS } = require('lit')
|
||||
const { guiIcons1_17_1 } = require('../hud')
|
||||
|
||||
function getEffectClass (effect) {
|
||||
switch (effect.id) {
|
||||
|
|
@ -49,7 +50,7 @@ class HealthBar extends LitElement {
|
|||
.heart {
|
||||
width: 9px;
|
||||
height: 9px;
|
||||
background-image: url('textures/1.17.1/gui/icons.png'), url('textures/1.17.1/gui/icons.png');
|
||||
background-image: url('${unsafeCSS(guiIcons1_17_1)}'), url('${unsafeCSS(guiIcons1_17_1)}');
|
||||
background-size: 256px, 256px;
|
||||
background-position: var(--bg-x) var(--bg-y), var(--bg-x) var(--bg-y);
|
||||
margin-left: -1px;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
const { LitElement, html, css } = require('lit')
|
||||
const { LitElement, html, css, unsafeCSS } = require('lit')
|
||||
const invsprite = require('../../invsprite.json')
|
||||
const { isGameActive } = require('../../globalState')
|
||||
|
||||
const widgetsTexture = require('minecraft-assets/minecraft-assets/data/1.16.4/gui/widgets.png')
|
||||
|
||||
class Hotbar extends LitElement {
|
||||
static get styles () {
|
||||
return css`
|
||||
|
|
@ -12,7 +14,7 @@ class Hotbar extends LitElement {
|
|||
transform: translate(-50%);
|
||||
width: 182px;
|
||||
height: 22px;
|
||||
background: url("textures/1.16.4/gui/widgets.png");
|
||||
background: url("${unsafeCSS(widgetsTexture)}");
|
||||
background-size: 256px;
|
||||
}
|
||||
|
||||
|
|
@ -22,7 +24,7 @@ class Hotbar extends LitElement {
|
|||
top: -1px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
background: url("textures/1.16.4/gui/widgets.png");
|
||||
background: url("${unsafeCSS(widgetsTexture)}");
|
||||
background-size: 256px;
|
||||
background-position-y: -22px;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
const { LitElement, html, css } = require('lit')
|
||||
const { LitElement, html, css, unsafeCSS } = require('lit')
|
||||
|
||||
const widgetsGui = require('minecraft-assets/minecraft-assets/data/1.17.1/gui/widgets.png')
|
||||
|
||||
class Slider extends LitElement {
|
||||
static get styles () {
|
||||
|
|
@ -39,7 +41,7 @@ class Slider extends LitElement {
|
|||
left: 0;
|
||||
width: 50%;
|
||||
height: 20px;
|
||||
background: url('textures/1.17.1/gui/widgets.png');
|
||||
background: url('${unsafeCSS(widgetsGui)}');
|
||||
background-size: 256px;
|
||||
background-position-y: var(--txrV);
|
||||
z-index: -1;
|
||||
|
|
@ -54,7 +56,7 @@ class Slider extends LitElement {
|
|||
left: 50%;
|
||||
width: 50%;
|
||||
height: 20px;
|
||||
background: url('textures/1.17.1/gui/widgets.png');
|
||||
background: url('${unsafeCSS(widgetsGui)}');
|
||||
background-size: 256px;
|
||||
background-position-x: calc(-200px + 100%);
|
||||
background-position-y: var(--txrV);
|
||||
|
|
|
|||
|
|
@ -1,10 +1,13 @@
|
|||
//@ts-check
|
||||
const { LitElement, html, css } = require('lit')
|
||||
const { LitElement, html, css, unsafeCSS } = require('lit')
|
||||
const { isMobile } = require('./components/common')
|
||||
const { showModal, miscUiState } = require('../globalState')
|
||||
const { options, watchValue } = require('../optionsStorage')
|
||||
const { getGamemodeNumber } = require('../utils')
|
||||
|
||||
export const guiIcons1_17_1 = require('minecraft-assets/minecraft-assets/data/1.17.1/gui/icons.png')
|
||||
export const guiIcons1_16_4 = require('minecraft-assets/minecraft-assets/data/1.16.4/gui/icons.png')
|
||||
|
||||
class Hud extends LitElement {
|
||||
static get styles () {
|
||||
return css`
|
||||
|
|
@ -21,7 +24,7 @@ class Hud extends LitElement {
|
|||
.crosshair {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background: url('textures/1.17.1/gui/icons.png');
|
||||
background: url('${unsafeCSS(guiIcons1_17_1)}');
|
||||
background-size: 256px;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
|
|
@ -49,7 +52,7 @@ class Hud extends LitElement {
|
|||
transform: translate(-50%);
|
||||
width: 182px;
|
||||
height: 5px;
|
||||
background-image: url('textures/1.16.4/gui/icons.png');
|
||||
background-image: url('${unsafeCSS(guiIcons1_16_4)}');
|
||||
background-size: 256px;
|
||||
background-position-y: -64px;
|
||||
}
|
||||
|
|
@ -57,7 +60,7 @@ class Hud extends LitElement {
|
|||
.xp-bar {
|
||||
width: 182px;
|
||||
height: 5px;
|
||||
background-image: url('textures/1.17.1/gui/icons.png');
|
||||
background-image: url('${unsafeCSS(guiIcons1_17_1)}');
|
||||
background-size: 256px;
|
||||
background-position-y: -69px;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,8 +2,9 @@
|
|||
const { LitElement, html, css } = require('lit')
|
||||
const { commonCss } = require('./components/common')
|
||||
const { addPanoramaCubeMap } = require('../panorama')
|
||||
const { hideModal, activeModalStacks, activeModalStack, replaceActiveModalStack } = require('../globalState')
|
||||
const { hideModal, activeModalStacks, activeModalStack, replaceActiveModalStack, miscUiState } = require('../globalState')
|
||||
const { guessProblem } = require('../guessProblem')
|
||||
const { fsState } = require('../loadFolder')
|
||||
|
||||
class LoadingErrorScreen extends LitElement {
|
||||
static get styles () {
|
||||
|
|
@ -84,6 +85,14 @@ class LoadingErrorScreen extends LitElement {
|
|||
}
|
||||
document.getElementById('play-screen').style.display = 'block'
|
||||
addPanoramaCubeMap()
|
||||
}}></pmui-button><pmui-button .hidden=${!(miscUiState.singleplayer && fsState.syncFs)} pmui-width="200px" pmui-label="Reset world" @pmui-click=${() => {
|
||||
if (!confirm('Are you sure you want to delete all local world content?')) return
|
||||
for (const key of Object.keys(localStorage)) {
|
||||
if (/^[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}$/g.test(key) || key === '/') {
|
||||
localStorage.removeItem(key)
|
||||
}
|
||||
}
|
||||
window.location.reload()
|
||||
}}></pmui-button><pmui-button @pmui-click=${() => window.location.reload()} pmui-label="Full Reload" pmui-width="200px"></pmui-button></div>`
|
||||
: ''
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,11 @@
|
|||
//@ts-check
|
||||
const { LitElement, html, css } = require('lit')
|
||||
const { openURL } = require('./components/common')
|
||||
const { hideCurrentModal, showModal } = require('../globalState')
|
||||
const { fsState } = require('../loadFolder')
|
||||
const { subscribe } = require('valtio')
|
||||
const { saveWorld } = require('../builtinCommands')
|
||||
const { notification } = require('./notification')
|
||||
|
||||
class PauseScreen extends LitElement {
|
||||
static get styles () {
|
||||
|
|
@ -67,16 +70,10 @@ class PauseScreen extends LitElement {
|
|||
<pmui-button pmui-width="98px" pmui-label="Discord" @pmui-click=${() => openURL('https://discord.gg/4Ucm684Fq3')}></pmui-button>
|
||||
</div>
|
||||
<pmui-button pmui-width="204px" pmui-label="Options" @pmui-click=${() => showModal(document.getElementById('options-screen'))}></pmui-button>
|
||||
<pmui-button pmui-width="204px" pmui-label="${!fsState.syncFs && !fsState.isReadonly ? 'Save & quit' : 'Disconnect'}" @pmui-click=${async () => {
|
||||
<pmui-button pmui-width="204px" pmui-label="${!fsState.syncFs && !fsState.isReadonly ? 'Save & Quit' : 'Disconnect'}" @pmui-click=${async () => {
|
||||
if (window.singlePlayerServer) {
|
||||
for (const player of window.singlePlayerServer.players) {
|
||||
const worlds = [singlePlayerServer.overworld]
|
||||
for (const world of worlds) {
|
||||
await world.storageProvider.close()
|
||||
}
|
||||
await player.save()
|
||||
singlePlayerServer.quit()
|
||||
}
|
||||
await saveWorld()
|
||||
singlePlayerServer.quit()
|
||||
}
|
||||
bot._client.emit('end')
|
||||
}}></pmui-button>
|
||||
|
|
|
|||
|
|
@ -1,8 +1,13 @@
|
|||
const { openWorldDirectory } = require('../browserfs')
|
||||
const { openWorldDirectory, openWorldZip } = require('../browserfs')
|
||||
const { showModal } = require('../globalState')
|
||||
const { fsState } = require('../loadFolder')
|
||||
const { openURL } = require('./components/common')
|
||||
const { LitElement, html, css } = require('lit')
|
||||
const { LitElement, html, css, unsafeCSS } = require('lit')
|
||||
|
||||
const mcImage = require('minecraft-assets/minecraft-assets/data/1.17.1/gui/title/minecraft.png')
|
||||
|
||||
// const SUPPORT_WORLD_LOADING = !!window.showDirectoryPicker
|
||||
const SUPPORT_WORLD_LOADING = true
|
||||
|
||||
class TitleScreen extends LitElement {
|
||||
static get styles () {
|
||||
|
|
@ -18,7 +23,7 @@ class TitleScreen extends LitElement {
|
|||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
background-image: url('textures/1.17.1/gui/title/minecraft.png');
|
||||
background-image: url('${unsafeCSS(mcImage)}');
|
||||
background-size: 256px;
|
||||
width: 155px;
|
||||
height: 44px;
|
||||
|
|
@ -29,7 +34,7 @@ class TitleScreen extends LitElement {
|
|||
position: absolute;
|
||||
top: 0;
|
||||
left: 155px;
|
||||
background-image: url('textures/1.17.1/gui/title/minecraft.png');
|
||||
background-image: url('${unsafeCSS(mcImage)}');
|
||||
background-size: 256px;
|
||||
width: 155px;
|
||||
height: 44px;
|
||||
|
|
@ -125,7 +130,9 @@ class TitleScreen extends LitElement {
|
|||
this.versionStatus = ''
|
||||
this.versionTitle = ''
|
||||
this.isOutdated = false
|
||||
if (process.env.NODE_ENV !== 'development') {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
this.versionStatus = '(dev)'
|
||||
} else {
|
||||
fetch('./version.txt').then(async (f) => {
|
||||
if (f.status === 404) return
|
||||
const contents = await f.text()
|
||||
|
|
@ -148,16 +155,42 @@ class TitleScreen extends LitElement {
|
|||
<div class="menu">
|
||||
<pmui-button pmui-width="200px" pmui-label="Connect to server" @pmui-click=${() => showModal(document.getElementById('play-screen'))}></pmui-button>
|
||||
<div style="display:flex;justify-content: space-between;">
|
||||
<pmui-button pmui-width="${window['showDirectoryPicker'] ? '170px' : '200px'}" pmui-label="Singleplayer" @pmui-click=${() => {
|
||||
<pmui-button pmui-width="${SUPPORT_WORLD_LOADING ? '170px' : '200px'}" pmui-label="Singleplayer" @pmui-click=${() => {
|
||||
this.style.display = 'none'
|
||||
Object.assign(fsState, {
|
||||
isReadonly: false,
|
||||
syncFs: true,
|
||||
})
|
||||
Object.assign(fsState, {
|
||||
isReadonly: false,
|
||||
syncFs: true,
|
||||
})
|
||||
this.dispatchEvent(new window.CustomEvent('singleplayer', {}))
|
||||
}}></pmui-button>
|
||||
${window['showDirectoryPicker'] ? html`<pmui-button pmui-icon="pixelarticons:folder" pmui-width="20px" pmui-label="" @pmui-click=${() => {
|
||||
openWorldDirectory()
|
||||
${SUPPORT_WORLD_LOADING ? html`<pmui-button pmui-icon="pixelarticons:folder" pmui-width="20px" pmui-label="" @pmui-click=${({ detail: e }) => {
|
||||
if (!!window.showDirectoryPicker && !e.shiftKey) {
|
||||
openWorldDirectory()
|
||||
} else {
|
||||
// create and show input picker
|
||||
/** @type {HTMLInputElement} */
|
||||
let picker = document.body.querySelector('input#file-zip-picker')
|
||||
if (!picker) {
|
||||
picker = document.createElement('input')
|
||||
picker.type = 'file'
|
||||
picker.accept = '.zip'
|
||||
|
||||
picker.addEventListener('change', () => {
|
||||
const file = picker.files[0]
|
||||
picker.value = ''
|
||||
if (!file) return
|
||||
if (!file.name.endsWith('.zip')) {
|
||||
const doContinue = confirm(`Are you sure ${file.name.slice(-20)} is .zip file? Only .zip files are supported. Continue?`)
|
||||
if (!doContinue) return
|
||||
}
|
||||
openWorldZip(file)
|
||||
})
|
||||
picker.hidden = true
|
||||
document.body.appendChild(picker)
|
||||
}
|
||||
|
||||
picker.click()
|
||||
}
|
||||
}}></pmui-button>` : ''}
|
||||
</div>
|
||||
<pmui-button pmui-width="200px" pmui-label="Options" @pmui-click=${() => showModal(document.getElementById('options-screen'))}></pmui-button>
|
||||
|
|
|
|||
|
|
@ -14,7 +14,9 @@ const defaultOptions = {
|
|||
maxMultiplayerRenderDistance: 6,
|
||||
excludeCommunicationDebugEvents: [],
|
||||
preventDevReloadWhilePlaying: false,
|
||||
numWorkers: 4
|
||||
numWorkers: 4,
|
||||
localServerOptions: {},
|
||||
localUsername: 'wanderer',
|
||||
}
|
||||
|
||||
export const options = proxy(
|
||||
|
|
|
|||
|
|
@ -84,7 +84,7 @@ function InventoryWrapper() {
|
|||
|
||||
useEffect(() => {
|
||||
if (isInventoryOpen) {
|
||||
document.exitPointerLock()
|
||||
document.exitPointerLock?.()
|
||||
}
|
||||
}, [isInventoryOpen])
|
||||
|
||||
|
|
@ -97,7 +97,7 @@ function InventoryWrapper() {
|
|||
|
||||
useEffect(() => {
|
||||
bot.inventory.on('updateSlot', () => {
|
||||
setSlots(bot.inventory.slots)
|
||||
setSlots([...bot.inventory.slots])
|
||||
})
|
||||
// todo need to think of better solution
|
||||
window['mcData'] = require('minecraft-data')(bot.version)
|
||||
|
|
@ -119,8 +119,6 @@ function InventoryWrapper() {
|
|||
`}>
|
||||
<InventoryNew slots={slots} action={(oldSlot, newSlotIndex) => {
|
||||
bot.moveSlotItem(oldSlot, newSlotIndex)
|
||||
// bot.inventory.selectedItem
|
||||
// bot.inventory.updateSlot(oldSlot, )
|
||||
} } />
|
||||
</div>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ html {
|
|||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
background: url('textures/1.17.1/gui/options_background.png'), rgba(0, 0, 0, 0.7);
|
||||
background: url('minecraft-assets/minecraft-assets/data/1.17.1/gui/options_background.png'), rgba(0, 0, 0, 0.7);
|
||||
background-size: 16px;
|
||||
background-repeat: repeat;
|
||||
width: 100%;
|
||||
|
|
|
|||
16
src/utils.js
16
src/utils.js
|
|
@ -1,6 +1,8 @@
|
|||
//@ts-check
|
||||
import { activeModalStack, miscUiState } from './globalState'
|
||||
import { notification } from './menus/notification'
|
||||
import * as crypto from 'crypto'
|
||||
import UUID from 'uuid-1345'
|
||||
|
||||
export const goFullscreen = async (doToggle = false) => {
|
||||
if (!document.fullscreenElement) {
|
||||
|
|
@ -116,3 +118,17 @@ export const getGamemodeNumber = (bot) => {
|
|||
export const isCypress = () => {
|
||||
return localStorage.cypress === 'true'
|
||||
}
|
||||
|
||||
// https://github.com/PrismarineJS/node-minecraft-protocol/blob/cf1f67117d586b5e6e21f0d9602da12e9fcf46b6/src/server/login.js#L170
|
||||
function javaUUID (s) {
|
||||
const hash = crypto.createHash('md5')
|
||||
hash.update(s, 'utf8')
|
||||
const buffer = hash.digest()
|
||||
buffer[6] = (buffer[6] & 0x0f) | 0x30
|
||||
buffer[8] = (buffer[8] & 0x3f) | 0x80
|
||||
return buffer
|
||||
}
|
||||
|
||||
export function nameToMcOfflineUUID (name) {
|
||||
return (new UUID(javaUUID('OfflinePlayer:' + name))).toString()
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue