enable strict null types which improves quality of codebase a lot!

todo add types to worker & world renderer
This commit is contained in:
Vitaly 2023-11-01 03:07:07 +03:00
commit d546fa8f41
31 changed files with 150 additions and 93 deletions

View file

@ -21,7 +21,7 @@ export class Viewer {
isSneaking: boolean
version: string
constructor (public renderer: THREE.WebGLRenderer, numWorkers = undefined) {
constructor (public renderer: THREE.WebGLRenderer, numWorkers?: number) {
this.scene = new THREE.Scene()
this.scene.background = new THREE.Color('lightblue')
@ -80,7 +80,7 @@ export class Viewer {
this.primitives.update(p)
}
setFirstPersonCamera (pos: Vec3, yaw: number, pitch: number, roll = 0) {
setFirstPersonCamera (pos: Vec3 | null, yaw: number, pitch: number, roll = 0) {
if (pos) {
let y = pos.y + this.playerHeight
if (this.isSneaking) y -= 0.3

View file

@ -158,6 +158,7 @@ export class WorldDataEmitter extends EventEmitter {
const positions = generateSpiralMatrix(this.viewDistance).map(([x, z]) => {
const pos = new Vec3((botX + x) * 16, 0, (botZ + z) * 16)
if (!this.loadedChunks[`${pos.x},${pos.z}`]) return pos
return undefined!
}).filter(Boolean)
this.lastPos.update(pos)
await this._loadChunks(positions)

View file

@ -13,7 +13,7 @@ import { versionToNumber } from './utils'
const legacyInvsprite = JSON.parse(fs.readFileSync(join(__dirname, '../../../src/invsprite.json'), 'utf8'))
//@ts-ignore
const latestMcAssetsVersion = McAssets.versions.at(-1)
const latestMcAssetsVersion = McAssets.versions.at(-1)!
// const latestVersion = minecraftDataLoader.supportedVersions.pc.at(-1)
const mcData = minecraftDataLoader(latestMcAssetsVersion)
const PBlock = BlockLoader(latestMcAssetsVersion)

View file

@ -57,7 +57,7 @@ export const renderSign = (blockEntity: SignBlockEntity, PrismarineChat: typeof
const defaultColor = ('front_text' in blockEntity ? blockEntity.front_text.color : blockEntity.Color) || 'black'
for (let [lineNum, text] of texts.slice(0, 4).entries()) {
// todo: in pre flatenning it seems the format was not json
const parsed = text.startsWith('{') ? parseSafe(text ?? '""', 'sign text') : text
const parsed = text?.startsWith('{') ? parseSafe(text ?? '""', 'sign text') : text
if (!parsed || (typeof parsed !== 'object' && typeof parsed !== 'string')) continue
// todo fix type
const message = typeof parsed === 'string' ? fromFormattedString(parsed) : new PrismarineChat(parsed) as never

View file

@ -4,8 +4,8 @@ let audioContext: AudioContext
const sounds: Record<string, any> = {}
// 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 = []
const convertedSounds = []
const loadingSounds = [] as string[]
const convertedSounds = [] as string[]
export async function loadSound (path: string) {
if (loadingSounds.includes(path)) return
loadingSounds.push(path)

View file

@ -57,6 +57,7 @@ fs.promises = new Proxy(Object.fromEntries(['readFile', 'writeFile', 'stat', 'mk
})
//@ts-expect-error
fs.promises.open = async (...args) => {
//@ts-expect-error
const fd = await promisify(fs.open)(...args)
return {
...Object.fromEntries(['read', 'write', 'close'].map(x => [x, async (...args) => {
@ -127,7 +128,7 @@ export const mkdirRecursive = async (path: string) => {
export const uniqueFileNameFromWorldName = async (title: string, savePath: string) => {
const name = sanitizeFilename(title)
let resultPath: string
let resultPath!: string
// getUniqueFolderName
let i = 0
let free = false
@ -176,7 +177,7 @@ export const mountExportFolder = async () => {
}
export async function removeFileRecursiveAsync (path) {
const errors = []
const errors = [] as Array<[string, Error]>
try {
const files = await fs.promises.readdir(path)

View file

@ -53,7 +53,7 @@ export const exportWorld = async (path: string, type: 'zip' | 'folder', zipName
setLoadingScreenStatus('Preparing export folder')
let dest = '/'
if ((await fs.promises.readdir('/export')).length) {
const { levelDat } = await readLevelDat(path)
const { levelDat } = (await readLevelDat(path))!
dest = await uniqueFileNameFromWorldName(levelDat.LevelName, path)
}
setLoadingScreenStatus(`Copying files to ${dest} of selected folder`)

View file

@ -229,6 +229,7 @@ class ChatBox extends LitElement {
}
notification.show = false
// @ts-expect-error
const chat = this.shadowRoot.getElementById('chat-messages')
/** @type {HTMLInputElement} */
// @ts-expect-error
@ -258,10 +259,12 @@ class ChatBox extends LitElement {
* @param {import('minecraft-protocol').Client} client
*/
init (client) {
// @ts-expect-error
const chat = this.shadowRoot.getElementById('chat-messages')
/** @type {HTMLInputElement} */
// @ts-expect-error
const chatInput = this.shadowRoot.getElementById('chatinput')
/** @type {any} */
this.chatInput = chatInput
// Show chat
@ -330,6 +333,7 @@ class ChatBox extends LitElement {
fading: false,
faded: false
}]
/** @type {any} */
const message = this.messages.at(-1)
chat.scrollTop = chat.scrollHeight // Stay bottom of the list
@ -358,6 +362,7 @@ class ChatBox extends LitElement {
const completeValue = this.getCompleteValue()
this.completePadText = completeValue === '/' ? '' : completeValue
if (this.completeRequestValue === completeValue) {
/** @type {any} */
const lastWord = chatInput.value.split(' ').at(-1)
this.completionItems = this.completionItemsSource.filter(i => {
const compareableParts = i.split(/[_:]/)
@ -425,6 +430,7 @@ class ChatBox extends LitElement {
}
/** @type {string[]} */
// @ts-expect-error
const applyStyles = [
color ? colorF(color.toLowerCase()) + `; text-shadow: 1px 1px 0px ${getColorShadow(colorF(color.toLowerCase()).replace('color:', ''))}` : messageFormatStylesMap.white,
italic && messageFormatStylesMap.italic,
@ -458,6 +464,7 @@ class ChatBox extends LitElement {
}
updateInputValue (value) {
/** @type {any} */
const { chatInput } = this
chatInput.value = value
chatInput.dispatchEvent(new Event('input'))

View file

@ -29,8 +29,8 @@ export const contro = new ControMax({
prevHotbarSlot: [null, 'Right Bumper'],
attackDestroy: [null, 'Right Trigger'],
interactPlace: [null, 'Left Trigger'],
chat: [['KeyT', 'Enter'], null],
command: ['Slash', null],
chat: [['KeyT', 'Enter']],
command: ['Slash'],
},
ui: {
back: [null/* 'Escape' */, 'B'],
@ -81,7 +81,8 @@ contro.on('movementUpdate', ({ vector, gamepadIndex }) => {
if (v === undefined || Math.abs(v) < 0.3) continue
// todo use raw values eg for slow movement
const mappedValue = v < 0 ? -1 : 1
const foundAction = coordToAction.find(([c, mapV]) => c === coord && mapV === mappedValue)?.[2]
// 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
}
@ -223,13 +224,14 @@ contro.on('release', ({ command }) => {
const hardcodedPressedKeys = new Set<string>()
document.addEventListener('keydown', (e) => {
if (!isGameActive(false)) return
if (hardcodedPressedKeys.has('F3')) {
// reload chunks
if (e.code === 'KeyA') {
//@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 })
worldView!.unloadChunk({ x, z })
}
if (localServer) {
localServer.players[0].world.columns = {}
@ -280,7 +282,11 @@ const startFlyLoop = () => {
endFlyLoop?.()
endFlyLoop = makeInterval(() => {
if (!bot) endFlyLoop()
if (!bot) {
endFlyLoop?.()
return
}
bot.entity.position.add(currentFlyVector.clone().multiply(new Vec3(0, 0.5, 0)))
}, 50)
}

View file

@ -1,7 +1,9 @@
import { options } from './optionsStorage'
import { assertDefined } from './utils'
export default () => {
bot.on('time', () => {
assertDefined(viewer)
// 0 morning
const dayTotal = 24_000
const evening = 12_542

View file

@ -7,7 +7,7 @@ const getFixedFilesize = (bytes: number) => {
return prettyBytes(bytes, { minimumFractionDigits: 2, maximumFractionDigits: 2 })
}
export default async () => {
const inner = async () => {
const qs = new URLSearchParams(window.location.search)
let mapUrl = qs.get('map')
const texturepack = qs.get('texturepack')
@ -33,13 +33,15 @@ export default async () => {
if (!contentType || !contentType.startsWith('application/zip')) {
alert('Invalid map file')
}
const contentLength = +response.headers.get('Content-Length')
setLoadingScreenStatus(`Downloading ${downloadThing} ${name}: have to download ${getFixedFilesize(contentLength)}...`)
const contentLengthStr = response.headers?.get('Content-Length')
const contentLength = contentLengthStr && +contentLengthStr
setLoadingScreenStatus(`Downloading ${downloadThing} ${name}: have to download ${contentLength && getFixedFilesize(contentLength)}...`)
let downloadedBytes = 0
const buffer = await new Response(
new ReadableStream({
async start (controller) {
if (!response.body) throw new Error('Server returned no response!')
const reader = response.body.getReader()
// eslint-disable-next-line no-constant-condition
@ -54,12 +56,9 @@ export default async () => {
downloadedBytes += value.byteLength
// Calculate download progress as a percentage
const progress = (downloadedBytes / contentLength) * 100
const progress = contentLength ? (downloadedBytes / contentLength) * 100 : undefined
setLoadingScreenStatus(`Download ${downloadThing} progress: ${progress === undefined ? '?' : Math.floor(progress)}% (${getFixedFilesize(downloadedBytes)} / ${contentLength && getFixedFilesize(contentLength)})`, false, true)
// Update your progress bar or display the progress value as needed
if (contentLength) {
setLoadingScreenStatus(`Download ${downloadThing} progress: ${Math.floor(progress)}% (${getFixedFilesize(downloadedBytes)} / ${getFixedFilesize(contentLength)})`, false, true)
}
// Pass the received data to the controller
controller.enqueue(value)
@ -74,3 +73,12 @@ export default async () => {
await openWorldZip(buffer)
}
}
export default async () => {
try {
return await inner()
} catch (err) {
setLoadingScreenStatus(`Failed to download. Either refresh page or remove mapUrl param from URL. Reason: ${err.message}`)
return true
}
}

View file

@ -96,7 +96,7 @@ export const hideModal = (modal = activeModalStack.at(-1), data: any = undefined
}
}
export const hideCurrentModal = (_data = undefined, onHide = undefined) => {
export const hideCurrentModal = (_data?, onHide?: () => void) => {
if (hideModal(undefined, undefined)) {
onHide?.()
}

View file

@ -110,7 +110,7 @@ viewer.entities.entitiesOptions = {
watchOptionsAfterViewerInit()
watchTexturepackInViewer(viewer)
let renderInterval: number
let renderInterval: number | false
watchValue(options, (o) => {
renderInterval = o.frameLimit && 1000 / o.frameLimit
})
@ -214,7 +214,7 @@ function listenGlobalEvents () {
})
}
let listeners = []
let listeners = [] as Array<{ target, event, callback }>
// only for dom listeners (no removeAllListeners)
// todo refactor them out of connect fn instead
const registerListener: import('./utilsTs').RegisterListener = (target, event, callback) => {
@ -241,7 +241,7 @@ const cleanConnectIp = (host: string | undefined, defaultPort: string | undefine
}
async function connect (connectOptions: {
server?: string; singleplayer?: any; username?: string; password?: any; proxy?: any; botVersion?: any; serverOverrides?; peerId?: string
server?: string; singleplayer?: any; username: string; password?: any; proxy?: any; botVersion?: any; serverOverrides?; peerId?: string
}) {
document.getElementById('play-screen').style = 'display: none;'
removePanorama()
@ -261,7 +261,7 @@ async function connect (connectOptions: {
setLoadingScreenStatus('Logging in')
let ended = false
let bot: typeof __type_bot
let bot!: typeof __type_bot
const destroyAll = () => {
if (ended) return
ended = true
@ -275,7 +275,9 @@ async function connect (connectOptions: {
bot.emit('end', '')
bot.removeAllListeners()
bot._client.removeAllListeners()
//@ts-expect-error TODO?
bot._client = undefined
//@ts-expect-error
window.bot = bot = undefined
}
if (singleplayer && !fsState.inMemorySave) {
@ -394,10 +396,10 @@ async function connect (connectOptions: {
setLoadingScreenStatus(initialLoadingText)
bot = mineflayer.createBot({
host: server.host,
port: +server.port,
port: server.port ? +server.port : undefined,
version: connectOptions.botVersion || false,
...p2pMultiplayer ? {
stream: await connectToPeer(connectOptions.peerId),
stream: await connectToPeer(connectOptions.peerId!),
} : {},
...singleplayer || p2pMultiplayer ? {
keepAlive: false,
@ -495,6 +497,7 @@ async function connect (connectOptions: {
onBotCreate()
bot.once('login', () => {
if (!connectOptions.server) return
// server is ok, add it to the history
const serverHistory: string[] = JSON.parse(localStorage.getItem('serverHistory') || '[]')
serverHistory.unshift(connectOptions.server)
@ -517,7 +520,7 @@ async function connect (connectOptions: {
const center = bot.entity.position
const worldView = window.worldView = new WorldDataEmitter(bot.world, singleplayer ? renderDistance : Math.min(renderDistance, maxMultiplayerRenderDistance), center)
const worldView = window.worldView = new WorldDataEmitter(bot.world, singleplayer ? renderDistance : Math.min(renderDistance, maxMultiplayerRenderDistance!), center)
setRenderDistance()
bot.on('physicsTick', () => updateCursor())
@ -537,9 +540,9 @@ async function connect (connectOptions: {
try {
const gl = renderer.getContext()
debugMenu.rendererDevice = gl.getParameter(gl.getExtension('WEBGL_debug_renderer_info').UNMASKED_RENDERER_WEBGL)
debugMenu.rendererDevice = gl.getParameter(gl.getExtension('WEBGL_debug_renderer_info')!.UNMASKED_RENDERER_WEBGL)
} catch (err) {
console.error(err)
console.warn(err)
debugMenu.rendererDevice = '???'
}
@ -554,7 +557,7 @@ async function connect (connectOptions: {
function botPosition () {
// this might cause lag, but not sure
viewer.setFirstPersonCamera(bot.entity.position, bot.entity.yaw, bot.entity.pitch)
worldView.updatePosition(bot.entity.position)
void worldView.updatePosition(bot.entity.position)
}
bot.on('move', botPosition)
botPosition()
@ -585,7 +588,7 @@ async function connect (connectOptions: {
let virtualClickActive = false
let virtualClickTimeout
let screenTouches = 0
let capturedPointer: { id; x; y; sourceX; sourceY; activateCameraMove; time } | null
let capturedPointer: { id; x; y; sourceX; sourceY; activateCameraMove; time } | undefined
registerListener(document, 'pointerdown', (e) => {
const clickedEl = e.composedPath()[0]
if (!isGameActive(true) || !miscUiState.currentTouch || clickedEl !== cameraControlEl || e.pointerId === undefined) {
@ -731,7 +734,7 @@ downloadAndOpenFile().then((downloadAction) => {
const peerId = qs.get('connectPeer')
const version = qs.get('peerVersion')
if (peerId) {
let username = options.guestUsername
let username: string | null = options.guestUsername
if (options.askGuestName) username = prompt('Enter your username', username)
if (!username) return
options.guestUsername = username

View file

@ -39,7 +39,7 @@ export const readLevelDat = async (path) => {
}
const { parsed } = await nbt.parse(Buffer.from(levelDatContent))
const levelDat: import('./mcTypes').LevelDat = nbt.simplify(parsed).Data
return { levelDat, dataRaw: parsed.value.Data.value as Record<string, any> }
return { levelDat, dataRaw: parsed.value.Data!.value as Record<string, any> }
}
export const loadSave = async (root = '/world') => {
@ -54,7 +54,7 @@ export const loadSave = async (root = '/world') => {
// todo check jsHeapSizeLimit
const warnings: string[] = []
const { levelDat, dataRaw } = await readLevelDat(root)
const { levelDat, dataRaw } = (await readLevelDat(root))!
if (levelDat === undefined) {
if (fsState.isReadonly) {
throw new Error('level.dat not found, ensure you are loading world folder')
@ -63,7 +63,7 @@ export const loadSave = async (root = '/world') => {
}
}
let version: string | undefined
let version: string | undefined | null
let isFlat = false
if (levelDat) {
const qs = new URLSearchParams(window.location.search)
@ -73,7 +73,7 @@ export const loadSave = async (root = '/world') => {
if (!newVersion) return
version = newVersion
}
const lastSupportedVersion = supportedVersions.at(-1)
const lastSupportedVersion = supportedVersions.at(-1)!
const firstSupportedVersion = supportedVersions[0]
const lowerBound = isMajorVersionGreater(firstSupportedVersion, version)
const upperBound = isMajorVersionGreater(version, lastSupportedVersion)

View file

@ -29,7 +29,7 @@ export const getJoinLink = () => {
const copyJoinLink = async () => {
miscUiState.wanOpened = true
const joinLink = getJoinLink()
const joinLink = getJoinLink()!
if (navigator.clipboard) {
await navigator.clipboard.writeText(joinLink)
} else {

View file

@ -1,4 +1,3 @@
//@ts-check
const { LitElement, html, css, unsafeCSS } = require('lit')
const { showModal, miscUiState } = require('../globalState')
const { options, watchValue } = require('../optionsStorage')

View file

@ -20,6 +20,7 @@ import PrismarineBlockLoader from 'prismarine-block'
import { activeModalStack, hideCurrentModal, miscUiState, showModal } from './globalState'
import invspriteJson from './invsprite.json'
import { options } from './optionsStorage'
import { assertDefined } from './utils'
const itemsAtlases: ItemsAtlasesOutputJson = _itemsAtlases
const loadedImagesCache = new Map<string, HTMLImageElement>()
@ -74,12 +75,13 @@ export const onGameLoad = (onLoad) => {
text: `[client error] cannot open unimplemented window ${win.id} (${win.type}). Items: ${win.slots.map(slot => slot?.name).join(', ')}`
})
})
bot.currentWindow['close']()
bot.currentWindow?.['close']()
}
})
}
const findTextureInBlockStates = (name) => {
assertDefined(viewer)
const blockStates: BlockStates = viewer.world.customBlockStatesData || viewer.world.downloadedBlockStatesData
const vars = blockStates[name]?.variants
if (!vars) return
@ -92,7 +94,7 @@ const findTextureInBlockStates = (name) => {
}
const svSuToCoordinates = (path: string, u, v, su, sv = su) => {
const img = getImage({ path })
const img = getImage({ path })!
if (!img.width) throw new Error(`Image ${path} is not loaded`)
return [u * img.width, v * img.height, su * img.width, sv * img.height]
}
@ -130,6 +132,7 @@ const getInvspriteSlice = (name) => {
}
const getImageSrc = (path): string | HTMLImageElement => {
assertDefined(viewer)
switch (path) {
case 'gui/container/inventory': return InventoryGui
case 'blocks': return viewer.world.customTexturesDataUrl || viewer.world.downloadedTextureImage
@ -145,8 +148,9 @@ const getImageSrc = (path): string | HTMLImageElement => {
return Dirt
}
const getImage = ({ path = undefined, texture = undefined, blockData = undefined }, onLoad = () => {}) => {
const loadPath = blockData ? 'blocks' : path ?? texture
const getImage = ({ path = undefined as string | undefined, texture = undefined as string | undefined, blockData = undefined as any }, onLoad = () => {}) => {
if (!path && !texture) throw new Error('Either pass path or texture')
const loadPath = (blockData ? 'blocks' : path ?? texture)!
if (loadedImagesCache.has(loadPath)) {
onLoad()
} else {
@ -184,7 +188,7 @@ const isFullBlock = (block: string) => {
return shape[0] === 0 && shape[1] === 0 && shape[2] === 0 && shape[3] === 1 && shape[4] === 1 && shape[5] === 1
}
const renderSlot = (slot: import('prismarine-item').Item, skipBlock = false): { texture: string, blockData?, scale?: number, slice?: number[] } => {
const renderSlot = (slot: import('prismarine-item').Item, skipBlock = false): { texture: string, blockData?, scale?: number, slice?: number[] } | undefined => {
const itemName = slot.name
const isItem = loadedData.itemsByName[itemName]
const fullBlock = isFullBlock(itemName)
@ -230,7 +234,7 @@ export const renderSlotExternal = (slot) => {
const data = renderSlot(slot, true)
if (!data) return
return {
imageDataUrl: data.texture === 'invsprite' ? undefined : getImage({ path: data.texture }).src,
imageDataUrl: data.texture === 'invsprite' ? undefined : getImage({ path: data.texture })?.src,
sprite: data.slice && data.texture !== 'invsprite' ? data.slice.map(x => x * 2) : data.slice
}
}
@ -238,7 +242,7 @@ export const renderSlotExternal = (slot) => {
const upInventory = (inventory: boolean) => {
// inv.pwindow.inv.slots[2].displayName = 'test'
// inv.pwindow.inv.slots[2].blockData = getBlockData('dirt')
const updateSlots = (inventory ? bot.inventory : bot.currentWindow).slots.map(slot => {
const updateSlots = (inventory ? bot.inventory : bot.currentWindow)!.slots.map(slot => {
// todo stateid
if (!slot) return
@ -277,7 +281,7 @@ const implementedContainersGuiMap = {
const openWindow = (type: string | undefined) => {
// if (activeModalStack.some(x => x.reactType?.includes?.('player_win:'))) {
if (activeModalStack.length) { // game is not in foreground, don't close current modal
if (type) bot.currentWindow['close']()
if (type) bot.currentWindow?.['close']()
return
}
showModal({

View file

@ -4,7 +4,7 @@ import styles from './appStatus.module.css'
import Button from './Button'
import Screen from './Screen'
export default ({ status, isError, hideDots = false, lastStatus = '', backAction = undefined, actionsSlot = undefined }) => {
export default ({ status, isError, hideDots = false, lastStatus = '', backAction = undefined as undefined | (() => void), actionsSlot = undefined }) => {
const [loadingDots, setLoadingDots] = useState('')
useEffect(() => {

View file

@ -27,11 +27,11 @@ export default () => {
useDidUpdateEffect(() => {
// todo play effect only when world successfully loaded
if (!isOpen) {
const divingElem: HTMLElement = document.querySelector('#viewer-canvas')
const divingElem: HTMLElement = document.querySelector('#viewer-canvas')!
divingElem.style.animationName = 'dive-animation'
divingElem.parentElement.style.perspective = '1200px'
divingElem.parentElement!.style.perspective = '1200px'
divingElem.onanimationend = () => {
divingElem.parentElement.style.perspective = ''
divingElem.parentElement!.style.perspective = ''
divingElem.onanimationend = null
}
}
@ -47,7 +47,7 @@ export default () => {
appStatusState.isError = false
resetState()
miscUiState.gameLoaded = false
miscUiState.loadedDataVersion = undefined
miscUiState.loadedDataVersion = null
window.loadedData = undefined
if (activeModalStacks['main-menu']) {
insertActiveModalStack('main-menu')

View file

@ -17,7 +17,7 @@ void loadSound('button_click.mp3')
export default forwardRef<HTMLButtonElement, Props>(({ label, icon, children, inScreen, ...args }, ref) => {
const onClick = (e) => {
void playSound('button_click.mp3')
args.onClick(e)
args.onClick?.(e)
}
if (inScreen) {
args.style ??= {}

View file

@ -21,6 +21,7 @@ export default ({ initialTooltip, ...args }: Props) => {
useEffect(() => {
let timeout
function hide () {
if (!localStorageKey) return
localStorage[localStorageKey] = 'false'
setShowTooltips(false)
}

View file

@ -22,7 +22,7 @@ export default ({ cancelClick, createClick, customizeClick, versions, defaultVer
useEffect(() => {
creatingWorldState.version = defaultVersion
void navigator.storage.estimate().then(({ quota, usage }) => {
setQuota(`Storage usage: ${filesize(usage)} / ${filesize(quota)}`)
setQuota(`Storage usage: ${usage === undefined ? '?' : filesize(usage)} / ${quota ? filesize(quota) : '?'}`)
})
}, [])

View file

@ -1,5 +1,6 @@
import React, { useEffect, useState } from 'react'
import { openURL } from '../menus/components/common'
import { haveDirectoryPicker } from '../utils'
import styles from './mainMenu.module.css'
import Button from './Button'
import ButtonWithTooltip from './ButtonWithTooltip'
@ -18,7 +19,7 @@ interface Props {
const refreshApp = async () => {
const registration = await navigator.serviceWorker.getRegistration()
await registration.unregister()
await registration?.unregister()
window.location.reload()
}
@ -81,7 +82,7 @@ export default ({ connectToServerAction, mapsProvider, singleplayerAction, optio
icon='pixelarticons:folder'
onClick={openFileAction}
initialTooltip={{
content: 'Load any 1.8-1.16 Java world' + (window.showDirectoryPicker ? '' : ' (zip)'),
content: 'Load any 1.8-1.16 Java world' + (haveDirectoryPicker() ? '' : ' (zip)'),
placement: 'bottom-start',
}}
/>

View file

@ -46,7 +46,7 @@ export default () => {
}
showModal({ reactType: 'singleplayer' })
}}
githubAction={() => openURL(process.env.GITHUB_URL)}
githubAction={() => openURL(process.env.GITHUB_URL!)}
optionsAction={() => openOptionsMenu('main')}
discordAction={() => openURL('https://discord.gg/4Ucm684Fq3')}
openFileAction={e => {

View file

@ -30,12 +30,12 @@ export type OptionMeta = GeneralItem & ({
})
export const OptionButton = ({ item }: { item: Extract<OptionMeta, { type: 'toggle' }> }) => {
const optionValue = useSnapshot(options)[item.id]
const optionValue = useSnapshot(options)[item.id!]
return <Button
label={`${item.text}: ${optionValue ? 'ON' : 'OFF'}`}
onClick={() => {
options[item.id] = !options[item.id]
options[item.id!] = !options[item.id!]
}}
title={item.disabledReason ? `${item.disabledReason} | ${item.tooltip}` : item.tooltip}
disabled={!!item.disabledReason}
@ -46,15 +46,15 @@ export const OptionButton = ({ item }: { item: Extract<OptionMeta, { type: 'togg
}
export const OptionSlider = ({ item }: { item: Extract<OptionMeta, { type: 'slider' }> }) => {
const optionValue = useSnapshot(options)[item.id]
const optionValue = useSnapshot(options)[item.id!]
const valueDisplay = useMemo(() => {
if (item.valueText) return item.valueText(optionValue)
return undefined // default display
}, [optionValue])
return <Slider label={item.text} value={options[item.id]} min={item.min} max={item.max} updateValue={(value) => {
options[item.id] = value
return <Slider label={item.text!} value={options[item.id!]} min={item.min} max={item.max} updateValue={(value) => {
options[item.id!] = value
}} unit={item.unit} valueDisplay={valueDisplay} updateOnDragEnd={item.delayApply} />
}

View file

@ -35,6 +35,7 @@ const World = ({ name, isFocused, title, lastPlayed, size, detail = '', onFocus,
return formatter.format(-minutes, 'minute')
}, [lastPlayed])
const sizeFormatted = useMemo(() => {
if (!size) return
return filesize(size)
}, [size])
@ -43,7 +44,7 @@ const World = ({ name, isFocused, title, lastPlayed, size, detail = '', onFocus,
e.preventDefault()
onInteraction?.(e.code === 'Enter' ? 'enter' : 'space')
}
}} onDoubleClick={() => onInteraction('enter')}>
}} onDoubleClick={() => onInteraction?.('enter')}>
<img className={styles.world_image} src={missingWorldPreview} />
<div className={styles.world_info}>
<div className={styles.world_title} title='level.dat world name'>{title}</div>
@ -61,7 +62,7 @@ interface Props {
export default ({ worldData, onGeneralAction, onWorldAction }: Props) => {
const containerRef = useRef<any>()
const firstButton = useRef<HTMLButtonElement>()
const firstButton = useRef<HTMLButtonElement>(null!)
useTypedEventListener(window, 'keydown', (e) => {
if (e.code === 'ArrowDown' || e.code === 'ArrowUp') {

View file

@ -4,7 +4,7 @@ import { useEffect } from 'react'
import { fsState, loadSave, longArrayToNumber, readLevelDat } from '../loadSave'
import { mountExportFolder, removeFileRecursiveAsync } from '../browserfs'
import { hideCurrentModal, showModal } from '../globalState'
import { setLoadingScreenStatus } from '../utils'
import { haveDirectoryPicker, setLoadingScreenStatus } from '../utils'
import { exportWorld } from '../builtinCommands'
import Singleplayer, { WorldProps } from './Singleplayer'
import { useIsModalActive } from './utils'
@ -17,7 +17,7 @@ export const readWorlds = () => {
try {
const worlds = await fs.promises.readdir(`/data/worlds`)
worldsProxy.value = (await Promise.allSettled(worlds.map(async (world) => {
const { levelDat } = await readLevelDat(`/data/worlds/${world}`)
const { levelDat } = (await readLevelDat(`/data/worlds/${world}`))!
let size = 0
// todo use whole dir size
for (const region of await fs.promises.readdir(`/data/worlds/${world}/region`)) {
@ -28,7 +28,7 @@ export const readWorlds = () => {
name: world,
title: levelDat.LevelName,
lastPlayed: levelDat.LastPlayed && longArrayToNumber(levelDat.LastPlayed),
detail: `${levelDat.Version.Name ?? 'unknown version'}, ${world}`,
detail: `${levelDat.Version?.Name ?? 'unknown version'}, ${world}`,
size,
} satisfies WorldProps
}))).filter(x => {
@ -81,7 +81,7 @@ export default () => {
}
if (action === 'export') {
const selectedVariant =
window.showDirectoryPicker
haveDirectoryPicker()
? await showOptionsModal('Select export type', ['Select folder (recommended)', 'Download ZIP file'])
: await showOptionsModal('Select export type', ['Download ZIP file'])
if (!selectedVariant) return

View file

@ -67,20 +67,20 @@ window.getScreenRefreshRate = getScreenRefreshRate
* Allows to obtain the estimated Hz of the primary monitor in the system.
*/
export async function getScreenRefreshRate (): Promise<number> {
let requestId = null
let requestId = null as number | null
let callbackTriggered = false
let resolve
const DOMHighResTimeStampCollection = []
const DOMHighResTimeStampCollection = [] as number[]
const triggerAnimation = (DOMHighResTimeStamp) => {
DOMHighResTimeStampCollection.unshift(DOMHighResTimeStamp)
if (DOMHighResTimeStampCollection.length > 10) {
const t0 = DOMHighResTimeStampCollection.pop()
const t0 = DOMHighResTimeStampCollection.pop()!
const fps = Math.floor(1000 * 10 / (DOMHighResTimeStamp - t0))
if (!callbackTriggered) {
if (!callbackTriggered || fps > 1000) {
resolve(Math.max(fps, 1000)/* , DOMHighResTimeStampCollection */)
}
@ -93,7 +93,7 @@ export async function getScreenRefreshRate (): Promise<number> {
window.requestAnimationFrame(triggerAnimation)
window.setTimeout(() => {
window.cancelAnimationFrame(requestId)
window.cancelAnimationFrame(requestId!)
requestId = null
}, 500)
@ -122,7 +122,7 @@ export const isMajorVersionGreater = (ver1: string, ver2: string) => {
return +a1 > +a2 || (+a1 === +a2 && +b1 > +b2)
}
let ourLastStatus = ''
let ourLastStatus: string | undefined = ''
export const setLoadingScreenStatus = function (status: string | undefined | null, isError = false, hideDots = false, fromFlyingSquid = false) {
// null can come from flying squid, should restore our last status
if (status === null) {
@ -168,6 +168,7 @@ export const toMajorVersion = (version) => {
let prevRenderDistance = options.renderDistance
export const setRenderDistance = () => {
assertDefined(worldView)
worldView.viewDistance = options.renderDistance
if (localServer) {
localServer.players[0].emit('playerChangeRenderDistance', options.renderDistance)
@ -182,14 +183,14 @@ export const reloadChunks = async () => {
export const openFilePicker = (specificCase?: 'resourcepack') => {
// create and show input picker
let picker: HTMLInputElement = document.body.querySelector('input#file-zip-picker')
let picker: HTMLInputElement = document.body.querySelector('input#file-zip-picker')!
if (!picker) {
picker = document.createElement('input')
picker.type = 'file'
picker.accept = '.zip'
picker.addEventListener('change', () => {
const file = picker.files[0]
const file = picker.files?.[0]
picker.value = ''
if (!file) return
if (!file.name.endsWith('.zip')) {
@ -217,3 +218,11 @@ export const resolveTimeout = async (promise, timeout = 10_000) => {
}, timeout)
})
}
export function assertDefined<T> (x: T | undefined): asserts x is T {
if (!x) throw new Error('Assertion failed. Something is not available')
}
export const haveDirectoryPicker = () => {
return !!window.showDirectoryPicker
}

View file

@ -15,6 +15,7 @@ watchValue(options, o => {
export const watchOptionsAfterViewerInit = () => {
watchValue(options, o => {
if (!viewer) return
viewer.world.showChunkBorders = o.showChunkBorders
})
}

View file

@ -14,6 +14,7 @@ import destroyStage9 from 'minecraft-assets/minecraft-assets/data/1.10/blocks/de
import { Vec3 } from 'vec3'
import { isGameActive } from './globalState'
import { assertDefined } from './utils'
function getViewDirection (pitch, yaw) {
const csPitch = Math.cos(pitch)
@ -30,8 +31,19 @@ class WorldInteraction {
prevBreakState
currentDigTime
prevOnGround
/** @type {number} */
lastBlockPlaced
buttons = [false, false, false]
lastButtons = [false, false, false]
/** @type {number | undefined} */
breakStartTime = 0
/** @type {import('prismarine-block').Block | null} */
cursorBlock = null
/** @type {THREE.Mesh} */
blockBreakMesh
init () {
assertDefined(viewer)
bot.on('physicsTick', () => { if (this.lastBlockPlaced < 4) this.lastBlockPlaced++ })
bot.on('diggingCompleted', () => {
this.breakStartTime = undefined
@ -40,12 +52,6 @@ class WorldInteraction {
this.breakStartTime = undefined
})
// Init state
this.buttons = [false, false, false]
this.lastButtons = [false, false, false]
this.breakStartTime = 0
this.cursorBlock = null
const loader = new THREE.TextureLoader()
this.breakTextures = []
const destroyStagesImages = [
@ -114,16 +120,21 @@ class WorldInteraction {
})
}
updateBlockInteractionLines (/** @type {Vec3 | null} */blockPos, /** @type {{position, width, height, depth}[]} */shapePositions = undefined) {
updateBlockInteractionLines (/** @type {Vec3 | null} */blockPos, /** @type {{position, width, height, depth}[] | undefined} */shapePositions = undefined) {
assertDefined(viewer)
if (blockPos && this.interactionLines && blockPos.equals(this.interactionLines.blockPos)) {
return
}
if (this.interactionLines !== null) {
viewer.scene.remove(this.interactionLines.mesh)
this.interactionLines = null
}
if (blockPos === null || (this.interactionLines && blockPos.equals(this.interactionLines.blockPos))) {
if (blockPos === null) {
return
}
const group = new THREE.Group()
//@ts-expect-error
for (const { position, width, height, depth } of shapePositions) {
const geometry = new THREE.BoxGeometry(1.001 * width, 1.001 * height, 1.001 * depth)
const mesh = new THREE.LineSegments(new THREE.EdgesGeometry(geometry), new THREE.LineBasicMaterial({ color: 0 }))
@ -139,7 +150,7 @@ class WorldInteraction {
update () {
const cursorBlock = bot.blockAtCursor(5)
let cursorBlockDiggable = cursorBlock
if (!bot.canDigBlock(cursorBlock) && bot.game.gameMode !== 'creative') cursorBlockDiggable = null
if (cursorBlock && !bot.canDigBlock(cursorBlock) && bot.game.gameMode !== 'creative') cursorBlockDiggable = null
let cursorChanged = !cursorBlock !== !this.cursorBlock
if (cursorBlock && this.cursorBlock) {
@ -178,9 +189,9 @@ class WorldInteraction {
&& (!this.lastButtons[0] || (cursorChanged && Date.now() - (this.lastDigged ?? 0) > 100) || onGround !== this.prevOnGround)
&& onGround
) {
this.currentDigTime = bot.digTime(cursorBlock)
this.currentDigTime = bot.digTime(cursorBlockDiggable)
this.breakStartTime = performance.now()
bot.dig(cursorBlock, 'ignore').catch((err) => {
bot.dig(cursorBlockDiggable, 'ignore').catch((err) => {
if (err.message === 'Digging aborted') return
throw err
})
@ -216,16 +227,18 @@ class WorldInteraction {
}
// Show break animation
if (this.breakStartTime && bot.game.gameMode !== 'creative') {
if (cursorBlockDiggable && this.breakStartTime && bot.game.gameMode !== 'creative') {
const elapsed = performance.now() - this.breakStartTime
const time = bot.digTime(cursorBlock)
const time = bot.digTime(cursorBlockDiggable)
if (time !== this.currentDigTime) {
console.warn('dig time changed! cancelling!', time, 'from', this.currentDigTime) // todo
try { bot.stopDigging() } catch { }
}
const state = Math.floor((elapsed / time) * 10)
//@ts-expect-error
this.blockBreakMesh.material.map = this.breakTextures[state] ?? this.breakTextures.at(-1)
if (state !== this.prevBreakState) {
//@ts-expect-error
this.blockBreakMesh.material.needsUpdate = true
}
this.prevBreakState = state

View file

@ -17,8 +17,8 @@
"skipLibCheck": true,
// this the only options that allows smooth transition from js to ts (by not dropping types from js files)
// however might need to consider includeing *only needed libraries* instead of using this
"maxNodeModuleJsDepth": 1
// "strictNullChecks": true
"maxNodeModuleJsDepth": 1,
"strictNullChecks": true
},
"include": [
"src",