Release (#388)
This commit is contained in:
commit
cedb077cce
40 changed files with 652 additions and 328 deletions
|
|
@ -16,6 +16,10 @@
|
|||
{
|
||||
"ip": "wss://play2.mcraft.fun"
|
||||
},
|
||||
{
|
||||
"ip": "wss://play-creative.mcraft.fun",
|
||||
"description": "Might be available soon, stay tuned!"
|
||||
},
|
||||
{
|
||||
"ip": "kaboom.pw",
|
||||
"version": "1.20.3",
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
{
|
||||
"alwaysReconnectButton": true
|
||||
"alwaysReconnectButton": true,
|
||||
"reportBugButtonWithReconnect": true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import * as THREE from 'three'
|
||||
import { loadThreeJsTextureFromBitmap } from '../renderer/viewer/lib/utils/skins'
|
||||
|
||||
// Create scene, camera and renderer
|
||||
const scene = new THREE.Scene()
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
"dev-proxy": "node server.js",
|
||||
"start": "run-p dev-proxy dev-rsbuild watch-mesher",
|
||||
"start2": "run-p dev-rsbuild watch-mesher",
|
||||
"start-metrics": "ENABLE_METRICS=true rsbuild dev",
|
||||
"build": "pnpm build-other-workers && rsbuild build",
|
||||
"build-analyze": "BUNDLE_ANALYZE=true rsbuild build && pnpm build-other-workers",
|
||||
"build-single-file": "SINGLE_FILE_BUILD=true rsbuild build",
|
||||
|
|
@ -32,7 +33,8 @@
|
|||
"run-all": "run-p start run-playground",
|
||||
"build-playground": "rsbuild build --config renderer/rsbuild.config.ts",
|
||||
"watch-playground": "rsbuild dev --config renderer/rsbuild.config.ts",
|
||||
"update-git-deps": "tsx scripts/updateGitDeps.ts"
|
||||
"update-git-deps": "tsx scripts/updateGitDeps.ts",
|
||||
"request-data": "tsx scripts/requestData.ts"
|
||||
},
|
||||
"keywords": [
|
||||
"prismarine",
|
||||
|
|
|
|||
14
pnpm-lock.yaml
generated
14
pnpm-lock.yaml
generated
|
|
@ -136,7 +136,7 @@ importers:
|
|||
version: 4.17.21
|
||||
mcraft-fun-mineflayer:
|
||||
specifier: ^0.1.23
|
||||
version: 0.1.23(encoding@0.1.13)(mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/2cd1e3da257a0c0415e9dc5524fb14bd5844f0d8(encoding@0.1.13))
|
||||
version: 0.1.23(encoding@0.1.13)(mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/3daf1f4bdc6afad0dedd87b879875f3dbb7b0980(encoding@0.1.13))
|
||||
minecraft-data:
|
||||
specifier: 3.92.0
|
||||
version: 3.92.0
|
||||
|
|
@ -341,7 +341,7 @@ importers:
|
|||
version: https://codeload.github.com/zardoy/minecraft-inventory-gui/tar.gz/89c33d396f3fde4804c71f4be3c203ade1833b41(@types/react@18.3.18)(react@18.3.1)
|
||||
mineflayer:
|
||||
specifier: github:zardoy/mineflayer#gen-the-master
|
||||
version: https://codeload.github.com/zardoy/mineflayer/tar.gz/2cd1e3da257a0c0415e9dc5524fb14bd5844f0d8(encoding@0.1.13)
|
||||
version: https://codeload.github.com/zardoy/mineflayer/tar.gz/3daf1f4bdc6afad0dedd87b879875f3dbb7b0980(encoding@0.1.13)
|
||||
mineflayer-mouse:
|
||||
specifier: ^0.1.11
|
||||
version: 0.1.11
|
||||
|
|
@ -6678,8 +6678,8 @@ packages:
|
|||
resolution: {integrity: sha512-GtW4hkijyZbSu5LKYYD89xZu+XY7OoP7IkrCnNEn6EdPm0+vr2THoJgFGKrlze9/81+T+P3E4qvJXNFiU/zeJg==}
|
||||
engines: {node: '>=22'}
|
||||
|
||||
mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/2cd1e3da257a0c0415e9dc5524fb14bd5844f0d8:
|
||||
resolution: {tarball: https://codeload.github.com/zardoy/mineflayer/tar.gz/2cd1e3da257a0c0415e9dc5524fb14bd5844f0d8}
|
||||
mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/3daf1f4bdc6afad0dedd87b879875f3dbb7b0980:
|
||||
resolution: {tarball: https://codeload.github.com/zardoy/mineflayer/tar.gz/3daf1f4bdc6afad0dedd87b879875f3dbb7b0980}
|
||||
version: 4.30.0
|
||||
engines: {node: '>=22'}
|
||||
|
||||
|
|
@ -16956,12 +16956,12 @@ snapshots:
|
|||
maxrects-packer: '@zardoy/maxrects-packer@2.7.4'
|
||||
zod: 3.24.2
|
||||
|
||||
mcraft-fun-mineflayer@0.1.23(encoding@0.1.13)(mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/2cd1e3da257a0c0415e9dc5524fb14bd5844f0d8(encoding@0.1.13)):
|
||||
mcraft-fun-mineflayer@0.1.23(encoding@0.1.13)(mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/3daf1f4bdc6afad0dedd87b879875f3dbb7b0980(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/6c2204a813690ead420e2b8c7f0ef32ca357d176(patch_hash=a8726e6981ddc3486262d981d1e2030f379901c055ac9c4bf3036b4149e860e0)(encoding@0.1.13)
|
||||
mineflayer: https://codeload.github.com/zardoy/mineflayer/tar.gz/2cd1e3da257a0c0415e9dc5524fb14bd5844f0d8(encoding@0.1.13)
|
||||
mineflayer: https://codeload.github.com/zardoy/mineflayer/tar.gz/3daf1f4bdc6afad0dedd87b879875f3dbb7b0980(encoding@0.1.13)
|
||||
prismarine-item: 1.16.0
|
||||
ws: 8.18.1
|
||||
transitivePeerDependencies:
|
||||
|
|
@ -17379,7 +17379,7 @@ snapshots:
|
|||
- encoding
|
||||
- supports-color
|
||||
|
||||
mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/2cd1e3da257a0c0415e9dc5524fb14bd5844f0d8(encoding@0.1.13):
|
||||
mineflayer@https://codeload.github.com/zardoy/mineflayer/tar.gz/3daf1f4bdc6afad0dedd87b879875f3dbb7b0980(encoding@0.1.13):
|
||||
dependencies:
|
||||
'@nxg-org/mineflayer-physics-util': 1.8.10
|
||||
minecraft-data: 3.92.0
|
||||
|
|
|
|||
|
|
@ -542,7 +542,6 @@ export function getSectionGeometry (sx: number, sy: number, sz: number, world: W
|
|||
heads: {},
|
||||
signs: {},
|
||||
// isFull: true,
|
||||
highestBlocks: new Map(),
|
||||
hadErrors: false,
|
||||
blocksCount: 0
|
||||
}
|
||||
|
|
@ -552,12 +551,6 @@ export function getSectionGeometry (sx: number, sy: number, sz: number, world: W
|
|||
for (cursor.z = sz; cursor.z < sz + 16; cursor.z++) {
|
||||
for (cursor.x = sx; cursor.x < sx + 16; cursor.x++) {
|
||||
let block = world.getBlock(cursor, blockProvider, attr)!
|
||||
if (!INVISIBLE_BLOCKS.has(block.name)) {
|
||||
const highest = attr.highestBlocks.get(`${cursor.x},${cursor.z}`)
|
||||
if (!highest || highest.y < cursor.y) {
|
||||
attr.highestBlocks.set(`${cursor.x},${cursor.z}`, { y: cursor.y, stateId: block.stateId, biomeId: block.biome.id })
|
||||
}
|
||||
}
|
||||
if (INVISIBLE_BLOCKS.has(block.name)) continue
|
||||
if ((block.name.includes('_sign') || block.name === 'sign') && !world.config.disableSignsMapsSupport) {
|
||||
const key = `${cursor.x},${cursor.y},${cursor.z}`
|
||||
|
|
|
|||
|
|
@ -42,7 +42,6 @@ export type MesherGeometryOutput = {
|
|||
heads: Record<string, any>,
|
||||
signs: Record<string, any>,
|
||||
// isFull: boolean
|
||||
highestBlocks: Map<string, HighestBlockInfo>
|
||||
hadErrors: boolean
|
||||
blocksCount: number
|
||||
customBlockModels?: CustomBlockModels
|
||||
|
|
|
|||
|
|
@ -1,11 +0,0 @@
|
|||
import { fromFormattedString } from '@xmcl/text-component'
|
||||
|
||||
export const formattedStringToSimpleString = (str) => {
|
||||
const result = fromFormattedString(str)
|
||||
str = result.text
|
||||
// todo recursive
|
||||
for (const extra of result.extra) {
|
||||
str += extra.text
|
||||
}
|
||||
return str
|
||||
}
|
||||
|
|
@ -1,30 +1,3 @@
|
|||
import * as THREE from 'three'
|
||||
import { loadThreeJsTextureFromUrl, loadThreeJsTextureFromUrlSync } from './utils/skins'
|
||||
|
||||
let textureCache: Record<string, THREE.Texture> = {}
|
||||
let imagesPromises: Record<string, Promise<THREE.Texture>> = {}
|
||||
|
||||
export async function loadTexture (texture: string, cb: (texture: THREE.Texture) => void, onLoad?: () => void): Promise<void> {
|
||||
const cached = textureCache[texture]
|
||||
if (!cached) {
|
||||
const { promise, resolve } = Promise.withResolvers<THREE.Texture>()
|
||||
const t = loadThreeJsTextureFromUrlSync(texture)
|
||||
textureCache[texture] = t.texture
|
||||
void t.promise.then(resolve)
|
||||
imagesPromises[texture] = promise
|
||||
}
|
||||
|
||||
cb(textureCache[texture])
|
||||
void imagesPromises[texture].then(() => {
|
||||
onLoad?.()
|
||||
})
|
||||
}
|
||||
|
||||
export const clearTextureCache = () => {
|
||||
textureCache = {}
|
||||
imagesPromises = {}
|
||||
}
|
||||
|
||||
export const loadScript = async function (scriptSrc: string, highPriority = true): Promise<HTMLScriptElement> {
|
||||
const existingScript = document.querySelector<HTMLScriptElement>(`script[src="${scriptSrc}"]`)
|
||||
if (existingScript) {
|
||||
|
|
@ -52,3 +25,33 @@ export const loadScript = async function (scriptSrc: string, highPriority = true
|
|||
document.head.appendChild(scriptElement)
|
||||
})
|
||||
}
|
||||
|
||||
const detectFullOffscreenCanvasSupport = () => {
|
||||
if (typeof OffscreenCanvas === 'undefined') return false
|
||||
try {
|
||||
const canvas = new OffscreenCanvas(1, 1)
|
||||
// Try to get a WebGL context - this will fail on iOS where only 2D is supported (iOS 16)
|
||||
const gl = canvas.getContext('webgl2') || canvas.getContext('webgl')
|
||||
return gl !== null
|
||||
} catch (e) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
const hasFullOffscreenCanvasSupport = detectFullOffscreenCanvasSupport()
|
||||
|
||||
export const createCanvas = (width: number, height: number): OffscreenCanvas => {
|
||||
if (hasFullOffscreenCanvasSupport) {
|
||||
return new OffscreenCanvas(width, height)
|
||||
}
|
||||
const canvas = document.createElement('canvas')
|
||||
canvas.width = width
|
||||
canvas.height = height
|
||||
return canvas as unknown as OffscreenCanvas // todo-low
|
||||
}
|
||||
|
||||
export async function loadImageFromUrl (imageUrl: string): Promise<ImageBitmap> {
|
||||
const response = await fetch(imageUrl)
|
||||
const blob = await response.blob()
|
||||
return createImageBitmap(blob)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,68 +1,7 @@
|
|||
import { loadSkinToCanvas } from 'skinview-utils'
|
||||
import * as THREE from 'three'
|
||||
import stevePng from 'mc-assets/dist/other-textures/latest/entity/player/wide/steve.png'
|
||||
import { getLoadedImage } from 'mc-assets/dist/utils'
|
||||
import { createCanvas, loadImageFromUrl } from '../utils'
|
||||
|
||||
const detectFullOffscreenCanvasSupport = () => {
|
||||
if (typeof OffscreenCanvas === 'undefined') return false
|
||||
try {
|
||||
const canvas = new OffscreenCanvas(1, 1)
|
||||
// Try to get a WebGL context - this will fail on iOS where only 2D is supported (iOS 16)
|
||||
const gl = canvas.getContext('webgl2') || canvas.getContext('webgl')
|
||||
return gl !== null
|
||||
} catch (e) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
const hasFullOffscreenCanvasSupport = detectFullOffscreenCanvasSupport()
|
||||
|
||||
export const loadThreeJsTextureFromUrlSync = (imageUrl: string) => {
|
||||
const texture = new THREE.Texture()
|
||||
const promise = getLoadedImage(imageUrl).then(image => {
|
||||
texture.image = image
|
||||
texture.needsUpdate = true
|
||||
return texture
|
||||
})
|
||||
return {
|
||||
texture,
|
||||
promise
|
||||
}
|
||||
}
|
||||
|
||||
export const createCanvas = (width: number, height: number): OffscreenCanvas => {
|
||||
if (hasFullOffscreenCanvasSupport) {
|
||||
return new OffscreenCanvas(width, height)
|
||||
}
|
||||
const canvas = document.createElement('canvas')
|
||||
canvas.width = width
|
||||
canvas.height = height
|
||||
return canvas as unknown as OffscreenCanvas // todo-low
|
||||
}
|
||||
|
||||
export const loadThreeJsTextureFromUrl = async (imageUrl: string) => {
|
||||
const loaded = new THREE.TextureLoader().loadAsync(imageUrl)
|
||||
return loaded
|
||||
}
|
||||
export const loadThreeJsTextureFromBitmap = (image: ImageBitmap) => {
|
||||
const canvas = createCanvas(image.width, image.height)
|
||||
const ctx = canvas.getContext('2d')!
|
||||
ctx.drawImage(image, 0, 0)
|
||||
const texture = new THREE.Texture(canvas)
|
||||
texture.magFilter = THREE.NearestFilter
|
||||
texture.minFilter = THREE.NearestFilter
|
||||
return texture
|
||||
}
|
||||
|
||||
export const stevePngUrl = stevePng
|
||||
export const steveTexture = loadThreeJsTextureFromUrl(stevePngUrl)
|
||||
|
||||
|
||||
export async function loadImageFromUrl (imageUrl: string): Promise<ImageBitmap> {
|
||||
const response = await fetch(imageUrl)
|
||||
const blob = await response.blob()
|
||||
return createImageBitmap(blob)
|
||||
}
|
||||
export { default as stevePngUrl } from 'mc-assets/dist/other-textures/latest/entity/player/wide/steve.png'
|
||||
|
||||
const config = {
|
||||
apiEnabled: true,
|
||||
|
|
|
|||
|
|
@ -123,7 +123,6 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
|
|||
|
||||
handleResize = () => { }
|
||||
highestBlocksByChunks = new Map<string, { [chunkKey: string]: HighestBlockInfo }>()
|
||||
highestBlocksBySections = new Map<string, { [sectionKey: string]: HighestBlockInfo }>()
|
||||
blockEntities = {}
|
||||
|
||||
workersProcessAverageTime = 0
|
||||
|
|
@ -389,8 +388,6 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
|
|||
this.logWorkerWork(() => `-> ${data.workerIndex} geometry ${data.key} ${JSON.stringify({ dataSize: JSON.stringify(data).length })}`)
|
||||
this.geometryReceiveCount[data.workerIndex] ??= 0
|
||||
this.geometryReceiveCount[data.workerIndex]++
|
||||
const { geometry } = data
|
||||
this.highestBlocksBySections[data.key] = geometry.highestBlocks
|
||||
const chunkCoords = data.key.split(',').map(Number)
|
||||
this.lastChunkDistance = Math.max(...this.getDistance(new Vec3(chunkCoords[0], 0, chunkCoords[2])))
|
||||
}
|
||||
|
|
@ -688,7 +685,6 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
|
|||
for (let y = this.worldSizeParams.minY; y < this.worldSizeParams.worldHeight; y += 16) {
|
||||
this.setSectionDirty(new Vec3(x, y, z), false)
|
||||
delete this.finishedSections[`${x},${y},${z}`]
|
||||
this.highestBlocksBySections.delete(`${x},${y},${z}`)
|
||||
}
|
||||
this.highestBlocksByChunks.delete(`${x},${z}`)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { fromFormattedString, render, RenderNode, TextComponent } from '@xmcl/text-component'
|
||||
import type { ChatMessage } from 'prismarine-chat'
|
||||
import { createCanvas } from '../lib/utils'
|
||||
|
||||
type SignBlockEntity = {
|
||||
Color?: string
|
||||
|
|
@ -32,29 +32,40 @@ const parseSafe = (text: string, task: string) => {
|
|||
}
|
||||
}
|
||||
|
||||
export const renderSign = (blockEntity: SignBlockEntity, PrismarineChat: typeof ChatMessage, ctxHook = (ctx) => { }) => {
|
||||
const LEGACY_COLORS = {
|
||||
black: '#000000',
|
||||
dark_blue: '#0000AA',
|
||||
dark_green: '#00AA00',
|
||||
dark_aqua: '#00AAAA',
|
||||
dark_red: '#AA0000',
|
||||
dark_purple: '#AA00AA',
|
||||
gold: '#FFAA00',
|
||||
gray: '#AAAAAA',
|
||||
dark_gray: '#555555',
|
||||
blue: '#5555FF',
|
||||
green: '#55FF55',
|
||||
aqua: '#55FFFF',
|
||||
red: '#FF5555',
|
||||
light_purple: '#FF55FF',
|
||||
yellow: '#FFFF55',
|
||||
white: '#FFFFFF',
|
||||
}
|
||||
|
||||
export const renderSign = (
|
||||
blockEntity: SignBlockEntity,
|
||||
isHanging: boolean,
|
||||
PrismarineChat: typeof ChatMessage,
|
||||
ctxHook = (ctx) => { },
|
||||
canvasCreator = (width, height): OffscreenCanvas => { return createCanvas(width, height) }
|
||||
) => {
|
||||
// todo don't use texture rendering, investigate the font rendering when possible
|
||||
// or increase factor when needed
|
||||
const factor = 40
|
||||
const fontSize = 1.6 * factor
|
||||
const signboardY = [16, 9]
|
||||
const heightOffset = signboardY[0] - signboardY[1]
|
||||
const heightScalar = heightOffset / 16
|
||||
|
||||
let canvas: HTMLCanvasElement | undefined
|
||||
let _ctx: CanvasRenderingContext2D | null = null
|
||||
const getCtx = () => {
|
||||
if (_ctx) return _ctx
|
||||
canvas = document.createElement('canvas')
|
||||
|
||||
canvas.width = 16 * factor
|
||||
canvas.height = heightOffset * factor
|
||||
|
||||
_ctx = canvas.getContext('2d')!
|
||||
_ctx.imageSmoothingEnabled = false
|
||||
|
||||
ctxHook(_ctx)
|
||||
return _ctx
|
||||
}
|
||||
// todo the text should be clipped based on it's render width (needs investigate)
|
||||
|
||||
const texts = 'front_text' in blockEntity ? /* > 1.20 */ blockEntity.front_text.messages : [
|
||||
blockEntity.Text1,
|
||||
|
|
@ -62,78 +73,144 @@ export const renderSign = (blockEntity: SignBlockEntity, PrismarineChat: typeof
|
|||
blockEntity.Text3,
|
||||
blockEntity.Text4
|
||||
]
|
||||
|
||||
if (!texts.some((text) => text !== 'null')) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const canvas = canvasCreator(16 * factor, heightOffset * factor)
|
||||
|
||||
const _ctx = canvas.getContext('2d')!
|
||||
|
||||
ctxHook(_ctx)
|
||||
const defaultColor = ('front_text' in blockEntity ? blockEntity.front_text.color : blockEntity.Color) || 'black'
|
||||
for (const [lineNum, text] of texts.slice(0, 4).entries()) {
|
||||
// todo: in pre flatenning it seems the format was not json
|
||||
if (text === 'null') continue
|
||||
const parsed = text?.startsWith('{') || text?.startsWith('"') ? parseSafe(text ?? '""', 'sign text') : text
|
||||
if (!parsed || (typeof parsed !== 'object' && typeof parsed !== 'string')) continue
|
||||
// todo fix type
|
||||
const message = typeof parsed === 'string' ? fromFormattedString(parsed) : new PrismarineChat(parsed) as never
|
||||
const patchExtra = ({ extra }: TextComponent) => {
|
||||
if (!extra) return
|
||||
for (const child of extra) {
|
||||
if (child.color) {
|
||||
child.color = child.color === 'dark_green' ? child.color.toUpperCase() : child.color.toLowerCase()
|
||||
}
|
||||
patchExtra(child)
|
||||
}
|
||||
}
|
||||
patchExtra(message)
|
||||
const rendered = render(message)
|
||||
|
||||
const toRenderCanvas: Array<{
|
||||
fontStyle: string
|
||||
fillStyle: string
|
||||
underlineStyle: boolean
|
||||
strikeStyle: boolean
|
||||
text: string
|
||||
}> = []
|
||||
let plainText = ''
|
||||
// todo the text should be clipped based on it's render width (needs investigate)
|
||||
const MAX_LENGTH = 50 // avoid abusing the signboard
|
||||
const renderText = (node: RenderNode) => {
|
||||
const { component } = node
|
||||
let { text } = component
|
||||
if (plainText.length + text.length > MAX_LENGTH) {
|
||||
text = text.slice(0, MAX_LENGTH - plainText.length)
|
||||
if (!text) return false
|
||||
}
|
||||
plainText += text
|
||||
toRenderCanvas.push({
|
||||
fontStyle: `${component.bold ? 'bold' : ''} ${component.italic ? 'italic' : ''}`,
|
||||
fillStyle: node.style['color'] || defaultColor,
|
||||
underlineStyle: component.underlined ?? false,
|
||||
strikeStyle: component.strikethrough ?? false,
|
||||
text
|
||||
})
|
||||
for (const child of node.children) {
|
||||
const stop = renderText(child) === false
|
||||
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
|
||||
|
||||
let renderedWidth = 0
|
||||
for (const { fillStyle, fontStyle, strikeStyle, text, underlineStyle } of toRenderCanvas) {
|
||||
// todo strikeStyle, underlineStyle
|
||||
ctx.fillStyle = fillStyle
|
||||
ctx.font = `${fontStyle} ${fontSize}px mojangles`
|
||||
ctx.fillText(text, (canvas!.width - textWidth) / 2 + renderedWidth, fontSize * (lineNum + 1))
|
||||
renderedWidth += ctx.measureText(text).width // todo isn't the font is monospace?
|
||||
}
|
||||
renderComponent(text, PrismarineChat, canvas, fontSize, defaultColor, fontSize * (lineNum + 1) + (isHanging ? 0 : -8))
|
||||
}
|
||||
// ctx.fillStyle = 'red'
|
||||
// ctx.fillRect(0, 0, canvas.width, canvas.height)
|
||||
|
||||
return canvas
|
||||
}
|
||||
|
||||
export const renderComponent = (
|
||||
text: JsonEncodedType | string | undefined,
|
||||
PrismarineChat: typeof ChatMessage,
|
||||
canvas: OffscreenCanvas,
|
||||
fontSize: number,
|
||||
defaultColor: string,
|
||||
offset = 0
|
||||
) => {
|
||||
// todo: in pre flatenning it seems the format was not json
|
||||
const parsed = typeof text === 'string' && (text?.startsWith('{') || text?.startsWith('"')) ? parseSafe(text ?? '""', 'sign text') : text
|
||||
if (!parsed || (typeof parsed !== 'object' && typeof parsed !== 'string')) return
|
||||
// todo fix type
|
||||
|
||||
const ctx = canvas.getContext('2d')!
|
||||
if (!ctx) throw new Error('Could not get 2d context')
|
||||
ctx.imageSmoothingEnabled = false
|
||||
ctx.font = `${fontSize}px mojangles`
|
||||
|
||||
type Formatting = {
|
||||
color: string | undefined
|
||||
underlined: boolean | undefined
|
||||
strikethrough: boolean | undefined
|
||||
bold: boolean | undefined
|
||||
italic: boolean | undefined
|
||||
}
|
||||
|
||||
type Message = ChatMessage & Formatting & { text: string }
|
||||
|
||||
const message = new PrismarineChat(parsed) as Message
|
||||
|
||||
const toRenderCanvas: Array<{
|
||||
fontStyle: string
|
||||
fillStyle: string
|
||||
underlineStyle: boolean
|
||||
strikeStyle: boolean
|
||||
offset: number
|
||||
text: string
|
||||
}> = []
|
||||
let visibleFormatting = false
|
||||
let plainText = ''
|
||||
let textOffset = offset
|
||||
const textWidths: number[] = []
|
||||
|
||||
const renderText = (component: Message, parentFormatting?: Formatting | undefined) => {
|
||||
const { text } = component
|
||||
const formatting = {
|
||||
color: component.color ?? parentFormatting?.color,
|
||||
underlined: component.underlined ?? parentFormatting?.underlined,
|
||||
strikethrough: component.strikethrough ?? parentFormatting?.strikethrough,
|
||||
bold: component.bold ?? parentFormatting?.bold,
|
||||
italic: component.italic ?? parentFormatting?.italic
|
||||
}
|
||||
visibleFormatting = visibleFormatting || formatting.underlined || formatting.strikethrough || false
|
||||
if (text?.includes('\n')) {
|
||||
for (const line of text.split('\n')) {
|
||||
addTextPart(line, formatting)
|
||||
textOffset += fontSize
|
||||
plainText = ''
|
||||
}
|
||||
} else if (text) {
|
||||
addTextPart(text, formatting)
|
||||
}
|
||||
if (component.extra) {
|
||||
for (const child of component.extra) {
|
||||
renderText(child as Message, formatting)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const addTextPart = (text: string, formatting: Formatting) => {
|
||||
plainText += text
|
||||
textWidths[textOffset] = ctx.measureText(plainText).width
|
||||
let color = formatting.color ?? defaultColor
|
||||
if (!color.startsWith('#')) {
|
||||
color = LEGACY_COLORS[color.toLowerCase()] || color
|
||||
}
|
||||
toRenderCanvas.push({
|
||||
fontStyle: `${formatting.bold ? 'bold' : ''} ${formatting.italic ? 'italic' : ''}`,
|
||||
fillStyle: color,
|
||||
underlineStyle: formatting.underlined ?? false,
|
||||
strikeStyle: formatting.strikethrough ?? false,
|
||||
offset: textOffset,
|
||||
text
|
||||
})
|
||||
}
|
||||
|
||||
renderText(message)
|
||||
|
||||
// skip rendering empty lines
|
||||
if (!visibleFormatting && !message.toString().trim()) return
|
||||
|
||||
let renderedWidth = 0
|
||||
let previousOffsetY = 0
|
||||
for (const { fillStyle, fontStyle, underlineStyle, strikeStyle, offset: offsetY, text } of toRenderCanvas) {
|
||||
if (previousOffsetY !== offsetY) {
|
||||
renderedWidth = 0
|
||||
}
|
||||
previousOffsetY = offsetY
|
||||
ctx.fillStyle = fillStyle
|
||||
ctx.textRendering = 'optimizeLegibility'
|
||||
ctx.font = `${fontStyle} ${fontSize}px mojangles`
|
||||
const textWidth = textWidths[offsetY] ?? ctx.measureText(text).width
|
||||
const offsetX = (canvas.width - textWidth) / 2 + renderedWidth
|
||||
ctx.fillText(text, offsetX, offsetY)
|
||||
if (strikeStyle) {
|
||||
ctx.lineWidth = fontSize / 8
|
||||
ctx.strokeStyle = fillStyle
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(offsetX, offsetY - ctx.lineWidth * 2.5)
|
||||
ctx.lineTo(offsetX + ctx.measureText(text).width, offsetY - ctx.lineWidth * 2.5)
|
||||
ctx.stroke()
|
||||
}
|
||||
if (underlineStyle) {
|
||||
ctx.lineWidth = fontSize / 8
|
||||
ctx.strokeStyle = fillStyle
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(offsetX, offsetY + ctx.lineWidth)
|
||||
ctx.lineTo(offsetX + ctx.measureText(text).width, offsetY + ctx.lineWidth)
|
||||
ctx.stroke()
|
||||
}
|
||||
renderedWidth += ctx.measureText(text).width
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,9 +21,14 @@ const blockEntity = {
|
|||
|
||||
await document.fonts.load('1em mojangles')
|
||||
|
||||
const canvas = renderSign(blockEntity, PrismarineChat, (ctx) => {
|
||||
const canvas = renderSign(blockEntity, false, PrismarineChat, (ctx) => {
|
||||
ctx.drawImage(img, 0, 0, ctx.canvas.width, ctx.canvas.height)
|
||||
})
|
||||
}, (width, height) => {
|
||||
const canvas = document.createElement('canvas')
|
||||
canvas.width = width
|
||||
canvas.height = height
|
||||
return canvas as unknown as OffscreenCanvas
|
||||
}) as unknown as HTMLCanvasElement
|
||||
|
||||
if (canvas) {
|
||||
canvas.style.imageRendering = 'pixelated'
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ global.document = {
|
|||
|
||||
const render = (entity) => {
|
||||
ctxTexts = []
|
||||
renderSign(entity, PrismarineChat)
|
||||
renderSign(entity, true, PrismarineChat)
|
||||
return ctxTexts.map(({ text, y }) => [y / 64, text])
|
||||
}
|
||||
|
||||
|
|
@ -37,10 +37,6 @@ test('sign renderer', () => {
|
|||
} as any
|
||||
expect(render(blockEntity)).toMatchInlineSnapshot(`
|
||||
[
|
||||
[
|
||||
1,
|
||||
"",
|
||||
],
|
||||
[
|
||||
1,
|
||||
"Minecraft ",
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
//@ts-check
|
||||
import EventEmitter from 'events'
|
||||
import { UnionToIntersection } from 'type-fest'
|
||||
import nbt from 'prismarine-nbt'
|
||||
import * as TWEEN from '@tweenjs/tween.js'
|
||||
import * as THREE from 'three'
|
||||
import { PlayerObject, PlayerAnimation } from 'skinview3d'
|
||||
import { loadSkinToCanvas, loadEarsToCanvasFromSkin, inferModelType, loadCapeToCanvas, loadImage } from 'skinview-utils'
|
||||
import { PlayerAnimation, PlayerObject } from 'skinview3d'
|
||||
import { inferModelType, loadCapeToCanvas, loadEarsToCanvasFromSkin } from 'skinview-utils'
|
||||
// todo replace with url
|
||||
import { degreesToRadians } from '@nxg-org/mineflayer-tracker/lib/mathUtils'
|
||||
import { NameTagObject } from 'skinview3d/libs/nametag'
|
||||
|
|
@ -13,22 +12,24 @@ import { flat, fromFormattedString } from '@xmcl/text-component'
|
|||
import mojangson from 'mojangson'
|
||||
import { snakeCase } from 'change-case'
|
||||
import { Item } from 'prismarine-item'
|
||||
import { BlockModel } from 'mc-assets'
|
||||
import { isEntityAttackable } from 'mineflayer-mouse/dist/attackableEntity'
|
||||
import { Vec3 } from 'vec3'
|
||||
import { Team } from 'mineflayer'
|
||||
import PrismarineChatLoader from 'prismarine-chat'
|
||||
import { EntityMetadataVersions } from '../../../src/mcDataTypes'
|
||||
import { ItemSpecificContextProperties } from '../lib/basePlayerState'
|
||||
import { loadSkinImage, loadSkinFromUsername, stevePngUrl, steveTexture, createCanvas } from '../lib/utils/skins'
|
||||
import { loadTexture } from '../lib/utils'
|
||||
import { loadSkinFromUsername, loadSkinImage, stevePngUrl } from '../lib/utils/skins'
|
||||
import { renderComponent } from '../sign-renderer'
|
||||
import { createCanvas } from '../lib/utils'
|
||||
import { getBlockMeshFromModel } from './holdingBlock'
|
||||
import * as Entity from './entity/EntityMesh'
|
||||
import { getMesh } from './entity/EntityMesh'
|
||||
import { WalkingGeneralSwing } from './entity/animations'
|
||||
import { disposeObject } from './threeJsUtils'
|
||||
import { armorModel, elytraTexture, armorTextures } from './entity/armorModels'
|
||||
import { disposeObject, loadTexture, loadThreeJsTextureFromUrl } from './threeJsUtils'
|
||||
import { armorModel, armorTextures, elytraTexture } from './entity/armorModels'
|
||||
import { WorldRendererThree } from './worldrendererThree'
|
||||
|
||||
export const steveTexture = loadThreeJsTextureFromUrl(stevePngUrl)
|
||||
|
||||
export const TWEEN_DURATION = 120
|
||||
|
||||
type PlayerObjectType = PlayerObject & {
|
||||
|
|
@ -96,8 +97,11 @@ function getUsernameTexture ({
|
|||
username,
|
||||
nameTagBackgroundColor = 'rgba(0, 0, 0, 0.3)',
|
||||
nameTagTextOpacity = 255
|
||||
}: any, { fontFamily = 'sans-serif' }: any) {
|
||||
}: any, { fontFamily = 'mojangles' }: any, version: string) {
|
||||
const canvas = createCanvas(64, 64)
|
||||
|
||||
const PrismarineChat = PrismarineChatLoader(version)
|
||||
|
||||
const ctx = canvas.getContext('2d')
|
||||
if (!ctx) throw new Error('Could not get 2d context')
|
||||
|
||||
|
|
@ -105,35 +109,36 @@ function getUsernameTexture ({
|
|||
const padding = 5
|
||||
ctx.font = `${fontSize}px ${fontFamily}`
|
||||
|
||||
const lines = String(username).split('\n')
|
||||
|
||||
const plainLines = String(typeof username === 'string' ? username : new PrismarineChat(username).toString()).split('\n')
|
||||
let textWidth = 0
|
||||
for (const line of lines) {
|
||||
for (const line of plainLines) {
|
||||
const width = ctx.measureText(line).width + padding * 2
|
||||
if (width > textWidth) textWidth = width
|
||||
}
|
||||
|
||||
canvas.width = textWidth
|
||||
canvas.height = (fontSize + padding) * lines.length
|
||||
canvas.height = (fontSize + padding) * plainLines.length
|
||||
|
||||
ctx.fillStyle = nameTagBackgroundColor
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height)
|
||||
|
||||
ctx.font = `${fontSize}px ${fontFamily}`
|
||||
ctx.fillStyle = `rgba(255, 255, 255, ${nameTagTextOpacity / 255})`
|
||||
let i = 0
|
||||
for (const line of lines) {
|
||||
i++
|
||||
ctx.fillText(line, (textWidth - ctx.measureText(line).width) / 2, -padding + fontSize * i)
|
||||
}
|
||||
ctx.globalAlpha = nameTagTextOpacity / 255
|
||||
|
||||
renderComponent(username, PrismarineChat, canvas, fontSize, 'white', -padding + fontSize)
|
||||
|
||||
ctx.globalAlpha = 1
|
||||
|
||||
return canvas
|
||||
}
|
||||
|
||||
const addNametag = (entity, options, mesh) => {
|
||||
const addNametag = (entity, options: { fontFamily: string }, mesh, version: string) => {
|
||||
for (const c of mesh.children) {
|
||||
if (c.name === 'nametag') {
|
||||
c.removeFromParent()
|
||||
}
|
||||
}
|
||||
if (entity.username !== undefined) {
|
||||
if (mesh.children.some(c => c.name === 'nametag')) return // todo update
|
||||
const canvas = getUsernameTexture(entity, options)
|
||||
const canvas = getUsernameTexture(entity, options, version)
|
||||
const tex = new THREE.Texture(canvas)
|
||||
tex.needsUpdate = true
|
||||
let nameTag
|
||||
|
|
@ -174,7 +179,7 @@ const nametags = {}
|
|||
|
||||
const isFirstUpperCase = (str) => str.charAt(0) === str.charAt(0).toUpperCase()
|
||||
|
||||
function getEntityMesh (entity: import('prismarine-entity').Entity & { delete?: any; pos?: any; name?: any }, world: WorldRendererThree | undefined, options: { fontFamily: string }, overrides) {
|
||||
function getEntityMesh (entity: import('prismarine-entity').Entity & { delete?: any; pos?: any; name?: any }, world: WorldRendererThree, options: { fontFamily: string }, overrides) {
|
||||
if (entity.name) {
|
||||
try {
|
||||
// https://github.com/PrismarineJS/prismarine-viewer/pull/410
|
||||
|
|
@ -182,7 +187,7 @@ function getEntityMesh (entity: import('prismarine-entity').Entity & { delete?:
|
|||
const e = new Entity.EntityMesh('1.16.4', entityName, world, overrides)
|
||||
|
||||
if (e.mesh) {
|
||||
addNametag(entity, options, e.mesh)
|
||||
addNametag(entity, options, e.mesh, world.version)
|
||||
return e.mesh
|
||||
}
|
||||
} catch (err) {
|
||||
|
|
@ -200,7 +205,7 @@ function getEntityMesh (entity: import('prismarine-entity').Entity & { delete?:
|
|||
addNametag({
|
||||
username: entity.name,
|
||||
height: entity.height,
|
||||
}, options, cube)
|
||||
}, options, cube, world.version)
|
||||
}
|
||||
return cube
|
||||
}
|
||||
|
|
@ -486,6 +491,7 @@ export class Entities {
|
|||
.some(channel => channel !== 0)
|
||||
}
|
||||
|
||||
// todo true/undefined doesnt reset the skin to the default one
|
||||
// eslint-disable-next-line max-params
|
||||
async updatePlayerSkin (entityId: string | number, username: string | undefined, uuidCache: string | undefined, skinUrl: string | true, capeUrl: string | true | undefined = undefined) {
|
||||
if (uuidCache) {
|
||||
|
|
@ -966,21 +972,22 @@ export class Entities {
|
|||
// entity specific meta
|
||||
const textDisplayMeta = getSpecificEntityMetadata('text_display', entity)
|
||||
const displayTextRaw = textDisplayMeta?.text || meta.custom_name_visible && meta.custom_name
|
||||
const displayText = this.parseEntityLabel(displayTextRaw)
|
||||
if (entity.name !== 'player' && displayText) {
|
||||
if (entity.name !== 'player' && displayTextRaw) {
|
||||
const nameTagFixed = textDisplayMeta && (textDisplayMeta.billboard_render_constraints === 'fixed' || !textDisplayMeta.billboard_render_constraints)
|
||||
const nameTagBackgroundColor = textDisplayMeta && toRgba(textDisplayMeta.background_color)
|
||||
const nameTagBackgroundColor = (textDisplayMeta && (parseInt(textDisplayMeta.style_flags, 10) & 0x04) === 0) ? toRgba(textDisplayMeta.background_color) : undefined
|
||||
let nameTagTextOpacity: any
|
||||
if (textDisplayMeta?.text_opacity) {
|
||||
const rawOpacity = parseInt(textDisplayMeta?.text_opacity, 10)
|
||||
nameTagTextOpacity = rawOpacity > 0 ? rawOpacity : 256 - rawOpacity
|
||||
}
|
||||
addNametag(
|
||||
{ ...entity, username: displayText, nameTagBackgroundColor, nameTagTextOpacity, nameTagFixed,
|
||||
{ ...entity, username: typeof displayTextRaw === 'string' ? mojangson.simplify(mojangson.parse(displayTextRaw)) : nbt.simplify(displayTextRaw),
|
||||
nameTagBackgroundColor, nameTagTextOpacity, nameTagFixed,
|
||||
nameTagScale: textDisplayMeta?.scale, nameTagTranslation: textDisplayMeta && (textDisplayMeta.translation || new THREE.Vector3(0, 0, 0)),
|
||||
nameTagRotationLeft: toQuaternion(textDisplayMeta?.left_rotation), nameTagRotationRight: toQuaternion(textDisplayMeta?.right_rotation) },
|
||||
this.entitiesOptions,
|
||||
mesh
|
||||
mesh,
|
||||
this.worldRenderer.version
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import ocelotPng from '../../../../node_modules/mc-assets/dist/other-textures/la
|
|||
import arrowTexture from '../../../../node_modules/mc-assets/dist/other-textures/1.21.2/entity/projectiles/arrow.png'
|
||||
import spectralArrowTexture from '../../../../node_modules/mc-assets/dist/other-textures/1.21.2/entity/projectiles/spectral_arrow.png'
|
||||
import tippedArrowTexture from '../../../../node_modules/mc-assets/dist/other-textures/1.21.2/entity/projectiles/tipped_arrow.png'
|
||||
import { loadTexture } from '../../lib/utils'
|
||||
import { loadTexture } from '../threeJsUtils'
|
||||
import { WorldRendererThree } from '../worldrendererThree'
|
||||
import entities from './entities.json'
|
||||
import { externalModels } from './objModels'
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import * as THREE from 'three'
|
||||
import { loadSkinToCanvas } from 'skinview-utils'
|
||||
import { loadSkinFromUsername, loadSkinImage, steveTexture } from './utils/skins'
|
||||
import { loadSkinFromUsername, loadSkinImage } from '../lib/utils/skins'
|
||||
import { steveTexture } from './entities'
|
||||
|
||||
|
||||
export const getMyHand = async (image?: string, userName?: string) => {
|
||||
let newMap: THREE.Texture
|
||||
|
|
@ -4,12 +4,12 @@ import PrismarineItem from 'prismarine-item'
|
|||
import worldBlockProvider, { WorldBlockProvider } from 'mc-assets/dist/worldBlockProvider'
|
||||
import { BlockModel } from 'mc-assets'
|
||||
import { getThreeBlockModelGroup, renderBlockThree, setBlockPosition } from '../lib/mesher/standaloneRenderer'
|
||||
import { getMyHand } from '../lib/hand'
|
||||
import { MovementState, PlayerStateRenderer } from '../lib/basePlayerState'
|
||||
import { DebugGui } from '../lib/DebugGui'
|
||||
import { SmoothSwitcher } from '../lib/smoothSwitcher'
|
||||
import { watchProperty } from '../lib/utils/proxy'
|
||||
import { WorldRendererConfig } from '../lib/worldrendererCommon'
|
||||
import { getMyHand } from './hand'
|
||||
import { WorldRendererThree } from './worldrendererThree'
|
||||
import { disposeObject } from './threeJsUtils'
|
||||
|
||||
|
|
|
|||
|
|
@ -7,9 +7,9 @@ import type { GraphicsInitOptions } from '../../../src/appViewer'
|
|||
import { WorldDataEmitter } from '../lib/worldDataEmitter'
|
||||
import { defaultWorldRendererConfig, WorldRendererCommon } from '../lib/worldrendererCommon'
|
||||
import { getDefaultRendererState } from '../baseGraphicsBackend'
|
||||
import { loadThreeJsTextureFromUrl, loadThreeJsTextureFromUrlSync } from '../lib/utils/skins'
|
||||
import { ResourcesManager } from '../../../src/resourcesManager'
|
||||
import { getInitialPlayerStateRenderer } from '../lib/basePlayerState'
|
||||
import { loadThreeJsTextureFromUrl, loadThreeJsTextureFromUrlSync } from './threeJsUtils'
|
||||
import { WorldRendererThree } from './worldrendererThree'
|
||||
import { EntityMesh } from './entity/EntityMesh'
|
||||
import { DocumentRenderer } from './documentRenderer'
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
import * as THREE from 'three'
|
||||
import { getLoadedImage } from 'mc-assets/dist/utils'
|
||||
import { createCanvas } from '../lib/utils'
|
||||
|
||||
export const disposeObject = (obj: THREE.Object3D, cleanTextures = false) => {
|
||||
// not cleaning texture there as it might be used by other objects, but would be good to also do that
|
||||
|
|
@ -16,3 +18,56 @@ export const disposeObject = (obj: THREE.Object3D, cleanTextures = false) => {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
let textureCache: Record<string, THREE.Texture> = {}
|
||||
let imagesPromises: Record<string, Promise<THREE.Texture>> = {}
|
||||
|
||||
export const loadThreeJsTextureFromUrlSync = (imageUrl: string) => {
|
||||
const texture = new THREE.Texture()
|
||||
const promise = getLoadedImage(imageUrl).then(image => {
|
||||
texture.image = image
|
||||
texture.needsUpdate = true
|
||||
return texture
|
||||
})
|
||||
return {
|
||||
texture,
|
||||
promise
|
||||
}
|
||||
}
|
||||
|
||||
export const loadThreeJsTextureFromUrl = async (imageUrl: string) => {
|
||||
const loaded = new THREE.TextureLoader().loadAsync(imageUrl)
|
||||
return loaded
|
||||
}
|
||||
|
||||
export const loadThreeJsTextureFromBitmap = (image: ImageBitmap) => {
|
||||
const canvas = createCanvas(image.width, image.height)
|
||||
const ctx = canvas.getContext('2d')!
|
||||
ctx.drawImage(image, 0, 0)
|
||||
const texture = new THREE.Texture(canvas)
|
||||
texture.magFilter = THREE.NearestFilter
|
||||
texture.minFilter = THREE.NearestFilter
|
||||
return texture
|
||||
}
|
||||
|
||||
export async function loadTexture (texture: string, cb: (texture: THREE.Texture) => void, onLoad?: () => void): Promise<void> {
|
||||
const cached = textureCache[texture]
|
||||
if (!cached) {
|
||||
const { promise, resolve } = Promise.withResolvers<THREE.Texture>()
|
||||
const t = loadThreeJsTextureFromUrlSync(texture)
|
||||
textureCache[texture] = t.texture
|
||||
void t.promise.then(resolve)
|
||||
imagesPromises[texture] = promise
|
||||
}
|
||||
|
||||
cb(textureCache[texture])
|
||||
void imagesPromises[texture].then(() => {
|
||||
onLoad?.()
|
||||
})
|
||||
}
|
||||
|
||||
export const clearTextureCache = () => {
|
||||
textureCache = {}
|
||||
imagesPromises = {}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
import * as THREE from 'three'
|
||||
import { LineMaterial, LineSegmentsGeometry, Wireframe } from 'three-stdlib'
|
||||
import { Vec3 } from 'vec3'
|
||||
import { subscribeKey } from 'valtio/utils'
|
||||
import { Block } from 'prismarine-block'
|
||||
import { BlockShape, BlocksShapes } from 'renderer/viewer/lib/basePlayerState'
|
||||
import { WorldRendererThree } from '../worldrendererThree'
|
||||
import { loadThreeJsTextureFromUrl } from '../threeJsUtils'
|
||||
import destroyStage0 from '../../../../assets/destroy_stage_0.png'
|
||||
import destroyStage1 from '../../../../assets/destroy_stage_1.png'
|
||||
import destroyStage2 from '../../../../assets/destroy_stage_2.png'
|
||||
|
|
@ -15,7 +14,6 @@ import destroyStage6 from '../../../../assets/destroy_stage_6.png'
|
|||
import destroyStage7 from '../../../../assets/destroy_stage_7.png'
|
||||
import destroyStage8 from '../../../../assets/destroy_stage_8.png'
|
||||
import destroyStage9 from '../../../../assets/destroy_stage_9.png'
|
||||
import { loadThreeJsTextureFromUrl } from '../../lib/utils/skins'
|
||||
|
||||
export class CursorBlock {
|
||||
_cursorLinesHidden = false
|
||||
|
|
|
|||
|
|
@ -10,13 +10,12 @@ import { WorldRendererCommon } from '../lib/worldrendererCommon'
|
|||
import { addNewStat } from '../lib/ui/newStats'
|
||||
import { MesherGeometryOutput } from '../lib/mesher/shared'
|
||||
import { ItemSpecificContextProperties } from '../lib/basePlayerState'
|
||||
import { getMyHand } from '../lib/hand'
|
||||
import { setBlockPosition } from '../lib/mesher/standaloneRenderer'
|
||||
import { loadThreeJsTextureFromBitmap } from '../lib/utils/skins'
|
||||
import { getMyHand } from './hand'
|
||||
import HoldingBlock from './holdingBlock'
|
||||
import { getMesh } from './entity/EntityMesh'
|
||||
import { armorModel } from './entity/armorModels'
|
||||
import { disposeObject } from './threeJsUtils'
|
||||
import { disposeObject, loadThreeJsTextureFromBitmap } from './threeJsUtils'
|
||||
import { CursorBlock } from './world/cursorBlock'
|
||||
import { getItemUv } from './appShared'
|
||||
import { Entities } from './entities'
|
||||
|
|
@ -426,7 +425,7 @@ export class WorldRendererThree extends WorldRendererCommon {
|
|||
this.scene.add(object)
|
||||
}
|
||||
|
||||
getSignTexture (position: Vec3, blockEntity, backSide = false) {
|
||||
getSignTexture (position: Vec3, blockEntity, isHanging, backSide = false) {
|
||||
const chunk = chunkPos(position)
|
||||
let textures = this.chunkTextures.get(`${chunk[0]},${chunk[1]}`)
|
||||
if (!textures) {
|
||||
|
|
@ -438,7 +437,7 @@ export class WorldRendererThree extends WorldRendererCommon {
|
|||
if (textures[texturekey]) return textures[texturekey]
|
||||
|
||||
const PrismarineChat = PrismarineChatLoader(this.version)
|
||||
const canvas = renderSign(blockEntity, PrismarineChat)
|
||||
const canvas = renderSign(blockEntity, isHanging, PrismarineChat)
|
||||
if (!canvas) return
|
||||
const tex = new THREE.Texture(canvas)
|
||||
tex.magFilter = THREE.NearestFilter
|
||||
|
|
@ -784,7 +783,7 @@ export class WorldRendererThree extends WorldRendererCommon {
|
|||
}
|
||||
|
||||
renderSign (position: Vec3, rotation: number, isWall: boolean, isHanging: boolean, blockEntity) {
|
||||
const tex = this.getSignTexture(position, blockEntity)
|
||||
const tex = this.getSignTexture(position, blockEntity, isHanging)
|
||||
|
||||
if (!tex) return
|
||||
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import { appAndRendererSharedConfig } from './renderer/rsbuildSharedConfig'
|
|||
import { genLargeDataAliases } from './scripts/genLargeDataAliases'
|
||||
import sharp from 'sharp'
|
||||
import supportedVersions from './src/supportedVersions.mjs'
|
||||
import { startWsServer } from './scripts/wsServer'
|
||||
|
||||
const SINGLE_FILE_BUILD = process.env.SINGLE_FILE_BUILD === 'true'
|
||||
|
||||
|
|
@ -59,6 +60,8 @@ const configSource = (SINGLE_FILE_BUILD ? 'BUNDLED' : (process.env.CONFIG_JSON_S
|
|||
|
||||
const faviconPath = 'favicon.png'
|
||||
|
||||
const enableMetrics = process.env.ENABLE_METRICS === 'true'
|
||||
|
||||
// base options are in ./renderer/rsbuildSharedConfig.ts
|
||||
const appConfig = defineConfig({
|
||||
html: {
|
||||
|
|
@ -159,6 +162,7 @@ const appConfig = defineConfig({
|
|||
'process.env.INLINED_APP_CONFIG': JSON.stringify(configSource === 'BUNDLED' ? configJson : null),
|
||||
'process.env.ENABLE_COOKIE_STORAGE': JSON.stringify(process.env.ENABLE_COOKIE_STORAGE || true),
|
||||
'process.env.COOKIE_STORAGE_PREFIX': JSON.stringify(process.env.COOKIE_STORAGE_PREFIX || ''),
|
||||
'process.env.WS_PORT': JSON.stringify(enableMetrics ? 8081 : false),
|
||||
},
|
||||
},
|
||||
server: {
|
||||
|
|
@ -216,6 +220,12 @@ const appConfig = defineConfig({
|
|||
await execAsync('pnpm run build-mesher')
|
||||
}
|
||||
fs.writeFileSync('./dist/version.txt', buildingVersion, 'utf-8')
|
||||
|
||||
// Start WebSocket server in development
|
||||
if (dev && enableMetrics) {
|
||||
await startWsServer(8081, false)
|
||||
}
|
||||
|
||||
console.timeEnd('total-prep')
|
||||
}
|
||||
if (!dev) {
|
||||
|
|
|
|||
42
scripts/requestData.ts
Normal file
42
scripts/requestData.ts
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
import WebSocket from 'ws'
|
||||
|
||||
function formatBytes(bytes: number) {
|
||||
return `${(bytes).toFixed(2)} MB`
|
||||
}
|
||||
|
||||
function formatTime(ms: number) {
|
||||
return `${(ms / 1000).toFixed(2)}s`
|
||||
}
|
||||
|
||||
const ws = new WebSocket('ws://localhost:8081')
|
||||
|
||||
ws.on('open', () => {
|
||||
console.log('Connected to metrics server, waiting for metrics...')
|
||||
})
|
||||
|
||||
ws.on('message', (data) => {
|
||||
try {
|
||||
const metrics = JSON.parse(data.toString())
|
||||
console.log('\nPerformance Metrics:')
|
||||
console.log('------------------')
|
||||
console.log(`Load Time: ${formatTime(metrics.loadTime)}`)
|
||||
console.log(`Memory Usage: ${formatBytes(metrics.memoryUsage)}`)
|
||||
console.log(`Timestamp: ${new Date(metrics.timestamp).toLocaleString()}`)
|
||||
if (!process.argv.includes('-f')) { // follow mode
|
||||
process.exit(0)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error parsing metrics:', error)
|
||||
}
|
||||
})
|
||||
|
||||
ws.on('error', (error) => {
|
||||
console.error('WebSocket error:', error)
|
||||
process.exit(1)
|
||||
})
|
||||
|
||||
// Exit if no metrics received after 5 seconds
|
||||
setTimeout(() => {
|
||||
console.error('Timeout waiting for metrics')
|
||||
process.exit(1)
|
||||
}, 5000)
|
||||
45
scripts/wsServer.ts
Normal file
45
scripts/wsServer.ts
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
import {WebSocketServer} from 'ws'
|
||||
|
||||
export function startWsServer(port: number = 8081, tryOtherPort: boolean = true): Promise<number> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const tryPort = (currentPort: number) => {
|
||||
const wss = new WebSocketServer({ port: currentPort })
|
||||
.on('listening', () => {
|
||||
console.log(`WebSocket server started on port ${currentPort}`)
|
||||
resolve(currentPort)
|
||||
})
|
||||
.on('error', (err: any) => {
|
||||
if (err.code === 'EADDRINUSE' && tryOtherPort) {
|
||||
console.log(`Port ${currentPort} in use, trying ${currentPort + 1}`)
|
||||
wss.close()
|
||||
tryPort(currentPort + 1)
|
||||
} else {
|
||||
reject(err)
|
||||
}
|
||||
})
|
||||
|
||||
wss.on('connection', (ws) => {
|
||||
console.log('Client connected')
|
||||
|
||||
ws.on('message', (message) => {
|
||||
try {
|
||||
// Simply relay the message to all connected clients except sender
|
||||
wss.clients.forEach(client => {
|
||||
if (client !== ws && client.readyState === WebSocket.OPEN) {
|
||||
client.send(message.toString())
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error processing message:', error)
|
||||
}
|
||||
})
|
||||
|
||||
ws.on('close', () => {
|
||||
console.log('Client disconnected')
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
tryPort(port)
|
||||
})
|
||||
}
|
||||
|
|
@ -24,6 +24,7 @@ export type MobileButtonConfig = {
|
|||
readonly icon?: string
|
||||
readonly action?: ActionType
|
||||
readonly actionHold?: ActionType | ActionHoldConfig
|
||||
readonly iconStyle?: React.CSSProperties
|
||||
}
|
||||
|
||||
export type AppConfig = {
|
||||
|
|
@ -56,9 +57,12 @@ export type AppConfig = {
|
|||
defaultUsername?: string
|
||||
skinTexturesProxy?: string
|
||||
alwaysReconnectButton?: boolean
|
||||
reportBugButtonWithReconnect?: boolean
|
||||
disabledCommands?: string[] // Array of command IDs to disable (e.g. ['general.jump', 'general.chat'])
|
||||
}
|
||||
|
||||
export const loadAppConfig = (appConfig: AppConfig) => {
|
||||
|
||||
if (miscUiState.appConfig) {
|
||||
Object.assign(miscUiState.appConfig, appConfig)
|
||||
} else {
|
||||
|
|
@ -70,7 +74,7 @@ export const loadAppConfig = (appConfig: AppConfig) => {
|
|||
if (value) {
|
||||
disabledSettings.value.add(key)
|
||||
// since the setting is forced, we need to set it to that value
|
||||
if (appConfig.defaultSettings?.[key] && !qsOptions[key]) {
|
||||
if (appConfig.defaultSettings && key in appConfig.defaultSettings && !qsOptions[key]) {
|
||||
options[key] = appConfig.defaultSettings[key]
|
||||
}
|
||||
} else {
|
||||
|
|
@ -78,6 +82,7 @@ export const loadAppConfig = (appConfig: AppConfig) => {
|
|||
}
|
||||
}
|
||||
}
|
||||
// todo apply defaultSettings to defaults even if not forced in case of remote config
|
||||
|
||||
if (appConfig.keybindings) {
|
||||
Object.assign(customKeymaps, defaultsDeep(appConfig.keybindings, customKeymaps))
|
||||
|
|
|
|||
|
|
@ -116,6 +116,10 @@ export const contro = new ControMax({
|
|||
window.controMax = contro
|
||||
export type Command = CommandEventArgument<typeof contro['_commandsRaw']>['command']
|
||||
|
||||
export const isCommandDisabled = (command: Command) => {
|
||||
return miscUiState.appConfig?.disabledCommands?.includes(command)
|
||||
}
|
||||
|
||||
onControInit()
|
||||
|
||||
updateBinds(customKeymaps)
|
||||
|
|
@ -544,6 +548,8 @@ const customCommandsHandler = ({ command }) => {
|
|||
contro.on('trigger', customCommandsHandler)
|
||||
|
||||
contro.on('trigger', ({ command }) => {
|
||||
if (isCommandDisabled(command)) return
|
||||
|
||||
const willContinue = !isGameActive(true)
|
||||
alwaysPressedHandledCommand(command)
|
||||
if (willContinue) return
|
||||
|
|
@ -677,6 +683,8 @@ contro.on('trigger', ({ command }) => {
|
|||
})
|
||||
|
||||
contro.on('release', ({ command }) => {
|
||||
if (isCommandDisabled(command)) return
|
||||
|
||||
inModalCommand(command, false)
|
||||
onTriggerOrReleased(command, false)
|
||||
})
|
||||
|
|
@ -996,9 +1004,16 @@ export const handleMobileButtonCustomAction = (action: CustomAction) => {
|
|||
}
|
||||
}
|
||||
|
||||
export const triggerCommand = (command: Command, isDown: boolean) => {
|
||||
handleMobileButtonActionCommand(command, isDown)
|
||||
}
|
||||
|
||||
export const handleMobileButtonActionCommand = (command: ActionType | ActionHoldConfig, isDown: boolean) => {
|
||||
const commandValue = typeof command === 'string' ? command : 'command' in command ? command.command : command
|
||||
|
||||
// Check if command is disabled before proceeding
|
||||
if (typeof commandValue === 'string' && isCommandDisabled(commandValue as Command)) return
|
||||
|
||||
if (typeof commandValue === 'string' && !stringStartsWith(commandValue, 'custom')) {
|
||||
const event: CommandEventArgument<typeof contro['_commandsRaw']> = {
|
||||
command: commandValue as Command,
|
||||
|
|
|
|||
103
src/devtools.ts
103
src/devtools.ts
|
|
@ -209,3 +209,106 @@ setInterval(() => {
|
|||
}, 1000)
|
||||
|
||||
// ---
|
||||
|
||||
// Add type declaration for performance.memory
|
||||
declare global {
|
||||
interface Performance {
|
||||
memory?: {
|
||||
usedJSHeapSize: number
|
||||
totalJSHeapSize: number
|
||||
jsHeapSizeLimit: number
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Performance metrics WebSocket client
|
||||
let ws: WebSocket | null = null
|
||||
let wsReconnectTimeout: NodeJS.Timeout | null = null
|
||||
let metricsInterval: NodeJS.Timeout | null = null
|
||||
|
||||
// Start collecting metrics immediately
|
||||
const startTime = performance.now()
|
||||
|
||||
function collectAndSendMetrics () {
|
||||
if (!ws || ws.readyState !== WebSocket.OPEN) return
|
||||
|
||||
const metrics = {
|
||||
loadTime: performance.now() - startTime,
|
||||
memoryUsage: (performance.memory?.usedJSHeapSize ?? 0) / 1024 / 1024,
|
||||
timestamp: Date.now()
|
||||
}
|
||||
|
||||
ws.send(JSON.stringify(metrics))
|
||||
}
|
||||
|
||||
function getWebSocketUrl () {
|
||||
const wsPort = process.env.WS_PORT
|
||||
if (!wsPort) return null
|
||||
|
||||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
|
||||
const { hostname } = window.location
|
||||
return `${protocol}//${hostname}:${wsPort}`
|
||||
}
|
||||
|
||||
function connectWebSocket () {
|
||||
if (ws) return
|
||||
|
||||
const wsUrl = getWebSocketUrl()
|
||||
if (!wsUrl) {
|
||||
console.log('WebSocket server not configured')
|
||||
return
|
||||
}
|
||||
|
||||
ws = new WebSocket(wsUrl)
|
||||
|
||||
ws.onopen = () => {
|
||||
console.log('Connected to metrics server')
|
||||
if (wsReconnectTimeout) {
|
||||
clearTimeout(wsReconnectTimeout)
|
||||
wsReconnectTimeout = null
|
||||
}
|
||||
|
||||
// Start sending metrics immediately after connection
|
||||
collectAndSendMetrics()
|
||||
|
||||
// Clear existing interval if any
|
||||
if (metricsInterval) {
|
||||
clearInterval(metricsInterval)
|
||||
}
|
||||
|
||||
// Set new interval
|
||||
metricsInterval = setInterval(collectAndSendMetrics, 500)
|
||||
}
|
||||
|
||||
ws.onclose = () => {
|
||||
console.log('Disconnected from metrics server')
|
||||
ws = null
|
||||
|
||||
// Clear metrics interval
|
||||
if (metricsInterval) {
|
||||
clearInterval(metricsInterval)
|
||||
metricsInterval = null
|
||||
}
|
||||
|
||||
// Try to reconnect after 3 seconds
|
||||
wsReconnectTimeout = setTimeout(connectWebSocket, 3000)
|
||||
}
|
||||
|
||||
ws.onerror = (error) => {
|
||||
console.error('WebSocket error:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// Connect immediately
|
||||
connectWebSocket()
|
||||
|
||||
// Add command to request current metrics
|
||||
window.requestMetrics = () => {
|
||||
const metrics = {
|
||||
loadTime: performance.now() - startTime,
|
||||
memoryUsage: (performance.memory?.usedJSHeapSize ?? 0) / 1024 / 1024,
|
||||
timestamp: Date.now()
|
||||
}
|
||||
console.log('Current metrics:', metrics)
|
||||
return metrics
|
||||
}
|
||||
|
|
|
|||
|
|
@ -198,46 +198,40 @@ customEvents.on('gameLoaded', () => {
|
|||
}
|
||||
})
|
||||
|
||||
// Texture override from packet properties
|
||||
bot._client.on('player_info', (packet) => {
|
||||
const applySkinTexturesProxy = (url: string) => {
|
||||
const { appConfig } = miscUiState
|
||||
if (appConfig?.skinTexturesProxy) {
|
||||
return url?.replace('http://textures.minecraft.net/', appConfig.skinTexturesProxy)
|
||||
.replace('https://textures.minecraft.net/', appConfig.skinTexturesProxy)
|
||||
}
|
||||
return url
|
||||
const applySkinTexturesProxy = (url: string | undefined) => {
|
||||
const { appConfig } = miscUiState
|
||||
if (appConfig?.skinTexturesProxy) {
|
||||
return url?.replace('http://textures.minecraft.net/', appConfig.skinTexturesProxy)
|
||||
.replace('https://textures.minecraft.net/', appConfig.skinTexturesProxy)
|
||||
}
|
||||
return url
|
||||
}
|
||||
|
||||
for (const playerEntry of packet.data) {
|
||||
if (!playerEntry.player && !playerEntry.properties) continue
|
||||
let textureProperty = playerEntry.properties?.find(prop => prop?.name === 'textures')
|
||||
if (!textureProperty) {
|
||||
textureProperty = playerEntry.player?.properties?.find(prop => prop?.key === 'textures')
|
||||
}
|
||||
if (textureProperty) {
|
||||
try {
|
||||
const textureData = JSON.parse(Buffer.from(textureProperty.value, 'base64').toString())
|
||||
const skinUrl = applySkinTexturesProxy(textureData.textures?.SKIN?.url)
|
||||
const capeUrl = applySkinTexturesProxy(textureData.textures?.CAPE?.url)
|
||||
// Texture override from packet properties
|
||||
const updateSkin = (player: import('mineflayer').Player) => {
|
||||
if (!player.uuid || !player.username || !player.skinData) return
|
||||
|
||||
// Find entity with matching UUID and update skin
|
||||
let entityId = ''
|
||||
for (const [entId, entity] of Object.entries(bot.entities)) {
|
||||
if (entity.uuid === playerEntry.uuid) {
|
||||
entityId = entId
|
||||
break
|
||||
}
|
||||
}
|
||||
// even if not found, still record to cache
|
||||
void getThreeJsRendererMethods()?.updatePlayerSkin(entityId, playerEntry.player?.name, playerEntry.uuid, skinUrl, capeUrl)
|
||||
} catch (err) {
|
||||
console.error('Error decoding player texture:', err)
|
||||
try {
|
||||
const skinUrl = applySkinTexturesProxy(player.skinData.url)
|
||||
const capeUrl = applySkinTexturesProxy((player.skinData as any).capeUrl)
|
||||
|
||||
// Find entity with matching UUID and update skin
|
||||
let entityId = ''
|
||||
for (const [entId, entity] of Object.entries(bot.entities)) {
|
||||
if (entity.uuid === player.uuid) {
|
||||
entityId = entId
|
||||
break
|
||||
}
|
||||
}
|
||||
// even if not found, still record to cache
|
||||
void getThreeJsRendererMethods()?.updatePlayerSkin(entityId, player.username, player.uuid, skinUrl ?? true, capeUrl)
|
||||
} catch (err) {
|
||||
console.error('Error decoding player texture:', err)
|
||||
}
|
||||
}
|
||||
|
||||
})
|
||||
bot.on('playerJoined', updateSkin)
|
||||
bot.on('playerUpdated', updateSkin)
|
||||
|
||||
bot.on('teamUpdated', (team: Team) => {
|
||||
for (const entity of Object.values(bot.entities)) {
|
||||
|
|
|
|||
1
src/env.d.ts
vendored
1
src/env.d.ts
vendored
|
|
@ -3,6 +3,7 @@ declare namespace NodeJS {
|
|||
// Build configuration
|
||||
NODE_ENV: 'development' | 'production'
|
||||
SINGLE_FILE_BUILD?: string
|
||||
WS_PORT?: string
|
||||
DISABLE_SERVICE_WORKER?: string
|
||||
CONFIG_JSON_SOURCE?: 'BUNDLED' | 'REMOTE'
|
||||
LOCAL_CONFIG_FILE?: string
|
||||
|
|
|
|||
|
|
@ -58,9 +58,17 @@ export const getItemMetadata = (item: GeneralInputItem, resourcesManager: Resour
|
|||
}
|
||||
if (customModelDataDefinitions) {
|
||||
const customModelDataComponent: any = componentMap.get('custom_model_data')
|
||||
if (customModelDataComponent?.data && typeof customModelDataComponent.data === 'number') {
|
||||
const customModelData = customModelDataComponent.data
|
||||
if (customModelDataDefinitions[customModelData]) {
|
||||
if (customModelDataComponent?.data) {
|
||||
let customModelData: number | undefined
|
||||
if (typeof customModelDataComponent.data === 'number') {
|
||||
customModelData = customModelDataComponent.data
|
||||
} else if (typeof customModelDataComponent.data === 'object'
|
||||
&& 'floats' in customModelDataComponent.data
|
||||
&& Array.isArray(customModelDataComponent.data.floats)
|
||||
&& customModelDataComponent.data.floats.length > 0) {
|
||||
customModelData = customModelDataComponent.data.floats[0]
|
||||
}
|
||||
if (customModelData && customModelDataDefinitions[customModelData]) {
|
||||
customModel = customModelDataDefinitions[customModelData]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@
|
|||
--offset: calc(-1 * 25px);
|
||||
--bg-x: calc(-1 * 16px);
|
||||
--bg-y: calc(-1 * 9px);
|
||||
pointer-events: none;
|
||||
image-rendering: pixelated;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@
|
|||
--offset: calc(-1 * 16px);
|
||||
--bg-x: calc(-1 * 16px);
|
||||
--bg-y: calc(-1 * 18px);
|
||||
pointer-events: none;
|
||||
image-rendering: pixelated;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ div.chat-wrapper {
|
|||
|
||||
/* Only apply overflow hidden when not in mobile mode */
|
||||
div.chat-wrapper:not(.display-mobile):not(.input-mobile) {
|
||||
overflow: hidden;
|
||||
/* overflow: hidden; */
|
||||
}
|
||||
|
||||
.chat-messages-wrapper {
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@
|
|||
--offset: calc(-1 * (52px + (9px * (4 * var(--kind) + var(--lightened) * 2))));
|
||||
--bg-x: calc(-1 * (16px + 9px * var(--lightened)));
|
||||
--bg-y: calc(-1 * 27px);
|
||||
pointer-events: none;
|
||||
image-rendering: pixelated;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@
|
|||
--offset: calc(-1 * (52px + (9px * (4 * var(--kind) + var(--lightened) * 2)) ));
|
||||
--bg-x: calc(-1 * (16px + 9px * var(--lightened)));
|
||||
--bg-y: calc(-1 * var(--hardcore) * 45px);
|
||||
pointer-events: none;
|
||||
image-rendering: pixelated;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,15 +2,15 @@ import { useEffect, useRef, useState } from 'react'
|
|||
import { motion, AnimatePresence } from 'framer-motion'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { subscribe, useSnapshot } from 'valtio'
|
||||
import { openItemsCanvas, openPlayerInventory, upInventoryItems } from '../inventoryWindows'
|
||||
import { openItemsCanvas, upInventoryItems } from '../inventoryWindows'
|
||||
import { activeModalStack, isGameActive, miscUiState } from '../globalState'
|
||||
import { currentScaling } from '../scaleInterface'
|
||||
import { watchUnloadForCleanup } from '../gameUnload'
|
||||
import { getItemNameRaw } from '../mineflayer/items'
|
||||
import { isInRealGameSession } from '../utils'
|
||||
import { triggerCommand } from '../controls'
|
||||
import MessageFormattedString from './MessageFormattedString'
|
||||
import SharedHudVars from './SharedHudVars'
|
||||
import { packetsReplayState } from './state/packetsReplayState'
|
||||
|
||||
|
||||
const ItemName = ({ itemKey }: { itemKey: string }) => {
|
||||
|
|
@ -75,6 +75,8 @@ const HotbarInner = () => {
|
|||
const container = useRef<HTMLDivElement>(null!)
|
||||
const [itemKey, setItemKey] = useState('')
|
||||
const hasModals = useSnapshot(activeModalStack).length
|
||||
const { currentTouch, appConfig } = useSnapshot(miscUiState)
|
||||
const mobileOpenInventory = currentTouch && !appConfig?.disabledCommands?.includes('general.inventory')
|
||||
|
||||
useEffect(() => {
|
||||
const controller = new AbortController()
|
||||
|
|
@ -105,7 +107,7 @@ const HotbarInner = () => {
|
|||
canvasManager.setScale(currentScaling.scale)
|
||||
|
||||
canvasManager.windowHeight = 25 * canvasManager.scale
|
||||
canvasManager.windowWidth = (210 - (inv.inventory.supportsOffhand ? 0 : 25) + (miscUiState.currentTouch ? 28 : 0)) * canvasManager.scale
|
||||
canvasManager.windowWidth = (210 - (inv.inventory.supportsOffhand ? 0 : 25) + (mobileOpenInventory ? 28 : 0)) * canvasManager.scale
|
||||
}
|
||||
setSize()
|
||||
watchUnloadForCleanup(subscribe(currentScaling, setSize))
|
||||
|
|
@ -119,8 +121,9 @@ const HotbarInner = () => {
|
|||
canvasManager.canvas.onclick = (e) => {
|
||||
if (!isGameActive(true)) return
|
||||
const pos = inv.canvasManager.getMousePos(inv.canvas, e)
|
||||
if (canvasManager.canvas.width - pos.x < 35 * inv.canvasManager.scale) {
|
||||
openPlayerInventory()
|
||||
if (canvasManager.canvas.width - pos.x < 35 * inv.canvasManager.scale && mobileOpenInventory) {
|
||||
triggerCommand('general.inventory', true)
|
||||
triggerCommand('general.inventory', false)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -180,17 +183,8 @@ const HotbarInner = () => {
|
|||
})
|
||||
document.addEventListener('touchend', (e) => {
|
||||
if (touchStart && (e.target as HTMLElement).closest('.hotbar') && Date.now() - touchStart > 700) {
|
||||
// drop item
|
||||
bot._client.write('block_dig', {
|
||||
'status': 4,
|
||||
'location': {
|
||||
'x': 0,
|
||||
'z': 0,
|
||||
'y': 0
|
||||
},
|
||||
'face': 0,
|
||||
sequence: 0
|
||||
})
|
||||
triggerCommand('general.dropStack', true)
|
||||
triggerCommand('general.dropStack', false)
|
||||
}
|
||||
touchStart = 0
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
import { useRef, useState, useMemo } from 'react'
|
||||
import { GameMode } from 'mineflayer'
|
||||
import { useSnapshot } from 'valtio'
|
||||
import { options } from '../optionsStorage'
|
||||
import { armor } from './armorValues'
|
||||
import HealthBar from './HealthBar'
|
||||
import FoodBar from './FoodBar'
|
||||
|
|
@ -8,6 +10,8 @@ import BreathBar from './BreathBar'
|
|||
import './HealthBar.css'
|
||||
|
||||
export default () => {
|
||||
const { disabledUiParts } = useSnapshot(options)
|
||||
|
||||
const [damaged, setDamaged] = useState(false)
|
||||
const [healthValue, setHealthValue] = useState(bot.health)
|
||||
const [food, setFood] = useState(bot.food)
|
||||
|
|
@ -91,7 +95,7 @@ export default () => {
|
|||
}, [])
|
||||
|
||||
return <div className='hud-bars-container'>
|
||||
<HealthBar
|
||||
{!disabledUiParts.includes('health-bar') && <HealthBar
|
||||
gameMode={gameMode}
|
||||
isHardcore={isHardcore}
|
||||
damaged={damaged}
|
||||
|
|
@ -102,12 +106,12 @@ export default () => {
|
|||
setEffectToAdd(null)
|
||||
setEffectToRemove(null)
|
||||
}}
|
||||
/>
|
||||
<ArmorBar
|
||||
/>}
|
||||
{!disabledUiParts.includes('armor-bar') && <ArmorBar
|
||||
armorValue={armorValue}
|
||||
style={gameMode !== 'survival' && gameMode !== 'adventure' ? { display: 'none' } : { display: 'flex' }}
|
||||
/>
|
||||
<FoodBar
|
||||
/>}
|
||||
{!disabledUiParts.includes('food-bar') && <FoodBar
|
||||
gameMode={gameMode}
|
||||
food={food}
|
||||
effectToAdd={effectToAdd}
|
||||
|
|
@ -116,9 +120,9 @@ export default () => {
|
|||
setEffectToAdd(null)
|
||||
setEffectToRemove(null)
|
||||
}}
|
||||
/>
|
||||
<BreathBar
|
||||
/>}
|
||||
{!disabledUiParts.includes('breath-bar') && <BreathBar
|
||||
oxygen={gameMode !== 'survival' && gameMode !== 'adventure' ? 0 : oxygen}
|
||||
/>
|
||||
/>}
|
||||
</div>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ import Screen from './Screen'
|
|||
import styles from './PauseScreen.module.css'
|
||||
import { DiscordButton } from './DiscordButton'
|
||||
import { showNotification } from './NotificationProvider'
|
||||
import { appStatusState, reconnectReload } from './AppStatusProvider'
|
||||
import { appStatusState, lastConnectOptions, reconnectReload } from './AppStatusProvider'
|
||||
import NetworkStatus from './NetworkStatus'
|
||||
import PauseLinkButtons from './PauseLinkButtons'
|
||||
import { pixelartIcons } from './PixelartIcon'
|
||||
|
|
@ -265,7 +265,7 @@ export default () => {
|
|||
<div className={styles.pause_container}>
|
||||
<Button className="button" style={{ width: '204px' }} onClick={onReturnPress}>Back to Game</Button>
|
||||
<PauseLinkButtons />
|
||||
<Button className="button" style={{ width: '204px' }} onClick={() => openOptionsMenu('main')}>Options</Button>
|
||||
<Button className="button" style={{ width: '204px' }} onClick={() => openOptionsMenu('main')}>Options...</Button>
|
||||
{singleplayer ? (
|
||||
<div className={styles.row}>
|
||||
<Button className="button" style={{ width: '170px' }} onClick={async () => clickJoinLinkButton()}>
|
||||
|
|
@ -295,9 +295,53 @@ export default () => {
|
|||
</Button>
|
||||
</>}
|
||||
{(noConnection || appConfig?.alwaysReconnectButton) && (
|
||||
<Button className="button" style={{ width: '204px' }} onClick={reconnectReload}>
|
||||
Reconnect
|
||||
</Button>
|
||||
<div className={styles.row}>
|
||||
<Button className="button" style={{ width: appConfig?.reportBugButtonWithReconnect ? '98px' : '204px' }} onClick={reconnectReload}>
|
||||
Reconnect
|
||||
</Button>
|
||||
{appConfig?.reportBugButtonWithReconnect && (
|
||||
<Button
|
||||
label="Report Problem"
|
||||
className="button"
|
||||
style={{ width: '98px' }}
|
||||
onClick={async () => {
|
||||
const platform = (navigator as any).userAgentData?.platform ?? navigator.platform
|
||||
const body = `Version: ${window.location.hostname}\nServer: ${lastConnectOptions.value?.server ?? '<not a server>'}\nPlatform: ${platform}\nWebsite: ${window.location.href}`
|
||||
const currentHost = window.location.hostname
|
||||
const options = [
|
||||
'GitHub (please use it if you can)',
|
||||
'Email',
|
||||
...((currentHost === 'mcraft.fun' || currentHost === 'ru.mcraft.fun') ? ['Try Beta Version'] : []),
|
||||
// 'Use previous versions of client'
|
||||
]
|
||||
const action = await showOptionsModal('Report client issue', options)
|
||||
if (!action) return
|
||||
|
||||
switch (action) {
|
||||
case 'GitHub (please use it if you can)':
|
||||
openGithub(`/issues/new?body=${encodeURIComponent(body)}&title=${encodeURIComponent('[Bug Report] <describe your issue here>')}&labels=bug`)
|
||||
break
|
||||
case 'Email': {
|
||||
window.location.href = `mailto:support@mcraft.fun?body=${encodeURIComponent(body)}`
|
||||
break
|
||||
}
|
||||
case 'Try Beta Version': {
|
||||
if (currentHost === 'mcraft.fun') {
|
||||
window.location.href = 'https://s.mcraft.fun'
|
||||
} else if (currentHost === 'ru.mcraft.fun') {
|
||||
window.location.href = 'https://s.pcm.gg'
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'Use previous versions of client':
|
||||
// TODO: Implement versions screen
|
||||
void showOptionsModal('Previous versions', [])
|
||||
break
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<LoadingTimer />
|
||||
|
|
|
|||
|
|
@ -163,7 +163,7 @@ export const reloadChunks = async () => {
|
|||
}
|
||||
|
||||
export const openGithub = (addUrl = '') => {
|
||||
window.open(`${process.env.GITHUB_URL}${addUrl}`, '_blank')
|
||||
window.open(`${process.env.GITHUB_URL?.replace(/\/$/, '')}${addUrl}`, '_blank')
|
||||
}
|
||||
|
||||
export const resolveTimeout = async (promise, timeout = 10_000) => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue