Prevent stale tile entities from crashing on invalid block states#557
Prevent stale tile entities from crashing on invalid block states#557frostdev-ops wants to merge 1 commit into
Conversation
0c9d01d to
c47f6f6
Compare
|
Upstream implementation removes the tile entity before removing the block itself, so the crash you've experienced is likely caused by another mod or server patch corrupting the world. Also, there's no reason only WarpDrive would be affected here, a vanilla furnace would have the same issue. |
c47f6f6 to
c2de114
Compare
|
Took your advice and tried to reproduce in a clean environment (Forge 2847 + WarpDrive only, fresh dedicated server world). Learned something in the process. You're right more than I expected. A plain "TE saved but block is now air" mismatch doesn't even crash: But that load guard resolves the block with chunk-local masked coords ( Steps: fresh world, With this PR it's a single warn line ( On the furnace comparison: an idle furnace only touches its block state on burn transitions, so an orphaned one ticks harmlessly. Most of our TEs read block properties every tick (lift reads I'll attach the production crash report once I'm back on that network, but the repro above is mod-clean. And agreed - no claim that WarpDrive causes the mismatch, this only survives the symptom. region corruption script (python, nbtlib)"""Corrupt a saved warpdrive:lift tile entity by shifting its stored position
one chunk over (+16 on x). The chunk-load guard masks x&15, so the entry still
matches the real lift block and registers - but its world position resolves to
air in the neighboring chunk, reproducing the production 'ticking TE without
its block' crash from a corruption baked into the region file.
Usage: python corrupt_lift_te_v2.py <world_dir> <x> <y> <z>
"""
import io
import sys
import zlib
import gzip
import struct
from pathlib import Path
import nbtlib
from nbtlib import Int
world = Path(sys.argv[1])
te_x, te_y, te_z = int(sys.argv[2]), int(sys.argv[3]), int(sys.argv[4])
chunk_x, chunk_z = te_x >> 4, te_z >> 4
region_path = world / "region" / f"r.{chunk_x >> 5}.{chunk_z >> 5}.mca"
raw = bytearray(region_path.read_bytes())
index = ((chunk_x & 31) + (chunk_z & 31) * 32) * 4
entry = struct.unpack(">I", raw[index:index + 4])[0]
offset_sectors, count_sectors = entry >> 8, entry & 0xFF
assert offset_sectors > 0, "chunk not present"
pos = offset_sectors * 4096
length, compression = struct.unpack(">IB", raw[pos:pos + 5])
payload = bytes(raw[pos + 5:pos + 4 + length])
decompressed = zlib.decompress(payload) if compression == 2 else gzip.decompress(payload)
chunk_nbt = nbtlib.File.parse(io.BytesIO(decompressed))
chunk_root = chunk_nbt[""] if "" in chunk_nbt else chunk_nbt
tile_entities = chunk_root["Level"]["TileEntities"]
found = False
for te in tile_entities:
if str(te["id"]) == "warpdrive:lift" and int(te["x"]) == te_x and int(te["y"]) == te_y and int(te["z"]) == te_z:
te["x"] = Int(te_x + 16)
found = True
print(f"lift TE found in chunk ({chunk_x},{chunk_z}); stored position shifted to ({te_x + 16},{te_y},{te_z})")
break
assert found, "lift TE not found in chunk"
buffer = io.BytesIO()
chunk_nbt.write(buffer)
recompressed = zlib.compress(buffer.getvalue())
needed_sectors = (len(recompressed) + 5 + 4095) // 4096
new_block = struct.pack(">IB", len(recompressed) + 1, 2) + recompressed
if needed_sectors <= count_sectors:
raw[pos:pos + len(new_block)] = new_block
else:
new_offset = (len(raw) + 4095) // 4096
raw.extend(b"\0" * (new_offset * 4096 - len(raw)))
raw.extend(new_block)
raw.extend(b"\0" * (needed_sectors * 4096 - len(new_block)))
raw[index:index + 4] = struct.pack(">I", (new_offset << 8) | needed_sectors)
region_path.write_bytes(bytes(raw))
print("region file updated OK") |
Why
A saved Lift tile entity can remain in a chunk after its block state has become air. Its next tick then reads properties that air does not define and crashes the server.
Scope
Chunk.EnumCreateEntityType.CHECK, so a replacement tile entity at the same position is never removed by identity mistake.The Ship Core guard and repository wiki page are intentionally excluded.
Evidence
test NO-SOURCE).CHECKlookup before invalidation/removal.Manual testing
A live Forge regression remains: load a saved Lift tile entity over air, verify cleanup without a crash, then replace it before cleanup and verify the replacement survives.