1051 lines
32 KiB
TypeScript
1051 lines
32 KiB
TypeScript
//@ts-check
|
|
|
|
import { Vec3 } from 'vec3'
|
|
import { proxy, subscribe } from 'valtio'
|
|
|
|
import { ControMax } from 'contro-max/build/controMax'
|
|
import { CommandEventArgument, SchemaCommandInput } from 'contro-max/build/types'
|
|
import { stringStartsWith } from 'contro-max/build/stringUtils'
|
|
import { GameMode } from 'mineflayer'
|
|
import { getThreeJsRendererMethods } from 'renderer/viewer/three/threeJsMethods'
|
|
import { isGameActive, showModal, gameAdditionalState, activeModalStack, hideCurrentModal, miscUiState, hideModal, hideAllModals } from './globalState'
|
|
import { goFullscreen, isInRealGameSession, pointerLock, reloadChunks } from './utils'
|
|
import { options } from './optionsStorage'
|
|
import { openPlayerInventory } from './inventoryWindows'
|
|
import { chatInputValueGlobal } from './react/Chat'
|
|
import { fsState } from './loadSave'
|
|
import { customCommandsConfig } from './customCommands'
|
|
import type { CustomCommand } from './react/KeybindingsCustom'
|
|
import { showOptionsModal } from './react/SelectOption'
|
|
import widgets from './react/widgets'
|
|
import { getItemFromBlock } from './chatUtils'
|
|
import { gamepadUiCursorState, moveGamepadCursorByPx } from './react/GamepadUiCursor'
|
|
import { completeResourcepackPackInstall, copyServerResourcePackToRegular, resourcePackState } from './resourcePack'
|
|
import { showNotification } from './react/NotificationProvider'
|
|
import { lastConnectOptions } from './react/AppStatusProvider'
|
|
import { onCameraMove, onControInit } from './cameraRotationControls'
|
|
import { createNotificationProgressReporter } from './core/progressReporter'
|
|
import { appStorage } from './react/appStorageProvider'
|
|
import { switchGameMode } from './packetsReplay/replayPackets'
|
|
import { tabListState } from './react/PlayerListOverlayProvider'
|
|
import { type ActionType, type ActionHoldConfig, type CustomAction } from './appConfig'
|
|
import { playerState } from './mineflayer/playerState'
|
|
|
|
export const customKeymaps = proxy(appStorage.keybindings)
|
|
subscribe(customKeymaps, () => {
|
|
appStorage.keybindings = customKeymaps
|
|
})
|
|
|
|
const controlOptions = {
|
|
preventDefault: true
|
|
}
|
|
|
|
export const contro = new ControMax({
|
|
commands: {
|
|
general: {
|
|
// movement
|
|
jump: ['Space', 'A'],
|
|
inventory: ['KeyE', 'X'],
|
|
drop: ['KeyQ', 'B'],
|
|
dropStack: [null],
|
|
sneak: ['ShiftLeft'],
|
|
toggleSneakOrDown: [null, 'Right Stick'],
|
|
sprint: ['ControlLeft', 'Left Stick'],
|
|
// game interactions
|
|
nextHotbarSlot: [null, 'Right Bumper'],
|
|
prevHotbarSlot: [null, 'Left Bumper'],
|
|
attackDestroy: [null, 'Right Trigger'],
|
|
interactPlace: [null, 'Left Trigger'],
|
|
swapHands: ['KeyF'],
|
|
selectItem: ['KeyH'],
|
|
rotateCameraLeft: [null],
|
|
rotateCameraRight: [null],
|
|
rotateCameraUp: [null],
|
|
rotateCameraDown: [null],
|
|
// ui?
|
|
chat: [['KeyT', 'Enter']],
|
|
command: ['Slash'],
|
|
playersList: ['Tab'],
|
|
debugOverlay: ['F3'],
|
|
debugOverlayHelpMenu: [null],
|
|
// client side
|
|
zoom: ['KeyC'],
|
|
viewerConsole: ['Backquote'],
|
|
togglePerspective: ['F5'],
|
|
},
|
|
ui: {
|
|
toggleFullscreen: ['F11'],
|
|
back: [null/* 'Escape' */, 'B'],
|
|
toggleMap: ['KeyJ'],
|
|
leftClick: [null, 'A'],
|
|
rightClick: [null, 'Y'],
|
|
speedupCursor: [null, 'Left Stick'],
|
|
pauseMenu: [null, 'Start']
|
|
},
|
|
communication: {
|
|
toggleMicrophone: ['KeyM'],
|
|
},
|
|
advanced: {
|
|
lockUrl: ['KeyY'],
|
|
},
|
|
custom: {} as Record<string, SchemaCommandInput & { type: string, input: any[] }>,
|
|
// waila: {
|
|
// showLookingBlockRecipe: ['Numpad3'],
|
|
// showLookingBlockUsages: ['Numpad4']
|
|
// }
|
|
} satisfies Record<string, Record<string, SchemaCommandInput>>,
|
|
movementKeymap: 'WASD',
|
|
movementVector: '2d',
|
|
groupedCommands: {
|
|
general: {
|
|
switchSlot: ['Digits', []]
|
|
}
|
|
},
|
|
}, {
|
|
defaultControlOptions: controlOptions,
|
|
target: document,
|
|
captureEvents () {
|
|
return true
|
|
},
|
|
storeProvider: {
|
|
load: () => customKeymaps,
|
|
save () { },
|
|
},
|
|
gamepadPollingInterval: 10
|
|
})
|
|
window.controMax = contro
|
|
export type Command = CommandEventArgument<typeof contro['_commandsRaw']>['command']
|
|
|
|
export const isCommandDisabled = (command: Command) => {
|
|
return miscUiState.appConfig?.disabledCommands?.includes(command)
|
|
}
|
|
|
|
onControInit()
|
|
|
|
updateBinds(customKeymaps)
|
|
|
|
const updateDoPreventDefault = () => {
|
|
controlOptions.preventDefault = miscUiState.gameLoaded && !activeModalStack.length
|
|
}
|
|
|
|
subscribe(miscUiState, updateDoPreventDefault)
|
|
subscribe(activeModalStack, updateDoPreventDefault)
|
|
updateDoPreventDefault()
|
|
|
|
const setSprinting = (state: boolean) => {
|
|
bot.setControlState('sprint', state)
|
|
gameAdditionalState.isSprinting = state
|
|
}
|
|
|
|
const isSpectatingEntity = () => {
|
|
return appViewer.playerState.utils.isSpectatingEntity()
|
|
}
|
|
|
|
contro.on('movementUpdate', ({ vector, soleVector, gamepadIndex }) => {
|
|
// Don't allow movement while spectating an entity
|
|
if (isSpectatingEntity()) return
|
|
|
|
if (gamepadIndex !== undefined && gamepadUiCursorState.display) {
|
|
const deadzone = 0.1 // TODO make deadzone configurable
|
|
if (Math.abs(soleVector.x) < deadzone && Math.abs(soleVector.z) < deadzone) {
|
|
return
|
|
}
|
|
moveGamepadCursorByPx(soleVector.x, true)
|
|
moveGamepadCursorByPx(soleVector.z, false)
|
|
emitMousemove()
|
|
}
|
|
miscUiState.usingGamepadInput = gamepadIndex !== undefined
|
|
if (!bot || !isGameActive(false)) return
|
|
|
|
// if (viewer.world.freeFlyMode) {
|
|
// // Create movement vector from input
|
|
// const direction = new THREE.Vector3(0, 0, 0)
|
|
// if (vector.z !== undefined) direction.z = vector.z
|
|
// if (vector.x !== undefined) direction.x = vector.x
|
|
|
|
// // Apply camera rotation to movement direction
|
|
// direction.applyQuaternion(viewer.camera.quaternion)
|
|
|
|
// // Update freeFlyState position with normalized direction
|
|
// const moveSpeed = 1
|
|
// direction.multiplyScalar(moveSpeed)
|
|
// viewer.world.freeFlyState.position.add(new Vec3(direction.x, direction.y, direction.z))
|
|
// return
|
|
// }
|
|
|
|
// gamepadIndex will be used for splitscreen in future
|
|
const coordToAction = [
|
|
['z', -1, 'forward'],
|
|
['z', 1, 'back'],
|
|
['x', -1, 'left'],
|
|
['x', 1, 'right'],
|
|
] as const
|
|
|
|
const newState: Partial<typeof bot.controlState> = {}
|
|
for (const [coord, v] of Object.entries(vector)) {
|
|
if (v === undefined || Math.abs(v) < 0.3) continue
|
|
// todo use raw values eg for slow movement
|
|
const mappedValue = v < 0 ? -1 : 1
|
|
// eslint-disable-next-line @typescript-eslint/no-non-null-asserted-optional-chain
|
|
const foundAction = coordToAction.find(([c, mapV]) => c === coord && mapV === mappedValue)?.[2]!
|
|
newState[foundAction] = true
|
|
}
|
|
|
|
for (const key of ['forward', 'back', 'left', 'right'] as const) {
|
|
if (newState[key] === bot.controlState[key]) continue
|
|
const action = !!newState[key]
|
|
if (action && !isGameActive(true)) continue
|
|
bot.setControlState(key, action)
|
|
|
|
if (key === 'forward') {
|
|
// todo workaround: need to refactor
|
|
if (action) {
|
|
void contro.emit('trigger', { command: 'general.forward' } as any)
|
|
} else {
|
|
void contro.emit('release', { command: 'general.forward' } as any)
|
|
setSprinting(false)
|
|
}
|
|
}
|
|
}
|
|
})
|
|
|
|
let lastCommandTrigger = null as { command: string, time: number } | null
|
|
|
|
const secondActionActivationTimeout = 300
|
|
const secondActionCommands = {
|
|
'general.jump' () {
|
|
// if (bot.game.gameMode === 'spectator') return
|
|
toggleFly()
|
|
},
|
|
'general.forward' () {
|
|
setSprinting(true)
|
|
}
|
|
}
|
|
|
|
// detect pause open, as ANY keyup event is not fired when you exit pointer lock (esc)
|
|
subscribe(activeModalStack, () => {
|
|
if (activeModalStack.length) {
|
|
// iterate over pressedKeys
|
|
for (const key of contro.pressedKeys) {
|
|
contro.pressedKeyOrButtonChanged({ code: key }, false)
|
|
}
|
|
}
|
|
})
|
|
|
|
const emitMousemove = () => {
|
|
const { x, y } = gamepadUiCursorState
|
|
const xAbs = x / 100 * window.innerWidth
|
|
const yAbs = y / 100 * window.innerHeight
|
|
const element = document.elementFromPoint(xAbs, yAbs) as HTMLElement | null
|
|
if (!element) return
|
|
element.dispatchEvent(new MouseEvent('mousemove', {
|
|
clientX: xAbs,
|
|
clientY: yAbs
|
|
}))
|
|
}
|
|
|
|
let lastClickedEl = null as HTMLElement | null
|
|
let lastClickedElTimeout: ReturnType<typeof setTimeout> | undefined
|
|
const inModalCommand = (command: Command, pressed: boolean) => {
|
|
if (pressed && !gamepadUiCursorState.display) return
|
|
|
|
if (pressed) {
|
|
if (command === 'ui.back') {
|
|
hideCurrentModal()
|
|
}
|
|
if (command === 'ui.pauseMenu') {
|
|
// hide all modals
|
|
hideAllModals()
|
|
}
|
|
if (command === 'ui.leftClick' || command === 'ui.rightClick') {
|
|
// in percent
|
|
const { x, y } = gamepadUiCursorState
|
|
const xAbs = x / 100 * window.innerWidth
|
|
const yAbs = y / 100 * window.innerHeight
|
|
const el = document.elementFromPoint(xAbs, yAbs) as HTMLElement
|
|
if (el) {
|
|
if (el === lastClickedEl && command === 'ui.leftClick') {
|
|
el.dispatchEvent(new MouseEvent('dblclick', {
|
|
bubbles: true,
|
|
clientX: xAbs,
|
|
clientY: yAbs
|
|
}))
|
|
return
|
|
}
|
|
el.dispatchEvent(new MouseEvent('mousedown', {
|
|
button: command === 'ui.leftClick' ? 0 : 2,
|
|
bubbles: true,
|
|
clientX: xAbs,
|
|
clientY: yAbs
|
|
}))
|
|
el.dispatchEvent(new MouseEvent(command === 'ui.leftClick' ? 'click' : 'contextmenu', {
|
|
bubbles: true,
|
|
clientX: xAbs,
|
|
clientY: yAbs
|
|
}))
|
|
el.dispatchEvent(new MouseEvent('mouseup', {
|
|
button: command === 'ui.leftClick' ? 0 : 2,
|
|
bubbles: true,
|
|
clientX: xAbs,
|
|
clientY: yAbs
|
|
}))
|
|
el.focus()
|
|
lastClickedEl = el
|
|
if (lastClickedElTimeout) clearTimeout(lastClickedElTimeout)
|
|
lastClickedElTimeout = setTimeout(() => {
|
|
lastClickedEl = null
|
|
}, 500)
|
|
}
|
|
}
|
|
}
|
|
|
|
if (command === 'ui.speedupCursor') {
|
|
gamepadUiCursorState.multiply = pressed ? 2 : 1
|
|
}
|
|
}
|
|
|
|
// 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) {
|
|
// Don't allow movement while spectating an entity
|
|
if (isSpectatingEntity()) return
|
|
|
|
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)
|
|
|
|
}
|
|
|
|
const onTriggerOrReleased = (command: Command, pressed: boolean) => {
|
|
// always allow release!
|
|
if (!bot || !isGameActive(false)) return
|
|
if (stringStartsWith(command, 'general')) {
|
|
// handle general commands
|
|
// eslint-disable-next-line @typescript-eslint/switch-exhaustiveness-check
|
|
switch (command) {
|
|
case 'general.jump':
|
|
if (isSpectatingEntity()) break
|
|
// if (viewer.world.freeFlyMode) {
|
|
// const moveSpeed = 0.5
|
|
// viewer.world.freeFlyState.position.add(new Vec3(0, pressed ? moveSpeed : 0, 0))
|
|
// } else {
|
|
bot.setControlState('jump', pressed)
|
|
// }
|
|
break
|
|
case 'general.sneak':
|
|
// if (viewer.world.freeFlyMode) {
|
|
// const moveSpeed = 0.5
|
|
// viewer.world.freeFlyState.position.add(new Vec3(0, pressed ? -moveSpeed : 0, 0))
|
|
// } else {
|
|
setSneaking(pressed)
|
|
// }
|
|
break
|
|
case 'general.sprint':
|
|
// todo add setting to change behavior
|
|
if (pressed) {
|
|
setSprinting(pressed)
|
|
}
|
|
break
|
|
case 'general.toggleSneakOrDown':
|
|
if (gameAdditionalState.isFlying) {
|
|
setSneaking(pressed)
|
|
} else if (pressed) {
|
|
setSneaking(!gameAdditionalState.isSneaking)
|
|
}
|
|
break
|
|
case 'general.attackDestroy':
|
|
document.dispatchEvent(new MouseEvent(pressed ? 'mousedown' : 'mouseup', { button: 0 }))
|
|
break
|
|
case 'general.interactPlace':
|
|
document.dispatchEvent(new MouseEvent(pressed ? 'mousedown' : 'mouseup', { button: 2 }))
|
|
break
|
|
case 'general.zoom':
|
|
gameAdditionalState.isZooming = pressed
|
|
break
|
|
case 'general.debugOverlay':
|
|
if (pressed) {
|
|
miscUiState.showDebugHud = !miscUiState.showDebugHud
|
|
}
|
|
break
|
|
case 'general.debugOverlayHelpMenu':
|
|
if (pressed) {
|
|
void onF3LongPress()
|
|
}
|
|
break
|
|
case 'general.rotateCameraLeft':
|
|
case 'general.rotateCameraRight':
|
|
case 'general.rotateCameraUp':
|
|
case 'general.rotateCameraDown':
|
|
cameraRotationControls.handleCommand(command, pressed)
|
|
break
|
|
case 'general.playersList':
|
|
tabListState.isOpen = pressed
|
|
break
|
|
case 'general.viewerConsole':
|
|
if (lastConnectOptions.value?.viewerWsConnect) {
|
|
showModal({ reactType: 'console' })
|
|
}
|
|
break
|
|
case 'general.togglePerspective':
|
|
if (pressed) {
|
|
const currentPerspective = playerState.reactive.perspective
|
|
// eslint-disable-next-line sonarjs/no-nested-switch
|
|
switch (currentPerspective) {
|
|
case 'first_person':
|
|
playerState.reactive.perspective = 'third_person_back'
|
|
break
|
|
case 'third_person_back':
|
|
playerState.reactive.perspective = 'third_person_front'
|
|
break
|
|
case 'third_person_front':
|
|
playerState.reactive.perspective = 'first_person'
|
|
break
|
|
}
|
|
}
|
|
break
|
|
}
|
|
} else if (stringStartsWith(command, 'ui')) {
|
|
switch (command) {
|
|
case 'ui.pauseMenu':
|
|
if (pressed) {
|
|
if (activeModalStack.length) {
|
|
hideCurrentModal()
|
|
} else {
|
|
showModal({ reactType: 'pause-screen' })
|
|
}
|
|
}
|
|
break
|
|
case 'ui.back':
|
|
case 'ui.toggleFullscreen':
|
|
case 'ui.toggleMap':
|
|
case 'ui.leftClick':
|
|
case 'ui.rightClick':
|
|
case 'ui.speedupCursor':
|
|
// These are handled elsewhere
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
// im still not sure, maybe need to refactor to handle in inventory instead
|
|
const alwaysPressedHandledCommand = (command: Command) => {
|
|
inModalCommand(command, true)
|
|
// triggered even outside of the game
|
|
if (command === 'general.inventory') {
|
|
if (activeModalStack.at(-1)?.reactType?.startsWith?.('player_win:')) { // todo?
|
|
hideCurrentModal()
|
|
}
|
|
}
|
|
if (command === 'advanced.lockUrl') {
|
|
lockUrl()
|
|
}
|
|
if (command === 'communication.toggleMicrophone') {
|
|
toggleMicrophoneMuted?.()
|
|
}
|
|
}
|
|
|
|
export function lockUrl () {
|
|
let newQs = ''
|
|
if (fsState.saveLoaded && fsState.inMemorySave) {
|
|
const worldFolder = fsState.inMemorySavePath
|
|
const save = worldFolder.split('/').at(-1)
|
|
newQs = `loadSave=${save}`
|
|
} else if (process.env.NODE_ENV === 'development') {
|
|
newQs = `reconnect=1`
|
|
} else if (lastConnectOptions.value?.server) {
|
|
const qs = new URLSearchParams()
|
|
const { server, botVersion, proxy, username } = lastConnectOptions.value
|
|
qs.set('ip', server)
|
|
if (botVersion) qs.set('version', botVersion)
|
|
if (proxy) qs.set('proxy', proxy)
|
|
if (username) qs.set('username', username)
|
|
newQs = String(qs.toString())
|
|
}
|
|
|
|
if (newQs) {
|
|
window.history.replaceState({}, '', `${window.location.pathname}?${newQs}`)
|
|
}
|
|
}
|
|
|
|
function cycleHotbarSlot (dir: 1 | -1) {
|
|
const newHotbarSlot = (bot.quickBarSlot + dir + 9) % 9
|
|
bot.setQuickBarSlot(newHotbarSlot)
|
|
}
|
|
|
|
// custom commands handler
|
|
const customCommandsHandler = ({ command }) => {
|
|
const [section, name] = command.split('.')
|
|
if (!isGameActive(true) || section !== 'custom') return
|
|
|
|
if (contro.userConfig?.custom) {
|
|
customCommandsConfig[(contro.userConfig.custom[name] as CustomCommand).type].handler((contro.userConfig.custom[name] as CustomCommand).inputs)
|
|
}
|
|
}
|
|
contro.on('trigger', customCommandsHandler)
|
|
|
|
contro.on('trigger', ({ command }) => {
|
|
if (isCommandDisabled(command)) return
|
|
|
|
const willContinue = !isGameActive(true)
|
|
alwaysPressedHandledCommand(command)
|
|
if (willContinue) return
|
|
|
|
const secondActionCommand = secondActionCommands[command]
|
|
if (secondActionCommand) {
|
|
if (command === lastCommandTrigger?.command && Date.now() - lastCommandTrigger.time < secondActionActivationTimeout) {
|
|
const commandToTrigger = secondActionCommands[lastCommandTrigger.command]
|
|
commandToTrigger()
|
|
lastCommandTrigger = null
|
|
} else {
|
|
lastCommandTrigger = {
|
|
command,
|
|
time: Date.now(),
|
|
}
|
|
}
|
|
}
|
|
|
|
onTriggerOrReleased(command, true)
|
|
|
|
if (stringStartsWith(command, 'general')) {
|
|
switch (command) {
|
|
case 'general.jump':
|
|
case 'general.sneak':
|
|
case 'general.toggleSneakOrDown':
|
|
case 'general.sprint':
|
|
case 'general.attackDestroy':
|
|
case 'general.rotateCameraLeft':
|
|
case 'general.rotateCameraRight':
|
|
case 'general.rotateCameraUp':
|
|
case 'general.rotateCameraDown':
|
|
case 'general.debugOverlay':
|
|
case 'general.debugOverlayHelpMenu':
|
|
case 'general.playersList':
|
|
case 'general.togglePerspective':
|
|
// no-op
|
|
break
|
|
case 'general.swapHands': {
|
|
if (isSpectatingEntity()) break
|
|
bot._client.write('block_dig', {
|
|
'status': 6,
|
|
'location': {
|
|
'x': 0,
|
|
'z': 0,
|
|
'y': 0
|
|
},
|
|
'face': 0,
|
|
})
|
|
break
|
|
}
|
|
case 'general.interactPlace':
|
|
// handled in onTriggerOrReleased
|
|
break
|
|
case 'general.inventory':
|
|
if (isSpectatingEntity()) break
|
|
document.exitPointerLock?.()
|
|
openPlayerInventory()
|
|
break
|
|
case 'general.drop': {
|
|
if (isSpectatingEntity()) break
|
|
// protocol 1.9+
|
|
bot._client.write('block_dig', {
|
|
'status': 4,
|
|
'location': {
|
|
'x': 0,
|
|
'z': 0,
|
|
'y': 0
|
|
},
|
|
'face': 0,
|
|
sequence: 0
|
|
})
|
|
const slot = bot.inventory.hotbarStart + bot.quickBarSlot
|
|
const item = bot.inventory.slots[slot]
|
|
if (item) {
|
|
item.count--
|
|
bot.inventory.updateSlot(slot, item.count > 0 ? item : null!)
|
|
}
|
|
break
|
|
}
|
|
case 'general.dropStack': {
|
|
if (bot.heldItem) {
|
|
void bot.tossStack(bot.heldItem)
|
|
}
|
|
break
|
|
}
|
|
case 'general.chat':
|
|
showModal({ reactType: 'chat' })
|
|
break
|
|
case 'general.command':
|
|
chatInputValueGlobal.value = '/'
|
|
showModal({ reactType: 'chat' })
|
|
break
|
|
case 'general.selectItem':
|
|
if (isSpectatingEntity()) break
|
|
void selectItem()
|
|
break
|
|
case 'general.nextHotbarSlot':
|
|
if (isSpectatingEntity()) break
|
|
cycleHotbarSlot(1)
|
|
break
|
|
case 'general.prevHotbarSlot':
|
|
if (isSpectatingEntity()) break
|
|
cycleHotbarSlot(-1)
|
|
break
|
|
case 'general.zoom':
|
|
break
|
|
case 'general.viewerConsole':
|
|
if (lastConnectOptions.value?.viewerWsConnect) {
|
|
showModal({ reactType: 'console' })
|
|
}
|
|
break
|
|
}
|
|
}
|
|
|
|
if (command === 'ui.toggleFullscreen') {
|
|
void goFullscreen(true)
|
|
}
|
|
})
|
|
|
|
// show-hide Fullmap
|
|
contro.on('trigger', ({ command }) => {
|
|
if (command !== 'ui.toggleMap') return
|
|
const isActive = isGameActive(true)
|
|
if (activeModalStack.at(-1)?.reactType === 'full-map') {
|
|
miscUiState.displayFullmap = false
|
|
hideModal({ reactType: 'full-map' })
|
|
} else if (isActive && !activeModalStack.length) {
|
|
miscUiState.displayFullmap = true
|
|
showModal({ reactType: 'full-map' })
|
|
}
|
|
})
|
|
|
|
contro.on('release', ({ command }) => {
|
|
if (isCommandDisabled(command)) return
|
|
|
|
inModalCommand(command, false)
|
|
onTriggerOrReleased(command, false)
|
|
})
|
|
|
|
// hard-coded keybindings
|
|
|
|
export const f3Keybinds: Array<{
|
|
key?: string,
|
|
action: () => void | Promise<void>,
|
|
mobileTitle: string
|
|
enabled?: () => boolean
|
|
}> = [
|
|
{
|
|
key: 'KeyA',
|
|
action () {
|
|
//@ts-expect-error
|
|
const loadedChunks = Object.entries(worldView.loadedChunks).filter(([, v]) => v).map(([key]) => key.split(',').map(Number))
|
|
for (const [x, z] of loadedChunks) {
|
|
worldView!.unloadChunk({ x, z })
|
|
}
|
|
// for (const child of viewer.scene.children) {
|
|
// if (child.name === 'chunk') { // should not happen
|
|
// viewer.scene.remove(child)
|
|
// console.warn('forcefully removed chunk from scene')
|
|
// }
|
|
// }
|
|
if (localServer) {
|
|
//@ts-expect-error not sure why it is private... maybe revisit api?
|
|
localServer.players[0].world.columns = {}
|
|
}
|
|
void reloadChunks()
|
|
if (appViewer.backend?.backendMethods && typeof appViewer.backend.backendMethods.reloadWorld === 'function') {
|
|
appViewer.backend.backendMethods.reloadWorld()
|
|
}
|
|
},
|
|
mobileTitle: 'Reload chunks',
|
|
},
|
|
{
|
|
key: 'KeyG',
|
|
action () {
|
|
options.showChunkBorders = !options.showChunkBorders
|
|
},
|
|
mobileTitle: 'Toggle chunk borders',
|
|
},
|
|
{
|
|
key: 'KeyH',
|
|
action () {
|
|
showModal({ reactType: 'chunks-debug' })
|
|
},
|
|
mobileTitle: 'Show Chunks Debug',
|
|
},
|
|
{
|
|
action () {
|
|
showModal({ reactType: 'renderer-debug' })
|
|
},
|
|
mobileTitle: 'Renderer Debug Menu',
|
|
},
|
|
{
|
|
key: 'KeyY',
|
|
async action () {
|
|
// waypoints
|
|
const widgetNames = widgets.map(widget => widget.name)
|
|
const widget = await showOptionsModal('Open Widget', widgetNames)
|
|
if (!widget) return
|
|
showModal({ reactType: `widget-${widget}` })
|
|
},
|
|
mobileTitle: 'Open Widget'
|
|
},
|
|
{
|
|
key: 'KeyT',
|
|
async action () {
|
|
// TODO!
|
|
if (resourcePackState.resourcePackInstalled || gameAdditionalState.usingServerResourcePack) {
|
|
showNotification('Reloading textures...')
|
|
await completeResourcepackPackInstall('default', 'default', gameAdditionalState.usingServerResourcePack, createNotificationProgressReporter())
|
|
}
|
|
},
|
|
mobileTitle: 'Reload Textures'
|
|
},
|
|
{
|
|
key: 'F4',
|
|
async action () {
|
|
let nextGameMode: GameMode
|
|
switch (bot.game.gameMode) {
|
|
case 'creative': {
|
|
nextGameMode = 'survival'
|
|
|
|
break
|
|
}
|
|
case 'survival': {
|
|
nextGameMode = 'adventure'
|
|
|
|
break
|
|
}
|
|
case 'adventure': {
|
|
nextGameMode = 'spectator'
|
|
|
|
break
|
|
}
|
|
case 'spectator': {
|
|
nextGameMode = 'creative'
|
|
|
|
break
|
|
}
|
|
// No default
|
|
}
|
|
if (lastConnectOptions.value?.worldStateFileContents) {
|
|
switchGameMode(nextGameMode)
|
|
} else {
|
|
bot.chat(`/gamemode ${nextGameMode}`)
|
|
}
|
|
},
|
|
mobileTitle: 'Cycle Game Mode'
|
|
},
|
|
{
|
|
key: 'KeyP',
|
|
async action () {
|
|
const { uuid, ping: playerPing, username } = bot.player
|
|
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 Player & Ping Details',
|
|
enabled: () => !lastConnectOptions.value?.singleplayer && !!bot.player
|
|
},
|
|
{
|
|
action () {
|
|
void copyServerResourcePackToRegular()
|
|
},
|
|
mobileTitle: 'Copy Server Resource Pack',
|
|
enabled: () => !!gameAdditionalState.usingServerResourcePack
|
|
}
|
|
]
|
|
|
|
export const reloadChunksAction = () => {
|
|
const action = f3Keybinds.find(f3Keybind => f3Keybind.key === 'KeyA')
|
|
void action!.action()
|
|
}
|
|
|
|
document.addEventListener('keydown', (e) => {
|
|
if (!isGameActive(false)) return
|
|
if (contro.pressedKeys.has('F3')) {
|
|
const keybind = f3Keybinds.find((v) => v.key === e.code)
|
|
if (keybind && (keybind.enabled?.() ?? true)) {
|
|
void keybind.action()
|
|
e.stopPropagation()
|
|
}
|
|
}
|
|
}, {
|
|
capture: true,
|
|
})
|
|
|
|
const isFlying = () => (bot.entity as any).flying
|
|
|
|
const startFlying = (sendAbilities = true) => {
|
|
if (sendAbilities) {
|
|
bot._client.write('abilities', {
|
|
flags: 2,
|
|
})
|
|
}
|
|
(bot.entity as any).flying = true
|
|
}
|
|
|
|
const endFlying = (sendAbilities = true) => {
|
|
if (!isFlying()) return
|
|
if (sendAbilities) {
|
|
bot._client.write('abilities', {
|
|
flags: 0,
|
|
})
|
|
}
|
|
(bot.entity as any).flying = false
|
|
}
|
|
|
|
export const onBotCreate = () => {
|
|
}
|
|
|
|
const toggleFly = (newState = !isFlying(), sendAbilities?: boolean) => {
|
|
if (!bot.entity.canFly) return
|
|
|
|
if (newState) {
|
|
startFlying(sendAbilities)
|
|
} else {
|
|
endFlying(sendAbilities)
|
|
}
|
|
gameAdditionalState.isFlying = isFlying()
|
|
}
|
|
|
|
const selectItem = async () => {
|
|
const block = bot.blockAtCursor(5)
|
|
if (!block) return
|
|
const itemId = getItemFromBlock(block)?.id
|
|
if (!itemId) return
|
|
const Item = require('prismarine-item')(bot.version)
|
|
const item = new Item(itemId, 1, 0)
|
|
await bot.creative.setInventorySlot(bot.inventory.hotbarStart + bot.quickBarSlot, item)
|
|
bot.updateHeldItem()
|
|
}
|
|
|
|
addEventListener('mousedown', async (e) => {
|
|
// always prevent default for side buttons (back / forward navigation)
|
|
if (e.button === 3 || e.button === 4) {
|
|
e.preventDefault()
|
|
}
|
|
|
|
if ((e.target as HTMLElement).matches?.('#VRButton')) return
|
|
if (!isInRealGameSession() && !(e.target as HTMLElement).id.includes('ui-root')) return
|
|
void pointerLock.requestPointerLock()
|
|
if (!bot) return
|
|
getThreeJsRendererMethods()?.onPageInteraction()
|
|
// wheel click
|
|
// todo support ctrl+wheel (+nbt)
|
|
if (e.button === 1) {
|
|
await selectItem()
|
|
}
|
|
})
|
|
|
|
window.addEventListener('keydown', (e) => {
|
|
if (e.code !== 'Escape') return
|
|
if (!activeModalStack.length) {
|
|
getThreeJsRendererMethods()?.onPageInteraction()
|
|
}
|
|
|
|
if (activeModalStack.length) {
|
|
const hideAll = e.ctrlKey || e.metaKey
|
|
if (hideAll) {
|
|
hideAllModals()
|
|
} else {
|
|
hideCurrentModal()
|
|
}
|
|
if (activeModalStack.length === 0) {
|
|
getThreeJsRendererMethods()?.onPageInteraction()
|
|
pointerLock.justHitEscape = true
|
|
}
|
|
} else if (pointerLock.hasPointerLock) {
|
|
document.exitPointerLock?.()
|
|
if (options.autoExitFullscreen) {
|
|
void document.exitFullscreen()
|
|
}
|
|
} else {
|
|
document.dispatchEvent(new Event('pointerlockchange'))
|
|
}
|
|
})
|
|
|
|
window.addEventListener('keydown', (e) => {
|
|
if (e.code !== 'F2' || e.repeat || !isGameActive(true)) return
|
|
e.preventDefault()
|
|
const canvas = document.getElementById('viewer-canvas') as HTMLCanvasElement
|
|
if (!canvas) return
|
|
const link = document.createElement('a')
|
|
link.href = canvas.toDataURL('image/png')
|
|
const date = new Date()
|
|
link.download = `screenshot ${date.toLocaleString().replaceAll('.', '-').replace(',', '')}.png`
|
|
link.click()
|
|
})
|
|
|
|
window.addEventListener('keydown', (e) => {
|
|
if (e.code !== 'F1' || e.repeat || !isGameActive(true)) return
|
|
e.preventDefault()
|
|
miscUiState.showUI = !miscUiState.showUI
|
|
})
|
|
|
|
// #region experimental debug things
|
|
window.addEventListener('keydown', (e) => {
|
|
if (e.code === 'KeyL' && e.altKey && !e.shiftKey && !e.ctrlKey && !e.metaKey) {
|
|
console.clear()
|
|
}
|
|
if (e.code === 'KeyK' && e.altKey && e.shiftKey && !e.ctrlKey && !e.metaKey) {
|
|
if (sessionStorage.delayLoadUntilFocus) {
|
|
sessionStorage.removeItem('delayLoadUntilFocus')
|
|
} else {
|
|
sessionStorage.setItem('delayLoadUntilFocus', 'true')
|
|
}
|
|
}
|
|
if (e.code === 'KeyK' && e.altKey && !e.shiftKey && !e.ctrlKey && !e.metaKey) {
|
|
// eslint-disable-next-line no-debugger
|
|
debugger
|
|
}
|
|
})
|
|
// #endregion
|
|
|
|
export function updateBinds (commands: any) {
|
|
contro.inputSchema.commands.custom = Object.fromEntries(Object.entries(commands?.custom ?? {}).map(([key, value]) => {
|
|
return [key, {
|
|
keys: [],
|
|
gamepad: [],
|
|
type: '',
|
|
inputs: []
|
|
}]
|
|
}))
|
|
|
|
for (const [group, actions] of Object.entries(commands)) {
|
|
contro.userConfig![group] = Object.fromEntries(Object.entries(actions).map(([key, value]) => {
|
|
const newValue = {
|
|
keys: value?.keys ?? undefined,
|
|
gamepad: value?.gamepad ?? undefined,
|
|
}
|
|
|
|
if (group === 'custom') {
|
|
newValue['type'] = (value).type
|
|
newValue['inputs'] = (value).inputs
|
|
}
|
|
|
|
return [key, newValue]
|
|
}))
|
|
}
|
|
}
|
|
|
|
export const onF3LongPress = async () => {
|
|
const actions = f3Keybinds.filter(f3Keybind => {
|
|
return f3Keybind.mobileTitle && (f3Keybind.enabled?.() ?? true)
|
|
})
|
|
const actionNames = actions.map(f3Keybind => {
|
|
return `${f3Keybind.mobileTitle}${f3Keybind.key ? ` (F3+${f3Keybind.key})` : ''}`
|
|
})
|
|
const select = await showOptionsModal('', actionNames)
|
|
if (!select) return
|
|
const actionIndex = actionNames.indexOf(select)
|
|
const f3Keybind = actions[actionIndex]!
|
|
void f3Keybind.action()
|
|
}
|
|
|
|
export const handleMobileButtonCustomAction = (action: CustomAction) => {
|
|
const handler = customCommandsConfig[action.type]?.handler
|
|
if (handler) {
|
|
handler([...action.input])
|
|
}
|
|
}
|
|
|
|
export const triggerCommand = (command: Command, isDown: boolean) => {
|
|
handleMobileButtonActionCommand(command, isDown)
|
|
}
|
|
|
|
export const handleMobileButtonActionCommand = (command: ActionType | ActionHoldConfig, isDown: boolean) => {
|
|
const commandValue = typeof command === 'string' ? command : 'command' in command ? command.command : command
|
|
|
|
// Check if command is disabled before proceeding
|
|
if (typeof commandValue === 'string' && isCommandDisabled(commandValue as Command)) return
|
|
|
|
if (typeof commandValue === 'string' && !stringStartsWith(commandValue, 'custom')) {
|
|
const event: CommandEventArgument<typeof contro['_commandsRaw']> = {
|
|
command: commandValue as Command,
|
|
schema: {
|
|
keys: [],
|
|
gamepad: []
|
|
}
|
|
}
|
|
if (isDown) {
|
|
contro.emit('trigger', event)
|
|
} else {
|
|
contro.emit('release', event)
|
|
}
|
|
} else if (typeof commandValue === 'object') {
|
|
if (isDown) {
|
|
handleMobileButtonCustomAction(commandValue)
|
|
}
|
|
}
|
|
}
|
|
|
|
export const handleMobileButtonLongPress = (actionHold: ActionHoldConfig) => {
|
|
if (typeof actionHold.longPressAction === 'string' && actionHold.longPressAction === 'general.debugOverlayHelpMenu') {
|
|
void onF3LongPress()
|
|
} else if (actionHold.longPressAction) {
|
|
handleMobileButtonActionCommand(actionHold.longPressAction, true)
|
|
}
|
|
}
|