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;