diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/core/commands/SlimefunTabCompleter.java b/src/main/java/io/github/thebusybiscuit/slimefun4/core/commands/SlimefunTabCompleter.java index 4e38255030..556686f021 100644 --- a/src/main/java/io/github/thebusybiscuit/slimefun4/core/commands/SlimefunTabCompleter.java +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/core/commands/SlimefunTabCompleter.java @@ -45,6 +45,8 @@ public List onTabComplete(CommandSender sender, Command cmd, String labe Bukkit.getWorlds().stream().map(WorldInfo::getName).toList()); list.add("*"); return createReturnList(list, args[1]); + } else if (args[0].equalsIgnoreCase("tick")) { + return createReturnList(List.of("at", "freeze", "query", "rate", "show", "unfreeze"), args[1]); } return null; } else if (args.length == 3) { diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/core/commands/subcommands/SlimefunSubCommands.java b/src/main/java/io/github/thebusybiscuit/slimefun4/core/commands/subcommands/SlimefunSubCommands.java index 4a211cef09..1bf4ad1292 100644 --- a/src/main/java/io/github/thebusybiscuit/slimefun4/core/commands/subcommands/SlimefunSubCommands.java +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/core/commands/subcommands/SlimefunSubCommands.java @@ -47,6 +47,7 @@ public static Collection getAllCommands(@Nonnull SlimefunCommand cmd commands.add(new BanItemCommand(plugin, cmd)); commands.add(new UnbanItemCommand(plugin, cmd)); commands.add(new ClearDataCommand(plugin, cmd)); + commands.add(new TickCommand(plugin, cmd)); return commands; } } diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/core/commands/subcommands/TickCommand.java b/src/main/java/io/github/thebusybiscuit/slimefun4/core/commands/subcommands/TickCommand.java new file mode 100644 index 0000000000..804e35a7c8 --- /dev/null +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/core/commands/subcommands/TickCommand.java @@ -0,0 +1,159 @@ +package io.github.thebusybiscuit.slimefun4.core.commands.subcommands; + +import com.xzavier0722.mc.plugin.slimefun4.storage.util.StorageCacheUtils; +import io.github.thebusybiscuit.slimefun4.api.items.SlimefunItem; +import io.github.thebusybiscuit.slimefun4.core.commands.SlimefunCommand; +import io.github.thebusybiscuit.slimefun4.core.commands.SubCommand; +import io.github.thebusybiscuit.slimefun4.implementation.Slimefun; +import javax.annotation.Nonnull; +import javax.annotation.ParametersAreNonnullByDefault; +import org.bukkit.FluidCollisionMode; +import org.bukkit.block.Block; +import org.bukkit.command.CommandSender; +import org.bukkit.command.ConsoleCommandSender; +import org.bukkit.entity.Player; + +/** + * @author balugaq + */ +public class TickCommand extends SubCommand { + @ParametersAreNonnullByDefault + public TickCommand(Slimefun plugin, SlimefunCommand cmd) { + super(plugin, cmd, "tick", false); + } + + @Override + public void onExecute(@Nonnull CommandSender sender, @Nonnull String[] args) { + if (sender.hasPermission("slimefun.command.tick") || sender instanceof ConsoleCommandSender) { + if (args.length == 1) { + Slimefun.getTickerTask().setTickFreeze(!Slimefun.getTickerTask().isTickFreeze()); + Slimefun.getLocalization() + .sendMessage( + sender, + "messages.tick-mode", + true, + msg -> msg.replace( + "%mode%", Slimefun.getTickerTask().isTickFreeze() ? "开启" : "关闭")); + return; + } + + if (args.length == 2) { + if (sender instanceof Player player && args[1].equalsIgnoreCase("at")) { + Block b = player.getTargetBlockExact(8, FluidCollisionMode.NEVER); + SlimefunItem sf = null; + if (b != null) { + sf = StorageCacheUtils.getSlimefunItem(b.getLocation()); + } + + if (sf == null) { + sf = SlimefunItem.getByItem(player.getInventory().getItemInMainHand()); + } + + if (sf == null) { + Slimefun.getLocalization().sendMessage(sender, "messages.not-found", true); + return; + } + + final SlimefunItem finalSf = sf; + Slimefun.getTickerTask() + .setTickFreezePredicate( + entry -> entry.getItem().getId().equals(finalSf.getId())); + + return; + } else if (args[1].equalsIgnoreCase("show")) { + Slimefun.getTickerTask().showWaitingList(); + return; + } else if (args[1].equalsIgnoreCase("freeze")) { + Slimefun.getTickerTask().setTickFreeze(true); + Slimefun.getLocalization() + .sendMessage(sender, "messages.tick-mode", true, msg -> msg.replace("%mode%", "开启")); + return; + } else if (args[1].equalsIgnoreCase("unfreeze")) { + Slimefun.getTickerTask().setTickFreeze(false); + Slimefun.getLocalization() + .sendMessage(sender, "messages.tick-mode", true, msg -> msg.replace("%mode%", "关闭")); + return; + } else if (args[1].equalsIgnoreCase("query")) { + Slimefun.getLocalization() + .sendMessage( + sender, + "messages.tick-query", + true, + msg -> msg.replace( + "%mode%", + Slimefun.getTickerTask().isTickFreeze() ? "开启" : "关闭") + .replace( + "%tick-rate%", + "" + + Slimefun.getTickerTask() + .getTickRate())); + return; + } else { + SlimefunItem item = SlimefunItem.getById(args[1].toUpperCase()); + if (item != null) { + Slimefun.getTickerTask() + .setTickFreezePredicate(entry -> entry.getItem().equals(item)); + } else { + Slimefun.getLocalization().sendMessage(sender, "messages.not-found"); + } + return; + } + } + + if (args[1].equalsIgnoreCase("rate") && args.length == 3) { + try { + int rate = Integer.parseInt(args[2]); + Slimefun.getTickerTask().setTickRate(rate); + Slimefun.getLocalization() + .sendMessage( + sender, + "messages.tick-rate", + true, + msg -> msg.replace("%tick-rate%", String.valueOf(rate))); + } catch (NumberFormatException e) { + Slimefun.getLocalization().sendMessage(sender, "messages.not-a-number", true); + } + return; + } + + if (args.length >= 4) { + int offset = 0; + if (args.length == 5) { + offset = 1; + } + String worldname = args.length == 5 + ? args[1] + : (sender instanceof Player p ? p.getWorld().getName() : "world"); + try { + int x = Integer.parseInt(args[1 + offset]); + int y = Integer.parseInt(args[2 + offset]); + int z = Integer.parseInt(args[3 + offset]); + Slimefun.getTickerTask().setTickFreezePredicate(entry -> { + var l = entry.getLocation(); + return l.getWorld().getName().equals(worldname) + && l.getBlockX() == x + && l.getBlockY() == y + && l.getBlockZ() == z; + }); + } catch (NumberFormatException e) { + Slimefun.getLocalization().sendMessage(sender, "messages.not-a-number", true); + } + + Slimefun.getLocalization() + .sendMessage( + sender, + "messages.usage", + true, + msg -> msg.replace("%usage%", "/sf tick ( | )")); + } + } else { + Slimefun.getLocalization().sendMessage(sender, "messages.no-permission", true); + } + } + + @Nonnull + @Override + public String getDescription() { + return "commands.tick.description"; + } +} diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/tasks/TickerTask.java b/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/tasks/TickerTask.java index 3638beb7af..4f03374ad5 100644 --- a/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/tasks/TickerTask.java +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/implementation/tasks/TickerTask.java @@ -11,22 +11,34 @@ import io.github.thebusybiscuit.slimefun4.api.items.SlimefunItem; import io.github.thebusybiscuit.slimefun4.core.ticker.TickLocation; import io.github.thebusybiscuit.slimefun4.implementation.Slimefun; +import io.github.thebusybiscuit.slimefun4.utils.ParticleUtil; +import java.util.ArrayDeque; import java.util.Collections; +import java.util.Deque; import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Predicate; import java.util.logging.Level; import java.util.stream.Collectors; import javax.annotation.Nonnull; import javax.annotation.Nullable; import javax.annotation.ParametersAreNonnullByDefault; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.Getter; import lombok.Setter; import me.mrCookieSlime.Slimefun.Objects.handlers.BlockTicker; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.event.ClickEvent; +import net.kyori.adventure.text.format.TextColor; import org.apache.commons.lang.Validate; +import org.bukkit.Bukkit; import org.bukkit.Chunk; import org.bukkit.Location; +import org.bukkit.entity.Player; import org.bukkit.scheduler.BukkitScheduler; /** @@ -38,6 +50,7 @@ * @see BlockTicker * */ +@Getter public class TickerTask implements Runnable { /** @@ -52,13 +65,37 @@ public class TickerTask implements Runnable { */ private final Map bugs = new ConcurrentHashMap<>(); + private int count = 0; + + @Setter private int tickRate; + private boolean halted = false; private boolean running = false; @Setter private volatile boolean paused = false; + private Deque waiting = new ArrayDeque<>(256); + + private final int PAGE_SIZE = 10; + + @Setter + private boolean tickFreeze = false; + + @Setter + private Predicate tickFreezePredicate = entry -> false; + + @Data + @AllArgsConstructor + public static class WaitingEntry { + private Location location; + private SlimefunItem item; + private ASlimefunDataContainer data; + private long timestamp; + private boolean sync; + } + /** * This method starts the {@link TickerTask} on an asynchronous schedule. * @@ -69,7 +106,7 @@ public void start(@Nonnull Slimefun plugin) { this.tickRate = Slimefun.getCfg().getInt("URID.custom-ticker-delay"); BukkitScheduler scheduler = plugin.getServer().getScheduler(); - scheduler.runTaskTimerAsynchronously(plugin, this, 100L, tickRate); + scheduler.runTaskTimerAsynchronously(plugin, this, 100L, 1); } /** @@ -85,6 +122,21 @@ public void run() { return; } + if (tickFreeze && !waiting.isEmpty()) { + return; + } + + count += 1; + if (count < tickRate) { + return; + } + count = 0; + + int length = waiting.size(); + for (int i = 0; i < length; i++) { + tickBlock(); + } + try { // If this method is actually still running... DON'T if (running) { @@ -115,6 +167,9 @@ public void run() { reset(); Slimefun.getProfiler().stop(); + if (tickFreeze) { + showWaitingList(); + } } catch (Exception | LinkageError x) { Slimefun.logger() .log( @@ -163,20 +218,11 @@ private void tickLocation(@Nonnull Set tickers, @Nonnull Location l Slimefun.getProfiler().scheduleEntries(1); item.getBlockTicker().update(); - /** - * We are inserting a new timestamp because synchronized actions - * are always ran with a 50ms delay (1 game tick) - */ - Slimefun.runSync(() -> { - if (blockData.isPendingRemove()) { - return; - } - tickBlock(l, item, blockData, System.nanoTime()); - }); + tickBlock(l, item, blockData, System.nanoTime(), true); } else { long timestamp = Slimefun.getProfiler().newEntry(); item.getBlockTicker().update(); - tickBlock(l, item, blockData, timestamp); + tickBlock(l, item, blockData, timestamp, false); } tickers.add(item.getBlockTicker()); @@ -209,12 +255,12 @@ private void tickUniversalLocation(UUID uuid, Location l, @Nonnull Set { + ASlimefunDataContainer blockData = entry.getData(); + if (blockData.isPendingRemove()) { + return; + } + tickBlock(entry); + }); + } else { + tickBlock(entry); + } + } + + @ParametersAreNonnullByDefault + private void tickBlock(WaitingEntry entry) { + Location l = entry.location; + SlimefunItem item = entry.item; + ASlimefunDataContainer data = entry.data; + long timestamp = entry.timestamp; try { if (item.getBlockTicker().isUniversal()) { if (data instanceof SlimefunUniversalData universalData) { @@ -439,4 +529,82 @@ public void disableTicker(@Nonnull UUID uuid) { tickingLocations.values().forEach(loc -> loc.removeIf(tk -> uuid.equals(tk.getUuid()))); } } + + public void showWaitingList() { + showWaitingList(1); + } + + public void showWaitingList(int page) { + var builder = Component.text() + .color(TextColor.color(0xFFD700)) + .append(Component.text("===== Ticker 等待列表 =====")) + .appendNewline(); + + int j = 0; + for (var entry : + waiting.stream().skip((page - 1) * PAGE_SIZE).limit(PAGE_SIZE).toList()) { + int id = (page - 1) * PAGE_SIZE + j + 1; + String head = id + ". " + entry.item.getItemName() + " "; + builder.color(TextColor.color(0x00B7B7)) + .append(Component.text() + .append(Component.text(head)) + .hoverEvent(Component.text("点击步过").clickEvent(ClickEvent.callback(p2 -> { + for (int i = 0; i < id; i++) { + tickBlock(); + } + showWaitingList(); + })))) + .append(Component.text(" ".repeat(Math.max(0, 12 - head.length())))) + .append(Component.text("[运行到] ") + .hoverEvent(Component.text("点击运行到此并停止")) + .clickEvent(ClickEvent.callback(p2 -> { + for (int i = 0; i < id - 1; i++) { + tickBlock(); + } + showWaitingList(); + }))) + .append(Component.text("[步过] ") + .hoverEvent(Component.text("点击步过")) + .clickEvent(ClickEvent.callback(p2 -> { + for (int i = 0; i < id; i++) { + tickBlock(); + } + showWaitingList(); + }))) + .append(Component.text("[高亮] ") + .hoverEvent(Component.text("点击高亮方块")) + .clickEvent(ClickEvent.callback(p2 -> { + if (p2 instanceof Player p) { + ParticleUtil.highlightBlock(p, entry.getLocation(), 3); + } + }))) + .appendNewline(); + j++; + } + int totalPage = (waiting.size() - 1) / PAGE_SIZE + 1; + builder.color(TextColor.color(0xFFD700)) + .append(Component.text() + .append(Component.text("=== 上一页 < ") + .hoverEvent(Component.text("点击跳转到上一页 (" + (page - 1) + ")")) + .clickEvent(ClickEvent.callback(p2 -> { + if (page - 1 < 1) { + return; + } + showWaitingList(page - 1); + }))) + .append(Component.text(page + " / " + totalPage) + .append(Component.text(" > 下一页 ===") + .hoverEvent(Component.text("点击跳转到下一页 (" + (page + 1) + ")")) + .clickEvent(ClickEvent.callback(p2 -> { + if (page + 1 > totalPage) { + return; + } + showWaitingList(page + 1); + }))))); + + Component text = builder.build(); + Bukkit.getServer().getOnlinePlayers().stream().filter(Player::isOp).forEach(player -> { + player.sendMessage(text); + }); + } } diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/utils/JavaUtil.java b/src/main/java/io/github/thebusybiscuit/slimefun4/utils/JavaUtil.java new file mode 100644 index 0000000000..ab2fdff550 --- /dev/null +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/utils/JavaUtil.java @@ -0,0 +1,24 @@ +package io.github.thebusybiscuit.slimefun4.utils; + +import org.jetbrains.annotations.NotNull; + +/** + * @author Final_ROOT + */ +public class JavaUtil { + public static double[] disperse(int size, Number @NotNull ... value) { + if (size == 1 && value.length > 0) { + return new double[] {value[0].doubleValue()}; + } else if (size == 0 || value.length == 0) { + return new double[0]; + } + double[] result = new double[size--]; + for (int i = 0; i <= size; i++) { + double p = ((double) i) / size * (value.length - 1); + double value1 = value[(int) Math.floor(p)].doubleValue() * (1 - p + Math.floor(p)); + double value2 = value[(int) Math.ceil(p)].doubleValue() * (p - Math.floor(p)); + result[i] = value1 + value2; + } + return result; + } +} diff --git a/src/main/java/io/github/thebusybiscuit/slimefun4/utils/ParticleUtil.java b/src/main/java/io/github/thebusybiscuit/slimefun4/utils/ParticleUtil.java new file mode 100644 index 0000000000..d6d4f50c6d --- /dev/null +++ b/src/main/java/io/github/thebusybiscuit/slimefun4/utils/ParticleUtil.java @@ -0,0 +1,122 @@ +package io.github.thebusybiscuit.slimefun4.utils; + +import io.github.thebusybiscuit.slimefun4.implementation.Slimefun; +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.Particle; +import org.bukkit.World; +import org.bukkit.block.Block; +import org.bukkit.entity.Player; +import org.bukkit.plugin.Plugin; +import org.jetbrains.annotations.NotNull; + +/** + * @author Final_ROOT + * @author balugaq + * @since 2.0 + */ +@SuppressWarnings("DuplicatedCode") +public class ParticleUtil { + private static final double[] BLOCK_CUBE_OFFSET_X = new double[] {0, 1, 0, 0, 1, 1, 0, 1}; + private static final double[] BLOCK_CUBE_OFFSET_Y = new double[] {0, 0, 1, 0, 1, 0, 1, 1}; + private static final double[] BLOCK_CUBE_OFFSET_Z = new double[] {0, 0, 0, 1, 0, 1, 1, 1}; + + public static void drawLineByTotalAmount( + @NotNull Particle particle, int totalAmount, @NotNull Location @NotNull ... locations) { + for (int i = 0; i < locations.length; i++) { + if ((i + 1) < locations.length) { + Location location1 = locations[i]; + Location location2 = locations[i + 1]; + + if (totalAmount < 1 || location1.getWorld() == null || location1.getWorld() != location2.getWorld()) { + return; + } + World world = location1.getWorld(); + double[] x = JavaUtil.disperse(totalAmount, location1.getX(), location2.getX()); + double[] y = JavaUtil.disperse(totalAmount, location1.getY(), location2.getY()); + double[] z = JavaUtil.disperse(totalAmount, location1.getZ(), location2.getZ()); + for (int j = 0; j < totalAmount; j++) { + world.spawnParticle(particle, x[j], y[j], z[j], 1, 0, 0, 0, 0); + } + } + } + } + + public static void drawCubeByBlock( + @NotNull final Plugin plugin, + @NotNull final Particle particle, + final long interval, + @NotNull final Block @NotNull ... blocks) { + int time = 0; + for (final Block block : blocks) { + final Location location = block.getLocation(); + final World world = location.getWorld(); + if (world == null) { + continue; + } + final int x = location.getBlockX(); + final int y = location.getBlockY(); + final int z = location.getBlockZ(); + if (time < 50) { + for (int i = 0; i < BLOCK_CUBE_OFFSET_X.length; i++) { + world.spawnParticle( + particle, + x + BLOCK_CUBE_OFFSET_X[i], + y + BLOCK_CUBE_OFFSET_Y[i], + z + BLOCK_CUBE_OFFSET_Z[i], + 1, + 0, + 0, + 0, + 0); + } + } else { + plugin.getServer() + .getScheduler() + .runTaskLaterAsynchronously( + plugin, + () -> { + for (int i = 0; i < BLOCK_CUBE_OFFSET_X.length; i++) { + world.spawnParticle( + particle, + x + BLOCK_CUBE_OFFSET_X[i], + y + BLOCK_CUBE_OFFSET_Y[i], + z + BLOCK_CUBE_OFFSET_Z[i], + 1, + 0, + 0, + 0, + 0); + } + }, + time / 50); + } + time += (int) interval; + } + } + + public static void drawLineFrom(final @NotNull Location location1, final @NotNull Location location2) { + drawLineByTotalAmount(Particle.WAX_OFF, (int) location1.distance(location2) * 4, location1, location2); + } + + public static void highlightBlock(final @NotNull Location location) { + highlightBlock(location.getBlock()); + } + + public static void highlightBlock(final Block block) { + drawCubeByBlock(Slimefun.instance(), Particle.WAX_ON, 1, block); + } + + public static void highlightBlock(@NotNull Player player, @NotNull Location location, int shrinkTimes) { + for (int i = 0; i < shrinkTimes; i++) { + Bukkit.getScheduler() + .runTaskLaterAsynchronously( + Slimefun.instance(), + () -> { + drawLineFrom(player.getEyeLocation().clone().add(0D, -0.5D, 0D), location); + highlightBlock(location); + }, + 20L * i); + } + } +} diff --git a/src/main/resources/languages/zh-CN/messages.yml b/src/main/resources/languages/zh-CN/messages.yml index 157624b38e..b539a04dae 100644 --- a/src/main/resources/languages/zh-CN/messages.yml +++ b/src/main/resources/languages/zh-CN/messages.yml @@ -69,6 +69,8 @@ commands: in-progress: "&a迁移正在进行中..." blockdata: description: '操作粘液方块数据' + tick: + description: '操作粘液刻断点' placeholderapi: profile-loading: '加载中...' guide: @@ -164,6 +166,8 @@ actionbar: radiation: '&6辐射暴露等级:&c%level%&7/&e100' messages: + tick-rate: "Tick rate: %tick-rate%" + tick-query: "Tick Freeze: %mode%, Tick rate: %tick-rate%" android-no-permission: "&c你的机器人在某个你没有权限的领地/地皮中无法工作, 机器人已自动暂停运行." not-researched: '&4你的知识还不足以理解这个物品. &c你需要先解锁 &f"%item%"&f' not-enough-xp: '&4你没有足够的经验来解锁这个研究' @@ -277,6 +281,9 @@ messages: above-limit-level: '&c你要附/祛魔物品的附魔等级超过了 %level% 级!' pickaxe-of-the-seeker: no-ores: '&c附近找不到任何矿石!' + tick-mode: "&eTick Freeze: %mode%" + not-found: "&c无效的参数" + not-a-number: "&c请输入一个数字" machines: pattern-not-found: '&e抱歉, 这不是正确的合成配方, 请检查发射器里放置物品的顺序.' unknown-material: '&e抱歉, 我无法识别在发射器里的物品. 请按照合成配方放置物品.' diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index e39fa73963..c98312f089 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -110,3 +110,6 @@ permissions: slimefun.debugging: description: Allows you to use the debugging tool from Slimefun default: op + slimefun.command.tick: + description: Allows you to do /sf tick + default: op