new cool things (#58)

This commit is contained in:
Vitaly 2024-02-08 23:58:36 +03:00 committed by GitHub
commit ceaf169c7b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
40 changed files with 601 additions and 166 deletions

View file

@ -1,5 +1,7 @@
# Minecraft Web Client
![banner](./docs-assets/banner.jpg)
A true Minecraft client running in your browser! A port of the original game to the web, written in JavaScript using modern web technologies.
This project is a work in progress, but I consider it to be usable. If you encounter any bugs or usability issues, please report them!

BIN
assets/loading-bg.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

BIN
docs-assets/banner.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 217 KiB

View file

@ -4,22 +4,59 @@
<script>
window.startLoad = Date.now()
</script>
<script type="module">
const checkLoadEruda = () => {
if (window.location.hash === '#dev') {
// todo precache (check offline)?
import('https://cdn.skypack.dev/eruda').then(({ default: eruda }) => {
eruda.init()
})
<script async>
const loadingDiv = `
<div class="initial-loader" style="position: fixed;transition:opacity 0.2s;inset: 0;background:black;display: flex;justify-content: center;align-items: center;font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', Arial, Helvetica, sans-serif;gap: 15px;" ontransitionend="this.remove()">
<div>
<img src="./loading-bg.jpg" alt="Prismarine Web Client" style="position:fixed;inset:0;width:100%;height:100%;z-index: -2;object-fit: cover;filter: blur(3px);">
<div style="position: fixed;inset: 0;z-index: -1;background-color: rgba(0, 0, 0, 0.8);"></div>
</div>
<div>
<div style="font-size: calc(var(--font-size) * 1.8);color: lightgray;">Loading...</div>
<div style="font-size: var(--font-size);color: rgb(176, 176, 176);">A true Minecraft client in your browser!</div>
</div>
</div>
`
const loadingDivElem = document.createElement('div')
loadingDivElem.innerHTML = loadingDiv
if (!window.pageLoaded) {
document.documentElement.appendChild(loadingDivElem)
}
}
checkLoadEruda()
window.addEventListener('hashchange', (e) => {
setTimeout(() => {
checkLoadEruda()
</script>
<script type="module" async>
const checkLoadEruda = () => {
if (window.location.hash === '#dev') {
// todo precache (check offline)?
import('https://cdn.skypack.dev/eruda').then(({ default: eruda }) => {
eruda.init()
})
}
}
checkLoadEruda()
window.addEventListener('hashchange', (e) => {
setTimeout(() => {
checkLoadEruda()
})
})
})
</script>
</script>
<style>
html {
background: black;
}
.initial-loader {
--font-size: 20px;
}
@media screen and (min-width: 550px) {
.initial-loader {
--font-size: 30px;
}
}
@media screen and (min-width: 950px) {
.initial-loader {
--font-size: 50px;
}
}
</style>
<title>Prismarine Web Client</title>
<link rel="stylesheet" href="index.css">
<link rel="favicon" href="favicon.ico">

View file

@ -29,6 +29,7 @@
"dependencies": {
"@dimaka/interface": "0.0.3-alpha.0",
"@floating-ui/react": "^0.26.1",
"@mui/base": "5.0.0-beta.34",
"@types/react": "^18.2.20",
"@types/react-dom": "^18.2.7",
"@types/wicg-file-system-access": "^2023.10.2",
@ -45,14 +46,14 @@
"esbuild": "^0.19.3",
"esbuild-plugin-polyfill-node": "^0.3.0",
"express": "^4.18.2",
"flying-squid": "github:zardoy/space-squid#everything",
"flying-squid": "npm:@zardoy/flying-squid@^0.0.9",
"fs-extra": "^11.1.1",
"iconify-icon": "^1.0.8",
"jszip": "^3.10.1",
"lit": "^2.8.0",
"lodash-es": "^4.17.21",
"minecraft-assets": "^1.12.2",
"minecraft-data": "3.58.0",
"minecraft-data": "3.60.0",
"net-browserify": "github:zardoy/prismarinejs-net-browserify",
"node-gzip": "^1.1.2",
"peerjs": "^1.5.0",
@ -126,9 +127,9 @@
"diamond-square": "github:zardoy/diamond-square",
"prismarine-block": "github:zardoy/prismarine-block#next-era",
"prismarine-world": "github:zardoy/prismarine-world#next-era",
"minecraft-data": "3.58.0",
"minecraft-data": "3.60.0",
"prismarine-provider-anvil": "github:zardoy/prismarine-provider-anvil#everything",
"minecraft-protocol": "github:zardoy/minecraft-protocol#custom-client-extra",
"minecraft-protocol": "github:zardoy/minecraft-protocol#everything",
"react": "^18.2.0"
}
}

379
pnpm-lock.yaml generated

File diff suppressed because it is too large Load diff

View file

@ -28,7 +28,6 @@
"looks-same": "^8.2.3",
"minecraft-wrap": "^1.3.0",
"minecrafthawkeye": "^1.3.6",
"node-canvas-webgl": "^0.3.0",
"prismarine-block": "^1.7.3",
"prismarine-chunk": "^1.22.0",
"prismarine-schematic": "^1.2.0",
@ -40,5 +39,8 @@
"three.meshline": "^1.3.0",
"tsx": "^4.7.0",
"vec3": "^0.1.7"
},
"optionalDependencies": {
"node-canvas-webgl": "^0.3.0"
}
}

View file

@ -80,6 +80,7 @@ export class Entities extends EventEmitter {
this.debugMode = 'none'
this.onSkinUpdate = () => { }
this.clock = new THREE.Clock()
this.visible = true
}
clear () {
@ -102,6 +103,13 @@ export class Entities extends EventEmitter {
}
}
setVisible(visible, /** @type {THREE.Object3D?} */entity = null) {
this.visible = visible
for (const mesh of entity ? [entity] : Object.values(this.entities)) {
mesh.visible = visible
}
}
render () {
const dt = this.clock.getDelta()
for (const entityId of Object.keys(this.entities)) {
@ -284,6 +292,7 @@ export class Entities extends EventEmitter {
this.updatePlayerSkin(entity.id, '', stevePng)
}
this.setDebugMode(this.debugMode, group)
this.setVisible(this.visible, group)
}
// this can be undefined in case where packet entity_destroy was sent twice (so it was already deleted)

View file

@ -85,7 +85,7 @@ export class World {
const block = this.blockCache[stateId]
block.position = loc
block.biome = this.biomeCache[column.getBiome(locInChunk)]
block.biome = this.biomeCache[column.getBiome(locInChunk)] ?? this.biomeCache[1] ?? this.biomeCache[0]
if (block.name === 'redstone_ore') block.transparent = false
return block
}

View file

@ -1,7 +1,7 @@
import { chunkPos } from './simpleUtils'
// todo refactor into its own commons module
import { generateSpiralMatrix, ViewRect } from 'flying-squid/src/utils'
import { generateSpiralMatrix, ViewRect } from 'flying-squid/dist/utils'
import { Vec3 } from 'vec3'
import { EventEmitter } from 'events'
import { BotEvents } from 'mineflayer'

View file

@ -24,6 +24,7 @@ export class WorldRenderer {
showChunkBorders = false
active = false
version = undefined as string | undefined
chunkTextures = new Map<string, { [pos: string]: THREE.Texture }>()
loadedChunks = {}
sectionsOutstanding = new Set()
renderUpdateEmitter = new EventEmitter()
@ -106,7 +107,9 @@ export class WorldRenderer {
const [x, y, z] = posKey.split(',')
const signBlockEntity = this.blockEntities[posKey]
if (!signBlockEntity) continue
object.add(this.renderSign(new Vec3(+x, +y, +z), rotation, isWall, nbt.simplify(signBlockEntity)))
const sign = this.renderSign(new Vec3(+x, +y, +z), rotation, isWall, nbt.simplify(signBlockEntity));
if (!sign) continue
object.add(sign)
}
}
this.sectionObjects[data.key] = object
@ -142,13 +145,43 @@ export class WorldRenderer {
}
}
renderSign (position: Vec3, rotation: number, isWall: boolean, blockEntity) {
signsCache = new Map<string, any>()
getSignTexture (position: Vec3, blockEntity, backSide = false) {
const chunk = chunkPos(position)
let textures = this.chunkTextures.get(`${chunk[0]},${chunk[1]}`)
if (!textures) {
textures = {}
this.chunkTextures.set(`${chunk[0]},${chunk[1]}`, textures)
}
const texturekey = `${position.x},${position.y},${position.z}`;
// todo investigate bug and remove this so don't need to clean in section dirty
if (textures[texturekey]) return textures[texturekey]
const PrismarineChat = PrismarineChatLoader(this.version!)
const canvas = renderSign(blockEntity, PrismarineChat)
if (!canvas) return
const tex = new THREE.Texture(canvas)
tex.magFilter = THREE.NearestFilter
tex.minFilter = THREE.NearestFilter
tex.needsUpdate = true
textures[texturekey] = tex
return tex
}
renderSign (position: Vec3, rotation: number, isWall: boolean, blockEntity) {
const tex = this.getSignTexture(position, blockEntity)
if (!tex) return
// todo implement
// const key = JSON.stringify({ position, rotation, isWall })
// if (this.signsCache.has(key)) {
// console.log('cached', key)
// } else {
// this.signsCache.set(key, tex)
// }
const mesh = new THREE.Mesh(new THREE.PlaneGeometry(1, 1), new THREE.MeshBasicMaterial({ map: tex, transparent: true, }))
mesh.renderOrder = 999
@ -266,7 +299,17 @@ export class WorldRenderer {
}
}
cleanChunkTextures (x, z) {
const textures = this.chunkTextures.get(`${Math.floor(x / 16)},${Math.floor(z / 16)}`) ?? {}
for (const key of Object.keys(textures)) {
textures[key].dispose()
delete textures[key]
}
}
removeColumn (x, z) {
this.cleanChunkTextures(x, z)
delete this.loadedChunks[`${x},${z}`]
for (const worker of this.workers) {
worker.postMessage({ type: 'unloadChunk', x, z })
@ -297,6 +340,7 @@ export class WorldRenderer {
}
setSectionDirty (pos, value = true) {
this.cleanChunkTextures(pos.x, pos.z) // todo don't do this!
// Dispatch sections to workers based on position
// This guarantees uniformity accross workers and that a given section
// is always dispatched to the same worker

View file

@ -33,20 +33,28 @@ const parseSafe = (text: string, task: string) => {
}
export const renderSign = (blockEntity: SignBlockEntity, PrismarineChat: typeof ChatMessage, ctxHook = (ctx) => { }) => {
const canvas = document.createElement('canvas')
const factor = 50
// todo don't use texture rendering, investigate the font rendering when possible
// or increase factor when needed
const factor = 40
const signboardY = [16, 9]
const heightOffset = signboardY[0] - signboardY[1]
const heightScalar = heightOffset / 16
canvas.width = 16 * factor
canvas.height = heightOffset * factor
let canvas: HTMLCanvasElement | undefined
let _ctx: CanvasRenderingContext2D | null = null
const getCtx = () => {
if (_ctx) return _ctx
canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')!
ctx.imageSmoothingEnabled = false
canvas.width = 16 * factor
canvas.height = heightOffset * factor
ctxHook(ctx)
_ctx = canvas.getContext('2d')!
_ctx.imageSmoothingEnabled = false
ctxHook(_ctx)
return _ctx
}
const texts = 'front_text' in blockEntity ? /* > 1.20 */ blockEntity.front_text.messages : [
blockEntity.Text1,
@ -104,8 +112,13 @@ export const renderSign = (blockEntity: SignBlockEntity, PrismarineChat: typeof
if (stop) return false
}
}
renderText(rendered)
// skip rendering empty lines (and possible signs)
if (!plainText.trim()) continue
const ctx = getCtx()
const fontSize = 1.6 * factor;
ctx.font = `${fontSize}px mojangles`
const textWidth = ctx.measureText(plainText).width
@ -115,7 +128,7 @@ export const renderSign = (blockEntity: SignBlockEntity, PrismarineChat: typeof
// todo strikeStyle, underlineStyle
ctx.fillStyle = fillStyle
ctx.font = `${fontStyle} ${fontSize}px mojangles`
ctx.fillText(text, (canvas.width - textWidth) / 2 + renderedWidth, fontSize * (lineNum + 1))
ctx.fillText(text, (canvas!.width - textWidth) / 2 + renderedWidth, fontSize * (lineNum + 1))
renderedWidth += ctx.measureText(text).width // todo isn't the font is monospace?
}
}

View file

@ -19,8 +19,15 @@ const blockEntity = {
"Text1": "{\"extra\":[{\"color\":\"dark_green\",\"text\":\"Minecraft \"},{\"text\":\"Tools\"}],\"text\":\"\"}"
} as const
await document.fonts.load('1em mojangles')
const canvas = renderSign(blockEntity, PrismarineChat, (ctx) => {
ctx.drawImage(img, 0, 0, ctx.canvas.width, ctx.canvas.height)
})
document.body.appendChild(canvas)
if (canvas) {
canvas.style.imageRendering = 'pixelated'
document.body.appendChild(canvas)
} else {
console.log('Render skipped')
}

View file

@ -23,7 +23,7 @@ global.document = {
const render = (entity) => {
ctxTexts = []
renderSign(entity, PrismarineChat)
return ctxTexts.map(({ text, y }) => [y / 80, text])
return ctxTexts.map(({ text, y }) => [y / 64, text])
}
test('sign renderer', () => {
@ -49,18 +49,6 @@ test('sign renderer', () => {
1,
"Tools",
],
[
2,
"",
],
[
3,
"",
],
[
4,
"",
],
]
`)

View file

@ -1 +1 @@
["1.8.8", "1.9.4", "1.10.2", "1.11.2", "1.12.2", "1.13.2", "1.14.4", "1.15.2", "1.16.1", "1.16.4", "1.17.1", "1.18.1"]
["1.8.8", "1.9.4", "1.10.2", "1.11.2", "1.12.2", "1.13.2", "1.14.4", "1.15.2", "1.16.1", "1.16.4", "1.17.1", "1.18.1", "1.18.2"]

View file

@ -4,6 +4,10 @@ import { polyfillNode } from 'esbuild-plugin-polyfill-node'
import { join, dirname, basename } from 'path'
import * as fs from 'fs'
import { filesize } from 'filesize'
import MCProtocol from 'minecraft-protocol'
import MCData from 'minecraft-data'
const { supportedVersions } = MCProtocol
const prod = process.argv.includes('--prod')
let connectedClients = []
@ -23,15 +27,14 @@ const plugins = [
build.onLoad({
filter: /minecraft-data[\/\\]data.js$/,
}, (args) => {
const version = supportedVersions.at(-1);
const data = MCData(version)
const defaultVersionsObj = {
// default protocol data, needed for auto-version
"1.20.1": {
version: {
"minecraftVersion": "1.20.1",
"version": 763,
"majorVersion": "1.20"
},
protocol: JSON.parse(fs.readFileSync(join(args.path, '..', 'minecraft-data/data/pc/1.20/protocol.json'), 'utf8')),
[version]: {
version: data.version,
// protocol: JSON.parse(fs.readFileSync(join(args.path, '..', 'minecraft-data/data/pc/1.20/protocol.json'), 'utf8')),
protocol: data.protocol,
}
}
return {

View file

@ -47,6 +47,7 @@ for (const version of [...supportedVersions].reverse()) {
if (fs.existsSync(dataPath)) {
console.log('using blockCollisionShapes of version', version)
const data = JSON.parse(fs.readFileSync(dataPath, 'utf8'))
data.version = version
processData(data)
fs.writeFileSync('./generated/latestBlockCollisionsShapes.json', JSON.stringify(data), 'utf8')
break

View file

@ -60,7 +60,7 @@ const server = isProd ?
}
if (netInterface) {
const address = netInterface.ip4
console.log(`You can access the server on http://localhost:8080 or http://${address}:${port}`)
console.log(`You can access the server on http://localhost:${port} or http://${address}:${port}`)
}
}
})

View file

@ -12,6 +12,8 @@ import { options } from './optionsStorage'
import { openPlayerInventory } from './playerWindows'
import { chatInputValueGlobal } from './react/ChatContainer'
import { fsState } from './loadSave'
import { showOptionsModal } from './react/SelectOption'
import widgets from './react/widgets'
// doesnt seem to work for now
const customKeymaps = proxy(JSON.parse(localStorage.keymap || '{}'))
@ -290,6 +292,17 @@ export const f3Keybinds = [
viewer.world.updateShowChunksBorder(options.showChunkBorders)
},
mobileTitle: 'Toggle chunk borders',
},
{
key: 'KeyT',
async action () {
// waypoints
const widgetNames = widgets.map(widget => widget.name)
const widget = await showOptionsModal('Open Widget', widgetNames)
if (!widget) return
showModal({ reactType: `widget-${widget}` })
},
mobileTitle: 'Open Widget'
}
]

View file

@ -1,12 +1,14 @@
import { LocalServer } from './customServer'
const { createMCServer } = require('flying-squid/src')
const { createMCServer } = require('flying-squid/dist')
export const startLocalServer = (serverOptions) => {
const passOptions = { ...serverOptions, Server: LocalServer }
const server: NonNullable<typeof localServer> = createMCServer(passOptions)
//@ts-expect-error browser patch
server.formatMessage = (message) => `[server] ${message}`
server.options = passOptions
//@ts-expect-error todo remove
server.looseProtocolMode = true
return server
}

View file

@ -18,12 +18,14 @@ export function nameToMcOfflineUUID (name) {
}
export async function savePlayers () {
//@ts-expect-error TODO
await localServer!.savePlayersSingleplayer()
}
// todo flying squid should expose save function instead
export const saveServer = async () => {
if (!localServer || fsState.isReadonly) return
const worlds = [localServer.overworld] as Array<import('prismarine-world').world.World>
// todo
const worlds = [(localServer as any).overworld] as Array<import('prismarine-world').world.World>
await Promise.all([savePlayers(), ...worlds.map(async world => world.saveNow())])
}

View file

@ -1,11 +1,12 @@
import { adoptBlockOrItemNamesFromLatest } from 'flying-squid/src/blockRenames'
import { adoptBlockOrItemNamesFromLatest } from 'flying-squid/dist/blockRenames'
import collisionShapesInit from '../generated/latestBlockCollisionsShapes.json'
import outputInteractionShapesJson from './interactionShapesGenerated.json'
// defining globally to be used in loaded data, not sure of better workaround
window.globalGetCollisionShapes = (version) => {
// todo use the same in resourcepack
const renamedBlocks = adoptBlockOrItemNamesFromLatest('blocks', version, Object.keys(collisionShapesInit.blocks))
const versionFrom = collisionShapesInit.version
const renamedBlocks = adoptBlockOrItemNamesFromLatest('blocks', Object.keys(collisionShapesInit.blocks), versionFrom, version)
const collisionShapes = {
...collisionShapesInit,
blocks: Object.fromEntries(Object.entries(collisionShapesInit.blocks).map(([, shape], i) => [renamedBlocks[i], shape]))
@ -16,11 +17,12 @@ window.globalGetCollisionShapes = (version) => {
export default () => {
customEvents.on('gameLoaded', () => {
// todo also remap block states (e.g. redstone)!
const renamedBlocksInteraction = adoptBlockOrItemNamesFromLatest('blocks', bot.version, Object.keys(outputInteractionShapesJson))
const renamedBlocksInteraction = adoptBlockOrItemNamesFromLatest('blocks', Object.keys(outputInteractionShapesJson), '1.20.2', bot.version)
const interactionShapes = {
...outputInteractionShapesJson,
...Object.fromEntries(Object.entries(outputInteractionShapesJson).map(([block, shape], i) => [renamedBlocksInteraction[i], shape]))
}
interactionShapes[''] = interactionShapes['air']
// todo make earlier
window.interactionShapes = interactionShapes
})

2
src/globals.d.ts vendored
View file

@ -6,7 +6,7 @@ declare const bot: Omit<import('mineflayer').Bot, 'world'> & { world: import('pr
declare const __type_bot: typeof bot
declare const viewer: import('prismarine-viewer/viewer/lib/viewer').Viewer
declare const worldView: import('prismarine-viewer/viewer/lib/worldDataEmitter').WorldDataEmitter | undefined
declare const localServer: import('flying-squid/dist/types').FullServer & { options } | undefined
declare const localServer: import('flying-squid/dist/index').FullServer & { options } | undefined
/** all currently loaded mc data */
declare const mcData: Record<string, any>
declare const loadedData: import('minecraft-data').IndexedData

View file

@ -7,6 +7,7 @@ import './devtools'
import './entities'
import initCollisionShapes from './getCollisionShapes'
import { onGameLoad } from './playerWindows'
import { supportedVersions } from 'minecraft-protocol'
import './menus/components/button'
import './menus/components/edit_box'
@ -364,6 +365,16 @@ async function connect (connectOptions: {
const serverOptions = _.defaultsDeep({}, connectOptions.serverOverrides ?? {}, options.localServerOptions, defaultServerOptions)
Object.assign(serverOptions, connectOptions.serverOverridesFlat ?? {})
const downloadMcData = async (version: string) => {
// todo expose cache
const lastVersion = supportedVersions.at(-1)
if (version === lastVersion) {
// ignore cache hit
versionsByMinecraftVersion.pc[lastVersion]!['dataVersion']!++
}
if (!document.fonts.check('1em mojangles')) {
// todo instead re-render signs on load
await document.fonts.load('1em mojangles').catch(() => { })
}
setLoadingScreenStatus(`Downloading data for ${version}`)
await downloadSoundsIfNeeded()
await loadScript(`./mc-data/${toMajorVersion(version)}.js`)
@ -449,11 +460,6 @@ async function connect (connectOptions: {
respawn: options.autoRespawn,
maxCatchupTicks: 0,
async versionSelectedHook (client) {
// todo keep in sync with esbuild preload, expose cache ideally
if (client.version === '1.20.1') {
// ignore cache hit
versionsByMinecraftVersion.pc['1.20.1']!['dataVersion']!++
}
await downloadMcData(client.version)
setLoadingScreenStatus(initialLoadingText)
}
@ -518,7 +524,7 @@ async function connect (connectOptions: {
destroyAll()
})
let lastPacket
let lastPacket = undefined as string | undefined
const packetBeforePlay = (_, __, ___, fullBuffer) => {
lastPacket = fullBuffer.toString()
}
@ -553,8 +559,10 @@ async function connect (connectOptions: {
setLoadingScreenStatus('Loading world')
})
const spawnEarlier = !singleplayer && !p2pMultiplayer
// don't use spawn event, player can be dead
bot.once('health', () => {
bot.once(spawnEarlier ? 'forcedMove' : 'health', () => {
errorAbortController.abort()
const mcData = MinecraftData(bot.version)
window.PrismarineBlock = PrismarineBlock(mcData.version.minecraftVersion!)
window.loadedData = mcData
@ -725,7 +733,6 @@ async function connect (connectOptions: {
hud.style.display = 'block'
})
errorAbortController.abort()
if (appStatusState.isError) return
setLoadingScreenStatus(undefined)
void viewer.waitForChunksToRender().then(() => {
@ -789,6 +796,13 @@ document.body.addEventListener('touchstart', (e) => {
}, { passive: false })
// #endregion
void window.fetch('config.json').then(async res => res.json()).then(c => c, (error) => {
console.warn('Failed to load optional app config.json', error)
return {}
}).then((config) => {
miscUiState.appConfig = config
})
downloadAndOpenFile().then((downloadAction) => {
if (downloadAction) return
@ -814,3 +828,10 @@ downloadAndOpenFile().then((downloadAction) => {
console.error(err)
alert(`Failed to download file: ${err}`)
})
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
const initialLoader = document.querySelector('.initial-loader') as HTMLElement | null
if (initialLoader) {
initialLoader.style.opacity = '0'
window.pageLoaded = true
}

View file

@ -4196,7 +4196,7 @@
16
]
},
"light": "LightBlock",
"light": [],
"sunflower": [
0,
0,

View file

@ -1,5 +1,5 @@
import fs from 'fs'
import { supportedVersions } from 'flying-squid/src/lib/version'
import { supportedVersions } from 'flying-squid/dist/lib/version'
import * as nbt from 'prismarine-nbt'
import { proxy } from 'valtio'
import { gzip } from 'node-gzip'

View file

@ -1,8 +1,8 @@
//@ts-check
const { LitElement, html, css } = require('lit')
const viewerSupportedVersions = require('prismarine-viewer/viewer/supportedVersions.json')
const { supportedVersions } = require('minecraft-protocol')
const { hideCurrentModal, miscUiState } = require('../globalState')
const { default: supportedVersions } = require('../supportedVersions.mjs')
const { commonCss } = require('./components/common')
const fullySupporedVersions = viewerSupportedVersions

View file

@ -53,6 +53,7 @@ const defaultOptions = {
askGuestName: true,
/** Actually might be useful */
showCursorBlockInSpectator: false,
renderEntities: true,
// advanced bot options
autoRespawn: false,

View file

@ -7,7 +7,7 @@ import Button from './Button'
import styles from './createWorld.module.css'
// const worldTypes = ['default', 'flat', 'largeBiomes', 'amplified', 'customized', 'buffet', 'debug_all_block_states']
const worldTypes = ['plains', 'flat'/* , 'void' */]
const worldTypes = ['default', 'flat'/* , 'void' */]
export const creatingWorldState = proxy({
title: '',

View file

@ -1,4 +1,4 @@
import { supportedVersions } from 'flying-squid/src/lib/version'
import { supportedVersions } from 'flying-squid/dist/lib/version'
import { hideCurrentModal, showModal } from '../globalState'
import defaultLocalServerOptions from '../defaultLocalServerOptions'
import { mkdirRecursive, uniqueFileNameFromWorldName } from '../browserfs'

View file

@ -0,0 +1,10 @@
import Screen from './Screen'
import { useIsWidgetActive } from './utils'
export default ({ name, title, children }) => {
const isWidgetActive = useIsWidgetActive(name)
if (!isWidgetActive) return null
return <Screen backdrop title={title}>{children}</Screen>
}

View file

@ -0,0 +1,45 @@
import { useEffect, useRef } from 'react'
import FullScreenWidget from './FullScreenWidget'
export const name = 'signs'
export default () => {
const signs = [...viewer.world.chunkTextures.values()].flatMap(textures => {
return Object.entries(textures).map(([signPosKey, texture]) => {
const pos = signPosKey.split(',').map(Number)
return <div key={signPosKey}>
<div style={{ color: 'white' }}>{pos.join(', ')}</div>
<div style={{ background: 'rgba(255, 255, 255, 0.5)', padding: 5, borderRadius: 5 }}>
<AddElem elem={texture.image} />
</div>
</div>
})
})
return <FullScreenWidget name='signs' title='Loaded Signs'>
<div>
{signs.length} signs currently loaded:
</div>
{signs}
</FullScreenWidget>
}
const AddElem = ({ elem }) => {
const ref = useRef<HTMLDivElement>(null)
useEffect(() => {
elem.style.width = '100%'
ref.current!.appendChild(elem)
return () => {
elem.remove()
}
}, [])
return <div ref={ref}></div>
}
// for (const key of Object.keys(viewer.world.sectionObjects)) {
// const section = viewer.world.sectionObjects[key]
// for (const child of section.children) {
// if (child.name === 'mesh') child.visible = false
// }
// }

View file

@ -1,5 +1,7 @@
.potential-problem {
color: #808080;
word-break: break-all;
padding: 0 10px;
}
.last-status {

View file

@ -6,6 +6,10 @@ export const useIsModalActive = (modal: string) => {
return useSnapshot(activeModalStack).at(-1)?.reactType === modal
}
export const useIsWidgetActive = (name: string) => {
return useIsModalActive(`widget-${name}`)
}
export function useDidUpdateEffect (fn, inputs) {
const isMountingRef = useRef(false)

3
src/react/widgets.ts Normal file
View file

@ -0,0 +1,3 @@
import * as ModuleSignsViewer from './ModuleSignsViewer'
export default [ModuleSignsViewer]

View file

@ -16,6 +16,8 @@ import EnterFullscreenButton from './react/EnterFullscreenButton'
import ChatProvider from './react/ChatProvider'
import SoundMuffler from './react/SoundMuffler'
import TouchControls from './react/TouchControls'
import widgets from './react/widgets'
import { useIsWidgetActive } from './react/utils'
const Portal = ({ children, to }) => {
return createPortal(children, to)
@ -65,11 +67,23 @@ const InGameUi = () => {
</>
}
const AllWidgets = () => {
return widgets.map(widget => <WidgetDisplay key={widget.name} name={widget.name} Component={widget.default} />)
}
const WidgetDisplay = ({ name, Component }) => {
const isWidgetActive = useIsWidgetActive(name)
if (!isWidgetActive) return null
return <Component />
}
const App = () => {
return <div>
<EnterFullscreenButton />
<InGameUi />
<Portal to={document.querySelector('#ui-root')}>
<AllWidgets />
<SingleplayerProvider />
<CreateWorldProvider />
<AppStatusProvider />

View file

@ -140,6 +140,7 @@ subscribeKey(miscUiState, 'gameLoaded', async () => {
let lastStepSound = 0
const movementHappening = async () => {
if (!bot.player) return // no info yet
const VELOCITY_THRESHOLD = 0.1
const { x, z, y } = bot.player.entity.velocity
if (bot.entity.onGround && Math.abs(x) < VELOCITY_THRESHOLD && (Math.abs(z) > VELOCITY_THRESHOLD || Math.abs(y) > VELOCITY_THRESHOLD)) {

View file

@ -151,6 +151,7 @@ export const setLoadingScreenStatus = function (status: string | undefined | nul
export const disconnect = async () => {
if (localServer) {
await saveServer()
//@ts-expect-error todo expose!
void localServer.quit() // todo investigate we should await
}
window.history.replaceState({}, '', `${window.location.pathname}`) // remove qs

View file

@ -29,4 +29,8 @@ export const watchOptionsAfterViewerInit = () => {
viewer.composer = undefined
}
})
watchValue(options, o => {
viewer.entities.setVisible(o.renderEntities)
})
}

View file

@ -189,8 +189,14 @@ class WorldInteraction {
//@ts-expect-error todo
bot._placeBlockWithOptions(cursorBlock, vecArray[cursorBlock.face], { delta, forceLook: 'ignore' }).catch(console.warn)
} else {
// https://discord.com/channels/413438066984747026/413438150594265099/1198724637572477098
const oldLookAt = bot.lookAt
//@ts-expect-error
bot.activateBlock(cursorBlock, vecArray[cursorBlock.face], delta).catch(console.warn)
bot.lookAt = (pos) => { }
//@ts-expect-error
bot.activateBlock(cursorBlock, vecArray[cursorBlock.face], delta).finally(() => {
bot.lookAt = oldLookAt
}).catch(console.warn)
}
this.lastBlockPlaced = 0
} else {