diff --git a/config.json b/config.json index e4f860606..7813b5911 100644 --- a/config.json +++ b/config.json @@ -3,6 +3,8 @@ "defaultHost": "", "defaultProxy": "proxy.mcraft.fun", "mapsProvider": "https://maps.mcraft.fun/", + "peerJsServer": "", + "peerJsServerFallback": "https://p2p.mcraft.fun", "promoteServers": [ { "ip": "kaboom.pw", @@ -10,9 +12,9 @@ "description": "Chaos and destruction server. Free for everyone." }, { - "ip": "go.mineberry.org", + "ip": "play.applemc.fun", "version": "1.18.2", - "description": "One of the best servers here. Join now!" + "description": "Very nice server. Try it now!" }, { "ip": "sus.shhnowisnottheti.me", diff --git a/package.json b/package.json index 4b41660a6..f8bd15576 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,7 @@ "@floating-ui/react": "^0.26.1", "@mui/base": "5.0.0-beta.40", "@nxg-org/mineflayer-auto-jump": "^0.7.12", - "@nxg-org/mineflayer-tracker": "^1.2.3", + "@nxg-org/mineflayer-tracker": "1.2.1", "@react-oauth/google": "^0.12.1", "@stylistic/eslint-plugin": "^2.6.1", "@types/gapi": "^0.0.47", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 05b83f3cf..6aca0d129 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -48,8 +48,8 @@ importers: specifier: ^0.7.12 version: 0.7.12 '@nxg-org/mineflayer-tracker': - specifier: ^1.2.3 - version: 1.2.3 + specifier: 1.2.1 + version: 1.2.1 '@react-oauth/google': specifier: ^0.12.1 version: 0.12.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0) @@ -2116,8 +2116,8 @@ packages: '@nxg-org/mineflayer-physics-util@1.5.8': resolution: {integrity: sha512-KmCkAqpUo8BbuRdIBs6+V2hWHehz++PRz3lRwIsb47CuG0u4sgLYh37RY3ifAznC6uWvmPK+q3B4ZXwJzPy1MQ==} - '@nxg-org/mineflayer-tracker@1.2.3': - resolution: {integrity: sha512-E7Ik/scU117Rr6kQUHHMBk8qOGh63YlTCGN33jMfeP7L8xmLeSHN3JtV/fbog8Y+R+HgO99yfZiRAaV7z1T6gQ==} + '@nxg-org/mineflayer-tracker@1.2.1': + resolution: {integrity: sha512-SI1ffF8zvg3/ZNE021Ja2W0FZPN+WbQDZf8yFqOcXtPRXAtM9W6HvoACdzXep8BZid7WYgYLIgjKpB+9RqvCNQ==} '@nxg-org/mineflayer-trajectories@1.1.1': resolution: {integrity: sha512-X103KXlX8+L3uMeK4jQxMUdTizv01sQRSfBizAF/iOAdfQZehRLXr3CYKeJzfwPYGLN0X0JCl++cMEcZVn4vbg==} @@ -5372,7 +5372,7 @@ packages: resolution: {integrity: sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==} engines: {node: '>= 4.0'} os: [darwin] - deprecated: The v1 package contains DANGEROUS / INSECURE binaries. Upgrade to safe fsevents v2 + deprecated: Upgrade to fsevents v2 to mitigate potential security issues fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} @@ -7747,6 +7747,7 @@ packages: range@0.0.3: resolution: {integrity: sha512-OxK2nY2bmeEB4NxoBraQIBOOeOIxoBvm6yt8MA1kLappgkG3SyLf173iOtT5woWycrtESDD2g0Nl2yt8YPoUnw==} engines: {node: '>=0.8'} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. raw-body@2.5.1: resolution: {integrity: sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==} @@ -8217,6 +8218,10 @@ packages: resolution: {integrity: sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==} engines: {node: '>= 0.8.0'} + send@0.19.0: + resolution: {integrity: sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==} + engines: {node: '>= 0.8.0'} + sentence-case@3.0.4: resolution: {integrity: sha512-8LS0JInaQMCRoQ7YUytAo/xUu5W2XnQxV2HI/6uM6U7CITS1RqPElr30V6uIqyMKM9lJGRVFy5/4CuzcixNYSg==} @@ -11523,7 +11528,7 @@ snapshots: dependencies: '@nxg-org/mineflayer-util-plugin': 1.8.3 - '@nxg-org/mineflayer-tracker@1.2.3': + '@nxg-org/mineflayer-tracker@1.2.1': dependencies: '@nxg-org/mineflayer-trajectories': 1.1.1 '@nxg-org/mineflayer-util-plugin': 1.8.3 @@ -17188,7 +17193,7 @@ snapshots: object-assign: 4.1.1 opn: 6.0.0 proxy-middleware: 0.15.0 - send: 0.18.0 + send: 0.19.0 serve-index: 1.9.1 transitivePeerDependencies: - supports-color @@ -19502,6 +19507,24 @@ snapshots: transitivePeerDependencies: - supports-color + send@0.19.0: + dependencies: + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + encodeurl: 1.0.2 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 0.5.2 + http-errors: 2.0.0 + mime: 1.6.0 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.1 + transitivePeerDependencies: + - supports-color + sentence-case@3.0.4: dependencies: no-case: 3.0.4 diff --git a/prismarine-viewer/viewer/lib/entities.ts b/prismarine-viewer/viewer/lib/entities.ts index ee659263a..c7476f6c3 100644 --- a/prismarine-viewer/viewer/lib/entities.ts +++ b/prismarine-viewer/viewer/lib/entities.ts @@ -340,6 +340,7 @@ export class Entities extends EventEmitter { } update (entity: import('prismarine-entity').Entity & { delete?; pos }, overrides) { + console.log('entity', entity) const isPlayerModel = entity.name === 'player' if (entity.name === 'zombie' || entity.name === 'zombie_villager' || entity.name === 'husk') { overrides.texture = `textures/1.16.4/entity/${entity.name === 'zombie_villager' ? 'zombie_villager/zombie_villager.png' : `zombie/${entity.name}.png`}` diff --git a/prismarine-viewer/viewer/lib/worldDataEmitter.ts b/prismarine-viewer/viewer/lib/worldDataEmitter.ts index d832d3db2..92a8ac4fb 100644 --- a/prismarine-viewer/viewer/lib/worldDataEmitter.ts +++ b/prismarine-viewer/viewer/lib/worldDataEmitter.ts @@ -5,7 +5,7 @@ import { EventEmitter } from 'events' import { generateSpiralMatrix, ViewRect } from 'flying-squid/dist/utils' import { Vec3 } from 'vec3' import { BotEvents } from 'mineflayer' -import { getItemFromBlock } from '../../../src/botUtils' +import { getItemFromBlock } from '../../../src/chatUtils' import { chunkPos } from './simpleUtils' export type ChunkPosKey = string diff --git a/src/botUtils.ts b/src/botUtils.ts index 79b10118e..f474d5325 100644 --- a/src/botUtils.ts +++ b/src/botUtils.ts @@ -1,122 +1,20 @@ -// this should actually be moved to mineflayer / prismarine-viewer +import { versionToNumber } from 'prismarine-viewer/viewer/prepare/utils' -import { fromFormattedString, TextComponent } from '@xmcl/text-component' -import type { IndexedData } from 'minecraft-data' - -export type MessageFormatPart = Pick & { - text: string - color?: string - bold?: boolean - italic?: boolean - underlined?: boolean - strikethrough?: boolean - obfuscated?: boolean -} - -type MessageInput = { - text?: string - translate?: string - with?: Array - color?: string - bold?: boolean - italic?: boolean - underlined?: boolean - strikethrough?: boolean - obfuscated?: boolean - extra?: MessageInput[] - json?: any -} - -const global = globalThis as any - -// todo move to sign-renderer, replace with prismarine-chat, fix mcData issue! -export const formatMessage = (message: MessageInput, mcData: IndexedData = global.loadedData) => { - let msglist: MessageFormatPart[] = [] - - const readMsg = (msg: MessageInput) => { - const styles = { - color: msg.color, - bold: !!msg.bold, - italic: !!msg.italic, - underlined: !!msg.underlined, - strikethrough: !!msg.strikethrough, - obfuscated: !!msg.obfuscated - } - - if (msg.text) { - msglist.push({ - ...msg, - text: msg.text, - ...styles - }) - } else if (msg.translate) { - const tText = mcData?.language[msg.translate] ?? msg.translate - - if (msg.with) { - const splitted = tText.split(/%s|%\d+\$s/g) - - let i = 0 - for (const [j, part] of splitted.entries()) { - msglist.push({ text: part, ...styles }) - - if (j + 1 < splitted.length) { - if (msg.with[i]) { - const msgWith = msg.with[i] - if (typeof msgWith === 'string') { - readMsg({ - ...styles, - text: msgWith - }) - } else { - readMsg({ - ...styles, - ...msgWith - }) - } - } - i++ - } - } - } else { - msglist.push({ - ...msg, - text: tText, - ...styles - }) - } - } - - if (msg.extra) { - for (const ex of msg.extra) { - readMsg({ ...styles, ...ex }) - } - } +export const displayClientChat = (text: string) => { + const message = { + text } - - readMsg(message) - - const flat = (msg) => { - return [msg, msg.extra?.flatMap(flat) ?? []] + if (versionToNumber(bot.version) >= versionToNumber('1.19')) { + bot._client.emit('systemChat', { + formattedMessage: JSON.stringify(message), + position: 0, + sender: 'minecraft:chat' + }) + return } - - msglist = msglist.map(msg => { - // normalize § - if (!msg.text.includes?.('§')) return msg - const newMsg = fromFormattedString(msg.text) - return flat(newMsg) - }).flat(Infinity) - - return msglist -} - -const blockToItemRemaps = { - water: 'water_bucket', - lava: 'lava_bucket', - redstone_wire: 'redstone', - tripwire: 'tripwire_hook' -} - -export const getItemFromBlock = (block: import('prismarine-block').Block) => { - const item = global.loadedData.itemsByName[blockToItemRemaps[block.name] ?? block.name] - return item + bot._client.write('chat', { + message: JSON.stringify(message), + position: 0, + sender: 'minecraft:chat' + }) } diff --git a/src/builtinCommands.ts b/src/builtinCommands.ts index 21b06ca6c..ede5480e8 100644 --- a/src/builtinCommands.ts +++ b/src/builtinCommands.ts @@ -6,6 +6,7 @@ import { closeWan, openToWanAndCopyJoinLink } from './localServerMultiplayer' import { copyFilesAsync, uniqueFileNameFromWorldName } from './browserfs' import { saveServer } from './flyingSquidUtils' import { setLoadingScreenStatus } from './utils' +import { displayClientChat } from './botUtils' const notImplemented = () => { return 'Not implemented yet' @@ -75,9 +76,7 @@ const exportLoadedWorld = async () => { window.exportWorld = exportLoadedWorld const writeText = (text) => { - bot._client.emit('chat', { - message: JSON.stringify({ text }) - }) + displayClientChat(text) } const commands: Array<{ diff --git a/src/botUtils.test.ts b/src/chatUtils.test.ts similarity index 96% rename from src/botUtils.test.ts rename to src/chatUtils.test.ts index 99aa07b45..e717da284 100644 --- a/src/botUtils.test.ts +++ b/src/chatUtils.test.ts @@ -1,6 +1,6 @@ import { test, expect } from 'vitest' import mcData from 'minecraft-data' -import { formatMessage } from './botUtils' +import { formatMessage } from './chatUtils' //@ts-expect-error globalThis.loadedData ??= mcData('1.20.1') diff --git a/src/chatUtils.ts b/src/chatUtils.ts new file mode 100644 index 000000000..e1cd7df21 --- /dev/null +++ b/src/chatUtils.ts @@ -0,0 +1,123 @@ +// this should actually be moved to mineflayer / prismarine-viewer + +import { fromFormattedString, TextComponent } from '@xmcl/text-component' +import type { IndexedData } from 'minecraft-data' +import { versionToNumber } from 'prismarine-viewer/viewer/prepare/utils' + +export type MessageFormatPart = Pick & { + text: string + color?: string + bold?: boolean + italic?: boolean + underlined?: boolean + strikethrough?: boolean + obfuscated?: boolean +} + +type MessageInput = { + text?: string + translate?: string + with?: Array + color?: string + bold?: boolean + italic?: boolean + underlined?: boolean + strikethrough?: boolean + obfuscated?: boolean + extra?: MessageInput[] + json?: any +} + +const global = globalThis as any + +// todo move to sign-renderer, replace with prismarine-chat, fix mcData issue! +export const formatMessage = (message: MessageInput, mcData: IndexedData = global.loadedData) => { + let msglist: MessageFormatPart[] = [] + + const readMsg = (msg: MessageInput) => { + const styles = { + color: msg.color, + bold: !!msg.bold, + italic: !!msg.italic, + underlined: !!msg.underlined, + strikethrough: !!msg.strikethrough, + obfuscated: !!msg.obfuscated + } + + if (msg.text) { + msglist.push({ + ...msg, + text: msg.text, + ...styles + }) + } else if (msg.translate) { + const tText = mcData?.language[msg.translate] ?? msg.translate + + if (msg.with) { + const splitted = tText.split(/%s|%\d+\$s/g) + + let i = 0 + for (const [j, part] of splitted.entries()) { + msglist.push({ text: part, ...styles }) + + if (j + 1 < splitted.length) { + if (msg.with[i]) { + const msgWith = msg.with[i] + if (typeof msgWith === 'string') { + readMsg({ + ...styles, + text: msgWith + }) + } else { + readMsg({ + ...styles, + ...msgWith + }) + } + } + i++ + } + } + } else { + msglist.push({ + ...msg, + text: tText, + ...styles + }) + } + } + + if (msg.extra) { + for (const ex of msg.extra) { + readMsg({ ...styles, ...ex }) + } + } + } + + readMsg(message) + + const flat = (msg) => { + return [msg, msg.extra?.flatMap(flat) ?? []] + } + + msglist = msglist.map(msg => { + // normalize § + if (!msg.text.includes?.('§')) return msg + const newMsg = fromFormattedString(msg.text) + return flat(newMsg) + }).flat(Infinity) + + return msglist +} + +const blockToItemRemaps = { + water: 'water_bucket', + lava: 'lava_bucket', + redstone_wire: 'redstone', + tripwire: 'tripwire_hook' +} + +export const getItemFromBlock = (block: import('prismarine-block').Block) => { + const item = global.loadedData.itemsByName[blockToItemRemaps[block.name] ?? block.name] + return item +} diff --git a/src/connect.ts b/src/connect.ts index 12a1fc5bf..40a476693 100644 --- a/src/connect.ts +++ b/src/connect.ts @@ -15,4 +15,5 @@ export type ConnectOptions = { serverIndex?: string /** If true, will show a UI to authenticate with a new account */ authenticatedAccount?: AuthenticatedAccount | true + peerOptions?: any } diff --git a/src/controls.ts b/src/controls.ts index d882e9673..6effdcd4e 100644 --- a/src/controls.ts +++ b/src/controls.ts @@ -17,10 +17,11 @@ import { customCommandsConfig } from './customCommands' import type { CustomCommand } from './react/KeybindingsCustom' import { showOptionsModal } from './react/SelectOption' import widgets from './react/widgets' -import { getItemFromBlock } from './botUtils' +import { getItemFromBlock } from './chatUtils' import { gamepadUiCursorState, moveGamepadCursorByPx } from './react/GamepadUiCursor' import { completeTexturePackInstall, resourcePackState } from './resourcePack' import { showNotification } from './react/NotificationProvider' +import { lastConnectOptions } from './react/AppStatusProvider' export const customKeymaps = proxy(JSON.parse(localStorage.keymap || '{}')) as UserOverridesConfig @@ -303,15 +304,17 @@ function lockUrl () { newQs = `loadSave=${save}` } else if (process.env.NODE_ENV === 'development') { newQs = `reconnect=1` - } else { + } else if (lastConnectOptions.value?.server) { const qs = new URLSearchParams() - const { server, version } = localStorage + const { server, botVersion } = lastConnectOptions.value qs.set('server', server) - if (version) qs.set('version', version) + if (botVersion) qs.set('version', botVersion) newQs = String(qs.toString()) } - window.history.replaceState({}, '', `${window.location.pathname}?${newQs}`) + if (newQs) { + window.history.replaceState({}, '', `${window.location.pathname}?${newQs}`) + } } function cycleHotbarSlot (dir: 1 | -1) { diff --git a/src/entities.ts b/src/entities.ts index de97fb578..26641f5ac 100644 --- a/src/entities.ts +++ b/src/entities.ts @@ -75,10 +75,9 @@ customEvents.on('gameLoaded', () => { const isWalking = Math.abs(speed.x) > WALKING_SPEED || Math.abs(speed.z) > WALKING_SPEED const isSprinting = Math.abs(speed.x) > SPRINTING_SPEED || Math.abs(speed.z) > SPRINTING_SPEED const newAnimation = isWalking ? (isSprinting ? 'running' : 'walking') : 'idle' - const username = e.username! - if (newAnimation !== playerPerAnimation[username]) { + if (newAnimation !== playerPerAnimation[id]) { viewer.entities.playAnimation(e.id, newAnimation) - playerPerAnimation[username] = newAnimation + playerPerAnimation[id] = newAnimation } } }) @@ -122,7 +121,7 @@ customEvents.on('gameLoaded', () => { } viewer.entities.addListener('remove', (e) => { loadedSkinEntityIds.delete(e.id) - playerPerAnimation[e.username] = '' + playerPerAnimation[e.id] = '' bot.tracker.stopTrackingEntity(e, true) }) diff --git a/src/globalState.ts b/src/globalState.ts index b0a447f21..cefa78103 100644 --- a/src/globalState.ts +++ b/src/globalState.ts @@ -109,6 +109,8 @@ export type AppConfig = { defaultProxy?: string // defaultProxySave?: string // defaultVersion?: string + peerJsServer?: string + peerJsServerFallback?: string promoteServers?: Array<{ ip, description, version? }> mapsProvider?: string } @@ -120,6 +122,7 @@ export const miscUiState = proxy({ singleplayer: false, flyingSquid: false, wanOpened: false, + wanOpening: false, /** wether game hud is shown (in playing state) */ gameLoaded: false, showUI: true, diff --git a/src/index.ts b/src/index.ts index db855960f..b26619c33 100644 --- a/src/index.ts +++ b/src/index.ts @@ -72,7 +72,7 @@ import defaultServerOptions from './defaultLocalServerOptions' import dayCycle from './dayCycle' import { onAppLoad, resourcepackReload } from './resourcePack' -import { connectToPeer } from './localServerMultiplayer' +import { ConnectPeerOptions, connectToPeer } from './localServerMultiplayer' import CustomChannelClient from './customClient' import { loadScript } from 'prismarine-viewer/viewer/lib/utils' import { registerServiceWorker } from './serviceWorker' @@ -486,7 +486,7 @@ async function connect (connectOptions: ConnectOptions) { port: server.port ? +server.port : undefined, version: connectOptions.botVersion || false, ...p2pMultiplayer ? { - stream: await connectToPeer(connectOptions.peerId!), + stream: await connectToPeer(connectOptions.peerId!, connectOptions.peerOptions), } : {}, ...singleplayer || p2pMultiplayer ? { keepAlive: false, @@ -1022,6 +1022,10 @@ downloadAndOpenFile().then((downloadAction) => { void Promise.resolve().then(() => { // try to connect to peer const peerId = qs.get('connectPeer') + const peerOptions = {} as ConnectPeerOptions + if (qs.get('server')) { + peerOptions.server = qs.get('server')! + } const version = qs.get('peerVersion') if (peerId) { let username: string | null = options.guestUsername @@ -1031,7 +1035,8 @@ downloadAndOpenFile().then((downloadAction) => { void connect({ username, botVersion: version || undefined, - peerId + peerId, + peerOptions }) } }) diff --git a/src/inventoryWindows.ts b/src/inventoryWindows.ts index 23e898a5a..bf847098b 100644 --- a/src/inventoryWindows.ts +++ b/src/inventoryWindows.ts @@ -11,14 +11,16 @@ import PItem, { Item } from 'prismarine-item' import { ItemsRenderer } from 'mc-assets/dist/itemsRenderer' import { versionToNumber } from 'prismarine-viewer/viewer/prepare/utils' import { getRenamedData } from 'flying-squid/dist/blockRenames' +import PrismarineChatLoader from 'prismarine-chat' import Generic95 from '../assets/generic_95.png' import { appReplacableResources } from './generated/resources' import { activeModalStack, hideCurrentModal, hideModal, miscUiState, showModal } from './globalState' import { options } from './optionsStorage' import { assertDefined, inGameError } from './utils' -import { MessageFormatPart } from './botUtils' +import { displayClientChat } from './botUtils' import { currentScaling } from './scaleInterface' import { getItemDescription } from './itemsDescriptions' +import { MessageFormatPart } from './chatUtils' const loadedImagesCache = new Map() const cleanLoadedImagesCache = () => { @@ -59,11 +61,7 @@ export const onGameLoad = (onLoad) => { openWindow('ChestWin') } else { // todo format - bot._client.emit('chat', { - message: JSON.stringify({ - text: `[client error] cannot open unimplemented window ${win.id} (${win.type}). Slots: ${win.slots.map(item => getItemName(item)).filter(Boolean).join(', ')}` - }) - }) + displayClientChat(`[client error] cannot open unimplemented window ${win.id} (${win.type}). Slots: ${win.slots.map(item => getItemName(item)).filter(Boolean).join(', ')}`) bot.currentWindow?.['close']() } }) @@ -288,6 +286,7 @@ const implementedContainersGuiMap = { 'minecraft:furnace': 'FurnaceWin', 'minecraft:smoker': 'FurnaceWin', 'minecraft:crafting': 'CraftingWin', + 'minecraft:crafting3x3': 'CraftingWin', // todo different result slot 'minecraft:anvil': 'AnvilWin', // enchant 'minecraft:enchanting_table': 'EnchantingWin', @@ -365,7 +364,18 @@ const openWindow = (type: string | undefined) => { cleanLoadedImagesCache() const inv = openItemsCanvas(type) inv.canvasManager.children[0].mobileHelpers = miscUiState.currentTouch - inv.canvasManager.children[0].customTitleText = bot.currentWindow?.title ? fromFormattedString(bot.currentWindow.title).text : undefined + const title = bot.currentWindow?.title + const PrismarineChat = PrismarineChatLoader(bot.version) + try { + inv.canvasManager.children[0].customTitleText = title ? + typeof title === 'string' ? + fromFormattedString(title).text : + new PrismarineChat(title).toString() : + undefined + } catch (err) { + reportError?.(err) + inv.canvasManager.children[0].customTitleText = undefined + } // todo inv.canvasManager.setScale(currentScaling.scale === 1 ? 1.5 : currentScaling.scale) inv.canvas.style.zIndex = '10' diff --git a/src/localServerMultiplayer.ts b/src/localServerMultiplayer.ts index c8e4bc5e5..7d147d0d7 100644 --- a/src/localServerMultiplayer.ts +++ b/src/localServerMultiplayer.ts @@ -19,6 +19,8 @@ class CustomDuplex extends Duplex { let peerInstance: Peer | undefined +let overridePeerJsServer = null as string | null + export const getJoinLink = () => { if (!peerInstance) return const url = new URL(window.location.href) @@ -27,6 +29,11 @@ export const getJoinLink = () => { } url.searchParams.set('connectPeer', peerInstance.id) url.searchParams.set('peerVersion', localServer!.options.version) + const host = (overridePeerJsServer ?? miscUiState.appConfig?.peerJsServer) ?? undefined + if (host) { + // TODO! use miscUiState.appConfig.peerJsServer + url.searchParams.set('server', host) + } return url.toString() } @@ -46,8 +53,12 @@ export const openToWanAndCopyJoinLink = async (writeText: (text) => void, doCopy if (doCopy) await copyJoinLink() return 'Already opened to wan. Join link copied' } + miscUiState.wanOpening = true + const host = (overridePeerJsServer ?? miscUiState.appConfig?.peerJsServer) || undefined + const params = host ? parseUrl(host) : undefined const peer = new Peer({ debug: 3, + ...params }) peerInstance = peer peer.on('connection', (connection) => { @@ -83,34 +94,91 @@ export const openToWanAndCopyJoinLink = async (writeText: (text) => void, doCopy connection.on('close', disconnected) connection.on('error', disconnected) }) + const fallbackServer = miscUiState.appConfig?.peerJsServerFallback + const hasFallback = fallbackServer && peer.options.host !== fallbackServer + let hadErrorReported = false peer.on('error', (error) => { - console.error(error) - writeText(error.message) + console.error('peerJS error', error) + if (error.type === 'server-error' && hasFallback) { + return + } + hadErrorReported = true + writeText(error.message || JSON.stringify(error)) }) - return new Promise(resolve => { + let timeout + const destroy = () => { + clearTimeout(timeout) + timeout = undefined + peer.destroy() + peerInstance = undefined + } + + const result = await new Promise(resolve => { peer.on('open', async () => { await copyJoinLink() resolve('Copied join link to clipboard') }) - setTimeout(() => { + timeout = setTimeout(() => { + if (!hadErrorReported && timeout !== undefined) { + writeText('timeout') + } resolve('Failed to open to wan (timeout)') - }, 5000) + }, 6000) + + // fallback + peer.on('error', async (error) => { + if (!peer.open) { + if (hasFallback) { + destroy() + + overridePeerJsServer = fallbackServer + console.log('Trying fallback server', fallbackServer) + resolve((await openToWanAndCopyJoinLink(writeText, doCopy))!) + } + } + }) }) + if (!peerInstance.open) { + destroy() + } + miscUiState.wanOpening = false + return result +} + +const parseUrl = (url: string) => { + // peerJS does this internally for some reason: const url = new URL(`${protocol}://${host}:${port}${path}${key}/${method}`) + if (!url.startsWith('http')) url = `${location.protocol}//${url}` + const urlObj = new URL(url) + const key = urlObj.searchParams.get('key') + return { + host: urlObj.hostname, + path: urlObj.pathname, + protocol: urlObj.protocol.slice(0, -1), + ...urlObj.port ? { port: +urlObj.port } : {}, + ...key ? { key } : {}, + } } export const closeWan = () => { - if (!peerInstance) return - peerInstance.destroy() + peerInstance?.destroy() peerInstance = undefined miscUiState.wanOpened = false - return 'Closed to wan' + return 'Closed WAN' +} + +export type ConnectPeerOptions = { + server?: string } -export const connectToPeer = async (peerId: string) => { +export const connectToPeer = async (peerId: string, options: ConnectPeerOptions = {}) => { setLoadingScreenStatus('Connecting to peer server') // todo destroy connection on error + // TODO! use miscUiState.appConfig.peerJsServer + const host = options.server + const params = host ? parseUrl(host) : undefined const peer = new Peer({ debug: 3, + ...params }) await resolveTimeout(new Promise(resolve => { peer.once('open', resolve) diff --git a/src/react/Chat.stories.tsx b/src/react/Chat.stories.tsx index 35fbc9fbd..192d5cb4e 100644 --- a/src/react/Chat.stories.tsx +++ b/src/react/Chat.stories.tsx @@ -1,7 +1,7 @@ import type { Meta, StoryObj } from '@storybook/react' import { useEffect, useState } from 'react' -import { formatMessage } from '../botUtils' +import { formatMessage } from '../chatUtils' import Chat, { fadeMessage, chatInputValueGlobal } from './Chat' import Button from './Button' diff --git a/src/react/Chat.tsx b/src/react/Chat.tsx index bd9a1714c..106b0a2f8 100644 --- a/src/react/Chat.tsx +++ b/src/react/Chat.tsx @@ -1,6 +1,6 @@ import { proxy, subscribe } from 'valtio' import { useEffect, useMemo, useRef, useState } from 'react' -import { MessageFormatPart } from '../botUtils' +import { MessageFormatPart } from '../chatUtils' import { MessagePart } from './MessageFormatted' import './Chat.css' import { isIos, reactKeyForMessage } from './utils' diff --git a/src/react/ChatProvider.tsx b/src/react/ChatProvider.tsx index 3ff39e4f0..892360ce6 100644 --- a/src/react/ChatProvider.tsx +++ b/src/react/ChatProvider.tsx @@ -1,6 +1,6 @@ import { useEffect, useMemo, useRef, useState } from 'react' import { useSnapshot } from 'valtio' -import { formatMessage } from '../botUtils' +import { formatMessage } from '../chatUtils' import { getBuiltinCommandsList, tryHandleBuiltinCommand } from '../builtinCommands' import { hideCurrentModal, loadedGameState, miscUiState } from '../globalState' import { options } from '../optionsStorage' diff --git a/src/react/DeathScreen.tsx b/src/react/DeathScreen.tsx index ea4dc7262..8f4c3f005 100644 --- a/src/react/DeathScreen.tsx +++ b/src/react/DeathScreen.tsx @@ -1,5 +1,5 @@ import './deathScreen.css' -import type { MessageFormatPart } from '../botUtils' +import type { MessageFormatPart } from '../chatUtils' import MessageFormatted from './MessageFormatted' import Button from './Button' diff --git a/src/react/DeathScreenProvider.tsx b/src/react/DeathScreenProvider.tsx index 8d5ab1c87..d3b598649 100644 --- a/src/react/DeathScreenProvider.tsx +++ b/src/react/DeathScreenProvider.tsx @@ -1,7 +1,7 @@ import { useEffect } from 'react' import { proxy, useSnapshot } from 'valtio' import { disconnect } from '../flyingSquidUtils' -import { MessageFormatPart, formatMessage } from '../botUtils' +import { MessageFormatPart, formatMessage } from '../chatUtils' import { showModal, hideModal } from '../globalState' import { options } from '../optionsStorage' import DeathScreen from './DeathScreen' diff --git a/src/react/MessageFormatted.tsx b/src/react/MessageFormatted.tsx index ebfaf2edc..3b727fc8a 100644 --- a/src/react/MessageFormatted.tsx +++ b/src/react/MessageFormatted.tsx @@ -3,7 +3,7 @@ import { render } from '@xmcl/text-component' import { noCase } from 'change-case' import mojangson from 'mojangson' import { openURL } from 'prismarine-viewer/viewer/lib/simpleUtils' -import { MessageFormatPart } from '../botUtils' +import { MessageFormatPart } from '../chatUtils' import { chatInputValueGlobal } from './Chat' import './MessageFormatted.css' diff --git a/src/react/MessageFormattedString.tsx b/src/react/MessageFormattedString.tsx index 32b8fad6a..ac261889e 100644 --- a/src/react/MessageFormattedString.tsx +++ b/src/react/MessageFormattedString.tsx @@ -1,6 +1,6 @@ import { useMemo } from 'react' import { fromFormattedString } from '@xmcl/text-component' -import { formatMessage } from '../botUtils' +import { formatMessage } from '../chatUtils' import MessageFormatted from './MessageFormatted' /** like MessageFormatted, but receives raw string or json instead, uses window.loadedData */ diff --git a/src/react/PauseScreen.tsx b/src/react/PauseScreen.tsx index 1c4fdcd4e..19f1385cb 100644 --- a/src/react/PauseScreen.tsx +++ b/src/react/PauseScreen.tsx @@ -151,7 +151,7 @@ export default () => { const isModalActive = useIsModalActive('pause-screen') const fsStateSnap = useSnapshot(fsState) const activeModalStackSnap = useSnapshot(activeModalStack) - const { singleplayer, wanOpened } = useSnapshot(miscUiState) + const { singleplayer, wanOpened, wanOpening } = useSnapshot(miscUiState) const handlePointerLockChange = () => { if (!pointerLock.hasPointerLock && activeModalStack.length === 0) { @@ -188,7 +188,10 @@ export default () => { return } if (!wanOpened || !qr) { - await openToWanAndCopyJoinLink(() => { }, !qr) + await openToWanAndCopyJoinLink((err) => { + if (!miscUiState.wanOpening) return + alert(`Something went wrong: ${err}`) + }, !qr) } if (qr) { const joinLink = getJoinLink() @@ -230,7 +233,7 @@ export default () => { {singleplayer ? (
{(navigator.share as typeof navigator.share | undefined) ? (