next release preparing (#10)

This commit is contained in:
Vitaly 2023-08-30 12:31:57 +03:00 committed by GitHub
commit abb6b06b53
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
37 changed files with 661 additions and 207 deletions

View file

@ -21,4 +21,3 @@ jobs:
with:
name: cypress-images
path: cypress/integration/__image_snapshots__/
if-no-files-found: ignore

View file

@ -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
View file

@ -6,6 +6,7 @@ public
.env.local
Thumbs.db
build
localSettings.mjs
dist
.DS_Store
.idea/

8
.vscode/launch.json vendored
View file

@ -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*",

View file

@ -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

View file

@ -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')

View file

@ -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,

View file

@ -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"
}
}

View file

@ -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 = [

View file

@ -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 }

View file

@ -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')

View file

@ -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) {

View file

@ -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
View 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()
}
}

View file

@ -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>

View file

@ -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']

View file

@ -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

View file

@ -1,7 +1,6 @@
module.exports = {
'motd': 'A Minecraft Server \nRunning flying-squid',
// host: '',
customPackets: true,
'port': 25565,
'max-players': 10,
'online-mode': false,

View file

@ -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)
}
})

View file

@ -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
View file

@ -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
}

View file

@ -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 }))

View file

@ -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
} : {}
},
}))
}

View file

@ -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);
}

View file

@ -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, }))
}
}

View file

@ -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;

View file

@ -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;

View file

@ -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;
}

View file

@ -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);

View file

@ -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;
}

View file

@ -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>`
: ''
}

View file

@ -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>

View file

@ -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>

View file

@ -14,7 +14,9 @@ const defaultOptions = {
maxMultiplayerRenderDistance: 6,
excludeCommunicationDebugEvents: [],
preventDevReloadWhilePlaying: false,
numWorkers: 4
numWorkers: 4,
localServerOptions: {},
localUsername: 'wanderer',
}
export const options = proxy(

View file

@ -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>
}

View file

@ -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%;

View file

@ -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()
}