Releasing soon (#183)

This commit is contained in:
Vitaly 2024-08-26 03:34:02 +03:00 committed by GitHub
commit e3525dd5ec
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
34 changed files with 338 additions and 101 deletions

View file

@ -3,7 +3,7 @@ env:
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
ALIASES: ${{ vars.ALIASES }}
MAIN_MENU_LINKS: ${{ secrets.MAIN_MENU_LINKS }}
MAIN_MENU_LINKS: ${{ vars.MAIN_MENU_LINKS }}
on:
push:
branches:

View file

@ -2,7 +2,7 @@ name: Release
env:
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
MAIN_MENU_LINKS: ${{ secrets.MAIN_MENU_LINKS }}
MAIN_MENU_LINKS: ${{ vars.MAIN_MENU_LINKS }}
on:
push:
branches: [release]

View file

@ -141,7 +141,31 @@ Single player specific:
- `?singleplayer=1` - Create empty world on load. Nothing will be saved
- `?version=<version>` - Set the version for the singleplayer world (when used with `?singleplayer=1`)
- `?noSave=true` - Disable auto save on unload / disconnect / export whenever a world is loaded. Only manual save with `/save` command will work.
- `?map=<map_url>` - Load the map from ZIP. You can use any url, but it must be CORS enabled.
- `?map=<map_url>` - Load the map from ZIP. You can use any url, but it must be **CORS enabled**.
- `?mapDir=<index_file_url>` - Load the map from a file descriptor. It's recommended and the fastest way to load world but requires additional setup. The file must be in the following format:
```json
{
"baseUrl": "<url>",
"index": {
"level.dat": null,
"region": {
"r.-1.-1.mca": null,
"r.-1.0.mca": null,
"r.0.-1.mca": null,
"r.0.0.mca": null,
}
}
}
```
Note that `mapDir` also accepts base64 encoded JSON like so:
`?mapDir=data:application/json;base64,...` where `...` is the base64 encoded JSON of the index file.
In this case you must use `?mapDirBaseUrl` to specify the base URL to fetch the files from index.
- `?mapDirBaseUrl` - See above.
<!-- - `?mapDirGuess=<base_url>` - Load the map from the provided URL and paths will be guessed with a few additional fetch requests. -->
General:

View file

@ -68,7 +68,7 @@
"esbuild-plugin-polyfill-node": "^0.3.0",
"express": "^4.18.2",
"filesize": "^10.0.12",
"flying-squid": "npm:@zardoy/flying-squid@^0.0.34",
"flying-squid": "npm:@zardoy/flying-squid@^0.0.35",
"fs-extra": "^11.1.1",
"google-drive-browserfs": "github:zardoy/browserfs#google-drive",
"jszip": "^3.10.1",

10
pnpm-lock.yaml generated
View file

@ -117,8 +117,8 @@ importers:
specifier: ^10.0.12
version: 10.0.12
flying-squid:
specifier: npm:@zardoy/flying-squid@^0.0.34
version: '@zardoy/flying-squid@0.0.34(encoding@0.1.13)'
specifier: npm:@zardoy/flying-squid@^0.0.35
version: '@zardoy/flying-squid@0.0.35(encoding@0.1.13)'
fs-extra:
specifier: ^11.1.1
version: 11.1.1
@ -3386,8 +3386,8 @@ packages:
resolution: {integrity: sha512-6xm38yGVIa6mKm/DUCF2zFFJhERh/QWp1ufm4cNUvxsONBmfPg8uZ9pZBdOmF6qFGr/HlT6ABBkCSx/dlEtvWg==}
engines: {node: '>=12 <14 || 14.2 - 14.9 || >14.10.0'}
'@zardoy/flying-squid@0.0.34':
resolution: {integrity: sha512-1q9AE4GfmRQhKnSJ3QJtLZIznjJ/IcvwjjKKBS/LrxzaN+qsa3RI2H68OOULj5r/tiGU9DQNncW3CpMlezH6gA==}
'@zardoy/flying-squid@0.0.35':
resolution: {integrity: sha512-6cZdDi7yaqxh6KbOPhDueipcr9DBgJ3mJY+/QwAjaSzhP//5n1BLjyVGlx2Ncs/6Vns2grTOmeuDhJjMbVgjQg==}
engines: {node: '>=8'}
hasBin: true
@ -12923,7 +12923,7 @@ snapshots:
'@types/emscripten': 1.39.8
tslib: 1.14.1
'@zardoy/flying-squid@0.0.34(encoding@0.1.13)':
'@zardoy/flying-squid@0.0.35(encoding@0.1.13)':
dependencies:
'@tootallnate/once': 2.0.0
change-case: 4.1.2

View file

@ -15,7 +15,7 @@ import { WalkingGeneralSwing } from './entity/animations'
import externalTexturesJson from './entity/externalTextures.json'
import { disposeObject } from './threeJsUtils'
export const TWEEN_DURATION = 50 // todo should be 100
export const TWEEN_DURATION = 120
/**
* @param {string} username

View file

@ -354,6 +354,8 @@ function renderElement (world: World, cursor: Vec3, element: BlockElement, doAO:
}
}
const makeLooseObj = <T extends string> (obj: Record<T, any>) => obj
const invisibleBlocks = new Set(['air', 'cave_air', 'void_air', 'barrier'])
const isBlockWaterlogged = (block: Block) => block.getProperties().waterlogged === true || block.getProperties().waterlogged === 'true'
@ -363,7 +365,7 @@ let erroredBlockModel: BlockModelPartsResolved
export function getSectionGeometry (sx, sy, sz, world: World) {
let delayedRender = [] as Array<() => void>
const attr = {
const attr = makeLooseObj({
sx: sx + 8,
sy: sy + 8,
sz: sz + 8,
@ -379,14 +381,24 @@ export function getSectionGeometry (sx, sy, sz, world: World) {
tiles: {},
// todo this can be removed here
signs: {},
highestBlocks: {},
hadErrors: false
} as Record<string, any>
} as Record<string, any>)
const cursor = new Vec3(0, 0, 0)
for (cursor.y = sy; cursor.y < sy + 16; cursor.y++) {
for (cursor.z = sz; cursor.z < sz + 16; cursor.z++) {
for (cursor.x = sx; cursor.x < sx + 16; cursor.x++) {
const block = world.getBlock(cursor)!
if (!invisibleBlocks.has(block.name)) {
const highest = attr.highestBlocks[`${cursor.x},${cursor.z}`]
if (!highest || highest.y < cursor.y) {
attr.highestBlocks[`${cursor.x},${cursor.z}`] = {
y: cursor.y,
name: block.name
}
}
}
if (invisibleBlocks.has(block.name)) continue
if (block.name.includes('_sign') || block.name === 'sign') {
const key = `${cursor.x},${cursor.y},${cursor.z}`

View file

@ -170,6 +170,8 @@ export class Viewer {
})
}
addChunksBatchWaitTime = 200
// todo type
listen (emitter: EventEmitter) {
emitter.on('entity', (e) => {
@ -180,9 +182,26 @@ export class Viewer {
// this.updatePrimitive(p)
})
let currentLoadChunkBatch = null as {
timeout
data
} | null
emitter.on('loadChunk', ({ x, z, chunk, worldConfig, isLightUpdate }) => {
this.world.worldConfig = worldConfig
this.addColumn(x, z, chunk, isLightUpdate)
if (!currentLoadChunkBatch) {
// add a setting to use debounce instead
currentLoadChunkBatch = {
data: [],
timeout: setTimeout(() => {
for (const args of currentLoadChunkBatch!.data) {
//@ts-expect-error
this.addColumn(...args)
}
currentLoadChunkBatch = null
}, this.addChunksBatchWaitTime)
}
}
currentLoadChunkBatch.data.push([x, z, chunk, isLightUpdate])
})
// todo remove and use other architecture instead so data flow is clear
emitter.on('blockEntities', (blockEntities) => {

View file

@ -19,6 +19,7 @@ export class WorldDataEmitter extends EventEmitter {
private readonly lastPos: Vec3
private eventListeners: Record<string, any> = {}
private readonly emitter: WorldDataEmitter
keepChunksDistance = 0
constructor (public world: typeof __type_bot['world'], public viewDistance: number, position: Vec3 = new Vec3(0, 0, 0)) {
super()
@ -150,6 +151,9 @@ export class WorldDataEmitter extends EventEmitter {
}
}
// debugGotChunkLatency = [] as number[]
// lastTime = 0
async loadChunk (pos: ChunkPos, isLightUpdate = false) {
const [botX, botZ] = chunkPos(this.lastPos)
const dx = Math.abs(botX - Math.floor(pos.x / 16))
@ -158,6 +162,9 @@ export class WorldDataEmitter extends EventEmitter {
// eslint-disable-next-line @typescript-eslint/await-thenable -- todo allow to use async world provider but not sure if needed
const column = await this.world.getColumnAt(pos['y'] ? pos as Vec3 : new Vec3(pos.x, 0, pos.z))
if (column) {
// const latency = Math.floor(performance.now() - this.lastTime)
// this.debugGotChunkLatency.push(latency)
// this.lastTime = performance.now()
// todo optimize toJson data, make it clear why it is used
const chunk = column.toJson()
// TODO: blockEntities
@ -191,14 +198,14 @@ export class WorldDataEmitter extends EventEmitter {
const [botX, botZ] = chunkPos(pos)
if (lastX !== botX || lastZ !== botZ || force) {
this.emitter.emit('chunkPosUpdate', { pos })
const newView = new ViewRect(botX, botZ, this.viewDistance)
const newViewToUnload = new ViewRect(botX, botZ, this.viewDistance + this.keepChunksDistance)
const chunksToUnload: Vec3[] = []
for (const coords of Object.keys(this.loadedChunks)) {
const x = parseInt(coords.split(',')[0], 10)
const z = parseInt(coords.split(',')[1], 10)
const p = new Vec3(x, 0, z)
const [chunkX, chunkZ] = chunkPos(p)
if (!newView.contains(chunkX, chunkZ)) {
if (!newViewToUnload.contains(chunkX, chunkZ)) {
chunksToUnload.push(p)
}
}

View file

@ -75,6 +75,7 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
handleResize = () => { }
mesherConfig = defaultMesherConfig
camera: THREE.PerspectiveCamera
highestBlocks: Record<string, { y: number, name: string }> = {}
blockstatesModels: any
customBlockStates: Record<string, any> | undefined
customModels: Record<string, any> | undefined
@ -109,7 +110,15 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
const handleMessage = (data) => {
if (!this.active) return
this.handleWorkerMessage(data)
if (data.type === 'sectionFinished') {
if (data.type === 'geometry') {
for (const key in data.geometry.highestBlocks) {
const highest = data.geometry.highestBlocks[key]
if (!this.highestBlocks[key] || this.highestBlocks[key].y < highest.y) {
this.highestBlocks[key] = highest
}
}
}
if (data.type === 'sectionFinished') { // on after load & unload section
if (!this.sectionsOutstanding.get(data.key)) throw new Error(`sectionFinished event for non-outstanding section ${data.key}`)
this.sectionsOutstanding.set(data.key, this.sectionsOutstanding.get(data.key)! - 1)
if (this.sectionsOutstanding.get(data.key) === 0) this.sectionsOutstanding.delete(data.key)
@ -302,6 +311,19 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
}
this.allChunksFinished = Object.keys(this.finishedChunks).length === this.chunksLength
delete this.finishedChunks[`${x},${z}`]
for (let y = this.worldConfig.minY; y < this.worldConfig.worldHeight; y += 16) {
this.setSectionDirty(new Vec3(x, y, z), false)
}
// remove from highestBlocks
const startX = Math.floor(x / 16) * 16
const startZ = Math.floor(z / 16) * 16
const endX = Math.ceil((x + 1) / 16) * 16
const endZ = Math.ceil((z + 1) / 16) * 16
for (let x = startX; x < endX; x += 16) {
for (let z = startZ; z < endZ; z += 16) {
delete this.highestBlocks[`${x},${z}`]
}
}
}
setBlockStateId (pos: Vec3, stateId: number) {
@ -320,7 +342,7 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
queueAwaited = false
messagesQueue = {} as { [workerIndex: string]: any[] }
setSectionDirty (pos: Vec3, value = true) {
setSectionDirty (pos: Vec3, value = true) { // value false is used for unloading chunks
if (this.viewDistance === -1) throw new Error('viewDistance not set')
this.allChunksFinished = false
const distance = this.getDistance(pos)

View file

@ -66,6 +66,7 @@ export class WorldRendererThree extends WorldRendererCommon {
}
}
// debugRecomputedDeletedObjects = 0
handleWorkerMessage (data: any): void {
if (data.type !== 'geometry') return
let object: THREE.Object3D = this.sectionObjects[data.key]
@ -78,6 +79,10 @@ export class WorldRendererThree extends WorldRendererCommon {
const chunkCoords = data.key.split(',')
if (!this.loadedChunks[chunkCoords[0] + ',' + chunkCoords[2]] || !data.geometry.positions.length || !this.active) return
// if (object) {
// this.debugRecomputedDeletedObjects++
// }
// if (!this.initialChunksLoad && this.enableChunksLoadDelay) {
// const newPromise = new Promise(resolve => {
// if (this.droppedFpsPercentage > 0.5) {

View file

@ -49,11 +49,12 @@ export default defineConfig({
'minecraft-protocol$': 'minecraft-protocol/src/index.js',
'buffer$': 'buffer',
// avoid bundling, not used on client side
'prismarine-auth': './src/shims/empty.ts',
'prismarine-auth': './src/shims/prismarineAuthReplacement.ts',
perf_hooks: './src/shims/perf_hooks_replacement.js',
crypto: './src/shims/crypto.js',
dns: './src/shims/dns.js',
yggdrasil: './src/shims/yggdrasilReplacement.ts',
'three$': 'three/src/Three.js'
},
entry: {
index: './src/index.ts',
@ -182,9 +183,10 @@ export default defineConfig({
]
}
},
performance: {
// bundleAnalyze: {
// analyzerMode: 'json',
// },
},
// performance: {
// bundleAnalyze: {
// analyzerMode: 'json',
// reportFilename: 'report.json',
// },
// },
})

View file

@ -17,6 +17,7 @@ const app = express()
const isProd = process.argv.includes('--prod')
app.use(compression())
// app.use(cors())
app.use(netApi({ allowOrigin: '*' }))
if (!isProd) {
app.use('/sounds', express.static(path.join(__dirname, './generated/sounds/')))

View file

@ -233,6 +233,7 @@ export const mountGoogleDriveFolder = async (readonly: boolean, rootId: string)
fsState.isReadonly = readonly
fsState.syncFs = false
fsState.inMemorySave = false
fsState.remoteBackend = true
return true
}
@ -313,6 +314,7 @@ export const openWorldDirectory = async (dragndropHandle?: FileSystemDirectoryHa
fsState.isReadonly = !writeAccess
fsState.syncFs = false
fsState.inMemorySave = false
fsState.remoteBackend = false
await loadSave()
}
@ -352,7 +354,33 @@ export const possiblyCleanHandle = (callback = () => { }) => {
}
}
export const copyFilesAsyncWithProgress = async (pathSrc: string, pathDest: string, throwRootNotExist = true) => {
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`)
@ -387,7 +415,7 @@ export const copyFilesAsyncWithProgress = async (pathSrc: string, pathDest: stri
let copied = 0
await copyFilesAsync(pathSrc, pathDest, (name) => {
copied++
setLoadingScreenStatus(`Copying files (${copied}/${filesCount}): ${name}`)
setLoadingScreenStatus(`Copying files${addMsg} (${copied}/${filesCount}): ${name}`)
})
} finally {
setLoadingScreenStatus(undefined)
@ -402,6 +430,19 @@ export const existsViaStats = async (path: string) => {
}
}
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)
@ -433,6 +474,45 @@ export const copyFilesAsync = async (pathSrc: string, pathDest: string, fileCopi
}))
}
export const openWorldFromHttpDir = async (fileDescriptorUrl: string/* | undefined */, baseUrl = fileDescriptorUrl.split('/').slice(0, -1).join('/')) => {
// todo try go guess mode
let index
const file = await fetch(fileDescriptorUrl).then(async a => a.json())
if (file.baseUrl) {
baseUrl = new URL(file.baseUrl, baseUrl).toString()
index = file.index
} else {
index = file
}
if (!index) throw new Error(`The provided mapDir file is not valid descriptor file! ${fileDescriptorUrl}`)
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']) => {
await new Promise<void>(async resolve => {
@ -459,6 +539,7 @@ const openWorldZipInner = async (file: File | ArrayBuffer, name = file['name'])
fsState.isReadonly = true
fsState.syncFs = true
fsState.inMemorySave = false
fsState.remoteBackend = false
if (fs.existsSync('/world/level.dat')) {
await loadSave()

View file

@ -1,5 +1,5 @@
import prettyBytes from 'pretty-bytes'
import { openWorldZip } from './browserfs'
import { openWorldFromHttpDir, openWorldZip } from './browserfs'
import { getResourcePackNames, installTexturePack, resourcePackState, updateTexturePackInstalledState } from './resourcePack'
import { setLoadingScreenStatus } from './utils'
@ -9,6 +9,17 @@ export const getFixedFilesize = (bytes: number) => {
const inner = async () => {
const qs = new URLSearchParams(window.location.search)
const mapUrlDir = qs.get('mapDir')
const mapUrlDirGuess = qs.get('mapDirGuess')
const mapUrlDirBaseUrl = qs.get('mapDirBaseUrl')
if (mapUrlDir) {
await openWorldFromHttpDir(mapUrlDir, mapUrlDirBaseUrl ?? undefined)
return true
}
if (mapUrlDirGuess) {
// await openWorldFromHttpDir(undefined, mapUrlDirGuess)
return true
}
let mapUrl = qs.get('map')
const texturepack = qs.get('texturepack')
// fixme

View file

@ -35,24 +35,11 @@ subscribe(activeModalStack, () => {
}
})
export const customDisplayManageKeyword = 'custom'
const defaultModalActions = {
show (modal: Modal) {
if (modal.elem) modal.elem.style.display = 'block'
},
hide (modal: Modal) {
if (modal.elem) modal.elem.style.display = 'none'
}
}
/**
* @returns true if operation was successful
*/
const showModalInner = (modal: Modal) => {
const cancel = modal.elem?.show?.()
if (cancel && cancel !== customDisplayManageKeyword) return false
if (cancel !== 'custom') defaultModalActions.show(modal)
return true
}
@ -60,7 +47,6 @@ export const showModal = (elem: /* (HTMLElement & Record<string, any>) | */{ re
const resolved = elem
const curModal = activeModalStack.at(-1)
if (/* elem === curModal?.elem || */(elem.reactType && elem.reactType === curModal?.reactType) || !showModalInner(resolved)) return
if (curModal) defaultModalActions.hide(curModal)
activeModalStack.push(resolved)
}
@ -71,21 +57,21 @@ export const showModal = (elem: /* (HTMLElement & Record<string, any>) | */{ re
export const hideModal = (modal = activeModalStack.at(-1), data: any = undefined, options: { force?: boolean; restorePrevious?: boolean } = {}) => {
const { force = false, restorePrevious = true } = options
if (!modal) return
let cancel
if (modal.elem) {
cancel = modal.elem.hide?.(data)
} else if (modal.reactType) {
cancel = notHideableModalsWithoutForce.has(modal.reactType) ? !force : undefined
}
if (force && cancel !== customDisplayManageKeyword) {
let cancel = notHideableModalsWithoutForce.has(modal.reactType) ? !force : undefined
if (force) {
cancel = undefined
}
if (!cancel || cancel === customDisplayManageKeyword) {
if (cancel !== customDisplayManageKeyword) defaultModalActions.hide(modal)
activeModalStack.pop()
if (!cancel) {
const lastModal = activeModalStack.at(-1)
for (let i = activeModalStack.length - 1; i >= 0; i--) {
if (activeModalStack[i].reactType === modal.reactType) {
activeModalStack.splice(i, 1)
break
}
}
const newModal = activeModalStack.at(-1)
if (newModal && restorePrevious) {
if (newModal && lastModal !== newModal && restorePrevious) {
// would be great to ignore cancel I guess?
showModalInner(newModal)
}

View file

@ -24,7 +24,7 @@ import './reactUi'
import { contro, onBotCreate } from './controls'
import './dragndrop'
import { possiblyCleanHandle, resetStateAfterDisconnect } from './browserfs'
import { watchOptionsAfterViewerInit } from './watchOptions'
import { watchOptionsAfterViewerInit, watchOptionsAfterWorldViewInit } from './watchOptions'
import downloadAndOpenFile from './downloadAndOpenFile'
import fs from 'fs'
@ -98,7 +98,6 @@ import { signInMessageState } from './react/SignInMessageProvider'
import { updateAuthenticatedAccountData, updateLoadedServerData } from './react/ServersListProvider'
import { versionToNumber } from 'prismarine-viewer/viewer/prepare/utils'
import packetsPatcher from './packetsPatcher'
import blockstatesModels from 'mc-assets/dist/blockStatesModels.json'
import { mainMenuState } from './react/MainMenuRenderApp'
import { ItemsRenderer } from 'mc-assets/dist/itemsRenderer'
import './mobileShim'
@ -410,7 +409,7 @@ async function connect (connectOptions: ConnectOptions) {
throw err
}
}
viewer.world.blockstatesModels = blockstatesModels
viewer.world.blockstatesModels = await import('mc-assets/dist/blockStatesModels.json')
viewer.setVersion(version, options.useVersionsTextures === 'latest' ? version : options.useVersionsTextures)
}
@ -473,6 +472,7 @@ async function connect (connectOptions: ConnectOptions) {
setCacheResult (result) {
newTokensCacheResult = result
},
connectingServer: server.host
}) : undefined
bot = mineflayer.createBot({
@ -495,7 +495,7 @@ async function connect (connectOptions: ConnectOptions) {
signInMessageState.link = data.verification_uri
signInMessageState.expiresOn = Date.now() + data.expires_in * 1000
},
sessionServer: authData?.sessionEndpoint,
sessionServer: authData?.sessionEndpoint?.toString(),
auth: connectOptions.authenticatedAccount ? async (client, options) => {
authData!.setOnMsaCodeCallback(options.onMsaCode)
//@ts-expect-error
@ -690,6 +690,7 @@ async function connect (connectOptions: ConnectOptions) {
const center = bot.entity.position
const worldView = window.worldView = new WorldDataEmitter(bot.world, renderDistance, center)
watchOptionsAfterWorldViewInit()
bot.on('physicsTick', () => updateCursor())

View file

@ -166,7 +166,7 @@ const renderSlot = (slot: RenderSlot, skipBlock = false): {
itemTexture = itemsRenderer.getItemTexture(itemName) ?? itemsRenderer.getItemTexture('item/missing_texture')!
} catch (err) {
itemTexture = itemsRenderer.getItemTexture('block/errored')!
inGameError(err)
inGameError(`Failed to render item ${itemName} on ${bot.version} (resourcepack: ${options.enabledResourcepack}): ${err.message}`)
}
if ('type' in itemTexture) {
// is item

View file

@ -21,6 +21,7 @@ export const fsState = proxy({
saveLoaded: false,
openReadOperations: 0,
openWriteOperations: 0,
remoteBackend: false
})
const PROPOSE_BACKUP = true

View file

@ -10,12 +10,12 @@ export const getProxyDetails = async (proxyBaseUrl: string) => {
return result
}
export default async ({ tokenCaches, proxyBaseUrl, setProgressText = (text) => { }, setCacheResult }) => {
export default async ({ tokenCaches, proxyBaseUrl, setProgressText = (text) => { }, setCacheResult, connectingServer }) => {
let onMsaCodeCallback
// const authEndpoint = 'http://localhost:3000/'
// const sessionEndpoint = 'http://localhost:3000/session'
let authEndpoint = ''
let sessionEndpoint = ''
let authEndpoint: URL | undefined
let sessionEndpoint: URL | undefined
const result = await getProxyDetails(proxyBaseUrl)
try {
@ -29,7 +29,6 @@ export default async ({ tokenCaches, proxyBaseUrl, setProgressText = (text) => {
}
const authFlow = {
async getMinecraftJavaToken () {
setProgressText('Authenticating with Microsoft account')
let result = null
await fetch(authEndpoint, {
@ -37,10 +36,14 @@ export default async ({ tokenCaches, proxyBaseUrl, setProgressText = (text) => {
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(tokenCaches),
body: JSON.stringify({
...tokenCaches,
// important to set this param and not fake it as auth server might reject the request otherwise
connectingServer
}),
}).then(async response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}: ${await response.text()}`)
throw new Error(`Auth server error (${response.status}): ${await response.text()}`)
}
const reader = response.body!.getReader()
@ -158,8 +161,9 @@ function pemToArrayBuffer (pem) {
}
const urlWithBase = (url: string, base: string) => {
if (!base.startsWith('http')) base = `https://${base}`
const urlObj = new URL(url, base)
base = base.replace(/^https?:\/\//, '')
urlObj.host = base.includes(':') ? base : `${base}:${isPageSecure() ? '443' : '80'}`
return urlObj.toString()
return urlObj
}

View file

@ -83,6 +83,11 @@ export const guiOptionsScheme: {
},
starfieldRendering: {},
renderEntities: {},
keepChunksDistance: {
max: 5,
unit: '',
tooltip: 'Additional distance to keep the chunks loading before unloading them by marking them as too far',
},
},
],
main: [

View file

@ -7,6 +7,7 @@ import { omitObj } from '@zardoy/utils'
const defaultOptions = {
renderDistance: 3,
keepChunksDistance: 1,
multiplayerRenderDistance: 3,
closeConfirmation: true,
autoFullScreen: false,

View file

@ -101,14 +101,13 @@ export default ({ onBack, onConfirm, title = 'Add a Server', initialData, parseQ
}}>
<label style={{ fontSize: 12, marginBottom: 1, color: 'lightgray' }}>Version Override</label>
<SelectGameVersion
selected={{ value: versionOverride, label: versionOverride }}
versions={versions?.map(v => { return { value: v, label: v } }) ?? []}
onChange={(value) => {
setVersionOverride(value)
}}
// inputProps={{
// placeholder: 'Optional, but recommended to specify',
// disabled: lockConnect && qsParamVersion !== null
// }}
placeholder="Optional, but recommended to specify"
disabled={lockConnect && qsParamVersion !== null}
/>
</div>

View file

@ -81,7 +81,7 @@ export default ({ cancelClick, createClick, customizeClick, versions, defaultVer
</Button>
<Button disabled={!title} onClick={createClick}>Create</Button>
</div>
<div className='muted' style={{ fontSize: 9 }}>Note: store important saves in folders on the drive!</div>
<div className='muted' style={{ fontSize: 9 }}>Note: save important worlds in folders on your hard drive!</div>
<div className='muted' style={{ fontSize: 9 }}>{quota}</div>
</Screen>
}

View file

@ -8,7 +8,7 @@ export const DiscordButton = () => {
const links: DropdownButtonItem[] = [
{
text: 'Support Official Server (mcraft.fun)',
clickHandler: () => openURL('https://discord.gg/JCPnD4Qh')
clickHandler: () => openURL('https://discord.gg/xzGRhxtRUt')
},
{
text: 'Community Server (PrismarineJS)',

View file

@ -49,6 +49,7 @@
.undo-keyboard,
.undo-gamepad {
aspect-ratio: 1;
min-width: 20px;
}
.button {

View file

@ -40,7 +40,7 @@ export default ({
}: Props) => {
if (!bottomRightLinks?.trim()) bottomRightLinks = undefined
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
const linksParsed = bottomRightLinks?.split(';').map(l => {
const linksParsed = bottomRightLinks?.split(/;|\n/g).map(l => {
const parts = l.split(':')
return [parts[0], parts.slice(1).join(':')]
}) as Array<[string, string]> | undefined

View file

@ -1,4 +1,5 @@
import { join } from 'path'
import fs from 'fs'
import { useEffect } from 'react'
import { useSnapshot } from 'valtio'
import { usedServerPathsV1 } from 'flying-squid/dist/lib/modules/world'
@ -14,13 +15,14 @@ import { fsState } from '../loadSave'
import { disconnect } from '../flyingSquidUtils'
import { pointerLock, setLoadingScreenStatus } from '../utils'
import { closeWan, openToWanAndCopyJoinLink, getJoinLink } from '../localServerMultiplayer'
import { copyFilesAsyncWithProgress, mkdirRecursive, uniqueFileNameFromWorldName } from '../browserfs'
import { collectFilesToCopy, fileExistsAsyncOptimized, mkdirRecursive, uniqueFileNameFromWorldName } from '../browserfs'
import { useIsModalActive } from './utilsApp'
import { showOptionsModal } from './SelectOption'
import Button from './Button'
import Screen from './Screen'
import styles from './PauseScreen.module.css'
import { DiscordButton } from './DiscordButton'
import { showNotification } from './NotificationProvider'
export const saveToBrowserMemory = async () => {
setLoadingScreenStatus('Saving world')
@ -29,12 +31,46 @@ export const saveToBrowserMemory = async () => {
const { worldFolder } = localServer.options
const saveRootPath = await uniqueFileNameFromWorldName(worldFolder.split('/').pop(), `/data/worlds`)
await mkdirRecursive(saveRootPath)
for (const copyPath of [...usedServerPathsV1, 'icon.png']) {
const srcPath = join(worldFolder, copyPath)
const savePath = join(saveRootPath, copyPath)
const allRootPaths = [...usedServerPathsV1]
const allFilesToCopy = [] as string[]
for (const dirBase of allRootPaths) {
// eslint-disable-next-line no-await-in-loop
await copyFilesAsyncWithProgress(srcPath, savePath, false)
if (dirBase.includes('.') && await fileExistsAsyncOptimized(join(worldFolder, dirBase))) {
allFilesToCopy.push(dirBase)
continue
}
// eslint-disable-next-line no-await-in-loop
let res = await collectFilesToCopy(join(worldFolder, dirBase), true)
if (dirBase === 'region') {
res = res.filter(x => x.endsWith('.mca'))
}
allFilesToCopy.push(...res.map(x => join(dirBase, x)))
}
const pathsSplit = allFilesToCopy.reduce((acc, cur, i) => {
if (i % 15 === 0) {
acc.push([])
}
acc.at(-1)!.push(cur)
return acc
// eslint-disable-next-line @typescript-eslint/prefer-reduce-type-parameter
}, [] as string[][])
let copied = 0
const upProgress = () => {
copied++
const action = fsState.remoteBackend ? 'Downloading & copying' : 'Copying'
setLoadingScreenStatus(`${action} files (${copied}/${allFilesToCopy.length})`)
}
for (const copyPaths of pathsSplit) {
// eslint-disable-next-line no-await-in-loop
await Promise.all(copyPaths.map(async (copyPath) => {
const srcPath = join(worldFolder, copyPath)
const savePath = join(saveRootPath, copyPath)
await mkdirRecursive(savePath)
await fs.promises.writeFile(savePath, await fs.promises.readFile(srcPath))
upProgress()
}))
}
return saveRootPath
} catch (err) {
void showOptionsModal(`Error while saving the world: ${err.message}`, [])
@ -100,7 +136,14 @@ export default () => {
}
const action = await showOptionsModal('World actions...', ['Save to browser memory'])
if (action === 'Save to browser memory') {
await saveToBrowserMemory()
const path = await saveToBrowserMemory()
if (!path) return
const saveName = path.split('/').at(-1)
showNotification(`World saved to ${saveName}`, 'Load it to keep your progress!')
// fsState.inMemorySave = true
// fsState.syncFs = false
// fsState.isReadonly = false
// fsState.remoteBackend = false
}
}

View file

@ -14,11 +14,9 @@ export const Primary: Story = {
args: {
initialOptions: [{ value: '1', label: 'option 1' }, { value: '2', label: 'option 2' }, { value: '3', label: 'option 3' },],
updateOptions (options) {},
processInput (input) {
getCssOnInput (input) {
console.log('input:', input)
if (input === 'option 3') return { border: '1px solid yellow' } as CSSProperties
},
iconInput: 'user',
iconOption: 'user'
},
}

View file

@ -13,25 +13,23 @@ export interface OptionStorage {
interface Props {
initialOptions: OptionStorage[]
updateOptions: (options: string) => void
processInput?: (input: string) => CSSProperties | undefined
processOption?: (option: string) => string
getCssOnInput?: (input: string) => CSSProperties | undefined
onValueChange?: (newVal: string) => void
defaultValue?: { value: string, label: string }
iconInput?: string
placeholder?: string
iconOption?: string
containerStyle?: CSSProperties
inputProps?: React.ComponentProps<typeof Input>
disabled?: boolean
}
export default ({
initialOptions,
updateOptions,
processInput,
getCssOnInput,
onValueChange,
defaultValue,
containerStyle,
placeholder
placeholder,
disabled
}: Props) => {
const [inputValue, setInputValue] = useState<string | undefined>(defaultValue?.label ?? '')
const [currValue, setCurrValue] = useState<string | undefined>(defaultValue?.label ?? '')
@ -40,7 +38,6 @@ export default ({
return <Creatable
options={initialOptions}
aria-invalid="true"
defaultValue={defaultValue}
blurInputOnSelect={true}
hideSelectedOptions={false}
@ -49,6 +46,7 @@ export default ({
formatCreateLabel={(value) => {
return 'Use "' + value + '"'
}}
isDisabled={disabled}
placeholder={placeholder ?? ''}
onChange={(e, action) => {
console.log('value:', e?.value)
@ -56,7 +54,7 @@ export default ({
setInputValue(e?.label)
onValueChange?.(e?.value ?? '')
updateOptions?.(e?.value ?? '')
setInputStyle(processInput?.(e?.value ?? '') ?? {})
setInputStyle(getCssOnInput?.(e?.value ?? '') ?? {})
}}
onInputChange={(e) => {
setIsFirstClick(false)
@ -72,6 +70,7 @@ export default ({
onMenuOpen={() => {
setIsFirstClick(true)
}}
menuPortalTarget={document.body}
classNames={{
control (state) {
return styles.container
@ -84,7 +83,8 @@ export default ({
}
}}
styles={{
container (base, state) { return { ...base, position: 'relative' } },
menuPortal (base, state) { return { ...base, zIndex: 10, transform: 'scale(var(--guiScale))', transformOrigin: 'top left' } },
container (base, state) { return { ...base, position: 'relative', zIndex: 10 } },
control (base, state) { return { ...containerStyle, ...inputStyle } },
menu (base, state) { return { position: 'absolute', zIndex: 10 } },
option (base, state) {
@ -106,4 +106,3 @@ export default ({
}}
/>
}

View file

@ -1,19 +1,16 @@
import React, { CSSProperties } from 'react'
import Select from './Select'
import Input from './Input'
type Version = { value: string, label: string }
export default (
{ versions, selected, onChange, updateOptions, containerStyle }:
{
versions: Version[],
selected?: Version,
inputProps?: React.ComponentProps<typeof Input>,
onChange?: (newValue: string) => void,
updateOptions?: (newSel: string) => void,
containerStyle?: CSSProperties
}
{
versions: Version[],
selected?: Version,
onChange?: (newValue: string) => void,
updateOptions?: (newSel: string) => void,
} & Pick<React.ComponentProps<typeof Select>, 'containerStyle' | 'placeholder' | 'disabled'>
) => {
return <Select
initialOptions={versions}
@ -23,7 +20,7 @@ export default (
}}
onValueChange={onChange}
containerStyle={containerStyle ?? { width: '190px' }}
processInput={(value) => {
getCssOnInput={(value) => {
if (!versions || !value) return {}
const parsedsupportedVersions = versions.map(x => x.value.split('.').map(Number))
const parsedValue = value.split('.').map(Number)

View file

@ -33,7 +33,7 @@ export default ({
background: 'white',
padding: '20px 18px',
width: 300,
height: 213,
maxHeight: 240,
color: 'black',
// borderRadius: 8,
}}

View file

@ -0,0 +1,7 @@
// Auth is done in the connection option callback. In this app the auth implementation is server side.
const Titles = {}
export {
Titles
}

View file

@ -63,3 +63,14 @@ export const watchOptionsAfterViewerInit = () => {
viewer.world.starField.enabled = o.starfieldRendering
})
}
let viewWatched = false
export const watchOptionsAfterWorldViewInit = () => {
worldView!.keepChunksDistance = options.keepChunksDistance
if (viewWatched) return
viewWatched = true
watchValue(options, o => {
if (!worldView) return
worldView.keepChunksDistance = o.keepChunksDistance
})
}