Merge remote-tracking branch 'origin/next' into light-engine
This commit is contained in:
commit
ec6b2494c8
47 changed files with 1003 additions and 513 deletions
|
|
@ -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",
|
||||
|
|
|
|||
79
pnpm-lock.yaml
generated
79
pnpm-lock.yaml
generated
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -155,7 +155,7 @@ const generateItemsGui = async (models: Record<string, BlockModelMcAssets>, 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()
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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<AppQsParams>({} 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] ?? []
|
||||
},
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -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<typeof openWorldZipInner>
|
|||
}
|
||||
}
|
||||
|
||||
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')!
|
||||
|
|
|
|||
|
|
@ -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> | 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
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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<string, any> } & { reactType: string })
|
||||
|
||||
|
|
|
|||
2
src/globals.d.ts
vendored
2
src/globals.d.ts
vendored
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
15
src/index.ts
15
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')
|
||||
|
|
|
|||
|
|
@ -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<RenderItem | Item | null>, 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<RenderItem | Item | null>, 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
|
||||
]
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<OptionMeta<AppOptions[K]>> } & { custom? }>
|
||||
|
|
@ -450,9 +453,19 @@ export const guiOptionsScheme: {
|
|||
return <Button
|
||||
inScreen
|
||||
onClick={() => {
|
||||
if (confirm('Are you sure you want to reset all settings?')) resetLocalStorageWithoutWorld()
|
||||
if (confirm('Are you sure you want to reset all settings?')) resetOptions()
|
||||
}}
|
||||
>Reset all settings</Button>
|
||||
>Reset settings</Button>
|
||||
},
|
||||
},
|
||||
{
|
||||
custom () {
|
||||
return <Button
|
||||
inScreen
|
||||
onClick={() => {
|
||||
if (confirm('Are you sure you want to remove all data (settings, keybindings, servers, username, auth, proxies)?')) resetLocalStorage()
|
||||
}}
|
||||
>Remove all data</Button>
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
@ -460,6 +473,11 @@ export const guiOptionsScheme: {
|
|||
return <Category>Developer</Category>
|
||||
},
|
||||
},
|
||||
{
|
||||
custom () {
|
||||
return <Button label='Export/Import...' onClick={() => openOptionsMenu('export-import')} inScreen />
|
||||
}
|
||||
},
|
||||
{
|
||||
custom () {
|
||||
const { active } = useSnapshot(packetsRecordingState)
|
||||
|
|
@ -495,7 +513,7 @@ export const guiOptionsScheme: {
|
|||
{
|
||||
custom () {
|
||||
const { serversAutoVersionSelect } = useSnapshot(options)
|
||||
const allVersions = [...supportedVersions, 'latest', 'auto']
|
||||
const allVersions = [...[...supportedVersions].sort((a, b) => versionToNumber(a) - versionToNumber(b)), 'latest', 'auto']
|
||||
const currentIndex = allVersions.indexOf(serversAutoVersionSelect)
|
||||
|
||||
const getDisplayValue = (version: string) => {
|
||||
|
|
@ -508,10 +526,11 @@ export const guiOptionsScheme: {
|
|||
return <div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||
<Slider
|
||||
style={{ width: 150 }}
|
||||
label='Server Version'
|
||||
label='Prefer Server Version'
|
||||
value={currentIndex}
|
||||
min={0}
|
||||
max={allVersions.length - 1}
|
||||
unit=''
|
||||
valueDisplay={getDisplayValue(serversAutoVersionSelect)}
|
||||
updateValue={(newVal) => {
|
||||
options.serversAutoVersionSelect = allVersions[newVal]
|
||||
|
|
@ -521,8 +540,94 @@ export const guiOptionsScheme: {
|
|||
},
|
||||
},
|
||||
],
|
||||
'export-import': [
|
||||
{
|
||||
custom () {
|
||||
return <Category>Export/Import Data</Category>
|
||||
}
|
||||
},
|
||||
{
|
||||
custom () {
|
||||
return <Button
|
||||
inScreen
|
||||
disabled={true}
|
||||
onClick={() => {}}
|
||||
>Import Data</Button>
|
||||
}
|
||||
},
|
||||
{
|
||||
custom () {
|
||||
return <Button
|
||||
inScreen
|
||||
onClick={async () => {
|
||||
const data = await showInputsModal('Export Profile', {
|
||||
profileName: {
|
||||
type: 'text',
|
||||
},
|
||||
exportSettings: {
|
||||
type: 'checkbox',
|
||||
defaultValue: true,
|
||||
},
|
||||
exportKeybindings: {
|
||||
type: 'checkbox',
|
||||
defaultValue: true,
|
||||
},
|
||||
exportServers: {
|
||||
type: 'checkbox',
|
||||
defaultValue: true,
|
||||
},
|
||||
saveUsernameAndProxy: {
|
||||
type: 'checkbox',
|
||||
defaultValue: true,
|
||||
},
|
||||
})
|
||||
const fileName = `${data.profileName ? `${data.profileName}-` : ''}web-client-profile.json`
|
||||
const json = {
|
||||
_about: 'Minecraft Web Client (mcraft.fun) Profile',
|
||||
...data.exportSettings ? {
|
||||
options: getChangedSettings(),
|
||||
} : {},
|
||||
...data.exportKeybindings ? {
|
||||
keybindings: customKeymaps,
|
||||
} : {},
|
||||
...data.exportServers ? {
|
||||
servers: appStorage.serversList,
|
||||
} : {},
|
||||
...data.saveUsernameAndProxy ? {
|
||||
username: appStorage.username,
|
||||
proxy: appStorage.proxiesData?.selected,
|
||||
} : {},
|
||||
}
|
||||
const blob = new Blob([JSON.stringify(json, null, 2)], { type: 'application/json' })
|
||||
const url = URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = fileName
|
||||
a.click()
|
||||
URL.revokeObjectURL(url)
|
||||
}}
|
||||
>Export Data</Button>
|
||||
}
|
||||
},
|
||||
{
|
||||
custom () {
|
||||
return <Button
|
||||
inScreen
|
||||
disabled
|
||||
>Export Worlds</Button>
|
||||
}
|
||||
},
|
||||
{
|
||||
custom () {
|
||||
return <Button
|
||||
inScreen
|
||||
disabled
|
||||
>Export Resource Pack</Button>
|
||||
}
|
||||
}
|
||||
],
|
||||
}
|
||||
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 }) => <div style={{
|
||||
fontSize: 9,
|
||||
|
|
|
|||
|
|
@ -1,11 +1,9 @@
|
|||
// todo implement async options storage
|
||||
|
||||
import { proxy, subscribe } from 'valtio/vanilla'
|
||||
// weird webpack configuration bug: it cant import valtio/utils in this file
|
||||
import { subscribeKey } from 'valtio/utils'
|
||||
import { omitObj } from '@zardoy/utils'
|
||||
import { appQueryParamsArray } from './appParams'
|
||||
import type { AppConfig } from './appConfig'
|
||||
import { appStorage } from './react/appStorageProvider'
|
||||
|
||||
const isDev = process.env.NODE_ENV === 'development'
|
||||
const initialAppConfig = process.env?.INLINED_APP_CONFIG as AppConfig ?? {}
|
||||
|
|
@ -63,6 +61,7 @@ const defaultOptions = {
|
|||
preciseMouseInput: false,
|
||||
// todo ui setting, maybe enable by default?
|
||||
waitForChunksRender: 'sp-only' as 'sp-only' | boolean,
|
||||
jeiEnabled: true as boolean | Array<'creative' | 'survival' | 'adventure' | 'spectator'>,
|
||||
|
||||
// antiAliasing: false,
|
||||
|
||||
|
|
@ -163,12 +162,31 @@ const migrateOptions = (options: Partial<AppOptions & Record<string, any>>) => {
|
|||
|
||||
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 = <T extends Record<string, any>>(proxy: T, callback: (p: T, isChanged: boolean) => void) => () => void
|
||||
|
|
|
|||
|
|
@ -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<void>
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
|||
<InputWithLabel
|
||||
required
|
||||
label="Server IP"
|
||||
autoFocus={!lockConnect}
|
||||
value={serverIp}
|
||||
disabled={lockConnect && parsedQsIp.host !== null}
|
||||
onChange={({ target: { value } }) => {
|
||||
|
|
@ -256,20 +255,8 @@ export default ({ onBack, onConfirm, title = 'Add a Server', initialData, parseQ
|
|||
|
||||
const ButtonWrapper = ({ ...props }: React.ComponentProps<typeof Button>) => {
|
||||
props.style ??= {}
|
||||
props.style.width = ELEMENTS_WIDTH
|
||||
props.style.width = INPUT_LABEL_WIDTH
|
||||
return <Button {...props} />
|
||||
}
|
||||
|
||||
const InputWithLabel = ({ label, span, ...props }: React.ComponentProps<typeof Input> & { label, span? }) => {
|
||||
return <div style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gridRow: span ? 'span 2 / span 2' : undefined,
|
||||
}}
|
||||
>
|
||||
<label style={{ fontSize: 12, marginBottom: 1, color: 'lightgray' }}>{label}</label>
|
||||
<Input rootStyles={{ width: ELEMENTS_WIDTH }} {...props} />
|
||||
</div>
|
||||
}
|
||||
|
||||
const fallbackIfNotFound = (index: number) => (index === -1 ? undefined : index)
|
||||
|
|
|
|||
|
|
@ -69,7 +69,9 @@ export default ({
|
|||
>
|
||||
{isError && (
|
||||
<>
|
||||
{showReconnect && onReconnect && <Button label="Reconnect" onClick={onReconnect} />}
|
||||
{showReconnect && onReconnect && <Button onClick={onReconnect}>
|
||||
<b>Reconnect</b>
|
||||
</Button>}
|
||||
{actionsSlot}
|
||||
<Button
|
||||
onClick={() => {
|
||||
|
|
@ -79,8 +81,9 @@ export default ({
|
|||
window.location.reload()
|
||||
}
|
||||
}}
|
||||
label="Reset App (recommended)"
|
||||
/>
|
||||
>
|
||||
<b>Reset App (recommended)</b>
|
||||
</Button>
|
||||
{!lockConnect && backAction && <Button label="Back" onClick={backAction} />}
|
||||
</>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -28,9 +28,11 @@ export default () => {
|
|||
addBossBar(bossBar as BossBarType)
|
||||
})
|
||||
bot.on('bossBarUpdated', (bossBar) => {
|
||||
if (!bossBar) return
|
||||
addBossBar(bossBar as BossBarType)
|
||||
})
|
||||
bot.on('bossBarDeleted', (bossBar) => {
|
||||
if (!bossBar) return
|
||||
removeBossBar(bossBar as BossBarType)
|
||||
})
|
||||
}, [])
|
||||
|
|
|
|||
|
|
@ -29,6 +29,12 @@ div.chat-wrapper {
|
|||
gap: 1px;
|
||||
}
|
||||
|
||||
.chat-submit-button {
|
||||
visibility: hidden;
|
||||
position: absolute;
|
||||
pointer-events: none !important;
|
||||
}
|
||||
|
||||
.chat-input-wrapper form {
|
||||
display: flex;
|
||||
}
|
||||
|
|
@ -84,11 +90,11 @@ div.chat-wrapper {
|
|||
::-webkit-scrollbar {
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
background-color: rgb(24, 24, 24);
|
||||
background-color: #272727;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background-color: rgb(50, 50, 50);
|
||||
background-color: #747474;
|
||||
}
|
||||
|
||||
.chat-completions-items>div {
|
||||
|
|
@ -154,20 +160,24 @@ input[type=text],
|
|||
padding-bottom: 1px;
|
||||
padding-left: 2px;
|
||||
padding-right: 2px;
|
||||
height: 15px;
|
||||
}
|
||||
|
||||
.chat-mobile-hidden {
|
||||
width: 8px;
|
||||
height: 0;
|
||||
.chat-mobile-input-hidden {
|
||||
position: absolute;
|
||||
width: 8px;
|
||||
height: 1px !important;
|
||||
display: block !important;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
height: 1px !important;
|
||||
/* ios: using z-index, pointer-events: none or top below -10px breaks arrows */
|
||||
}
|
||||
|
||||
.chat-mobile-hidden:nth-last-child(1) {
|
||||
height: 8px;
|
||||
.chat-mobile-input-hidden-up {
|
||||
top: -10px;
|
||||
}
|
||||
|
||||
.chat-mobile-input-hidden-down {
|
||||
top: -5px;
|
||||
}
|
||||
|
||||
#chatinput:focus {
|
||||
|
|
|
|||
|
|
@ -71,6 +71,8 @@ export default ({
|
|||
}: Props) => {
|
||||
const sendHistoryRef = useRef(JSON.parse(window.sessionStorage.chatHistory || '[]'))
|
||||
const [isInputFocused, setIsInputFocused] = useState(false)
|
||||
// const [spellCheckEnabled, setSpellCheckEnabled] = useState(false)
|
||||
const spellCheckEnabled = false
|
||||
|
||||
const [completePadText, setCompletePadText] = useState('')
|
||||
const completeRequestValue = useRef('')
|
||||
|
|
@ -107,9 +109,28 @@ export default ({
|
|||
}, 0)
|
||||
}
|
||||
|
||||
const auxInputFocus = (fireKey: string) => {
|
||||
const handleArrowUp = () => {
|
||||
if (chatHistoryPos.current === 0) return
|
||||
if (chatHistoryPos.current === sendHistoryRef.current.length) { // started navigating history
|
||||
inputCurrentlyEnteredValue.current = chatInput.current.value
|
||||
}
|
||||
chatHistoryPos.current--
|
||||
updateInputValue(sendHistoryRef.current[chatHistoryPos.current] || '')
|
||||
}
|
||||
|
||||
const handleArrowDown = () => {
|
||||
if (chatHistoryPos.current === sendHistoryRef.current.length) return
|
||||
chatHistoryPos.current++
|
||||
updateInputValue(sendHistoryRef.current[chatHistoryPos.current] || inputCurrentlyEnteredValue.current || '')
|
||||
}
|
||||
|
||||
const auxInputFocus = (direction: 'up' | 'down') => {
|
||||
chatInput.current.focus()
|
||||
chatInput.current.dispatchEvent(new KeyboardEvent('keydown', { code: fireKey, bubbles: true }))
|
||||
if (direction === 'up') {
|
||||
handleArrowUp()
|
||||
} else {
|
||||
handleArrowDown()
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -125,6 +146,7 @@ export default ({
|
|||
if (opened) {
|
||||
updateInputValue(chatInputValueGlobal.value)
|
||||
chatInputValueGlobal.value = ''
|
||||
chatHistoryPos.current = sendHistoryRef.current.length
|
||||
if (!usingTouch) {
|
||||
chatInput.current.focus()
|
||||
}
|
||||
|
|
@ -167,6 +189,8 @@ export default ({
|
|||
const onMainInputChange = () => {
|
||||
const completeValue = getCompleteValue()
|
||||
setCompletePadText(completeValue === '/' ? '' : completeValue)
|
||||
// not sure if enabling would be useful at all (maybe make as a setting in the future?)
|
||||
// setSpellCheckEnabled(!chatInput.current.value.startsWith('/'))
|
||||
if (completeRequestValue.current === completeValue) {
|
||||
updateFilteredCompleteItems(completionItemsSource)
|
||||
return
|
||||
|
|
@ -271,20 +295,23 @@ export default ({
|
|||
{isIos && <input
|
||||
value=''
|
||||
type="text"
|
||||
className="chat-mobile-hidden"
|
||||
className="chat-mobile-input-hidden chat-mobile-input-hidden-up"
|
||||
id="chatinput-next-command"
|
||||
spellCheck={false}
|
||||
autoComplete="off"
|
||||
onFocus={() => auxInputFocus('ArrowUp')}
|
||||
onFocus={() => auxInputFocus('up')}
|
||||
onChange={() => { }}
|
||||
/>}
|
||||
<input
|
||||
defaultValue=''
|
||||
// ios doesn't support toggling autoCorrect on the fly so we need to re-create the input
|
||||
key={spellCheckEnabled ? 'true' : 'false'}
|
||||
ref={chatInput}
|
||||
type="text"
|
||||
className="chat-input"
|
||||
id="chatinput"
|
||||
spellCheck={false}
|
||||
spellCheck={spellCheckEnabled}
|
||||
autoCorrect={spellCheckEnabled ? 'on' : 'off'}
|
||||
autoComplete="off"
|
||||
aria-autocomplete="both"
|
||||
onChange={onMainInputChange}
|
||||
|
|
@ -294,16 +321,9 @@ export default ({
|
|||
onBlur={() => setIsInputFocused(false)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.code === 'ArrowUp') {
|
||||
if (chatHistoryPos.current === 0) return
|
||||
if (chatHistoryPos.current === sendHistoryRef.current.length) { // started navigating history
|
||||
inputCurrentlyEnteredValue.current = e.currentTarget.value
|
||||
}
|
||||
chatHistoryPos.current--
|
||||
updateInputValue(sendHistoryRef.current[chatHistoryPos.current] || '')
|
||||
handleArrowUp()
|
||||
} else if (e.code === 'ArrowDown') {
|
||||
if (chatHistoryPos.current === sendHistoryRef.current.length) return
|
||||
chatHistoryPos.current++
|
||||
updateInputValue(sendHistoryRef.current[chatHistoryPos.current] || inputCurrentlyEnteredValue.current || '')
|
||||
handleArrowDown()
|
||||
}
|
||||
if (e.code === 'Tab') {
|
||||
if (completionItemsSource.length) {
|
||||
|
|
@ -327,15 +347,15 @@ export default ({
|
|||
{isIos && <input
|
||||
value=''
|
||||
type="text"
|
||||
className="chat-mobile-hidden"
|
||||
className="chat-mobile-input-hidden chat-mobile-input-hidden-down"
|
||||
id="chatinput-prev-command"
|
||||
spellCheck={false}
|
||||
autoComplete="off"
|
||||
onFocus={() => auxInputFocus('ArrowDown')}
|
||||
onFocus={() => auxInputFocus('down')}
|
||||
onChange={() => { }}
|
||||
/>}
|
||||
{/* for some reason this is needed to make Enter work on android chrome */}
|
||||
<button type='submit' style={{ visibility: 'hidden' }} />
|
||||
<button type='submit' className="chat-submit-button" />
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -88,7 +88,7 @@ export default () => {
|
|||
// normalize
|
||||
items = items.map(item => `/${item}`)
|
||||
}
|
||||
if (localServer) {
|
||||
if (items.length) {
|
||||
items = [...items, ...getBuiltinCommandsList()]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -90,7 +90,7 @@ export default () => {
|
|||
upArmour()
|
||||
}, [])
|
||||
|
||||
return <div>
|
||||
return <div className='hud-bars-container'>
|
||||
<HealthBar
|
||||
gameMode={gameMode}
|
||||
isHardcore={isHardcore}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ interface Props extends Omit<React.ComponentProps<'input'>, '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<HTMLInputElement>(null!)
|
||||
|
|
@ -51,3 +51,19 @@ export default ({ autoFocus, rootStyles, inputRef, validateInput, defaultValue,
|
|||
/>
|
||||
</div>
|
||||
}
|
||||
|
||||
export default Input
|
||||
|
||||
export const INPUT_LABEL_WIDTH = 190
|
||||
|
||||
export const InputWithLabel = ({ label, span, ...props }: React.ComponentProps<typeof Input> & { label, span? }) => {
|
||||
return <div style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gridRow: span ? 'span 2 / span 2' : undefined,
|
||||
}}
|
||||
>
|
||||
<label style={{ fontSize: 12, marginBottom: 1, color: 'lightgray' }}>{label}</label>
|
||||
<Input rootStyles={{ width: INPUT_LABEL_WIDTH }} {...props} />
|
||||
</div>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<HTMLButtonElement>) => 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
|
||||
</Button>
|
||||
<div className={styles['menu-row']}>
|
||||
<ButtonWithTooltip
|
||||
initialTooltip={{
|
||||
content: 'Report bugs or request features!',
|
||||
}}
|
||||
style={{ width: '98px' }}
|
||||
onClick={githubAction}
|
||||
>
|
||||
GitHub
|
||||
</ButtonWithTooltip>
|
||||
{linksButton}
|
||||
<PauseLinkButtons />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -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={<DiscordButton />}
|
||||
bottomRightLinks={process.env.MAIN_MENU_LINKS}
|
||||
openFileAction={e => {
|
||||
if (!!window.showDirectoryPicker && !e.shiftKey) {
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ export default function PacketsReplayProvider () {
|
|||
return (
|
||||
<ReplayPanel
|
||||
style={{
|
||||
transform: 'scale(0.5)',
|
||||
transform: 'scale(0.4)',
|
||||
transformOrigin: 'top right'
|
||||
}}
|
||||
replayName={state.replayName}
|
||||
|
|
|
|||
50
src/react/PauseLinkButtons.tsx
Normal file
50
src/react/PauseLinkButtons.tsx
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
import { useSnapshot } from 'valtio'
|
||||
import { openURL } from 'renderer/viewer/lib/simpleUtils'
|
||||
import { ErrorBoundary } from '@zardoy/react-util'
|
||||
import { miscUiState } from '../globalState'
|
||||
import { openGithub } from '../utils'
|
||||
import Button from './Button'
|
||||
import { DiscordButton } from './DiscordButton'
|
||||
import styles from './PauseScreen.module.css'
|
||||
|
||||
function PauseLinkButtonsInner () {
|
||||
const { appConfig } = useSnapshot(miscUiState)
|
||||
const pauseLinksConfig = appConfig?.pauseLinks
|
||||
|
||||
if (!pauseLinksConfig) return null
|
||||
|
||||
const renderButton = (button: Record<string, any>, style: React.CSSProperties, key: number) => {
|
||||
if (button.type === 'discord') {
|
||||
return <DiscordButton key={key} style={style} text={button.text}/>
|
||||
}
|
||||
if (button.type === 'github') {
|
||||
return <Button key={key} className="button" style={style} onClick={() => openGithub()}>{button.text ?? 'GitHub'}</Button>
|
||||
}
|
||||
if (button.type === 'url' && button.text) {
|
||||
return <Button key={key} className="button" style={style} onClick={() => openURL(button.url)}>{button.text}</Button>
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{pauseLinksConfig.map((row, i) => {
|
||||
const style = { width: (204 / row.length - (row.length > 1 ? 4 : 0)) + 'px' }
|
||||
return (
|
||||
<div key={i} className={styles.row}>
|
||||
{row.map((button, k) => renderButton(button, style, k))}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default () => {
|
||||
return <ErrorBoundary renderError={(error) => {
|
||||
console.error(error)
|
||||
return null
|
||||
}}>
|
||||
<PauseLinkButtonsInner />
|
||||
</ErrorBoundary>
|
||||
}
|
||||
|
|
@ -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<void>(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(<DiscordButton key={key} style={style} text={button.text}/>)
|
||||
} else if (button.type === 'github') {
|
||||
rowButtons.push(<Button key={key} className="button" style={style} onClick={() => openGithub()}>{button.text ?? 'GitHub'}</Button>)
|
||||
} else if (button.type === 'url' && button.text) {
|
||||
rowButtons.push(<Button key={key} className="button" style={style} onClick={() => openURL(button.url)}>{button.text}</Button>)
|
||||
}
|
||||
}
|
||||
pauseLinks.push(<div className={styles.row}>{rowButtons}</div>)
|
||||
}
|
||||
}
|
||||
|
||||
return <Screen title='Game Menu'>
|
||||
<div style={{ position: 'fixed', top: '5px', left: 'calc(env(safe-area-inset-left) + 5px)', display: 'flex', flexDirection: 'column', gap: '5px' }}>
|
||||
<Button
|
||||
icon="pixelarticons:folder"
|
||||
onClick={async () => openWorldActions()}
|
||||
/>
|
||||
{displayRecordButton && (
|
||||
{displayPacketsButtons && (
|
||||
<>
|
||||
<Button
|
||||
icon={packetsReplaceActive ? 'pixelarticons:debug-stop' : 'pixelarticons:circle'}
|
||||
|
|
@ -263,10 +245,14 @@ export default () => {
|
|||
/>
|
||||
{packetsReplaceHasRecordedPackets && (
|
||||
<Button
|
||||
icon="pixelarticons:download"
|
||||
icon={pixelartIcons['briefcase-download']}
|
||||
onClick={async () => downloadPacketsReplay()}
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
icon={pixelartIcons['download']}
|
||||
onClick={async () => bot.downloadCurrentWorldState()}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -277,7 +263,7 @@ export default () => {
|
|||
</ErrorBoundary>
|
||||
<div className={styles.pause_container}>
|
||||
<Button className="button" style={{ width: '204px' }} onClick={onReturnPress}>Back to Game</Button>
|
||||
{pauseLinks}
|
||||
<PauseLinkButtons />
|
||||
<Button className="button" style={{ width: '204px' }} onClick={() => openOptionsMenu('main')}>Options</Button>
|
||||
{singleplayer ? (
|
||||
<div className={styles.row}>
|
||||
|
|
|
|||
|
|
@ -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 ({
|
|||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<div style={{ fontSize: '12px', fontWeight: 'bold' }}>{replayName || 'Unnamed Replay'}</div>
|
||||
<button
|
||||
onClick={() => setIsMinimized(true)}
|
||||
onClick={() => { packetsReplayState.isMinimized = true }}
|
||||
style={{
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
|
|
|
|||
|
|
@ -1,10 +1,14 @@
|
|||
import { proxy, useSnapshot } from 'valtio'
|
||||
import { useEffect, useRef } from 'react'
|
||||
import { noCase } from 'change-case'
|
||||
import { titleCase } from 'title-case'
|
||||
import { hideCurrentModal, showModal } from '../globalState'
|
||||
import { parseFormattedMessagePacket } from '../botUtils'
|
||||
import Screen from './Screen'
|
||||
import { useIsModalActive } from './utilsApp'
|
||||
import Button from './Button'
|
||||
import MessageFormattedString from './MessageFormattedString'
|
||||
import Input, { InputWithLabel } from './Input'
|
||||
|
||||
const state = proxy({
|
||||
title: '',
|
||||
|
|
@ -12,6 +16,8 @@ const state = proxy({
|
|||
showCancel: true,
|
||||
minecraftJsonMessage: null as null | Record<string, any>,
|
||||
behavior: 'resolve-close' as 'resolve-close' | 'close-resolve',
|
||||
inputs: {} as Record<string, InputOption>,
|
||||
inputsConfirmButton: ''
|
||||
})
|
||||
|
||||
let resolve
|
||||
|
|
@ -35,17 +41,63 @@ export const showOptionsModal = async <T extends string> (
|
|||
title,
|
||||
options,
|
||||
showCancel: cancel,
|
||||
minecraftJsonMessage: minecraftJsonMessageParsed
|
||||
minecraftJsonMessage: minecraftJsonMessageParsed,
|
||||
inputs: {},
|
||||
inputsConfirmButton: ''
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
type InputOption = {
|
||||
type: 'text' | 'checkbox'
|
||||
defaultValue?: string | boolean
|
||||
label?: string
|
||||
}
|
||||
export const showInputsModal = async <T extends Record<string, InputOption>>(
|
||||
title: string,
|
||||
inputs: T,
|
||||
{ cancel = true, minecraftJsonMessage }: { cancel?: boolean, minecraftJsonMessage? } = {}
|
||||
): Promise<{
|
||||
[K in keyof T]: T[K] extends { type: 'text' }
|
||||
? string
|
||||
: T[K] extends { type: 'checkbox' }
|
||||
? boolean
|
||||
: never
|
||||
}> => {
|
||||
showModal({ reactType: 'general-select' })
|
||||
let minecraftJsonMessageParsed
|
||||
if (minecraftJsonMessage) {
|
||||
const parseResult = parseFormattedMessagePacket(minecraftJsonMessage)
|
||||
minecraftJsonMessageParsed = parseResult.formatted
|
||||
if (parseResult.plain) {
|
||||
title += ` (${parseResult.plain})`
|
||||
}
|
||||
}
|
||||
return new Promise((_resolve) => {
|
||||
resolve = _resolve
|
||||
Object.assign(state, {
|
||||
title,
|
||||
inputs,
|
||||
showCancel: cancel,
|
||||
minecraftJsonMessage: minecraftJsonMessageParsed,
|
||||
options: [],
|
||||
inputsConfirmButton: 'Confirm'
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export default () => {
|
||||
const { title, options, showCancel, minecraftJsonMessage } = useSnapshot(state)
|
||||
const { title, options, showCancel, minecraftJsonMessage, inputs, inputsConfirmButton } = useSnapshot(state)
|
||||
const isModalActive = useIsModalActive('general-select')
|
||||
const inputValues = useRef({})
|
||||
|
||||
useEffect(() => {
|
||||
inputValues.current = Object.fromEntries(Object.entries(inputs).map(([key, input]) => [key, input.defaultValue ?? (input.type === 'checkbox' ? false : '')]))
|
||||
}, [inputs])
|
||||
|
||||
if (!isModalActive) return
|
||||
|
||||
const resolveClose = (value: string | undefined) => {
|
||||
const resolveClose = (value: any) => {
|
||||
if (state.behavior === 'resolve-close') {
|
||||
resolve(value)
|
||||
hideCurrentModal()
|
||||
|
|
@ -59,17 +111,66 @@ export default () => {
|
|||
{minecraftJsonMessage && <div style={{ textAlign: 'center', }}>
|
||||
<MessageFormattedString message={minecraftJsonMessage} />
|
||||
</div>}
|
||||
{options.map(option => <Button
|
||||
key={option} onClick={() => {
|
||||
resolveClose(option)
|
||||
}}
|
||||
>{option}
|
||||
</Button>)}
|
||||
{showCancel && <Button
|
||||
style={{ marginTop: 30 }} onClick={() => {
|
||||
resolveClose(undefined)
|
||||
}}
|
||||
>Cancel
|
||||
</Button>}
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 9 }}>
|
||||
{options.length > 0 && <div style={{ display: 'flex', flexDirection: 'column', gap: 5 }}>
|
||||
{options.map(option => <Button
|
||||
key={option} onClick={() => {
|
||||
resolveClose(option)
|
||||
}}
|
||||
>{option}
|
||||
</Button>)}
|
||||
</div>}
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
|
||||
{Object.entries(inputs).map(([key, input]) => {
|
||||
const label = input.label ?? titleCase(noCase(key))
|
||||
return <div key={key}>
|
||||
{input.type === 'text' && (
|
||||
<InputWithLabel
|
||||
label={label}
|
||||
autoFocus
|
||||
type='text'
|
||||
defaultValue={input.defaultValue as string}
|
||||
onChange={(e) => {
|
||||
inputValues.current[key] = e.target.value
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{input.type === 'checkbox' && (
|
||||
<label style={{ display: 'flex', alignItems: 'center', gap: 4, fontSize: 12 }}>
|
||||
<input
|
||||
type='checkbox'
|
||||
style={{ marginBottom: -1, }}
|
||||
defaultChecked={input.defaultValue as boolean}
|
||||
onChange={(e) => {
|
||||
inputValues.current[key] = e.target.checked
|
||||
}}
|
||||
/>
|
||||
{label}
|
||||
</label>
|
||||
)}
|
||||
</div>
|
||||
})}
|
||||
</div>
|
||||
{inputs && inputsConfirmButton && (
|
||||
<Button
|
||||
// style={{ marginTop: 30 }}
|
||||
onClick={() => {
|
||||
resolveClose(inputValues.current)
|
||||
}}
|
||||
>
|
||||
{inputsConfirmButton}
|
||||
</Button>
|
||||
)}
|
||||
{showCancel && (
|
||||
<Button
|
||||
// style={{ marginTop: 30 }}
|
||||
onClick={() => {
|
||||
resolveClose(undefined)
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</Screen>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<typeof Singleplayer> {
|
||||
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 <Singleplayer
|
||||
{...props}
|
||||
firstRowChildrenOverride={<form
|
||||
|
|
@ -85,7 +84,6 @@ export default ({
|
|||
onMouseEnter={() => setActiveHighlight('quick-connect')}
|
||||
onMouseLeave={() => setActiveHighlight(undefined)}
|
||||
>
|
||||
{/* todo history */}
|
||||
<Input
|
||||
required
|
||||
placeholder='Quick Connect IP (:version)'
|
||||
|
|
@ -102,8 +100,8 @@ export default ({
|
|||
spellCheck="false"
|
||||
/>
|
||||
<datalist id="server-history">
|
||||
{serverHistory?.map((server) => (
|
||||
<option key={server.ip} value={`${server.ip}${server.versionOverride ? `:${server.versionOverride}` : ''}`} />
|
||||
{[...(snap.serversHistory ?? [])].sort((a, b) => b.numConnects - a.numConnects).map((server) => (
|
||||
<option key={server.ip} value={`${server.ip}${server.version ? `:${server.version}` : ''}`} />
|
||||
))}
|
||||
</datalist>
|
||||
<label style={{ fontSize: 10, display: 'flex', alignItems: 'center', gap: 5, height: '100%', marginTop: '-1px' }}>
|
||||
|
|
@ -126,10 +124,10 @@ export default ({
|
|||
? <PixelartIcon iconName={pixelartIcons.server} styles={{ fontSize: 14, color: 'lightgray', marginLeft: 2 }} onClick={onProfileClick} />
|
||||
: <span style={{ color: 'lightgray', fontSize: 14 }}>Proxy:</span>}
|
||||
<Select
|
||||
initialOptions={proxies.proxies.map(p => { return { value: p, label: p } })}
|
||||
defaultValue={{ value: proxies.selected, label: proxies.selected }}
|
||||
initialOptions={proxiesData.proxies.map(p => { return { value: p, label: p } })}
|
||||
defaultValue={{ value: proxiesData.selected, label: proxiesData.selected }}
|
||||
updateOptions={(newSel) => {
|
||||
updateProxies({ proxies: [...proxies.proxies], selected: newSel })
|
||||
updateProxies({ proxies: [...proxiesData.proxies], selected: newSel })
|
||||
}}
|
||||
containerStyle={{
|
||||
width: isSmallWidth ? 140 : 180,
|
||||
|
|
@ -139,6 +137,7 @@ export default ({
|
|||
<Input
|
||||
rootStyles={{ width: 80 }}
|
||||
value={username}
|
||||
disabled={appQueryParams.username !== undefined}
|
||||
onChange={({ target: { value } }) => setUsername(value)}
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
|
|
|
|||
|
|
@ -2,19 +2,24 @@ import { useEffect, useMemo, useState } from 'react'
|
|||
import { useUtilsEffect } from '@zardoy/react-util'
|
||||
import { useSnapshot } from 'valtio'
|
||||
import { ConnectOptions } from '../connect'
|
||||
import { activeModalStack, hideCurrentModal, miscUiState, showModal } from '../globalState'
|
||||
import { activeModalStack, hideCurrentModal, miscUiState, notHideableModalsWithoutForce, showModal } from '../globalState'
|
||||
import supportedVersions from '../supportedVersions.mjs'
|
||||
import { appQueryParams } from '../appParams'
|
||||
import { fetchServerStatus, isServerValid } from '../api/mcStatusApi'
|
||||
import { getServerInfo } from '../mineflayer/mc-protocol'
|
||||
import { parseServerAddress } from '../parseServerAddress'
|
||||
import ServersList from './ServersList'
|
||||
import ServersList, { getCurrentProxy, getCurrentUsername } from './ServersList'
|
||||
import AddServerOrConnect, { BaseServerInfo } from './AddServerOrConnect'
|
||||
import { useDidUpdateEffect } from './utils'
|
||||
import { useIsModalActive } from './utilsApp'
|
||||
import { showOptionsModal } from './SelectOption'
|
||||
import { useCopyKeybinding } from './simpleHooks'
|
||||
import { AuthenticatedAccount, getInitialServersList, getServerConnectionHistory, setNewServersList, StoreServerItem } from './serversStorage'
|
||||
import { AuthenticatedAccount, getInitialServersList, getServerConnectionHistory, setNewServersList } from './serversStorage'
|
||||
import { appStorage, StoreServerItem } from './appStorageProvider'
|
||||
|
||||
if (appQueryParams.lockConnect) {
|
||||
notHideableModalsWithoutForce.add('editServer')
|
||||
}
|
||||
|
||||
type AdditionalDisplayData = {
|
||||
textNameRightGrayed: string
|
||||
|
|
@ -24,21 +29,6 @@ type AdditionalDisplayData = {
|
|||
offline?: boolean
|
||||
}
|
||||
|
||||
const serversListQs = appQueryParams.serversList
|
||||
const proxyQs = appQueryParams.proxy
|
||||
|
||||
const getInitialProxies = () => {
|
||||
const proxies = [] as string[]
|
||||
if (miscUiState.appConfig?.defaultProxy) {
|
||||
proxies.push(miscUiState.appConfig.defaultProxy)
|
||||
}
|
||||
if (localStorage['proxy']) {
|
||||
proxies.push(localStorage['proxy'])
|
||||
localStorage.removeItem('proxy')
|
||||
}
|
||||
return proxies
|
||||
}
|
||||
|
||||
// todo move to base
|
||||
const normalizeIp = (ip: string) => ip.replace(/https?:\/\//, '').replace(/\/(:|$)/, '')
|
||||
|
||||
|
|
@ -46,45 +36,30 @@ const FETCH_DELAY = 100 // ms between each request
|
|||
const MAX_CONCURRENT_REQUESTS = 10
|
||||
|
||||
const Inner = ({ hidden, customServersList }: { hidden?: boolean, customServersList?: string[] }) => {
|
||||
const [proxies, setProxies] = useState<readonly string[]>(localStorage['proxies'] ? JSON.parse(localStorage['proxies']) : getInitialProxies())
|
||||
const [selectedProxy, setSelectedProxy] = useState(proxyQs ?? localStorage.getItem('selectedProxy') ?? proxies?.[0] ?? '')
|
||||
const [serverEditScreen, setServerEditScreen] = useState<StoreServerItem | true | null>(null) // true for add
|
||||
const [defaultUsername, _setDefaultUsername] = useState(localStorage['username'] ?? (`mcrafter${Math.floor(Math.random() * 1000)}`))
|
||||
const [authenticatedAccounts, _setAuthenticatedAccounts] = useState<AuthenticatedAccount[]>(JSON.parse(localStorage['authenticatedAccounts'] || '[]'))
|
||||
const { authenticatedAccounts } = useSnapshot(appStorage)
|
||||
const [quickConnectIp, setQuickConnectIp] = useState('')
|
||||
const [selectedIndex, setSelectedIndex] = useState(0)
|
||||
|
||||
// Save username to localStorage when component mounts if it doesn't exist
|
||||
useEffect(() => {
|
||||
if (!localStorage['username']) {
|
||||
localStorage.setItem('username', defaultUsername)
|
||||
}
|
||||
}, [])
|
||||
const { serversList: savedServersList } = useSnapshot(appStorage)
|
||||
|
||||
const setAuthenticatedAccounts = (newState: typeof authenticatedAccounts) => {
|
||||
_setAuthenticatedAccounts(newState)
|
||||
localStorage.setItem('authenticatedAccounts', JSON.stringify(newState))
|
||||
}
|
||||
const serversListDisplay = useMemo(() => {
|
||||
return (
|
||||
customServersList
|
||||
? customServersList.map((row): StoreServerItem => {
|
||||
const [ip, name] = row.split(' ')
|
||||
const [_ip, _port, version] = ip.split(':')
|
||||
return {
|
||||
ip,
|
||||
versionOverride: version,
|
||||
name,
|
||||
}
|
||||
})
|
||||
: [...getInitialServersList()]
|
||||
)
|
||||
}, [customServersList, savedServersList])
|
||||
|
||||
const setDefaultUsername = (newState: typeof defaultUsername) => {
|
||||
_setDefaultUsername(newState)
|
||||
localStorage.setItem('username', newState)
|
||||
}
|
||||
|
||||
const saveNewProxy = () => {
|
||||
if (!selectedProxy || proxyQs) return
|
||||
localStorage.setItem('selectedProxy', selectedProxy)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (proxies.length) {
|
||||
localStorage.setItem('proxies', JSON.stringify(proxies))
|
||||
}
|
||||
saveNewProxy()
|
||||
}, [proxies])
|
||||
|
||||
const [serversList, setServersList] = useState<StoreServerItem[]>(() => (customServersList ? [] : getInitialServersList()))
|
||||
const [additionalData, setAdditionalData] = useState<Record<string, AdditionalDisplayData>>({})
|
||||
const [additionalServerData, setAdditionalServerData] = useState<Record<string, AdditionalDisplayData>>({})
|
||||
|
||||
// Add keyboard handler for moving servers
|
||||
useEffect(() => {
|
||||
|
|
@ -92,49 +67,31 @@ const Inner = ({ hidden, customServersList }: { hidden?: boolean, customServersL
|
|||
if (['input', 'textarea', 'select'].includes((e.target as HTMLElement)?.tagName?.toLowerCase())) return
|
||||
if (!e.shiftKey || selectedIndex === undefined) return
|
||||
if (e.key !== 'ArrowUp' && e.key !== 'ArrowDown') return
|
||||
if (customServersList) return
|
||||
e.preventDefault()
|
||||
e.stopImmediatePropagation()
|
||||
|
||||
const newIndex = e.key === 'ArrowUp'
|
||||
? Math.max(0, selectedIndex - 1)
|
||||
: Math.min(serversList.length - 1, selectedIndex + 1)
|
||||
: Math.min(serversListDisplay.length - 1, selectedIndex + 1)
|
||||
|
||||
if (newIndex === selectedIndex) return
|
||||
|
||||
// Move server in the list
|
||||
const newList = [...serversList]
|
||||
const newList = [...serversListDisplay]
|
||||
const oldItem = newList[selectedIndex]
|
||||
newList[selectedIndex] = newList[newIndex]
|
||||
newList[newIndex] = oldItem
|
||||
|
||||
setServersList(newList)
|
||||
appStorage.serversList = newList
|
||||
setSelectedIndex(newIndex)
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', handleKeyDown)
|
||||
return () => document.removeEventListener('keydown', handleKeyDown)
|
||||
}, [selectedIndex, serversList])
|
||||
}, [selectedIndex, serversListDisplay])
|
||||
|
||||
useEffect(() => {
|
||||
if (customServersList) {
|
||||
setServersList(customServersList.map(row => {
|
||||
const [ip, name] = row.split(' ')
|
||||
const [_ip, _port, version] = ip.split(':')
|
||||
return {
|
||||
ip,
|
||||
versionOverride: version,
|
||||
name,
|
||||
}
|
||||
}))
|
||||
}
|
||||
}, [customServersList])
|
||||
|
||||
useDidUpdateEffect(() => {
|
||||
// save data only on user changes
|
||||
setNewServersList(serversList)
|
||||
}, [serversList])
|
||||
|
||||
const serversListSorted = useMemo(() => serversList.map((server, index) => ({ ...server, index })), [serversList])
|
||||
const serversListSorted = useMemo(() => serversListDisplay.map((server, index) => ({ ...server, index })), [serversListDisplay])
|
||||
// by lastJoined
|
||||
// const serversListSorted = useMemo(() => {
|
||||
// return serversList.map((server, index) => ({ ...server, index })).sort((a, b) => (b.lastJoined ?? 0) - (a.lastJoined ?? 0))
|
||||
|
|
@ -182,10 +139,10 @@ const Inner = ({ hidden, customServersList }: { hidden?: boolean, customServersL
|
|||
offline: false
|
||||
}
|
||||
} else {
|
||||
data = await fetchServerStatus(server.ip/* , signal */) // DONT ADD SIGNAL IT WILL CRUSH JS RUNTIME
|
||||
data = await fetchServerStatus(server.ip, /* signal */undefined, server.versionOverride) // DONT ADD SIGNAL IT WILL CRUSH JS RUNTIME
|
||||
}
|
||||
if (data) {
|
||||
setAdditionalData(old => ({
|
||||
setAdditionalServerData(old => ({
|
||||
...old,
|
||||
[server.ip]: data
|
||||
}))
|
||||
|
|
@ -224,7 +181,7 @@ const Inner = ({ hidden, customServersList }: { hidden?: boolean, customServersL
|
|||
}, [isEditScreenModal])
|
||||
|
||||
useCopyKeybinding(() => {
|
||||
const item = serversList[selectedIndex]
|
||||
const item = serversListDisplay[selectedIndex]
|
||||
if (!item) return
|
||||
let str = `${item.ip}`
|
||||
if (item.versionOverride) {
|
||||
|
|
@ -236,8 +193,8 @@ const Inner = ({ hidden, customServersList }: { hidden?: boolean, customServersL
|
|||
const editModalJsx = isEditScreenModal ? <AddServerOrConnect
|
||||
allowAutoConnect={miscUiState.appConfig?.allowAutoConnect}
|
||||
placeholders={{
|
||||
proxyOverride: selectedProxy,
|
||||
usernameOverride: defaultUsername,
|
||||
proxyOverride: getCurrentProxy(),
|
||||
usernameOverride: getCurrentUsername(),
|
||||
}}
|
||||
parseQs={!serverEditScreen}
|
||||
onBack={() => {
|
||||
|
|
@ -247,12 +204,13 @@ const Inner = ({ hidden, customServersList }: { hidden?: boolean, customServersL
|
|||
if (!serverEditScreen) return
|
||||
if (serverEditScreen === true) {
|
||||
const server: StoreServerItem = { ...info, lastJoined: Date.now() } // so it appears first
|
||||
setServersList(old => [...old, server])
|
||||
appStorage.serversList = [server, ...(appStorage.serversList ?? serversListDisplay)]
|
||||
} else {
|
||||
const index = serversList.indexOf(serverEditScreen)
|
||||
const { lastJoined } = serversList[index]
|
||||
serversList[index] = { ...info, lastJoined }
|
||||
setServersList([...serversList])
|
||||
const index = appStorage.serversList?.indexOf(serverEditScreen)
|
||||
if (index !== undefined) {
|
||||
const { lastJoined } = appStorage.serversList![index]
|
||||
appStorage.serversList![index] = { ...info, lastJoined }
|
||||
}
|
||||
}
|
||||
setServerEditScreen(null)
|
||||
}}
|
||||
|
|
@ -262,9 +220,9 @@ const Inner = ({ hidden, customServersList }: { hidden?: boolean, customServersL
|
|||
} : serverEditScreen}
|
||||
onQsConnect={(info) => {
|
||||
const connectOptions: ConnectOptions = {
|
||||
username: info.usernameOverride || defaultUsername,
|
||||
username: info.usernameOverride || getCurrentUsername() || '',
|
||||
server: normalizeIp(info.ip),
|
||||
proxy: info.proxyOverride || selectedProxy,
|
||||
proxy: info.proxyOverride || getCurrentProxy(),
|
||||
botVersion: info.versionOverride,
|
||||
ignoreQs: true,
|
||||
}
|
||||
|
|
@ -304,11 +262,11 @@ const Inner = ({ hidden, customServersList }: { hidden?: boolean, customServersL
|
|||
}
|
||||
|
||||
const lastJoinedUsername = serversListSorted.find(s => s.usernameOverride)?.usernameOverride
|
||||
let username = overrides.usernameOverride || defaultUsername
|
||||
let username = overrides.usernameOverride || getCurrentUsername() || ''
|
||||
if (!username) {
|
||||
username = prompt('Username', lastJoinedUsername || '')
|
||||
if (!username) return
|
||||
setDefaultUsername(username)
|
||||
const promptUsername = prompt('Enter username', lastJoinedUsername || '')
|
||||
if (!promptUsername) return
|
||||
username = promptUsername
|
||||
}
|
||||
let authenticatedAccount: AuthenticatedAccount | true | undefined
|
||||
if (overrides.authenticatedAccountOverride) {
|
||||
|
|
@ -321,15 +279,15 @@ const Inner = ({ hidden, customServersList }: { hidden?: boolean, customServersL
|
|||
const options = {
|
||||
username,
|
||||
server: normalizeIp(ip),
|
||||
proxy: overrides.proxyOverride || selectedProxy,
|
||||
proxy: overrides.proxyOverride || getCurrentProxy(),
|
||||
botVersion: overrides.versionOverride ?? /* legacy */ overrides['version'],
|
||||
ignoreQs: true,
|
||||
autoLoginPassword: server?.autoLogin?.[username],
|
||||
authenticatedAccount,
|
||||
saveServerToHistory: shouldSave,
|
||||
onSuccessfulPlay () {
|
||||
if (shouldSave && !serversList.some(s => s.ip === ip)) {
|
||||
const newServersList: StoreServerItem[] = [...serversList, {
|
||||
if (shouldSave && !serversListDisplay.some(s => s.ip === ip)) {
|
||||
const newServersList: StoreServerItem[] = [...serversListDisplay, {
|
||||
ip,
|
||||
lastJoined: Date.now(),
|
||||
versionOverride: overrides.versionOverride,
|
||||
|
|
@ -341,10 +299,10 @@ const Inner = ({ hidden, customServersList }: { hidden?: boolean, customServersL
|
|||
|
||||
if (shouldSave === undefined) { // loading saved
|
||||
// find and update
|
||||
const server = serversList.find(s => s.ip === ip)
|
||||
const server = serversListDisplay.find(s => s.ip === ip)
|
||||
if (server) {
|
||||
// move to top
|
||||
const newList = [...serversList]
|
||||
const newList = [...serversListDisplay]
|
||||
const index = newList.indexOf(server)
|
||||
const thisItem = newList[index]
|
||||
newList.splice(index, 1)
|
||||
|
|
@ -352,40 +310,31 @@ const Inner = ({ hidden, customServersList }: { hidden?: boolean, customServersL
|
|||
|
||||
server.lastJoined = Date.now()
|
||||
server.numConnects = (server.numConnects || 0) + 1
|
||||
setNewServersList(serversList)
|
||||
setNewServersList(newList)
|
||||
}
|
||||
}
|
||||
|
||||
// save new selected proxy (if new)
|
||||
if (!proxies.includes(selectedProxy)) {
|
||||
// setProxies([...proxies, selectedProxy])
|
||||
localStorage.setItem('proxies', JSON.stringify([...proxies, selectedProxy]))
|
||||
}
|
||||
saveNewProxy()
|
||||
},
|
||||
serverIndex: shouldSave ? serversList.length.toString() : indexOrIp // assume last
|
||||
serverIndex: shouldSave ? serversListDisplay.length.toString() : indexOrIp // assume last
|
||||
} satisfies ConnectOptions
|
||||
dispatchEvent(new CustomEvent('connect', { detail: options }))
|
||||
// qsOptions
|
||||
}}
|
||||
lockedEditing={!!customServersList}
|
||||
username={defaultUsername}
|
||||
setUsername={setDefaultUsername}
|
||||
setQuickConnectIp={setQuickConnectIp}
|
||||
onProfileClick={async () => {
|
||||
const username = await showOptionsModal('Select authenticated account to remove', authenticatedAccounts.map(a => a.username))
|
||||
if (!username) return
|
||||
setAuthenticatedAccounts(authenticatedAccounts.filter(a => a.username !== username))
|
||||
appStorage.authenticatedAccounts = authenticatedAccounts.filter(a => a.username !== username)
|
||||
}}
|
||||
onWorldAction={(action, index) => {
|
||||
const server = serversList[index]
|
||||
const server = serversListDisplay[index]
|
||||
if (!server) return
|
||||
|
||||
if (action === 'edit') {
|
||||
setServerEditScreen(server)
|
||||
}
|
||||
if (action === 'delete') {
|
||||
setServersList(old => old.filter(s => s !== server))
|
||||
appStorage.serversList = appStorage.serversList!.filter(s => s !== server)
|
||||
}
|
||||
}}
|
||||
onGeneralAction={(action) => {
|
||||
|
|
@ -397,7 +346,7 @@ const Inner = ({ hidden, customServersList }: { hidden?: boolean, customServersL
|
|||
}
|
||||
}}
|
||||
worldData={serversListSorted.map(server => {
|
||||
const additional = additionalData[server.ip]
|
||||
const additional = additionalServerData[server.ip]
|
||||
return {
|
||||
name: server.index.toString(),
|
||||
title: server.name || server.ip,
|
||||
|
|
@ -407,30 +356,14 @@ const Inner = ({ hidden, customServersList }: { hidden?: boolean, customServersL
|
|||
worldNameRightGrayed: additional?.textNameRightGrayed ?? '',
|
||||
iconSrc: additional?.icon,
|
||||
offline: additional?.offline,
|
||||
group: 'Custom Servers'
|
||||
group: customServersList ? 'Provided Servers' : 'Saved Servers'
|
||||
}
|
||||
})}
|
||||
initialProxies={{
|
||||
proxies,
|
||||
selected: selectedProxy,
|
||||
}}
|
||||
updateProxies={({ proxies, selected }) => {
|
||||
// new proxy is saved in joinServer
|
||||
setProxies(proxies)
|
||||
setSelectedProxy(selected)
|
||||
}}
|
||||
hidden={hidden}
|
||||
onRowSelect={(_, i) => {
|
||||
setSelectedIndex(i)
|
||||
}}
|
||||
selectedRow={selectedIndex}
|
||||
serverHistory={getServerConnectionHistory()
|
||||
.sort((a, b) => b.numConnects - a.numConnects)
|
||||
.map(server => ({
|
||||
ip: server.ip,
|
||||
versionOverride: server.version,
|
||||
numConnects: server.numConnects
|
||||
}))}
|
||||
/>
|
||||
return <>
|
||||
{serversListJsx}
|
||||
|
|
@ -439,6 +372,7 @@ const Inner = ({ hidden, customServersList }: { hidden?: boolean, customServersL
|
|||
}
|
||||
|
||||
export default () => {
|
||||
const serversListQs = appQueryParams.serversList
|
||||
const [customServersList, setCustomServersList] = useState<string[] | undefined>(serversListQs ? [] : undefined)
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -455,7 +389,7 @@ export default () => {
|
|||
setCustomServersList(serversListQs.split(','))
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
}, [serversListQs])
|
||||
|
||||
const modalStack = useSnapshot(activeModalStack)
|
||||
const hasServersListModal = modalStack.some(x => x.reactType === 'serversList')
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@
|
|||
|
||||
.action-bar {
|
||||
position: fixed;
|
||||
bottom: 20%;
|
||||
bottom: calc(var(--safe-area-inset-bottom) + 60px);
|
||||
left: 0;
|
||||
right: 0;
|
||||
font-size: 0.7rem;
|
||||
|
|
|
|||
135
src/react/appStorageProvider.ts
Normal file
135
src/react/appStorageProvider.ts
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
import { proxy, ref, subscribe } from 'valtio'
|
||||
import { UserOverridesConfig } from 'contro-max/build/types/store'
|
||||
import { subscribeKey } from 'valtio/utils'
|
||||
import { CustomCommand } from './KeybindingsCustom'
|
||||
import { AuthenticatedAccount } from './serversStorage'
|
||||
import type { BaseServerInfo } from './AddServerOrConnect'
|
||||
|
||||
// when opening html file locally in browser, localStorage is shared between all ever opened html files, so we try to avoid conflicts
|
||||
const localStoragePrefix = process.env?.SINGLE_FILE_BUILD ? 'minecraft-web-client:' : ''
|
||||
|
||||
export interface SavedProxiesData {
|
||||
proxies: string[]
|
||||
selected: string
|
||||
}
|
||||
|
||||
export interface ServerHistoryEntry {
|
||||
ip: string
|
||||
version?: string
|
||||
numConnects: number
|
||||
lastConnected: number
|
||||
}
|
||||
|
||||
export interface StoreServerItem extends BaseServerInfo {
|
||||
lastJoined?: number
|
||||
description?: string
|
||||
optionsOverride?: Record<string, any>
|
||||
autoLogin?: Record<string, string>
|
||||
numConnects?: number // Track number of connections
|
||||
}
|
||||
|
||||
type StorageData = {
|
||||
customCommands: Record<string, CustomCommand> | undefined
|
||||
username: string | undefined
|
||||
keybindings: UserOverridesConfig | undefined
|
||||
options: any
|
||||
proxiesData: SavedProxiesData | undefined
|
||||
serversHistory: ServerHistoryEntry[]
|
||||
authenticatedAccounts: AuthenticatedAccount[]
|
||||
serversList: StoreServerItem[] | undefined
|
||||
}
|
||||
|
||||
const oldKeysAliases: Partial<Record<keyof StorageData, string>> = {
|
||||
serversHistory: 'serverConnectionHistory',
|
||||
}
|
||||
|
||||
const migrateLegacyData = () => {
|
||||
const proxies = localStorage.getItem('proxies')
|
||||
const selectedProxy = localStorage.getItem('selectedProxy')
|
||||
if (proxies && selectedProxy) {
|
||||
appStorage.proxiesData = {
|
||||
proxies: JSON.parse(proxies),
|
||||
selected: selectedProxy,
|
||||
}
|
||||
}
|
||||
|
||||
const username = localStorage.getItem('username')
|
||||
if (username && !username.startsWith('"')) {
|
||||
appStorage.username = username
|
||||
}
|
||||
|
||||
const serversHistoryLegacy = localStorage.getItem('serverConnectionHistory')
|
||||
if (serversHistoryLegacy) {
|
||||
appStorage.serversHistory = JSON.parse(serversHistoryLegacy)
|
||||
}
|
||||
localStorage.removeItem('proxies')
|
||||
localStorage.removeItem('selectedProxy')
|
||||
localStorage.removeItem('serverConnectionHistory')
|
||||
}
|
||||
|
||||
const defaultStorageData: StorageData = {
|
||||
customCommands: undefined,
|
||||
username: undefined,
|
||||
keybindings: undefined,
|
||||
options: {},
|
||||
proxiesData: undefined,
|
||||
serversHistory: [],
|
||||
authenticatedAccounts: [],
|
||||
serversList: undefined,
|
||||
}
|
||||
|
||||
export const setStorageDataOnAppConfigLoad = () => {
|
||||
appStorage.username ??= `mcrafter${Math.floor(Math.random() * 1000)}`
|
||||
}
|
||||
|
||||
export const appStorage = proxy({ ...defaultStorageData })
|
||||
window.appStorage = appStorage
|
||||
|
||||
// Restore data from localStorage
|
||||
for (const key of Object.keys(defaultStorageData)) {
|
||||
const prefixedKey = `${localStoragePrefix}${key}`
|
||||
const aliasedKey = oldKeysAliases[key]
|
||||
const storedValue = localStorage.getItem(prefixedKey) ?? (aliasedKey ? localStorage.getItem(aliasedKey) : undefined)
|
||||
if (storedValue) {
|
||||
try {
|
||||
const parsed = JSON.parse(storedValue)
|
||||
// appStorage[key] = parsed && typeof parsed === 'object' ? ref(parsed) : parsed
|
||||
appStorage[key] = parsed
|
||||
} catch (e) {
|
||||
console.error(`Failed to parse stored value for ${key}:`, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const saveKey = (key: keyof StorageData) => {
|
||||
const prefixedKey = `${localStoragePrefix}${key}`
|
||||
const value = appStorage[key]
|
||||
if (value === undefined) {
|
||||
localStorage.removeItem(prefixedKey)
|
||||
} else {
|
||||
localStorage.setItem(prefixedKey, JSON.stringify(value))
|
||||
}
|
||||
}
|
||||
|
||||
subscribe(appStorage, (ops) => {
|
||||
for (const op of ops) {
|
||||
const [type, path, value] = op
|
||||
const key = path[0]
|
||||
saveKey(key as keyof StorageData)
|
||||
}
|
||||
})
|
||||
// Subscribe to changes and save to localStorage
|
||||
|
||||
export const resetAppStorage = () => {
|
||||
for (const key of Object.keys(appStorage)) {
|
||||
appStorage[key as keyof StorageData] = defaultStorageData[key as keyof StorageData]
|
||||
}
|
||||
|
||||
for (const key of Object.keys(localStorage)) {
|
||||
if (key.startsWith(localStoragePrefix)) {
|
||||
localStorage.removeItem(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
migrateLegacyData()
|
||||
|
|
@ -1,16 +1,10 @@
|
|||
import { appQueryParams } from '../appParams'
|
||||
import { miscUiState } from '../globalState'
|
||||
import { BaseServerInfo } from './AddServerOrConnect'
|
||||
import { appStorage, StoreServerItem } from './appStorageProvider'
|
||||
|
||||
const serversListQs = appQueryParams.serversList
|
||||
|
||||
export interface StoreServerItem extends BaseServerInfo {
|
||||
lastJoined?: number
|
||||
description?: string
|
||||
optionsOverride?: Record<string, any>
|
||||
autoLogin?: Record<string, string>
|
||||
numConnects?: number // Track number of connections
|
||||
}
|
||||
export interface AuthenticatedAccount {
|
||||
// type: 'microsoft'
|
||||
username: string
|
||||
|
|
@ -19,6 +13,7 @@ export interface AuthenticatedAccount {
|
|||
expiresOn: number
|
||||
}
|
||||
}
|
||||
|
||||
export interface ServerConnectionHistory {
|
||||
ip: string
|
||||
numConnects: number
|
||||
|
|
@ -28,7 +23,7 @@ export interface ServerConnectionHistory {
|
|||
|
||||
export function updateServerConnectionHistory (ip: string, version?: string) {
|
||||
try {
|
||||
const history: ServerConnectionHistory[] = JSON.parse(localStorage.getItem('serverConnectionHistory') || '[]')
|
||||
const history = [...(appStorage.serversHistory ?? [])]
|
||||
const existingServer = history.find(s => s.ip === ip)
|
||||
if (existingServer) {
|
||||
existingServer.numConnects++
|
||||
|
|
@ -42,53 +37,36 @@ export function updateServerConnectionHistory (ip: string, version?: string) {
|
|||
version
|
||||
})
|
||||
}
|
||||
localStorage.setItem('serverConnectionHistory', JSON.stringify(history))
|
||||
appStorage.serversHistory = history
|
||||
} catch (err) {
|
||||
console.error('Failed to update server connection history:', err)
|
||||
}
|
||||
}
|
||||
|
||||
export const updateLoadedServerData = (callback: (data: StoreServerItem) => StoreServerItem, index = miscUiState.loadedServerIndex) => {
|
||||
if (!index) index = miscUiState.loadedServerIndex
|
||||
if (!index) return
|
||||
// function assumes component is not mounted to avoid sync issues after save
|
||||
const servers = getInitialServersList()
|
||||
if (index === undefined) index = miscUiState.loadedServerIndex
|
||||
if (index === undefined) return
|
||||
|
||||
const servers = [...(appStorage.serversList ?? [])]
|
||||
const server = servers[index]
|
||||
if (!server) return
|
||||
servers[index] = callback(server)
|
||||
setNewServersList(servers)
|
||||
}
|
||||
|
||||
export const setNewServersList = (serversList: StoreServerItem[], force = false) => {
|
||||
if (serversListQs && !force) return
|
||||
localStorage['serversList'] = JSON.stringify(serversList)
|
||||
|
||||
// cleanup legacy
|
||||
localStorage.removeItem('serverHistory')
|
||||
localStorage.removeItem('server')
|
||||
localStorage.removeItem('password')
|
||||
localStorage.removeItem('version')
|
||||
appStorage.serversList = serversList
|
||||
}
|
||||
|
||||
export const getInitialServersList = () => {
|
||||
if (localStorage['serversList']) return JSON.parse(localStorage['serversList']) as StoreServerItem[]
|
||||
// If we already have servers in appStorage, use those
|
||||
if (appStorage.serversList) return appStorage.serversList
|
||||
|
||||
const servers = [] as StoreServerItem[]
|
||||
|
||||
const legacyServersList = localStorage['serverHistory'] ? JSON.parse(localStorage['serverHistory']) as string[] : null
|
||||
if (legacyServersList) {
|
||||
for (const server of legacyServersList) {
|
||||
if (!server || localStorage['server'] === server) continue
|
||||
servers.push({ ip: server, lastJoined: Date.now() })
|
||||
}
|
||||
}
|
||||
|
||||
if (localStorage['server']) {
|
||||
const legacyLastJoinedServer: StoreServerItem = {
|
||||
ip: localStorage['server'],
|
||||
versionOverride: localStorage['version'],
|
||||
lastJoined: Date.now()
|
||||
}
|
||||
servers.push(legacyLastJoinedServer)
|
||||
}
|
||||
|
||||
if (servers.length === 0) { // server list is empty, let's suggest some
|
||||
if (servers.length === 0) {
|
||||
// server list is empty, let's suggest some
|
||||
for (const server of miscUiState.appConfig?.promoteServers ?? []) {
|
||||
servers.push({
|
||||
ip: server.ip,
|
||||
|
|
@ -100,16 +78,13 @@ export const getInitialServersList = () => {
|
|||
|
||||
return servers
|
||||
}
|
||||
|
||||
export const updateAuthenticatedAccountData = (callback: (data: AuthenticatedAccount[]) => AuthenticatedAccount[]) => {
|
||||
const accounts = JSON.parse(localStorage['authenticatedAccounts'] || '[]') as AuthenticatedAccount[]
|
||||
const accounts = appStorage.authenticatedAccounts
|
||||
const newAccounts = callback(accounts)
|
||||
localStorage['authenticatedAccounts'] = JSON.stringify(newAccounts)
|
||||
appStorage.authenticatedAccounts = newAccounts
|
||||
}
|
||||
|
||||
export function getServerConnectionHistory (): ServerConnectionHistory[] {
|
||||
try {
|
||||
return JSON.parse(localStorage.getItem('serverConnectionHistory') || '[]')
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
return appStorage.serversHistory ?? []
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { appQueryParams, updateQsParam } from '../../appParams'
|
|||
export const packetsReplayState = proxy({
|
||||
packetsPlayback: [] as PacketData[],
|
||||
isOpen: false,
|
||||
isMinimized: false,
|
||||
replayName: '',
|
||||
isPlaying: false,
|
||||
progress: {
|
||||
|
|
|
|||
|
|
@ -1,13 +0,0 @@
|
|||
import { CustomCommand } from './KeybindingsCustom'
|
||||
|
||||
type StorageData = {
|
||||
customCommands: Record<string, CustomCommand>
|
||||
// ...
|
||||
}
|
||||
|
||||
export const getStoredValue = <T extends keyof StorageData> (name: T): StorageData[T] | undefined => {
|
||||
return localStorage[name] ? JSON.parse(localStorage[name]) : undefined
|
||||
}
|
||||
export const setStoredValue = <T extends keyof StorageData> (name: T, value: StorageData[T]) => {
|
||||
localStorage[name] = JSON.stringify(value)
|
||||
}
|
||||
|
|
@ -128,32 +128,36 @@ const InGameUi = () => {
|
|||
<RobustPortal to={document.querySelector('#ui-root')}>
|
||||
{/* apply scaling */}
|
||||
<div style={{ display: showUI ? 'block' : 'none' }}>
|
||||
<GameInteractionOverlay zIndex={7} />
|
||||
{!disabledUiParts.includes('death-screen') && <DeathScreenProvider />}
|
||||
{!disabledUiParts.includes('debug-overlay') && <DebugOverlay />}
|
||||
{!disabledUiParts.includes('mobile-top-buttons') && <MobileTopButtons />}
|
||||
{!disabledUiParts.includes('players-list') && <PlayerListOverlayProvider />}
|
||||
{!disabledUiParts.includes('chat') && <ChatProvider />}
|
||||
<SoundMuffler />
|
||||
{showMinimap !== 'never' && <MinimapProvider adapter={adapter} displayMode='minimapOnly' />}
|
||||
{!disabledUiParts.includes('title') && <TitleProvider />}
|
||||
{!disabledUiParts.includes('scoreboard') && <ScoreboardProvider />}
|
||||
{!disabledUiParts.includes('effects-indicators') && <IndicatorEffectsProvider />}
|
||||
{!disabledUiParts.includes('crosshair') && <Crosshair />}
|
||||
{!disabledUiParts.includes('books') && <BookProvider />}
|
||||
{!disabledUiParts.includes('bossbars') && displayBossBars && <BossBarOverlayProvider />}
|
||||
<PerComponentErrorBoundary>
|
||||
<GameInteractionOverlay zIndex={7} />
|
||||
{!disabledUiParts.includes('death-screen') && <DeathScreenProvider />}
|
||||
{!disabledUiParts.includes('debug-overlay') && <DebugOverlay />}
|
||||
{!disabledUiParts.includes('mobile-top-buttons') && <MobileTopButtons />}
|
||||
{!disabledUiParts.includes('players-list') && <PlayerListOverlayProvider />}
|
||||
{!disabledUiParts.includes('chat') && <ChatProvider />}
|
||||
<SoundMuffler />
|
||||
{showMinimap !== 'never' && <MinimapProvider adapter={adapter} displayMode='minimapOnly' />}
|
||||
{!disabledUiParts.includes('title') && <TitleProvider />}
|
||||
{!disabledUiParts.includes('scoreboard') && <ScoreboardProvider />}
|
||||
{!disabledUiParts.includes('effects-indicators') && <IndicatorEffectsProvider />}
|
||||
{!disabledUiParts.includes('crosshair') && <Crosshair />}
|
||||
{!disabledUiParts.includes('books') && <BookProvider />}
|
||||
{!disabledUiParts.includes('bossbars') && displayBossBars && <BossBarOverlayProvider />}
|
||||
</PerComponentErrorBoundary>
|
||||
</div>
|
||||
|
||||
<PauseScreen />
|
||||
<MineflayerPluginHud />
|
||||
<MineflayerPluginConsole />
|
||||
{showUI && <TouchInteractionHint />}
|
||||
<div style={{ display: showUI ? 'block' : 'none' }}>
|
||||
{!disabledUiParts.includes('xp-bar') && <XPBarProvider />}
|
||||
{!disabledUiParts.includes('hud-bars') && <HudBarsProvider />}
|
||||
<BedTime />
|
||||
</div>
|
||||
{showUI && !disabledUiParts.includes('hotbar') && <HotbarRenderApp />}
|
||||
<PerComponentErrorBoundary>
|
||||
<PauseScreen />
|
||||
<MineflayerPluginHud />
|
||||
<MineflayerPluginConsole />
|
||||
{showUI && <TouchInteractionHint />}
|
||||
<div style={{ display: showUI ? 'block' : 'none' }}>
|
||||
{!disabledUiParts.includes('xp-bar') && <XPBarProvider />}
|
||||
{!disabledUiParts.includes('hud-bars') && <HudBarsProvider />}
|
||||
<BedTime />
|
||||
</div>
|
||||
{showUI && !disabledUiParts.includes('hotbar') && <HotbarRenderApp />}
|
||||
</PerComponentErrorBoundary>
|
||||
</RobustPortal>
|
||||
<PerComponentErrorBoundary>
|
||||
<SignEditorProvider />
|
||||
|
|
|
|||
|
|
@ -449,26 +449,34 @@ export const onAppLoad = () => {
|
|||
customEvents.on('mineflayerBotCreated', () => {
|
||||
// todo also handle resourcePack
|
||||
const handleResourcePackRequest = async (packet) => {
|
||||
const start = Date.now()
|
||||
console.log('Received resource pack request', packet)
|
||||
if (options.serverResourcePacks === 'never') return
|
||||
const promptMessagePacket = ('promptMessage' in packet && packet.promptMessage) ? packet.promptMessage : undefined
|
||||
const promptMessageText = promptMessagePacket ? '' : 'Do you want to use server resource pack?'
|
||||
// TODO!
|
||||
const hash = 'hash' in packet ? packet.hash : '-'
|
||||
const forced = 'forced' in packet ? packet.forced : false
|
||||
const choice = options.serverResourcePacks === 'always'
|
||||
? true
|
||||
: await showOptionsModal(promptMessageText, ['Download & Install (recommended)', 'Pretend Installed (not recommended)'], {
|
||||
cancel: !forced,
|
||||
minecraftJsonMessage: promptMessagePacket,
|
||||
const choice = options.serverResourcePacks === 'never'
|
||||
? false
|
||||
: options.serverResourcePacks === 'always'
|
||||
? true
|
||||
: await showOptionsModal(promptMessageText, ['Download & Install (recommended)', 'Pretend Installed (not recommended)'], {
|
||||
cancel: !forced,
|
||||
minecraftJsonMessage: promptMessagePacket,
|
||||
})
|
||||
if (Date.now() - start < 700) { // wait for state protocol switch
|
||||
await new Promise(resolve => {
|
||||
setTimeout(resolve, 700)
|
||||
})
|
||||
}
|
||||
if (choice === false) {
|
||||
bot.acceptResourcePack()
|
||||
return
|
||||
}
|
||||
if (!choice) {
|
||||
bot.denyResourcePack()
|
||||
return
|
||||
}
|
||||
await new Promise(resolve => {
|
||||
setTimeout(resolve, 500)
|
||||
})
|
||||
console.log('accepting resource pack')
|
||||
bot.acceptResourcePack()
|
||||
if (choice === true || choice === 'Download & Install (recommended)') {
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@ export const pointerLock = {
|
|||
}
|
||||
|
||||
export const isInRealGameSession = () => {
|
||||
return isGameActive(true) && !packetsReplayState.isOpen && !gameAdditionalState.viewerConnection
|
||||
return isGameActive(true) && (!packetsReplayState.isOpen || packetsReplayState.isMinimized) && !gameAdditionalState.viewerConnection
|
||||
}
|
||||
|
||||
window.getScreenRefreshRate = getScreenRefreshRate
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue