diff --git a/main/src/main/java/net/citizensnpcs/Citizens.java b/main/src/main/java/net/citizensnpcs/Citizens.java index 67827672f..498353cbf 100644 --- a/main/src/main/java/net/citizensnpcs/Citizens.java +++ b/main/src/main/java/net/citizensnpcs/Citizens.java @@ -467,7 +467,6 @@ public class Citizens extends JavaPlugin implements CitizensPlugin { shops.loadFromDisk(); shops.load(); - getServer().getPluginManager().callEvent(new CitizensReloadEvent()); } diff --git a/main/src/main/java/net/citizensnpcs/commands/NPCCommands.java b/main/src/main/java/net/citizensnpcs/commands/NPCCommands.java index 02e0cf1c7..927c92e32 100644 --- a/main/src/main/java/net/citizensnpcs/commands/NPCCommands.java +++ b/main/src/main/java/net/citizensnpcs/commands/NPCCommands.java @@ -1286,7 +1286,7 @@ public class NPCCommands { @Command( aliases = { "npc" }, - usage = "hologram add [text] | set [line #] [text] | remove [line #] | bgcolor [line #] (red,green,blue(,alpha)) | clear | lineheight [height] | viewrange [range] | margintop [line #] [margin] | marginbottom [line #] [margin]", + usage = "hologram add [text] | insert [line #] [text] | set [line #] [text] | remove [line #] | bgcolor [line #] (red,green,blue(,alpha)) | clear | lineheight [height] | viewrange [range] | margintop [line #] [margin] | marginbottom [line #] [margin]", desc = "", modifiers = { "hologram" }, min = 1, @@ -1295,8 +1295,8 @@ public class NPCCommands { public void hologram(CommandContext args, CommandSender sender, NPC npc, @Arg( value = 1, - completions = { "add", "set", "bgcolor", "remove", "clear", "lineheight", "viewrange", "margintop", - "marginbottom" }) String action, + completions = { "add", "insert", "set", "bgcolor", "remove", "clear", "lineheight", "viewrange", + "margintop", "marginbottom" }) String action, @Arg(value = 2, completionsProvider = HologramTrait.TabCompletions.class) String secondCompletion) throws CommandException { HologramTrait trait = npc.getOrAddTrait(HologramTrait.class); @@ -1314,7 +1314,8 @@ public class NPCCommands { if (args.argsLength() == 2) throw new CommandException(Messages.HOLOGRAM_INVALID_LINE); - int idx = Math.max(0, args.getInteger(2)); + int idx = args.getString(2).equals("bottom") ? 0 + : args.getString(2).equals("top") ? trait.getLines().size() - 1 : Math.max(0, args.getInteger(2)); if (idx >= trait.getLines().size()) throw new CommandException(Messages.HOLOGRAM_INVALID_LINE); @@ -1328,7 +1329,9 @@ public class NPCCommands { trait.setDefaultBackgroundColor(Util.parseColor(args.getString(2))); Messaging.sendTr(sender, Messages.HOLOGRAM_DEFAULT_BACKGROUND_COLOR_SET, args.getString(2)); } else { - int idx = Math.max(0, args.getInteger(2)); + int idx = args.getString(2).equals("bottom") ? 0 + : args.getString(2).equals("top") ? trait.getLines().size() - 1 + : Math.max(0, args.getInteger(2)); if (idx >= trait.getLines().size()) throw new CommandException(Messages.HOLOGRAM_INVALID_LINE); trait.setBackgroundColor(idx, Util.parseColor(args.getString(3))); @@ -1346,11 +1349,26 @@ public class NPCCommands { trait.addLine(args.getJoinedStrings(2)); Messaging.sendTr(sender, Messages.HOLOGRAM_LINE_ADD, args.getJoinedStrings(2)); + } else if (action.equalsIgnoreCase("insert")) { + if (args.argsLength() == 2) + throw new CommandException(Messages.HOLOGRAM_INVALID_LINE); + + if (args.argsLength() == 3) + throw new CommandException(Messages.HOLOGRAM_TEXT_MISSING); + + int idx = args.getString(2).equals("bottom") ? 0 + : args.getString(2).equals("top") ? trait.getLines().size() - 1 : Math.max(0, args.getInteger(2)); + if (idx > trait.getLines().size()) + throw new CommandException(Messages.HOLOGRAM_INVALID_LINE); + + trait.insertLine(idx, args.getJoinedStrings(3)); + Messaging.sendTr(sender, Messages.HOLOGRAM_LINE_ADD, args.getJoinedStrings(3)); } else if (action.equalsIgnoreCase("remove")) { if (args.argsLength() == 2) throw new CommandException(Messages.HOLOGRAM_INVALID_LINE); - int idx = Math.max(0, args.getInteger(2)); + int idx = args.getString(2).equals("bottom") ? 0 + : args.getString(2).equals("top") ? trait.getLines().size() - 1 : Math.max(0, args.getInteger(2)); if (idx >= trait.getLines().size()) throw new CommandException(Messages.HOLOGRAM_INVALID_LINE); @@ -1369,7 +1387,8 @@ public class NPCCommands { if (args.argsLength() == 2) throw new CommandException(Messages.HOLOGRAM_INVALID_LINE); - int idx = Math.max(0, args.getInteger(2)); + int idx = args.getString(2).equals("bottom") ? 0 + : args.getString(2).equals("top") ? trait.getLines().size() - 1 : Math.max(0, args.getInteger(2)); if (idx >= trait.getLines().size()) throw new CommandException(Messages.HOLOGRAM_INVALID_LINE); @@ -1382,7 +1401,8 @@ public class NPCCommands { if (args.argsLength() == 2) throw new CommandException(Messages.HOLOGRAM_INVALID_LINE); - int idx = Math.max(0, args.getInteger(2)); + int idx = args.getString(2).equals("bottom") ? 0 + : args.getString(2).equals("top") ? trait.getLines().size() - 1 : Math.max(0, args.getInteger(2)); if (idx >= trait.getLines().size()) throw new CommandException(Messages.HOLOGRAM_INVALID_LINE); @@ -2725,7 +2745,7 @@ public class NPCCommands { @Command( aliases = { "npc" }, - usage = "rotate (--towards [x,y,z]) (--body [yaw]) (--head [yaw]) (--pitch [pitch]) (-s(mooth))", + usage = "rotate (--towards [x,y,z]) (--toentity [name|uuid|me]) (--body [yaw]) (--head [yaw]) (--pitch [pitch]) (-s(mooth))", desc = "", flags = "s", modifiers = { "rotate" }, @@ -2733,7 +2753,8 @@ public class NPCCommands { max = 1, permission = "citizens.npc.rotate") public void rotate(CommandContext args, CommandSender sender, NPC npc, @Flag("body") Float yaw, - @Flag("head") Float head, @Flag("pitch") Float pitch, @Flag("towards") Location towards) { + @Flag("head") Float head, @Flag("pitch") Float pitch, @Flag("towards") Location towards, + @Flag("toentity") String entity) throws CommandException { if (args.hasFlag('s')) { if (pitch == null) { pitch = npc.getStoredLocation().getPitch(); @@ -2748,6 +2769,18 @@ public class NPCCommands { npc.getOrAddTrait(RotationTrait.class).getPhysicalSession().rotateToHave(yaw, pitch); return; } + if (entity != null) { + if (entity.equals("me")) { + towards = args.getSenderLocation(); + } else { + try { + UUID uuid = UUID.fromString(entity); + towards = Bukkit.getPlayer(uuid).getLocation(); + } catch (IllegalArgumentException ex) { + towards = Bukkit.getPlayerExact(entity).getLocation(); + } + } + } if (towards != null) { npc.getOrAddTrait(RotationTrait.class).getPhysicalSession().rotateToFace(towards); return; diff --git a/main/src/main/java/net/citizensnpcs/trait/HologramTrait.java b/main/src/main/java/net/citizensnpcs/trait/HologramTrait.java index e72616293..ac6e094b0 100644 --- a/main/src/main/java/net/citizensnpcs/trait/HologramTrait.java +++ b/main/src/main/java/net/citizensnpcs/trait/HologramTrait.java @@ -220,6 +220,11 @@ public class HologramTrait extends Trait { return viewRange; } + public void insertLine(int idx, String text) { + lines.add(idx, new HologramLine(text, true, -1, createDefaultHologramRenderer())); + reloadLineHolograms(); + } + @Override public void load(DataKey root) { clear(); diff --git a/main/src/main/java/net/citizensnpcs/trait/text/Text.java b/main/src/main/java/net/citizensnpcs/trait/text/Text.java index 29b62bcac..3a67b7437 100644 --- a/main/src/main/java/net/citizensnpcs/trait/text/Text.java +++ b/main/src/main/java/net/citizensnpcs/trait/text/Text.java @@ -49,6 +49,8 @@ public class Text extends Trait implements Runnable, Listener { private double range = Setting.DEFAULT_TALK_CLOSE_RANGE.asDouble(); @Persist(value = "realistic-looking") private boolean realisticLooker = Setting.DEFAULT_REALISTIC_LOOKING.asBoolean(); + @Persist(value = "send-text-to-chat") + private boolean sendTextToChat = true; @Persist(value = "speech-bubble-duration") private int speechBubbleDuration = Setting.DEFAULT_TEXT_SPEECH_BUBBLE_DURATION.asTicks(); @Persist(value = "speech-bubbles") @@ -56,7 +58,7 @@ public class Text extends Trait implements Runnable, Listener { @Persist(value = "talk-close") private boolean talkClose = Setting.DEFAULT_TALK_CLOSE.asBoolean(); @Persist - private volatile List text = new ArrayList<>(); + private final List text = new ArrayList<>(); public Text() { super("text"); @@ -211,10 +213,16 @@ public class Text extends Trait implements Runnable, Listener { HologramTrait trait = npc.getOrAddTrait(HologramTrait.class); trait.addTemporaryLine(Placeholders.replace(text.get(index), player, npc), speechBubbleDuration); } - npc.getDefaultSpeechController().speak(new SpeechContext(text.get(index), player)); + if (sendTextToChat) { + npc.getDefaultSpeechController().speak(new SpeechContext(text.get(index), player)); + } return true; } + public boolean sendTextToChat() { + return sendTextToChat; + } + /** * Set the text delay between messages. * @@ -288,6 +296,13 @@ public class Text extends Trait implements Runnable, Listener { return realisticLooker = !realisticLooker; } + /** + * Toggles sending text through chat + */ + public boolean toggleSendTextToChat() { + return sendTextToChat = !sendTextToChat; + } + /** * Toggles using speech bubbles instead of messages. */ diff --git a/main/src/main/java/net/citizensnpcs/trait/text/TextBasePrompt.java b/main/src/main/java/net/citizensnpcs/trait/text/TextBasePrompt.java index 9204a8df6..646f5a35a 100644 --- a/main/src/main/java/net/citizensnpcs/trait/text/TextBasePrompt.java +++ b/main/src/main/java/net/citizensnpcs/trait/text/TextBasePrompt.java @@ -74,6 +74,8 @@ public class TextBasePrompt extends StringPrompt { } } else if (input.equalsIgnoreCase("random")) { text.toggleRandomTalker(); + } else if (original.trim().equalsIgnoreCase("send text to chat")) { + text.toggleSendTextToChat(); } else if (original.trim().equalsIgnoreCase("realistic looking")) { text.toggleRealisticLooking(); } else if (original.trim().equalsIgnoreCase("speech bubbles")) { @@ -121,7 +123,7 @@ public class TextBasePrompt extends StringPrompt { Messaging.send((Player) context.getForWhom(), Messaging.tr(Messages.TEXT_EDITOR_START_PROMPT, colorToggleableText(text.shouldTalkClose()), colorToggleableText(text.isRandomTalker()), colorToggleableText(text.useSpeechBubbles()), - colorToggleableText(text.useRealisticLooking()))); + colorToggleableText(text.useRealisticLooking()), colorToggleableText(text.sendTextToChat()))); int page = context.getSessionData("page") == null ? 1 : (int) context.getSessionData("page"); text.sendPage((Player) context.getForWhom(), page); return ""; diff --git a/main/src/main/resources/en.json b/main/src/main/resources/en.json index 978a95680..d3f0743d4 100644 --- a/main/src/main/resources/en.json +++ b/main/src/main/resources/en.json @@ -709,7 +709,7 @@ "citizens.editors.text.range-set" : "[[Range]] set to [[{0}]].", "citizens.editors.text.realistic-looking-set" : "[[Realistic looking]] set to [[{0}]].", "citizens.editors.text.speech-bubbles-set" : "[[Speech bubbles]] set to [[{0}]].", - "citizens.editors.text.start-prompt" : "Add text | default to clear)\">item | range | delay
{0}talk close | {1}random | {2}speech bubble duration | {2}speech bubbles | {3}realistic", + "citizens.editors.text.start-prompt" : "Add text | default to clear)\">item | range | delay
{0}talk close | {1}random | {2}speech bubble duration | {2}speech bubbles | {3}realistic | {4}send to chat", "citizens.editors.text.talk-item-set" : "[[Talk item pattern]] set to [[{0}]].", "citizens.editors.text.text-list-header" : "Current text:", "citizens.editors.waypoints.guided.added-available" : "Added a [[destination]] waypoint which the NPC will randomly pathfind between.", diff --git a/v1_20_R4/src/main/java/net/citizensnpcs/nms/v1_20_R4/util/NMSImpl.java b/v1_20_R4/src/main/java/net/citizensnpcs/nms/v1_20_R4/util/NMSImpl.java index 55fe7b8a4..8a1b3adbd 100644 --- a/v1_20_R4/src/main/java/net/citizensnpcs/nms/v1_20_R4/util/NMSImpl.java +++ b/v1_20_R4/src/main/java/net/citizensnpcs/nms/v1_20_R4/util/NMSImpl.java @@ -84,6 +84,7 @@ import net.citizensnpcs.api.trait.TraitInfo; import net.citizensnpcs.api.util.BoundingBox; import net.citizensnpcs.api.util.EntityDim; import net.citizensnpcs.api.util.Messaging; +import net.citizensnpcs.api.util.SpigotUtil; import net.citizensnpcs.api.util.SpigotUtil.InventoryViewAPI; import net.citizensnpcs.nms.v1_20_R4.entity.AllayController; import net.citizensnpcs.nms.v1_20_R4.entity.ArmadilloController; @@ -260,6 +261,14 @@ import net.citizensnpcs.util.Util; import net.minecraft.core.BlockPos; import net.minecraft.core.Holder; import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.nbt.ByteArrayTag; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.IntArrayTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.LongArrayTag; +import net.minecraft.nbt.NumericTag; +import net.minecraft.nbt.StringTag; +import net.minecraft.nbt.Tag; import net.minecraft.network.Connection; import net.minecraft.network.chat.Component; import net.minecraft.network.chat.MutableComponent; @@ -621,6 +630,35 @@ public class NMSImpl implements NMSBridge { return shape.isEmpty() ? BoundingBox.EMPTY : NMSBoundingBox.wrap(shape.bounds()); } + @Override + public Map getComponentMap(org.bukkit.inventory.ItemStack item) { + if (META_COMPOUND_TAG == null) { + try { + META_COMPOUND_TAG = NMS.getGetter(Class.forName( + "org.bukkit.craftbukkit." + SpigotUtil.getMinecraftPackage() + ".inventory.CraftMetaItem"), + "customTag"); + } catch (ClassNotFoundException e) { + e.printStackTrace(); + } + } + Map base = Maps.newHashMap(NMSBridge.super.getComponentMap(item)); + CompoundTag ct; + try { + ct = (CompoundTag) META_COMPOUND_TAG.invoke(item.getItemMeta()); + } catch (Throwable e) { + e.printStackTrace(); + return base; + } + if (ct == null) + return base; + Map custom = Maps.newHashMap(); + for (String key : ct.getAllKeys()) { + custom.put(key, deserialiseNBT(ct.get(key))); + } + base.put("custom", custom); + return base; + } + @Override public Location getDestination(org.bukkit.entity.Entity entity) { Entity handle = getHandle(entity); @@ -2085,6 +2123,42 @@ public class NMSImpl implements NMSBridge { } } + private static Object deserialiseNBT(Tag tag) { + switch (tag.getId()) { + case Tag.TAG_COMPOUND: + CompoundTag ct = (CompoundTag) tag; + Map map = Maps.newHashMapWithExpectedSize(ct.size()); + for (String key : ct.getAllKeys()) { + map.put(key, deserialiseNBT(ct.get(key))); + } + return map; + case Tag.TAG_LIST: + ListTag list = (ListTag) tag; + List res = Lists.newArrayList(list.size()); + for (int i = 0; i < list.size(); i++) { + res.add(deserialiseNBT(list.get(i))); + } + return res; + case Tag.TAG_BYTE_ARRAY: + return ((ByteArrayTag) tag).getAsByteArray(); + case Tag.TAG_INT_ARRAY: + return ((IntArrayTag) tag).getAsIntArray(); + case Tag.TAG_LONG_ARRAY: + return ((LongArrayTag) tag).getAsLongArray(); + case Tag.TAG_STRING: + return ((StringTag) tag).getAsString(); + case Tag.TAG_ANY_NUMERIC: + case Tag.TAG_LONG: + case Tag.TAG_FLOAT: + case Tag.TAG_DOUBLE: + case Tag.TAG_INT: + case Tag.TAG_BYTE: + case Tag.TAG_SHORT: + return ((NumericTag) tag).getAsNumber(); + } + throw new IllegalArgumentException(); + } + public static void flyingMoveLogic(LivingEntity entity, Vec3 vec3d) { if (entity.isEffectiveAi() || entity.isControlledByLocalInstance()) { double d0 = 0.08D; @@ -2564,6 +2638,7 @@ public class NMSImpl implements NMSBridge { private static final MethodHandle ATTRIBUTE_PROVIDER_MAP_SETTER = NMS.getFirstFinalSetter(AttributeSupplier.class, Map.class); + private static final MethodHandle ATTRIBUTE_SUPPLIER = NMS.getFirstGetter(AttributeMap.class, AttributeSupplier.class); private static final MethodHandle AVAILABLE_BEHAVIORS_BY_PRIORITY = NMS.getGetter(Brain.class, "f"); @@ -2610,6 +2685,7 @@ public class NMSImpl implements NMSBridge { private static EntityDataAccessor INTERACTION_WIDTH = null; private static final MethodHandle JUMP_FIELD = NMS.getGetter(LivingEntity.class, "bn"); private static final MethodHandle LOOK_CONTROL_SETTER = NMS.getFirstSetter(Mob.class, LookControl.class); + private static MethodHandle META_COMPOUND_TAG; private static final MethodHandle MINECRAFT_CLIENT = NMS.getFirstGetter(YggdrasilMinecraftSessionService.class, MinecraftClient.class); private static final MethodHandle MOVE_CONTROLLER_OPERATION = NMS.getSetter(MoveControl.class, "k"); diff --git a/v1_21_R1/src/main/java/net/citizensnpcs/nms/v1_21_R1/util/NMSImpl.java b/v1_21_R1/src/main/java/net/citizensnpcs/nms/v1_21_R1/util/NMSImpl.java index af88ba9dd..2e74c3c2e 100644 --- a/v1_21_R1/src/main/java/net/citizensnpcs/nms/v1_21_R1/util/NMSImpl.java +++ b/v1_21_R1/src/main/java/net/citizensnpcs/nms/v1_21_R1/util/NMSImpl.java @@ -84,6 +84,7 @@ import net.citizensnpcs.api.trait.TraitInfo; import net.citizensnpcs.api.util.BoundingBox; import net.citizensnpcs.api.util.EntityDim; import net.citizensnpcs.api.util.Messaging; +import net.citizensnpcs.api.util.SpigotUtil; import net.citizensnpcs.api.util.SpigotUtil.InventoryViewAPI; import net.citizensnpcs.nms.v1_21_R1.entity.AllayController; import net.citizensnpcs.nms.v1_21_R1.entity.ArmadilloController; @@ -260,6 +261,14 @@ import net.citizensnpcs.util.Util; import net.minecraft.core.BlockPos; import net.minecraft.core.Holder; import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.nbt.ByteArrayTag; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.IntArrayTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.LongArrayTag; +import net.minecraft.nbt.NumericTag; +import net.minecraft.nbt.StringTag; +import net.minecraft.nbt.Tag; import net.minecraft.network.Connection; import net.minecraft.network.chat.Component; import net.minecraft.network.chat.MutableComponent; @@ -601,6 +610,35 @@ public class NMSImpl implements NMSBridge { return shape.isEmpty() ? BoundingBox.EMPTY : NMSBoundingBox.wrap(shape.bounds()); } + @Override + public Map getComponentMap(org.bukkit.inventory.ItemStack item) { + if (META_COMPOUND_TAG == null) { + try { + META_COMPOUND_TAG = NMS.getGetter(Class.forName( + "org.bukkit.craftbukkit." + SpigotUtil.getMinecraftPackage() + ".inventory.CraftMetaItem"), + "customTag"); + } catch (ClassNotFoundException e) { + e.printStackTrace(); + } + } + Map base = Maps.newHashMap(NMSBridge.super.getComponentMap(item)); + CompoundTag ct; + try { + ct = (CompoundTag) META_COMPOUND_TAG.invoke(item.getItemMeta()); + } catch (Throwable e) { + e.printStackTrace(); + return base; + } + if (ct == null) + return base; + Map custom = Maps.newHashMap(); + for (String key : ct.getAllKeys()) { + custom.put(key, deserialiseNBT(ct.get(key))); + } + base.put("custom", custom); + return base; + } + @Override public Location getDestination(org.bukkit.entity.Entity entity) { Entity handle = getHandle(entity); @@ -2079,6 +2117,42 @@ public class NMSImpl implements NMSBridge { } } + private static Object deserialiseNBT(Tag tag) { + switch (tag.getId()) { + case Tag.TAG_COMPOUND: + CompoundTag ct = (CompoundTag) tag; + Map map = Maps.newHashMapWithExpectedSize(ct.size()); + for (String key : ct.getAllKeys()) { + map.put(key, deserialiseNBT(ct.get(key))); + } + return map; + case Tag.TAG_LIST: + ListTag list = (ListTag) tag; + List res = Lists.newArrayList(list.size()); + for (int i = 0; i < list.size(); i++) { + res.add(deserialiseNBT(list.get(i))); + } + return res; + case Tag.TAG_BYTE_ARRAY: + return ((ByteArrayTag) tag).getAsByteArray(); + case Tag.TAG_INT_ARRAY: + return ((IntArrayTag) tag).getAsIntArray(); + case Tag.TAG_LONG_ARRAY: + return ((LongArrayTag) tag).getAsLongArray(); + case Tag.TAG_STRING: + return ((StringTag) tag).getAsString(); + case Tag.TAG_ANY_NUMERIC: + case Tag.TAG_LONG: + case Tag.TAG_FLOAT: + case Tag.TAG_DOUBLE: + case Tag.TAG_INT: + case Tag.TAG_BYTE: + case Tag.TAG_SHORT: + return ((NumericTag) tag).getAsNumber(); + } + throw new IllegalArgumentException(); + } + public static void flyingMoveLogic(LivingEntity entity, Vec3 vec3d) { if (entity.isEffectiveAi() || entity.isControlledByLocalInstance()) { double d0 = 0.08D; @@ -2541,9 +2615,12 @@ public class NMSImpl implements NMSBridge { } private static final MethodHandle ARMADILLO_SCUTE_TIME = NMS.getSetter(Armadillo.class, "cn"); + private static final MethodHandle ATTRIBUTE_PROVIDER_MAP = NMS.getFirstGetter(AttributeSupplier.class, Map.class); + private static final MethodHandle ATTRIBUTE_PROVIDER_MAP_SETTER = NMS.getFirstFinalSetter(AttributeSupplier.class, Map.class); + private static final MethodHandle ATTRIBUTE_SUPPLIER = NMS.getFirstGetter(AttributeMap.class, AttributeSupplier.class); private static final MethodHandle AVAILABLE_BEHAVIORS_BY_PRIORITY = NMS.getGetter(Brain.class, "f"); @@ -2589,6 +2666,7 @@ public class NMSImpl implements NMSBridge { private static EntityDataAccessor INTERACTION_WIDTH = null; private static final MethodHandle JUMP_FIELD = NMS.getGetter(LivingEntity.class, "bn"); private static final MethodHandle LOOK_CONTROL_SETTER = NMS.getFirstSetter(Mob.class, LookControl.class); + private static MethodHandle META_COMPOUND_TAG; private static final MethodHandle MINECRAFT_CLIENT = NMS.getFirstGetter(YggdrasilMinecraftSessionService.class, MinecraftClient.class); private static final MethodHandle MOVE_CONTROLLER_OPERATION = NMS.getSetter(MoveControl.class, "k");