From 2b75d8613aa06b995615837afd2d16a8a255c590 Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky Date: Fri, 9 Feb 2024 04:06:34 +0300 Subject: [PATCH 01/23] set alias only on next branch --- .github/workflows/preview.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/preview.yml b/.github/workflows/preview.yml index cbd2f1a2..f3722d3a 100644 --- a/.github/workflows/preview.yml +++ b/.github/workflows/preview.yml @@ -37,6 +37,8 @@ jobs: run: vercel deploy --prebuilt --token=${{ secrets.VERCEL_TOKEN }} id: deploy - name: Set deployment alias + # only if on branch next + if: github.ref == 'refs/heads/next' run: vercel alias set ${{ steps.deploy.outputs.stdout }} ${{ secrets.TEST_PREVIEW_DOMAIN }} --token=${{ secrets.VERCEL_TOKEN }} - uses: mshick/add-pr-comment@v2 with: From 23a6af0fed85d945c44aecc62d1fd4da5bcb82f9 Mon Sep 17 00:00:00 2001 From: Vitaly Date: Mon, 12 Feb 2024 08:11:07 +0300 Subject: [PATCH 02/23] add noSave to qs --- src/builtinCommands.ts | 2 +- src/flyingSquidUtils.ts | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/builtinCommands.ts b/src/builtinCommands.ts index 71208baf..e68daa03 100644 --- a/src/builtinCommands.ts +++ b/src/builtinCommands.ts @@ -106,7 +106,7 @@ const commands: Array<{ { command: ['/save'], async invoke () { - await saveServer() + await saveServer(false) } } ] diff --git a/src/flyingSquidUtils.ts b/src/flyingSquidUtils.ts index 1aef8d07..09a9760c 100644 --- a/src/flyingSquidUtils.ts +++ b/src/flyingSquidUtils.ts @@ -17,15 +17,16 @@ export function nameToMcOfflineUUID (name) { return (new UUID(javaUUID('OfflinePlayer:' + name))).toString() } -export async function savePlayers () { +export async function savePlayers (autoSave: boolean) { + if (autoSave && new URL(location.href).searchParams.get('noSave') === 'true') return //@ts-expect-error TODO await localServer!.savePlayersSingleplayer() } // todo flying squid should expose save function instead -export const saveServer = async () => { +export const saveServer = async (autoSave = true) => { if (!localServer || fsState.isReadonly) return // todo const worlds = [(localServer as any).overworld] as Array - await Promise.all([savePlayers(), ...worlds.map(async world => world.saveNow())]) + await Promise.all([savePlayers(autoSave), ...worlds.map(async world => world.saveNow())]) } From b9e9b52b2e904a91e4ecf4bc397f19bdadc179f3 Mon Sep 17 00:00:00 2001 From: Vitaly Date: Mon, 12 Feb 2024 08:13:54 +0300 Subject: [PATCH 03/23] experimentalal inventory singleplayer crafting! --- src/playerWindows.ts | 81 +++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 77 insertions(+), 4 deletions(-) diff --git a/src/playerWindows.ts b/src/playerWindows.ts index 9899ccaa..8e4c197e 100644 --- a/src/playerWindows.ts +++ b/src/playerWindows.ts @@ -9,7 +9,7 @@ import DispenserGui from 'minecraft-assets/minecraft-assets/data/1.17.1/gui/cont import Dirt from 'minecraft-assets/minecraft-assets/data/1.17.1/blocks/dirt.png' import { subscribeKey } from 'valtio/utils' -import MinecraftData from 'minecraft-data' +import MinecraftData, { RecipeItem } from 'minecraft-data' import { getVersion } from 'prismarine-viewer/viewer/lib/version' import { versionToNumber } from 'prismarine-viewer/viewer/prepare/utils' import itemsPng from 'prismarine-viewer/public/textures/items.png' @@ -20,6 +20,8 @@ import PrismarineBlockLoader from 'prismarine-block' import { flat } from '@xmcl/text-component' import mojangson from 'mojangson' import nbt from 'prismarine-nbt' +import { splitEvery, equals } from 'rambda' +import PItem, { Item } from 'prismarine-item' import { activeModalStack, hideCurrentModal, miscUiState, showModal } from './globalState' import invspriteJson from './invsprite.json' import { options } from './optionsStorage' @@ -53,6 +55,7 @@ let lastWindow /** bot version */ let version: string let PrismarineBlock: typeof PrismarineBlockLoader.Block +let PrismarineItem: typeof Item export const onGameLoad = (onLoad) => { let loaded = 0 @@ -65,6 +68,7 @@ export const onGameLoad = (onLoad) => { getImage({ path: 'items' }, onImageLoaded) getImage({ path: 'items-legacy' }, onImageLoaded) PrismarineBlock = PrismarineBlockLoader(version) + PrismarineItem = PItem(version) bot.on('windowOpen', (win) => { if (implementedContainersGuiMap[win.type]) { @@ -76,12 +80,35 @@ export const onGameLoad = (onLoad) => { // todo format bot._client.emit('chat', { message: JSON.stringify({ - text: `[client error] cannot open unimplemented window ${win.id} (${win.type}). Items: ${win.slots.map(slot => slot?.name).join(', ')}` + text: `[client error] cannot open unimplemented window ${win.id} (${win.type}). Slots: ${win.slots.map(item => getItemName(item) ?? '(empty)').join(', ')}` }) }) bot.currentWindow?.['close']() } }) + + bot.inventory.on('updateSlot', ((_oldSlot, oldItem, newItem) => { + const oldSlot = _oldSlot as number + if (!miscUiState.singleplayer) return + const { craftingResultSlot } = bot.inventory + if (oldSlot === craftingResultSlot && oldItem && !newItem) { + for (let i = 1; i < 5; i++) { + const count = bot.inventory.slots[i]?.count + if (count && count > 1) { + const slot = bot.inventory.slots[i]! + slot.count-- + void bot.creative.setInventorySlot(i, slot) + } else { + void bot.creative.setInventorySlot(i, null) + } + } + return + } + const craftingSlots = bot.inventory.slots.slice(1, 5) + const resultingItem = getResultingRecipe(craftingSlots, 2) + if (!resultingItem) return + void bot.creative.setInventorySlot(craftingResultSlot, resultingItem) + }) as any) } const findTextureInBlockStates = (name) => { @@ -239,8 +266,8 @@ type PossibleItemProps = { Damage?: number display?: { Name?: JsonString } // {"text":"Knife","color":"white","italic":"true"} } -export const getItemName = (item: import('prismarine-item').Item) => { - if (!item.nbt) return +export const getItemName = (item: import('prismarine-item').Item | null) => { + if (!item?.nbt) return const itemNbt: PossibleItemProps = nbt.simplify(item.nbt) const customName = itemNbt.display?.Name if (!customName) return @@ -357,3 +384,49 @@ let destroyFn = () => { } export const openPlayerInventory = () => { openWindow(undefined) } + +const getResultingRecipe = (slots: Array, gridRows: number) => { + const inputSlotsItems = slots.map(blockSlot => blockSlot?.type) + let currentShape = splitEvery(gridRows, inputSlotsItems as Array) + // todo rewrite with candidates search + if (currentShape.length > 1) { + // eslint-disable-next-line @typescript-eslint/no-for-in-array + for (const slotX in currentShape[0]) { + if (currentShape[0][slotX] !== undefined) { + for (const [otherY] of Array.from({ length: gridRows }).entries()) { + if (currentShape[otherY]?.[slotX] === undefined) { + currentShape[otherY]![slotX] = null + } + } + } + } + } + currentShape = currentShape.map(arr => arr.filter(x => x !== undefined)).filter(x => x.length !== 0) + + // todo rewrite + // eslint-disable-next-line @typescript-eslint/require-array-sort-compare + const slotsIngredients = [...inputSlotsItems].sort().filter(item => item !== undefined) + type Result = RecipeItem | undefined + let shapelessResult: Result + let shapeResult: Result + outer: for (const [id, recipeVariants] of Object.entries(loadedData.recipes)) { + for (const recipeVariant of recipeVariants) { + if ('inShape' in recipeVariant && equals(currentShape, recipeVariant.inShape as number[][])) { + shapeResult = recipeVariant.result! + break outer + } + if ('ingredients' in recipeVariant && equals(slotsIngredients, recipeVariant.ingredients?.sort() as number[])) { + shapelessResult = recipeVariant.result + break outer + } + } + } + const result = shapeResult ?? shapelessResult + if (!result) return + const id = typeof result === 'number' ? result : Array.isArray(result) ? result[0] : result.id + if (!id) return + const count = (typeof result === 'number' ? undefined : Array.isArray(result) ? result[1] : result.count) ?? 1 + const metadata = typeof result === 'object' && !Array.isArray(result) ? result.metadata : undefined + const item = new PrismarineItem(id, count, metadata) + return item +} From 6dddca83c5dd6a8d7178bc6c98043aa1be37a8f1 Mon Sep 17 00:00:00 2001 From: Vitaly Date: Mon, 12 Feb 2024 08:18:20 +0300 Subject: [PATCH 04/23] and qs docs --- README.MD | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/README.MD b/README.MD index 36d20865..029786a7 100644 --- a/README.MD +++ b/README.MD @@ -83,6 +83,20 @@ You can also drag and drop any .dat or .mca (region files) into the browser wind world chunks have a *yellow* border, hostile mobs have a *red* outline, passive mobs have a *green* outline, players have a *blue* outline. +### Query Parameters + +Press `Y` to set query parameters to url of your current game state. + +- `?server=` - Display connect screen to the server on load +- `?username=` - Set the username on load +- `?proxy=` - Set the proxy server address on load +- `?version=` - Set the version on load +- `?reconnect=true` - Reconnect to the server on page reloads. Available in **dev mode only** and very useful on server testing. + +- `?loadSave=` - Load the save on load with the specified folder name (not title) +- `?singleplayer=1` - Create empty world on load. Nothing will be saved +- `?noSave=true` - Disable auto save on unload / disconnect / export. Only manual save with `/save` command will work + ### Notable Things that Power this Project - [Mineflayer](https://github.com/PrismarineJS/mineflayer) - Handles all client-side communications with the server (including the builtin one) - forked From 3ff4ba38494c31486a5cd96b89221c6a47dafaed Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky Date: Mon, 12 Feb 2024 16:25:22 +0300 Subject: [PATCH 05/23] ui fixes: 0 in chat and initial loader didn't show --- src/react/ChatContainer.tsx | 4 ++-- src/styles.css | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/react/ChatContainer.tsx b/src/react/ChatContainer.tsx index 91c9b425..c6dc9883 100644 --- a/src/react/ChatContainer.tsx +++ b/src/react/ChatContainer.tsx @@ -13,7 +13,7 @@ export type Message = { faded?: boolean } -const MessageLine = ({ message }: {message: Message}) => { +const MessageLine = ({ message }: { message: Message }) => { const classes = { 'chat-message-fadeout': message.fading, 'chat-message-fade': message.fading, @@ -205,7 +205,7 @@ export default ({ messages, opacity = 1, fetchCompletionItems, opened, sendMessa {messages.map((m) => ( ))} - } + || undefined} ) : null} - auxInputFocus('ArrowUp')} - onChange={() => { }} - /> - { - 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] || '') - } else if (e.code === 'ArrowDown') { - if (chatHistoryPos.current === sendHistoryRef.current.length) return - chatHistoryPos.current++ - updateInputValue(sendHistoryRef.current[chatHistoryPos.current] || inputCurrentlyEnteredValue.current || '') +
{ + e.preventDefault() + const message = chatInput.current.value + if (message) { + setSendHistory([...sendHistoryRef.current, message]) + const result = sendMessage?.(message) + if (result !== false) { + onClose?.() } - if (e.code === 'Tab') { - if (completionItemsSource.length) { - if (completionItems.length) { - acceptComplete(completionItems[0]) + } + }}> + auxInputFocus('ArrowUp')} + onChange={() => { }} + /> + { + if (e.code === 'ArrowUp') { + if (chatHistoryPos.current === 0) return + if (chatHistoryPos.current === sendHistoryRef.current.length) { // started navigating history + inputCurrentlyEnteredValue.current = e.currentTarget.value } - } else { - void fetchCompletions(false) + chatHistoryPos.current-- + updateInputValue(sendHistoryRef.current[chatHistoryPos.current] || '') + } else if (e.code === 'ArrowDown') { + if (chatHistoryPos.current === sendHistoryRef.current.length) return + chatHistoryPos.current++ + updateInputValue(sendHistoryRef.current[chatHistoryPos.current] || inputCurrentlyEnteredValue.current || '') } - e.preventDefault() - } - if (e.code === 'Space') { - resetCompletionItems() - if (chatInput.current.value.startsWith('/')) { - // alternative we could just simply use keyup, but only with keydown we can display suggestions popup as soon as possible - void fetchCompletions(true, getCompleteValue(getDefaultCompleteValue() + ' ')) + if (e.code === 'Tab') { + if (completionItemsSource.length) { + if (completionItems.length) { + acceptComplete(completionItems[0]) + } + } else { + void fetchCompletions(false) + } + e.preventDefault() } - } - if (e.code === 'Enter') { - const message = chatInput.current.value - if (message) { - setSendHistory([...sendHistoryRef.current, message]) - const result = sendMessage?.(message) - if (result !== false) { - onClose?.() + if (e.code === 'Space') { + resetCompletionItems() + if (chatInput.current.value.startsWith('/')) { + // alternative we could just simply use keyup, but only with keydown we can display suggestions popup as soon as possible + void fetchCompletions(true, getCompleteValue(getDefaultCompleteValue() + ' ')) } } - } - }} - /> - auxInputFocus('ArrowDown')} - onChange={() => { }} - /> + }} + /> + auxInputFocus('ArrowDown')} + onChange={() => { }} + /> +