diff --git a/sakura-server/src/main/java/me/samsuik/sakura/command/BaseMenuCommand.java b/sakura-server/src/main/java/me/samsuik/sakura/command/BaseMenuCommand.java new file mode 100644 index 0000000..3574de6 --- /dev/null +++ b/sakura-server/src/main/java/me/samsuik/sakura/command/BaseMenuCommand.java @@ -0,0 +1,88 @@ +package me.samsuik.sakura.command; + +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; +import org.bukkit.command.Command; +import org.bukkit.command.CommandSender; +import org.jspecify.annotations.NullMarked; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +@NullMarked +public abstract class BaseMenuCommand extends BaseSubCommand { + private static final String HEADER_MESSAGE = "| "; + private static final String COMMAND_MSG = "| * /"; + + public BaseMenuCommand(String name) { + super(name); + } + + public abstract String header(); + + public Iterable helpCommands() { + return this.subCommands(); + } + + public abstract Iterable subCommands(); + + @Override + public final void execute(CommandSender sender, String[] args) { + if (args.length > 0) { + for (final Command base : this.subCommands()) { + if (base.getName().equalsIgnoreCase(args[0])) { + base.execute(sender, "", Arrays.copyOfRange(args, 1, args.length)); + return; + } + } + } + + this.sendHelpMessage(sender); + } + + private void sendHelpMessage(CommandSender sender) { + sender.sendMessage(Component.text(".", NamedTextColor.DARK_PURPLE)); + for (final String header : this.header().split("\n")) { + if (!header.isEmpty()) { + sender.sendRichMessage(HEADER_MESSAGE, Placeholder.unparsed("message", header)); + } + } + + for (final Command command : this.helpCommands()) { + if (command != this) { + sender.sendRichMessage(COMMAND_MSG, Placeholder.unparsed("command", command.getName())); + } + } + + sender.sendMessage(Component.text("'", NamedTextColor.DARK_PURPLE)); + } + + @Override + public final List tabComplete(CommandSender sender, String alias, String[] args) throws IllegalArgumentException { + if (!this.testPermissionSilent(sender) || args.length == 0) { + return Collections.emptyList(); + } + + final Command command = SakuraCommands.getCommand(args[0]); + final List completions = new ArrayList<>(); + if (command != null && args.length > 1) { + final String[] newArgs = Arrays.copyOfRange(args, 1, args.length); + completions.addAll(command.tabComplete(sender, alias, newArgs)); + } else { + for (final Command subCommand : SakuraCommands.SUB_COMMANDS) { + final String commandName = subCommand.getName(); + if (commandName.startsWith(args[0])) { + completions.add(commandName); + } + } + } + + final String lastArg = args[args.length - 1]; + return completions.stream() + .filter(result -> result.startsWith(lastArg)) + .toList(); + } +} diff --git a/sakura-server/src/main/java/me/samsuik/sakura/command/BaseSubCommand.java b/sakura-server/src/main/java/me/samsuik/sakura/command/BaseSubCommand.java index 0fb9c8a..f5cfdb3 100644 --- a/sakura-server/src/main/java/me/samsuik/sakura/command/BaseSubCommand.java +++ b/sakura-server/src/main/java/me/samsuik/sakura/command/BaseSubCommand.java @@ -42,6 +42,10 @@ public abstract class BaseSubCommand extends Command { return completions; } + public void sendPlayerOnlyMessage(CommandSender sender) { + sender.sendRichMessage("This command can only be ran by players"); + } + protected final Optional parseInt(String[] args, int index) { return this.parse(args, index, Integer::parseInt); } diff --git a/sakura-server/src/main/java/me/samsuik/sakura/command/SakuraCommand.java b/sakura-server/src/main/java/me/samsuik/sakura/command/SakuraCommand.java index 79273bc..33d20a2 100644 --- a/sakura-server/src/main/java/me/samsuik/sakura/command/SakuraCommand.java +++ b/sakura-server/src/main/java/me/samsuik/sakura/command/SakuraCommand.java @@ -1,84 +1,33 @@ package me.samsuik.sakura.command; import com.google.common.collect.Iterables; -import net.kyori.adventure.text.Component; -import net.kyori.adventure.text.format.NamedTextColor; -import net.kyori.adventure.text.minimessage.MiniMessage; -import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; import net.minecraft.server.MinecraftServer; import org.bukkit.command.Command; -import org.bukkit.command.CommandSender; import org.jspecify.annotations.NullMarked; import java.util.*; @NullMarked -public final class SakuraCommand extends Command { - private static final Component HEADER_MESSAGE = MiniMessage.miniMessage().deserialize(""" - . - | This is the main command for Sakura. - | All exclusive commands are listed below.""" - ); - - private static final String COMMAND_MSG = "| * /"; - +public final class SakuraCommand extends BaseMenuCommand { public SakuraCommand(String name) { super(name); this.description = ""; - this.usageMessage = "/sakura"; - this.setPermission("bukkit.command.sakura"); } @Override - public boolean execute(CommandSender sender, String commandLabel, String[] args) { - if (args.length > 0) { - final Command versionCommand = MinecraftServer.getServer().server.getCommandMap().getCommand("version"); - for (final Command base : Iterables.concat(SakuraCommands.SUB_COMMANDS, List.of(versionCommand))) { - if (base.getName().equalsIgnoreCase(args[0])) { - return base.execute(sender, commandLabel, Arrays.copyOfRange(args, 1, args.length)); - } - } - } - - this.sendHelpMessage(sender); - return false; - } - - private void sendHelpMessage(CommandSender sender) { - sender.sendMessage(HEADER_MESSAGE); - - for (final Command command : SakuraCommands.COMMANDS.values()) { - if (command != this) { - sender.sendRichMessage(COMMAND_MSG, Placeholder.unparsed("command", command.getName())); - } - } - - sender.sendMessage(Component.text("'", NamedTextColor.DARK_PURPLE)); + public String header() { + return "This is the main command for Sakura.\n" + + "All exclusive commands are listed below."; } @Override - public List tabComplete(CommandSender sender, String alias, String[] args) throws IllegalArgumentException { - if (!this.testPermissionSilent(sender) || args.length == 0) { - return Collections.emptyList(); - } + public Iterable helpCommands() { + return SakuraCommands.COMMANDS.values(); + } - final Command command = SakuraCommands.getCommand(args[0]); - final List completions = new ArrayList<>(); - if (command != null && args.length > 1) { - final String[] newArgs = Arrays.copyOfRange(args, 1, args.length); - completions.addAll(command.tabComplete(sender, alias, newArgs)); - } else { - for (final Command subCommand : SakuraCommands.SUB_COMMANDS) { - final String commandName = subCommand.getName(); - if (commandName.startsWith(args[0])) { - completions.add(commandName); - } - } - } - - final String lastArg = args[args.length - 1]; - return completions.stream() - .filter(result -> result.startsWith(lastArg)) - .toList(); + @Override + public Iterable subCommands() { + final Command versionCommand = MinecraftServer.getServer().server.getCommandMap().getCommand("version"); + return Iterables.concat(SakuraCommands.SUB_COMMANDS, List.of(versionCommand)); } } diff --git a/sakura-server/src/main/java/me/samsuik/sakura/command/SakuraCommands.java b/sakura-server/src/main/java/me/samsuik/sakura/command/SakuraCommands.java index ef67a54..109600c 100644 --- a/sakura-server/src/main/java/me/samsuik/sakura/command/SakuraCommands.java +++ b/sakura-server/src/main/java/me/samsuik/sakura/command/SakuraCommands.java @@ -1,6 +1,8 @@ package me.samsuik.sakura.command; import me.samsuik.sakura.command.subcommands.*; +import me.samsuik.sakura.command.subcommands.debug.DebugLocalRegions; +import me.samsuik.sakura.command.subcommands.debug.DebugRedstoneCache; import me.samsuik.sakura.player.visibility.VisibilityTypes; import net.minecraft.server.MinecraftServer; import org.bukkit.command.Command; @@ -14,8 +16,9 @@ import java.util.Set; @NullMarked public final class SakuraCommands { - static final Map COMMANDS = new HashMap<>(); - static final Set SUB_COMMANDS = new HashSet<>(); + public static final Map COMMANDS = new HashMap<>(); + public static final Set SUB_COMMANDS = new HashSet<>(); + public static final Set DEBUG_COMMANDS = new HashSet<>(); static { COMMANDS.put("config", new ConfigCommand("config")); @@ -27,6 +30,8 @@ public final class SakuraCommands { SUB_COMMANDS.add(new DebugCommand("debug")); // "sakura" isn't a subcommand COMMANDS.put("sakura", new SakuraCommand("sakura")); + DEBUG_COMMANDS.add(new DebugRedstoneCache("redstone-cache")); + DEBUG_COMMANDS.add(new DebugLocalRegions("local-regions")); } public static void registerCommands(MinecraftServer server) { diff --git a/sakura-server/src/main/java/me/samsuik/sakura/command/subcommands/DebugCommand.java b/sakura-server/src/main/java/me/samsuik/sakura/command/subcommands/DebugCommand.java index 8bfb75c..2d2289d 100644 --- a/sakura-server/src/main/java/me/samsuik/sakura/command/subcommands/DebugCommand.java +++ b/sakura-server/src/main/java/me/samsuik/sakura/command/subcommands/DebugCommand.java @@ -1,74 +1,23 @@ package me.samsuik.sakura.command.subcommands; -import me.samsuik.sakura.command.BaseSubCommand; -import me.samsuik.sakura.redstone.RedstoneNetwork; -import net.minecraft.core.BlockPos; -import net.minecraft.server.level.ServerPlayer; -import net.minecraft.world.level.Level; -import org.bukkit.DyeColor; -import org.bukkit.Location; -import org.bukkit.Material; -import org.bukkit.command.CommandSender; -import org.bukkit.craftbukkit.entity.CraftPlayer; -import org.bukkit.craftbukkit.util.CraftLocation; -import org.bukkit.entity.Player; +import me.samsuik.sakura.command.BaseMenuCommand; +import me.samsuik.sakura.command.SakuraCommands; +import org.bukkit.command.Command; import org.jspecify.annotations.NullMarked; -import java.util.HashSet; -import java.util.List; -import java.util.Set; -import java.util.concurrent.ThreadLocalRandom; - @NullMarked -public final class DebugCommand extends BaseSubCommand { +public final class DebugCommand extends BaseMenuCommand { public DebugCommand(String name) { super(name); } @Override - public void execute(CommandSender sender, String[] args) { - if (!(sender instanceof Player player) || args.length == 0) { - return; - } - - ServerPlayer nmsPlayer = ((CraftPlayer) player).getHandle(); - if (args[0].equalsIgnoreCase("redstone-cache")) { - this.showCachedWires(player, nmsPlayer.level()); - } + public String header() { + return "Debug command for testing Sakura features and api"; } @Override - public void tabComplete(List list, String[] args) throws IllegalArgumentException { - list.add("redstone-cache"); - } - - private void showCachedWires(Player player, Level level) { - Set locations = new HashSet<>(); - for (RedstoneNetwork network : level.redstoneWireCache.getNetworkCache().values()) { - byte randomColour = (byte) ThreadLocalRandom.current().nextInt(16); - DyeColor dyeColour = DyeColor.getByWoolData(randomColour); - Material material = Material.matchMaterial(dyeColour.name() + "_WOOL"); - - if (!network.isRegistered()) { - continue; - } - - for (BlockPos pos : network.getWirePositions()) { - Location location = CraftLocation.toBukkit(pos, level); - if (player.getLocation().distance(location) >= 64.0) { - continue; - } - player.sendBlockChange(location, material.createBlockData()); - locations.add(location); - } - } - - player.sendRichMessage("Displaying %dx cached redstone wires".formatted(locations.size())); - - level.levelTickScheduler.delayedTask(() -> { - for (Location loc : locations) { - player.sendBlockChange(loc, loc.getBlock().getBlockData()); - } - }, 1200); + public Iterable subCommands() { + return SakuraCommands.DEBUG_COMMANDS; } } diff --git a/sakura-server/src/main/java/me/samsuik/sakura/command/subcommands/debug/DebugLocalRegions.java b/sakura-server/src/main/java/me/samsuik/sakura/command/subcommands/debug/DebugLocalRegions.java new file mode 100644 index 0000000..2727680 --- /dev/null +++ b/sakura-server/src/main/java/me/samsuik/sakura/command/subcommands/debug/DebugLocalRegions.java @@ -0,0 +1,54 @@ +package me.samsuik.sakura.command.subcommands.debug; + +import me.samsuik.sakura.command.BaseSubCommand; +import me.samsuik.sakura.local.LocalRegion; +import me.samsuik.sakura.local.storage.LocalStorageHandler; +import me.samsuik.sakura.local.storage.LocalValueStorage; +import org.bukkit.Location; +import org.bukkit.World; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; +import org.jspecify.annotations.NullMarked; + +import java.util.Optional; + +@NullMarked +public final class DebugLocalRegions extends BaseSubCommand { + private static final int DEFAULT_REGION_SIZE = 16; + + public DebugLocalRegions(String name) { + super(name); + } + + @Override + public void execute(CommandSender sender, String[] args) { + if (!(sender instanceof Player player)) { + this.sendPlayerOnlyMessage(sender); + return; + } + + final Location location = player.getLocation(); + final World world = location.getWorld(); + final LocalStorageHandler storageHandler = world.getStorageHandler(); + final int blockX = location.getBlockX(); + final int blockZ = location.getBlockZ(); + final Optional currentRegion = storageHandler.locate(blockX, blockZ); + + if ("create".equalsIgnoreCase(args[0])) { + final int size = parseInt(args, 1).orElse(DEFAULT_REGION_SIZE); + final LocalRegion region = LocalRegion.at(blockX, blockZ, size); + storageHandler.put(region, new LocalValueStorage()); + } + + if ("get".equalsIgnoreCase(args[0])) { + sender.sendRichMessage("" + (currentRegion.isPresent() ? currentRegion.get() : "not inside of a region")); + } + + if (currentRegion.isPresent()) { + final LocalRegion region = currentRegion.get(); + if ("delete".equalsIgnoreCase(args[0])) { + storageHandler.remove(region); + } + } + } +} diff --git a/sakura-server/src/main/java/me/samsuik/sakura/command/subcommands/debug/DebugRedstoneCache.java b/sakura-server/src/main/java/me/samsuik/sakura/command/subcommands/debug/DebugRedstoneCache.java new file mode 100644 index 0000000..7fec461 --- /dev/null +++ b/sakura-server/src/main/java/me/samsuik/sakura/command/subcommands/debug/DebugRedstoneCache.java @@ -0,0 +1,64 @@ +package me.samsuik.sakura.command.subcommands.debug; + +import me.samsuik.sakura.command.BaseSubCommand; +import me.samsuik.sakura.redstone.RedstoneNetwork; +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.level.Level; +import org.bukkit.DyeColor; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.command.CommandSender; +import org.bukkit.craftbukkit.entity.CraftPlayer; +import org.bukkit.craftbukkit.util.CraftLocation; +import org.bukkit.entity.Player; +import org.jspecify.annotations.NullMarked; + +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.ThreadLocalRandom; + +@NullMarked +public final class DebugRedstoneCache extends BaseSubCommand { + public DebugRedstoneCache(String name) { + super(name); + } + + @Override + public void execute(CommandSender sender, String[] args) { + if (!(sender instanceof Player player)) { + this.sendPlayerOnlyMessage(sender); + return; + } + + ServerPlayer nmsPlayer = ((CraftPlayer) player).getHandle(); + Level level = nmsPlayer.level(); + Set locations = new HashSet<>(); + for (RedstoneNetwork network : level.redstoneWireCache.getNetworkCache().values()) { + byte randomColour = (byte) ThreadLocalRandom.current().nextInt(16); + DyeColor dyeColour = DyeColor.getByWoolData(randomColour); + Material material = Material.matchMaterial(dyeColour.name() + "_WOOL"); + + if (!network.isRegistered()) { + continue; + } + + for (BlockPos pos : network.getWirePositions()) { + Location location = CraftLocation.toBukkit(pos, level); + if (player.getLocation().distance(location) >= 64.0) { + continue; + } + player.sendBlockChange(location, material.createBlockData()); + locations.add(location); + } + } + + player.sendRichMessage("Displaying %dx cached redstone wires".formatted(locations.size())); + + level.levelTickScheduler.delayedTask(() -> { + for (Location loc : locations) { + player.sendBlockChange(loc, loc.getBlock().getBlockData()); + } + }, 1200); + } +} diff --git a/sakura-server/src/main/java/me/samsuik/sakura/configuration/local/LocalConfigManager.java b/sakura-server/src/main/java/me/samsuik/sakura/configuration/local/LocalConfigManager.java index 824ef0a..288307c 100644 --- a/sakura-server/src/main/java/me/samsuik/sakura/configuration/local/LocalConfigManager.java +++ b/sakura-server/src/main/java/me/samsuik/sakura/configuration/local/LocalConfigManager.java @@ -62,10 +62,12 @@ public final class LocalConfigManager implements LocalStorageHandler { @Override public synchronized void put(@NonNull LocalRegion region, @NonNull LocalValueStorage storage) { - this.ensureNotOverlapping(region); int shift = this.regionExponent; int regionChunks = regionChunks(region, shift); + // make sure there's no overlapping regions + this.ensureRegionIsNotOverlapping(region, regionChunks); + if (regionChunks <= SMALL_REGION_SIZE) { this.forEachRegionChunks(region, pos -> { this.smallRegions.computeIfAbsent(pos, k -> new ArrayList<>()) @@ -73,8 +75,9 @@ public final class LocalConfigManager implements LocalStorageHandler { }); } else { this.largeRegions.add(region); - // The region exponent might be too small - if (this.largeRegions.size() % 24 == 0) { + + // The region exponent may be too small + if ((this.largeRegions.size() & 15) == 0) { this.resizeRegions(); } } @@ -189,14 +192,18 @@ public final class LocalConfigManager implements LocalStorageHandler { return config; } - private void ensureNotOverlapping(LocalRegion region) { + private void ensureRegionIsNotOverlapping(LocalRegion region, int regionChunks) { Set nearbyRegions = new ReferenceOpenHashSet<>(); - this.forEachRegionChunks(region, pos -> { - nearbyRegions.addAll(this.smallRegions.getOrDefault(pos, List.of())); - }); + if (regionChunks > SMALL_REGION_SIZE) { + nearbyRegions.addAll(this.storageMap.keySet()); + } else { + this.forEachRegionChunks(region, pos -> { + nearbyRegions.addAll(this.smallRegions.getOrDefault(pos, List.of())); + }); + } for (LocalRegion present : Iterables.concat(nearbyRegions, this.largeRegions)) { if (present != region && present.intersects(region)) { - throw new UnsupportedOperationException("overlapping region (%s, %s)".formatted(present, region)); + throw new OverlappingRegionException(present, region); } } } diff --git a/sakura-server/src/main/java/me/samsuik/sakura/configuration/local/OverlappingRegionException.java b/sakura-server/src/main/java/me/samsuik/sakura/configuration/local/OverlappingRegionException.java new file mode 100644 index 0000000..cd7e530 --- /dev/null +++ b/sakura-server/src/main/java/me/samsuik/sakura/configuration/local/OverlappingRegionException.java @@ -0,0 +1,9 @@ +package me.samsuik.sakura.configuration.local; + +import me.samsuik.sakura.local.LocalRegion; + +public final class OverlappingRegionException extends RuntimeException { + public OverlappingRegionException(LocalRegion presentRegion, LocalRegion region) { + super("overlapping region (%s, %s)".formatted(presentRegion, region)); + } +}