9
0
mirror of https://github.com/Samsuik/Sakura.git synced 2025-12-19 14:59:30 +00:00

add regions to debug command and fix large overlapping regions

This commit is contained in:
Samsuik
2025-04-07 15:46:03 +01:00
parent 4652ed3898
commit 9b5339f0bf
9 changed files with 260 additions and 131 deletions

View File

@@ -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 = "<dark_purple>| <white><message>";
private static final String COMMAND_MSG = "<dark_purple>| <dark_gray>*</dark_gray> /<light_purple><command>";
public BaseMenuCommand(String name) {
super(name);
}
public abstract String header();
public Iterable<Command> helpCommands() {
return this.subCommands();
}
public abstract Iterable<Command> 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<String> 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<String> 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();
}
}

View File

@@ -42,6 +42,10 @@ public abstract class BaseSubCommand extends Command {
return completions;
}
public void sendPlayerOnlyMessage(CommandSender sender) {
sender.sendRichMessage("<red>This command can only be ran by players");
}
protected final Optional<Integer> parseInt(String[] args, int index) {
return this.parse(args, index, Integer::parseInt);
}

View File

@@ -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("""
<dark_purple>.</dark_purple>
<dark_purple>| <white>This is the main command for <gradient:red:light_purple:0.5>Sakura</gradient>.
<dark_purple>| <white>All exclusive commands are listed below."""
);
private static final String COMMAND_MSG = "<dark_purple>| <dark_gray>*</dark_gray> /<light_purple><command>";
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 <gradient:red:light_purple:0.5>Sakura</gradient>.\n" +
"All exclusive commands are listed below.";
}
@Override
public List<String> tabComplete(CommandSender sender, String alias, String[] args) throws IllegalArgumentException {
if (!this.testPermissionSilent(sender) || args.length == 0) {
return Collections.emptyList();
}
public Iterable<Command> helpCommands() {
return SakuraCommands.COMMANDS.values();
}
final Command command = SakuraCommands.getCommand(args[0]);
final List<String> 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<Command> subCommands() {
final Command versionCommand = MinecraftServer.getServer().server.getCommandMap().getCommand("version");
return Iterables.concat(SakuraCommands.SUB_COMMANDS, List.of(versionCommand));
}
}

View File

@@ -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<String, Command> COMMANDS = new HashMap<>();
static final Set<Command> SUB_COMMANDS = new HashSet<>();
public static final Map<String, Command> COMMANDS = new HashMap<>();
public static final Set<Command> SUB_COMMANDS = new HashSet<>();
public static final Set<Command> 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) {

View File

@@ -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<String> list, String[] args) throws IllegalArgumentException {
list.add("redstone-cache");
}
private void showCachedWires(Player player, Level level) {
Set<Location> 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("<red>Displaying %dx cached redstone wires".formatted(locations.size()));
level.levelTickScheduler.delayedTask(() -> {
for (Location loc : locations) {
player.sendBlockChange(loc, loc.getBlock().getBlockData());
}
}, 1200);
public Iterable<Command> subCommands() {
return SakuraCommands.DEBUG_COMMANDS;
}
}

View File

@@ -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<LocalRegion> 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("<red>" + (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);
}
}
}
}

View File

@@ -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<Location> 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("<red>Displaying %dx cached redstone wires".formatted(locations.size()));
level.levelTickScheduler.delayedTask(() -> {
for (Location loc : locations) {
player.sendBlockChange(loc, loc.getBlock().getBlockData());
}
}, 1200);
}
}

View File

@@ -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<LocalRegion> 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);
}
}
}

View File

@@ -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));
}
}