Compare commits

...
Sign in to create a new pull request.

5 commits

Author SHA1 Message Date
Vitaly Turovsky
9236d5612b fix player pos display 2024-09-04 04:57:56 +03:00
Vitaly Turovsky
079f9404f6 try diff copy size 2024-09-04 03:54:22 +03:00
Vitaly Turovsky
488240f32a fix visual display on small screens 2024-09-04 03:37:06 +03:00
Vitaly Turovsky
6c157245bb add css 2024-09-04 03:19:19 +03:00
Vitaly Turovsky
753821b01a feat: display progress of downloading chunks visually 2024-09-04 03:15:32 +03:00
9 changed files with 263 additions and 37 deletions

View file

@ -282,6 +282,7 @@ function renderElement (world: World, cursor: Vec3, element: BlockElement, doAO:
const aos: number[] = [] const aos: number[] = []
const neighborPos = position.plus(new Vec3(...dir)) const neighborPos = position.plus(new Vec3(...dir))
// 10%
const baseLight = world.getLight(neighborPos, undefined, undefined, block.name) / 15 const baseLight = world.getLight(neighborPos, undefined, undefined, block.name) / 15
for (const pos of corners) { for (const pos of corners) {
let vertex = [ let vertex = [
@ -290,7 +291,7 @@ function renderElement (world: World, cursor: Vec3, element: BlockElement, doAO:
(pos[2] ? maxz : minz) (pos[2] ? maxz : minz)
] ]
if (!needTiles) { if (!needTiles) { // 10%
vertex = vecadd3(matmul3(localMatrix, vertex), localShift) vertex = vecadd3(matmul3(localMatrix, vertex), localShift)
vertex = vecadd3(matmul3(globalMatrix, vertex), globalShift) vertex = vecadd3(matmul3(globalMatrix, vertex), globalShift)
vertex = vertex.map(v => v / 16) vertex = vertex.map(v => v / 16)
@ -411,7 +412,7 @@ export function getSectionGeometry (sx, sy, sz, world: World) {
// todo this can be removed here // todo this can be removed here
signs: {}, signs: {},
// isFull: true, // isFull: true,
highestBlocks: {}, highestBlocks: {}, // todo migrate to map for 2% boost perf
hadErrors: false hadErrors: false
} }
@ -449,7 +450,7 @@ export function getSectionGeometry (sx, sy, sz, world: World) {
} }
const biome = block.biome.name const biome = block.biome.name
if (world.preflat) { if (world.preflat) { // 10% perf
const patchProperties = preflatBlockCalculation(block, world, cursor) const patchProperties = preflatBlockCalculation(block, world, cursor)
if (patchProperties) { if (patchProperties) {
block._originalProperties ??= block._properties block._originalProperties ??= block._properties
@ -505,6 +506,7 @@ export function getSectionGeometry (sx, sy, sz, world: World) {
const model = modelVars[useVariant] ?? modelVars[0] const model = modelVars[useVariant] ?? modelVars[0]
if (!model) continue if (!model) continue
// #region 10%
let globalMatrix = null as any let globalMatrix = null as any
let globalShift = null as any let globalShift = null as any
for (const axis of ['x', 'y', 'z'] as const) { for (const axis of ['x', 'y', 'z'] as const) {
@ -518,6 +520,7 @@ export function getSectionGeometry (sx, sy, sz, world: World) {
globalShift = [8, 8, 8] globalShift = [8, 8, 8]
globalShift = vecsub3(globalShift, matmul3(globalMatrix, globalShift)) globalShift = vecsub3(globalShift, matmul3(globalMatrix, globalShift))
} }
// #endregion
for (const element of model.elements ?? []) { for (const element of model.elements ?? []) {
const ao = model.ao ?? true const ao = model.ao ?? true
@ -527,6 +530,7 @@ export function getSectionGeometry (sx, sy, sz, world: World) {
renderElement(world, pos, element, ao, attr, globalMatrix, globalShift, block, biome) renderElement(world, pos, element, ao, attr, globalMatrix, globalShift, block, biome)
}) })
} else { } else {
// 60%
renderElement(world, cursor, element, ao, attr, globalMatrix, globalShift, block, biome) renderElement(world, cursor, element, ao, attr, globalMatrix, globalShift, block, biome)
} }
} }

View file

@ -2,8 +2,18 @@ import { useEffect, useState } from 'react'
import styles from './appStatus.module.css' import styles from './appStatus.module.css'
import Button from './Button' import Button from './Button'
import Screen from './Screen' import Screen from './Screen'
import LoadingChunks from './LoadingChunks'
export default ({ status, isError, hideDots = false, lastStatus = '', backAction = undefined as undefined | (() => void), description = '', actionsSlot = null as React.ReactNode | null }) => { export default ({
status,
isError,
hideDots = false,
lastStatus = '',
backAction = undefined as undefined | (() => void),
description = '',
actionsSlot = null as React.ReactNode | null,
children
}) => {
const [loadingDots, setLoadingDots] = useState('') const [loadingDots, setLoadingDots] = useState('')
useEffect(() => { useEffect(() => {
@ -52,6 +62,7 @@ export default ({ status, isError, hideDots = false, lastStatus = '', backAction
<Button onClick={() => window.location.reload()} label="Reset App (recommended)" /> <Button onClick={() => window.location.reload()} label="Reset App (recommended)" />
</> </>
)} )}
{children}
</Screen> </Screen>
) )
} }

View file

@ -1,6 +1,6 @@
import { proxy, useSnapshot } from 'valtio' import { proxy, useSnapshot } from 'valtio'
import { useEffect, useState } from 'react' import { useEffect, useRef, useState } from 'react'
import { activeModalStack, activeModalStacks, hideModal, insertActiveModalStack, miscUiState } from '../globalState' import { activeModalStack, activeModalStacks, hideModal, insertActiveModalStack, miscUiState, showModal } from '../globalState'
import { resetLocalStorageWorld } from '../browserfs' import { resetLocalStorageWorld } from '../browserfs'
import { fsState } from '../loadSave' import { fsState } from '../loadSave'
import { guessProblem } from '../errorLoadingScreenHelpers' import { guessProblem } from '../errorLoadingScreenHelpers'
@ -14,6 +14,7 @@ import { useIsModalActive } from './utilsApp'
import Button from './Button' import Button from './Button'
import { AuthenticatedAccount, updateAuthenticatedAccountData, updateLoadedServerData } from './ServersListProvider' import { AuthenticatedAccount, updateAuthenticatedAccountData, updateLoadedServerData } from './ServersListProvider'
import { showOptionsModal } from './SelectOption' import { showOptionsModal } from './SelectOption'
import LoadingChunks from './LoadingChunks'
const initialState = { const initialState = {
status: '', status: '',
@ -22,9 +23,12 @@ const initialState = {
descriptionHint: '', descriptionHint: '',
isError: false, isError: false,
hideDots: false, hideDots: false,
loadingChunksData: null as null | Record<string, string>,
loadingChunksDataPlayerChunk: null as null | { x: number, z: number },
isDisplaying: false
} }
export const appStatusState = proxy(initialState) export const appStatusState = proxy(initialState)
const resetState = () => { export const resetAppStatusState = () => {
Object.assign(appStatusState, initialState) Object.assign(appStatusState, initialState)
} }
@ -33,7 +37,7 @@ export const lastConnectOptions = {
} }
export default () => { export default () => {
const { isError, lastStatus, maybeRecoverable, status, hideDots, descriptionHint } = useSnapshot(appStatusState) const { isError, lastStatus, maybeRecoverable, status, hideDots, descriptionHint, loadingChunksData, loadingChunksDataPlayerChunk } = useSnapshot(appStatusState)
const { active: replayActive } = useSnapshot(packetsReplaceSessionState) const { active: replayActive } = useSnapshot(packetsReplaceSessionState)
const isOpen = useIsModalActive('app-status') const isOpen = useIsModalActive('app-status')
@ -52,7 +56,7 @@ export default () => {
}, [isOpen]) }, [isOpen])
const reconnect = () => { const reconnect = () => {
resetState() resetAppStatusState()
window.dispatchEvent(new window.CustomEvent('connect', { window.dispatchEvent(new window.CustomEvent('connect', {
detail: lastConnectOptions.value detail: lastConnectOptions.value
})) }))
@ -93,7 +97,7 @@ export default () => {
lastStatus={lastStatus} lastStatus={lastStatus}
description={displayAuthButton ? '' : (isError ? guessProblem(status) : '') || descriptionHint} description={displayAuthButton ? '' : (isError ? guessProblem(status) : '') || descriptionHint}
backAction={maybeRecoverable ? () => { backAction={maybeRecoverable ? () => {
resetState() resetAppStatusState()
miscUiState.gameLoaded = false miscUiState.gameLoaded = false
miscUiState.loadedDataVersion = null miscUiState.loadedDataVersion = null
window.loadedData = undefined window.loadedData = undefined
@ -113,10 +117,25 @@ export default () => {
{replayActive && <Button label='Download Packets Replay' onClick={downloadPacketsReplay} />} {replayActive && <Button label='Download Packets Replay' onClick={downloadPacketsReplay} />}
</> </>
} }
/> >
{loadingChunksData && <LoadingChunks regionFiles={Object.keys(loadingChunksData)} stateMap={loadingChunksData} playerChunk={loadingChunksDataPlayerChunk} />}
{isOpen && <DisplayingIndicator />}
</AppStatus>
</DiveTransition> </DiveTransition>
} }
const DisplayingIndicator = () => {
useEffect(() => {
requestAnimationFrame(() => {
requestAnimationFrame(() => {
appStatusState.isDisplaying = true
})
})
}, [])
return <div />
}
const PossiblyVpnBypassProxyButton = ({ reconnect }: { reconnect: () => void }) => { const PossiblyVpnBypassProxyButton = ({ reconnect }: { reconnect: () => void }) => {
const [vpnBypassProxy, setVpnBypassProxy] = useState('') const [vpnBypassProxy, setVpnBypassProxy] = useState('')

View file

@ -9,7 +9,7 @@ const endExitStyle = { opacity: 0, transform: 'translateZ(150px)' }
const endStyle = { opacity: 1, transform: 'translateZ(0)' } const endStyle = { opacity: 1, transform: 'translateZ(0)' }
const stateStyles = { const stateStyles = {
entering: startStyle, entering: endStyle,
entered: endStyle, entered: endStyle,
exiting: endExitStyle, exiting: endExitStyle,
exited: endExitStyle, exited: endExitStyle,
@ -26,6 +26,15 @@ export default ({ children, open }) => {
if (!mounted && open) { if (!mounted && open) {
setMounted(true) setMounted(true)
} }
let timeout
if (mounted && !open) {
timeout = setTimeout(() => {
setMounted(false)
}, duration)
}
return () => {
if (timeout) clearTimeout(timeout)
}
}, [open]) }, [open])
if (!mounted) return null if (!mounted) return null
@ -43,5 +52,4 @@ export default ({ children, open }) => {
</div> </div>
}} }}
</Transition> </Transition>
} }

View file

@ -0,0 +1,12 @@
@keyframes loading-chunks-loading-animation {
/* blink */
0% {
opacity: 0.5;
}
50% {
opacity: 1;
}
100% {
opacity: 0.5;
}
}

View file

@ -0,0 +1,43 @@
import type { Meta, StoryObj } from '@storybook/react'
import { useEffect, useState } from 'react'
import LoadingChunks from './LoadingChunks'
const meta: Meta<typeof LoadingChunks> = {
component: LoadingChunks,
render (args) {
const [stateMap, setStateMap] = useState(Object.fromEntries(args.regionFiles!.map(x => x.split('.').slice(1, 3).map(Number).map(y => y.toString()).join(',')).map(x => [x, 'loading'])))
useEffect(() => {
const interval = setInterval(() => {
// pick random and set to done
const random = Math.floor(Math.random() * args.regionFiles!.length)
const [x, z] = args.regionFiles![random].split('.').slice(1, 3).map(Number)
setStateMap(prev => ({ ...prev, [`${x},${z}`]: 'done' }))
}, 1000)
return () => clearInterval(interval)
}, [])
return <LoadingChunks stateMap={stateMap} {...args} />
},
}
export default meta
type Story = StoryObj<typeof LoadingChunks>
export const Primary: Story = {
args: {
regionFiles: [
'r.-1.-1.mca',
'r.-1.0.mca',
'r.0.-1.mca',
'r.0.0.mca',
'r.0.1.mca',
],
playerChunk: {
x: -1,
z: 0
},
displayText: true,
},
}

View file

@ -0,0 +1,60 @@
import { useEffect, useRef } from 'react'
import './LoadingChunks.css'
export default ({ regionFiles = [] as string[], stateMap = {} as Record<string, string>, displayText = false, playerChunk = null as null | { x: number, z: number } }) => {
// visualize downloading chunks
const regionNumbers = regionFiles.map(x => x.split('.').slice(1, 3).map(Number))
const minX = Math.min(...regionNumbers.map(([x]) => x))
const maxX = Math.max(...regionNumbers.map(([x]) => x))
const minZ = Math.min(...regionNumbers.map(([, z]) => z))
const maxZ = Math.max(...regionNumbers.map(([, z]) => z))
const xChunks = maxX - minX + 1
const zChunks = maxZ - minZ + 1
return <div style={{
// maxWidth: '80%',
// maxHeight: '80%',
// aspectRatio: '1',
display: 'grid',
gridTemplateColumns: `repeat(${xChunks}, 1fr)`,
gridTemplateRows: `repeat(${zChunks}, 1fr)`,
gap: 1,
width: '110px',
height: '110px',
}}>
{Array.from({ length: xChunks * zChunks }).map((_, i) => {
const x = minX + i % xChunks
const z = minZ + Math.floor(i / xChunks)
const file = `r.${x}.${z}.mca`
const state = stateMap[file]
if (!regionFiles.includes(file)) return <div key={i} style={{ background: 'gray' }} />
return <Chunk key={i} x={x} z={z} state={state} displayText={displayText} currentPlayer={playerChunk?.x === x && playerChunk?.z === z} />
})}
</div>
}
const Chunk = ({ x, z, state, displayText, currentPlayer }) => {
const text = displayText ? `${x},${z}` : undefined
return <div style={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
background: state === 'errored' ? 'red' : state === 'loading' ? 'white' : 'limegreen',
animation: state === 'loading' ? `loading-chunks-loading-animation 4s infinite cubic-bezier(0.4, 0, 0.2, 1)` : undefined,
transition: 'background 1s',
color: state === 'loading' ? 'black' : 'white',
position: 'relative',
zIndex: 1,
}}>
{/* green dot */}
{currentPlayer && <div style={{
position: 'absolute',
background: 'reed',
borderRadius: '50%',
width: '5px',
height: '5px',
zIndex: -1,
}} />}
{text}</div>
}

View file

@ -1,9 +1,12 @@
import { join } from 'path' import { join } from 'path'
import fs from 'fs' import fs from 'fs'
import { useEffect } from 'react' import { useEffect } from 'react'
import { useSnapshot } from 'valtio' import { subscribe, useSnapshot } from 'valtio'
import { usedServerPathsV1 } from 'flying-squid/dist/lib/modules/world' import { usedServerPathsV1 } from 'flying-squid/dist/lib/modules/world'
import { openURL } from 'prismarine-viewer/viewer/lib/simpleUtils' import { openURL } from 'prismarine-viewer/viewer/lib/simpleUtils'
import { Vec3 } from 'vec3'
import { generateSpiralMatrix } from 'flying-squid/dist/utils'
import { subscribeKey } from 'valtio/utils'
import { import {
activeModalStack, activeModalStack,
showModal, showModal,
@ -23,14 +26,29 @@ import Screen from './Screen'
import styles from './PauseScreen.module.css' import styles from './PauseScreen.module.css'
import { DiscordButton } from './DiscordButton' import { DiscordButton } from './DiscordButton'
import { showNotification } from './NotificationProvider' import { showNotification } from './NotificationProvider'
import { appStatusState } from './AppStatusProvider'
const waitForPotentialRender = async () => {
return new Promise<void>(resolve => {
requestAnimationFrame(() => requestAnimationFrame(resolve as any))
})
}
export const saveToBrowserMemory = async () => { export const saveToBrowserMemory = async () => {
setLoadingScreenStatus('Saving world') setLoadingScreenStatus('Saving world')
try { try {
await new Promise<void>(resolve => {
subscribeKey(appStatusState, 'isDisplaying', () => {
if (appStatusState.isDisplaying) {
resolve()
}
})
})
//@ts-expect-error //@ts-expect-error
const { worldFolder } = localServer.options const { worldFolder } = localServer.options
const saveRootPath = await uniqueFileNameFromWorldName(worldFolder.split('/').pop(), `/data/worlds`) const saveRootPath = await uniqueFileNameFromWorldName(worldFolder.split('/').pop(), `/data/worlds`)
await mkdirRecursive(saveRootPath) await mkdirRecursive(saveRootPath)
console.log('made world folder', saveRootPath)
const allRootPaths = [...usedServerPathsV1] const allRootPaths = [...usedServerPathsV1]
const allFilesToCopy = [] as string[] const allFilesToCopy = [] as string[]
for (const dirBase of allRootPaths) { for (const dirBase of allRootPaths) {
@ -46,39 +64,87 @@ export const saveToBrowserMemory = async () => {
} }
allFilesToCopy.push(...res.map(x => join(dirBase, x))) allFilesToCopy.push(...res.map(x => join(dirBase, x)))
} }
const pathsSplit = allFilesToCopy.reduce((acc, cur, i) => { console.log('paths collected')
if (i % 15 === 0) { const pathsSplitBasic = allFilesToCopy.filter(path => {
acc.push([]) if (!path.startsWith('region/')) return true
} const [x, z] = path.split('/').at(-1)!.split('.').slice(1, 3).map(Number)
acc.at(-1)!.push(cur) return Math.abs(x) > 50 || Math.abs(z) > 50 // HACK: otherwise it's too big and we can't handle it in visual display
return acc })
// eslint-disable-next-line @typescript-eslint/prefer-reduce-type-parameter
}, [] as string[][])
let copied = 0 let copied = 0
const upProgress = () => { let isRegionFiles = false
const upProgress = (totalSize: number) => {
copied++ copied++
const action = fsState.remoteBackend ? 'Downloading & copying' : 'Copying' let action = fsState.remoteBackend ? 'Downloading & copying' : 'Copying'
setLoadingScreenStatus(`${action} files (${copied}/${allFilesToCopy.length})`) action += isRegionFiles ? ' region files (world chunks)' : ' basic save files'
setLoadingScreenStatus(`${action} files (${copied}/${totalSize})`)
} }
for (const copyPaths of pathsSplit) { const copyFiles = async (copyPaths: string[][]) => {
// eslint-disable-next-line no-await-in-loop const totalSIze = copyPaths.flat().length
await Promise.all(copyPaths.map(async (copyPath) => { for (const copyFileGroup of copyPaths) {
const srcPath = join(worldFolder, copyPath) // eslint-disable-next-line no-await-in-loop, @typescript-eslint/no-loop-func
const savePath = join(saveRootPath, copyPath) await Promise.all(copyFileGroup.map(async (copyPath) => {
await mkdirRecursive(savePath) const srcPath = join(worldFolder, copyPath)
await fs.promises.writeFile(savePath, await fs.promises.readFile(srcPath)) const savePath = join(saveRootPath, copyPath)
upProgress() await mkdirRecursive(savePath)
})) await fs.promises.writeFile(savePath, await fs.promises.readFile(srcPath))
upProgress(totalSIze)
if (isRegionFiles) {
const regionFile = copyPath.split('/').at(-1)!
appStatusState.loadingChunksData![regionFile] = 'done'
}
}))
// eslint-disable-next-line no-await-in-loop
await waitForPotentialRender()
}
} }
// basic save files
await copyFiles(splitByCopySize(pathsSplitBasic))
setLoadingScreenStatus('Preparing world chunks copying')
await waitForPotentialRender()
// region files
isRegionFiles = true
copied = 0
const regionFiles = allFilesToCopy.filter(x => !pathsSplitBasic.includes(x))
const regionFilesNumbers = regionFiles.map(x => x.split('/').at(-1)!.split('.').slice(1, 3).map(Number))
const xMin = Math.min(...regionFilesNumbers.flatMap(x => x[0]))
const zMin = Math.min(...regionFilesNumbers.flatMap(x => x[1]))
const xMax = Math.max(...regionFilesNumbers.flatMap(x => x[0]))
const zMax = Math.max(...regionFilesNumbers.flatMap(x => x[1]))
const playerPosRegion = bot.entity.position.divide(new Vec3(32 * 16, 32 * 16, 32 * 16)).floored()
const maxDistantRegion = Math.max(
Math.abs(playerPosRegion.x - xMin),
Math.abs(playerPosRegion.z - zMin),
Math.abs(playerPosRegion.x - xMax),
Math.abs(playerPosRegion.z - zMax)
)
const spiral = generateSpiralMatrix(maxDistantRegion)
const filesWithSpiral = spiral.filter(x => allFilesToCopy.includes(`region/r.${x[0]}.${x[1]}.mca`)).map(x => `region/r.${x[0]}.${x[1]}.mca`)
if (filesWithSpiral.length !== regionFiles.length) throw new Error('Something went wrong with region files')
appStatusState.loadingChunksData = Object.fromEntries(regionFiles.map(x => [x.split('/').at(-1)!, 'loading']))
appStatusState.loadingChunksDataPlayerChunk = { x: playerPosRegion.x, z: playerPosRegion.z }
await copyFiles(splitByCopySize(filesWithSpiral, 10))
return saveRootPath return saveRootPath
} catch (err) { } catch (err) {
console.error(err)
void showOptionsModal(`Error while saving the world: ${err.message}`, []) void showOptionsModal(`Error while saving the world: ${err.message}`, [])
} finally { } finally {
setLoadingScreenStatus(undefined) setLoadingScreenStatus(undefined)
} }
} }
const splitByCopySize = (files: string[], copySize = 15) => {
return files.reduce<string[][]>((acc, cur, i) => {
if (i % copySize === 0) {
acc.push([])
}
acc.at(-1)!.push(cur)
return acc
}, [])
}
export default () => { export default () => {
const qsParams = new URLSearchParams(window.location.search) const qsParams = new URLSearchParams(window.location.search)
const lockConnect = qsParams?.get('lockConnect') === 'true' const lockConnect = qsParams?.get('lockConnect') === 'true'

View file

@ -1,6 +1,6 @@
import { hideModal, isGameActive, miscUiState, showModal } from './globalState' import { activeModalStack, hideModal, isGameActive, miscUiState, showModal } from './globalState'
import { options } from './optionsStorage' import { options } from './optionsStorage'
import { appStatusState } from './react/AppStatusProvider' import { appStatusState, resetAppStatusState } from './react/AppStatusProvider'
import { notificationProxy, showNotification } from './react/NotificationProvider' import { notificationProxy, showNotification } from './react/NotificationProvider'
export const goFullscreen = async (doToggle = false) => { export const goFullscreen = async (doToggle = false) => {
@ -139,7 +139,10 @@ export const setLoadingScreenStatus = function (status: string | undefined | nul
return return
} }
// todo update in component instead if (!activeModalStack.some(x => x.reactType === 'app-status')) {
// just showing app status
resetAppStatusState()
}
showModal({ reactType: 'app-status' }) showModal({ reactType: 'app-status' })
if (appStatusState.isError) { if (appStatusState.isError) {
miscUiState.gameLoaded = false miscUiState.gameLoaded = false