diff --git a/src/main/java/me/youhavetrouble/standin/StandinDialog.java b/src/main/java/me/youhavetrouble/standin/StandinDialog.java index 7aef413..7716560 100644 --- a/src/main/java/me/youhavetrouble/standin/StandinDialog.java +++ b/src/main/java/me/youhavetrouble/standin/StandinDialog.java @@ -4,13 +4,21 @@ import io.papermc.paper.dialog.Dialog; import io.papermc.paper.registry.data.dialog.ActionButton; import io.papermc.paper.registry.data.dialog.DialogBase; import io.papermc.paper.registry.data.dialog.action.DialogAction; +import io.papermc.paper.registry.data.dialog.body.DialogBody; import io.papermc.paper.registry.data.dialog.input.DialogInput; +import io.papermc.paper.registry.data.dialog.input.SingleOptionDialogInput; import io.papermc.paper.registry.data.dialog.type.DialogType; +import me.youhavetrouble.standin.converter.ArmorStandToMannequinConverter; +import me.youhavetrouble.standin.converter.EntityConverter; +import me.youhavetrouble.standin.converter.MannequinToArmorStandConverter; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.event.ClickCallback; +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.minimessage.MiniMessage; import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer; import org.bukkit.entity.ArmorStand; import org.bukkit.entity.Entity; +import org.bukkit.entity.Mannequin; import org.bukkit.entity.Player; import org.jetbrains.annotations.NotNull; @@ -19,27 +27,123 @@ import java.util.ArrayList; import java.util.List; import java.util.UUID; +// TODO generify this mess + @SuppressWarnings("UnstableApiUsage") public class StandinDialog { - public static void openArmorStandDialog(@NotNull Player player, @NotNull ArmorStand armorStand) { - + public static void openConversionDialog(@NotNull Player player, @NotNull ArmorStand armorStand) { if (armorStand.isDead()) return; UUID armorStandId = armorStand.getUniqueId(); + UUID playerId = player.getUniqueId(); + + List inputs = List.of( + DialogInput.singleOption("entity_type", Component.text("New entity type"), List.of( + SingleOptionDialogInput.OptionEntry.create("armor_stand", Component.text("Armor Stand"), true), + SingleOptionDialogInput.OptionEntry.create("mannequin", Component.text("Mannequin"), false) + )).build() + ); + + ActionButton saveButton = ActionButton.builder(Component.text("Change type")).action( + DialogAction.customClick((view, audience) -> { + if (!(audience instanceof Player callbackPlayer)) return; + Entity entity = callbackPlayer.getWorld().getEntity(armorStandId); + if (playerId != callbackPlayer.getUniqueId()) return; + if (!(entity instanceof ArmorStand stand)) return; + if (stand.isDead()) return; + String newEntityType = view.getText("entity_type"); + switch (newEntityType) { + case "mannequin" -> { + EntityConverter mannequinConverter = new ArmorStandToMannequinConverter(); + Mannequin mannequin = mannequinConverter.spawn(stand); + if (mannequin == null) { + audience.sendMessage(Component.text("Error spawning new entity. Ensure new entity can spawn in this spot.").color(NamedTextColor.RED)); + return; + } + stand.remove(); + } + case null, default -> { + } + } + }, ClickCallback.Options.builder().lifetime(Duration.ofHours(1)).uses(1).build()) + ).build(); + + Dialog dialog = Dialog.create(builder -> builder.empty() + .base( + DialogBase.builder(Component.text("Armor Stand Conversion")) + .body(List.of( + DialogBody.plainMessage( + Component.text("Some settings might not persist between changing entity type").color(NamedTextColor.RED) + ) + )).inputs(inputs) + .build()) + .type(DialogType.confirmation(saveButton, ActionButton.builder(Component.text("Cancel")).build())) + ); + + player.showDialog(dialog); + } + + public static void openConversionDialog(@NotNull Player player, @NotNull Mannequin mannequin) { + if (mannequin.isDead()) return; + + UUID MannequinId = mannequin.getUniqueId(); + UUID playerId = player.getUniqueId(); + + List inputs = List.of( + DialogInput.singleOption("entity_type", Component.text("New entity type"), List.of( + SingleOptionDialogInput.OptionEntry.create("mannequin", Component.text("Mannequin"), true), + SingleOptionDialogInput.OptionEntry.create("armor_stand", Component.text("Armor Stand"), false) + )).build() + ); + + ActionButton saveButton = ActionButton.builder(Component.text("Change type")).action( + DialogAction.customClick((view, audience) -> { + if (!(audience instanceof Player callbackPlayer)) return; + if (playerId != callbackPlayer.getUniqueId()) return; + Entity entity = callbackPlayer.getWorld().getEntity(MannequinId); + if (!(entity instanceof Mannequin cMannequin)) return; + if (cMannequin.isDead()) return; + String newEntityType = view.getText("entity_type"); + switch (newEntityType) { + case "armor_stand" -> { + EntityConverter armorStandConverter = new MannequinToArmorStandConverter(); + ArmorStand armorStand = armorStandConverter.spawn(cMannequin); + if (armorStand == null) { + audience.sendMessage(Component.text("Error spawning new entity. Ensure new entity can spawn in this spot.").color(NamedTextColor.RED)); + return; + } + cMannequin.remove(); + } + case null, default -> { + } + } + }, ClickCallback.Options.builder().lifetime(Duration.ofMinutes(5)).uses(1).build()) + ).build(); + + Dialog dialog = Dialog.create(builder -> builder.empty() + .base( + DialogBase.builder(Component.text("Mannequin Conversion")) + .body(List.of( + DialogBody.plainMessage( + Component.text("Some settings might not persist between changing entity type").color(NamedTextColor.RED) + ) + )).inputs(inputs) + .build()) + .type(DialogType.confirmation(saveButton, ActionButton.builder(Component.text("Cancel")).build())) + ); + + player.showDialog(dialog); + } + + public static void openArmorStandDialog(@NotNull Player player, @NotNull ArmorStand armorStand) { + if (armorStand.isDead()) return; + + UUID armorStandId = armorStand.getUniqueId(); + UUID playerId = player.getUniqueId(); List inputs = new ArrayList<>(); - String currentName = ""; - if (armorStand.customName() != null) { - currentName = PlainTextComponentSerializer.plainText().serialize(armorStand.name()); - } - - inputs.add( - DialogInput.text("name", Component.text("Display name")) - .initial(currentName) - .build() - ); inputs.add( DialogInput.bool("invisible", Component.text("Invisible")) .initial(armorStand.isInvisible()) @@ -67,28 +171,106 @@ public class StandinDialog { ActionButton saveButton = ActionButton.builder(Component.text("Save")).action( DialogAction.customClick((view, audience) -> { if (!(audience instanceof Player callbackPlayer)) return; + if (playerId != callbackPlayer.getUniqueId()) return; Entity entity = callbackPlayer.getWorld().getEntity(armorStandId); if (!(entity instanceof ArmorStand stand)) return; if (stand.isDead()) return; - String customName = view.getText("name"); - if (customName == null || customName.isEmpty()) { - stand.customName(null); - } else { - stand.customName(Component.text(customName)); - } stand.setInvisible(Boolean.TRUE.equals(view.getBoolean("invisible"))); stand.setBasePlate(Boolean.TRUE.equals(view.getBoolean("basePlate"))); stand.setArms(Boolean.TRUE.equals(view.getBoolean("arms"))); stand.setSmall(Boolean.TRUE.equals(view.getBoolean("small"))); - }, ClickCallback.Options.builder().lifetime(Duration.ofHours(1)).uses(1).build()) + }, ClickCallback.Options.builder().lifetime(Duration.ofMinutes(5)).uses(1).build()) ).build(); + ActionButton changeTypeButton = ActionButton.builder(Component.text("Change type")) + .action( + DialogAction.customClick((view, audience) -> { + if (!(audience instanceof Player callbackPlayer)) return; + if (playerId != callbackPlayer.getUniqueId()) return; + Entity entity = callbackPlayer.getWorld().getEntity(armorStandId); + if (!(entity instanceof ArmorStand stand)) return; + if (stand.isDead()) return; + StandinDialog.openConversionDialog(callbackPlayer, armorStand); + }, ClickCallback.Options.builder().lifetime(Duration.ofMinutes(5)).uses(1).build() + ) + ).build(); + Dialog dialog = Dialog.create(builder -> builder.empty() .base( DialogBase.builder(Component.text("Armor Stand Editor")) .inputs(inputs) - .build()) - .type(DialogType.confirmation(saveButton, ActionButton.builder(Component.text("Cancel")).build())) + .build() + ).type( + DialogType.multiAction( + List.of(saveButton, changeTypeButton), + ActionButton.builder(Component.text("Cancel")).build(), + 1) + ) + ); + + player.showDialog(dialog); + } + + public static void openMannequinDialog(@NotNull Player player, @NotNull Mannequin mannequin) { + if (mannequin.isDead()) return; + + UUID mannequinId = mannequin.getUniqueId(); + UUID playerId = player.getUniqueId(); + List inputs = new ArrayList<>(); + + String name = ""; + Component customName = mannequin.customName(); + if (customName != null) { + name = PlainTextComponentSerializer.plainText().serialize(customName); + } + + inputs.add( + DialogInput.text("name", Component.text("Name")) + .initial(name) + .build() + ); + + ActionButton saveButton = ActionButton.builder(Component.text("Save")).action( + DialogAction.customClick((view, audience) -> { + if (!(audience instanceof Player callbackPlayer)) return; + if (playerId != callbackPlayer.getUniqueId()) return; + Entity entity = callbackPlayer.getWorld().getEntity(mannequinId); + if (!(entity instanceof Mannequin cMannequin)) return; + if (cMannequin.isDead()) return; + String newName = view.getText("name"); + Component displayName = null; + if (newName != null) { + displayName = MiniMessage.miniMessage().deserialize(newName); + } + cMannequin.customName(displayName); + + }, ClickCallback.Options.builder().lifetime(Duration.ofMinutes(5)).uses(1).build()) + ).build(); + + ActionButton changeTypeButton = ActionButton.builder(Component.text("Change type")) + .action( + DialogAction.customClick((view, audience) -> { + if (!(audience instanceof Player callbackPlayer)) return; + if (playerId != callbackPlayer.getUniqueId()) return; + Entity entity = callbackPlayer.getWorld().getEntity(mannequinId); + if (!(entity instanceof Mannequin cMannequin)) return; + if (cMannequin.isDead()) return; + StandinDialog.openConversionDialog(callbackPlayer, cMannequin); + }, ClickCallback.Options.builder().lifetime(Duration.ofMinutes(5)).uses(1).build() + ) + ).build(); + + Dialog dialog = Dialog.create(builder -> builder.empty() + .base( + DialogBase.builder(Component.text("Armor Stand Editor")) + .inputs(inputs) + .build() + ).type( + DialogType.multiAction( + List.of(saveButton, changeTypeButton), + ActionButton.builder(Component.text("Cancel")).build(), + 1) + ) ); player.showDialog(dialog); diff --git a/src/main/java/me/youhavetrouble/standin/converter/ArmorStandToMannequinConverter.java b/src/main/java/me/youhavetrouble/standin/converter/ArmorStandToMannequinConverter.java new file mode 100644 index 0000000..5692a57 --- /dev/null +++ b/src/main/java/me/youhavetrouble/standin/converter/ArmorStandToMannequinConverter.java @@ -0,0 +1,37 @@ +package me.youhavetrouble.standin.converter; + +import me.youhavetrouble.standin.StandIn; +import org.bukkit.entity.ArmorStand; +import org.bukkit.entity.Mannequin; +import org.bukkit.inventory.EquipmentSlot; +import org.jetbrains.annotations.NotNull; + +public class ArmorStandToMannequinConverter implements EntityConverter { + + @Override + public Class entityFrom() { + return ArmorStand.class; + } + + @Override + public Class entityTo() { + return Mannequin.class; + } + + @Override + public Mannequin spawn(@NotNull ArmorStand from) { + try { + return from.getWorld().spawn(from.getLocation(), entityTo(), (mannequin -> { + for (EquipmentSlot slot : EquipmentSlot.values()) { + try { + mannequin.getEquipment().setItem(slot, from.getItem(slot)); + } catch (IllegalArgumentException ignored) { + } + } + })); + } catch (IllegalArgumentException e) { + StandIn.getPlugin(StandIn.class).getSLF4JLogger().warn("Failed to spawn entity", e); + return null; + } + } +} diff --git a/src/main/java/me/youhavetrouble/standin/converter/EntityConverter.java b/src/main/java/me/youhavetrouble/standin/converter/EntityConverter.java new file mode 100644 index 0000000..f85e052 --- /dev/null +++ b/src/main/java/me/youhavetrouble/standin/converter/EntityConverter.java @@ -0,0 +1,19 @@ +package me.youhavetrouble.standin.converter; + +import org.bukkit.entity.Entity; +import org.jetbrains.annotations.NotNull; + +public interface EntityConverter { + + Class entityFrom(); + + Class entityTo(); + + /** + * Spawn the new entity in the old entity's spot. + * @param from Entity to base the new one from + * @return The spawned entity or null if entity cannot be spawned + */ + T spawn(@NotNull F from); + +} diff --git a/src/main/java/me/youhavetrouble/standin/converter/MannequinToArmorStandConverter.java b/src/main/java/me/youhavetrouble/standin/converter/MannequinToArmorStandConverter.java new file mode 100644 index 0000000..bcbdcc2 --- /dev/null +++ b/src/main/java/me/youhavetrouble/standin/converter/MannequinToArmorStandConverter.java @@ -0,0 +1,37 @@ +package me.youhavetrouble.standin.converter; + +import me.youhavetrouble.standin.StandIn; +import org.bukkit.entity.ArmorStand; +import org.bukkit.entity.Mannequin; +import org.bukkit.inventory.EquipmentSlot; +import org.jetbrains.annotations.NotNull; + +public class MannequinToArmorStandConverter implements EntityConverter { + + @Override + public Class entityFrom() { + return Mannequin.class; + } + + @Override + public Class entityTo() { + return ArmorStand.class; + } + + @Override + public ArmorStand spawn(@NotNull Mannequin from) { + try { + return from.getWorld().spawn(from.getLocation(), entityTo(), (armorStand -> { + for (EquipmentSlot slot : EquipmentSlot.values()) { + try { + armorStand.getEquipment().setItem(slot, from.getEquipment().getItem(slot)); + } catch (IllegalArgumentException ignored) { + } + } + })); + } catch (IllegalArgumentException e) { + StandIn.getPlugin(StandIn.class).getSLF4JLogger().warn("Failed to spawn entity", e); + return null; + } + } +} diff --git a/src/main/java/me/youhavetrouble/standin/stand/StandinInteractionListener.java b/src/main/java/me/youhavetrouble/standin/stand/StandinInteractionListener.java index b26337b..280cc8f 100644 --- a/src/main/java/me/youhavetrouble/standin/stand/StandinInteractionListener.java +++ b/src/main/java/me/youhavetrouble/standin/stand/StandinInteractionListener.java @@ -1,10 +1,12 @@ package me.youhavetrouble.standin.stand; +import io.papermc.paper.event.player.PlayerPickEntityEvent; import me.youhavetrouble.standin.StandinDialog; import org.bukkit.attribute.Attribute; import org.bukkit.attribute.AttributeInstance; import org.bukkit.entity.ArmorStand; import org.bukkit.entity.Entity; +import org.bukkit.entity.Mannequin; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; import org.bukkit.event.EventPriority; @@ -26,6 +28,11 @@ public class StandinInteractionListener implements Listener { return true; } + if (entity instanceof Mannequin mannequin && player.hasPermission("standin.edit.mannequin")) { + StandinDialog.openMannequinDialog(player, mannequin); + return true; + } + return false; } @@ -74,4 +81,22 @@ public class StandinInteractionListener implements Listener { event.setCancelled(true); } + @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) + public void onInteractWithStands(PlayerPickEntityEvent event) { + if (!event.getPlayer().isSneaking()) return; + if (event.getEntity() instanceof ArmorStand armorStand) { + StandinDialog.openConversionDialog(event.getPlayer(), armorStand); + event.setCancelled(true); + return; + } + + // This currently does not work since pick entity does not fire for mannequins + // https://github.com/PaperMC/Paper/issues/13340 + if (event.getEntity() instanceof Mannequin mannequin) { + StandinDialog.openConversionDialog(event.getPlayer(), mannequin); + event.setCancelled(true); + return; + } + } + }