diff --git a/build.gradle.kts b/build.gradle.kts index 258748b0..96e2609e 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -50,13 +50,13 @@ repositories { includeGroup("com.ticxo.modelengine") } } -/* + maven { url = uri("https://repo.minebench.de") content { includeGroup("de.themoep") } - }*/ + } } paperweight.reobfArtifactConfiguration = ReobfArtifactConfiguration.MOJANG_PRODUCTION @@ -76,8 +76,11 @@ dependencies { compileOnly("me.clip:placeholderapi:${project.property("papi_version")}") implementation("org.java-websocket:Java-WebSocket:1.5.7") + { + exclude("org.slf4j") + } - //implementation("de.themoep:inventorygui:1.6.3-SNAPSHOT") + implementation("de.themoep:inventorygui:1.6.3-SNAPSHOT") //compileOnly("dev.majek:hexnicks:3.1.1") @@ -198,6 +201,7 @@ tasks.shadowJar { relocate("xiamomc.pluginbase", "xiamomc.morph.shaded.pluginbase") relocate("org.bstats", "xiamomc.morph.shaded.bstats") relocate("de.tr7zw.changeme.nbtapi", "xiamomc.morph.shaded.nbtapi") + relocate("de.themoep.inventorygui", "xiamomc.morph.shaded.inventorygui") } tasks.withType() { diff --git a/src/main/java/xiamomc/morph/MorphPlugin.java b/src/main/java/xiamomc/morph/MorphPlugin.java index 8438b17e..e81c0a1d 100644 --- a/src/main/java/xiamomc/morph/MorphPlugin.java +++ b/src/main/java/xiamomc/morph/MorphPlugin.java @@ -20,6 +20,7 @@ import xiamomc.morph.misc.NetworkingHelper; import xiamomc.morph.misc.PlayerOperationSimulator; import xiamomc.morph.misc.disguiseProperty.DisguiseProperties; +import xiamomc.morph.misc.gui.IconLookup; import xiamomc.morph.misc.integrations.modelengine.ModelEngineHelper; import xiamomc.morph.misc.integrations.placeholderapi.PlaceholderIntegration; import xiamomc.morph.misc.integrations.residence.ResidenceEventProcessor; @@ -212,6 +213,9 @@ public void onEnable() clientHandler.sendReAuth(Bukkit.getOnlinePlayers()); }); + + //Init GUI IconLookup + IconLookup.instance(); } @ApiStatus.Internal diff --git a/src/main/java/xiamomc/morph/commands/MorphCommand.java b/src/main/java/xiamomc/morph/commands/MorphCommand.java index 0133797f..0dfb1dce 100644 --- a/src/main/java/xiamomc/morph/commands/MorphCommand.java +++ b/src/main/java/xiamomc/morph/commands/MorphCommand.java @@ -10,6 +10,7 @@ import xiamomc.morph.messages.HelpStrings; import xiamomc.morph.messages.MessageUtils; import xiamomc.morph.messages.MorphStrings; +import xiamomc.morph.misc.gui.DisguiseSelectScreenWrapper; import xiamomc.pluginbase.Annotations.Resolved; import xiamomc.pluginbase.Command.IPluginCommand; import xiamomc.pluginbase.Messages.FormattableMessage; @@ -35,9 +36,15 @@ public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command } if (args.length >= 1) + { morphManager.morph(sender, player, args[0], player.getTargetEntity(5)); + } else - sender.sendMessage(MessageUtils.prefixes(sender, MorphStrings.disguiseNotDefinedString())); + { + var gui = new DisguiseSelectScreenWrapper(player, 0); + gui.show(); + //sender.sendMessage(MessageUtils.prefixes(sender, MorphStrings.disguiseNotDefinedString())); + } } return true; diff --git a/src/main/java/xiamomc/morph/messages/GuiStrings.java b/src/main/java/xiamomc/morph/messages/GuiStrings.java new file mode 100644 index 00000000..7e790db3 --- /dev/null +++ b/src/main/java/xiamomc/morph/messages/GuiStrings.java @@ -0,0 +1,31 @@ +package xiamomc.morph.messages; + +import xiamomc.pluginbase.Messages.FormattableMessage; + +public class GuiStrings extends AbstractMorphStrings +{ + public static FormattableMessage nextPage() + { + return getFormattable(getKey("next_page"), "[Fallback] 下一页"); + } + + public static FormattableMessage prevPage() + { + return getFormattable(getKey("prev_page"), "[Fallback] 上一页"); + } + + public static FormattableMessage unDisguise() + { + return getFormattable(getKey("undisguise"), "[Fallback] 取消伪装"); + } + + public static FormattableMessage selectDisguise() + { + return getFormattable(getKey("title_select_disguise"), "[Fallback] 选择伪装"); + } + + private static String getKey(String key) +{ + return "chestui." + key; +} +} diff --git a/src/main/java/xiamomc/morph/misc/gui/DisguiseSelectScreenWrapper.java b/src/main/java/xiamomc/morph/misc/gui/DisguiseSelectScreenWrapper.java new file mode 100644 index 00000000..98a7f4ac --- /dev/null +++ b/src/main/java/xiamomc/morph/misc/gui/DisguiseSelectScreenWrapper.java @@ -0,0 +1,233 @@ +package xiamomc.morph.misc.gui; + +import de.themoep.inventorygui.InventoryGui; +import de.themoep.inventorygui.StaticGuiElement; +import it.unimi.dsi.fastutil.objects.ObjectArrayList; +import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer; +import org.bukkit.Material; +import org.bukkit.entity.Player; +import org.bukkit.event.inventory.InventoryType; +import org.bukkit.inventory.ItemStack; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import xiamomc.morph.MorphManager; +import xiamomc.morph.MorphPluginObject; +import xiamomc.morph.messages.GuiStrings; +import xiamomc.morph.messages.MessageUtils; +import xiamomc.morph.misc.DisguiseMeta; +import xiamomc.pluginbase.Annotations.Resolved; + +import java.util.List; + +public class DisguiseSelectScreenWrapper extends MorphPluginObject +{ + @NotNull + private final InventoryGui gui; + + private final Player bindingPlayer; + + private final int pageOffset; + + private final List disguises; + + private final String playerLocale; + + @Resolved(shouldSolveImmediately = true) + private MorphManager manager; + + /** + * 获取此GUI的行数 + * @return + */ + protected int getRowCount() + { + return 4; + } + + /** + * 获取此GUI的最大伪装显示物品承载量 + * @return + */ + private int getElementCapacity() + { + // 行数 - 1(Footer) + return ( getRowCount() - 1 ) * 9; + } + + /** + * 获取此页面在伪装列表中的起始Index + * @return + */ + private int getStartingIndex() + { + return this.pageOffset * (this.getRowCount() - 1) * 9; + } + + public DisguiseSelectScreenWrapper(Player bindingPlayer, int pageOffset) + { + this.disguises = manager.getAvaliableDisguisesFor(bindingPlayer); + this.bindingPlayer = bindingPlayer; + this.pageOffset = pageOffset; + this.playerLocale = MessageUtils.getLocale(bindingPlayer); + + this.gui = this.preparePage(); + initElements(); + } + + public void show() + { + this.gui.show(bindingPlayer); + } + + private InventoryGui preparePage() + { + var columns = 9; + + List rows = new ObjectArrayList<>(); + var index = this.getStartingIndex(); + + StringBuilder builder = new StringBuilder(); + + // Build element slots + for (int x = 1; x <= this.getRowCount() - 1; x++) + { + for (int y = 1; y <= columns; y++) + { + index++; + builder.append(this.getElementCharAt(index)); + } + + rows.add(builder.toString()); + builder = new StringBuilder(); + } + + // Build footer slots + var isLast = isLastPage(); + var isFirst = this.pageOffset == 0; + + builder.append(isFirst ? "x" : "P").append("xxxUxxx").append(isLast ? "x" : "N"); + rows.add(builder.toString()); + + // Build page + var array = rows.toArray(new String[]{}); + + var page = new InventoryGui(plugin, GuiStrings.selectDisguise().toString(playerLocale), array); + + page.setCloseAction(close -> false); + + return page; + } + + private char getElementCharAt(int index) + { + return (char)(1000 + index); + } + + private void initElements() + { + // Fill disguise entries + var endIndex = Math.min(disguises.size(), getStartingIndex() + getElementCapacity() + 1); + for (int index = getStartingIndex(); index < endIndex; index++) + { + var meta = disguises.get(index); + var element = new StaticGuiElement(this.getElementCharAt(index), + IconLookup.instance().lookup(meta.rawIdentifier), + 1, + click -> + { + manager.morph(bindingPlayer, bindingPlayer, meta.rawIdentifier, bindingPlayer.getTargetEntity(5)); + gui.close(); + + return true; + }, + // They don't seem to support Components... Sad :( + "§r" + PlainTextComponentSerializer.plainText().serialize(meta.asComponent(playerLocale))); + + gui.addElement(element); + } + + // Fill controls + var borderElement = new StaticGuiElement('x', + new ItemStack(Material.PINK_STAINED_GLASS_PANE), + 1, + click -> true, + "§§"); + + gui.addElement(borderElement); + + var prevButton = new StaticGuiElement('P', + new ItemStack(Material.LIME_STAINED_GLASS_PANE), + 1, + click -> + { + schedulePrevPage(); + return true; + }, + "§r" + GuiStrings.prevPage().toString(playerLocale)); + + gui.addElement(prevButton); + + var nextButton = new StaticGuiElement('N', + new ItemStack(Material.LIGHT_BLUE_STAINED_GLASS_PANE), + 1, + click -> + { + scheduleNextPage(); + return true; + }, + "§r" + GuiStrings.nextPage().toString(playerLocale)); + + gui.addElement(nextButton); + + var unDisguiseButton = new StaticGuiElement('U', + new ItemStack(Material.RED_STAINED_GLASS_PANE), + 1, + click -> + { + manager.unMorph(bindingPlayer); + this.gui.close(); + return true; + }, + "§r" + GuiStrings.unDisguise().toString(playerLocale)); + + gui.addElement(unDisguiseButton); + } + + private boolean isLastPage() + { + return (this.getStartingIndex() + this.getElementCapacity()) > disguises.size(); + } + + @Nullable + private Runnable scheduledAction; + + private void scheduleNextPage() + { + if (isLastPage()) return; + + if (scheduledAction != null) return; + + scheduledAction = () -> + { + var next = new DisguiseSelectScreenWrapper(bindingPlayer, this.pageOffset + 1); + next.show(); + }; + + this.addSchedule(scheduledAction); + } + + private void schedulePrevPage() + { + if (this.pageOffset == 0) return; + + if (scheduledAction != null) return; + + scheduledAction = () -> + { + var next = new DisguiseSelectScreenWrapper(bindingPlayer, this.pageOffset - 1); + next.show(); + }; + + this.addSchedule(scheduledAction); + } +} diff --git a/src/main/java/xiamomc/morph/misc/gui/IconLookup.java b/src/main/java/xiamomc/morph/misc/gui/IconLookup.java new file mode 100644 index 00000000..db643624 --- /dev/null +++ b/src/main/java/xiamomc/morph/misc/gui/IconLookup.java @@ -0,0 +1,174 @@ +package xiamomc.morph.misc.gui; + +import com.mojang.authlib.properties.PropertyMap; +import net.minecraft.core.component.DataComponentMap; +import net.minecraft.core.component.DataComponents; +import net.minecraft.world.item.component.ResolvableProfile; +import org.bukkit.Material; +import org.bukkit.craftbukkit.inventory.CraftItemStack; +import org.bukkit.entity.EntityType; +import org.bukkit.inventory.ItemStack; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import xiamomc.morph.misc.DisguiseTypes; + +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; + +public class IconLookup +{ + private static IconLookup instance; + + public static IconLookup instance() + { + if (instance == null) instance = new IconLookup(); + + return instance; + } + + public IconLookup() + { + init(); + } + + // DisguiseIdentifier <-> IconItem + private final Map registry = new ConcurrentHashMap<>(); + + private final ItemStack defaultItem = new ItemStack(Material.BEDROCK); + + private void init() + { + for (EntityType value : EntityType.values()) + { + if (value == EntityType.PLAYER || value == EntityType.UNKNOWN) continue; + if (!value.isAlive()) continue; + + this.register(value); + } + + register(EntityType.ENDER_DRAGON, Material.DRAGON_HEAD); + register(EntityType.CREEPER, Material.CREEPER_HEAD); + register(EntityType.SKELETON, Material.SKELETON_SKULL); + register(EntityType.WITHER_SKELETON, Material.WITHER_SKELETON_SKULL); + register(EntityType.ZOMBIE, Material.ZOMBIE_HEAD); + + register(EntityType.PIGLIN, Material.PIGLIN_HEAD); + register(EntityType.PIGLIN_BRUTE, Material.GOLDEN_AXE); + + register(EntityType.ARMADILLO, Material.ARMADILLO_SCUTE); + register(EntityType.AXOLOTL, Material.AXOLOTL_BUCKET); + register(EntityType.ARMOR_STAND, Material.ARMOR_STAND); + register(EntityType.GIANT, Material.ZOMBIE_HEAD); + register(EntityType.ILLUSIONER, Material.SPECTRAL_ARROW); + register(EntityType.BLAZE, Material.BLAZE_ROD); + register(EntityType.BOGGED, Material.BROWN_MUSHROOM); + register(EntityType.BREEZE, Material.BREEZE_ROD); + register(EntityType.CAMEL, Material.CACTUS); + //register(EntityType.CAT, Material.STRING); + register(EntityType.CHICKEN, Material.FEATHER); + register(EntityType.COD, Material.COD); + register(EntityType.COW, Material.MILK_BUCKET); + register(EntityType.DROWNED, Material.TRIDENT); + register(EntityType.ENDERMAN, Material.ENDER_PEARL); + register(EntityType.FOX, Material.SWEET_BERRIES); + register(EntityType.GLOW_SQUID, Material.GLOW_INK_SAC); + register(EntityType.GUARDIAN, Material.PRISMARINE_CRYSTALS); + register(EntityType.HORSE, Material.SADDLE); + register(EntityType.HUSK, Material.SAND); + register(EntityType.IRON_GOLEM, Material.IRON_BLOCK); + register(EntityType.MAGMA_CUBE, Material.MAGMA_BLOCK); + register(EntityType.MOOSHROOM, Material.RED_MUSHROOM_BLOCK); + register(EntityType.PANDA, Material.BAMBOO); + register(EntityType.PHANTOM, Material.PHANTOM_MEMBRANE); + register(EntityType.PIG, Material.PORKCHOP); + register(EntityType.PILLAGER, Material.CROSSBOW); + //register(EntityType.POLAR_BEAR, Material.SNOWBALL); + register(EntityType.PUFFERFISH, Material.PUFFERFISH); + register(EntityType.RABBIT, Material.RABBIT_FOOT); + register(EntityType.SALMON, Material.SALMON); + register(EntityType.SHEEP, Material.WHITE_WOOL); + register(EntityType.SHULKER, Material.SHULKER_SHELL); + //register(EntityType.SKELETON_HORSE, Material.BONE); + register(EntityType.SLIME, Material.SLIME_BALL); + register(EntityType.SNIFFER, Material.SNIFFER_EGG); + register(EntityType.SNOW_GOLEM, Material.SNOWBALL); + register(EntityType.SPIDER, Material.SPIDER_EYE); + register(EntityType.SQUID, Material.INK_SAC); + register(EntityType.STRAY, Material.TIPPED_ARROW); + register(EntityType.STRIDER, Material.WARPED_FUNGUS_ON_A_STICK); + register(EntityType.TROPICAL_FISH, Material.TROPICAL_FISH_BUCKET); + register(EntityType.TURTLE, Material.TURTLE_EGG); + register(EntityType.WARDEN, Material.SCULK_CATALYST); + register(EntityType.WITCH, Material.SPLASH_POTION); + register(EntityType.WITHER, Material.WITHER_ROSE); + register(EntityType.WOLF, Material.BONE); + register(EntityType.ZOMBIFIED_PIGLIN, Material.GOLDEN_SWORD); + + register(EntityType.TADPOLE, Material.TADPOLE_BUCKET); + } + + private Material lookupEntitySpawnEgg(EntityType type) + { + var name = "%s_SPAWN_EGG".formatted(type.name().toUpperCase()); + + var match = Material.matchMaterial(name); + if (match == null) + { + //MorphPlugin.getInstance().getSLF4JLogger().warn("No spawn egg found for type " + type + "!"); + return defaultItem.getType(); + } + else + { + return match; + } + } + + private void register(EntityType type) + { + this.register(type, lookupEntitySpawnEgg(type)); + } + + private void register(EntityType type, Material material) + { + this.register(type.key().asString(), new ItemStack(material)); + } + + private void register(String disguiseIdentifier, ItemStack stack) + { + registry.put(disguiseIdentifier, stack); + } + + public ItemStack lookup(String disguiseIdentifier) + { + ItemStack item; + if (disguiseIdentifier.startsWith(DisguiseTypes.PLAYER.getNameSpace())) + item = lookupPlayer(DisguiseTypes.PLAYER.toStrippedId(disguiseIdentifier)); + else + item = this.registry.getOrDefault(disguiseIdentifier, defaultItem); + + return item.clone(); + } + + public ItemStack lookupPlayer(String playerName) + { + var stack = CraftItemStack.asCraftCopy(new ItemStack(Material.PLAYER_HEAD)); + var nmsHandle = stack.handle; + + /* + // No, this won't display the skin. + stack.editMeta(SkullMeta.class, skull -> + { + skull.setOwningPlayer(Bukkit.getOfflinePlayer(playerName)); + }); + */ + + // This displays the skin, but only under online mode + var profile = new ResolvableProfile(Optional.of(playerName), Optional.empty(), new PropertyMap()); + nmsHandle.applyComponents(DataComponentMap.builder() + .set(DataComponents.PROFILE, profile) + .build()); + + return stack; + } +} diff --git a/src/main/resources/assets/feathermorph/lang/en_us.json b/src/main/resources/assets/feathermorph/lang/en_us.json index 09c7957b..2886771f 100644 --- a/src/main/resources/assets/feathermorph/lang/en_us.json +++ b/src/main/resources/assets/feathermorph/lang/en_us.json @@ -223,5 +223,9 @@ "emote.morphclient.digdown": "Dig Down", "emote.morphclient.appear": "Appear", "emote.morphclient.not_available": "Can't play action while the skill is still in cooldown", - "emote.morphclient.unknown": "Unknown" + "emote.morphclient.unknown": "Unknown", + "chestui.next_page": "Next page", + "chestui.prev_page": "Last page", + "chestui.undisguise": "Undisguise", + "chestui.title_select_disguise": "Select disguise" } \ No newline at end of file diff --git a/src/main/resources/assets/feathermorph/lang/zh_cn.json b/src/main/resources/assets/feathermorph/lang/zh_cn.json index cd94c749..bbfbe1ef 100644 --- a/src/main/resources/assets/feathermorph/lang/zh_cn.json +++ b/src/main/resources/assets/feathermorph/lang/zh_cn.json @@ -223,5 +223,9 @@ "emote.morphclient.digdown": "掘地", "emote.morphclient.appear": "出现", "emote.morphclient.not_available": "技能冷却时不能播放动作", - "emote.morphclient.unknown": "未知" + "emote.morphclient.unknown": "未知", + "chestui.next_page": "下一页", + "chestui.prev_page": "上一页", + "chestui.undisguise": "取消伪装", + "chestui.title_select_disguise": "选择伪装" } \ No newline at end of file