From d546fa8f415f93f94dafbafc283310fafd7782aa Mon Sep 17 00:00:00 2001 From: Vitaly Date: Wed, 1 Nov 2023 03:07:07 +0300 Subject: [PATCH] enable strict null types which improves quality of codebase a lot! todo add types to worker & world renderer --- prismarine-viewer/viewer/lib/viewer.ts | 4 +- .../viewer/lib/worldDataEmitter.ts | 1 + .../viewer/prepare/genItemsAtlas.ts | 2 +- .../viewer/sign-renderer/index.ts | 2 +- src/basicSounds.ts | 4 +- src/browserfs.ts | 5 ++- src/builtinCommands.ts | 2 +- src/chat.js | 7 ++++ src/controls.ts | 16 +++++--- src/dayCycle.ts | 2 + src/downloadAndOpenFile.ts | 24 ++++++++---- src/globalState.ts | 2 +- src/index.ts | 27 +++++++------ src/loadSave.ts | 8 ++-- src/localServerMultiplayer.ts | 2 +- src/menus/hud.js | 1 - src/playerWindows.ts | 20 ++++++---- src/react/AppStatus.tsx | 2 +- src/react/AppStatusProvider.tsx | 8 ++-- src/react/Button.tsx | 2 +- src/react/ButtonWithTooltip.tsx | 1 + src/react/CreateWorld.tsx | 2 +- src/react/MainMenu.tsx | 5 ++- src/react/MainMenuRenderApp.tsx | 2 +- src/react/OptionsItems.tsx | 10 ++--- src/react/Singleplayer.tsx | 5 ++- src/react/SingleplayerProvider.tsx | 8 ++-- src/utils.ts | 25 ++++++++---- src/watchOptions.ts | 1 + src/worldInteractions.js | 39 ++++++++++++------- tsconfig.json | 4 +- 31 files changed, 150 insertions(+), 93 deletions(-) diff --git a/prismarine-viewer/viewer/lib/viewer.ts b/prismarine-viewer/viewer/lib/viewer.ts index c1faf3d9..3ff8e6ca 100644 --- a/prismarine-viewer/viewer/lib/viewer.ts +++ b/prismarine-viewer/viewer/lib/viewer.ts @@ -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 diff --git a/prismarine-viewer/viewer/lib/worldDataEmitter.ts b/prismarine-viewer/viewer/lib/worldDataEmitter.ts index 8693db95..23df1bff 100644 --- a/prismarine-viewer/viewer/lib/worldDataEmitter.ts +++ b/prismarine-viewer/viewer/lib/worldDataEmitter.ts @@ -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) diff --git a/prismarine-viewer/viewer/prepare/genItemsAtlas.ts b/prismarine-viewer/viewer/prepare/genItemsAtlas.ts index 62ddc41d..788f1b60 100644 --- a/prismarine-viewer/viewer/prepare/genItemsAtlas.ts +++ b/prismarine-viewer/viewer/prepare/genItemsAtlas.ts @@ -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) diff --git a/prismarine-viewer/viewer/sign-renderer/index.ts b/prismarine-viewer/viewer/sign-renderer/index.ts index 5eb6a516..6b45b82f 100644 --- a/prismarine-viewer/viewer/sign-renderer/index.ts +++ b/prismarine-viewer/viewer/sign-renderer/index.ts @@ -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 diff --git a/src/basicSounds.ts b/src/basicSounds.ts index cceba38c..b5a7f535 100644 --- a/src/basicSounds.ts +++ b/src/basicSounds.ts @@ -4,8 +4,8 @@ let audioContext: AudioContext const sounds: Record = {} // 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) diff --git a/src/browserfs.ts b/src/browserfs.ts index ed528a4b..1dfbd478 100644 --- a/src/browserfs.ts +++ b/src/browserfs.ts @@ -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) diff --git a/src/builtinCommands.ts b/src/builtinCommands.ts index 670bc752..02574197 100644 --- a/src/builtinCommands.ts +++ b/src/builtinCommands.ts @@ -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`) diff --git a/src/chat.js b/src/chat.js index 3aa6abec..c03bf1dd 100644 --- a/src/chat.js +++ b/src/chat.js @@ -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')) diff --git a/src/controls.ts b/src/controls.ts index 2b3d7091..c109cde2 100644 --- a/src/controls.ts +++ b/src/controls.ts @@ -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() 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) } diff --git a/src/dayCycle.ts b/src/dayCycle.ts index 80c0acee..1b95eee3 100644 --- a/src/dayCycle.ts +++ b/src/dayCycle.ts @@ -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 diff --git a/src/downloadAndOpenFile.ts b/src/downloadAndOpenFile.ts index 143af297..5efaedc1 100644 --- a/src/downloadAndOpenFile.ts +++ b/src/downloadAndOpenFile.ts @@ -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 + } +} diff --git a/src/globalState.ts b/src/globalState.ts index 605a4e19..9815ca69 100644 --- a/src/globalState.ts +++ b/src/globalState.ts @@ -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?.() } diff --git a/src/index.ts b/src/index.ts index 5f314c34..3dec30d9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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 diff --git a/src/loadSave.ts b/src/loadSave.ts index a8515ec8..874b9fd0 100644 --- a/src/loadSave.ts +++ b/src/loadSave.ts @@ -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 } + return { levelDat, dataRaw: parsed.value.Data!.value as Record } } 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) diff --git a/src/localServerMultiplayer.ts b/src/localServerMultiplayer.ts index 1717e7a0..27256a54 100644 --- a/src/localServerMultiplayer.ts +++ b/src/localServerMultiplayer.ts @@ -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 { diff --git a/src/menus/hud.js b/src/menus/hud.js index 79781abd..d152ca65 100644 --- a/src/menus/hud.js +++ b/src/menus/hud.js @@ -1,4 +1,3 @@ -//@ts-check const { LitElement, html, css, unsafeCSS } = require('lit') const { showModal, miscUiState } = require('../globalState') const { options, watchValue } = require('../optionsStorage') diff --git a/src/playerWindows.ts b/src/playerWindows.ts index c228d291..4f18a876 100644 --- a/src/playerWindows.ts +++ b/src/playerWindows.ts @@ -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() @@ -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({ diff --git a/src/react/AppStatus.tsx b/src/react/AppStatus.tsx index 9384b944..9c618205 100644 --- a/src/react/AppStatus.tsx +++ b/src/react/AppStatus.tsx @@ -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(() => { diff --git a/src/react/AppStatusProvider.tsx b/src/react/AppStatusProvider.tsx index 1fa594e8..ed2b0337 100644 --- a/src/react/AppStatusProvider.tsx +++ b/src/react/AppStatusProvider.tsx @@ -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') diff --git a/src/react/Button.tsx b/src/react/Button.tsx index a70b9395..fb9e3f04 100644 --- a/src/react/Button.tsx +++ b/src/react/Button.tsx @@ -17,7 +17,7 @@ void loadSound('button_click.mp3') export default forwardRef(({ label, icon, children, inScreen, ...args }, ref) => { const onClick = (e) => { void playSound('button_click.mp3') - args.onClick(e) + args.onClick?.(e) } if (inScreen) { args.style ??= {} diff --git a/src/react/ButtonWithTooltip.tsx b/src/react/ButtonWithTooltip.tsx index 82960589..390b074c 100644 --- a/src/react/ButtonWithTooltip.tsx +++ b/src/react/ButtonWithTooltip.tsx @@ -21,6 +21,7 @@ export default ({ initialTooltip, ...args }: Props) => { useEffect(() => { let timeout function hide () { + if (!localStorageKey) return localStorage[localStorageKey] = 'false' setShowTooltips(false) } diff --git a/src/react/CreateWorld.tsx b/src/react/CreateWorld.tsx index 011047cf..adde4ff6 100644 --- a/src/react/CreateWorld.tsx +++ b/src/react/CreateWorld.tsx @@ -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) : '?'}`) }) }, []) diff --git a/src/react/MainMenu.tsx b/src/react/MainMenu.tsx index b630dbd1..2c994bcf 100644 --- a/src/react/MainMenu.tsx +++ b/src/react/MainMenu.tsx @@ -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', }} /> diff --git a/src/react/MainMenuRenderApp.tsx b/src/react/MainMenuRenderApp.tsx index a58c4be2..9e06bf66 100644 --- a/src/react/MainMenuRenderApp.tsx +++ b/src/react/MainMenuRenderApp.tsx @@ -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 => { diff --git a/src/react/OptionsItems.tsx b/src/react/OptionsItems.tsx index 332b6d36..92f1de5e 100644 --- a/src/react/OptionsItems.tsx +++ b/src/react/OptionsItems.tsx @@ -30,12 +30,12 @@ export type OptionMeta = GeneralItem & ({ }) export const OptionButton = ({ item }: { item: Extract }) => { - const optionValue = useSnapshot(options)[item.id] + const optionValue = useSnapshot(options)[item.id!] return