247 lines
8.5 KiB
TypeScript
247 lines
8.5 KiB
TypeScript
import Chunks from 'prismarine-chunk'
|
|
import mcData from 'minecraft-data'
|
|
import { Block } from 'prismarine-block'
|
|
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 { INVISIBLE_BLOCKS } from './worldConstants'
|
|
|
|
const ignoreAoBlocks = Object.keys(moreBlockDataGeneratedJson.noOcclusions)
|
|
|
|
const ALWAYS_WATERLOGGED = new Set([
|
|
'seagrass',
|
|
'tall_seagrass',
|
|
'kelp',
|
|
'kelp_plant',
|
|
'bubble_column'
|
|
])
|
|
|
|
function columnKey (x, z) {
|
|
return `${x},${z}`
|
|
}
|
|
|
|
function isCube (shapes) {
|
|
if (!shapes || shapes.length !== 1) return false
|
|
const shape = shapes[0]
|
|
return shape[0] === 0 && shape[1] === 0 && shape[2] === 0 && shape[3] === 1 && shape[4] === 1 && shape[5] === 1
|
|
}
|
|
|
|
export type BlockModelPartsResolved = ReturnType<WorldBlockProvider['getAllResolvedModels0_1']>
|
|
|
|
export type WorldBlock = Omit<Block, 'position'> & {
|
|
// todo
|
|
isCube: boolean
|
|
/** cache */
|
|
models?: BlockModelPartsResolved | null
|
|
_originalProperties?: Record<string, any>
|
|
_properties?: Record<string, any>
|
|
}
|
|
|
|
|
|
export class World {
|
|
config = defaultMesherConfig
|
|
Chunk: typeof import('prismarine-chunk/types/index').PCChunk
|
|
columns = {} as { [key: string]: import('prismarine-chunk/types/index').PCChunk }
|
|
blockCache = {}
|
|
biomeCache: { [id: number]: mcData.Biome }
|
|
preflat: boolean
|
|
erroredBlockModel?: BlockModelPartsResolved
|
|
|
|
constructor (version) {
|
|
this.Chunk = Chunks(version) as any
|
|
this.biomeCache = mcData(version).biomes
|
|
this.preflat = !mcData(version).supportFeature('blockStateId')
|
|
this.config.version = version
|
|
}
|
|
|
|
getLight (pos: Vec3, isNeighbor = false, skipMoreChecks = false, curBlockName = '') {
|
|
// for easier testing
|
|
if (!(pos instanceof Vec3)) pos = new Vec3(...pos as [number, number, number])
|
|
const { enableLighting, skyLight } = this.config
|
|
if (!enableLighting) return 15
|
|
// const key = `${pos.x},${pos.y},${pos.z}`
|
|
// if (lightsCache.has(key)) return lightsCache.get(key)
|
|
const column = this.getColumnByPos(pos)
|
|
if (!column || !hasChunkSection(column, pos)) return 15
|
|
let result = Math.min(
|
|
15,
|
|
Math.max(
|
|
column.getBlockLight(posInChunk(pos)),
|
|
Math.min(skyLight, column.getSkyLight(posInChunk(pos)))
|
|
) + 2
|
|
)
|
|
// lightsCache.set(key, result)
|
|
if (result === 2 && [this.getBlock(pos)?.name ?? '', curBlockName].some(x => /_stairs|slab|glass_pane/.exec(x)) && !skipMoreChecks) { // todo this is obviously wrong
|
|
const lights = [
|
|
this.getLight(pos.offset(0, 1, 0), undefined, true),
|
|
this.getLight(pos.offset(0, -1, 0), undefined, true),
|
|
this.getLight(pos.offset(0, 0, 1), undefined, true),
|
|
this.getLight(pos.offset(0, 0, -1), undefined, true),
|
|
this.getLight(pos.offset(1, 0, 0), undefined, true),
|
|
this.getLight(pos.offset(-1, 0, 0), undefined, true)
|
|
].filter(x => x !== 2)
|
|
if (lights.length) {
|
|
const min = Math.min(...lights)
|
|
result = min
|
|
}
|
|
}
|
|
if (isNeighbor && result === 2) result = 15 // TODO
|
|
return result
|
|
}
|
|
|
|
addColumn (x, z, json) {
|
|
const chunk = this.Chunk.fromJson(json)
|
|
this.columns[columnKey(x, z)] = chunk as any
|
|
return chunk
|
|
}
|
|
|
|
removeColumn (x, z) {
|
|
delete this.columns[columnKey(x, z)]
|
|
}
|
|
|
|
getColumn (x, z) {
|
|
return this.columns[columnKey(x, z)]
|
|
}
|
|
|
|
setBlockStateId (pos: Vec3, stateId) {
|
|
if (stateId === undefined) throw new Error('stateId is undefined')
|
|
const key = columnKey(Math.floor(pos.x / 16) * 16, Math.floor(pos.z / 16) * 16)
|
|
|
|
const column = this.columns[key]
|
|
// null column means chunk not loaded
|
|
if (!column) return false
|
|
|
|
column.setBlockStateId(posInChunk(pos.floored()), stateId)
|
|
|
|
return true
|
|
}
|
|
|
|
getColumnByPos (pos: Vec3) {
|
|
return this.getColumn(Math.floor(pos.x / 16) * 16, Math.floor(pos.z / 16) * 16)
|
|
}
|
|
|
|
getBlock (pos: Vec3, blockProvider?, attr?): WorldBlock | null {
|
|
// 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 column = this.columns[key]
|
|
// null column means chunk not loaded
|
|
if (!column) return null
|
|
|
|
const loc = pos.floored()
|
|
const locInChunk = posInChunk(loc)
|
|
const stateId = column.getBlockStateId(locInChunk)
|
|
|
|
if (!this.blockCache[stateId]) {
|
|
const b = column.getBlock(locInChunk) as unknown as WorldBlock
|
|
b.isCube = isCube(b.shapes)
|
|
this.blockCache[stateId] = b
|
|
Object.defineProperty(b, 'position', {
|
|
get () {
|
|
throw new Error('position is not reliable, use pos parameter instead of block.position')
|
|
}
|
|
})
|
|
if (this.preflat) {
|
|
b._properties = {}
|
|
|
|
const namePropsStr = legacyJson.blocks[b.type + ':' + b.metadata] || findClosestLegacyBlockFallback(b.type, b.metadata, pos)
|
|
if (namePropsStr) {
|
|
b.name = namePropsStr.split('[')[0]
|
|
const propsStr = namePropsStr.split('[')?.[1]?.split(']')
|
|
if (propsStr) {
|
|
const newProperties = Object.fromEntries(propsStr.join('').split(',').map(x => {
|
|
let [key, val] = x.split('=')
|
|
if (!isNaN(val)) val = parseInt(val, 10)
|
|
return [key, val]
|
|
}))
|
|
b._properties = newProperties
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
const block = this.blockCache[stateId]
|
|
|
|
if (block.models === undefined && blockProvider) {
|
|
if (!attr) throw new Error('attr is required')
|
|
const props = block.getProperties()
|
|
|
|
// Patch waterlogged property for ocean plants
|
|
if (ALWAYS_WATERLOGGED.has(block.name)) {
|
|
props.waterlogged = 'true'
|
|
}
|
|
|
|
try {
|
|
// fixme
|
|
if (this.preflat) {
|
|
if (block.name === 'cobblestone_wall') {
|
|
props.up = 'true'
|
|
for (const key of ['north', 'south', 'east', 'west']) {
|
|
const val = props[key]
|
|
if (val === 'false' || val === 'true') {
|
|
props[key] = val === 'true' ? 'low' : 'none'
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
block.models = blockProvider.getAllResolvedModels0_1({
|
|
name: block.name,
|
|
properties: props,
|
|
}, this.preflat)! // 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)
|
|
}
|
|
block.models = null
|
|
}
|
|
} catch (err) {
|
|
this.erroredBlockModel ??= blockProvider.getAllResolvedModels0_1({ name: 'errored', properties: {} })
|
|
block.models ??= this.erroredBlockModel
|
|
console.error(`Critical assets error. Unable to get block model for ${block.name}[${JSON.stringify(props)}]: ` + err.message, err.stack)
|
|
attr.hadErrors = true
|
|
}
|
|
}
|
|
|
|
if (block.name === 'flowing_water') block.name = 'water'
|
|
if (block.name === 'flowing_lava') block.name = 'lava'
|
|
if (block.name === 'bubble_column') block.name = 'water' // TODO need to distinguish between water and bubble column
|
|
// block.position = loc // it overrides position of all currently loaded blocks
|
|
block.biome = this.biomeCache[column.getBiome(locInChunk)] ?? this.biomeCache[1] ?? this.biomeCache[0]
|
|
if (block.name === 'redstone_ore') block.transparent = false
|
|
return block
|
|
}
|
|
|
|
shouldMakeAo (block: WorldBlock | null) {
|
|
return block?.isCube && !ignoreAoBlocks.includes(block.name)
|
|
}
|
|
}
|
|
|
|
const findClosestLegacyBlockFallback = (id, metadata, pos) => {
|
|
console.warn(`[mesher] Unknown block with ${id}:${metadata} at ${pos}, falling back`) // todo has known issues
|
|
for (const [key, value] of Object.entries(legacyJson.blocks)) {
|
|
const [idKey, meta] = key.split(':')
|
|
if (idKey === id) return value
|
|
}
|
|
return null
|
|
}
|
|
|
|
// todo export in chunk instead
|
|
const hasChunkSection = (column, pos) => {
|
|
if (column._getSection) return column._getSection(pos)
|
|
if (column.skyLightSections) {
|
|
return column.skyLightSections[getLightSectionIndex(pos, column.minY)] || column.blockLightSections[getLightSectionIndex(pos, column.minY)]
|
|
}
|
|
if (column.sections) return column.sections[pos.y >> 4]
|
|
}
|
|
|
|
function posInChunk (pos) {
|
|
return new Vec3(Math.floor(pos.x) & 15, Math.floor(pos.y), Math.floor(pos.z) & 15)
|
|
}
|
|
|
|
function getLightSectionIndex (pos, minY = 0) {
|
|
return Math.floor((pos.y - minY) / 16) + 1
|
|
}
|