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

chunk .dump() doesn't match section mask for all versions <=1.17 #205

Open
kf106 opened this issue Jan 2, 2023 · 5 comments
Open

chunk .dump() doesn't match section mask for all versions <=1.17 #205

kf106 opened this issue Jan 2, 2023 · 5 comments

Comments

@kf106
Copy link

kf106 commented Jan 2, 2023

In 1.15.2 PC version if you build up a chunk, then delete the blocks (replacing them with air), the chunk.getMask value returned is that for the previous tall chunk, i.e. it doesn't recompute the mask to only count upward to the last non-air block.

This means that if you build a tall tower, delete it with mining, save the chunk using chunk.dump, save the bitmask value using chunk.getMask, and then try to load the saved chunk using chunk.load with the stored getMask value, you get an error that you're reading past the memory bound.

This is using flying-squid and prismarine-web-client

@nickelpro
Copy link
Member

nickelpro commented Jan 2, 2023

So two things:

  • We never re-compress a chunk or chunk column (which is something we can argue about the correctness of)
  • Therefore, once a block has been set inside a chunk, that chunk is always part of the sectionMask. Once a chunk is non-empty, it is forever non-empty from the point of view of pris-chunk. We never zero any bits in the sectionMask

So .getMask() at runtime is correct, no bug there, because internally that's how the chunk is calculating. Ie, a section object with a palette exists for that part of the chunk column.

The problem is how we serialize in .dump(), we ask the wrong question, we ask:

if (section !== null && !section.isEmpty()) {
  section.write(smartBuffer)
}

Ostensibly as a space-saving measure. And this is wrong, or at least a tricky corner. I think we need to rethink this. It might be we need do a naive compression pass (eliminating empty chunks and recalculating the section mask) whenever dump() is called, but we need some way to communicate that getMask() calls from prior to the dump() are invalid after the dump().

@nickelpro nickelpro changed the title getMask for 1.15.2 returns wrong value in some cases chunk .dump() doesn't match section mask for versions all versions <=1.17 Jan 2, 2023
@nickelpro
Copy link
Member

I'm not looking at any docs here, is it invalid to serialize a chunk with zero solid blocks? I don't think so.

The bug fix might be that we remove the !section.isEmpty() check from affected versions. And add a separate compress() method to their chunk column objects that does this removal of empty sections and recalcs the section mask.

Ping @extremeheat

@kf106
Copy link
Author

kf106 commented Jan 2, 2023

I was thinking of adding a return value to .dump() that returns the actual mask for the dumped chunk. But removing isEmpty() and having a compress function which removes empty sections and recalculates the section mask makes more sense to me.

@nickelpro nickelpro changed the title chunk .dump() doesn't match section mask for versions all versions <=1.17 chunk .dump() doesn't match section mask for all versions <=1.17 Jan 2, 2023
@kf106
Copy link
Author

kf106 commented Jan 2, 2023

I tried implementing your suggestion, and indeed there are other areas which subsequently fail if you update the section mask.

So I took the quick and easy route and added a new function to get the actual mask for the dumped buffer:

    dumpMask () {
      let newMask = this.sectionMask
      this.sections.forEach((section, i) => {
        if (section !== null && section.isEmpty()) {
          newMask &= ~(1 << i)
        }
      })
      return newMask
    }

When I'm using dump and load in my fs plugin, instead of calling getMask I call dumpMask and get the correct value, and the rest of the code works as before.

@extremeheat
Copy link
Member

Ostensibly as a space-saving measure. And this is wrong, or at least a tricky corner. I think we need to rethink this. It might be we need do a naive compression pass (eliminating empty chunks and recalculating the section mask) whenever dump() is called, but we need some way to communicate that getMask() calls from prior to the dump() are invalid after the dump().

Yeah, on the bedrock implementation we have some handling for this:

// These compation functions reduces the size of the chunk by removing unused blocks
// and reordering the palette to reduce the number of bits per block
isCompactable (layer) {
let newPaletteLength = 0
for (const block of this.palette[layer]) {
if (block.count > 0) {
newPaletteLength++
}
}
return newPaletteLength < this.palette[layer].length
}
compact (layer) {
const newPalette = []
const map = []
for (const block of this.palette[layer]) {

And compact() on serializatized export:

if (compact) this.compact(l) // Compact before encoding

Works by tracking the count of the items in the palette and rebuilding the storage at the end (if needed) when something like dump() would be called.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants