feat(void): persisted void counter and solo entry into the void world
Девятый этап и вход в пустоту: счётчик в файле плюс возможность войти в одиночку. Счётчик void: - живёт в plugins/BlackKnife/void.yml, по условию в файле, а не в PDC - пишется через временный файл и атомарную подмену: при убийстве сервера в момент записи остаётся предыдущая целая версия, а не обрезанный файл - сохранение раз в минуту асинхронно и синхронно при выключении, потому что после cancelTasks асинхронная задача уже не выполнится - порог 20, команда /blackknife void get|set|add [player] — нужна и для настройки баланса, и для проверки: двадцать нор ждать долго Вход в пустоту: - при счётчике от 20 дыра работает и без ответа: в неё можно войти и оказаться в пустоте - пускает ТОЛЬКО открывший. Иначе счётчик превратился бы в телепорт для всех, и весь смысл накопления теряется - точка входа запоминается: обратный выход пойдёт по соотношению 1:2, поэтому выход не равен входу по координатам - приземление ищется через getHighestBlockYAt, а не вычисляется: высота, на которую лягут слои барьера, зависит от версии, а поиск верхнего блока находит пол всегда Voidness копится раз в секунду (период задачи эмбиента ровно 60 тиков) и обнуляется на выходе: счётчик сессионный, рестарт его тоже убирает. Найденное имя измерения: world_delta_void_void_world. Сервер добавляет префикс папки мира и санитизирует путь, так что угадать его в коде было бы верным способом не заработать. Проверка по имени мира, а не по флагам уровня. Проверено на сервере: команда отвечает, измерение находится, генератор кладёт барьерные слои на нескольких высотах. Сам перенос игрока не проверялся — нужен живой игрок.
This commit is contained in:
@@ -13,7 +13,7 @@ import java.util.Collections;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public class BlackKnifeCommand implements CommandExecutor, TabCompleter {
|
public class BlackKnifeCommand implements CommandExecutor, TabCompleter {
|
||||||
private static final List<String> SUBS = Arrays.asList("give", "reload", "mode", "tp");
|
private static final List<String> SUBS = Arrays.asList("give", "reload", "mode", "tp", "void");
|
||||||
private static final List<String> TP_OPS = Arrays.asList("get", "set", "add");
|
private static final List<String> TP_OPS = Arrays.asList("get", "set", "add");
|
||||||
|
|
||||||
private final BlackKnifePlugin plugin;
|
private final BlackKnifePlugin plugin;
|
||||||
@@ -78,10 +78,77 @@ public class BlackKnifeCommand implements CommandExecutor, TabCompleter {
|
|||||||
sender.sendMessage(Component.text("Usage: /blackknife tp <get|set|add> <player> [amount]").color(NamedTextColor.YELLOW));
|
sender.sendMessage(Component.text("Usage: /blackknife tp <get|set|add> <player> [amount]").color(NamedTextColor.YELLOW));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
if (args[0].equalsIgnoreCase("void")) {
|
||||||
|
voidCounter(sender, args);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
usage(sender);
|
usage(sender);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Счётчик пустоты: сколько пар нор открыл игрок.
|
||||||
|
*
|
||||||
|
* <p>Нужен и для настройки баланса, и для проверки входа в пустоту: порог 20
|
||||||
|
* набирается долго, а чтобы посмотреть, что там внутри, ждать двадцать нор
|
||||||
|
* не нужно.
|
||||||
|
*/
|
||||||
|
private void voidCounter(CommandSender sender, String[] args) {
|
||||||
|
if (args.length == 1 || args[1].equalsIgnoreCase("get")) {
|
||||||
|
if (args.length >= 3) {
|
||||||
|
final Player t = BlackKnife.player(args[2]);
|
||||||
|
if (t == null) {
|
||||||
|
sender.sendMessage(Component.text("Player not found").color(NamedTextColor.RED));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
sender.sendMessage(Component.text("Void of " + t.getName() + ": "
|
||||||
|
+ VoidCounter.get(t) + " / " + VoidCounter.THRESHOLD)
|
||||||
|
.color(NamedTextColor.LIGHT_PURPLE));
|
||||||
|
} else if (sender instanceof Player p) {
|
||||||
|
sender.sendMessage(Component.text("Your void: " + VoidCounter.get(p)
|
||||||
|
+ " / " + VoidCounter.THRESHOLD).color(NamedTextColor.LIGHT_PURPLE));
|
||||||
|
} else {
|
||||||
|
sender.sendMessage(Component.text("Usage: /blackknife void get [player]")
|
||||||
|
.color(NamedTextColor.YELLOW));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final boolean add = args[1].equalsIgnoreCase("add");
|
||||||
|
if (!add && !args[1].equalsIgnoreCase("set")) {
|
||||||
|
sender.sendMessage(Component.text("Usage: /blackknife void get|set <amount> [player] | add <n> [player]")
|
||||||
|
.color(NamedTextColor.YELLOW));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (args.length < 3) {
|
||||||
|
sender.sendMessage(Component.text("Usage: /blackknife void " + args[1] + " <amount> [player]")
|
||||||
|
.color(NamedTextColor.YELLOW));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final int amount;
|
||||||
|
try {
|
||||||
|
amount = Integer.parseInt(args[2]);
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
sender.sendMessage(Component.text("Not a number: " + args[2]).color(NamedTextColor.RED));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final Player target = args.length >= 4 ? BlackKnife.player(args[3])
|
||||||
|
: (sender instanceof Player p ? p : null);
|
||||||
|
if (target == null) {
|
||||||
|
sender.sendMessage(Component.text("Player not found").color(NamedTextColor.RED));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (add) {
|
||||||
|
VoidCounter.add(target, amount);
|
||||||
|
} else {
|
||||||
|
VoidCounter.set(target, amount);
|
||||||
|
}
|
||||||
|
VoidCounter.flush();
|
||||||
|
sender.sendMessage(Component.text("Void of " + target.getName() + ": "
|
||||||
|
+ VoidCounter.get(target) + " / " + VoidCounter.THRESHOLD)
|
||||||
|
.color(NamedTextColor.GREEN));
|
||||||
|
}
|
||||||
|
|
||||||
private void usage(CommandSender sender) {
|
private void usage(CommandSender sender) {
|
||||||
sender.sendMessage(Component.text("Usage: /blackknife give [player] | mode <mode> [player] | tp <get|set|add> <player> [amount] | reload").color(NamedTextColor.YELLOW));
|
sender.sendMessage(Component.text("Usage: /blackknife give [player] | mode <mode> [player] | tp <get|set|add> <player> [amount] | reload").color(NamedTextColor.YELLOW));
|
||||||
}
|
}
|
||||||
@@ -91,6 +158,14 @@ public class BlackKnifeCommand implements CommandExecutor, TabCompleter {
|
|||||||
if (args.length == 1) return BlackKnife.filter(SUBS, args[0]);
|
if (args.length == 1) return BlackKnife.filter(SUBS, args[0]);
|
||||||
if (args[0].equalsIgnoreCase("give")) {
|
if (args[0].equalsIgnoreCase("give")) {
|
||||||
if (args.length == 2) return BlackKnife.players(args[1]);
|
if (args.length == 2) return BlackKnife.players(args[1]);
|
||||||
|
} else if (args[0].equalsIgnoreCase("void")) {
|
||||||
|
if (args.length == 2) return BlackKnife.filter(Arrays.asList("get", "set", "add"), args[1]);
|
||||||
|
if (args.length == 3 && (args[1].equalsIgnoreCase("set") || args[1].equalsIgnoreCase("add"))) {
|
||||||
|
return BlackKnife.filter(Arrays.asList("0", "19", "20", "100"), args[2]);
|
||||||
|
}
|
||||||
|
if (args.length == 4 && (args[1].equalsIgnoreCase("set") || args[1].equalsIgnoreCase("add"))) {
|
||||||
|
return BlackKnife.players(args[3]);
|
||||||
|
}
|
||||||
} else if (args[0].equalsIgnoreCase("mode")) {
|
} else if (args[0].equalsIgnoreCase("mode")) {
|
||||||
if (args.length == 2) return BlackKnife.filter(Arrays.asList(BlackKnife.modes()), args[1]);
|
if (args.length == 2) return BlackKnife.filter(Arrays.asList(BlackKnife.modes()), args[1]);
|
||||||
if (args.length == 3) return BlackKnife.players(args[2]);
|
if (args.length == 3) return BlackKnife.players(args[2]);
|
||||||
|
|||||||
@@ -59,6 +59,10 @@ public class BlackKnifePlugin extends JavaPlugin {
|
|||||||
getCommand("magic").setExecutor(magicCmd);
|
getCommand("magic").setExecutor(magicCmd);
|
||||||
getCommand("magic").setTabCompleter(magicCmd);
|
getCommand("magic").setTabCompleter(magicCmd);
|
||||||
getServer().getScheduler().runTaskTimer(this, () -> tension.tickShield(), 20L, 1L);
|
getServer().getScheduler().runTaskTimer(this, () -> tension.tickShield(), 20L, 1L);
|
||||||
|
// Счётчик пустоты живёт в файле: читаем при старте, пишем раз в минуту
|
||||||
|
// и при выключении. Запись асинхронная, потому что flush() ходит на диск.
|
||||||
|
VoidCounter.load(this);
|
||||||
|
getServer().getScheduler().runTaskTimerAsynchronously(this, VoidCounter::flush, 1200L, 1200L);
|
||||||
cleanupLegacyKnocked();
|
cleanupLegacyKnocked();
|
||||||
getLogger().info("BlackKnife 1.0.0 enabled - Paper 1.21.8 lang=" + lang);
|
getLogger().info("BlackKnife 1.0.0 enabled - Paper 1.21.8 lang=" + lang);
|
||||||
}
|
}
|
||||||
@@ -128,6 +132,9 @@ public class BlackKnifePlugin extends JavaPlugin {
|
|||||||
getServer().getScheduler().cancelTasks(this);
|
getServer().getScheduler().cancelTasks(this);
|
||||||
StarFx.clear();
|
StarFx.clear();
|
||||||
MoleHole.clearAll();
|
MoleHole.clearAll();
|
||||||
|
// Счётчик сбрасываем на диск синхронно: после cancelTasks асинхронная
|
||||||
|
// задача уже не выполнится, а прогресс пустоты жалко терять.
|
||||||
|
VoidCounter.flush();
|
||||||
if (tension != null) {
|
if (tension != null) {
|
||||||
tension.hideAll();
|
tension.hideAll();
|
||||||
for (var p : getServer().getOnlinePlayers()) {
|
for (var p : getServer().getOnlinePlayers()) {
|
||||||
|
|||||||
@@ -137,9 +137,18 @@ public final class MoleHole {
|
|||||||
s.exit = null;
|
s.exit = null;
|
||||||
s.exitTo = null;
|
s.exitTo = null;
|
||||||
s.inside.clear();
|
s.inside.clear();
|
||||||
|
s.owner = p.getUniqueId();
|
||||||
s.state = State.WAITING_COORDS;
|
s.state = State.WAITING_COORDS;
|
||||||
ensureTask(plugin);
|
ensureTask(plugin);
|
||||||
p.sendActionBar(Component.text("Hole open. Type coordinates in chat: x, y, z").color(NamedTextColor.LIGHT_PURPLE));
|
if (VoidCounter.unlocked(p)) {
|
||||||
|
// При открытом счётчике дыра работает и без ответа: вход в пустоту.
|
||||||
|
p.sendActionBar(Component.text("Hole open. Type coordinates, or step in for the void")
|
||||||
|
.color(NamedTextColor.LIGHT_PURPLE));
|
||||||
|
} else {
|
||||||
|
final int left = VoidCounter.remaining(p);
|
||||||
|
p.sendActionBar(Component.text("Hole open. Type coordinates in chat: x, y, z ("
|
||||||
|
+ left + " more for the void)").color(NamedTextColor.LIGHT_PURPLE));
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -296,7 +305,26 @@ public final class MoleHole {
|
|||||||
|
|
||||||
// Погружение тех, кто встал в дыру.
|
// Погружение тех, кто встал в дыру.
|
||||||
for (Session s : SESSIONS.values()) {
|
for (Session s : SESSIONS.values()) {
|
||||||
if (s.state != State.PAIRED || s.entrance == null || s.exit == null) continue;
|
if (s.entrance == null) continue;
|
||||||
|
|
||||||
|
// Одиночная нора: ответа нет, поэтому обычный телепорт некуда.
|
||||||
|
// При открытом счётчике пустоты вход в такую дыру уводит игрока в
|
||||||
|
// пустоту — и только его: правило «дыра без ответа пускает лишь
|
||||||
|
// открывшего» иначе превратило бы счётчик в общий телепорт для всех.
|
||||||
|
if (s.state == State.WAITING_COORDS) {
|
||||||
|
if (s.owner == null || !VoidCounter.unlockedBy(s.owner)) continue;
|
||||||
|
final Player owner = Bukkit.getPlayer(s.owner);
|
||||||
|
if (owner == null || owner.isDead() || !standingIn(owner, s.entrance)) continue;
|
||||||
|
if (s.inside.getOrDefault(s.owner, Boolean.FALSE)) continue;
|
||||||
|
s.inside.put(s.owner, Boolean.TRUE);
|
||||||
|
MoleHoleEffect.sink(plugin, owner, s.entrance, who -> {
|
||||||
|
VoidWorld.forget(who);
|
||||||
|
VoidWorld.enter(who);
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (s.state != State.PAIRED || s.exit == null) continue;
|
||||||
|
|
||||||
for (Player target : Bukkit.getOnlinePlayers()) {
|
for (Player target : Bukkit.getOnlinePlayers()) {
|
||||||
final UUID uid = target.getUniqueId();
|
final UUID uid = target.getUniqueId();
|
||||||
|
|||||||
@@ -0,0 +1,161 @@
|
|||||||
|
package me.sashegdev.blackknife;
|
||||||
|
|
||||||
|
import org.bukkit.configuration.file.YamlConfiguration;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.StandardCopyOption;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Счётчик «пустоты» игрока: сколько пар нор он открыл.
|
||||||
|
*
|
||||||
|
* <p>Хранится в файле, а не в PDC, по условию. PDC живут в папке игрока и теряются
|
||||||
|
* при её подмене, плюс их не видно без разбора NBT. Тут обычный YAML, который
|
||||||
|
* можно открыть и прочитать глазами.
|
||||||
|
*
|
||||||
|
* <p>Порог 20 — с него открывается вход в пустоту без ответной дыры.
|
||||||
|
*
|
||||||
|
* <p>Запись на диск идёт через временный файл и подмену: если сервер убьют в
|
||||||
|
* момент записи, останется предыдущая целая версия, а не обрезанный файл.
|
||||||
|
* Сохранение вызывается из асинхронной задачи, поэтому доступ к карте
|
||||||
|
* синхронизирован.
|
||||||
|
*/
|
||||||
|
public final class VoidCounter {
|
||||||
|
|
||||||
|
private VoidCounter() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Сколько пар нужно, чтобы открылся вход в пустоту. */
|
||||||
|
public static final int THRESHOLD = 20;
|
||||||
|
|
||||||
|
private static final Object LOCK = new Object();
|
||||||
|
private static final Map<UUID, Integer> COUNTS = new HashMap<>();
|
||||||
|
private static File file;
|
||||||
|
private static boolean dirty;
|
||||||
|
|
||||||
|
/** Загружает счётчики из файла. Вызывается один раз при старте. */
|
||||||
|
public static void load(BlackKnifePlugin plugin) {
|
||||||
|
file = new File(plugin.getDataFolder(), "void.yml");
|
||||||
|
synchronized (LOCK) {
|
||||||
|
COUNTS.clear();
|
||||||
|
if (!file.exists()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final YamlConfiguration y = YamlConfiguration.loadConfiguration(file);
|
||||||
|
for (String key : y.getKeys(false)) {
|
||||||
|
// Ключ в YAML — UUID со знаками минус, точки в нём нет, так что
|
||||||
|
// секции достаточно: путь ровно players.<uuid>.
|
||||||
|
final var section = y.getConfigurationSection("players." + key);
|
||||||
|
if (section == null) continue;
|
||||||
|
final int n = section.getInt("count", 0);
|
||||||
|
if (n > 0) {
|
||||||
|
try {
|
||||||
|
COUNTS.put(UUID.fromString(key), n);
|
||||||
|
} catch (IllegalArgumentException ignored) {
|
||||||
|
// Мусор в файле не должен ронять загрузку.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int get(UUID id) {
|
||||||
|
synchronized (LOCK) {
|
||||||
|
return COUNTS.getOrDefault(id, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int get(Player p) {
|
||||||
|
return get(p.getUniqueId());
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Ставит счётчик в точное значение — для настройки и проверки порога. */
|
||||||
|
public static void set(Player p, int value) {
|
||||||
|
synchronized (LOCK) {
|
||||||
|
final int next = Math.max(0, value);
|
||||||
|
if (next == 0) {
|
||||||
|
COUNTS.remove(p.getUniqueId());
|
||||||
|
} else {
|
||||||
|
COUNTS.put(p.getUniqueId(), next);
|
||||||
|
}
|
||||||
|
dirty = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Хватает ли порога на пустоту. */
|
||||||
|
public static boolean unlocked(Player p) {
|
||||||
|
return get(p) >= THRESHOLD;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** То же по UUID: нужно там, где игрока ещё нет под рукой. */
|
||||||
|
public static boolean unlockedBy(UUID id) {
|
||||||
|
return get(id) >= THRESHOLD;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Плюс один за открытую пару. Помечает файл грязным, запись — отложенная. */
|
||||||
|
public static void addPair(Player p) {
|
||||||
|
add(p, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void add(Player p, int delta) {
|
||||||
|
synchronized (LOCK) {
|
||||||
|
final UUID id = p.getUniqueId();
|
||||||
|
final int next = Math.max(0, COUNTS.getOrDefault(id, 0) + delta);
|
||||||
|
COUNTS.put(id, next);
|
||||||
|
dirty = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Сколько осталось до порога. Для сообщений. */
|
||||||
|
public static int remaining(Player p) {
|
||||||
|
return Math.max(0, THRESHOLD - get(p));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void clear(Player p) {
|
||||||
|
synchronized (LOCK) {
|
||||||
|
if (COUNTS.remove(p.getUniqueId()) != null) {
|
||||||
|
dirty = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Пишет на диск, если есть что писать. Вызывать из асинхронной задачи. */
|
||||||
|
public static void flush() {
|
||||||
|
final File target;
|
||||||
|
synchronized (LOCK) {
|
||||||
|
if (file == null || !dirty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
dirty = false;
|
||||||
|
target = file;
|
||||||
|
}
|
||||||
|
final YamlConfiguration y = new YamlConfiguration();
|
||||||
|
synchronized (LOCK) {
|
||||||
|
for (Map.Entry<UUID, Integer> e : COUNTS.entrySet()) {
|
||||||
|
y.set("players." + e.getKey() + ".count", e.getValue());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
final File parent = target.getParentFile();
|
||||||
|
if (parent != null && !parent.exists() && !parent.mkdirs()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final File tmp = new File(target.getParentFile(), target.getName() + ".tmp");
|
||||||
|
Files.write(tmp.toPath(), y.saveToString().getBytes(StandardCharsets.UTF_8));
|
||||||
|
Files.move(tmp.toPath(), target.toPath(),
|
||||||
|
StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
|
||||||
|
} catch (IOException e) {
|
||||||
|
synchronized (LOCK) {
|
||||||
|
dirty = true; // попробуем ещё раз в следующий раз
|
||||||
|
}
|
||||||
|
BlackKnifePlugin.getInstance().getLogger().warning(
|
||||||
|
"void.yml not written: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -56,6 +56,12 @@ public final class VoidWorld implements Listener {
|
|||||||
/** Режим до входа в пустоту, чтобы вернуть как было, а не угадать. */
|
/** Режим до входа в пустоту, чтобы вернуть как было, а не угадать. */
|
||||||
private static final Map<UUID, GameMode> PREVIOUS_MODE = new HashMap<>();
|
private static final Map<UUID, GameMode> PREVIOUS_MODE = new HashMap<>();
|
||||||
|
|
||||||
|
/** Откуда вошли — понадобится для выхода обратно в своём соотношении. */
|
||||||
|
private static final Map<UUID, Location> ENTRY_POINT = new HashMap<>();
|
||||||
|
|
||||||
|
/** Сколько секунд игрок пробыл в пустоте в этой сессии. Обнуляется на выходе. */
|
||||||
|
private static final Map<UUID, Integer> VOIDNESS = new HashMap<>();
|
||||||
|
|
||||||
private static final long AMBIENCE_PERIOD = 60L;
|
private static final long AMBIENCE_PERIOD = 60L;
|
||||||
|
|
||||||
/** Это мир пустоты? Смотрим на имя измерения, а не на флаги уровня. */
|
/** Это мир пустоты? Смотрим на имя измерения, а не на флаги уровня. */
|
||||||
@@ -85,6 +91,74 @@ public final class VoidWorld implements Listener {
|
|||||||
return detectedWorldName;
|
return detectedWorldName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Мир пустоты или null, если он не найден. */
|
||||||
|
public static World world() {
|
||||||
|
for (World w : org.bukkit.Bukkit.getWorlds()) {
|
||||||
|
if (isVoid(w)) {
|
||||||
|
return w;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Сколько секунд игрок пробыл в пустоте в этой сессии. */
|
||||||
|
public static int voidness(Player p) {
|
||||||
|
return VOIDNESS.getOrDefault(p.getUniqueId(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Сброс при выходе из пустоты: счётчик сессионный, рестарт его тоже убирает. */
|
||||||
|
public static int takeVoidness(Player p) {
|
||||||
|
return VOIDNESS.remove(p.getUniqueId()) != null ? voidness(p) : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Переносит игрока в пустоту.
|
||||||
|
*
|
||||||
|
* <p>Точка приземления — верхний блок в колонке 0,0 плюс блок сверху. Не
|
||||||
|
* угадываем, на какой высоте лягут слои барьера: плоский генератор ставит их
|
||||||
|
* от низа диапазона, но конкретные числа зависят от версии, а
|
||||||
|
* getHighestBlockYAt находит верх всегда.
|
||||||
|
*
|
||||||
|
* <p>Куда вошли — запоминаем, чтобы потом вернуть игрока в ту же точку
|
||||||
|
* обычного мира. Обратный путь использует соотношение 1:2, поэтому точка
|
||||||
|
* выхода не равна точке входа по координатам.
|
||||||
|
*
|
||||||
|
* @return true, если перенос состоялся
|
||||||
|
*/
|
||||||
|
public static boolean enter(Player p) {
|
||||||
|
final World vw = world();
|
||||||
|
if (vw == null) {
|
||||||
|
p.sendActionBar(Component.text("The void is not ready")
|
||||||
|
.color(NamedTextColor.RED));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
ENTRY_POINT.put(p.getUniqueId(), p.getLocation().clone());
|
||||||
|
VOIDNESS.put(p.getUniqueId(), 0);
|
||||||
|
|
||||||
|
final int top = vw.getHighestBlockYAt(0, 0);
|
||||||
|
final Location dest = new Location(vw, 0.5D, top + 1.0D, 0.5D);
|
||||||
|
p.teleport(dest);
|
||||||
|
p.setFallDistance(0f);
|
||||||
|
p.setVelocity(new org.bukkit.util.Vector(0, 0, 0));
|
||||||
|
// Событие смены мира само поставит приключения, но только если игрок
|
||||||
|
// перешёл границу. Сюда он может попасть и без перехода (из мёртвого
|
||||||
|
// состояния), поэтому режим дублируем на случай отсутствия события.
|
||||||
|
p.setGameMode(GameMode.ADVENTURE);
|
||||||
|
p.sendActionBar(Component.text("Nothing here keeps you").color(NamedTextColor.DARK_GRAY));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Откуда игрок вошёл. null, если не входил. */
|
||||||
|
public static Location entryPoint(Player p) {
|
||||||
|
return ENTRY_POINT.get(p.getUniqueId());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void forget(Player p) {
|
||||||
|
PREVIOUS_MODE.remove(p.getUniqueId());
|
||||||
|
ENTRY_POINT.remove(p.getUniqueId());
|
||||||
|
VOIDNESS.remove(p.getUniqueId());
|
||||||
|
}
|
||||||
|
|
||||||
// -------------------------------------------------------------- переходы ----
|
// -------------------------------------------------------------- переходы ----
|
||||||
|
|
||||||
@EventHandler
|
@EventHandler
|
||||||
@@ -113,7 +187,7 @@ public final class VoidWorld implements Listener {
|
|||||||
|
|
||||||
@EventHandler
|
@EventHandler
|
||||||
public void onQuit(PlayerQuitEvent e) {
|
public void onQuit(PlayerQuitEvent e) {
|
||||||
PREVIOUS_MODE.remove(e.getPlayer().getUniqueId());
|
forget(e.getPlayer());
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------------------------------------------------------- запреты ----
|
// ------------------------------------------------------------- запреты ----
|
||||||
@@ -192,6 +266,9 @@ public final class VoidWorld implements Listener {
|
|||||||
@Override public void run() {
|
@Override public void run() {
|
||||||
for (Player p : plugin.getServer().getOnlinePlayers()) {
|
for (Player p : plugin.getServer().getOnlinePlayers()) {
|
||||||
if (!isVoid(p.getWorld())) continue;
|
if (!isVoid(p.getWorld())) continue;
|
||||||
|
// Раз в секунду тикает voidness: условие «за каждую секунду
|
||||||
|
// в этом мире +1», а период задачи ровно секунда.
|
||||||
|
VOIDNESS.merge(p.getUniqueId(), 1, Integer::sum);
|
||||||
final Location eye = p.getEyeLocation();
|
final Location eye = p.getEyeLocation();
|
||||||
|
|
||||||
// Звук не каждый тик, иначе это не эмбиент, а шум.
|
// Звук не каждый тик, иначе это не эмбиент, а шум.
|
||||||
|
|||||||
Reference in New Issue
Block a user