Releasing soon (#183)
This commit is contained in:
commit
e3525dd5ec
34 changed files with 338 additions and 101 deletions
2
.github/workflows/next-deploy.yml
vendored
2
.github/workflows/next-deploy.yml
vendored
|
|
@ -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:
|
||||
|
|
|
|||
2
.github/workflows/publish.yml
vendored
2
.github/workflows/publish.yml
vendored
|
|
@ -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]
|
||||
|
|
|
|||
26
README.MD
26
README.MD
|
|
@ -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:
|
||||
|
||||
|
|
|
|||
|
|
@ -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
10
pnpm-lock.yaml
generated
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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}`
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
// },
|
||||
// },
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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/')))
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ export const fsState = proxy({
|
|||
saveLoaded: false,
|
||||
openReadOperations: 0,
|
||||
openWriteOperations: 0,
|
||||
remoteBackend: false
|
||||
})
|
||||
|
||||
const PROPOSE_BACKUP = true
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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: [
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import { omitObj } from '@zardoy/utils'
|
|||
|
||||
const defaultOptions = {
|
||||
renderDistance: 3,
|
||||
keepChunksDistance: 1,
|
||||
multiplayerRenderDistance: 3,
|
||||
closeConfirmation: true,
|
||||
autoFullScreen: false,
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)',
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@
|
|||
.undo-keyboard,
|
||||
.undo-gamepad {
|
||||
aspect-ratio: 1;
|
||||
min-width: 20px;
|
||||
}
|
||||
|
||||
.button {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 ({
|
|||
}}
|
||||
/>
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ export default ({
|
|||
background: 'white',
|
||||
padding: '20px 18px',
|
||||
width: 300,
|
||||
height: 213,
|
||||
maxHeight: 240,
|
||||
color: 'black',
|
||||
// borderRadius: 8,
|
||||
}}
|
||||
|
|
|
|||
7
src/shims/prismarineAuthReplacement.ts
Normal file
7
src/shims/prismarineAuthReplacement.ts
Normal 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
|
||||
}
|
||||
|
|
@ -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
|
||||
})
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue