- Version: ModelEngine
R4.2.0, Leaf1.21.4-525 - Period covered: Oct 13 to Nov 4, 2026
- Occurrences: 374,711, with 355,139 on Oct 13 and 18,110 on Oct 14, then up to about 200 per day
- Location:
BasicCulling.updateCulledPlayer:66, called byDummyEntityData.cullUpdate:92
Checked against
ModelEngine-R4.2.0.jar. The line numbers in the stack traces match the bytecode of this jar.
Logs
[16:33:28] [Craft Scheduler Thread - 935 - ModelEngine/WARN]: java.lang.IllegalArgumentException: World unloaded
[16:33:28] [Craft Scheduler Thread - 935 - ModelEngine/WARN]: at com.google.common.base.Preconditions.checkArgument(Preconditions.java:143)
[16:33:28] [Craft Scheduler Thread - 935 - ModelEngine/WARN]: at org.bukkit.Location.getWorld(Location.java:110)
[16:33:28] [Craft Scheduler Thread - 935 - ModelEngine/WARN]: at ModelEngine.jar//com.ticxo.modelengine.api.entity.cull.BasicCulling.updateCulledPlayer(BasicCulling.java:66)
[16:33:28] [Craft Scheduler Thread - 935 - ModelEngine/WARN]: at ModelEngine.jar//com.ticxo.modelengine.api.entity.data.DummyEntityData.cullUpdate(DummyEntityData.java:92)
[16:33:28] [Craft Scheduler Thread - 935 - ModelEngine/WARN]: at ModelEngine.jar//com.ticxo.modelengine.api.entity.EntityDataTrackers$Tracker.asyncFetchEntityData(EntityDataTrackers.java:132)
[16:33:28] [Craft Scheduler Thread - 935 - ModelEngine/WARN]: at ModelEngine.jar//com.ticxo.modelengine.api.entity.EntityDataTrackers.lambda$setup$1(EntityDataTrackers.java:62)
Problem
A dummy has no link to its world's lifetime. DummyEntityData.isDataValid() only checks whether the dummy is marked as removed. WorldListener handles WorldUnloadEvent through getWorld().getEntities(), which only covers real entities. A dummy can therefore stay tracked after its world is unloaded.
Culling assumes the world is still there. BasicCulling.updateCulledPlayer calls Location.getWorld() for each tracked player. That call throws once the location's world reference is gone.
One bad dummy interrupts the whole culling pass. Tracker.asyncFetchEntityData loops over all its entries with no guard around each one. The exception ends the loop, so the entries after the bad dummy are skipped for that pass.
The error repeats until restart or removal. PseudoThread.tick catches the exception, prints it and keeps the repeating task. The dummy stays tracked, so the next pass fails the same way.
Calling ModeledEntity.destroy() is not enough to release a dummy. This call does not remove the model from uuidLookup or mark the dummy as removed, so the tracker keeps the dummy data. An owner has to mark the dummy as removed as well, which is easy to miss.
Solution
- Treat a dummy whose world is unloaded as invalid, and remove it from the trackers and model registry.
- Check that the world is still available at the start of
updateCulledPlayer. - Guard each entry in
asyncFetchEntityData, so one failure cannot skip culling for other models. - Log this failure once per dummy.
- Document that dummy owners must mark the dummy as removed, or make
ModeledEntity.destroy()release it.