Skip to content

fix: Folia thread-safety for rotation reads and death model cleanup#286

Open
arkarang wants to merge 3 commits intotoxicity188:v2-devfrom
arkarang:fix/folia-thread-safety-and-death-cleanup
Open

fix: Folia thread-safety for rotation reads and death model cleanup#286
arkarang wants to merge 3 commits intotoxicity188:v2-devfrom
arkarang:fix/folia-thread-safety-and-death-cleanup

Conversation

@arkarang
Copy link

@arkarang arkarang commented Feb 23, 2026

Summary

  • EntityTracker: updateBaseEntity()에서 asyncTaskLatertaskLater(entity)로 변경 — entity state 읽기를 owning region thread에서 실행
  • EntityBodyRotator: pitch(), bodyYaw(), headYaw(), yaw(), onWalk() 직접 호출을 throttleTickFloat/throttleTickBoolean으로 래핑 — EXECUTOR 워커 스레드에서 NMS 핸들 직접 읽기 방지 (Folia 스레드 소유권 위반 해결)
  • EntityManager (Bukkit + Fabric): 사망 핸들러에서 forRemoval(true)를 항상 설정 — death 애니메이션 유무와 무관하게 트래커가 사망 상태로 표시됨
  • EntityTrackerRegistry#despawn(): forRemoval 트래커를 스킵하지 않고 close() 호출 — 엔티티가 월드에서 제거될 때 확실히 정리

Context

회전값 throttle (EntityBodyRotator)

bodyRotation0(), stableBodyYaw(), head/body supplier 람다에서 entity.pitch(), entity.bodyYaw() 등이 EXECUTOR 10ms tick에서 직접 호출됨. Folia에서는 이 값들이 Region Thread에서만 원자적으로 업데이트되므로, 다른 스레드에서 읽으면 stale read 또는 스레드 소유권 검증 실패 발생.

기존 FunctionUtil.throttleTickFloat 패턴(AtomicLong.compareAndSet + volatile cache)을 활용하여 50ms 간격 thread-safe 캐싱 적용. EntityTracker에서 damageTick, walkSpeed 등은 이미 이 패턴 사용 중이었으나 EntityBodyRotator는 미적용 상태였음.

사망 모델 잔류 (EntityManager + EntityTrackerRegistry)

death 애니메이션이 없는 몹 사망 시:

  1. animate("death")false 반환
  2. 기존: forRemoval(true) 미호출 → 트래커 활성 상태 유지
  3. EntityRemoveFromWorldEventdespawn()forRemoval 안 되어있으니 despawn() 호출 → entity.dead() 시점에 따라 close() 안 될 수 있음

수정 후:

  1. forRemoval(true) 항상 설정
  2. despawn()에서 forRemoval 트래커를 close()EntityRemoveFromWorldEvent 시점에 확실히 정리

Test plan

  • Folia 서버에서 모델 적용 엔티티 이동/회전 싱크 확인
  • 빠른 방향 전환 시 모델 회전 추종 확인
  • 사망 애니메이션 없는 몹 사망 → 모델 사라지는지 확인
  • 사망 애니메이션 있는 몹 사망 → 애니메이션 재생 후 모델 사라지는지 확인
  • 청크 경계에서 몹 사망 → 모델 잔류 없는지 확인
  • 서버 로그에 Thread failed main thread check 없는지 확인

🤖 Generated with Claude Code

@arkarang arkarang force-pushed the fix/folia-thread-safety-and-death-cleanup branch from 05ae69f to e2d1df2 Compare February 23, 2026 16:12
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이미 최종값인 head랑 body 부터 throttle이 걸려있는데 왜 넣은 거임?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AI 피셜:

headSupplier/bodySupplier는 최종 합성값(Quaternionf/ModelRotation)에 throttle이 걸려있는데, throttle이 만료되서 람다가
실행될 때 entity.pitch(), entity.bodyYaw() 등이 EXECUTOR 스레드에서 NMS 핸들(handle().xRot 등)을 직접 읽어요.
EntityTracker에서 damageTick, walkSpeed에 이미 같은 패턴이 적용되어있는 것과 동일한 이유입니다.

그렇데

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

그거 엔티티 사망 때 애니메이션이 있으면 모델 삭제를 미루기 위한 flag에 가까운데 테스트 해본 거 맞음?
death가 없더라도 엔티티가 remove 되기 전까진 사망했다고 안 지워질 텐데

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

하하 AI의 대침공 너도 당해봐라

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

맞아요 그래서 수정했습니다. 20틱 fallback은 제거하고, forRemoval(true)를 항상 설정 + despawn()에서 forRemoval 트래커를
스킵하지 않고 close() 하도록 변경했어요. death 애니메이션 있으면 콜백에서 close, 없으면 EntityRemoveFromWorldEvent →
despawn() → close() 흐름으로 정리됩니다.

그렇데

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

근데 MobModel 관련해서 MythicMobs 쪽 호환성 이슈도 있어보이긴 해서 저도 테스트 좀 더 해봐야할듯.

주말에 테스트하고 결과알랴줌

@arkarang arkarang force-pushed the fix/folia-thread-safety-and-death-cleanup branch from e2d1df2 to c3c06c6 Compare February 25, 2026 07:45
@arkarang arkarang changed the title fix: Folia thread safety for rotation reads and entity death cleanup fix: use entity region scheduler for updateBaseEntity() Feb 25, 2026
@toxicity188 toxicity188 changed the base branch from master to v2-dev February 25, 2026 07:57
@arkarang arkarang force-pushed the fix/folia-thread-safety-and-death-cleanup branch from fdd3b48 to 3f7cdae Compare February 25, 2026 13:24
Change updateBaseEntity() from asyncTaskLater to taskLater(entity)
so syncEntity/syncPosition reads run on the owning region thread.
@arkarang arkarang force-pushed the fix/folia-thread-safety-and-death-cleanup branch from 37a80a4 to 4374e49 Compare February 25, 2026 13:41
EntityBodyRotator: wrap pitch/bodyYaw/headYaw/yaw/onWalk in
throttleTickFloat/Boolean so EXECUTOR worker threads read cached
values instead of hitting NMS handles off the owning region thread.

EntityManager (Bukkit + Fabric): always set forRemoval(true) on
death, and schedule a 20-tick fallback close() when no death
animation exists — prevents display entities from lingering.

EntityTrackerRegistry#despawn(): close forRemoval trackers on
world removal instead of skipping them.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@arkarang arkarang changed the title fix: use entity region scheduler for updateBaseEntity() fix: Folia thread-safety for rotation reads and death model cleanup Feb 25, 2026
The despawn() safety net in EntityTrackerRegistry already closes
forRemoval trackers when EntityRemoveFromWorldEvent fires,
making the delayed taskLater redundant.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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.

2 participants