Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: custom block models via custom channel #277

Merged
merged 24 commits into from
Feb 17, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions renderer/viewer/lib/mesher/mesher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,19 +106,26 @@ const handleMessage = data => {
}
case 'chunk': {
world.addColumn(data.x, data.z, data.chunk)

if (data.customBlockModels) {
const chunkKey = `${data.x},${data.z}`
world.customBlockModels.set(chunkKey, data.customBlockModels)
}
break
}
case 'unloadChunk': {
world.removeColumn(data.x, data.z)
world.customBlockModels.delete(`${data.x},${data.z}`)
if (Object.keys(world.columns).length === 0) softCleanup()

break
}
case 'blockUpdate': {
const loc = new Vec3(data.pos.x, data.pos.y, data.pos.z).floored()
world.setBlockStateId(loc, data.stateId)

const chunkKey = `${Math.floor(loc.x / 16) * 16},${Math.floor(loc.z / 16) * 16}`
if (data.customBlockModels) {
world.customBlockModels.set(chunkKey, data.customBlockModels)
}
break
}
case 'reset': {
Expand Down
5 changes: 5 additions & 0 deletions renderer/viewer/lib/mesher/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ export const defaultMesherConfig = {
disableSignsMapsSupport: false
}

export type CustomBlockModels = {
[blockPosKey: string]: string // blockPosKey is "x,y,z" -> model name
}

export type MesherConfig = typeof defaultMesherConfig

export type MesherGeometryOutput = {
Expand All @@ -36,6 +40,7 @@ export type MesherGeometryOutput = {
highestBlocks: Map<string, HighestBlockInfo>
hadErrors: boolean
blocksCount: number
customBlockModels?: CustomBlockModels
}

export type HighestBlockInfo = { y: number, stateId: number | undefined, biomeId: number | undefined }
19 changes: 14 additions & 5 deletions renderer/viewer/lib/mesher/world.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { Vec3 } from 'vec3'
import { WorldBlockProvider } from 'mc-assets/dist/worldBlockProvider'
import moreBlockDataGeneratedJson from '../moreBlockDataGenerated.json'
import legacyJson from '../../../../src/preflatMap.json'
import { defaultMesherConfig } from './shared'
import { defaultMesherConfig, CustomBlockModels } from './shared'
import { INVISIBLE_BLOCKS } from './worldConstants'

const ignoreAoBlocks = Object.keys(moreBlockDataGeneratedJson.noOcclusions)
Expand Down Expand Up @@ -48,6 +48,7 @@ export class World {
biomeCache: { [id: number]: mcData.Biome }
preflat: boolean
erroredBlockModel?: BlockModelPartsResolved
customBlockModels = new Map<string, CustomBlockModels>() // chunkKey -> blockModels

constructor (version) {
this.Chunk = Chunks(version) as any
Expand Down Expand Up @@ -126,6 +127,8 @@ export class World {
// for easier testing
if (!(pos instanceof Vec3)) pos = new Vec3(...pos as [number, number, number])
const key = columnKey(Math.floor(pos.x / 16) * 16, Math.floor(pos.z / 16) * 16)
const blockPosKey = `${pos.x},${pos.y},${pos.z}`
const modelOverride = this.customBlockModels.get(key)?.[blockPosKey]

const column = this.columns[key]
// null column means chunk not loaded
Expand All @@ -135,10 +138,15 @@ export class World {
const locInChunk = posInChunk(loc)
const stateId = column.getBlockStateId(locInChunk)

if (!this.blockCache[stateId]) {
const cacheKey = modelOverride ? `${stateId}:${modelOverride}` : stateId

if (!this.blockCache[cacheKey]) {
const b = column.getBlock(locInChunk) as unknown as WorldBlock
if (modelOverride) {
b.name = modelOverride
}
b.isCube = isCube(b.shapes)
this.blockCache[stateId] = b
this.blockCache[cacheKey] = b
Object.defineProperty(b, 'position', {
get () {
throw new Error('position is not reliable, use pos parameter instead of block.position')
Expand All @@ -163,7 +171,7 @@ export class World {
}
}

const block = this.blockCache[stateId]
const block = this.blockCache[cacheKey]

if (block.models === undefined && blockProvider) {
if (!attr) throw new Error('attr is required')
Expand All @@ -188,10 +196,11 @@ export class World {
}
}

const useFallbackModel = this.preflat || modelOverride
block.models = blockProvider.getAllResolvedModels0_1({
name: block.name,
properties: props,
}, this.preflat)! // fixme! this is a hack (also need a setting for all versions)
}, useFallbackModel)! // fixme! this is a hack (also need a setting for all versions)
if (!block.models!.length) {
if (block.name !== 'water' && block.name !== 'lava' && !INVISIBLE_BLOCKS.has(block.name)) {
console.debug('[mesher] block to render not found', block.name, props)
Expand Down
1 change: 0 additions & 1 deletion renderer/viewer/lib/viewer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,6 @@ export class Viewer {
async demoModel () {
//@ts-expect-error
const pos = cursorBlockRel(0, 1, 0).position
const blockProvider = worldBlockProvider(this.world.blockstatesModels, this.world.blocksAtlasParser!.atlas, 'latest')

const mesh = await getMyHand()
// mesh.rotation.y = THREE.MathUtils.degToRad(90)
Expand Down
35 changes: 29 additions & 6 deletions renderer/viewer/lib/worldrendererCommon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,11 @@ import { LineMaterial } from 'three-stdlib'
import christmasPack from 'mc-assets/dist/textureReplacements/christmas'
import { ItemsRenderer } from 'mc-assets/dist/itemsRenderer'
import itemDefinitionsJson from 'mc-assets/dist/itemDefinitions.json'
import worldBlockProvider, { WorldBlockProvider } from 'mc-assets/dist/worldBlockProvider'
import { dynamicMcDataFiles } from '../../buildMesherConfig.mjs'
import { toMajorVersion } from '../../../src/utils'
import { buildCleanupDecorator } from './cleanupDecorator'
import { defaultMesherConfig, HighestBlockInfo, MesherGeometryOutput } from './mesher/shared'
import { defaultMesherConfig, HighestBlockInfo, MesherGeometryOutput, CustomBlockModels } from './mesher/shared'
import { chunkPos } from './simpleUtils'
import { HandItemBlock } from './holdingBlock'
import { updateStatText } from './ui/newStats'
Expand Down Expand Up @@ -150,7 +151,10 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
@worldCleanup()
itemsRenderer: ItemsRenderer | undefined

customBlockModels = new Map<string, CustomBlockModels>()

abstract outputFormat: 'threeJs' | 'webgpu'
worldBlockProvider: WorldBlockProvider

abstract changeBackgroundColor (color: [number, number, number]): void

Expand Down Expand Up @@ -315,7 +319,7 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
this.mesherConfig.version = this.version!

this.sendMesherMcData()
await this.updateTexturesData()
await this.updateAssetsData()
}

sendMesherMcData () {
Expand All @@ -332,7 +336,7 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
}
}

async updateTexturesData (resourcePackUpdate = false, prioritizeBlockTextures?: string[]) {
async updateAssetsData (resourcePackUpdate = false, prioritizeBlockTextures?: string[]) {
const blocksAssetsParser = new AtlasParser(this.sourceData.blocksAtlases, blocksAtlasLatest, blocksAtlasLegacy)
const itemsAssetsParser = new AtlasParser(this.sourceData.itemsAtlases, itemsAtlasLatest, itemsAtlasLegacy)

Expand All @@ -356,6 +360,7 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
this.itemsAtlasParser = new AtlasParser({ latest: itemsAtlas }, itemsCanvas.toDataURL())

this.itemsRenderer = new ItemsRenderer(this.version!, this.blockstatesModels, this.itemsAtlasParser, this.blocksAtlasParser)
this.worldBlockProvider = worldBlockProvider(this.blockstatesModels, this.blocksAtlasParser.atlas, 'latest')

const texture = await new THREE.TextureLoader().loadAsync(this.blocksAtlasParser.latestImage)
texture.magFilter = THREE.NearestFilter
Expand Down Expand Up @@ -409,9 +414,18 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
this.initialChunkLoadWasStartedIn ??= Date.now()
this.loadedChunks[`${x},${z}`] = true
this.updateChunksStatsText()

const chunkKey = `${x},${z}`
const customBlockModels = this.customBlockModels.get(chunkKey)

for (const worker of this.workers) {
// todo optimize
worker.postMessage({ type: 'chunk', x, z, chunk })
worker.postMessage({
type: 'chunk',
x,
z,
chunk,
customBlockModels: customBlockModels || undefined
})
}
for (let y = this.worldMinYRender; y < this.worldConfig.worldHeight; y += 16) {
const loc = new Vec3(x, y, z)
Expand Down Expand Up @@ -461,8 +475,17 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>

setBlockStateId (pos: Vec3, stateId: number) {
const needAoRecalculation = true
const chunkKey = `${Math.floor(pos.x / 16) * 16},${Math.floor(pos.z / 16) * 16}`
const blockPosKey = `${pos.x},${pos.y},${pos.z}`
const customBlockModels = this.customBlockModels.get(chunkKey) || {}

for (const worker of this.workers) {
worker.postMessage({ type: 'blockUpdate', pos, stateId })
worker.postMessage({
type: 'blockUpdate',
pos,
stateId,
customBlockModels
})
}
this.setSectionDirty(pos, true, true)
if (this.neighborChunkUpdates) {
Expand Down
75 changes: 75 additions & 0 deletions src/customChannels.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { Vec3 } from 'vec3'
import { options } from './optionsStorage'

customEvents.on('mineflayerBotCreated', async () => {
if (!options.customChannels) return
await new Promise(resolve => {
bot.once('login', () => {
resolve(true)
})
})

const CHANNEL_NAME = 'minecraft-web-client:blockmodels'

const packetStructure = [
'container',
[
{
name: 'worldName', // currently not used
type: ['pstring', { countType: 'i16' }]
},
{
name: 'x',
type: 'i32'
},
{
name: 'y',
type: 'i32'
},
{
name: 'z',
type: 'i32'
},
{
name: 'model',
type: ['pstring', { countType: 'i16' }]
}
]
]

bot._client.registerChannel(CHANNEL_NAME, packetStructure, true)

bot._client.on(CHANNEL_NAME as any, (data) => {
const { worldName, x, y, z, model } = data
console.debug('Received model data:', { worldName, x, y, z, model })

if (viewer?.world) {
const chunkX = Math.floor(x / 16) * 16
const chunkZ = Math.floor(z / 16) * 16
const chunkKey = `${chunkX},${chunkZ}`
const blockPosKey = `${x},${y},${z}`

const chunkModels = viewer.world.customBlockModels.get(chunkKey) || {}

if (model) {
chunkModels[blockPosKey] = model
} else {
delete chunkModels[blockPosKey]
}

if (Object.keys(chunkModels).length > 0) {
viewer.world.customBlockModels.set(chunkKey, chunkModels)
} else {
viewer.world.customBlockModels.delete(chunkKey)
}

// Trigger update
const block = worldView!.world.getBlock(new Vec3(x, y, z))
if (block) {
worldView!.world.setBlockStateId(new Vec3(x, y, z), block.stateId)
}
}
})

console.debug(`registered custom channel ${CHANNEL_NAME} channel`)
})
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import './testCrasher'
import './globals'
import './devtools'
import './entities'
import './customChannels'
import './globalDomListeners'
import './mineflayer/maps'
import './mineflayer/cameraShake'
Expand Down
1 change: 1 addition & 0 deletions src/optionsStorage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ const defaultOptions = {
viewBobbing: true,
packetsLoggerPreset: 'all' as 'all' | 'no-buffers',
serversAutoVersionSelect: 'auto' as 'auto' | 'latest' | '1.20.4' | string,
customChannels: false,

// antiAliasing: false,

Expand Down
2 changes: 1 addition & 1 deletion src/resourcePack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -506,7 +506,7 @@ const updateTextures = async () => {
}
}
if (viewer.world.active) {
await viewer.world.updateTexturesData()
await viewer.world.updateAssetsData()
if (viewer.world instanceof WorldRendererThree) {
viewer.world.rerenderAllChunks?.()
}
Expand Down
Loading