diff --git a/package.json b/package.json index fbc8ff69..33030ce1 100644 --- a/package.json +++ b/package.json @@ -86,7 +86,7 @@ "mojangson": "^2.0.4", "net-browserify": "github:zardoy/prismarinejs-net-browserify", "node-gzip": "^1.1.2", - "mcraft-fun-mineflayer": "^0.1.8", + "mcraft-fun-mineflayer": "^0.1.14", "peerjs": "^1.5.0", "pixelarticons": "^1.8.1", "pretty-bytes": "^6.1.1", @@ -151,7 +151,7 @@ "http-server": "^14.1.1", "https-browserify": "^1.0.0", "mc-assets": "^0.2.42", - "mineflayer-mouse": "^0.0.8", + "mineflayer-mouse": "^0.1.2", "minecraft-inventory-gui": "github:zardoy/minecraft-inventory-gui#next", "mineflayer": "github:zardoy/mineflayer", "mineflayer-pathfinder": "^2.4.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 71a81bc8..ba932db8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -135,8 +135,8 @@ importers: specifier: ^4.17.21 version: 4.17.21 mcraft-fun-mineflayer: - specifier: ^0.1.8 - version: 0.1.8(encoding@0.1.13)(mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/748163e536abe94f3dc8ada7a542bcd689bbbf49(encoding@0.1.13)) + specifier: ^0.1.14 + version: 0.1.14(encoding@0.1.13)(mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/06e3050ddf4d9aa655fea6e2bed182937a81705d(encoding@0.1.13)) minecraft-data: specifier: 3.83.1 version: 3.83.1 @@ -363,10 +363,10 @@ importers: version: 0.0.4 mineflayer: specifier: github:zardoy/mineflayer - version: https://codeload.github.com/zardoy/mineflayer/tar.gz/748163e536abe94f3dc8ada7a542bcd689bbbf49(encoding@0.1.13) + version: https://codeload.github.com/zardoy/mineflayer/tar.gz/06e3050ddf4d9aa655fea6e2bed182937a81705d(encoding@0.1.13) mineflayer-mouse: - specifier: ^0.0.8 - version: 0.0.8(@types/debug@4.1.12)(@types/node@22.8.1)(terser@5.31.3)(tsx@4.7.0)(yaml@2.4.1) + specifier: ^0.1.2 + version: 0.1.2(@types/debug@4.1.12)(@types/node@22.8.1)(terser@5.31.3)(tsx@4.7.0)(yaml@2.4.1) mineflayer-pathfinder: specifier: ^2.4.4 version: 2.4.4 @@ -4132,6 +4132,10 @@ packages: resolution: {integrity: sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA==} engines: {node: '>= 0.4'} + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + callsites@3.1.0: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} @@ -6693,9 +6697,9 @@ packages: resolution: {integrity: sha512-j2D1RNYtB5Z9gFu9MVjyDBbiALI0mWZ3xW/A3PPefVAHm3HJ2T1vH+1XBov1spBGPl7u+Zo7mRXza3X0egbeOg==} engines: {node: '>=18.0.0'} - mcraft-fun-mineflayer@0.1.8: - resolution: {integrity: sha512-jyJTihNHfeToBPwVs3QMKBlVcaCABJ25YN2eoIBQEVTRVFzaXh13XRpElphLzTMj1Q5XFYqufHtMoR4tsb08qQ==} - version: 0.1.8 + mcraft-fun-mineflayer@0.1.14: + resolution: {integrity: sha512-q/qXQaNbkGJIvXjRvudUT7/k0EsJgphFcvYjrSRWYyGDJeb61MKRVqq1hhMjqx7UK7FMfBKvjfPSxq/QlAP7WQ==} + version: 0.1.14 engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} peerDependencies: '@roamhq/wrtc': '*' @@ -6907,6 +6911,11 @@ packages: resolution: {integrity: sha512-5p+Dx1SIdQhkKA8Wbm7slN0MR6s7pdnlV2MVSBSmAlR4zW8+FVpsNJfvMQ4XltRqKYyHybNDZEdJocdtdkfhpQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + minecraft-protocol@https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/3bd4dc1b2002cd7badfa5b9cf8dda35cd6cc9ac1: + resolution: {tarball: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/3bd4dc1b2002cd7badfa5b9cf8dda35cd6cc9ac1} + version: 1.54.0 + engines: {node: '>=22'} + minecraft-protocol@https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/5ec3dd4b367fcc039fbcb3edd214fe3cf8178a6d: resolution: {tarball: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/5ec3dd4b367fcc039fbcb3edd214fe3cf8178a6d} version: 1.54.0 @@ -6923,8 +6932,8 @@ packages: resolution: {tarball: https://codeload.github.com/zardoy/mineflayer-item-map-downloader/tar.gz/a8d210ecdcf78dd082fa149a96e1612cc9747824} version: 1.2.0 - mineflayer-mouse@0.0.8: - resolution: {integrity: sha512-Y6TfclMjx7ndV+ISznJsQ4SMwzjhYvwvkj8kKBsQXx9/bG6fDtkgfnAroO/f2ppV52WNrtERQlVXYs35gHtIFg==} + mineflayer-mouse@0.1.2: + resolution: {integrity: sha512-QPGEXkF9PurZEpRq0xakKE8SV6sMY/6kCM9cdMeFbtq95IpYeh8ZJdD/twX2A3g3s8MooxlGovfxbpeHdWcOEQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} mineflayer-pathfinder@2.4.4: @@ -6934,8 +6943,8 @@ packages: resolution: {integrity: sha512-q7cmpZFaSI6sodcMJxc2GkV8IO84HbsUP+xNipGKfGg+FMISKabzdJ838Axb60qRtZrp6ny7LluQE7lesHvvxQ==} engines: {node: '>=18'} - mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/748163e536abe94f3dc8ada7a542bcd689bbbf49: - resolution: {tarball: https://codeload.github.com/zardoy/mineflayer/tar.gz/748163e536abe94f3dc8ada7a542bcd689bbbf49} + mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/06e3050ddf4d9aa655fea6e2bed182937a81705d: + resolution: {tarball: https://codeload.github.com/zardoy/mineflayer/tar.gz/06e3050ddf4d9aa655fea6e2bed182937a81705d} version: 4.25.0 engines: {node: '>=18'} @@ -13610,7 +13619,7 @@ snapshots: flatmap: 0.0.3 long: 5.2.3 minecraft-data: 3.83.1 - minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/5ec3dd4b367fcc039fbcb3edd214fe3cf8178a6d(patch_hash=dkeyukcqlupmk563gwxsmjr3yu)(encoding@0.1.13) + minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/3bd4dc1b2002cd7badfa5b9cf8dda35cd6cc9ac1(patch_hash=dkeyukcqlupmk563gwxsmjr3yu)(encoding@0.1.13) mkdirp: 2.1.6 node-gzip: 1.1.2 node-rsa: 1.1.1 @@ -14372,6 +14381,11 @@ snapshots: call-bind-apply-helpers: 1.0.2 get-intrinsic: 1.3.0 + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + callsites@3.1.0: {} camel-case@4.1.2: @@ -15431,7 +15445,7 @@ snapshots: es-iterator-helpers@1.2.1: dependencies: call-bind: 1.0.8 - call-bound: 1.0.3 + call-bound: 1.0.4 define-properties: 1.2.1 es-abstract: 1.23.9 es-errors: 1.3.0 @@ -17586,12 +17600,12 @@ snapshots: maxrects-packer: 2.7.3 zod: 3.24.1 - mcraft-fun-mineflayer@0.1.8(encoding@0.1.13)(mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/748163e536abe94f3dc8ada7a542bcd689bbbf49(encoding@0.1.13)): + mcraft-fun-mineflayer@0.1.14(encoding@0.1.13)(mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/06e3050ddf4d9aa655fea6e2bed182937a81705d(encoding@0.1.13)): dependencies: '@zardoy/flying-squid': 0.0.49(encoding@0.1.13) exit-hook: 2.2.1 - minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/5ec3dd4b367fcc039fbcb3edd214fe3cf8178a6d(patch_hash=dkeyukcqlupmk563gwxsmjr3yu)(encoding@0.1.13) - mineflayer: https://codeload.github.com/zardoy/mineflayer/tar.gz/748163e536abe94f3dc8ada7a542bcd689bbbf49(encoding@0.1.13) + minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/3bd4dc1b2002cd7badfa5b9cf8dda35cd6cc9ac1(patch_hash=dkeyukcqlupmk563gwxsmjr3yu)(encoding@0.1.13) + mineflayer: https://codeload.github.com/zardoy/mineflayer/tar.gz/06e3050ddf4d9aa655fea6e2bed182937a81705d(encoding@0.1.13) prismarine-item: 1.16.0 ws: 8.18.0 transitivePeerDependencies: @@ -17903,6 +17917,32 @@ snapshots: dependencies: vec3: 0.1.10 + minecraft-protocol@https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/3bd4dc1b2002cd7badfa5b9cf8dda35cd6cc9ac1(patch_hash=dkeyukcqlupmk563gwxsmjr3yu)(encoding@0.1.13): + dependencies: + '@types/node-rsa': 1.1.4 + '@types/readable-stream': 4.0.12 + aes-js: 3.1.2 + buffer-equal: 1.0.1 + debug: 4.4.0(supports-color@8.1.1) + endian-toggle: 0.0.0 + lodash.get: 4.4.2 + lodash.merge: 4.6.2 + minecraft-data: 3.83.1 + minecraft-folder-path: 1.2.0 + node-fetch: 2.7.0(encoding@0.1.13) + node-rsa: 0.4.2 + prismarine-auth: 2.4.2(encoding@0.1.13) + prismarine-chat: 1.10.1 + prismarine-nbt: 2.5.0 + prismarine-realms: 1.3.2(encoding@0.1.13) + protodef: 1.18.0 + readable-stream: 4.5.2 + uuid-1345: 1.0.2 + yggdrasil: 1.7.0(encoding@0.1.13) + transitivePeerDependencies: + - encoding + - supports-color + minecraft-protocol@https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/5ec3dd4b367fcc039fbcb3edd214fe3cf8178a6d(patch_hash=dkeyukcqlupmk563gwxsmjr3yu)(encoding@0.1.13): dependencies: '@types/node-rsa': 1.1.4 @@ -17960,10 +18000,11 @@ snapshots: - encoding - supports-color - mineflayer-mouse@0.0.8(@types/debug@4.1.12)(@types/node@22.8.1)(terser@5.31.3)(tsx@4.7.0)(yaml@2.4.1): + mineflayer-mouse@0.1.2(@types/debug@4.1.12)(@types/node@22.8.1)(terser@5.31.3)(tsx@4.7.0)(yaml@2.4.1): dependencies: change-case: 5.4.4 debug: 4.4.0(supports-color@8.1.1) + prismarine-item: 1.16.0 prismarine-world: https://codeload.github.com/zardoy/prismarine-world/tar.gz/ab2146c9933eef3247c3f64446de4ccc2c484c7c vitest: 3.0.7(@types/debug@4.1.12)(@types/node@22.8.1)(terser@5.31.3)(tsx@4.7.0)(yaml@2.4.1) transitivePeerDependencies: @@ -18020,7 +18061,7 @@ snapshots: - encoding - supports-color - mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/748163e536abe94f3dc8ada7a542bcd689bbbf49(encoding@0.1.13): + mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/06e3050ddf4d9aa655fea6e2bed182937a81705d(encoding@0.1.13): dependencies: minecraft-data: 3.83.1 minecraft-protocol: https://codeload.github.com/PrismarineJS/node-minecraft-protocol/tar.gz/5ec3dd4b367fcc039fbcb3edd214fe3cf8178a6d(patch_hash=dkeyukcqlupmk563gwxsmjr3yu)(encoding@0.1.13) diff --git a/renderer/viewer/lib/guiRenderer.ts b/renderer/viewer/lib/guiRenderer.ts index f4af3d20..d2987ce6 100644 --- a/renderer/viewer/lib/guiRenderer.ts +++ b/renderer/viewer/lib/guiRenderer.ts @@ -155,7 +155,7 @@ const generateItemsGui = async (models: Record, isIt return null }, getTextureUV (texture) { - return textureAtlas.getTextureUV(texture.toString().slice(1).split('/').slice(1).join('/') as any) + return textureAtlas.getTextureUV(texture.toString().replace('minecraft:', '').replace('block/', '').replace('item/', '').replace('blocks/', '').replace('items/', '') as any) }, getTextureAtlas () { return textureAtlas.getTextureAtlas() diff --git a/renderer/viewer/lib/ui/newStats.ts b/renderer/viewer/lib/ui/newStats.ts index fff4d28c..d18ad16c 100644 --- a/renderer/viewer/lib/ui/newStats.ts +++ b/renderer/viewer/lib/ui/newStats.ts @@ -3,7 +3,7 @@ const rightOffset = 0 const stats = {} -let lastY = 20 +let lastY = 40 export const addNewStat = (id: string, width = 80, x = rightOffset, y = lastY) => { const pane = document.createElement('div') pane.style.position = 'fixed' diff --git a/src/api/mcStatusApi.ts b/src/api/mcStatusApi.ts index 20d27813..8ac429dd 100644 --- a/src/api/mcStatusApi.ts +++ b/src/api/mcStatusApi.ts @@ -15,7 +15,7 @@ export const isServerValid = (ip: string) => { return !isInLocalNetwork && VALID_IP_OR_DOMAIN } -export async function fetchServerStatus (ip: string, signal?: AbortSignal) { +export async function fetchServerStatus (ip: string, signal?: AbortSignal, versionOverride?: string) { if (!isServerValid(ip)) return const response = await fetch(`https://api.mcstatus.io/v2/status/java/${ip}`, { signal }) @@ -25,7 +25,7 @@ export async function fetchServerStatus (ip: string, signal?: AbortSignal) { return { formattedText: data.motd?.raw ?? '', textNameRight: data.online ? - `${versionClean} ${data.players?.online ?? '??'}/${data.players?.max ?? '??'}` : + `${versionOverride ?? versionClean} ${data.players?.online ?? '??'}/${data.players?.max ?? '??'}` : '', icon: data.icon, offline: !data.online, diff --git a/src/appConfig.ts b/src/appConfig.ts index 3d6d8f93..b8f83ad1 100644 --- a/src/appConfig.ts +++ b/src/appConfig.ts @@ -1,6 +1,7 @@ import { disabledSettings, options, qsOptions } from './optionsStorage' import { miscUiState } from './globalState' import { setLoadingScreenStatus } from './appStatus' +import { setStorageDataOnAppConfigLoad } from './react/appStorageProvider' export type AppConfig = { // defaultHost?: string @@ -42,6 +43,8 @@ export const loadAppConfig = (appConfig: AppConfig) => { } } } + + setStorageDataOnAppConfigLoad() } export const isBundledConfigUsed = !!process.env.INLINED_APP_CONFIG diff --git a/src/appParams.ts b/src/appParams.ts index 98d6ff62..b550ea02 100644 --- a/src/appParams.ts +++ b/src/appParams.ts @@ -1,4 +1,5 @@ import type { AppConfig } from './appConfig' +import { miscUiState } from './globalState' const qsParams = new URLSearchParams(window.location?.search ?? '') @@ -73,7 +74,7 @@ export const appQueryParams = new Proxy({} as AppQsParams, { } const qsParam = qsParams.get(property) if (qsParam) return qsParam - return initialAppConfig.appParams?.[property] + return miscUiState.appConfig?.appParams?.[property] }, }) @@ -84,7 +85,7 @@ export const appQueryParamsArray = new Proxy({} as AppQsParamsArrayTransformed, } const qsParam = qsParams.getAll(property) if (qsParam.length) return qsParam - return initialAppConfig.appParams?.[property] ?? [] + return miscUiState.appConfig?.appParams?.[property] ?? [] }, }) diff --git a/src/browserfs.ts b/src/browserfs.ts index 0f4579b8..9fb0771f 100644 --- a/src/browserfs.ts +++ b/src/browserfs.ts @@ -15,6 +15,7 @@ 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' const { GoogleDriveFileSystem } = require('google-drive-browserfs/src/backends/GoogleDrive') browserfs.install(window) @@ -620,24 +621,13 @@ export const openWorldZip = async (...args: Parameters } } -export const resetLocalStorageWorld = () => { - for (const key of Object.keys(localStorage)) { - if (/^[\da-fA-F]{8}(?:\b-[\da-fA-F]{4}){3}\b-[\da-fA-F]{12}$/g.test(key) || key === '/') { - localStorage.removeItem(key) - } - } -} - -export const resetLocalStorageWithoutWorld = () => { - for (const key of Object.keys(localStorage)) { - if (!/^[\da-fA-F]{8}(?:\b-[\da-fA-F]{4}){3}\b-[\da-fA-F]{12}$/g.test(key) && key !== '/') { - localStorage.removeItem(key) - } - } +export const resetLocalStorage = () => { resetOptions() + resetAppStorage() } -window.resetLocalStorageWorld = resetLocalStorageWorld +window.resetLocalStorage = resetLocalStorage + export const openFilePicker = (specificCase?: 'resourcepack') => { // create and show input picker let picker: HTMLInputElement = document.body.querySelector('input#file-zip-picker')! diff --git a/src/builtinCommands.ts b/src/builtinCommands.ts index 4bc21a73..278ff57f 100644 --- a/src/builtinCommands.ts +++ b/src/builtinCommands.ts @@ -79,8 +79,9 @@ const writeText = (text) => { displayClientChat(text) } -const commands: Array<{ +export const commands: Array<{ command: string[], + alwaysAvailable?: boolean, invoke (args: string[]): Promise | void //@ts-format-ignore-region }> = [ @@ -111,6 +112,7 @@ const commands: Array<{ }, { command: ['/pos'], + alwaysAvailable: true, async invoke ([type]) { let pos: { x: number, y: number, z: number } | undefined if (type === 'block') { @@ -131,13 +133,12 @@ const commands: Array<{ ] //@ts-format-ignore-endregion -export const getBuiltinCommandsList = () => commands.flatMap(command => command.command) +export const getBuiltinCommandsList = () => commands.filter(command => command.alwaysAvailable || localServer).flatMap(command => command.command) export const tryHandleBuiltinCommand = (message: string) => { - if (!localServer) return const [userCommand, ...args] = message.split(' ') - for (const command of commands) { + for (const command of commands.filter(command => command.alwaysAvailable || localServer)) { if (command.command.includes(userCommand)) { void command.invoke(args) // ignoring for now return true diff --git a/src/controls.ts b/src/controls.ts index a3a54ffb..98d32062 100644 --- a/src/controls.ts +++ b/src/controls.ts @@ -8,6 +8,7 @@ import { ControMax } from 'contro-max/build/controMax' import { CommandEventArgument, SchemaCommandInput } from 'contro-max/build/types' import { stringStartsWith } from 'contro-max/build/stringUtils' import { UserOverrideCommand, UserOverridesConfig } from 'contro-max/build/types/store' +import { GameMode } from 'mineflayer' import { isGameActive, showModal, gameAdditionalState, activeModalStack, hideCurrentModal, miscUiState, hideModal, hideAllModals } from './globalState' import { goFullscreen, isInRealGameSession, pointerLock, reloadChunks } from './utils' import { options } from './optionsStorage' @@ -25,11 +26,13 @@ import { showNotification } from './react/NotificationProvider' import { lastConnectOptions } from './react/AppStatusProvider' import { onCameraMove, onControInit } from './cameraRotationControls' import { createNotificationProgressReporter } from './core/progressReporter' +import { appStorage } from './react/appStorageProvider' +import { switchGameMode } from './packetsReplay/replayPackets' -export const customKeymaps = proxy(JSON.parse(localStorage.keymap || '{}')) as UserOverridesConfig +export const customKeymaps = proxy(appStorage.keybindings) subscribe(customKeymaps, () => { - localStorage.keymap = JSON.stringify(customKeymaps) + appStorage.keybindings = customKeymaps }) const controlOptions = { @@ -636,29 +639,35 @@ export const f3Keybinds: Array<{ { key: 'F4', async action () { + let nextGameMode: GameMode switch (bot.game.gameMode) { case 'creative': { - bot.chat('/gamemode survival') + nextGameMode = 'survival' break } case 'survival': { - bot.chat('/gamemode adventure') + nextGameMode = 'adventure' break } case 'adventure': { - bot.chat('/gamemode spectator') + nextGameMode = 'spectator' break } case 'spectator': { - bot.chat('/gamemode creative') + nextGameMode = 'creative' break } // No default } + if (lastConnectOptions.value?.worldStateFileContents) { + switchGameMode(nextGameMode) + } else { + bot.chat(`/gamemode ${nextGameMode}`) + } }, mobileTitle: 'Cycle Game Mode' }, @@ -809,15 +818,16 @@ let allowFlying = false export const onBotCreate = () => { let wasSpectatorFlying = false bot._client.on('abilities', ({ flags }) => { + allowFlying = !!(flags & 4) if (flags & 2) { // flying toggleFly(true, false) } else { toggleFly(false, false) } - allowFlying = !!(flags & 4) }) const gamemodeCheck = () => { if (bot.game.gameMode === 'spectator') { + allowFlying = true toggleFly(true, false) wasSpectatorFlying = true } else if (wasSpectatorFlying) { diff --git a/src/downloadAndOpenFile.ts b/src/downloadAndOpenFile.ts index 870a70b1..78cd6984 100644 --- a/src/downloadAndOpenFile.ts +++ b/src/downloadAndOpenFile.ts @@ -13,13 +13,43 @@ export const getFixedFilesize = (bytes: number) => { const inner = async () => { const { replayFileUrl } = appQueryParams if (replayFileUrl) { - setLoadingScreenStatus('Downloading replay file...') + setLoadingScreenStatus('Downloading replay file') const response = await fetch(replayFileUrl) const contentLength = response.headers?.get('Content-Length') const size = contentLength ? +contentLength : undefined const filename = replayFileUrl.split('/').pop() - const contents = await response.text() + let downloadedBytes = 0 + const buffer = await new Response(new ReadableStream({ + async start (controller) { + if (!response.body) throw new Error('Server returned no response!') + const reader = response.body.getReader() + + // eslint-disable-next-line no-constant-condition + while (true) { + const { done, value } = await reader.read() + + if (done) { + controller.close() + break + } + + downloadedBytes += value.byteLength + + // Calculate download progress as a percentage + const progress = size ? (downloadedBytes / size) * 100 : undefined + setLoadingScreenStatus(`Download replay file progress: ${progress === undefined ? '?' : Math.floor(progress)}% (${getFixedFilesize(downloadedBytes)} / ${size && getFixedFilesize(size)})`, false, true) + + // Pass the received data to the controller + controller.enqueue(value) + } + }, + })).arrayBuffer() + + // Convert buffer to text, handling any compression automatically + const decoder = new TextDecoder() + const contents = decoder.decode(buffer) + openFile({ contents, filename, diff --git a/src/globalState.ts b/src/globalState.ts index 0ee8671d..2d77b720 100644 --- a/src/globalState.ts +++ b/src/globalState.ts @@ -3,17 +3,12 @@ import { proxy, ref, subscribe } from 'valtio' import type { WorldWarp } from 'flying-squid/dist/lib/modules/warps' import type { OptionsGroupType } from './optionsGuiScheme' -import { appQueryParams } from './appParams' import { options, disabledSettings } from './optionsStorage' import { AppConfig } from './appConfig' // todo: refactor structure with support of hideNext=false -const notHideableModalsWithoutForce = new Set(['app-status']) - -if (appQueryParams.lockConnect) { - notHideableModalsWithoutForce.add('editServer') -} +export const notHideableModalsWithoutForce = new Set(['app-status']) type Modal = ({ elem?: HTMLElement & Record } & { reactType: string }) diff --git a/src/globals.d.ts b/src/globals.d.ts index 6b2c6640..1dfb0255 100644 --- a/src/globals.d.ts +++ b/src/globals.d.ts @@ -26,7 +26,7 @@ declare const customEvents: import('typed-emitter').default<{ mineflayerBotCreated (): void search (q: string): void activateItem (item: Item, slot: number, offhand: boolean): void - hurtAnimation (): void + hurtAnimation (yaw?: number): void }> declare const beforeRenderFrame: Array<() => void> diff --git a/src/index.ts b/src/index.ts index 812b8c51..45593447 100644 --- a/src/index.ts +++ b/src/index.ts @@ -363,6 +363,7 @@ export async function connect (connectOptions: ConnectOptions) { if (miscUiState.gameLoaded) return setLoadingScreenStatus(`Error encountered. ${err}`, true) + appStatusState.showReconnect = true onPossibleErrorDisconnect() destroyAll() } @@ -712,6 +713,7 @@ export async function connect (connectOptions: ConnectOptions) { console.log('You were kicked!', kickReason) const { formatted: kickReasonFormatted, plain: kickReasonString } = parseFormattedMessagePacket(kickReason) setLoadingScreenStatus(`The Minecraft server kicked you. Kick reason: ${kickReasonString}`, true, undefined, undefined, kickReasonFormatted) + appStatusState.showReconnect = true destroyAll() }) @@ -779,11 +781,13 @@ export async function connect (connectOptions: ConnectOptions) { setLoadingScreenStatus('Placing blocks (starting viewer)') if (!connectOptions.worldStateFileContents || connectOptions.worldStateFileContents.length < 3 * 1024 * 1024) { localStorage.lastConnectOptions = JSON.stringify(connectOptions) + if (process.env.NODE_ENV === 'development' && !localStorage.lockUrl && !Object.keys(window.debugQueryParams).length) { + lockUrl() + } + } else { + localStorage.removeItem('lastConnectOptions') } connectOptions.onSuccessfulPlay?.() - if (process.env.NODE_ENV === 'development' && !localStorage.lockUrl && !Object.keys(window.debugQueryParams).length) { - lockUrl() - } updateDataAfterJoin() if (connectOptions.autoLoginPassword) { bot.chat(`/login ${connectOptions.autoLoginPassword}`) @@ -827,6 +831,7 @@ export async function connect (connectOptions: ConnectOptions) { if (appStatusState.isError) return const waitForChunks = async () => { + if (appQueryParams.sp === '1') return //todo const waitForChunks = options.waitForChunksRender === 'sp-only' ? !!singleplayer : options.waitForChunksRender if (viewer.world.allChunksFinished || !waitForChunks) { return @@ -894,8 +899,8 @@ export async function connect (connectOptions: ConnectOptions) { const reconnectOptions = sessionStorage.getItem('reconnectOptions') ? JSON.parse(sessionStorage.getItem('reconnectOptions')!) : undefined listenGlobalEvents() -const unsubscribe = watchValue(miscUiState, async s => { - if (s.fsReady && s.appConfig) { +const unsubscribe = subscribe(miscUiState, async () => { + if (miscUiState.fsReady && miscUiState.appConfig) { unsubscribe() if (reconnectOptions) { sessionStorage.removeItem('reconnectOptions') diff --git a/src/inventoryWindows.ts b/src/inventoryWindows.ts index 6a5ab0d6..303e91be 100644 --- a/src/inventoryWindows.ts +++ b/src/inventoryWindows.ts @@ -37,6 +37,10 @@ export const allImagesLoadedState = proxy({ value: false }) +export const jeiCustomCategories = proxy({ + value: [] as Array<{ id: string, categoryTitle: string, items: any[] }> +}) + export const onGameLoad = (onLoad) => { allImagesLoadedState.value = false version = bot.version @@ -254,12 +258,25 @@ const getItemName = (slot: Item | RenderItem | null) => { return text.join('') } +let lastMappedSots = [] as any[] +const itemToVisualKey = (slot: RenderItem | Item | null) => { + if (!slot) return null + return slot.name + (slot['metadata'] ?? '-') + (slot.nbt ? JSON.stringify(slot.nbt) : '') + (slot['components'] ? JSON.stringify(slot['components']) : '') +} const mapSlots = (slots: Array, isJei = false) => { - return slots.map((slot, i) => { + const newSlots = slots.map((slot, i) => { // todo stateid if (!slot) return + if (!isJei) { + const oldKey = itemToVisualKey(lastMappedSots[i]) + if (oldKey && oldKey === itemToVisualKey(slot)) { + return lastMappedSots[i] + } + } + try { + if (slot.durabilityUsed && slot.maxDurability) slot.durabilityUsed = Math.min(slot.durabilityUsed, slot.maxDurability) const debugIsQuickbar = !isJei && i === bot.inventory.hotbarStart + bot.quickBarSlot const modelName = getItemModelName(slot, { 'minecraft:display_context': 'gui', }) const slotCustomProps = renderSlot({ modelName }, debugIsQuickbar) @@ -277,6 +294,8 @@ const mapSlots = (slots: Array, isJei = false) => { } return slot }) + lastMappedSots = newSlots + return newSlots } export const upInventoryItems = (isInventory: boolean, invWindow = lastWindow) => { @@ -324,9 +343,14 @@ const implementedContainersGuiMap = { const upJei = (search: string) => { search = search.toLowerCase() // todo fix pre flat - const matchedSlots = loadedData.itemsArray.map(x => { + const itemsArray = [ + ...jeiCustomCategories.value.flatMap(x => x.items).filter(x => x !== null), + ...loadedData.itemsArray.filter(x => x.displayName.toLowerCase().includes(search)).map(item => new PrismarineItem(item.id, 1)).filter(x => x !== null) + ] + const matchedSlots = itemsArray.map(x => { + x.displayName = getItemName(x) ?? x.displayName if (!x.displayName.toLowerCase().includes(search)) return null - return new PrismarineItem(x.id, 1) + return x }).filter(a => a !== null) lastWindow.pwindow.win.jeiSlotsPage = 0 lastWindow.pwindow.win.jeiSlots = mapSlots(matchedSlots, true) @@ -344,7 +368,7 @@ export const openItemsCanvas = (type, _bot = bot as typeof bot | null) => { return [...allRecipes ?? [], ...itemDescription ? [ [ 'GenericDescription', - mapSlots([item])[0], + mapSlots([item], true)[0], [], itemDescription ] @@ -448,26 +472,43 @@ const openWindow = (type: string | undefined) => { inGameError(`Item for block ${slotItem.name} not found`) return } - const item = new PrismarineItem(itemId, isRightclick ? 64 : 1, slotItem.metadata) + const item = PrismarineItem.fromNotch({ + ...slotItem, + itemId, + itemCount: isRightclick ? 64 : 1, + components: slotItem.components ?? [], + removeComponents: slotItem.removedComponents ?? [], + itemDamage: slotItem.metadata ?? 0, + nbt: slotItem.nbt, + }) if (bot.game.gameMode === 'creative') { const freeSlot = bot.inventory.firstEmptyInventorySlot() if (freeSlot === null) return void bot.creative.setInventorySlot(freeSlot, item) } else { - inv.canvasManager.children[0].showRecipesOrUsages(!isRightclick, mapSlots([item])[0]) + inv.canvasManager.children[0].showRecipesOrUsages(!isRightclick, mapSlots([item], true)[0]) } } - // if (bot.game.gameMode !== 'spectator') { - lastWindow.pwindow.win.jeiSlotsPage = 0 - // todo workaround so inventory opens immediately (though it still lags) - setTimeout(() => { - upJei('') - }) - miscUiState.displaySearchInput = true - // } else { - // lastWindow.pwindow.win.jeiSlots = [] - // } + const isJeiEnabled = () => { + if (typeof options.jeiEnabled === 'boolean') return options.jeiEnabled + if (Array.isArray(options.jeiEnabled)) { + return options.jeiEnabled.includes(bot.game?.gameMode as any) + } + return false + } + + if (isJeiEnabled()) { + lastWindow.pwindow.win.jeiSlotsPage = 0 + // todo workaround so inventory opens immediately (though it still lags) + setTimeout(() => { + upJei('') + }) + miscUiState.displaySearchInput = true + } else { + lastWindow.pwindow.win.jeiSlots = [] + miscUiState.displaySearchInput = false + } if (type === undefined) { // player inventory @@ -574,8 +615,8 @@ const getAllItemRecipes = (itemName: string) => { return results.map(({ result, ingredients, description }) => { return [ 'CraftingTableGuide', - mapSlots([result])[0], - mapSlots(ingredients), + mapSlots([result], true)[0], + mapSlots(ingredients, true), description ] }) diff --git a/src/mineflayer/cameraShake.ts b/src/mineflayer/cameraShake.ts index 043eb207..b2c7f273 100644 --- a/src/mineflayer/cameraShake.ts +++ b/src/mineflayer/cameraShake.ts @@ -10,10 +10,10 @@ class CameraShake { this.rollAngle = 0 } - shakeFromDamage () { + shakeFromDamage (yaw?: number) { // Add roll animation const startRoll = this.rollAngle - const targetRoll = startRoll + (Math.random() < 0.5 ? -1 : 1) * this.damageRollAmount + const targetRoll = startRoll + (yaw ?? (Math.random() < 0.5 ? -1 : 1)) * this.damageRollAmount this.rollAnimation = { startTime: performance.now(), @@ -85,12 +85,14 @@ customEvents.on('mineflayerBotCreated', () => { }) } - customEvents.on('hurtAnimation', () => { - cameraShake.shakeFromDamage() + customEvents.on('hurtAnimation', (yaw) => { + cameraShake.shakeFromDamage(yaw) }) - bot._client.on('hurt_animation', () => { - customEvents.emit('hurtAnimation') + bot._client.on('hurt_animation', ({ entityId, yaw }) => { + if (entityId === bot.entity.id) { + customEvents.emit('hurtAnimation', yaw) + } }) bot.on('entityHurt', ({ id }) => { if (id === bot.entity.id) { diff --git a/src/mineflayer/items.ts b/src/mineflayer/items.ts index bb437ef4..3c2134f1 100644 --- a/src/mineflayer/items.ts +++ b/src/mineflayer/items.ts @@ -39,7 +39,7 @@ export const getItemMetadata = (item: GeneralInputItem) => { const customTextComponent = componentMap.get('custom_name') || componentMap.get('item_name') if (customTextComponent) { - customText = nbt.simplify(customTextComponent.data) + customText = typeof customTextComponent.data === 'string' ? customTextComponent.data : nbt.simplify(customTextComponent.data) } const customModelComponent = componentMap.get('item_model') if (customModelComponent) { diff --git a/src/mineflayer/plugins/packetsRecording.ts b/src/mineflayer/plugins/packetsRecording.ts index f1ff18cf..53a63bd8 100644 --- a/src/mineflayer/plugins/packetsRecording.ts +++ b/src/mineflayer/plugins/packetsRecording.ts @@ -26,20 +26,39 @@ export const localRelayServerPlugin = (bot: Bot) => { }) ) - bot.downloadCurrentWorldState = () => { - const worldState = bot.webViewer._unstable.createStateCaptureFile() + const downloadFile = (contents: string, filename: string) => { const a = document.createElement('a') - const textContents = worldState.contents - const blob = new Blob([textContents], { type: 'text/plain' }) + const blob = new Blob([contents], { type: 'text/plain' }) const url = URL.createObjectURL(blob) a.href = url - // add readable timestamp to filename - const timestamp = new Date().toISOString().replaceAll(/[-:Z]/g, '') - a.download = `${bot.username}-world-state-${timestamp}.${WORLD_STATE_FILE_EXTENSION}` + a.download = filename a.click() URL.revokeObjectURL(url) } + bot.downloadCurrentWorldState = () => { + const worldState = bot.webViewer._unstable.createStateCaptureFile() + // add readable timestamp to filename + const timestamp = new Date().toISOString().replaceAll(/[-:Z]/g, '') + downloadFile(worldState.contents, `${bot.username}-world-state-${timestamp}.${WORLD_STATE_FILE_EXTENSION}`) + } + + let logger: PacketsLogger | undefined + bot.startPacketsRecording = () => { + bot.webViewer._unstable.startRecording((l) => { + logger = l + }) + } + + bot.stopPacketsRecording = () => { + if (!logger) return + const packets = logger?.contents + logger = undefined + const timestamp = new Date().toISOString().replaceAll(/[-:Z]/g, '') + downloadFile(packets, `${bot.username}-packets-${timestamp}.${PACKETS_REPLAY_FILE_EXTENSION}`) + bot.webViewer._unstable.stopRecording() + } + circularBuffer = new CircularBuffer(AUTO_CAPTURE_PACKETS_COUNT) let position = 0 bot._client.on('writePacket' as any, (name, params) => { @@ -87,12 +106,16 @@ subscribe(packetsRecordingState, () => { declare module 'mineflayer' { interface Bot { downloadCurrentWorldState: () => void + startPacketsRecording: () => void + stopPacketsRecording: () => void } } export const getLastAutoCapturedPackets = () => circularBuffer?.size export const downloadAutoCapturedPackets = () => { const logger = new PacketsLogger({ minecraftVersion: lastConnectVersion }) + logger.relativeTime = false + logger.formattedTime = true for (const packet of circularBuffer?.getLastElements() ?? []) { logger.log(packet.isFromServer, { name: packet.name, state: packet.state, time: packet.timestamp }, packet.params) } diff --git a/src/optionsGuiScheme.tsx b/src/optionsGuiScheme.tsx index 7032e644..ac0503ca 100644 --- a/src/optionsGuiScheme.tsx +++ b/src/optionsGuiScheme.tsx @@ -2,20 +2,23 @@ import { useRef, useState } from 'react' import { useSnapshot } from 'valtio' import { openURL } from 'renderer/viewer/lib/simpleUtils' import { noCase } from 'change-case' +import { versionToNumber } from 'mc-assets/dist/utils' import { gameAdditionalState, miscUiState, openOptionsMenu, showModal } from './globalState' -import { AppOptions, options } from './optionsStorage' +import { AppOptions, getChangedSettings, options, resetOptions } from './optionsStorage' import Button from './react/Button' import { OptionMeta, OptionSlider } from './react/OptionsItems' import Slider from './react/Slider' import { getScreenRefreshRate } from './utils' import { setLoadingScreenStatus } from './appStatus' -import { openFilePicker, resetLocalStorageWithoutWorld } from './browserfs' +import { openFilePicker, resetLocalStorage } from './browserfs' import { completeResourcepackPackInstall, getResourcePackNames, resourcePackState, uninstallResourcePack } from './resourcePack' import { downloadPacketsReplay, packetsRecordingState } from './packetsReplay/packetsReplayLegacy' -import { showOptionsModal } from './react/SelectOption' +import { showInputsModal, showOptionsModal } from './react/SelectOption' import supportedVersions from './supportedVersions.mjs' import { getVersionAutoSelect } from './connect' import { createNotificationProgressReporter } from './core/progressReporter' +import { customKeymaps } from './controls' +import { appStorage } from './react/appStorageProvider' export const guiOptionsScheme: { [t in OptionsGroupType]: Array<{ [K in keyof AppOptions]?: Partial> } & { custom? }> @@ -450,9 +453,19 @@ export const guiOptionsScheme: { return + >Reset settings + }, + }, + { + custom () { + return }, }, { @@ -460,6 +473,11 @@ export const guiOptionsScheme: { return Developer }, }, + { + custom () { + return + } + }, + { + custom () { + return + } + }, + { + custom () { + return + } + }, + { + custom () { + return + } + } + ], } -export type OptionsGroupType = 'main' | 'render' | 'interface' | 'controls' | 'sound' | 'advanced' | 'VR' +export type OptionsGroupType = 'main' | 'render' | 'interface' | 'controls' | 'sound' | 'advanced' | 'VR' | 'export-import' const Category = ({ children }) =>
, // antiAliasing: false, @@ -163,12 +162,31 @@ const migrateOptions = (options: Partial>) => { export type AppOptions = typeof defaultOptions -// when opening html file locally in browser, localStorage is shared between all ever opened html files, so we try to avoid conflicts -const localStorageKey = process.env.SINGLE_FILE_BUILD ? 'minecraftWebClientOptions' : 'options' +const isDeepEqual = (a: any, b: any): boolean => { + if (a === b) return true + if (typeof a !== typeof b) return false + if (typeof a !== 'object') return false + if (a === null || b === null) return a === b + if (Array.isArray(a) && Array.isArray(b)) { + if (a.length !== b.length) return false + return a.every((item, index) => isDeepEqual(item, b[index])) + } + const keysA = Object.keys(a) + const keysB = Object.keys(b) + if (keysA.length !== keysB.length) return false + return keysA.every(key => isDeepEqual(a[key], b[key])) +} + +export const getChangedSettings = () => { + return Object.fromEntries( + Object.entries(options).filter(([key, value]) => !isDeepEqual(defaultOptions[key], value)) + ) +} + export const options: AppOptions = proxy({ ...defaultOptions, ...initialAppConfig.defaultSettings, - ...migrateOptions(JSON.parse(localStorage[localStorageKey] || '{}')), + ...migrateOptions(appStorage.options), ...qsOptions }) @@ -180,14 +198,14 @@ export const resetOptions = () => { Object.defineProperty(window, 'debugChangedOptions', { get () { - return Object.fromEntries(Object.entries(options).filter(([key, v]) => defaultOptions[key] !== v)) + return getChangedSettings() }, }) subscribe(options, () => { // Don't save disabled settings to localStorage const saveOptions = omitObj(options, [...disabledSettings.value] as any) - localStorage[localStorageKey] = JSON.stringify(saveOptions) + appStorage.options = saveOptions }) type WatchValue = >(proxy: T, callback: (p: T, isChanged: boolean) => void) => () => void diff --git a/src/packetsReplay/replayPackets.ts b/src/packetsReplay/replayPackets.ts index 9e777b38..f1c85a94 100644 --- a/src/packetsReplay/replayPackets.ts +++ b/src/packetsReplay/replayPackets.ts @@ -3,6 +3,7 @@ import { createServer, ServerClient } from 'minecraft-protocol' import { ParsedReplayPacket, parseReplayContents } from 'mcraft-fun-mineflayer/build/packetsLogger' import { PACKETS_REPLAY_FILE_EXTENSION, WORLD_STATE_FILE_EXTENSION } from 'mcraft-fun-mineflayer/build/worldState' import MinecraftData from 'minecraft-data' +import { GameMode } from 'mineflayer' import { LocalServer } from '../customServer' import { UserError } from '../mineflayer/userError' import { packetsReplayState } from '../react/state/packetsReplayState' @@ -188,6 +189,10 @@ const mainPacketsReplayer = async (client: ServerClient, packets: ParsedReplayPa } if (packet.isFromServer) { + if (packet.params === null) { + console.warn('packet.params is null', packet) + continue + } playServerPacket(packet.name, packet.params) await new Promise(resolve => { setTimeout(resolve, packet.diff * packetsReplayState.speed + ADDITIONAL_DELAY * (packetsReplayState.customButtons.packetsSenderDelay.state ? 1 : 0)) @@ -216,6 +221,7 @@ const mainPacketsReplayer = async (client: ServerClient, packets: ParsedReplayPa setTimeout(resolve, 1000) })] : []) ]) + clientsPacketsWaiter.stopWaiting() clientPackets = [] } } @@ -226,6 +232,25 @@ const mainPacketsReplayer = async (client: ServerClient, packets: ParsedReplayPa } } +export const switchGameMode = (gameMode: GameMode) => { + const gamemodes = { + survival: 0, + creative: 1, + adventure: 2, + spectator: 3 + } + if (gameMode === 'spectator') { + bot._client.emit('abilities', { + // can fly + is flying + flags: 6 + }) + } + bot._client.emit('game_state_change', { + reason: 3, + gameMode: gamemodes[gameMode] + }) +} + interface PacketsWaiterOptions { unexpectedPacketReceived?: (name: string, params: any) => void expectedPacketReceived?: (name: string, params: any) => void @@ -236,6 +261,7 @@ interface PacketsWaiterOptions { interface PacketsWaiter { addPacket(name: string, params: any): void waitForPackets(packets: string[]): Promise + stopWaiting(): void } const createPacketsWaiter = (options: PacketsWaiterOptions = {}): PacketsWaiter => { @@ -296,6 +322,11 @@ const createPacketsWaiter = (options: PacketsWaiterOptions = {}): PacketsWaiter isWaiting = false packetHandler = null } + }, + stopWaiting () { + isWaiting = false + packetHandler = null + queuedPackets.length = 0 } } } diff --git a/src/panorama.ts b/src/panorama.ts index 65678a6a..7fdd72b7 100644 --- a/src/panorama.ts +++ b/src/panorama.ts @@ -13,7 +13,7 @@ import { miscUiState } from './globalState' import { loadMinecraftData } from './connect' let panoramaCubeMap -let shouldDisplayPanorama = false +let shouldDisplayPanorama = true const panoramaFiles = [ 'panorama_3.png', // right (+x) @@ -34,13 +34,12 @@ export async function addPanoramaCubeMap () { setTimeout(resolve, 0) // wait for viewer to be initialized }) viewer.camera.fov = 85 + if (!shouldDisplayPanorama) return if (process.env.SINGLE_FILE_BUILD_MODE) { void initDemoWorld() return } - shouldDisplayPanorama = true - let time = 0 viewer.camera.near = 0.05 viewer.camera.updateProjectionMatrix() @@ -102,12 +101,16 @@ export async function addPanoramaCubeMap () { panoramaCubeMap = group } -subscribeKey(miscUiState, 'fsReady', () => { - if (miscUiState.fsReady) { - // don't do it earlier to load fs and display menu faster - void addPanoramaCubeMap() - } -}) +if (process.env.SINGLE_FILE_BUILD_MODE) { + subscribeKey(miscUiState, 'fsReady', () => { + if (miscUiState.fsReady) { + // don't do it earlier to load fs and display menu faster + void addPanoramaCubeMap() + } + }) +} else { + void addPanoramaCubeMap() +} export function removePanorama () { for (const unloadPanoramaCallback of unloadPanoramaCallbacks) { diff --git a/src/react/AddServerOrConnect.tsx b/src/react/AddServerOrConnect.tsx index 1186fd9a..08ef7f29 100644 --- a/src/react/AddServerOrConnect.tsx +++ b/src/react/AddServerOrConnect.tsx @@ -3,7 +3,7 @@ import { appQueryParams } from '../appParams' import { fetchServerStatus, isServerValid } from '../api/mcStatusApi' import { parseServerAddress } from '../parseServerAddress' import Screen from './Screen' -import Input from './Input' +import Input, { INPUT_LABEL_WIDTH, InputWithLabel } from './Input' import Button from './Button' import SelectGameVersion from './SelectGameVersion' import { usePassesScaledDimensions } from './UIProvider' @@ -32,8 +32,6 @@ interface Props { allowAutoConnect?: boolean } -const ELEMENTS_WIDTH = 190 - export default ({ onBack, onConfirm, title = 'Add a Server', initialData, parseQs, onQsConnect, placeholders, accounts, versions, allowAutoConnect }: Props) => { const isSmallHeight = !usePassesScaledDimensions(null, 350) const qsParamName = parseQs ? appQueryParams.name : undefined @@ -159,6 +157,7 @@ export default ({ onBack, onConfirm, title = 'Add a Server', initialData, parseQ { @@ -256,20 +255,8 @@ export default ({ onBack, onConfirm, title = 'Add a Server', initialData, parseQ const ButtonWrapper = ({ ...props }: React.ComponentProps) => { props.style ??= {} - props.style.width = ELEMENTS_WIDTH + props.style.width = INPUT_LABEL_WIDTH return } {actionsSlot} {!lockConnect && backAction &&
diff --git a/src/react/ChatProvider.tsx b/src/react/ChatProvider.tsx index e261c086..cf849df7 100644 --- a/src/react/ChatProvider.tsx +++ b/src/react/ChatProvider.tsx @@ -88,7 +88,7 @@ export default () => { // normalize items = items.map(item => `/${item}`) } - if (localServer) { + if (items.length) { items = [...items, ...getBuiltinCommandsList()] } } diff --git a/src/react/HudBarsProvider.tsx b/src/react/HudBarsProvider.tsx index ea78ae08..db061500 100644 --- a/src/react/HudBarsProvider.tsx +++ b/src/react/HudBarsProvider.tsx @@ -90,7 +90,7 @@ export default () => { upArmour() }, []) - return
+ return
, 'width'> { width?: number } -export default ({ autoFocus, rootStyles, inputRef, validateInput, defaultValue, width, ...inputProps }: Props) => { +const Input = ({ autoFocus, rootStyles, inputRef, validateInput, defaultValue, width, ...inputProps }: Props) => { if (width) rootStyles = { ...rootStyles, width } const ref = useRef(null!) @@ -51,3 +51,19 @@ export default ({ autoFocus, rootStyles, inputRef, validateInput, defaultValue, />
} + +export default Input + +export const INPUT_LABEL_WIDTH = 190 + +export const InputWithLabel = ({ label, span, ...props }: React.ComponentProps & { label, span? }) => { + return
+ + +
+} diff --git a/src/react/MainMenu.tsx b/src/react/MainMenu.tsx index 09214af2..85c5367e 100644 --- a/src/react/MainMenu.tsx +++ b/src/react/MainMenu.tsx @@ -7,6 +7,7 @@ import Button from './Button' import ButtonWithTooltip from './ButtonWithTooltip' import { pixelartIcons } from './PixelartIcon' import useLongPress from './useLongPress' +import PauseLinkButtons from './PauseLinkButtons' type Action = (e: React.MouseEvent) => void @@ -15,7 +16,6 @@ interface Props { singleplayerAction?: Action optionsAction?: Action githubAction?: Action - linksButton?: JSX.Element openFileAction?: Action mapsProvider?: string versionStatus?: string @@ -35,7 +35,6 @@ export default ({ singleplayerAction, optionsAction, githubAction, - linksButton, openFileAction, versionText, onVersionTextClick, @@ -62,8 +61,9 @@ export default ({ const versionLongPress = useLongPress( () => { - const buildDate = process.env.BUILD_VERSION ? new Date(process.env.BUILD_VERSION) : null - alert(`BUILD INFO:\n${buildDate?.toLocaleString() || 'Development build'}`) + const buildDate = process.env.BUILD_VERSION ? new Date(process.env.BUILD_VERSION + ':00:00.000Z') : null + const hoursAgo = buildDate ? Math.round((Date.now() - buildDate.getTime()) / (1000 * 60 * 60)) : null + alert(`BUILD DATE:\n${buildDate?.toLocaleString() || 'Development build'}${hoursAgo ? `\nBuilt ${hoursAgo} hours ago` : ''}`) }, () => onVersionTextClick?.(), ) @@ -143,16 +143,7 @@ export default ({ Options
- - GitHub - - {linksButton} +
diff --git a/src/react/MainMenuRenderApp.tsx b/src/react/MainMenuRenderApp.tsx index cdcfc096..f678e7f7 100644 --- a/src/react/MainMenuRenderApp.tsx +++ b/src/react/MainMenuRenderApp.tsx @@ -8,7 +8,6 @@ import { setLoadingScreenStatus } from '../appStatus' import { openFilePicker, copyFilesAsync, mkdirRecursive, openWorldDirectory, removeFileRecursiveAsync } from '../browserfs' import MainMenu from './MainMenu' -import { DiscordButton } from './DiscordButton' const isMainMenu = () => { return activeModalStack.length === 0 && !miscUiState.gameLoaded @@ -123,29 +122,10 @@ export default () => { singleplayerAvailable={singleplayerAvailable} connectToServerAction={() => showModal({ reactType: 'serversList' })} singleplayerAction={async () => { - const oldFormatSave = fs.existsSync('./world/level.dat') - if (oldFormatSave) { - setLoadingScreenStatus('Migrating old save, don\'t close the page') - try { - await mkdirRecursive('/data/worlds/local') - await copyFilesAsync('/world/', '/data/worlds/local') - try { - await removeFileRecursiveAsync('/world/') - } catch (err) { - console.error(err) - } - } catch (err) { - console.warn(err) - alert('Failed to migrate world from localStorage') - } finally { - setLoadingScreenStatus(undefined) - } - } showModal({ reactType: 'singleplayer' }) }} githubAction={() => openGithub()} optionsAction={() => openOptionsMenu('main')} - linksButton={} bottomRightLinks={process.env.MAIN_MENU_LINKS} openFileAction={e => { if (!!window.showDirectoryPicker && !e.shiftKey) { diff --git a/src/react/PacketsReplayProvider.tsx b/src/react/PacketsReplayProvider.tsx index 782134c2..8ffd1f6b 100644 --- a/src/react/PacketsReplayProvider.tsx +++ b/src/react/PacketsReplayProvider.tsx @@ -22,7 +22,7 @@ export default function PacketsReplayProvider () { return ( , style: React.CSSProperties, key: number) => { + if (button.type === 'discord') { + return + } + if (button.type === 'github') { + return + } + if (button.type === 'url' && button.text) { + return + } + return null + } + + return ( + <> + {pauseLinksConfig.map((row, i) => { + const style = { width: (204 / row.length - (row.length > 1 ? 4 : 0)) + 'px' } + return ( +
+ {row.map((button, k) => renderButton(button, style, k))} +
+ ) + })} + + ) +} + +export default () => { + return { + console.error(error) + return null + }}> + + +} diff --git a/src/react/PauseScreen.tsx b/src/react/PauseScreen.tsx index 35d873ea..f051f9e1 100644 --- a/src/react/PauseScreen.tsx +++ b/src/react/PauseScreen.tsx @@ -34,6 +34,8 @@ import { DiscordButton } from './DiscordButton' import { showNotification } from './NotificationProvider' import { appStatusState, reconnectReload } from './AppStatusProvider' import NetworkStatus from './NetworkStatus' +import PauseLinkButtons from './PauseLinkButtons' +import { pixelartIcons } from './PixelartIcon' const waitForPotentialRender = async () => { return new Promise(resolve => { @@ -160,7 +162,7 @@ export default () => { const { singleplayer, wanOpened, wanOpening } = useSnapshot(miscUiState) const { noConnection } = useSnapshot(gameAdditionalState) const { active: packetsReplaceActive, hasRecordedPackets: packetsReplaceHasRecordedPackets } = useSnapshot(packetsRecordingState) - const { displayRecordButton } = useSnapshot(options) + const { displayRecordButton: displayPacketsButtons } = useSnapshot(options) const handlePointerLockChange = () => { if (!pointerLock.hasPointerLock && activeModalStack.length === 0) { @@ -227,33 +229,13 @@ export default () => { if (!isModalActive) return null - const pauseLinks: React.ReactNode[] = [] - const pauseLinksConfig = miscUiState.appConfig?.pauseLinks - if (pauseLinksConfig) { - for (const [i, row] of pauseLinksConfig.entries()) { - const rowButtons: React.ReactNode[] = [] - for (const [k, button] of row.entries()) { - const key = `${i}-${k}` - const style = { width: (204 / row.length - (row.length > 1 ? 4 : 0)) + 'px' } - if (button.type === 'discord') { - rowButtons.push() - } else if (button.type === 'github') { - rowButtons.push() - } else if (button.type === 'url' && button.text) { - rowButtons.push() - } - } - pauseLinks.push(
{rowButtons}
) - } - } - return
@@ -277,7 +263,7 @@ export default () => {
- {pauseLinks} + {singleplayer ? (
diff --git a/src/react/ReplayPanel.tsx b/src/react/ReplayPanel.tsx index fd4082af..3b709882 100644 --- a/src/react/ReplayPanel.tsx +++ b/src/react/ReplayPanel.tsx @@ -1,9 +1,11 @@ import { useState, useEffect } from 'react' +import { useSnapshot } from 'valtio' import { filterPackets } from './packetsFilter' import { DARK_COLORS } from './components/replay/constants' import FilterInput from './components/replay/FilterInput' import PacketList from './components/replay/PacketList' import ProgressBar from './components/replay/ProgressBar' +import { packetsReplayState } from './state/packetsReplayState' interface Props { replayName: string @@ -41,7 +43,7 @@ export default function ReplayPanel ({ style }: Props) { const [filter, setFilter] = useState(defaultFilter) - const [isMinimized, setIsMinimized] = useState(false) + const { isMinimized } = useSnapshot(packetsReplayState) const { filtered: filteredPackets, hiddenCount } = filterPackets(packets.slice(-500), filter) useEffect(() => { @@ -50,7 +52,7 @@ export default function ReplayPanel ({ const handlePlayPauseClick = () => { if (isMinimized) { - setIsMinimized(false) + packetsReplayState.isMinimized = false } else { onPlayPause?.(!isPlaying) } @@ -113,7 +115,7 @@ export default function ReplayPanel ({
{replayName || 'Unnamed Replay'}
)} - {showCancel && } +
+ {options.length > 0 &&
+ {options.map(option => )} +
} +
+ {Object.entries(inputs).map(([key, input]) => { + const label = input.label ?? titleCase(noCase(key)) + return
+ {input.type === 'text' && ( + { + inputValues.current[key] = e.target.value + }} + /> + )} + {input.type === 'checkbox' && ( + + )} +
+ })} +
+ {inputs && inputsConfirmButton && ( + + )} + {showCancel && ( + + )} +
} diff --git a/src/react/ServersList.tsx b/src/react/ServersList.tsx index 46541af6..7b34f016 100644 --- a/src/react/ServersList.tsx +++ b/src/react/ServersList.tsx @@ -1,64 +1,61 @@ -import React from 'react' +import React, { useMemo } from 'react' +import { useSnapshot } from 'valtio' +import { miscUiState } from '../globalState' +import { appQueryParams } from '../appParams' import Singleplayer from './Singleplayer' import Input from './Input' import Button from './Button' import PixelartIcon, { pixelartIcons } from './PixelartIcon' - import Select from './Select' import { BaseServerInfo } from './AddServerOrConnect' import { useIsSmallWidth } from './simpleHooks' +import { appStorage, SavedProxiesData, ServerHistoryEntry } from './appStorageProvider' + +const getInitialProxies = () => { + const proxies = [] as string[] + if (miscUiState.appConfig?.defaultProxy) { + proxies.push(miscUiState.appConfig.defaultProxy) + } + return proxies +} + +export const getCurrentProxy = (): string | undefined => { + return appQueryParams.proxy ?? appStorage.proxiesData?.selected ?? getInitialProxies()[0] +} + +export const getCurrentUsername = () => { + return appQueryParams.username ?? appStorage.username +} interface Props extends React.ComponentProps { joinServer: (info: BaseServerInfo | string, additional: { shouldSave?: boolean index?: number }) => void - initialProxies: SavedProxiesLocalStorage - updateProxies: (proxies: SavedProxiesLocalStorage) => void - username: string - setUsername: (username: string) => void onProfileClick?: () => void setQuickConnectIp?: (ip: string) => void - serverHistory?: Array<{ - ip: string - versionOverride?: string - numConnects: number - }> -} - -export interface SavedProxiesLocalStorage { - proxies: readonly string[] - selected: string -} - -type ProxyStatusResult = { - time: number - ping: number - status: 'success' | 'error' | 'unknown' } export default ({ - initialProxies, - updateProxies: updateProxiesProp, joinServer, - username, - setUsername, onProfileClick, setQuickConnectIp, - serverHistory, ...props }: Props) => { - const [proxies, setProxies] = React.useState(initialProxies) - - const updateProxies = (newData: SavedProxiesLocalStorage) => { - setProxies(newData) - updateProxiesProp(newData) - } - + const snap = useSnapshot(appStorage) + const username = useMemo(() => getCurrentUsername(), [appQueryParams.username, appStorage.username]) const [serverIp, setServerIp] = React.useState('') const [save, setSave] = React.useState(true) const [activeHighlight, setActiveHighlight] = React.useState(undefined as 'quick-connect' | 'server-list' | undefined) + const updateProxies = (newData: SavedProxiesData) => { + appStorage.proxiesData = newData + } + + const setUsername = (username: string) => { + appStorage.username = username + } + const getActiveHighlightStyles = (type: typeof activeHighlight) => { const styles: React.CSSProperties = { transition: 'filter 0.2s', @@ -71,6 +68,8 @@ export default ({ const isSmallWidth = useIsSmallWidth() + const initialProxies = getInitialProxies() + const proxiesData = snap.proxiesData ?? { proxies: initialProxies, selected: initialProxies[0] } return setActiveHighlight('quick-connect')} onMouseLeave={() => setActiveHighlight(undefined)} > - {/* todo history */} - {serverHistory?.map((server) => ( -