From 081feaa2deb52dd71080f60f87b8c6849673fcb1 Mon Sep 17 00:00:00 2001 From: Samsuik Date: Sun, 12 Oct 2025 21:34:40 +0100 Subject: [PATCH] Add /mechanic command to view server mechanics --- gradle.properties | 3 +- .../mechanics/MinecraftMechanicsTarget.java | 5 + .../sakura/command/SakuraCommands.java | 1 + .../command/subcommands/MechanicCommand.java | 96 +++++++++++++++++++ .../configuration/GlobalConfiguration.java | 11 +++ 5 files changed, 115 insertions(+), 1 deletion(-) create mode 100644 sakura-server/src/main/java/me/samsuik/sakura/command/subcommands/MechanicCommand.java diff --git a/gradle.properties b/gradle.properties index ef5071a..def504e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -9,4 +9,5 @@ org.gradle.vfs.watch=false org.gradle.caching=true org.gradle.parallel=true # org.gradle.daemon=true -# org.gradle.configureondemand=true \ No newline at end of file +# org.gradle.configureondemand=true +paper.runWorkDir=../1.21.9-dev \ No newline at end of file diff --git a/sakura-api/src/main/java/me/samsuik/sakura/mechanics/MinecraftMechanicsTarget.java b/sakura-api/src/main/java/me/samsuik/sakura/mechanics/MinecraftMechanicsTarget.java index c65ea24..790693f 100644 --- a/sakura-api/src/main/java/me/samsuik/sakura/mechanics/MinecraftMechanicsTarget.java +++ b/sakura-api/src/main/java/me/samsuik/sakura/mechanics/MinecraftMechanicsTarget.java @@ -109,4 +109,9 @@ public record MinecraftMechanicsTarget(short mechanicVersion, byte serverType) { return new MinecraftMechanicsTarget(mechanicVersion, serverType); } + + @Override + public String toString() { + return MechanicVersion.name(this.mechanicVersion) + "+" + ServerType.name(this.serverType); + } } 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 08bb108..1b462f2 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 @@ -26,6 +26,7 @@ public final class SakuraCommands { COMMANDS.put("fps", new FPSCommand("fps")); COMMANDS.put("tntvisibility", new VisualCommand(VisibilityTypes.TNT, "tnttoggle")); COMMANDS.put("sandvisibility", new VisualCommand(VisibilityTypes.SAND, "sandtoggle")); + COMMANDS.put("mechanic", new MechanicCommand("mechanic")); SUB_COMMANDS.addAll(COMMANDS.values()); SUB_COMMANDS.add(new DebugCommand("debug")); // "sakura" isn't a subcommand diff --git a/sakura-server/src/main/java/me/samsuik/sakura/command/subcommands/MechanicCommand.java b/sakura-server/src/main/java/me/samsuik/sakura/command/subcommands/MechanicCommand.java new file mode 100644 index 0000000..d67058f --- /dev/null +++ b/sakura-server/src/main/java/me/samsuik/sakura/command/subcommands/MechanicCommand.java @@ -0,0 +1,96 @@ +package me.samsuik.sakura.command.subcommands; + +import me.samsuik.sakura.command.PlayerOnlySubCommand; +import me.samsuik.sakura.configuration.GlobalConfiguration; +import me.samsuik.sakura.configuration.local.CachedLocalConfiguration; +import net.kyori.adventure.text.minimessage.MiniMessage; +import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; +import net.minecraft.core.BlockPos; +import org.bukkit.Location; +import org.bukkit.craftbukkit.CraftWorld; +import org.bukkit.craftbukkit.util.CraftLocation; +import org.bukkit.entity.Entity; +import org.bukkit.entity.FallingBlock; +import org.bukkit.entity.Player; +import org.bukkit.entity.TNTPrimed; +import org.bukkit.event.entity.EntitySpawnEvent; +import org.bukkit.util.Vector; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +import java.util.List; + +@NullMarked +public final class MechanicCommand extends PlayerOnlySubCommand { + private static final String MECHANIC_INFORMATION_MESSAGE = """ + Mechanic Version: + Height Parity: + Tnt Spread: + Tnt Flow: + Redstone Implementation: + "optimize-explosions": + Consistent Radius: + Lava Flow Speed: """; + + public MechanicCommand(final String name) { + super(name); + this.setAliases(List.of("mechanics", "mech")); + this.description = "Displays information related to cannon mechanics"; + } + + @Override + public void execute(final Player player, final String[] args) { + final Location location = player.getLocation(); + final BlockPos blockPos = CraftLocation.toBlockPosition(location); + final CraftWorld craftWorld = ((CraftWorld) location.getWorld()); + final CachedLocalConfiguration config = craftWorld.getHandle().localConfig().at(blockPos); + + player.sendMessage(GlobalConfiguration.get().messages.mechanicInformationComponent( + MiniMessage.miniMessage().deserialize( + MECHANIC_INFORMATION_MESSAGE, + Placeholder.unparsed("mechanic_version", config.mechanicsTarget.toString()), + Placeholder.unparsed("height_parity", String.valueOf(this.hasHeightParity(location))), + Placeholder.unparsed("tnt_spread", this.getTntSpread(location)), + Placeholder.unparsed("tnt_flow", String.valueOf(this.hasTntFlow(location))), + Placeholder.unparsed("redstone_implementation", config.redstoneBehaviour.implementation().getFriendlyName()), + Placeholder.unparsed("broken_explosion_behaviour", String.valueOf(craftWorld.getHandle().paperConfig().environment.optimizeExplosions)), + Placeholder.unparsed("consistent_radius", String.valueOf(config.consistentExplosionRadius)), + Placeholder.unparsed("lava_flow_speed", String.valueOf(config.lavaFlowSpeed)) + ) + )); + } + + private boolean hasHeightParity(final Location location) { + final FallingBlock fallingBlock = this.createTestEntity(location, FallingBlock.class); + return fallingBlock != null && fallingBlock.getHeightParity(); + } + + private String getTntSpread(final Location location) { + final TNTPrimed tnt = this.createTestEntity(location, TNTPrimed.class); + if (tnt == null) { + return "unknown"; + } + + final Vector velocity = tnt.getVelocity(); + final String spread; + if (velocity.getX() != 0.0 && velocity.getY() != 0.0) { + spread = "ALL"; + } else if (velocity.getY() != 0.0) { + spread = "Y"; + } else { + spread = "NONE"; + } + + return spread; + } + + private boolean hasTntFlow(final Location location) { + final TNTPrimed tnt = this.createTestEntity(location, TNTPrimed.class); + return tnt == null || tnt.isPushedByFluid(); + } + + private @Nullable T createTestEntity(final Location location, final Class type) { + final T entity = location.getWorld().createEntity(location, type); + return new EntitySpawnEvent(entity).callEvent() ? entity : null; + } +} diff --git a/sakura-server/src/main/java/me/samsuik/sakura/configuration/GlobalConfiguration.java b/sakura-server/src/main/java/me/samsuik/sakura/configuration/GlobalConfiguration.java index 088505d..11571b5 100644 --- a/sakura-server/src/main/java/me/samsuik/sakura/configuration/GlobalConfiguration.java +++ b/sakura-server/src/main/java/me/samsuik/sakura/configuration/GlobalConfiguration.java @@ -5,8 +5,11 @@ import io.papermc.paper.configuration.Configuration; import io.papermc.paper.configuration.ConfigurationPart; import io.papermc.paper.configuration.type.number.IntOr; import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.event.HoverEvent; import net.kyori.adventure.text.minimessage.MiniMessage; +import net.kyori.adventure.text.minimessage.tag.Tag; import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; +import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver; import org.bukkit.Material; import org.slf4j.Logger; import org.spongepowered.configurate.objectmapping.meta.Comment; @@ -34,6 +37,7 @@ public final class GlobalConfiguration extends ConfigurationPart { public class Messages extends ConfigurationPart { public String durableBlockInteraction = "(S) This block has of "; public String fpsSettingChange = "(S) "; + public String mechanicInformation = "(S) For mechanic information: "; public boolean tpsShowEntityAndChunkCount = true; public Component fpsSettingChangeComponent(final String name, final String state) { @@ -51,6 +55,13 @@ public final class GlobalConfiguration extends ConfigurationPart { Placeholder.unparsed("durability", String.valueOf(durability)) ); } + + public Component mechanicInformationComponent(final Component hoverComponent) { + return MiniMessage.miniMessage().deserialize( + this.mechanicInformation, + TagResolver.resolver("information", Tag.styling(HoverEvent.showText(hoverComponent))) + ); + } } public Fps fps;