a huge progress on packet replay component, fix bugs add switches
This commit is contained in:
parent
9613a0e644
commit
bdea1fc50c
14 changed files with 246 additions and 73 deletions
|
|
@ -81,7 +81,7 @@
|
|||
"mojangson": "^2.0.4",
|
||||
"net-browserify": "github:zardoy/prismarinejs-net-browserify",
|
||||
"node-gzip": "^1.1.2",
|
||||
"mcraft-fun-mineflayer": "^0.1.3",
|
||||
"mcraft-fun-mineflayer": "^0.1.4",
|
||||
"peerjs": "^1.5.0",
|
||||
"pixelarticons": "^1.8.1",
|
||||
"pretty-bytes": "^6.1.1",
|
||||
|
|
|
|||
12
pnpm-lock.yaml
generated
12
pnpm-lock.yaml
generated
|
|
@ -135,8 +135,8 @@ importers:
|
|||
specifier: ^4.17.21
|
||||
version: 4.17.21
|
||||
mcraft-fun-mineflayer:
|
||||
specifier: ^0.1.3
|
||||
version: 0.1.3(encoding@0.1.13)(mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/748163e536abe94f3dc8ada7a542bcd689bbbf49(encoding@0.1.13))
|
||||
specifier: ^0.1.4
|
||||
version: 0.1.4(encoding@0.1.13)(mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/748163e536abe94f3dc8ada7a542bcd689bbbf49(encoding@0.1.13))
|
||||
minecraft-data:
|
||||
specifier: 3.83.1
|
||||
version: 3.83.1
|
||||
|
|
@ -6252,9 +6252,9 @@ packages:
|
|||
resolution: {integrity: sha512-JBi9frIACmzmpKL38YudZJpml+tWP3UuCeb8ko5iJRHpmCmmChE+X3xzVEbEYnYBI2dMiO7915/5eYnKUVys3Q==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
|
||||
mcraft-fun-mineflayer@0.1.3:
|
||||
resolution: {integrity: sha512-3Xds5XBLwYHFgH09RS9fHNK7NJI2ZStXiYvU/9FVUMd9TOwcMwLorn1kNYOw022/0nGi3hibT3ldj6vLCjASIg==}
|
||||
version: 0.1.3
|
||||
mcraft-fun-mineflayer@0.1.4:
|
||||
resolution: {integrity: sha512-h6Dmnj/1No1aZO9FG14GTCZ80xPHVnkhByUFKIZr0PV2Hto7PoG4hBG8fa1fifwQIgLwBa+bURPyBw3KzzFPhw==}
|
||||
version: 0.1.4
|
||||
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
||||
peerDependencies:
|
||||
'@roamhq/wrtc': '*'
|
||||
|
|
@ -16691,7 +16691,7 @@ snapshots:
|
|||
apl-image-packer: 1.1.0
|
||||
zod: 3.24.1
|
||||
|
||||
mcraft-fun-mineflayer@0.1.3(encoding@0.1.13)(mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/748163e536abe94f3dc8ada7a542bcd689bbbf49(encoding@0.1.13)):
|
||||
mcraft-fun-mineflayer@0.1.4(encoding@0.1.13)(mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/748163e536abe94f3dc8ada7a542bcd689bbbf49(encoding@0.1.13)):
|
||||
dependencies:
|
||||
'@zardoy/flying-squid': 0.0.49(encoding@0.1.13)
|
||||
exit-hook: 2.2.1
|
||||
|
|
|
|||
|
|
@ -149,6 +149,14 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
|
|||
lastChunkDistance = 0
|
||||
debugStopGeometryUpdate = false
|
||||
|
||||
@worldCleanup()
|
||||
freeFlyMode = false
|
||||
@worldCleanup()
|
||||
freeFlyState = {
|
||||
yaw: 0,
|
||||
pitch: 0,
|
||||
position: new Vec3(0, 0, 0)
|
||||
}
|
||||
@worldCleanup()
|
||||
itemsRenderer: ItemsRenderer | undefined
|
||||
|
||||
|
|
|
|||
|
|
@ -223,8 +223,15 @@ export class WorldRendererThree extends WorldRendererCommon {
|
|||
}
|
||||
|
||||
updateCamera (pos: Vec3 | null, yaw: number, pitch: number): void {
|
||||
if (this.freeFlyMode) {
|
||||
pos = this.freeFlyState.position
|
||||
pitch = this.freeFlyState.pitch
|
||||
yaw = this.freeFlyState.yaw
|
||||
}
|
||||
|
||||
if (pos) {
|
||||
new tweenJs.Tween(this.camera.position).to({ x: pos.x, y: pos.y, z: pos.z }, 50).start()
|
||||
this.freeFlyState.position = pos
|
||||
}
|
||||
this.camera.rotation.set(pitch, yaw, this.cameraRoll, 'ZYX')
|
||||
}
|
||||
|
|
@ -234,7 +241,7 @@ export class WorldRendererThree extends WorldRendererCommon {
|
|||
// eslint-disable-next-line @typescript-eslint/non-nullable-type-assertion-style
|
||||
const cam = this.camera instanceof THREE.Group ? this.camera.children.find(child => child instanceof THREE.PerspectiveCamera) as THREE.PerspectiveCamera : this.camera
|
||||
this.renderer.render(this.scene, cam)
|
||||
if (this.config.showHand) {
|
||||
if (this.config.showHand && !this.freeFlyMode) {
|
||||
this.holdingBlock.render(this.camera, this.renderer, viewer.ambientLight, viewer.directionalLight)
|
||||
this.holdingBlockLeft.render(this.camera, this.renderer, viewer.ambientLight, viewer.directionalLight)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -45,6 +45,9 @@ export type AppQsParams = {
|
|||
replaySpeed?: string
|
||||
replayFileUrl?: string
|
||||
replayValidateClient?: string
|
||||
replayStopOnError?: string
|
||||
replaySkipMissingOnTimeout?: string
|
||||
replayPacketsSenderDelay?: string
|
||||
}
|
||||
|
||||
export type AppQsParamsArray = {
|
||||
|
|
@ -76,6 +79,16 @@ export const appQueryParamsArray = new Proxy({} as AppQsParamsArrayTransformed,
|
|||
},
|
||||
})
|
||||
|
||||
export function updateQsParam (name: keyof AppQsParams, value: string | undefined) {
|
||||
const url = new URL(window.location.href)
|
||||
if (value) {
|
||||
url.searchParams.set(name, value)
|
||||
} else {
|
||||
url.searchParams.delete(name)
|
||||
}
|
||||
window.history.replaceState({}, '', url.toString())
|
||||
}
|
||||
|
||||
// Helper function to check if a specific query parameter exists
|
||||
export const hasQueryParam = (param: keyof AppQsParams) => qsParams.has(param)
|
||||
|
||||
|
|
|
|||
|
|
@ -48,6 +48,14 @@ export const moveCameraRawHandler = ({ x, y }: { x: number; y: number }) => {
|
|||
const minPitch = -0.5 * Math.PI
|
||||
|
||||
viewer.world.lastCamUpdate = Date.now()
|
||||
|
||||
if (viewer.world.freeFlyMode) {
|
||||
// Update freeFlyState directly
|
||||
viewer.world.freeFlyState.yaw = (viewer.world.freeFlyState.yaw - x) % (2 * Math.PI)
|
||||
viewer.world.freeFlyState.pitch = Math.max(minPitch, Math.min(maxPitch, viewer.world.freeFlyState.pitch - y))
|
||||
return
|
||||
}
|
||||
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
import { Vec3 } from 'vec3'
|
||||
import { proxy, subscribe } from 'valtio'
|
||||
import * as THREE from 'three'
|
||||
|
||||
import { ControMax } from 'contro-max/build/controMax'
|
||||
import { CommandEventArgument, SchemaCommandInput } from 'contro-max/build/types'
|
||||
|
|
@ -127,6 +128,23 @@ contro.on('movementUpdate', ({ vector, soleVector, gamepadIndex }) => {
|
|||
}
|
||||
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'],
|
||||
|
|
@ -333,10 +351,20 @@ const onTriggerOrReleased = (command: Command, pressed: boolean) => {
|
|||
// eslint-disable-next-line @typescript-eslint/switch-exhaustiveness-check
|
||||
switch (command) {
|
||||
case 'general.jump':
|
||||
bot.setControlState('jump', pressed)
|
||||
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':
|
||||
setSneaking(pressed)
|
||||
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
|
||||
|
|
|
|||
|
|
@ -776,7 +776,9 @@ export async function connect (connectOptions: ConnectOptions) {
|
|||
playerState.onlineMode = !!connectOptions.authenticatedAccount
|
||||
|
||||
setLoadingScreenStatus('Placing blocks (starting viewer)')
|
||||
localStorage.lastConnectOptions = JSON.stringify(connectOptions)
|
||||
if (connectOptions.worldStateFileContents && connectOptions.worldStateFileContents.length < 3 * 1024 * 1024) {
|
||||
localStorage.lastConnectOptions = JSON.stringify(connectOptions)
|
||||
}
|
||||
connectOptions.onSuccessfulPlay?.()
|
||||
if (process.env.NODE_ENV === 'development' && !localStorage.lockUrl && !Object.keys(window.debugQueryParams).length) {
|
||||
lockUrl()
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
import { createServer, ServerClient } from 'minecraft-protocol'
|
||||
import { parseReplayContents } from 'mcraft-fun-mineflayer/build/packetsLogger'
|
||||
import { WorldStateHeader, PACKETS_REPLAY_FILE_EXTENSION, WORLD_STATE_FILE_EXTENSION } from 'mcraft-fun-mineflayer/build/worldState'
|
||||
import MinecraftData from 'minecraft-data'
|
||||
import { LocalServer } from '../customServer'
|
||||
import { UserError } from '../mineflayer/userError'
|
||||
import { packetsReplayState } from '../react/state/packetsReplayState'
|
||||
|
|
@ -24,7 +25,6 @@ interface OpenFileOptions {
|
|||
|
||||
export function openFile ({ contents, filename = 'unnamed', filesize }: OpenFileOptions) {
|
||||
packetsReplayState.replayName = `${filename} (${getFixedFilesize(filesize ?? contents.length)})`
|
||||
packetsReplayState.isOpen = true
|
||||
packetsReplayState.isPlaying = false
|
||||
|
||||
const connectOptions = {
|
||||
|
|
@ -72,11 +72,12 @@ export const startLocalReplayServer = (contents: string) => {
|
|||
'online-mode': false
|
||||
})
|
||||
|
||||
server.on('login', async client => {
|
||||
const data = MinecraftData(def.minecraftVersion)
|
||||
server.on(data.supportFeature('hasConfigurationState') ? 'playerJoin' : 'login' as any, async client => {
|
||||
await mainPacketsReplayer(
|
||||
client,
|
||||
replayData,
|
||||
appQueryParams.replayValidateClient === 'true' ? true : undefined
|
||||
packetsReplayState.customButtons.validateClientPackets.state ? true : undefined
|
||||
)
|
||||
})
|
||||
|
||||
|
|
@ -122,6 +123,8 @@ const IGNORE_SERVER_PACKETS = new Set([
|
|||
'kick_disconnect',
|
||||
])
|
||||
|
||||
const ADDITIONAL_DELAY = 500
|
||||
|
||||
const mainPacketsReplayer = async (client: ServerClient, replayData: ReturnType<typeof parseReplayContents>, ignoreClientPacketsWait: string[] | true = []) => {
|
||||
const writePacket = (name: string, data: any) => {
|
||||
data = restoreData(data)
|
||||
|
|
@ -139,49 +142,107 @@ const mainPacketsReplayer = async (client: ServerClient, replayData: ReturnType<
|
|||
expectedPacketReceived (name, params) {
|
||||
console.log('expectedPacketReceived', name, params)
|
||||
addPacketToReplayer(name, params, true, true)
|
||||
},
|
||||
unexpectedPacketsLimit: 15,
|
||||
onUnexpectedPacketsLimitReached () {
|
||||
addPacketToReplayer('...', {}, true)
|
||||
}
|
||||
})
|
||||
bot._client.on('writePacket' as any, (name, params) => {
|
||||
console.log('writePacket', name, params)
|
||||
clientsPacketsWaiter.addPacket(name, params)
|
||||
})
|
||||
|
||||
console.log('start replaying!')
|
||||
for (const [i, packet] of playPackets.entries()) {
|
||||
if (packet.isFromServer) {
|
||||
writePacket(packet.name, packet.params)
|
||||
addPacketToReplayer(packet.name, packet.params, false)
|
||||
await new Promise(resolve => {
|
||||
setTimeout(resolve, packet.diff * packetsReplayState.speed)
|
||||
})
|
||||
} else if (ignoreClientPacketsWait !== true && !ignoreClientPacketsWait.includes(packet.name)) {
|
||||
clientPackets.push({ name: packet.name, params: packet.params })
|
||||
if (playPackets[i + 1]?.isFromServer) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-loop-func
|
||||
clientPackets = clientPackets.filter((p, index) => {
|
||||
return !FLATTEN_CLIENT_PACKETS.has(p.name) || index === clientPackets.findIndex(clientPacket => clientPacket.name === p.name)
|
||||
})
|
||||
for (const packet of clientPackets) {
|
||||
packetsReplayState.packetsPlayback.push({
|
||||
name: packet.name,
|
||||
data: packet.params,
|
||||
isFromClient: true,
|
||||
position: positions.client++,
|
||||
timestamp: Date.now(),
|
||||
isUpcoming: true,
|
||||
})
|
||||
}
|
||||
// Patch console.error to detect errors
|
||||
const originalConsoleError = console.error
|
||||
let lastSentPacket: { name: string, params: any } | null = null
|
||||
console.error = (...args) => {
|
||||
if (lastSentPacket) {
|
||||
console.log('Got error after packet', lastSentPacket.name, lastSentPacket.params)
|
||||
}
|
||||
originalConsoleError.apply(console, args)
|
||||
if (packetsReplayState.customButtons.stopOnError.state) {
|
||||
packetsReplayState.isPlaying = false
|
||||
throw new Error('Replay stopped due to error: ' + args.join(' '))
|
||||
}
|
||||
}
|
||||
|
||||
await clientsPacketsWaiter.waitForPackets(clientPackets.map(p => p.name))
|
||||
clientPackets = []
|
||||
const playServerPacket = (name: string, params: any) => {
|
||||
try {
|
||||
writePacket(name, params)
|
||||
addPacketToReplayer(name, params, false)
|
||||
lastSentPacket = { name, params }
|
||||
} catch (err) {
|
||||
console.error('Error processing packet:', err)
|
||||
if (packetsReplayState.customButtons.stopOnError.state) {
|
||||
packetsReplayState.isPlaying = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
bot.on('error', (err) => {
|
||||
console.error('Mineflayer error:', err)
|
||||
})
|
||||
|
||||
client.on('writePacket' as any, (name, params) => {
|
||||
console.log('writePacket', name, params)
|
||||
clientsPacketsWaiter.addPacket(name, params)
|
||||
})
|
||||
|
||||
console.log('start replaying!')
|
||||
for (const [i, packet] of playPackets.entries()) {
|
||||
if (!packetsReplayState.isPlaying) {
|
||||
await new Promise<void>(resolve => {
|
||||
const interval = setInterval(() => {
|
||||
if (packetsReplayState.isPlaying) {
|
||||
clearInterval(interval)
|
||||
resolve()
|
||||
}
|
||||
}, 100)
|
||||
})
|
||||
}
|
||||
|
||||
if (packet.isFromServer) {
|
||||
playServerPacket(packet.name, packet.params)
|
||||
await new Promise(resolve => {
|
||||
setTimeout(resolve, packet.diff * packetsReplayState.speed + ADDITIONAL_DELAY * (packetsReplayState.customButtons.packetsSenderDelay.state ? 1 : 0))
|
||||
})
|
||||
} else if (ignoreClientPacketsWait !== true && !ignoreClientPacketsWait.includes(packet.name)) {
|
||||
clientPackets.push({ name: packet.name, params: packet.params })
|
||||
if (playPackets[i + 1]?.isFromServer) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-loop-func
|
||||
clientPackets = clientPackets.filter((p, index) => {
|
||||
return !FLATTEN_CLIENT_PACKETS.has(p.name) || index === clientPackets.findIndex(clientPacket => clientPacket.name === p.name)
|
||||
})
|
||||
for (const packet of clientPackets) {
|
||||
packetsReplayState.packetsPlayback.push({
|
||||
name: packet.name,
|
||||
data: packet.params,
|
||||
isFromClient: true,
|
||||
position: positions.client++,
|
||||
timestamp: Date.now(),
|
||||
isUpcoming: true,
|
||||
})
|
||||
}
|
||||
|
||||
await Promise.race([
|
||||
clientsPacketsWaiter.waitForPackets(clientPackets.map(p => p.name)),
|
||||
...(packetsReplayState.customButtons.skipMissingOnTimeout.state ? [new Promise(resolve => {
|
||||
setTimeout(resolve, 1000)
|
||||
})] : [])
|
||||
])
|
||||
clientPackets = []
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
// Restore original console.error
|
||||
console.error = originalConsoleError
|
||||
}
|
||||
}
|
||||
|
||||
interface PacketsWaiterOptions {
|
||||
unexpectedPacketReceived?: (name: string, params: any) => void
|
||||
expectedPacketReceived?: (name: string, params: any) => void
|
||||
onUnexpectedPacketsLimitReached?: () => void
|
||||
unexpectedPacketsLimit?: number
|
||||
}
|
||||
|
||||
interface PacketsWaiter {
|
||||
|
|
@ -193,13 +254,19 @@ const createPacketsWaiter = (options: PacketsWaiterOptions = {}): PacketsWaiter
|
|||
let packetHandler: ((data: any, name: string) => void) | null = null
|
||||
const queuedPackets: Array<{ name: string, params: any }> = []
|
||||
let isWaiting = false
|
||||
|
||||
let unexpectedPacketsCount = 0
|
||||
const handlePacket = (data: any, name: string, waitingPackets: string[], resolve: () => void) => {
|
||||
if (waitingPackets.includes(name)) {
|
||||
waitingPackets.splice(waitingPackets.indexOf(name), 1)
|
||||
options.expectedPacketReceived?.(name, data)
|
||||
} else {
|
||||
options.unexpectedPacketReceived?.(name, data)
|
||||
if (options.unexpectedPacketsLimit && unexpectedPacketsCount < options.unexpectedPacketsLimit) {
|
||||
options.unexpectedPacketReceived?.(name, data)
|
||||
}
|
||||
if (options.onUnexpectedPacketsLimitReached && unexpectedPacketsCount === options.unexpectedPacketsLimit) {
|
||||
options.onUnexpectedPacketsLimitReached?.()
|
||||
}
|
||||
unexpectedPacketsCount++
|
||||
}
|
||||
|
||||
if (waitingPackets.length === 0) {
|
||||
|
|
@ -220,6 +287,7 @@ const createPacketsWaiter = (options: PacketsWaiterOptions = {}): PacketsWaiter
|
|||
if (isWaiting) {
|
||||
throw new Error('Already waiting for packets')
|
||||
}
|
||||
unexpectedPacketsCount = 0
|
||||
isWaiting = true
|
||||
|
||||
try {
|
||||
|
|
@ -253,6 +321,7 @@ const isArrayEqual = (a: any[], b: any[]) => {
|
|||
}
|
||||
|
||||
const restoreData = (json: any) => {
|
||||
if (!json) return json
|
||||
const keys = Object.keys(json)
|
||||
|
||||
if (isArrayEqual(keys.sort(), ['data', 'type'].sort())) {
|
||||
|
|
|
|||
|
|
@ -27,7 +27,8 @@ export const Primary: Story = {
|
|||
_title: { text: 'Boss', translate: 'entity.minecraft.ender_dragon' },
|
||||
_color: 'red',
|
||||
_dividers: 2,
|
||||
_health: 100
|
||||
_health: 100,
|
||||
lastUpdated: 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,20 +1,10 @@
|
|||
import { useSnapshot } from 'valtio'
|
||||
import { useMemo } from 'react'
|
||||
import { appQueryParams } from '../appParams'
|
||||
import { appQueryParams, updateQsParam } from '../appParams'
|
||||
import { miscUiState } from '../globalState'
|
||||
import { packetsReplayState } from './state/packetsReplayState'
|
||||
import { onChangeButtonState, packetsReplayState } from './state/packetsReplayState'
|
||||
import ReplayPanel from './ReplayPanel'
|
||||
|
||||
function updateQsParam (name: string, value: string | undefined) {
|
||||
const url = new URL(window.location.href)
|
||||
if (value) {
|
||||
url.searchParams.set(name, value)
|
||||
} else {
|
||||
url.searchParams.delete(name)
|
||||
}
|
||||
window.history.replaceState({}, '', url.toString())
|
||||
}
|
||||
|
||||
export default function PacketsReplayProvider () {
|
||||
const state = useSnapshot(packetsReplayState)
|
||||
const { gameLoaded } = useSnapshot(miscUiState)
|
||||
|
|
@ -58,7 +48,7 @@ export default function PacketsReplayProvider () {
|
|||
updateQsParam('replayFilter', filter)
|
||||
}}
|
||||
onCustomButtonToggle={(button) => {
|
||||
packetsReplayState.customButtons[button] = !state.customButtons[button]
|
||||
onChangeButtonState(button as keyof typeof packetsReplayState.customButtons, !state.customButtons[button].state)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -12,12 +12,12 @@ interface Props {
|
|||
progress: { current: number; total: number }
|
||||
speed: number
|
||||
defaultFilter?: string
|
||||
customButtons: { button1: boolean; button2: boolean }
|
||||
customButtons: Readonly<Record<string, { state: boolean; label: string; tooltip?: string }>>
|
||||
onPlayPause?: (isPlaying: boolean) => void
|
||||
onRestart?: () => void
|
||||
onSpeedChange?: (speed: number) => void
|
||||
onFilterChange: (filter: string) => void
|
||||
onCustomButtonToggle: (button: 'button1' | 'button2') => void
|
||||
onCustomButtonToggle: (buttonId: string) => void
|
||||
clientPacketsAutocomplete: string[]
|
||||
serverPacketsAutocomplete: string[]
|
||||
}
|
||||
|
|
@ -139,22 +139,24 @@ export default function ReplayPanel ({
|
|||
}}
|
||||
/>
|
||||
|
||||
{[1, 2].map(num => (
|
||||
{Object.entries(customButtons).map(([buttonId, { state, label, tooltip }]) => (
|
||||
<button
|
||||
key={num}
|
||||
onClick={() => onCustomButtonToggle(`button${num}` as 'button1' | 'button2')}
|
||||
key={buttonId}
|
||||
onClick={() => onCustomButtonToggle(buttonId)}
|
||||
title={tooltip}
|
||||
style={{
|
||||
padding: '4px 8px',
|
||||
borderRadius: '4px',
|
||||
border: `1px solid ${DARK_COLORS.border}`,
|
||||
background: customButtons[`button${num}`]
|
||||
? (num === 1 ? DARK_COLORS.client : DARK_COLORS.server)
|
||||
background: state
|
||||
? (buttonId.startsWith('client') ? DARK_COLORS.client : DARK_COLORS.server)
|
||||
: DARK_COLORS.input,
|
||||
color: DARK_COLORS.text,
|
||||
cursor: 'pointer'
|
||||
cursor: 'pointer',
|
||||
minWidth: '32px'
|
||||
}}
|
||||
>
|
||||
{num}
|
||||
{label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,12 +1,17 @@
|
|||
import { useRef, useState } from 'react'
|
||||
import { PacketData } from '../../ReplayPanel'
|
||||
import { useScrollBehavior } from '../../hooks/useScrollBehavior'
|
||||
import { ClientOnMap } from '../../../generatedServerPackets'
|
||||
import { DARK_COLORS } from './constants'
|
||||
|
||||
const formatters: Record<string, (data: any) => string> = {
|
||||
position: (data) => `x:${data.x.toFixed(2)} y:${data.y.toFixed(2)} z:${data.z.toFixed(2)}`,
|
||||
chat: (data) => data.message,
|
||||
// Add more formatters as needed
|
||||
// chat: (data) => data,
|
||||
map_chunk (data: ClientOnMap['map_chunk'] | any) {
|
||||
const sizeOfChunk = data.chunkData?.length
|
||||
const blockEntitiesCount = data.blockEntities?.length
|
||||
return `x:${data.x} z:${data.z} C:${sizeOfChunk} E:${blockEntitiesCount}`
|
||||
},
|
||||
}
|
||||
|
||||
const getPacketIcon = (name: string): string => {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { proxy } from 'valtio'
|
||||
import type { PacketData } from '../ReplayPanel'
|
||||
import { appQueryParams } from '../../appParams'
|
||||
import { appQueryParams, updateQsParam } from '../../appParams'
|
||||
|
||||
export const packetsReplayState = proxy({
|
||||
packetsPlayback: [] as PacketData[],
|
||||
|
|
@ -13,7 +13,47 @@ export const packetsReplayState = proxy({
|
|||
},
|
||||
speed: appQueryParams.replaySpeed ? parseFloat(appQueryParams.replaySpeed) : 1,
|
||||
customButtons: {
|
||||
button1: false,
|
||||
button2: false
|
||||
validateClientPackets: {
|
||||
state: appQueryParams.replayValidateClient === 'true',
|
||||
label: 'C',
|
||||
tooltip: 'Validate client packets'
|
||||
},
|
||||
stopOnError: {
|
||||
state: appQueryParams.replayStopOnError === 'true',
|
||||
label: 'E',
|
||||
tooltip: 'Stop the replay when an error occurs'
|
||||
},
|
||||
skipMissingOnTimeout: {
|
||||
state: appQueryParams.replaySkipMissingOnTimeout === 'true',
|
||||
label: 'M',
|
||||
tooltip: 'Skip missing packets on timeout'
|
||||
},
|
||||
packetsSenderDelay: {
|
||||
state: appQueryParams.replayPacketsSenderDelay === 'true',
|
||||
label: 'D',
|
||||
tooltip: 'Send packets with an additional delay'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
export const onChangeButtonState = (button: keyof typeof packetsReplayState.customButtons, state: boolean) => {
|
||||
packetsReplayState.customButtons[button].state = state
|
||||
switch (button) {
|
||||
case 'validateClientPackets': {
|
||||
updateQsParam('replayValidateClient', String(state))
|
||||
break
|
||||
}
|
||||
case 'stopOnError': {
|
||||
updateQsParam('replayStopOnError', String(state))
|
||||
break
|
||||
}
|
||||
case 'skipMissingOnTimeout': {
|
||||
updateQsParam('replaySkipMissingOnTimeout', String(state))
|
||||
break
|
||||
}
|
||||
case 'packetsSenderDelay': {
|
||||
updateQsParam('replayPacketsSenderDelay', String(state))
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue