- Version: ModelEngine
R4.2.0, Leaf1.21.4-525 - Period covered: Oct 18 to Oct 31, 2026
- Occurrences: 6
Checked against
ModelEngine-R4.2.0.jar. The line numbers in the stack traces match the bytecode of this jar.
| Failure | Occurrences | Location |
|---|---|---|
"models" is null |
3 | ModelUpdaters.forRenderers:167 |
"bones" is null |
1 | DisplayRendererImpl.forBehavior:444 |
"source" is null |
1 | ActiveModelImpl$ResolvedBoneOrder.resolve:584 |
ConcurrentModificationException during EntitiesUnloadEvent |
1 | DisplayParser.remove:521 |
Logs
[08:04:41] [ForkJoinPool-7-worker-9/WARN]: java.lang.NullPointerException: Cannot invoke "java.util.Map.isEmpty()" because "models" is null
[08:04:41] [ForkJoinPool-7-worker-9/WARN]: at ModelEngine.jar//com.ticxo.modelengine.api.model.ModelUpdaters.forRenderers(ModelUpdaters.java:167)
[03:22:58] [ForkJoinPool-7-worker-8/WARN]: java.lang.NullPointerException: Cannot invoke "java.util.Map.get(Object)" because "bones" is null
[03:22:58] [ForkJoinPool-7-worker-8/WARN]: at ModelEngine.jar//com.ticxo.modelengine.core.model.render.DisplayRendererImpl.forBehavior(DisplayRendererImpl.java:444)
[08:35:35] [ForkJoinPool-6-worker-6/WARN]: java.lang.NullPointerException: Cannot invoke "java.util.Map.get(Object)" because "source" is null
[08:35:35] [ForkJoinPool-6-worker-6/WARN]: at ModelEngine.jar//com.ticxo.modelengine.core.model.ActiveModelImpl$ResolvedBoneOrder.resolve(ActiveModelImpl.java:584)
[08:35:35] [ForkJoinPool-6-worker-6/WARN]: at ModelEngine.jar//com.ticxo.modelengine.core.model.ActiveModelImpl.tick(ActiveModelImpl.java:235)
[03:19:13] [Server thread/ERROR]: Could not pass event EntitiesUnloadEvent to ModelEngine vR4.2.0
java.util.ConcurrentModificationException: null
at java.base/java.util.HashMap.forEach(HashMap.java:1433) ~[?:?]
at ModelEngine.jar/com.ticxo.modelengine.v1_21_R3.parser.model.DisplayParser.remove(DisplayParser.java:521) ~[ModelEngine.jar:?]
at ModelEngine.jar/com.ticxo.modelengine.v1_21_R3.parser.model.DisplayParser.destroy(DisplayParser.java:204) ~[ModelEngine.jar:?]
Problem
Teardown and ticks can overlap. ModelUpdaters.tick submits work to the pool and returns while workers keep running. forceRemoveModeledEntity destroys the model at once on Paper-based servers, including from EntitiesUnloadEvent on the main thread. Only the Folia path defers the destroy.
getModels() can return null. ModeledEntityImpl.getModels() reads cachedModels for its null check, then reads it again to return it. destroy() sets that field to null, so it can change between the two reads. This matches "models" is null.
getBones() has the same race. CachedImmutableMap.get() also reads its volatile cache twice, and ActiveModelImpl.destroy() calls invalidate(), which sets it to null. Both forBehavior and ActiveModelImpl.tick get their map from getBones(), which matches "bones" is null and "source" is null.
The rendered bones are in a plain HashMap. DisplayParser.remove iterates renderer.getRendered() during teardown, while DisplayRendererImpl writes to that map from the render step on a worker. This matches the ConcurrentModificationException.
Solution
- In
getModels()andCachedImmutableMap.get(), store the field in a local variable, then check and return that same value. - Defer
destroy()until the running tick has finished, on every platform. - Have the tick stop safely when its model is being destroyed.
- Keep
renderedand the other render maps on one owner, or iterate a safe snapshot during teardown.