Сборка сервера
Sable 2.0.3

Sable 2.0.3 — 3 краша: дедлок Rapier, «Body has been removed», «nonexistent plot holder»

Minecraft 1.21.1 NeoForge 21.1.228+ Сервер (клиент не обязателен) Обновлено 15.07.2026

Если ваш краш-лог содержит одну из этих строк — вы на верной странице:

1. Симптом дедлока — вотчдог (появляется в логе через минуты после реального зависания)
java.lang.Error: ServerHangWatchdog detected that a single server tick took 60000004.00 seconds (should be max 0.05)
	at dev.ryanhcode.sable.physics.impl.rapier.Rapier3D.newVoxelCollider(Native Method)
	at dev.ryanhcode.sable.physics.impl.rapier.Rapier3D.createVoxelColliderEntry(Rapier3D.java:289)
	at dev.ryanhcode.sable.physics.impl.rapier.collider.RapierVoxelColliderBakery.buildPhysicsDataForBlock(RapierVoxelColliderBakery.java:60)
1. Глубже в том же стеке — откуда реально пошёл дедлок
	at dev.ryanhcode.sable.physics.impl.rapier.Rapier3D.step(Native Method)
	at dev.ryanhcode.sable.physics.impl.rapier.RapierPhysicsPipeline.physicsTick(RapierPhysicsPipeline.java:174)
2. Краш автосейва — тело физики уже удалено
java.lang.RuntimeException: Body has been removed
	at dev.ryanhcode.sable.physics.impl.rapier.RapierPhysicsPipeline.assertBodyValid(RapierPhysicsPipeline.java:578)
	at dev.ryanhcode.sable.physics.impl.rapier.RapierPhysicsPipeline.getLinearVelocity(RapierPhysicsPipeline.java:485)
	at dev.ryanhcode.sable.sublevel.storage.serialization.SubLevelSerializer.serialize(SubLevelSerializer.java:62)
	at dev.ryanhcode.sable.sublevel.storage.holding.SubLevelHoldingChunkMap.saveAll(SubLevelHoldingChunkMap.java:240)
3. Краш тикающего блока внутри уже снесённого «плота»
java.lang.UnsupportedOperationException: Cannot change blocks in nonexistent plot holder
	at net.minecraft.server.level.ServerChunkCache.blockChanged(ServerChunkCache.java)
	at net.minecraft.server.level.ServerLevel.sendBlockUpdated(ServerLevel.java:1071)
	at net.minecraft.world.level.block.entity.trialspawner.TrialSpawner.tickServer(TrialSpawner.java:298)
Скачать фикс Готовый .jar из GitHub Releases — положить в mods/
Скачать последний релиз Исходный код

Причина краша

Все три краша — один и тот же паттерн: что-то в Sable уже удалено/снесено, а другая часть мода обращается к этому мгновением позже без проверки, и вместо аккуратного пропуска падает всё исключение целиком, роняя сервер.

1. Дедлок в физдвижке Rapier

Число секунд у вотчдога («60000004.00») ничего не значит буквально — это не то, сколько реально длился тик, а результат того, что серверный поток уже давно (тихо, без единой строки в логе) намертво завис внутри нативного вызова, а поток вотчдога тем временем накопил огромный собственный счётчик. Настоящая проблема происходит незаметно, задолго до появления краш-репорта.

RapierPhysicsPipeline.physicsTick() вызывает нативный Rapier3D.step() — это сам шаг симуляции физдвижка (Rust/C++). Прямо во время этого шага Rapier может дёрнуть Java-коллбэк: например, FragileBlockCallback ломает «хрупкий» блок (листва, бамбук, дыня, тыква, кактус, лёд, кувшинка), если по нему на скорости выше 4.0 врезался физический объект (самолёт/машина на Aeronautics или Offroad).

Разрушение блока проходит по обычной цепочке Minecraft и попадает в собственный хук Sable — handleBlockChange(), который держит кэш коллайдеров физдвижка синхронным с миром. Если данные коллайдера для нового состояния блока (или одного из 6 соседей) ещё не запечены — он снова зовёт нативный Rapier3D.newVoxelCollider(), но уже изнутри того самого step(), который всё это запустил. Повторный вход в нативный движок не поддерживается — второй вызов виснет навсегда, а JVM не может прервать нативный код.

Это состояние гонки: кэш коллайдеров живёт на экземпляр физпайплайна (то есть на измерение) и сбрасывается при его (пере)загрузке. Дедлок возможен только при первой запечке конкретного состояния блока в этой сессии — поэтому баг воспроизводится не каждый раз, а лишь изредка, чаще всего вскоре после рестарта сервера в нетронутой локации.

2. «Body has been removed» при автосейве

getLinearVelocity()/getAngularVelocity() в начале вызывают assertBodyValid(), который бросает обычный RuntimeException, если тело физики уже удалено (корабль/контраптион был разобран или деспавнут). Периодический автосейв мира (SubLevelHoldingChunkMap.saveAll()SubLevelSerializer.serialize()) читает скорость сублевела именно через эти два метода, не проверив заранее RigidBodyHandle.isValid() — хотя такая проверка уже есть в самом Sable. Если тело оказалось удалено прямо перед тиком автосейва — падает не только сохранение этого сублевела, а вообще весь тик сервера.

3. «Cannot change blocks in nonexistent plot holder»

Гигантские координаты в этом краше (вида 20482989, -15, 20558967) — не порча мира, а штатная схема Sable: каждый активный сублевел (корабль/контраптион) представлен «плотом» — областью чанков в том же измерении, но далеко за пределами обычной игровой зоны. Изменения блоков в этой области роутятся через миксин на ванильный ServerChunkCache.blockChanged. Если позиция попадает в границы плот-пространства, но конкретный чанк-холдер для неё уже не зарегистрирован — Sable бросает исключение вместо того, чтобы просто проигнорировать. В нашем случае так падал ещё тикающий trial spawner внутри уже снесённого плота — тот же «удалено, но кто-то ещё держит ссылку» паттерн, что и в первых двух крашах, только в системе чанков, а не физики.

Как работает фикс

1. Дедлок Rapier

Мод-миксин отслеживает (на каждом экземпляре физпайплайна отдельно) момент, когда выполнение находится внутри physicsTick(), и на это время пропускает handleBlockChange(). Блок к этому моменту на стороне Minecraft уже разрушен в любом случае — хук вызывается уже после применения изменения, — так что пропуск лишь откладывает обновление кэша коллайдера физдвижка для этой точки на один тик. Он досчитается на следующем изменении блока рядом — почти незаметный косметический пробел вместо зависания всего сервера.

Rapier3D.step() сам по себе — package-private нативный метод, поэтому обернуть именно его вызов напрямую из чужого пакета миксина нельзя (та же история, что с приватным классом Create). Вместо этого миксин оборачивает флагом весь публичный метод physicsTick() целиком — эта нативная деталь вообще не трогается.

Опциональное логирование (какая именно точка была спасена от дедлока) выключено по умолчанию и включается конфигом — на реальном сервере фикс уже подтверждённо сработал и был пойман в логе.

2. «Body has been removed»

Тот же миксин-класс теперь перехватывает getLinearVelocity() и getAngularVelocity(): если тело уже удалено — возвращается нулевой вектор вместо исключения. У удалённого тела и должна быть нулевая скорость — это не заглушка, а физически верный ответ.

3. «Nonexistent plot holder»

Отдельный миксин на ванильный ServerChunkCache.blockChanged повторяет ту же проверку границ/холдера, что делает сам Sable, но с более низким приоритетом (900 против 1000 по умолчанию) — чтобы отработать раньше и отменить вызов до того, как код Sable успеет бросить исключение. Тихий пропуск уведомления безопасен: если холдера для этого чанка уже нет, значит его никто и не отслеживает.

Совместимость и установка

МодSable 2.0.3 (только эта версия)
Minecraft1.21.1
ЗагрузчикNeoForge 21.1.228+
Куда ставитьСервер (клиент не обязателен)

Требуется Sable 2.0.3 — мод не загрузится с другими версиями (строгий диапазон в neoforge.mods.toml). Как только выйдет версия Sable с официальным фиксом реентерабельности — этот мод можно удалить.

Вся цепочка бага серверная (ServerLevelSubLevelPhysicsSystemRapierPhysicsPipeline) — ставить на клиент не обязательно, но и не вредно.

Runes & Rails открыто делится этим и другими фиксами с сообществом Sable и Minecraft в целом — исходники и релиз доступны бесплатно, под MIT-лицензией. Ставьте на свой сервер, форкайте, дорабатывайте: репозиторий на GitHub. Все фиксы — на странице «Фиксы крашей».

Ко всем фиксам