All code references are using official mappings for 20w46a.
The skeleton trap code currently relies on the fact that
the goal used for the skeleton traps is always the last in the
LinkedHashSetused forGoalSelector.availableGoalsandthe (current) iterator implementation for
LinkedHashSetonly checks for concurrent modifications innext, but not inhasNext
GoalSelector.tick calls getRunningGoals().forEach(WrappedGoal::tick);. SkeletonTrapGoal.tick calls SkeletonHorse.setTrap, which directly modifies GoalSelector.availableGoals, the underlying set for the stream returned by getRunningGoals(). This is generally bad style, and in this case relies on undocumented behavior of LinkedHashSet iterators (the second point above): As the last element in the set is the one modifying the set, Iterator.next is not called after the modification, only Iterator.hasNext is. If any goals are added to the set after the trap goal, triggering the trap will crash the server (this happened on a modded server, that's how I found the issue). This never happens in vanilla, but I would still consider it a vanilla bug as it relies on undocumented behavior that could change in any Java update.
The cleanest fix for this would probably be to add a second boolean to SkeletonHorse indicating whether the horse should remain a trap, and to update the goals in SkeletonHorse.tick if this boolean does not match isTrap.
This is still present in 1.21.11,
GoalSelector.a(boolean)(goalTick) iteratesthis.cvia for each and callsWrappedGoal.tick()($$2.a()) on each running goal.SkeletonTrapGoal.tick()callssetTrap(false)(this.a.x(false)) mid iteration.setTrap(false)callsremoveGoal()(this.cs.a(this.cv)), which callsremoveIfdirectly onthis.c, the set currently being iterated:However, based on my analysis, it is worth noting that the structural difference from the original report:
this.cis nowObjectLinkedOpenHashSet(fastutil) rather thanjava.util.LinkedHashSet. The original safe in vanilla assumption relied onLinkedHashSet's iterator only checkingmodCountinnext(), nothasNext(). WithObjectLinkedOpenHashSet, modification during for each iteration produces undefined behavior rather than a guaranteed CME.