enforce style that was used in repo! indent back to 2

This commit is contained in:
Vitaly 2023-09-26 08:26:20 +03:00
commit ff2c9e4c0c
12 changed files with 507 additions and 486 deletions

View file

@ -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",
// ---

View file

@ -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<AppOptions>) => {
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))
})

View file

@ -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

View file

@ -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

View file

@ -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
}
}
})

View file

@ -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]

View file

@ -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
}

View file

@ -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

View file

@ -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'
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()
}
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)
serverDuplex.on('end', endConnection)
serverDuplex.on('force-close', endConnection)
client.on('end', endConnection)
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)
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)
const disconnected = () => {
serverDuplex.end()
client.end()
}
connection.on('iceStateChanged', (state) => {
console.log('iceStateChanged', state)
if (state === 'disconnected') {
disconnected()
}
})
peer.on('error', (error) => {
console.error(error)
writeText(error.message)
})
return new Promise<string>(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<string>(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<void>((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<void>((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
}

View file

@ -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 (
<div
className={css`
if (!usingTouch) return null
return (
<div
className={css`
position: fixed;
inset: 0;
height: 100%;
@ -69,57 +69,57 @@ const TouchControls = () => {
pointer-events: auto;
}
`}
>
<LeftTouchArea />
<div />
<RightTouchArea />
</div>
)
>
<LeftTouchArea />
<div />
<RightTouchArea />
</div>
)
}
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(<div
style={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
zIndex: 15
}}
onClick={() => {
miscUiState.currentDisplayQr = null
}}
>
<QRCodeSVG size={384} value={currentDisplayQr} style={{display: 'block', border: '2px solid black',}} />
</div>, document.body)
return createPortal(<div
style={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
zIndex: 15
}}
onClick={() => {
miscUiState.currentDisplayQr = null
}}
>
<QRCodeSVG size={384} value={currentDisplayQr} style={{display: 'block', border: '2px solid black',}} />
</div>, document.body)
}
const App = () => {
const isBotAvailable = useIsBotAvailable()
if (!isBotAvailable) return null
const isBotAvailable = useIsBotAvailable()
if (!isBotAvailable) return null
return <div>
<DisplayQr />
<TouchControls />
</div>
return <div>
<DisplayQr />
<TouchControls />
</div>
}
renderToDom(<App />, {
strictMode: false,
selector: '#react-root',
strictMode: false,
selector: '#react-root',
})

View file

@ -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
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')
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 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))
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<void>(resolve => {
_imgCustom.addEventListener('load', () => {
imgCustom = _imgCustom
resolve()
})
_imgCustom.onerror = () => {
console.log('Skipping issued texture', fileName)
resolve()
}
}
if (await existsAsync(blocksGenereatedPath)) {
applyTexturePackData(version, JSON.parse(await fs.promises.readFile(genereatedPathData, 'utf8')), await fs.promises.readFile(blocksGenereatedPath, 'utf8'))
return
_imgCustom.src = `data:image/png;base64,${fileBase64}`
})
} catch {
console.log('Skipping not found texture', fileName)
}
setLoadingScreenStatus('Generating custom textures')
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 firstBlockFile = (await fs.promises.readdir(blocksBasePath)).find(f => f.endsWith('.png'))
if (!firstBlockFile) {
return
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 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)
// 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<void>(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)
}
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 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()
})
}

View file

@ -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<number> {
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
}
}