From f8ae3e17d721b778c777aa52bb1544204668dc74 Mon Sep 17 00:00:00 2001 From: Vitaly Date: Tue, 26 Sep 2023 08:26:20 +0300 Subject: [PATCH] enforce style that was used in repo! indent back to 2 --- .eslintrc.json | 21 ++ cypress/integration/index.spec.ts | 94 ++++---- src/browserfs.ts | 16 +- src/builtinCommands.ts | 6 +- src/controls.ts | 68 +++--- src/dragndrop.ts | 8 +- src/inventory.ts | 6 +- src/loadSave.ts | 2 +- src/localServerMultiplayer.ts | 232 +++++++++--------- src/reactUi.jsx | 142 +++++------ src/texturePack.ts | 386 +++++++++++++++--------------- src/utils.ts | 12 +- 12 files changed, 507 insertions(+), 486 deletions(-) diff --git a/.eslintrc.json b/.eslintrc.json index d4a10b20d..3e0781152 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,6 +1,27 @@ { "extends": "zardoy", "rules": { + "semi": [ + "error", + "never" + ], + "indent": [ + "error", + 2, + { + "SwitchCase": 2, + "ignoredNodes": [ + "TemplateLiteral" + ] + } + ], + "quotes": [ + "error", + "single", + { + "allowTemplateLiterals": true + } + ], // perf "import/no-deprecated": "off", // --- diff --git a/cypress/integration/index.spec.ts b/cypress/integration/index.spec.ts index 3c184b46d..d8744a4e9 100644 --- a/cypress/integration/index.spec.ts +++ b/cypress/integration/index.spec.ts @@ -2,78 +2,78 @@ import type { AppOptions } from '../../src/optionsStorage' const cleanVisit = () => { - window.localStorage.clear() - visit() + window.localStorage.clear() + visit() } const visit = (url = '/') => { - window.localStorage.cypress = 'true' - cy.visit(url) + window.localStorage.cypress = 'true' + cy.visit(url) } // todo use ssl const compareRenderedFlatWorld = () => { - // wait for render - // cy.wait(6000) - // cy.get('body').toMatchImageSnapshot({ - // name: 'superflat-world', - // }) + // wait for render + // cy.wait(6000) + // cy.get('body').toMatchImageSnapshot({ + // name: 'superflat-world', + // }) } const testWorldLoad = () => { - cy.document().then({ timeout: 20_000, }, doc => { - return new Cypress.Promise(resolve => { - doc.addEventListener('cypress-world-ready', resolve) - }) - }).then(() => { - compareRenderedFlatWorld() + cy.document().then({ timeout: 20_000, }, doc => { + return new Cypress.Promise(resolve => { + doc.addEventListener('cypress-world-ready', resolve) }) + }).then(() => { + compareRenderedFlatWorld() + }) } const setOptions = (options: Partial) => { - cy.window().then(win => { - Object.assign(win['options'], options) - }) + cy.window().then(win => { + Object.assign(win['options'], options) + }) } it('Loads & renders singleplayer', () => { - cleanVisit() - setOptions({ - localServerOptions: { - generation: { - name: 'superflat', - // eslint-disable-next-line unicorn/numeric-separators-style - options: { seed: 250869072 } - }, - }, - renderDistance: 2 - }) - cy.get('#title-screen').find('[data-test-id="singleplayer-button"]', { includeShadowDom: true, }).click() - testWorldLoad() + cleanVisit() + setOptions({ + localServerOptions: { + generation: { + name: 'superflat', + // eslint-disable-next-line unicorn/numeric-separators-style + options: { seed: 250869072 } + }, + }, + renderDistance: 2 + }) + cy.get('#title-screen').find('[data-test-id="singleplayer-button"]', { includeShadowDom: true, }).click() + testWorldLoad() }) it('Joins to server', () => { - // visit('/?version=1.16.1') - window.localStorage.version = '1.16.1' - visit() - // todo replace with data-test - cy.get('#title-screen').find('[data-test-id="connect-screen-button"]', { includeShadowDom: true, }).click() - cy.get('input#serverip', { includeShadowDom: true, }).clear().focus().type('localhost') - cy.get('[data-test-id="connect-to-server"]', { includeShadowDom: true, }).click() - testWorldLoad() + // visit('/?version=1.16.1') + window.localStorage.version = '1.16.1' + visit() + // todo replace with data-test + cy.get('#title-screen').find('[data-test-id="connect-screen-button"]', { includeShadowDom: true, }).click() + cy.get('input#serverip', { includeShadowDom: true, }).clear().focus().type('localhost') + cy.get('[data-test-id="connect-to-server"]', { includeShadowDom: true, }).click() + testWorldLoad() }) it('Loads & renders zip world', () => { - cleanVisit() - cy.get('#title-screen').find('[data-test-id="select-file-folder"]', { includeShadowDom: true, }).click({ shiftKey: true }) - cy.get('input[type="file"]').selectFile('cypress/superflat.zip', { force: true }) - testWorldLoad() + cleanVisit() + cy.get('#title-screen').find('[data-test-id="select-file-folder"]', { includeShadowDom: true, }).click({ shiftKey: true }) + cy.get('input[type="file"]').selectFile('cypress/superflat.zip', { force: true }) + testWorldLoad() }) it.skip('Performance test', () => { - // select that world - // from -2 85 24 - // await bot.loadPlugin(pathfinder.pathfinder) - // bot.pathfinder.goto(new pathfinder.goals.GoalXZ(28, -28)) + // select that world + // from -2 85 24 + // await bot.loadPlugin(pathfinder.pathfinder) + // bot.pathfinder.goto(new pathfinder.goals.GoalXZ(28, -28)) }) diff --git a/src/browserfs.ts b/src/browserfs.ts index bac4d5dc5..26316b58f 100644 --- a/src/browserfs.ts +++ b/src/browserfs.ts @@ -13,7 +13,7 @@ import { installTexturePack, updateTexturePackInstalledState } from './texturePa browserfs.install(window) // todo migrate to StorageManager API for localsave as localstorage has only 5mb limit, when localstorage is fallback test limit warning on 4mb const deafultMountablePoints = { - "/world": { fs: "LocalStorage" }, + '/world': { fs: 'LocalStorage' }, '/userData': { fs: 'IndexedDB' }, } browserfs.configure({ @@ -154,8 +154,8 @@ export const openWorldDirectory = async (dragndropHandle?: FileSystemDirectoryHa // todo fs: 'MountableFileSystem', options: { - "/world": { - fs: "FileSystemAccess", + '/world': { + fs: 'FileSystemAccess', options: { handle: directoryHandle } @@ -215,8 +215,8 @@ const openWorldZipInner = async (file: File | ArrayBuffer, name = file['name']) fs: 'MountableFileSystem', options: { ...deafultMountablePoints, - "/world": { - fs: "ZipFS", + '/world': { + fs: 'ZipFS', options: { zipData: Buffer.from(file instanceof File ? (await file.arrayBuffer()) : file), name @@ -276,12 +276,12 @@ export async function generateAndDownloadWorldZip() { zip.folder('world') // Generate the ZIP archive content - const zipContent = await zip.generateAsync({ type: "blob" }) + const zipContent = await zip.generateAsync({ type: 'blob' }) // Create a download link and trigger the download - const downloadLink = document.createElement("a") + const downloadLink = document.createElement('a') downloadLink.href = URL.createObjectURL(zipContent) - downloadLink.download = "prismarine-world.zip" + downloadLink.download = 'prismarine-world.zip' downloadLink.click() // Clean up the URL object after download diff --git a/src/builtinCommands.ts b/src/builtinCommands.ts index 197ebb004..34abe8eea 100644 --- a/src/builtinCommands.ts +++ b/src/builtinCommands.ts @@ -37,12 +37,12 @@ const exportWorld = async () => { await addFolderToZip(worldFolder, zip, '') // Generate the ZIP archive content - const zipContent = await zip.generateAsync({ type: "blob" }) + const zipContent = await zip.generateAsync({ type: 'blob' }) // Create a download link and trigger the download - const downloadLink = document.createElement("a") + const downloadLink = document.createElement('a') downloadLink.href = URL.createObjectURL(zipContent) - downloadLink.download = "world-exported.zip" + downloadLink.download = 'world-exported.zip' downloadLink.click() // Clean up the URL object after download diff --git a/src/controls.ts b/src/controls.ts index 1f573eea3..3ca9a329b 100644 --- a/src/controls.ts +++ b/src/controls.ts @@ -139,22 +139,22 @@ const onTriggerOrReleased = (command: Command, pressed: boolean) => { // handle general commands // eslint-disable-next-line @typescript-eslint/switch-exhaustiveness-check switch (command) { - case 'general.jump': - bot.setControlState('jump', pressed) - break - case 'general.sneak': - gameAdditionalState.isSneaking = pressed - bot.setControlState('sneak', pressed) - break - case 'general.sprint': + case 'general.jump': + bot.setControlState('jump', pressed) + break + case 'general.sneak': + gameAdditionalState.isSneaking = pressed + bot.setControlState('sneak', pressed) + break + case 'general.sprint': // todo add setting to change behavior - if (pressed) { - setSprinting(pressed) - } - break - case 'general.attackDestroy': - document.dispatchEvent(new MouseEvent(pressed ? 'mousedown' : 'mouseup', { button: 0 })) - break + if (pressed) { + setSprinting(pressed) + } + break + case 'general.attackDestroy': + document.dispatchEvent(new MouseEvent(pressed ? 'mousedown' : 'mouseup', { button: 0 })) + break } } } @@ -192,26 +192,26 @@ contro.on('trigger', ({ command }) => { if (stringStartsWith(command, 'general')) { // eslint-disable-next-line @typescript-eslint/switch-exhaustiveness-check switch (command) { - case 'general.inventory': - document.exitPointerLock?.() - showModal({ reactType: 'inventory' }) - break - case 'general.drop': - if (bot.heldItem) bot.tossStack(bot.heldItem) - break - case 'general.chat': - document.getElementById('hud').shadowRoot.getElementById('chat').enableChat() - break - case 'general.command': - document.getElementById('hud').shadowRoot.getElementById('chat').enableChat('/') - break - case 'general.interactPlace': - document.dispatchEvent(new MouseEvent('mousedown', { button: 2 })) - setTimeout(() => { + case 'general.inventory': + document.exitPointerLock?.() + showModal({ reactType: 'inventory' }) + break + case 'general.drop': + if (bot.heldItem) bot.tossStack(bot.heldItem) + break + case 'general.chat': + document.getElementById('hud').shadowRoot.getElementById('chat').enableChat() + break + case 'general.command': + document.getElementById('hud').shadowRoot.getElementById('chat').enableChat('/') + break + case 'general.interactPlace': + document.dispatchEvent(new MouseEvent('mousedown', { button: 2 })) + setTimeout(() => { // todo cleanup - document.dispatchEvent(new MouseEvent('mouseup', { button: 2 })) - }) - break + document.dispatchEvent(new MouseEvent('mouseup', { button: 2 })) + }) + break } } }) diff --git a/src/dragndrop.ts b/src/dragndrop.ts index 5a1145531..46a20788c 100644 --- a/src/dragndrop.ts +++ b/src/dragndrop.ts @@ -5,19 +5,19 @@ import { openWorldDirectory, openWorldZip } from './browserfs' import { isGameActive } from './globalState' const parseNbt = promisify(nbt.parse) -window.nbt = nbt; +window.nbt = nbt // todo display drop zone -for (const event of ["drag", "dragstart", "dragend", "dragover", "dragenter", "dragleave", "drop"]) { +for (const event of ['drag', 'dragstart', 'dragend', 'dragover', 'dragenter', 'dragleave', 'drop']) { window.addEventListener(event, (e: any) => { - if (e.dataTransfer && !e.dataTransfer.types.includes("Files")) { + if (e.dataTransfer && !e.dataTransfer.types.includes('Files')) { // e.dataTransfer.effectAllowed = "none" return } e.preventDefault() }) } -window.addEventListener("drop", async e => { +window.addEventListener('drop', async e => { if (!e.dataTransfer?.files.length) return const { items } = e.dataTransfer const item = items[0] diff --git a/src/inventory.ts b/src/inventory.ts index e4dd2d859..c5212215c 100644 --- a/src/inventory.ts +++ b/src/inventory.ts @@ -88,9 +88,9 @@ const getItemSlice = (name) => { const getImageSrc = (path) => { switch (path) { - case 'gui/container/inventory': return InventoryGui - case 'blocks': return globalThis.texturePackDataUrl || `textures/${version}.png` - case 'invsprite': return `invsprite.png` + case 'gui/container/inventory': return InventoryGui + case 'blocks': return globalThis.texturePackDataUrl || `textures/${version}.png` + case 'invsprite': return `invsprite.png` } return Dirt } diff --git a/src/loadSave.ts b/src/loadSave.ts index 22aff77d0..9167197ab 100644 --- a/src/loadSave.ts +++ b/src/loadSave.ts @@ -121,7 +121,7 @@ export const loadSave = async (root = '/world') => { if (!fsState.isReadonly) { // todo allow also to ctrl+s - alert("Note: the world is saved only on /save or disconnect! ENSURE YOU HAVE BACKUP!") + alert('Note: the world is saved only on /save or disconnect! ENSURE YOU HAVE BACKUP!') } fsState.saveLoaded = true diff --git a/src/localServerMultiplayer.ts b/src/localServerMultiplayer.ts index 71efd25bd..3096e9321 100644 --- a/src/localServerMultiplayer.ts +++ b/src/localServerMultiplayer.ts @@ -5,146 +5,146 @@ import { resolveTimeout, setLoadingScreenStatus } from './utils' import { miscUiState } from './globalState' class CustomDuplex extends Duplex { - constructor(options, public writeAction) { - super(options) - } + constructor(options, public writeAction) { + super(options) + } - _read() { } + _read() { } - _write(chunk, encoding, callback) { - this.writeAction(chunk) - callback() - } + _write(chunk, encoding, callback) { + this.writeAction(chunk) + callback() + } } let peerInstance: Peer | undefined export const getJoinLink = () => { - if (!peerInstance) return - const url = new URL(window.location.href) - url.searchParams.set('connectPeer', peerInstance.id) - url.searchParams.set('peerVersion', localServer.options.version) - return url.toString() + if (!peerInstance) return + const url = new URL(window.location.href) + url.searchParams.set('connectPeer', peerInstance.id) + url.searchParams.set('peerVersion', localServer.options.version) + return url.toString() } const copyJoinLink = async () => { - miscUiState.wanOpened = true - const joinLink = getJoinLink() - if (navigator.clipboard) { - await navigator.clipboard.writeText(joinLink) - } else { - window.prompt('Copy to clipboard: Ctrl+C, Enter', joinLink) - } + miscUiState.wanOpened = true + const joinLink = getJoinLink() + if (navigator.clipboard) { + await navigator.clipboard.writeText(joinLink) + } else { + window.prompt('Copy to clipboard: Ctrl+C, Enter', joinLink) + } } export const openToWanAndCopyJoinLink = async (writeText: (text) => void, doCopy = true) => { - if (!localServer) return - if (peerInstance) { - if (doCopy) await copyJoinLink() - return 'Already opened to wan. Join link copied' - } - const peer = new Peer({ - debug: 3, - }) - peerInstance = peer - peer.on('connection', (connection) => { - console.log('connection') - const serverDuplex = new CustomDuplex({}, (data) => connection.send(data)) - const client = new Client(true, localServer.options.version, undefined) - client.setSocket(serverDuplex) - localServer._server.emit('connection', client) - - connection.on('data', (data: any) => { - serverDuplex.push(Buffer.from(data)) - }) - // our side disconnect - const endConnection = () => { - console.log('connection.close') - serverDuplex.end() - connection.close() - } - serverDuplex.on('end', endConnection) - serverDuplex.on('force-close', endConnection) - client.on('end', endConnection) + if (!localServer) return + if (peerInstance) { + if (doCopy) await copyJoinLink() + return 'Already opened to wan. Join link copied' + } + const peer = new Peer({ + debug: 3, + }) + peerInstance = peer + peer.on('connection', (connection) => { + console.log('connection') + const serverDuplex = new CustomDuplex({}, (data) => connection.send(data)) + const client = new Client(true, localServer.options.version, undefined) + client.setSocket(serverDuplex) + localServer._server.emit('connection', client) - const disconnected = () => { - serverDuplex.end() - client.end() - } - connection.on('iceStateChanged', (state) => { - console.log('iceStateChanged', state) - if (state === 'disconnected') { - disconnected() - } - }) - connection.on('close', disconnected) - connection.on('error', disconnected) + connection.on('data', (data: any) => { + serverDuplex.push(Buffer.from(data)) }) - peer.on('error', (error) => { - console.error(error) - writeText(error.message) + // our side disconnect + const endConnection = () => { + console.log('connection.close') + serverDuplex.end() + connection.close() + } + serverDuplex.on('end', endConnection) + serverDuplex.on('force-close', endConnection) + client.on('end', endConnection) + + const disconnected = () => { + serverDuplex.end() + client.end() + } + connection.on('iceStateChanged', (state) => { + console.log('iceStateChanged', state) + if (state === 'disconnected') { + disconnected() + } }) - return new Promise(resolve => { - peer.on('open', async () => { - await copyJoinLink() - resolve('Copied join link to clipboard') - }) - setTimeout(() => { - resolve('Failed to open to wan (timeout)') - }, 5000) + connection.on('close', disconnected) + connection.on('error', disconnected) + }) + peer.on('error', (error) => { + console.error(error) + writeText(error.message) + }) + return new Promise(resolve => { + peer.on('open', async () => { + await copyJoinLink() + resolve('Copied join link to clipboard') }) + setTimeout(() => { + resolve('Failed to open to wan (timeout)') + }, 5000) + }) } export const closeWan = () => { - if (!peerInstance) return - peerInstance.destroy() - peerInstance = undefined - miscUiState.wanOpened = false - return 'Closed to wan' + if (!peerInstance) return + peerInstance.destroy() + peerInstance = undefined + miscUiState.wanOpened = false + return 'Closed to wan' } export const connectToPeer = async (peerId: string) => { - setLoadingScreenStatus('Connecting to peer server') - // todo destroy connection on error - const peer = new Peer({ - debug: 3, + setLoadingScreenStatus('Connecting to peer server') + // todo destroy connection on error + const peer = new Peer({ + debug: 3, + }) + await resolveTimeout(new Promise(resolve => { + peer.once('open', resolve) + })) + setLoadingScreenStatus('Connecting to the peer') + const connection = peer.connect(peerId, { + serialization: 'raw', + }) + await resolveTimeout(new Promise((resolve, reject) => { + connection.once('error', (error) => { + console.log(error.type, error.name) + console.log(error) + reject(error.message) }) - await resolveTimeout(new Promise(resolve => { - peer.once('open', resolve) - })) - setLoadingScreenStatus('Connecting to the peer') - const connection = peer.connect(peerId, { - serialization: 'raw', - }) - await resolveTimeout(new Promise((resolve, reject) => { - connection.once('error', (error) => { - console.log(error.type, error.name) - console.log(error) - reject(error.message); - }) - connection.once('open', resolve) - })) + connection.once('open', resolve) + })) - const clientDuplex = new CustomDuplex({}, (data) => { - // todo rm debug - console.debug('sending', data.toString()) - connection.send(data) - }) - connection.on('data', (data: any) => { - console.debug('received', Buffer.from(data).toString()) - clientDuplex.push(Buffer.from(data)) - }) - connection.on('close', () => { - console.log('connection closed') - clientDuplex.end() - // bot._client.end() - // bot.end() - bot.emit('end', 'Disconnected.') - }) - connection.on('error', (error) => { - console.error(error) - clientDuplex.end() - }) + const clientDuplex = new CustomDuplex({}, (data) => { + // todo rm debug + console.debug('sending', data.toString()) + connection.send(data) + }) + connection.on('data', (data: any) => { + console.debug('received', Buffer.from(data).toString()) + clientDuplex.push(Buffer.from(data)) + }) + connection.on('close', () => { + console.log('connection closed') + clientDuplex.end() + // bot._client.end() + // bot.end() + bot.emit('end', 'Disconnected.') + }) + connection.on('error', (error) => { + console.error(error) + clientDuplex.end() + }) - return clientDuplex + return clientDuplex } diff --git a/src/reactUi.jsx b/src/reactUi.jsx index 136fcb8b4..64713aa67 100644 --- a/src/reactUi.jsx +++ b/src/reactUi.jsx @@ -13,49 +13,49 @@ import { options, watchValue } from './optionsStorage' // todo useInterfaceState.setState({ - isFlying: false, - uiCustomization: { - touchButtonSize: 40, - }, - updateCoord([coord, state]) { - const coordToAction = [ - ['z', -1, 'KeyW'], - ['z', 1, 'KeyS'], - ['x', -1, 'KeyA'], - ['x', 1, 'KeyD'], - ['y', 1, 'Space'], // todo jump - ['y', -1, 'ShiftLeft'], // todo jump - ] - // todo refactor - const actionAndState = state === 0 ? coordToAction.filter(([axis]) => axis === coord) : coordToAction.find(([axis, value]) => axis === coord && value === state) - if (!bot) return - if (state === 0) { - for (const action of actionAndState) { - contro.pressedKeyOrButtonChanged({code: action[2],}, false) - } - } else { - //@ts-expect-error - contro.pressedKeyOrButtonChanged({code: actionAndState[2],}, true) - } + isFlying: false, + uiCustomization: { + touchButtonSize: 40, + }, + updateCoord([coord, state]) { + const coordToAction = [ + ['z', -1, 'KeyW'], + ['z', 1, 'KeyS'], + ['x', -1, 'KeyA'], + ['x', 1, 'KeyD'], + ['y', 1, 'Space'], // todo jump + ['y', -1, 'ShiftLeft'], // todo jump + ] + // todo refactor + const actionAndState = state === 0 ? coordToAction.filter(([axis]) => axis === coord) : coordToAction.find(([axis, value]) => axis === coord && value === state) + if (!bot) return + if (state === 0) { + for (const action of actionAndState) { + contro.pressedKeyOrButtonChanged({code: action[2],}, false) + } + } else { + //@ts-expect-error + contro.pressedKeyOrButtonChanged({code: actionAndState[2],}, true) } + } }) watchValue(options, (o) => { - useInterfaceState.setState({ - uiCustomization: { - touchButtonSize: o.touchButtonsSize, - }, - }) + useInterfaceState.setState({ + uiCustomization: { + touchButtonSize: o.touchButtonsSize, + }, + }) }) const TouchControls = () => { - // todo setting - const usingTouch = useUsingTouch() + // todo setting + const usingTouch = useUsingTouch() - if (!usingTouch) return null - return ( -
{ pointer-events: auto; } `} - > - -
- -
- ) + > + +
+ +
+ ) } function useIsBotAvailable() { - const stack = useSnapshot(activeModalStack) + const stack = useSnapshot(activeModalStack) - return isGameActive(false) + return isGameActive(false) } const DisplayQr = () => { - const { currentDisplayQr } = useSnapshot(miscUiState) + const { currentDisplayQr } = useSnapshot(miscUiState) - if (!currentDisplayQr) return null + if (!currentDisplayQr) return null - return createPortal(
{ - miscUiState.currentDisplayQr = null - }} - > - -
, document.body) + return createPortal(
{ + miscUiState.currentDisplayQr = null + }} + > + +
, document.body) } const App = () => { - const isBotAvailable = useIsBotAvailable() - if (!isBotAvailable) return null + const isBotAvailable = useIsBotAvailable() + if (!isBotAvailable) return null - return
- - -
+ return
+ + +
} renderToDom(, { - strictMode: false, - selector: '#react-root', + strictMode: false, + selector: '#react-root', }) diff --git a/src/texturePack.ts b/src/texturePack.ts index df58ceead..83a783186 100644 --- a/src/texturePack.ts +++ b/src/texturePack.ts @@ -11,262 +11,262 @@ import { removeFileRecursiveAsync } from './browserfs' import { setLoadingScreenStatus } from './utils' export const resourcePackState = proxy({ - resourcePackInstalled: false, - currentTexturesDataUrl: undefined as string | undefined, - currentTexturesBlockStates: undefined as BlockStates | undefined, + resourcePackInstalled: false, + currentTexturesDataUrl: undefined as string | undefined, + currentTexturesBlockStates: undefined as BlockStates | undefined, }) function nextPowerOfTwo(n) { - if (n === 0) return 1 - n-- - n |= n >> 1 - n |= n >> 2 - n |= n >> 4 - n |= n >> 8 - n |= n >> 16 - return n + 1 + if (n === 0) return 1 + n-- + n |= n >> 1 + n |= n >> 2 + n |= n >> 4 + n |= n >> 8 + n |= n >> 16 + return n + 1 } const mkdirRecursive = async (path) => { - const parts = path.split('/') - let current = '' - for (const part of parts) { - current += part + '/' - try { - await fs.promises.mkdir(current) - } catch (err) { - } + const parts = path.split('/') + let current = '' + for (const part of parts) { + current += part + '/' + try { + await fs.promises.mkdir(current) + } catch (err) { } + } } const texturePackBasePath = '/userData/resourcePacks/default' export const uninstallTexturePack = async () => { - await removeFileRecursiveAsync(texturePackBasePath) - setCustomTexturePackData(undefined, undefined) + await removeFileRecursiveAsync(texturePackBasePath) + setCustomTexturePackData(undefined, undefined) } export const getResourcePackName = async () => { - // temp - try { - return await fs.promises.readFile(join(texturePackBasePath, 'name.txt'), 'utf8') - } catch (err) { - return '???' - } + // temp + try { + return await fs.promises.readFile(join(texturePackBasePath, 'name.txt'), 'utf8') + } catch (err) { + return '???' + } } export const fromTexturePackPath = (path) => { - return join(texturePackBasePath, path) + return join(texturePackBasePath, path) } export const updateTexturePackInstalledState = async () => { - try { - resourcePackState.resourcePackInstalled = await existsAsync(texturePackBasePath) - } catch { - } + try { + resourcePackState.resourcePackInstalled = await existsAsync(texturePackBasePath) + } catch { + } } export const installTexturePack = async (file: File | ArrayBuffer) => { - try { - await uninstallTexturePack() - } catch (err) { - } - const status = 'Installing resource pack: copying all files' - setLoadingScreenStatus(status) - // extract the zip and write to fs every file in it - const zip = new JSZip() - const zipFile = await zip.loadAsync(file) - if (!zipFile.file('pack.mcmeta')) throw new Error('Not a resource pack: missing pack.mcmeta') - await mkdirRecursive(texturePackBasePath) + try { + await uninstallTexturePack() + } catch (err) { + } + const status = 'Installing resource pack: copying all files' + setLoadingScreenStatus(status) + // extract the zip and write to fs every file in it + const zip = new JSZip() + const zipFile = await zip.loadAsync(file) + if (!zipFile.file('pack.mcmeta')) throw new Error('Not a resource pack: missing pack.mcmeta') + await mkdirRecursive(texturePackBasePath) - const allFilesArr = Object.entries(zipFile.files) - let done = 0 - const upStatus = () => { - setLoadingScreenStatus(`${status} ${Math.round(++done / allFilesArr.length * 100)}%`) - } - await Promise.all(allFilesArr.map(async ([path, file]) => { - const writePath = join(texturePackBasePath, path) - if (path.endsWith('/')) return - await mkdirRecursive(dirname(writePath)) - await fs.promises.writeFile(writePath, Buffer.from(await file.async('arraybuffer'))) - done++ - upStatus() - })) - await fs.promises.writeFile(join(texturePackBasePath, 'name.txt'), file['name'] ?? '??', 'utf8') + const allFilesArr = Object.entries(zipFile.files) + let done = 0 + const upStatus = () => { + setLoadingScreenStatus(`${status} ${Math.round(++done / allFilesArr.length * 100)}%`) + } + await Promise.all(allFilesArr.map(async ([path, file]) => { + const writePath = join(texturePackBasePath, path) + if (path.endsWith('/')) return + await mkdirRecursive(dirname(writePath)) + await fs.promises.writeFile(writePath, Buffer.from(await file.async('arraybuffer'))) + done++ + upStatus() + })) + await fs.promises.writeFile(join(texturePackBasePath, 'name.txt'), file['name'] ?? '??', 'utf8') - if (viewer?.world.active) { - await genTexturePackTextures(viewer.version) - } - setLoadingScreenStatus(undefined) - showNotification({ - message: 'Texturepack installed!', - }) + if (viewer?.world.active) { + await genTexturePackTextures(viewer.version) + } + setLoadingScreenStatus(undefined) + showNotification({ + message: 'Texturepack installed!', + }) } const existsAsync = async (path) => { - try { - await fs.promises.stat(path) - return true - } catch (err) { - return false - } + try { + await fs.promises.stat(path) + return true + } catch (err) { + return false + } } type TextureResolvedData = { - blockSize: number - // itemsUrlContent: string + blockSize: number + // itemsUrlContent: string } const arrEqual = (a: any[], b: any[]) => a.length === b.length && a.every((x) => b.includes(x)) const applyTexturePackData = async (version: string, { blockSize }: TextureResolvedData, blocksUrlContent: string) => { - const result = await fetch(`blocksStates/${version}.json`) - const blockStates: BlockStates = await result.json() - const factor = blockSize / 16 + const result = await fetch(`blocksStates/${version}.json`) + const blockStates: BlockStates = await result.json() + const factor = blockSize / 16 - // this will be refactored with generateTextures refactor - const processObj = (x) => { - if (typeof x !== 'object' || !x) return - if (Array.isArray(x)) { - for (const v of x) { - processObj(v) - } + // this will be refactored with generateTextures refactor + const processObj = (x) => { + if (typeof x !== 'object' || !x) return + if (Array.isArray(x)) { + for (const v of x) { + processObj(v) + } - } else { - const actual = Object.keys(x) - const needed = ['u', 'v', 'su', 'sv'] + } else { + const actual = Object.keys(x) + const needed = ['u', 'v', 'su', 'sv'] - if (!arrEqual(actual, needed)) { - for (const v of Object.values(x)) { - processObj(v) - } - return - } - for (const k of needed) { - x[k] *= factor - } + if (!arrEqual(actual, needed)) { + for (const v of Object.values(x)) { + processObj(v) } + return + } + for (const k of needed) { + x[k] *= factor + } } - processObj(blockStates) - setCustomTexturePackData(blocksUrlContent, blockStates) + } + processObj(blockStates) + setCustomTexturePackData(blocksUrlContent, blockStates) } const setCustomTexturePackData = (blockTextures, blockStates) => { - resourcePackState.currentTexturesBlockStates = blockStates && ref(blockStates) - resourcePackState.currentTexturesDataUrl = blockTextures - resourcePackState.resourcePackInstalled = blockTextures !== undefined + resourcePackState.currentTexturesBlockStates = blockStates && ref(blockStates) + resourcePackState.currentTexturesDataUrl = blockTextures + resourcePackState.resourcePackInstalled = blockTextures !== undefined } const getSizeFromImage = async (filePath: string) => { - const probeImg = new Image() - const file = await fs.promises.readFile(filePath, 'base64') - probeImg.src = `data:image/png;base64,${file}` - await new Promise((resolve, reject) => { - probeImg.addEventListener('load', resolve) - }) - if (probeImg.width !== probeImg.height) throw new Error(`Probe texture ${filePath} is not square`) - return probeImg.width + const probeImg = new Image() + const file = await fs.promises.readFile(filePath, 'base64') + probeImg.src = `data:image/png;base64,${file}` + await new Promise((resolve, reject) => { + probeImg.addEventListener('load', resolve) + }) + if (probeImg.width !== probeImg.height) throw new Error(`Probe texture ${filePath} is not square`) + return probeImg.width } export const genTexturePackTextures = async (version: string) => { - setCustomTexturePackData(undefined, undefined) - let blocksBasePath = '/userData/resourcePacks/default/assets/minecraft/textures/block' - // todo not clear why this is needed - const blocksBasePathAlt = '/userData/resourcePacks/default/assets/minecraft/textures/blocks' - const blocksGenereatedPath = `/userData/resourcePacks/default/${version}.png` - const genereatedPathData = `/userData/resourcePacks/default/${version}.json` - if (!(await existsAsync(blocksBasePath))) { - if (await existsAsync(blocksBasePathAlt)) { - blocksBasePath = blocksBasePathAlt - } else { - return - } - } - if (await existsAsync(blocksGenereatedPath)) { - applyTexturePackData(version, JSON.parse(await fs.promises.readFile(genereatedPathData, 'utf8')), await fs.promises.readFile(blocksGenereatedPath, 'utf8')) - return + setCustomTexturePackData(undefined, undefined) + let blocksBasePath = '/userData/resourcePacks/default/assets/minecraft/textures/block' + // todo not clear why this is needed + const blocksBasePathAlt = '/userData/resourcePacks/default/assets/minecraft/textures/blocks' + const blocksGenereatedPath = `/userData/resourcePacks/default/${version}.png` + const genereatedPathData = `/userData/resourcePacks/default/${version}.json` + if (!(await existsAsync(blocksBasePath))) { + if (await existsAsync(blocksBasePathAlt)) { + blocksBasePath = blocksBasePathAlt + } else { + return } + } + if (await existsAsync(blocksGenereatedPath)) { + applyTexturePackData(version, JSON.parse(await fs.promises.readFile(genereatedPathData, 'utf8')), await fs.promises.readFile(blocksGenereatedPath, 'utf8')) + return + } - setLoadingScreenStatus('Generating custom textures') + setLoadingScreenStatus('Generating custom textures') - const textureFiles = blocksFileNames.indexes[version].map(k => blocksFileNames.blockNames[k]) - textureFiles.unshift('missing_texture.png') + const textureFiles = blocksFileNames.indexes[version].map(k => blocksFileNames.blockNames[k]) + textureFiles.unshift('missing_texture.png') - const texSize = nextPowerOfTwo(Math.ceil(Math.sqrt(textureFiles.length))) - const originalTileSize = 16 + const texSize = nextPowerOfTwo(Math.ceil(Math.sqrt(textureFiles.length))) + const originalTileSize = 16 - const firstBlockFile = (await fs.promises.readdir(blocksBasePath)).find(f => f.endsWith('.png')) - if (!firstBlockFile) { - return - } + const firstBlockFile = (await fs.promises.readdir(blocksBasePath)).find(f => f.endsWith('.png')) + if (!firstBlockFile) { + return + } - // we get the size of image from the first block file, which is not ideal but works in 99% cases - const tileSize = await getSizeFromImage(join(blocksBasePath, firstBlockFile)) + // we get the size of image from the first block file, which is not ideal but works in 99% cases + const tileSize = await getSizeFromImage(join(blocksBasePath, firstBlockFile)) - const imgSize = texSize * tileSize - - const canvas = document.createElement('canvas') - canvas.width = imgSize - canvas.height = imgSize - const src = `textures/${version}.png` - const ctx = canvas.getContext('2d') - ctx.imageSmoothingEnabled = false - const img = new Image() - img.src = src - await new Promise((resolve, reject) => { - img.onerror = reject - img.addEventListener('load', resolve) - }) - for (const [i, fileName] of textureFiles.entries()) { - const x = (i % texSize) * tileSize - const y = Math.floor(i / texSize) * tileSize - const xOrig = (i % texSize) * originalTileSize - const yOrig = Math.floor(i / texSize) * originalTileSize - let imgCustom: HTMLImageElement - try { - const fileBase64 = await fs.promises.readFile(join(blocksBasePath, fileName), 'base64') - const _imgCustom = new Image() - await new Promise(resolve => { - _imgCustom.addEventListener('load', () => { - imgCustom = _imgCustom - resolve() - }) - _imgCustom.onerror = () => { - console.log('Skipping issued texture', fileName) - resolve() - } - _imgCustom.src = `data:image/png;base64,${fileBase64}` - }) - } catch { - console.log('Skipping not found texture', fileName) - } + const imgSize = texSize * tileSize - if (imgCustom) { - ctx.drawImage(imgCustom, x, y, tileSize, tileSize) - } else { - // todo this involves incorrect mappings for existing textures when the size is different - ctx.drawImage(img, xOrig, yOrig, originalTileSize, originalTileSize, x, y, tileSize, tileSize) + const canvas = document.createElement('canvas') + canvas.width = imgSize + canvas.height = imgSize + const src = `textures/${version}.png` + const ctx = canvas.getContext('2d') + ctx.imageSmoothingEnabled = false + const img = new Image() + img.src = src + await new Promise((resolve, reject) => { + img.onerror = reject + img.addEventListener('load', resolve) + }) + for (const [i, fileName] of textureFiles.entries()) { + const x = (i % texSize) * tileSize + const y = Math.floor(i / texSize) * tileSize + const xOrig = (i % texSize) * originalTileSize + const yOrig = Math.floor(i / texSize) * originalTileSize + let imgCustom: HTMLImageElement + try { + const fileBase64 = await fs.promises.readFile(join(blocksBasePath, fileName), 'base64') + const _imgCustom = new Image() + await new Promise(resolve => { + _imgCustom.addEventListener('load', () => { + imgCustom = _imgCustom + resolve() + }) + _imgCustom.onerror = () => { + console.log('Skipping issued texture', fileName) + resolve() } + _imgCustom.src = `data:image/png;base64,${fileBase64}` + }) + } catch { + console.log('Skipping not found texture', fileName) } - const blockDataUrl = canvas.toDataURL('image/png') - const newData: TextureResolvedData = { - blockSize: tileSize, + + if (imgCustom) { + ctx.drawImage(imgCustom, x, y, tileSize, tileSize) + } else { + // todo this involves incorrect mappings for existing textures when the size is different + ctx.drawImage(img, xOrig, yOrig, originalTileSize, originalTileSize, x, y, tileSize, tileSize) } - await fs.promises.writeFile(genereatedPathData, JSON.stringify(newData), 'utf8') - await fs.promises.writeFile(blocksGenereatedPath, blockDataUrl, 'utf8') - await applyTexturePackData(version, newData, blockDataUrl) + } + const blockDataUrl = canvas.toDataURL('image/png') + const newData: TextureResolvedData = { + blockSize: tileSize, + } + await fs.promises.writeFile(genereatedPathData, JSON.stringify(newData), 'utf8') + await fs.promises.writeFile(blocksGenereatedPath, blockDataUrl, 'utf8') + await applyTexturePackData(version, newData, blockDataUrl) - // const a = document.createElement('a') - // a.href = dataUrl - // a.download = 'pack.png' - // a.click() + // const a = document.createElement('a') + // a.href = dataUrl + // a.download = 'pack.png' + // a.click() } export const watchTexturepackInViewer = (viewer: Viewer) => { - subscribeKey(resourcePackState, 'currentTexturesDataUrl', () => { - console.log('applying resourcepack world data') - viewer.world.texturesDataUrl = resourcePackState.currentTexturesDataUrl - viewer.world.blockStatesData = resourcePackState.currentTexturesBlockStates - if (!viewer?.world.active) return - viewer.world.updateTexturesData() - }) + subscribeKey(resourcePackState, 'currentTexturesDataUrl', () => { + console.log('applying resourcepack world data') + viewer.world.texturesDataUrl = resourcePackState.currentTexturesDataUrl + viewer.world.blockStatesData = resourcePackState.currentTexturesBlockStates + if (!viewer?.world.active) return + viewer.world.updateTexturesData() + }) } diff --git a/src/utils.ts b/src/utils.ts index 40e05463c..706f2f638 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -48,7 +48,7 @@ export const pointerLock = { unadjustedMovement: options.mouseRawInput }) promise?.catch((error) => { - if (error.name === "NotSupportedError") { + if (error.name === 'NotSupportedError') { // Some platforms may not support unadjusted movement, request again a regular pointer lock. document.documentElement.requestPointerLock() } else if (error.name === 'SecurityError') { @@ -106,11 +106,11 @@ export async function getScreenRefreshRate(): Promise { export const getGamemodeNumber = (bot) => { switch (bot.game.gameMode) { - case 'survival': return 0 - case 'creative': return 1 - case 'adventure': return 2 - case 'spectator': return 3 - default: return -1 + case 'survival': return 0 + case 'creative': return 1 + case 'adventure': return 2 + case 'spectator': return 3 + default: return -1 } }