feat: signs rendering support!
feat: shulker box rendering support chore: minor improvements
This commit is contained in:
parent
b68ef154b9
commit
60934372af
25 changed files with 755 additions and 75 deletions
|
|
@ -1,7 +1,8 @@
|
|||
{
|
||||
"extends": "zardoy",
|
||||
"ignorePatterns": [
|
||||
"!*.js"
|
||||
"!*.js",
|
||||
"prismarine-viewer/"
|
||||
],
|
||||
"rules": {
|
||||
"space-infix-ops": "error",
|
||||
|
|
|
|||
10
.vscode/launch.json
vendored
10
.vscode/launch.json
vendored
|
|
@ -15,12 +15,12 @@
|
|||
"outFiles": [
|
||||
"${workspaceFolder}/dist/**/*.js",
|
||||
// "!${workspaceFolder}/dist/**/*vendors*",
|
||||
"!${workspaceFolder}/dist/**/*minecraftData*",
|
||||
"!${workspaceFolder}/dist/**/*mc-data*",
|
||||
"!**/node_modules/**"
|
||||
],
|
||||
"skipFiles": [
|
||||
// "<node_internals>/**/*vendors*"
|
||||
"<node_internals>/**/*minecraftData*"
|
||||
"<node_internals>/**/*mc-data*"
|
||||
],
|
||||
"port": 9222,
|
||||
},
|
||||
|
|
@ -36,12 +36,12 @@
|
|||
"outFiles": [
|
||||
"${workspaceFolder}/dist/**/*.js",
|
||||
// "!${workspaceFolder}/dist/**/*vendors*",
|
||||
"!${workspaceFolder}/dist/**/*minecraftData*",
|
||||
"!${workspaceFolder}/dist/**/*mc-data*",
|
||||
"!**/node_modules/**"
|
||||
],
|
||||
"skipFiles": [
|
||||
// "<node_internals>/**/*vendors*"
|
||||
"<node_internals>/**/*minecraftData*"
|
||||
"<node_internals>/**/*mc-data*"
|
||||
],
|
||||
},
|
||||
{
|
||||
|
|
@ -54,7 +54,7 @@
|
|||
"webRoot": "${workspaceFolder}/",
|
||||
"skipFiles": [
|
||||
// "<node_internals>/**/*vendors*"
|
||||
"<node_internals>/**/*minecraftData*"
|
||||
"<node_internals>/**/*mc-data*"
|
||||
],
|
||||
},
|
||||
]
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@
|
|||
"web",
|
||||
"client"
|
||||
],
|
||||
"bin": "./server.js",
|
||||
"author": "PrismarineJS",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
|
|
|||
6
pnpm-lock.yaml
generated
6
pnpm-lock.yaml
generated
|
|
@ -62,7 +62,7 @@ importers:
|
|||
version: 4.18.2
|
||||
flying-squid:
|
||||
specifier: github:zardoy/space-squid#everything
|
||||
version: github.com/zardoy/space-squid/30bfb8d16bf18397e8f3ee927ac33faad60d0e7c
|
||||
version: github.com/zardoy/space-squid/458eee79e4ff20fccdff8027d3aae16161b9fb1c
|
||||
fs-extra:
|
||||
specifier: ^11.1.1
|
||||
version: 11.1.1
|
||||
|
|
@ -10745,8 +10745,8 @@ packages:
|
|||
- utf-8-validate
|
||||
dev: false
|
||||
|
||||
github.com/zardoy/space-squid/30bfb8d16bf18397e8f3ee927ac33faad60d0e7c:
|
||||
resolution: {tarball: https://codeload.github.com/zardoy/space-squid/tar.gz/30bfb8d16bf18397e8f3ee927ac33faad60d0e7c}
|
||||
github.com/zardoy/space-squid/458eee79e4ff20fccdff8027d3aae16161b9fb1c:
|
||||
resolution: {tarball: https://codeload.github.com/zardoy/space-squid/tar.gz/458eee79e4ff20fccdff8027d3aae16161b9fb1c}
|
||||
name: flying-squid
|
||||
version: 1.5.0
|
||||
engines: {node: '>=8'}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
packages:
|
||||
- "."
|
||||
- "prismarine-viewer"
|
||||
- "prismarine-viewer/viewer/sign-renderer/"
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
//@ts-check
|
||||
/* global THREE, fetch */
|
||||
const _ = require('lodash')
|
||||
const { WorldView, Viewer, MapControls } = require('../viewer')
|
||||
const { WorldDataEmitter, Viewer, MapControls } = require('../viewer')
|
||||
const { Vec3 } = require('vec3')
|
||||
const { Schematic } = require('prismarine-schematic')
|
||||
const BlockLoader = require('prismarine-block')
|
||||
|
|
@ -10,9 +10,9 @@ const BlockLoader = require('prismarine-block')
|
|||
const ChunkLoader = require('prismarine-chunk')
|
||||
/** @type {import('prismarine-world')['default']} */
|
||||
//@ts-ignore
|
||||
const WorldLoader = require('prismarine-world');
|
||||
const WorldLoader = require('prismarine-world')
|
||||
const THREE = require('three')
|
||||
const {GUI} = require('lil-gui')
|
||||
const { GUI } = require('lil-gui')
|
||||
const { toMajor } = require('../viewer/lib/version')
|
||||
const { loadScript } = require('../viewer/lib/utils')
|
||||
globalThis.THREE = THREE
|
||||
|
|
@ -26,8 +26,8 @@ const params = {
|
|||
skip: '',
|
||||
version: globalThis.includedVersions.sort((a, b) => {
|
||||
const s = (x) => {
|
||||
const parts = x.split('.');
|
||||
return +parts[0]+(+parts[1])
|
||||
const parts = x.split('.')
|
||||
return +parts[0] + (+parts[1])
|
||||
}
|
||||
return s(a) - s(b)
|
||||
}).at(-1),
|
||||
|
|
@ -39,6 +39,7 @@ const params = {
|
|||
this.entity = ''
|
||||
},
|
||||
entityRotate: false,
|
||||
camera: ''
|
||||
}
|
||||
|
||||
const qs = new URLSearchParams(window.location.search)
|
||||
|
|
@ -59,13 +60,18 @@ const setQs = () => {
|
|||
async function main () {
|
||||
const { version } = params
|
||||
// temporary solution until web worker is here, cache data for faster reloads
|
||||
if (!window['mcData']['version']) {
|
||||
const sessionKey = `mcData-${version}`;
|
||||
const globalMcData = window['mcData'];
|
||||
if (!globalMcData['version']) {
|
||||
const major = toMajor(version);
|
||||
const sessionKey = `mcData-${major}`
|
||||
if (sessionStorage[sessionKey]) {
|
||||
window['mcData'][version] = JSON.parse(sessionStorage[sessionKey])
|
||||
Object.assign(globalMcData, JSON.parse(sessionStorage[sessionKey]))
|
||||
} else {
|
||||
await loadScript(`./mc-data/${toMajor(version)}.js`)
|
||||
sessionStorage[sessionKey] = JSON.stringify(window['mcData'][version])
|
||||
if (sessionStorage.length > 1) sessionStorage.clear()
|
||||
await loadScript(`./mc-data/${major}.js`)
|
||||
try {
|
||||
sessionStorage[sessionKey] = JSON.stringify(Object.fromEntries(Object.entries(globalMcData).filter(([ver]) => ver.startsWith(major))))
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -88,20 +94,30 @@ async function main () {
|
|||
// const data = await fetch('smallhouse1.schem').then(r => r.arrayBuffer())
|
||||
// const schem = await Schematic.read(Buffer.from(data), version)
|
||||
|
||||
const viewDistance = 1
|
||||
const viewDistance = 2
|
||||
const center = new Vec3(0, 90, 0)
|
||||
|
||||
const World = WorldLoader(version)
|
||||
|
||||
// const diamondSquare = require('diamond-square')({ version, seed: Math.floor(Math.random() * Math.pow(2, 31)) })
|
||||
const targetBlockPos = center
|
||||
|
||||
const targetPos = center
|
||||
//@ts-ignore
|
||||
const chunk1 = new Chunk()
|
||||
//@ts-ignore
|
||||
const chunk2 = new Chunk()
|
||||
chunk1.setBlockStateId(center, 34)
|
||||
chunk2.setBlockStateId(center.offset(1, 0, 0), 34)
|
||||
const world = new World((chunkX, chunkZ) => {
|
||||
// if (chunkX === 0 && chunkZ === 0) return chunk1
|
||||
// if (chunkX === 1 && chunkZ === 0) return chunk2
|
||||
//@ts-ignore
|
||||
return new Chunk()
|
||||
const chunk = new Chunk();
|
||||
return chunk
|
||||
})
|
||||
|
||||
// await schem.paste(world, new Vec3(0, 60, 0))
|
||||
|
||||
|
||||
const worldView = new WorldDataEmitter(world, viewDistance, center)
|
||||
|
||||
// Create three.js context, add to page
|
||||
|
|
@ -113,29 +129,33 @@ async function main () {
|
|||
// Create viewer
|
||||
const viewer = new Viewer(renderer)
|
||||
viewer.setVersion(version)
|
||||
|
||||
viewer.listen(worldView)
|
||||
// Initialize viewer, load chunks
|
||||
// Load chunks
|
||||
worldView.init(center)
|
||||
window['worldView'] = worldView
|
||||
window['viewer'] = viewer
|
||||
|
||||
|
||||
// const controls = new MapControls(viewer.camera, renderer.domElement)
|
||||
// controls.update()
|
||||
//@ts-ignore
|
||||
const controls = new THREE.OrbitControls(viewer.camera, renderer.domElement)
|
||||
controls.target.set(center.x + 0.5, center.y + 0.5, center.z + 0.5)
|
||||
controls.update()
|
||||
|
||||
const cameraPos = center.offset(2, 2, 2)
|
||||
const pitch = THREE.MathUtils.degToRad(-45)
|
||||
const yaw = THREE.MathUtils.degToRad(45)
|
||||
viewer.camera.rotation.set(pitch, yaw, 0, 'ZYX')
|
||||
viewer.camera.lookAt(center.x + 0.5, center.y + 0.5, center.z + 0.5)
|
||||
viewer.camera.position.set(cameraPos.x + 0.5, cameraPos.y + 0.5, cameraPos.z + 0.5)
|
||||
|
||||
let blockProps = {}
|
||||
const getBlock = () => {
|
||||
const getBlock = () => {
|
||||
return mcData.blocksByName[params.block || 'air']
|
||||
}
|
||||
const onUpdate = {
|
||||
block() {
|
||||
const {states} = mcData.blocksByStateId[getBlock()?.minStateId] ?? {}
|
||||
block () {
|
||||
const { states } = mcData.blocksByStateId[getBlock()?.minStateId] ?? {}
|
||||
folder.destroy()
|
||||
if (!states) {
|
||||
return
|
||||
|
|
@ -146,16 +166,16 @@ async function main () {
|
|||
switch (state.type) {
|
||||
case 'enum':
|
||||
defaultValue = state.values[0]
|
||||
break;
|
||||
break
|
||||
case 'bool':
|
||||
defaultValue = false
|
||||
break;
|
||||
break
|
||||
case 'int':
|
||||
defaultValue = 0
|
||||
break;
|
||||
break
|
||||
case 'direction':
|
||||
defaultValue = 'north'
|
||||
break;
|
||||
break
|
||||
|
||||
default:
|
||||
continue
|
||||
|
|
@ -173,14 +193,14 @@ async function main () {
|
|||
viewer.entities.clear()
|
||||
if (!params.entity) return
|
||||
worldView.emit('entity', {
|
||||
id: 'id', name: params.entity, pos: targetBlockPos.offset(0, 1, 0), width: 1, height: 1, username: 'username'
|
||||
id: 'id', name: params.entity, pos: targetPos.offset(0, 1, 0), width: 1, height: 1, username: 'username'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const applyChanges = (metadataUpdate = false) => {
|
||||
const blockId = getBlock()?.id;
|
||||
const blockId = getBlock()?.id
|
||||
/** @type {BlockLoader.Block} */
|
||||
let block
|
||||
if (metadataUpdate) {
|
||||
|
|
@ -197,14 +217,14 @@ async function main () {
|
|||
block = Block.fromProperties(blockId ?? -1, blockProps, 0)
|
||||
}
|
||||
|
||||
viewer.setBlockStateId(targetBlockPos, block.stateId)
|
||||
viewer.setBlockStateId(targetPos, block.stateId)
|
||||
console.log('up', block.stateId)
|
||||
params.metadata = block.metadata
|
||||
metadataGui.updateDisplay()
|
||||
viewer.setBlockStateId(targetBlockPos.offset(0, -1, 0), params.supportBlock ? 1 : 0)
|
||||
viewer.setBlockStateId(targetPos.offset(0, -1, 0), params.supportBlock ? 1 : 0)
|
||||
setQs()
|
||||
}
|
||||
gui.onChange(({property}) => {
|
||||
gui.onChange(({ property }) => {
|
||||
if (property === 'camera') return
|
||||
onUpdate[property]?.()
|
||||
applyChanges(property === 'metadata')
|
||||
|
|
@ -233,6 +253,39 @@ async function main () {
|
|||
})
|
||||
animate()
|
||||
|
||||
// #region camera rotation
|
||||
if (params.camera) {
|
||||
const [x, y] = params.camera.split(',')
|
||||
viewer.camera.rotation.set(parseFloat(x), parseFloat(y), 0, 'ZYX')
|
||||
controls.update()
|
||||
console.log(viewer.camera.rotation.x, parseFloat(x))
|
||||
}
|
||||
const throttledCamQsUpdate = _.throttle(() => {
|
||||
const { camera } = viewer
|
||||
// params.camera = `${camera.rotation.x.toFixed(2)},${camera.rotation.y.toFixed(2)}`
|
||||
setQs()
|
||||
}, 200)
|
||||
controls.addEventListener('change', () => {
|
||||
throttledCamQsUpdate()
|
||||
animate()
|
||||
})
|
||||
// #endregion
|
||||
|
||||
window.onresize = () => {
|
||||
// const vec3 = new THREE.Vector3()
|
||||
// vec3.set(-1, -1, -1).unproject(viewer.camera)
|
||||
// console.log(vec3)
|
||||
// box.position.set(vec3.x, vec3.y, vec3.z-1)
|
||||
|
||||
const { camera } = viewer
|
||||
viewer.camera.aspect = window.innerWidth / window.innerHeight
|
||||
viewer.camera.updateProjectionMatrix()
|
||||
renderer.setSize(window.innerWidth, window.innerHeight)
|
||||
|
||||
animate()
|
||||
}
|
||||
window.dispatchEvent(new Event('resize'))
|
||||
|
||||
setTimeout(() => {
|
||||
// worldView.emit('entity', {
|
||||
// id: 'id', name: 'player', pos: center.offset(1, -2, 0), width: 1, height: 1, username: 'username'
|
||||
|
|
|
|||
|
|
@ -8,7 +8,8 @@
|
|||
"pretest": "npm run lint",
|
||||
"lint": "standard",
|
||||
"fix": "standard --fix",
|
||||
"postinstall": "tsx viewer/prepare/generateTextures.ts && node buildWorker.mjs"
|
||||
"postinstall": "pnpm generate-textures && node buildWorker.mjs",
|
||||
"generate-textures": "tsx viewer/prepare/generateTextures.ts"
|
||||
},
|
||||
"author": "PrismarineJS",
|
||||
"license": "MIT",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
//@ts-nocheck
|
||||
import { Vec3 } from 'vec3'
|
||||
import { BlockStatesOutput } from '../prepare/modelsBuilder'
|
||||
import { World } from './world'
|
||||
|
||||
const tints = {}
|
||||
let blockStates: BlockStatesOutput
|
||||
|
|
@ -367,7 +368,7 @@ function renderElement (world, cursor, element, doAO, attr, globalMatrix, global
|
|||
}
|
||||
}
|
||||
|
||||
export function getSectionGeometry (sx, sy, sz, world) {
|
||||
export function getSectionGeometry (sx, sy, sz, world: World) {
|
||||
const attr = {
|
||||
sx: sx + 8,
|
||||
sy: sy + 8,
|
||||
|
|
@ -380,7 +381,9 @@ export function getSectionGeometry (sx, sy, sz, world) {
|
|||
t_normals: [],
|
||||
t_colors: [],
|
||||
t_uvs: [],
|
||||
indices: []
|
||||
indices: [],
|
||||
// todo this can be removed here
|
||||
signs: {}
|
||||
}
|
||||
|
||||
const cursor = new Vec3(0, 0, 0)
|
||||
|
|
@ -388,6 +391,21 @@ export function getSectionGeometry (sx, sy, sz, world) {
|
|||
for (cursor.z = sz; cursor.z < sz + 16; cursor.z++) {
|
||||
for (cursor.x = sx; cursor.x < sx + 16; cursor.x++) {
|
||||
const block = world.getBlock(cursor)
|
||||
if (block.name.includes('sign')) {
|
||||
const key = `${cursor.x},${cursor.y},${cursor.z}`
|
||||
const props = block.getProperties();
|
||||
const facingRotationMap = {
|
||||
"north": 2,
|
||||
"south": 0,
|
||||
"west": 1,
|
||||
"east": 3
|
||||
}
|
||||
const isWall = block.name.endsWith('wall_sign') || block.name.endsWith('hanging_sign');
|
||||
attr.signs[key] = {
|
||||
isWall,
|
||||
rotation: isWall ? facingRotationMap[props.facing] : +props.rotation
|
||||
}
|
||||
}
|
||||
const biome = block.biome.name
|
||||
if (block.variant === undefined) {
|
||||
block.variant = getModelVariants(block)
|
||||
|
|
|
|||
|
|
@ -99,11 +99,13 @@ export class Viewer {
|
|||
this.updatePrimitive(p)
|
||||
})
|
||||
|
||||
emitter.on('loadChunk', ({ x, z, chunk, blockEntities }) => {
|
||||
// todo! clean stay in sync instead!
|
||||
Object.assign(this.world.blockEntities, blockEntities)
|
||||
emitter.on('loadChunk', ({ x, z, chunk }) => {
|
||||
this.addColumn(x, z, chunk)
|
||||
})
|
||||
// todo remove and use other architecture instead so data flow is clear
|
||||
emitter.on('blockEntities', (blockEntities) => {
|
||||
this.world.blockEntities = blockEntities
|
||||
})
|
||||
|
||||
emitter.on('unloadChunk', ({ x, z }) => {
|
||||
this.removeColumn(x, z)
|
||||
|
|
@ -113,6 +115,8 @@ export class Viewer {
|
|||
this.setBlockStateId(new Vec3(pos.x, pos.y, pos.z), stateId)
|
||||
})
|
||||
|
||||
emitter.emit('listening')
|
||||
|
||||
this.domElement.addEventListener('pointerdown', (evt) => {
|
||||
const raycaster = new THREE.Raycaster()
|
||||
const mouse = new THREE.Vector2()
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
import { chunkPos } from './simpleUtils'
|
||||
|
||||
// todo refactor into its own commons module
|
||||
import { generateSpiralMatrix, ViewRect } from 'flying-squid/src/utils'
|
||||
import { Vec3 } from 'vec3'
|
||||
import { EventEmitter } from 'events'
|
||||
|
|
@ -16,7 +18,7 @@ export class WorldDataEmitter extends EventEmitter {
|
|||
private eventListeners: Record<string, any> = {};
|
||||
private emitter: WorldDataEmitter
|
||||
|
||||
constructor(public world: import('prismarine-world').world.World, public viewDistance: number, position: Vec3 = new Vec3(0, 0, 0)) {
|
||||
constructor(public world: import('prismarine-world').world.World | typeof __type_bot['world'], public viewDistance: number, position: Vec3 = new Vec3(0, 0, 0)) {
|
||||
super()
|
||||
this.loadedChunks = {}
|
||||
this.lastPos = new Vec3(0, 0, 0).update(position)
|
||||
|
|
@ -33,7 +35,7 @@ export class WorldDataEmitter extends EventEmitter {
|
|||
})
|
||||
}
|
||||
|
||||
listenToBot (bot: import('mineflayer').Bot) {
|
||||
listenToBot (bot: typeof __type_bot) {
|
||||
this.eventListeners[bot.username] = {
|
||||
// 'move': botPosition,
|
||||
entitySpawn: (e: any) => {
|
||||
|
|
@ -55,6 +57,21 @@ export class WorldDataEmitter extends EventEmitter {
|
|||
}
|
||||
}
|
||||
|
||||
this.emitter.on('listening', () => {
|
||||
this.emitter.emit('blockEntities', new Proxy({}, {
|
||||
get(_target, posKey, receiver) {
|
||||
if (typeof posKey !== 'string') return
|
||||
const [x, y, z] = posKey.split(',').map(Number)
|
||||
console.log('get entity', x, y, z)
|
||||
return bot.world.getBlock(new Vec3(x, y, z)).entity
|
||||
},
|
||||
}))
|
||||
})
|
||||
// node.js stream data event pattern
|
||||
if (this.emitter.listenerCount('blockEntities')) {
|
||||
this.emitter.emit('listening')
|
||||
}
|
||||
|
||||
for (const [evt, listener] of Object.entries(this.eventListeners[bot.username])) {
|
||||
bot.on(evt as any, listener)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,15 @@
|
|||
//@ts-check
|
||||
const THREE = require('three')
|
||||
const Vec3 = require('vec3').Vec3
|
||||
const { Vec3 } = require('vec3')
|
||||
const { loadTexture, loadJSON } = globalThis.isElectron ? require('./utils.electron.js') : require('./utils')
|
||||
const { EventEmitter } = require('events')
|
||||
const { dispose3 } = require('./dispose')
|
||||
const { dynamicMcDataFiles } = require('../../buildWorkerConfig.mjs')
|
||||
const mcDataRaw = require('minecraft-data/data.js')
|
||||
const nbt = require('prismarine-nbt')
|
||||
const { dynamicMcDataFiles } = require('../../buildWorkerConfig.mjs')
|
||||
const { dispose3 } = require('./dispose')
|
||||
const { toMajor } = require('./version.js')
|
||||
const PrismarineChatLoader = require('prismarine-chat')
|
||||
const { renderSign } = require('../sign-renderer/')
|
||||
|
||||
function mod (x, n) {
|
||||
return ((x % n) + n) % n
|
||||
|
|
@ -37,9 +40,13 @@ class WorldRenderer {
|
|||
|
||||
/** @type {any} */
|
||||
const worker = new Worker(src)
|
||||
worker.onmessage = ({ data }) => {
|
||||
worker.onmessage = async ({ data }) => {
|
||||
if (!this.active) return
|
||||
await new Promise(resolve => {
|
||||
setTimeout(resolve, 0)
|
||||
})
|
||||
if (data.type === 'geometry') {
|
||||
/** @type {THREE.Object3D} */
|
||||
let mesh = this.sectionMeshs[data.key]
|
||||
if (mesh) {
|
||||
this.scene.remove(mesh)
|
||||
|
|
@ -48,7 +55,7 @@ class WorldRenderer {
|
|||
}
|
||||
|
||||
const chunkCoords = data.key.split(',')
|
||||
if (!this.loadedChunks[chunkCoords[0] + ',' + chunkCoords[2]]) return
|
||||
if (!this.loadedChunks[chunkCoords[0] + ',' + chunkCoords[2]] || !data.geometry.positions.length) return
|
||||
|
||||
const geometry = new THREE.BufferGeometry()
|
||||
geometry.setAttribute('position', new THREE.BufferAttribute(data.geometry.positions, 3))
|
||||
|
|
@ -57,8 +64,23 @@ class WorldRenderer {
|
|||
geometry.setAttribute('uv', new THREE.BufferAttribute(data.geometry.uvs, 2))
|
||||
geometry.setIndex(data.geometry.indices)
|
||||
|
||||
mesh = new THREE.Mesh(geometry, this.material)
|
||||
mesh.position.set(data.geometry.sx, data.geometry.sy, data.geometry.sz)
|
||||
const _mesh = new THREE.Mesh(geometry, this.material)
|
||||
_mesh.position.set(data.geometry.sx, data.geometry.sy, data.geometry.sz)
|
||||
const boxHelper = new THREE.BoxHelper(_mesh, 0xffff00)
|
||||
// shouldnt it compute once
|
||||
if (Object.keys(data.geometry.signs).length) {
|
||||
mesh = new THREE.Group()
|
||||
mesh.add(_mesh)
|
||||
mesh.add(boxHelper)
|
||||
for (const [posKey, { isWall, rotation }] of Object.entries(data.geometry.signs)) {
|
||||
const [x, y, z] = posKey.split(',')
|
||||
const signBlockEntity = this.blockEntities[posKey]
|
||||
if (!signBlockEntity) continue
|
||||
mesh.add(this.renderSign(new Vec3(+x, +y, +z), rotation, isWall, nbt.simplify(signBlockEntity)))
|
||||
}
|
||||
} else {
|
||||
mesh = _mesh
|
||||
}
|
||||
this.sectionMeshs[data.key] = mesh
|
||||
this.scene.add(mesh)
|
||||
} else if (data.type === 'sectionFinished') {
|
||||
|
|
@ -71,6 +93,36 @@ class WorldRenderer {
|
|||
}
|
||||
}
|
||||
|
||||
renderSign (/** @type {Vec3} */position, /** @type {number} */rotation, isWall, blockEntity) {
|
||||
// @ts-ignore
|
||||
const PrismarineChat = PrismarineChatLoader(this.version)
|
||||
const canvas = renderSign(blockEntity, PrismarineChat)
|
||||
const tex = new THREE.Texture(canvas)
|
||||
tex.magFilter = THREE.NearestFilter
|
||||
tex.minFilter = THREE.NearestFilter
|
||||
tex.needsUpdate = true
|
||||
const mesh = new THREE.Mesh(new THREE.PlaneGeometry(1, 1), new THREE.MeshBasicMaterial({ map: tex, transparent: true, }))
|
||||
mesh.renderOrder = 999
|
||||
|
||||
// todo @sa2urami shouldnt all this be done in worker?
|
||||
mesh.scale.set(1, 7 / 16, 1)
|
||||
if (isWall) {
|
||||
mesh.position.set(0, 0, -(8 - 1.5) / 16 + 0.001)
|
||||
} else {
|
||||
// standing
|
||||
const faceEnd = 8.75
|
||||
mesh.position.set(0, 0, (faceEnd - 16 / 2) / 16 + 0.001)
|
||||
}
|
||||
|
||||
const group = new THREE.Group()
|
||||
const rotateStep = isWall ? 2 : 4
|
||||
group.rotation.set(0, -(Math.PI / rotateStep) * rotation, 0)
|
||||
group.add(mesh)
|
||||
const y = isWall ? 4.5 / 16 + mesh.scale.y / 2 : (1 - (mesh.scale.y / 2))
|
||||
group.position.set(position.x + 0.5, position.y + y, position.z + 0.5)
|
||||
return group
|
||||
}
|
||||
|
||||
resetWorld () {
|
||||
this.active = false
|
||||
for (const mesh of Object.values(this.sectionMeshs)) {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import { Canvas, Image } from 'canvas'
|
||||
import { getAdditionalTextures } from './moreGeneratedBlocks'
|
||||
|
||||
function nextPowerOfTwo (n) {
|
||||
if (n === 0) return 1
|
||||
|
|
@ -28,7 +29,10 @@ export function makeTextureAtlas (mcAssets) {
|
|||
const textureFiles = fs.readdirSync(blocksTexturePath).filter(file => file.endsWith('.png'))
|
||||
textureFiles.unshift(...localTextures)
|
||||
|
||||
const texSize = nextPowerOfTwo(Math.ceil(Math.sqrt(textureFiles.length)))
|
||||
const {generated:additionalTextures, twoBlockTextures} = getAdditionalTextures()
|
||||
textureFiles.push(...Object.keys(additionalTextures))
|
||||
|
||||
const texSize = nextPowerOfTwo(Math.ceil(Math.sqrt(textureFiles.length + twoBlockTextures.length)))
|
||||
const tileSize = 16
|
||||
|
||||
const imgSize = texSize * tileSize
|
||||
|
|
@ -46,13 +50,16 @@ export function makeTextureAtlas (mcAssets) {
|
|||
const name = textureFiles[i].split('.')[0]
|
||||
|
||||
const img = new Image()
|
||||
img.src = 'data:image/png;base64,' + readTexture(blocksTexturePath, textureFiles[i])
|
||||
const needsMoreWidth = img.width > tileSize
|
||||
if (needsMoreWidth) {
|
||||
console.log('needs more', name, img.width, img.height)
|
||||
if (additionalTextures[name]) {
|
||||
img.src = additionalTextures[name]
|
||||
} else {
|
||||
img.src = 'data:image/png;base64,' + readTexture(blocksTexturePath, textureFiles[i])
|
||||
}
|
||||
const twoTileWidth = twoBlockTextures.includes(name)
|
||||
if (twoTileWidth) {
|
||||
offset++
|
||||
}
|
||||
const renderWidth = needsMoreWidth ? tileSize * 2 : tileSize
|
||||
const renderWidth = twoTileWidth ? tileSize * 2 : tileSize
|
||||
g.drawImage(img, 0, 0, img.width, img.height, x, y, renderWidth, tileSize)
|
||||
|
||||
texturesIndex[name] = { u: x / imgSize, v: y / imgSize, su: renderWidth / imgSize, sv: tileSize / imgSize }
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { makeTextureAtlas } from './atlas'
|
|||
import { McAssets, prepareBlocksStates } from './modelsBuilder'
|
||||
import mcAssets from 'minecraft-assets'
|
||||
import fs from 'fs-extra'
|
||||
import { prepareMoreGeneratedBlocks } from './moreGeneratedBlocks'
|
||||
|
||||
const publicPath = path.resolve(__dirname, '../../public')
|
||||
|
||||
|
|
@ -16,9 +17,17 @@ fs.mkdirSync(texturesPath, { recursive: true })
|
|||
const blockStatesPath = path.join(publicPath, 'blocksStates')
|
||||
fs.mkdirSync(blockStatesPath, { recursive: true })
|
||||
|
||||
const warnings = new Set<string>()
|
||||
Promise.resolve().then(async () => {
|
||||
console.time('generateTextures')
|
||||
for (const version of mcAssets.versions) {
|
||||
// for debugging (e.g. when above is overridden)
|
||||
if (!mcAssets.versions.includes(version)) {
|
||||
throw new Error(`Version ${version} is not supported by minecraft-assets, skipping...`)
|
||||
}
|
||||
const assets = mcAssets(version)
|
||||
const { warnings: _warnings } = await prepareMoreGeneratedBlocks(assets)
|
||||
_warnings.forEach(x => warnings.add(x))
|
||||
// #region texture atlas
|
||||
const atlas = makeTextureAtlas(assets)
|
||||
const out = fs.createWriteStream(path.resolve(texturesPath, version + '.png'))
|
||||
|
|
@ -34,4 +43,6 @@ Promise.resolve().then(async () => {
|
|||
}
|
||||
|
||||
fs.writeFileSync(path.join(publicPath, 'supportedVersions.json'), '[' + mcAssets.versions.map(v => `"${v}"`).toString() + ']')
|
||||
warnings.forEach(x => console.warn(x))
|
||||
console.timeEnd('generateTextures')
|
||||
})
|
||||
|
|
|
|||
300
prismarine-viewer/viewer/prepare/moreGeneratedBlocks.ts
Normal file
300
prismarine-viewer/viewer/prepare/moreGeneratedBlocks.ts
Normal file
|
|
@ -0,0 +1,300 @@
|
|||
import Jimp from 'jimp'
|
||||
import minecraftData from 'minecraft-data'
|
||||
import prismarineRegistry from 'prismarine-registry'
|
||||
import { McAssets } from './modelsBuilder'
|
||||
|
||||
// todo refactor
|
||||
const twoBlockTextures = []
|
||||
let currentImage: Jimp
|
||||
let currentBlockName: string
|
||||
let currentMcAssets: McAssets
|
||||
let isPreFlattening = false
|
||||
const postFlatenningRegistry = prismarineRegistry('1.13')
|
||||
|
||||
type SidesType = {
|
||||
"up": string
|
||||
"north": string
|
||||
"east": string
|
||||
"south": string
|
||||
"west": string
|
||||
"down": string
|
||||
}
|
||||
|
||||
const getBlockStates = (name: string, postFlatenningName = name) => {
|
||||
const mcData = isPreFlattening ? postFlatenningRegistry : minecraftData(currentMcAssets.version)
|
||||
return mcData.blocksByName[isPreFlattening ? postFlatenningName : name]?.states
|
||||
}
|
||||
|
||||
export const addBlockCustomSidesModel = (name: string, sides: SidesType) => {
|
||||
currentMcAssets.blocksStates[name] = {
|
||||
"variants": {
|
||||
"": {
|
||||
"model": name
|
||||
}
|
||||
}
|
||||
}
|
||||
currentMcAssets.blocksModels[name] = {
|
||||
"parent": "block/cube",
|
||||
"textures": sides
|
||||
}
|
||||
}
|
||||
|
||||
type TextureMap = [
|
||||
x: number,
|
||||
y: number,
|
||||
width?: number,
|
||||
height?: number,
|
||||
]
|
||||
|
||||
const justCrop = (x: number, y: number, width = 16, height = 16) => {
|
||||
return currentImage.clone().crop(x, y, width, height)
|
||||
}
|
||||
|
||||
const combineTextures = (locations: TextureMap[]) => {
|
||||
const resized: Jimp[] = []
|
||||
for (const [x, y, height = 16, width = 16] of locations) {
|
||||
resized.push(justCrop(x, y, width, height))
|
||||
}
|
||||
|
||||
const combinedImage = new Jimp(locations[0]![2] ?? 16, locations[0]![3] ?? 16)
|
||||
for (const image of resized) {
|
||||
combinedImage.blit(image, 0, 0)
|
||||
}
|
||||
return combinedImage
|
||||
}
|
||||
|
||||
const generatedImageTextures: { [blockName: string]: /* base64 */string } = {}
|
||||
|
||||
const getBlockTexturesFromJimp = async <T extends Record<string, Jimp>> (sides: T, withUv = false): Promise<Record<keyof T, any>> => {
|
||||
const sidesTextures = {} as any
|
||||
for (const [side, jimp] of Object.entries(sides)) {
|
||||
const textureName = `${currentBlockName}_${side}`
|
||||
const sideTexture = withUv ? { uv: [0, 0, jimp.getWidth(), jimp.getHeight()], texture: textureName } : textureName
|
||||
const base64 = await jimp.getBase64Async(jimp.getMIME())
|
||||
if (side === 'side') {
|
||||
sidesTextures['north'] = sideTexture
|
||||
sidesTextures['east'] = sideTexture
|
||||
sidesTextures['south'] = sideTexture
|
||||
sidesTextures['west'] = sideTexture
|
||||
} else {
|
||||
sidesTextures[side] = sideTexture
|
||||
}
|
||||
generatedImageTextures[textureName] = base64
|
||||
}
|
||||
|
||||
return sidesTextures
|
||||
}
|
||||
|
||||
const addSimpleCubeWithSides = async (sides: Record<string, Jimp>) => {
|
||||
const sidesTextures = await getBlockTexturesFromJimp(sides)
|
||||
|
||||
addBlockCustomSidesModel(currentBlockName, sidesTextures as any)
|
||||
}
|
||||
|
||||
const handleShulkerBox = async (dataBase: string, match: RegExpExecArray) => {
|
||||
const [, shulkerColor = ''] = match
|
||||
currentImage = await Jimp.read(dataBase + `entity/shulker/shulker${shulkerColor && `_${shulkerColor}`}.png`)
|
||||
|
||||
const shulkerBoxTextures = {
|
||||
// todo do all sides
|
||||
side: combineTextures([
|
||||
[0, 16], // top
|
||||
[0, 36], // bottom
|
||||
]),
|
||||
up: justCrop(16, 0),
|
||||
down: justCrop(32, 28)
|
||||
}
|
||||
|
||||
await addSimpleCubeWithSides(shulkerBoxTextures)
|
||||
}
|
||||
|
||||
const handleSign = async (dataBase: string, match: RegExpExecArray) => {
|
||||
const states = getBlockStates(currentBlockName, currentBlockName === 'wall_sign' ? 'wall_sign' : 'sign')
|
||||
if (!states) return
|
||||
|
||||
const [, signMaterial = ''] = match
|
||||
currentImage = await Jimp.read(`${dataBase}entity/${signMaterial ? `signs/${signMaterial}` : 'sign'}.png`)
|
||||
// todo cache
|
||||
const signTextures = {
|
||||
// todo correct mapping
|
||||
// todo alg to fit to the side
|
||||
signboard_side: justCrop(0, 2, 2, 12),
|
||||
face: justCrop(2, 2, 24, 12),
|
||||
up: justCrop(2, 0, 24, 2),
|
||||
support: justCrop(0, 16, 2, 14)
|
||||
}
|
||||
const blockTextures = await getBlockTexturesFromJimp(signTextures, true)
|
||||
|
||||
const isWall = currentBlockName.includes('wall_')
|
||||
const isHanging = currentBlockName.includes('hanging_')
|
||||
const rotationState = states.find(state => state.name === 'rotation')
|
||||
if (isWall || isHanging) {
|
||||
// todo isHanging
|
||||
if (!isHanging) {
|
||||
const facingState = states.find(state => state.name === 'facing')
|
||||
const facingMap = {
|
||||
south: 0,
|
||||
west: 90,
|
||||
north: 180,
|
||||
east: 270
|
||||
}
|
||||
|
||||
currentMcAssets.blocksStates[currentBlockName] = {
|
||||
"variants": Object.fromEntries(
|
||||
facingState.values!.map((_val, i) => {
|
||||
const val = _val as string
|
||||
return [`facing=${val}`, {
|
||||
"model": currentBlockName,
|
||||
y: facingMap[val],
|
||||
}]
|
||||
})
|
||||
)
|
||||
}
|
||||
currentMcAssets.blocksModels[currentBlockName] = {
|
||||
elements: [
|
||||
{
|
||||
// signboard
|
||||
"from": [0, 4.5, 0],
|
||||
"to": [16, 11.5, 1.5],
|
||||
faces: {
|
||||
// north: { texture: blockTextures.face, uv: [0, 0, 16, 16] },
|
||||
south: { texture: blockTextures.face.texture, uv: [0, 0, 16, 16] },
|
||||
east: { texture: blockTextures.signboard_side.texture, uv: [0, 0, 16, 16] },
|
||||
west: { texture: blockTextures.signboard_side.texture, uv: [0, 0, 16, 16] },
|
||||
up: { texture: blockTextures.up.texture, uv: [0, 0, 16, 16] },
|
||||
down: { texture: blockTextures.up.texture, uv: [0, 0, 16, 16] },
|
||||
},
|
||||
}
|
||||
],
|
||||
}
|
||||
}
|
||||
} else if (rotationState) {
|
||||
currentMcAssets.blocksStates[currentBlockName] = {
|
||||
"variants": Object.fromEntries(
|
||||
Array.from({ length: 16 }).map((_val, i) => {
|
||||
return [`rotation=${i}`, {
|
||||
"model": currentBlockName,
|
||||
y: i * 45,
|
||||
}]
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
const supportTexture = blockTextures.support
|
||||
// TODO fix models.ts, apply textures for signs correctly!
|
||||
// const supportTexture = { texture: supportTextureImg, uv: [0, 0, 16, 16] }
|
||||
currentMcAssets.blocksModels[currentBlockName] = {
|
||||
elements: [
|
||||
{
|
||||
// support post
|
||||
"from": [7.5, 0, 7.5],
|
||||
"to": [8.5, 9, 8.5],
|
||||
faces: {
|
||||
// todo 14
|
||||
north: supportTexture,
|
||||
east: supportTexture,
|
||||
south: supportTexture,
|
||||
west: supportTexture,
|
||||
}
|
||||
},
|
||||
{
|
||||
// signboard
|
||||
"from": [0, 9, 7.25],
|
||||
"to": [16, 16, 8.75],
|
||||
faces: {
|
||||
north: { texture: blockTextures.face.texture, uv: [0, 0, 16, 16] },
|
||||
south: { texture: blockTextures.face.texture, uv: [0, 0, 16, 16] },
|
||||
east: { texture: blockTextures.signboard_side.texture, uv: [0, 0, 16, 16] },
|
||||
west: { texture: blockTextures.signboard_side.texture, uv: [0, 0, 16, 16] },
|
||||
up: { texture: blockTextures.up.texture, uv: [0, 0, 16, 16] },
|
||||
down: { texture: blockTextures.up.texture, uv: [0, 0, 16, 16] },
|
||||
},
|
||||
}
|
||||
],
|
||||
}
|
||||
}
|
||||
twoBlockTextures.push(blockTextures.face.texture)
|
||||
}
|
||||
|
||||
const handlers = [
|
||||
[/(.+)_shulker_box$/, handleShulkerBox],
|
||||
[/^shulker_box$/, handleShulkerBox],
|
||||
[/^sign$/, handleSign],
|
||||
[/^standing_sign$/, handleSign],
|
||||
[/^wall_sign$/, handleSign],
|
||||
[/(.+)_wall_sign$/, handleSign],
|
||||
[/(.+)_sign$/, handleSign],
|
||||
// no-op just suppress warning
|
||||
[/(^light|^moving_piston$)/, true],
|
||||
] as const
|
||||
|
||||
export const tryHandleBlockEntity = async (dataBase, blockName) => {
|
||||
currentBlockName = blockName
|
||||
for (const [regex, handler] of handlers) {
|
||||
const match = regex.exec(blockName)
|
||||
if (!match) continue
|
||||
if (handler !== true) {
|
||||
await handler(dataBase, match)
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
export const prepareMoreGeneratedBlocks = async (mcAssets: McAssets) => {
|
||||
const mcData = minecraftData(mcAssets.version)
|
||||
//@ts-expect-error
|
||||
isPreFlattening = !mcData.supportFeature('blockStateId')
|
||||
const allTheBlocks = mcData.blocksArray.map(x => x.name)
|
||||
|
||||
currentMcAssets = mcAssets
|
||||
const handledBlocks = ['water', 'lava', 'barrier']
|
||||
// todo
|
||||
const ignoredBlocks = ['skull', 'structure_void', 'banner', 'bed', 'end_portal']
|
||||
|
||||
for (const theBlock of allTheBlocks) {
|
||||
try {
|
||||
if (await tryHandleBlockEntity(mcAssets.directory, theBlock)) {
|
||||
handledBlocks.push(theBlock)
|
||||
}
|
||||
} catch (err) {
|
||||
// todo remove when all warnings are resolved
|
||||
console.warn(`[${mcAssets.version}] failed to generate block ${theBlock}`)
|
||||
}
|
||||
}
|
||||
|
||||
const warnings = []
|
||||
for (const [name, model] of Object.entries(mcAssets.blocksModels)) {
|
||||
if (Object.keys(model).length === 1 && model.textures) {
|
||||
const keys = Object.keys(model.textures)
|
||||
if (keys.length === 1 && keys[0] === 'particle') {
|
||||
if (handledBlocks.includes(name) || ignoredBlocks.includes(name)) continue
|
||||
warnings.push(`unhandled block ${name}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { warnings }
|
||||
}
|
||||
|
||||
export const getAdditionalTextures = () => {
|
||||
return { generated: generatedImageTextures, twoBlockTextures }
|
||||
}
|
||||
|
||||
// test below
|
||||
// const dataBase = '...'
|
||||
// const blockName = 'light_blue_shulker_box'
|
||||
|
||||
// currentMcAssets = {
|
||||
// blocksModels: {},
|
||||
// blocksStates: {}
|
||||
// } as any
|
||||
// tryHandleBlockEntity(dataBase, blockName).then(() => {
|
||||
// for (const [key, value] of Object.entries(generatedImageTextures)) {
|
||||
// console.log(key, value)
|
||||
// }
|
||||
// console.log(currentMcAssets)
|
||||
// })
|
||||
// { name: 'chest_top', x: 0, y: 0, width: 16, height: 15 },
|
||||
// { name: 'chest_side_top', x: 0, y: 15, width: 16, height: 5 },
|
||||
// { name: 'chest_side_bottom', x: 0, y: 34, width: 16, height: 9 },
|
||||
// { name: 'chest_front', x: 0, y: 43, width: 16, height: 14 },
|
||||
21
prismarine-viewer/viewer/sign-renderer/index.html
Normal file
21
prismarine-viewer/viewer/sign-renderer/index.html
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no, viewport-fit=cover" />
|
||||
<title>%VITE_NAME%</title>
|
||||
<style>
|
||||
@font-face {
|
||||
font-family: mojangles;
|
||||
src: url(../../../assets/mojangles.ttf);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="root" style="font-family: mojangles;">test</div>
|
||||
<script src="./playground.ts" type="module"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
124
prismarine-viewer/viewer/sign-renderer/index.ts
Normal file
124
prismarine-viewer/viewer/sign-renderer/index.ts
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
import { fromFormattedString, render, RenderNode, TextComponent } from '@xmcl/text-component'
|
||||
import type { ChatMessage } from 'prismarine-chat'
|
||||
|
||||
type SignBlockEntity = {
|
||||
Color?: string
|
||||
GlowingText?: 0 | 1
|
||||
Text1?: string
|
||||
Text2?: string
|
||||
Text3?: string
|
||||
Text4?: string
|
||||
} | {
|
||||
// todo
|
||||
is_waxed: 0 | 1
|
||||
front_text: {
|
||||
// todo
|
||||
// has_glowing_text: 0 | 1
|
||||
color: string
|
||||
messages: string[]
|
||||
}
|
||||
// todo
|
||||
// back_text: {}
|
||||
}
|
||||
|
||||
type JsonEncodedType = string | null | Record<string, any>
|
||||
|
||||
const parseSafe = (text: string, task: string) => {
|
||||
try {
|
||||
return JSON.parse(text)
|
||||
} catch (e) {
|
||||
console.warn(`Failed to parse ${task}`, e)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export const renderSign = (blockEntity: SignBlockEntity, PrismarineChat: typeof ChatMessage, ctxHook = (ctx) => { }) => {
|
||||
const canvas = document.createElement('canvas')
|
||||
|
||||
const factor = 50
|
||||
const signboardY = [16, 9]
|
||||
const heightOffset = signboardY[0] - signboardY[1]
|
||||
const heightScalar = heightOffset / 16
|
||||
|
||||
canvas.width = 16 * factor
|
||||
canvas.height = heightOffset * factor
|
||||
|
||||
const ctx = canvas.getContext('2d')!
|
||||
ctx.imageSmoothingEnabled = false
|
||||
|
||||
ctxHook(ctx)
|
||||
|
||||
const texts = 'is_waxed' in blockEntity ? /* > 1.20 */ blockEntity.front_text.messages : [
|
||||
blockEntity.Text1,
|
||||
blockEntity.Text2,
|
||||
blockEntity.Text3,
|
||||
blockEntity.Text4
|
||||
]
|
||||
const defaultColor = ('front_text' in blockEntity ? blockEntity.front_text.color : blockEntity.Color) || 'black'
|
||||
for (let [lineNum, text] of texts.slice(0, 4).entries()) {
|
||||
// todo test mojangson parsing
|
||||
const parsed = parseSafe(text ?? '""', 'sign 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: {
|
||||
fontStyle: string
|
||||
fillStyle: string
|
||||
underlineStyle: boolean
|
||||
strikeStyle: boolean
|
||||
text: string
|
||||
}[] = []
|
||||
let plainText = ''
|
||||
const MAX_LENGTH = 15 // 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)
|
||||
|
||||
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?
|
||||
}
|
||||
}
|
||||
// ctx.fillStyle = 'red'
|
||||
// ctx.fillRect(0, 0, canvas.width, canvas.height)
|
||||
|
||||
return canvas
|
||||
}
|
||||
1
prismarine-viewer/viewer/sign-renderer/noop.js
Normal file
1
prismarine-viewer/viewer/sign-renderer/noop.js
Normal file
|
|
@ -0,0 +1 @@
|
|||
module.exports = {}
|
||||
14
prismarine-viewer/viewer/sign-renderer/package.json
Normal file
14
prismarine-viewer/viewer/sign-renderer/package.json
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"name": "sign-renderer",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"license": "MIT",
|
||||
"main": "index.ts",
|
||||
"scripts": {
|
||||
"start": "vite"
|
||||
},
|
||||
"dependencies": {
|
||||
"@xmcl/text-component": "^2.1.2",
|
||||
"vite": "^4.4.9"
|
||||
}
|
||||
}
|
||||
26
prismarine-viewer/viewer/sign-renderer/playground.ts
Normal file
26
prismarine-viewer/viewer/sign-renderer/playground.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import { renderSign } from '.'
|
||||
import PrismarineChatLoader from 'prismarine-chat'
|
||||
|
||||
const PrismarineChat = PrismarineChatLoader({ language: {} } as any)
|
||||
|
||||
const img = new Image()
|
||||
img.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAMCAYAAAB4MH11AAABbElEQVR4AY3BQY6cMBBA0Q+yQZZVi+ndcJVcKGfMgegdvShKVtuokzGSWwwiUd7rfv388Vst0UgMXCobmgsSA5VaQmKgUks0EgNHji8SA9W8GJCQwVNpLhzJ4KFs4B1HEgPVvBiQkMFTaS44tYTEQDXdIkfiHbuyobmguaDPFzIWGrWExEA13SJH4h1uzS/WbPyvroM1v6jWbFRrNv7GfX5EdmXjzTvUEjJ4zjQXjiQGdmXjzTvUEjJ4HF/UEt/kQqW5UEkMzIshY08jg6dRS3yTC5XmgpsXY7pFztQSEgPNJCNv3lGpJVSfTLfImVpCYsB1HdwfxpU1G9eeNF0H94dxZc2G+/yI7MoG3vEv82LI2NNIDLyVDbzjzFE2mnkxZOy5IoNnkpFGc2FXNpp5MWTsOXJ4h1qikrGnkhjYlY1m1icy9lQSA+TCzjvUEpWMPZXEwK5suPvDOFuzcdZ1sOYX1ZqNas3GlTUbzR+jQbEAcs8ZQAAAAABJRU5ErkJggg=='
|
||||
|
||||
await new Promise<void>(resolve => {
|
||||
img.onload = () => resolve()
|
||||
})
|
||||
|
||||
const blockEntity = {
|
||||
"GlowingText": 0,
|
||||
"Color": "black",
|
||||
"Text4": "{\"text\":\"\"}",
|
||||
"Text3": "{\"text\":\"\"}",
|
||||
"Text2": "{\"text\":\"\"}",
|
||||
"Text1": "{\"extra\":[{\"color\":\"dark_green\",\"text\":\"Minecraft \"},{\"text\":\"Tools\"}],\"text\":\"\"}"
|
||||
} as const
|
||||
|
||||
const canvas = renderSign(blockEntity, PrismarineChat, (ctx) => {
|
||||
ctx.drawImage(img, 0, 0, ctx.canvas.width, ctx.canvas.height)
|
||||
})
|
||||
|
||||
document.body.appendChild(canvas)
|
||||
10
prismarine-viewer/viewer/sign-renderer/vite.config.ts
Normal file
10
prismarine-viewer/viewer/sign-renderer/vite.config.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import { defineConfig } from 'vite'
|
||||
|
||||
export default defineConfig({
|
||||
resolve: {
|
||||
alias: {
|
||||
'prismarine-registry': "./noop.js",
|
||||
'prismarine-nbt': "./noop.js"
|
||||
},
|
||||
},
|
||||
})
|
||||
|
|
@ -228,6 +228,9 @@ document.addEventListener('keydown', (e) => {
|
|||
for (const [x, z] of loadedChunks) {
|
||||
worldView.unloadChunk({ x, z })
|
||||
}
|
||||
if (localServer) {
|
||||
localServer.players[0].world.columns = {}
|
||||
}
|
||||
reloadChunks()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ export const startLocalServer = () => {
|
|||
const server = mcServer.createMCServer(passOptions)
|
||||
server.formatMessage = (message) => `[server] ${message}`
|
||||
server.options = passOptions
|
||||
server.looseProtocolMode = true
|
||||
return server
|
||||
}
|
||||
|
||||
|
|
|
|||
3
src/globals.d.ts
vendored
3
src/globals.d.ts
vendored
|
|
@ -2,7 +2,8 @@
|
|||
|
||||
declare const THREE: typeof import('three')
|
||||
// todo make optional
|
||||
declare const bot: import('mineflayer').Bot
|
||||
declare const bot: Omit<import('mineflayer').Bot, 'world'> & { world: import('prismarine-world').world.WorldSync }
|
||||
declare const __type_bot: typeof bot
|
||||
declare const viewer: import('prismarine-viewer/viewer/lib/viewer').Viewer | undefined
|
||||
declare const worldView: import('prismarine-viewer/viewer/lib/worldDataEmitter').WorldDataEmitter | undefined
|
||||
declare const localServer: any
|
||||
|
|
|
|||
20
src/index.ts
20
src/index.ts
|
|
@ -85,22 +85,15 @@ import { connectToPeer } from './localServerMultiplayer'
|
|||
import CustomChannelClient from './customClient'
|
||||
import debug from 'debug'
|
||||
import { loadScript } from 'prismarine-viewer/viewer/lib/utils'
|
||||
import { registerServiceWorker } from './serviceWorker'
|
||||
|
||||
window.debug = debug
|
||||
window.THREE = THREE
|
||||
|
||||
if ('serviceWorker' in navigator && !isCypress() && process.env.NODE_ENV !== 'development') {
|
||||
window.addEventListener('load', () => {
|
||||
navigator.serviceWorker.register('./service-worker.js').then(registration => {
|
||||
console.log('SW registered:', registration)
|
||||
}).catch(registrationError => {
|
||||
console.log('SW registration failed:', registrationError)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// ACTUAL CODE
|
||||
|
||||
void registerServiceWorker()
|
||||
|
||||
// Create three.js context, add to page
|
||||
const renderer = new THREE.WebGLRenderer({
|
||||
powerPreference: options.highPerformanceGpu ? 'high-performance' : 'default',
|
||||
|
|
@ -183,7 +176,6 @@ function onCameraMove(e) {
|
|||
x: e.movementX * mouseSensX * 0.0001,
|
||||
y: e.movementY * mouseSensY * 0.0001
|
||||
})
|
||||
// todo do it also on every block update within radius 5
|
||||
updateCursor()
|
||||
}
|
||||
window.addEventListener('mousemove', onCameraMove, { capture: true })
|
||||
|
|
@ -263,7 +255,7 @@ async function connect(connectOptions: {
|
|||
setLoadingScreenStatus('Logging in')
|
||||
|
||||
let ended = false
|
||||
let bot: mineflayer.Bot
|
||||
let bot: typeof __type_bot
|
||||
const destroyAll = () => {
|
||||
if (ended) return
|
||||
ended = true
|
||||
|
|
@ -404,7 +396,7 @@ async function connect(connectOptions: {
|
|||
await downloadMcData(client.version)
|
||||
setLoadingScreenStatus('Connecting to server')
|
||||
}
|
||||
})
|
||||
}) as unknown as typeof __type_bot
|
||||
window.bot = bot
|
||||
if (singeplayer || p2pMultiplayer) {
|
||||
// p2pMultiplayer still uses the same flying-squid server
|
||||
|
|
@ -495,7 +487,7 @@ async function connect(connectOptions: {
|
|||
|
||||
const center = bot.entity.position
|
||||
|
||||
window.worldView = new WorldDataEmitter(bot.world, singeplayer ? renderDistance : Math.min(renderDistance, maxMultiplayerRenderDistance), center)
|
||||
const worldView = window.worldView = new WorldDataEmitter(bot.world, singeplayer ? renderDistance : Math.min(renderDistance, maxMultiplayerRenderDistance), center)
|
||||
setRenderDistance()
|
||||
|
||||
const updateFov = () => {
|
||||
|
|
|
|||
23
src/serviceWorker.ts
Normal file
23
src/serviceWorker.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import { isCypress } from './utils'
|
||||
|
||||
export const registerServiceWorker = async () => {
|
||||
if (!('serviceWorker' in navigator)) return
|
||||
if (!isCypress() && process.env.NODE_ENV !== 'development') {
|
||||
window.addEventListener('load', () => {
|
||||
navigator.serviceWorker.register('./service-worker.js').then(registration => {
|
||||
console.log('SW registered:', registration)
|
||||
}).catch(registrationError => {
|
||||
console.log('SW registration failed:', registrationError)
|
||||
})
|
||||
})
|
||||
} else {
|
||||
// force unregister service worker in development mode
|
||||
const registrations = await navigator.serviceWorker.getRegistrations()
|
||||
for (const registration of registrations) {
|
||||
await registration.unregister() // eslint-disable-line no-await-in-loop
|
||||
}
|
||||
if (registrations.length) {
|
||||
location.reload()
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue