New release with important fixes (#41)
This commit is contained in:
commit
bc2a994cb6
30 changed files with 581 additions and 113 deletions
|
|
@ -53,11 +53,14 @@ There is world renderer playground ([link](https://mcon.vercel.app/playground.ht
|
|||
|
||||
However, there are many things that can be done in online version. You can access some global variables in the console and useful examples:
|
||||
|
||||
- `localStorage.debug = '*'` - Enables all debug messages!
|
||||
- `localStorage.debug = '*'` - Enables all debug messages! Warning: this will start all packets spam.
|
||||
Instead I recommend setting `options.debugLogNotFrequentPackets`. Also you can use `debugTopPackets` (with JSON.stringify) to see what packets were received/sent by name
|
||||
|
||||
- `bot` - Mineflayer bot instance. See Mineflayer documentation for more.
|
||||
- `viewer` - Three.js viewer instance, basically does all the rendering.
|
||||
- `viewer.world.sectionObjects` - Object with all active chunk sections (geometries) in the world. Each chunk section is a Three.js mesh or group.
|
||||
- `debugSceneChunks` - The same as above, but relative to current bot position (e.g. 0,0 is the current chunk).
|
||||
- `debugChangedOptions` - See what options are changed. Don't change options here.
|
||||
- `localServer` - Only for singleplayer mode/host. Flying Squid server instance, see it's documentation for more.
|
||||
- `localServer.overworld.storageProvider.regions` - See ALL LOADED region files with all raw data.
|
||||
|
||||
|
|
@ -70,7 +73,7 @@ You can also drag and drop any .dat file into the browser window to see it's con
|
|||
- `F3` - Toggle debug overlay
|
||||
- `F3 + A` - Reload all chunks (these that are loaded from the server)
|
||||
<!-- <!-- - `F3 + N` - Restart local server (basically resets the world!) -->
|
||||
- `F3 + G` - Toggle chunk sections (geometries) border visibility (aka Three.js geometry helpers) - most probably need to reload chunks after toggling
|
||||
- `F3 + G` - Toggle chunk sections (geometries) border visibility (aka Three.js geometry helpers)
|
||||
|
||||
### Notable Things that Power this Project
|
||||
|
||||
|
|
|
|||
|
|
@ -6,8 +6,9 @@
|
|||
"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 -f && node esbuild.mjs --minify --prod",
|
||||
"check-build": "tsc && pnpm build",
|
||||
"check-build": "tsc && pnpm test-unit && pnpm build",
|
||||
"test:cypress": "cypress run",
|
||||
"test-unit": "vitest",
|
||||
"test:e2e": "start-test http-get://localhost:8080 test:cypress",
|
||||
"prod-start": "node server.js",
|
||||
"postinstall": "node scripts/gen-texturepack-files.mjs && tsx scripts/optimizeBlockCollisions.ts",
|
||||
|
|
|
|||
21
pnpm-lock.yaml
generated
21
pnpm-lock.yaml
generated
|
|
@ -73,7 +73,7 @@ importers:
|
|||
version: 4.18.2
|
||||
flying-squid:
|
||||
specifier: github:zardoy/space-squid#everything
|
||||
version: github.com/zardoy/space-squid/a639714c26e2252b34be833f64f23d9f45f136de
|
||||
version: github.com/zardoy/space-squid/9d72f865da99bcc55db2c5071754f61a5d935c73
|
||||
fs-extra:
|
||||
specifier: ^11.1.1
|
||||
version: 11.1.1
|
||||
|
|
@ -4604,7 +4604,7 @@ packages:
|
|||
/@types/connect@3.4.36:
|
||||
resolution: {integrity: sha512-P63Zd/JUGq+PdrM1lv0Wv5SBYeA2+CORvbrXbngriYY0jzLUWfQMQQxOhjONEz/wlHOAxOdY7CY65rgQdTjq2w==}
|
||||
dependencies:
|
||||
'@types/node': 20.8.0
|
||||
'@types/node': 20.8.10
|
||||
dev: true
|
||||
|
||||
/@types/cookie@0.4.1:
|
||||
|
|
@ -4620,7 +4620,7 @@ packages:
|
|||
/@types/cross-spawn@6.0.3:
|
||||
resolution: {integrity: sha512-BDAkU7WHHRHnvBf5z89lcvACsvkz/n7Tv+HyD/uW76O29HoH1Tk/W6iQrepaZVbisvlEek4ygwT8IW7ow9XLAA==}
|
||||
dependencies:
|
||||
'@types/node': 20.8.0
|
||||
'@types/node': 20.8.10
|
||||
dev: true
|
||||
|
||||
/@types/detect-port@1.3.3:
|
||||
|
|
@ -4691,7 +4691,7 @@ packages:
|
|||
/@types/graceful-fs@4.1.7:
|
||||
resolution: {integrity: sha512-MhzcwU8aUygZroVwL2jeYk6JisJrPl/oov/gsgGCue9mkgl9wjGbzReYQClxiUgFDnib9FuHqTndccKeZKxTRw==}
|
||||
dependencies:
|
||||
'@types/node': 20.8.0
|
||||
'@types/node': 20.8.10
|
||||
dev: true
|
||||
|
||||
/@types/http-cache-semantics@4.0.2:
|
||||
|
|
@ -4844,7 +4844,7 @@ packages:
|
|||
/@types/resolve@1.17.1:
|
||||
resolution: {integrity: sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw==}
|
||||
dependencies:
|
||||
'@types/node': 20.8.0
|
||||
'@types/node': 20.8.10
|
||||
dev: false
|
||||
|
||||
/@types/sat@0.0.31:
|
||||
|
|
@ -4862,7 +4862,7 @@ packages:
|
|||
resolution: {integrity: sha512-aAG6yRf6r0wQ29bkS+x97BIs64ZLxeE/ARwyS6wrldMm3C1MdKwCcnnEwMC1slI8wuxJOpiUH9MioC0A0i+GJw==}
|
||||
dependencies:
|
||||
'@types/mime': 1.3.3
|
||||
'@types/node': 20.8.0
|
||||
'@types/node': 20.8.10
|
||||
dev: true
|
||||
|
||||
/@types/serve-static@1.15.3:
|
||||
|
|
@ -9606,7 +9606,7 @@ packages:
|
|||
resolution: {integrity: sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ==}
|
||||
engines: {node: '>= 10.13.0'}
|
||||
dependencies:
|
||||
'@types/node': 20.8.0
|
||||
'@types/node': 20.8.10
|
||||
merge-stream: 2.0.0
|
||||
supports-color: 7.2.0
|
||||
dev: false
|
||||
|
|
@ -9615,7 +9615,7 @@ packages:
|
|||
resolution: {integrity: sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==}
|
||||
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
|
||||
dependencies:
|
||||
'@types/node': 20.8.0
|
||||
'@types/node': 20.8.10
|
||||
jest-util: 29.7.0
|
||||
merge-stream: 2.0.0
|
||||
supports-color: 8.1.1
|
||||
|
|
@ -14750,13 +14750,12 @@ packages:
|
|||
- utf-8-validate
|
||||
dev: false
|
||||
|
||||
github.com/zardoy/space-squid/a639714c26e2252b34be833f64f23d9f45f136de:
|
||||
resolution: {tarball: https://codeload.github.com/zardoy/space-squid/tar.gz/a639714c26e2252b34be833f64f23d9f45f136de}
|
||||
github.com/zardoy/space-squid/9d72f865da99bcc55db2c5071754f61a5d935c73:
|
||||
resolution: {tarball: https://codeload.github.com/zardoy/space-squid/tar.gz/9d72f865da99bcc55db2c5071754f61a5d935c73}
|
||||
name: flying-squid
|
||||
version: 0.0.0-dev
|
||||
engines: {node: '>=8'}
|
||||
hasBin: true
|
||||
prepare: true
|
||||
requiresBuild: true
|
||||
dependencies:
|
||||
change-case: 4.1.2
|
||||
|
|
|
|||
|
|
@ -90,19 +90,18 @@ async function main () {
|
|||
// const schem = await Schematic.read(Buffer.from(data), version)
|
||||
|
||||
const viewDistance = 0
|
||||
const center = new Vec3(0, 90, 0)
|
||||
const targetPos = new Vec3(2, 90, 2)
|
||||
|
||||
const World = WorldLoader(version)
|
||||
|
||||
// const diamondSquare = require('diamond-square')({ version, seed: Math.floor(Math.random() * Math.pow(2, 31)) })
|
||||
|
||||
const targetPos = center
|
||||
//@ts-ignore
|
||||
const chunk1 = new Chunk()
|
||||
//@ts-ignore
|
||||
const chunk2 = new Chunk()
|
||||
chunk1.setBlockStateId(center, 34)
|
||||
chunk2.setBlockStateId(center.offset(1, 0, 0), 34)
|
||||
chunk1.setBlockStateId(targetPos, 34)
|
||||
chunk2.setBlockStateId(targetPos.offset(1, 0, 0), 34)
|
||||
const world = new World((chunkX, chunkZ) => {
|
||||
// if (chunkX === 0 && chunkZ === 0) return chunk1
|
||||
// if (chunkX === 1 && chunkZ === 0) return chunk2
|
||||
|
|
@ -113,7 +112,7 @@ async function main () {
|
|||
|
||||
// await schem.paste(world, new Vec3(0, 60, 0))
|
||||
|
||||
const worldView = new WorldDataEmitter(world, viewDistance, center)
|
||||
const worldView = new WorldDataEmitter(world, viewDistance, targetPos)
|
||||
|
||||
// Create three.js context, add to page
|
||||
const renderer = new THREE.WebGLRenderer()
|
||||
|
|
@ -127,20 +126,20 @@ async function main () {
|
|||
|
||||
viewer.listen(worldView)
|
||||
// Load chunks
|
||||
await worldView.init(center)
|
||||
await worldView.init(targetPos)
|
||||
window['worldView'] = worldView
|
||||
window['viewer'] = viewer
|
||||
|
||||
|
||||
//@ts-ignore
|
||||
const controls = new globalThis.THREE.OrbitControls(viewer.camera, renderer.domElement)
|
||||
controls.target.set(center.x + 0.5, center.y + 0.5, center.z + 0.5)
|
||||
controls.target.set(targetPos.x + 0.5, targetPos.y + 0.5, targetPos.z + 0.5)
|
||||
|
||||
const cameraPos = center.offset(2, 2, 2)
|
||||
const cameraPos = targetPos.offset(2, 2, 2)
|
||||
const pitch = THREE.MathUtils.degToRad(-45)
|
||||
const yaw = THREE.MathUtils.degToRad(45)
|
||||
viewer.camera.rotation.set(pitch, yaw, 0, 'ZYX')
|
||||
viewer.camera.lookAt(center.x + 0.5, center.y + 0.5, center.z + 0.5)
|
||||
viewer.camera.lookAt(targetPos.x + 0.5, targetPos.y + 0.5, targetPos.z + 0.5)
|
||||
viewer.camera.position.set(cameraPos.x + 0.5, cameraPos.y + 0.5, cameraPos.z + 0.5)
|
||||
controls.update()
|
||||
|
||||
|
|
@ -158,30 +157,30 @@ async function main () {
|
|||
const { states } = mcData.blocksByStateId[getBlock()?.minStateId] ?? {}
|
||||
folder = gui.addFolder('metadata')
|
||||
if (states) {
|
||||
for (const state of states) {
|
||||
let defaultValue
|
||||
switch (state.type) {
|
||||
case 'enum':
|
||||
defaultValue = state.values[0]
|
||||
break
|
||||
case 'bool':
|
||||
defaultValue = false
|
||||
break
|
||||
case 'int':
|
||||
defaultValue = 0
|
||||
break
|
||||
case 'direction':
|
||||
defaultValue = 'north'
|
||||
break
|
||||
for (const state of states) {
|
||||
let defaultValue
|
||||
switch (state.type) {
|
||||
case 'enum':
|
||||
defaultValue = state.values[0]
|
||||
break
|
||||
case 'bool':
|
||||
defaultValue = false
|
||||
break
|
||||
case 'int':
|
||||
defaultValue = 0
|
||||
break
|
||||
case 'direction':
|
||||
defaultValue = 'north'
|
||||
break
|
||||
|
||||
default:
|
||||
continue
|
||||
}
|
||||
blockProps[state.name] = defaultValue
|
||||
if (state.type === 'enum') {
|
||||
folder.add(blockProps, state.name, state.values)
|
||||
} else {
|
||||
folder.add(blockProps, state.name)
|
||||
default:
|
||||
continue
|
||||
}
|
||||
blockProps[state.name] = defaultValue
|
||||
if (state.type === 'enum') {
|
||||
folder.add(blockProps, state.name, state.values)
|
||||
} else {
|
||||
folder.add(blockProps, state.name)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
|
@ -214,8 +213,8 @@ async function main () {
|
|||
}
|
||||
} else {
|
||||
try {
|
||||
//@ts-ignore
|
||||
block = Block.fromProperties(blockId ?? -1, blockProps, 0)
|
||||
//@ts-ignore
|
||||
block = Block.fromProperties(blockId ?? -1, blockProps, 0)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
block = Block.fromStateId(0, 0)
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ function getEntityMesh (entity, scene, options) {
|
|||
tex.needsUpdate = true
|
||||
const spriteMat = new THREE.SpriteMaterial({ map: tex })
|
||||
const sprite = new THREE.Sprite(spriteMat)
|
||||
sprite.renderOrder = 1000
|
||||
sprite.scale.set(canvas.width * 0.005, canvas.height * 0.005, 1)
|
||||
sprite.position.y += entity.height + 0.6
|
||||
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ export class Viewer {
|
|||
playerHeight: number
|
||||
isSneaking: boolean
|
||||
version: string
|
||||
cameraObjectOverride?: THREE.Object3D // for xr
|
||||
|
||||
constructor (public renderer: THREE.WebGLRenderer, numWorkers?: number) {
|
||||
this.scene = new THREE.Scene()
|
||||
|
|
@ -81,12 +82,13 @@ export class Viewer {
|
|||
}
|
||||
|
||||
setFirstPersonCamera (pos: Vec3 | null, yaw: number, pitch: number, roll = 0) {
|
||||
const cam = this.cameraObjectOverride || this.camera
|
||||
if (pos) {
|
||||
let y = pos.y + this.playerHeight
|
||||
if (this.isSneaking) y -= 0.3
|
||||
new tweenJs.Tween(this.camera.position).to({ x: pos.x, y, z: pos.z }, 50).start()
|
||||
new tweenJs.Tween(cam.position).to({ x: pos.x, y, z: pos.z }, 50).start()
|
||||
}
|
||||
this.camera.rotation.set(pitch, yaw, roll, 'ZYX')
|
||||
cam.rotation.set(pitch, yaw, roll, 'ZYX')
|
||||
}
|
||||
|
||||
// todo type
|
||||
|
|
@ -116,6 +118,10 @@ export class Viewer {
|
|||
this.setBlockStateId(new Vec3(pos.x, pos.y, pos.z), stateId)
|
||||
})
|
||||
|
||||
emitter.on('chunkPosUpdate', ({ pos }) => {
|
||||
this.world.updateViewerPosition(pos)
|
||||
})
|
||||
|
||||
emitter.emit('listening')
|
||||
|
||||
this.domElement.addEventListener('pointerdown', (evt) => {
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ export class WorldDataEmitter extends EventEmitter {
|
|||
private eventListeners: Record<string, any> = {};
|
||||
private emitter: WorldDataEmitter
|
||||
|
||||
constructor (public world: import('prismarine-world').world.World | typeof __type_bot['world'], public viewDistance: number, position: Vec3 = new Vec3(0, 0, 0)) {
|
||||
constructor(public world: import('prismarine-world').world.World | typeof __type_bot['world'], public viewDistance: number, position: Vec3 = new Vec3(0, 0, 0)) {
|
||||
super()
|
||||
this.loadedChunks = {}
|
||||
this.lastPos = new Vec3(0, 0, 0).update(position)
|
||||
|
|
@ -91,6 +91,7 @@ export class WorldDataEmitter extends EventEmitter {
|
|||
}
|
||||
|
||||
async init (pos: Vec3) {
|
||||
this.emitter.emit('chunkPosUpdate', { pos })
|
||||
const [botX, botZ] = chunkPos(pos)
|
||||
|
||||
const positions = generateSpiralMatrix(this.viewDistance).map(([x, z]) => new Vec3((botX + x) * 16, 0, (botZ + z) * 16))
|
||||
|
|
@ -138,6 +139,7 @@ export class WorldDataEmitter extends EventEmitter {
|
|||
const [lastX, lastZ] = chunkPos(this.lastPos)
|
||||
const [botX, botZ] = chunkPos(pos)
|
||||
if (lastX !== botX || lastZ !== botZ || force) {
|
||||
this.emitter.emit('chunkPosUpdate', { pos })
|
||||
const newView = new ViewRect(botX, botZ, this.viewDistance)
|
||||
const chunksToUnload: Vec3[] = []
|
||||
for (const coords of Object.keys(this.loadedChunks)) {
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import { dispose3 } from './dispose'
|
|||
import { toMajor } from './version.js'
|
||||
import PrismarineChatLoader from 'prismarine-chat'
|
||||
import { renderSign } from '../sign-renderer/'
|
||||
import { chunkPos } from './simpleUtils'
|
||||
|
||||
function mod (x, n) {
|
||||
return ((x % n) + n) % n
|
||||
|
|
@ -31,8 +32,10 @@ export class WorldRenderer {
|
|||
downloadedBlockStatesData = undefined as any
|
||||
downloadedTextureImage = undefined as any
|
||||
workers: any[] = []
|
||||
viewerPosition?: Vec3
|
||||
|
||||
texturesVersion?: string
|
||||
|
||||
constructor (public scene: THREE.Scene, numWorkers = 4) {
|
||||
// init workers
|
||||
for (let i = 0; i < numWorkers; i++) {
|
||||
|
|
@ -67,13 +70,16 @@ export class WorldRenderer {
|
|||
|
||||
const mesh = new THREE.Mesh(geometry, this.material)
|
||||
mesh.position.set(data.geometry.sx, data.geometry.sy, data.geometry.sz)
|
||||
mesh.name = 'mesh'
|
||||
object = new THREE.Group()
|
||||
object.add(mesh)
|
||||
if (this.showChunkBorders) {
|
||||
const boxHelper = new THREE.BoxHelper(mesh, 0xffff00)
|
||||
object.add(boxHelper)
|
||||
const boxHelper = new THREE.BoxHelper(mesh, 0xffff00)
|
||||
boxHelper.name = 'helper'
|
||||
object.add(boxHelper)
|
||||
if (!this.showChunkBorders) {
|
||||
boxHelper.visible = false
|
||||
}
|
||||
// should not it compute once
|
||||
// should not compute it once
|
||||
if (Object.keys(data.geometry.signs).length) {
|
||||
for (const [posKey, { isWall, rotation }] of Object.entries(data.geometry.signs)) {
|
||||
const [x, y, z] = posKey.split(',')
|
||||
|
|
@ -83,6 +89,7 @@ export class WorldRenderer {
|
|||
}
|
||||
}
|
||||
this.sectionObjects[data.key] = object
|
||||
this.updatePosDataChunk(data.key)
|
||||
this.scene.add(object)
|
||||
} else if (data.type === 'sectionFinished') {
|
||||
this.sectionsOutstanding.delete(data.key)
|
||||
|
|
@ -94,6 +101,26 @@ export class WorldRenderer {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Optionally update data that are depedendent on the viewer position
|
||||
*/
|
||||
updatePosDataChunk (key: string) {
|
||||
if (!this.viewerPosition) return
|
||||
const [x, y, z] = key.split(',').map(x => Math.floor(+x / 16))
|
||||
const [xPlayer, yPlayer, zPlayer] = this.viewerPosition.toArray().map(x => Math.floor(x / 16))
|
||||
// sum of distances: x + y + z
|
||||
const chunkDistance = Math.abs(x - xPlayer) + Math.abs(y - yPlayer) + Math.abs(z - zPlayer)
|
||||
const section = this.sectionObjects[key].children.find(child => child.name === 'mesh')!
|
||||
section.renderOrder = 500 - chunkDistance
|
||||
}
|
||||
|
||||
updateViewerPosition (pos: Vec3) {
|
||||
this.viewerPosition = pos
|
||||
for (const key of Object.keys(this.sectionObjects)) {
|
||||
this.updatePosDataChunk(key)
|
||||
}
|
||||
}
|
||||
|
||||
renderSign (position: Vec3, rotation: number, isWall: boolean, blockEntity) {
|
||||
const PrismarineChat = PrismarineChatLoader(this.version!)
|
||||
const canvas = renderSign(blockEntity, PrismarineChat)
|
||||
|
|
@ -124,6 +151,17 @@ export class WorldRenderer {
|
|||
return group
|
||||
}
|
||||
|
||||
updateShowChunksBorder (value: boolean) {
|
||||
this.showChunkBorders = value
|
||||
for (const object of Object.values(this.sectionObjects)) {
|
||||
for (const child of object.children) {
|
||||
if (child.name === 'helper') {
|
||||
child.visible = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resetWorld () {
|
||||
this.active = false
|
||||
for (const mesh of Object.values(this.sectionObjects)) {
|
||||
|
|
@ -182,6 +220,15 @@ export class WorldRenderer {
|
|||
})
|
||||
}
|
||||
|
||||
getLoadedChunksRelative (pos: Vec3) {
|
||||
const [currentX, currentZ] = chunkPos(pos)
|
||||
return Object.fromEntries(Object.entries(this.sectionObjects).map(([key, o]) => {
|
||||
const [xRaw, yRaw, zRaw] = key.split(',').map(Number)
|
||||
const [x, z] = chunkPos({x: xRaw, z: zRaw})
|
||||
return [`${x - currentX},${z - currentZ}`, o]
|
||||
}))
|
||||
}
|
||||
|
||||
addColumn (x, z, chunk) {
|
||||
this.loadedChunks[`${x},${z}`] = true
|
||||
for (const worker of this.workers) {
|
||||
|
|
|
|||
|
|
@ -57,7 +57,8 @@ export const renderSign = (blockEntity: SignBlockEntity, PrismarineChat: typeof
|
|||
const defaultColor = ('front_text' in blockEntity ? blockEntity.front_text.color : blockEntity.Color) || 'black'
|
||||
for (let [lineNum, text] of texts.slice(0, 4).entries()) {
|
||||
// todo: in pre flatenning it seems the format was not json
|
||||
const parsed = text?.startsWith('{') ? parseSafe(text ?? '""', 'sign text') : text
|
||||
if (text === 'null') continue
|
||||
const parsed = text?.startsWith('{') || text?.startsWith('"') ? parseSafe(text ?? '""', 'sign text') : text
|
||||
if (!parsed || (typeof parsed !== 'object' && typeof parsed !== 'string')) continue
|
||||
// todo fix type
|
||||
const message = typeof parsed === 'string' ? fromFormattedString(parsed) : new PrismarineChat(parsed) as never
|
||||
|
|
|
|||
85
prismarine-viewer/viewer/sign-renderer/tests.test.ts
Normal file
85
prismarine-viewer/viewer/sign-renderer/tests.test.ts
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
import { test, expect } from 'vitest'
|
||||
import { renderSign } from '.'
|
||||
import PrismarineChatLoader from 'prismarine-chat'
|
||||
|
||||
const PrismarineChat = PrismarineChatLoader({ language: {} } as any)
|
||||
let ctxTexts = [] as any[]
|
||||
|
||||
global.document = {
|
||||
createElement () {
|
||||
return {
|
||||
getContext () {
|
||||
return {
|
||||
fillText (text, x, y) {
|
||||
ctxTexts.push({ text, x, y })
|
||||
},
|
||||
measureText () { return 0 }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} as any
|
||||
|
||||
const render = (entity) => {
|
||||
ctxTexts = []
|
||||
renderSign(entity, PrismarineChat)
|
||||
return ctxTexts.map(({ text, y }) => [y / 80, text])
|
||||
}
|
||||
|
||||
test('sign renderer', () => {
|
||||
let blockEntity = {
|
||||
"GlowingText": 0,
|
||||
"Color": "black",
|
||||
"Text4": "{\"text\":\"\"}",
|
||||
"Text3": "{\"text\":\"\"}",
|
||||
"Text2": "{\"text\":\"\"}",
|
||||
"Text1": "{\"extra\":[{\"color\":\"dark_green\",\"text\":\"Minecraft \"},{\"text\":\"Tools\"}],\"text\":\"\"}"
|
||||
} as any
|
||||
expect(render(blockEntity)).toMatchInlineSnapshot(`
|
||||
[
|
||||
[
|
||||
1,
|
||||
"",
|
||||
],
|
||||
[
|
||||
1,
|
||||
"Minecraft ",
|
||||
],
|
||||
[
|
||||
1,
|
||||
"Tools",
|
||||
],
|
||||
[
|
||||
2,
|
||||
"",
|
||||
],
|
||||
[
|
||||
3,
|
||||
"",
|
||||
],
|
||||
[
|
||||
4,
|
||||
"",
|
||||
],
|
||||
]
|
||||
`)
|
||||
|
||||
blockEntity = { // pre flatenning
|
||||
"Text1": "Welcome to",
|
||||
"Text2": "",
|
||||
"Text3": "null",
|
||||
"Text4": "\"Version 2.1\"",
|
||||
} as const
|
||||
expect(render(blockEntity)).toMatchInlineSnapshot(`
|
||||
[
|
||||
[
|
||||
1,
|
||||
"Welcome to",
|
||||
],
|
||||
[
|
||||
4,
|
||||
"Version 2.1",
|
||||
],
|
||||
]
|
||||
`)
|
||||
})
|
||||
|
|
@ -17,6 +17,10 @@ export async function loadSound (path: string) {
|
|||
}
|
||||
|
||||
export async function playSound (path) {
|
||||
const volume = options.volume / 100
|
||||
|
||||
if (!volume) return
|
||||
|
||||
audioContext ??= new window.AudioContext()
|
||||
|
||||
for (const [soundName, sound] of Object.entries(sounds)) {
|
||||
|
|
@ -25,8 +29,6 @@ export async function playSound (path) {
|
|||
convertedSounds.push(soundName)
|
||||
}
|
||||
|
||||
const volume = options.volume / 100
|
||||
|
||||
const soundBuffer = sounds[path]
|
||||
if (!soundBuffer) {
|
||||
console.warn(`Sound ${path} not loaded`)
|
||||
|
|
|
|||
|
|
@ -226,12 +226,10 @@ contro.on('release', ({ command }) => {
|
|||
|
||||
// hard-coded keybindings
|
||||
|
||||
const hardcodedPressedKeys = new Set<string>()
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (!isGameActive(false)) return
|
||||
if (hardcodedPressedKeys.has('F3')) {
|
||||
// reload chunks
|
||||
if (e.code === 'KeyA') {
|
||||
export const f3Keybinds = [
|
||||
{
|
||||
key: 'KeyA',
|
||||
action () {
|
||||
//@ts-expect-error
|
||||
const loadedChunks = Object.entries(worldView.loadedChunks).filter(([, v]) => v).map(([key]) => key.split(',').map(Number))
|
||||
for (const [x, z] of loadedChunks) {
|
||||
|
|
@ -242,12 +240,25 @@ document.addEventListener('keydown', (e) => {
|
|||
localServer.players[0].world.columns = {}
|
||||
}
|
||||
void reloadChunks()
|
||||
}
|
||||
if (e.code === 'KeyG') {
|
||||
// todo make it work without reload
|
||||
},
|
||||
mobileTitle: 'Reload chunks',
|
||||
},
|
||||
{
|
||||
key: 'KeyG',
|
||||
action () {
|
||||
options.showChunkBorders = !options.showChunkBorders
|
||||
void reloadChunks()
|
||||
}
|
||||
viewer.world.updateShowChunksBorder(options.showChunkBorders)
|
||||
},
|
||||
mobileTitle: 'Toggle chunk borders',
|
||||
}
|
||||
]
|
||||
|
||||
const hardcodedPressedKeys = new Set<string>()
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (!isGameActive(false)) return
|
||||
if (hardcodedPressedKeys.has('F3')) {
|
||||
const keybind = f3Keybinds.find((v) => v.key === e.code)
|
||||
if (keybind) keybind.action()
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -388,6 +399,7 @@ const selectItem = async () => {
|
|||
}
|
||||
|
||||
addEventListener('mousedown', async (e) => {
|
||||
if ((e.target as HTMLElement).matches?.('#VRButton')) return
|
||||
void pointerLock.requestPointerLock()
|
||||
if (!bot) return
|
||||
// wheel click
|
||||
|
|
|
|||
|
|
@ -62,6 +62,7 @@ class CustomChannelClient extends EventEmitter {
|
|||
debug(params)
|
||||
}
|
||||
|
||||
this.emit('writePacket', name, params)
|
||||
customCommunication.sendData.call(this, { name, params, state: this.state })
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { openWorldZip } from './browserfs'
|
|||
import { getResourcePackName, installTexturePack, resourcePackState, updateTexturePackInstalledState } from './texturePack'
|
||||
import { setLoadingScreenStatus } from './utils'
|
||||
|
||||
const getFixedFilesize = (bytes: number) => {
|
||||
export const getFixedFilesize = (bytes: number) => {
|
||||
return prettyBytes(bytes, { minimumFractionDigits: 2, maximumFractionDigits: 2 })
|
||||
}
|
||||
|
||||
|
|
|
|||
39
src/index.ts
39
src/index.ts
|
|
@ -105,6 +105,11 @@ document.body.appendChild(renderer.domElement)
|
|||
// Create viewer
|
||||
const viewer: import('prismarine-viewer/viewer/lib/viewer').Viewer = new Viewer(renderer, options.numWorkers)
|
||||
window.viewer = viewer
|
||||
Object.defineProperty(window, 'debugSceneChunks', {
|
||||
get () {
|
||||
return viewer.world.getLoadedChunksRelative(bot.entity.position)
|
||||
},
|
||||
})
|
||||
viewer.entities.entitiesOptions = {
|
||||
fontFamily: 'mojangles'
|
||||
}
|
||||
|
|
@ -124,7 +129,7 @@ let previousWindowHeight = window.innerHeight
|
|||
const renderFrame = (time: DOMHighResTimeStamp) => {
|
||||
if (window.stopLoop) return
|
||||
window.requestAnimationFrame(renderFrame)
|
||||
if (window.stopRender) return
|
||||
if (window.stopRender || renderer.xr.isPresenting) return
|
||||
if (renderInterval) {
|
||||
delta += time - lastTime
|
||||
lastTime = time
|
||||
|
|
@ -200,8 +205,8 @@ function hideCurrentScreens () {
|
|||
insertActiveModalStack('', [])
|
||||
}
|
||||
|
||||
const loadSingleplayer = (serverOverrides = {}) => {
|
||||
void connect({ singleplayer: true, username: options.localUsername, password: '', serverOverrides })
|
||||
const loadSingleplayer = (serverOverrides = {}, flattenedServerOverrides = {}) => {
|
||||
void connect({ singleplayer: true, username: options.localUsername, password: '', serverOverrides, serverOverridesFlat: flattenedServerOverrides })
|
||||
}
|
||||
function listenGlobalEvents () {
|
||||
const menu = document.getElementById('play-screen')
|
||||
|
|
@ -242,7 +247,7 @@ const cleanConnectIp = (host: string | undefined, defaultPort: string | undefine
|
|||
}
|
||||
|
||||
async function connect (connectOptions: {
|
||||
server?: string; singleplayer?: any; username: string; password?: any; proxy?: any; botVersion?: any; serverOverrides?; peerId?: string
|
||||
server?: string; singleplayer?: any; username: string; password?: any; proxy?: any; botVersion?: any; serverOverrides?; serverOverridesFlat?; peerId?: string
|
||||
}) {
|
||||
document.getElementById('play-screen').style = 'display: none;'
|
||||
removePanorama()
|
||||
|
|
@ -335,6 +340,7 @@ async function connect (connectOptions: {
|
|||
let localServer
|
||||
try {
|
||||
const serverOptions = _.defaultsDeep({}, connectOptions.serverOverrides ?? {}, options.localServerOptions, defaultServerOptions)
|
||||
Object.assign(serverOptions, connectOptions.serverOverridesFlat ?? {})
|
||||
const downloadMcData = async (version: string) => {
|
||||
setLoadingScreenStatus(`Downloading data for ${version}`)
|
||||
await loadScript(`./mc-data/${toMajorVersion(version)}.js`)
|
||||
|
|
@ -535,7 +541,7 @@ async function connect (connectOptions: {
|
|||
|
||||
window.debugMenu = debugMenu
|
||||
|
||||
void initVR(bot, renderer, viewer)
|
||||
void initVR()
|
||||
|
||||
postRenderFrameFn = () => {
|
||||
viewer.setFirstPersonCamera(null, bot.entity.yaw, bot.entity.pitch)
|
||||
|
|
@ -577,6 +583,7 @@ async function connect (connectOptions: {
|
|||
|
||||
function changeCallback () {
|
||||
notification.show = false
|
||||
if (renderer.xr.isPresenting) return // todo
|
||||
if (!pointerLock.hasPointerLock && activeModalStack.length === 0) {
|
||||
showModal(pauseMenu)
|
||||
}
|
||||
|
|
@ -693,7 +700,7 @@ watchValue(miscUiState, async s => {
|
|||
if (s.appLoaded) { // fs ready
|
||||
const qs = new URLSearchParams(window.location.search)
|
||||
if (qs.get('singleplayer') === '1') {
|
||||
loadSingleplayer({
|
||||
loadSingleplayer({}, {
|
||||
worldFolder: undefined
|
||||
})
|
||||
}
|
||||
|
|
@ -711,19 +718,33 @@ watchValue(miscUiState, async s => {
|
|||
})
|
||||
|
||||
// #region fire click event on touch as we disable default behaviors
|
||||
let activeTouch: { touch: Touch, elem: HTMLElement } | undefined
|
||||
let activeTouch: { touch: Touch, elem: HTMLElement, start: number } | undefined
|
||||
document.body.addEventListener('touchend', (e) => {
|
||||
if (!isGameActive(true)) return
|
||||
if (activeTouch?.touch.identifier !== e.changedTouches[0].identifier) return
|
||||
activeTouch.elem.click()
|
||||
if (Date.now() - activeTouch.start > 500) {
|
||||
activeTouch.elem.dispatchEvent(new Event('longtouch', { bubbles: true }))
|
||||
} else {
|
||||
activeTouch.elem.click()
|
||||
}
|
||||
activeTouch = undefined
|
||||
})
|
||||
document.body.addEventListener('touchstart', (e) => {
|
||||
if (!isGameActive(true)) return
|
||||
e.preventDefault()
|
||||
let firstClickable // todo remove composedPath and this workaround when lit-element is fully dropped
|
||||
const path = e.composedPath() as Array<{ click?: () => void }>
|
||||
for (const elem of path) {
|
||||
if (elem.click) {
|
||||
firstClickable = elem
|
||||
break
|
||||
}
|
||||
}
|
||||
if (!firstClickable) return
|
||||
activeTouch = {
|
||||
touch: e.touches[0],
|
||||
elem: e.composedPath()[0] as HTMLElement
|
||||
elem: firstClickable,
|
||||
start: Date.now(),
|
||||
}
|
||||
}, { passive: false })
|
||||
// #endregion
|
||||
|
|
|
|||
|
|
@ -22,6 +22,9 @@ let peerInstance: Peer | undefined
|
|||
export const getJoinLink = () => {
|
||||
if (!peerInstance) return
|
||||
const url = new URL(window.location.href)
|
||||
for (const key of url.searchParams.keys()) {
|
||||
url.searchParams.delete(key)
|
||||
}
|
||||
url.searchParams.set('connectPeer', peerInstance.id)
|
||||
url.searchParams.set('peerVersion', localServer!.options.version)
|
||||
return url.toString()
|
||||
|
|
|
|||
|
|
@ -1,4 +1,8 @@
|
|||
const { LitElement, html, css } = require('lit')
|
||||
const { subscribeKey } = require('valtio/utils')
|
||||
const { miscUiState } = require('../../globalState')
|
||||
const { options } = require('../../optionsStorage')
|
||||
const { getFixedFilesize } = require('../../downloadAndOpenFile')
|
||||
|
||||
class DebugOverlay extends LitElement {
|
||||
static get styles () {
|
||||
|
|
@ -55,7 +59,8 @@ class DebugOverlay extends LitElement {
|
|||
cursorBlock: { type: Object },
|
||||
rendererDevice: { type: String },
|
||||
bot: { type: Object },
|
||||
customEntries: { type: Object }
|
||||
customEntries: { type: Object },
|
||||
packetsString: { type: String }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -63,6 +68,7 @@ class DebugOverlay extends LitElement {
|
|||
super()
|
||||
this.showOverlay = false
|
||||
this.customEntries = {}
|
||||
this.packetsString = ''
|
||||
}
|
||||
|
||||
firstUpdated () {
|
||||
|
|
@ -72,6 +78,120 @@ class DebugOverlay extends LitElement {
|
|||
e.preventDefault()
|
||||
}
|
||||
})
|
||||
|
||||
let receivedTotal = 0
|
||||
let received = {
|
||||
count: 0,
|
||||
size: 0
|
||||
}
|
||||
let sent = {
|
||||
count: 0,
|
||||
size: 0
|
||||
}
|
||||
const packetsCountByNamePerSec = {
|
||||
received: {},
|
||||
sent: {}
|
||||
}
|
||||
const hardcodedListOfDebugPacketsToIgnore = {
|
||||
received: [
|
||||
'entity_velocity',
|
||||
'sound_effect',
|
||||
'rel_entity_move',
|
||||
'entity_head_rotation',
|
||||
'entity_metadata',
|
||||
'entity_move_look',
|
||||
'teams',
|
||||
'entity_teleport',
|
||||
'entity_look',
|
||||
'ping',
|
||||
'entity_update_attributes',
|
||||
'player_info',
|
||||
'update_time',
|
||||
'animation',
|
||||
'entity_equipment',
|
||||
'entity_destroy',
|
||||
'named_entity_spawn',
|
||||
'update_light',
|
||||
'set_slot',
|
||||
'block_break_animation',
|
||||
'map_chunk',
|
||||
'spawn_entity',
|
||||
'world_particles',
|
||||
'keep_alive',
|
||||
'chat',
|
||||
'playerlist_header',
|
||||
'scoreboard_objective',
|
||||
'scoreboard_score'
|
||||
],
|
||||
sent: [
|
||||
'pong',
|
||||
'position',
|
||||
'look',
|
||||
'keep_alive',
|
||||
'position_look'
|
||||
]
|
||||
} // todo cleanup?
|
||||
const ignoredPackets = new Set('')
|
||||
Object.defineProperty(window, 'debugTopPackets', {
|
||||
get () {
|
||||
return Object.fromEntries(Object.entries(packetsCountByName).map(([s, packets]) => [s, Object.fromEntries(Object.entries(packets).sort(([, n1], [, n2]) => {
|
||||
return n2 - n1
|
||||
}))]))
|
||||
}
|
||||
})
|
||||
setInterval(() => {
|
||||
this.packetsString = `↓ ${received.count} (${(received.size / 1024).toFixed(2)} KB/s, ${getFixedFilesize(receivedTotal)}) ↑ ${sent.count}`
|
||||
received = {
|
||||
count: 0,
|
||||
size: 0
|
||||
}
|
||||
sent = {
|
||||
count: 0,
|
||||
size: 0
|
||||
}
|
||||
packetsCountByNamePerSec.received = {}
|
||||
packetsCountByNamePerSec.sent = {}
|
||||
}, 1000)
|
||||
const packetsCountByName = {
|
||||
received: {},
|
||||
sent: {}
|
||||
}
|
||||
|
||||
const managePackets = (type, name, data) => {
|
||||
packetsCountByName[type][name] ??= 0
|
||||
packetsCountByName[type][name]++
|
||||
if (options.debugLogNotFrequentPackets && !ignoredPackets.has(name) && !hardcodedListOfDebugPacketsToIgnore[type].includes(name)) {
|
||||
packetsCountByNamePerSec[type][name] ??= 0
|
||||
packetsCountByNamePerSec[type][name]++
|
||||
if (packetsCountByNamePerSec[type][name] > 5 || packetsCountByName[type][name] > 100) { // todo think of tracking the count within 10s
|
||||
console.info(`[packet ${name} was ${type} too frequent] Ignoring...`)
|
||||
ignoredPackets.add(name)
|
||||
} else {
|
||||
console.info(`[packet ${type}] ${name}`, /* ${JSON.stringify(data, null, 2)}` */ data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
subscribeKey(miscUiState, 'gameLoaded', () => {
|
||||
if (!miscUiState.gameLoaded) return
|
||||
packetsCountByName.received = {}
|
||||
packetsCountByName.sent = {}
|
||||
const readPacket = (data, { name }, _buf, fullBuffer) => {
|
||||
if (fullBuffer) {
|
||||
const size = fullBuffer.byteLength
|
||||
receivedTotal += size
|
||||
received.size += size
|
||||
}
|
||||
received.count++
|
||||
managePackets('received', name, data)
|
||||
}
|
||||
bot._client.on('packet', readPacket)
|
||||
bot._client.on('packet_name', (name, data) => readPacket(data, { name })) // custom client
|
||||
bot._client.on('writePacket', (name, data) => {
|
||||
sent.count++
|
||||
managePackets('sent', name, data)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
updated (changedProperties) {
|
||||
|
|
@ -128,6 +248,7 @@ class DebugOverlay extends LitElement {
|
|||
<div class="empty"></div>
|
||||
<p>XYZ: ${pos.x.toFixed(3)} / ${pos.y.toFixed(3)} / ${pos.z.toFixed(3)}</p>
|
||||
<p>Chunk: ${Math.floor(pos.x % 16)} ~ ${Math.floor(pos.z % 16)} in ${Math.floor(pos.x / 16)} ~ ${Math.floor(pos.z / 16)}</p>
|
||||
<p>Packets: ${this.packetsString}</p>
|
||||
<p>Facing (viewer): ${rot[0].toFixed(3)} ${rot[1].toFixed(3)}</p>
|
||||
<p>Facing (minecraft): ${quadsDescription[minecraftQuad]} (${minecraftYaw.toFixed(1)} ${(rot[1] * -180 / Math.PI).toFixed(1)})</p>
|
||||
<p>Light: ${skyL} (${skyL} sky)</p>
|
||||
|
|
|
|||
|
|
@ -1,3 +1,6 @@
|
|||
import { f3Keybinds } from '../controls'
|
||||
import { showOptionsModal } from '../react/SelectOption'
|
||||
|
||||
const { LitElement, html, css, unsafeCSS } = require('lit')
|
||||
const { showModal, miscUiState } = require('../globalState')
|
||||
const { options, watchValue } = require('../optionsStorage')
|
||||
|
|
@ -219,7 +222,12 @@ class Hud extends LitElement {
|
|||
<div class="debug-btn" @pointerdown=${(e) => {
|
||||
window.dispatchEvent(new MouseEvent('mousedown', { button: 1 }))
|
||||
}}>S</div>
|
||||
<div class="debug-btn" @pointerdown=${(e) => {
|
||||
<div class="debug-btn" @longtouch=${async () => {
|
||||
const select = await showOptionsModal('', f3Keybinds.filter(f3Keybind => f3Keybind.mobileTitle).map(f3Keybind => f3Keybind.mobileTitle))
|
||||
if (!select) return
|
||||
const f3Keybind = f3Keybinds.find(f3Keybind => f3Keybind.mobileTitle === select)
|
||||
f3Keybind.action()
|
||||
}} @pointerdown=${(e) => {
|
||||
this.shadowRoot.getElementById('debug-overlay').showOverlay = !this.shadowRoot.getElementById('debug-overlay').showOverlay
|
||||
}}>F3</div>
|
||||
<div class="chat-btn" @pointerdown=${(e) => {
|
||||
|
|
|
|||
|
|
@ -123,6 +123,11 @@ export const guiOptionsScheme: {
|
|||
custom () {
|
||||
return <Button label='Advanced...' onClick={() => openOptionsMenu('advanced')} inScreen />
|
||||
},
|
||||
},
|
||||
{
|
||||
custom () {
|
||||
return <Button label='VR...' onClick={() => openOptionsMenu('VR')} inScreen />
|
||||
},
|
||||
}
|
||||
],
|
||||
interface: [
|
||||
|
|
@ -178,6 +183,16 @@ export const guiOptionsScheme: {
|
|||
{ volume: {} },
|
||||
// { ignoreSilentSwitch: {} },
|
||||
],
|
||||
VR: [
|
||||
{
|
||||
custom () {
|
||||
return <>
|
||||
<span style={{ fontSize: 9, display: 'flex', justifyContent: 'center', alignItems: 'center' }}>VR currently has basic support</span>
|
||||
<div />
|
||||
</>
|
||||
},
|
||||
}
|
||||
],
|
||||
advanced: [
|
||||
{
|
||||
custom () {
|
||||
|
|
@ -188,4 +203,4 @@ export const guiOptionsScheme: {
|
|||
}
|
||||
],
|
||||
}
|
||||
export type OptionsGroupType = 'main' | 'render' | 'interface' | 'controls' | 'sound' | 'advanced'
|
||||
export type OptionsGroupType = 'main' | 'render' | 'interface' | 'controls' | 'sound' | 'advanced' | 'VR'
|
||||
|
|
|
|||
|
|
@ -4,8 +4,6 @@ import { proxy, subscribe } from 'valtio/vanilla'
|
|||
// weird webpack configuration bug: it cant import valtio/utils in this file
|
||||
import { subscribeKey } from 'valtio/utils'
|
||||
|
||||
const mergeAny: <T>(arg1: T, arg2: any) => T = Object.assign
|
||||
|
||||
const defaultOptions = {
|
||||
renderDistance: 2,
|
||||
multiplayerRenderDistance: 2,
|
||||
|
|
@ -31,6 +29,8 @@ const defaultOptions = {
|
|||
highPerformanceGpu: false,
|
||||
/** @unstable */
|
||||
disableAssets: false,
|
||||
/** @unstable */
|
||||
debugLogNotFrequentPackets: false,
|
||||
unimplementedContainers: false,
|
||||
dayCycleAndLighting: true,
|
||||
|
||||
|
|
@ -53,9 +53,10 @@ const defaultOptions = {
|
|||
|
||||
export type AppOptions = typeof defaultOptions
|
||||
|
||||
export const options = proxy(
|
||||
mergeAny(defaultOptions, JSON.parse(localStorage.options || '{}'))
|
||||
)
|
||||
export const options: AppOptions = proxy({
|
||||
...defaultOptions,
|
||||
...JSON.parse(localStorage.options || '{}')
|
||||
})
|
||||
|
||||
window.options = window.settings = options
|
||||
|
||||
|
|
@ -63,6 +64,12 @@ export const resetOptions = () => {
|
|||
Object.assign(options, defaultOptions)
|
||||
}
|
||||
|
||||
Object.defineProperty(window, 'debugChangedOptions', {
|
||||
get () {
|
||||
return Object.fromEntries(Object.entries(options).filter(([key, v]) => defaultOptions[key] !== v))
|
||||
},
|
||||
})
|
||||
|
||||
subscribe(options, () => {
|
||||
localStorage.options = JSON.stringify(options)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -117,10 +117,10 @@ export async function addPanoramaCubeMap () {
|
|||
}
|
||||
|
||||
export function removePanorama () {
|
||||
shouldDisplayPanorama = false
|
||||
if (!panoramaCubeMap) return
|
||||
viewer.camera = new THREE.PerspectiveCamera(options.fov, window.innerWidth / window.innerHeight, 0.1, 1000)
|
||||
viewer.camera.updateProjectionMatrix()
|
||||
viewer.scene.remove(panoramaCubeMap)
|
||||
panoramaCubeMap = null
|
||||
shouldDisplayPanorama = false
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,8 +30,8 @@ export default () => {
|
|||
console.error(err)
|
||||
}
|
||||
})
|
||||
bot.on('death', () => {
|
||||
if (dieReasonProxy.value) return
|
||||
bot.on('health', () => { // bot.isAlive can be already false so can't use death event (respawn packet)
|
||||
if (dieReasonProxy.value || bot.health > 0) return
|
||||
dieReasonProxy.value = []
|
||||
})
|
||||
|
||||
|
|
|
|||
28
src/react/EnterFullscreenButton.tsx
Normal file
28
src/react/EnterFullscreenButton.tsx
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import { useUsingTouch } from '@dimaka/interface'
|
||||
import { useEffect, useState } from 'react'
|
||||
import Button from './Button'
|
||||
|
||||
export default () => {
|
||||
const [fullScreen, setFullScreen] = useState(false)
|
||||
useEffect(() => {
|
||||
document.documentElement.addEventListener('fullscreenchange', () => {
|
||||
setFullScreen(!!document.fullscreenElement)
|
||||
})
|
||||
}, [])
|
||||
|
||||
const usingTouch = useUsingTouch()
|
||||
if (!usingTouch || !document.documentElement.requestFullscreen || fullScreen) return null
|
||||
|
||||
return <Button
|
||||
icon='pixelarticons:scale'
|
||||
style={{
|
||||
position: 'fixed',
|
||||
top: 5,
|
||||
left: 5,
|
||||
width: 22,
|
||||
}}
|
||||
onClick={() => {
|
||||
void document.documentElement.requestFullscreen()
|
||||
}}
|
||||
/>
|
||||
}
|
||||
|
|
@ -22,7 +22,7 @@ const Slider: React.FC<Props> = ({
|
|||
width,
|
||||
value: valueProp,
|
||||
valueDisplay,
|
||||
min = 1,
|
||||
min = 0,
|
||||
max = 100,
|
||||
disabledReason,
|
||||
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import SingleplayerProvider from './react/SingleplayerProvider'
|
|||
import CreateWorldProvider from './react/CreateWorldProvider'
|
||||
import AppStatusProvider from './react/AppStatusProvider'
|
||||
import SelectOption from './react/SelectOption'
|
||||
import EnterFullscreenButton from './react/EnterFullscreenButton'
|
||||
|
||||
// todo
|
||||
useInterfaceState.setState({
|
||||
|
|
@ -135,6 +136,7 @@ const InGameUi = () => {
|
|||
|
||||
const App = () => {
|
||||
return <div>
|
||||
<EnterFullscreenButton />
|
||||
<InGameUi />
|
||||
<Portal to={document.querySelector('#ui-root')}>
|
||||
<SingleplayerProvider />
|
||||
|
|
|
|||
|
|
@ -29,6 +29,8 @@
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-top: 35px;
|
||||
/* todo remove it but without it in chrome android the screen is not scrollable */
|
||||
overflow: auto;
|
||||
/* todo I'm not sure about it */
|
||||
/* margin-top: calc(100% / 6 - 16px); */
|
||||
width: 310px;
|
||||
|
|
|
|||
|
|
@ -64,6 +64,12 @@ body {
|
|||
text-shadow: 1px 1px #222;
|
||||
}
|
||||
|
||||
#VRButton {
|
||||
background: rgba(0, 0, 0, 0.3) !important;
|
||||
opacity: 0.7 !important;
|
||||
position: fixed !important;
|
||||
}
|
||||
|
||||
.dirt-bg {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
|
|
|
|||
|
|
@ -155,8 +155,9 @@ export const setLoadingScreenStatus = function (status: string | undefined | nul
|
|||
export const disconnect = async () => {
|
||||
if (localServer) {
|
||||
await saveServer()
|
||||
localServer.quit()
|
||||
void localServer.quit() // todo investigate we should await
|
||||
}
|
||||
window.history.replaceState({}, '', `${window.location.pathname}`) // remove qs
|
||||
bot.end('You left the server')
|
||||
}
|
||||
|
||||
|
|
|
|||
127
src/vr.js
127
src/vr.js
|
|
@ -1,13 +1,13 @@
|
|||
/* global THREE */
|
||||
|
||||
const { VRButton } = require('three/examples/jsm/webxr/VRButton.js')
|
||||
const { GLTFLoader } = require('three/examples/jsm/loaders/GLTFLoader.js')
|
||||
const { XRControllerModelFactory } = require('three/examples/jsm/webxr/XRControllerModelFactory.js')
|
||||
const TWEEN = require('@tweenjs/tween.js')
|
||||
const { buttonMap: standardButtonsMap } = require('contro-max/build/gamepad')
|
||||
const { activeModalStack, hideModal } = require('./globalState')
|
||||
|
||||
async function initVR (bot, renderer, viewer) {
|
||||
async function initVR () {
|
||||
const { renderer } = viewer
|
||||
if (!('xr' in navigator)) return
|
||||
const isSupported = await navigator.xr.isSessionSupported('immersive-vr')
|
||||
const isSupported = await navigator.xr.isSessionSupported('immersive-vr') && !!XRSession.prototype.updateRenderState // e.g. android webview doesn't support updateRenderState
|
||||
if (!isSupported) return
|
||||
|
||||
// VR
|
||||
|
|
@ -21,32 +21,70 @@ async function initVR (bot, renderer, viewer) {
|
|||
const controllerModelFactory = new XRControllerModelFactory(new GLTFLoader())
|
||||
const controller1 = renderer.xr.getControllerGrip(0)
|
||||
const controller2 = renderer.xr.getControllerGrip(1)
|
||||
|
||||
// todo the logic written here can be hard to understand as it was designed to work in gamepad api emulation mode, will be refactored once there is a contro-max rewrite is done
|
||||
const virtualGamepadIndex = 4
|
||||
let connectedVirtualGamepad
|
||||
const manageXrInputSource = ({ gamepad, handedness = defaultHandedness }, defaultHandedness, removeAction = false) => {
|
||||
if (handedness === 'right') {
|
||||
const event = new Event(removeAction ? 'gamepaddisconnected' : 'gamepadconnected') // todo need to expose and use external gamepads api in contro-max instead
|
||||
event.gamepad = removeAction ? connectedVirtualGamepad : { ...gamepad, mapping: 'standard', index: virtualGamepadIndex }
|
||||
connectedVirtualGamepad = event.gamepad
|
||||
window.dispatchEvent(event)
|
||||
}
|
||||
}
|
||||
let hand1 = controllerModelFactory.createControllerModel(controller1)
|
||||
controller1.addEventListener('connected', (event) => {
|
||||
hand1.xrInputSource = event.data
|
||||
manageXrInputSource(event.data, 'left')
|
||||
user.add(controller1)
|
||||
})
|
||||
controller1.add(hand1)
|
||||
let hand2 = controllerModelFactory.createControllerModel(controller2)
|
||||
controller2.addEventListener('connected', (event) => {
|
||||
hand2.xrInputSource = event.data
|
||||
manageXrInputSource(event.data, 'right')
|
||||
user.add(controller2)
|
||||
})
|
||||
controller2.add(hand2)
|
||||
|
||||
viewer.setFirstPersonCamera = function (pos, yaw, pitch) {
|
||||
if (pos) new TWEEN.Tween(user.position).to({ x: pos.x, y: pos.y, z: pos.z }, 50).start()
|
||||
user.rotation.set(pitch, yaw, 0, 'ZYX')
|
||||
controller1.addEventListener('disconnected', () => {
|
||||
// don't handle removal of gamepads for now as is don't affect contro-max
|
||||
hand1.xrInputSource = undefined
|
||||
manageXrInputSource(hand1.xrInputSource, 'left', true)
|
||||
})
|
||||
controller2.addEventListener('disconnected', () => {
|
||||
hand2.xrInputSource = undefined
|
||||
manageXrInputSource(hand1.xrInputSource, 'right', true)
|
||||
})
|
||||
|
||||
const originalGetGamepads = navigator.getGamepads.bind(navigator)
|
||||
navigator.getGamepads = () => {
|
||||
const originalGamepads = originalGetGamepads()
|
||||
if (!hand1.xrInputSource || !hand2.xrInputSource) return originalGamepads
|
||||
return [
|
||||
...originalGamepads,
|
||||
{
|
||||
axes: remapAxes(hand2.xrInputSource.gamepad.axes, hand1.xrInputSource.gamepad.axes),
|
||||
buttons: remapButtons(hand2.xrInputSource.gamepad.buttons, hand1.xrInputSource.gamepad.buttons),
|
||||
connected: true,
|
||||
mapping: 'standard',
|
||||
id: '',
|
||||
index: virtualGamepadIndex
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
let rotSnapReset = true
|
||||
let yawOffset = 0
|
||||
renderer.setAnimationLoop(() => {
|
||||
if (!renderer.xr.isPresenting) return
|
||||
if (hand1.xrInputSource && hand2.xrInputSource) {
|
||||
hand1.xAxis = hand1.xrInputSource.gamepad.axes[2]
|
||||
hand1.yAxis = hand1.xrInputSource.gamepad.axes[3]
|
||||
hand2.xAxis = hand2.xrInputSource.gamepad.axes[2]
|
||||
hand2.yAxis = hand2.xrInputSource.gamepad.axes[3]
|
||||
// hand2 should be right
|
||||
if (hand1.xrInputSource.handedness === 'right') {
|
||||
const tmp = hand2
|
||||
hand2 = hand1
|
||||
|
|
@ -63,23 +101,70 @@ async function initVR (bot, renderer, viewer) {
|
|||
rotSnapReset = true
|
||||
}
|
||||
|
||||
viewer.setFirstPersonCamera(null, yawOffset, 0)
|
||||
// viewer.setFirstPersonCamera(null, yawOffset, 0)
|
||||
viewer.setFirstPersonCamera(null, bot.entity.yaw, bot.entity.pitch)
|
||||
|
||||
const xrCamera = renderer.xr.getCamera(viewer.camera)
|
||||
const d = xrCamera.getWorldDirection()
|
||||
bot.entity.yaw = Math.atan2(-d.x, -d.z)
|
||||
bot.entity.pitch = Math.asin(d.y)
|
||||
// todo restore this logic (need to preserve ability to move camera)
|
||||
// const xrCamera = renderer.xr.getCamera(viewer.camera)
|
||||
// const d = xrCamera.getWorldDirection() // todo target
|
||||
// bot.entity.yaw = Math.atan2(-d.x, -d.z)
|
||||
// bot.entity.pitch = Math.asin(d.y)
|
||||
|
||||
bot.physics.stepHeight = 1
|
||||
bot.setControlState('forward', hand2.yAxis < -0.5)
|
||||
bot.setControlState('back', hand2.yAxis > 0.5)
|
||||
bot.setControlState('right', hand2.xAxis < -0.5)
|
||||
bot.setControlState('left', hand2.xAxis > 0.5)
|
||||
// todo ?
|
||||
// bot.physics.stepHeight = 1
|
||||
|
||||
TWEEN.update()
|
||||
viewer.update()
|
||||
renderer.render(viewer.scene, viewer.camera)
|
||||
viewer.render()
|
||||
})
|
||||
renderer.xr.addEventListener('sessionstart', () => {
|
||||
viewer.cameraObjectOverride = user
|
||||
// close all modals to be in game
|
||||
for (const _modal of activeModalStack) {
|
||||
hideModal(undefined, {}, { force: true })
|
||||
}
|
||||
})
|
||||
renderer.xr.addEventListener('sessionend', () => {
|
||||
viewer.cameraObjectOverride = undefined
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = { initVR }
|
||||
module.exports.initVR = initVR
|
||||
|
||||
const xrStandardRightButtonsMap = [
|
||||
[0 /* trigger */, 'Right Trigger'],
|
||||
[1 /* squeeze */, 'Right Bumper'],
|
||||
// need to think of a way to support touchpad input
|
||||
[3 /* Thumbstick Press */, 'Right Stick'],
|
||||
[4 /* A */, 'A'],
|
||||
[5 /* B */, 'B'],
|
||||
]
|
||||
const xrStandardLeftButtonsMap = [
|
||||
[0 /* trigger */, 'Left Trigger'],
|
||||
[1 /* squeeze */, 'Left Bumper'],
|
||||
// need to think of a way to support touchpad input
|
||||
[3 /* Thumbstick Press */, 'Left Stick'],
|
||||
[4 /* A */, 'X'],
|
||||
[5 /* B */, 'Y'],
|
||||
]
|
||||
const remapButtons = (rightButtons, leftButtons) => {
|
||||
// return remapped buttons
|
||||
const remapped = []
|
||||
const remapWithMap = (buttons, map) => {
|
||||
for (const [index, standardName] of map) {
|
||||
const standardMappingIndex = standardButtonsMap.findIndex((aliases) => aliases.find(alias => standardName === alias))
|
||||
remapped[standardMappingIndex] = buttons[index]
|
||||
}
|
||||
}
|
||||
remapWithMap(rightButtons, xrStandardRightButtonsMap)
|
||||
remapWithMap(leftButtons, xrStandardLeftButtonsMap)
|
||||
return remapped
|
||||
}
|
||||
const remapAxes = (axesRight, axesLeft) => {
|
||||
// 0, 1 are reserved for touch
|
||||
return [
|
||||
axesLeft[2],
|
||||
axesLeft[3],
|
||||
axesRight[2],
|
||||
axesRight[3]
|
||||
]
|
||||
}
|
||||
|
|
|
|||
10
vitest.config.ts
Normal file
10
vitest.config.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import { defineConfig } from 'vitest/config'
|
||||
|
||||
export default defineConfig({
|
||||
root: 'prismarine-viewer/viewer',
|
||||
test: {
|
||||
include: [
|
||||
'**/*.test.ts'
|
||||
],
|
||||
},
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue