Skip to content

fix: defer profession assignment outside the owning region - #525

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

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

Conversation

@PluginsKers

Copy link
Copy Markdown

A villager can retain a POTENTIAL_JOB_SITE in another dimension. AssignProfessionFromJobSite checks numeric distance, erases that memory, writes JOB_SITE, and then reads the destination world's POI manager. If the coordinates are nearby but the dimension differs, the POI ownership check throws and Level.guardEntityTick discards the villager.

Reject a potential site outside the current dimension or owning region before either memory changes. Local assignment and VillagerCareerChangeEvent handling remain on the existing path. Deferral preserves the potential site for retry; the existing GoToPotentialJobSite.stop erases it and queues POI release on its owner region when abandoned.

VillagerGoalPackages.getCorePackage registers this behavior. The captured production stack reaches PoiManager.getType -> getOrLoad -> TickThread.ensureTickThread through the villager's Brain. The exception handler in Level.guardEntityTick discards non-player entities. The stack establishes this failure path, but does not establish how the potential-site memory first became stale.

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:1496)
 -> 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:388)
 -> net.minecraft.world.entity.ai.Brain.startEachNonRunningBehavior(Brain.java:427)
 -> net.minecraft.world.entity.ai.behavior.OneShot.tryStart(OneShot.java:17)
 -> net.minecraft.world.entity.ai.behavior.declarative.BehaviorBuilder$1.trigger(BehaviorBuilder.java:46)
 -> net.minecraft.world.entity.ai.behavior.AssignProfessionFromJobSite.lambda$create$2(AssignProfessionFromJobSite.java:36)
 -> java.base/java.util.Optional.flatMap(Optional.java:289)
 -> net.minecraft.world.entity.ai.behavior.AssignProfessionFromJobSite.lambda$create$3(AssignProfessionFromJobSite.java:36)
 -> net.minecraft.world.entity.ai.village.poi.PoiManager.getType(PoiManager.java:416)
 -> net.minecraft.world.entity.ai.village.poi.PoiManager.getOrLoad(PoiManager.java:91)
 -> 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.

  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=profession -jar folia.jar --nogui.
  2. The fixture creates an unemployed villager at overworld (2.5, 65, 2.5), disables autonomous AI, erases JOB_SITE, and sets POTENTIAL_JOB_SITE to a Nether GlobalPos at (3, 65, 3). Objects and POIs are prepared on their owning regions.
  3. On the villager's region, call AssignProfessionFromJobSite.create().tryStart(level, villager, 0L) inside level.guardEntityTick, with the villager as the guarded entity. The unchanged server logs Accessing poi chunk off-main and removes the villager. With the guard, it returns false, retains the villager and potential-site memory, and leaves JOB_SITE absent.
  4. The fixture also moves a deferred villager to its site using teleportAsync and retries on the new owner region, then exercises local assignment, a cancelled career-change event, and the existing 1,200-tick timeout with POI release observed on the POI region.

In 26.3, assignProfessionWhenSpawned no longer bypasses the distance gate. The fixture's far same-world site is a control, not the failing current-version case. The paired baseline stops at the foreign-dimension failure, so subsequent controls in that recording are fixed-only.

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 profession-assignment 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