From 4ae65ef68757b8b67f7ef4bf055de7043f631fa3 Mon Sep 17 00:00:00 2001 From: Marc Neuhaus Date: Mon, 11 May 2026 21:48:33 +0200 Subject: [PATCH 1/2] Support 26.1.2 chunk fluid counts --- src/index.js | 3 ++- src/pc/1.18/ChunkColumn.js | 29 +++++++++++++++++++++++++--- src/pc/common/PaletteChunkSection.js | 25 ++++++++++++++++++++---- test/ChunkColumn.test.js | 29 ++++++++++++++++++++++++++++ test/ChunkSection.test.js | 25 ++++++++++++++++++++++++ test/versions.js | 2 +- 6 files changed, 104 insertions(+), 9 deletions(-) diff --git a/src/index.js b/src/index.js index 10ff985f..b4a2abe4 100644 --- a/src/index.js +++ b/src/index.js @@ -13,7 +13,8 @@ const chunkImplementations = { 1.18: require('./pc/1.18/chunk'), 1.19: require('./pc/1.18/chunk'), '1.20': require('./pc/1.18/chunk'), - 1.21: require('./pc/1.18/chunk') + 1.21: require('./pc/1.18/chunk'), + 26.1: require('./pc/1.18/chunk') }, bedrock: { 0.14: require('./bedrock/0.14/chunk'), diff --git a/src/pc/1.18/ChunkColumn.js b/src/pc/1.18/ChunkColumn.js index 88163f20..b83cf179 100644 --- a/src/pc/1.18/ChunkColumn.js +++ b/src/pc/1.18/ChunkColumn.js @@ -13,6 +13,19 @@ const CAVES_UPDATE_WORLD_HEIGHT = 384 module.exports = (Block, mcData) => { // 1.21.5+ writes no size prefix before chunk containers, it's computed dynamically to save 1 byte const noSizePrefix = mcData.version['>=']('1.21.5') + // 26.1.2 serializes a fluid-count short after nonEmptyBlockCount in each section. + const hasFluidCount = mcData.version.minecraftVersion === '26.1.2' + const fluidStateCache = new Map() + const fluidBlockNames = new Set(['water', 'lava', 'seagrass', 'tall_seagrass', 'kelp', 'kelp_plant', 'bubble_column']) + function hasFluidState (stateId) { + if (!stateId) return false + if (!fluidStateCache.has(stateId)) { + const block = Block.fromStateId(stateId) + fluidStateCache.set(stateId, fluidBlockNames.has(block.name) || block.isWaterlogged === true) + } + return fluidStateCache.get(stateId) + } + return class ChunkColumn extends CommonChunkColumn { static get section () { return ChunkSection } constructor (options) { @@ -24,7 +37,7 @@ module.exports = (Block, mcData) => { this.maxBitsPerBiome = neededBits(Object.values(mcData.biomes).length) this.sections = options?.sections ?? Array.from( - { length: this.numSections }, _ => new ChunkSection({ noSizePrefix, maxBitsPerBlock: this.maxBitsPerBlock }) + { length: this.numSections }, _ => new ChunkSection({ noSizePrefix, hasFluidCount, maxBitsPerBlock: this.maxBitsPerBlock }) ) this.biomes = options?.biomes ?? Array.from( { length: this.numSections }, _ => new BiomeSection({ noSizePrefix }) @@ -180,7 +193,16 @@ module.exports = (Block, mcData) => { setBlockStateId (pos, stateId) { const section = this.sections[(pos.y - this.minY) >> 4] - if (section) { section.set(toSectionPos(pos, this.minY), stateId) } + if (section) { + const sectionPos = toSectionPos(pos, this.minY) + if (hasFluidCount) { + const oldStateId = section.get(sectionPos) + const oldHasFluid = hasFluidState(oldStateId) + const newHasFluid = hasFluidState(stateId) + if (oldHasFluid !== newHasFluid) section.fluidCount += newHasFluid ? 1 : -1 + } + section.set(sectionPos, stateId) + } } setBlockLight (pos, light) { @@ -252,7 +274,7 @@ module.exports = (Block, mcData) => { load (data) { const reader = SmartBuffer.fromBuffer(data) for (let i = 0; i < this.numSections; ++i) { - this.sections[i] = ChunkSection.read(reader, this.maxBitsPerBlock, noSizePrefix) + this.sections[i] = ChunkSection.read(reader, this.maxBitsPerBlock, noSizePrefix, hasFluidCount) this.biomes[i] = BiomeSection.read(reader, this.maxBitsPerBiome, noSizePrefix) } } @@ -317,6 +339,7 @@ module.exports = (Block, mcData) => { const raiseUnknownBiome = biome => { throw new Error(`Failed to map ${JSON.stringify(biome)} to a biome ID`) } this.sections[y + minCY] = ChunkSection.fromLocalPalette({ noSizePrefix, + hasFluidCount, data: BitArray.fromLongArray(blockStates.data || {}, blockStates.bitsPerBlock), palette: blockStates.palette .map(e => Block.fromProperties(e.Name.replace('minecraft:', ''), e.Properties || {}) ?? raiseUnknownBlock(e)) diff --git a/src/pc/common/PaletteChunkSection.js b/src/pc/common/PaletteChunkSection.js index b2b28359..7c01a82f 100644 --- a/src/pc/common/PaletteChunkSection.js +++ b/src/pc/common/PaletteChunkSection.js @@ -12,6 +12,8 @@ function getBlockIndex (pos) { class ChunkSection { constructor (options) { this.noSizePrefix = options?.noSizePrefix // 1.21.5+ writes no size prefix before chunk containers, it's computed dynamically to save 1 byte + this.hasFluidCount = options?.hasFluidCount ?? false + this.fluidCount = options?.fluidCount ?? 0 this.data = options?.data if (!this.data) { const value = options?.singleValue ?? 0 @@ -26,6 +28,7 @@ class ChunkSection { this.solidBlockCount = value ? constants.BLOCK_SECTION_VOLUME : 0 } else { this.solidBlockCount = options?.solidBlockCount ?? 0 + this.fluidCount = options?.fluidCount ?? 0 if (options?.solidBlockCount == null) { for (let i = 0; i < constants.BLOCK_SECTION_VOLUME; ++i) { if (this.data.get(i)) { this.solidBlockCount++ } @@ -38,7 +41,9 @@ class ChunkSection { toJson () { return JSON.stringify({ data: this.data.toJson(), - solidBlockCount: this.solidBlockCount + solidBlockCount: this.solidBlockCount, + hasFluidCount: this.hasFluidCount, + fluidCount: this.fluidCount }) } @@ -46,7 +51,9 @@ class ChunkSection { const parsed = JSON.parse(j) return new ChunkSection({ data: paletteContainer.fromJson(parsed.data), - solidBlockCount: parsed.solidBlockCount + solidBlockCount: parsed.solidBlockCount, + hasFluidCount: parsed.hasFluidCount, + fluidCount: parsed.fluidCount }) } @@ -74,12 +81,15 @@ class ChunkSection { write (smartBuffer) { smartBuffer.writeInt16BE(this.solidBlockCount) + if (this.hasFluidCount) smartBuffer.writeInt16BE(this.fluidCount ?? 0) this.data.write(smartBuffer) } - static fromLocalPalette ({ data, palette, noSizePrefix }) { + static fromLocalPalette ({ data, palette, noSizePrefix, hasFluidCount, fluidCount }) { return new ChunkSection({ noSizePrefix, + hasFluidCount, + fluidCount, data: palette.length === 1 ? new SingleValueContainer({ noSizePrefix, @@ -96,15 +106,18 @@ class ChunkSection { }) } - static read (smartBuffer, maxBitsPerBlock = constants.GLOBAL_BITS_PER_BLOCK, noSizePrefix) { + static read (smartBuffer, maxBitsPerBlock = constants.GLOBAL_BITS_PER_BLOCK, noSizePrefix, hasFluidCount = false) { const solidBlockCount = smartBuffer.readInt16BE() + const fluidCount = hasFluidCount ? smartBuffer.readInt16BE() : 0 const bitsPerBlock = smartBuffer.readUInt8() if (bitsPerBlock > 16) throw new Error(`Bits per block is too big: ${bitsPerBlock}`) // Case 1: Single Value Container (all blocks in the section are the same) if (bitsPerBlock === 0) { const section = new ChunkSection({ noSizePrefix, + hasFluidCount, solidBlockCount, + fluidCount, singleValue: varInt.read(smartBuffer), maxBitsPerBlock }) @@ -116,7 +129,9 @@ class ChunkSection { if (bitsPerBlock > constants.MAX_BITS_PER_BLOCK) { return new ChunkSection({ noSizePrefix, + hasFluidCount, solidBlockCount, + fluidCount, data: new DirectPaletteContainer({ noSizePrefix, bitsPerValue: maxBitsPerBlock, @@ -134,7 +149,9 @@ class ChunkSection { return new ChunkSection({ noSizePrefix, + hasFluidCount, solidBlockCount, + fluidCount, data: new IndirectPaletteContainer({ noSizePrefix, bitsPerValue: bitsPerBlock, diff --git a/test/ChunkColumn.test.js b/test/ChunkColumn.test.js index c442b543..e504b0c0 100644 --- a/test/ChunkColumn.test.js +++ b/test/ChunkColumn.test.js @@ -205,5 +205,34 @@ for (const version of allVersions) { assert.strictEqual(biomeId, column.getBiomeId(pos)) }) } + + if (version === '26.1.2') { + it('updates section fluid count when block states change', () => { + const column = new ChunkColumn() + const pos = new Vec3(0, 0, 0) + const section = column.sections[(pos.y - column.minY) >> 4] + const airStateId = registry.blocksByName.air.defaultState + const waterStateId = registry.blocksByName.water.defaultState + const oakStairs = registry.blocksByName.oak_stairs + let waterloggedStairsStateId + let dryStairsStateId + + for (let stateId = oakStairs.minStateId; stateId <= oakStairs.maxStateId; stateId++) { + const block = Block.fromStateId(stateId) + if (block.isWaterlogged === true && waterloggedStairsStateId === undefined) waterloggedStairsStateId = stateId + if (block.isWaterlogged === false && dryStairsStateId === undefined) dryStairsStateId = stateId + } + + assert.strictEqual(section.fluidCount, 0) + column.setBlockStateId(pos, waterStateId) + assert.strictEqual(section.fluidCount, 1) + column.setBlockStateId(pos.offset(1, 0, 0), waterloggedStairsStateId) + assert.strictEqual(section.fluidCount, 2) + column.setBlockStateId(pos, airStateId) + assert.strictEqual(section.fluidCount, 1) + column.setBlockStateId(pos.offset(1, 0, 0), dryStairsStateId) + assert.strictEqual(section.fluidCount, 0) + }) + } }) } diff --git a/test/ChunkSection.test.js b/test/ChunkSection.test.js index f4893995..38852e22 100644 --- a/test/ChunkSection.test.js +++ b/test/ChunkSection.test.js @@ -2,7 +2,9 @@ const Vec3 = require('vec3').Vec3 const ChunkSection = require('../src/pc/1.13/ChunkSection') +const PaletteChunkSection = require('../src/pc/common/PaletteChunkSection') const constants = require('../src/pc/common/constants') +const SmartBuffer = require('smart-buffer').SmartBuffer const assert = require('assert') describe('pc 1.13 ChunkSection', () => { @@ -37,3 +39,26 @@ describe('pc 1.13 ChunkSection', () => { } }) }) + +describe('pc palette ChunkSection', () => { + it('preserves fluid count through binary serialization', () => { + const section = new PaletteChunkSection({ hasFluidCount: true, fluidCount: 9 }) + const writer = new SmartBuffer() + + section.write(writer) + + const read = PaletteChunkSection.read(SmartBuffer.fromBuffer(writer.toBuffer()), undefined, undefined, true) + assert.strictEqual(read.hasFluidCount, true) + assert.strictEqual(read.fluidCount, 9) + }) + + it('preserves fluid count through JSON serialization', () => { + const section = new PaletteChunkSection({ hasFluidCount: true, fluidCount: 4 }) + + const read = PaletteChunkSection.fromJson(section.toJson()) + + assert.strictEqual(read.hasFluidCount, true) + assert.strictEqual(read.fluidCount, 4) + assert.strictEqual(read.toJson(), section.toJson()) + }) +}) diff --git a/test/versions.js b/test/versions.js index dbb1ff0c..f0c82886 100644 --- a/test/versions.js +++ b/test/versions.js @@ -1,5 +1,5 @@ const fs = require('fs') -const pcVersions = ['bedrock_0.14', 'bedrock_1.0', '1.8', '1.9', '1.10', '1.11', '1.12', '1.13.2', '1.14.4', '1.15.2', '1.16.1', '1.17', '1.18', '1.19', '1.20'] +const pcVersions = ['bedrock_0.14', 'bedrock_1.0', '1.8', '1.9', '1.10', '1.11', '1.12', '1.13.2', '1.14.4', '1.15.2', '1.16.1', '1.17', '1.18', '1.19', '1.20', '26.1.2'] const bedrockVersions = ['bedrock_1.16.220', 'bedrock_1.17.40', 'bedrock_1.18.0'] const allVersions = [...bedrockVersions, ...pcVersions] From af619d32bfd2478a0ba08650e2966e0c0f6326a8 Mon Sep 17 00:00:00 2001 From: Marc Neuhaus Date: Tue, 12 May 2026 15:00:10 +0200 Subject: [PATCH 2/2] Use 26.1.2 data branch in tests --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index bc285b4f..83aa3bd1 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,7 @@ "typescript": "^5.0.4" }, "dependencies": { + "minecraft-data": "github:mneuhaus/node-minecraft-data#add-26.1.2-data-wrapper", "prismarine-biome": "^1.2.0", "prismarine-block": "^1.14.1", "prismarine-nbt": "^2.2.1",