diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index a52e6ab95..3a898779c 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -19,6 +19,7 @@ jobs: - run: vercel pull --yes --environment=production --token=${{ secrets.VERCEL_TOKEN }} # will install + build to .vercel/output/static - run: vercel build --token=${{ secrets.VERCEL_TOKEN }} --prod + - run: pnpm build-storybook - name: Copy playground files run: node prismarine-viewer/esbuild.mjs && cp prismarine-viewer/public/index.html .vercel/output/static/playground.html && cp prismarine-viewer/public/playground.js .vercel/output/static/playground.js - name: Download Generated Sounds map diff --git a/package.json b/package.json index 45e58fff6..163e5fef1 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "test-mc-server": "tsx cypress/minecraft-server.mjs", "lint": "eslint \"{src,cypress}/**/*.{ts,js,jsx,tsx}\"", "storybook": "storybook dev -p 6006", - "build-storybook": "storybook build", + "build-storybook": "storybook build && node scripts/build.js moveStorybookFiles", "start-experiments": "vite --config experiments/vite.config.ts", "watch-worker": "node prismarine-viewer/buildWorker.mjs -w" }, @@ -98,7 +98,7 @@ "http-server": "^14.1.1", "https-browserify": "^1.0.0", "minecraft-inventory-gui": "github:zardoy/minecraft-inventory-gui#next", - "mineflayer": "github:zardoy/mineflayer#custom", + "mineflayer": "github:PrismarineJS/mineflayer", "mineflayer-pathfinder": "^2.4.4", "npm-run-all": "^4.1.5", "os-browserify": "^0.3.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f146457e1..52eca7eaa 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -226,8 +226,8 @@ importers: specifier: github:zardoy/minecraft-inventory-gui#next version: github.com/zardoy/minecraft-inventory-gui/69003692b3041d94a420a65c7d3cc1b37737e838(@types/react@18.2.20)(react@18.2.0) mineflayer: - specifier: github:zardoy/mineflayer#custom - version: github.com/zardoy/mineflayer/e828c161aab120f2d926fba48de3b4d57c361710 + specifier: github:PrismarineJS/mineflayer + version: github.com/PrismarineJS/mineflayer/5c71edf48bb2f2dfa16cddb9af5baa0c4d55cf0d mineflayer-pathfinder: specifier: ^2.4.4 version: 2.4.4 @@ -14846,6 +14846,34 @@ packages: react: 18.2.0 dev: false + github.com/PrismarineJS/mineflayer/5c71edf48bb2f2dfa16cddb9af5baa0c4d55cf0d: + resolution: {tarball: https://codeload.github.com/PrismarineJS/mineflayer/tar.gz/5c71edf48bb2f2dfa16cddb9af5baa0c4d55cf0d} + name: mineflayer + version: 4.17.0 + engines: {node: '>=18'} + dependencies: + minecraft-data: 3.58.0 + minecraft-protocol: github.com/zardoy/minecraft-protocol/436e0f2945d82408cfd1eb4262535c205bcba8d0 + prismarine-biome: 1.3.0(minecraft-data@3.58.0)(prismarine-registry@1.7.0) + prismarine-block: github.com/zardoy/prismarine-block/00cd810ca6853024b2e73ff0d405d1b1e397defc + prismarine-chat: 1.9.1 + prismarine-chunk: 1.35.0(minecraft-data@3.58.0) + prismarine-entity: 2.3.1 + prismarine-item: 1.14.0 + prismarine-nbt: 2.2.1 + prismarine-physics: 1.8.0 + prismarine-recipe: 1.3.1(prismarine-registry@1.7.0) + prismarine-registry: 1.7.0 + prismarine-windows: 2.8.0 + prismarine-world: github.com/zardoy/prismarine-world/c358222204d21fe7d45379fbfcefb047f926c786 + protodef: 1.15.0 + typed-emitter: 1.4.0 + vec3: 0.1.8 + transitivePeerDependencies: + - encoding + - supports-color + dev: true + github.com/PrismarineJS/node-process/380d0b4f4c86f1b65b216c311bf00431f314e88e: resolution: {tarball: https://codeload.github.com/PrismarineJS/node-process/tar.gz/380d0b4f4c86f1b65b216c311bf00431f314e88e} name: process @@ -14912,34 +14940,6 @@ packages: - encoding - supports-color - github.com/zardoy/mineflayer/e828c161aab120f2d926fba48de3b4d57c361710: - resolution: {tarball: https://codeload.github.com/zardoy/mineflayer/tar.gz/e828c161aab120f2d926fba48de3b4d57c361710} - name: mineflayer - version: 4.14.0 - engines: {node: '>=14'} - dependencies: - minecraft-data: 3.58.0 - minecraft-protocol: github.com/zardoy/minecraft-protocol/436e0f2945d82408cfd1eb4262535c205bcba8d0 - prismarine-biome: 1.3.0(minecraft-data@3.58.0)(prismarine-registry@1.7.0) - prismarine-block: github.com/zardoy/prismarine-block/00cd810ca6853024b2e73ff0d405d1b1e397defc - prismarine-chat: 1.9.1 - prismarine-chunk: 1.35.0(minecraft-data@3.58.0) - prismarine-entity: 2.3.1 - prismarine-item: 1.14.0 - prismarine-nbt: 2.2.1 - prismarine-physics: 1.8.0 - prismarine-recipe: 1.3.1(prismarine-registry@1.7.0) - prismarine-registry: 1.7.0 - prismarine-windows: 2.8.0 - prismarine-world: github.com/zardoy/prismarine-world/c358222204d21fe7d45379fbfcefb047f926c786 - protodef: 1.15.0 - typed-emitter: 1.4.0 - vec3: 0.1.8 - transitivePeerDependencies: - - encoding - - supports-color - dev: true - github.com/zardoy/prismarine-block/00cd810ca6853024b2e73ff0d405d1b1e397defc: resolution: {tarball: https://codeload.github.com/zardoy/prismarine-block/tar.gz/00cd810ca6853024b2e73ff0d405d1b1e397defc} name: prismarine-block diff --git a/scripts/build.js b/scripts/build.js index 547bc8114..fa6640f09 100644 --- a/scripts/build.js +++ b/scripts/build.js @@ -91,6 +91,10 @@ exports.getSwAdditionalEntries = () => { return output } +exports.moveStorybookFiles = () => { + fs.renameSync('storybook-static', 'dist/storybook') +} + const fn = require.main === module && exports[process.argv[2]] if (fn) { diff --git a/src/botUtils.ts b/src/botUtils.ts index 2df5ca49f..cc624c5b0 100644 --- a/src/botUtils.ts +++ b/src/botUtils.ts @@ -1,8 +1,8 @@ // this should actually be moved to mineflayer / prismarine-viewer -import { fromFormattedString } from '@xmcl/text-component' +import { fromFormattedString, TextComponent } from '@xmcl/text-component' -export type MessageFormatPart = { +export type MessageFormatPart = Pick & { text: string color?: string bold?: boolean diff --git a/src/controls.ts b/src/controls.ts index 43f861155..8298d5f85 100644 --- a/src/controls.ts +++ b/src/controls.ts @@ -10,7 +10,7 @@ import { isGameActive, showModal, gameAdditionalState, activeModalStack, hideCur import { goFullscreen, pointerLock, reloadChunks } from './utils' import { options } from './optionsStorage' import { openPlayerInventory } from './playerWindows' -import { initialChatOpenValue } from './react/ChatContainer' +import { chatInputValueGlobal } from './react/ChatContainer' import { fsState } from './loadSave' // doesnt seem to work for now @@ -226,7 +226,7 @@ contro.on('trigger', ({ command }) => { showModal({ reactType: 'chat' }) break case 'general.command': - initialChatOpenValue.value = '/' + chatInputValueGlobal.value = '/' showModal({ reactType: 'chat' }) break case 'general.selectItem': diff --git a/src/globalState.ts b/src/globalState.ts index e9391c830..7b6985a32 100644 --- a/src/globalState.ts +++ b/src/globalState.ts @@ -187,6 +187,9 @@ export const showNotification = (newNotification: Partial) // todo restore auto-save on interval for player data! (or implement it in flying squid since there is already auto-save for world) window.addEventListener('unload', (e) => { + if (!window.justReloaded) { + sessionStorage.justReloaded = false + } void saveServer() }) @@ -201,6 +204,10 @@ window.inspectPlayer = () => require('fs').promises.readFile('/world/playerdata/ // todo move from global state window.addEventListener('beforeunload', (event) => { + if (!window.justReloaded) { + sessionStorage.justReloaded = false + } + // todo-low maybe exclude chat? if (!isGameActive(true) && activeModalStack.at(-1)?.elem?.id !== 'chat') return if (sessionStorage.lastReload && !options.preventDevReloadWhilePlaying) return diff --git a/src/index.ts b/src/index.ts index 7c0bc0e9d..3848a96b6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -433,6 +433,7 @@ async function connect (connectOptions: { noPongTimeout: 240 * 1000, closeTimeout: 240 * 1000, respawn: options.autoRespawn, + maxCatchupTicks: 0, async versionSelectedHook (client) { // todo keep in sync with esbuild preload, expose cache ideally if (client.version === '1.20.1') { @@ -447,12 +448,12 @@ async function connect (connectOptions: { if (singleplayer || p2pMultiplayer) { // in case of p2pMultiplayer there is still flying-squid on the host side const _supportFeature = bot.supportFeature - bot.supportFeature = (feature) => { + bot.supportFeature = ((feature) => { if (unsupportedLocalServerFeatures.includes(feature)) { return false } return _supportFeature(feature) - } + }) as typeof bot.supportFeature bot.emit('inject_allowed') bot._client.emit('connect') diff --git a/src/react/Chat.stories.tsx b/src/react/Chat.stories.tsx index 32718d4d4..a24cda372 100644 --- a/src/react/Chat.stories.tsx +++ b/src/react/Chat.stories.tsx @@ -2,7 +2,7 @@ import type { Meta, StoryObj } from '@storybook/react' import { useEffect, useState } from 'react' import { formatMessage } from '../botUtils' -import Chat, { fadeMessage, initialChatOpenValue } from './ChatContainer' +import Chat, { fadeMessage, chatInputValueGlobal } from './ChatContainer' import Button from './Button' window.spamMessage = window.spamMessage ?? '' @@ -20,7 +20,7 @@ const meta: Meta = { const abortController = new AbortController() addEventListener('keyup', (e) => { if (e.code === 'KeyY') { - initialChatOpenValue.value = '/' + chatInputValueGlobal.value = '/' setOpen(true) e.stopImmediatePropagation() } @@ -103,7 +103,6 @@ export const Primary: Story = { 'underlined': false, 'strikethrough': false, 'obfuscated': false, - //@ts-expect-error 'json': { 'insertion': 'pviewer672', 'clickEvent': { @@ -129,6 +128,7 @@ export const Primary: Story = { }, 'hoverEvent': { 'action': 'show_entity', + //@ts-expect-error 'contents': { 'type': 'minecraft:player', 'id': 'ecd0eeb1-625e-3fea-b16e-cb449dcfa434', diff --git a/src/react/ChatContainer.tsx b/src/react/ChatContainer.tsx index 729ffc301..710328121 100644 --- a/src/react/ChatContainer.tsx +++ b/src/react/ChatContainer.tsx @@ -1,4 +1,5 @@ import { useUsingTouch } from '@dimaka/interface' +import { proxy, subscribe } from 'valtio' import { useEffect, useMemo, useRef, useState } from 'react' import { isCypress } from '../standaloneUtils' import { MessageFormatPart } from '../botUtils' @@ -12,7 +13,7 @@ export type Message = { faded?: boolean } -const MessageLine = ({ message }) => { +const MessageLine = ({ message }: {message: Message}) => { const classes = { 'chat-message-fadeout': message.fading, 'chat-message-fade': message.fading, @@ -35,9 +36,9 @@ type Props = { // width?: number } -export const initialChatOpenValue = { +export const chatInputValueGlobal = proxy({ value: '' -} +}) export const fadeMessage = (message: Message, initialTimeout: boolean, requestUpdate: () => void) => { setTimeout(() => { @@ -100,11 +101,18 @@ export default ({ messages, opacity = 1, fetchCompletionItems, opened, sendMessa useEffect(() => { if (opened) { - updateInputValue(initialChatOpenValue.value) - initialChatOpenValue.value = '' + updateInputValue(chatInputValueGlobal.value) + chatInputValueGlobal.value = '' if (!usingTouch) { chatInput.current.focus() } + const unsubscribe = subscribe(chatInputValueGlobal, () => { + if (!chatInputValueGlobal.value) return + updateInputValue(chatInputValueGlobal.value) + chatInputValueGlobal.value = '' + chatInput.current.focus() + }) + return unsubscribe } if (!opened && chatMessages.current) { chatMessages.current.scrollTop = chatMessages.current.scrollHeight diff --git a/src/react/DeathScreen.tsx b/src/react/DeathScreen.tsx index 4de577872..290e62859 100644 --- a/src/react/DeathScreen.tsx +++ b/src/react/DeathScreen.tsx @@ -4,7 +4,7 @@ import MessageFormatted from './MessageFormatted' import Button from './Button' type Props = { - dieReasonMessage: readonly MessageFormatPart[] + dieReasonMessage: MessageFormatPart[] respawnCallback: () => void disconnectCallback: () => void } diff --git a/src/react/DeathScreenProvider.tsx b/src/react/DeathScreenProvider.tsx index 677863d18..94b09e7b7 100644 --- a/src/react/DeathScreenProvider.tsx +++ b/src/react/DeathScreenProvider.tsx @@ -56,7 +56,7 @@ export default () => { if (!isModalActive || !dieReasonMessage || options.autoRespawn) return null return { bot._client.write('client_command', bot.supportFeature('respawnIsPayload') ? { payload: 0 } : { actionId: 0 }) }} diff --git a/src/react/MainMenu.tsx b/src/react/MainMenu.tsx index 813142452..5d240b01b 100644 --- a/src/react/MainMenu.tsx +++ b/src/react/MainMenu.tsx @@ -20,6 +20,8 @@ interface Props { const refreshApp = async () => { const registration = await navigator.serviceWorker.getRegistration() await registration?.unregister() + window.justReloaded = true + sessionStorage.justReloaded = true window.location.reload() } @@ -36,7 +38,13 @@ export default ({ connectToServerAction, mapsProvider, singleplayerAction, optio fetch('./version.txt').then(async (f) => { if (f.status === 404) return const contents = await f.text() - setVersionStatus(`(${contents === process.env.BUILD_VERSION ? 'latest' : 'new version available'})`) + const isLatest = contents === process.env.BUILD_VERSION + if (!isLatest && sessionStorage.justReloaded) { + // try to force bypass cache + location.search = '?update=true' + } + sessionStorage.justReloaded = false + setVersionStatus(`(${isLatest ? 'latest' : 'new version available'})`) setVersionTitle(`Loaded: ${process.env.BUILD_VERSION}. Remote: ${contents}`) }, () => { }) } @@ -114,7 +122,10 @@ export default ({ connectToServerAction, mapsProvider, singleplayerAction, optio
{ + setVersionStatus('(reloading)') + await refreshApp() + }} className={styles['product-info']} > Prismarine Web Client {versionStatus} diff --git a/src/react/MessageFormatted.tsx b/src/react/MessageFormatted.tsx index f4e072979..c83c70f6d 100644 --- a/src/react/MessageFormatted.tsx +++ b/src/react/MessageFormatted.tsx @@ -1,8 +1,63 @@ import { ComponentProps } from 'react' +import { render } from '@xmcl/text-component' +import { noCase } from 'change-case' import { MessageFormatPart } from '../botUtils' +import { openURL } from '../menus/components/common' +import { chatInputValueGlobal } from './ChatContainer' + +const hoverItemToText = (hoverEvent: MessageFormatPart['hoverEvent']) => { + if (!hoverEvent) return undefined + const contents = hoverEvent['contents'] ?? hoverEvent.value + if (typeof contents === 'string') return contents + // if (hoverEvent.action === 'show_text') { + // return contents + // } + if (hoverEvent.action === 'show_item') { + return contents.id + } + if (hoverEvent.action === 'show_entity') { + let str = noCase(contents.type.replace('minecraft:', '')) + if (contents.name) str += `: ${contents.name.text}` + return str + } +} + +const clickEventToProps = (clickEvent: MessageFormatPart['clickEvent']) => { + if (!clickEvent) return + if (clickEvent.action === 'run_command' || clickEvent.action === 'suggest_command') { + return { + onClick () { + chatInputValueGlobal.value = clickEvent.value + } + } + } + if (clickEvent.action === 'open_url') { + return { + onClick () { + const confirm = window.confirm(`Open ${clickEvent.value}?`) + if (confirm) { + openURL(clickEvent.value) + } + } + } + } + //@ts-expect-error todo + if (clickEvent.action === 'copy_to_clipboard') { + return { + onClick () { + navigator.clipboard.writeText(clickEvent.value) + } + } + } +} export const MessagePart = ({ part, ...props }: { part: MessageFormatPart } & ComponentProps<'span'>) => { - const { color, italic, bold, underlined, strikethrough, text } = part + + const { color, italic, bold, underlined, strikethrough, text, clickEvent, hoverEvent, obfuscated } = part + + const clickProps = clickEventToProps(clickEvent) + const hoverMessageRaw = hoverItemToText(hoverEvent) + const hoverItemText = hoverMessageRaw && typeof hoverMessageRaw !== 'string' ? render(hoverMessageRaw).children.map(child => child.component.text).join('') : hoverMessageRaw const applyStyles = [ color ? colorF(color.toLowerCase()) + `; text-shadow: 1px 1px 0px ${getColorShadow(colorF(color.toLowerCase()).replace('color:', ''))}` : messageFormatStylesMap.white, @@ -10,10 +65,11 @@ export const MessagePart = ({ part, ...props }: { part: MessageFormatPart } & Co bold && messageFormatStylesMap.bold, italic && messageFormatStylesMap.italic, underlined && messageFormatStylesMap.underlined, - strikethrough && messageFormatStylesMap.strikethrough + strikethrough && messageFormatStylesMap.strikethrough, + obfuscated && messageFormatStylesMap.obfuscated ].filter(Boolean) - return {text} + return {text} } export default ({ parts }: { parts: readonly MessageFormatPart[] }) => { @@ -69,5 +125,6 @@ export const messageFormatStylesMap = { bold: 'font-weight:900', strikethrough: 'text-decoration:line-through', underlined: 'text-decoration:underline', - italic: 'font-style:italic' + italic: 'font-style:italic', + obfuscated: 'color: #222326;background-color: #222326;' } diff --git a/src/react/OptionsGroup.stories.tsx b/src/react/OptionsGroup.stories.tsx deleted file mode 100644 index bc1aed23f..000000000 --- a/src/react/OptionsGroup.stories.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import type { Meta, StoryObj } from '@storybook/react' - -import OptionsGroup from './OptionsGroup' - -const meta: Meta = { - component: OptionsGroup, - // render: () => -} - -export default meta -type Story = StoryObj; - -export const Primary: Story = { - args: { - group: 'controls', - backButtonAction () { } - }, -}