Merge branch 'next' into nextgen-physics
This commit is contained in:
commit
2d7ec12a75
57 changed files with 2063 additions and 680 deletions
40
.github/workflows/ci.yml
vendored
40
.github/workflows/ci.yml
vendored
|
|
@ -40,25 +40,25 @@ jobs:
|
|||
# if: ${{ github.event.pull_request.base.ref == 'release' }}
|
||||
# env:
|
||||
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
dedupe-check:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event.pull_request.head.ref == 'next'
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
# dedupe-check:
|
||||
# runs-on: ubuntu-latest
|
||||
# if: github.event.pull_request.head.ref == 'next'
|
||||
# steps:
|
||||
# - name: Checkout repository
|
||||
# uses: actions/checkout@v2
|
||||
|
||||
- name: Install pnpm
|
||||
run: npm install -g pnpm@9.0.4
|
||||
|
||||
- name: Run pnpm dedupe
|
||||
run: pnpm dedupe
|
||||
# - name: Install pnpm
|
||||
# run: npm install -g pnpm@9.0.4
|
||||
|
||||
- name: Check for changes
|
||||
run: |
|
||||
if ! git diff --exit-code --quiet pnpm-lock.yaml; then
|
||||
echo "pnpm dedupe introduced changes:"
|
||||
git diff --color=always pnpm-lock.yaml
|
||||
exit 1
|
||||
else
|
||||
echo "No changes detected after pnpm dedupe in pnpm-lock.yaml"
|
||||
fi
|
||||
# - name: Run pnpm dedupe
|
||||
# run: pnpm dedupe
|
||||
|
||||
# - name: Check for changes
|
||||
# run: |
|
||||
# if ! git diff --exit-code --quiet pnpm-lock.yaml; then
|
||||
# echo "pnpm dedupe introduced changes:"
|
||||
# git diff --color=always pnpm-lock.yaml
|
||||
# exit 1
|
||||
# else
|
||||
# echo "No changes detected after pnpm dedupe in pnpm-lock.yaml"
|
||||
# fi
|
||||
|
|
|
|||
11
Dockerfile
11
Dockerfile
|
|
@ -6,9 +6,14 @@ WORKDIR /app
|
|||
COPY . /app
|
||||
# install pnpm
|
||||
RUN npm i -g pnpm@9.0.4
|
||||
# Build arguments
|
||||
ARG DOWNLOAD_SOUNDS=false
|
||||
ARG DISABLE_SERVICE_WORKER=false
|
||||
# TODO need flat --no-root-optional
|
||||
RUN node ./scripts/dockerPrepare.mjs
|
||||
RUN pnpm i
|
||||
# Download sounds if flag is enabled
|
||||
RUN if [ "$DOWNLOAD_SOUNDS" = "true" ] ; then node scripts/downloadSoundsMap.mjs ; fi
|
||||
|
||||
# TODO for development
|
||||
# EXPOSE 9090
|
||||
|
|
@ -17,7 +22,9 @@ RUN pnpm i
|
|||
# ENTRYPOINT ["pnpm", "run", "run-all"]
|
||||
|
||||
# only for prod
|
||||
RUN GITHUB_REPOSITORY=zardoy/minecraft-web-client pnpm run build
|
||||
RUN GITHUB_REPOSITORY=zardoy/minecraft-web-client \
|
||||
DISABLE_SERVICE_WORKER=$DISABLE_SERVICE_WORKER \
|
||||
pnpm run build
|
||||
|
||||
# ---- Run Stage ----
|
||||
FROM node:18-alpine
|
||||
|
|
@ -31,5 +38,5 @@ RUN npm i -g pnpm@9.0.4
|
|||
RUN npm init -yp
|
||||
RUN pnpm i express github:zardoy/prismarinejs-net-browserify compression cors
|
||||
EXPOSE 8080
|
||||
VOLUME /app/dist
|
||||
VOLUME /app/public
|
||||
ENTRYPOINT ["node", "server.js", "--prod"]
|
||||
|
|
|
|||
18
pnpm-lock.yaml
generated
18
pnpm-lock.yaml
generated
|
|
@ -147,7 +147,7 @@ importers:
|
|||
version: 2.0.4
|
||||
net-browserify:
|
||||
specifier: github:zardoy/prismarinejs-net-browserify
|
||||
version: https://codeload.github.com/zardoy/prismarinejs-net-browserify/tar.gz/ab3721ca833308a0be099d14ea0053fbd8459ace
|
||||
version: https://codeload.github.com/zardoy/prismarinejs-net-browserify/tar.gz/624cc67c16f5e8b23b772e7eaabae16ba84b8590
|
||||
node-gzip:
|
||||
specifier: ^1.1.2
|
||||
version: 1.1.2
|
||||
|
|
@ -162,7 +162,7 @@ importers:
|
|||
version: 6.1.1
|
||||
prismarine-provider-anvil:
|
||||
specifier: github:zardoy/prismarine-provider-anvil#everything
|
||||
version: https://codeload.github.com/zardoy/prismarine-provider-anvil/tar.gz/a3a7d031069373cc3e0cd05e54512dd9461ca34b(minecraft-data@3.83.1)
|
||||
version: https://codeload.github.com/zardoy/prismarine-provider-anvil/tar.gz/1d548fac63fe977c8281f0a9a522b37e4d92d0b7(minecraft-data@3.83.1)
|
||||
prosemirror-example-setup:
|
||||
specifier: ^1.2.2
|
||||
version: 1.2.2
|
||||
|
|
@ -6633,8 +6633,8 @@ packages:
|
|||
neo-async@2.6.2:
|
||||
resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==}
|
||||
|
||||
net-browserify@https://codeload.github.com/zardoy/prismarinejs-net-browserify/tar.gz/ab3721ca833308a0be099d14ea0053fbd8459ace:
|
||||
resolution: {tarball: https://codeload.github.com/zardoy/prismarinejs-net-browserify/tar.gz/ab3721ca833308a0be099d14ea0053fbd8459ace}
|
||||
net-browserify@https://codeload.github.com/zardoy/prismarinejs-net-browserify/tar.gz/624cc67c16f5e8b23b772e7eaabae16ba84b8590:
|
||||
resolution: {tarball: https://codeload.github.com/zardoy/prismarinejs-net-browserify/tar.gz/624cc67c16f5e8b23b772e7eaabae16ba84b8590}
|
||||
version: 0.2.4
|
||||
|
||||
nice-try@1.0.5:
|
||||
|
|
@ -7188,8 +7188,8 @@ packages:
|
|||
resolution: {tarball: https://codeload.github.com/zardoy/prismarine-physics/tar.gz/353e25b800149393f40539ec381218be44cbb03b}
|
||||
version: 1.9.0
|
||||
|
||||
prismarine-provider-anvil@https://codeload.github.com/zardoy/prismarine-provider-anvil/tar.gz/a3a7d031069373cc3e0cd05e54512dd9461ca34b:
|
||||
resolution: {tarball: https://codeload.github.com/zardoy/prismarine-provider-anvil/tar.gz/a3a7d031069373cc3e0cd05e54512dd9461ca34b}
|
||||
prismarine-provider-anvil@https://codeload.github.com/zardoy/prismarine-provider-anvil/tar.gz/1d548fac63fe977c8281f0a9a522b37e4d92d0b7:
|
||||
resolution: {tarball: https://codeload.github.com/zardoy/prismarine-provider-anvil/tar.gz/1d548fac63fe977c8281f0a9a522b37e4d92d0b7}
|
||||
version: 2.8.0
|
||||
|
||||
prismarine-realms@1.3.2:
|
||||
|
|
@ -12791,7 +12791,7 @@ snapshots:
|
|||
prismarine-entity: 2.3.1
|
||||
prismarine-item: 1.16.0
|
||||
prismarine-nbt: 2.5.0
|
||||
prismarine-provider-anvil: https://codeload.github.com/zardoy/prismarine-provider-anvil/tar.gz/a3a7d031069373cc3e0cd05e54512dd9461ca34b(minecraft-data@3.83.1)
|
||||
prismarine-provider-anvil: https://codeload.github.com/zardoy/prismarine-provider-anvil/tar.gz/1d548fac63fe977c8281f0a9a522b37e4d92d0b7(minecraft-data@3.83.1)
|
||||
prismarine-windows: 2.9.0
|
||||
prismarine-world: https://codeload.github.com/zardoy/prismarine-world/tar.gz/ab2146c9933eef3247c3f64446de4ccc2c484c7c
|
||||
rambda: 9.2.0
|
||||
|
|
@ -17224,7 +17224,7 @@ snapshots:
|
|||
|
||||
neo-async@2.6.2: {}
|
||||
|
||||
net-browserify@https://codeload.github.com/zardoy/prismarinejs-net-browserify/tar.gz/ab3721ca833308a0be099d14ea0053fbd8459ace:
|
||||
net-browserify@https://codeload.github.com/zardoy/prismarinejs-net-browserify/tar.gz/624cc67c16f5e8b23b772e7eaabae16ba84b8590:
|
||||
dependencies:
|
||||
body-parser: 1.20.2
|
||||
express: 4.18.2
|
||||
|
|
@ -17869,7 +17869,7 @@ snapshots:
|
|||
prismarine-nbt: 2.5.0
|
||||
vec3: 0.1.8
|
||||
|
||||
prismarine-provider-anvil@https://codeload.github.com/zardoy/prismarine-provider-anvil/tar.gz/a3a7d031069373cc3e0cd05e54512dd9461ca34b(minecraft-data@3.83.1):
|
||||
prismarine-provider-anvil@https://codeload.github.com/zardoy/prismarine-provider-anvil/tar.gz/1d548fac63fe977c8281f0a9a522b37e4d92d0b7(minecraft-data@3.83.1):
|
||||
dependencies:
|
||||
prismarine-block: https://codeload.github.com/zardoy/prismarine-block/tar.gz/853c559bff2b402863ee9a75b125a3ca320838f9(prismarine-registry@1.11.0)
|
||||
prismarine-chunk: https://codeload.github.com/zardoy/prismarine-chunk/tar.gz/e68e9a423b5b1907535878fb636f12c28a1a9374(minecraft-data@3.83.1)
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import { getMesh } from './entity/EntityMesh'
|
|||
import { WalkingGeneralSwing } from './entity/animations'
|
||||
import { disposeObject } from './threeJsUtils'
|
||||
import { armorModels } from './entity/objModels'
|
||||
import { Viewer } from './viewer'
|
||||
const { loadTexture } = globalThis.isElectron ? require('./utils.electron.js') : require('./utils')
|
||||
|
||||
export const TWEEN_DURATION = 120
|
||||
|
|
@ -163,12 +164,12 @@ const nametags = {}
|
|||
|
||||
const isFirstUpperCase = (str) => str.charAt(0) === str.charAt(0).toUpperCase()
|
||||
|
||||
function getEntityMesh (entity, scene, options, overrides) {
|
||||
function getEntityMesh (entity, world, options, overrides) {
|
||||
if (entity.name) {
|
||||
try {
|
||||
// https://github.com/PrismarineJS/prismarine-viewer/pull/410
|
||||
const entityName = (isFirstUpperCase(entity.name) ? snakeCase(entity.name) : entity.name).toLowerCase()
|
||||
const e = new Entity.EntityMesh('1.16.4', entityName, scene, overrides)
|
||||
const e = new Entity.EntityMesh('1.16.4', entityName, world, overrides)
|
||||
|
||||
if (e.mesh) {
|
||||
addNametag(entity, options, e.mesh)
|
||||
|
|
@ -211,6 +212,8 @@ export class Entities extends EventEmitter {
|
|||
clock = new THREE.Clock()
|
||||
rendering = true
|
||||
itemsTexture: THREE.Texture | null = null
|
||||
cachedMapsImages = {} as Record<number, string>
|
||||
itemFrameMaps = {} as Record<number, Array<THREE.Mesh<THREE.PlaneGeometry, THREE.MeshLambertMaterial>>>
|
||||
getItemUv: undefined | ((idOrName: number | string) => {
|
||||
texture: THREE.Texture;
|
||||
u: number;
|
||||
|
|
@ -220,7 +223,7 @@ export class Entities extends EventEmitter {
|
|||
size?: number;
|
||||
})
|
||||
|
||||
constructor (public scene: THREE.Scene) {
|
||||
constructor (public viewer: Viewer) {
|
||||
super()
|
||||
this.entitiesOptions = {}
|
||||
this.debugMode = 'none'
|
||||
|
|
@ -229,7 +232,7 @@ export class Entities extends EventEmitter {
|
|||
|
||||
clear () {
|
||||
for (const mesh of Object.values(this.entities)) {
|
||||
this.scene.remove(mesh)
|
||||
this.viewer.scene.remove(mesh)
|
||||
disposeObject(mesh)
|
||||
}
|
||||
this.entities = {}
|
||||
|
|
@ -251,9 +254,9 @@ export class Entities extends EventEmitter {
|
|||
this.rendering = rendering
|
||||
for (const ent of entity ? [entity] : Object.values(this.entities)) {
|
||||
if (rendering) {
|
||||
if (!this.scene.children.includes(ent)) this.scene.add(ent)
|
||||
if (!this.viewer.scene.children.includes(ent)) this.viewer.scene.add(ent)
|
||||
} else {
|
||||
this.scene.remove(ent)
|
||||
this.viewer.scene.remove(ent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -417,6 +420,7 @@ export class Entities extends EventEmitter {
|
|||
}
|
||||
|
||||
getItemMesh (item) {
|
||||
// TODO: Render proper model (especially for blocks) instead of flat texture
|
||||
const textureUv = this.getItemUv?.(item.itemId ?? item.blockId)
|
||||
if (textureUv) {
|
||||
// todo use geometry buffer uv instead!
|
||||
|
|
@ -470,9 +474,13 @@ export class Entities extends EventEmitter {
|
|||
|
||||
update (entity: import('prismarine-entity').Entity & { delete?; pos, name }, overrides) {
|
||||
const isPlayerModel = entity.name === 'player'
|
||||
if (entity.name === 'zombie' || entity.name === 'zombie_villager' || entity.name === 'husk') {
|
||||
if (entity.name === 'zombie_villager' || entity.name === 'husk') {
|
||||
overrides.texture = `textures/1.16.4/entity/${entity.name === 'zombie_villager' ? 'zombie_villager/zombie_villager.png' : `zombie/${entity.name}.png`}`
|
||||
}
|
||||
if (entity.name === 'glow_item_frame') {
|
||||
if (!overrides.textures) overrides.textures = []
|
||||
overrides.textures['background'] = 'block:glow_item_frame'
|
||||
}
|
||||
// this can be undefined in case where packet entity_destroy was sent twice (so it was already deleted)
|
||||
let e = this.entities[entity.id]
|
||||
|
||||
|
|
@ -480,7 +488,7 @@ export class Entities extends EventEmitter {
|
|||
if (!e) return
|
||||
if (e.additionalCleanup) e.additionalCleanup()
|
||||
this.emit('remove', entity)
|
||||
this.scene.remove(e)
|
||||
this.viewer.scene.remove(e)
|
||||
disposeObject(e)
|
||||
// todo dispose textures as well ?
|
||||
delete this.entities[entity.id]
|
||||
|
|
@ -551,7 +559,7 @@ export class Entities extends EventEmitter {
|
|||
//@ts-expect-error
|
||||
playerObject.animation.isMoving = false
|
||||
} else {
|
||||
mesh = getEntityMesh(entity, this.scene, this.entitiesOptions, overrides)
|
||||
mesh = getEntityMesh(entity, this.viewer.world, this.entitiesOptions, overrides)
|
||||
}
|
||||
if (!mesh) return
|
||||
mesh.name = 'mesh'
|
||||
|
|
@ -570,7 +578,7 @@ export class Entities extends EventEmitter {
|
|||
group.add(mesh)
|
||||
group.add(boxHelper)
|
||||
boxHelper.visible = false
|
||||
this.scene.add(group)
|
||||
this.viewer.scene.add(group)
|
||||
|
||||
e = group
|
||||
this.entities[entity.id] = e
|
||||
|
|
@ -694,31 +702,51 @@ export class Entities extends EventEmitter {
|
|||
}
|
||||
|
||||
// todo handle map, map_chunks events
|
||||
// if (entity.name === 'item_frame' || entity.name === 'glow_item_frame') {
|
||||
// const example = {
|
||||
// "present": true,
|
||||
// "itemId": 847,
|
||||
// "itemCount": 1,
|
||||
// "nbtData": {
|
||||
// "type": "compound",
|
||||
// "name": "",
|
||||
// "value": {
|
||||
// "map": {
|
||||
// "type": "int",
|
||||
// "value": 2146483444
|
||||
// },
|
||||
// "interactiveboard": {
|
||||
// "type": "byte",
|
||||
// "value": 1
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// const item = entity.metadata?.[8]
|
||||
// if (item.nbtData) {
|
||||
// const nbt = nbt.simplify(item.nbtData)
|
||||
// }
|
||||
// }
|
||||
let itemFrameMeta = getSpecificEntityMetadata('item_frame', entity)
|
||||
if (!itemFrameMeta) {
|
||||
itemFrameMeta = getSpecificEntityMetadata('glow_item_frame', entity)
|
||||
}
|
||||
if (itemFrameMeta) {
|
||||
// TODO: fix type
|
||||
// todo! fix errors in mc-data (no entities data prior 1.18.2)
|
||||
const item = (itemFrameMeta?.item ?? entity.metadata?.[8]) as any as { itemId, blockId, components, nbtData: { value: { map: { value: number } } } }
|
||||
mesh.scale.set(1, 1, 1)
|
||||
e.rotation.x = -entity.pitch
|
||||
e.children.find(c => {
|
||||
if (c.name.startsWith('map_')) {
|
||||
disposeObject(c)
|
||||
const existingMapNumber = parseInt(c.name.split('_')[1], 10)
|
||||
this.itemFrameMaps[existingMapNumber] = this.itemFrameMaps[existingMapNumber]?.filter(mesh => mesh !== c)
|
||||
if (c instanceof THREE.Mesh) {
|
||||
c.material?.map?.dispose()
|
||||
}
|
||||
return true
|
||||
} else if (c.name === 'item') {
|
||||
disposeObject(c)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})?.removeFromParent()
|
||||
if (item && (item.itemId ?? item.blockId ?? 0) !== 0) {
|
||||
const rotation = (itemFrameMeta.rotation as any as number) ?? 0
|
||||
const mapNumber = item.nbtData?.value?.map?.value ?? item.components?.find(x => x.type === 'map_id')?.data
|
||||
if (mapNumber) {
|
||||
// TODO: Use proper larger item frame model when a map exists
|
||||
mesh.scale.set(16 / 12, 16 / 12, 1)
|
||||
this.addMapModel(e, mapNumber, rotation)
|
||||
} else {
|
||||
const itemMesh = this.getItemMesh(item)
|
||||
if (itemMesh) {
|
||||
itemMesh.mesh.position.set(0, 0, 0.43)
|
||||
itemMesh.mesh.scale.set(0.5, 0.5, 0.5)
|
||||
itemMesh.mesh.rotateY(Math.PI)
|
||||
itemMesh.mesh.rotateZ(rotation * Math.PI / 4)
|
||||
itemMesh.mesh.name = 'item'
|
||||
e.add(itemMesh.mesh)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (entity.username) {
|
||||
e.username = entity.username
|
||||
|
|
@ -741,6 +769,74 @@ export class Entities extends EventEmitter {
|
|||
}
|
||||
}
|
||||
|
||||
updateMap (mapNumber: string | number, data: string) {
|
||||
this.cachedMapsImages[mapNumber] = data
|
||||
let itemFrameMeshes = this.itemFrameMaps[mapNumber]
|
||||
if (!itemFrameMeshes) return
|
||||
itemFrameMeshes = itemFrameMeshes.filter(mesh => mesh.parent)
|
||||
this.itemFrameMaps[mapNumber] = itemFrameMeshes
|
||||
if (itemFrameMeshes) {
|
||||
for (const mesh of itemFrameMeshes) {
|
||||
mesh.material.map = this.loadMap(data)
|
||||
mesh.material.needsUpdate = true
|
||||
mesh.visible = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
addMapModel (entityMesh: THREE.Object3D, mapNumber: number, rotation: number) {
|
||||
const imageData = this.cachedMapsImages?.[mapNumber]
|
||||
let texture: THREE.Texture | null = null
|
||||
if (imageData) {
|
||||
texture = this.loadMap(imageData)
|
||||
}
|
||||
const parameters = {
|
||||
transparent: true,
|
||||
alphaTest: 0.1,
|
||||
}
|
||||
if (texture) {
|
||||
parameters['map'] = texture
|
||||
}
|
||||
const material = new THREE.MeshLambertMaterial(parameters)
|
||||
|
||||
const mapMesh = new THREE.Mesh(new THREE.PlaneGeometry(1, 1), material)
|
||||
|
||||
mapMesh.rotation.set(0, Math.PI, 0)
|
||||
entityMesh.add(mapMesh)
|
||||
let isInvisible = false
|
||||
entityMesh.traverseVisible(c => {
|
||||
if (c.name === 'geometry_frame') {
|
||||
isInvisible = false
|
||||
}
|
||||
})
|
||||
if (isInvisible) {
|
||||
mapMesh.position.set(0, 0, 0.499)
|
||||
} else {
|
||||
mapMesh.position.set(0, 0, 0.437)
|
||||
}
|
||||
mapMesh.rotateZ(Math.PI * 2 - rotation * Math.PI / 2)
|
||||
mapMesh.name = `map_${mapNumber}`
|
||||
|
||||
if (!texture) {
|
||||
mapMesh.visible = false
|
||||
}
|
||||
|
||||
if (!this.itemFrameMaps[mapNumber]) {
|
||||
this.itemFrameMaps[mapNumber] = []
|
||||
}
|
||||
this.itemFrameMaps[mapNumber].push(mapMesh)
|
||||
}
|
||||
|
||||
loadMap (data: any) {
|
||||
const texture = new THREE.TextureLoader().load(data)
|
||||
if (texture) {
|
||||
texture.magFilter = THREE.NearestFilter
|
||||
texture.minFilter = THREE.NearestFilter
|
||||
texture.needsUpdate = true
|
||||
}
|
||||
return texture
|
||||
}
|
||||
|
||||
handleDamageEvent (entityId, damageAmount) {
|
||||
const entityMesh = this.entities[entityId]?.children.find(c => c.name === 'mesh')
|
||||
if (entityMesh) {
|
||||
|
|
@ -808,7 +904,7 @@ function addArmorModel (entityMesh: THREE.Object3D, slotType: string, item: Item
|
|||
material.map = texture
|
||||
})
|
||||
} else {
|
||||
mesh = getMesh(texturePath, armorModels.armorModel[slotType])
|
||||
mesh = getMesh(viewer.world, texturePath, armorModels.armorModel[slotType])
|
||||
mesh.name = meshName
|
||||
material = mesh.material
|
||||
material.side = THREE.DoubleSide
|
||||
|
|
|
|||
|
|
@ -94,7 +94,7 @@ function dot(a, b) {
|
|||
return a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
|
||||
}
|
||||
|
||||
function addCube(attr, boneId, bone, cube, texWidth = 64, texHeight = 64, mirror = false) {
|
||||
function addCube(attr, boneId, bone, cube, sameTextureForAllFaces = false, texWidth = 64, texHeight = 64, mirror = false) {
|
||||
const cubeRotation = new THREE.Euler(0, 0, 0)
|
||||
if (cube.rotation) {
|
||||
cubeRotation.x = -cube.rotation[0] * Math.PI / 180
|
||||
|
|
@ -107,8 +107,15 @@ function addCube(attr, boneId, bone, cube, texWidth = 64, texHeight = 64, mirror
|
|||
const eastOrWest = dir[0] !== 0
|
||||
const faceUvs = []
|
||||
for (const pos of corners) {
|
||||
const u = (cube.uv[0] + dot(pos[3] ? u1 : u0, cube.size)) / texWidth
|
||||
const v = (cube.uv[1] + dot(pos[4] ? v1 : v0, cube.size)) / texHeight
|
||||
let u
|
||||
let v
|
||||
if (sameTextureForAllFaces) {
|
||||
u = (cube.uv[0] + pos[3] * cube.size[0]) / texWidth
|
||||
v = (cube.uv[1] + pos[4] * cube.size[1]) / texHeight
|
||||
} else {
|
||||
u = (cube.uv[0] + dot(pos[3] ? u1 : u0, cube.size)) / texWidth
|
||||
v = (cube.uv[1] + dot(pos[4] ? v1 : v0, cube.size)) / texHeight
|
||||
}
|
||||
|
||||
const posX = eastOrWest && mirror ? pos[0] ^ 1 : pos[0]
|
||||
const posY = pos[1]
|
||||
|
|
@ -148,7 +155,23 @@ function addCube(attr, boneId, bone, cube, texWidth = 64, texHeight = 64, mirror
|
|||
}
|
||||
}
|
||||
|
||||
export function getMesh(texture, jsonModel, overrides = {}) {
|
||||
export function getMesh(worldRenderer, texture, jsonModel, overrides = {}) {
|
||||
let textureWidth = jsonModel.texturewidth ?? 64
|
||||
let textureHeight = jsonModel.textureheight ?? 64
|
||||
let textureOffset
|
||||
const useBlockTexture = texture.startsWith('block:')
|
||||
if (useBlockTexture) {
|
||||
const blockName = texture.slice(6)
|
||||
const textureInfo = worldRenderer.blocksAtlasParser.getTextureInfo(blockName)
|
||||
if (textureInfo) {
|
||||
textureWidth = worldRenderer.material.map.image.width
|
||||
textureHeight = worldRenderer.material.map.image.height
|
||||
textureOffset = [textureInfo.u, textureInfo.v]
|
||||
} else {
|
||||
console.error(`Unknown block ${blockName}`)
|
||||
}
|
||||
}
|
||||
|
||||
const bones = {}
|
||||
|
||||
const geoData = {
|
||||
|
|
@ -186,7 +209,7 @@ export function getMesh(texture, jsonModel, overrides = {}) {
|
|||
|
||||
if (jsonBone.cubes) {
|
||||
for (const cube of jsonBone.cubes) {
|
||||
addCube(geoData, i, bone, cube, jsonModel.texturewidth, jsonModel.textureheight, jsonBone.mirror)
|
||||
addCube(geoData, i, bone, cube, useBlockTexture, textureWidth, textureHeight, jsonBone.mirror)
|
||||
}
|
||||
}
|
||||
i++
|
||||
|
|
@ -215,18 +238,25 @@ export function getMesh(texture, jsonModel, overrides = {}) {
|
|||
mesh.bind(skeleton)
|
||||
mesh.scale.set(1 / 16, 1 / 16, 1 / 16)
|
||||
|
||||
loadTexture(texture, texture => {
|
||||
if (material.map) {
|
||||
// texture is already loaded
|
||||
return
|
||||
}
|
||||
texture.magFilter = THREE.NearestFilter
|
||||
texture.minFilter = THREE.NearestFilter
|
||||
texture.flipY = false
|
||||
texture.wrapS = THREE.RepeatWrapping
|
||||
texture.wrapT = THREE.RepeatWrapping
|
||||
if (textureOffset) {
|
||||
texture = worldRenderer.material.map.clone()
|
||||
texture.offset.set(textureOffset[0], textureOffset[1])
|
||||
texture.needsUpdate = true
|
||||
material.map = texture
|
||||
})
|
||||
} else {
|
||||
loadTexture(texture.endsWith('.png') || texture.startsWith('data:image/') ? texture : texture + '.png', texture => {
|
||||
if (material.map) {
|
||||
// texture is already loaded
|
||||
return
|
||||
}
|
||||
texture.magFilter = THREE.NearestFilter
|
||||
texture.minFilter = THREE.NearestFilter
|
||||
texture.flipY = false
|
||||
texture.wrapS = THREE.RepeatWrapping
|
||||
texture.wrapT = THREE.RepeatWrapping
|
||||
material.map = texture
|
||||
})
|
||||
}
|
||||
|
||||
return mesh
|
||||
}
|
||||
|
|
@ -252,6 +282,7 @@ export const temporaryMap = {
|
|||
'hopper_minecart': 'minecart',
|
||||
'command_block_minecart': 'minecart',
|
||||
'tnt_minecart': 'minecart',
|
||||
'glow_item_frame': 'item_frame',
|
||||
'glow_squid': 'squid',
|
||||
'trader_llama': 'llama',
|
||||
'chest_boat': 'boat',
|
||||
|
|
@ -321,7 +352,7 @@ const offsetEntity = {
|
|||
|
||||
// eslint-disable-next-line @typescript-eslint/no-extraneous-class
|
||||
export class EntityMesh {
|
||||
constructor(version, type, scene, /** @type {{textures?, rotation?: Record<string, {x,y,z}>}} */overrides = {}) {
|
||||
constructor(version, type, worldRenderer, /** @type {{textures?, rotation?: Record<string, {x,y,z}>}} */overrides = {}) {
|
||||
const originalType = type
|
||||
const mappedValue = temporaryMap[type]
|
||||
if (mappedValue) type = mappedValue
|
||||
|
|
@ -388,7 +419,7 @@ export class EntityMesh {
|
|||
const texture = overrides.textures?.[name] ?? e.textures[name]
|
||||
if (!texture) continue
|
||||
// console.log(JSON.stringify(jsonModel, null, 2))
|
||||
const mesh = getMesh(texture + '.png', jsonModel, overrides)
|
||||
const mesh = getMesh(worldRenderer, texture, jsonModel, overrides)
|
||||
mesh.name = `geometry_${name}`
|
||||
this.mesh.add(mesh)
|
||||
|
||||
|
|
|
|||
|
|
@ -7838,6 +7838,53 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"item_frame": {
|
||||
"identifier": "minecraft:item_frame",
|
||||
"materials": {"default": "item_frame"},
|
||||
"textures": {
|
||||
"background": "block:item_frame",
|
||||
"frame": "block:oak_planks"
|
||||
},
|
||||
"geometry": {
|
||||
"background": {
|
||||
"bones": [
|
||||
{
|
||||
"name": "base"
|
||||
},
|
||||
{
|
||||
"name": "background",
|
||||
"parent": "base",
|
||||
"rotation": [0, 180, 0],
|
||||
"pivot": [0, 0, 0],
|
||||
"cubes": [
|
||||
{"origin": [-5, -5, -8], "size": [10, 10, 0.5], "uv": [3, 3]}
|
||||
]
|
||||
}
|
||||
],
|
||||
"texturewidth": 16,
|
||||
"textureheight": 16
|
||||
},
|
||||
"frame": {
|
||||
"bones": [
|
||||
{
|
||||
"name": "frame",
|
||||
"parent": "base",
|
||||
"rotation": [0, 180, 0],
|
||||
"pivot": [0, 0, 0],
|
||||
"cubes": [
|
||||
{"origin": [-6, -6, -8], "size": [12, 1, 1], "uv": [2, 2]},
|
||||
{"origin": [-6, 5, -8], "size": [12, 1, 1], "uv": [2, 13]},
|
||||
{"origin": [-6, -5, -8], "size": [1, 10, 1], "uv": [2, 3]},
|
||||
{"origin": [5, -5, -8], "size": [1, 10, 1], "uv": [13, 3]}
|
||||
]
|
||||
}
|
||||
],
|
||||
"texturewidth": 16,
|
||||
"textureheight": 16
|
||||
}
|
||||
},
|
||||
"render_controllers": ["controller.render.item_frame"]
|
||||
},
|
||||
"leash_knot": {
|
||||
"identifier": "minecraft:leash_knot",
|
||||
"materials": {"default": "leash_knot"},
|
||||
|
|
@ -7847,7 +7894,8 @@
|
|||
"bones": [
|
||||
{
|
||||
"name": "knot",
|
||||
"cubes": [{"origin": [-3, 2, -3], "size": [6, 8, 6]}]
|
||||
"rotation": [0, 180, 0],
|
||||
"cubes": [{"origin": [5, 6, 5], "size": [6, 8, 6], "uv": [0, 0]}]
|
||||
}
|
||||
],
|
||||
"texturewidth": 32,
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ export class Viewer {
|
|||
this.threeJsWorld = new WorldRendererThree(this.scene, this.renderer, worldConfig)
|
||||
this.setWorld()
|
||||
this.resetScene()
|
||||
this.entities = new Entities(this.scene)
|
||||
this.entities = new Entities(this)
|
||||
// this.primitives = new Primitives(this.scene, this.camera)
|
||||
|
||||
this.domElement = renderer.domElement
|
||||
|
|
|
|||
|
|
@ -75,6 +75,10 @@ export class WorldDataEmitter extends EventEmitter {
|
|||
this.eventListeners = {
|
||||
// 'move': botPosition,
|
||||
entitySpawn (e: any) {
|
||||
if (e.name === 'item_frame' || e.name === 'glow_item_frame') {
|
||||
// Item frames use block positions in the protocol, not their center. Fix that.
|
||||
e.position.translate(0.5, 0.5, 0.5)
|
||||
}
|
||||
emitEntity(e)
|
||||
},
|
||||
entityUpdate (e: any) {
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ const execAsync = promisify(childProcess.exec)
|
|||
const buildingVersion = new Date().toISOString().split(':')[0]
|
||||
|
||||
const dev = process.env.NODE_ENV === 'development'
|
||||
const disableServiceWorker = process.env.DISABLE_SERVICE_WORKER === 'true'
|
||||
|
||||
let releaseTag
|
||||
let releaseChangelog
|
||||
|
|
@ -59,6 +60,7 @@ const appConfig = defineConfig({
|
|||
'process.env.DEPS_VERSIONS': JSON.stringify({}),
|
||||
'process.env.RELEASE_TAG': JSON.stringify(releaseTag),
|
||||
'process.env.RELEASE_CHANGELOG': JSON.stringify(releaseChangelog),
|
||||
'process.env.DISABLE_SERVICE_WORKER': JSON.stringify(disableServiceWorker),
|
||||
},
|
||||
},
|
||||
server: {
|
||||
|
|
@ -103,6 +105,9 @@ const appConfig = defineConfig({
|
|||
configJson.defaultProxy = ':8080'
|
||||
}
|
||||
fs.writeFileSync('./dist/config.json', JSON.stringify({ ...configJson, ...configLocalJson }), 'utf8')
|
||||
if (fs.existsSync('./generated/sounds.js')) {
|
||||
fs.copyFileSync('./generated/sounds.js', './dist/sounds.js')
|
||||
}
|
||||
// childProcess.execSync('./scripts/prepareSounds.mjs', { stdio: 'inherit' })
|
||||
// childProcess.execSync('tsx ./scripts/genMcDataTypes.ts', { stdio: 'inherit' })
|
||||
// childProcess.execSync('tsx ./scripts/genPixelartTypes.ts', { stdio: 'inherit' })
|
||||
|
|
@ -121,15 +126,17 @@ const appConfig = defineConfig({
|
|||
prep()
|
||||
})
|
||||
build.onAfterBuild(async () => {
|
||||
const { count, size, warnings } = await generateSW({
|
||||
// dontCacheBustURLsMatching: [new RegExp('...')],
|
||||
globDirectory: 'dist',
|
||||
skipWaiting: true,
|
||||
clientsClaim: true,
|
||||
additionalManifestEntries: getSwAdditionalEntries(),
|
||||
globPatterns: [],
|
||||
swDest: './dist/service-worker.js',
|
||||
})
|
||||
if (!disableServiceWorker) {
|
||||
const { count, size, warnings } = await generateSW({
|
||||
// dontCacheBustURLsMatching: [new RegExp('...')],
|
||||
globDirectory: 'dist',
|
||||
skipWaiting: true,
|
||||
clientsClaim: true,
|
||||
additionalManifestEntries: getSwAdditionalEntries(),
|
||||
globPatterns: [],
|
||||
swDest: './dist/service-worker.js',
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
build.onBeforeStartDevServer(() => prep())
|
||||
|
|
|
|||
|
|
@ -1,9 +1,12 @@
|
|||
import fs from 'fs'
|
||||
|
||||
const url = 'https://github.com/zardoy/minecraft-web-client/raw/sounds-generated/sounds.js'
|
||||
const savePath = 'dist/sounds.js'
|
||||
const url = 'https://github.com/zardoy/minecraft-web-client/raw/sounds-generated/sounds-v2.js'
|
||||
fetch(url).then(res => res.text()).then(data => {
|
||||
fs.writeFileSync(savePath, data, 'utf8')
|
||||
if (fs.existsSync('./dist')) {
|
||||
fs.writeFileSync('./dist/sounds.js', data, 'utf8')
|
||||
}
|
||||
fs.mkdirSync('./generated', { recursive: true })
|
||||
fs.writeFileSync('./generated/sounds.js', data, 'utf8')
|
||||
if (fs.existsSync('.vercel/output/static/')) {
|
||||
fs.writeFileSync('.vercel/output/static/sounds.js', data, 'utf8')
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,26 +10,31 @@ import { build } from 'esbuild'
|
|||
|
||||
const __dirname = path.dirname(fileURLToPath(new URL(import.meta.url)))
|
||||
|
||||
const targetedVersions = ['1.20.1', '1.19.2', '1.18.2', '1.17.1', '1.16.5', '1.15.2', '1.14.4', '1.13.2', '1.12.2', '1.11.2', '1.10.2', '1.9.4', '1.8.9']
|
||||
const targetedVersions = ['1.21.1', '1.20.6', '1.20.1', '1.19.2', '1.18.2', '1.17.1', '1.16.5', '1.15.2', '1.14.4', '1.13.2', '1.12.2', '1.11.2', '1.10.2', '1.9.4', '1.8.9']
|
||||
|
||||
/** @type {{name, size, hash}[]} */
|
||||
let prevSounds = null
|
||||
|
||||
const burgerDataUrl = (version) => `https://raw.githubusercontent.com/Pokechu22/Burger/gh-pages/${version}.json`
|
||||
const burgerDataPath = './generated/burger.json'
|
||||
const EXISTING_CACHE_PATH = './generated/existing-sounds-cache.json'
|
||||
|
||||
// const perVersionData: Record<string, { removed: string[],
|
||||
|
||||
const soundsPathVersionsRemap = {}
|
||||
|
||||
const downloadAllSounds = async () => {
|
||||
const downloadAllSoundsAndCreateMap = async () => {
|
||||
let existingSoundsCache = {}
|
||||
try {
|
||||
existingSoundsCache = JSON.parse(await fs.promises.readFile(EXISTING_CACHE_PATH, 'utf8'))
|
||||
} catch (err) {}
|
||||
const { versions } = await getVersionList()
|
||||
const lastVersion = versions.filter(version => !version.id.includes('w'))[0]
|
||||
// if (lastVersion.id !== targetedVersions[0]) throw new Error('last version is not the same as targetedVersions[0], update')
|
||||
for (const targetedVersion of targetedVersions) {
|
||||
const versionData = versions.find(x => x.id === targetedVersion)
|
||||
if (!versionData) throw new Error('no version data for ' + targetedVersion)
|
||||
console.log('Getting assets for version', targetedVersion)
|
||||
for (const version of targetedVersions) {
|
||||
const versionData = versions.find(x => x.id === version)
|
||||
if (!versionData) throw new Error('no version data for ' + version)
|
||||
console.log('Getting assets for version', version)
|
||||
const { assetIndex } = await fetch(versionData.url).then((r) => r.json())
|
||||
/** @type {{objects: {[a: string]: { size, hash }}}} */
|
||||
const index = await fetch(assetIndex.url).then((r) => r.json())
|
||||
|
|
@ -45,26 +50,30 @@ const downloadAllSounds = async () => {
|
|||
const changedSize = soundAssets.filter(x => prevSoundNames.has(x.name) && prevSounds.find(y => y.name === x.name).size !== x.size)
|
||||
console.log('changed size', changedSize.map(x => ({ name: x.name, prev: prevSounds.find(y => y.name === x.name).size, curr: x.size })))
|
||||
if (addedSounds.length || changedSize.length) {
|
||||
soundsPathVersionsRemap[targetedVersion] = [...addedSounds, ...changedSize].map(x => x.name.replace('minecraft/sounds/', '').replace('.ogg', ''))
|
||||
soundsPathVersionsRemap[version] = [...addedSounds, ...changedSize].map(x => x.name.replace('minecraft/sounds/', '').replace('.ogg', ''))
|
||||
}
|
||||
if (addedSounds.length) {
|
||||
console.log('downloading new sounds for version', targetedVersion)
|
||||
downloadSounds(addedSounds, targetedVersion + '/')
|
||||
console.log('downloading new sounds for version', version)
|
||||
downloadSounds(version, addedSounds, version + '/')
|
||||
}
|
||||
if (changedSize.length) {
|
||||
console.log('downloading changed sounds for version', targetedVersion)
|
||||
downloadSounds(changedSize, targetedVersion + '/')
|
||||
console.log('downloading changed sounds for version', version)
|
||||
downloadSounds(version, changedSize, version + '/')
|
||||
}
|
||||
} else {
|
||||
console.log('downloading sounds for version', targetedVersion)
|
||||
downloadSounds(soundAssets)
|
||||
console.log('downloading sounds for version', version)
|
||||
downloadSounds(version, soundAssets)
|
||||
}
|
||||
prevSounds = soundAssets
|
||||
}
|
||||
async function downloadSound({ name, hash, size }, namePath, log) {
|
||||
const cached =
|
||||
!!namePath.replace('.ogg', '.mp3').split('/').reduce((acc, cur) => acc?.[cur], existingSoundsCache.sounds) ||
|
||||
!!namePath.replace('.ogg', '.ogg').split('/').reduce((acc, cur) => acc?.[cur], existingSoundsCache.sounds)
|
||||
const savePath = path.resolve(`generated/sounds/${namePath}`)
|
||||
if (fs.existsSync(savePath)) {
|
||||
if (cached || fs.existsSync(savePath)) {
|
||||
// console.log('skipped', name)
|
||||
existingSoundsCache.sounds[namePath] = true
|
||||
return
|
||||
}
|
||||
log()
|
||||
|
|
@ -86,7 +95,12 @@ const downloadAllSounds = async () => {
|
|||
}
|
||||
writer.close()
|
||||
}
|
||||
async function downloadSounds(assets, addPath = '') {
|
||||
async function downloadSounds(version, assets, addPath = '') {
|
||||
if (addPath && existingSoundsCache.sounds[version]) {
|
||||
console.log('using existing sounds for version', version)
|
||||
return
|
||||
}
|
||||
console.log(version, 'have to download', assets.length, 'sounds')
|
||||
for (let i = 0; i < assets.length; i += 5) {
|
||||
await Promise.all(assets.slice(i, i + 5).map((asset, j) => downloadSound(asset, `${addPath}${asset.name}`, () => {
|
||||
console.log('downloading', addPath, asset.name, i + j, '/', assets.length)
|
||||
|
|
@ -95,6 +109,7 @@ const downloadAllSounds = async () => {
|
|||
}
|
||||
|
||||
fs.writeFileSync('./generated/soundsPathVersionsRemap.json', JSON.stringify(soundsPathVersionsRemap), 'utf8')
|
||||
fs.writeFileSync(EXISTING_CACHE_PATH, JSON.stringify(existingSoundsCache), 'utf8')
|
||||
}
|
||||
|
||||
const lightpackOverrideSounds = {
|
||||
|
|
@ -106,7 +121,8 @@ const lightpackOverrideSounds = {
|
|||
// this is not done yet, will be used to select only sounds for bundle (most important ones)
|
||||
const isSoundWhitelisted = (name) => name.startsWith('random/') || name.startsWith('note/') || name.endsWith('/say1') || name.endsWith('/death') || (name.startsWith('mob/') && name.endsWith('/step1')) || name.endsWith('/swoop1') || /* name.endsWith('/break1') || */ name.endsWith('dig/stone1')
|
||||
|
||||
const ffmpeg = 'C:/Users/Vitaly/Documents/LosslessCut-win-x64/resources/ffmpeg.exe' // will be ffmpeg-static
|
||||
// const ffmpeg = 'C:/Users/Vitaly/Documents/LosslessCut-win-x64/resources/ffmpeg.exe' // can be ffmpeg-static
|
||||
const ffmpegExec = 'ffmpeg'
|
||||
const maintainBitrate = true
|
||||
|
||||
const scanFilesDeep = async (root, onOggFile) => {
|
||||
|
|
@ -127,7 +143,7 @@ const convertSounds = async () => {
|
|||
})
|
||||
|
||||
const convertSound = async (i) => {
|
||||
const proc = promisify(exec)(`${ffmpeg} -i "${toConvert[i]}" -y -codec:a libmp3lame ${maintainBitrate ? '-qscale:a 2' : ''} "${toConvert[i].replace('.ogg', '.mp3')}"`)
|
||||
const proc = promisify(exec)(`${ffmpegExec} -i "${toConvert[i]}" -y -codec:a libmp3lame ${maintainBitrate ? '-qscale:a 2' : ''} "${toConvert[i].replace('.ogg', '.mp3')}"`)
|
||||
// pipe stdout to the console
|
||||
proc.child.stdout.pipe(process.stdout)
|
||||
await proc
|
||||
|
|
@ -147,8 +163,8 @@ const getSoundsMap = (burgerData) => {
|
|||
}
|
||||
|
||||
const writeSoundsMap = async () => {
|
||||
// const burgerData = await fetch(burgerDataUrl(targetedVersions[0])).then((r) => r.json())
|
||||
// fs.writeFileSync(burgerDataPath, JSON.stringify(burgerData[0].sounds), 'utf8')
|
||||
const burgerData = await fetch(burgerDataUrl(targetedVersions[0])).then((r) => r.json())
|
||||
fs.writeFileSync(burgerDataPath, JSON.stringify(burgerData[0].sounds), 'utf8')
|
||||
|
||||
const allSoundsMapOutput = {}
|
||||
let prevMap
|
||||
|
|
@ -174,16 +190,22 @@ const writeSoundsMap = async () => {
|
|||
// const includeSound = isSoundWhitelisted(firstName)
|
||||
// if (!includeSound) continue
|
||||
const mostUsedSound = sounds.sort((a, b) => b.weight - a.weight)[0]
|
||||
const targetSound = sounds[0]
|
||||
// outputMap[id] = { subtitle, sounds: mostUsedSound }
|
||||
// outputMap[id] = { subtitle, sounds }
|
||||
const soundFilePath = `generated/sounds/minecraft/sounds/${targetSound.name}.mp3`
|
||||
// const soundFilePath = `generated/sounds/minecraft/sounds/${targetSound.name}.mp3`
|
||||
// if (!fs.existsSync(soundFilePath)) {
|
||||
// console.warn('no sound file', targetSound.name)
|
||||
// continue
|
||||
// }
|
||||
let outputUseSoundLine = []
|
||||
const minWeight = sounds.reduce((acc, cur) => cur.weight ? Math.min(acc, cur.weight) : acc, sounds[0].weight ?? 1)
|
||||
if (isNaN(minWeight)) debugger
|
||||
for (const sound of sounds) {
|
||||
if (sound.weight && isNaN(sound.weight)) debugger
|
||||
outputUseSoundLine.push(`${sound.volume ?? 1};${sound.name};${sound.weight ?? minWeight}`)
|
||||
}
|
||||
const key = `${id};${name}`
|
||||
outputIdMap[key] = `${targetSound.volume ?? 1};${targetSound.name}`
|
||||
outputIdMap[key] = outputUseSoundLine.join(',')
|
||||
if (prevMap && prevMap[key]) {
|
||||
keysStats.same++
|
||||
} else {
|
||||
|
|
@ -221,7 +243,7 @@ const makeSoundsBundle = async () => {
|
|||
|
||||
const allSoundsMeta = {
|
||||
format: 'mp3',
|
||||
baseUrl: 'https://raw.githubusercontent.com/zardoy/minecraft-web-client/sounds-generated/sounds/'
|
||||
baseUrl: `https://raw.githubusercontent.com/${process.env.REPO_SLUG}/sounds/sounds/`
|
||||
}
|
||||
|
||||
await build({
|
||||
|
|
@ -235,9 +257,25 @@ const makeSoundsBundle = async () => {
|
|||
},
|
||||
metafile: true,
|
||||
})
|
||||
// copy also to generated/sounds.js
|
||||
fs.copyFileSync('./dist/sounds.js', './generated/sounds.js')
|
||||
}
|
||||
|
||||
// downloadAllSounds()
|
||||
// convertSounds()
|
||||
// writeSoundsMap()
|
||||
// makeSoundsBundle()
|
||||
const action = process.argv[2]
|
||||
if (action) {
|
||||
const execFn = {
|
||||
download: downloadAllSoundsAndCreateMap,
|
||||
convert: convertSounds,
|
||||
write: writeSoundsMap,
|
||||
bundle: makeSoundsBundle,
|
||||
}[action]
|
||||
|
||||
if (execFn) {
|
||||
execFn()
|
||||
}
|
||||
} else {
|
||||
// downloadAllSoundsAndCreateMap()
|
||||
// convertSounds()
|
||||
// writeSoundsMap()
|
||||
makeSoundsBundle()
|
||||
}
|
||||
|
|
|
|||
109
scripts/uploadSoundFiles.ts
Normal file
109
scripts/uploadSoundFiles.ts
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
import fetch from 'node-fetch';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { glob } from 'glob';
|
||||
|
||||
// Git details
|
||||
const REPO_SLUG = process.env.REPO_SLUG;
|
||||
const owner = REPO_SLUG.split('/')[0];
|
||||
const repo = REPO_SLUG.split('/')[1];
|
||||
const branch = "sounds";
|
||||
|
||||
// GitHub token for authentication
|
||||
const token = process.env.GITHUB_TOKEN;
|
||||
|
||||
// GitHub API endpoint
|
||||
const baseUrl = `https://api.github.com/repos/${owner}/${repo}/contents`;
|
||||
|
||||
const headers = {
|
||||
Authorization: `token ${token}`,
|
||||
'Content-Type': 'application/json'
|
||||
};
|
||||
|
||||
async function getShaForExistingFile(repoFilePath: string): Promise<string | null> {
|
||||
const url = `${baseUrl}/${repoFilePath}?ref=${branch}`;
|
||||
const response = await fetch(url, { headers });
|
||||
if (response.status === 404) {
|
||||
return null; // File does not exist
|
||||
}
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch ${url}: ${response.statusText}`);
|
||||
}
|
||||
const data = await response.json();
|
||||
return data.sha;
|
||||
}
|
||||
|
||||
async function uploadFiles() {
|
||||
const commitMessage = "Upload multiple files via script";
|
||||
const committer = {
|
||||
name: "GitHub",
|
||||
email: "noreply@github.com"
|
||||
};
|
||||
|
||||
const filesToUpload = glob.sync("generated/sounds/**/*.mp3").map(localPath => {
|
||||
const repoPath = localPath.replace(/^generated\//, '');
|
||||
return { localPath, repoPath };
|
||||
});
|
||||
|
||||
const files = await Promise.all(filesToUpload.map(async file => {
|
||||
const content = fs.readFileSync(file.localPath, 'base64');
|
||||
const sha = await getShaForExistingFile(file.repoPath);
|
||||
return {
|
||||
path: file.repoPath,
|
||||
mode: "100644",
|
||||
type: "blob",
|
||||
sha: sha || undefined,
|
||||
content: content
|
||||
};
|
||||
}));
|
||||
|
||||
const treeResponse = await fetch(`${baseUrl}/git/trees`, {
|
||||
method: 'POST',
|
||||
headers: headers,
|
||||
body: JSON.stringify({
|
||||
base_tree: null,
|
||||
tree: files
|
||||
})
|
||||
});
|
||||
|
||||
if (!treeResponse.ok) {
|
||||
throw new Error(`Failed to create tree: ${treeResponse.statusText}`);
|
||||
}
|
||||
|
||||
const treeData = await treeResponse.json();
|
||||
|
||||
const commitResponse = await fetch(`${baseUrl}/git/commits`, {
|
||||
method: 'POST',
|
||||
headers: headers,
|
||||
body: JSON.stringify({
|
||||
message: commitMessage,
|
||||
tree: treeData.sha,
|
||||
parents: [branch],
|
||||
committer: committer
|
||||
})
|
||||
});
|
||||
|
||||
if (!commitResponse.ok) {
|
||||
throw new Error(`Failed to create commit: ${commitResponse.statusText}`);
|
||||
}
|
||||
|
||||
const commitData = await commitResponse.json();
|
||||
|
||||
const updateRefResponse = await fetch(`${baseUrl}/git/refs/heads/${branch}`, {
|
||||
method: 'PATCH',
|
||||
headers: headers,
|
||||
body: JSON.stringify({
|
||||
sha: commitData.sha
|
||||
})
|
||||
});
|
||||
|
||||
if (!updateRefResponse.ok) {
|
||||
throw new Error(`Failed to update ref: ${updateRefResponse.statusText}`);
|
||||
}
|
||||
|
||||
console.log("Files uploaded successfully");
|
||||
}
|
||||
|
||||
uploadFiles().catch(error => {
|
||||
console.error("Error uploading files:", error);
|
||||
});
|
||||
67
scripts/uploadSounds.ts
Normal file
67
scripts/uploadSounds.ts
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
import fs from 'fs'
|
||||
|
||||
// GitHub details
|
||||
const owner = "zardoy";
|
||||
const repo = "minecraft-web-client";
|
||||
const branch = "sounds-generated";
|
||||
const filePath = "dist/sounds.js"; // Local file path
|
||||
const repoFilePath = "sounds-v2.js"; // Path in the repo
|
||||
|
||||
// GitHub token for authentication
|
||||
const token = process.env.GITHUB_TOKEN;
|
||||
|
||||
// GitHub API endpoint
|
||||
const baseUrl = `https://api.github.com/repos/${owner}/${repo}/contents/${repoFilePath}`;
|
||||
|
||||
const headers = {
|
||||
Authorization: `token ${token}`,
|
||||
'Content-Type': 'application/json'
|
||||
};
|
||||
|
||||
async function getShaForExistingFile(): Promise<string | null> {
|
||||
const url = `${baseUrl}?ref=${branch}`;
|
||||
const response = await fetch(url, { headers });
|
||||
if (response.status === 404) {
|
||||
return null; // File does not exist
|
||||
}
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch ${url}: ${response.statusText}`);
|
||||
}
|
||||
const data = await response.json();
|
||||
return data.sha;
|
||||
}
|
||||
|
||||
async function uploadFile() {
|
||||
const content = fs.readFileSync(filePath, 'utf8');
|
||||
const base64Content = Buffer.from(content).toString('base64');
|
||||
const sha = await getShaForExistingFile();
|
||||
console.log('got sha')
|
||||
|
||||
const body = {
|
||||
message: "Update sounds.js",
|
||||
content: base64Content,
|
||||
branch: branch,
|
||||
committer: {
|
||||
name: "GitHub",
|
||||
email: "noreply@github.com"
|
||||
},
|
||||
sha: sha || undefined
|
||||
};
|
||||
|
||||
const response = await fetch(baseUrl, {
|
||||
method: 'PUT',
|
||||
headers: headers,
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to upload file: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const responseData = await response.json();
|
||||
console.log("File uploaded successfully:", responseData);
|
||||
}
|
||||
|
||||
uploadFile().catch(error => {
|
||||
console.error("Error uploading file:", error);
|
||||
});
|
||||
10
server.js
10
server.js
|
|
@ -26,6 +26,7 @@ if (!isProd) {
|
|||
app.get('/config.json', (req, res, next) => {
|
||||
// read original file config
|
||||
let config = {}
|
||||
let publicConfig = {}
|
||||
try {
|
||||
config = require('./config.json')
|
||||
} catch {
|
||||
|
|
@ -33,9 +34,13 @@ app.get('/config.json', (req, res, next) => {
|
|||
config = require('./dist/config.json')
|
||||
} catch { }
|
||||
}
|
||||
try {
|
||||
publicConfig = require('./public/config.json')
|
||||
} catch { }
|
||||
res.json({
|
||||
...config,
|
||||
'defaultProxy': '', // use current url (this server)
|
||||
...publicConfig,
|
||||
})
|
||||
})
|
||||
if (isProd) {
|
||||
|
|
@ -45,6 +50,11 @@ if (isProd) {
|
|||
res.setHeader('Cross-Origin-Embedder-Policy', 'require-corp')
|
||||
next()
|
||||
})
|
||||
|
||||
// First serve from the override directory (volume mount)
|
||||
app.use(express.static(path.join(__dirname, './public')))
|
||||
|
||||
// Then fallback to the original dist directory
|
||||
app.use(express.static(path.join(__dirname, './dist')))
|
||||
}
|
||||
|
||||
|
|
|
|||
54
src/api/mcStatusApi.ts
Normal file
54
src/api/mcStatusApi.ts
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
export const isServerValid = (ip: string) => {
|
||||
const isInLocalNetwork = ip.startsWith('192.168.') ||
|
||||
ip.startsWith('10.') ||
|
||||
ip.startsWith('172.') ||
|
||||
ip.startsWith('127.') ||
|
||||
ip.startsWith('localhost') ||
|
||||
ip.startsWith(':')
|
||||
const VALID_IP_OR_DOMAIN = ip.includes('.')
|
||||
|
||||
return !isInLocalNetwork && VALID_IP_OR_DOMAIN
|
||||
}
|
||||
|
||||
export async function fetchServerStatus (ip: string, signal?: AbortSignal) {
|
||||
if (!isServerValid(ip)) return
|
||||
|
||||
const response = await fetch(`https://api.mcstatus.io/v2/status/java/${ip}`, { signal })
|
||||
const data: ServerResponse = await response.json()
|
||||
const versionClean = data.version?.name_raw.replace(/^[^\d.]+/, '')
|
||||
|
||||
return {
|
||||
formattedText: data.motd?.raw ?? '',
|
||||
textNameRight: data.online ?
|
||||
`${versionClean} ${data.players?.online ?? '??'}/${data.players?.max ?? '??'}` :
|
||||
'',
|
||||
icon: data.icon,
|
||||
offline: !data.online,
|
||||
raw: data
|
||||
}
|
||||
}
|
||||
|
||||
export type ServerResponse = {
|
||||
online: boolean
|
||||
version?: {
|
||||
name_raw: string
|
||||
}
|
||||
// display tooltip
|
||||
players?: {
|
||||
online: number
|
||||
max: number
|
||||
list: Array<{
|
||||
name_raw: string
|
||||
name_clean: string
|
||||
}>
|
||||
}
|
||||
icon?: string
|
||||
motd?: {
|
||||
raw: string
|
||||
}
|
||||
// todo circle error icon
|
||||
mods?: Array<{ name: string, version: string }>
|
||||
// todo display via hammer icon
|
||||
software?: string
|
||||
plugins?: Array<{ name, version }>
|
||||
}
|
||||
78
src/appParams.ts
Normal file
78
src/appParams.ts
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
const qsParams = new URLSearchParams(window.location.search)
|
||||
|
||||
export type AppQsParams = {
|
||||
// AddServerOrConnect.tsx params
|
||||
ip?: string
|
||||
name?: string
|
||||
version?: string
|
||||
proxy?: string
|
||||
username?: string
|
||||
lockConnect?: string
|
||||
autoConnect?: string
|
||||
// googledrive.ts params
|
||||
state?: string
|
||||
// ServersListProvider.tsx params
|
||||
serversList?: string
|
||||
// Map and texture params
|
||||
texturepack?: string
|
||||
map?: string
|
||||
mapDirBaseUrl?: string
|
||||
mapDirGuess?: string
|
||||
// Singleplayer params
|
||||
singleplayer?: string
|
||||
sp?: string
|
||||
loadSave?: string
|
||||
// Server params
|
||||
reconnect?: string
|
||||
server?: string
|
||||
// Peer connection params
|
||||
connectPeer?: string
|
||||
peerVersion?: string
|
||||
// UI params
|
||||
modal?: string
|
||||
viewerConnect?: string
|
||||
// Map version param
|
||||
mapVersion?: string
|
||||
// Command params
|
||||
command?: string
|
||||
// Misc params
|
||||
suggest_save?: string
|
||||
scene?: string
|
||||
}
|
||||
|
||||
export type AppQsParamsArray = {
|
||||
mapDir?: string[]
|
||||
setting?: string[]
|
||||
serverSetting?: string[]
|
||||
command?: string[]
|
||||
}
|
||||
|
||||
type AppQsParamsArrayTransformed = {
|
||||
[k in keyof AppQsParamsArray]: string[]
|
||||
}
|
||||
|
||||
export const appQueryParams = new Proxy<AppQsParams>({} as AppQsParams, {
|
||||
get (target, property) {
|
||||
if (typeof property !== 'string') {
|
||||
return null
|
||||
}
|
||||
return qsParams.get(property)
|
||||
},
|
||||
})
|
||||
|
||||
export const appQueryParamsArray = new Proxy({} as AppQsParamsArrayTransformed, {
|
||||
get (target, property) {
|
||||
if (typeof property !== 'string') {
|
||||
return null
|
||||
}
|
||||
return qsParams.getAll(property)
|
||||
},
|
||||
})
|
||||
|
||||
// Helper function to check if a specific query parameter exists
|
||||
export const hasQueryParam = (param: keyof AppQsParams) => qsParams.has(param)
|
||||
|
||||
// Helper function to get all query parameters as a URLSearchParams object
|
||||
export const getRawQueryParams = () => qsParams;
|
||||
|
||||
(globalThis as any).debugQueryParams = Object.fromEntries(qsParams.entries())
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
import { subscribeKey } from 'valtio/utils'
|
||||
import { options } from './optionsStorage'
|
||||
import { isCypress } from './standaloneUtils'
|
||||
import { reportWarningOnce } from './utils'
|
||||
|
|
@ -5,9 +6,14 @@ import { reportWarningOnce } from './utils'
|
|||
let audioContext: AudioContext
|
||||
const sounds: Record<string, any> = {}
|
||||
|
||||
// Track currently playing sounds and their gain nodes
|
||||
const activeSounds: Array<{ source: AudioBufferSourceNode; gainNode: GainNode; volumeMultiplier: number }> = []
|
||||
window.activeSounds = activeSounds
|
||||
|
||||
// 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
|
||||
const loadingSounds = [] as string[]
|
||||
const convertedSounds = [] as string[]
|
||||
|
||||
export async function loadSound (path: string, contents = path) {
|
||||
if (loadingSounds.includes(path)) return true
|
||||
loadingSounds.push(path)
|
||||
|
|
@ -24,15 +30,15 @@ export async function loadSound (path: string, contents = path) {
|
|||
loadingSounds.splice(loadingSounds.indexOf(path), 1)
|
||||
}
|
||||
|
||||
export const loadOrPlaySound = async (url, soundVolume = 1) => {
|
||||
export const loadOrPlaySound = async (url, soundVolume = 1, loadTimeout = 500) => {
|
||||
const soundBuffer = sounds[url]
|
||||
if (!soundBuffer) {
|
||||
const start = Date.now()
|
||||
const cancelled = await loadSound(url)
|
||||
if (cancelled || Date.now() - start > 500) return
|
||||
if (cancelled || Date.now() - start > loadTimeout) return
|
||||
}
|
||||
|
||||
await playSound(url)
|
||||
return playSound(url, soundVolume)
|
||||
}
|
||||
|
||||
export async function playSound (url, soundVolume = 1) {
|
||||
|
|
@ -49,6 +55,7 @@ export async function playSound (url, soundVolume = 1) {
|
|||
|
||||
for (const [soundName, sound] of Object.entries(sounds)) {
|
||||
if (convertedSounds.includes(soundName)) continue
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
sounds[soundName] = await audioContext.decodeAudioData(sound)
|
||||
convertedSounds.push(soundName)
|
||||
}
|
||||
|
|
@ -66,4 +73,51 @@ export async function playSound (url, soundVolume = 1) {
|
|||
gainNode.connect(audioContext.destination)
|
||||
gainNode.gain.value = volume
|
||||
source.start(0)
|
||||
|
||||
// Add to active sounds
|
||||
activeSounds.push({ source, gainNode, volumeMultiplier: soundVolume })
|
||||
|
||||
const callbacks = [] as Array<() => void>
|
||||
source.onended = () => {
|
||||
// Remove from active sounds when finished
|
||||
const index = activeSounds.findIndex(s => s.source === source)
|
||||
if (index !== -1) activeSounds.splice(index, 1)
|
||||
|
||||
for (const callback of callbacks) {
|
||||
callback()
|
||||
}
|
||||
callbacks.length = 0
|
||||
}
|
||||
|
||||
return {
|
||||
onEnded (callback: () => void) {
|
||||
callbacks.push(callback)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export function stopAllSounds () {
|
||||
for (const { source } of activeSounds) {
|
||||
try {
|
||||
source.stop()
|
||||
} catch (err) {
|
||||
console.warn('Failed to stop sound:', err)
|
||||
}
|
||||
}
|
||||
activeSounds.length = 0
|
||||
}
|
||||
|
||||
export function changeVolumeOfCurrentlyPlayingSounds (newVolume: number) {
|
||||
const normalizedVolume = newVolume / 100
|
||||
for (const { gainNode, volumeMultiplier } of activeSounds) {
|
||||
try {
|
||||
gainNode.gain.value = normalizedVolume * volumeMultiplier
|
||||
} catch (err) {
|
||||
console.warn('Failed to change sound volume:', err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
subscribeKey(options, 'volume', () => {
|
||||
changeVolumeOfCurrentlyPlayingSounds(options.volume)
|
||||
})
|
||||
|
|
|
|||
80
src/cameraRotationControls.ts
Normal file
80
src/cameraRotationControls.ts
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
import { contro } from './controls'
|
||||
import { activeModalStack, isGameActive, miscUiState, showModal } from './globalState'
|
||||
import { options } from './optionsStorage'
|
||||
import { hideNotification, notificationProxy } from './react/NotificationProvider'
|
||||
import { pointerLock } from './utils'
|
||||
import worldInteractions from './worldInteractions'
|
||||
|
||||
let lastMouseMove: number
|
||||
|
||||
export const updateCursor = () => {
|
||||
worldInteractions.update()
|
||||
}
|
||||
|
||||
export type CameraMoveEvent = {
|
||||
movementX: number
|
||||
movementY: number
|
||||
type: string
|
||||
stopPropagation?: () => void
|
||||
}
|
||||
|
||||
export function onCameraMove (e: MouseEvent | CameraMoveEvent) {
|
||||
if (!isGameActive(true)) return
|
||||
if (e.type === 'mousemove' && !document.pointerLockElement) return
|
||||
e.stopPropagation?.()
|
||||
const now = performance.now()
|
||||
// todo: limit camera movement for now to avoid unexpected jumps
|
||||
if (now - lastMouseMove < 4) return
|
||||
lastMouseMove = now
|
||||
let { mouseSensX, mouseSensY } = options
|
||||
if (mouseSensY === -1) mouseSensY = mouseSensX
|
||||
moveCameraRawHandler({
|
||||
x: e.movementX * mouseSensX * 0.0001,
|
||||
y: e.movementY * mouseSensY * 0.0001
|
||||
})
|
||||
updateCursor()
|
||||
}
|
||||
|
||||
export const moveCameraRawHandler = ({ x, y }: { x: number; y: number }) => {
|
||||
const maxPitch = 0.5 * Math.PI
|
||||
const minPitch = -0.5 * Math.PI
|
||||
|
||||
viewer.world.lastCamUpdate = Date.now()
|
||||
if (!bot?.entity) return
|
||||
const pitch = bot.entity.pitch - y
|
||||
void bot.look(bot.entity.yaw - x, Math.max(minPitch, Math.min(maxPitch, pitch)), true)
|
||||
}
|
||||
|
||||
|
||||
window.addEventListener('mousemove', (e: MouseEvent) => {
|
||||
onCameraMove(e)
|
||||
}, { capture: true })
|
||||
|
||||
export const onControInit = () => {
|
||||
contro.on('stickMovement', ({ stick, vector }) => {
|
||||
if (!isGameActive(true)) return
|
||||
if (stick !== 'right') return
|
||||
let { x, z } = vector
|
||||
if (Math.abs(x) < 0.18) x = 0
|
||||
if (Math.abs(z) < 0.18) z = 0
|
||||
onCameraMove({
|
||||
movementX: x * 10,
|
||||
movementY: z * 10,
|
||||
type: 'stickMovement',
|
||||
stopPropagation () {}
|
||||
} as CameraMoveEvent)
|
||||
miscUiState.usingGamepadInput = true
|
||||
})
|
||||
}
|
||||
|
||||
function pointerLockChangeCallback () {
|
||||
if (notificationProxy.id === 'pointerlockchange') {
|
||||
hideNotification()
|
||||
}
|
||||
if (viewer.renderer.xr.isPresenting) return // todo
|
||||
if (!pointerLock.hasPointerLock && activeModalStack.length === 0) {
|
||||
showModal({ reactType: 'pause-screen' })
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('pointerlockchange', pointerLockChangeCallback, false)
|
||||
|
|
@ -2,7 +2,7 @@ import { versionsByMinecraftVersion } from 'minecraft-data'
|
|||
import minecraftInitialDataJson from '../generated/minecraft-initial-data.json'
|
||||
import { AuthenticatedAccount } from './react/ServersListProvider'
|
||||
import { setLoadingScreenStatus } from './utils'
|
||||
import { downloadSoundsIfNeeded } from './soundSystem'
|
||||
import { downloadSoundsIfNeeded } from './sounds/botSoundSystem'
|
||||
import { miscUiState } from './globalState'
|
||||
|
||||
export type ConnectOptions = {
|
||||
|
|
|
|||
133
src/controls.ts
133
src/controls.ts
|
|
@ -7,7 +7,7 @@ import { ControMax } from 'contro-max/build/controMax'
|
|||
import { CommandEventArgument, SchemaCommandInput } from 'contro-max/build/types'
|
||||
import { stringStartsWith } from 'contro-max/build/stringUtils'
|
||||
import { UserOverrideCommand, UserOverridesConfig } from 'contro-max/build/types/store'
|
||||
import { isGameActive, showModal, gameAdditionalState, activeModalStack, hideCurrentModal, miscUiState, loadedGameState, hideModal } from './globalState'
|
||||
import { isGameActive, showModal, gameAdditionalState, activeModalStack, hideCurrentModal, miscUiState, hideModal, hideAllModals } from './globalState'
|
||||
import { goFullscreen, pointerLock, reloadChunks } from './utils'
|
||||
import { options } from './optionsStorage'
|
||||
import { openPlayerInventory } from './inventoryWindows'
|
||||
|
|
@ -19,9 +19,10 @@ import { showOptionsModal } from './react/SelectOption'
|
|||
import widgets from './react/widgets'
|
||||
import { getItemFromBlock } from './chatUtils'
|
||||
import { gamepadUiCursorState, moveGamepadCursorByPx } from './react/GamepadUiCursor'
|
||||
import { completeTexturePackInstall, resourcePackState } from './resourcePack'
|
||||
import { completeTexturePackInstall, copyServerResourcePackToRegular, resourcePackState } from './resourcePack'
|
||||
import { showNotification } from './react/NotificationProvider'
|
||||
import { lastConnectOptions } from './react/AppStatusProvider'
|
||||
import { onCameraMove, onControInit } from './cameraRotationControls'
|
||||
|
||||
|
||||
export const customKeymaps = proxy(JSON.parse(localStorage.keymap || '{}')) as UserOverridesConfig
|
||||
|
|
@ -50,7 +51,11 @@ export const contro = new ControMax({
|
|||
command: ['Slash'],
|
||||
swapHands: ['KeyF'],
|
||||
zoom: ['KeyC'],
|
||||
selectItem: ['KeyH'] // default will be removed
|
||||
selectItem: ['KeyH'], // default will be removed
|
||||
rotateCameraLeft: ['ArrowLeft'],
|
||||
rotateCameraRight: ['ArrowRight'],
|
||||
rotateCameraUp: ['ArrowUp'],
|
||||
rotateCameraDown: ['ArrowDown']
|
||||
},
|
||||
ui: {
|
||||
toggleFullscreen: ['F11'],
|
||||
|
|
@ -92,6 +97,8 @@ export const contro = new ControMax({
|
|||
window.controMax = contro
|
||||
export type Command = CommandEventArgument<typeof contro['_commandsRaw']>['command']
|
||||
|
||||
onControInit()
|
||||
|
||||
updateBinds(customKeymaps)
|
||||
|
||||
const updateDoPreventDefault = () => {
|
||||
|
|
@ -245,6 +252,73 @@ const inModalCommand = (command: Command, pressed: boolean) => {
|
|||
}
|
||||
}
|
||||
|
||||
// Camera rotation controls
|
||||
const cameraRotationControls = {
|
||||
activeDirections: new Set<'left' | 'right' | 'up' | 'down'>(),
|
||||
interval: null as ReturnType<typeof setInterval> | null,
|
||||
config: {
|
||||
speed: 1, // movement per interval
|
||||
interval: 5 // ms between movements
|
||||
},
|
||||
movements: {
|
||||
left: { movementX: -0.5, movementY: 0 },
|
||||
right: { movementX: 0.5, movementY: 0 },
|
||||
up: { movementX: 0, movementY: -0.5 },
|
||||
down: { movementX: 0, movementY: 0.5 }
|
||||
},
|
||||
updateMovement () {
|
||||
if (cameraRotationControls.activeDirections.size === 0) {
|
||||
if (cameraRotationControls.interval) {
|
||||
clearInterval(cameraRotationControls.interval)
|
||||
cameraRotationControls.interval = null
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (!cameraRotationControls.interval) {
|
||||
cameraRotationControls.interval = setInterval(() => {
|
||||
// Combine all active movements
|
||||
const movement = { movementX: 0, movementY: 0 }
|
||||
for (const direction of cameraRotationControls.activeDirections) {
|
||||
movement.movementX += cameraRotationControls.movements[direction].movementX
|
||||
movement.movementY += cameraRotationControls.movements[direction].movementY
|
||||
}
|
||||
|
||||
onCameraMove({
|
||||
...movement,
|
||||
type: 'keyboardRotation',
|
||||
stopPropagation () {}
|
||||
})
|
||||
}, cameraRotationControls.config.interval)
|
||||
}
|
||||
},
|
||||
start (direction: 'left' | 'right' | 'up' | 'down') {
|
||||
cameraRotationControls.activeDirections.add(direction)
|
||||
cameraRotationControls.updateMovement()
|
||||
},
|
||||
stop (direction: 'left' | 'right' | 'up' | 'down') {
|
||||
cameraRotationControls.activeDirections.delete(direction)
|
||||
cameraRotationControls.updateMovement()
|
||||
},
|
||||
handleCommand (command: string, pressed: boolean) {
|
||||
const directionMap = {
|
||||
'general.rotateCameraLeft': 'left',
|
||||
'general.rotateCameraRight': 'right',
|
||||
'general.rotateCameraUp': 'up',
|
||||
'general.rotateCameraDown': 'down'
|
||||
} as const
|
||||
|
||||
const direction = directionMap[command]
|
||||
if (direction) {
|
||||
if (pressed) cameraRotationControls.start(direction)
|
||||
else cameraRotationControls.stop(direction)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
window.cameraRotationControls = cameraRotationControls
|
||||
|
||||
const setSneaking = (state: boolean) => {
|
||||
gameAdditionalState.isSneaking = state
|
||||
bot.setControlState('sneak', state)
|
||||
|
|
@ -275,7 +349,6 @@ const onTriggerOrReleased = (command: Command, pressed: boolean) => {
|
|||
} else if (pressed) {
|
||||
setSneaking(!gameAdditionalState.isSneaking)
|
||||
}
|
||||
|
||||
break
|
||||
case 'general.attackDestroy':
|
||||
document.dispatchEvent(new MouseEvent(pressed ? 'mousedown' : 'mouseup', { button: 0 }))
|
||||
|
|
@ -286,6 +359,12 @@ const onTriggerOrReleased = (command: Command, pressed: boolean) => {
|
|||
case 'general.zoom':
|
||||
gameAdditionalState.isZooming = pressed
|
||||
break
|
||||
case 'general.rotateCameraLeft':
|
||||
case 'general.rotateCameraRight':
|
||||
case 'general.rotateCameraUp':
|
||||
case 'general.rotateCameraDown':
|
||||
cameraRotationControls.handleCommand(command, pressed)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -370,6 +449,12 @@ contro.on('trigger', ({ command }) => {
|
|||
case 'general.toggleSneakOrDown':
|
||||
case 'general.sprint':
|
||||
case 'general.attackDestroy':
|
||||
case 'general.rotateCameraLeft':
|
||||
case 'general.rotateCameraRight':
|
||||
case 'general.rotateCameraUp':
|
||||
case 'general.rotateCameraDown':
|
||||
// no-op
|
||||
break
|
||||
case 'general.swapHands': {
|
||||
bot._client.write('entity_action', {
|
||||
entityId: bot.entity.id,
|
||||
|
|
@ -450,7 +535,12 @@ contro.on('release', ({ command }) => {
|
|||
|
||||
// hard-coded keybindings
|
||||
|
||||
export const f3Keybinds = [
|
||||
export const f3Keybinds: Array<{
|
||||
key?: string,
|
||||
action: () => void,
|
||||
mobileTitle: string
|
||||
enabled?: () => boolean
|
||||
}> = [
|
||||
{
|
||||
key: 'KeyA',
|
||||
action () {
|
||||
|
|
@ -496,9 +586,9 @@ export const f3Keybinds = [
|
|||
key: 'KeyT',
|
||||
async action () {
|
||||
// TODO!
|
||||
if (resourcePackState.resourcePackInstalled || loadedGameState.usingServerResourcePack) {
|
||||
if (resourcePackState.resourcePackInstalled || gameAdditionalState.usingServerResourcePack) {
|
||||
showNotification('Reloading textures...')
|
||||
await completeTexturePackInstall('default', 'default', loadedGameState.usingServerResourcePack)
|
||||
await completeTexturePackInstall('default', 'default', gameAdditionalState.usingServerResourcePack)
|
||||
}
|
||||
},
|
||||
mobileTitle: 'Reload Textures'
|
||||
|
|
@ -539,7 +629,15 @@ export const f3Keybinds = [
|
|||
const proxyPing = await bot['pingProxy']()
|
||||
void showOptionsModal(`${username}: last known total latency (ping): ${playerPing}. Connected to ${lastConnectOptions.value?.proxy} with current ping ${proxyPing}. Player UUID: ${uuid}`, [])
|
||||
},
|
||||
mobileTitle: 'Show Proxy & Ping Details'
|
||||
mobileTitle: 'Show Proxy & Ping Details',
|
||||
enabled: () => !!lastConnectOptions.value?.proxy
|
||||
},
|
||||
{
|
||||
action () {
|
||||
void copyServerResourcePackToRegular()
|
||||
},
|
||||
mobileTitle: 'Copy Server Resource Pack',
|
||||
enabled: () => !!gameAdditionalState.usingServerResourcePack
|
||||
}
|
||||
]
|
||||
|
||||
|
|
@ -548,7 +646,7 @@ document.addEventListener('keydown', (e) => {
|
|||
if (!isGameActive(false)) return
|
||||
if (hardcodedPressedKeys.has('F3')) {
|
||||
const keybind = f3Keybinds.find((v) => v.key === e.code)
|
||||
if (keybind) {
|
||||
if (keybind && (keybind.enabled?.() ?? true)) {
|
||||
keybind.action()
|
||||
e.stopPropagation()
|
||||
}
|
||||
|
|
@ -740,19 +838,12 @@ window.addEventListener('keydown', (e) => {
|
|||
if (activeModalStack.length) {
|
||||
const hideAll = e.ctrlKey || e.metaKey
|
||||
if (hideAll) {
|
||||
while (activeModalStack.length > 0) {
|
||||
hideCurrentModal(undefined, () => {
|
||||
if (!activeModalStack.length) {
|
||||
pointerLock.justHitEscape = true
|
||||
}
|
||||
})
|
||||
}
|
||||
hideAllModals()
|
||||
} else {
|
||||
hideCurrentModal(undefined, () => {
|
||||
if (!activeModalStack.length) {
|
||||
pointerLock.justHitEscape = true
|
||||
}
|
||||
})
|
||||
hideCurrentModal()
|
||||
}
|
||||
if (activeModalStack.length === 0) {
|
||||
pointerLock.justHitEscape = true
|
||||
}
|
||||
} else if (pointerLock.hasPointerLock) {
|
||||
document.exitPointerLock?.()
|
||||
|
|
|
|||
|
|
@ -2,16 +2,16 @@ import prettyBytes from 'pretty-bytes'
|
|||
import { openWorldFromHttpDir, openWorldZip } from './browserfs'
|
||||
import { getResourcePackNames, installTexturePack, resourcePackState, updateTexturePackInstalledState } from './resourcePack'
|
||||
import { setLoadingScreenStatus } from './utils'
|
||||
import { appQueryParams, appQueryParamsArray } from './appParams'
|
||||
|
||||
export const getFixedFilesize = (bytes: number) => {
|
||||
return prettyBytes(bytes, { minimumFractionDigits: 2, maximumFractionDigits: 2 })
|
||||
}
|
||||
|
||||
const inner = async () => {
|
||||
const qs = new URLSearchParams(window.location.search)
|
||||
const mapUrlDir = qs.getAll('mapDir')
|
||||
const mapUrlDirGuess = qs.get('mapDirGuess')
|
||||
const mapUrlDirBaseUrl = qs.get('mapDirBaseUrl')
|
||||
const mapUrlDir = appQueryParamsArray.mapDir ?? []
|
||||
const mapUrlDirGuess = appQueryParams.mapDirGuess
|
||||
const mapUrlDirBaseUrl = appQueryParams.mapDirBaseUrl
|
||||
if (mapUrlDir.length) {
|
||||
await openWorldFromHttpDir(mapUrlDir, mapUrlDirBaseUrl ?? undefined)
|
||||
return true
|
||||
|
|
@ -20,8 +20,8 @@ const inner = async () => {
|
|||
// await openWorldFromHttpDir(undefined, mapUrlDirGuess)
|
||||
return true
|
||||
}
|
||||
let mapUrl = qs.get('map')
|
||||
const texturepack = qs.get('texturepack')
|
||||
let mapUrl = appQueryParams.map
|
||||
const { texturepack } = appQueryParams
|
||||
// fixme
|
||||
if (texturepack) mapUrl = texturepack
|
||||
if (!mapUrl) return false
|
||||
|
|
|
|||
|
|
@ -86,10 +86,21 @@ export const hideCurrentModal = (_data?, onHide?: () => void) => {
|
|||
}
|
||||
}
|
||||
|
||||
export const hideAllModals = () => {
|
||||
while (activeModalStack.length > 0) {
|
||||
if (!hideModal()) break
|
||||
}
|
||||
return activeModalStack.length === 0
|
||||
}
|
||||
|
||||
export const openOptionsMenu = (group: OptionsGroupType) => {
|
||||
showModal({ reactType: `options-${group}` })
|
||||
}
|
||||
|
||||
subscribe(activeModalStack, () => {
|
||||
document.body.style.setProperty('--has-modals-z', activeModalStack.length ? '-1' : null)
|
||||
})
|
||||
|
||||
// ---
|
||||
|
||||
export const currentContextMenu = proxy({ items: [] as ContextMenuItem[] | null, x: 0, y: 0 })
|
||||
|
|
@ -139,12 +150,6 @@ export const miscUiState = proxy({
|
|||
displaySearchInput: false,
|
||||
})
|
||||
|
||||
export const loadedGameState = proxy({
|
||||
username: '',
|
||||
serverIp: '' as string | null,
|
||||
usingServerResourcePack: false,
|
||||
})
|
||||
|
||||
export const isGameActive = (foregroundCheck: boolean) => {
|
||||
if (foregroundCheck && activeModalStack.length) return false
|
||||
return miscUiState.gameLoaded
|
||||
|
|
@ -158,7 +163,9 @@ export const gameAdditionalState = proxy({
|
|||
isSprinting: false,
|
||||
isSneaking: false,
|
||||
isZooming: false,
|
||||
warps: [] as WorldWarp[]
|
||||
warps: [] as WorldWarp[],
|
||||
|
||||
usingServerResourcePack: false,
|
||||
})
|
||||
|
||||
window.gameAdditionalState = gameAdditionalState
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { loadGoogleDriveApi, loadInMemorySave } from './react/SingleplayerProvid
|
|||
import { setLoadingScreenStatus } from './utils'
|
||||
import { mountGoogleDriveFolder } from './browserfs'
|
||||
import { showOptionsModal } from './react/SelectOption'
|
||||
import { appQueryParams } from './appParams'
|
||||
|
||||
const CLIENT_ID = '137156026346-igv2gkjsj2hlid92rs3q7cjjnc77s132.apps.googleusercontent.com'
|
||||
// const CLIENT_ID = process.env.GOOGLE_CLIENT_ID
|
||||
|
|
@ -45,7 +46,7 @@ export const useGoogleLogIn = () => {
|
|||
}
|
||||
|
||||
export const possiblyHandleStateVariable = async () => {
|
||||
const stateParam = new URLSearchParams(window.location.search).get('state')
|
||||
const stateParam = appQueryParams.state
|
||||
if (!stateParam) return
|
||||
setLoadingScreenStatus('Opening world in read only mode, waiting for login...')
|
||||
await loadGoogleDriveApi()
|
||||
|
|
|
|||
255
src/index.ts
255
src/index.ts
|
|
@ -7,12 +7,10 @@ import './entities'
|
|||
import './globalDomListeners'
|
||||
import './mineflayer/maps'
|
||||
import './mineflayer/cameraShake'
|
||||
import initCollisionShapes from './getCollisionInteractionShapes'
|
||||
import { onGameLoad } from './inventoryWindows'
|
||||
import { supportedVersions } from 'minecraft-protocol'
|
||||
import initCollisionShapes from './getCollisionInteractionShapes'
|
||||
import protocolMicrosoftAuth from 'minecraft-protocol/src/client/microsoftAuth'
|
||||
import microsoftAuthflow from './microsoftAuthflow'
|
||||
import nbt from 'prismarine-nbt'
|
||||
|
||||
import 'core-js/features/array/at'
|
||||
import 'core-js/features/promise/with-resolvers'
|
||||
|
|
@ -24,7 +22,7 @@ import PrismarineItem from 'prismarine-item'
|
|||
|
||||
import { options, watchValue } from './optionsStorage'
|
||||
import './reactUi'
|
||||
import { contro, lockUrl, onBotCreate } from './controls'
|
||||
import { lockUrl, onBotCreate } from './controls'
|
||||
import './dragndrop'
|
||||
import { possiblyCleanHandle, resetStateAfterDisconnect } from './browserfs'
|
||||
import { watchOptionsAfterViewerInit, watchOptionsAfterWorldViewInit } from './watchOptions'
|
||||
|
|
@ -40,7 +38,7 @@ import { Vec3 } from 'vec3'
|
|||
import worldInteractions from './worldInteractions'
|
||||
|
||||
import * as THREE from 'three'
|
||||
import MinecraftData, { versionsByMinecraftVersion } from 'minecraft-data'
|
||||
import MinecraftData from 'minecraft-data'
|
||||
import debug from 'debug'
|
||||
import { defaultsDeep } from 'lodash-es'
|
||||
import initializePacketsReplay from './packetsReplay'
|
||||
|
|
@ -53,16 +51,12 @@ import {
|
|||
hideModal,
|
||||
insertActiveModalStack,
|
||||
isGameActive,
|
||||
loadedGameState,
|
||||
miscUiState,
|
||||
showModal
|
||||
} from './globalState'
|
||||
|
||||
|
||||
import {
|
||||
pointerLock,
|
||||
toMajorVersion,
|
||||
setLoadingScreenStatus
|
||||
pointerLock, setLoadingScreenStatus
|
||||
} from './utils'
|
||||
import { isCypress } from './standaloneUtils'
|
||||
|
||||
|
|
@ -77,7 +71,6 @@ import dayCycle from './dayCycle'
|
|||
import { onAppLoad, resourcepackReload } from './resourcePack'
|
||||
import { ConnectPeerOptions, connectToPeer } from './localServerMultiplayer'
|
||||
import CustomChannelClient from './customClient'
|
||||
import { loadScript } from 'prismarine-viewer/viewer/lib/utils'
|
||||
import { registerServiceWorker } from './serviceWorker'
|
||||
import { appStatusState, lastConnectOptions } from './react/AppStatusProvider'
|
||||
|
||||
|
|
@ -85,9 +78,7 @@ import { fsState } from './loadSave'
|
|||
import { watchFov } from './rendererUtils'
|
||||
import { loadInMemorySave } from './react/SingleplayerProvider'
|
||||
|
||||
import { downloadSoundsIfNeeded } from './soundSystem'
|
||||
import { ua } from './react/utils'
|
||||
import { handleMovementStickDelta, joystickPointer } from './react/TouchAreasControls'
|
||||
import { possiblyHandleStateVariable } from './googledrive'
|
||||
import flyingSquidEvents from './flyingSquidEvents'
|
||||
import { hideNotification, notificationProxy, showNotification } from './react/NotificationProvider'
|
||||
|
|
@ -106,6 +97,8 @@ import { ItemsRenderer } from 'mc-assets/dist/itemsRenderer'
|
|||
import './mobileShim'
|
||||
import { parseFormattedMessagePacket } from './botUtils'
|
||||
import { getViewerVersionData, getWsProtocolStream } from './viewerConnector'
|
||||
import { appQueryParams, appQueryParamsArray } from './appParams'
|
||||
import { updateCursor } from './cameraRotationControls'
|
||||
|
||||
window.debug = debug
|
||||
window.THREE = THREE
|
||||
|
|
@ -201,44 +194,13 @@ viewer.entities.entitiesOptions = {
|
|||
}
|
||||
watchOptionsAfterViewerInit()
|
||||
|
||||
let mouseMovePostHandle = (e) => { }
|
||||
let lastMouseMove: number
|
||||
const updateCursor = () => {
|
||||
worldInteractions.update()
|
||||
}
|
||||
function onCameraMove (e) {
|
||||
if (e.type !== 'touchmove' && !pointerLock.hasPointerLock) return
|
||||
e.stopPropagation?.()
|
||||
const now = performance.now()
|
||||
// todo: limit camera movement for now to avoid unexpected jumps
|
||||
if (now - lastMouseMove < 4) return
|
||||
lastMouseMove = now
|
||||
let { mouseSensX, mouseSensY } = options
|
||||
if (mouseSensY === -1) mouseSensY = mouseSensX
|
||||
mouseMovePostHandle({
|
||||
x: e.movementX * mouseSensX * 0.0001,
|
||||
y: e.movementY * mouseSensY * 0.0001
|
||||
})
|
||||
updateCursor()
|
||||
}
|
||||
window.addEventListener('mousemove', onCameraMove, { capture: true })
|
||||
contro.on('stickMovement', ({ stick, vector }) => {
|
||||
if (!isGameActive(true)) return
|
||||
if (stick !== 'right') return
|
||||
let { x, z } = vector
|
||||
if (Math.abs(x) < 0.18) x = 0
|
||||
if (Math.abs(z) < 0.18) z = 0
|
||||
onCameraMove({ movementX: x * 10, movementY: z * 10, type: 'touchmove' })
|
||||
miscUiState.usingGamepadInput = true
|
||||
})
|
||||
|
||||
function hideCurrentScreens () {
|
||||
activeModalStacks['main-menu'] = [...activeModalStack]
|
||||
insertActiveModalStack('', [])
|
||||
}
|
||||
|
||||
const loadSingleplayer = (serverOverrides = {}, flattenedServerOverrides = {}) => {
|
||||
const serverSettingsQsRaw = new URLSearchParams(window.location.search).getAll('serverSetting')
|
||||
const serverSettingsQsRaw = appQueryParamsArray.serverSetting ?? []
|
||||
const serverSettingsQs = serverSettingsQsRaw.map(x => x.split(':')).reduce<Record<string, string>>((acc, [key, value]) => {
|
||||
acc[key] = JSON.parse(value)
|
||||
return acc
|
||||
|
|
@ -286,7 +248,7 @@ const cleanConnectIp = (host: string | undefined, defaultPort: string | undefine
|
|||
}
|
||||
}
|
||||
|
||||
async function connect (connectOptions: ConnectOptions) {
|
||||
export async function connect (connectOptions: ConnectOptions) {
|
||||
if (miscUiState.gameLoaded) return
|
||||
miscUiState.hasErrors = false
|
||||
lastConnectOptions.value = connectOptions
|
||||
|
|
@ -301,6 +263,10 @@ async function connect (connectOptions: ConnectOptions) {
|
|||
if (connectOptions.proxy?.startsWith(':')) {
|
||||
connectOptions.proxy = `${location.protocol}//${location.hostname}${connectOptions.proxy}`
|
||||
}
|
||||
if (connectOptions.proxy && location.port !== '80' && location.port !== '443' && !/:\d+$/.test(connectOptions.proxy)) {
|
||||
const https = connectOptions.proxy.startsWith('https://') || location.protocol === 'https:'
|
||||
connectOptions.proxy = `${connectOptions.proxy}:${https ? 443 : 80}`
|
||||
}
|
||||
const proxy = cleanConnectIp(connectOptions.proxy, undefined)
|
||||
let { username } = connectOptions
|
||||
|
||||
|
|
@ -702,7 +668,7 @@ async function connect (connectOptions: ConnectOptions) {
|
|||
setLoadingScreenStatus('Placing blocks (starting viewer)')
|
||||
localStorage.lastConnectOptions = JSON.stringify(connectOptions)
|
||||
connectOptions.onSuccessfulPlay?.()
|
||||
if (process.env.NODE_ENV === 'development' && !localStorage.lockUrl && new URLSearchParams(location.search).size === 0) {
|
||||
if (process.env.NODE_ENV === 'development' && !localStorage.lockUrl && !Object.keys(window.debugQueryParams).length) {
|
||||
lockUrl()
|
||||
}
|
||||
updateDataAfterJoin()
|
||||
|
|
@ -746,154 +712,11 @@ async function connect (connectOptions: ConnectOptions) {
|
|||
|
||||
setLoadingScreenStatus('Setting callbacks')
|
||||
|
||||
const maxPitch = 0.5 * Math.PI
|
||||
const minPitch = -0.5 * Math.PI
|
||||
mouseMovePostHandle = ({ x, y }) => {
|
||||
viewer.world.lastCamUpdate = Date.now()
|
||||
const pitch = bot.entity.pitch - y
|
||||
void bot.look(bot.entity.yaw - x, Math.max(minPitch, Math.min(maxPitch, pitch)), true)
|
||||
}
|
||||
|
||||
function changeCallback () {
|
||||
if (notificationProxy.id === 'pointerlockchange') {
|
||||
hideNotification()
|
||||
}
|
||||
if (renderer.xr.isPresenting) return // todo
|
||||
if (!pointerLock.hasPointerLock && activeModalStack.length === 0) {
|
||||
showModal({ reactType: 'pause-screen' })
|
||||
}
|
||||
}
|
||||
|
||||
registerListener(document, 'pointerlockchange', changeCallback, false)
|
||||
|
||||
const cameraControlEl = document.querySelector('#ui-root')
|
||||
|
||||
/** 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 } | undefined
|
||||
registerListener(document, 'pointerdown', (e) => {
|
||||
const usingJoystick = options.touchControlsType === 'joystick-buttons'
|
||||
const clickedEl = e.composedPath()[0]
|
||||
if (!isGameActive(true) || !miscUiState.currentTouch || clickedEl !== cameraControlEl || e.pointerId === undefined) {
|
||||
return
|
||||
}
|
||||
screenTouches++
|
||||
if (screenTouches === 3) {
|
||||
// todo needs fixing!
|
||||
// window.dispatchEvent(new MouseEvent('mousedown', { button: 1 }))
|
||||
}
|
||||
if (usingJoystick) {
|
||||
if (!joystickPointer.pointer && e.clientX < window.innerWidth / 2) {
|
||||
joystickPointer.pointer = {
|
||||
pointerId: e.pointerId,
|
||||
x: e.clientX,
|
||||
y: e.clientY
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
if (capturedPointer) {
|
||||
return
|
||||
}
|
||||
cameraControlEl.setPointerCapture(e.pointerId)
|
||||
capturedPointer = {
|
||||
id: e.pointerId,
|
||||
x: e.clientX,
|
||||
y: e.clientY,
|
||||
sourceX: e.clientX,
|
||||
sourceY: e.clientY,
|
||||
activateCameraMove: false,
|
||||
time: Date.now()
|
||||
}
|
||||
if (options.touchControlsType !== 'joystick-buttons') {
|
||||
virtualClickTimeout ??= setTimeout(() => {
|
||||
virtualClickActive = true
|
||||
document.dispatchEvent(new MouseEvent('mousedown', { button: 0 }))
|
||||
}, touchStartBreakingBlockMs)
|
||||
}
|
||||
})
|
||||
registerListener(document, 'pointermove', (e) => {
|
||||
if (e.pointerId === undefined) return
|
||||
const supportsPressure = (e as any).pressure !== undefined && (e as any).pressure !== 0 && (e as any).pressure !== 0.5 && (e as any).pressure !== 1 && (e.pointerType === 'touch' || e.pointerType === 'pen')
|
||||
if (e.pointerId === joystickPointer.pointer?.pointerId) {
|
||||
handleMovementStickDelta(e)
|
||||
if (supportsPressure && (e as any).pressure > 0.5) {
|
||||
bot.setControlState('sprint', true)
|
||||
// todo
|
||||
}
|
||||
return
|
||||
}
|
||||
if (e.pointerId !== capturedPointer?.id) return
|
||||
window.scrollTo(0, 0)
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
|
||||
const allowedJitter = 1.1
|
||||
if (supportsPressure) {
|
||||
bot.setControlState('jump', (e as any).pressure > 0.5)
|
||||
}
|
||||
const xDiff = Math.abs(e.pageX - capturedPointer.sourceX) > allowedJitter
|
||||
const yDiff = Math.abs(e.pageY - capturedPointer.sourceY) > allowedJitter
|
||||
if (!capturedPointer.activateCameraMove && (xDiff || yDiff)) capturedPointer.activateCameraMove = true
|
||||
if (capturedPointer.activateCameraMove) {
|
||||
clearTimeout(virtualClickTimeout)
|
||||
}
|
||||
onCameraMove({ movementX: e.pageX - capturedPointer.x, movementY: e.pageY - capturedPointer.y, type: 'touchmove' })
|
||||
capturedPointer.x = e.pageX
|
||||
capturedPointer.y = e.pageY
|
||||
}, { passive: false })
|
||||
|
||||
const pointerUpHandler = (e: PointerEvent) => {
|
||||
if (e.pointerId === undefined) return
|
||||
if (e.pointerId === joystickPointer.pointer?.pointerId) {
|
||||
handleMovementStickDelta()
|
||||
joystickPointer.pointer = null
|
||||
return
|
||||
}
|
||||
if (e.pointerId !== capturedPointer?.id) return
|
||||
clearTimeout(virtualClickTimeout)
|
||||
virtualClickTimeout = undefined
|
||||
|
||||
if (options.touchControlsType !== 'joystick-buttons') {
|
||||
if (virtualClickActive) {
|
||||
// button 0 is left click
|
||||
document.dispatchEvent(new MouseEvent('mouseup', { button: 0 }))
|
||||
virtualClickActive = false
|
||||
} else if (!capturedPointer.activateCameraMove && (Date.now() - capturedPointer.time < touchStartBreakingBlockMs)) {
|
||||
document.dispatchEvent(new MouseEvent('mousedown', { button: 2 }))
|
||||
worldInteractions.update()
|
||||
document.dispatchEvent(new MouseEvent('mouseup', { button: 2 }))
|
||||
}
|
||||
}
|
||||
capturedPointer = undefined
|
||||
screenTouches--
|
||||
}
|
||||
registerListener(document, 'pointerup', pointerUpHandler)
|
||||
registerListener(document, 'pointercancel', pointerUpHandler)
|
||||
registerListener(document, 'lostpointercapture', pointerUpHandler)
|
||||
|
||||
registerListener(document, 'contextmenu', (e) => e.preventDefault(), false)
|
||||
|
||||
registerListener(document, 'blur', (e) => {
|
||||
bot.clearControlStates()
|
||||
}, false)
|
||||
|
||||
console.log('Done!')
|
||||
|
||||
// todo
|
||||
onGameLoad(async () => {
|
||||
loadedGameState.serverIp = server.host ?? null
|
||||
loadedGameState.username = username
|
||||
})
|
||||
onGameLoad(() => {})
|
||||
|
||||
if (appStatusState.isError) return
|
||||
setTimeout(() => {
|
||||
// todo
|
||||
const qs = new URLSearchParams(window.location.search)
|
||||
if (qs.get('suggest_save')) {
|
||||
if (appQueryParams.suggest_save) {
|
||||
showNotification('Suggestion', 'Save the world to keep your progress!', false, undefined, async () => {
|
||||
const savePath = await saveToBrowserMemory()
|
||||
if (!savePath) return
|
||||
|
|
@ -912,7 +735,7 @@ async function connect (connectOptions: ConnectOptions) {
|
|||
// todo might not emit as servers simply don't send chunk if it's empty
|
||||
if (!viewer.world.allChunksFinished || done) return
|
||||
done = true
|
||||
console.log('All done and ready! In', (Date.now() - start) / 1000, 's')
|
||||
console.log('All chunks done and ready! Time from renderer open to ready', (Date.now() - start) / 1000, 's')
|
||||
viewer.render() // ensure the last state is rendered
|
||||
document.dispatchEvent(new Event('cypress-world-ready'))
|
||||
})
|
||||
|
|
@ -925,8 +748,8 @@ async function connect (connectOptions: ConnectOptions) {
|
|||
if (!connectOptions.ignoreQs) {
|
||||
// todo cleanup
|
||||
customEvents.on('gameLoaded', () => {
|
||||
const qs = new URLSearchParams(window.location.search)
|
||||
for (let command of qs.getAll('command')) {
|
||||
const commands = appQueryParamsArray.command ?? []
|
||||
for (let command of commands) {
|
||||
if (!command.startsWith('/')) command = `/${command}`
|
||||
bot.chat(command)
|
||||
}
|
||||
|
|
@ -937,17 +760,14 @@ async function connect (connectOptions: ConnectOptions) {
|
|||
listenGlobalEvents()
|
||||
watchValue(miscUiState, async s => {
|
||||
if (s.appLoaded) { // fs ready
|
||||
const qs = new URLSearchParams(window.location.search)
|
||||
const moreServerOptions = {} as Record<string, any>
|
||||
if (qs.has('version')) moreServerOptions.version = qs.get('version')
|
||||
if (qs.get('singleplayer') === '1' || qs.get('sp') === '1') {
|
||||
if (appQueryParams.singleplayer === '1' || appQueryParams.sp === '1') {
|
||||
loadSingleplayer({}, {
|
||||
worldFolder: undefined,
|
||||
...moreServerOptions
|
||||
...appQueryParams.version ? { version: appQueryParams.version } : {}
|
||||
})
|
||||
}
|
||||
if (qs.get('loadSave')) {
|
||||
const savePath = `/data/worlds/${qs.get('loadSave')}`
|
||||
if (appQueryParams.loadSave) {
|
||||
const savePath = `/data/worlds/${appQueryParams.loadSave}`
|
||||
try {
|
||||
await fs.promises.stat(savePath)
|
||||
} catch (err) {
|
||||
|
|
@ -1003,22 +823,19 @@ void window.fetch('config.json').then(async res => res.json()).then(c => c, (err
|
|||
// qs open actions
|
||||
downloadAndOpenFile().then((downloadAction) => {
|
||||
if (downloadAction) return
|
||||
const qs = new URLSearchParams(window.location.search)
|
||||
if (qs.get('reconnect') && process.env.NODE_ENV === 'development') {
|
||||
const ip = qs.get('ip')
|
||||
if (appQueryParams.reconnect && process.env.NODE_ENV === 'development') {
|
||||
const lastConnect = JSON.parse(localStorage.lastConnectOptions ?? {})
|
||||
void connect({
|
||||
botVersion: qs.get('version') ?? undefined,
|
||||
...lastConnect, // todo mixing is not good idea
|
||||
ip: ip || undefined
|
||||
botVersion: appQueryParams.version ?? undefined,
|
||||
...lastConnect,
|
||||
ip: appQueryParams.ip || undefined
|
||||
})
|
||||
return
|
||||
}
|
||||
if (qs.get('ip') || qs.get('proxy')) {
|
||||
const waitAppConfigLoad = !qs.get('proxy')
|
||||
if (appQueryParams.ip || appQueryParams.proxy) {
|
||||
const waitAppConfigLoad = !appQueryParams.proxy
|
||||
const openServerEditor = () => {
|
||||
hideModal()
|
||||
// show server editor for connect or save
|
||||
showModal({ reactType: 'editServer' })
|
||||
}
|
||||
showModal({ reactType: 'empty' })
|
||||
|
|
@ -1040,12 +857,12 @@ downloadAndOpenFile().then((downloadAction) => {
|
|||
|
||||
void Promise.resolve().then(() => {
|
||||
// try to connect to peer
|
||||
const peerId = qs.get('connectPeer')
|
||||
const peerId = appQueryParams.connectPeer
|
||||
const peerOptions = {} as ConnectPeerOptions
|
||||
if (qs.get('server')) {
|
||||
peerOptions.server = qs.get('server')!
|
||||
if (appQueryParams.server) {
|
||||
peerOptions.server = appQueryParams.server
|
||||
}
|
||||
const version = qs.get('peerVersion')
|
||||
const version = appQueryParams.peerVersion
|
||||
if (peerId) {
|
||||
let username: string | null = options.guestUsername
|
||||
if (options.askGuestName) username = prompt('Enter your username', username)
|
||||
|
|
@ -1060,11 +877,11 @@ downloadAndOpenFile().then((downloadAction) => {
|
|||
}
|
||||
})
|
||||
|
||||
if (qs.get('serversList')) {
|
||||
if (appQueryParams.serversList) {
|
||||
showModal({ reactType: 'serversList' })
|
||||
}
|
||||
|
||||
const viewerWsConnect = qs.get('viewerConnect')
|
||||
const viewerWsConnect = appQueryParams.viewerConnect
|
||||
if (viewerWsConnect) {
|
||||
void connect({
|
||||
username: `viewer-${Math.random().toString(36).slice(2, 10)}`,
|
||||
|
|
@ -1072,8 +889,8 @@ downloadAndOpenFile().then((downloadAction) => {
|
|||
})
|
||||
}
|
||||
|
||||
if (qs.get('modal')) {
|
||||
const modals = qs.get('modal')!.split(',')
|
||||
if (appQueryParams.modal) {
|
||||
const modals = appQueryParams.modal.split(',')
|
||||
for (const modal of modals) {
|
||||
showModal({ reactType: modal })
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import { isMajorVersionGreater } from './utils'
|
|||
|
||||
import { activeModalStacks, insertActiveModalStack, miscUiState } from './globalState'
|
||||
import supportedVersions from './supportedVersions.mjs'
|
||||
import { appQueryParams } from './appParams'
|
||||
|
||||
// todo include name of opened handle (zip)!
|
||||
// additional fs metadata
|
||||
|
|
@ -84,8 +85,7 @@ export const loadSave = async (root = '/world') => {
|
|||
let version: string | undefined | null
|
||||
let isFlat = false
|
||||
if (levelDat) {
|
||||
const qs = new URLSearchParams(window.location.search)
|
||||
version = qs.get('mapVersion') ?? levelDat.Version?.Name
|
||||
version = appQueryParams.mapVersion ?? levelDat.Version?.Name
|
||||
if (!version) {
|
||||
// const newVersion = disablePrompts ? '1.8.8' : 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')
|
||||
// if (!newVersion) return
|
||||
|
|
|
|||
|
|
@ -16,5 +16,8 @@ setImageConverter((buf: Uint8Array) => {
|
|||
customEvents.on('mineflayerBotCreated', () => {
|
||||
bot.on('login', () => {
|
||||
bot.loadPlugin(mapDownloader)
|
||||
bot.mapDownloader.on('new_map', ({ png, id }) => {
|
||||
viewer.entities.updateMap(id, png)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { useRef, useState } from 'react'
|
|||
import { useSnapshot } from 'valtio'
|
||||
import { openURL } from 'prismarine-viewer/viewer/lib/simpleUtils'
|
||||
import { noCase } from 'change-case'
|
||||
import { loadedGameState, miscUiState, openOptionsMenu, showModal } from './globalState'
|
||||
import { gameAdditionalState, miscUiState, openOptionsMenu, showModal } from './globalState'
|
||||
import { AppOptions, options } from './optionsStorage'
|
||||
import Button from './react/Button'
|
||||
import { OptionMeta, OptionSlider } from './react/OptionsItems'
|
||||
|
|
@ -157,7 +157,7 @@ export const guiOptionsScheme: {
|
|||
{
|
||||
custom () {
|
||||
const { resourcePackInstalled } = useSnapshot(resourcePackState)
|
||||
const { usingServerResourcePack } = useSnapshot(loadedGameState)
|
||||
const { usingServerResourcePack } = useSnapshot(gameAdditionalState)
|
||||
const { enabledResourcepack } = useSnapshot(options)
|
||||
return <Button
|
||||
label={`Resource Pack: ${usingServerResourcePack ? 'SERVER ON' : resourcePackInstalled ? enabledResourcepack ? 'ON' : 'OFF' : 'NO'}`} inScreen onClick={async () => {
|
||||
|
|
@ -233,6 +233,19 @@ export const guiOptionsScheme: {
|
|||
chatSelect: {
|
||||
},
|
||||
},
|
||||
{
|
||||
custom () {
|
||||
return <Category>World</Category>
|
||||
},
|
||||
highlightBlockColor: {
|
||||
text: 'Block Highlight Color',
|
||||
values: [
|
||||
['auto', 'Auto'],
|
||||
['blue', 'Blue'],
|
||||
['classic', 'Classic']
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
custom () {
|
||||
return <Category>Sign Editor</Category>
|
||||
|
|
@ -342,33 +355,38 @@ export const guiOptionsScheme: {
|
|||
touchButtonsSize: {
|
||||
min: 40,
|
||||
disableIf: [
|
||||
'touchControlsType',
|
||||
'joystick-buttons'
|
||||
'touchMovementType',
|
||||
'modern'
|
||||
],
|
||||
},
|
||||
touchButtonsOpacity: {
|
||||
min: 10,
|
||||
max: 90,
|
||||
disableIf: [
|
||||
'touchControlsType',
|
||||
'joystick-buttons'
|
||||
'touchMovementType',
|
||||
'modern'
|
||||
],
|
||||
},
|
||||
touchButtonsPosition: {
|
||||
max: 80,
|
||||
disableIf: [
|
||||
'touchControlsType',
|
||||
'joystick-buttons'
|
||||
'touchMovementType',
|
||||
'modern'
|
||||
],
|
||||
},
|
||||
touchControlsType: {
|
||||
values: [['classic', 'Classic'], ['joystick-buttons', 'New']],
|
||||
touchMovementType: {
|
||||
text: 'Movement Controls',
|
||||
values: [['modern', 'Modern'], ['classic', 'Classic']],
|
||||
},
|
||||
touchInteractionType: {
|
||||
text: 'Interaction Controls',
|
||||
values: [['classic', 'Classic'], ['buttons', 'Buttons']],
|
||||
},
|
||||
},
|
||||
{
|
||||
custom () {
|
||||
const { touchControlsType } = useSnapshot(options)
|
||||
return <Button label='Setup Touch Buttons' onClick={() => showModal({ reactType: 'touch-buttons-setup' })} inScreen disabled={touchControlsType !== 'joystick-buttons'} />
|
||||
const { touchInteractionType, touchMovementType } = useSnapshot(options)
|
||||
return <Button label='Setup Touch Buttons' onClick={() => showModal({ reactType: 'touch-buttons-setup' })} inScreen disabled={touchInteractionType === 'classic' && touchMovementType === 'classic'} />
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { proxy, subscribe } from 'valtio/vanilla'
|
|||
// weird webpack configuration bug: it cant import valtio/utils in this file
|
||||
import { subscribeKey } from 'valtio/utils'
|
||||
import { omitObj } from '@zardoy/utils'
|
||||
import { appQueryParamsArray } from './appParams'
|
||||
|
||||
const isDev = process.env.NODE_ENV === 'development'
|
||||
const defaultOptions = {
|
||||
|
|
@ -25,6 +26,7 @@ const defaultOptions = {
|
|||
chatOpacityOpened: 100,
|
||||
messagesLimit: 200,
|
||||
volume: 50,
|
||||
enableMusic: false,
|
||||
// fov: 70,
|
||||
fov: 75,
|
||||
guiScale: 3,
|
||||
|
|
@ -33,7 +35,8 @@ const defaultOptions = {
|
|||
touchButtonsOpacity: 80,
|
||||
touchButtonsPosition: 12,
|
||||
touchControlsPositions: getDefaultTouchControlsPositions(),
|
||||
touchControlsType: 'classic' as 'classic' | 'joystick-buttons',
|
||||
touchMovementType: 'modern' as 'modern' | 'classic',
|
||||
touchInteractionType: 'classic' as 'classic' | 'buttons',
|
||||
gpuPreference: 'default' as 'default' | 'high-performance' | 'low-power',
|
||||
backgroundRendering: '20fps' as 'full' | '20fps' | '5fps',
|
||||
/** @unstable */
|
||||
|
|
@ -95,6 +98,7 @@ const defaultOptions = {
|
|||
displayBossBars: false, // boss bar overlay was removed for some reason, enable safely
|
||||
disabledUiParts: [] as string[],
|
||||
neighborChunkUpdates: true,
|
||||
highlightBlockColor: 'auto' as 'auto' | 'blue' | 'classic',
|
||||
}
|
||||
|
||||
function getDefaultTouchControlsPositions () {
|
||||
|
|
@ -118,7 +122,8 @@ function getDefaultTouchControlsPositions () {
|
|||
} as Record<string, [number, number]>
|
||||
}
|
||||
|
||||
const qsOptionsRaw = new URLSearchParams(location.search).getAll('setting')
|
||||
// const qsOptionsRaw = new URLSearchParams(location.search).getAll('setting')
|
||||
const qsOptionsRaw = appQueryParamsArray.setting ?? []
|
||||
export const qsOptions = Object.fromEntries(qsOptionsRaw.map(o => {
|
||||
const [key, value] = o.split(':')
|
||||
return [key, JSON.parse(value)]
|
||||
|
|
@ -135,6 +140,9 @@ const migrateOptions = (options: Partial<AppOptions & Record<string, any>>) => {
|
|||
if (options.touchControlsPositions?.jump === undefined) {
|
||||
options.touchControlsPositions!.jump = defaultOptions.touchControlsPositions.jump
|
||||
}
|
||||
if (options.touchControlsType === 'joystick-buttons') {
|
||||
options.touchInteractionType = 'buttons'
|
||||
}
|
||||
|
||||
return options
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
import React, { useEffect } from 'react'
|
||||
import { appQueryParams } from '../appParams'
|
||||
import { fetchServerStatus, isServerValid } from '../api/mcStatusApi'
|
||||
import Screen from './Screen'
|
||||
import Input from './Input'
|
||||
import Button from './Button'
|
||||
import SelectGameVersion from './SelectGameVersion'
|
||||
import { useIsSmallWidth } from './simpleHooks'
|
||||
import { useIsSmallWidth, usePassesWindowDimensions } from './simpleHooks'
|
||||
|
||||
export interface BaseServerInfo {
|
||||
ip: string
|
||||
|
|
@ -32,13 +34,13 @@ interface Props {
|
|||
const ELEMENTS_WIDTH = 190
|
||||
|
||||
export default ({ onBack, onConfirm, title = 'Add a Server', initialData, parseQs, onQsConnect, placeholders, accounts, versions, allowAutoConnect }: Props) => {
|
||||
const qsParams = parseQs ? new URLSearchParams(window.location.search) : undefined
|
||||
const qsParamName = qsParams?.get('name')
|
||||
const qsParamIp = qsParams?.get('ip')
|
||||
const qsParamVersion = qsParams?.get('version')
|
||||
const qsParamProxy = qsParams?.get('proxy')
|
||||
const qsParamUsername = qsParams?.get('username')
|
||||
const qsParamLockConnect = qsParams?.get('lockConnect')
|
||||
const isSmallHeight = !usePassesWindowDimensions(null, 350)
|
||||
const qsParamName = parseQs ? appQueryParams.name : undefined
|
||||
const qsParamIp = parseQs ? appQueryParams.ip : undefined
|
||||
const qsParamVersion = parseQs ? appQueryParams.version : undefined
|
||||
const qsParamProxy = parseQs ? appQueryParams.proxy : undefined
|
||||
const qsParamUsername = parseQs ? appQueryParams.username : undefined
|
||||
const qsParamLockConnect = parseQs ? appQueryParams.lockConnect : undefined
|
||||
|
||||
const qsIpParts = qsParamIp?.split(':')
|
||||
const ipParts = initialData?.ip ? initialData?.ip.split(':') : undefined
|
||||
|
|
@ -70,8 +72,55 @@ export default ({ onBack, onConfirm, title = 'Add a Server', initialData, parseQ
|
|||
authenticatedAccountOverride,
|
||||
}
|
||||
|
||||
const [fetchedServerInfoIp, setFetchedServerInfoIp] = React.useState<string | undefined>(undefined)
|
||||
const [serverOnline, setServerOnline] = React.useState(null as boolean | null)
|
||||
const [onlinePlayersList, setOnlinePlayersList] = React.useState<string[]>([])
|
||||
|
||||
useEffect(() => {
|
||||
if (qsParams?.get('autoConnect') === 'true' && qsParams?.get('ip') && allowAutoConnect) {
|
||||
const controller = new AbortController()
|
||||
|
||||
const checkServer = async () => {
|
||||
if (!qsParamIp || !isServerValid(qsParamIp)) return
|
||||
|
||||
try {
|
||||
const status = await fetchServerStatus(qsParamIp)
|
||||
if (!status) return
|
||||
|
||||
setServerOnline(status.raw.online)
|
||||
setOnlinePlayersList(status.raw.players?.list.map(p => p.name_raw) ?? [])
|
||||
setFetchedServerInfoIp(qsParamIp)
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch server status:', err)
|
||||
}
|
||||
}
|
||||
|
||||
void checkServer()
|
||||
return () => controller.abort()
|
||||
}, [qsParamIp])
|
||||
|
||||
const validateUsername = (username: string) => {
|
||||
if (!username) return undefined
|
||||
if (onlinePlayersList.includes(username)) {
|
||||
return { border: 'red solid 1px' }
|
||||
}
|
||||
const MINECRAFT_USERNAME_REGEX = /^\w{3,16}$/
|
||||
if (!MINECRAFT_USERNAME_REGEX.test(username)) {
|
||||
return { border: 'red solid 1px' }
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
const validateServerIp = () => {
|
||||
if (!serverIp) return undefined
|
||||
if (serverOnline) {
|
||||
return { border: 'lightgreen solid 1px' }
|
||||
} else {
|
||||
return { border: 'red solid 1px' }
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (qsParamIp && qsParamVersion && allowAutoConnect) {
|
||||
onQsConnect?.(commonUseOptions)
|
||||
}
|
||||
}, [])
|
||||
|
|
@ -99,9 +148,19 @@ export default ({ onBack, onConfirm, title = 'Add a Server', initialData, parseQ
|
|||
<InputWithLabel label="Server Name" value={serverName} onChange={({ target: { value } }) => setServerName(value)} placeholder='Defaults to IP' />
|
||||
</div>
|
||||
</>}
|
||||
<InputWithLabel required label="Server IP" value={serverIp} disabled={lockConnect && qsIpParts?.[0] !== null} onChange={({ target: { value } }) => setServerIp(value)} />
|
||||
<InputWithLabel
|
||||
required
|
||||
label="Server IP"
|
||||
value={serverIp}
|
||||
disabled={lockConnect && qsIpParts?.[0] !== null}
|
||||
onChange={({ target: { value } }) => {
|
||||
setServerIp(value)
|
||||
setServerOnline(false)
|
||||
}}
|
||||
validateInput={serverOnline === null || fetchedServerInfoIp !== serverIp ? undefined : validateServerIp}
|
||||
/>
|
||||
<InputWithLabel label="Server Port" value={serverPort} disabled={lockConnect && qsIpParts?.[1] !== null} onChange={({ target: { value } }) => setServerPort(value)} placeholder='25565' />
|
||||
<div style={{ gridColumn: smallWidth ? '' : 'span 2' }}>Overrides:</div>
|
||||
{isSmallHeight ? <div style={{ gridColumn: 'span 2', marginTop: 10, }} /> : <div style={{ gridColumn: smallWidth ? '' : 'span 2' }}>Overrides:</div>}
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
|
|
@ -114,12 +173,19 @@ export default ({ onBack, onConfirm, title = 'Add a Server', initialData, parseQ
|
|||
setVersionOverride(value)
|
||||
}}
|
||||
placeholder="Optional, but recommended to specify"
|
||||
disabled={lockConnect && qsParamVersion !== null}
|
||||
disabled={lockConnect}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<InputWithLabel label="Proxy Override" value={proxyOverride} disabled={lockConnect && qsParamProxy !== null} onChange={({ target: { value } }) => setProxyOverride(value)} placeholder={placeholders?.proxyOverride} />
|
||||
<InputWithLabel label="Username Override" value={usernameOverride} disabled={!noAccountSelected || lockConnect && qsParamUsername !== null} onChange={({ target: { value } }) => setUsernameOverride(value)} placeholder={placeholders?.usernameOverride} />
|
||||
<InputWithLabel label="Proxy Override" value={proxyOverride} disabled={lockConnect && (qsParamProxy !== null || !!placeholders?.proxyOverride)} onChange={({ target: { value } }) => setProxyOverride(value)} placeholder={placeholders?.proxyOverride} />
|
||||
<InputWithLabel
|
||||
label="Username Override"
|
||||
value={usernameOverride}
|
||||
disabled={!noAccountSelected || (lockConnect && qsParamUsername !== null)}
|
||||
onChange={({ target: { value } }) => setUsernameOverride(value)}
|
||||
placeholder={placeholders?.usernameOverride}
|
||||
validateInput={!serverOnline || fetchedServerInfoIp !== serverIp ? undefined : validateUsername}
|
||||
/>
|
||||
<label style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
|
|
@ -135,6 +201,7 @@ export default ({ onBack, onConfirm, title = 'Add a Server', initialData, parseQ
|
|||
fontSize: 13,
|
||||
}}
|
||||
defaultValue={initialAccount === true ? -2 : initialAccount === undefined ? -1 : (fallbackIfNotFound((accounts ?? []).indexOf(initialAccount)) ?? -2)}
|
||||
disabled={lockConnect && qsParamUsername !== null}
|
||||
>
|
||||
<option value={-1}>Offline Account (Username)</option>
|
||||
{accounts?.map((account, i) => <option key={i} value={i}>{account} (Logged In)</option>)}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { useEffect, useState } from 'react'
|
||||
import { appQueryParams } from '../appParams'
|
||||
import styles from './appStatus.module.css'
|
||||
import Button from './Button'
|
||||
import Screen from './Screen'
|
||||
|
|
@ -15,6 +16,7 @@ export default ({
|
|||
children
|
||||
}) => {
|
||||
const [loadingDotIndex, setLoadingDotIndex] = useState(0)
|
||||
const lockConnect = appQueryParams.lockConnect === 'true'
|
||||
|
||||
useEffect(() => {
|
||||
const statusRunner = async () => {
|
||||
|
|
@ -65,7 +67,7 @@ export default ({
|
|||
>
|
||||
{isError && (
|
||||
<>
|
||||
{backAction && <Button label="Back" onClick={backAction} />}
|
||||
{!lockConnect && backAction && <Button label="Back" onClick={backAction} />}
|
||||
{actionsSlot}
|
||||
<Button onClick={() => window.location.reload()} label="Reset App (recommended)" />
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ interface Props extends React.ComponentProps<'button'> {
|
|||
children?: React.ReactNode
|
||||
inScreen?: boolean
|
||||
rootRef?: Ref<HTMLButtonElement>
|
||||
overlayColor?: string
|
||||
}
|
||||
|
||||
const ButtonContext = createContext({
|
||||
|
|
@ -23,7 +24,7 @@ export const ButtonProvider: FC<{ children, onClick }> = ({ children, onClick })
|
|||
return <ButtonContext.Provider value={{ onClick }}>{children}</ButtonContext.Provider>
|
||||
}
|
||||
|
||||
export default (({ label, icon, children, inScreen, rootRef, type = 'button', postLabel, ...args }) => {
|
||||
export default (({ label, icon, children, inScreen, rootRef, type = 'button', postLabel, overlayColor, ...args }) => {
|
||||
const ctx = useContext(ButtonContext)
|
||||
|
||||
const onClick = (e) => {
|
||||
|
|
@ -45,6 +46,13 @@ export default (({ label, icon, children, inScreen, rootRef, type = 'button', po
|
|||
{label}
|
||||
{postLabel}
|
||||
{children}
|
||||
{overlayColor && <div style={{
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
backgroundColor: overlayColor,
|
||||
opacity: 0.5,
|
||||
pointerEvents: 'none'
|
||||
}} />}
|
||||
</button>
|
||||
</SharedHudVars>
|
||||
}) satisfies FC<Props>
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { useEffect, useMemo, useRef, useState } from 'react'
|
|||
import { useSnapshot } from 'valtio'
|
||||
import { formatMessage } from '../chatUtils'
|
||||
import { getBuiltinCommandsList, tryHandleBuiltinCommand } from '../builtinCommands'
|
||||
import { hideCurrentModal, loadedGameState, miscUiState } from '../globalState'
|
||||
import { hideCurrentModal, miscUiState } from '../globalState'
|
||||
import { options } from '../optionsStorage'
|
||||
import Chat, { Message, fadeMessage } from './Chat'
|
||||
import { useIsModalActive } from './utilsApp'
|
||||
|
|
@ -54,7 +54,7 @@ export default () => {
|
|||
updateLoadedServerData((server) => {
|
||||
server.autoLogin ??= {}
|
||||
const password = message.split(' ')[1]
|
||||
server.autoLogin[loadedGameState.username] = password
|
||||
server.autoLogin[bot.player.username] = password
|
||||
return server
|
||||
})
|
||||
hideNotification()
|
||||
|
|
|
|||
187
src/react/GameInteractionOverlay.tsx
Normal file
187
src/react/GameInteractionOverlay.tsx
Normal file
|
|
@ -0,0 +1,187 @@
|
|||
import { useRef } from 'react'
|
||||
import { useSnapshot } from 'valtio'
|
||||
import { useUtilsEffect } from '@zardoy/react-util'
|
||||
import { options } from '../optionsStorage'
|
||||
import { activeModalStack, isGameActive, miscUiState } from '../globalState'
|
||||
import worldInteractions from '../worldInteractions'
|
||||
import { onCameraMove, CameraMoveEvent } from '../cameraRotationControls'
|
||||
import { handleMovementStickDelta, joystickPointer } from './TouchAreasControls'
|
||||
|
||||
/** after what time of holding the finger start breaking the block */
|
||||
const touchStartBreakingBlockMs = 500
|
||||
|
||||
function GameInteractionOverlayInner ({ zIndex }: { zIndex: number }) {
|
||||
const overlayRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
useUtilsEffect(({ signal }) => {
|
||||
if (!overlayRef.current) return
|
||||
|
||||
const cameraControlEl = overlayRef.current
|
||||
let virtualClickActive = false
|
||||
let virtualClickTimeout: NodeJS.Timeout | undefined
|
||||
let screenTouches = 0
|
||||
let capturedPointer: {
|
||||
id: number;
|
||||
x: number;
|
||||
y: number;
|
||||
sourceX: number;
|
||||
sourceY: number;
|
||||
activateCameraMove: boolean;
|
||||
time: number
|
||||
} | undefined
|
||||
|
||||
const pointerDownHandler = (e: PointerEvent) => {
|
||||
const clickedEl = e.composedPath()[0]
|
||||
if (!isGameActive(true) || clickedEl !== cameraControlEl || e.pointerId === undefined) {
|
||||
return
|
||||
}
|
||||
screenTouches++
|
||||
if (screenTouches === 3) {
|
||||
// todo maybe mouse wheel click?
|
||||
}
|
||||
const usingModernMovement = options.touchMovementType === 'modern'
|
||||
if (usingModernMovement) {
|
||||
if (!joystickPointer.pointer && e.clientX < window.innerWidth / 2) {
|
||||
cameraControlEl.setPointerCapture(e.pointerId)
|
||||
joystickPointer.pointer = {
|
||||
pointerId: e.pointerId,
|
||||
x: e.clientX,
|
||||
y: e.clientY
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
if (capturedPointer) {
|
||||
return
|
||||
}
|
||||
cameraControlEl.setPointerCapture(e.pointerId)
|
||||
capturedPointer = {
|
||||
id: e.pointerId,
|
||||
x: e.clientX,
|
||||
y: e.clientY,
|
||||
sourceX: e.clientX,
|
||||
sourceY: e.clientY,
|
||||
activateCameraMove: false,
|
||||
time: Date.now()
|
||||
}
|
||||
if (options.touchInteractionType === 'classic') {
|
||||
virtualClickTimeout ??= setTimeout(() => {
|
||||
virtualClickActive = true
|
||||
document.dispatchEvent(new MouseEvent('mousedown', { button: 0 }))
|
||||
}, touchStartBreakingBlockMs)
|
||||
}
|
||||
}
|
||||
|
||||
const pointerMoveHandler = (e: PointerEvent) => {
|
||||
if (e.pointerId === undefined) return
|
||||
const supportsPressure = (e as any).pressure !== undefined &&
|
||||
(e as any).pressure !== 0 &&
|
||||
(e as any).pressure !== 0.5 &&
|
||||
(e as any).pressure !== 1 &&
|
||||
(e.pointerType === 'touch' || e.pointerType === 'pen')
|
||||
|
||||
if (e.pointerId === joystickPointer.pointer?.pointerId) {
|
||||
handleMovementStickDelta(e)
|
||||
if (supportsPressure && (e as any).pressure > 0.5) {
|
||||
bot.setControlState('sprint', true)
|
||||
}
|
||||
return
|
||||
}
|
||||
if (e.pointerId !== capturedPointer?.id) return
|
||||
// window.scrollTo(0, 0)
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
|
||||
const allowedJitter = 1.1
|
||||
if (supportsPressure) {
|
||||
bot.setControlState('jump', (e as any).pressure > 0.5)
|
||||
}
|
||||
const xDiff = Math.abs(e.pageX - capturedPointer.sourceX) > allowedJitter
|
||||
const yDiff = Math.abs(e.pageY - capturedPointer.sourceY) > allowedJitter
|
||||
if (!capturedPointer.activateCameraMove && (xDiff || yDiff)) {
|
||||
capturedPointer.activateCameraMove = true
|
||||
}
|
||||
if (capturedPointer.activateCameraMove) {
|
||||
clearTimeout(virtualClickTimeout)
|
||||
}
|
||||
|
||||
onCameraMove({
|
||||
movementX: e.pageX - capturedPointer.x,
|
||||
movementY: e.pageY - capturedPointer.y,
|
||||
type: 'touchmove',
|
||||
stopPropagation: () => e.stopPropagation()
|
||||
} as CameraMoveEvent)
|
||||
capturedPointer.x = e.pageX
|
||||
capturedPointer.y = e.pageY
|
||||
}
|
||||
|
||||
const pointerUpHandler = (e: PointerEvent) => {
|
||||
if (e.pointerId === undefined) return
|
||||
if (e.pointerId === joystickPointer.pointer?.pointerId) {
|
||||
handleMovementStickDelta()
|
||||
joystickPointer.pointer = null
|
||||
return
|
||||
}
|
||||
if (e.pointerId !== capturedPointer?.id) return
|
||||
clearTimeout(virtualClickTimeout)
|
||||
virtualClickTimeout = undefined
|
||||
|
||||
if (virtualClickActive) {
|
||||
// button 0 is left click
|
||||
document.dispatchEvent(new MouseEvent('mouseup', { button: 0 }))
|
||||
virtualClickActive = false
|
||||
} else if (!capturedPointer.activateCameraMove && (Date.now() - capturedPointer.time < touchStartBreakingBlockMs)) {
|
||||
document.dispatchEvent(new MouseEvent('mousedown', { button: 2 }))
|
||||
worldInteractions.update()
|
||||
document.dispatchEvent(new MouseEvent('mouseup', { button: 2 }))
|
||||
}
|
||||
|
||||
capturedPointer = undefined
|
||||
screenTouches--
|
||||
}
|
||||
|
||||
const contextMenuHandler = (e: Event) => {
|
||||
e.preventDefault()
|
||||
}
|
||||
|
||||
const blurHandler = () => {
|
||||
bot.clearControlStates()
|
||||
}
|
||||
|
||||
cameraControlEl.addEventListener('pointerdown', pointerDownHandler, { signal })
|
||||
cameraControlEl.addEventListener('pointermove', pointerMoveHandler, { signal })
|
||||
cameraControlEl.addEventListener('pointerup', pointerUpHandler, { signal })
|
||||
cameraControlEl.addEventListener('pointercancel', pointerUpHandler, { signal })
|
||||
cameraControlEl.addEventListener('lostpointercapture', pointerUpHandler, { signal })
|
||||
cameraControlEl.addEventListener('contextmenu', contextMenuHandler, { signal })
|
||||
window.addEventListener('blur', blurHandler, { signal })
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<OverlayElement divRef={overlayRef} zIndex={zIndex} />
|
||||
)
|
||||
}
|
||||
|
||||
const OverlayElement = ({ divRef, zIndex }: { divRef: React.RefObject<HTMLDivElement>, zIndex: number }) => {
|
||||
return <div
|
||||
className='game-interaction-overlay'
|
||||
ref={divRef}
|
||||
style={{
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
zIndex,
|
||||
touchAction: 'none',
|
||||
userSelect: 'none'
|
||||
}}
|
||||
/>
|
||||
}
|
||||
|
||||
export default function GameInteractionOverlay ({ zIndex }: { zIndex: number }) {
|
||||
const modalStack = useSnapshot(activeModalStack)
|
||||
const { currentTouch } = useSnapshot(miscUiState)
|
||||
if (modalStack.length > 0 || !currentTouch) return null
|
||||
return <GameInteractionOverlayInner zIndex={zIndex} />
|
||||
}
|
||||
|
|
@ -19,7 +19,7 @@ export default () => {
|
|||
updateHeldMap()
|
||||
})
|
||||
|
||||
bot.on('new_map', () => {
|
||||
bot.on('new_map', ({ id }) => {
|
||||
// total maps: Object.keys(bot.mapDownloader.maps).length
|
||||
updateHeldMap()
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
.effectsScreen-container {
|
||||
position: fixed;
|
||||
top: 6%;
|
||||
top: max(6%, 30px);
|
||||
left: 0px;
|
||||
z-index: -2;
|
||||
pointer-events: none;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React, { CSSProperties, useEffect, useRef, useState } from 'react'
|
||||
import React, { CSSProperties, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { isMobile } from 'prismarine-viewer/viewer/lib/simpleUtils'
|
||||
import styles from './input.module.css'
|
||||
|
||||
|
|
@ -28,6 +28,10 @@ export default ({ autoFocus, rootStyles, inputRef, validateInput, defaultValue,
|
|||
}, [])
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
setValidationStyle(validateInput?.(value as any) ?? {})
|
||||
}, [value, validateInput])
|
||||
|
||||
return <div id='input-container' className={styles.container} style={rootStyles}>
|
||||
<input
|
||||
ref={ref}
|
||||
|
|
@ -41,7 +45,6 @@ export default ({ autoFocus, rootStyles, inputRef, validateInput, defaultValue,
|
|||
{...inputProps}
|
||||
value={value}
|
||||
onChange={(e) => {
|
||||
setValidationStyle(validateInput?.(e.target.value) ?? {})
|
||||
setValue(e.target.value)
|
||||
inputProps.onChange?.(e)
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -15,11 +15,12 @@ import { useSnapshot } from 'valtio'
|
|||
import BlockData from '../../prismarine-viewer/viewer/lib/moreBlockDataGenerated.json'
|
||||
import preflatMap from '../preflatMap.json'
|
||||
import { contro } from '../controls'
|
||||
import { gameAdditionalState, showModal, hideModal, miscUiState, loadedGameState, activeModalStack } from '../globalState'
|
||||
import { gameAdditionalState, showModal, hideModal, miscUiState, activeModalStack } from '../globalState'
|
||||
import { options } from '../optionsStorage'
|
||||
import Minimap, { DisplayMode } from './Minimap'
|
||||
import { ChunkInfo, DrawerAdapter, MapUpdates, MinimapDrawer } from './MinimapDrawer'
|
||||
import { useIsModalActive } from './utilsApp'
|
||||
import { lastConnectOptions } from './AppStatusProvider'
|
||||
|
||||
const getBlockKey = (x: number, z: number) => {
|
||||
return `${x},${z}`
|
||||
|
|
@ -167,9 +168,9 @@ export class DrawerAdapterImpl extends TypedEventEmitter<MapUpdates> implements
|
|||
// type suppressed until server is updated. It works fine
|
||||
void (localServer as any).setWarp(warp, remove)
|
||||
} else if (remove) {
|
||||
localStorage.removeItem(`warps: ${loadedGameState.username} ${loadedGameState.serverIp}`)
|
||||
localStorage.removeItem(`warps: ${bot.player.username} ${lastConnectOptions.value!.server}`)
|
||||
} else {
|
||||
localStorage.setItem(`warps: ${loadedGameState.username} ${loadedGameState.serverIp}`, JSON.stringify(this.warps))
|
||||
localStorage.setItem(`warps: ${bot.player.username} ${lastConnectOptions.value!.server}`, JSON.stringify(this.warps))
|
||||
}
|
||||
this.emit('updateWarps')
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
left: 50%;
|
||||
transform: translate(-50%);
|
||||
gap: 0 5px;
|
||||
z-index: -1;
|
||||
z-index: var(--has-modals-z, 7);
|
||||
}
|
||||
|
||||
.pause-btn,
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ export default () => {
|
|||
}, [])
|
||||
|
||||
const onLongPress = async () => {
|
||||
const select = await showOptionsModal('', f3Keybinds.filter(f3Keybind => f3Keybind.mobileTitle).map(f3Keybind => f3Keybind.mobileTitle))
|
||||
const select = await showOptionsModal('', f3Keybinds.filter(f3Keybind => f3Keybind.mobileTitle && (f3Keybind.enabled?.() ?? true)).map(f3Keybind => f3Keybind.mobileTitle))
|
||||
if (!select) return
|
||||
const f3Keybind = f3Keybinds.find(f3Keybind => f3Keybind.mobileTitle === select)
|
||||
if (f3Keybind) f3Keybind.action()
|
||||
|
|
|
|||
|
|
@ -3,11 +3,12 @@ import { noCase } from 'change-case'
|
|||
import { titleCase } from 'title-case'
|
||||
import { useMemo } from 'react'
|
||||
import { options, qsOptions } from '../optionsStorage'
|
||||
import { miscUiState } from '../globalState'
|
||||
import { hideAllModals, miscUiState } from '../globalState'
|
||||
import Button from './Button'
|
||||
import Slider from './Slider'
|
||||
import Screen from './Screen'
|
||||
import { showOptionsModal } from './SelectOption'
|
||||
import PixelartIcon, { pixelartIcons } from './PixelartIcon'
|
||||
|
||||
type GeneralItem<T extends string | number | boolean> = {
|
||||
id?: string
|
||||
|
|
@ -188,10 +189,18 @@ interface Props {
|
|||
}
|
||||
|
||||
export default ({ items, title, backButtonAction }: Props) => {
|
||||
const { currentTouch } = useSnapshot(miscUiState)
|
||||
|
||||
return <Screen
|
||||
title={title}
|
||||
>
|
||||
<div className='screen-items'>
|
||||
{currentTouch && (
|
||||
<div style={{ position: 'fixed', marginLeft: '-30px', display: 'flex', flexDirection: 'column', gap: 1, }}>
|
||||
<Button icon={pixelartIcons['close']} onClick={hideAllModals} style={{ color: '#ff5d5d', }} />
|
||||
<Button icon={pixelartIcons['chevron-left']} onClick={backButtonAction} style={{ color: 'yellow', }} />
|
||||
</div>
|
||||
)}
|
||||
{items.map((element, i) => {
|
||||
// make sure its unique!
|
||||
return <RenderOption key={element.id ?? `${title}-${i}`} item={element} />
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import { disconnect } from '../flyingSquidUtils'
|
|||
import { pointerLock, setLoadingScreenStatus } from '../utils'
|
||||
import { closeWan, openToWanAndCopyJoinLink, getJoinLink } from '../localServerMultiplayer'
|
||||
import { collectFilesToCopy, fileExistsAsyncOptimized, mkdirRecursive, uniqueFileNameFromWorldName } from '../browserfs'
|
||||
import { appQueryParams } from '../appParams'
|
||||
import { useIsModalActive } from './utilsApp'
|
||||
import { showOptionsModal } from './SelectOption'
|
||||
import Button from './Button'
|
||||
|
|
@ -86,7 +87,7 @@ export const saveToBrowserMemory = async () => {
|
|||
const srcPath = join(worldFolder, copyPath)
|
||||
const savePath = join(saveRootPath, copyPath)
|
||||
await mkdirRecursive(savePath)
|
||||
await fs.promises.writeFile(savePath, await fs.promises.readFile(srcPath))
|
||||
await fs.promises.writeFile(savePath, await fs.promises.readFile(srcPath) as any)
|
||||
upProgress(totalSIze)
|
||||
if (isRegionFiles) {
|
||||
const regionFile = copyPath.split('/').at(-1)!
|
||||
|
|
@ -146,8 +147,7 @@ const splitByCopySize = (files: string[], copySize = 15) => {
|
|||
}
|
||||
|
||||
export default () => {
|
||||
const qsParams = new URLSearchParams(window.location.search)
|
||||
const lockConnect = qsParams?.get('lockConnect') === 'true'
|
||||
const lockConnect = appQueryParams.lockConnect === 'true'
|
||||
const isModalActive = useIsModalActive('pause-screen')
|
||||
const fsStateSnap = useSnapshot(fsState)
|
||||
const activeModalStackSnap = useSnapshot(activeModalStack)
|
||||
|
|
|
|||
|
|
@ -1,15 +1,16 @@
|
|||
import { useSnapshot } from 'valtio'
|
||||
import { useState, useEffect, useMemo } from 'react'
|
||||
import { isGameActive, loadedGameState } from '../globalState'
|
||||
import { isGameActive } from '../globalState'
|
||||
import PlayerListOverlay from './PlayerListOverlay'
|
||||
import './PlayerListOverlay.css'
|
||||
import { lastConnectOptions } from './AppStatusProvider'
|
||||
|
||||
const MAX_ROWS_PER_COL = 10
|
||||
|
||||
type Players = typeof bot.players
|
||||
|
||||
export default () => {
|
||||
const { serverIp } = useSnapshot(loadedGameState)
|
||||
const serverIp = lastConnectOptions.value?.server
|
||||
const [clientId, setClientId] = useState('')
|
||||
const [players, setPlayers] = useState<Players>({})
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ import { useSnapshot } from 'valtio'
|
|||
import { ConnectOptions } from '../connect'
|
||||
import { activeModalStack, hideCurrentModal, miscUiState, showModal } from '../globalState'
|
||||
import supportedVersions from '../supportedVersions.mjs'
|
||||
import { appQueryParams } from '../appParams'
|
||||
import { fetchServerStatus, isServerValid } from '../api/mcStatusApi'
|
||||
import ServersList from './ServersList'
|
||||
import AddServerOrConnect, { BaseServerInfo } from './AddServerOrConnect'
|
||||
import { useDidUpdateEffect } from './utils'
|
||||
|
|
@ -11,42 +13,18 @@ import { useIsModalActive } from './utilsApp'
|
|||
import { showOptionsModal } from './SelectOption'
|
||||
import { useCopyKeybinding } from './simpleHooks'
|
||||
|
||||
interface StoreServerItem extends BaseServerInfo {
|
||||
export interface StoreServerItem extends BaseServerInfo {
|
||||
lastJoined?: number
|
||||
description?: string
|
||||
optionsOverride?: Record<string, any>
|
||||
autoLogin?: Record<string, string>
|
||||
}
|
||||
|
||||
type ServerResponse = {
|
||||
online: boolean
|
||||
version?: {
|
||||
name_raw: string
|
||||
}
|
||||
// display tooltip
|
||||
players?: {
|
||||
online: number
|
||||
max: number
|
||||
list: Array<{
|
||||
name_raw: string
|
||||
name_clean: string
|
||||
}>
|
||||
}
|
||||
icon?: string
|
||||
motd?: {
|
||||
raw: string
|
||||
}
|
||||
// todo circle error icon
|
||||
mods?: Array<{ name, version }>
|
||||
// todo display via hammer icon
|
||||
software?: string
|
||||
plugins?: Array<{ name, version }>
|
||||
}
|
||||
|
||||
type AdditionalDisplayData = {
|
||||
formattedText: string
|
||||
textNameRight: string
|
||||
icon?: string
|
||||
offline?: boolean
|
||||
}
|
||||
|
||||
export interface AuthenticatedAccount {
|
||||
|
|
@ -93,8 +71,8 @@ const getInitialServersList = () => {
|
|||
return servers
|
||||
}
|
||||
|
||||
const serversListQs = new URLSearchParams(window.location.search).get('serversList')
|
||||
const proxyQs = new URLSearchParams(window.location.search).get('proxy')
|
||||
const serversListQs = appQueryParams.serversList
|
||||
const proxyQs = appQueryParams.proxy
|
||||
|
||||
const setNewServersList = (serversList: StoreServerItem[], force = false) => {
|
||||
if (serversListQs && !force) return
|
||||
|
|
@ -138,6 +116,9 @@ export const updateAuthenticatedAccountData = (callback: (data: AuthenticatedAcc
|
|||
// todo move to base
|
||||
const normalizeIp = (ip: string) => ip.replace(/https?:\/\//, '').replace(/\/(:|$)/, '')
|
||||
|
||||
const FETCH_DELAY = 100 // ms between each request
|
||||
const MAX_CONCURRENT_REQUESTS = 10
|
||||
|
||||
const Inner = ({ hidden, customServersList }: { hidden?: boolean, customServersList?: string[] }) => {
|
||||
const [proxies, setProxies] = useState<readonly string[]>(localStorage['proxies'] ? JSON.parse(localStorage['proxies']) : getInitialProxies())
|
||||
const [selectedProxy, setSelectedProxy] = useState(proxyQs ?? localStorage.getItem('selectedProxy') ?? proxies?.[0] ?? '')
|
||||
|
|
@ -196,36 +177,59 @@ const Inner = ({ hidden, customServersList }: { hidden?: boolean, customServersL
|
|||
return serversList.map((server, index) => ({ ...server, index })).sort((a, b) => (b.lastJoined ?? 0) - (a.lastJoined ?? 0))
|
||||
}, [serversList])
|
||||
|
||||
useUtilsEffect(({ signal }) => {
|
||||
const update = async () => {
|
||||
for (const server of serversListSorted) {
|
||||
const isInLocalNetwork = server.ip.startsWith('192.168.') || server.ip.startsWith('10.') || server.ip.startsWith('172.') || server.ip.startsWith('127.') || server.ip.startsWith('localhost')
|
||||
if (isInLocalNetwork || signal.aborted) continue
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await fetch(`https://api.mcstatus.io/v2/status/java/${server.ip}`, {
|
||||
// TODO: bounty for this who fix it
|
||||
// signal
|
||||
}).then(async r => r.json()).then((data: ServerResponse) => {
|
||||
const versionClean = data.version?.name_raw.replace(/^[^\d.]+/, '')
|
||||
if (!versionClean) return
|
||||
setAdditionalData(old => {
|
||||
return ({
|
||||
...old,
|
||||
[server.ip]: {
|
||||
formattedText: data.motd?.raw ?? '',
|
||||
textNameRight: `${versionClean} ${data.players?.online ?? '??'}/${data.players?.max ?? '??'}`,
|
||||
icon: data.icon,
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
void update().catch((err) => {})
|
||||
}, [serversListSorted])
|
||||
|
||||
const isEditScreenModal = useIsModalActive('editServer')
|
||||
|
||||
useUtilsEffect(({ signal }) => {
|
||||
if (isEditScreenModal) return
|
||||
const update = async () => {
|
||||
const queue = serversListSorted
|
||||
.map(server => {
|
||||
if (!isServerValid(server.ip) || signal.aborted) return null
|
||||
|
||||
return server
|
||||
})
|
||||
.filter(x => x !== null)
|
||||
|
||||
const activeRequests = new Set<Promise<void>>()
|
||||
|
||||
let lastRequestStart = 0
|
||||
for (const server of queue) {
|
||||
// Wait if at concurrency limit
|
||||
if (activeRequests.size >= MAX_CONCURRENT_REQUESTS) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await Promise.race(activeRequests)
|
||||
}
|
||||
|
||||
// Create and track new request
|
||||
// eslint-disable-next-line @typescript-eslint/no-loop-func
|
||||
const request = new Promise<void>(resolve => {
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
lastRequestStart = Date.now()
|
||||
if (signal.aborted) return
|
||||
const data = await fetchServerStatus(server.ip/* , signal */) // DONT ADD SIGNAL IT WILL CRUSH JS RUNTIME
|
||||
if (data) {
|
||||
setAdditionalData(old => ({
|
||||
...old,
|
||||
[server.ip]: data
|
||||
}))
|
||||
}
|
||||
} finally {
|
||||
activeRequests.delete(request)
|
||||
resolve()
|
||||
}
|
||||
}, lastRequestStart ? Math.max(0, FETCH_DELAY - (Date.now() - lastRequestStart)) : 0)
|
||||
})
|
||||
|
||||
activeRequests.add(request)
|
||||
}
|
||||
|
||||
await Promise.all(activeRequests)
|
||||
}
|
||||
|
||||
void update()
|
||||
}, [serversListSorted, isEditScreenModal])
|
||||
|
||||
useDidUpdateEffect(() => {
|
||||
if (serverEditScreen && !isEditScreenModal) {
|
||||
showModal({ reactType: 'editServer' })
|
||||
|
|
@ -394,10 +398,10 @@ const Inner = ({ hidden, customServersList }: { hidden?: boolean, customServersL
|
|||
name: server.index.toString(),
|
||||
title: server.name || server.ip,
|
||||
detail: (server.versionOverride ?? '') + ' ' + (server.usernameOverride ?? ''),
|
||||
// lastPlayed: server.lastJoined,
|
||||
formattedTextOverride: additional?.formattedText,
|
||||
worldNameRight: additional?.textNameRight ?? '',
|
||||
iconSrc: additional?.icon,
|
||||
offline: additional?.offline
|
||||
}
|
||||
})}
|
||||
initialProxies={{
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import Button from './Button'
|
|||
import Tabs from './Tabs'
|
||||
import MessageFormattedString from './MessageFormattedString'
|
||||
import { useIsSmallWidth } from './simpleHooks'
|
||||
import PixelartIcon from './PixelartIcon'
|
||||
|
||||
export interface WorldProps {
|
||||
name: string
|
||||
|
|
@ -26,9 +27,10 @@ export interface WorldProps {
|
|||
onFocus?: (name: string) => void
|
||||
onInteraction?(interaction: 'enter' | 'space')
|
||||
elemRef?: React.Ref<HTMLDivElement>
|
||||
offline?: boolean
|
||||
}
|
||||
|
||||
const World = ({ name, isFocused, title, lastPlayed, size, detail = '', onFocus, onInteraction, iconSrc, formattedTextOverride, worldNameRight, elemRef }: WorldProps & { ref?: React.Ref<HTMLDivElement> }) => {
|
||||
const World = ({ name, isFocused, title, lastPlayed, size, detail = '', onFocus, onInteraction, iconSrc, formattedTextOverride, worldNameRight, elemRef, offline }: WorldProps & { ref?: React.Ref<HTMLDivElement> }) => {
|
||||
const timeRelativeFormatted = useMemo(() => {
|
||||
if (!lastPlayed) return ''
|
||||
const formatter = new Intl.RelativeTimeFormat('en', { numeric: 'auto' })
|
||||
|
|
@ -60,7 +62,14 @@ const World = ({ name, isFocused, title, lastPlayed, size, detail = '', onFocus,
|
|||
<div className={styles.world_info}>
|
||||
<div className={styles.world_title}>
|
||||
<div>{title}</div>
|
||||
<div className={styles.world_title_right}>{worldNameRight}</div>
|
||||
<div className={styles.world_title_right}>
|
||||
{offline ? (
|
||||
<span style={{ color: 'red', display: 'flex', alignItems: 'center', gap: 4 }}>
|
||||
<PixelartIcon iconName="signal-off" width={12} />
|
||||
Offline
|
||||
</span>
|
||||
) : worldNameRight}
|
||||
</div>
|
||||
</div>
|
||||
{formattedTextOverride ? <div className={styles.world_info_formatted}>
|
||||
<MessageFormattedString message={formattedTextOverride} />
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { useState } from 'react'
|
||||
import { useSnapshot } from 'valtio'
|
||||
import { hideCurrentModal } from '../globalState'
|
||||
import { lastPlayedSounds } from '../soundSystem'
|
||||
import { lastPlayedSounds } from '../sounds/botSoundSystem'
|
||||
import { options } from '../optionsStorage'
|
||||
import Button from './Button'
|
||||
import Screen from './Screen'
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { CSSProperties, PointerEvent, useEffect, useRef } from 'react'
|
|||
import { proxy, ref, useSnapshot } from 'valtio'
|
||||
import { contro } from '../controls'
|
||||
import worldInteractions from '../worldInteractions'
|
||||
import { options } from '../optionsStorage'
|
||||
import PixelartIcon from './PixelartIcon'
|
||||
import Button from './Button'
|
||||
|
||||
|
|
@ -9,13 +10,6 @@ export type ButtonName = 'action' | 'sneak' | 'break' | 'jump'
|
|||
|
||||
type ButtonsPositions = Record<ButtonName, [number, number]>
|
||||
|
||||
interface Props {
|
||||
touchActive: boolean
|
||||
setupActive: boolean
|
||||
buttonsPositions: ButtonsPositions
|
||||
closeButtonsSetup: (newPositions?: ButtonsPositions) => void
|
||||
}
|
||||
|
||||
const getCurrentAppScaling = () => {
|
||||
// body has css property --guiScale
|
||||
const guiScale = getComputedStyle(document.body).getPropertyValue('--guiScale')
|
||||
|
|
@ -51,15 +45,23 @@ export const handleMovementStickDelta = (e?: { clientX, clientY }) => {
|
|||
})
|
||||
}
|
||||
|
||||
export default ({ touchActive, setupActive, buttonsPositions, closeButtonsSetup }: Props) => {
|
||||
type Props = {
|
||||
setupActive: boolean
|
||||
closeButtonsSetup: (newPositions?: ButtonsPositions) => void
|
||||
foregroundGameActive: boolean
|
||||
}
|
||||
|
||||
const Z_INDEX_INTERACTIBLE = 8
|
||||
|
||||
export default ({ setupActive, closeButtonsSetup, foregroundGameActive }: Props) => {
|
||||
const bot = window.bot as typeof __type_bot | undefined
|
||||
if (setupActive) touchActive = true
|
||||
const { touchControlsPositions, touchMovementType, touchInteractionType } = useSnapshot(options)
|
||||
const buttonsPositions = touchControlsPositions as ButtonsPositions
|
||||
|
||||
const joystickOuter = useRef<HTMLDivElement>(null)
|
||||
const joystickInner = useRef<HTMLDivElement>(null)
|
||||
|
||||
const { pointer } = useSnapshot(joystickPointer)
|
||||
// const { isFlying, isSneaking } = useSnapshot(gameAdditionalState)
|
||||
const newButtonPositions = { ...buttonsPositions }
|
||||
|
||||
const buttonProps = (name: ButtonName) => {
|
||||
|
|
@ -146,6 +148,7 @@ export default ({ touchActive, setupActive, buttonsPositions, closeButtonsSetup
|
|||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
transition: 'background 0.1s',
|
||||
zIndex: Z_INDEX_INTERACTIBLE,
|
||||
} satisfies CSSProperties,
|
||||
onPointerDown (e: PType) {
|
||||
const elem = e.currentTarget as HTMLElement
|
||||
|
|
@ -162,8 +165,8 @@ export default ({ touchActive, setupActive, buttonsPositions, closeButtonsSetup
|
|||
const elem = e.currentTarget as HTMLElement
|
||||
const size = 32
|
||||
const scale = getCurrentAppScaling()
|
||||
const xPerc = (e.clientX - size / 4 / scale) / window.innerWidth * 100
|
||||
const yPerc = (e.clientY - size / 4 / scale) / window.innerHeight * 100
|
||||
const xPerc = (e.clientX - (size * scale) / 2) / window.innerWidth * 100
|
||||
const yPerc = (e.clientY - (size * scale) / 2) / window.innerHeight * 100
|
||||
elem.style.left = `${xPerc}%`
|
||||
elem.style.top = `${yPerc}%`
|
||||
newButtonPositions[name] = [xPerc, yPerc]
|
||||
|
|
@ -178,55 +181,65 @@ export default ({ touchActive, setupActive, buttonsPositions, closeButtonsSetup
|
|||
useEffect(() => {
|
||||
joystickPointer.joystickInner = joystickInner.current && ref(joystickInner.current)
|
||||
// todo antipattern
|
||||
}, [touchActive])
|
||||
}, [foregroundGameActive])
|
||||
|
||||
if (!touchActive) return null
|
||||
if (!foregroundGameActive && !setupActive) return null
|
||||
|
||||
return <div>
|
||||
<div
|
||||
className='movement_joystick_outer'
|
||||
ref={joystickOuter}
|
||||
style={{
|
||||
display: pointer ? 'flex' : 'none',
|
||||
borderRadius: '50%',
|
||||
width: 50,
|
||||
height: 50,
|
||||
border: '2px solid rgba(0, 0, 0, 0.5)',
|
||||
backgroundColor: 'rgba(255, 255, div, 0.5)',
|
||||
position: 'fixed',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
translate: '-50% -50%',
|
||||
...pointer ? {
|
||||
left: `${pointer.x / window.innerWidth * 100}%`,
|
||||
top: `${pointer.y / window.innerHeight * 100}%`
|
||||
} : {}
|
||||
}}
|
||||
>
|
||||
{touchMovementType === 'modern' && (
|
||||
<div
|
||||
className='movement_joystick_inner'
|
||||
className='movement_joystick_outer'
|
||||
ref={joystickOuter}
|
||||
style={{
|
||||
display: pointer ? 'flex' : 'none',
|
||||
borderRadius: '50%',
|
||||
width: 20,
|
||||
height: 20,
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
||||
position: 'absolute',
|
||||
width: 50,
|
||||
height: 50,
|
||||
border: '2px solid rgba(0, 0, 0, 0.5)',
|
||||
backgroundColor: 'rgba(255, 255, div, 0.5)',
|
||||
position: 'fixed',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
translate: '-50% -50%',
|
||||
...pointer ? {
|
||||
left: `${pointer.x / window.innerWidth * 100}%`,
|
||||
top: `${pointer.y / window.innerHeight * 100}%`
|
||||
} : {}
|
||||
}}
|
||||
ref={joystickInner}
|
||||
/>
|
||||
</div>
|
||||
<div {...buttonProps('action')}>
|
||||
<PixelartIcon iconName='circle' />
|
||||
</div>
|
||||
<div {...buttonProps('sneak')}>
|
||||
<PixelartIcon iconName='arrow-down' />
|
||||
</div>
|
||||
<div {...buttonProps('jump')}>
|
||||
<PixelartIcon iconName='arrow-up' />
|
||||
</div>
|
||||
<div {...buttonProps('break')}>
|
||||
<MineIcon />
|
||||
</div>
|
||||
>
|
||||
<div
|
||||
className='movement_joystick_inner'
|
||||
style={{
|
||||
borderRadius: '50%',
|
||||
width: 20,
|
||||
height: 20,
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
||||
position: 'absolute',
|
||||
}}
|
||||
ref={joystickInner}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{touchMovementType === 'modern' && (
|
||||
<>
|
||||
<div {...buttonProps('sneak')}>
|
||||
<PixelartIcon iconName='arrow-down' />
|
||||
</div>
|
||||
<div {...buttonProps('jump')}>
|
||||
<PixelartIcon iconName='arrow-up' />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{touchInteractionType === 'buttons' && (
|
||||
<>
|
||||
<div {...buttonProps('action')}>
|
||||
<PixelartIcon iconName='circle' />
|
||||
</div>
|
||||
<div {...buttonProps('break')}>
|
||||
<MineIcon />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{setupActive && <div style={{
|
||||
position: 'fixed',
|
||||
bottom: 0,
|
||||
|
|
|
|||
|
|
@ -8,12 +8,10 @@ export default () => {
|
|||
const usingTouch = useUsingTouch()
|
||||
const hasModals = useSnapshot(activeModalStack).length !== 0
|
||||
const setupActive = useIsModalActive('touch-buttons-setup')
|
||||
const { touchControlsPositions, touchControlsType } = useSnapshot(options)
|
||||
|
||||
return <TouchAreasControls
|
||||
touchActive={!!bot && !!usingTouch && !hasModals && touchControlsType === 'joystick-buttons'}
|
||||
foregroundGameActive={!!bot && !!usingTouch && !hasModals}
|
||||
setupActive={setupActive}
|
||||
buttonsPositions={touchControlsPositions as any}
|
||||
closeButtonsSetup={(newPositions) => {
|
||||
if (newPositions) {
|
||||
options.touchControlsPositions = newPositions
|
||||
|
|
@ -21,5 +19,4 @@ export default () => {
|
|||
hideModal()
|
||||
}}
|
||||
/>
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -49,9 +49,9 @@ export default () => {
|
|||
const usingTouch = useUsingTouch()
|
||||
const { usingGamepadInput } = useSnapshot(miscUiState)
|
||||
const modals = useSnapshot(activeModalStack)
|
||||
const { touchControlsType } = useSnapshot(options)
|
||||
const { touchMovementType } = useSnapshot(options)
|
||||
|
||||
if (!usingTouch || usingGamepadInput || touchControlsType !== 'classic') return null
|
||||
if (!usingTouch || usingGamepadInput || touchMovementType !== 'classic') return null
|
||||
return (
|
||||
<div
|
||||
style={{ zIndex: modals.length ? 7 : 8 }}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,18 @@ export const useIsSmallWidth = () => {
|
|||
return useMedia(SMALL_SCREEN_MEDIA.replace('@media ', ''))
|
||||
}
|
||||
|
||||
export const usePassesWindowDimensions = (minWidth: number | null = null, minHeight: number | null = null) => {
|
||||
let media = '('
|
||||
if (minWidth !== null) {
|
||||
media += `min-width: ${minWidth}px, `
|
||||
}
|
||||
if (minHeight !== null) {
|
||||
media += `min-height: ${minHeight}px, `
|
||||
}
|
||||
media += ')'
|
||||
return useMedia(media)
|
||||
}
|
||||
|
||||
export const useCopyKeybinding = (getCopyText: () => string | undefined) => {
|
||||
useUtilsEffect(({ signal }) => {
|
||||
addEventListener('keydown', (e) => {
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ import { options } from './optionsStorage'
|
|||
import BossBarOverlayProvider from './react/BossBarOverlayProvider'
|
||||
import DebugEdges from './react/DebugEdges'
|
||||
import LibraryVersions from './react/components/LibraryVersions'
|
||||
import GameInteractionOverlay from './react/GameInteractionOverlay'
|
||||
|
||||
const RobustPortal = ({ children, to }) => {
|
||||
return createPortal(<PerComponentErrorBoundary>{children}</PerComponentErrorBoundary>, to)
|
||||
|
|
@ -117,6 +118,7 @@ const InGameUi = () => {
|
|||
<RobustPortal to={document.querySelector('#ui-root')}>
|
||||
{/* apply scaling */}
|
||||
<div style={{ display: showUI ? 'block' : 'none' }}>
|
||||
<GameInteractionOverlay zIndex={7} />
|
||||
{!disabledUiParts.includes('death-screen') && <DeathScreenProvider />}
|
||||
{!disabledUiParts.includes('debug-overlay') && <DebugOverlay />}
|
||||
{!disabledUiParts.includes('mobile-top-buttons') && <MobileTopButtons />}
|
||||
|
|
|
|||
|
|
@ -4,14 +4,14 @@ import fs from 'fs'
|
|||
import JSZip from 'jszip'
|
||||
import { proxy, subscribe } from 'valtio'
|
||||
import { WorldRendererThree } from 'prismarine-viewer/viewer/lib/worldrendererThree'
|
||||
import { mkdirRecursive, removeFileRecursiveAsync } from './browserfs'
|
||||
import { collectFilesToCopy, copyFilesAsyncWithProgress, mkdirRecursive, removeFileRecursiveAsync } from './browserfs'
|
||||
import { setLoadingScreenStatus } from './utils'
|
||||
import { showNotification } from './react/NotificationProvider'
|
||||
import { options } from './optionsStorage'
|
||||
import { showOptionsModal } from './react/SelectOption'
|
||||
import { appStatusState } from './react/AppStatusProvider'
|
||||
import { appReplacableResources, resourcesContentOriginal } from './generated/resources'
|
||||
import { loadedGameState, miscUiState } from './globalState'
|
||||
import { gameAdditionalState, miscUiState } from './globalState'
|
||||
import { watchUnloadForCleanup } from './gameUnload'
|
||||
|
||||
export const resourcePackState = proxy({
|
||||
|
|
@ -32,7 +32,7 @@ const texturePackBasePath = '/data/resourcePacks/'
|
|||
export const uninstallTexturePack = async (name = 'default') => {
|
||||
if (await existsAsync('/resourcepack/pack.mcmeta')) {
|
||||
await removeFileRecursiveAsync('/resourcepack')
|
||||
loadedGameState.usingServerResourcePack = false
|
||||
gameAdditionalState.usingServerResourcePack = false
|
||||
}
|
||||
const basePath = texturePackBasePath + name
|
||||
if (!(await existsAsync(basePath))) return
|
||||
|
|
@ -113,7 +113,7 @@ export const installTexturePack = async (file: File | ArrayBuffer, displayName =
|
|||
done++
|
||||
upStatus()
|
||||
}))
|
||||
console.log('done')
|
||||
console.log('resource pack install done')
|
||||
await completeTexturePackInstall(displayName, name, isServer)
|
||||
}
|
||||
|
||||
|
|
@ -129,7 +129,7 @@ export const completeTexturePackInstall = async (displayName: string | undefined
|
|||
showNotification('Texturepack installed & enabled')
|
||||
await updateTexturePackInstalledState()
|
||||
if (isServer) {
|
||||
loadedGameState.usingServerResourcePack = true
|
||||
gameAdditionalState.usingServerResourcePack = true
|
||||
} else {
|
||||
options.enabledResourcepack = name
|
||||
}
|
||||
|
|
@ -157,7 +157,7 @@ const getSizeFromImage = async (filePath: string) => {
|
|||
return probeImg.width
|
||||
}
|
||||
|
||||
export const getActiveTexturepackBasePath = async () => {
|
||||
export const getActiveResourcepackBasePath = async () => {
|
||||
if (await existsAsync('/resourcepack/pack.mcmeta')) {
|
||||
return '/resourcepack'
|
||||
}
|
||||
|
|
@ -198,7 +198,7 @@ const getFilesMapFromDir = async (dir: string) => {
|
|||
}
|
||||
|
||||
export const getResourcepackTiles = async (type: 'blocks' | 'items', existingTextures: string[]) => {
|
||||
const basePath = await getActiveTexturepackBasePath()
|
||||
const basePath = await getActiveResourcepackBasePath()
|
||||
if (!basePath) return
|
||||
let firstTextureSize: number | undefined
|
||||
const namespaces = await fs.promises.readdir(join(basePath, 'assets'))
|
||||
|
|
@ -282,7 +282,7 @@ const prepareBlockstatesAndModels = async () => {
|
|||
viewer.world.customBlockStates = {}
|
||||
viewer.world.customModels = {}
|
||||
const usedTextures = new Set<string>()
|
||||
const basePath = await getActiveTexturepackBasePath()
|
||||
const basePath = await getActiveResourcepackBasePath()
|
||||
if (!basePath) return
|
||||
if (appStatusState.status) {
|
||||
setLoadingScreenStatus('Reading resource pack blockstates and models')
|
||||
|
|
@ -361,6 +361,7 @@ export const onAppLoad = () => {
|
|||
customEvents.on('mineflayerBotCreated', () => {
|
||||
// todo also handle resourcePack
|
||||
const handleResourcePackRequest = async (packet) => {
|
||||
console.log('Received resource pack request', packet)
|
||||
if (options.serverResourcePacks === 'never') return
|
||||
const promptMessagePacket = ('promptMessage' in packet && packet.promptMessage) ? packet.promptMessage : undefined
|
||||
const promptMessageText = promptMessagePacket ? '' : 'Do you want to use server resource pack?'
|
||||
|
|
@ -397,7 +398,7 @@ export const onAppLoad = () => {
|
|||
}
|
||||
|
||||
const updateAllReplacableTextures = async () => {
|
||||
const basePath = await getActiveTexturepackBasePath()
|
||||
const basePath = await getActiveResourcepackBasePath()
|
||||
const setCustomCss = async (path: string | null, varName: string, repeat = 1) => {
|
||||
if (path && await existsAsync(path)) {
|
||||
const contents = await fs.promises.readFile(path, 'base64')
|
||||
|
|
@ -462,3 +463,29 @@ const updateTextures = async () => {
|
|||
export const resourcepackReload = async (version) => {
|
||||
await updateTextures()
|
||||
}
|
||||
|
||||
export const copyServerResourcePackToRegular = async (name = 'default') => {
|
||||
// Check if server resource pack exists
|
||||
if (!(await existsAsync('/resourcepack/pack.mcmeta'))) {
|
||||
throw new Error('No server resource pack is currently installed')
|
||||
}
|
||||
|
||||
// Get display name from server resource pack if available
|
||||
let displayName
|
||||
try {
|
||||
displayName = await fs.promises.readFile('/resourcepack/name.txt', 'utf8')
|
||||
} catch {
|
||||
displayName = 'Server Resource Pack'
|
||||
}
|
||||
|
||||
// Copy all files from server resource pack to regular location
|
||||
const destPath = texturePackBasePath + name
|
||||
await mkdirRecursive(destPath)
|
||||
|
||||
setLoadingScreenStatus('Copying server resource pack to regular location')
|
||||
await copyFilesAsyncWithProgress('/resourcepack', destPath, true, ' (server -> regular)')
|
||||
|
||||
// Complete the installation
|
||||
await completeTexturePackInstall(displayName, name, false)
|
||||
showNotification('Server resource pack copied to regular location')
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { isCypress } from './standaloneUtils'
|
|||
|
||||
// might not resolve at all
|
||||
export const registerServiceWorker = async () => {
|
||||
if (process.env.DISABLE_SERVICE_WORKER) return
|
||||
if (!('serviceWorker' in navigator)) return
|
||||
if (!isCypress() && process.env.NODE_ENV !== 'development') {
|
||||
return new Promise<void>(resolve => {
|
||||
|
|
|
|||
|
|
@ -1,50 +1,51 @@
|
|||
import { subscribeKey } from 'valtio/utils'
|
||||
import { Vec3 } from 'vec3'
|
||||
import { versionToMajor, versionToNumber, versionsMapToMajor } from 'prismarine-viewer/viewer/prepare/utils'
|
||||
import { versionToNumber } from 'prismarine-viewer/viewer/prepare/utils'
|
||||
import { loadScript } from 'prismarine-viewer/viewer/lib/utils'
|
||||
import type { Block } from 'prismarine-block'
|
||||
import { miscUiState } from './globalState'
|
||||
import { options } from './optionsStorage'
|
||||
import { loadOrPlaySound } from './basicSounds'
|
||||
import { showNotification } from './react/NotificationProvider'
|
||||
import { subscribeKey } from 'valtio/utils'
|
||||
import { miscUiState } from '../globalState'
|
||||
import { options } from '../optionsStorage'
|
||||
import { loadOrPlaySound } from '../basicSounds'
|
||||
import { getActiveResourcepackBasePath, resourcePackState } from '../resourcePack'
|
||||
import { createSoundMap, SoundMap } from './soundsMap'
|
||||
import { musicSystem } from './musicSystem'
|
||||
|
||||
const globalObject = window as {
|
||||
allSoundsMap?: Record<string, Record<string, string>>,
|
||||
allSoundsVersionedMap?: Record<string, string[]>,
|
||||
let soundMap: SoundMap | undefined
|
||||
|
||||
const updateResourcePack = async () => {
|
||||
if (!soundMap) return
|
||||
soundMap.activeResourcePackBasePath = await getActiveResourcepackBasePath() ?? undefined
|
||||
}
|
||||
|
||||
let musicInterval: ReturnType<typeof setInterval> | null = null
|
||||
|
||||
subscribeKey(miscUiState, 'gameLoaded', async () => {
|
||||
if (!miscUiState.gameLoaded) return
|
||||
const soundsLegacyMap = window.allSoundsVersionedMap as Record<string, string[]>
|
||||
const { allSoundsMap } = globalObject
|
||||
const allSoundsMeta = window.allSoundsMeta as { format: string, baseUrl: string }
|
||||
if (!allSoundsMap) {
|
||||
if (!miscUiState.gameLoaded || !loadedData.sounds) {
|
||||
stopMusicSystem()
|
||||
soundMap?.quit()
|
||||
return
|
||||
}
|
||||
|
||||
const allSoundsMajor = versionsMapToMajor(allSoundsMap)
|
||||
const soundsMap = allSoundsMajor[versionToMajor(bot.version)] ?? Object.values(allSoundsMajor)[0]
|
||||
|
||||
if (!soundsMap || !miscUiState.gameLoaded || !loadedData.sounds) {
|
||||
return
|
||||
}
|
||||
|
||||
// const soundsPerId = Object.fromEntries(Object.entries(soundsMap).map(([id, sound]) => [+id.split(';')[0], sound]))
|
||||
const soundsPerName = Object.fromEntries(Object.entries(soundsMap).map(([id, sound]) => [id.split(';')[1], sound]))
|
||||
console.log(`Loading sounds for version ${bot.version}. Resourcepack state: ${JSON.stringify(resourcePackState)}`)
|
||||
soundMap = createSoundMap(bot.version) ?? undefined
|
||||
if (!soundMap) return
|
||||
void updateResourcePack()
|
||||
startMusicSystem()
|
||||
|
||||
const playGeneralSound = async (soundKey: string, position?: Vec3, volume = 1, pitch?: number) => {
|
||||
if (!options.volume) return
|
||||
const soundStaticData = soundsPerName[soundKey]?.split(';')
|
||||
if (!soundStaticData) return
|
||||
const soundVolume = +soundStaticData[0]!
|
||||
const soundPath = soundStaticData[1]!
|
||||
const versionedSound = getVersionedSound(bot.version, soundPath, Object.entries(soundsLegacyMap))
|
||||
// todo test versionedSound
|
||||
const url = allSoundsMeta.baseUrl.replace(/\/$/, '') + (versionedSound ? `/${versionedSound}` : '') + '/minecraft/sounds/' + soundPath + '.' + allSoundsMeta.format
|
||||
const isMuted = options.mutedSounds.includes(soundKey) || options.mutedSounds.includes(soundPath) || options.volume === 0
|
||||
if (!options.volume || !soundMap) return
|
||||
const soundData = await soundMap.getSoundUrl(soundKey, volume)
|
||||
if (!soundData) return
|
||||
|
||||
const isMuted = options.mutedSounds.includes(soundKey) || options.volume === 0
|
||||
if (position) {
|
||||
if (!isMuted) {
|
||||
viewer.playSound(position, url, soundVolume * Math.max(Math.min(volume, 1), 0) * (options.volume / 100), Math.max(Math.min(pitch ?? 1, 2), 0.5))
|
||||
viewer.playSound(
|
||||
position,
|
||||
soundData.url,
|
||||
soundData.volume * (options.volume / 100),
|
||||
Math.max(Math.min(pitch ?? 1, 2), 0.5)
|
||||
)
|
||||
}
|
||||
if (getDistance(bot.entity.position, position) < 4 * 16) {
|
||||
lastPlayedSounds.lastServerPlayed[soundKey] ??= { count: 0, last: 0 }
|
||||
|
|
@ -53,7 +54,7 @@ subscribeKey(miscUiState, 'gameLoaded', async () => {
|
|||
}
|
||||
} else {
|
||||
if (!isMuted) {
|
||||
await loadOrPlaySound(url, volume)
|
||||
await loadOrPlaySound(soundData.url, volume)
|
||||
}
|
||||
lastPlayedSounds.lastClientPlayed.push(soundKey)
|
||||
if (lastPlayedSounds.lastClientPlayed.length > 10) {
|
||||
|
|
@ -61,84 +62,72 @@ subscribeKey(miscUiState, 'gameLoaded', async () => {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
const musicStartCheck = async (force = false) => {
|
||||
if (!soundMap) return
|
||||
// 20% chance to start music
|
||||
if (Math.random() > 0.2 && !force && !options.enableMusic) return
|
||||
|
||||
const musicKeys = ['music.game']
|
||||
if (bot.game.gameMode === 'creative') {
|
||||
musicKeys.push('music.creative')
|
||||
}
|
||||
const randomMusicKey = musicKeys[Math.floor(Math.random() * musicKeys.length)]
|
||||
const soundData = await soundMap.getSoundUrl(randomMusicKey)
|
||||
if (!soundData) return
|
||||
await musicSystem.playMusic(soundData.url, soundData.volume)
|
||||
}
|
||||
|
||||
function startMusicSystem () {
|
||||
if (musicInterval) return
|
||||
musicInterval = setInterval(() => {
|
||||
void musicStartCheck()
|
||||
}, 10_000)
|
||||
}
|
||||
|
||||
window.forceStartMusic = () => {
|
||||
void musicStartCheck(true)
|
||||
}
|
||||
|
||||
|
||||
function stopMusicSystem () {
|
||||
if (musicInterval) {
|
||||
clearInterval(musicInterval)
|
||||
musicInterval = null
|
||||
}
|
||||
}
|
||||
|
||||
const playHardcodedSound = async (soundKey: string, position?: Vec3, volume = 1, pitch?: number) => {
|
||||
await playGeneralSound(soundKey, position, volume, pitch)
|
||||
}
|
||||
|
||||
bot.on('soundEffectHeard', async (soundId, position, volume, pitch) => {
|
||||
await playHardcodedSound(soundId, position, volume, pitch)
|
||||
})
|
||||
|
||||
bot.on('hardcodedSoundEffectHeard', async (soundIdNum, soundCategory, position, volume, pitch) => {
|
||||
const fixOffset = versionToNumber('1.20.4') === versionToNumber(bot.version) ? -1 : 0
|
||||
const soundKey = loadedData.sounds[soundIdNum + fixOffset]?.name
|
||||
if (soundKey === undefined) return
|
||||
await playGeneralSound(soundKey, position, volume, pitch)
|
||||
})
|
||||
// workaround as mineflayer doesn't support soundEvent
|
||||
|
||||
bot._client.on('sound_effect', async (packet) => {
|
||||
const soundResource = packet['soundEvent']?.resource as string | undefined
|
||||
if (packet.soundId !== 0 || !soundResource) return
|
||||
const pos = new Vec3(packet.x / 8, packet.y / 8, packet.z / 8)
|
||||
await playHardcodedSound(soundResource.replace('minecraft:', ''), pos, packet.volume, packet.pitch)
|
||||
})
|
||||
|
||||
bot.on('entityHurt', async (entity) => {
|
||||
if (entity.id === bot.entity.id) {
|
||||
await playHardcodedSound('entity.player.hurt')
|
||||
}
|
||||
})
|
||||
|
||||
const useBlockSound = (blockName: string, category: string, fallback: string) => {
|
||||
blockName = {
|
||||
// todo somehow generated, not full
|
||||
grass_block: 'grass',
|
||||
tall_grass: 'grass',
|
||||
fern: 'grass',
|
||||
large_fern: 'grass',
|
||||
dead_bush: 'grass',
|
||||
seagrass: 'grass',
|
||||
tall_seagrass: 'grass',
|
||||
kelp: 'grass',
|
||||
kelp_plant: 'grass',
|
||||
sugar_cane: 'grass',
|
||||
bamboo: 'grass',
|
||||
vine: 'grass',
|
||||
nether_sprouts: 'grass',
|
||||
nether_wart: 'grass',
|
||||
twisting_vines: 'grass',
|
||||
weeping_vines: 'grass',
|
||||
|
||||
cobblestone: 'stone',
|
||||
stone_bricks: 'stone',
|
||||
mossy_stone_bricks: 'stone',
|
||||
cracked_stone_bricks: 'stone',
|
||||
chiseled_stone_bricks: 'stone',
|
||||
stone_brick_slab: 'stone',
|
||||
stone_brick_stairs: 'stone',
|
||||
stone_brick_wall: 'stone',
|
||||
polished_granite: 'stone',
|
||||
}[blockName] ?? blockName
|
||||
const key = 'block.' + blockName + '.' + category
|
||||
return soundsPerName[key] ? key : fallback
|
||||
}
|
||||
|
||||
const getStepSound = (blockUnder: Block) => {
|
||||
// const soundsMap = globalObject.allSoundsMap?.[bot.version]
|
||||
// if (!soundsMap) return
|
||||
// let soundResult = 'block.stone.step'
|
||||
// for (const x of Object.keys(soundsMap).map(n => n.split(';')[1])) {
|
||||
// const match = /block\.(.+)\.step/.exec(x)
|
||||
// const block = match?.[1]
|
||||
// if (!block) continue
|
||||
// if (loadedData.blocksByName[block]?.name === blockUnder.name) {
|
||||
// soundResult = x
|
||||
// break
|
||||
// }
|
||||
// }
|
||||
return useBlockSound(blockUnder.name, 'step', 'block.stone.step')
|
||||
}
|
||||
|
||||
let lastStepSound = 0
|
||||
const movementHappening = async () => {
|
||||
if (!bot.player) return // no info yet
|
||||
if (!bot.player || !soundMap) return // no info yet
|
||||
const VELOCITY_THRESHOLD = 0.1
|
||||
const { x, z, y } = bot.player.entity.velocity
|
||||
if (bot.entity.onGround && Math.abs(x) < VELOCITY_THRESHOLD && (Math.abs(z) > VELOCITY_THRESHOLD || Math.abs(y) > VELOCITY_THRESHOLD)) {
|
||||
|
|
@ -146,9 +135,9 @@ subscribeKey(miscUiState, 'gameLoaded', async () => {
|
|||
if (Date.now() - lastStepSound > 300) {
|
||||
const blockUnder = bot.world.getBlock(bot.entity.position.offset(0, -1, 0))
|
||||
if (blockUnder) {
|
||||
const stepSound = getStepSound(blockUnder)
|
||||
const stepSound = soundMap.getStepSound(blockUnder.name)
|
||||
if (stepSound) {
|
||||
await playHardcodedSound(stepSound, undefined, 0.6)// todo not sure why 0.6
|
||||
await playHardcodedSound(stepSound, undefined, 0.6)
|
||||
lastStepSound = Date.now()
|
||||
}
|
||||
}
|
||||
|
|
@ -157,8 +146,8 @@ subscribeKey(miscUiState, 'gameLoaded', async () => {
|
|||
}
|
||||
|
||||
const playBlockBreak = async (blockName: string, position?: Vec3) => {
|
||||
const sound = useBlockSound(blockName, 'break', 'block.stone.break')
|
||||
|
||||
if (!soundMap) return
|
||||
const sound = soundMap.getBreakSound(blockName)
|
||||
await playHardcodedSound(sound, position, 0.6, 1)
|
||||
}
|
||||
|
||||
|
|
@ -200,8 +189,8 @@ subscribeKey(miscUiState, 'gameLoaded', async () => {
|
|||
if (effectId === 1010) {
|
||||
console.log('play record', data)
|
||||
}
|
||||
// todo add support for all current world events
|
||||
})
|
||||
|
||||
let diggingBlock: Block | null = null
|
||||
customEvents.on('digStart', () => {
|
||||
diggingBlock = bot.blockAtCursor(5)
|
||||
|
|
@ -214,40 +203,14 @@ subscribeKey(miscUiState, 'gameLoaded', async () => {
|
|||
}
|
||||
|
||||
registerEvents()
|
||||
|
||||
// 1.20+ soundEffectHeard is broken atm
|
||||
// bot._client.on('packet', (data, { name }, buffer) => {
|
||||
// if (name === 'sound_effect') {
|
||||
// console.log(data, buffer)
|
||||
// }
|
||||
// })
|
||||
})
|
||||
|
||||
// todo
|
||||
// const music = {
|
||||
// activated: false,
|
||||
// playing: '',
|
||||
// activate () {
|
||||
// const gameMusic = Object.entries(globalObject.allSoundsMap?.[bot.version] ?? {}).find(([id, sound]) => sound.includes('music.game'))
|
||||
// if (!gameMusic) return
|
||||
// const soundPath = gameMusic[0].split(';')[1]
|
||||
// const next = () => {}
|
||||
// }
|
||||
// }
|
||||
|
||||
const getVersionedSound = (version: string, item: string, itemsMapSortedEntries: Array<[string, string[]]>) => {
|
||||
const verNumber = versionToNumber(version)
|
||||
for (const [itemsVer, items] of itemsMapSortedEntries) {
|
||||
// 1.18 < 1.18.1
|
||||
// 1.13 < 1.13.2
|
||||
if (items.includes(item) && verNumber <= versionToNumber(itemsVer)) {
|
||||
return itemsVer
|
||||
}
|
||||
}
|
||||
}
|
||||
subscribeKey(resourcePackState, 'resourcePackInstalled', async () => {
|
||||
await updateResourcePack()
|
||||
})
|
||||
|
||||
export const downloadSoundsIfNeeded = async () => {
|
||||
if (!globalObject.allSoundsMap) {
|
||||
if (!window.allSoundsMap) {
|
||||
try {
|
||||
await loadScript('./sounds.js')
|
||||
} catch (err) {
|
||||
33
src/sounds/musicSystem.ts
Normal file
33
src/sounds/musicSystem.ts
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import { loadOrPlaySound } from '../basicSounds'
|
||||
import { options } from '../optionsStorage'
|
||||
|
||||
class MusicSystem {
|
||||
private currentMusic: string | null = null
|
||||
|
||||
async playMusic (url: string, musicVolume = 1) {
|
||||
if (!options.enableMusic || this.currentMusic) return
|
||||
|
||||
try {
|
||||
const { onEnded } = await loadOrPlaySound(url, 0.5 * musicVolume, 5000) ?? {}
|
||||
|
||||
if (!onEnded) return
|
||||
|
||||
this.currentMusic = url
|
||||
|
||||
onEnded(() => {
|
||||
this.currentMusic = null
|
||||
})
|
||||
} catch (err) {
|
||||
console.warn('Failed to play music:', err)
|
||||
this.currentMusic = null
|
||||
}
|
||||
}
|
||||
|
||||
stopMusic () {
|
||||
if (this.currentMusic) {
|
||||
this.currentMusic = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const musicSystem = new MusicSystem()
|
||||
347
src/sounds/soundsMap.ts
Normal file
347
src/sounds/soundsMap.ts
Normal file
|
|
@ -0,0 +1,347 @@
|
|||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import { versionsMapToMajor, versionToMajor, versionToNumber } from 'prismarine-viewer/viewer/prepare/utils'
|
||||
|
||||
import { stopAllSounds } from '../basicSounds'
|
||||
import { musicSystem } from './musicSystem'
|
||||
|
||||
interface SoundMeta {
|
||||
format: string
|
||||
baseUrl: string
|
||||
}
|
||||
|
||||
interface SoundData {
|
||||
volume: number
|
||||
path: string
|
||||
}
|
||||
|
||||
interface SoundMapData {
|
||||
allSoundsMap: Record<string, Record<string, string>>
|
||||
soundsLegacyMap: Record<string, string[]>
|
||||
soundsMeta: SoundMeta
|
||||
}
|
||||
|
||||
interface BlockSoundMap {
|
||||
[blockName: string]: string
|
||||
}
|
||||
|
||||
interface SoundEntry {
|
||||
file: string
|
||||
weight: number
|
||||
volume: number
|
||||
}
|
||||
|
||||
export class SoundMap {
|
||||
private readonly soundsPerName: Record<string, SoundEntry[]>
|
||||
private readonly existingResourcePackPaths: Set<string>
|
||||
public activeResourcePackBasePath: string | undefined
|
||||
|
||||
constructor (
|
||||
private readonly soundData: SoundMapData,
|
||||
private readonly version: string
|
||||
) {
|
||||
const allSoundsMajor = versionsMapToMajor(soundData.allSoundsMap)
|
||||
const soundsMap = allSoundsMajor[versionToMajor(version)] ?? Object.values(allSoundsMajor)[0]
|
||||
this.soundsPerName = Object.fromEntries(
|
||||
Object.entries(soundsMap).map(([id, soundsStr]) => {
|
||||
const sounds = soundsStr.split(',').map(s => {
|
||||
const [volume, name, weight] = s.split(';')
|
||||
if (isNaN(Number(volume))) throw new Error('volume is not a number')
|
||||
if (isNaN(Number(weight))) {
|
||||
// debugger
|
||||
throw new TypeError('weight is not a number')
|
||||
}
|
||||
return {
|
||||
file: name,
|
||||
weight: Number(weight),
|
||||
volume: Number(volume)
|
||||
}
|
||||
})
|
||||
return [id.split(';')[1], sounds]
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
async updateExistingResourcePackPaths () {
|
||||
if (!this.activeResourcePackBasePath) return
|
||||
// todo support sounds.js from resource pack
|
||||
const soundsBasePath = path.join(this.activeResourcePackBasePath, 'assets/minecraft/sounds')
|
||||
// scan recursively for sounds files
|
||||
const scan = async (dir: string) => {
|
||||
const entries = await fs.promises.readdir(dir, { withFileTypes: true })
|
||||
for (const entry of entries) {
|
||||
const entryPath = path.join(dir, entry.name)
|
||||
if (entry.isDirectory()) {
|
||||
await scan(entryPath)
|
||||
} else if (entry.isFile() && entry.name.endsWith('.ogg')) {
|
||||
const relativePath = path.relative(soundsBasePath, entryPath)
|
||||
this.existingResourcePackPaths.add(relativePath)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await scan(soundsBasePath)
|
||||
}
|
||||
|
||||
async getSoundUrl (soundKey: string, volume = 1): Promise<{ url: string; volume: number } | undefined> {
|
||||
const sounds = this.soundsPerName[soundKey]
|
||||
if (!sounds?.length) return undefined
|
||||
|
||||
// Pick a random sound based on weights
|
||||
const totalWeight = sounds.reduce((sum, s) => sum + s.weight, 0)
|
||||
let random = Math.random() * totalWeight
|
||||
const sound = sounds.find(s => {
|
||||
random -= s.weight
|
||||
return random <= 0
|
||||
}) ?? sounds[0]
|
||||
|
||||
const versionedSound = this.getVersionedSound(sound.file)
|
||||
|
||||
let url = this.soundData.soundsMeta.baseUrl.replace(/\/$/, '') +
|
||||
(versionedSound ? `/${versionedSound}` : '') +
|
||||
'/minecraft/sounds/' +
|
||||
sound.file +
|
||||
'.' +
|
||||
this.soundData.soundsMeta.format
|
||||
|
||||
// Try loading from resource pack file first
|
||||
if (this.activeResourcePackBasePath) {
|
||||
const tryFormat = async (format: string) => {
|
||||
try {
|
||||
const resourcePackPath = path.join(this.activeResourcePackBasePath!, `/assets/minecraft/sounds/${sound.file}.${format}`)
|
||||
const fileData = await fs.promises.readFile(resourcePackPath)
|
||||
url = `data:audio/${format};base64,${fileData.toString('base64')}`
|
||||
return true
|
||||
} catch (err) {
|
||||
}
|
||||
}
|
||||
const success = await tryFormat(this.soundData.soundsMeta.format)
|
||||
if (!success && this.soundData.soundsMeta.format !== 'ogg') {
|
||||
await tryFormat('ogg')
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
url,
|
||||
volume: sound.volume * Math.max(Math.min(volume, 1), 0)
|
||||
}
|
||||
}
|
||||
|
||||
private getVersionedSound (item: string): string | undefined {
|
||||
const verNumber = versionToNumber(this.version)
|
||||
const entries = Object.entries(this.soundData.soundsLegacyMap)
|
||||
for (const [itemsVer, items] of entries) {
|
||||
if (items.includes(item) && verNumber <= versionToNumber(itemsVer)) {
|
||||
return itemsVer
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
getBlockSound (blockName: string, category: string, fallback: string): string {
|
||||
const mappedName = blockSoundAliases[blockName] ?? blockName
|
||||
const key = `block.${mappedName}.${category}`
|
||||
return this.soundsPerName[key] ? key : fallback
|
||||
}
|
||||
|
||||
getStepSound (blockName: string): string {
|
||||
return this.getBlockSound(blockName, 'step', 'block.stone.step')
|
||||
}
|
||||
|
||||
getBreakSound (blockName: string): string {
|
||||
return this.getBlockSound(blockName, 'break', 'block.stone.break')
|
||||
}
|
||||
|
||||
quit () {
|
||||
musicSystem.stopMusic()
|
||||
stopAllSounds()
|
||||
}
|
||||
}
|
||||
|
||||
export function createSoundMap (version: string): SoundMap | null {
|
||||
const globalObject = window as {
|
||||
allSoundsMap?: Record<string, Record<string, string>>,
|
||||
allSoundsVersionedMap?: Record<string, string[]>,
|
||||
allSoundsMeta?: { format: string, baseUrl: string }
|
||||
}
|
||||
if (!globalObject.allSoundsMap) return null
|
||||
return new SoundMap({
|
||||
allSoundsMap: globalObject.allSoundsMap,
|
||||
soundsLegacyMap: globalObject.allSoundsVersionedMap ?? {},
|
||||
soundsMeta: globalObject.allSoundsMeta!
|
||||
}, version)
|
||||
}
|
||||
|
||||
// Block name mappings for sound effects
|
||||
const blockSoundAliases: BlockSoundMap = {
|
||||
// Grass-like blocks
|
||||
grass_block: 'grass',
|
||||
tall_grass: 'grass',
|
||||
fern: 'grass',
|
||||
large_fern: 'grass',
|
||||
dead_bush: 'grass',
|
||||
seagrass: 'grass',
|
||||
tall_seagrass: 'grass',
|
||||
kelp: 'grass',
|
||||
kelp_plant: 'grass',
|
||||
sugar_cane: 'grass',
|
||||
bamboo: 'grass',
|
||||
vine: 'grass',
|
||||
nether_sprouts: 'grass',
|
||||
nether_wart: 'grass',
|
||||
twisting_vines: 'grass',
|
||||
weeping_vines: 'grass',
|
||||
sweet_berry_bush: 'grass',
|
||||
glow_lichen: 'grass',
|
||||
moss_carpet: 'grass',
|
||||
moss_block: 'grass',
|
||||
hanging_roots: 'grass',
|
||||
spore_blossom: 'grass',
|
||||
small_dripleaf: 'grass',
|
||||
big_dripleaf: 'grass',
|
||||
flowering_azalea: 'grass',
|
||||
azalea: 'grass',
|
||||
azalea_leaves: 'grass',
|
||||
flowering_azalea_leaves: 'grass',
|
||||
|
||||
// Stone-like blocks
|
||||
cobblestone: 'stone',
|
||||
stone_bricks: 'stone',
|
||||
mossy_stone_bricks: 'stone',
|
||||
cracked_stone_bricks: 'stone',
|
||||
chiseled_stone_bricks: 'stone',
|
||||
stone_brick_slab: 'stone',
|
||||
stone_brick_stairs: 'stone',
|
||||
stone_brick_wall: 'stone',
|
||||
polished_granite: 'stone',
|
||||
granite: 'stone',
|
||||
andesite: 'stone',
|
||||
diorite: 'stone',
|
||||
polished_andesite: 'stone',
|
||||
polished_diorite: 'stone',
|
||||
deepslate: 'deepslate',
|
||||
cobbled_deepslate: 'deepslate',
|
||||
polished_deepslate: 'deepslate',
|
||||
deepslate_bricks: 'deepslate_bricks',
|
||||
deepslate_tiles: 'deepslate_tiles',
|
||||
calcite: 'stone',
|
||||
tuff: 'stone',
|
||||
smooth_stone: 'stone',
|
||||
smooth_sandstone: 'stone',
|
||||
smooth_quartz: 'stone',
|
||||
smooth_red_sandstone: 'stone',
|
||||
|
||||
// Wood-like blocks
|
||||
oak_planks: 'wood',
|
||||
spruce_planks: 'wood',
|
||||
birch_planks: 'wood',
|
||||
jungle_planks: 'wood',
|
||||
acacia_planks: 'wood',
|
||||
dark_oak_planks: 'wood',
|
||||
crimson_planks: 'wood',
|
||||
warped_planks: 'wood',
|
||||
oak_log: 'wood',
|
||||
spruce_log: 'wood',
|
||||
birch_log: 'wood',
|
||||
jungle_log: 'wood',
|
||||
acacia_log: 'wood',
|
||||
dark_oak_log: 'wood',
|
||||
crimson_stem: 'stem',
|
||||
warped_stem: 'stem',
|
||||
|
||||
// Metal blocks
|
||||
iron_block: 'metal',
|
||||
gold_block: 'metal',
|
||||
copper_block: 'copper',
|
||||
exposed_copper: 'copper',
|
||||
weathered_copper: 'copper',
|
||||
oxidized_copper: 'copper',
|
||||
netherite_block: 'netherite_block',
|
||||
ancient_debris: 'ancient_debris',
|
||||
lodestone: 'lodestone',
|
||||
chain: 'chain',
|
||||
anvil: 'anvil',
|
||||
chipped_anvil: 'anvil',
|
||||
damaged_anvil: 'anvil',
|
||||
|
||||
// Glass blocks
|
||||
glass: 'glass',
|
||||
glass_pane: 'glass',
|
||||
white_stained_glass: 'glass',
|
||||
orange_stained_glass: 'glass',
|
||||
magenta_stained_glass: 'glass',
|
||||
light_blue_stained_glass: 'glass',
|
||||
yellow_stained_glass: 'glass',
|
||||
lime_stained_glass: 'glass',
|
||||
pink_stained_glass: 'glass',
|
||||
gray_stained_glass: 'glass',
|
||||
light_gray_stained_glass: 'glass',
|
||||
cyan_stained_glass: 'glass',
|
||||
purple_stained_glass: 'glass',
|
||||
blue_stained_glass: 'glass',
|
||||
brown_stained_glass: 'glass',
|
||||
green_stained_glass: 'glass',
|
||||
red_stained_glass: 'glass',
|
||||
black_stained_glass: 'glass',
|
||||
tinted_glass: 'glass',
|
||||
|
||||
// Wool blocks
|
||||
white_wool: 'wool',
|
||||
orange_wool: 'wool',
|
||||
magenta_wool: 'wool',
|
||||
light_blue_wool: 'wool',
|
||||
yellow_wool: 'wool',
|
||||
lime_wool: 'wool',
|
||||
pink_wool: 'wool',
|
||||
gray_wool: 'wool',
|
||||
light_gray_wool: 'wool',
|
||||
cyan_wool: 'wool',
|
||||
purple_wool: 'wool',
|
||||
blue_wool: 'wool',
|
||||
brown_wool: 'wool',
|
||||
green_wool: 'wool',
|
||||
red_wool: 'wool',
|
||||
black_wool: 'wool',
|
||||
|
||||
// Nether blocks
|
||||
netherrack: 'netherrack',
|
||||
nether_bricks: 'nether_bricks',
|
||||
red_nether_bricks: 'nether_bricks',
|
||||
nether_wart_block: 'wart_block',
|
||||
warped_wart_block: 'wart_block',
|
||||
soul_sand: 'soul_sand',
|
||||
soul_soil: 'soul_soil',
|
||||
basalt: 'basalt',
|
||||
polished_basalt: 'basalt',
|
||||
blackstone: 'gilded_blackstone',
|
||||
gilded_blackstone: 'gilded_blackstone',
|
||||
|
||||
// Amethyst blocks
|
||||
amethyst_block: 'amethyst_block',
|
||||
amethyst_cluster: 'amethyst_cluster',
|
||||
large_amethyst_bud: 'large_amethyst_bud',
|
||||
medium_amethyst_bud: 'medium_amethyst_bud',
|
||||
small_amethyst_bud: 'small_amethyst_bud',
|
||||
|
||||
// Miscellaneous
|
||||
sand: 'sand',
|
||||
red_sand: 'sand',
|
||||
gravel: 'gravel',
|
||||
snow: 'snow',
|
||||
snow_block: 'snow',
|
||||
powder_snow: 'powder_snow',
|
||||
ice: 'glass',
|
||||
packed_ice: 'glass',
|
||||
blue_ice: 'glass',
|
||||
slime_block: 'slime_block',
|
||||
honey_block: 'honey_block',
|
||||
scaffolding: 'scaffolding',
|
||||
ladder: 'ladder',
|
||||
lantern: 'lantern',
|
||||
soul_lantern: 'lantern',
|
||||
pointed_dripstone: 'pointed_dripstone',
|
||||
dripstone_block: 'dripstone_block',
|
||||
rooted_dirt: 'rooted_dirt',
|
||||
sculk_sensor: 'sculk_sensor',
|
||||
shroomlight: 'shroomlight'
|
||||
}
|
||||
8
src/sounds/testSounds.ts
Normal file
8
src/sounds/testSounds.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import { createSoundMap } from './soundsMap'
|
||||
|
||||
//@ts-expect-error
|
||||
globalThis.window = {}
|
||||
require('../../generated/sounds.js')
|
||||
|
||||
const soundMap = createSoundMap('1.20.1')
|
||||
console.log(soundMap?.getSoundUrl('ambient.cave'))
|
||||
|
|
@ -6,6 +6,7 @@ import * as THREE from 'three'
|
|||
import { Vec3 } from 'vec3'
|
||||
import { LineMaterial } from 'three-stdlib'
|
||||
import { Entity } from 'prismarine-entity'
|
||||
import { subscribeKey } from 'valtio/utils'
|
||||
import destroyStage0 from '../assets/destroy_stage_0.png'
|
||||
import destroyStage1 from '../assets/destroy_stage_1.png'
|
||||
import destroyStage2 from '../assets/destroy_stage_2.png'
|
||||
|
|
@ -149,7 +150,16 @@ class WorldInteraction {
|
|||
const inCreative = bot.game.gameMode === 'creative'
|
||||
const pixelRatio = viewer.renderer.getPixelRatio()
|
||||
viewer.world.threejsCursorLineMaterial = new LineMaterial({
|
||||
color: inCreative ? 0x40_80_ff : 0x00_00_00,
|
||||
color: (() => {
|
||||
switch (options.highlightBlockColor) {
|
||||
case 'blue':
|
||||
return 0x40_80_ff
|
||||
case 'classic':
|
||||
return 0x00_00_00
|
||||
default:
|
||||
return inCreative ? 0x40_80_ff : 0x00_00_00
|
||||
}
|
||||
})(),
|
||||
linewidth: Math.max(pixelRatio * 0.7, 1) * 2,
|
||||
// dashed: true,
|
||||
// dashSize: 5,
|
||||
|
|
@ -158,6 +168,8 @@ class WorldInteraction {
|
|||
upLineMaterial()
|
||||
// todo use gamemode update only
|
||||
bot.on('game', upLineMaterial)
|
||||
// Update material when highlight color setting changes
|
||||
subscribeKey(options, 'highlightBlockColor', upLineMaterial)
|
||||
}
|
||||
|
||||
activateEntity (entity: Entity) {
|
||||
|
|
@ -189,9 +201,18 @@ class WorldInteraction {
|
|||
}
|
||||
}
|
||||
|
||||
beforeUpdateChecks () {
|
||||
if (!document.hasFocus()) {
|
||||
// deactive all buttson
|
||||
this.buttons.fill(false)
|
||||
}
|
||||
}
|
||||
|
||||
// todo this shouldnt be done in the render loop, migrate the code to dom events to avoid delays on lags
|
||||
update () {
|
||||
this.beforeUpdateChecks()
|
||||
const inSpectator = bot.game.gameMode === 'spectator'
|
||||
const inAdventure = bot.game.gameMode === 'adventure'
|
||||
const entity = getEntityCursor()
|
||||
let cursorBlock = inSpectator && !options.showCursorBlockInSpectator ? null : bot.blockAtCursor(5)
|
||||
if (entity) {
|
||||
|
|
@ -199,7 +220,7 @@ class WorldInteraction {
|
|||
}
|
||||
|
||||
let cursorBlockDiggable = cursorBlock
|
||||
if (cursorBlock && !bot.canDigBlock(cursorBlock) && bot.game.gameMode !== 'creative') cursorBlockDiggable = null
|
||||
if (cursorBlock && (!bot.canDigBlock(cursorBlock) || inAdventure) && bot.game.gameMode !== 'creative') cursorBlockDiggable = null
|
||||
|
||||
const cursorChanged = cursorBlock && viewer.world.cursorBlock ? !viewer.world.cursorBlock.equals(cursorBlock.position) : viewer.world.cursorBlock !== cursorBlock
|
||||
|
||||
|
|
@ -393,6 +414,43 @@ const getDataFromShape = (shape) => {
|
|||
return { position, width, height, depth }
|
||||
}
|
||||
|
||||
// Blocks that can be interacted with in adventure mode
|
||||
const activatableBlockPatterns = [
|
||||
// Containers
|
||||
/^(chest|barrel|hopper|dispenser|dropper)$/,
|
||||
/^.*shulker_box$/,
|
||||
/^.*(furnace|smoker)$/,
|
||||
/^(brewing_stand|beacon)$/,
|
||||
// Crafting
|
||||
/^.*table$/,
|
||||
/^(grindstone|stonecutter|loom)$/,
|
||||
/^.*anvil$/,
|
||||
// Redstone
|
||||
/^(lever|repeater|comparator|daylight_detector|observer|note_block|jukebox|bell)$/,
|
||||
// Buttons
|
||||
/^.*button$/,
|
||||
// Doors and trapdoors
|
||||
/^.*door$/,
|
||||
/^.*trapdoor$/,
|
||||
// Functional blocks
|
||||
/^(enchanting_table|lectern|composter|respawn_anchor|lodestone|conduit)$/,
|
||||
/^.*bee.*$/,
|
||||
// Beds
|
||||
/^.*bed$/,
|
||||
// Misc
|
||||
/^(cake|decorated_pot|crafter|trial_spawner|vault)$/
|
||||
]
|
||||
|
||||
function isBlockActivatable (blockName: string) {
|
||||
return activatableBlockPatterns.some(pattern => pattern.test(blockName))
|
||||
}
|
||||
|
||||
function isLookingAtActivatableBlock () {
|
||||
const cursorBlock = bot.blockAtCursor(5)
|
||||
if (!cursorBlock) return false
|
||||
return isBlockActivatable(cursorBlock.name)
|
||||
}
|
||||
|
||||
export const getEntityCursor = () => {
|
||||
const entity = bot.nearestEntity((e) => {
|
||||
if (e.position.distanceTo(bot.entity.position) <= (bot.game.gameMode === 'creative' ? 5 : 3)) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue