Skip to content

Commit

Permalink
protocol & errors improvements & cleanup (#22)
Browse files Browse the repository at this point in the history
zardoy authored Sep 25, 2023

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
2 parents ee8380b + 3ca17f1 commit 358203f
Showing 25 changed files with 557 additions and 400 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -11,7 +11,7 @@ jobs:
- name: Install pnpm
run: npm i -g pnpm
- run: pnpm install
- run: pnpm build
- run: pnpm check-build
- run: nohup pnpm prod-start &
- run: nohup node cypress/minecraft-server.mjs &
- uses: cypress-io/github-action@v5
Binary file modified cypress/integration/__image_snapshots__/superflat-world #0.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
72 changes: 46 additions & 26 deletions cypress/integration/index.spec.ts
Original file line number Diff line number Diff line change
@@ -1,58 +1,78 @@
/// <reference types="cypress" />
import type { AppOptions } from '../../src/optionsStorage'

const setLocalStorageSettings = () => {
const cleanVisit = () => {
window.localStorage.clear()
visit()
}

const visit = (url = '/') => {
window.localStorage.cypress = 'true'
window.localStorage.server = 'localhost'
cy.visit(url)
}

// todo use ssl

const compareRenderedFlatWorld = () => {
// wait for render
// cy.wait(6000)
// cy.get('body').toMatchImageSnapshot({
// name: 'superflat-world',
// })
}

const testWorldLoad = () => {
cy.document().then({ timeout: 20_000, }, doc => {
return new Cypress.Promise(resolve => {
doc.addEventListener('cypress-world-ready', resolve)
})
}).then(() => {
compareRenderedFlatWorld()
})
}

const setOptions = (options: Partial<AppOptions>) => {
cy.window().then(win => {
Object.assign(win['options'], options)
})
}

it('Loads & renders singleplayer', () => {
cy.visit('/')
window.localStorage.clear()
window.localStorage.setItem('options', JSON.stringify({
cleanVisit()
setOptions({
localServerOptions: {
generation: {
name: 'superflat',
options: { seed: 250869072 }
}
},
},
renderDistance: 2
}))
setLocalStorageSettings()
})
cy.get('#title-screen').find('[data-test-id="singleplayer-button"]', { includeShadowDom: true, }).click()
// todo implement load event
cy.wait(12000)
testWorldLoad()
})

// even on local testing indeed it doesn't work sometimes, but sometimes it does
it.skip('Joins to server', () => {
cy.visit('/')
setLocalStorageSettings()
window.localStorage.version = ''
it('Joins to server', () => {
// visit('/?version=1.16.1')
window.localStorage.version = '1.16.1'
visit()
// todo replace with data-test
cy.get('#title-screen').find('[data-test-id="connect-screen-button"]', { includeShadowDom: true, }).click()
cy.get('input#serverip', { includeShadowDom: true, }).clear().focus().type('localhost')
cy.get('[data-test-id="connect-to-server"]', { includeShadowDom: true, }).click()
// todo implement load event
cy.wait(16000)
testWorldLoad()
})

it('Loads & renders zip world', () => {
cy.visit('/')
setLocalStorageSettings()
cleanVisit()
cy.get('#title-screen').find('[data-test-id="select-file-folder"]', { includeShadowDom: true, }).click({ shiftKey: true })
cy.get('input[type="file"]').selectFile('cypress/superflat.zip', { force: true })
// todo implement load event
cy.wait(10000)
testWorldLoad()
})

it.skip('Performance test', () => {
cy.visit('/')
window.localStorage.cypress = 'true'
window.localStorage.setItem('renderDistance', '6')
cy.get('#title-screen').find('.menu > div:nth-child(2) > pmui-button:nth-child(1)', { includeShadowDom: true, }).selectFile('worlds')
// -2 85 24
// select that world
// from -2 85 24
// await bot.loadPlugin(pathfinder.pathfinder)
// bot.pathfinder.goto(new pathfinder.goals.GoalXZ(28, -28))
})
8 changes: 6 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -5,7 +5,8 @@
"scripts": {
"start": "node scripts/build.js copyFilesDev && node scripts/prepareData.mjs && node esbuild.mjs --watch",
"start-watch-script": "nodemon -w esbuild.mjs esbuild.mjs",
"build": "node scripts/build.js copyFiles && node scripts/prepareData.mjs && node esbuild.mjs --minify --prod",
"build": "node scripts/build.js copyFiles && node scripts/prepareData.mjs -f && node esbuild.mjs --minify --prod",
"check-build": "tsc && pnpm build",
"watch": "node scripts/build.js copyFilesDev && webpack serve --config webpack.dev.js --progress",
"test:cypress": "cypress run",
"test:e2e": "start-test http-get://localhost:8080 test:cypress",
@@ -31,9 +32,10 @@
"browserfs": "github:zardoy/browserfs#build",
"compression": "^1.7.4",
"cypress-plugin-snapshots": "^1.4.4",
"debug": "^4.3.4",
"diamond-square": "^1.2.0",
"eruda": "^3.0.1",
"esbuild": "^0.19.2",
"esbuild": "^0.19.3",
"esbuild-loader": "^4.0.0",
"esbuild-plugin-polyfill-node": "^0.3.0",
"express": "^4.18.2",
@@ -42,6 +44,7 @@
"iconify-icon": "^1.0.8",
"jszip": "^3.10.1",
"lit": "^2.8.0",
"lodash": "^4.17.21",
"minecraft-data": "^3.0.0",
"net-browserify": "github:PrismarineJS/net-browserify",
"peerjs": "^1.5.0",
@@ -90,6 +93,7 @@
"style-loader": "^3.3.3",
"three": "0.128.0",
"timers-browserify": "^2.0.12",
"typescript": "^5.2.2",
"url-loader": "^4.1.1",
"use-typed-event-listener": "^4.0.2",
"vite": "^4.4.9",
348 changes: 190 additions & 158 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

10 changes: 7 additions & 3 deletions prismarine-viewer/buildWorker.mjs
Original file line number Diff line number Diff line change
@@ -29,9 +29,13 @@ const buildOptions = {
setup (build) {
build.onResolve({ filter: /\.json$/ }, args => {
const fileName = args.path.split('/').pop().replace('.json', '')
if (args.resolveDir.includes('minecraft-data') && !allowedWorkerFiles.includes(fileName)) {
// console.log('skipped', fileName)
return { path: args.path, namespace: 'empty-file', }
if (args.resolveDir.includes('minecraft-data')) {
if (args.path.replaceAll('\\', '/').endsWith('bedrock/common/protocolVersions.json')) {
return
}
if (!allowedWorkerFiles.includes(fileName) || args.path.includes('bedrock')) {
return { path: args.path, namespace: 'empty-file', }
}
}
})
build.onResolve({
2 changes: 1 addition & 1 deletion prismarine-viewer/package.json
Original file line number Diff line number Diff line change
@@ -8,7 +8,7 @@
"pretest": "npm run lint",
"lint": "standard",
"fix": "standard --fix",
"prepare": "node viewer/prerender.js && node buildWorker.mjs"
"prepare": "node viewer/generateTextures.js && node buildWorker.mjs"
},
"author": "PrismarineJS",
"license": "MIT",
File renamed without changes.
34 changes: 16 additions & 18 deletions scripts/build.js
Original file line number Diff line number Diff line change
@@ -1,39 +1,37 @@
//@ts-check
const fsExtra = require('fs-extra')
const defaultLocalServerOptions = require('../src/defaultLocalServerOptions')
const glob = require('glob')
const fs = require('fs')
const crypto = require('crypto')
const path = require('path')

// these files need to be copied before build for now
const filesAlwaysToCopy = [
// { from: './node_modules/prismarine-viewer2/public/supportedVersions.json', to: './prismarine-viewer/public/supportedVersions.json' },
]
// 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 prismarineViewerBase = "./node_modules/prismarine-viewer"

// these files could be copied at build time eg with copy plugin, but copy plugin slows down the config so we copy them there, alternative we could inline it in esbuild config
const webpackFilesToCopy = [
{ from: './prismarine-viewer/public/blocksStates/', to: 'dist/blocksStates/' },
// { from: './prismarine-viewer/public/textures/', to: 'dist/textures/' },
// { from: './prismarine-viewer/public/textures/1.17.1/gui', to: 'dist/gui' },
{ from: './prismarine-viewer/public/worker.js', to: 'dist/worker.js' },
// { from: './prismarine-viewer/public/supportedVersions.json', to: 'dist/supportedVersions.json' },
{ from: `${prismarineViewerBase}/public/blocksStates/`, to: 'dist/blocksStates/' },
{ from: `${prismarineViewerBase}/public/worker.js`, to: 'dist/worker.js' },
{ from: './assets/', to: './dist/' },
{ from: './config.json', to: 'dist/config.json' }
{ from: './config.json', to: 'dist/config.json' },
{ from: `${prismarineViewerBase}/public/textures/1.16.4/entity`, to: 'dist/textures/1.16.4/entity' },
]
exports.webpackFilesToCopy = webpackFilesToCopy
exports.copyFiles = (isDev = false) => {
console.time('copy files');
[...filesAlwaysToCopy, ...webpackFilesToCopy].forEach(file => {
fsExtra.copySync(file.from, file.to)
})
// todo copy directly only needed
const cwd = './prismarine-viewer/public/textures/'
const files = glob.sync('{*/entity/**,*.png}', { cwd: cwd, nodir: true, })
console.time('copy files')
// copy glob
const cwd = `${prismarineViewerBase}/public/textures/`
const files = glob.sync('*.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)
}

webpackFilesToCopy.forEach(file => {
fsExtra.copySync(file.from, file.to)
})

console.timeEnd('copy files')
}

2 changes: 1 addition & 1 deletion scripts/prepareData.mjs
Original file line number Diff line number Diff line change
@@ -4,7 +4,7 @@ import { existsSync } from 'node:fs'
import Module from "node:module"
import { dirname } from 'node:path'

if (existsSync('dist/mc-data')) {
if (existsSync('dist/mc-data') && !process.argv.includes('-f')) {
console.log('using cached prepared data')
process.exit(0)
}
11 changes: 8 additions & 3 deletions server.js
Original file line number Diff line number Diff line change
@@ -29,9 +29,14 @@ if (process.argv[3] === 'dev') {
app.use(express.static(path.join(__dirname, './dist')))
}

const portArg = process.argv.indexOf('--port')
const port = (require.main === module ? process.argv[2] : portArg !== -1 ? process.argv[portArg + 1] : undefined) || 8080

// Start the server
const server = process.argv.includes('--prod') ? undefined : app.listen(require.main !== module || process.argv[2] === undefined ? 8080 : process.argv[2], function () {
console.log('Server listening on port ' + server.address().port)
})
const server = process.argv.includes('--prod') ?
undefined :
app.listen(port, function () {
console.log('Server listening on port ' + server.address().port)
})

module.exports = { app }
90 changes: 60 additions & 30 deletions src/cursor.js → src/blockInteraction.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
//@ts-check
/* global THREE performance */

// wouldn't better to create atlas instead?
import destroyStage0 from 'minecraft-assets/minecraft-assets/data/1.10/blocks/destroy_stage_0.png'
@@ -24,25 +23,18 @@ function getViewDirection (pitch, yaw) {
return new Vec3(-snYaw * csPitch, snPitch, -csYaw * csPitch)
}

class Cursor {
class BlockInteraction {
static instance = null

constructor (viewer, renderer, /** @type {import('mineflayer').Bot} */bot) {
init () {
bot.on('physicsTick', () => { if (this.lastBlockPlaced < 4) this.lastBlockPlaced++ })
if (Cursor.instance) return Cursor.instance

// Init state
this.buttons = [false, false, false]
this.lastButtons = [false, false, false]
this.breakStartTime = 0
this.cursorBlock = null

// Setup graphics
const blockGeometry = new THREE.BoxGeometry(1.001, 1.001, 1.001)
this.cursorMesh = new THREE.LineSegments(new THREE.EdgesGeometry(blockGeometry), new THREE.LineBasicMaterial({ color: 0 }))
this.cursorMesh.visible = false
viewer.scene.add(this.cursorMesh)

const loader = new THREE.TextureLoader()
this.breakTextures = []
const destroyStagesImages = [
@@ -67,7 +59,7 @@ class Cursor {
transparent: true,
blending: THREE.MultiplyBlending
})
this.blockBreakMesh = new THREE.Mesh(blockGeometry, breakMaterial)
this.blockBreakMesh = new THREE.Mesh(new THREE.BoxGeometry(1, 1, 1), breakMaterial)
this.blockBreakMesh.visible = false
this.blockBreakMesh.renderOrder = 999
viewer.scene.add(this.blockBreakMesh)
@@ -111,8 +103,31 @@ class Cursor {
})
}

/** @type {null | {blockPos,mesh}} */
interactionLines = null
updateBlockInteractionLines (/** @type {Vec3 | null} */blockPos, /** @type {{position, width, height, depth}[]} */shapePositions = undefined) {
if (this.interactionLines !== null) {
viewer.scene.remove(this.interactionLines.mesh)
this.interactionLines = null
}
if (blockPos === null || (this.interactionLines && blockPos.equals(this.interactionLines.blockPos))) {
return
}

const group = new THREE.Group()
for (const { position, width, height, depth } of shapePositions) {
const geometry = new THREE.BoxGeometry(1.001 * width, 1.001 * height, 1.001 * depth)
const mesh = new THREE.LineSegments(new THREE.EdgesGeometry(geometry), new THREE.LineBasicMaterial({ color: 0 }))
const pos = blockPos.plus(position)
mesh.position.set(pos.x, pos.y, pos.z)
group.add(mesh)
}
viewer.scene.add(group)
this.interactionLines = { blockPos, mesh: group }
}

// todo this shouldnt be done in the render loop, migrate the code to dom events to avoid delays on lags
update (/** @type {import('mineflayer').Bot} */bot) {
update () {
const cursorBlock = bot.blockAtCursor(5)
let cursorBlockDiggable = cursorBlock
if (!bot.canDigBlock(cursorBlock) && bot.game.gameMode !== 'creative') cursorBlockDiggable = null
@@ -153,25 +168,29 @@ class Cursor {

// Show cursor
if (!cursorBlock) {
this.cursorMesh.visible = false
this.updateBlockInteractionLines(null)
} else {
for (const collisionData of [...cursorBlock.shapes, ...cursorBlock['interactionShapes'] ?? []].slice(0, 1) ?? []) {
const width = collisionData[3] - collisionData[0]
const height = collisionData[4] - collisionData[1]
const depth = collisionData[5] - collisionData[2]

const initialSize = 1.001
this.cursorMesh.scale.set(width * initialSize, height * initialSize, depth * initialSize)
this.blockBreakMesh.scale.set(width * initialSize, height * initialSize, depth * initialSize)
// this.cursorMesh.position.set(cursorBlock.position.x + 0.5, cursorBlock.position.y + 0.5, cursorBlock.position.z + 0.5)
const centerX = (collisionData[3] + collisionData[0]) / 2
const centerY = (collisionData[4] + collisionData[1]) / 2
const centerZ = (collisionData[5] + collisionData[2]) / 2
this.cursorMesh.position.set(cursorBlock.position.x + centerX, cursorBlock.position.y + centerY, cursorBlock.position.z + centerZ)
this.blockBreakMesh.position.set(cursorBlock.position.x + centerX, cursorBlock.position.y + centerY, cursorBlock.position.z + centerZ)
const allShapes = [...cursorBlock.shapes, ...cursorBlock['interactionShapes'] ?? []]
this.updateBlockInteractionLines(cursorBlock.position, allShapes.map(shape => {
return getDataFromShape(shape)
}))
{
// union of all values
const breakShape = allShapes.reduce((acc, cur) => {
return [
Math.min(acc[0], cur[0]),
Math.min(acc[1], cur[1]),
Math.min(acc[2], cur[2]),
Math.max(acc[3], cur[3]),
Math.max(acc[4], cur[4]),
Math.max(acc[5], cur[5])
]
})
const { position, width, height, depth } = getDataFromShape(breakShape)
this.blockBreakMesh.scale.set(width * 1.001, height * 1.001, depth * 1.001)
position.add(cursorBlock.position)
this.blockBreakMesh.position.set(position.x, position.y, position.z)
}
this.cursorMesh.visible = true
// change
}

// Show break animation
@@ -193,4 +212,15 @@ class Cursor {
}
}

export default Cursor
const getDataFromShape = (shape) => {
const width = shape[3] - shape[0]
const height = shape[4] - shape[1]
const depth = shape[5] - shape[2]
const centerX = (shape[3] + shape[0]) / 2
const centerY = (shape[4] + shape[1]) / 2
const centerZ = (shape[5] + shape[2]) / 2
const position = new Vec3(centerX, centerY, centerZ)
return { position, width, height, depth }
}

export default new BlockInteraction()
2 changes: 2 additions & 0 deletions src/builtinCommands.ts
Original file line number Diff line number Diff line change
@@ -96,6 +96,8 @@ const commands = [
}
]

export const getBuiltinCommandsList = () => commands.flatMap(command => command.command)

export const tryHandleBuiltinCommand = (message) => {
if (!localServer) return

14 changes: 8 additions & 6 deletions src/chat.js
Original file line number Diff line number Diff line change
@@ -5,7 +5,7 @@ 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 { getBuiltinCommandsList, tryHandleBuiltinCommand } from './builtinCommands'
import { notification } from './menus/notification'
import { options } from './optionsStorage'

@@ -466,13 +466,13 @@ class ChatBox extends LitElement {
chatInput.addEventListener('keydown', (e) => {
if (e.code === 'Tab') {
if (this.completionItems.length) {
this.tabComplete(this.completionItems[0])
this.acceptComplete(this.completionItems[0])
} else {
void this.fetchCompletion(chatInput.value)
}
e.preventDefault()
}
if (e.code === 'Space' && options.autoRequestCompletions) {
if (e.code === 'Space' && options.autoRequestCompletions && chatInput.value.startsWith('/')) {
// alternative we could just simply use keyup, but only with keydown we can display suggestions popup as soon as possible
void this.fetchCompletion(this.getCompleteValue(chatInput.value + ' '))
}
@@ -488,6 +488,7 @@ class ChatBox extends LitElement {
}

async fetchCompletion (value = this.getCompleteValue()) {
this.completionItemsSource = []
this.completionItems = []
this.completeRequestValue = value
let items = await bot.tabComplete(value, true, true)
@@ -496,6 +497,7 @@ class ChatBox extends LitElement {
if (items[0].match) items = items.map(i => i.match)
}
if (value !== this.completeRequestValue) return
if (this.completeRequestValue === '/') items = [...items, ...getBuiltinCommandsList()]
this.completionItems = items
this.completionItemsSource = items
}
@@ -551,8 +553,8 @@ class ChatBox extends LitElement {
this.chatInput.focus()
}

tabComplete (item) {
const base = this.completeRequestValue === '/' ? '' : this.completeRequestValue
acceptComplete (item) {
const base = this.completeRequestValue === '/' ? '' : this.getCompleteValue()
this.updateInputValue(base + item)
// would be cool but disabled because some comands don't need args (like ping)
// // trigger next tab complete
@@ -574,7 +576,7 @@ class ChatBox extends LitElement {
${this.completionItems.length ? html`<div class="chat-completions">
<div class="chat-completions-pad-text">${this.completePadText}</div>
<div class="chat-completions-items">
${repeat(this.completionItems, (i) => i, (i) => html`<div @click=${() => this.tabComplete(i)}>${i}</div>`)}
${repeat(this.completionItems, (i) => i, (i) => html`<div @click=${() => this.acceptComplete(i)}>${i}</div>`)}
</div>
</div>` : ''}
<input type="text" class="chat-mobile-hidden" id="chatinput-next-command" spellcheck="false" autocomplete="off" @focus=${() => {
71 changes: 71 additions & 0 deletions src/customClient.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
//@ts-check
const EventEmitter = require('events').EventEmitter
const debug = require('debug')('minecraft-protocol')
const states = require('minecraft-protocol/src/states')

window.serverDataChannel ??= {}
export const customCommunication = {
sendData (data) {
//@ts-ignore
setTimeout(() => {
window.serverDataChannel[this.isServer ? 'emitClient' : 'emitServer'](data)
})
},
receiverSetup (processData) {
//@ts-ignore
window.serverDataChannel[this.isServer ? 'emitServer' : 'emitClient'] = (data) => {
processData(data)
}
}
}

class CustomChannelClient extends EventEmitter {
constructor (isServer, version) {
super()
this.version = version
this.isServer = !!isServer
this.state = states.HANDSHAKING
}

get state () {
return this.protocolState
}

setSerializer (state) {
customCommunication.receiverSetup.call(this, (/** @type {{name, params, state?}} */parsed) => {
debug(`receive in ${this.isServer ? 'server' : 'client'}: ${parsed.name}`)
this.emit(parsed.name, parsed.params, parsed)
this.emit('packet_name', parsed.name, parsed.params, parsed)
})
}

set state (newProperty) {
const oldProperty = this.protocolState
this.protocolState = newProperty

this.setSerializer(this.protocolState)

this.emit('state', newProperty, oldProperty)
}

end (reason) {
this._endReason = reason
}

write (name, params) {
debug(`[${this.state}] from ${this.isServer ? 'server' : 'client'}: ` + name)
debug(params)

customCommunication.sendData.call(this, { name, params, state: this.state })
}

writeBundle (packets) {
// no-op
}

writeRaw (buffer) {
// no-op
}
}

export default CustomChannelClient
20 changes: 2 additions & 18 deletions src/customServer.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,6 @@
import EventEmitter from 'events'

import CustomChannelClient from 'minecraft-protocol/src/customChannelClient'

window.serverDataChannel ??= {}
export const customCommunication = {
sendData(data) {
//@ts-ignore
setTimeout(() => {
window.serverDataChannel[this.isServer ? 'emitClient' : 'emitServer'](data)
})
},
receiverSetup(processData) {
//@ts-ignore
window.serverDataChannel[this.isServer ? 'emitServer' : 'emitClient'] = (data) => {
processData(data)
}
}
}
import CustomChannelClient from './customClient'

export class LocalServer extends EventEmitter.EventEmitter {
socketServer = null
@@ -29,7 +13,7 @@ export class LocalServer extends EventEmitter.EventEmitter {
}

listen() {
this.emit('connection', new CustomChannelClient(true, this.version, customCommunication))
this.emit('connection', new CustomChannelClient(true, this.version))
}

close() { }
85 changes: 58 additions & 27 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -41,7 +41,7 @@ import { WorldView, Viewer } from 'prismarine-viewer/viewer'
import pathfinder from 'mineflayer-pathfinder'
import { Vec3 } from 'vec3'

import Cursor from './cursor'
import blockInteraction from './blockInteraction'

import * as THREE from 'three'

@@ -59,9 +59,7 @@ import {

import {
pointerLock,
goFullscreen,
toNumber,
isCypress,
goFullscreen, isCypress,
loadScript,
toMajorVersion,
setLoadingScreenStatus,
@@ -76,19 +74,19 @@ import {

import { startLocalServer, unsupportedLocalServerFeatures } from './createLocalServer'
import serverOptions from './defaultLocalServerOptions'
import { customCommunication } from './customServer'
import updateTime from './updateTime'
import { options, watchValue } from './optionsStorage'
import { subscribeKey } from 'valtio/utils'
import _ from 'lodash'
import { contro } from './controls'
import { genTexturePackTextures, watchTexturepackInViewer } from './texturePack'
import { connectToPeer } from './localServerMultiplayer'
import CustomChannelClient from './customClient'
import debug from 'debug'

window.debug = debug
//@ts-ignore
window.THREE = THREE
// workaround to be used in prismarine-block
globalThis.emptyShapeReplacer = [[0.0, 0.0, 0.0, 1.0, 1.0, 1.0]]

if ('serviceWorker' in navigator && !isCypress() && process.env.NODE_ENV !== 'development') {
window.addEventListener('load', () => {
@@ -102,6 +100,7 @@ if ('serviceWorker' in navigator && !isCypress() && process.env.NODE_ENV !== 'de

// ACTUAL CODE

// todo stats-gl
let stats
let stats2
stats = new Stats()
@@ -200,12 +199,11 @@ const pauseMenu = document.getElementById('pause-screen')

let mouseMovePostHandle = (e) => { }
let lastMouseMove: number
let cursor: Cursor
let debugMenu
const updateCursor = () => {
cursor.update(bot)
blockInteraction.update()
debugMenu ??= hud.shadowRoot.querySelector('#debug-overlay')
debugMenu.cursorBlock = cursor.cursorBlock
debugMenu.cursorBlock = blockInteraction.cursorBlock
}
function onCameraMove(e) {
if (e.type !== 'touchmove' && !pointerLock.hasPointerLock) return
@@ -338,7 +336,9 @@ async function connect(connectOptions: {
bot.removeAllListeners()
bot._client.removeAllListeners()
bot._client = undefined
bot = undefined
// for debugging
window._botDisconnected = undefined
window.bot = bot = undefined
}
removeAllListeners()
for (const timeout of timeouts) {
@@ -365,6 +365,7 @@ async function connect(connectOptions: {

setLoadingScreenStatus(`Error encountered. Error message: ${err}`, true)
destroyAll()
if (isCypress()) throw err
}

const errorAbortController = new AbortController()
@@ -446,21 +447,21 @@ async function connect(connectOptions: {
}
}

const botDuplex = !p2pMultiplayer ? undefined/* clientDuplex */ : await connectToPeer(connectOptions.peerId)

setLoadingScreenStatus('Creating mineflayer bot')
bot = mineflayer.createBot({
host,
port,
version: !connectOptions.botVersion ? false : connectOptions.botVersion,
...p2pMultiplayer ? {
stream: await connectToPeer(connectOptions.peerId),
} : {},
...singeplayer || p2pMultiplayer ? {
keepAlive: false,
stream: botDuplex,
} : {},
...singeplayer ? {
version: serverOptions.version,
connect() { },
customCommunication,
Client: CustomChannelClient as any,
} : {},
username,
password,
@@ -470,8 +471,10 @@ async function connect(connectOptions: {
closeTimeout: 240 * 1000,
async versionSelectedHook(client) {
await downloadMcData(client.version)
setLoadingScreenStatus('Connecting to server')
}
})
window.bot = bot
if (singeplayer || p2pMultiplayer) {
// p2pMultiplayer still uses the same flying-squid server
const _supportFeature = bot.supportFeature
@@ -484,12 +487,24 @@ async function connect(connectOptions: {

bot.emit('inject_allowed')
bot._client.emit('connect')
} else {
bot._client.socket.on('connect', () => {
console.log('TCP connection established')
//@ts-ignore
bot._client.socket._ws.addEventListener('close', () => {
console.log('TCP connection closed')
setTimeout(() => {
if (bot) {
bot.emit('end', 'TCP connection closed with unknown reason')
}
})
})
})
}
} catch (err) {
handleError(err)
}
if (!bot) return
cursor = new Cursor(viewer, renderer, bot)
// bot.on('move', () => updateCursor())

let p2pConnectTimeout = p2pMultiplayer ? setTimeout(() => { throw new Error('Spawn timeout. There might be error on other side, check console.') }, 20_000) : undefined
@@ -511,6 +526,7 @@ async function connect(connectOptions: {
console.log('disconnected for', endReason)
destroyAll()
setLoadingScreenStatus(`You have been disconnected from the server. End reason: ${endReason}`, true)
if (isCypress()) throw new Error(`disconnected: ${endReason}`)
})

bot.once('login', () => {
@@ -566,7 +582,6 @@ async function connect(connectOptions: {
const debugMenu = hud.shadowRoot.querySelector('#debug-overlay')

window.loadedData = mcData
window.bot = bot
window.Vec3 = Vec3
window.pathfinder = pathfinder
window.debugMenu = debugMenu
@@ -619,19 +634,30 @@ async function connect(connectOptions: {

registerListener(document, 'pointerlockchange', changeCallback, false)

let holdingTouch: { touch: Touch, elem: HTMLElement } | undefined
document.body.addEventListener('touchend', (e) => {
if (!isGameActive(true)) return
if (holdingTouch?.touch.identifier !== e.changedTouches[0].identifier) return
holdingTouch.elem.click()
holdingTouch = undefined
})
document.body.addEventListener('touchstart', (e) => {
if (!isGameActive(true)) return
e.preventDefault()
holdingTouch = {
touch: e.touches[0],
elem: e.composedPath()[0] as HTMLElement
}
}, { passive: false })

const cameraControlEl = hud

// after what time of holding the finger start breaking the block
/** after what time of holding the finger start breaking the block */
const touchStartBreakingBlockMs = 500
let virtualClickActive = false
let virtualClickTimeout
let screenTouches = 0
let capturedPointer: { id; x; y; sourceX; sourceY; activateCameraMove; time } | null
document.body.addEventListener('touchstart', (e) => {
if (isGameActive(true)) {
e.preventDefault()
}
}, { passive: false })
registerListener(document, 'pointerdown', (e) => {
const clickedEl = e.composedPath()[0]
if (!isGameActive(true) || !miscUiState.currentTouch || clickedEl !== cameraControlEl || e.pointerId === undefined) {
@@ -652,8 +678,9 @@ async function connect(connectOptions: {
sourceX: e.clientX,
sourceY: e.clientY,
activateCameraMove: false,
time: new Date()
time: Date.now()
}
console.log('capture!')
virtualClickTimeout ??= setTimeout(() => {
virtualClickActive = true
document.dispatchEvent(new MouseEvent('mousedown', { button: 0 }))
@@ -697,9 +724,8 @@ async function connect(connectOptions: {
virtualClickActive = false
} 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 }))
})
blockInteraction.update()
document.dispatchEvent(new MouseEvent('mouseup', { button: 2 }))
}
capturedPointer = undefined
}, { passive: false })
@@ -724,13 +750,18 @@ async function connect(connectOptions: {

hud.init(renderer, bot, host)
hud.style.display = 'block'
blockInteraction.init()

setTimeout(function () {
errorAbortController.abort()
if (loadingScreen.hasError) return
// remove loading screen, wait a second to make sure a frame has properly rendered
setLoadingScreenStatus(undefined)
hideCurrentScreens()
viewer.waitForChunksToRender().then(() => {
console.log('All done and ready!')
document.dispatchEvent(new Event('cypress-world-ready'))
})
}, singeplayer ? 0 : 2500)
})
}
7 changes: 6 additions & 1 deletion src/menus/advanced_options_screen.js
Original file line number Diff line number Diff line change
@@ -2,7 +2,7 @@
const { html, css, LitElement } = require('lit')
const { commonCss, openURL } = require('./components/common')
const { hideCurrentModal } = require('../globalState')
const { toNumber, getScreenRefreshRate } = require('../utils')
const { getScreenRefreshRate } = require('../utils')
const { subscribe } = require('valtio')
const { options } = require('../optionsStorage')

@@ -74,6 +74,11 @@ class AdvancedOptionsScreen extends LitElement {
this.requestUpdate()
}}></pmui-button>
</div>
<div class="wrapper">
<pmui-slider pmui-width="150px" pmui-label="Touch Buttons Size" pmui-value="${options.touchButtonsSize}" pmui-type="%" pmui-min="20" pmui-max="100" @input=${(e) => {
options.touchButtonsSize = +e.target.value
}}></pmui-slider>
</div>
<pmui-button pmui-width="200px" pmui-label="Done" @pmui-click=${() => hideCurrentModal()}></pmui-button>
</main>
48 changes: 40 additions & 8 deletions src/menus/components/button.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,46 @@
//@ts-check
const { LitElement, html, css, unsafeCSS } = require('lit')
const widgetsGui = require('minecraft-assets/minecraft-assets/data/1.17.1/gui/widgets.png')
const { options, watchValue } = require('../../optionsStorage')
const { options } = require('../../optionsStorage')

let audioContext
/** @type {Record<string, any>} */
const sounds = {}

const buttonClickAudio = new Audio()
buttonClickAudio.src = 'button_click.mp3'
// load as many resources on page load as possible instead on demand as user can disable internet connection after he thinks the page is loaded
buttonClickAudio.load()
watchValue(options, o => {
buttonClickAudio.volume = o.volume / 100
})
let loadingSounds = []
async function loadSound (path) {
loadingSounds.push(path)
const res = await window.fetch(path)
const data = await res.arrayBuffer()

// sounds[path] = await audioContext.decodeAudioData(data)
sounds[path] = data
loadingSounds.splice(loadingSounds.indexOf(path), 1)
}

export async function playSound (path) {
if (!audioContext) {
audioContext = new window.AudioContext()
for (const [soundName, sound] of Object.entries(sounds)) {
sounds[soundName] = await audioContext.decodeAudioData(sound)
}
}

const volume = options.volume / 100

if (loadingSounds.includes(path)) return
const soundBuffer = sounds[path]
if (!soundBuffer) throw new Error(`Sound ${path} not loaded`)

const gainNode = audioContext.createGain()
const source = audioContext.createBufferSource()
source.buffer = soundBuffer
source.connect(gainNode)
gainNode.connect(audioContext.destination)
gainNode.gain.value = volume
source.start(0)
}

class Button extends LitElement {
static get styles () {
@@ -134,9 +165,10 @@ class Button extends LitElement {
}

onBtnClick (e) {
buttonClickAudio.play()
playSound('button_click.mp3')
this.dispatchEvent(new window.CustomEvent('pmui-click', { detail: e, }))
}
}

loadSound('button_click.mp3')
window.customElements.define('pmui-button', Button)
48 changes: 10 additions & 38 deletions src/menus/components/hotbar.js
Original file line number Diff line number Diff line change
@@ -187,48 +187,20 @@ class Hotbar extends LitElement {
<div class="hotbar">
<p id="hotbar-item-name">${this.activeItemName}</p>
<div id="hotbar-selected"></div>
<div id="hotbar-items-wrapper" @touchstart=${(e) => {
<div id="hotbar-items-wrapper" @pointerdown=${(e) => {
if (!e.target.id.startsWith('hotbar')) return
const slot = +e.target.id.split('-')[1]
this.reloadHotbarSelected(slot)
}}>
<div class="hotbar-item" id="hotbar-0">
<div class="item-icon"></div>
<span class="item-stack"></span>
</div>
<div class="hotbar-item" id="hotbar-1">
<div class="item-icon"></div>
<span class="item-stack"></span>
</div>
<div class="hotbar-item" id="hotbar-2">
<div class="item-icon"></div>
<span class="item-stack"></span>
</div>
<div class="hotbar-item" id="hotbar-3">
<div class="item-icon"></div>
<span class="item-stack"></span>
</div>
<div class="hotbar-item" id="hotbar-4">
<div class="item-icon"></div>
<span class="item-stack"></span>
</div>
<div class="hotbar-item" id="hotbar-5">
<div class="item-icon"></div>
<span class="item-stack"></span>
</div>
<div class="hotbar-item" id="hotbar-6">
<div class="item-icon"></div>
<span class="item-stack"></span>
</div>
<div class="hotbar-item" id="hotbar-7">
<div class="item-icon"></div>
<span class="item-stack"></span>
</div>
<div class="hotbar-item" id="hotbar-8">
<div class="item-icon"></div>
<span class="item-stack"></span>
</div>
${miscUiState.currentTouch ? html`<div class="hotbar-item hotbar-more" @click=${() => {
${Array.from({ length: 9 }).map((_, i) => html`
<div class="hotbar-item" id="${`hotbar-${i}`}" @pointerdown=${(e) => {
this.reloadHotbarSelected(i)
}}>
<div class="item-icon"></div>
<span class="item-stack"></span>
</div>
`)}
${miscUiState.currentTouch ? html`<div class="hotbar-item hotbar-more" @pointerdown=${() => {
showModal({ reactType: 'inventory', })
}}>` : undefined}
</div>
52 changes: 0 additions & 52 deletions src/menus/hud.js
Original file line number Diff line number Diff line change
@@ -303,17 +303,6 @@ class Hud extends LitElement {
/** @param {boolean} bl */
showMobileControls (bl) {
this.shadowRoot.querySelector('#mobile-top').style.display = bl ? 'flex' : 'none'
// this.shadowRoot.querySelector('#mobile-left').style.display = bl ? 'block' : 'none'
// this.shadowRoot.querySelector('#mobile-right').style.display = bl ? 'flex' : 'none'
}

/**
* @param {any} id
* @param {boolean} action
*/
mobileControl (e, id, action) {
e.stopPropagation()
this.bot.setControlState(id, action)
}

render () {
@@ -334,47 +323,6 @@ class Hud extends LitElement {
showModal(document.getElementById('pause-screen'))
}}></button>
</div>
<!-- <div class="mobile-controls-left" id="mobile-left">
<button
class="mobile-control-forward"
@pointerenter=${(e) => this.mobileControl(e, 'forward', true)}
@pointerleave=${(e) => this.mobileControl(e, 'forward', false)}
@mousedown=${(e) => this.mobileControl(e, 'forward', true)}
@mouseup=${(e) => this.mobileControl(e, 'forward', false)}
></button>
<button
class="mobile-control-back"
@pointerenter=${(e) => this.mobileControl(e, 'back', true)}
@pointerleave=${(e) => this.mobileControl(e, 'back', false)}
@mousedown=${(e) => this.mobileControl(e, 'back', true)}
@mouseup=${(e) => this.mobileControl(e, 'back', false)}
></button>
<button class="mobile-control-left"
@pointerenter=${(e) => this.mobileControl(e, 'left', true)}
@pointerleave=${(e) => this.mobileControl(e, 'left', false)}
@mousedown=${(e) => this.mobileControl(e, 'left', true)}
@mouseup=${(e) => this.mobileControl(e, 'left', false)}
></button>
<button class="mobile-control-right"
@pointerenter=${(e) => this.mobileControl(e, 'right', true)}
@pointerleave=${(e) => this.mobileControl(e, 'right', false)}
@mousedown=${(e) => this.mobileControl(e, 'right', true)}
@mouseup=${(e) => this.mobileControl(e, 'right', false)}
></button>
<button class="mobile-control-sneak" @dblclick=${(e) => {
e.stopPropagation()
const b = e.target.classList.toggle('is-down')
this.bot.setControlState('sneak', b)
}}></button>
</div>
<div class="mobile-controls-right" id="mobile-right">
<button class="mobile-control-jump"
@touchstart=${(e) => this.mobileControl(e, 'jump', true)}
@touchend=${(e) => this.mobileControl(e, 'jump', false)}
@mousedown=${(e) => this.mobileControl(e, 'jump', true)}
@mouseup=${(e) => this.mobileControl(e, 'jump', false)}
></button>
</div> -->
<pmui-debug-overlay id="debug-overlay"></pmui-debug-overlay>
<pmui-playerlist-overlay id="playerlist-overlay"></pmui-playerlist-overlay>
3 changes: 3 additions & 0 deletions src/optionsStorage.ts
Original file line number Diff line number Diff line change
@@ -24,6 +24,7 @@ const defaultOptions = {
fov: 75,
guiScale: 3,
autoRequestCompletions: true,
touchButtonsSize: 40,

frameLimit: false as number | false,
alwaysBackupWorldBeforeLoading: undefined as boolean | undefined | null,
@@ -39,6 +40,8 @@ const defaultOptions = {
askGuestName: true
}

export type AppOptions = typeof defaultOptions

export const options = proxy(
mergeAny(defaultOptions, JSON.parse(localStorage.options || '{}'))
)
10 changes: 9 additions & 1 deletion src/reactUi.jsx
Original file line number Diff line number Diff line change
@@ -4,12 +4,12 @@ import { renderToDom } from '@zardoy/react-util'
import { LeftTouchArea, RightTouchArea, useUsingTouch, useInterfaceState } from '@dimaka/interface'
import { css } from '@emotion/css'
import { activeModalStack, isGameActive, miscUiState } from './globalState'
import { isProbablyIphone } from './menus/components/common'
// import DeathScreen from './react/DeathScreen'
import { useSnapshot } from 'valtio'
import { contro } from './controls'
import { QRCodeSVG } from 'qrcode.react'
import { createPortal } from 'react-dom'
import { options, watchValue } from './optionsStorage'

// todo
useInterfaceState.setState({
@@ -40,6 +40,14 @@ useInterfaceState.setState({
}
})

watchValue(options, (o) => {
useInterfaceState.setState({
uiCustomization: {
touchButtonSize: o.touchButtonsSize,
},
})
})

const TouchControls = () => {
// todo setting
const usingTouch = useUsingTouch()
2 changes: 1 addition & 1 deletion src/texturePack.ts
Original file line number Diff line number Diff line change
@@ -123,7 +123,7 @@ const applyTexturePackData = async (version: string, { blockSize }: TextureResol
const blockStates: BlockStates = await result.json()
const factor = blockSize / 16

// this will be refactored with prerender refactor
// this will be refactored with generateTextures refactor
const processObj = (x) => {
if (typeof x !== 'object' || !x) return
if (Array.isArray(x)) {
16 changes: 11 additions & 5 deletions tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,18 +1,24 @@
{
"compilerOptions": {
"target": "ES2022",
"target": "ESNext",
"moduleResolution": "Node",
"module": "CommonJS",
"module": "ESNext",
"allowJs": true,
"jsx": "react-jsx",
"allowSyntheticDefaultImports": true,
"noEmit": true,
"strictFunctionTypes": true,
"resolveJsonModule": true,
"noFallthroughCasesInSwitch": true
"strictFunctionTypes": true,
"noImplicitAny": false,
"noFallthroughCasesInSwitch": true,
"allowUnreachableCode": true,
"forceConsistentCasingInFileNames": true,
"useUnknownInCatchVariables": false,
"skipLibCheck": true
// "strictNullChecks": true
},
"include": [
"src"
"src",
"cypress"
]
}

0 comments on commit 358203f

Please sign in to comment.