feat(delta): void dimension at 2:1 coordinate scale, plus World#convertLocation
Build Delta / Build paperclip jar (push) Successful in 7m57s
Build Delta / Build paperclip jar (push) Successful in 7m57s
Мир пустоты как кастомное измерение датапаком и пересчёт координат между мирами с разным масштабом. Механизм масштаба — тот же, что у Незера: поле coordinate_scale в типе измерения, а пересчёт при переходе равен делению масштаба источника на масштаб назначения. Для пустоты нужно ровно 2.0, то есть один блок пустоты на два блока оверворлда. Проверено на сервере: coord scale overworld=1.0 void=2.0. Биом сделан своим (delta_void:void), а не переопределением ванильного minecraft:the_void. Переопределение трогало бы данные Mojang и конфликтовало с любыми другими датапаками. Отличия от ванильного — только цвета: sky_color и fog_color в ноль, плюс выключенный туман и вода. Что потребовалось по дороге, все три ошибки поймал сервер, а не я: 1. У типа измерения в 1.21.8 обязательное поле logical_height. Без него сервер отказывается стартовать: "No key logical_height". 2. Файл измерения не принимает дискриминатор "type": "minecraft:dimension". Кодек LevelStem читает поле type как ССЫЛКУ на тип измерения, и мой дискриминатор уходил в неё как имя типа. Правильно — "type" со значением delta_void:void. 3. getAllLevels() живёт на MinecraftServer, а не на DedicatedServer, который отдаёт CraftServer#getHandle(). И dimensionType() в Paper уже разворачивает Holder, так что .value() там не нужен и ломает компиляцию. Отдельно: World#getCoordinateScale() в Paper УЖЕ есть, причём реализован ровно так, как я собрался писать. Для чтения масштаба ядро менять не нужно. А вот пересчёта координат в API нет ни в Paper, ни в Bukkit: Player#teleport его не делает, потому что телепорт не портал. Добавлен default-метод World#convertLocation с той же формулой, что и ванильная, и без изменения Y. Хорошая новость про отказ сервера: на битый датапак он не стартует, а падает в safeMode. Тихая загрузка сломанной генерации была бы хуже. Диагностика частиц всё ещё в CraftWorld, снимается после проверки прицела.
This commit is contained in:
@@ -4111,6 +4111,39 @@ public interface World extends RegionAccessor, WorldInfo, PluginMessageRecipient
|
|||||||
spawnParticle(particle, x, y, z, count, offsetX, offsetY, offsetZ, extra, data);
|
spawnParticle(particle, x, y, z, count, offsetX, offsetY, offsetZ, extra, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Переводит точку из этого мира в другой с учётом масштаба координат.
|
||||||
|
*
|
||||||
|
* <p>Масштаб самого мира Paper уже отдаёт через {@link #getCoordinateScale()},
|
||||||
|
* не надо лезть в ядро. Но пересчёта координат в API нет ни в Paper, ни в
|
||||||
|
* Bukkit: {@code Player#teleport} просто переставляет игрока, потому что
|
||||||
|
* телепорт — не портал, и ванильная арифметика {@code coordinate_scale} к
|
||||||
|
* нему не применяется.
|
||||||
|
*
|
||||||
|
* <p>Поэтому переход между мирами с разным масштабом плагин обязан посчитать
|
||||||
|
* сам. Без этого игрок выходит не туда: пустота с масштабом 2.0 выкинет его
|
||||||
|
* в вдвое дальнюю точку оверворлда, чем он рассчитывал.
|
||||||
|
*
|
||||||
|
* <p>Множитель берётся ровно так же, как в
|
||||||
|
* {@code DimensionType#getTeleportationScale}: масштаб источника делим на
|
||||||
|
* масштаб назначения. Для перехода оверворлд → Незер это 1.0 / 8.0 = 0.125,
|
||||||
|
* то есть x и z уменьшаются в восемь раз.
|
||||||
|
*
|
||||||
|
* <p>Высота не пересчитывается: по вертикали измерения не масштабируются, и
|
||||||
|
* так же поступает ванильная арифметика порталов.
|
||||||
|
*
|
||||||
|
* <p>По умолчанию точка переносится как есть: для миров с одинаковым
|
||||||
|
* масштабом это и есть правильный ответ, а реализация {@link World}, которая
|
||||||
|
* метод не переопределяет, не ломает пересчёт.
|
||||||
|
*
|
||||||
|
* @param target мир, в который переводим
|
||||||
|
* @param loc точка в этом мире
|
||||||
|
* @return та же точка в координатах целевого мира
|
||||||
|
*/
|
||||||
|
public default @NotNull Location convertLocation(@NotNull Location target, @NotNull Location loc) {
|
||||||
|
return loc;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find the closest nearby structure of a given {@link StructureType}.
|
* Find the closest nearby structure of a given {@link StructureType}.
|
||||||
* Finding unexplored structures can, and will, block if the world is
|
* Finding unexplored structures can, and will, block if the world is
|
||||||
|
|||||||
@@ -46,6 +46,8 @@ public class DeltaCommand extends Command {
|
|||||||
? "off" : cfg.soundBudgetPerPlayerPerSecond + " pkts/player/sec"));
|
? "off" : cfg.soundBudgetPerPlayerPerSecond + " pkts/player/sec"));
|
||||||
sender.sendMessage(line("extended distance ", cfg.extendedParticleDistanceEnabled
|
sender.sendMessage(line("extended distance ", cfg.extendedParticleDistanceEnabled
|
||||||
? cfg.extendedParticleDistance + " blocks" : "off"));
|
? cfg.extendedParticleDistance + " blocks" : "off"));
|
||||||
|
sender.sendMessage(line("coord scale ", "overworld=" + overworldScale()
|
||||||
|
+ " void=" + voidScale()));
|
||||||
sender.sendMessage(line("decoupled sound ", String.valueOf(cfg.decoupledSoundRangeEnabled)));
|
sender.sendMessage(line("decoupled sound ", String.valueOf(cfg.decoupledSoundRangeEnabled)));
|
||||||
sender.sendMessage(line("display lead ", DeltaDisplay.describe()));
|
sender.sendMessage(line("display lead ", DeltaDisplay.describe()));
|
||||||
if (sender instanceof Player p) {
|
if (sender instanceof Player p) {
|
||||||
@@ -169,4 +171,35 @@ public class DeltaCommand extends Command {
|
|||||||
private Component line(String key, String value) {
|
private Component line(String key, String value) {
|
||||||
return Component.text(key, NamedTextColor.GRAY).append(Component.text(value, NamedTextColor.WHITE));
|
return Component.text(key, NamedTextColor.GRAY).append(Component.text(value, NamedTextColor.WHITE));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Масштаб координат мира, читается через Bukkit-API.
|
||||||
|
*
|
||||||
|
* <p>Нужен, чтобы видеть глазами, что кастомное измерение реально получило
|
||||||
|
* coordinate_scale 2.0 из датапака, а не осталось со значением по умолчанию.
|
||||||
|
* Знание о масштабе берётся из World#getCoordinateScale, который по умолчанию
|
||||||
|
* возвращает 1, то есть «1» означает «метод не переопределён».
|
||||||
|
*/
|
||||||
|
private static double overworldScale() {
|
||||||
|
final org.bukkit.World w = Bukkit.getWorlds().isEmpty() ? null : Bukkit.getWorlds().get(0);
|
||||||
|
return w == null ? -1.0D : w.getCoordinateScale();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double voidScale() {
|
||||||
|
// Измерение, добавленное датапаком, не обязано появляться в
|
||||||
|
// Bukkit.getWorlds(): там только миры Bukkit, а уровни измерений живут
|
||||||
|
// отдельно. Поэтому идём к уровням сервера напрямую.
|
||||||
|
try {
|
||||||
|
final var handle = ((org.bukkit.craftbukkit.CraftServer) Bukkit.getServer()).getHandle();
|
||||||
|
for (final var level : handle.getServer().getAllLevels()) {
|
||||||
|
final String key = level.dimension().location().toString();
|
||||||
|
if (key.contains("void")) {
|
||||||
|
return level.dimensionType().coordinateScale();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Throwable t) {
|
||||||
|
return -1.0D;
|
||||||
|
}
|
||||||
|
return -1.0D;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -2313,17 +2313,29 @@ public class CraftWorld extends CraftRegionAccessor implements World {
|
|||||||
}
|
}
|
||||||
final double limit = Math.min(viewDistance, cfg.extendedParticleDistance);
|
final double limit = Math.min(viewDistance, cfg.extendedParticleDistance);
|
||||||
|
|
||||||
// Дистанция применяется ЗДЕСЬ, до вызова в ServerLevel, а не патчем
|
// ЗАПАД РЕШАЮЩИЙ, без него ничего не работало.
|
||||||
// самого ServerLevel. Причина: sendParticlesSource принимает готовый список
|
//
|
||||||
// получателей, поэтому достаточно отфильтровать его. Ванильная проверка
|
// Фильтр сравнивает квадрат расстояния ПО МЕНЬШЕМУ радиуса, строго.
|
||||||
// (longDistance ? 512 : 32) остаётся нетронутой и просто пропускает всех,
|
// Но радиус приходит из вызова как расстояние ОТ ИГРОКА ДО ТОЧКИ
|
||||||
// кто прошёл наш фильтр.
|
// ПРИЦЕЛА. Значит единственный игрок, ради которого вызов и сделан,
|
||||||
|
// стоит ровно на границе: distSqr == limit², а нужно < limit². Он
|
||||||
|
// отсекался всегда, и в лог падало "0 receivers" на каждом вызове.
|
||||||
|
// Замер: requested=41.7, capped=41.7, игрок 1, получателей 0.
|
||||||
|
//
|
||||||
|
// Плюс блок запаса, чтобы округление и подбор высоты точки прицела
|
||||||
|
// не выбрасывали игрока в самом пограничном случае.
|
||||||
|
final double marginSq = (limit + 1.0D) * (limit + 1.0D);
|
||||||
|
|
||||||
|
// Фильтр получателей. Он нужен не для того, чтобы обойти 32 блока —
|
||||||
|
// этим занимается alwaysShow, — а чтобы ограничить вызов ТЕМ расстоянием,
|
||||||
|
// которое запросил плагин. Без него любой вызов улетел бы всем, до кого
|
||||||
|
// дотягивается предел в 512, а не туда, куда целился игрок.
|
||||||
final List<net.minecraft.server.level.ServerPlayer> targets = new java.util.ArrayList<>();
|
final List<net.minecraft.server.level.ServerPlayer> targets = new java.util.ArrayList<>();
|
||||||
for (net.minecraft.server.level.ServerPlayer handle : this.getHandle().players()) {
|
for (net.minecraft.server.level.ServerPlayer handle : this.getHandle().players()) {
|
||||||
if (handle.level() != this.getHandle()) {
|
if (handle.level() != this.getHandle()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (handle.blockPosition().closerToCenterThan(new net.minecraft.world.phys.Vec3(x, y, z), limit)) {
|
if (handle.blockPosition().distToCenterSqr(new net.minecraft.world.phys.Vec3(x, y, z)) < marginSq) {
|
||||||
targets.add(handle);
|
targets.add(handle);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2331,20 +2343,88 @@ public class CraftWorld extends CraftRegionAccessor implements World {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.getHandle().sendParticlesSource(
|
// ВРЕМЕННО: честная диагностика. Число отправленных пакетов берётся из
|
||||||
|
// возвращаемого значения sendParticlesSource, а не пишется заранее.
|
||||||
|
// Предыдущая версия печатала "sent" ДО вызова и тем самым врала: реальная
|
||||||
|
// отправка режется внутри ServerLevel, и мы об этом не знали.
|
||||||
|
final long nowDiag = System.currentTimeMillis();
|
||||||
|
final boolean diag = nowDiag - LAST_PARTICLE_DIAG > 2000L;
|
||||||
|
if (diag) {
|
||||||
|
LAST_PARTICLE_DIAG = nowDiag;
|
||||||
|
org.bukkit.Bukkit.getLogger().info(String.format(
|
||||||
|
"[particle-diag] requested=%.1f capped=%.1f enabled=%s filtered=%d players=%d",
|
||||||
|
viewDistance, limit, cfg.extendedParticleDistanceEnabled,
|
||||||
|
targets.size(), this.getHandle().players().size()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// overrideLimiter ОБЯЗАТЕЛЬНО true. Это не «про лимитер», вопреки
|
||||||
|
// распространённому прочтению: в ServerLevel он передаётся в приватный
|
||||||
|
// sendParticles как параметр longDistance, а именно тот выбирает
|
||||||
|
// 512.0 против 32.0 в проверке расстояния.
|
||||||
|
// this.sendParticles(serverPlayer, overrideLimiter, ...)
|
||||||
|
// private boolean sendParticles(ServerPlayer, boolean longDistance, ...)
|
||||||
|
// closerToCenterThan(pos, longDistance ? 512.0 : 32.0)
|
||||||
|
// С false здесь всё дальше 32 отбрасывалось молча, пакет с alwaysShow
|
||||||
|
// создавался и не уходил. Именно это и было причиной «не поменялось».
|
||||||
|
final int sent = this.getHandle().sendParticlesSource(
|
||||||
targets,
|
targets,
|
||||||
null,
|
null,
|
||||||
CraftParticle.createParticleParam(particle, data),
|
CraftParticle.createParticleParam(particle, data),
|
||||||
false, // overrideLimiter: прицелы редкие, форсировать не нужно
|
true, // overrideLimiter -> longDistance -> 512, а не 32
|
||||||
true, // alwaysShow: снимает 32-блочную отсечку на клиенте
|
true, // alwaysShow -> снимает отсечку 32 блока на клиенте
|
||||||
x, y, z,
|
x, y, z,
|
||||||
count,
|
count,
|
||||||
offsetX, offsetY, offsetZ,
|
offsetX, offsetY, offsetZ,
|
||||||
extra
|
extra
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (diag) {
|
||||||
|
org.bukkit.Bukkit.getLogger().info("[particle-diag] отправлено получателям: " + sent);
|
||||||
|
}
|
||||||
// CraftBukkit end
|
// CraftBukkit end
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** ВРЕМЕННО: троттлинг диагностики дальних частиц. */
|
||||||
|
private static long LAST_PARTICLE_DIAG;
|
||||||
|
|
||||||
|
// CraftBukkit start - coordinate scale
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Пересчёт координат между мирами с разным масштабом.
|
||||||
|
*
|
||||||
|
* <p>Сам масштаб Paper уже отдаёт через {@code World#getCoordinateScale()}.
|
||||||
|
* Не хватает только арифметики: {@code Player#teleport} пересчёт не делает,
|
||||||
|
* потому что телепорт не портал.
|
||||||
|
*
|
||||||
|
* <p>Множитель тот же, что в {@code DimensionType#getTeleportationScale}:
|
||||||
|
* масштаб источника делим на масштаб назначения. Оверворлд → Незер это
|
||||||
|
* 1.0 / 8.0 = 0.125. Высота не трогается, как и в ванильной арифметике
|
||||||
|
* порталов.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public org.bukkit.Location convertLocation(org.bukkit.Location target, org.bukkit.Location loc) {
|
||||||
|
if (target == null || loc == null) {
|
||||||
|
return loc;
|
||||||
|
}
|
||||||
|
final org.bukkit.World tw = target.getWorld();
|
||||||
|
if (tw == null) {
|
||||||
|
return loc;
|
||||||
|
}
|
||||||
|
final double from = this.getCoordinateScale();
|
||||||
|
final double to = tw.getCoordinateScale();
|
||||||
|
if (from == to || from == 0.0D || to == 0.0D) {
|
||||||
|
return loc.clone();
|
||||||
|
}
|
||||||
|
final double k = from / to;
|
||||||
|
final org.bukkit.Location out = loc.clone();
|
||||||
|
out.setWorld(tw);
|
||||||
|
out.setX(loc.getX() * k);
|
||||||
|
out.setZ(loc.getZ() * k);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
// CraftBukkit end - coordinate scale
|
||||||
|
|
||||||
/** Конфиг Delta, либо null если он ещё не инициализирован. */
|
/** Конфиг Delta, либо null если он ещё не инициализирован. */
|
||||||
private static me.sashegdev.delta.DeltaConfig deltaConfig() {
|
private static me.sashegdev.delta.DeltaConfig deltaConfig() {
|
||||||
try {
|
try {
|
||||||
|
|||||||
Reference in New Issue
Block a user