Stop reporting tile entities the world already dropped - #201
Merged
Conversation
EverNife
force-pushed
the
fix/tile-entities-in-range
branch
from
August 28, 2026 23:16
0c47bed to
1f2e244
Compare
Vanilla read the world's own tile entity list, which its tick sweeps clean, so neither of these readers could return a dead entry or miss a live one. Forge changed the source to Chunk.chunkTileEntityMap for the chunk-scoped lookup and compensated on both counts: an inclusive maximum corner, and a !isInvalid() guard backed by a Chunk.removeInvalidTileEntity it had to add, called only from World.updateEntities. Cauldron re-ported CraftBukkit's version over Forge's and kept the new data source without either compensation; that is what this tree inherited. Both losses are real here. The maximum corner is exclusive while the callers compiled against Forge pass an inclusive one, so a caller asking for a whole chunk silently misses the last block row and column - 31 of 256 columns. And the sweep that reconciles the chunk map is the Spigot time boxed loop, with a rolling cursor and a max-tick-time.tile budget: an entry invalidated after the cursor has already passed it stays visible for the rest of the tick whatever the load, and for an unbounded number of ticks once the budget runs out. WorldServer.func_147486_a takes the maximum corner as inclusive again and skips invalidated entries. EntityPlayerMP does the same filtering where it collects a chunk's tile entities to send: it feeds them straight to getDescriptionPacket, which is mod code running on an object the world considers dead. Measured on a modded server with a headless Forge 1.7.10 client, 60 CustomNPCs blocks placed and then invalidated the way a mod does on a state change, the filter toggled at runtime between the two sends: with the guard, tiles=0 and 62 dead entries skipped; without it, 46 of them had getDescriptionPacket called and went to the client.
EverNife
force-pushed
the
fix/tile-entities-in-range
branch
from
September 1, 2026 17:56
1f2e244 to
61f2df6
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Vanilla read the world's own tile entity list, which its tick sweeps clean, so neither of these
readers could return a dead entry or miss a live one. Forge changed the source to
Chunk.chunkTileEntityMapfor the chunk-scoped lookup and compensated on both counts: an inclusivemaximum corner, and a
!isInvalid()guard backed by aChunk.removeInvalidTileEntityit had toadd, called only from
World.updateEntities. Cauldron re-ported CraftBukkit's version over Forge'sand kept the new data source without either compensation. That is what this tree inherited — traced
to the initial Thermos import, never a deliberate decision here.
Both losses are real:
WorldServer.func_147486_atreats the maximum corner as exclusive while every callercompiled against Forge passes an inclusive one. A caller asking for a whole chunk silently misses
the last block row and column — 31 of 256 columns. The method has no caller left in this tree,
but mods call it.
rolling cursor and a
max-tick-time.tilebudget, so under load it does not finish in a tick andan invalidated entry stays visible for an unbounded number of them.
Change
func_147486_atakes the maximum corner as inclusive again and skips invalidated entries.EntityPlayerMPdoes the same filtering where it collects a chunk's tile entities to send — itfeeds them straight to
getDescriptionPacket, which is mod code running on an object the worldconsiders dead.
Verification
A HeadlessMC Forge 1.7.10 client joined a modded Crucible (15 mods, CustomNPC-Plus among them) and
streamed chunks for real. 60 CustomNPCs blocks that carry a tile entity were placed around the
player —
npcWaypoint,npcBorder,npcLampUnlit,npcCarpentyBench,npcChair,npcTombstone,npcBarrel,npcRedstoneBlock,npcCampfireUnlit,npcCandleUnlit,npcSignand others, drawnat random from the 31 registered types.
Each was then invalidated the way a mod does on a state change:
invalidate()only, block and chunkmap entry left alone. The chunks were re-queued in the same breath, so the send happens while the
dead entries are still in
chunkTileEntityMap— the exact window the guard exists for. The filterwas toggled at runtime between the two runs, so both sides saw the same server, the same world and
the same kind of tile entity:
Without the guard, 46 tile entities the world had already dropped had
getDescriptionPacket()called on them and were shipped to the client. With it, none were:
tiles=0, and the 62 deadentries were skipped. None of the CustomNPCs types threw on the call, so the harm here is stale
state reaching the client rather than a crash — but the packet is built from mod code running on an
invalidated object, and what that returns is the mod's business, not something this path can assume.
The bounds half has no runtime coverage: nothing in this tree calls
func_147486_aany more, sothat change is argued from the Forge sources it realigns with, not demonstrated by a test.