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

@xmcl/world - Chunk corruption fix for palette sizes >=16 #250

Merged
merged 5 commits into from
Jan 14, 2023
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
Binary file added mock/saves/1.16.5/region/r.0.0.mca
Binary file not shown.
Binary file added mock/saves/1.19.3/region/r.0.0.mca
Binary file not shown.
129 changes: 73 additions & 56 deletions packages/world/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,47 +5,34 @@ import { deserialize } from "@xmcl/nbt";
import { FileSystem, openFileSystem } from "@xmcl/system";
import Long from "long";

/**
* Compute the bit length from new region section
*/
function computeBitLen(palette: NewRegionSectionDataFrame["Palette"], blockStates: Long[]) {
let computedBitLen = log2DeBruijn(palette.length);
let avgBitLen = blockStates.length * 64 / 4096;
return computedBitLen >= 9 ? computedBitLen : avgBitLen;
}

/**
* Create bit vector from a long array
*/
function createBitVector(arr: Long[], bitLen: number): number[] {
let maxEntryValue = Long.fromNumber(1).shiftLeft(bitLen).sub(1);
let result = new Array<number>(4096);
for (let i = 0; i < 4096; ++i) {
result[i] = seek(arr, bitLen, i, maxEntryValue);
result[i] = Number(seek(arr, bitLen, i, maxEntryValue));
}
return result;
}

/**
* Seek block id from a long array (new chunk format)
* @param data The block state id long array
* @param bitLen The bit length
* @param blockstates The block state id long array
* @param indexLength The bit length
* @param index The index (composition of xyz) in chunk
* @param maxEntryValue The max entry value
*
* @author Adapted from: Jean-Baptiste Skutnik's <https://github.com/spoutn1k> {@link https://github.com/spoutn1k/mcmap/blob/fec14647c600244bc7808b242b99331e7ee0ec38/src/chunk_format_versions/section_format.cpp| Reference C++ code}
*/
function seek(data: Long[], bitLen: number, index: number, maxEntryValue = Long.fromNumber(1).shiftLeft(bitLen).sub(1)) {
let offset = index * bitLen;
let j = offset >> 6;
let k = ((index + 1) * bitLen - 1) >>> 6;
let l = offset ^ j << 6;

if (j == k) {
return data[j].shiftRightUnsigned(l).and(maxEntryValue).toInt();
} else {
let shiftLeft = 64 - l;
const v = data[j].shiftRightUnsigned(l).or(data[k].shiftLeft(shiftLeft));
return v.and(maxEntryValue).toInt();
}
function seek(blockstates: Long[], indexLength: number, index: number, maxEntryValue = Long.fromNumber(1).shiftLeft(indexLength).sub(1)) {
const blocksPerLong = Math.floor(64/indexLength);
const longIndex = Math.floor(index / blocksPerLong);
const padding = Math.floor((index - longIndex * blocksPerLong) * indexLength);
const long = blockstates[longIndex];
const blockIndex = (long.shiftRightUnsigned(padding)).and(maxEntryValue)
return blockIndex;
}

/**
Expand All @@ -64,25 +51,6 @@ function seekLegacy(blocks: number[], data: number[], add: number[] | null, i: n
return (additional << 12) | ((blocks[i] & 255) << 4) | getFromNibbleArray(data, i);
}

const MULTIPLY_DE_BRUIJN_BIT_POSITION = [0, 1, 28, 2, 29, 14, 24, 3, 30, 22, 20, 15, 25, 17, 4, 8, 31, 27, 13, 23, 21, 19, 16, 7, 26, 12, 18, 6, 11, 5, 10, 9];

function log2DeBruijn(value: number) {
function isPowerOfTwo(v: number) {
return v !== 0 && (v & v - 1) === 0;
}
function smallestEncompassingPowerOfTwo(value: number) {
let i = value - 1;
i = i | i >> 1;
i = i | i >> 2;
i = i | i >> 4;
i = i | i >> 8;
i = i | i >> 16;
return i + 1;
}
value = isPowerOfTwo(value) ? value : smallestEncompassingPowerOfTwo(value);
return MULTIPLY_DE_BRUIJN_BIT_POSITION[Long.fromInt(value).multiply(125613361).shiftRight(27).and(31).low];
}

function getChunkOffset(buffer: Uint8Array, x: number, z: number) {
// get internal chunk offset should be in the rest of 5 bits (from >> 5)
x &= 31;
Expand Down Expand Up @@ -216,6 +184,51 @@ export namespace RegionReader {
return region.Level.Sections[0].Y === 0 ? region.Level.Sections[chunkY] : region.Level.Sections[chunkY + 1];
}

/**
* Returns the palette, blockStates and bitLength for a section
* @param section The chunk section
*/
export function getSectionInformation(section: NewRegionSectionDataFrame) {
let blockStates = section.BlockStates;
let palette = section.Palette;

if(blockStates == undefined) {
blockStates = (section.block_states || {}).data;
}
if(palette == undefined) {
palette = (section.block_states || {}).palette;
}

if(palette == undefined || blockStates == undefined) {
palette = [];
blockStates = [];
}

let bitLength = Math.ceil(Math.log2(palette.length))
if(bitLength < 4) {
bitLength = 4;
}

return {
palette: palette,
blockStates: blockStates,
bitLength: bitLength
}
}


/**
* Create an array of block ids from the chunk section given
* @param section The chunk section
*/
export function getSectionBlockIdArray(section: NewRegionSectionDataFrame) {
const sectionInformation = getSectionInformation(section);

const vector = createBitVector(sectionInformation.blockStates, sectionInformation.bitLength);

return vector;
}

/**
* Walk through all the position in this chunk and emit all the id in every position.
* @param section The chunk section
Expand All @@ -229,8 +242,7 @@ export namespace RegionReader {
let blocks = section.Blocks;
seekFunc = (i) => seekLegacy(blocks, data, add, i);
} else {
let blockStates = section.BlockStates;
let vector = createBitVector(blockStates, computeBitLen(section.Palette, blockStates));
const vector = getSectionBlockIdArray(section);
seekFunc = (i) => vector[i];
}
for (let i = 0; i < 4096; ++i) {
Expand All @@ -248,12 +260,12 @@ export namespace RegionReader {
* @param index The chunk index
*/
export function seekBlockStateId(section: NewRegionSectionDataFrame | LegacyRegionSectionDataFrame, index: ChunkIndex) {
if ("BlockStates" in section) {
const blockStates = section.BlockStates;
const bitLen = computeBitLen(section.Palette, blockStates);
return seek(blockStates, bitLen, index);
if ("Blocks" in section) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry maybe I missed something,

Added a new exported function to directly export the section's block array directly for non-legacy section formats, otherwise returns a blank array.

Which function do you mean here?

I just want to make sure we still have legacy support.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No worries I should have been more clear.

When it detects a new chunk format (not the legacy one) it uses getSectionBlockIdArray to get the block id array (variable was previously called vector).

I have also marked this function as exported so you can choose to use this directly.

This should be a non-breaking change that both maintains backwards support with older versions, fixes the chunk corruption and adds the new exported function getSectionBlockIdArray useable on sections with a non-legacy format.

Please let me know if this doesn't make sense.

Copy link
Collaborator

@ci010 ci010 Jan 12, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No worries I should have been more clear.

When it detects a new chunk format (not the legacy one) it uses getSectionBlockIdArray to get the block id array (variable was previously called vector).

I have also marked this function as exported so you can choose to use this directly.

This should be a non-breaking change that both maintains backwards support with older versions, fixes the chunk corruption and adds the new exported function getSectionBlockIdArray useable on sections with a non-legacy format.

Please let me know if this doesn't make sense.

Sure, let's update the branch with master and run tests for it. As long as the old tests for 1.12.2 and 1.14.4 don't fail, it will be totally ok for me.

return seekLegacy(section.Blocks, section.Data, section.Add, index);
}
return seekLegacy(section.Blocks, section.Data, section.Add, index);

const sectionInformation = getSectionInformation(section);
return Number(seek(sectionInformation.blockStates, sectionInformation.bitLength, index));
}

/**
Expand All @@ -262,9 +274,10 @@ export namespace RegionReader {
* @param index The chunk index, which is a number in range [0, 4096)
*/
export function seekBlockState(section: NewRegionSectionDataFrame, index: ChunkIndex): BlockStateData {
const blockStates = section.BlockStates;
const bitLen = computeBitLen(section.Palette, blockStates);
return section.Palette[seek(section.BlockStates, bitLen, index)];
const sectionInformation = getSectionInformation(section);
const blockStateId = seekBlockStateId(section, index);

return sectionInformation.palette[blockStateId];
}
}

Expand Down Expand Up @@ -517,8 +530,12 @@ export type LegacyRegionSectionDataFrame = {
Y: number;
}
export type NewRegionSectionDataFrame = {
BlockStates: Long[],
Palette: Array<BlockStateData>;
BlockStates?: Long[],
Palette?: Array<BlockStateData>;
block_states?: {
data:Long[],
palette:Array<BlockStateData>
}
Data: number[];
BlockLight: number[];
SkyLight: number[];
Expand Down