diff --git a/build.gradle.kts b/build.gradle.kts index 96e2609e..26611af0 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -165,6 +165,7 @@ bukkit { permissionRoot + "lookup", permissionRoot + "skin_cache", permissionRoot + "switch_backend", + permissionRoot + "make_disguise_tool", permissionRoot + "mirror.immune", diff --git a/src/main/java/xiamomc/morph/MorphManager.java b/src/main/java/xiamomc/morph/MorphManager.java index 64918488..ed5c5a10 100644 --- a/src/main/java/xiamomc/morph/MorphManager.java +++ b/src/main/java/xiamomc/morph/MorphManager.java @@ -218,10 +218,10 @@ private void tryBackends() //endregion Backends - private Material actionItem; + @Deprecated(forRemoval = true) public Material getActionItem() { - return actionItem; + return Material.AIR; } @Resolved @@ -251,18 +251,6 @@ private void load() fallbackProvider )); - var actionItemId = config.getBindable(String.class, ConfigOption.SKILL_ITEM); - actionItemId.onValueChanged((o, n) -> - { - var item = Material.matchMaterial(n); - var disabled = "disabled"; - - if (item == null && !disabled.equals(n)) - logger.warn("Cannot find any item that matches \"" + n + "\" to set for the skill item, some related features may not work!"); - - actionItem = item; - }, true); - Bukkit.getPluginManager().callEvent(new ManagerFinishedInitializeEvent(this)); } @@ -1106,17 +1094,6 @@ private void afterDisguise(DisguiseBuildResult result, } else { - if (!playerOptions.shownServerSkillHint && actionItem != null) - { - var locale = MessageUtils.getLocale(player); - var skillHintMessage = HintStrings.skillString() - .withLocale(locale) - .resolve("item", vanillaMessageStore.get(actionItem.translationKey(), "???", locale)); - - player.sendMessage(MessageUtils.prefixes(player, skillHintMessage)); - playerOptions.shownServerSkillHint = true; - } - if (clientHandler.clientInitialized(player) && !playerOptions.shownDisplayToSelfHint) { player.sendMessage(MessageUtils.prefixes(player, HintStrings.morphVisibleAfterCommandString())); diff --git a/src/main/java/xiamomc/morph/MorphPlugin.java b/src/main/java/xiamomc/morph/MorphPlugin.java index 1ac1b36b..d2f6bb65 100644 --- a/src/main/java/xiamomc/morph/MorphPlugin.java +++ b/src/main/java/xiamomc/morph/MorphPlugin.java @@ -19,6 +19,7 @@ import xiamomc.morph.messages.vanilla.VanillaMessageStore; import xiamomc.morph.misc.NetworkingHelper; import xiamomc.morph.misc.PlayerOperationSimulator; +import xiamomc.morph.misc.recipe.RecipeManager; import xiamomc.morph.misc.disguiseProperty.DisguiseProperties; import xiamomc.morph.misc.gui.IconLookup; import xiamomc.morph.misc.integrations.modelengine.ModelEngineHelper; @@ -230,6 +231,8 @@ public void onEnable() dependencyManager.cache(DisguiseProperties.INSTANCE); + dependencyManager.cache(new RecipeManager()); + mirrorProcessor = new InteractionMirrorProcessor(); //注册EventProcessor diff --git a/src/main/java/xiamomc/morph/backends/server/renderer/network/PacketFactory.java b/src/main/java/xiamomc/morph/backends/server/renderer/network/PacketFactory.java index ddce6ba5..c3ce8e6d 100644 --- a/src/main/java/xiamomc/morph/backends/server/renderer/network/PacketFactory.java +++ b/src/main/java/xiamomc/morph/backends/server/renderer/network/PacketFactory.java @@ -56,6 +56,9 @@ public List buildSpawnPackets(DisplayParameters parameters) List packets = new ObjectArrayList<>(); + if (watcher.readEntryOrDefault(CustomEntries.VANISHED, false)) + return packets; + //logger.info("Build spawn packets, player is " + player.getName() + " :: parameters are " + parameters); var disguiseEntityType = watcher.getEntityType(); diff --git a/src/main/java/xiamomc/morph/backends/server/renderer/network/datawatcher/watchers/types/WardenWatcher.java b/src/main/java/xiamomc/morph/backends/server/renderer/network/datawatcher/watchers/types/WardenWatcher.java index 2cbf5c08..52e78ad6 100755 --- a/src/main/java/xiamomc/morph/backends/server/renderer/network/datawatcher/watchers/types/WardenWatcher.java +++ b/src/main/java/xiamomc/morph/backends/server/renderer/network/datawatcher/watchers/types/WardenWatcher.java @@ -17,6 +17,8 @@ import xiamomc.morph.misc.NmsRecord; import xiamomc.morph.misc.AnimationNames; +import java.util.concurrent.atomic.AtomicBoolean; + public class WardenWatcher extends EHasAttackAnimationWatcher { public WardenWatcher(Player bindingPlayer) @@ -24,8 +26,6 @@ public WardenWatcher(Player bindingPlayer) super(bindingPlayer, EntityType.WARDEN); } - private final Pose DIG_PLACEHOLDER_POSE = Pose.SLEEPING; - @Override protected void onEntryWrite(RegistryKey key, X oldVal, X newVal) { @@ -48,20 +48,20 @@ protected void onEntryWrite(RegistryKey key, X oldVal, X newVal) { case AnimationNames.ROAR -> { - if (this.read(ValueIndex.BASE_LIVING.POSE) == DIG_PLACEHOLDER_POSE) return; + if (this.readEntryOrDefault(CustomEntries.VANISHED, false)) return; this.block(ValueIndex.BASE_LIVING.POSE); this.writePersistent(ValueIndex.BASE_LIVING.POSE, Pose.ROARING); } case AnimationNames.ROAR_SOUND -> { - if (this.read(ValueIndex.BASE_LIVING.POSE) == DIG_PLACEHOLDER_POSE) return; + if (this.readEntryOrDefault(CustomEntries.VANISHED, false)) return; world.playSound(bindingPlayer.getLocation(), Sound.ENTITY_WARDEN_ROAR, SoundCategory.HOSTILE, 3, 1); } case AnimationNames.SNIFF -> { - if (this.read(ValueIndex.BASE_LIVING.POSE) == DIG_PLACEHOLDER_POSE) return; + if (this.readEntryOrDefault(CustomEntries.VANISHED, false)) return; this.block(ValueIndex.BASE_LIVING.POSE); this.writePersistent(ValueIndex.BASE_LIVING.POSE, Pose.SNIFFING); @@ -70,7 +70,7 @@ protected void onEntryWrite(RegistryKey key, X oldVal, X newVal) } case AnimationNames.DIGDOWN -> { - if (this.read(ValueIndex.BASE_LIVING.POSE) == DIG_PLACEHOLDER_POSE) return; + if (this.readEntryOrDefault(CustomEntries.VANISHED, false)) return; this.block(ValueIndex.BASE_LIVING.POSE); this.writePersistent(ValueIndex.BASE_LIVING.POSE, Pose.DIGGING); @@ -78,12 +78,13 @@ protected void onEntryWrite(RegistryKey key, X oldVal, X newVal) } case AnimationNames.VANISH -> { - this.writePersistent(ValueIndex.BASE_LIVING.POSE, DIG_PLACEHOLDER_POSE); this.writePersistent(ValueIndex.BASE_ENTITY.GENERAL, (byte)0x20); this.writePersistent(ValueIndex.BASE_LIVING.SILENT, true); + this.writeEntry(CustomEntries.VANISHED, true); } case AnimationNames.APPEAR -> { + this.writeEntry(CustomEntries.VANISHED, false); this.block(ValueIndex.BASE_LIVING.POSE); this.remove(ValueIndex.BASE_ENTITY.GENERAL); this.writePersistent(ValueIndex.BASE_LIVING.POSE, Pose.EMERGING); @@ -102,7 +103,9 @@ protected void onEntryWrite(RegistryKey key, X oldVal, X newVal) } case AnimationNames.TRY_RESET -> { - if (this.read(ValueIndex.BASE_LIVING.POSE) == DIG_PLACEHOLDER_POSE) return; + // 如果当前已消失,则不要调用重置 + // 因为重置会将一些动作数据重新同步为玩家的数据 + if (this.readEntryOrDefault(CustomEntries.VANISHED, false)) return; reset(); } diff --git a/src/main/java/xiamomc/morph/backends/server/renderer/network/registries/CustomEntries.java b/src/main/java/xiamomc/morph/backends/server/renderer/network/registries/CustomEntries.java index 9a9dcff2..747acd86 100644 --- a/src/main/java/xiamomc/morph/backends/server/renderer/network/registries/CustomEntries.java +++ b/src/main/java/xiamomc/morph/backends/server/renderer/network/registries/CustomEntries.java @@ -40,4 +40,6 @@ public class CustomEntries public static final RegistryKey SPAWN_UUID = RegistryKey.of("spawn_uuid", Util.NIL_UUID); public static final RegistryKey SPAWN_ID = RegistryKey.of("spawn_id", -1); + + public static final RegistryKey VANISHED = RegistryKey.of("vanished", false); } diff --git a/src/main/java/xiamomc/morph/commands/AnimationCommand.java b/src/main/java/xiamomc/morph/commands/AnimationCommand.java index c26d8c92..715df5d2 100644 --- a/src/main/java/xiamomc/morph/commands/AnimationCommand.java +++ b/src/main/java/xiamomc/morph/commands/AnimationCommand.java @@ -10,6 +10,7 @@ import xiamomc.morph.messages.EmoteStrings; import xiamomc.morph.messages.HelpStrings; import xiamomc.morph.messages.MessageUtils; +import xiamomc.morph.misc.gui.AnimSelectScreenWrapper; import xiamomc.pluginbase.Annotations.Resolved; import xiamomc.pluginbase.Command.IPluginCommand; import xiamomc.pluginbase.Messages.FormattableMessage; @@ -66,18 +67,21 @@ public boolean onCommand(@NotNull CommandSender commandSender, @NotNull Command return true; } + var animationSet = state.getProvider() + .getAnimationProvider() + .getAnimationSetFor(state.getDisguiseIdentifier()); + if (args.length == 0) { - player.sendMessage(MessageUtils.prefixes(player, CommandStrings.listNoEnoughArguments())); + var screen = new AnimSelectScreenWrapper(state, animationSet.getAvailableAnimationsForClient()); + screen.show(); + + //player.sendMessage(MessageUtils.prefixes(player, CommandStrings.listNoEnoughArguments())); return true; } var animationId = args[0]; - var animationSet = state.getProvider() - .getAnimationProvider() - .getAnimationSetFor(state.getDisguiseIdentifier()); - var animations = animationSet.getAvailableAnimationsForClient(); if (!animations.contains(animationId)) diff --git a/src/main/java/xiamomc/morph/commands/MorphPluginCommand.java b/src/main/java/xiamomc/morph/commands/MorphPluginCommand.java index 1acc0efd..92adccf3 100644 --- a/src/main/java/xiamomc/morph/commands/MorphPluginCommand.java +++ b/src/main/java/xiamomc/morph/commands/MorphPluginCommand.java @@ -48,7 +48,8 @@ public FormattableMessage getHelpMessage() new StatSubCommand(), new CheckUpdateSubCommand(), new LookupSubCommand(), - new SkinCacheSubCommand() + new SkinCacheSubCommand(), + new MakeSkillItemSubCommand() //new BackendSubCommand() ); diff --git a/src/main/java/xiamomc/morph/commands/subcommands/plugin/MakeSkillItemSubCommand.java b/src/main/java/xiamomc/morph/commands/subcommands/plugin/MakeSkillItemSubCommand.java new file mode 100644 index 00000000..a8e320f3 --- /dev/null +++ b/src/main/java/xiamomc/morph/commands/subcommands/plugin/MakeSkillItemSubCommand.java @@ -0,0 +1,68 @@ +package xiamomc.morph.commands.subcommands.plugin; + +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import xiamomc.morph.MorphPluginObject; +import xiamomc.morph.messages.CommandStrings; +import xiamomc.morph.messages.MessageUtils; +import xiamomc.morph.misc.permissions.CommonPermissions; +import xiamomc.morph.utilities.ItemUtils; +import xiamomc.pluginbase.Command.ISubCommand; +import xiamomc.pluginbase.Messages.FormattableMessage; + +import java.util.List; + +public class MakeSkillItemSubCommand extends MorphPluginObject implements ISubCommand +{ + @Override + public @NotNull String getCommandName() + { + return "make_disguise_tool"; + } + + @Override + public @Nullable String getPermissionRequirement() + { + return CommonPermissions.MAKE_DISGUISE_TOOL; + } + + @Override + public FormattableMessage getHelpMessage() + { + return new FormattableMessage(plugin, "make selected a disguise tool"); + } + + private final List emptyList = List.of(); + + @Override + public @Nullable List onTabComplete(List args, CommandSender source) + { + return emptyList; + } + + @Override + public boolean onCommand(@NotNull CommandSender sender, @NotNull String[] args) + { + if (!(sender instanceof Player player)) + { + sender.sendMessage(MessageUtils.prefixes(sender, CommandStrings.noPermissionMessage())); + return true; + } + + var item = player.getEquipment().getItemInMainHand(); + if (item.isEmpty() || item.getType().isAir()) + { + sender.sendMessage(MessageUtils.prefixes(sender, CommandStrings.illegalArgumentString().resolve("detail", "air... :("))); + return true; + } + + item = ItemUtils.buildSkillItemFrom(item); + player.getEquipment().setItemInMainHand(item); + + sender.sendMessage(MessageUtils.prefixes(sender, CommandStrings.success())); + + return true; + } +} diff --git a/src/main/java/xiamomc/morph/commands/subcommands/plugin/ReloadSubCommand.java b/src/main/java/xiamomc/morph/commands/subcommands/plugin/ReloadSubCommand.java index 16bd3443..5258df58 100644 --- a/src/main/java/xiamomc/morph/commands/subcommands/plugin/ReloadSubCommand.java +++ b/src/main/java/xiamomc/morph/commands/subcommands/plugin/ReloadSubCommand.java @@ -12,6 +12,7 @@ import xiamomc.morph.messages.MessageUtils; import xiamomc.morph.messages.MorphMessageStore; import xiamomc.morph.messages.vanilla.VanillaMessageStore; +import xiamomc.morph.misc.recipe.RecipeManager; import xiamomc.morph.misc.skins.PlayerSkinProvider; import xiamomc.morph.network.multiInstance.MultiInstanceService; import xiamomc.morph.network.server.MorphClientHandler; @@ -62,6 +63,9 @@ public FormattableMessage getHelpMessage() @Resolved private MultiInstanceService multiInstanceService; + @Resolved + private RecipeManager recipeManager; + private final List subcommands = ObjectImmutableList.of("data", "message", "update_message"); @Override @@ -103,6 +107,8 @@ public boolean onCommand(@NotNull CommandSender sender, @NotNull String[] args) PlayerSkinProvider.getInstance().reload(); multiInstanceService.onReload(); + + recipeManager.reload(); } if (reloadsMessage) diff --git a/src/main/java/xiamomc/morph/config/ConfigOption.java b/src/main/java/xiamomc/morph/config/ConfigOption.java index e8a341a5..43778af6 100644 --- a/src/main/java/xiamomc/morph/config/ConfigOption.java +++ b/src/main/java/xiamomc/morph/config/ConfigOption.java @@ -7,6 +7,7 @@ import xiamomc.pluginbase.Configuration.ConfigNode; import java.util.ArrayList; +import java.util.HashMap; public enum ConfigOption { @@ -19,16 +20,21 @@ public enum ConfigOption SKILL_COOLDOWN_ON_DAMAGE(ConfigNode.create().append("cooldown_on_damage"), 15), - @Deprecated + @Deprecated(forRemoval = true) ACTION_ITEM(ConfigNode.create().append("action_item"), "", true), - SKILL_ITEM(ConfigNode.create().append("skill_item"), "minecraft:feather"), + + @Deprecated(forRemoval = true, since = "1.3.0") + SKILL_ITEM(ConfigNode.create().append("skill_item"), "", true), + + //@Deprecated(forRemoval = true) + //SKILL_ITEM_USE_COMPONENT(ConfigNode.create().append("skill_item_use_component_detection"), true, true), ARMORSTAND_SHOW_ARMS(ConfigNode.create().append("armorstand_show_arms"), true), MODIFY_BOUNDING_BOX(boundingBoxNode().append("modify_boxes"), false), CHECK_AVAILABLE_SPACE(boundingBoxNode().append("check_space"), true), - @Deprecated + @Deprecated(forRemoval = true) MODIFY_BOUNDING_BOX_LEGACY(ConfigNode.create().append("modify_bounding_boxes"), false, true), UNMORPH_ON_DEATH(ConfigNode.create().append("unmorph_on_death"), true), @@ -70,7 +76,7 @@ public enum ConfigOption FLYABILITY_DISALLOW_FLY_IN_WATER(flyAbilityNode().append("disallow_in_water"), new ArrayList()), FLYABILITY_DISALLOW_FLY_IN_LAVA(flyAbilityNode().append("disallow_in_lava"), new ArrayList()), - @Deprecated(since = "1.2.2") + @Deprecated(since = "1.2.2", forRemoval = true) FLYABILITY_NO_LIQUID(flyAbilityNode().append("no_fly_in_liquid"), true, true), LANGUAGE_CODE(languageNode().append("code"), "en_us"), @@ -112,11 +118,14 @@ public enum ConfigOption GUI_PATTERN(ConfigNode.create().append("gui_pattern"), new ArrayList()), + //ANIM_SELECT_PATTERN(ConfigNode.create().append("anim_select_pattern"), new ArrayList()), + HIDE_DISGUISED_PLAYERS_IN_TAB(ConfigNode.create().append("hide_disguised_players_in_tab"), false), // SRR -> ServerRenderer SR_SHOW_PLAYER_DISGUISES_IN_TAB(serverRendererNode().append("show_player_disguises_in_tab"), false), + VERSION(ConfigNode.create().append("version"), 0); public final ConfigNode node; diff --git a/src/main/java/xiamomc/morph/config/MorphConfigManager.java b/src/main/java/xiamomc/morph/config/MorphConfigManager.java index a15f6719..ef08aecd 100644 --- a/src/main/java/xiamomc/morph/config/MorphConfigManager.java +++ b/src/main/java/xiamomc/morph/config/MorphConfigManager.java @@ -168,7 +168,7 @@ public void reload() }); //更新配置 - int targetVersion = 35; + int targetVersion = 37; var configVersion = getOrDefault(Integer.class, ConfigOption.VERSION); @@ -228,11 +228,17 @@ public void reload() if (configVersion < 15) { //skill item - //noinspection deprecation + //noinspection removal var oldSkillItem = get(String.class, ConfigOption.ACTION_ITEM); + //noinspection removal + this.remove(ConfigOption.ACTION_ITEM); + if (oldSkillItem != null) + { + //noinspection removal newConfig.set(ConfigOption.SKILL_ITEM.toString(), oldSkillItem); + } } // ChatOverride消息的配置从messages迁移到config.yml中 @@ -257,16 +263,24 @@ public void reload() if (configVersion < 23) { + //noinspection removal var val = get(Boolean.class, ConfigOption.MODIFY_BOUNDING_BOX_LEGACY); + //noinspection removal + this.remove(ConfigOption.MODIFY_BOUNDING_BOX_LEGACY); + if (val != null) newConfig.set(ConfigOption.MODIFY_BOUNDING_BOX.toString(), val); } if (configVersion < 34) { + //noinspection removal var noFlyInLiquid = getOrDefault(Boolean.class, ConfigOption.FLYABILITY_NO_LIQUID, null); + //noinspection removal + this.remove(ConfigOption.FLYABILITY_NO_LIQUID); + if (noFlyInLiquid != null && noFlyInLiquid) { var list = Bukkit.getWorlds().stream().map(WorldInfo::getName).toList(); @@ -276,6 +290,12 @@ public void reload() } } + if (configVersion < 37) + { + //noinspection removal + this.remove(ConfigOption.SKILL_ITEM); + } + newConfig.set(ConfigOption.VERSION.toString(), targetVersion); //todo: 将~UNSET作为留空的保留字符串写入PluginBase @@ -292,6 +312,12 @@ public void reload() } } + public void remove(ConfigOption option) + { + this.set(option, null); + this.backendConfig.set(option.node.toString(), null); + } + public T get(Class type, ConfigOption option) { return get(type, option.node); diff --git a/src/main/java/xiamomc/morph/events/CommonEventProcessor.java b/src/main/java/xiamomc/morph/events/CommonEventProcessor.java index 9fc83927..c592ad69 100644 --- a/src/main/java/xiamomc/morph/events/CommonEventProcessor.java +++ b/src/main/java/xiamomc/morph/events/CommonEventProcessor.java @@ -2,9 +2,8 @@ import com.destroystokyo.paper.event.player.PlayerClientOptionsChangeEvent; import com.destroystokyo.paper.event.player.PlayerPostRespawnEvent; +import de.themoep.inventorygui.InventoryGui; import it.unimi.dsi.fastutil.objects.ObjectArrayList; -import net.kyori.adventure.key.Key; -import net.kyori.adventure.sound.Sound; import org.bukkit.Bukkit; import org.bukkit.Material; import org.bukkit.attribute.Attribute; @@ -16,8 +15,7 @@ import org.bukkit.event.block.BlockBreakEvent; import org.bukkit.event.entity.*; import org.bukkit.event.player.*; -import org.bukkit.inventory.EquipmentSlot; -import org.bukkit.inventory.InventoryHolder; +import org.bukkit.inventory.*; import xiamomc.morph.MorphManager; import xiamomc.morph.MorphPluginObject; import xiamomc.morph.RevealingHandler; @@ -28,11 +26,11 @@ import xiamomc.morph.messages.HintStrings; import xiamomc.morph.messages.MessageUtils; import xiamomc.morph.messages.MorphStrings; -import xiamomc.morph.messages.SkillStrings; import xiamomc.morph.messages.vanilla.VanillaMessageStore; import xiamomc.morph.misc.DisguiseTypes; -import xiamomc.morph.misc.NetworkingHelper; import xiamomc.morph.misc.OfflineDisguiseResult; +import xiamomc.morph.misc.gui.AnimSelectScreenWrapper; +import xiamomc.morph.misc.gui.DisguiseSelectScreenWrapper; import xiamomc.morph.misc.playerList.PlayerListHandler; import xiamomc.morph.misc.permissions.CommonPermissions; import xiamomc.morph.network.commands.S2C.S2CSwapCommand; @@ -41,6 +39,7 @@ import xiamomc.morph.network.server.ServerSetEquipCommand; import xiamomc.morph.skills.MorphSkillHandler; import xiamomc.morph.utilities.EntityTypeUtils; +import xiamomc.morph.utilities.ItemUtils; import xiamomc.pluginbase.Annotations.Initializer; import xiamomc.pluginbase.Annotations.Resolved; import xiamomc.pluginbase.Bindables.Bindable; @@ -67,11 +66,35 @@ public class CommonEventProcessor extends MorphPluginObject implements Listener @Resolved(shouldSolveImmediately = true) private RevealingHandler revealingHandler; - @Resolved(shouldSolveImmediately = true) - private NetworkingHelper networkingHelper; - private Bindable unMorphOnDeath; + private final Bindable doRevealing = new Bindable<>(true); + + private final Bindable allowAcquireMorphs = new Bindable<>(false); + + @Initializer + private void load() + { + config.bind(cooldownOnDamage, ConfigOption.SKILL_COOLDOWN_ON_DAMAGE); + config.bind(bruteIgnoreDisguises, ConfigOption.PIGLIN_BRUTE_IGNORE_DISGUISES); + config.bind(doRevealing, ConfigOption.REVEALING); + config.bind(allowAcquireMorphs, ConfigOption.ALLOW_ACQUIRE_MORPHS); + + unMorphOnDeath = config.getBindable(Boolean.class, ConfigOption.UNMORPH_ON_DEATH); + this.addSchedule(this::update); + } + + private void update() + { + this.addSchedule(this::update); + + if (plugin.getCurrentTick() % 8 == 0) + { + playersMinedGoldBlocks.clear(); + susIncreasedPlayers.clear(); + } + } + @EventHandler public void onEntityDeath(EntityDeathEvent e) { @@ -142,33 +165,6 @@ public void onPlayerTookDamage(EntityDamageEvent e) } } - private final Bindable doRevealing = new Bindable<>(true); - - private final Bindable allowAcquireMorphs = new Bindable<>(false); - - @Initializer - private void load() - { - config.bind(cooldownOnDamage, ConfigOption.SKILL_COOLDOWN_ON_DAMAGE); - config.bind(bruteIgnoreDisguises, ConfigOption.PIGLIN_BRUTE_IGNORE_DISGUISES); - config.bind(doRevealing, ConfigOption.REVEALING); - config.bind(allowAcquireMorphs, ConfigOption.ALLOW_ACQUIRE_MORPHS); - - unMorphOnDeath = config.getBindable(Boolean.class, ConfigOption.UNMORPH_ON_DEATH); - this.addSchedule(this::update); - } - - private void update() - { - this.addSchedule(this::update); - - if (plugin.getCurrentTick() % 8 == 0) - { - playersMinedGoldBlocks.clear(); - susIncreasedPlayers.clear(); - } - } - @EventHandler public void onPlayerInteractAtEntity(PlayerInteractAtEntityEvent e) { @@ -197,7 +193,7 @@ else if (item.getType() != Material.AIR) //workaround: 右键盔甲架不会触发事件、盔甲架是InteractAtEntityEvent if (e.getRightClicked() instanceof ArmorStand) - e.setCancelled(tryInvokeSkillOrQuickDisguise(e.getPlayer(), Action.RIGHT_CLICK_AIR, e.getHand()) || e.isCancelled()); + e.setCancelled(invokeOrDisguise(e.getPlayer(), Action.RIGHT_CLICK_AIR, e.getHand()) || e.isCancelled()); } @EventHandler @@ -205,66 +201,97 @@ public void onPlayerInteractEntity(PlayerInteractEntityEvent e) { //workaround: 右键继承了InventoryHolder的实体会打开他们的物品栏而不是使用技能 if (e.getRightClicked() instanceof InventoryHolder && e.getRightClicked().getType() != EntityType.PLAYER) - e.setCancelled(tryInvokeSkillOrQuickDisguise(e.getPlayer(), Action.RIGHT_CLICK_AIR, e.getHand()) || e.isCancelled()); + e.setCancelled(invokeOrDisguise(e.getPlayer(), Action.RIGHT_CLICK_AIR, e.getHand()) || e.isCancelled()); } @EventHandler public void onPlayerInteract(PlayerInteractEvent e) { - if (tryInvokeSkillOrQuickDisguise(e.getPlayer(), e.getAction(), e.getHand())) + if (invokeOrDisguise(e.getPlayer(), e.getAction(), e.getHand())) e.setCancelled(true); } + @EventHandler + public void onEntityHurtEntity(EntityDamageByEntityEvent event) + { + if (event.getCause() != EntityDamageEvent.DamageCause.ENTITY_ATTACK) + return; + + if (event.getDamager() instanceof Player player + && invokeOrDisguise(player, Action.LEFT_CLICK_AIR, EquipmentSlot.HAND)) + { + event.setCancelled(true); + } + } + /** * 尝试使用技能或快速伪装 * @param player 目标玩家 * @param action 动作 * @return 是否应该取消Interact事件 */ - private boolean tryInvokeSkillOrQuickDisguise(Player player, Action action, EquipmentSlot slot) + private boolean invokeOrDisguise(Player player, Action action, EquipmentSlot slot) { - var actionItem = morphs.getActionItem(); - if (slot != EquipmentSlot.HAND || actionItem == null) return false; - - var state = morphs.getDisguiseStateFor(player); var mainHandItem = player.getEquipment().getItemInMainHand(); - var mainHandItemType = mainHandItem.getType(); + if (mainHandItem.getType().isAir()) + return false; - if (mainHandItemType.isAir() || !player.isSneaking()) return false; + var disguiseState = morphs.getDisguiseStateFor(player); - //右键玩家头颅:快速伪装 - if (!action.equals(Action.RIGHT_CLICK_BLOCK) && !action.isLeftClick() && morphs.doQuickDisguise(player, false)) - return true; + // 因为快速伪装功能包含了玩家头颅,所以我们没有在上面检查物品是否为技能触发物品。 + if (player.isSneaking()) + { + if (action.isRightClick()) // 下蹲+右键:快速伪装、打开伪装菜单 + { + // 不要妨碍别人放置头颅 + if (mainHandItem.getType() == Material.PLAYER_HEAD && action == Action.RIGHT_CLICK_BLOCK) + return false; - if (mainHandItemType != actionItem || state == null) return false; + if (!morphs.doQuickDisguise(player, true) + && ItemUtils.isSkillActivateItem(mainHandItem)) + { + if (InventoryGui.getOpen(player) == null) + { + var guiScreen = new DisguiseSelectScreenWrapper(player, 0); + guiScreen.show(); + } - //激活技能或取消伪装 - if (action.isLeftClick()) - { - if (player.getEyeLocation().getDirection().getY() <= -0.95) - morphs.unMorph(player); - else - morphs.setSelfDisguiseVisible(player, !state.isSelfViewing(), true); + return true; + } - return true; - } + return false; + } + else // 下蹲+左键:取消伪装 + { + if (!ItemUtils.isSkillActivateItem(mainHandItem) || disguiseState == null) + return false; - if (state.getSkillCooldown() <= 0) - { - morphs.executeDisguiseSkill(player); + morphs.unMorph(player); + } } else { - //一段时间内内只接受一次右键触发 - //传送前后会触发两次Interact,而且这两个Interact还不一定在同个Tick里 - if (plugin.getCurrentTick() - skillHandler.getLastInvoke(player) <= 1) - return true; + if (!ItemUtils.isSkillActivateItem(mainHandItem) || disguiseState == null) + return false; - player.sendMessage(MessageUtils.prefixes(player, - SkillStrings.skillPreparing().resolve("time", state.getSkillCooldown() / 20 + ""))); + if (action.isRightClick()) // 站立+右键:技能 + { + if (disguiseState.getSkillCooldown() < 0) + morphs.executeDisguiseSkill(player); + } + else // 站立+左键:伪装动作 + { + if (InventoryGui.getOpen(player) == null) + { + var availableAnimations = disguiseState.getProvider() + .getAnimationProvider() + .getAnimationSetFor(disguiseState.getDisguiseIdentifier()) + .getAvailableAnimationsForClient(); - player.playSound(Sound.sound(Key.key("minecraft", "entity.villager.no"), - Sound.Source.PLAYER, 1f, 1f)); + var guiScreen = new AnimSelectScreenWrapper(disguiseState, availableAnimations); + guiScreen.show(); + } + } } return true; diff --git a/src/main/java/xiamomc/morph/messages/CommandStrings.java b/src/main/java/xiamomc/morph/messages/CommandStrings.java index 75be14f1..d178608b 100644 --- a/src/main/java/xiamomc/morph/messages/CommandStrings.java +++ b/src/main/java/xiamomc/morph/messages/CommandStrings.java @@ -231,6 +231,16 @@ public static FormattableMessage goingToPlayAnimation() "即将播放动画 "); } + public static FormattableMessage success() + { + return getFormattable(getKey("operation_success"), "操作成功执行"); + } + + public static FormattableMessage grantItemSuccess() + { + return getFormattable(getKey("grant_item_success"), "[Fallback] 成功给与物品。如果没有请检查是否背包已满"); + } + private static String getKey(String key) { return "commands." + key; diff --git a/src/main/java/xiamomc/morph/messages/GuiStrings.java b/src/main/java/xiamomc/morph/messages/GuiStrings.java index 7e790db3..629078a2 100644 --- a/src/main/java/xiamomc/morph/messages/GuiStrings.java +++ b/src/main/java/xiamomc/morph/messages/GuiStrings.java @@ -24,6 +24,16 @@ public static FormattableMessage selectDisguise() return getFormattable(getKey("title_select_disguise"), "[Fallback] 选择伪装"); } + public static FormattableMessage selectAnimation() + { + return getFormattable(getKey("title_select_emotes"), "[Fallback] 伪装动作"); + } + + public static FormattableMessage close() + { + return getFormattable(getKey("close"), "[Fallback] 关闭"); + } + private static String getKey(String key) { return "chestui." + key; diff --git a/src/main/java/xiamomc/morph/misc/AnimationNames.java b/src/main/java/xiamomc/morph/misc/AnimationNames.java index 0d032571..1dc5a13f 100644 --- a/src/main/java/xiamomc/morph/misc/AnimationNames.java +++ b/src/main/java/xiamomc/morph/misc/AnimationNames.java @@ -46,7 +46,14 @@ public class AnimationNames public static final String CRAWL = "crawl"; + /** + * 无论如何都重置动作数据 + */ public static final String RESET = "reset"; + + /** + * 尝试重置动作数据,如果当前情况允许的话。 + */ public static final String TRY_RESET = "try_reset"; public static final String NONE = "none"; diff --git a/src/main/java/xiamomc/morph/misc/disguiseProperty/PropertyHandler.java b/src/main/java/xiamomc/morph/misc/disguiseProperty/PropertyHandler.java index 8e1fdf83..a2ea73f4 100644 --- a/src/main/java/xiamomc/morph/misc/disguiseProperty/PropertyHandler.java +++ b/src/main/java/xiamomc/morph/misc/disguiseProperty/PropertyHandler.java @@ -1,12 +1,14 @@ package xiamomc.morph.misc.disguiseProperty; import it.unimi.dsi.fastutil.objects.Object2ObjectArrayMap; +import it.unimi.dsi.fastutil.objects.ObjectArrayList; import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import xiamomc.morph.MorphPlugin; import xiamomc.morph.misc.disguiseProperty.values.AbstractProperties; +import java.util.List; import java.util.Map; import java.util.Random; import java.util.concurrent.ConcurrentHashMap; @@ -15,12 +17,19 @@ public class PropertyHandler { private final Map, Object> propertyMap = new ConcurrentHashMap<>(); + private final List> validProperties = new ObjectArrayList<>(); private final Random random = ThreadLocalRandom.current(); + @Nullable + private AbstractProperties properties; + public void setProperties(AbstractProperties properties) { reset(); + + this.properties = properties; + validProperties.addAll(properties.getValues()); properties.getValues().forEach(this::addProperty); } @@ -36,6 +45,8 @@ private void addProperty(SingleProperty property) public void reset() { + this.validProperties.clear(); + this.properties = null; propertyMap.clear(); } @@ -49,9 +60,9 @@ private void writeGeneric(SingleProperty property, Object value) public void set(SingleProperty property, X value) { - if (!propertyMap.containsKey(property)) + if (!validProperties.contains(property)) { - MorphPlugin.getInstance().getSLF4JLogger().warn("The given property '%s' doesn't exist.".formatted(property)); + MorphPlugin.getInstance().getSLF4JLogger().warn("The given property '%s' doesn't exist in '%s'".formatted(property.id(), this.properties)); return; } diff --git a/src/main/java/xiamomc/morph/misc/disguiseProperty/values/AbstractProperties.java b/src/main/java/xiamomc/morph/misc/disguiseProperty/values/AbstractProperties.java index b1e95cab..2996fc20 100644 --- a/src/main/java/xiamomc/morph/misc/disguiseProperty/values/AbstractProperties.java +++ b/src/main/java/xiamomc/morph/misc/disguiseProperty/values/AbstractProperties.java @@ -38,6 +38,6 @@ protected void registerSingle(SingleProperty value) public List> getValues() { - return values; + return new ObjectArrayList<>(values); } } diff --git a/src/main/java/xiamomc/morph/misc/gui/AnimSelectScreenWrapper.java b/src/main/java/xiamomc/morph/misc/gui/AnimSelectScreenWrapper.java new file mode 100644 index 00000000..7a28228b --- /dev/null +++ b/src/main/java/xiamomc/morph/misc/gui/AnimSelectScreenWrapper.java @@ -0,0 +1,187 @@ +package xiamomc.morph.misc.gui; + +import de.themoep.inventorygui.DynamicGuiElement; +import de.themoep.inventorygui.InventoryGui; +import de.themoep.inventorygui.StaticGuiElement; +import it.unimi.dsi.fastutil.objects.ObjectArrayList; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer; +import org.bukkit.Bukkit; +import org.bukkit.Material; +import org.bukkit.block.data.Levelled; +import org.bukkit.inventory.ItemRarity; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.BlockDataMeta; +import xiamomc.morph.messages.EmoteStrings; +import xiamomc.morph.messages.GuiStrings; +import xiamomc.morph.messages.MessageUtils; +import xiamomc.morph.messages.MorphStrings; +import xiamomc.morph.misc.DisguiseState; +import xiamomc.pluginbase.Bindables.BindableList; + +import java.util.List; + +public class AnimSelectScreenWrapper extends ScreenWrapper +{ + private final BindableList pattern = new BindableList<>(List.of( + "XXXXE" + )); + + private static final char CHAR_ENTRY = 'X'; + + private List getTemplate() + { + return pattern; + } + + private final DisguiseState state; + private final List availableSequences; + + public AnimSelectScreenWrapper(DisguiseState state, List availableSequences) + { + super(state.getPlayer()); + + this.state = state; + this.availableSequences = availableSequences; + + this.guiInstance = preparePage(); + this.initElements(this.guiInstance); + } + + @Override + public void show() + { + getBindingPlayer().playSound(openSound); + + super.show(); + } + + private int capacity = 0; + + private char getCurrentIndexChar(int index) + { + return (char) (10000 + index); + } + + private InventoryGui preparePage() + { + var template = this.getTemplate(); + + if (template.size() > 6) + { + capacity = 0; + logger.error("May not have a inventory with more than 6 rows."); + return new InventoryGui(plugin, "missingno", new String[]{" "}); + } + + List rows = new ObjectArrayList<>(); + + for (String line : template) + { + StringBuilder builder = new StringBuilder(); + + int lineCapacity = 0; + for (char c : line.toCharArray()) + { + switch (c) + { + case CHAR_ENTRY -> + { + builder.append(getCurrentIndexChar(this.capacity + lineCapacity)); + + lineCapacity++; + } + default -> + { + builder.append(c); + } + } + } + + rows.add(builder.toString()); + capacity += lineCapacity; + } + + return new InventoryGui(plugin, GuiStrings.selectAnimation().toString(playerLocale), rows.toArray(new String[]{})); + } + + private void initElements(InventoryGui gui) + { + var actionItemBase = IconLookup.instance().lookup(state.getDisguiseIdentifier()); //new ItemStack(Material.LIGHT); + + if (IconLookup.instance().lookup(state.getDisguiseIdentifier()).getType() == Material.PLAYER_HEAD) + this.isDynamic.set(true); + + for (int i = 0; i < Math.min(capacity, availableSequences.size()); i++) + { + var guiChar = this.getCurrentIndexChar(i); + var itemClone = actionItemBase.clone(); + int finalIndex = i; + itemClone.editMeta(meta -> + { + meta.setRarity(ItemRarity.COMMON); + + if (meta instanceof BlockDataMeta blockDataMeta) + { + var blockData = blockDataMeta.getBlockData(Material.LIGHT); + if (blockData instanceof Levelled levelled) levelled.setLevel(1 + finalIndex); + + blockDataMeta.setBlockData(blockData); + } + }); + + var sequenceId = availableSequences.get(i); + + var sequenceDisplayName = EmoteStrings.get(sequenceId).withLocale(playerLocale).toString(); + + var element = new StaticGuiElement(guiChar, itemClone, 1 + i, click -> + { + var animationSet = state.getProvider() + .getAnimationProvider() + .getAnimationSetFor(state.getDisguiseIdentifier()); + + var sequencePair = animationSet.sequenceOf(sequenceId); + + getBindingPlayer().playSound(clickSound); + state.tryScheduleSequence(sequenceId, sequencePair.left(), sequencePair.right()); + guiInstance.close(); + return true; + }, + "§r" + sequenceDisplayName); + + gui.addElement(element); + } + + gui.addElement(new StaticGuiElement('!', + new ItemStack(Material.PINK_STAINED_GLASS_PANE), + 1, + click -> true, + "§r")); + + var closeElementItem = new ItemStack(Material.BARRIER); + closeElementItem.editMeta(meta -> meta.setRarity(ItemRarity.COMMON)); + + gui.addElement(new StaticGuiElement('E', + closeElementItem, + 1, + click -> + { + getBindingPlayer().playSound(clickSound); + guiInstance.close(); + return true; + }, + "§r" + GuiStrings.close().toString(playerLocale))); + + var disguiseElement = new StaticGuiElement('D', + IconLookup.instance().lookup(state.getDisguiseIdentifier()), + 1, + click -> true, + "§r" + MorphStrings.disguisingAsString().resolve("what", state.getPlayerDisplay()) + .toString(playerLocale)); + + if (isDynamic.get()) + gui.addElement(new DynamicGuiElement('D', viewer -> disguiseElement)); + else + gui.addElement(disguiseElement); + } +} diff --git a/src/main/java/xiamomc/morph/misc/gui/DisguiseSelectScreenWrapper.java b/src/main/java/xiamomc/morph/misc/gui/DisguiseSelectScreenWrapper.java index 9a00c6e0..3cd9baa8 100644 --- a/src/main/java/xiamomc/morph/misc/gui/DisguiseSelectScreenWrapper.java +++ b/src/main/java/xiamomc/morph/misc/gui/DisguiseSelectScreenWrapper.java @@ -27,13 +27,8 @@ import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; -public class DisguiseSelectScreenWrapper extends MorphPluginObject +public class DisguiseSelectScreenWrapper extends ScreenWrapper { - @NotNull - private final InventoryGui gui; - - private final Player bindingPlayer; - @Nullable private final DisguiseState bindingState; @@ -41,8 +36,6 @@ public class DisguiseSelectScreenWrapper extends MorphPluginObject private final List disguises; - private final String playerLocale; - private final boolean playOpenSound; @Resolved(shouldSolveImmediately = true) @@ -58,29 +51,20 @@ public DisguiseSelectScreenWrapper(Player bindingPlayer, int pageOffset) protected DisguiseSelectScreenWrapper(Player bindingPlayer, int pageOffset, boolean playOpenSound) { + super(bindingPlayer); + this.disguises = manager.getAvaliableDisguisesFor(bindingPlayer); - this.bindingPlayer = bindingPlayer; this.pageOffset = pageOffset; - this.playerLocale = MessageUtils.getLocale(bindingPlayer); this.bindingState = manager.getDisguiseStateFor(bindingPlayer); this.playOpenSound = playOpenSound; this.template.clear(); this.template.addAll(config.getBindableList(String.class, ConfigOption.GUI_PATTERN)); - this.gui = this.preparePage(); + this.guiInstance = this.preparePage(); initElements(); } - /** - * 获取此GUI的行数 - * @return - */ - protected int getRowCount() - { - return getTemplate().size(); - } - /** * 获取此GUI的最大伪装显示物品承载量 * @return @@ -105,7 +89,7 @@ private void updateCapacity(List template) private int capacity = -1; - private List template = ObjectArrayList.of( + private final List template = ObjectArrayList.of( "CxDDDxPUN" ); @@ -123,33 +107,13 @@ private int getStartingIndex() return this.pageOffset * this.getElementCapacity(); } - private final AtomicBoolean havePlayerHead = new AtomicBoolean(false); - - private static final Sound openSound = Sound.sound().type(Key.key("entity.experience_orb.pickup")).volume(0.55f).build(); - + @Override public void show() { - if (playOpenSound) - bindingPlayer.playSound(openSound); - - this.gui.show(bindingPlayer); - - if (havePlayerHead.get()) - this.addSchedule(this::update); - } - - private void update() - { - if (plugin.getCurrentTick() % 10 != 0) - { - this.addSchedule(this::update); - return; - } - - if (this.gui.equals(InventoryGui.getOpen(bindingPlayer))) - this.addSchedule(this::update); + super.show(); - this.gui.draw(bindingPlayer); + if (playOpenSound) + getBindingPlayer().playSound(openSound); } private InventoryGui preparePage() @@ -224,10 +188,10 @@ private char getElementCharAt(int index) return (char)(1000 + index); } - private static final Sound clickSound = Sound.sound().type(Key.key("ui.button.click")).volume(0.45f).build(); - private void initElements() { + var bindingPlayer = getBindingPlayer(); + // Fill disguise entries var endIndex = Math.min(disguises.size(), getStartingIndex() + getElementCapacity() + 1); for (int index = getStartingIndex(); index < endIndex; index++) @@ -242,7 +206,7 @@ private void initElements() { bindingPlayer.playSound(clickSound); manager.morph(bindingPlayer, bindingPlayer, meta.rawIdentifier, bindingPlayer.getTargetEntity(5)); - gui.close(); + guiInstance.close(); return true; }, @@ -251,12 +215,12 @@ private void initElements() if (meta.isPlayerDisguise()) { - this.havePlayerHead.set(true); - gui.addElement(new DynamicGuiElement(this.getElementCharAt(index), viewer -> staticElement)); + this.isDynamic.set(true); + guiInstance.addElement(new DynamicGuiElement(this.getElementCharAt(index), viewer -> staticElement)); } else { - gui.addElement(staticElement); + guiInstance.addElement(staticElement); } } @@ -267,7 +231,7 @@ private void initElements() click -> true, "§§"); - gui.addElement(borderElement); + guiInstance.addElement(borderElement); var borderGrayElement = new StaticGuiElement('t', new ItemStack(Material.BLACK_STAINED_GLASS_PANE), @@ -275,7 +239,7 @@ private void initElements() click -> true, "§§"); - gui.addElement(borderGrayElement); + guiInstance.addElement(borderGrayElement); var prevButton = new StaticGuiElement('P', new ItemStack(Material.LIME_STAINED_GLASS_PANE), @@ -288,7 +252,7 @@ private void initElements() }, "§r" + GuiStrings.prevPage().toString(playerLocale)); - gui.addElement(prevButton); + guiInstance.addElement(prevButton); var nextButton = new StaticGuiElement('N', new ItemStack(Material.LIGHT_BLUE_STAINED_GLASS_PANE), @@ -301,7 +265,7 @@ private void initElements() }, "§r" + GuiStrings.nextPage().toString(playerLocale)); - gui.addElement(nextButton); + guiInstance.addElement(nextButton); var unDisguiseButton = new StaticGuiElement('U', new ItemStack(Material.RED_STAINED_GLASS_PANE), @@ -310,12 +274,12 @@ private void initElements() { bindingPlayer.playSound(clickSound); manager.unMorph(bindingPlayer); - this.gui.close(); + this.guiInstance.close(); return true; }, "§r" + GuiStrings.unDisguise().toString(playerLocale)); - gui.addElement(unDisguiseButton); + guiInstance.addElement(unDisguiseButton); if (bindingState != null) { @@ -328,7 +292,7 @@ private void initElements() click -> true, name); - gui.addElement(currentDisguiseButton); + guiInstance.addElement(currentDisguiseButton); } } @@ -348,7 +312,7 @@ private void scheduleNextPage() scheduledAction = () -> { - var next = new DisguiseSelectScreenWrapper(bindingPlayer, this.pageOffset + 1, false); + var next = new DisguiseSelectScreenWrapper(getBindingPlayer(), this.pageOffset + 1, false); next.show(); }; @@ -363,7 +327,7 @@ private void schedulePrevPage() scheduledAction = () -> { - var next = new DisguiseSelectScreenWrapper(bindingPlayer, this.pageOffset - 1, false); + var next = new DisguiseSelectScreenWrapper(getBindingPlayer(), this.pageOffset - 1, false); next.show(); }; diff --git a/src/main/java/xiamomc/morph/misc/gui/ScreenWrapper.java b/src/main/java/xiamomc/morph/misc/gui/ScreenWrapper.java new file mode 100644 index 00000000..1f0d5645 --- /dev/null +++ b/src/main/java/xiamomc/morph/misc/gui/ScreenWrapper.java @@ -0,0 +1,86 @@ +package xiamomc.morph.misc.gui; + +import de.themoep.inventorygui.InventoryGui; +import net.kyori.adventure.key.Key; +import net.kyori.adventure.sound.Sound; +import org.bukkit.entity.Player; +import xiamomc.morph.MorphPluginObject; +import xiamomc.morph.messages.MessageUtils; +import xiamomc.pluginbase.Bindables.Bindable; +import xiamomc.pluginbase.Exceptions.NullDependencyException; +import xiamomc.pluginbase.ScheduleInfo; + +public class ScreenWrapper extends MorphPluginObject +{ + protected Bindable isDynamic = new Bindable<>(false); + + private final Player bindingPlayer; + + protected final String playerLocale; + + public static final Sound clickSound = Sound.sound().type(Key.key("ui.button.click")).volume(0.45f).build(); + public static final Sound openSound = Sound.sound().type(Key.key("entity.experience_orb.pickup")).volume(0.55f).build(); + + protected Player getBindingPlayer() + { + return bindingPlayer; + } + + protected InventoryGui guiInstance; + + private ScheduleInfo updateScheduleInfo; + + public ScreenWrapper(Player bindingPlayer) + { + this.bindingPlayer = bindingPlayer; + this.playerLocale = MessageUtils.getLocale(bindingPlayer); + + isDynamic.onValueChanged((o, n) -> + { + if (n && isCurrent()) updateScheduleInfo = this.addSchedule(this::update); + else if (this.updateScheduleInfo != null) this.updateScheduleInfo.cancel(); + }); + } + + protected boolean isCurrent() + { + if (guiInstance == null) return false; + + return guiInstance.equals(InventoryGui.getOpen(bindingPlayer)); + } + + protected void onUpdate() + { + } + + private void update() + { + if (plugin.getCurrentTick() % 10 != 0 && isDynamic.get()) + { + this.addSchedule(this::update); + return; + } + + if (!isCurrent()) return; + + this.onUpdate(); + + this.addSchedule(this::update); + + this.guiInstance.draw(bindingPlayer); + } + + public void show() + { + if (this.guiInstance == null) + { + logger.error("Attempting to show a null GUI to the player! No continuing..."); + return; + } + + this.guiInstance.show(bindingPlayer); + + if (isDynamic.get()) + updateScheduleInfo = this.addSchedule(this::update); + } +} diff --git a/src/main/java/xiamomc/morph/misc/permissions/CommonPermissions.java b/src/main/java/xiamomc/morph/misc/permissions/CommonPermissions.java index 2a22f7fe..80417a77 100644 --- a/src/main/java/xiamomc/morph/misc/permissions/CommonPermissions.java +++ b/src/main/java/xiamomc/morph/misc/permissions/CommonPermissions.java @@ -30,6 +30,8 @@ public class CommonPermissions public final static String ACCESS_SKIN_CACHE = PERM_ROOT + "skin_cache"; + public final static String MAKE_DISGUISE_TOOL = PERM_ROOT + "make_disguise_tool"; + public final static String SET_BACKEND = PERM_ROOT + "switch_backend"; public final static String SET_OPTIONS = PERM_ROOT + "toggle"; diff --git a/src/main/java/xiamomc/morph/misc/playerList/PlayerListHandler.java b/src/main/java/xiamomc/morph/misc/playerList/PlayerListHandler.java index e6f1e82e..60fb66e8 100644 --- a/src/main/java/xiamomc/morph/misc/playerList/PlayerListHandler.java +++ b/src/main/java/xiamomc/morph/misc/playerList/PlayerListHandler.java @@ -148,7 +148,11 @@ public void handle(Player player) if (isPlayerHiddenFromOthers) { var hidePacket = new ClientboundPlayerInfoRemovePacket(List.of(player.getUniqueId())); - Bukkit.getOnlinePlayers().forEach(p -> this.sendPacket(p, hidePacket)); + Bukkit.getOnlinePlayers().forEach(p -> + { + if (p != player) + this.sendPacket(p, hidePacket); + }); } this.fakePlayers.forEach((disguiseUUID, profile) -> diff --git a/src/main/java/xiamomc/morph/misc/recipe/RecipeManager.java b/src/main/java/xiamomc/morph/misc/recipe/RecipeManager.java new file mode 100644 index 00000000..dd65aa93 --- /dev/null +++ b/src/main/java/xiamomc/morph/misc/recipe/RecipeManager.java @@ -0,0 +1,182 @@ +package xiamomc.morph.misc.recipe; + +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.objects.ObjectArrayList; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.minimessage.MiniMessage; +import org.bukkit.Bukkit; +import org.bukkit.Material; +import org.bukkit.NamespacedKey; +import org.bukkit.inventory.*; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import xiamomc.morph.MorphPluginObject; +import xiamomc.morph.config.MorphConfigManager; +import xiamomc.morph.utilities.ItemUtils; +import xiamomc.pluginbase.Annotations.Initializer; + +import java.io.*; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +public class RecipeManager extends MorphPluginObject +{ + private final StandaloneYamlConfigManager configManager = new RecipeYamlConfigManager(new File(plugin.getDataFolder(), "recipes.yml"), "recipes.yml"); + + private boolean allowCrafting = false; + private boolean unShaped = false; + private List shape = new ObjectArrayList<>(); + private Map materials = new Object2ObjectOpenHashMap<>(); + private String resultMaterialId = "~UNSET"; + private String resultName = "~UNSET"; + private List resultLore = new ObjectArrayList<>(); + + @Initializer + private void load(MorphConfigManager configManager) + { + reload(); + } + + public void reload() + { + this.configManager.reload(); + + readValuesFromConfig(this.configManager); + prepareRecipe(); + } + + private void readValuesFromConfig(StandaloneYamlConfigManager configManager) + { + allowCrafting = configManager.getOrDefault(RecipeOptions.ALLOW_DISGUISE_TOOL_CRAFTING); + unShaped = configManager.getOrDefault(RecipeOptions.DISGUISE_TOOL_CRAFTING_UNSHAPED); + shape = configManager.getList(RecipeOptions.DISGUISE_TOOL_CRAFTING_SHAPE); + resultMaterialId = configManager.getOrDefault(RecipeOptions.DISGUISE_TOOL_RESULT_MATERIAL); + resultName = configManager.getOrDefault(RecipeOptions.DISGUISE_TOOL_RESULT_NAME); + resultLore = configManager.getList(RecipeOptions.DISGUISE_TOOL_RESULT_LORE); + var material = configManager.getMap(RecipeOptions.DISGUISE_TOOL_CRAFTING_MATERIALS); + this.materials.clear(); + + if (material != null) + this.materials.putAll(material); + } + + @Nullable + private Material getMaterialFrom(String str) + { + var key = NamespacedKey.fromString(str); + + return Arrays.stream(Material.values()).parallel().filter(m -> m.key().equals(key)) + .findFirst().orElse(null); + } + + @NotNull + public static final NamespacedKey SKILLITEM_CRAFTING_KEY = NamespacedKey.fromString("feathermorph:disguise_tool_crafting"); + + private void prepareRecipe() + { + if (!allowCrafting) + { + Bukkit.removeRecipe(SKILLITEM_CRAFTING_KEY); + return; + } + + var minimessage = MiniMessage.miniMessage(); + + Component name = this.resultName.equals("~UNSET") ? null : minimessage.deserialize(this.resultName); + List loreComponents = this.resultLore.isEmpty() ? null : this.resultLore.parallelStream().map(minimessage::deserialize).toList(); + + var resultMaterial = this.getMaterialFrom(this.resultMaterialId); + if (resultMaterial == null) + { + logger.error("Invalid result material ID: '%s', skipping...".formatted(resultMaterialId)); + return; + } + + Map materialsReal = new Object2ObjectOpenHashMap<>(); + this.materials.forEach((str, id) -> + { + var material = Arrays.stream(Material.values()) + .filter(m -> m.key().equals(NamespacedKey.fromString(id))) + .findFirst() + .orElse(null); + + if (material == null) + { + logger.warn("Invalid material '%s', skipping...".formatted(id)); + return; + } + + materialsReal.put(str, material); + }); + + var recipeProperty = new RecipeProperty(SKILLITEM_CRAFTING_KEY, + !this.unShaped, this.shape, + materialsReal, + resultMaterial, + name, + loreComponents); + + buildAndAddRecipe(recipeProperty); + } + + private void buildAndAddRecipe(RecipeProperty recipeProperty) + { + var resultItem = ItemUtils.buildSkillItemFrom(ItemStack.of(recipeProperty.resultMaterial())); + resultItem.editMeta(meta -> + { + meta.setRarity(ItemRarity.UNCOMMON); + meta.setEnchantmentGlintOverride(true); + + var name = recipeProperty.resultName(); + if (name != null) + meta.itemName(name); + + var lore = recipeProperty.lore(); + if (lore != null && !lore.isEmpty()) + meta.lore(lore); + }); + + var key = recipeProperty.key(); + + CraftingRecipe recipe; + + if (recipeProperty.shaped()) + { + var shaped = new ShapedRecipe(key, resultItem); + shaped.shape(recipeProperty.shape().toArray(new String[]{})); + recipeProperty.materials().forEach((ch, material) -> shaped.setIngredient(ch.charAt(0), material)); + + recipe = shaped; + } + else + { + var shapeless = new ShapelessRecipe(key, resultItem); + recipeProperty.materials().forEach((ignored, material) -> shapeless.addIngredient(material)); + + recipe = shapeless; + } + + Bukkit.removeRecipe(key); + Bukkit.addRecipe(recipe, true); + } + + private void test_dumpExsampleConfig() + { + allowCrafting = true; + unShaped = true; + shape = List.of( + "ABC", + "DEF", + "GHI" + ); + resultMaterialId = Material.BEDROCK.key().asString(); + resultName = "技能物品"; + resultLore = List.of( + "技能测试1", "技能测试2" + ); + materials = new Object2ObjectOpenHashMap<>(); + materials.put("A", Material.BEDROCK.key().asString()); + materials.put("B", Material.ACACIA_BOAT.key().asString()); + } +} diff --git a/src/main/java/xiamomc/morph/misc/recipe/RecipeOptions.java b/src/main/java/xiamomc/morph/misc/recipe/RecipeOptions.java new file mode 100644 index 00000000..bfaa553d --- /dev/null +++ b/src/main/java/xiamomc/morph/misc/recipe/RecipeOptions.java @@ -0,0 +1,29 @@ +package xiamomc.morph.misc.recipe; + +import xiamomc.pluginbase.Configuration.ConfigNode; +import xiamomc.pluginbase.Configuration.ConfigOption; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class RecipeOptions +{ + public static final ConfigOption ALLOW_DISGUISE_TOOL_CRAFTING = new ConfigOption<>(skillItemNode().append("enabled"), true); + public static final ConfigOption DISGUISE_TOOL_CRAFTING_UNSHAPED = new ConfigOption<>(skillItemNode().append("shapeless"), true); + public static final ConfigOption> DISGUISE_TOOL_CRAFTING_SHAPE = new ConfigOption<>(skillItemNode().append("crafting_shape"), new ArrayList<>()); + public static final ConfigOption> DISGUISE_TOOL_CRAFTING_MATERIALS = new ConfigOption<>(skillItemNode().append("crafting_materials"), new HashMap<>()); + public static final ConfigOption DISGUISE_TOOL_RESULT_MATERIAL = new ConfigOption<>(skillItemNode().append("result_material"), "minecraft:feather"); + public static final ConfigOption DISGUISE_TOOL_RESULT_NAME = new ConfigOption<>(skillItemNode().append("result_item_name"), "~UNSET"); + public static final ConfigOption> DISGUISE_TOOL_RESULT_LORE = new ConfigOption<>(skillItemNode().append("result_item_lore"), new ArrayList<>()); + + private static ConfigNode craftingNode() + { + return ConfigNode.create().append("item_crafting"); + } + private static ConfigNode skillItemNode() + { + return craftingNode().append("disguise_tool"); + } +} diff --git a/src/main/java/xiamomc/morph/misc/recipe/RecipeProperty.java b/src/main/java/xiamomc/morph/misc/recipe/RecipeProperty.java new file mode 100644 index 00000000..a0a70196 --- /dev/null +++ b/src/main/java/xiamomc/morph/misc/recipe/RecipeProperty.java @@ -0,0 +1,19 @@ +package xiamomc.morph.misc.recipe; + +import net.kyori.adventure.text.Component; +import org.bukkit.Material; +import org.bukkit.NamespacedKey; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.List; +import java.util.Map; + +public record RecipeProperty(NamespacedKey key, + boolean shaped, List shape, + Map materials, + @NotNull Material resultMaterial, + @Nullable Component resultName, + @Nullable List lore) +{ +} diff --git a/src/main/java/xiamomc/morph/misc/recipe/RecipeYamlConfigManager.java b/src/main/java/xiamomc/morph/misc/recipe/RecipeYamlConfigManager.java new file mode 100644 index 00000000..c7daba54 --- /dev/null +++ b/src/main/java/xiamomc/morph/misc/recipe/RecipeYamlConfigManager.java @@ -0,0 +1,75 @@ +package xiamomc.morph.misc.recipe; + +import com.google.common.base.Charsets; +import it.unimi.dsi.fastutil.objects.ObjectArrayList; +import org.jetbrains.annotations.Nullable; +import xiamomc.morph.utilities.PluginAssetUtils; +import xiamomc.pluginbase.Configuration.ConfigOption; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.OutputStreamWriter; +import java.util.List; + +public class RecipeYamlConfigManager extends StandaloneYamlConfigManager +{ + public RecipeYamlConfigManager(File file, @Nullable String internalResourceName) + { + super(file, internalResourceName); + } + + /** + * Copy internal resource to the location + * + * @return Whether this operation was successful + */ + @Override + protected boolean copyInternalResource() + { + try + { + if (!configFile.createNewFile()) + return false; + + try (var writer = new OutputStreamWriter(new FileOutputStream(configFile), Charsets.UTF_8)) + { + writer.write(PluginAssetUtils.getFileStrings("recipes.yml")); + } + catch (Throwable t) + { + logger.error("Can't write content: " + t.getMessage()); + return false; + } + + return true; + } + catch (Throwable t) + { + logger.error("Can't create config file: " + t.getMessage()); + } + + return true; + } + + @Override + protected int getExpectedConfigVersion() + { + return 1; + } + + private final List> options = new ObjectArrayList<>(List.of( + RecipeOptions.DISGUISE_TOOL_CRAFTING_SHAPE, + RecipeOptions.DISGUISE_TOOL_RESULT_LORE, + RecipeOptions.DISGUISE_TOOL_RESULT_NAME, + RecipeOptions.ALLOW_DISGUISE_TOOL_CRAFTING, + RecipeOptions.DISGUISE_TOOL_CRAFTING_MATERIALS, + RecipeOptions.DISGUISE_TOOL_CRAFTING_UNSHAPED, + RecipeOptions.DISGUISE_TOOL_RESULT_MATERIAL + )); + + @Override + protected List> getAllOptions() + { + return options; + } +} diff --git a/src/main/java/xiamomc/morph/misc/recipe/StandaloneYamlConfigManager.java b/src/main/java/xiamomc/morph/misc/recipe/StandaloneYamlConfigManager.java new file mode 100644 index 00000000..d66b69f5 --- /dev/null +++ b/src/main/java/xiamomc/morph/misc/recipe/StandaloneYamlConfigManager.java @@ -0,0 +1,192 @@ +package xiamomc.morph.misc.recipe; + +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import org.bukkit.configuration.file.YamlConfiguration; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import xiamomc.morph.MorphPluginObject; +import xiamomc.pluginbase.Configuration.ConfigNode; +import xiamomc.pluginbase.Configuration.ConfigOption; + +import java.io.File; +import java.util.*; + +public abstract class StandaloneYamlConfigManager extends MorphPluginObject +{ + protected YamlConfiguration backendConfiguration; + + @NotNull + protected final File configFile; + + @Nullable + private final String internalResourceName; + + public static final ConfigOption CONFIG_VERSION = new ConfigOption<>(ConfigNode.create().append("version"), 0); + + public StandaloneYamlConfigManager(@NotNull File file, @Nullable String internalResourceName) + { + this.configFile = file; + this.internalResourceName = internalResourceName; + } + + /** + * Copy internal resource to the location + * @return Whether this operation was successful + */ + protected abstract boolean copyInternalResource(); + + protected abstract int getExpectedConfigVersion(); + + public void reload() + { + var newConfig = new YamlConfiguration(); + + if (!configFile.exists()) + { + if (!copyInternalResource()) + { + logger.error("Can't create file to save configuration! Not reloading recipes..."); + return; + } + } + + try + { + newConfig.load(configFile); + } + catch (Throwable e) + { + logger.error("Unable to load recipe configuration: " + e.getMessage()); + return; + } + + if (this.backendConfiguration == null) + this.backendConfiguration = newConfig; + + var configVersion = newConfig.getInt(CONFIG_VERSION.toString(), 0); + if (configVersion < this.getExpectedConfigVersion()) + this.migrate(this.backendConfiguration, newConfig); + + this.backendConfiguration = newConfig; + } + + @NotNull + protected Map getAllNotDefault(Collection> options) + { + var map = new Object2ObjectOpenHashMap(); + + for (var o : options) + { + Object val; + + if (o.getDefault() instanceof List) + val = getList(o); + else if (o.getDefault() instanceof Map) + val = getMap(o); + else + val = getOrDefault((ConfigOption) o, o.getDefault()); + + if (!o.getDefault().equals(val)) map.put(o.node(), val); + } + + return map; + } + + protected abstract List> getAllOptions(); + + private void migrate(@Nullable YamlConfiguration currentConfig, YamlConfiguration newConfig) + { + var allNotDefault = this.getAllNotDefault(this.getAllOptions()); + + if (internalResourceName != null) + plugin.saveResource(internalResourceName, true); + + this.onMigrate(currentConfig, newConfig, allNotDefault); + + allNotDefault.forEach((node, val) -> + { + var matching = this.getAllOptions().stream().filter(option -> option.node().equals(node)) + .findFirst().orElse(null); + + if (matching == null) + return; + + newConfig.set(node.toString(), val); + }); + } + + protected void onMigrate(@Nullable YamlConfiguration currentConfig, YamlConfiguration newConfig, Map nonDefaultValues) + { + } + + /** + * @return NULL if not found + */ + public T get(ConfigOption option) + { + return getOrDefault(option, null); + } + + @NotNull + public List getList(ConfigOption option) + { + var node = option.toString(); + return backendConfiguration.getStringList(node); + } + + /** + * @return NULL if the given node doesn't exist in the configuration + */ + @Nullable + public Map getMap(ConfigOption option) + { + var node = option.toString(); + + var configSection = backendConfiguration.getConfigurationSection(node); + + if (configSection == null) + return null; + + Map map = new Object2ObjectOpenHashMap<>(); + + configSection.getKeys(false).forEach(key -> map.put(key, configSection.getString(key))); + + return map; + } + + public T getOrDefault(ConfigOption option) + { + return getOrDefault(option, option.getDefault()); + } + + /** + * @apiNote List classes will ALWAYS return ArrayList + */ + public T getOrDefault(ConfigOption option, T defaultVal) + { + var node = option.toString(); + + Object backendResult = backendConfiguration.get(node, defaultVal); + if (backendResult == null && defaultVal == null) return null; + + var optionDefault = option.getDefault(); + + // 对列表单独处理 + if (List.class.isAssignableFrom(optionDefault.getClass())) + { + throw new IllegalArgumentException("Use getList() instead."); + } + else if (Map.class.isAssignableFrom(optionDefault.getClass())) + { + throw new IllegalArgumentException("Use getMap() instead."); + } + else + { + // ConfigOption#getDefault is always NotNull + if (optionDefault.getClass().isAssignableFrom(backendResult.getClass())) + return (T) backendResult; + else + return defaultVal; + } + } +} diff --git a/src/main/java/xiamomc/morph/misc/skins/PlayerSkinProvider.java b/src/main/java/xiamomc/morph/misc/skins/PlayerSkinProvider.java index f96c811b..6a503299 100644 --- a/src/main/java/xiamomc/morph/misc/skins/PlayerSkinProvider.java +++ b/src/main/java/xiamomc/morph/misc/skins/PlayerSkinProvider.java @@ -20,7 +20,6 @@ import java.util.Optional; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; public class PlayerSkinProvider extends MorphPluginObject @@ -38,40 +37,22 @@ public static PlayerSkinProvider getInstance() return instance; } - private CompletableFuture> getProfileAsyncV2(String name) - { - var executor = Util.PROFILE_EXECUTOR; - - return CompletableFuture.supplyAsync(() -> this.fetchProfileV2(name), executor) - .whenCompleteAsync((optional, throwable) -> {}, executor); - } - private final SkinCache skinCache = new SkinCache(); - public static boolean isValidUsername(String name) { - if (name != null && !name.isEmpty() && name.length() <= 16) { - int i = 0; - - for(int len = name.length(); i < len; ++i) { - char c = name.charAt(i); - if ((c < 'a' || c > 'z') && (c < 'A' || c > 'Z') && (c < '0' || c > '9') && c != '_' && c != '.') { - return false; - } - } + private CompletableFuture> fetchPlayerInfoAsync(String name) + { + var executor = Util.PROFILE_EXECUTOR; - return true; - } else { - return false; - } + return CompletableFuture.supplyAsync(() -> this.fetchPlayerInfo(name), executor); } /** * 根据给定的名称搜索对应的Profile(不包含皮肤) - * @apiNote 此方法返回的GameProfile不包含皮肤,若要获取于此对应的皮肤,请使用 {@link PlayerSkinProvider#fetchSkinFromProfile(GameProfile)} + * @apiNote 此方法返回的GameProfile不包含皮肤,若要获取于此对应的皮肤,请使用 {@link PlayerSkinProvider#fetchSkin(GameProfile)} * @param name * @return */ - private Optional fetchProfileV2(String name) + private Optional fetchPlayerInfo(String name) { var profileRef = new AtomicReference(null); @@ -106,11 +87,25 @@ else if (exception instanceof AuthenticationUnavailableException) return profile == null ? Optional.empty() : Optional.of(profile); } + @Nullable + public GameProfile getCachedProfile(String name) + { + return skinCache.get(name).profileOptional().orElse(null); + } + + public void cacheProfile(@NotNull PlayerProfile playerProfile) + { + var gameProfile = new MorphGameProfile(playerProfile); + skinCache.cache(gameProfile); + } + + private final Map>> onGoingRequests = new ConcurrentHashMap<>(); + /** * 通过给定的Profile获取与其对应的皮肤 * @param profile 目标GameProfile */ - public CompletableFuture> fetchSkinFromProfile(GameProfile profile) + public CompletableFuture> fetchSkin(GameProfile profile) { if (profile.getProperties().containsKey("textures")) { @@ -136,20 +131,6 @@ public CompletableFuture> fetchSkinFromProfile(GameProfile } } - @Nullable - public GameProfile getCachedProfile(String name) - { - return skinCache.get(name).profileOptional().orElse(null); - } - - public void cacheProfile(@NotNull PlayerProfile playerProfile) - { - var gameProfile = new MorphGameProfile(playerProfile); - skinCache.cache(gameProfile); - } - - private final Map>> onGoingRequests = new ConcurrentHashMap<>(); - /** * 尝试获取与给定名称对应的皮肤 * @param profileName 目标名称 @@ -178,12 +159,12 @@ public CompletableFuture> fetchSkin(String profileName) if (prevReq != null) return prevReq; - var req = getProfileAsyncV2(profileName) + var req = fetchPlayerInfoAsync(profileName) .thenCompose(rawProfileOptional -> { if (rawProfileOptional.isPresent()) //如果查有此人,那么继续流程 { - return fetchSkinFromProfile(rawProfileOptional.get()); + return fetchSkin(rawProfileOptional.get()); } else if (cachedSkin.profileOptional().isPresent()) //否则,如果本地有缓存,那就使用本地缓存 { @@ -199,6 +180,11 @@ else if (cachedSkin.profileOptional().isPresent()) //否则,如果本地有缓 } }); + req.exceptionally(t -> + { + onGoingRequests.remove(profileName); + return Optional.empty(); + }); req.thenRun(() -> onGoingRequests.remove(profileName)); onGoingRequests.put(profileName, req); diff --git a/src/main/java/xiamomc/morph/storage/playerdata/PlayerMeta.java b/src/main/java/xiamomc/morph/storage/playerdata/PlayerMeta.java index 11e538d2..83324405 100644 --- a/src/main/java/xiamomc/morph/storage/playerdata/PlayerMeta.java +++ b/src/main/java/xiamomc/morph/storage/playerdata/PlayerMeta.java @@ -100,6 +100,7 @@ public void setUnlockedDisguiseIdentifiers(ObjectArrayList newList) public boolean shownDisplayToSelfHint = false; @Expose + @Deprecated(forRemoval = true) public boolean shownServerSkillHint; @Expose diff --git a/src/main/java/xiamomc/morph/utilities/ItemUtils.java b/src/main/java/xiamomc/morph/utilities/ItemUtils.java index 396dfaa1..cc8688e1 100644 --- a/src/main/java/xiamomc/morph/utilities/ItemUtils.java +++ b/src/main/java/xiamomc/morph/utilities/ItemUtils.java @@ -3,12 +3,15 @@ import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.mojang.serialization.JsonOps; +import net.minecraft.core.component.DataComponents; +import net.minecraft.world.item.component.CustomData; import org.bukkit.Bukkit; import org.bukkit.Material; import org.bukkit.craftbukkit.CraftWorld; import org.bukkit.craftbukkit.inventory.CraftItemStack; import org.bukkit.inventory.ItemStack; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import xiamomc.morph.MorphPlugin; public class ItemUtils @@ -73,6 +76,30 @@ public static String itemToStr(ItemStack stack) } } + public static final String SKILL_ACTIVATE_ITEM_KEY = "feathermorph:is_disguise_tool"; + + public static ItemStack buildSkillItemFrom(ItemStack stack) + { + var nms = net.minecraft.world.item.ItemStack.fromBukkitCopy(stack); + var customData = nms.getComponents().get(DataComponents.CUSTOM_DATA); + if (customData == null) customData = CustomData.EMPTY; + + customData = customData.update(tag -> tag.putBoolean(SKILL_ACTIVATE_ITEM_KEY, true)); + nms.set(DataComponents.CUSTOM_DATA, customData); + + return nms.asBukkitMirror(); + } + + public static boolean isSkillActivateItem(ItemStack stack) + { + var nms = net.minecraft.world.item.ItemStack.fromBukkitCopy(stack); + var customData = nms.getComponents().get(DataComponents.CUSTOM_DATA); + + if (customData == null || !customData.contains(SKILL_ACTIVATE_ITEM_KEY)) return false; + + return customData.copyTag().getBoolean(SKILL_ACTIVATE_ITEM_KEY); + } + /** * Check if the given {@link Material} is a continuous usable type (have consuming animation). * @param type {@link Material} diff --git a/src/main/resources/assets/feathermorph/lang/en_us.json b/src/main/resources/assets/feathermorph/lang/en_us.json index ee657a6a..03b4b86b 100644 --- a/src/main/resources/assets/feathermorph/lang/en_us.json +++ b/src/main/resources/assets/feathermorph/lang/en_us.json @@ -86,6 +86,8 @@ "commands.not_disguised": "You're not disguised!", "commands.no_such_animation": "No such animation", "commands.going_to_play_animation": "Going to play animation ", + "commands.grant_item_success": "Successfully grant item. If the item is not found please check if the inventory is full.", + "commands.operation_success": "Operation success", "common.command_not_found": "Command not found", "common.player_not_defined": "No player specified", "common.player_not_found": "No such player or they are offline", @@ -225,5 +227,7 @@ "chestui.next_page": "Next page", "chestui.prev_page": "Previous page", "chestui.undisguise": "Undisguise", - "chestui.title_select_disguise": "Select disguise" + "chestui.title_select_disguise": "Select disguise", + "chestui.title_select_emotes": "Disguise actions", + "chestui.close": "Close" } \ 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 a61ea2b4..0f0f8d6a 100644 --- a/src/main/resources/assets/feathermorph/lang/zh_cn.json +++ b/src/main/resources/assets/feathermorph/lang/zh_cn.json @@ -86,6 +86,8 @@ "commands.not_disguised": "你没有进行伪装!", "commands.no_such_animation": "此动画不可用", "commands.going_to_play_animation": "即将播放动作 ", + "commands.grant_item_success": "成功给与物品。如果没有请检查是否背包已满", + "commands.operation_success": "操作成功执行", "common.command_not_found" : "未找到此指令", "common.player_not_defined" : "未指定玩家", "common.player_not_found" : "未找到目标玩家或对方已离线", @@ -225,5 +227,7 @@ "chestui.next_page": "下一页", "chestui.prev_page": "上一页", "chestui.undisguise": "取消伪装", - "chestui.title_select_disguise": "选择伪装" + "chestui.title_select_disguise": "选择伪装", + "chestui.title_select_emotes": "伪装动作", + "chestui.close": "关闭" } \ No newline at end of file diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index 68fde1fe..a398fe39 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -97,10 +97,6 @@ root: # Should we check if there is enough space to disguise? check_space: true - # Skill item - # Defines which item should players use to disguise/undisguise or activate skills. - skill_item: minecraft:feather - # Should we make Armor Stand disguises show arms by default? armorstand_show_arms: true @@ -347,4 +343,4 @@ root: - SelectedItem # Do not touch unless you know what you're doing! - version: 35 + version: 37 diff --git a/src/main/resources/recipes.yml b/src/main/resources/recipes.yml new file mode 100644 index 00000000..bcb086f3 --- /dev/null +++ b/src/main/resources/recipes.yml @@ -0,0 +1,41 @@ +root: + # Item crafting options + item_crafting: + # Options for the Disguise Tool + disguise_tool: + enabled: true + + # Should this recipe shapeless? + shapeless: true + + # The crafting shape for this recipe + # Doesn't work if shapeless is enabled + crafting_shape: + - ABC + - DEF + - GHI + + # The material of the crafted result + result_material: minecraft:feather + + # The name for the item + # Use "~UNSET" to keep the material's original name + # Supports MiniMessage + result_item_name: ~UNSET + + # The lore for the item + # + # Example: + # result_item_lore: + # - Lore1 + # - Supports MiniMessage! + result_item_lore: [] + + # Materials required to craft this item. + # Left side is the letter present in crafting_shape, and right side is the item ID + crafting_materials: + A: minecraft:feather + B: minecraft:redstone + + # Don't touch unless you know what you're doing! + version: 1