685 lines
20 KiB
TypeScript
685 lines
20 KiB
TypeScript
import { join } from 'path'
|
|
import { promisify } from 'util'
|
|
import fs from 'fs'
|
|
import sanitizeFilename from 'sanitize-filename'
|
|
import { oneOf } from '@zardoy/utils'
|
|
import * as browserfs from 'browserfs'
|
|
import { options, resetOptions } from './optionsStorage'
|
|
|
|
import { fsState, loadSave } from './loadSave'
|
|
import { installResourcepackPack, installTexturePackFromHandle, updateTexturePackInstalledState } from './resourcePack'
|
|
import { miscUiState } from './globalState'
|
|
import { setLoadingScreenStatus } from './appStatus'
|
|
import { VALID_REPLAY_EXTENSIONS, openFile } from './packetsReplay/replayPackets'
|
|
import { getFixedFilesize } from './downloadAndOpenFile'
|
|
import { packetsReplayState } from './react/state/packetsReplayState'
|
|
import { createFullScreenProgressReporter } from './core/progressReporter'
|
|
import { showNotification } from './react/NotificationProvider'
|
|
import { resetAppStorage } from './react/appStorageProvider'
|
|
import { ConnectOptions } from './connect'
|
|
const { GoogleDriveFileSystem } = require('google-drive-browserfs/src/backends/GoogleDrive')
|
|
|
|
browserfs.install(window)
|
|
const defaultMountablePoints = {
|
|
'/data': { fs: 'IndexedDB' },
|
|
'/resourcepack': { fs: 'InMemory' }, // temporary storage for currently loaded resource pack
|
|
'/temp': { fs: 'InMemory' }
|
|
}
|
|
const fallbackMountablePoints = {
|
|
'/resourcepack': { fs: 'InMemory' }, // temporary storage for downloaded server resource pack
|
|
'/temp': { fs: 'InMemory' }
|
|
}
|
|
browserfs.configure({
|
|
fs: 'MountableFileSystem',
|
|
options: defaultMountablePoints,
|
|
}, async (e) => {
|
|
if (e) {
|
|
browserfs.configure({
|
|
fs: 'MountableFileSystem',
|
|
options: fallbackMountablePoints,
|
|
}, async (e2) => {
|
|
if (e2) {
|
|
showNotification('Unknown FS error, cannot continue', e2.message, true)
|
|
throw e2
|
|
}
|
|
showNotification('Failed to access device storage', `Check you have free space. ${e.message}`, true)
|
|
miscUiState.fsReady = true
|
|
miscUiState.singleplayerAvailable = false
|
|
})
|
|
return
|
|
}
|
|
await updateTexturePackInstalledState()
|
|
miscUiState.fsReady = true
|
|
miscUiState.singleplayerAvailable = true
|
|
})
|
|
|
|
export const forceCachedDataPaths = {}
|
|
export const forceRedirectPaths = {}
|
|
|
|
window.fs = fs
|
|
//@ts-expect-error
|
|
fs.promises = new Proxy(Object.fromEntries(['readFile', 'writeFile', 'stat', 'mkdir', 'rmdir', 'unlink', 'rename', /* 'copyFile', */'readdir'].map(key => [key, promisify(fs[key])])), {
|
|
get (target, p: string, receiver) {
|
|
if (!target[p]) throw new Error(`Not implemented fs.promises.${p}`)
|
|
return (...args) => {
|
|
// browser fs bug: if path doesn't start with / dirname will return . which would cause infinite loop, so we need to normalize paths
|
|
if (typeof args[0] === 'string' && !args[0].startsWith('/')) args[0] = '/' + args[0]
|
|
const toRemap = Object.entries(forceRedirectPaths).find(([from]) => args[0].startsWith(from))
|
|
if (toRemap) {
|
|
args[0] = args[0].replace(toRemap[0], toRemap[1])
|
|
}
|
|
// Write methods
|
|
// todo issue one-time warning (in chat I guess)
|
|
const readonly = fsState.isReadonly && !(args[0].startsWith('/data') && !fsState.inMemorySave) // allow copying worlds from external providers such as zip
|
|
if (readonly) {
|
|
if (oneOf(p, 'readFile', 'writeFile') && forceCachedDataPaths[args[0]]) {
|
|
if (p === 'readFile') {
|
|
return Promise.resolve(forceCachedDataPaths[args[0]])
|
|
} else if (p === 'writeFile') {
|
|
forceCachedDataPaths[args[0]] = args[1]
|
|
console.debug('Skipped writing to readonly fs', args[0])
|
|
return Promise.resolve()
|
|
}
|
|
}
|
|
if (oneOf(p, 'writeFile', 'mkdir', 'rename')) return
|
|
}
|
|
if (p === 'open' && fsState.isReadonly) {
|
|
args[1] = 'r' // read-only, zipfs throw otherwise
|
|
}
|
|
if (p === 'readFile') {
|
|
fsState.openReadOperations++
|
|
} else if (p === 'writeFile') {
|
|
fsState.openWriteOperations++
|
|
}
|
|
return target[p](...args).finally(() => {
|
|
if (p === 'readFile') {
|
|
fsState.openReadOperations--
|
|
} else if (p === 'writeFile') {
|
|
fsState.openWriteOperations--
|
|
}
|
|
})
|
|
}
|
|
}
|
|
})
|
|
//@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) => {
|
|
return new Promise(resolve => {
|
|
// todo it results in world corruption on interactions eg block placements
|
|
if (x === 'write' && fsState.isReadonly) {
|
|
resolve({ buffer: Buffer.from([]), bytesRead: 0 })
|
|
return
|
|
}
|
|
|
|
if (x === 'read') {
|
|
fsState.openReadOperations++
|
|
} else if (x === 'write' || x === 'close') {
|
|
fsState.openWriteOperations++
|
|
}
|
|
fs[x](fd, ...args, (err, bytesRead, buffer) => {
|
|
if (x === 'read') {
|
|
fsState.openReadOperations--
|
|
} else if (x === 'write' || x === 'close') {
|
|
// todo that's not correct
|
|
fsState.openWriteOperations--
|
|
}
|
|
if (err) throw err
|
|
// todo if readonly probably there is no need to open at all (return some mocked version - check reload)?
|
|
if (x === 'write' && !fsState.isReadonly) {
|
|
// flush data, though alternatively we can rely on close in unload
|
|
fs.fsync(fd, () => { })
|
|
}
|
|
resolve({ buffer, bytesRead })
|
|
})
|
|
})
|
|
}])),
|
|
// for debugging
|
|
fd,
|
|
filename: args[0],
|
|
async close () {
|
|
return new Promise<void>(resolve => {
|
|
fs.close(fd, (err) => {
|
|
if (err) {
|
|
throw err
|
|
} else {
|
|
resolve()
|
|
}
|
|
})
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
// for testing purposes, todo move it to core patch
|
|
const removeFileRecursiveSync = (path) => {
|
|
for (const file of fs.readdirSync(path)) {
|
|
const curPath = join(path, file)
|
|
if (fs.lstatSync(curPath).isDirectory()) {
|
|
// recurse
|
|
removeFileRecursiveSync(curPath)
|
|
fs.rmdirSync(curPath)
|
|
} else {
|
|
// delete file
|
|
fs.unlinkSync(curPath)
|
|
}
|
|
}
|
|
}
|
|
|
|
window.removeFileRecursiveSync = removeFileRecursiveSync
|
|
|
|
export const mkdirRecursive = async (path: string) => {
|
|
const parts = path.split('/')
|
|
let current = ''
|
|
for (const part of parts) {
|
|
current += part + '/'
|
|
try {
|
|
// eslint-disable-next-line no-await-in-loop
|
|
await fs.promises.mkdir(current)
|
|
} catch (err) {
|
|
}
|
|
}
|
|
}
|
|
|
|
export const uniqueFileNameFromWorldName = async (title: string, savePath: string) => {
|
|
const name = sanitizeFilename(title)
|
|
let resultPath!: string
|
|
// getUniqueFolderName
|
|
let i = 0
|
|
let free = false
|
|
while (!free) {
|
|
try {
|
|
resultPath = `${savePath.replace(/\$/, '')}/${name}${i === 0 ? '' : `-${i}`}`
|
|
// eslint-disable-next-line no-await-in-loop
|
|
await fs.promises.stat(resultPath)
|
|
i++
|
|
} catch (err) {
|
|
free = true
|
|
}
|
|
}
|
|
return resultPath
|
|
}
|
|
|
|
export const mountExportFolder = async () => {
|
|
let handle: FileSystemDirectoryHandle
|
|
try {
|
|
handle = await showDirectoryPicker({
|
|
id: 'world-export',
|
|
})
|
|
} catch (err) {
|
|
if (err instanceof DOMException && err.name === 'AbortError') return
|
|
throw err
|
|
}
|
|
if (!handle) return false
|
|
await new Promise<void>(resolve => {
|
|
browserfs.configure({
|
|
fs: 'MountableFileSystem',
|
|
options: {
|
|
...defaultMountablePoints,
|
|
'/export': {
|
|
fs: 'FileSystemAccess',
|
|
options: {
|
|
handle
|
|
}
|
|
}
|
|
},
|
|
}, (e) => {
|
|
if (e) throw e
|
|
resolve()
|
|
})
|
|
})
|
|
return true
|
|
}
|
|
|
|
let googleDriveFileSystem
|
|
|
|
/** Only cached! */
|
|
export const googleDriveGetFileIdFromPath = (path: string) => {
|
|
return googleDriveFileSystem._getExistingFileId(path)
|
|
}
|
|
|
|
export const mountGoogleDriveFolder = async (readonly: boolean, rootId: string) => {
|
|
googleDriveFileSystem = new GoogleDriveFileSystem()
|
|
googleDriveFileSystem.rootDirId = rootId
|
|
googleDriveFileSystem.isReadonly = readonly
|
|
await new Promise<void>(resolve => {
|
|
browserfs.configure({
|
|
fs: 'MountableFileSystem',
|
|
options: {
|
|
...defaultMountablePoints,
|
|
'/google': googleDriveFileSystem
|
|
},
|
|
}, (e) => {
|
|
if (e) throw e
|
|
resolve()
|
|
})
|
|
})
|
|
fsState.isReadonly = readonly
|
|
fsState.syncFs = false
|
|
fsState.inMemorySave = false
|
|
fsState.remoteBackend = true
|
|
return true
|
|
}
|
|
|
|
export async function removeFileRecursiveAsync (path, removeDirectoryItself = true) {
|
|
const errors = [] as Array<[string, Error]>
|
|
try {
|
|
const files = await fs.promises.readdir(path)
|
|
|
|
// Use Promise.all to parallelize file/directory removal
|
|
await Promise.all(files.map(async (file) => {
|
|
const curPath = join(path, file)
|
|
const stats = await fs.promises.stat(curPath)
|
|
if (stats.isDirectory()) {
|
|
// Recurse
|
|
await removeFileRecursiveAsync(curPath)
|
|
} else {
|
|
// Delete file
|
|
await fs.promises.unlink(curPath)
|
|
}
|
|
}))
|
|
|
|
// After removing all files/directories, remove the current directory
|
|
if (removeDirectoryItself) {
|
|
await fs.promises.rmdir(path)
|
|
}
|
|
} catch (error) {
|
|
errors.push([path, error])
|
|
}
|
|
|
|
if (errors.length) {
|
|
setTimeout(() => {
|
|
console.error(errors)
|
|
throw new Error(`Error removing directories/files: ${errors.map(([path, err]) => `${path}: ${err.message}`).join(', ')}`)
|
|
})
|
|
}
|
|
}
|
|
|
|
|
|
const SUPPORT_WRITE = true
|
|
|
|
export const openWorldDirectory = async (dragndropHandle?: FileSystemDirectoryHandle) => {
|
|
let _directoryHandle: FileSystemDirectoryHandle
|
|
if (dragndropHandle) {
|
|
_directoryHandle = dragndropHandle
|
|
} else {
|
|
try {
|
|
_directoryHandle = await window.showDirectoryPicker({
|
|
id: 'select-world', // important: this is used to remember user choice (start directory)
|
|
})
|
|
} catch (err) {
|
|
if (err instanceof DOMException && err.name === 'AbortError') return
|
|
throw err
|
|
}
|
|
}
|
|
const directoryHandle = _directoryHandle
|
|
|
|
const requestResult = SUPPORT_WRITE && !options.preferLoadReadonly ? await directoryHandle.requestPermission?.({ mode: 'readwrite' }) : undefined
|
|
const writeAccess = requestResult === 'granted'
|
|
|
|
const doContinue = writeAccess || !SUPPORT_WRITE || options.disableLoadPrompts || confirm('Continue in readonly mode?')
|
|
if (!doContinue) return
|
|
await new Promise<void>(resolve => {
|
|
browserfs.configure({
|
|
fs: 'MountableFileSystem',
|
|
options: {
|
|
...defaultMountablePoints,
|
|
'/world': {
|
|
fs: 'FileSystemAccess',
|
|
options: {
|
|
handle: directoryHandle
|
|
}
|
|
}
|
|
},
|
|
}, (e) => {
|
|
if (e) throw e
|
|
resolve()
|
|
})
|
|
})
|
|
|
|
fsState.isReadonly = !writeAccess
|
|
fsState.syncFs = false
|
|
fsState.inMemorySave = false
|
|
fsState.remoteBackend = false
|
|
await loadSave()
|
|
}
|
|
|
|
const tryToDetectResourcePack = async () => {
|
|
const askInstall = async () => {
|
|
// todo investigate browserfs read errors
|
|
return alert('ATM You can install texturepacks only via options menu.')
|
|
// if (confirm('Resource pack detected, do you want to install it?')) {
|
|
// await installTexturePackFromHandle()
|
|
// }
|
|
}
|
|
|
|
if (fs.existsSync('/world/pack.mcmeta')) {
|
|
await askInstall()
|
|
return true
|
|
}
|
|
// const jszip = new JSZip()
|
|
// let loaded = await jszip.loadAsync(file)
|
|
// if (loaded.file('pack.mcmeta')) {
|
|
// loaded = null
|
|
// askInstall()
|
|
// return true
|
|
// }
|
|
// loaded = null
|
|
}
|
|
|
|
export const possiblyCleanHandle = (callback = () => { }) => {
|
|
if (!fsState.saveLoaded) {
|
|
// todo clean handle
|
|
browserfs.configure({
|
|
fs: 'MountableFileSystem',
|
|
options: defaultMountablePoints,
|
|
}, (e) => {
|
|
callback()
|
|
if (e) throw e
|
|
})
|
|
}
|
|
}
|
|
|
|
const readdirSafe = async (path: string) => {
|
|
try {
|
|
return await fs.promises.readdir(path)
|
|
} catch (err) {
|
|
return null
|
|
}
|
|
}
|
|
|
|
export const collectFilesToCopy = async (basePath: string, safe = false): Promise<string[]> => {
|
|
const result: string[] = []
|
|
const countFiles = async (relPath: string) => {
|
|
const resolvedPath = join(basePath, relPath)
|
|
const files = relPath === '.' && !safe ? await fs.promises.readdir(resolvedPath) : await readdirSafe(resolvedPath)
|
|
if (!files) return null
|
|
await Promise.all(files.map(async file => {
|
|
const res = await countFiles(join(relPath, file))
|
|
if (res === null) {
|
|
// is file
|
|
result.push(join(relPath, file))
|
|
}
|
|
}))
|
|
}
|
|
await countFiles('.')
|
|
return result
|
|
}
|
|
|
|
export const copyFilesAsyncWithProgress = async (pathSrc: string, pathDest: string, throwRootNotExist = true, addMsg = '') => {
|
|
const stat = await existsViaStats(pathSrc)
|
|
if (!stat) {
|
|
if (throwRootNotExist) throw new Error(`Cannot copy. Source directory ${pathSrc} does not exist`)
|
|
console.debug('source directory does not exist', pathSrc)
|
|
return
|
|
}
|
|
if (!stat.isDirectory()) {
|
|
await fs.promises.writeFile(pathDest, await fs.promises.readFile(pathSrc) as any)
|
|
console.debug('copied single file', pathSrc, pathDest)
|
|
return
|
|
}
|
|
|
|
try {
|
|
setLoadingScreenStatus('Copying files')
|
|
let filesCount = 0
|
|
const countFiles = async (path: string) => {
|
|
const files = await fs.promises.readdir(path)
|
|
await Promise.all(files.map(async (file) => {
|
|
const curPath = join(path, file)
|
|
const stats = await fs.promises.stat(curPath)
|
|
if (stats.isDirectory()) {
|
|
// Recurse
|
|
await countFiles(curPath)
|
|
} else {
|
|
filesCount++
|
|
}
|
|
}))
|
|
}
|
|
console.debug('Counting files', pathSrc)
|
|
await countFiles(pathSrc)
|
|
console.debug('counted', filesCount)
|
|
let copied = 0
|
|
await copyFilesAsync(pathSrc, pathDest, (name) => {
|
|
copied++
|
|
setLoadingScreenStatus(`Copying files${addMsg} (${copied}/${filesCount}): ${name}`)
|
|
})
|
|
} finally {
|
|
setLoadingScreenStatus(undefined)
|
|
}
|
|
}
|
|
|
|
export const existsViaStats = async (path: string) => {
|
|
try {
|
|
return await fs.promises.stat(path)
|
|
} catch (e) {
|
|
return false
|
|
}
|
|
}
|
|
|
|
export const fileExistsAsyncOptimized = async (path: string) => {
|
|
try {
|
|
await fs.promises.readdir(path)
|
|
} catch (err) {
|
|
if (err.code === 'ENOTDIR') return true
|
|
// eslint-disable-next-line sonarjs/prefer-single-boolean-return
|
|
if (err.code === 'ENOENT') return false
|
|
// throw err
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
export const copyFilesAsync = async (pathSrc: string, pathDest: string, fileCopied?: (name) => void) => {
|
|
// query: can't use fs.copy! use fs.promises.writeFile and readFile
|
|
const files = await fs.promises.readdir(pathSrc)
|
|
|
|
if (!await existsViaStats(pathDest)) {
|
|
await fs.promises.mkdir(pathDest, { recursive: true })
|
|
}
|
|
|
|
// Use Promise.all to parallelize file/directory copying
|
|
await Promise.all(files.map(async (file) => {
|
|
const curPathSrc = join(pathSrc, file)
|
|
const curPathDest = join(pathDest, file)
|
|
const stats = await fs.promises.stat(curPathSrc)
|
|
if (stats.isDirectory()) {
|
|
// Recurse
|
|
await fs.promises.mkdir(curPathDest)
|
|
await copyFilesAsync(curPathSrc, curPathDest, fileCopied)
|
|
} else {
|
|
// Copy file
|
|
try {
|
|
await fs.promises.writeFile(curPathDest, await fs.promises.readFile(curPathSrc) as any)
|
|
console.debug('copied file', curPathSrc, curPathDest)
|
|
} catch (err) {
|
|
console.error('Error copying file', curPathSrc, curPathDest, err)
|
|
throw err
|
|
}
|
|
fileCopied?.(curPathDest)
|
|
}
|
|
}))
|
|
}
|
|
|
|
export const openWorldFromHttpDir = async (fileDescriptorUrls: string[]/* | undefined */, baseUrlParam) => {
|
|
// todo try go guess mode
|
|
let index
|
|
let baseUrl
|
|
for (const url of fileDescriptorUrls) {
|
|
let file
|
|
try {
|
|
setLoadingScreenStatus(`Trying to get world descriptor from ${new URL(url).host}`)
|
|
const controller = new AbortController()
|
|
setTimeout(() => {
|
|
controller.abort()
|
|
}, 3000)
|
|
// eslint-disable-next-line no-await-in-loop
|
|
const response = await fetch(url, { signal: controller.signal })
|
|
// eslint-disable-next-line no-await-in-loop
|
|
file = await response.json()
|
|
} catch (err) {
|
|
console.error('Error fetching file descriptor', url, err)
|
|
}
|
|
if (!file) continue
|
|
if (file.baseUrl) {
|
|
baseUrl = new URL(file.baseUrl, baseUrl).toString()
|
|
index = file.index
|
|
} else {
|
|
index = file
|
|
baseUrl = baseUrlParam ?? url.split('/').slice(0, -1).join('/')
|
|
}
|
|
break
|
|
}
|
|
if (!index) throw new Error(`The provided mapDir file is not valid descriptor file! ${fileDescriptorUrls.join(', ')}`)
|
|
await new Promise<void>(async resolve => {
|
|
browserfs.configure({
|
|
fs: 'MountableFileSystem',
|
|
options: {
|
|
...defaultMountablePoints,
|
|
'/world': {
|
|
fs: 'HTTPRequest',
|
|
options: {
|
|
index,
|
|
baseUrl
|
|
}
|
|
}
|
|
},
|
|
}, (e) => {
|
|
if (e) throw e
|
|
resolve()
|
|
})
|
|
})
|
|
|
|
fsState.saveLoaded = false
|
|
fsState.isReadonly = true
|
|
fsState.syncFs = false
|
|
fsState.inMemorySave = false
|
|
fsState.remoteBackend = true
|
|
|
|
await loadSave()
|
|
}
|
|
|
|
// todo rename method
|
|
const openWorldZipInner = async (file: File | ArrayBuffer, name = file['name'], connectOptions?: Partial<ConnectOptions>) => {
|
|
await new Promise<void>(async resolve => {
|
|
browserfs.configure({
|
|
// todo
|
|
fs: 'MountableFileSystem',
|
|
options: {
|
|
...defaultMountablePoints,
|
|
'/world': {
|
|
fs: 'ZipFS',
|
|
options: {
|
|
zipData: Buffer.from(file instanceof File ? (await file.arrayBuffer()) : file),
|
|
name
|
|
}
|
|
}
|
|
},
|
|
}, (e) => {
|
|
if (e) throw e
|
|
resolve()
|
|
})
|
|
})
|
|
|
|
fsState.saveLoaded = false
|
|
fsState.isReadonly = true
|
|
fsState.syncFs = true
|
|
fsState.inMemorySave = false
|
|
fsState.remoteBackend = false
|
|
|
|
if (fs.existsSync('/world/level.dat')) {
|
|
await loadSave()
|
|
} else {
|
|
const dirs = fs.readdirSync('/world')
|
|
const availableWorlds: string[] = []
|
|
for (const dir of dirs) {
|
|
if (fs.existsSync(`/world/${dir}/level.dat`)) {
|
|
availableWorlds.push(dir)
|
|
}
|
|
}
|
|
|
|
if (availableWorlds.length === 0) {
|
|
if (await tryToDetectResourcePack()) return
|
|
alert('No worlds found in the zip')
|
|
return
|
|
}
|
|
|
|
if (availableWorlds.length === 1) {
|
|
await loadSave(`/world/${availableWorlds[0]}`, connectOptions)
|
|
return
|
|
}
|
|
|
|
alert(`Many (${availableWorlds.length}) worlds found in the zip!`)
|
|
// todo prompt picker
|
|
// const selectWorld
|
|
}
|
|
}
|
|
|
|
export const openWorldZip = async (...args: Parameters<typeof openWorldZipInner>) => {
|
|
try {
|
|
return await openWorldZipInner(...args)
|
|
} finally {
|
|
possiblyCleanHandle()
|
|
}
|
|
}
|
|
|
|
export const resetLocalStorage = () => {
|
|
resetOptions()
|
|
resetAppStorage()
|
|
}
|
|
|
|
window.resetLocalStorage = resetLocalStorage
|
|
|
|
export const openFilePicker = (specificCase?: 'resourcepack') => {
|
|
// create and show input picker
|
|
let picker: HTMLInputElement = document.body.querySelector('input#file-zip-picker')!
|
|
if (!picker) {
|
|
picker = document.createElement('input')
|
|
picker.type = 'file'
|
|
picker.accept = specificCase ? '.zip' : [...VALID_REPLAY_EXTENSIONS, '.zip'].join(',')
|
|
|
|
picker.addEventListener('change', () => {
|
|
const file = picker.files?.[0]
|
|
picker.value = ''
|
|
if (!file) return
|
|
if (specificCase === 'resourcepack') {
|
|
if (!file.name.endsWith('.zip')) {
|
|
const doContinue = confirm(`Are you sure ${file.name.slice(-20)} is .zip file? ONLY .zip files are supported. Continue?`)
|
|
if (!doContinue) return
|
|
}
|
|
void installResourcepackPack(file, createFullScreenProgressReporter()).catch((err) => {
|
|
setLoadingScreenStatus(err.message, true)
|
|
})
|
|
} else {
|
|
// eslint-disable-next-line no-lonely-if
|
|
if (VALID_REPLAY_EXTENSIONS.some(ext => file.name.endsWith(ext)) || file.name.startsWith('packets-replay')) {
|
|
void file.text().then(contents => {
|
|
openFile({
|
|
contents,
|
|
filename: file.name,
|
|
filesize: file.size
|
|
})
|
|
})
|
|
} else {
|
|
void openWorldZip(file)
|
|
}
|
|
}
|
|
})
|
|
picker.hidden = true
|
|
document.body.appendChild(picker)
|
|
}
|
|
|
|
picker.click()
|
|
}
|
|
|
|
export const resetStateAfterDisconnect = () => {
|
|
miscUiState.gameLoaded = false
|
|
miscUiState.loadedDataVersion = null
|
|
miscUiState.singleplayer = false
|
|
miscUiState.flyingSquid = false
|
|
miscUiState.wanOpened = false
|
|
miscUiState.currentDisplayQr = null
|
|
|
|
fsState.saveLoaded = false
|
|
}
|