Skip to content

fix: guard villager bed lookups by region ownership - #526

Open
PluginsKers wants to merge 1 commit into
PaperMC:ver/26.3.xfrom
PluginsKers:fix/villager-jump-bed-region-26.3
Open

PluginsKers wants to merge 1 commit into
PaperMC:ver/26.3.xfrom
PluginsKers:fix/villager-jump-bed-region-26.3

Conversation

@PluginsKers

Copy link
Copy Markdown

JumpOnBed retains targetBed while a villager can move to another region. canStillUse reads that cached position through isJumpable. An unloaded foreign chunk reaches ServerChunkCache.syncLoad and throws an ownership exception. When this runs during passenger ticking, the exception can propagate to the vehicle's guardEntityTick boundary and discard the vehicle.

Check TickThread.isTickThreadFor(level, bodyPos) in the shared isJumpable helper before reading block state. This covers canStillUse, onOrOverBed, and onBedSurface, including start, walk-timeout, and jump-timeout checks. Preserve 26.3's VILLAGER_BABIES_CAN_JUMP_ON_BED tag. A rejected target takes the existing Behavior.tickOrStop -> doStop -> JumpOnBed.stop path, clearing the retained target and jump counters without changing NEAREST_BED or WALK_TARGET.

VillagerGoalPackages registers JumpOnBed in its play and idle groups. The production stack includes tickPassenger and both rideTick frames before the AI failure. Level.guardEntityTick discards the non-player entity passed to its outer boundary; the stack alone does not identify the production vehicle type or establish inventory loss.

Complete captured production call chain (26.2-7-14b7fee, entry to failure)
java.base/java.lang.Thread.run(Thread.java:1474)
 -> ca.spottedleaf.concurrentutil.util.LazyRunnable.run(LazyRunnable.java:25)
 -> ca.spottedleaf.concurrentutil.scheduler.StealingScheduledThreadPool$TickThreadRunner.run(StealingScheduledThreadPool.java:1150)
 -> ca.spottedleaf.concurrentutil.scheduler.StealingScheduledThreadPool$TickThreadRunner.doRun(StealingScheduledThreadPool.java:981)
 -> ca.spottedleaf.concurrentutil.scheduler.StealingScheduledThreadPool$TickThreadRunner.mainLoop(StealingScheduledThreadPool.java:1136)
 -> ca.spottedleaf.concurrentutil.scheduler.StealingScheduledThreadPool$TickThreadRunner.tick(StealingScheduledThreadPool.java:1086)
 -> io.papermc.paper.threadedregions.TickRegionScheduler$RegionScheduleHandle.runTick(TickRegionScheduler.java:481)
 -> io.papermc.paper.threadedregions.TickRegions$ConcreteRegionTickHandle.tickRegion(TickRegions.java:485)
 -> net.minecraft.server.dedicated.DedicatedServer.tickServer(DedicatedServer.java:406)
 -> net.minecraft.server.MinecraftServer.tickServer(MinecraftServer.java:1742)
 -> net.minecraft.server.MinecraftServer.tickChildren(MinecraftServer.java:1942)
 -> net.minecraft.server.level.ServerLevel.tick(ServerLevel.java:918)
 -> io.papermc.paper.threadedregions.RegionizedWorldData.forEachTickingEntity(RegionizedWorldData.java:615)
 -> net.minecraft.server.level.ServerLevel.lambda$tick$0(ServerLevel.java:937)
 -> net.minecraft.world.level.Level.guardEntityTick(Level.java:1573)
 -> net.minecraft.server.level.ServerLevel.tickNonPassenger(ServerLevel.java:1512)
 -> net.minecraft.server.level.ServerLevel.tickPassenger(ServerLevel.java:1539)
 -> net.minecraft.world.entity.LivingEntity.rideTick(LivingEntity.java:4021)
 -> net.minecraft.world.entity.Entity.rideTick(Entity.java:3303)
 -> net.minecraft.world.entity.npc.villager.Villager.tick(Villager.java:307)
 -> net.minecraft.world.entity.Mob.tick(Mob.java:422)
 -> net.minecraft.world.entity.LivingEntity.tick(LivingEntity.java:3460)
 -> net.minecraft.world.entity.AgeableMob.aiStep(AgeableMob.java:217)
 -> net.minecraft.world.entity.Mob.aiStep(Mob.java:544)
 -> net.minecraft.world.entity.LivingEntity.aiStep(LivingEntity.java:3754)
 -> net.minecraft.world.entity.Mob.serverAiStep(Mob.java:863)
 -> net.minecraft.world.entity.npc.villager.Villager.customServerAiStep(Villager.java:260)
 -> net.minecraft.world.entity.npc.villager.Villager.customServerAiStep(Villager.java:266)
 -> net.minecraft.world.entity.ai.Brain.tick(Brain.java:389)
 -> net.minecraft.world.entity.ai.Brain.tickEachRunningBehavior(Brain.java:439)
 -> net.minecraft.world.entity.ai.behavior.GateBehavior.tickOrStop(GateBehavior.java:84)
 -> net.minecraft.world.entity.ai.behavior.Behavior.tickOrStop(Behavior.java:76)
 -> net.minecraft.world.entity.ai.behavior.JumpOnBed.canStillUse(JumpOnBed.java:14)
 -> net.minecraft.world.entity.ai.behavior.JumpOnBed.canStillUse(JumpOnBed.java:60)
 -> net.minecraft.world.entity.ai.behavior.JumpOnBed.isBed(JumpOnBed.java:104)
 -> net.minecraft.world.level.Level.getBlockState(Level.java:1370)
 -> net.minecraft.world.level.Level.getChunk(Level.java:1064)
 -> net.minecraft.server.level.ServerChunkCache.getChunk(ServerChunkCache.java:321)
 -> net.minecraft.server.level.ServerChunkCache.getChunkFallback(ServerChunkCache.java:155)
 -> net.minecraft.server.level.ServerChunkCache.syncLoad(ServerChunkCache.java:102)
 -> ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(TickThread.java:89)

Original exception stack. These method names and line numbers belong to the captured 26.2 build. In 26.3, isBed is named isJumpable.

  1. Follow the standalone build and server setup, using baseline 597e41371802648e0455fc1571192ee1c83fca2b and this PR's commit for the fixed variant. Use the same probe JAR in fresh, loopback-only server directories and run java -Xms1G -Xmx2G -Duser.language=en -Dfolia.probe=bed -jar folia.jar --nogui.
  2. The fixture creates a baby villager in a real oak boat on region (0, 0). Keep chunk (1024, 0) unloaded and confirm it is not owned. Set NEAREST_BED to (16387, 65, 3), leave WALK_TARGET absent, and call new JumpOnBed(0.5f).tryStart(level, villager, 0L) to retain the target.
  3. Call behavior.tickOrStop(level, villager, 1L) inside level.guardEntityTick, passing the boat as the guarded entity. The unchanged server logs Cannot asynchronously load chunks and removes the boat. With the guard, the behavior stops, the boat and passenger remain, the target/counters reset, both memories remain unchanged, and the foreign chunk stays unloaded.
  4. The fixture also repeats the direct-villager boundary and retains a behavior across a real teleportAsync, then confirms stop/restart on a local bed. Other controls exercise real jump velocity, walk timeout, adult/non-bed cases, and same-coordinate beds across dimensions. NEAREST_BED stores a BlockPos, so its existing dimension-local interpretation is preserved.

The mounted fixture reproduces the guard boundary with a real boat and passenger; it does not drive the entire autonomous rideTick journey. That caller path is evidenced by the production stack. The paired baseline stops at the mounted unloaded-bed failure after its earlier controls.

The current 26.3 failure stack, baseline log, and fixed log are pinned alongside the fixture. The recorded fixed runtime contains the three independent guards; this PR contains only the bed-lookup change. The reproductions invoke real behaviors on owner threads with autonomous AI disabled; separate single-fix runtime JARs and complete autonomous gameplay runs were not recorded. Read assertion markers in the logs, since the fixture shuts down normally even after an assertion fails.

AI tools assisted the investigation, implementation, reproduction, and this description.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant