, Object> options,
- @Nullable NpcClickAction clickEvent)
- {
- this(location, UUID.randomUUID(), name);
- this.options.putAll(options);
- this.clickEvent = clickEvent;
- }
-
- /**
- * Creates a copy of this NPC at a new location.
- * The copied NPC will have a new UUID but will retain the original NPC's name, options, and click event.
- *
- * @param newLocation the location for the copied NPC. Must not be null.
- * @return the new NPC instance. Will not be null.
- */
- public @NotNull NPC copy(@NotNull Location newLocation)
- {
- return new NPC(newLocation, name, new HashMap<>(options), clickEvent == null ? null : clickEvent.copy());
- }
-
- /**
- * Checks if this NPC has been saved to a file.
- *
- * @return {@code true} if the NPC's data file exists, {@code false} otherwise.
- */
- public boolean isSaved()
- {
- return Files.exists(npcPath);
- }
-
- /**
- * Saves the NPC's data to a file.
- * This method serializes the NPC's current state and writes it to a .npc file.
- *
- * @throws IOException if an I/O error occurs during saving.
- */
- @Override
- public void save() throws IOException
- {
- npcPath.toFile().getParentFile().mkdirs();
- new ObjectSaver(npcPath.toFile()).write(SerializedNPC.serializedNPC(this), false);
- super.save();
- }
-
- /**
- * Gets the underlying server player representation for this NPC.
- *
- * @return the {@link ServerPlayer} instance for this NPC. Will not be null.
- */
- public @NotNull Object getServerPlayer()
- {
- return serverPlayer;
- }
-
- /**
- * Gets the click action associated with this NPC.
- *
- * @return the {@link NpcClickAction} for this NPC, or {@code null} if no action is set.
- */
- public @Nullable NpcClickAction getClickEvent()
- {
- return clickEvent;
- }
-
- /**
- * Sets the click action for this NPC.
- *
- * @param event the {@link NpcClickAction} to set, or {@code null} to remove the current action.
- * @return this NPC instance for method chaining. Will not be null.
- */
- public @NotNull NPC setClickEvent(@Nullable NpcClickAction event)
- {
- this.clickEvent = event;
- return this;
- }
-
- /**
- * Checks if the NPC is currently enabled.
- * An enabled NPC is visible and interactable (unless overridden by player permissions).
- *
- * @return {@code true} if the NPC is enabled, {@code false} otherwise.
- */
- public boolean isEnabled()
- {
- return getOption(NpcOption.ENABLED);
- }
-
- /**
- * Sets the enabled state of the NPC.
- * Changing this state will trigger a reload of the NPC for all viewers.
- *
- * @param enabled {@code true} to enable the NPC, {@code false} to disable it.
- */
- public void setEnabled(boolean enabled)
- {
- setOption(NpcOption.ENABLED, enabled);
- reload();
- }
-
- /**
- * Checks if this NPC is marked as editable through the {@code NpcPlugin}.
- *
- * The default state is {@code false}.
- *
- * @return {@code true} if the NPC is editable, {@code false} otherwise
- */
- public boolean isEditable()
- {
- return getOption(NpcOption.EDITABLE);
- }
-
- /**
- * Sets whether this NPC can be edited through the {@code NpcPlugin}.
- *
- * By default, an NPC is not editable ({@code false}).
- *
- * @param editable {@code true} if the NPC should be editable, {@code false} otherwise
- */
- public void setEditable(boolean editable)
- {
- setOption(NpcOption.EDITABLE, editable);
- }
-
- /**
- * Sets a specific option for this NPC.
- *
- * @param option the {@link NpcOption} to set. Must not be null.
- * @param value the value for the option. If {@code null}, the option will be removed (reverting to default).
- * @param the type of the option's value.
- */
- public void setOption(@NotNull NpcOption option, @Nullable T value)
- {
- if(value == null)
- options.remove(option);
- else
- options.put(option, value);
-
- if(NpcApi.config.autoUpdate())
- {
- viewers.forEach(uuid ->
- {
- Player player = Bukkit.getPlayer(uuid);
- if(player == null)
- return;
-
- option.getPacket(value, this, player).ifPresent(packetWrapper ->
- ((CraftPlayer) player).getHandle().connection.send((Packet>) packetWrapper));
- });
- }
- }
-
- /**
- * Gets the value of a specific option for this NPC.
- * If the option has not been explicitly set, its default value will be returned.
- *
- * @param option the {@link NpcOption} to get. Must not be null.
- * @param the type of the option's value.
- * @return the value of the option. Will not be null (guaranteed by NpcOption default values).
- */
- @SuppressWarnings("unchecked")
- public @NotNull T getOption(@NotNull NpcOption option)
- {
- return (T) options.getOrDefault(option, option.getDefaultValue());
- }
-
- /**
- * Plays an animation for this NPC, visible to the specified player.
- *
- * @param player the player who will see the animation. Must not be null.
- * @param animation the {@link AnimatePacket.Animation} to play. Must not be null.
- */
- public void playAnimation(@NotNull Player player, @NotNull AnimatePacket.Animation animation)
- {
- ((CraftPlayer) player).getHandle().connection.send((Packet>) AnimatePacket.create(serverPlayer, animation));
- }
-
- /**
- * Reloads the NPC for all current viewers.
- * This typically involves hiding and then re-showing the NPC to apply any changes.
- */
- public void reload()
- {
- final List viewers = new ArrayList<>(this.viewers);
- hideNpcFromAllPlayers();
- TeamManager.clear(getGameProfileName());
- viewers.stream().filter(uuid -> Bukkit.getPlayer(uuid) != null).forEach(uuid -> showNPCToPlayer(Bukkit.getPlayer(uuid)));
- }
-
- /**
- * Gets the current location of the NPC.
- *
- * @return the {@link Location} of the NPC. Will not be null.
- */
- public @NotNull Location getLocation()
- {
- return location;
- }
-
- /**
- * Sets the location of the NPC.
- * This will also update the underlying server player's position.
- *
- * @param location the new {@link Location} for the NPC. Must not be null.
- */
- public void setLocation(@NotNull Location location)
- {
- this.location = location;
- Var.moveEntity(serverPlayer, location.getX(), location.getY(), location.getZ(), location.getYaw(), location.getPitch());
- }
-
- /**
- * Gets the unique identifier (UUID) of this NPC.
- *
- * @return the {@link UUID} of the NPC. Will not be null.
- */
- public @NotNull UUID getUUID()
- {
- return serverPlayer.getUUID();
- }
-
- /**
- * Gets the display name of this NPC.
- *
- * @return the {@link Component} representing the NPC's name. Will not be null.
- */
- public @NotNull Component getName()
- {
- return name;
- }
-
- public @NotNull String getGameProfileName()
- {
- if(Versions.isCurrentVersionSmallerThan(Versions.V1_21_9))
- return (String) Reflections.invokeMethod(serverPlayer.getGameProfile(), "getName").get();
- return serverPlayer.getGameProfile().name();
- }
-
- /**
- * Sets the display name of this NPC.
- * This also updates the name for the underlying server player and its list name.
- *
- * @param name the new {@link Component} name for the NPC. Must not be null.
- */
- public void setName(@NotNull Component name)
- {
- this.name = name;
- serverPlayer.listName = CraftChatMessage.fromJSON(JSONComponentSerializer.json().serialize(name));
-
- viewers.stream().filter(uuid -> Bukkit.getPlayer(uuid) != null).forEach(
- uuid -> ((CraftPlayer) Bukkit.getPlayer(uuid)).getHandle().connection.send(
- ((Packet>) SetEntityDataPacket.create(((Display.TextDisplay) nameTag.getDisplay()).getId(),
- (SynchedEntityData) nameTag.applyData(
- isEnabled() ? name : NpcApi.DISABLED_MESSAGE_PROVIDER.apply(Bukkit.getPlayer(uuid))
- .appendNewline().append(name))))));
- }
-
- /**
- * Gets the timestamp when this NPC was created.
- *
- * @return the {@link Instant} of creation. Will not be null.
- */
- public Instant getCreatedAt()
- {
- return createdAt;
- }
-
- /**
- * Makes the NPC visible to all currently online players.
- * This respects the NPC's enabled state and player permissions.
- */
- public void showNpcToAllPlayers()
- {
- Bukkit.getOnlinePlayers().forEach(this::showNPCToPlayer);
- }
-
- /**
- * Makes the NPC visible to a specific player.
- * If the NPC is disabled and the player is not an operator, the NPC will not be shown.
- * This method handles sending all necessary packets to display the NPC correctly.
- *
- * @param player the player to show the NPC to. Must not be null.
- */
- public void showNPCToPlayer(@NotNull Player player)
- {
- if(!getOption(NpcOption.ENABLED) && !player.isOp())
- return;
-
- if(!player.getWorld().getName().equals(serverPlayer.getBukkitEntity().getWorld().getName()))
- {
- hideNpcFromPlayer(player);
- return;
- }
-
- if(!viewers.contains(player.getUniqueId()))
- viewers.add(player.getUniqueId());
-
- List> packets = new ArrayList<>();
-
- Arrays.stream(NpcOption.values()).filter(NpcOption::loadBefore)
- .forEach(npcOption -> npcOption.getPacket(getOption(npcOption), this, player).ifPresent(o -> packets.add((Packet>) o)));
-
- packets.add(ClientboundPlayerInfoUpdatePacket.createSinglePlayerInitializing(serverPlayer, true));
- packets.add(serverPlayer.getAddEntityPacket(Var.getServerEntity(serverPlayer, Var.getServerLevel(serverPlayer))));
-
- boolean modified = TeamManager.exists(player, getGameProfileName());
- PlayerTeam wrappedPlayerTeam = (PlayerTeam) TeamManager.create(player, getGameProfileName());
- wrappedPlayerTeam.setNameTagVisibility(Team.Visibility.NEVER);
-
- packets.add((Packet>) SetPlayerTeamPacket.createAddOrModifyPacket(wrappedPlayerTeam, !modified));
- packets.add((Packet>) SetPlayerTeamPacket.createPlayerPacket(wrappedPlayerTeam, getGameProfileName(),
- ClientboundSetPlayerTeamPacket.Action.ADD));
-
- packets.add(new ClientboundRotateHeadPacket(serverPlayer, (byte) ((location.getYaw() % 360) * 256 / 360)));
- packets.add(new ClientboundMoveEntityPacket.Rot(serverPlayer.getId(), (byte) location.getYaw(), (byte) location.getPitch(),
- serverPlayer.onGround));
-
- if(!getOption(NpcOption.HIDE_NAMETAG))
- {
- packets.add(((Display.TextDisplay) nameTag.getDisplay()).getAddEntityPacket(
- Var.getServerEntity((Display.TextDisplay) nameTag.getDisplay(), Var.getServerLevel(serverPlayer))));
-
- packets.add((Packet>) SetEntityDataPacket.create(((Display.TextDisplay) nameTag.getDisplay()).getId(),
- (SynchedEntityData) nameTag.applyData(isEnabled() ? name : NpcApi.DISABLED_MESSAGE_PROVIDER.apply(player)
- .appendNewline().append(name))));
-
- packets.add(new ClientboundSetPassengersPacket(serverPlayer));
- }
-
- Arrays.stream(NpcOption.values()).filter(npcOption -> !npcOption.equals(NpcOption.ENABLED))
- .forEach(npcOption -> npcOption.getPacket(getOption(npcOption), this, player).map(o -> (Packet>) o)
- .ifPresent(packets::add));
-
- NpcOption.ENABLED.getPacket(isEnabled(), this, player).map(o -> (Packet>) o).ifPresent(packets::add);
-
- ServerGamePacketListenerImpl connection = ((CraftPlayer) player).getHandle().connection;
- packets.forEach(connection::send);
- }
-
- /**
- * Hides the NPC from all currently online players.
- */
- public void hideNpcFromAllPlayers()
- {
- Bukkit.getOnlinePlayers().forEach(this::hideNpcFromPlayer);
- }
-
- /**
- * Hides the NPC from a specific player.
- * This method sends packets to remove the NPC and its associated entities from the player's view.
- *
- * @param player the player to hide the NPC from. Must not be null.
- */
- public void hideNpcFromPlayer(@NotNull Player player)
- {
- ServerGamePacketListenerImpl connection = ((CraftPlayer) player).getHandle().connection;
- connection.send(new ClientboundRemoveEntitiesPacket(serverPlayer.getId(), ((Display.TextDisplay) nameTag.getDisplay()).getId()));
-
- if(TeamManager.exists(player, getGameProfileName()))
- {
- PlayerTeam team = (PlayerTeam) TeamManager.create(player, getGameProfileName());
- connection.send((Packet>) SetPlayerTeamPacket.createPlayerPacket(team, getGameProfileName(),
- ClientboundSetPlayerTeamPacket.Action.REMOVE));
- connection.send((Packet>) SetPlayerTeamPacket.createRemovePacket(team));
- }
-
- connection.send(new ClientboundPlayerInfoRemovePacket(List.of(getUUID())));
-
- viewers.remove(player.getUniqueId());
- }
-
- /**
- * Deletes the NPC.
- * This hides the NPC from all players, removes it from the NPC manager, and deletes its saved data file.
- */
- public void delete() throws IOException
- {
- if(serverPlayer == null)
- return;
-
- hideNpcFromAllPlayers();
- NpcManager.removeNPC(this);
-
- serverPlayer.remove(Entity.RemovalReason.DISCARDED);
- serverPlayer = null;
-
- npcPath.toFile().getParentFile().mkdirs();
- Files.deleteIfExists(npcPath);
- }
-
- /**
- * Makes the NPC look at a specific player.
- * This calculates the required yaw and pitch and sends update packets to the viewing player.
- *
- * @param viewer the player the NPC should look at. Must not be null.
- */
- public void lookAtPlayer(@NotNull Player viewer)
- {
- Location npcLoc = serverPlayer.getBukkitEntity().getLocation();
- Location playerLoc = viewer.getLocation();
-
- if(npcLoc.getWorld() != playerLoc.getWorld())
- return;
-
- double dx = playerLoc.getX() - npcLoc.getX();
- double dy = ((playerLoc.getY() + viewer.getEyeHeight())) -
- ((npcLoc.getY() + serverPlayer.getBukkitEntity().getEyeHeight() * getOption(NpcOption.SCALE)));
- double dz = playerLoc.getZ() - npcLoc.getZ();
-
- double distanceXZ = Math.sqrt(dx * dx + dz * dz);
- float yaw = (float) Math.toDegrees(Math.atan2(-dx, dz));
- float pitch = (float) Math.toDegrees(-Math.atan2(dy, distanceXZ));
-
- byte yawByte = (byte) (yaw * 256 / 360);
- byte pitchByte = (byte) (pitch * 256 / 360);
-
- ServerGamePacketListenerImpl connection = ((CraftPlayer) viewer).getHandle().connection;
-
- connection.send(new ClientboundRotateHeadPacket(serverPlayer, yawByte));
- connection.send(new ClientboundMoveEntityPacket.Rot(serverPlayer.getId(), yawByte, pitchByte, serverPlayer.onGround()));
- }
-
- /**
- * Moves the NPC along a precomputed {@link de.eisi05.npc.api.pathfinding.Path}, simulating walking, jumping, and gravity.
- * The NPC's position and rotation are updated each tick and sent to the specified player(s).
- *
- * @param path The {@link de.eisi05.npc.api.pathfinding.Path} containing the ordered waypoints the NPC should follow.
- * @param player The player who should see the NPC move. If null, updates all viewers in the `viewers` set.
- * @param walkSpeed The walking speed of the NPC (clamped between 0.1 and 1).
- * @param changeRealLocation If true, the NPC's actual server-side location will be updated; otherwise only packets are sent.
- * @param onEnd A {@link Runnable} to be executed when the NPC reaches the end of the path.
- * @return The {@link BukkitTask} representing the movement task.
- */
- public @NotNull BukkitTask walkTo(@NotNull de.eisi05.npc.api.pathfinding.Path path, @Nullable Player player, double walkSpeed,
- boolean changeRealLocation, @Nullable Consumer onEnd)
- {
- final double speed = Math.max(Math.min(walkSpeed, 1), 0.1);
-
- final double gravity = -0.08;
- final double jumpVelocity = 0.5;
- final double terminal = -0.5;
- final double stepHeight = 0.6;
-
- return new BukkitRunnable()
- {
- final List pathPoints = path.asLocations();
- int index = 0;
- org.bukkit.util.Vector current = location.toVector();
- double yVel = 0.0;
- float previousYaw = location.getYaw();
- org.bukkit.util.Vector previousMovement = location.getDirection();
-
- @Override
- public void run()
- {
- if(index >= pathPoints.size())
- {
- if(!path.getWaypoints().isEmpty())
- {
- Location last = path.getWaypoints().getLast();
-
- org.bukkit.util.Vector lastVector = last.toVector();
- org.bukkit.util.Vector lastMovement = lastVector.clone().subtract(current);
-
- ClientboundRotateHeadPacket rotateHeadPacket = new ClientboundRotateHeadPacket(serverPlayer,
- (byte) (last.getYaw() * 256 / 360));
- ClientboundTeleportEntityPacket teleportEntityPacket = new ClientboundTeleportEntityPacket(serverPlayer.getId(),
- new PositionMoveRotation(new Vec3(lastVector.toVector3f()), new Vec3(lastMovement.toVector3f()), last.getYaw(),
- last.getPitch()), Set.of(), true);
-
- sendNpcMovePackets(player, teleportEntityPacket, rotateHeadPacket);
- }
-
- if(changeRealLocation)
- {
- setLocation(path.getWaypoints().isEmpty() ? pathPoints.getLast() : path.getWaypoints().getLast());
- if(player != null)
- {
- for(UUID uuid : viewers)
- {
- OfflinePlayer offlinePlayer = Bukkit.getOfflinePlayer(uuid);
- if(!offlinePlayer.isOnline() || uuid.equals(player.getUniqueId()))
- continue;
-
- hideNpcFromPlayer(offlinePlayer.getPlayer());
- showNPCToPlayer(offlinePlayer.getPlayer());
- }
- }
- }
-
- if(onEnd != null)
- onEnd.accept(Result.SUCCESS);
-
- cancel();
- return;
- }
-
- org.bukkit.util.Vector target = pathPoints.get(index).toVector();
- org.bukkit.util.Vector toTarget = target.clone().subtract(current);
-
- if(toTarget.lengthSquared() < 0.04 && Math.abs(toTarget.getY()) < 0.2)
- {
- index++;
- return;
- }
-
- org.bukkit.util.Vector horizontal = new org.bukkit.util.Vector(toTarget.getX(), 0, toTarget.getZ());
- org.bukkit.util.Vector horizontalMove =
- (horizontal.lengthSquared() > 1e-6) ? horizontal.clone().normalize().multiply(speed) : new org.bukkit.util.Vector(0, 0, 0);
-
- double nextDist = target.clone().subtract(current.clone().add(horizontalMove)).lengthSquared();
- if(nextDist > toTarget.lengthSquared())
- {
- current = target;
- index++;
- return;
- }
-
- World world = location.getWorld();
- int bx = (int) Math.floor(current.getX());
- int bz = (int) Math.floor(current.getZ());
- int searchStart = (int) Math.floor(current.getY());
- int groundBlockY = Integer.MIN_VALUE;
-
- for(int y = searchStart; y >= searchStart - 3; y--)
- {
- Block block = world.getBlockAt(bx, y - 1, bz);
- if(block.getType().isSolid() && !block.getType().isAir() && !block.isPassable())
- {
- groundBlockY = y - 1;
- break;
- }
- }
- if(groundBlockY == Integer.MIN_VALUE)
- groundBlockY = world.getHighestBlockYAt(bx, bz) - 1;
- double groundY = groundBlockY + 1.0;
- boolean onGround = current.getY() <= groundY + 1e-5;
-
- if(onGround)
- {
- if(toTarget.getY() > 0 && toTarget.getY() <= stepHeight && horizontal.lengthSquared() > 1e-6)
- {
- current = current.clone().add(new org.bukkit.util.Vector(0, Math.min(toTarget.getY(), stepHeight), 0));
- yVel = 0;
- onGround = true;
- }
- else if(toTarget.getY() > 0.5)
- {
- yVel = jumpVelocity;
- onGround = false;
- }
- else
- {
- yVel = 0;
- current = new org.bukkit.util.Vector(current.getX(), groundY, current.getZ());
- }
- }
-
- double yDelta = 0;
- if(!onGround)
- {
- yVel += gravity;
- if(yVel < terminal)
- yVel = terminal;
- yDelta = yVel;
-
- if(current.getY() + yDelta <= groundY)
- {
- yDelta = groundY - current.getY();
- yVel = 0;
- onGround = true;
- }
- }
-
- org.bukkit.util.Vector movement = new org.bukkit.util.Vector(horizontalMove.getX(), yDelta, horizontalMove.getZ());
- current = current.clone().add(movement);
-
- org.bukkit.util.Vector lookDir;
- if(index + 1 < pathPoints.size())
- {
- org.bukkit.util.Vector currentTarget = pathPoints.get(index).toVector().clone();
- org.bukkit.util.Vector nextTarget = pathPoints.get(index + 1).toVector().clone();
-
- lookDir = currentTarget.clone().add(nextTarget).multiply(0.5).subtract(current);
- }
- else
- lookDir = pathPoints.get(index).toVector().clone().subtract(current);
-
- org.bukkit.util.Vector horizontalVec = new org.bukkit.util.Vector(lookDir.getX(), 0, lookDir.getZ());
- if(horizontalVec.lengthSquared() < 1e-6)
- horizontalVec = previousMovement.clone();
-
- float targetYaw = (float) (Math.atan2(horizontalVec.getZ(), horizontalVec.getX()) * 180 / Math.PI - 90);
-
- while(targetYaw > 180)
- targetYaw -= 360;
- while(targetYaw < -180)
- targetYaw += 360;
-
- float diff = targetYaw - previousYaw;
- if(diff > 180)
- diff -= 360;
- if(diff < -180)
- diff += 360;
-
- float maxTurn = 15f;
- diff = Math.max(-maxTurn, Math.min(maxTurn, diff));
-
- float yaw = previousYaw + diff;
- previousYaw = yaw;
-
- previousMovement = horizontalVec.clone();
-
- org.bukkit.util.Vector targetVec = pathPoints.get(Math.min(index + 1, pathPoints.size() - 1)).toVector().clone().subtract(current);
- double horizontalLen = Math.sqrt(targetVec.getX() * targetVec.getX() + targetVec.getZ() * targetVec.getZ());
- float pitch = (float) (-Math.atan2(targetVec.getY(), horizontalLen) * 180 / Math.PI) / 1.5f;
-
- ClientboundRotateHeadPacket rotateHeadPacket = new ClientboundRotateHeadPacket(serverPlayer, (byte) (yaw * 256 / 360));
- ClientboundTeleportEntityPacket teleportEntityPacket = new ClientboundTeleportEntityPacket(serverPlayer.getId(),
- new PositionMoveRotation(new Vec3(current.toVector3f()), new Vec3(movement.toVector3f()), yaw, pitch), Set.of(), onGround);
-
- sendNpcMovePackets(player, teleportEntityPacket, rotateHeadPacket);
- }
-
- @Override
- public synchronized void cancel() throws IllegalStateException
- {
- super.cancel();
- if(onEnd != null)
- onEnd.accept(Result.CANCELLED);
- }
- }.runTaskTimer(NpcApi.plugin, 1L, 1L);
- }
-
- /**
- * Sends NPC movement and rotation packets to a specific player or all viewers.
- *
- * @param player The player to send packets to. If null, packets are sent to all viewers.
- * @param teleportEntityPacket The packet containing the NPC's teleport/move data. Must not be null.
- * @param rotateHeadPacket The packet containing the NPC's head rotation data. Must not be null.
- */
- private void sendNpcMovePackets(@Nullable Player player, @NotNull ClientboundTeleportEntityPacket teleportEntityPacket,
- @NotNull ClientboundRotateHeadPacket rotateHeadPacket)
- {
- if(player != null)
- {
- ServerPlayer serverPlayer1 = ((CraftPlayer) player).getHandle();
- serverPlayer1.connection.send(teleportEntityPacket);
- serverPlayer1.connection.send(rotateHeadPacket);
- }
- else
- {
- for(UUID uuid : viewers)
- {
- OfflinePlayer offlinePlayer = Bukkit.getOfflinePlayer(uuid);
- if(!offlinePlayer.isOnline())
- continue;
-
- ServerPlayer serverPlayer1 = ((CraftPlayer) offlinePlayer.getPlayer()).getHandle();
- serverPlayer1.connection.send(teleportEntityPacket);
- serverPlayer1.connection.send(rotateHeadPacket);
- }
- }
- }
-
- void changeUUID(@NotNull UUID newUUID)
- {
- try
- {
- Files.deleteIfExists(npcPath);
- npcPath = NpcApi.plugin.getDataFolder().toPath().resolve("NPC").resolve(newUUID + ".npc");
- save();
- } catch(Exception e)
- {
- throw new RuntimeException(e);
- }
- }
-
- public CustomNameTag getNameTag()
- {
- return nameTag;
- }
-
- /**
- * A serializable representation of an NPC, used for saving and loading NPC data.
- * This record stores all essential properties of an NPC that need to be persisted.
- *
- * @param world The UUID of the world where the NPC is located.
- * @param x The x-coordinate of the NPC's location.
- * @param y The y-coordinate of the NPC's location.
- * @param z The z-coordinate of the NPC's location.
- * @param yaw The yaw (horizontal rotation) of the NPC.
- * @param pitch The pitch (vertical rotation) of the NPC.
- * @param id The unique identifier (UUID) of the NPC.
- * @param name The serialized representation of the NPC's display name.
- * @param options A map of NPC options, where keys are option paths (strings) and values are serializable option values.
- * @param clickEvent The click action associated with the NPC. Can be null.
- * @param createdAt The timestamp when the NPC was originally created.
- */
- public record SerializedNPC(@NotNull UUID world, double x, double y, double z, float yaw, float pitch, @NotNull UUID id,
- @NotNull String name, @NotNull Map options,
- @Nullable NpcClickAction clickEvent, @NotNull Instant createdAt) implements Serializable
- {
- @Serial
- private static final long serialVersionUID = 1L;
-
- /**
- * Creates a {@link SerializedNPC} instance from an existing {@link NPC} object.
- *
- * @param npc The NPC to serialize. Must not be null.
- * @return A new {@link SerializedNPC} instance representing the given NPC. Will not be null.
- */
- public static @NotNull SerializedNPC serializedNPC(@NotNull NPC npc)
- {
- Map options = new HashMap<>();
- npc.options.forEach((key, value) -> options.put(key.getPath(), Var.unsafeCast(key.serialize(npc.getOption(key)))));
-
- return new SerializedNPC(npc.getLocation().getWorld().getUID(), npc.getLocation().getX(), npc.getLocation().getY(),
- npc.getLocation().getZ(), npc.getLocation().getYaw(), npc.getLocation().getPitch(), npc.getUUID(),
- JSONComponentSerializer.json().serialize(npc.getName()), options, npc.clickEvent, npc.createdAt);
- }
-
- /**
- * Deserializes this {@link SerializedNPC} object back into a fully functional {@link NPC} instance.
- *
- * @param The type of the NpcOption value.
- * @param The serializable type of the NpcOption value.
- * @return A new {@link NPC} instance reconstructed from the serialized data. Will not be null.
- */
- @SuppressWarnings("unchecked")
- public @NotNull NPC deserializedNPC()
- {
- NPC npc = new NPC(new Location(Bukkit.getWorld(world), x, y, z, yaw, pitch), id,
- JSONComponentSerializer.json().deserialize(name)).setClickEvent(
- clickEvent == null ? clickEvent : clickEvent.initialize());
- options.forEach((string, serializable) -> NpcOption.getOption(string)
- .ifPresent(npcOption -> npc.setOption((NpcOption) npcOption, (T) npcOption.deserialize(Var.unsafeCast(serializable)))));
- npc.createdAt = createdAt == null ? Instant.now() : createdAt;
- return npc;
- }
- }
-}
diff --git a/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/objects/NpcConfig.java b/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/objects/NpcConfig.java
deleted file mode 100644
index 2444fca..0000000
--- a/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/objects/NpcConfig.java
+++ /dev/null
@@ -1,150 +0,0 @@
-package de.eisi05.npc.api.objects;
-
-import org.jetbrains.annotations.NotNull;
-
-/**
- * Configuration settings for NPC behavior.
- * This class allows for customizing various aspects of an NPC, such as interaction timers.
- */
-public class NpcConfig
-{
- /**
- * The time in ticks an NPC will look at a player after interaction.
- * The default value is 5 ticks.
- */
- private long lookAtTimer = 5;
-
- /**
- * If true, command validation will be skipped.
- * Useful for allowing proxy commands like BungeeCord.
- */
- private boolean avoidCommandCheck = false;
-
- /**
- * If true, debug mode is enabled.
- * Can be used for logging or diagnostic purposes.
- */
- private boolean debug = false;
-
- /**
- * Time allowed for input, measured in seconds.
- * Default is 60 seconds.
- */
- private int inputTime = 60;
-
- /**
- * If true, NPCs are automatically updated when changed.
- */
- private boolean autoUpdate = false;
-
- /**
- * Sets the duration an NPC will look at a player after an interaction.
- *
- * @param time The time in ticks. For example, 20 ticks = 1 second.
- * @return This {@link NpcConfig} instance for method chaining. Will not be null.
- */
- public @NotNull NpcConfig lookAtTimer(long time)
- {
- lookAtTimer = time;
- return this;
- }
-
- /**
- * Sets whether to skip command validation.
- * Useful for allowing BungeeCord or proxy commands.
- *
- * @param avoidCommandCheck True to skip command checks, false to validate.
- * @return This {@link NpcConfig} instance for method chaining. Never null.
- */
- public @NotNull NpcConfig avoidCommandCheck(boolean avoidCommandCheck)
- {
- this.avoidCommandCheck = avoidCommandCheck;
- return this;
- }
-
- /**
- * Enables or disables debug mode.
- *
- * @param debug True to enable debug mode, false to disable it.
- * @return This {@link NpcConfig} instance for method chaining. Never null.
- */
- public @NotNull NpcConfig debug(boolean debug)
- {
- this.debug = debug;
- return this;
- }
-
- /**
- * Sets the input time limit.
- *
- * @param inputTime The time in seconds.
- * @return This {@link NpcConfig} instance for method chaining. Never null.
- */
- public @NotNull NpcConfig inputTime(int inputTime)
- {
- this.inputTime = inputTime;
- return this;
- }
-
- /**
- * Sets whether automatic updates should be enabled.
- *
- * @param autoUpdate True to enable auto updates, false otherwise.
- * @return This {@link NpcConfig} instance for method chaining. Never null.
- */
- public @NotNull NpcConfig autoUpdate(boolean autoUpdate)
- {
- this.autoUpdate = autoUpdate;
- return this;
- }
-
- /**
- * Gets the configured duration an NPC will look at a player.
- *
- * @return The time in ticks.
- */
- public long lookAtTimer()
- {
- return lookAtTimer;
- }
-
- /**
- * Checks whether command validation is disabled.
- *
- * @return True if validation is skipped; false otherwise.
- */
- public boolean avoidCommandCheck()
- {
- return avoidCommandCheck;
- }
-
- /**
- * Checks whether debug mode is enabled.
- *
- * @return True if debug is enabled; false otherwise.
- */
- public boolean debug()
- {
- return debug;
- }
-
- /**
- * Gets the configured input time limit.
- *
- * @return The time in seconds.
- */
- public int inputTime()
- {
- return inputTime;
- }
-
- /**
- * Checks whether automatic updates are enabled.
- *
- * @return True if auto updates are enabled; false otherwise.
- */
- public boolean autoUpdate()
- {
- return autoUpdate;
- }
-}
diff --git a/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/objects/NpcHolder.java b/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/objects/NpcHolder.java
deleted file mode 100644
index dc25fcd..0000000
--- a/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/objects/NpcHolder.java
+++ /dev/null
@@ -1,62 +0,0 @@
-package de.eisi05.npc.api.objects;
-
-import org.bukkit.inventory.Inventory;
-import org.bukkit.inventory.InventoryHolder;
-import org.jetbrains.annotations.NotNull;
-
-import java.io.IOException;
-
-/**
- * Abstract class representing an entity that can hold NPC-related data
- * and has a concept of unsaved changes.
- * It implements {@link InventoryHolder} but onlay as placeholder.
- */
-public abstract class NpcHolder implements InventoryHolder
-{
- /**
- * Flag indicating whether there are unsaved changes to this holder.
- * Defaults to {@code false}.
- */
- private boolean unsavedChanges = false;
-
- /**
- * Checks if there are any unsaved changes for this NPC holder.
- *
- * @return {@code true} if there are unsaved changes, {@code false} otherwise.
- */
-
- public boolean hasUnsavedChanges()
- {
- return unsavedChanges;
- }
-
- /**
- * Marks that there are unsaved changes to this NPC holder.
- * This should be called whenever a modifiable property of the holder is changed.
- */
- public void markChange()
- {
- unsavedChanges = true;
- }
-
- /**
- * Saves the current state of the NPC holder.
- * This method is intended to persist any changes. After successful execution,
- * the {@code unsavedChanges} flag is reset to {@code false}.
- *
- * @throws IOException if an error occurs during the saving process.
- */
- public void save() throws IOException
- {
- unsavedChanges = false;
- }
-
- /**
- * @throws UnsupportedOperationException always, as this inventory is not used.
- */
- @Override
- public @NotNull Inventory getInventory()
- {
- throw new UnsupportedOperationException("This inventory is not used!");
- }
-}
diff --git a/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/objects/NpcOption.java b/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/objects/NpcOption.java
deleted file mode 100644
index bb1f312..0000000
--- a/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/objects/NpcOption.java
+++ /dev/null
@@ -1,591 +0,0 @@
-package de.eisi05.npc.api.objects;
-
-import com.google.common.collect.Multimaps;
-import com.mojang.authlib.GameProfile;
-import com.mojang.authlib.properties.Property;
-import com.mojang.authlib.properties.PropertyMap;
-import com.mojang.datafixers.util.Pair;
-import de.eisi05.npc.api.NpcApi;
-import de.eisi05.npc.api.enums.SkinParts;
-import de.eisi05.npc.api.manager.TeamManager;
-import de.eisi05.npc.api.scheduler.Tasks;
-import de.eisi05.npc.api.utils.*;
-import de.eisi05.npc.api.wrapper.enums.ChatFormat;
-import de.eisi05.npc.api.wrapper.packets.SetEntityDataPacket;
-import de.eisi05.npc.api.wrapper.packets.SetPlayerTeamPacket;
-import net.minecraft.ChatFormatting;
-import net.minecraft.network.Connection;
-import net.minecraft.network.protocol.Packet;
-import net.minecraft.network.protocol.PacketFlow;
-import net.minecraft.network.protocol.game.*;
-import net.minecraft.network.syncher.EntityDataSerializers;
-import net.minecraft.network.syncher.SynchedEntityData;
-import net.minecraft.server.MinecraftServer;
-import net.minecraft.server.level.ClientInformation;
-import net.minecraft.server.level.ServerLevel;
-import net.minecraft.server.level.ServerPlayer;
-import net.minecraft.server.network.CommonListenerCookie;
-import net.minecraft.server.network.ServerGamePacketListenerImpl;
-import net.minecraft.world.entity.Entity;
-import net.minecraft.world.entity.ai.attributes.AttributeInstance;
-import net.minecraft.world.entity.ai.attributes.Attributes;
-import net.minecraft.world.scores.PlayerTeam;
-import org.bukkit.Bukkit;
-import org.bukkit.Location;
-import org.bukkit.craftbukkit.CraftServer;
-import org.bukkit.craftbukkit.CraftWorld;
-import org.bukkit.craftbukkit.entity.CraftPlayer;
-import org.bukkit.craftbukkit.inventory.CraftItemStack;
-import org.bukkit.entity.Player;
-import org.bukkit.entity.Pose;
-import org.bukkit.inventory.EquipmentSlot;
-import org.bukkit.inventory.ItemStack;
-import org.bukkit.scheduler.BukkitRunnable;
-import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
-
-import java.io.Serializable;
-import java.lang.reflect.Field;
-import java.util.*;
-import java.util.function.Function;
-
-/**
- * Represents a configurable option for an NPC.
- * Each option has a path, a default value, serialization/deserialization logic,
- * and a function to generate a network packet for applying the option.
- *
- * @param The type of the option's value in its usable form.
- * @param The type of the option's value in its serialized form.
- */
-public class NpcOption
-{
- /**
- * NPC option to determine if the NPC should use the skin of the viewing player.
- * If true, the NPC's skin will be dynamically set to the skin of the player looking at it.
- */
- public static final NpcOption USE_PLAYER_SKIN = new NpcOption<>("use-player-skin", false,
- aBoolean -> aBoolean, aBoolean -> aBoolean,
- (skin, npc, player) ->
- {
- if(!skin)
- return null;
-
- ServerPlayer serverPlayer = ((CraftPlayer) player).getHandle();
- ServerPlayer npcServerPlayer = (ServerPlayer) npc.getServerPlayer();
-
- if(!Versions.isCurrentVersionSmallerThan(Versions.V1_21_9))
- {
- var textureProperties = ((PropertyMap) Reflections.getField(serverPlayer.getGameProfile(), "properties")
- .get()).get("textures").iterator();
-
- var npcTextureProperties = ((PropertyMap) Reflections.getField(serverPlayer.getGameProfile(), "properties")
- .get()).get("textures").iterator();
-
- Property property = textureProperties.hasNext() ? textureProperties.next() : null;
- Property npcProperty = npcTextureProperties.hasNext() ? npcTextureProperties.next() : null;
-
- if((property == null && npcProperty == null) || (property != null && npcProperty != null &&
- Reflections.getField(property, "value").get().equals(Reflections.getField(npcProperty, "value").get())))
- return null;
-
- UUID newUUID = UUID.randomUUID();
- GameProfile profile = Reflections.getInstance(GameProfile.class, newUUID, "NPC" + newUUID.toString().substring(0, 13),
- Reflections.getInstance(PropertyMap.class, Multimaps.forMap(property == null ? Map.of() : Map.of("textures", property)))
- .orElseThrow()).orElseThrow();
-
- Location location = npc.getLocation();
- MinecraftServer server = ((CraftServer) Bukkit.getServer()).getServer();
- ServerLevel level = ((CraftWorld) location.getWorld()).getHandle();
- npc.serverPlayer = new ServerPlayer(server, level, profile, ClientInformation.createDefault());
- Var.moveEntity(npc.serverPlayer, location.getX(), location.getY(), location.getZ(), location.getYaw(), location.getPitch());
- npc.serverPlayer.connection = new ServerGamePacketListenerImpl(server, new Connection(PacketFlow.SERVERBOUND), npc.serverPlayer,
- CommonListenerCookie.createInitial(profile, true));
- npc.changeUUID(newUUID);
- return null;
- }
-
- PropertyMap playerProperty = (PropertyMap) Reflections.invokeMethod(serverPlayer.getGameProfile(), "getProperties").get();
- PropertyMap npcProperty = (PropertyMap) Reflections.invokeMethod(serverPlayer.getGameProfile(), "getProperties").get();
-
- var textureProperties = playerProperty.get("textures").iterator();
- npcProperty.removeAll("textures");
-
- if(!textureProperties.hasNext())
- return null;
-
- var textureProperty = textureProperties.next();
- npcProperty.put("textures", textureProperty);
- return null;
- }).loadBefore(!Versions.isCurrentVersionSmallerThan(Versions.V1_21_9));
-
- /**
- * NPC option to set a specific skin using a value and signature.
- * This is ignored if {@link #USE_PLAYER_SKIN} is true.
- */
- public static final NpcOption SKIN = new NpcOption("skin", null,
- skin -> skin, skin -> skin,
- (skin, npc, player) ->
- {
- if(npc.getOption(USE_PLAYER_SKIN))
- return null;
-
- ServerPlayer npcServerPlayer = (ServerPlayer) npc.getServerPlayer();
-
-
- if(!Versions.isCurrentVersionSmallerThan(Versions.V1_21_9))
- {
- var npcTextureProperties = ((PropertyMap) Reflections.getField(npcServerPlayer.getGameProfile(), "properties")
- .get()).get("textures").iterator();
-
- Property npcProperty = npcTextureProperties.hasNext() ? npcTextureProperties.next() : null;
-
- if((skin == null && npcProperty == null) ||
- (npcProperty != null && skin.value().equals(Reflections.getField(npcProperty, "value").get())))
- return null;
-
- UUID newUUID = UUID.randomUUID();
- var textures = new Property("textures", skin.value(), skin.signature());
-
- PropertyMap propertyMap = Reflections.getInstance(PropertyMap.class,
- Multimaps.forMap(skin == null ? Map.of() : Map.of("textures", textures))).orElseThrow();
-
- GameProfile profile = Reflections.getInstance(GameProfile.class, newUUID, "NPC" + newUUID.toString().substring(0, 13),
- propertyMap).orElseThrow();
-
- Location location = npc.getLocation();
- MinecraftServer server = ((CraftServer) Bukkit.getServer()).getServer();
- ServerLevel level = ((CraftWorld) location.getWorld()).getHandle();
- npc.serverPlayer = new ServerPlayer(server, level, profile, ClientInformation.createDefault());
- Var.moveEntity(npc.serverPlayer, location.getX(), location.getY(), location.getZ(), location.getYaw(), location.getPitch());
- npc.serverPlayer.connection = new ServerGamePacketListenerImpl(server, new Connection(PacketFlow.SERVERBOUND), npc.serverPlayer,
- CommonListenerCookie.createInitial(profile, true));
- npc.changeUUID(newUUID);
- return null;
- }
-
- PropertyMap properties = (PropertyMap) Reflections.invokeMethod(npcServerPlayer.getGameProfile(), "getProperties").get();
-
- properties.removeAll("textures");
-
- if(skin == null)
- return null;
-
- var textures = new Property("textures", skin.value(), skin.signature());
-
- properties.put("textures", textures);
- return null;
- }).loadBefore(!Versions.isCurrentVersionSmallerThan(Versions.V1_21_9));
-
- /**
- * NPC option to control whether the NPC is shown in the player tab list.
- * If false, the NPC will be removed from the tab list for the viewing player after a short delay.
- */
- public static final NpcOption SHOW_TAB_LIST = new NpcOption<>("show-tab-list", true,
- aBoolean -> aBoolean, aBoolean -> aBoolean,
- (show, npc, player) ->
- {
- if(show)
- return null;
-
- new BukkitRunnable()
- {
- @Override
- public void run()
- {
- ((CraftPlayer) player).getHandle().connection.send(new ClientboundPlayerInfoRemovePacket(List.of(npc.getUUID())));
- }
- }.runTaskLater(NpcApi.plugin, 50);
- return null;
- });
-
- /**
- * NPC option to set the simulated latency (ping) of the NPC in the tab list.
- */
- public static final NpcOption LATENCY = new NpcOption<>("latency", 0,
- aInteger -> aInteger, aInteger -> aInteger,
- (latency, npc, player) ->
- {
- ServerPlayer npcServerPlayer = (ServerPlayer) npc.getServerPlayer();
-
- CommonListenerCookie commonListenerCookie;
- if(Versions.isCurrentVersionSmallerThan(Versions.V1_21_7))
- commonListenerCookie = Reflections.getInstanceFirstConstructor(CommonListenerCookie.class,
- npcServerPlayer.getGameProfile(), latency, ClientInformation.createDefault(), true).orElseThrow();
- else
- commonListenerCookie = Reflections.getInstanceFirstConstructor(CommonListenerCookie.class,
- npcServerPlayer.getGameProfile(), latency, ClientInformation.createDefault(), true, null,
- new HashSet<>(), Reflections.getInstance("io.papermc.paper.util.KeepAlive").orElseThrow()).orElseThrow();
-
- if(Versions.isCurrentVersionSmallerThan(Versions.V1_21_9))
- npcServerPlayer.connection = new ServerGamePacketListenerImpl(
- (MinecraftServer) Reflections.invokeMethod(npcServerPlayer, "getServer").get(),
- new Connection(PacketFlow.SERVERBOUND), npcServerPlayer, commonListenerCookie);
- else
- npcServerPlayer.connection = new ServerGamePacketListenerImpl(npcServerPlayer.level().getServer(),
- new Connection(PacketFlow.SERVERBOUND), npcServerPlayer, commonListenerCookie);
-
- return new ClientboundPlayerInfoUpdatePacket(ClientboundPlayerInfoUpdatePacket.Action.UPDATE_LATENCY, npcServerPlayer);
- });
-
- /**
- * NPC option to control the visibility of the NPC's nametag.
- */
- public static final NpcOption HIDE_NAMETAG = new NpcOption<>("hide-nametag", false,
- aBoolean -> aBoolean, aBoolean -> aBoolean,
- (hide, npc, player) ->
- {
- if(!hide)
- return null;
-
- return new ClientboundRemoveEntitiesPacket(((Entity) npc.getNameTag().getDisplay()).getId());
- });
-
- /**
- * NPC option to set the pose of the NPC (e.g., standing, sleeping, swimming).
- * For a full list look at {@link Pose}.
- */
- public static final NpcOption POSE = new NpcOption<>("pose", Pose.STANDING,
- pose -> pose, pose -> pose,
- (pose, npc, player) ->
- {
- net.minecraft.world.entity.Pose nmsPose = net.minecraft.world.entity.Pose.values()[pose.ordinal()];
-
- if(nmsPose == null)
- throw new RuntimeException("Pose (" + pose.name() + ") not found");
-
- ServerPlayer npcServerPlayer = (ServerPlayer) npc.getServerPlayer();
-
- npcServerPlayer.setPose(nmsPose);
-
- SynchedEntityData data = npcServerPlayer.getEntityData();
- data.set(EntityDataSerializers.POSE.createAccessor(6), nmsPose);
-
- if(pose == Pose.SPIN_ATTACK)
- data.set(EntityDataSerializers.BYTE.createAccessor(8), (byte) 0x04);
- else
- data.set(EntityDataSerializers.BYTE.createAccessor(8), (byte) 0x01);
-
- return (Packet>) SetEntityDataPacket.create(npcServerPlayer.getId(), data);
- });
-
- /**
- * NPC option to set the equipment worn by the NPC (armor, items in hand).
- * The map uses {@link EquipmentSlot} as keys and {@link ItemStack} as values.
- * Serialized form uses item base64 strings.
- */
- public static final NpcOption, HashMap> EQUIPMENT = new NpcOption<>("equipment", Map.of(),
- map ->
- {
- HashMap serializedMap = new HashMap<>();
- map.forEach((slot, item) -> serializedMap.put(slot, ItemSerializer.itemStackToBase64(item)));
- return serializedMap;
- },
- serializedMap ->
- {
- HashMap map = new HashMap<>();
- serializedMap.forEach((slot, string) -> map.put(slot, ItemSerializer.itemStackFromBase64(string)));
- return map;
- },
- (map, npc, player) ->
- {
- if(map.isEmpty())
- return null;
-
- List> list = new ArrayList<>();
-
- map.forEach((slot, item) -> list.add(
- new Pair<>(net.minecraft.world.entity.EquipmentSlot.values()[slot.ordinal()], CraftItemStack.asNMSCopy(item))));
-
- return new ClientboundSetEquipmentPacket(((ServerPlayer) npc.getServerPlayer()).getId(), list);
- });
-
- /**
- * NPC option to control which parts of the NPC's skin are visible (e.g., hat, jacket).
- * For a full list look at {@link SkinParts}.
- */
- public static final NpcOption SKIN_PARTS = new NpcOption<>("skin-parts", SkinParts.values(),
- skinParts -> skinParts, skinParts -> skinParts,
- (skinParts, npc, player) ->
- {
- ServerPlayer npcServerPlayer = (ServerPlayer) npc.getServerPlayer();
- SynchedEntityData data = npcServerPlayer.getEntityData();
- data.set(EntityDataSerializers.BYTE.createAccessor(Versions.isCurrentVersionSmallerThan(Versions.V1_21_9) ? 17 : 16),
- (byte) Arrays.stream(skinParts).mapToInt(SkinParts::getValue).sum());
- return (Packet>) SetEntityDataPacket.create(npcServerPlayer.getId(), data);
- });
-
- /**
- * NPC option to make the NPC look at the player if they are within a certain distance.
- * The value is the maximum distance in blocks. A value of 0 or less disables this.
- * The actual looking logic is handled by {@link Tasks}.
- */
- public static final NpcOption LOOK_AT_PLAYER = new NpcOption<>("look-at-player", 0.0,
- distance -> distance, distance -> distance,
- (distance, npc, player) -> null);
-
- /**
- * NPC option to make the NPC glow with a specific color.
- * If null, the glowing effect is removed.
- */
- @SuppressWarnings("unchecked")
- public static final NpcOption GLOWING = new NpcOption<>("glowing", null,
- color -> color, color -> color,
- (color, npc, player) ->
- {
- ServerPlayer npcServerPlayer = (ServerPlayer) npc.getServerPlayer();
-
- if(color == null)
- {
- SynchedEntityData entityData = npcServerPlayer.getEntityData();
- entityData.set(EntityDataSerializers.BYTE.createAccessor(0), (byte) 0);
- return (Packet>) SetEntityDataPacket.create(npcServerPlayer.getId(), entityData);
- }
-
- String teamName = npc.getGameProfileName();
- boolean modified = TeamManager.exists(player, teamName);
- PlayerTeam team = (PlayerTeam) TeamManager.create(player, teamName);
-
- team.setColor(ChatFormatting.getByCode(color.getColorCode()));
-
- var teamPacket = SetPlayerTeamPacket.createAddOrModifyPacket(team, !modified);
-
- SynchedEntityData entityData = npcServerPlayer.getEntityData();
- entityData.set(EntityDataSerializers.BYTE.createAccessor(0), (byte) 0x40);
-
- return new ClientboundBundlePacket(List.of((Packet super net.minecraft.network.protocol.game.ClientGamePacketListener>) teamPacket,
- (Packet super net.minecraft.network.protocol.game.ClientGamePacketListener>) SetEntityDataPacket.create(
- npcServerPlayer.getId(), entityData)));
- });
-
- /**
- * NPC option to set the scale (size) of the NPC.
- * A value of 1.0 is normal size. Requires Minecraft 1.20.6 or newer.
- */
- public static final NpcOption SCALE = new NpcOption<>("scale", 1.0,
- scale -> scale, scale -> scale,
- (scale, npc, player) ->
- {
- ServerPlayer npcServerPlayer = (ServerPlayer) npc.getServerPlayer();
-
- AttributeInstance instance = npcServerPlayer.getAttribute(Attributes.SCALE);
- instance.setBaseValue(scale);
-
- return new ClientboundUpdateAttributesPacket(npcServerPlayer.getId(), List.of(instance));
- });
-
- /**
- * NPC option to control the position of the NPC in the TAB list.
- *
- * Only works on versions older than 1.21.2.
- * On 1.21.2 and newer, this option has no effect.
- *
- */
- public static final NpcOption LIST_ORDER = new NpcOption<>("list-order", 0,
- aInt -> aInt, aInt -> aInt,
- (order, npc, player) ->
- {
- if(!Versions.isCurrentVersionSmallerThan(Versions.V1_21_2))
- return null;
-
- ((ServerPlayer) npc.getServerPlayer()).listOrder = order;
-
- return new ClientboundPlayerInfoUpdatePacket(ClientboundPlayerInfoUpdatePacket.Action.UPDATE_LIST_ORDER,
- (ServerPlayer) npc.getServerPlayer());
- }).since(Versions.V1_21_2);
-
- /**
- * NPC option to control if the NPC is enabled (visible and interactable).
- * If false, a "DISABLED" marker may be shown.
- * This is an internal option, typically not directly set by users but controlled by {@link NPC#setEnabled(boolean)}.
- */
- static final NpcOption ENABLED = new NpcOption<>("enabled", false,
- aBoolean -> aBoolean, aBoolean -> aBoolean,
- (enabled, npc, player) -> null);
-
- /**
- * NPC option to control if the NPC is enabled (visible and interactable).
- * If false, a "DISABLED" marker may be shown.
- * This is an internal option, typically not directly set by users but controlled by {@link NPC#setEnabled(boolean)}.
- */
- static final NpcOption EDITABLE = new NpcOption<>("editable", false,
- aBoolean -> aBoolean, aBoolean -> aBoolean,
- (enabled, npc, player) -> null);
-
- private final String path;
- private final T defaultValue;
- private final Function serializer;
- private final Function deserializer;
- private final TriFunction> packet;
- private Versions since = Versions.V1_17;
- private boolean loadBefore = false;
-
- /**
- * Private constructor to create a new NpcOption.
- *
- * @param path The configuration path string. Must not be null.
- * @param defaultValue The default value for the option. Can be null.
- * @param serializer The serialization function. Must not be null.
- * @param deserializer The deserialization function. Must not be null.
- * @param packet The packet generation function. Must not be null.
- */
- private NpcOption(@NotNull String path, @Nullable T defaultValue, @NotNull Function serializer, @NotNull Function deserializer,
- @NotNull TriFunction> packet)
- {
- this.path = path;
- this.defaultValue = defaultValue;
- this.serializer = serializer;
- this.deserializer = deserializer;
- this.packet = packet;
- }
-
- /**
- * Retrieves all declared {@link NpcOption} constants within this class using reflection.
- *
- * @return An array of {@link NpcOption} instances. Will not be null.
- */
- public static @NotNull NpcOption, ?>[] values()
- {
- List fields = Arrays.stream(NpcOption.class.getDeclaredFields()).filter(field -> field.getType().equals(NpcOption.class)).toList();
-
- NpcOption, ?>[] values = new NpcOption[fields.size()];
-
- for(int i = 0; i < fields.size(); i++)
- {
- try
- {
- values[i] = (NpcOption, ?>) fields.get(i).get(null);
- } catch(IllegalAccessException e)
- {
- }
- }
-
- return values;
- }
-
- /**
- * Retrieves an {@link NpcOption} instance by its configuration path.
- *
- * @param path The configuration path string to search for. Must not be null.
- * @return An {@link Optional} containing the found {@link NpcOption}, or an empty Optional if no option matches the path.
- */
- public static @NotNull Optional> getOption(@NotNull String path)
- {
- return Arrays.stream(values()).filter(npcOption -> npcOption.getPath().equals(path)).findFirst();
- }
-
- /**
- * Sets the minimum Minecraft version required for this option.
- * Used for options that are only available in newer versions of the game.
- *
- * @param since The minimum {@link Versions} required. Must not be null.
- * @return This {@link NpcOption} instance for method chaining.
- */
- public @NotNull NpcOption since(@NotNull Versions since)
- {
- this.since = since;
- return this;
- }
-
- public @NotNull NpcOption loadBefore(boolean loadBefore)
- {
- this.loadBefore = loadBefore;
- return this;
- }
-
- public boolean loadBefore()
- {
- return loadBefore;
- }
-
- /**
- * Checks if this NPC option is compatible with the current server version.
- * An option is compatible if the current server version is greater than or equal to
- * the version specified by {@link #since()}.
- *
- * @return {@code true} if the option is compatible, {@code false} otherwise.
- */
- public boolean isCompatible()
- {
- return !Versions.isCurrentVersionSmallerThan(since);
- }
-
- /**
- * Gets the minimum Minecraft version required for this option.
- *
- * @return The {@link Versions} instance.
- */
- public Versions since()
- {
- return since;
- }
-
- /**
- * Gets the default value for this option.
- *
- * @return The default value, which can be null if defined as such.
- */
- public @Nullable T getDefaultValue()
- {
- return defaultValue;
- }
-
- /**
- * Gets the configuration path string for this option.
- *
- * @return The path string. Will not be null.
- */
- public @NotNull String getPath()
- {
- return path;
- }
-
- /**
- * Serializes the given value (of type T) into its serializable form (type S).
- *
- * @param var1 The value to serialize. Can be null.
- * @return The serialized value. Can be null if the input or serializer result is null.
- * @throws RuntimeException if a {@link ClassCastException} occurs during serialization,
- * indicating an incorrect type was passed.
- */
- @SuppressWarnings("unchecked")
- public @Nullable S serialize(@Nullable Object var1)
- {
- try
- {
- return serializer.apply((T) var1);
- } catch(ClassCastException e)
- {
- throw new RuntimeException(path + " -> " + var1);
- }
- }
-
- /**
- * Deserializes the given value (of type S) back into its usable form (type T).
- *
- * @param var1 The serialized value to deserialize. Can be null.
- * @return The deserialized value. Can be null if the input or deserializer result is null.
- */
- public @Nullable T deserialize(@Nullable S var1)
- {
- return deserializer.apply(var1);
- }
-
- /**
- * Generates the network packet(s) needed to apply this option's value to an NPC for a specific player.
- * The method checks for version compatibility before generating the packet.
- *
- * @param object The value of the option to apply. Can be null.
- * @param npc The {@link NPC} to apply the option to. Must not be null.
- * @param player The {@link Player} who will receive the update. Must not be null.
- * @return An {@link Optional} containing the {@link Packet} if one is generated and the option is compatible,
- * otherwise an empty Optional.
- */
- @SuppressWarnings("unchecked")
- public @NotNull Optional getPacket(@Nullable Object object, @NotNull NPC npc, Player player)
- {
- if(packet == null || !isCompatible())
- return Optional.empty();
-
- return Optional.ofNullable(packet.apply((T) object, npc, player));
- }
-}
diff --git a/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/objects/Skin.java b/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/objects/Skin.java
deleted file mode 100644
index e91de26..0000000
--- a/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/objects/Skin.java
+++ /dev/null
@@ -1,260 +0,0 @@
-package de.eisi05.npc.api.objects;
-
-import com.google.gson.JsonArray;
-import com.google.gson.JsonElement;
-import com.google.gson.JsonObject;
-import com.google.gson.JsonParser;
-import com.mojang.authlib.properties.Property;
-import com.mojang.authlib.properties.PropertyMap;
-import de.eisi05.npc.api.utils.Reflections;
-import de.eisi05.npc.api.utils.Versions;
-import net.minecraft.server.level.ServerPlayer;
-import org.bukkit.craftbukkit.entity.CraftPlayer;
-import org.bukkit.entity.Player;
-import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
-
-import java.io.*;
-import java.net.HttpURLConnection;
-import java.net.URI;
-import java.net.URL;
-import java.net.http.HttpClient;
-import java.net.http.HttpRequest;
-import java.net.http.HttpResponse;
-import java.nio.file.Files;
-import java.time.Duration;
-import java.util.*;
-import java.util.concurrent.CompletableFuture;
-
-/**
- * Represents a player's skin, containing its name, value, and signature.
- * This record is immutable and implements {@link Serializable} for easy persistence.
- * The skin value and signature are typically obtained from Mojang's session servers
- * and are used to display the correct player texture.
- *
- * @param name The name associated with the skin (usually the player's username). Can be {@code null}.
- * @param value The base64 encoded string representing the skin data (texture URL, model, etc.).
- * @param signature The signature used to verify the authenticity of the skin data.
- */
-public record Skin(@Nullable String name, @NotNull String value, @NotNull String signature) implements Serializable
-{
- @Serial
- private static final long serialVersionUID = 1L;
-
- /**
- * A static cache to store fetched skins, mapping UUIDs to Skin objects.
- * This helps reduce redundant API calls to Mojang's servers.
- */
- private static final Map skinCache = new HashMap<>();
-
- /**
- * Retrieves the skin data directly from a currently online Bukkit player.
- * This method uses reflection to access the player's game profile properties.
- *
- * @param player The Bukkit player from whom to retrieve the skin. Must not be {@code null}.
- * @return A {@link Skin} object representing the player's current skin, or {@code null} if no skin properties are found.
- */
- public static @Nullable Skin fromPlayer(@NotNull Player player)
- {
- if(Versions.isCurrentVersionSmallerThan(Versions.V1_21_9))
- {
- ServerPlayer serverPlayer = ((CraftPlayer) player).getHandle();
- PropertyMap properties = (PropertyMap) Reflections.invokeMethod(serverPlayer.getGameProfile(), "getProperties").get();
- Iterator it = properties.get("textures").iterator();
-
- if(!it.hasNext())
- return null;
-
- var property = it.next();
-
- return new Skin(player.getName(), property.value(), property.signature());
- }
-
- ServerPlayer serverPlayer = ((CraftPlayer) player).getHandle();
- var properties = serverPlayer.getGameProfile().properties().get("textures").iterator();
-
- if(!properties.hasNext())
- return null;
-
- var property = properties.next();
-
- return new Skin(player.getName(), property.value(), property.signature());
- }
-
- /**
- * Fetches a player's skin from Mojang's session server using their UUID.
- * This method first checks the local cache (`skinCache`) before making an HTTP request.
- * The fetched skin is added to the cache for future use.
- *
- * @param uuid The UUID of the player whose skin is to be fetched. Must not be {@code null}.
- * @return A {@link Skin} object if the skin is successfully fetched or found in cache, otherwise {@code null}.
- */
- public static @Nullable Skin fetchSkin(@NotNull UUID uuid)
- {
-
- if(skinCache.containsKey(uuid))
- return skinCache.get(uuid);
-
- try
- {
- URL url = URI.create("https://sessionserver.mojang.com/session/minecraft/profile/" + uuid + "?unsigned=false").toURL();
- HttpURLConnection connection = (HttpURLConnection) url.openConnection();
- connection.setReadTimeout(10000);
-
- try(InputStream is = connection.getInputStream(); Scanner scanner = new Scanner(is))
- {
- String response = scanner.useDelimiter("\\A").next();
-
- JsonObject json = JsonParser.parseString(response).getAsJsonObject();
- String name = json.get("name").getAsString();
-
- JsonArray properties = json.getAsJsonArray("properties");
- for(JsonElement prop : properties)
- {
- JsonObject obj = prop.getAsJsonObject();
- String value = obj.get("value").getAsString();
- String signature = obj.has("signature") ? obj.get("signature").getAsString() : null;
- Skin skin = new Skin(name, value, signature);
- skinCache.put(uuid, skin);
- return skin;
- }
-
- return null;
- }
- } catch(Exception e)
- {
- return null;
- }
- }
-
- /**
- * Fetches a player's skin by their username.
- * This method first calls Mojang's API to get the player's UUID from their name
- * and then uses the UUID to fetch the skin data.
- *
- * @param name The username of the player whose skin is to be fetched. Must not be {@code null}.
- * @return A {@link Skin} object if the skin is successfully fetched, otherwise {@code null}.
- */
- public static Skin fetchSkin(@NotNull String name)
- {
- try
- {
- URL url = URI.create("https://api.mojang.com/users/profiles/minecraft/" + name).toURL();
- HttpURLConnection conn = (HttpURLConnection) url.openConnection();
- conn.setReadTimeout(10000);
-
- try(InputStream is = conn.getInputStream(); Scanner scanner = new Scanner(is))
- {
- String response = scanner.useDelimiter("\\A").next();
- JsonObject json = JsonParser.parseString(response).getAsJsonObject();
- String id = json.get("id").getAsString();
- return fetchSkin(UUID.fromString(id.replaceFirst(
- "(\\w{8})(\\w{4})(\\w{4})(\\w{4})(\\w{12})",
- "$1-$2-$3-$4-$5")));
- }
- } catch(Exception e)
- {
- return null;
- }
- }
-
- /**
- * Uploads a skin file to MineSkin and retrieves the resulting {@link Skin}.
- *
- * @param skinFile the PNG file of the skin to upload.
- * @return an {@link Optional} containing the skin if successful, otherwise empty.
- * @throws IllegalArgumentException if the file does not exist.
- */
- public static Optional fetchSkin(@NotNull File skinFile)
- {
- if(!skinFile.exists())
- throw new IllegalArgumentException("File does not exist");
-
- try(HttpClient client = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(10)).build())
- {
- String boundary = "----MineSkinBoundary" + System.currentTimeMillis();
- String CRLF = "\r\n";
-
- byte[] fileBytes = Files.readAllBytes(skinFile.toPath());
- String bodyBuilder = "--" + boundary + CRLF +
- "Content-Disposition: form-data; name=\"file\"; filename=\"" +
- skinFile.getName() + "\"" + CRLF +
- "Content-Type: image/png" + CRLF + CRLF;
-
- String endPart = CRLF + "--" + boundary + "--" + CRLF;
-
- byte[] requestBody = combine(bodyBuilder.getBytes(), fileBytes, endPart.getBytes());
-
- HttpRequest request = HttpRequest.newBuilder()
- .uri(URI.create("https://api.mineskin.org/generate/upload"))
- .timeout(Duration.ofSeconds(10))
- .header("Content-Type", "multipart/form-data; boundary=" + boundary)
- .POST(HttpRequest.BodyPublishers.ofByteArray(requestBody))
- .build();
-
- HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString());
- JsonObject obj = JsonParser.parseString(response.body()).getAsJsonObject();
- JsonObject texture = obj.getAsJsonObject("data").getAsJsonObject("texture");
-
- String value = texture.get("value").getAsString();
- String signature = texture.get("signature").getAsString();
-
- return Optional.of(new Skin(null, value, signature));
- } catch(IOException | InterruptedException e)
- {
- return Optional.empty();
- }
- }
-
- /**
- * Asynchronously fetches a player's skin using their UUID.
- * This method wraps the synchronous {@link #fetchSkin(UUID)} call in a {@link CompletableFuture}
- * to prevent blocking the main thread.
- *
- * @param uuid The UUID of the player whose skin is to be fetched. Must not be {@code null}.
- * @return A {@link CompletableFuture} that will complete with the {@link Skin} object, or {@code null} if fetching fails.
- */
- public static CompletableFuture fetchSkinAsync(@NotNull UUID uuid)
- {
- return CompletableFuture.supplyAsync(() -> fetchSkin(uuid));
- }
-
- /**
- * Asynchronously fetches a player's skin using their username.
- * This method wraps the synchronous {@link #fetchSkin(String)} call in a {@link CompletableFuture}
- * to prevent blocking the main thread.
- *
- * @param name The username of the player whose skin is to be fetched. Must not be {@code null}.
- * @return A {@link CompletableFuture} that will complete with the {@link Skin} object, or {@code null} if fetching fails.
- */
- public static CompletableFuture fetchSkinAsync(@NotNull String name)
- {
- return CompletableFuture.supplyAsync(() -> fetchSkin(name));
- }
-
- /**
- * Asynchronously fetches a skin from a local file.
- *
- * @param skinFile the PNG file containing the skin
- * @return a CompletableFuture containing an Optional of the Skin
- */
- public static CompletableFuture> fetchSkinAsync(@NotNull File skinFile)
- {
- return CompletableFuture.supplyAsync(() -> fetchSkin(skinFile));
- }
-
- private static byte[] combine(byte[]... arrays) throws IOException
- {
- int length = 0;
- for(byte[] arr : arrays)
- length += arr.length;
- byte[] result = new byte[length];
- int pos = 0;
- for(byte[] arr : arrays)
- {
- System.arraycopy(arr, 0, result, pos, arr.length);
- pos += arr.length;
- }
- return result;
- }
-}
diff --git a/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/pathfinding/AStar.java b/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/pathfinding/AStar.java
deleted file mode 100644
index 4920fa6..0000000
--- a/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/pathfinding/AStar.java
+++ /dev/null
@@ -1,385 +0,0 @@
-package de.eisi05.npc.api.pathfinding;
-
-import org.bukkit.Location;
-import org.bukkit.World;
-import org.bukkit.block.Block;
-import org.jetbrains.annotations.NotNull;
-
-import java.util.*;
-
-/**
- * Implementation of the A* pathfinding algorithm for Bukkit worlds.
- * Calculates paths between two locations with optional diagonal movement.
- */
-public class AStar
-{
- private final int sx;
- private final int sy;
- private final int sz;
- private final int ex;
- private final int ey;
- private final int ez;
-
- private final World w;
- private final int maxIterations;
- private final String endUID;
- private final HashMap open = new HashMap<>();
- private final HashMap closed = new HashMap<>();
- private final Location start;
- private final boolean allowDiagonalMovement;
- private PathingResult result;
-
- /**
- * Constructs an A* pathfinder between two locations.
- *
- * @param start the starting location
- * @param end the target location
- * @param maxIterations maximum number of iterations before aborting
- * @param allowDiagonalMovement whether diagonal movement is allowed
- * @throws InvalidPathException if start or end location is not walkable
- */
- public AStar(Location start, Location end, int maxIterations, boolean allowDiagonalMovement) throws InvalidPathException
- {
- this.start = start;
- this.allowDiagonalMovement = allowDiagonalMovement;
-
- boolean s, e = true;
- if(!(s = isLocationWalkable(start)) || !(e = isLocationWalkable(end)))
- throw new InvalidPathException(s, e);
- this.w = start.getWorld();
- this.sx = start.getBlockX();
- this.sy = start.getBlockY();
- this.sz = start.getBlockZ();
- this.ex = end.getBlockX();
- this.ey = end.getBlockY();
- this.ez = end.getBlockZ();
- this.maxIterations = maxIterations;
- short sh = 0;
- Tile t = new Tile(sh, sh, sh, null);
- t.calculateBoth(this.sx, this.sy, this.sz, this.ex, this.ey, this.ez, true);
- this.open.put(t.getUID(), t);
- processAdjacentTiles(t);
- this.endUID = String.valueOf(this.ex - this.sx) + (this.ey - this.sy) + (this.ez - this.sz);
- }
-
- /**
- * Adds a tile to the open list if not already present.
- *
- * @param t the tile to add
- */
- private void addToOpenList(Tile t)
- {
- if(!this.open.containsKey(t.getUID()))
- this.open.put(t.getUID(), t);
- }
-
- /**
- * Adds a tile to the closed list if not already present.
- *
- * @param t the tile to add
- */
- private void addToClosedList(Tile t)
- {
- if(!this.closed.containsKey(t.getUID()))
- this.closed.put(t.getUID(), t);
- }
-
- /**
- * Returns the starting location of this pathfinding instance.
- *
- * @return the start location
- */
- public Location getStart()
- {
- return start;
- }
-
- /**
- * Returns the end location of this pathfinding instance.
- *
- * @return the end location
- */
- public Location getEndLocation()
- {
- return new Location(this.w, this.ex, this.ey, this.ez);
- }
-
- /**
- * Returns the result of the last pathfinding attempt.
- *
- * @return pathing result status
- */
- public PathingResult getPathingResult()
- {
- return this.result;
- }
-
- /**
- * Runs the A* iteration until a path is found or terminated.
- *
- * @return list of tiles representing the path, or null if no path exists
- */
- public ArrayList iterate()
- {
- Tile current = null;
- int iterations = 0;
- while(canContinue())
- {
- iterations++;
- if(iterations > this.maxIterations)
- {
- this.result = PathingResult.ITERATIONS_EXCEEDED;
- break;
- }
- current = getLowestFTile();
- processAdjacentTiles(current);
- }
-
- if(this.result != PathingResult.SUCCESS)
- return null;
- LinkedList routeTrace = new LinkedList<>();
- routeTrace.add(current);
- Tile parent;
-
- while((parent = current.getParent()) != null)
- {
- routeTrace.add(parent);
- current = parent;
- }
-
- Collections.reverse(routeTrace);
- return new ArrayList<>(routeTrace);
- }
-
- /**
- * Determines if the algorithm can continue.
- * Stops if the open list is empty or the target has been reached.
- *
- * @return true if the search can continue, false otherwise
- */
- private boolean canContinue()
- {
- if(this.open.isEmpty())
- {
- this.result = PathingResult.NO_PATH;
- return false;
- }
- if(this.closed.containsKey(this.endUID))
- {
- this.result = PathingResult.SUCCESS;
- return false;
- }
- return true;
- }
-
- /**
- * Finds and removes the tile with the lowest f-cost from the open list.
- *
- * @return tile with the lowest f-cost
- */
- private Tile getLowestFTile()
- {
- double f = 0.0D;
- Tile drop = null;
- for(Tile t : this.open.values())
- {
- if(f == 0.0D)
- {
- t.calculateBoth(this.sx, this.sy, this.sz, this.ex, this.ey, this.ez, true);
- f = t.getF();
- drop = t;
- continue;
- }
- t.calculateBoth(this.sx, this.sy, this.sz, this.ex, this.ey, this.ez, true);
- double posF = t.getF();
- if(posF < f)
- {
- f = posF;
- drop = t;
- }
- }
- this.open.remove(drop.getUID());
- addToClosedList(drop);
- return drop;
- }
-
- /**
- * Checks whether the given tile is already on the closed list.
- *
- * @param t the tile to check
- * @return true if the tile is on the closed list
- */
- private boolean isOnClosedList(Tile t)
- {
- return this.closed.containsKey(t.getUID());
- }
-
- /**
- * Processes the adjacent tiles around a given tile, adding valid ones to the open list.
- *
- * @param current the current tile to expand from
- */
- private void processAdjacentTiles(Tile current)
- {
- HashSet possible = new HashSet<>(allowDiagonalMovement ? 26 : 14);
-
- if(allowDiagonalMovement)
- for(byte x = -1; x <= 1; x = (byte) (x + 1))
- {
- for(byte y = -1; y <= 1; y = (byte) (y + 1))
- {
- for(byte z = -1; z <= 1; z = (byte) (z + 1))
- {
- if(x != 0 || y != 0 || z != 0)
- {
- Tile t = new Tile((short) (current.getX() + x), (short) (current.getY() + y), (short) (current.getZ() + z), current);
- if(!isOnClosedList(t) && isTileWalkable(t))
- {
- t.calculateBoth(this.sx, this.sy, this.sz, this.ex, this.ey, this.ez, true);
- possible.add(t);
- }
- }
- }
- }
- }
- else
- {
- int[][] directions = {
- {1, 0, 0}, {-1, 0, 0},
- {0, 1, 0}, {0, -1, 0},
- {0, 0, 1}, {0, 0, -1},
- {0, -1, 1}, {0, -1, -1},
- {0, 1, 1}, {0, 1, -1},
- {1, -1, 0}, {-1, -1, 0},
- {1, 1, 0}, {-1, 1, 0},
- };
-
- for(int[] d : directions)
- {
- Tile t = new Tile(
- (short) (current.getX() + d[0]),
- (short) (current.getY() + d[1]),
- (short) (current.getZ() + d[2]),
- current
- );
- if(!isOnClosedList(t) && isTileWalkable(t))
- {
- t.calculateBoth(this.sx, this.sy, this.sz, this.ex, this.ey, this.ez, true);
- possible.add(t);
- }
- }
- }
-
- for(Tile t : possible)
- {
- Tile openRef;
- if((openRef = isOnOpenList(t)) == null)
- {
- addToOpenList(t);
- continue;
- }
- if(t.getG() < openRef.getG())
- {
- openRef.setParent(current);
- openRef.calculateBoth(this.sx, this.sy, this.sz, this.ex, this.ey, this.ez, true);
- }
- }
- }
-
- /**
- * Returns a tile if it is already on the open list.
- *
- * @param t the tile to check
- * @return the reference from the open list, or null if not present
- */
- private Tile isOnOpenList(Tile t)
- {
- return this.open.getOrDefault(t.getUID(), null);
- }
-
- /**
- * Determines if the given tile is walkable (i.e., solid ground with space above).
- *
- * @param t the tile to check
- * @return true if walkable, false otherwise
- */
- private boolean isTileWalkable(Tile t)
- {
- Location l = new Location(this.w, (this.sx + t.getX()), (this.sy + t.getY()), (this.sz + t.getZ()));
- Block b = l.getBlock();
- return !b.isLiquid() && b.getType().isSolid() && (b.getRelative(0, 1, 0).isPassable() && b.getRelative(0, 2, 0).isPassable());
- }
-
- /**
- * Checks if the given location is a valid starting or ending point.
- *
- * @param l the location to check
- * @return true if the location is walkable, false otherwise
- */
- private boolean isLocationWalkable(Location l)
- {
- Block b = l.getBlock();
-
- if(!b.isLiquid() && b.getType().isSolid())
- return ((b.getRelative(0, 1, 0).getType().isAir() || b.getRelative(0, 1, 0).isPassable()) &&
- (b.getRelative(0, 2, 0).getType().isAir() || b.getRelative(0, 2, 0).isPassable()));
- return false;
- }
-
- /**
- * Exception thrown when path initialization fails due to invalid start or end.
- */
- public static class InvalidPathException extends Exception
- {
- private final boolean s;
- private final boolean e;
-
- /**
- * Creates an exception describing invalid start or end tiles.
- *
- * @param s whether the start is valid
- * @param e whether the end is valid
- */
- public InvalidPathException(boolean s, boolean e)
- {
- super(getErrorReason(s, e));
- this.s = s;
- this.e = e;
- }
-
- /**
- * Returns a textual explanation of why the path is invalid.
- *
- * @return error reason string
- */
- private static @NotNull String getErrorReason(boolean s, boolean e)
- {
- StringBuilder sb = new StringBuilder();
- if(!s)
- sb.append("Start Location was air.");
- if(!e)
- sb.append("End Location was air.");
- return sb.toString();
- }
-
- /**
- * Checks if the start location was invalid.
- *
- * @return true if the start location is not solid
- */
- public boolean isStartNotSolid()
- {
- return !this.s;
- }
-
- /**
- * Checks if the end location was invalid.
- *
- * @return true if the end location is not solid
- */
- public boolean isEndNotSolid()
- {
- return !this.e;
- }
- }
-}
diff --git a/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/pathfinding/Path.java b/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/pathfinding/Path.java
deleted file mode 100644
index 3c878b4..0000000
--- a/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/pathfinding/Path.java
+++ /dev/null
@@ -1,196 +0,0 @@
-package de.eisi05.npc.api.pathfinding;
-
-import org.bukkit.Bukkit;
-import org.bukkit.Location;
-import org.bukkit.World;
-import org.bukkit.configuration.serialization.ConfigurationSerializable;
-import org.bukkit.util.Vector;
-import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
-
-import java.io.Serial;
-import java.io.Serializable;
-import java.util.*;
-
-
-/**
- * Represents a path in the world, providing both {@link Location} and {@link Vector} representations
- * of the path's waypoints. The lists returned are unmodifiable to ensure immutability.
- */
-public class Path implements ConfigurationSerializable
-{
- private final List vectors;
- private final List locations;
- private final List waypoints;
-
- private String name;
-
- /**
- * Constructs a Path from a list of Bukkit {@link Location} objects.
- * Converts each location to a {@link Vector} for easy mathematical manipulation.
- *
- * @param nodes the ordered list of {@link Location} waypoints
- */
- public Path(@NotNull List nodes, @Nullable List waypoints)
- {
- this.locations = Collections.unmodifiableList(nodes);
- this.vectors = nodes.stream().map(Location::toVector).toList();
- this.waypoints = waypoints == null ? null : Collections.unmodifiableList(waypoints);
- }
-
- /**
- * Constructs a Path from a list of {@link Vector} waypoints in the given world.
- * Converts each vector to a {@link Location} for compatibility with Bukkit APIs.
- *
- * @param nodes the ordered list of {@link Vector} waypoints
- * @param world the Bukkit {@link World} where the locations reside
- */
- public Path(@NotNull List nodes, @NotNull World world, @Nullable List waypoints)
- {
- this.vectors = Collections.unmodifiableList(nodes);
- this.locations = nodes.stream().map(vector -> new Location(world, vector.getX(), vector.getY(), vector.getZ())).toList();
- this.waypoints = waypoints == null ? null : Collections.unmodifiableList(waypoints);
- }
-
- public @NotNull Path setName(@Nullable String name)
- {
- this.name = name;
- return this;
- }
-
- public @Nullable String getName()
- {
- return name;
- }
-
- @SuppressWarnings("unchecked")
- public static Path deserialize(Map map)
- {
- return new Path((List) map.get("locations"), (List) map.get("waypoints"));
- }
-
- /**
- * Returns the path as an unmodifiable list of {@link Location} objects.
- *
- * @return an unmodifiable list of Bukkit locations representing the path
- */
- public List asLocations()
- {
- return locations;
- }
-
- /**
- * Returns the path as an unmodifiable list of {@link Vector} objects.
- *
- * @return an unmodifiable list of vectors representing the path
- */
- public List asVectors()
- {
- return vectors;
- }
-
- @Override
- public String toString()
- {
- if(locations.isEmpty())
- return "Empty path";
-
- Vector start = vectors.getFirst();
- Vector end = vectors.getLast();
-
- return String.format("Start: [%.2f, %.2f, %.2f] -> End: [%.2f, %.2f, %.2f]",
- start.getX(), start.getY(), start.getZ(),
- end.getX(), end.getY(), end.getZ());
- }
-
- /**
- * Returns th waypoints with which the path was calculated.
- *
- * @return an unmodifiable list of {@link Location} objects
- */
- public @NotNull List getWaypoints()
- {
- return waypoints == null ? new ArrayList<>() : waypoints;
- }
-
- @Override
- public boolean equals(Object obj)
- {
- if(this == obj)
- return true;
-
- if(!(obj instanceof Path other))
- return false;
-
- return locations.equals(other.locations);
- }
-
- @Override
- public int hashCode()
- {
- return locations.hashCode();
- }
-
- @Override
- public @NotNull Map serialize()
- {
- return Map.of("locations", new ArrayList<>(locations), "waypoints", new ArrayList<>(waypoints));
- }
-
- public SerializablePath toSerializablePath()
- {
- return new SerializablePath(this);
- }
-
- public static class SerializablePath implements Serializable
- {
- @Serial
- private static final long serialVersionUID = 1L;
-
- private final List locations;
- private final List waypoints;
-
- private final String name;
-
- private SerializablePath(@NotNull Path path)
- {
- locations = new ArrayList<>(path.locations.stream().map(SerializableLocation::new).toList());
- waypoints = path.waypoints == null ? null : new ArrayList<>(path.waypoints.stream().map(SerializableLocation::new).toList());
- name = path.getName();
- }
-
- public @NotNull Path toPath()
- {
- return new Path(locations.stream().map(SerializableLocation::toLocation).toList(),
- waypoints == null ? null : waypoints.stream().map(SerializableLocation::toLocation).toList()).setName(name);
- }
-
- private static class SerializableLocation implements Serializable
- {
- @Serial
- private static final long serialVersionUID = 1L;
-
- private final double x;
- private final double y;
- private final double z;
- private final float pitch;
- private final float yaw;
- private final UUID world;
-
- public SerializableLocation(@NotNull Location location)
- {
- this.x = location.getX();
- this.y = location.getY();
- this.z = location.getZ();
- this.pitch = location.getPitch();
- this.yaw = location.getYaw();
- this.world = location.getWorld().getUID();
- }
-
- public @NotNull Location toLocation()
- {
- return new Location(Bukkit.getWorld(world), x, y, z, yaw, pitch);
- }
- }
- }
-}
diff --git a/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/pathfinding/PathfindingUtils.java b/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/pathfinding/PathfindingUtils.java
deleted file mode 100644
index 0d54976..0000000
--- a/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/pathfinding/PathfindingUtils.java
+++ /dev/null
@@ -1,97 +0,0 @@
-package de.eisi05.npc.api.pathfinding;
-
-import org.bukkit.Location;
-import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.concurrent.CompletableFuture;
-import java.util.function.BiConsumer;
-
-/**
- * Utility class for calculating paths between locations using A* pathfinding.
- * Provides synchronous and asynchronous methods.
- */
-public class PathfindingUtils
-{
- /**
- * Asynchronously calculates a path through a list of waypoints.
- *
- * Each segment between consecutive waypoints is calculated in parallel using {@link CompletableFuture}.
- * The returned future completes with a {@link Path} containing the full path, or completes exceptionally
- * if an {@link AStar.InvalidPathException} occurs.
- *
- * @param waypoints the ordered list of locations to traverse
- * @param maxIterations the maximum number of iterations the A* algorithm will attempt per segment
- * @param allowDiagonalMovement whether diagonal movement is allowed
- * @param progressListener a progress listener with the signature (segmentIndex, totalSegments)
- * @return a {@link CompletableFuture} that completes with the calculated {@link Path}
- */
- public static @NotNull CompletableFuture findPathAsync(@NotNull List waypoints, int maxIterations, boolean allowDiagonalMovement,
- @Nullable BiConsumer progressListener)
- {
- return CompletableFuture.supplyAsync(() ->
- {
- try
- {
- return findPath(waypoints, maxIterations, allowDiagonalMovement, progressListener);
- } catch(AStar.InvalidPathException e)
- {
- throw new RuntimeException(e);
- }
- });
- }
-
- /**
- * Synchronously calculates a path through a list of waypoints.
- *
- * Each segment between consecutive waypoints is calculated in parallel internally using {@link CompletableFuture},
- * but this method blocks until all segments are calculated and combined into a single {@link Path}.
- *
- * @param waypoints the ordered list of locations to traverse
- * @param maxIterations the maximum number of iterations the A* algorithm will attempt per segment
- * @param allowDiagonalMovement whether diagonal movement is allowed
- * @param progressListener a progress listener with the signature (segmentIndex, totalSegments)
- * @return the calculated {@link Path} containing all intermediate locations
- * @throws AStar.InvalidPathException if any segment's start or end location is invalid/unwalkable
- */
- public static @NotNull Path findPath(@NotNull List waypoints, int maxIterations, boolean allowDiagonalMovement,
- @Nullable BiConsumer progressListener) throws AStar.InvalidPathException
- {
- List>> futures = new ArrayList<>();
-
- for(int i = 0; i < waypoints.size() - 1; i++)
- {
- final int idx = i;
- futures.add(CompletableFuture.supplyAsync(() ->
- {
- try
- {
- AStar aStar = new AStar(waypoints.get(idx), waypoints.get(idx + 1), maxIterations, allowDiagonalMovement);
- var segment = aStar.iterate().stream()
- .map(tile -> tile.getLocation(aStar.getStart()).add(0.5, 1, 0.5))
- .toList();
-
- if(progressListener != null)
- progressListener.accept(idx + 1, waypoints.size());
-
- return segment;
- } catch(AStar.InvalidPathException e)
- {
- throw new RuntimeException(e);
- }
- }));
- }
-
- Path path = new Path(futures.stream()
- .map(CompletableFuture::join)
- .flatMap(List::stream)
- .toList(), waypoints);
-
- if(progressListener != null)
- progressListener.accept(waypoints.size(), waypoints.size());
-
- return path;
- }
-}
diff --git a/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/pathfinding/PathingResult.java b/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/pathfinding/PathingResult.java
deleted file mode 100644
index c1c0815..0000000
--- a/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/pathfinding/PathingResult.java
+++ /dev/null
@@ -1,44 +0,0 @@
-package de.eisi05.npc.api.pathfinding;
-
-/**
- * Represents the result of an A* pathfinding attempt.
- */
-public enum PathingResult
-{
- /**
- * Pathfinding succeeded and a path was found.
- */
- SUCCESS(0),
-
- /**
- * No valid path could be found.
- */
- NO_PATH(-1),
-
- /**
- * Pathfinding stopped because the maximum iteration limit was exceeded.
- */
- ITERATIONS_EXCEEDED(-2);
-
- private final int ec;
-
- /**
- * Creates a new pathing result with the given end code.
- *
- * @param ec numerical code representing the result
- */
- PathingResult(int ec)
- {
- this.ec = ec;
- }
-
- /**
- * Returns the numerical code associated with this pathing result.
- *
- * @return the end code
- */
- public int getEndCode()
- {
- return this.ec;
- }
-}
diff --git a/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/pathfinding/Tile.java b/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/pathfinding/Tile.java
deleted file mode 100644
index c8f8d3a..0000000
--- a/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/pathfinding/Tile.java
+++ /dev/null
@@ -1,284 +0,0 @@
-package de.eisi05.npc.api.pathfinding;
-
-import org.bukkit.Location;
-
-/**
- * Represents a tile (node) used in the A* pathfinding algorithm.
- * Each tile stores its relative coordinates, costs, and parent reference.
- */
-public class Tile
-{
- private final short x;
- private final short y;
- private final short z;
-
- private final String uid;
- private double g = -1.0D;
- private double h = -1.0D;
- private Tile parent;
-
- /**
- * Creates a new tile with given relative coordinates and parent.
- *
- * @param x relative x-coordinate
- * @param y relative y-coordinate
- * @param z relative z-coordinate
- * @param parent the parent tile leading to this tile
- */
- public Tile(short x, short y, short z, Tile parent)
- {
- this.x = x;
- this.y = y;
- this.z = z;
- this.parent = parent;
- this.uid = String.valueOf(x) + y + z;
- }
-
- /**
- * Converts this tile to an absolute {@link Location} using a start reference.
- *
- * @param start the starting location
- * @return absolute location of this tile
- */
- public Location getLocation(Location start)
- {
- return new Location(start.getWorld(), (start.getBlockX() + this.x), (start.getBlockY() + this.y), (start.getBlockZ() + this.z));
- }
-
- /**
- * Returns the parent tile of this tile.
- *
- * @return the parent tile, or null if none
- */
- public Tile getParent()
- {
- return this.parent;
- }
-
- /**
- * Updates the parent tile reference.
- *
- * @param parent the new parent tile
- */
- public void setParent(Tile parent)
- {
- this.parent = parent;
- }
-
- /**
- * Returns the relative x-coordinate.
- *
- * @return relative x
- */
- public short getX()
- {
- return this.x;
- }
-
- /**
- * Returns the absolute x-coordinate relative to a base location.
- *
- * @param i base location
- * @return absolute x
- */
- public int getX(Location i)
- {
- return i.getBlockX() + this.x;
- }
-
- /**
- * Returns the relative y-coordinate.
- *
- * @return relative y
- */
- public short getY()
- {
- return this.y;
- }
-
- /**
- * Returns the absolute y-coordinate relative to a base location.
- *
- * @param i base location
- * @return absolute y
- */
- public int getY(Location i)
- {
- return i.getBlockY() + this.y;
- }
-
- /**
- * Returns the relative z-coordinate.
- *
- * @return relative z
- */
- public short getZ()
- {
- return this.z;
- }
-
- /**
- * Returns the absolute z-coordinate relative to a base location.
- *
- * @param i base location
- * @return absolute z
- */
- public int getZ(Location i)
- {
- return i.getBlockZ() + this.z;
- }
-
- /**
- * Returns the unique identifier string for this tile.
- *
- * @return tile UID
- */
- public String getUID()
- {
- return this.uid;
- }
-
- /**
- * Checks whether this tile is equal to another based on coordinates.
- *
- * @param t the other tile
- * @return true if coordinates match
- */
- public boolean equals(Tile t)
- {
- return (t.getX() == this.x && t.getY() == this.y && t.getZ() == this.z);
- }
-
- /**
- * Calculates both G and H values for this tile.
- *
- * @param sx start x
- * @param sy start y
- * @param sz start z
- * @param ex end x
- * @param ey end y
- * @param ez end z
- * @param update whether to force recalculation
- */
- public void calculateBoth(int sx, int sy, int sz, int ex, int ey, int ez, boolean update)
- {
- calculateG(sx, sy, sz, update);
- calculateH(sx, sy, sz, ex, ey, ez, update);
- }
-
- /**
- * Calculates the heuristic (H) cost using Euclidean distance.
- *
- * @param sx start x
- * @param sy start y
- * @param sz start z
- * @param ex end x
- * @param ey end y
- * @param ez end z
- * @param update whether to force recalculation
- */
- public void calculateH(int sx, int sy, int sz, int ex, int ey, int ez, boolean update)
- {
- if(update || this.h == -1.0D)
- {
- int hx = sx + this.x, hy = sy + this.y, hz = sz + this.z;
- this.h = getEuclideanDistance(hx, hy, hz, ex, ey, ez);
- }
- }
-
- /**
- * Calculates the movement cost (G) from the start to this tile.
- *
- * @param sx start x
- * @param sy start y
- * @param sz start z
- * @param update whether to force recalculation
- */
- public void calculateG(int sx, int sy, int sz, boolean update)
- {
- if(update || this.g == -1.0D)
- {
- Tile currentParent, currentTile = this;
- int gCost = 0;
- while((currentParent = currentTile.getParent()) != null)
- {
- int dx = currentTile.getX() - currentParent.getX(),
- dy = currentTile.getY() - currentParent.getY(),
- dz = currentTile.getZ() - currentParent.getZ();
-
- dx = abs(dx);
- dy = abs(dy);
- dz = abs(dz);
-
- if(dx == 1 && dy == 1 && dz == 1)
- gCost = (int) (gCost + 1.7D);
-
- else if(((dx == 1 || dz == 1) && dy == 1) || ((dx == 1 || dz == 1) && dy == 0))
- gCost = (int) (gCost + 1.4D);
- else
- gCost = (int) (gCost + 1.0D);
-
- currentTile = currentParent;
- }
- this.g = gCost;
- }
- }
-
- /**
- * Returns the G cost (movement cost from start).
- *
- * @return g cost
- */
- public double getG()
- {
- return this.g;
- }
-
- /**
- * Returns the H cost (heuristic estimate to end).
- *
- * @return h cost
- */
- public double getH()
- {
- return this.h;
- }
-
- /**
- * Returns the total F cost (G + H).
- *
- * @return f cost
- */
- public double getF()
- {
- return this.h + this.g;
- }
-
- /**
- * Computes Euclidean distance between two points.
- *
- * @param sx start x
- * @param sy start y
- * @param sz start z
- * @param ex end x
- * @param ey end y
- * @param ez end z
- * @return Euclidean distance
- */
- private double getEuclideanDistance(int sx, int sy, int sz, int ex, int ey, int ez)
- {
- double dx = (sx - ex), dy = (sy - ey), dz = (sz - ez);
- return Math.sqrt(dx * dx + dy * dy + dz * dz);
- }
-
- /**
- * Returns the absolute value of the given integer.
- *
- * @param i input value
- * @return absolute value
- */
- private int abs(int i)
- {
- return (i < 0) ? -i : i;
- }
-}
diff --git a/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/scheduler/Tasks.java b/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/scheduler/Tasks.java
deleted file mode 100644
index 64ae065..0000000
--- a/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/scheduler/Tasks.java
+++ /dev/null
@@ -1,66 +0,0 @@
-package de.eisi05.npc.api.scheduler;
-
-import de.eisi05.npc.api.NpcApi;
-import de.eisi05.npc.api.manager.NpcManager;
-import de.eisi05.npc.api.objects.NpcOption;
-import net.minecraft.server.level.ServerPlayer;
-import org.bukkit.entity.Player;
-import org.bukkit.scheduler.BukkitRunnable;
-import org.bukkit.scheduler.BukkitTask;
-
-/**
- * The {@link Tasks} class manages and starts various recurring tasks
- * related to Non-Player Characters (NPCs) within the Bukkit environment.
- * These tasks often involve NPC behavior such as looking at nearby players.
- */
-public class Tasks
-{
- private static BukkitTask task;
-
- /**
- * Starts all defined NPC-related tasks.
- * This method should be called when the plugin is enabled to ensure
- * that NPC behaviors are active.
- */
- public static void start()
- {
- lookAtTask();
- }
-
- /**
- * Stops all defined NPC-related tasks.
- */
- public static void stop()
- {
- if(task != null && !task.isCancelled())
- task.cancel();
- }
-
- /**
- * Implements a recurring task that makes NPCs look at nearby players.
- * The task runs on a timer defined by {@code NpcApi.config.getLookAtTimer()}.
- * NPCs will only look at players within a specified range, which is
- * configured via {@link NpcOption#LOOK_AT_PLAYER}.
- */
- private static void lookAtTask()
- {
- task = new BukkitRunnable()
- {
- @Override
- public void run()
- {
- NpcManager.getList().forEach(npc ->
- {
- double range = npc.getOption(NpcOption.LOOK_AT_PLAYER);
-
- if(range <= 0)
- return;
-
- ((ServerPlayer) npc.getServerPlayer()).getBukkitEntity().getNearbyEntities(range, range, range)
- .stream().filter(entity -> entity instanceof Player)
- .forEach(entity -> npc.lookAtPlayer((Player) entity));
- });
- }
- }.runTaskTimer(NpcApi.plugin, 0, NpcApi.config.lookAtTimer());
- }
-}
diff --git a/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/utils/ItemSerializer.java b/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/utils/ItemSerializer.java
deleted file mode 100644
index 614a8a9..0000000
--- a/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/utils/ItemSerializer.java
+++ /dev/null
@@ -1,206 +0,0 @@
-package de.eisi05.npc.api.utils;
-
-import org.bukkit.Bukkit;
-import org.bukkit.inventory.Inventory;
-import org.bukkit.inventory.ItemStack;
-import org.bukkit.inventory.PlayerInventory;
-import org.bukkit.util.io.BukkitObjectInputStream;
-import org.bukkit.util.io.BukkitObjectOutputStream;
-import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
-import org.yaml.snakeyaml.external.biz.base64Coder.Base64Coder;
-
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.ObjectInputStream;
-import java.io.ObjectOutputStream;
-
-/**
- * The {@link ItemSerializer} class provides utility methods for serializing and deserializing
- * Bukkit {@link ItemStack} arrays, {@link PlayerInventory} contents, and generic {@link Inventory}
- * contents to and from Base64 encoded strings. This is useful for storing inventory data
- * in configurations or databases.
- */
-public class ItemSerializer
-{
- /**
- * Serializes the contents of a {@link PlayerInventory} into a Base64 encoded string array.
- * The array will contain two elements: the first for the main inventory storage contents,
- * and the second for the armor contents plus the off-hand item.
- *
- * @param playerInventory The {@link PlayerInventory} to serialize. Must not be {@code null}.
- * @return A {@code String[]} array containing two Base64 encoded strings:
- * index 0 is the main inventory, index 1 is armor and off-hand.
- * @throws IllegalStateException If an error occurs during serialization (e.g., I/O error).
- */
- public static @NotNull String[] playerInventoryToBase64(@NotNull PlayerInventory playerInventory) throws IllegalStateException
- {
- ItemStack[] storage = playerInventory.getStorageContents();
- if(storage == null)
- throw new IllegalStateException("Storage contents of player inventory is null");
-
- String content = itemStackArrayToBase64(storage);
- ItemStack[] items = new ItemStack[playerInventory.getArmorContents().length + 1];
-
- for(int i = 0; i < playerInventory.getArmorContents().length; ++i)
- items[i] = playerInventory.getArmorContents()[i];
-
- items[items.length - 1] = playerInventory.getItemInOffHand();
- String armor = itemStackArrayToBase64(items);
- return new String[]{content, armor};
- }
-
- /**
- * Serializes a single {@link ItemStack} into a Base64 encoded string.
- * This is a convenience method that wraps the item in an array and calls {@link #itemStackArrayToBase64(ItemStack[])}.
- *
- * @param item The {@link ItemStack} to serialize. Must not be {@code null}.
- * @return A Base64 encoded {@link String} representing the item.
- * Returns {@code null} if an error occurs during serialization.
- */
- public static @NotNull String itemStackToBase64(@NotNull ItemStack item)
- {
- return itemStackArrayToBase64(new ItemStack[]{item});
- }
-
- /**
- * Deserializes a single {@link ItemStack} from a Base64 encoded string.
- * This method expects the Base64 string to represent a single item.
- *
- * @param data The Base64 encoded {@link String} representing the item. Must not be {@code null}.
- * @return The deserialized {@link ItemStack}, or {@code null} if an error occurs during deserialization
- * or if the input data does not represent a valid item.
- */
- public static @Nullable ItemStack itemStackFromBase64(@NotNull String data)
- {
- try
- {
- return itemStackArrayFromBase64(data)[0];
- } catch(Exception e)
- {
- return null;
- }
- }
-
- /**
- * Serializes an array of {@link ItemStack} objects into a Base64 encoded string.
- * This method uses Bukkit's object serialization for {@link ItemStack}s.
- *
- * @param items An array of {@link ItemStack} objects to serialize. Must not be {@code null}.
- * @return A Base64 encoded {@link String} representing the array of items.
- * Returns {@code null} if an error occurs during serialization.
- */
- public static @Nullable String itemStackArrayToBase64(@NotNull ItemStack[] items)
- {
- try
- {
- ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
- ObjectOutputStream dataOutput = new ObjectOutputStream(outputStream);
- dataOutput.writeInt(items.length);
-
- for(ItemStack item : items)
- dataOutput.writeObject(item);
-
- dataOutput.close();
- return Base64Coder.encodeLines(outputStream.toByteArray());
- } catch(Exception e)
- {
- return null;
- }
- }
-
- /**
- * Serializes the contents of a generic {@link Inventory} into a Base64 encoded string.
- * This method is suitable for any type of inventory (e.g., chests, custom inventories).
- *
- * @param inventory The {@link Inventory} to serialize. Must not be {@code null}.
- * @return A Base64 encoded {@link String} representing the inventory contents.
- * Returns an empty string if an error occurs during serialization.
- */
- public static @NotNull String toBase64(@NotNull Inventory inventory)
- {
- try
- {
- ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
- ObjectOutputStream dataOutput = new ObjectOutputStream(outputStream);
- dataOutput.writeInt(inventory.getSize());
-
- for(ItemStack item : inventory.getContents())
- dataOutput.writeObject(item);
-
- dataOutput.close();
- return Base64Coder.encodeLines(outputStream.toByteArray());
- } catch(Exception e)
- {
- return "";
- }
- }
-
- /**
- * Deserializes two Base64 encoded strings back into two {@link ItemStack} arrays.
- * This is typically used for deserializing player inventory content and armor/off-hand.
- *
- * @param s A {@code String[]} array containing two Base64 encoded strings,
- * where {@code s[0]} is the main inventory and {@code s[1]} is armor/off-hand. Must not be {@code null}.
- * @return A two-dimensional {@code ItemStack[][]} array, where the first array
- * contains the deserialized main inventory items and the second array
- * contains the deserialized armor and off-hand items.
- */
- public static @NotNull ItemStack[][] doubleInventoryFromBase64(@NotNull String[] s)
- {
- return new ItemStack[][]{itemStackArrayFromBase64(s[0]), itemStackArrayFromBase64(s[1])};
- }
-
- /**
- * Deserializes a Base64 encoded string back into a Bukkit {@link Inventory} object.
- * The inventory's size and contents are restored from the provided data.
- *
- * @param data The Base64 encoded {@link String} representing the inventory. Must not be {@code null}.
- * @return The deserialized {@link Inventory} object, or {@code null} if an error occurs
- * during deserialization or if the input data is invalid.
- */
- public static @Nullable Inventory fromBase64(@NotNull String data)
- {
- try
- {
- ByteArrayInputStream inputStream = new ByteArrayInputStream(Base64Coder.decodeLines(data));
- ObjectInputStream dataInput = new ObjectInputStream(inputStream);
- Inventory inventory = Bukkit.getServer().createInventory(null, dataInput.readInt());
-
- for(int i = 0; i < inventory.getSize(); ++i)
- inventory.setItem(i, (ItemStack) dataInput.readObject());
-
- dataInput.close();
- return inventory;
- } catch(Exception e)
- {
- return null;
- }
- }
-
- /**
- * Deserializes a Base64 encoded string back into an array of {@link ItemStack} objects.
- *
- * @param data The Base64 encoded {@link String} representing the array of items. Must not be {@code null}.
- * @return An array of deserialized {@link ItemStack} objects.
- * Returns an empty {@code ItemStack[]} array if an error occurs during deserialization.
- */
- public static @NotNull ItemStack[] itemStackArrayFromBase64(@NotNull String data)
- {
- try
- {
- ByteArrayInputStream inputStream = new ByteArrayInputStream(Base64Coder.decodeLines(data));
- ObjectInputStream dataInput = new ObjectInputStream(inputStream);
- ItemStack[] items = new ItemStack[dataInput.readInt()];
-
- for(int i = 0; i < items.length; ++i)
- items[i] = (ItemStack) dataInput.readObject();
-
- dataInput.close();
- return items;
- } catch(Exception e)
- {
- return new ItemStack[0];
- }
- }
-}
diff --git a/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/utils/ObjectSaver.java b/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/utils/ObjectSaver.java
deleted file mode 100644
index 4f5d94e..0000000
--- a/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/utils/ObjectSaver.java
+++ /dev/null
@@ -1,164 +0,0 @@
-package de.eisi05.npc.api.utils;
-
-import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
-
-import java.io.*;
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * The {@link ObjectSaver} class provides utility methods for serializing and deserializing
- * Java objects and lists of objects to and from a file. It uses standard Java
- * object serialization.
- */
-public class ObjectSaver
-{
- private final File file;
-
- /**
- * Constructs a new {@code ObjectSaver} instance, creating a {@link File} object
- * from the given file path string. It ensures the parent directories exist
- * and creates the file if it doesn't already exist.
- *
- * @param file The path to the file where objects will be saved/loaded. Must not be {@code null}.
- * @throws RuntimeException If an {@link IOException} occurs while creating the file.
- */
- public ObjectSaver(@NotNull String file)
- {
- this(new File(file));
- }
-
- /**
- * Constructs a new {@code ObjectSaver} instance with the specified {@link File} object.
- * It ensures the parent directories of the file exist and creates the file itself
- * if it does not already exist.
- *
- * @param file The {@link File} object where objects will be saved/loaded. Must not be {@code null}.
- * @throws RuntimeException If an {@link IOException} occurs while creating the file.
- */
- public ObjectSaver(@NotNull File file)
- {
- file.getParentFile().mkdirs();
- this.file = file;
- if(!file.exists())
- {
- try
- {
- file.createNewFile();
- } catch(IOException e)
- {
- throw new RuntimeException(e);
- }
- }
- }
-
- /**
- * Writes a single serializable object to the file, appending to the file if it already exists.
- * This is a convenience method that calls {@link #write(Serializable, boolean)} with {@code append} set to {@code true}.
- *
- * @param object The object to write to the file. Must implement {@link Serializable}.
- * @param The type of the object, which must extend {@link Serializable}.
- * @throws IOException If an I/O error occurs during writing.
- */
- public void write(T object) throws IOException
- {
- this.write(object, true);
- }
-
- /**
- * Writes a single serializable object to the file.
- *
- * @param object The object to write to the file. Must implement {@link Serializable}.
- * @param append If {@code true}, the object will be appended to the end of the file.
- * If {@code false}, the file will be truncated (its contents deleted)
- * before writing the new object.
- * @param The type of the object, which must extend {@link Serializable}.
- * @throws IOException If an I/O error occurs during writing.
- */
- public void write(T object, boolean append) throws IOException
- {
- FileOutputStream fileOut = new FileOutputStream(this.file, append);
- ObjectOutputStream objectOut = new ObjectOutputStream(fileOut);
- objectOut.writeObject(object);
- objectOut.close();
- }
-
- /**
- * Writes a list of serializable objects to the file, appending to the file if it already exists.
- * This is a convenience method that calls {@link #writeList(List, boolean)} with {@code append} set to {@code true}.
- *
- * @param object The list of objects to write to the file. Each object in the list must implement {@link Serializable}. Must not be {@code null}.
- * @param The type of the objects in the list, which must extend {@link Serializable}.
- * @throws IOException If an I/O error occurs during writing.
- */
- public void writeList(@NotNull List object) throws IOException
- {
- this.writeList(object, true);
- }
-
- /**
- * Writes a list of serializable objects to the file.
- *
- * @param object The list of objects to write to the file. Each object in the list must implement {@link Serializable}. Must not be {@code null}.
- * @param append If {@code true}, the list will be appended to the end of the file.
- * If {@code false}, the file will be truncated (its contents deleted)
- * before writing the new list.
- * @param The type of the objects in the list, which must extend {@link Serializable}.
- * @throws IOException If an I/O error occurs during writing.
- */
- public void writeList(@NotNull List object, boolean append) throws IOException
- {
- FileOutputStream fileOut = new FileOutputStream(this.file, append);
- ObjectOutputStream objectOut = new ObjectOutputStream(fileOut);
- objectOut.writeObject(object);
- objectOut.close();
- }
-
- /**
- * Reads a single serializable object from the file.
- *
- * @param The expected type of the object, which must extend {@link Serializable}.
- * @return The deserialized object, or {@code null} if an error occurs during reading
- * (e.g., file not found, EOF, class not found, or I/O error).
- */
- @SuppressWarnings("unchecked")
- public @Nullable T read()
- {
- try
- {
- FileInputStream fileIn = new FileInputStream(this.file);
- ObjectInputStream objectOut = new ObjectInputStream(fileIn);
- Object object = objectOut.readObject();
- objectOut.close();
- return (T) object;
- } catch(IOException | ClassNotFoundException e)
- {
- throw new RuntimeException(e);
- }
- }
-
- /**
- * Reads a list of serializable objects from the file.
- *
- * @param The expected type of the objects in the list, which must extend {@link Serializable}.
- * @return The deserialized list of objects. Returns an empty {@link ArrayList} if an error occurs
- * during reading (e.g., file not found, EOF, class not found, or I/O error), or if the file is empty.
- */
- @SuppressWarnings("unchecked")
- public @NotNull List readList()
- {
- try
- {
- FileInputStream fileIn = new FileInputStream(this.file);
- ObjectInputStream objectOut = new ObjectInputStream(fileIn);
- Object object = objectOut.readObject();
- objectOut.close();
- return (List) object;
- } catch(Exception var4)
- {
- return new ArrayList();
- }
- }
-}
-
diff --git a/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/utils/PacketReader.java b/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/utils/PacketReader.java
deleted file mode 100644
index 12163b3..0000000
--- a/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/utils/PacketReader.java
+++ /dev/null
@@ -1,160 +0,0 @@
-package de.eisi05.npc.api.utils;
-
-import de.eisi05.npc.api.NpcApi;
-import de.eisi05.npc.api.enums.ClickActionType;
-import de.eisi05.npc.api.events.NpcInteractEvent;
-import de.eisi05.npc.api.manager.NpcManager;
-import de.eisi05.npc.api.objects.NPC;
-import io.netty.channel.Channel;
-import io.netty.channel.ChannelDuplexHandler;
-import io.netty.channel.ChannelHandlerContext;
-import net.minecraft.network.protocol.Packet;
-import net.minecraft.network.protocol.game.ServerboundInteractPacket;
-import net.minecraft.server.level.ServerPlayer;
-import net.minecraft.world.InteractionHand;
-import org.bukkit.Bukkit;
-import org.bukkit.craftbukkit.entity.CraftPlayer;
-import org.bukkit.entity.Player;
-import org.jetbrains.annotations.NotNull;
-
-import java.util.*;
-import java.util.function.BiConsumer;
-
-/**
- * The {@link PacketReader} class is responsible for injecting a custom Netty
- * channel handler into a player's network pipeline to intercept incoming packets.
- * It specifically listens for packets related to entity interaction (e.g., attacking or interacting with NPCs)
- * and dispatches custom events based on these interactions. It also allows for
- * custom packet readers to be added.
- */
-public class PacketReader
-{
- private static final Map channels = new HashMap<>();
- private static final List> readers = new ArrayList<>();
-
- /**
- * Adds a custom packet reader to the list of readers.
- * This reader will be called for every incoming packet processed by the injected handler.
- *
- * @param reader The {@link BiConsumer} to add. It accepts the {@link Player}
- * and the raw packet {@link Object}. Must not be {@code null}.
- */
- public static void addReader(@NotNull BiConsumer reader)
- {
- readers.add(reader);
- }
-
- /**
- * Injects a custom {@link ChannelDuplexHandler} into the specified player's Netty pipeline.
- * This handler intercepts incoming packets to check for NPC interactions.
- * The handler is named after the plugin's name to avoid conflicts and ensure proper removal.
- *
- * @param player The {@link Player} whose pipeline is to be injected. Must not be {@code null}.
- */
- public static void inject(@NotNull Player player)
- {
- Channel channel = ((CraftPlayer) player).getHandle().connection.connection.channel;
- channels.put(player.getUniqueId(), channel);
-
- if(channel.pipeline().get(NpcApi.plugin.getName()) != null)
- return;
-
- ChannelDuplexHandler duplexHandler = new ChannelDuplexHandler()
- {
- @Override
- public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception
- {
- checkForPacket(msg, player);
-
- readers.forEach(consumer -> consumer.accept(player, msg));
-
- super.channelRead(ctx, msg);
- }
- };
-
- if(channel.pipeline().get("packet_handler") == null)
- channel.pipeline().addLast(NpcApi.plugin.getName(), duplexHandler);
- else
- channel.pipeline().addBefore("packet_handler", NpcApi.plugin.getName(), duplexHandler);
- }
-
- /**
- * Checks if the given packet is a {@link ServerboundInteractPacket} and, if so,
- * processes the interaction to dispatch a {@link NpcInteractEvent}.
- * This method is responsible for determining if a player has clicked or attacked an NPC.
- *
- * @param packet The raw packet object received from the Netty pipeline. Must not be {@code null}.
- * @param player The {@link Player} who sent the packet. Must not be {@code null}.
- */
- private static void checkForPacket(@NotNull Object packet, @NotNull Player player)
- {
- if(!(packet instanceof Packet>))
- return;
-
- if(!(packet instanceof ServerboundInteractPacket interactPacket))
- return;
-
- int id = interactPacket.getEntityId();
-
- NPC npc = NpcManager.getList().stream().filter(npc1 -> ((ServerPlayer) npc1.getServerPlayer()).getId() == id).findFirst().orElse(null);
-
- if(npc == null)
- return;
-
- if(interactPacket.isAttack())
- {
- Bukkit.getScheduler().scheduleSyncDelayedTask(NpcApi.plugin,
- () -> Bukkit.getPluginManager().callEvent(new NpcInteractEvent(player, npc, ClickActionType.LEFT)), 0);
-
- return;
- }
-
- var action = Reflections.getField(interactPacket, "action");
-
- if(action.get().getClass().getDeclaredFields().length == 2)
- return;
-
- InteractionHand hand = (InteractionHand) action.thanGetField("hand").get();
-
- if(hand == InteractionHand.MAIN_HAND)
- Bukkit.getScheduler().scheduleSyncDelayedTask(NpcApi.plugin,
- () -> Bukkit.getPluginManager().callEvent(new NpcInteractEvent(player, npc, ClickActionType.RIGHT)), 0);
- }
-
- /**
- * Uninjects the custom {@link ChannelDuplexHandler} from the specified player's Netty pipeline.
- * This stops the interception of packets for that player.
- *
- * @param player The {@link Player} whose pipeline is to be uninject. Must not be {@code null}.
- */
- public static void uninject(@NotNull Player player)
- {
- Channel channel = channels.get(player.getUniqueId());
-
- if(channel == null)
- return;
-
- if(channel.pipeline().get(NpcApi.plugin.getName()) != null)
- channel.pipeline().remove(NpcApi.plugin.getName());
- }
-
- /**
- * Uninjects the custom packet handler from all currently online players.
- * This is typically called during plugin shutdown or reload.
- */
- public static void uninjectAll()
- {
- for(Player player : Bukkit.getOnlinePlayers())
- uninject(player);
- }
-
- /**
- * Injects the custom packet handler into all currently online players.
- * This is typically called during plugin startup or after a reload.
- */
- public static void injectAll()
- {
- for(Player player : Bukkit.getOnlinePlayers())
- inject(player);
- }
-}
diff --git a/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/utils/Reflections.java b/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/utils/Reflections.java
deleted file mode 100644
index e586d3c..0000000
--- a/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/utils/Reflections.java
+++ /dev/null
@@ -1,406 +0,0 @@
-package de.eisi05.npc.api.utils;
-
-import com.google.common.primitives.Primitives;
-import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
-
-import java.lang.reflect.Constructor;
-import java.lang.reflect.Field;
-import java.lang.reflect.Method;
-import java.util.Arrays;
-import java.util.Optional;
-
-/**
- * Utility class providing methods for reflective operations such as loading classes,
- * invoking methods, and accessing fields dynamically at runtime.
- */
-@SuppressWarnings("unchecked")
-public class Reflections
-{
- /**
- * Loads a class by its fully qualified name.
- *
- * @param path fully qualified class name
- * @param type of the class
- * @return Optional containing the Class if found, empty otherwise
- */
- public static @NotNull Optional> getClass(@NotNull String path)
- {
- try
- {
- return Optional.of((Class) Class.forName(path));
- } catch(ClassNotFoundException e)
- {
- e.printStackTrace();
- return Optional.empty();
- }
- }
-
- /**
- * Instantiates an object of the given class using the constructor that matches the argument types.
- *
- * @param path fully qualified class name
- * @param args constructor arguments
- * @param type of the instance
- * @return Optional containing the instance if created successfully, empty otherwise
- */
- public static @NotNull Optional getInstance(@NotNull String path, @Nullable Object... args)
- {
- return getClass(path).flatMap(objectClass -> getInstance((Class) objectClass, args));
- }
-
- public static @NotNull Optional getInstanceFirstConstructor(@NotNull Class clazz, @Nullable Object... args)
- {
- try
- {
- Constructor> ctor = clazz.getDeclaredConstructors()[0];
- ctor.setAccessible(true);
- return Optional.of((T) ctor.newInstance(args));
- } catch(Exception e)
- {
- e.printStackTrace();
- return Optional.empty();
- }
- }
-
- public static @NotNull Optional getInstance(@NotNull Class clazz, @Nullable Object... args)
- {
- try
- {
- Class>[] argTypes = Arrays.stream(args)
- .map(Object::getClass)
- .toArray(Class[]::new);
- Constructor> ctor = clazz.getDeclaredConstructor(argTypes);
- ctor.setAccessible(true);
- return Optional.of((T) ctor.newInstance(args));
- } catch(Exception e)
- {
- e.printStackTrace();
- return Optional.empty();
- }
- }
-
- /**
- * Finds a declared method in the specified class that matches the given name and parameter types.
- *
- * @param clazz the class to search in
- * @param name the name of the method
- * @param args the arguments whose types will be used to find the method
- * @return the matching Method object
- * @throws NoSuchMethodException if no matching method is found
- */
- private static @NotNull Method findMethod(@NotNull Class> clazz, @NotNull String name, @Nullable Object[] args) throws NoSuchMethodException
- {
- Class> current = clazz;
- Class>[] argTypes = Arrays.stream(args)
- .map(Object::getClass)
- .toArray(Class>[]::new);
-
- while(current != null)
- {
- for(Method method : current.getDeclaredMethods())
- {
- if(!method.getName().equals(name))
- continue;
-
- Class>[] paramTypes = method.getParameterTypes();
- boolean isVarArgs = method.isVarArgs();
-
- if(isCompatible(argTypes, paramTypes, isVarArgs))
- {
- method.setAccessible(true);
- return method;
- }
- }
- current = current.getSuperclass();
- }
-
- throw new NoSuchMethodException("No compatible method " + name + " found in class " + clazz.getName() + "(" + Arrays.toString(args) + ")");
- }
-
- /**
- * Checks if a given set of argument types is compatible with the parameter types of a method.
- *
- * This method supports both regular and varargs methods. It also handles primitive-to-wrapper
- * type conversions (e.g., int to Integer).
- *
- * @param args The types of the provided arguments.
- * @param params The types of the method's parameters.
- * @param isVarArgs Whether the method accepts a variable number of arguments (varargs).
- * @return {@code true} if the argument types are compatible with the parameter types; {@code false} otherwise.
- */
- private static boolean isCompatible(@NotNull Class>[] args, @NotNull Class>[] params, boolean isVarArgs)
- {
- if(!isVarArgs)
- {
- if(args.length != params.length)
- return false;
- for(int i = 0; i < args.length; i++)
- {
- if(!Primitives.wrap(params[i]).isAssignableFrom(Primitives.wrap(args[i])))
- return false;
- }
- return true;
- }
-
- if(args.length < params.length - 1)
- return false;
- for(int i = 0; i < params.length - 1; i++)
- {
- if(!Primitives.wrap(params[i]).isAssignableFrom(Primitives.wrap(args[i])))
- return false;
- }
-
- Class> varArgType = Primitives.wrap(params[params.length - 1].getComponentType());
- for(int i = params.length - 1; i < args.length; i++)
- {
- if(!varArgType.isAssignableFrom(Primitives.wrap(args[i])))
- return false;
- }
- return true;
- }
-
- /**
- * Invokes an instance method on the given object with specified arguments.
- *
- * @param object the target object
- * @param methodName name of the method to invoke
- * @param args method arguments
- * @param return type of the method
- * @return a ReflectionChain wrapping the method's return value
- */
- public static @NotNull ReflectionChain invokeMethod(@NotNull Object object, @NotNull String methodName, @Nullable Object... args)
- {
- try
- {
- Method method = findMethod(object.getClass(), methodName, args);
- method.setAccessible(true);
- return new ReflectionChain<>((V) method.invoke(object, args));
- } catch(Exception e)
- {
- throw new RuntimeException(e);
- }
- }
-
- /**
- * Invokes a static method of a class given its fully qualified name.
- *
- * @param classPath fully qualified class name
- * @param methodName name of the static method
- * @param args method arguments
- * @param return type of the method
- * @return a ReflectionChain wrapping the method's return value
- */
- public static @NotNull ReflectionChain invokeStaticMethod(@NotNull String classPath, @NotNull String methodName, @Nullable Object... args)
- {
- try
- {
- Class> clazz = Class.forName(classPath);
- Method method = findMethod(clazz, methodName, args);
- method.setAccessible(true);
- return new ReflectionChain<>((V) method.invoke(null, args));
- } catch(Exception e)
- {
- throw new RuntimeException(e);
- }
- }
-
- /**
- * Finds a declared field in the specified class by its name and makes it accessible.
- *
- * @param clazz the class to search in
- * @param fieldName the name of the field
- * @return the Field object with accessibility set to true
- * @throws NoSuchFieldException if the field is not found
- */
- private static @NotNull Field findField(@NotNull Class> clazz, @NotNull String fieldName) throws NoSuchFieldException
- {
- while(clazz != null)
- {
- try
- {
- Field field = clazz.getDeclaredField(fieldName);
- field.setAccessible(true);
- return field;
- } catch(NoSuchFieldException ignored)
- {
- clazz = clazz.getSuperclass();
- }
- }
- throw new NoSuchFieldException();
- }
-
- /**
- * Retrieves the value of a field from an object.
- *
- * @param object the target object
- * @param fieldName name of the field
- * @param type of the field value
- * @return a ReflectionChain wrapping the field's value
- */
- public static @NotNull ReflectionChain getField(@NotNull Object object, @NotNull String fieldName)
- {
- try
- {
- Field field = findField(object.getClass(), fieldName);
- return new ReflectionChain<>((T) field.get(object));
- } catch(Exception e)
- {
- throw new RuntimeException(e);
- }
- }
-
- /**
- * Retrieves the value of a static field from a class.
- *
- * @param clazz the target class
- * @param fieldName name of the static field
- * @param class type
- * @param type of the field value
- * @return the value of the static field, or null if inaccessible
- */
- public static @Nullable V getStaticField(@NotNull Class clazz, @Nullable String fieldName)
- {
- try
- {
- Field field = findField(clazz, fieldName);
- return (V) field.get(null);
- } catch(Exception e)
- {
- throw new RuntimeException(e);
- }
- }
-
- /**
- * Retrieves the value of a static field by class name.
- *
- * @param classPath fully qualified class name
- * @param fieldName name of the static field
- * @param type of the field value
- * @return the value of the static field, or null if inaccessible
- */
- public static @Nullable T getStaticField(@NotNull String classPath, @NotNull String fieldName)
- {
- try
- {
- Class> clazz = Class.forName(classPath);
- Field field = findField(clazz, fieldName);
- return (T) field.get(null);
- } catch(Exception e)
- {
- throw new RuntimeException(e);
- }
- }
-
- /**
- * Sets the value of a field on an object.
- *
- * @param object target object
- * @param fieldName name of the field
- * @param value new value to set
- */
- public static void setField(@NotNull Object object, @NotNull String fieldName, @Nullable Object value)
- {
- try
- {
- Field field = findField(object.getClass(), fieldName);
- field.set(object, value);
- } catch(Exception e)
- {
- throw new RuntimeException(e);
- }
- }
-
- /**
- * Sets the value of a static field in a class by class name.
- *
- * @param classPath fully qualified class name
- * @param fieldName name of the static field
- * @param value new value to set
- */
- public static void setStaticField(@NotNull String classPath, @NotNull String fieldName, @Nullable Object value)
- {
- try
- {
- Class> clazz = Class.forName(classPath);
- Field field = findField(clazz, fieldName);
- field.set(null, value);
- } catch(Exception e)
- {
- throw new RuntimeException(e);
- }
- }
-
- /**
- * Helper class to chain reflection calls on the result of previous reflective operations.
- *
- * @param the wrapped value type
- */
- public static class ReflectionChain
- {
- private final @Nullable V value;
-
- /**
- * Creates a new ReflectionChain wrapping the given value.
- *
- * @param value the wrapped value may be null
- */
- public ReflectionChain(@Nullable V value)
- {
- this.value = value;
- }
-
- /**
- * Invokes a method on the wrapped object.
- *
- * @param methodName name of the method
- * @param args method arguments
- * @return new ReflectionChain wrapping the method's result, or null if error
- */
- public @NotNull ReflectionChain thanInvoke(@NotNull String methodName, @Nullable Object... args)
- {
- if(value == null)
- return new ReflectionChain<>(null);
-
- try
- {
- Method method = findMethod(value.getClass(), methodName, args);
- method.setAccessible(true);
- return new ReflectionChain<>((V) method.invoke(value, args));
- } catch(Exception e)
- {
- throw new RuntimeException(e);
- }
- }
-
- /**
- * Retrieves a field value from the wrapped object.
- *
- * @param fieldName name of the field
- * @return new ReflectionChain wrapping the field's value, or null if error
- */
- public @NotNull ReflectionChain thanGetField(@NotNull String fieldName)
- {
- if(value == null)
- return new ReflectionChain<>(null);
- try
- {
- Field field = findField(value.getClass(), fieldName);
- return new ReflectionChain<>((V) field.get(value));
- } catch(Exception e)
- {
- throw new RuntimeException(e);
- }
- }
-
- /**
- * Returns the wrapped value.
- *
- * @return wrapped value may be null
- */
- public @Nullable V get()
- {
- return value;
- }
- }
-}
diff --git a/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/utils/TriFunction.java b/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/utils/TriFunction.java
deleted file mode 100644
index 7b2ea34..0000000
--- a/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/utils/TriFunction.java
+++ /dev/null
@@ -1,24 +0,0 @@
-package de.eisi05.npc.api.utils;
-
-/**
- * Represents a function that accepts three arguments and produces a result.
- * This is a functional interface whose functional method is {@link #apply(Object, Object, Object)}.
- *
- * @param the type of the first argument to the function
- * @param the type of the second argument to the function
- * @param the type of the third argument to the function
- * @param the type of the result of the function
- */
-@FunctionalInterface
-public interface TriFunction
-{
- /**
- * Applies this function to the given arguments.
- *
- * @param t the first function argument
- * @param u the second function argument
- * @param v the third function argument
- * @return the function result
- */
- R apply(T t, U u, V v);
-}
diff --git a/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/utils/Var.java b/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/utils/Var.java
deleted file mode 100644
index 2a17750..0000000
--- a/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/utils/Var.java
+++ /dev/null
@@ -1,103 +0,0 @@
-package de.eisi05.npc.api.utils;
-
-import net.minecraft.network.protocol.Packet;
-import net.minecraft.server.level.ServerEntity;
-import net.minecraft.server.level.ServerLevel;
-import net.minecraft.server.level.ServerPlayer;
-import net.minecraft.world.entity.Entity;
-import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
-
-import java.util.Set;
-import java.util.UUID;
-import java.util.function.BiConsumer;
-import java.util.function.Consumer;
-
-public class Var
-{
- /**
- * Performs an unchecked cast of an object to a specified type.
- * This method can be used to bypass Java's type checking at compile time,
- * but it comes with the risk of {@link ClassCastException} at runtime if the
- * object is not an instance of the target type.
- *
- * @param o The object to cast. Can be {@code null}.
- * @param The target type to which the object will be cast.
- * @return The object cast to the specified type, or {@code null} if the input object was {@code null}.
- */
- @SuppressWarnings("unchecked")
- public static @Nullable T unsafeCast(@Nullable Object o)
- {
- return (T) o;
- }
-
- public static void moveEntity(Entity entity, double x, double y, double z, float yaw, float pitch)
- {
- if(Versions.isCurrentVersionSmallerThan(Versions.V1_21_5))
- Reflections.invokeMethod(entity, "absMoveTo", x, y, z, yaw, pitch);
- else
- Reflections.invokeMethod(entity, "snapTo", x, y, z, yaw, pitch);
- }
-
- public static ServerLevel getServerLevel(ServerPlayer player)
- {
- return (ServerLevel) Reflections.invokeMethod(player, "level").get();
- }
-
- public static ServerEntity getServerEntity(Entity entity, ServerLevel level)
- {
- ServerEntity serverEntity;
- if(Versions.isCurrentVersionSmallerThan(Versions.V1_21_5))
- serverEntity = Reflections.getInstanceFirstConstructor(ServerEntity.class, level, entity, 0, false,
- new Consumer>()
- {
- @Override
- public void accept(Packet> packet)
- {
-
- }
-
- @Override
- public @NotNull Consumer> andThen(@NotNull Consumer super Packet>> after)
- {
- return Consumer.super.andThen(after);
- }
- },
- Set.of()).orElseThrow();
- else if(Versions.isCurrentVersionSmallerThan(Versions.V1_21_9))
- serverEntity = Reflections.getInstanceFirstConstructor(ServerEntity.class, level, entity, 0, false,
- new Consumer>()
- {
- @Override
- public void accept(Packet> packet)
- {
-
- }
-
- @Override
- public @NotNull Consumer> andThen(@NotNull Consumer super Packet>> after)
- {
- return Consumer.super.andThen(after);
- }
- },
- new BiConsumer, UUID>()
- {
- @Override
- public void accept(Packet> packet, UUID uuid)
- {
-
- }
-
- @Override
- public @NotNull BiConsumer, UUID> andThen(@NotNull BiConsumer super Packet>, ? super UUID> after)
- {
- return BiConsumer.super.andThen(after);
- }
- }, Set.of()).orElseThrow();
- else
- serverEntity = Reflections.getInstanceFirstConstructor(ServerEntity.class, level, entity, 0, false,
- null, Set.of()).orElseThrow();
-
- return serverEntity;
- }
-}
diff --git a/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/utils/Versions.java b/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/utils/Versions.java
deleted file mode 100644
index 07490d4..0000000
--- a/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/utils/Versions.java
+++ /dev/null
@@ -1,199 +0,0 @@
-package de.eisi05.npc.api.utils;
-
-import org.bukkit.Bukkit;
-import org.jetbrains.annotations.NotNull;
-
-import java.util.Arrays;
-
-/**
- * The {@link Versions} enum represents the supported Minecraft server versions
- * and provides utility methods for working with version-specific paths and comparisons.
- * It helps in adapting the plugin's functionality to different server environments.
- */
-public enum Versions
-{
- /**
- * Represents an unknown or unsupported version.
- */
- NONE(""),
- /**
- * Minecraft 1.17 version.
- */
- V1_17("v1_17_R1"),
- /**
- * Minecraft 1.18 version.
- */
- V1_18("v1_18_R1"),
- /**
- * Minecraft 1.18.2 version.
- */
- V1_18_2("v1_18_R2"),
- /**
- * Minecraft 1.19 version.
- */
- V1_19("v1_19_R1"),
- /**
- * Minecraft 1.19.1 version.
- */
- V1_19_1("v1_19_R1"),
- /**
- * Minecraft 1.19.3 version.
- */
- V1_19_3("v1_19_R2"),
- /**
- * Minecraft 1.19.4 version.
- */
- V1_19_4("v1_19_R3"),
- /**
- * Minecraft 1.20 version.
- */
- V1_20("v1_20_R1"),
- /**
- * Minecraft 1.20.2 version.
- */
- V1_20_2("v1_20_R2"),
- /**
- * Minecraft 1.20.4 version.
- */
- V1_20_4("v1_20_R3"),
- /**
- * Minecraft 1.20.6 version.
- */
- V1_20_6("v1_20_R4"),
- /**
- * Minecraft 1.21 version.
- */
- V1_21("v1_21_R1"),
- /**
- * Minecraft 1.21.2 version.
- */
- V1_21_2("v1_21_R2"),
- /**
- * Minecraft 1.21.4 version.
- */
- V1_21_4("v1_21_R3"),
- /**
- * Minecraft 1.21.5 version.
- */
- V1_21_5("v1_21_R4"),
- /**
- * Minecraft 1.21.5 version.
- */
- V1_21_6("v1_21_R5"),
-
- /**
- * Minecraft 1.21.5 version.
- */
- V1_21_7("v1_21_R5"),
-
- /**
- * Minecraft 1.21.9 version.
- */
- V1_21_9("v1_21_R6");
-
- /**
- * Caches the determined current server version to avoid repeated lookups.
- */
- private static Versions VERSION;
-
- /**
- * The NMS (Net Minecraft Server) path component corresponding to this version.
- * For example, "v1_17_R1" for Minecraft 1.17.
- */
- private final String path;
-
- /**
- * Constructs a {@code Versions} enum entry with the specified NMS path.
- *
- * @param path The NMS path string for this version. Must not be {@code null}.
- */
- Versions(@NotNull String path)
- {
- this.path = path;
- }
-
- /**
- * Determines and returns the current Minecraft server version based on the Bukkit server's package name.
- * The determined version is cached for later calls.
- *
- * @return The {@link Versions} enum entry corresponding to the current server version. Must not be {@code null}.
- */
- public static @NotNull Versions getVersion()
- {
- if(VERSION != null)
- return VERSION;
-
- return VERSION = switch(Bukkit.getMinecraftVersion())
- {
- case "1.17.1", "1.17.2" -> Versions.V1_17;
- case "1.18", "1.18.1" -> Versions.V1_18;
- case "1.18.2" -> Versions.V1_18_2;
- case "1.19" -> Versions.V1_19;
- case "1.19.1", "1.19.2" -> Versions.V1_19_1;
- case "1.19.3" -> Versions.V1_19_3;
- case "1.19.4", "1.19.5" -> Versions.V1_19_4;
- case "1.20", "1.20.1" -> Versions.V1_20;
- case "1.20.2", "1.20.3" -> Versions.V1_20_2;
- case "1.20.4", "1.20.5" -> Versions.V1_20_4;
- case "1.20.6" -> Versions.V1_20_6;
- case "1.21", "1.21.1" -> Versions.V1_21;
- case "1.21.2", "1.21.3" -> Versions.V1_21_2;
- case "1.21.4" -> Versions.V1_21_4;
- case "1.21.5" -> Versions.V1_21_5;
- case "1.21.6" -> Versions.V1_21_6;
- case "1.21.7", "1.21.8" -> V1_21_7;
- case "1.21.9", "1.21.10" -> V1_21_9;
- default -> Versions.NONE;
- };
- }
-
- /**
- * Returns an array of {@link Versions} enum entries that fall inclusively between
- * two specified versions (based on their ordinal values).
- * The {@code NONE} version is excluded from the result.
- *
- * @param versions1 The starting version (inclusive). Must not be {@code null}.
- * @param versions2 The ending version (inclusive). Must not be {@code null}.
- * @return An array of {@link Versions} enum entries within the specified range. Must not be {@code null}.
- */
- private static @NotNull Versions[] getVersionBetween(@NotNull Versions versions1, @NotNull Versions versions2)
- {
- return Arrays.stream(values())
- .filter(v -> v != Versions.NONE)
- .filter(v -> v.ordinal() >= versions1.ordinal() && v.ordinal() <= versions2.ordinal())
- .toArray(Versions[]::new);
- }
-
- /**
- * Checks if the current server version is numerically smaller than a specified version.
- * This comparison is based on the ordinal value of the enum entries.
- *
- * @param versions The version to compare against. Must not be {@code null}.
- * @return {@code true} if the current version is smaller, {@code false} otherwise.
- */
- public static boolean isCurrentVersionSmallerThan(@NotNull Versions versions)
- {
- return getVersion().ordinal() < versions.ordinal();
- }
-
- /**
- * Returns the NMS (Net Minecraft Server) path component associated with this version.
- *
- * @return The NMS path as a {@link String}. Must not be {@code null}.
- */
- public @NotNull String getPath()
- {
- return path;
- }
-
- /**
- * Returns the name of the version, with underscores replaced by dots for better readability.
- * For example, {@code V1_17} becomes "V1.17".
- *
- * @return The formatted name of the version as a {@link String}. Must not be {@code null}.
- */
- public @NotNull String getName()
- {
- return name().replace("_", ".");
- }
-}
diff --git a/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/wrapper/enums/ChatFormat.java b/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/wrapper/enums/ChatFormat.java
deleted file mode 100644
index 8147817..0000000
--- a/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/wrapper/enums/ChatFormat.java
+++ /dev/null
@@ -1,58 +0,0 @@
-package de.eisi05.npc.api.wrapper.enums;
-
-import net.kyori.adventure.text.format.NamedTextColor;
-import net.kyori.adventure.text.format.TextDecoration;
-import net.kyori.adventure.text.format.TextFormat;
-import net.kyori.adventure.text.serializer.legacy.Reset;
-
-import java.io.Serializable;
-
-public enum ChatFormat implements Serializable
-{
- BLACK('0', NamedTextColor.BLACK),
- DARK_BLUE('1', NamedTextColor.DARK_BLUE),
- DARK_GREEN('2', NamedTextColor.DARK_GREEN),
- DARK_AQUA('3', NamedTextColor.DARK_AQUA),
- DARK_RED('4', NamedTextColor.DARK_RED),
- DARK_PURPLE('5', NamedTextColor.DARK_PURPLE),
- GOLD('6', NamedTextColor.GOLD),
- GRAY('7', NamedTextColor.GRAY),
- DARK_GRAY('8', NamedTextColor.DARK_GRAY),
- BLUE('9', NamedTextColor.BLUE),
- GREEN('a', NamedTextColor.GREEN),
- AQUA('b', NamedTextColor.AQUA),
- RED('c', NamedTextColor.RED),
- LIGHT_PURPLE('d', NamedTextColor.LIGHT_PURPLE),
- YELLOW('e', NamedTextColor.YELLOW),
- WHITE('f', NamedTextColor.WHITE),
- OBFUSCATED('k', TextDecoration.OBFUSCATED),
- BOLD('l', TextDecoration.BOLD),
- STRIKETHROUGH('m', TextDecoration.STRIKETHROUGH),
- UNDERLINE('n', TextDecoration.UNDERLINED),
- ITALIC('o', TextDecoration.ITALIC),
- RESET('p', Reset.INSTANCE);
-
- private final char color;
- private final TextFormat textFormat;
-
- ChatFormat(char color, TextFormat textFormat)
- {
- this.color = color;
- this.textFormat = textFormat;
- }
-
- public char getColorCode()
- {
- return color;
- }
-
- public TextFormat getTextFormat()
- {
- return textFormat;
- }
-
- public boolean isColor()
- {
- return textFormat instanceof NamedTextColor;
- }
-}
diff --git a/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/wrapper/packets/AnimatePacket.java b/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/wrapper/packets/AnimatePacket.java
deleted file mode 100644
index 11f107d..0000000
--- a/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/wrapper/packets/AnimatePacket.java
+++ /dev/null
@@ -1,29 +0,0 @@
-package de.eisi05.npc.api.wrapper.packets;
-
-import net.minecraft.network.protocol.game.ClientboundAnimatePacket;
-import net.minecraft.network.protocol.game.ClientboundHurtAnimationPacket;
-import net.minecraft.server.level.ServerPlayer;
-import org.jetbrains.annotations.NotNull;
-
-import java.io.Serializable;
-
-public class AnimatePacket
-{
- public static Object create(@NotNull ServerPlayer player, @NotNull Animation animation)
- {
- if(animation != Animation.HURT)
- return new ClientboundAnimatePacket(player, animation.ordinal());
-
- return new ClientboundHurtAnimationPacket(player);
- }
-
- public enum Animation implements Serializable
- {
- SWING_MAIN_HAND,
- HURT,
- WAKE_UP,
- SWING_OFF_HAND,
- CRITICAL_HIT,
- MAGIC_CRITICAL_HIT
- }
-}
diff --git a/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/wrapper/packets/SetEntityDataPacket.java b/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/wrapper/packets/SetEntityDataPacket.java
deleted file mode 100644
index c8a4f74..0000000
--- a/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/wrapper/packets/SetEntityDataPacket.java
+++ /dev/null
@@ -1,13 +0,0 @@
-package de.eisi05.npc.api.wrapper.packets;
-
-import net.minecraft.network.protocol.game.ClientboundSetEntityDataPacket;
-import net.minecraft.network.syncher.SynchedEntityData;
-import org.jetbrains.annotations.NotNull;
-
-public class SetEntityDataPacket
-{
- public static Object create(int id, @NotNull SynchedEntityData data)
- {
- return new ClientboundSetEntityDataPacket(id, data.packAll());
- }
-}
diff --git a/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/wrapper/packets/SetPlayerTeamPacket.java b/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/wrapper/packets/SetPlayerTeamPacket.java
deleted file mode 100644
index a1093a6..0000000
--- a/src/main/NpcApi-Paper-master/src/main/java/de/eisi05/npc/api/wrapper/packets/SetPlayerTeamPacket.java
+++ /dev/null
@@ -1,24 +0,0 @@
-package de.eisi05.npc.api.wrapper.packets;
-
-import net.minecraft.network.protocol.game.ClientboundSetPlayerTeamPacket;
-import net.minecraft.world.scores.PlayerTeam;
-import org.jetbrains.annotations.NotNull;
-
-public class SetPlayerTeamPacket
-{
- public static Object createAddOrModifyPacket(@NotNull PlayerTeam team, boolean create)
- {
- return ClientboundSetPlayerTeamPacket.createAddOrModifyPacket(team, create);
- }
-
- public static Object createRemovePacket(@NotNull PlayerTeam team)
- {
- return ClientboundSetPlayerTeamPacket.createRemovePacket(team);
- }
-
- public static Object createPlayerPacket(@NotNull PlayerTeam team, @NotNull String playerName,
- @NotNull ClientboundSetPlayerTeamPacket.Action action)
- {
- return ClientboundSetPlayerTeamPacket.createPlayerPacket(team, playerName, action);
- }
-}
diff --git a/src/main/java/com/mmmm/story/Cleanable.java b/src/main/java/com/mmmm/story/Cleanable.java
new file mode 100644
index 0000000..6718250
--- /dev/null
+++ b/src/main/java/com/mmmm/story/Cleanable.java
@@ -0,0 +1,27 @@
+package com.mmmm.story;
+
+/**
+ * Implemented by listeners and managers that own boss bars, spawned entities or other
+ * runtime state which must be released when the plugin shuts down.
+ *
+ * Bukkit cancels a plugin's scheduler tasks on disable by itself, but nothing else:
+ * boss bars stay pinned to the screens of players who survive a {@code /reload}, and
+ * summoned bosses, warrior skeletons and VFX armour stands stay in the world with no
+ * listener tracking them. After the reload a fresh listener is constructed that knows
+ * nothing about them.
+ *
+ *
Everything registered through {@link MmmmStoryPlugin#registerListeners()} is
+ * collected into a single list, which {@link MmmmStoryPlugin#onDisable()} walks in
+ * reverse registration order.
+ */
+public interface Cleanable {
+
+ /**
+ * Cancel tasks, hide boss bars and drop any other runtime state.
+ *
+ *
Must be safe to call more than once - {@code cleanup()} also runs when a boss
+ * dies normally. The shutdown loop logs and continues on failure so that one broken
+ * component cannot block the rest.
+ */
+ void cleanup();
+}
diff --git a/src/main/java/com/mmmm/story/MmmmStoryPlugin.java b/src/main/java/com/mmmm/story/MmmmStoryPlugin.java
index 4ae08e8..c2c751a 100644
--- a/src/main/java/com/mmmm/story/MmmmStoryPlugin.java
+++ b/src/main/java/com/mmmm/story/MmmmStoryPlugin.java
@@ -1,18 +1,28 @@
package com.mmmm.story;
-import com.mmmm.story.commands.StoryCommand;
import com.mmmm.story.commands.ServerCommand;
+import com.mmmm.story.commands.StoryCommand;
+import com.mmmm.story.commands.TextureTestCommand;
import com.mmmm.story.listeners.*;
import com.mmmm.story.managers.*;
import de.eisi05.npc.api.NpcApi;
+import org.bukkit.command.CommandExecutor;
+import org.bukkit.command.PluginCommand;
+import org.bukkit.event.Listener;
import org.bukkit.plugin.java.JavaPlugin;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
import java.util.logging.Level;
public class MmmmStoryPlugin extends JavaPlugin {
-
+
private static MmmmStoryPlugin instance;
-
+
+ /** Registered components that must release runtime state on shutdown. */
+ private final List cleanables = new ArrayList<>();
+
private ConfigManager configManager;
private DataManager dataManager;
private ItemManager itemManager;
@@ -52,58 +62,108 @@ public void onEnable() {
menuManager = new MenuManager(this, messageManager, actManager, dialogManager);
// Register commands
- getCommand("story").setExecutor(new StoryCommand(this));
- getCommand("server").setExecutor(new ServerCommand(this));
-
+ registerCommand("story", new StoryCommand(this));
+ registerCommand("server", new ServerCommand(this));
+ registerCommand("testtexture", new TextureTestCommand(this));
+
// Register event listeners
registerListeners();
-
- // Start auto-save task (every 5 minutes)
- getServer().getScheduler().runTaskTimer(this, () -> {
- dataManager.save();
- }, 6000L, 6000L);
-
+
+ startAutoSaveTask();
+
getLogger().info(getMessageManager().getMessage("log.plugin_enabled"));
-
+
} catch (Exception e) {
getLogger().log(Level.SEVERE, "Failed to initialize plugin!", e);
getServer().getPluginManager().disablePlugin(this);
}
}
-
+
@Override
public void onDisable() {
getLogger().info("Saving data...");
-
+
if (dataManager != null) {
dataManager.save();
}
-
+
+ // Reverse order so components tear down opposite to how they were built.
+ // One failing component must not stop the rest from cleaning up.
+ List reversed = new ArrayList<>(cleanables);
+ Collections.reverse(reversed);
+ for (Cleanable cleanable : reversed) {
+ try {
+ cleanable.cleanup();
+ } catch (Exception e) {
+ getLogger().log(Level.WARNING,
+ "Cleanup failed for " + cleanable.getClass().getSimpleName(), e);
+ }
+ }
+ cleanables.clear();
+
if (npcManager != null) {
npcManager.cleanup();
}
-
- getLogger().info(getMessageManager().getMessage("log.plugin_disabled"));
+
+ // Deliberately not localised: messageManager is null when onEnable() failed
+ // early, and an NPE here would mask the original startup error.
+ getLogger().info("Mmmm Story Plugin disabled");
}
-
+
+ /**
+ * Persist story data every five minutes.
+ *
+ * Serialisation happens on the main thread (Bukkit configuration objects are
+ * not thread safe); only the file writes are pushed off it.
+ */
+ private void startAutoSaveTask() {
+ long fiveMinutes = 20L * 60L * 5L;
+ getServer().getScheduler().runTaskTimer(this, () -> dataManager.saveAsync(),
+ fiveMinutes, fiveMinutes);
+ }
+
+ /**
+ * Bind an executor to a command declared in plugin.yml, failing loudly when the
+ * two drift apart instead of throwing a bare NPE.
+ */
+ private void registerCommand(String name, CommandExecutor executor) {
+ PluginCommand command = getCommand(name);
+ if (command == null) {
+ getLogger().severe("Command '" + name + "' is missing from plugin.yml - not registered");
+ return;
+ }
+ command.setExecutor(executor);
+ }
+
private void registerListeners() {
act1Listener = new Act1Listener(this);
- getServer().getPluginManager().registerEvents(act1Listener, this);
- getServer().getPluginManager().registerEvents(new Act2Listener(this), this);
- getServer().getPluginManager().registerEvents(new Act3Listener(this), this);
- // Act4Listener disabled - artifacts only through chest search, not auto-spawn
- // getServer().getPluginManager().registerEvents(new Act4Listener(this), this);
- getServer().getPluginManager().registerEvents(new Act5Listener(this), this);
- getServer().getPluginManager().registerEvents(new PortalListener(this), this);
- getServer().getPluginManager().registerEvents(new PlayerListener(this), this);
- getServer().getPluginManager().registerEvents(new MobListener(this), this);
- getServer().getPluginManager().registerEvents(new ChestSpawnManager(this), this);
- getServer().getPluginManager().registerEvents(new StoryItemProtectionListener(this), this);
- getServer().getPluginManager().registerEvents(new BlockTrackingListener(this), this);
- getServer().getPluginManager().registerEvents(new PlayerJoinListener(this), this);
- getServer().getPluginManager().registerEvents(new MenuClickListener(this), this);
+ register(act1Listener);
+ register(new Act2Listener(this));
+ register(new Act3Listener(this));
+ if (getConfig().getBoolean("act4.autoSpawnArtifacts", false)) {
+ register(new Act4Listener(this));
+ }
+ register(new Act5Listener(this));
+ register(new PortalListener(this));
+ register(new PlayerListener(this));
+ register(new MobListener(this));
+ register(new ChestSpawnManager(this));
+ register(new StoryItemProtectionListener(this));
+ register(new BlockTrackingListener(this));
+ register(new PlayerJoinListener(this));
+ register(new MenuClickListener(this));
}
-
+
+ /**
+ * Register a listener and, when it owns runtime state, remember it for shutdown.
+ */
+ private void register(Listener listener) {
+ getServer().getPluginManager().registerEvents(listener, this);
+ if (listener instanceof Cleanable cleanable) {
+ cleanables.add(cleanable);
+ }
+ }
+
// Getters
public static MmmmStoryPlugin getInstance() {
@@ -126,10 +186,6 @@ public NPCManager getNPCManager() {
return npcManager;
}
- public NPCManager getNpcManager() {
- return npcManager;
- }
-
public ActManager getActManager() {
return actManager;
}
diff --git a/src/main/java/com/mmmm/story/bosses/backup/OriginalBoss2Backup.java b/src/main/java/com/mmmm/story/bosses/backup/OriginalBoss2Backup.java
deleted file mode 100644
index e7e0980..0000000
--- a/src/main/java/com/mmmm/story/bosses/backup/OriginalBoss2Backup.java
+++ /dev/null
@@ -1,393 +0,0 @@
-// BACKUP FILE: Original Boss #2 (Изверг Адских Глубин) Mechanics
-// This file contains the backed-up Wither-based boss implementation from Act2Listener
-// Created on: 2025-11-09
-// For rollback purposes if Enderman replacement needs to be reverted
-
-package com.mmmm.story.bosses.backup;
-
-import com.mmmm.story.MmmmStoryPlugin;
-import org.bukkit.*;
-import org.bukkit.entity.*;
-import org.bukkit.scheduler.BukkitRunnable;
-import org.bukkit.scheduler.BukkitTask;
-import org.bukkit.attribute.Attribute;
-import org.bukkit.potion.PotionEffect;
-import org.bukkit.potion.PotionEffectType;
-import org.bukkit.inventory.ItemStack;
-
-import net.kyori.adventure.bossbar.BossBar;
-import net.kyori.adventure.text.Component;
-import net.kyori.adventure.text.format.NamedTextColor;
-
-import java.util.*;
-
-/**
- * Backup of original Wither-based boss mechanics
- * This contains the complete boss implementation that was replaced by the Enderman boss
- */
-public class OriginalBoss2Backup {
-
- private final MmmmStoryPlugin plugin;
- private Wither bossEntity;
- private int bossPhase = 1;
- private BossBar bossBar;
-
- // Combat tracking (simplified for backup)
- private Map playersAboveBoss = new HashMap<>();
- private Map playersNearBoss = new HashMap<>();
- private Map teleportCooldown = new HashMap<>();
- private Map playerArrowsShot = new HashMap<>();
-
- // Task references
- private BukkitTask bossBarTask;
- private BukkitTask bossAITask;
- private BukkitTask heightCheckTask;
- private BukkitTask antiWallTask;
- private BukkitTask teleportTask;
-
- public OriginalBoss2Backup(MmmmStoryPlugin plugin) {
- this.plugin = plugin;
- }
-
- /**
- * Original boss summoning method
- */
- public void spawnOriginalBoss(Location location) {
- World world = location.getWorld();
-
- // Spawn Wither-based boss entity
- Location spawnLoc = location.clone().add(0, 3, 0);
- bossEntity = (Wither) world.spawnEntity(spawnLoc, EntityType.WITHER);
- bossEntity.setCustomName("Изверг Адских Глубин");
- bossEntity.setCustomNameVisible(true);
-
- // Set original attributes
- bossEntity.getAttribute(Attribute.GENERIC_MAX_HEALTH).setBaseValue(500.0);
- bossEntity.setHealth(500.0);
- bossEntity.getAttribute(Attribute.GENERIC_KNOCKBACK_RESISTANCE).setBaseValue(0.6);
-
- // Mark as boss
- bossEntity.setPersistent(true);
- bossEntity.setRemoveWhenFarAway(false);
-
- // Create boss bar
- bossBar = BossBar.bossBar(
- Component.text("Изверг Адских Глубин"),
- 1.0f,
- BossBar.Color.PURPLE,
- BossBar.Overlay.PROGRESS
- );
-
- // Add nearby players to boss bar
- for (Player player : world.getPlayers()) {
- if (player.getLocation().distance(bossEntity.getLocation()) < 100) {
- // Note: Adventure API may have different method names
- // bossBar.addPlayer(player); // Commented out for compatibility
- }
- }
-
- // Start combat tasks
- startBossTasks();
- }
-
- /**
- * Start original boss combat tasks
- */
- private void startBossTasks() {
- // Boss bar update task
- bossBarTask = new BukkitRunnable() {
- @Override
- public void run() {
- if (bossEntity == null || !bossEntity.isValid()) {
- this.cancel();
- return;
- }
-
- double healthPercentage = bossEntity.getHealth() / bossEntity.getMaxHealth();
- bossBar.progress((float) healthPercentage);
-
- // Update phase based on health
- int newPhase = healthPercentage > 0.5 ? 1 : 2;
- if (newPhase != bossPhase) {
- bossPhase = newPhase;
- onPhaseTransition(bossPhase);
- }
- }
- }.runTaskTimer(plugin, 0L, 5L);
-
- // Boss AI task
- bossAITask = new BukkitRunnable() {
- @Override
- public void run() {
- if (bossEntity == null || !bossEntity.isValid()) {
- this.cancel();
- return;
- }
-
- // Original boss AI logic
- executeOriginalBossAI();
- }
- }.runTaskTimer(plugin, 0L, 10L);
-
- // Height check task (anti-exploit)
- heightCheckTask = new BukkitRunnable() {
- @Override
- public void run() {
- checkPlayerHeightExploit();
- }
- }.runTaskTimer(plugin, 0L, 5L);
-
- // Anti-fortification task
- antiWallTask = new BukkitRunnable() {
- @Override
- public void run() {
- preventPlayerFortification();
- }
- }.runTaskTimer(plugin, 0L, 20L);
- }
-
- /**
- * Execute original boss AI
- */
- private void executeOriginalBossAI() {
- // Check for nearby players
- List nearbyPlayers = getNearbyPlayers(75);
- if (nearbyPlayers.isEmpty()) {
- return; // No players nearby, wait
- }
-
- // Target nearest player
- Player target = findNearestPlayer(nearbyPlayers);
- if (target != null) {
- bossEntity.setTarget(target);
-
- // Phase-specific behavior
- if (bossPhase == 1) {
- executePhase1Behavior(target);
- } else {
- executePhase2Behavior(target);
- }
- }
- }
-
- /**
- * Execute Phase 1 behavior (original)
- */
- private void executePhase1Behavior(Player target) {
- // Basic attacks
- if (bossEntity.getLocation().distance(target.getLocation()) < 3.0) {
- // Teleport behind player if too close
- executeProximityTeleport(target);
- }
- }
-
- /**
- * Execute Phase 2 behavior (original)
- */
- private void executePhase2Behavior(Player target) {
- // Enhanced aggression in Phase 2
- bossEntity.addPotionEffect(new PotionEffect(PotionEffectType.SPEED, 100, 1));
- bossEntity.addPotionEffect(new PotionEffect(PotionEffectType.STRENGTH, 100, 0));
- }
-
- /**
- * Execute proximity teleport (original mechanic)
- */
- private void executeProximityTeleport(Player player) {
- UUID playerId = player.getUniqueId();
- long currentTime = System.currentTimeMillis();
-
- // Check cooldown
- if (teleportCooldown.containsKey(playerId)) {
- long lastTeleport = teleportCooldown.get(playerId);
- if (currentTime - lastTeleport < 10000) { // 10 second cooldown
- return;
- }
- }
-
- // Teleport behind player
- Location behindPlayer = player.getLocation().clone()
- .add(player.getLocation().getDirection().multiply(-4));
- behindPlayer.setY(bossEntity.getLocation().getY());
-
- bossEntity.teleport(behindPlayer);
- teleportCooldown.put(playerId, currentTime);
-
- // Effects
- World world = bossEntity.getWorld();
- world.spawnParticle(Particle.PORTAL, behindPlayer, 50, 1, 1, 1, 0.2);
- world.playSound(behindPlayer, Sound.ENTITY_ENDERMAN_TELEPORT, 1.0f, 1.0f);
- }
-
- /**
- * Find nearest player to location
- */
- private Player findNearestPlayer(List players) {
- Player nearest = null;
- double minDistance = Double.MAX_VALUE;
-
- for (Player player : players) {
- double distance = bossEntity.getLocation().distance(player.getLocation());
- if (distance < minDistance) {
- minDistance = distance;
- nearest = player;
- }
- }
-
- return nearest;
- }
-
- /**
- * Get nearby players
- */
- private List getNearbyPlayers(double radius) {
- List players = new ArrayList<>();
- for (Entity entity : bossEntity.getNearbyEntities(radius, radius, radius)) {
- if (entity instanceof Player) {
- players.add((Player) entity);
- }
- }
- return players;
- }
-
- /**
- * Handle phase transition
- */
- private void onPhaseTransition(int newPhase) {
- // Handle phase transition effects
- World world = bossEntity.getWorld();
- world.spawnParticle(Particle.EXPLOSION, bossEntity.getLocation(), 50, 2, 2, 2, 0.1);
- world.playSound(bossEntity.getLocation(), Sound.ENTITY_WITHER_AMBIENT, 2.0f, 0.5f);
-
- if (newPhase == 2) {
- // Phase 2 enhancements
- bossEntity.addPotionEffect(new PotionEffect(PotionEffectType.SPEED, 999999, 1));
- bossEntity.addPotionEffect(new PotionEffect(PotionEffectType.STRENGTH, 999999, 0));
- }
- }
-
- /**
- * Check player height exploit
- */
- private void checkPlayerHeightExploit() {
- if (bossEntity == null) return;
-
- for (Player player : getNearbyPlayers(20)) {
- Location playerLoc = player.getLocation();
- Location bossLoc = bossEntity.getLocation();
-
- if (playerLoc.getY() > bossLoc.getY() + 5) {
- // Player is too high above boss
- UUID playerId = player.getUniqueId();
- long currentTime = System.currentTimeMillis();
-
- if (!playersAboveBoss.containsKey(player.getUniqueId())) {
- playersAboveBoss.put(player.getUniqueId(), currentTime);
- } else if (currentTime - playersAboveBoss.get(player.getUniqueId()) > 3000) {
- // Knock player back after 3 seconds
- knockPlayerBack(player);
- playersAboveBoss.remove(playerId);
- }
- } else {
- playersAboveBoss.remove(player.getUniqueId());
- }
- }
- }
-
- /**
- * Knock player back
- */
- private void knockPlayerBack(Player player) {
- org.bukkit.util.Vector knockback = player.getLocation().toVector()
- .subtract(bossEntity.getLocation().toVector())
- .normalize()
- .multiply(3.0);
- knockback.setY(1.5);
-
- player.setVelocity(knockback);
- player.playSound(player.getLocation(), Sound.ENTITY_GENERIC_EXPLODE, 1.0f, 1.0f);
- }
-
- /**
- * Prevent player fortification
- */
- private void preventPlayerFortification() {
- if (bossEntity == null) return;
-
- Location bossLoc = bossEntity.getLocation();
- World world = bossLoc.getWorld();
-
- // Break blocks in 3-block radius
- for (int x = -3; x <= 3; x++) {
- for (int y = -1; y <= 3; y++) {
- for (int z = -3; z <= 3; z++) {
- Location checkLoc = bossLoc.clone().add(x, y, z);
- if (shouldBreakBlock(checkLoc.getBlock().getType())) {
- checkLoc.getBlock().setType(Material.AIR);
- }
- }
- }
- }
- }
-
- /**
- * Check if block should be broken
- */
- private boolean shouldBreakBlock(Material type) {
- return type == Material.OAK_PLANKS || type == Material.COBBLESTONE ||
- type == Material.STONE_BRICKS || type == Material.IRON_BARS;
- }
-
- /**
- * Get current boss phase
- */
- public int getBossPhase() {
- return bossPhase;
- }
-
- /**
- * Get boss entity
- */
- public Wither getBossEntity() {
- return bossEntity;
- }
-
- /**
- * Check if boss is active
- */
- public boolean isBossActive() {
- return bossEntity != null && bossEntity.isValid();
- }
-
- /**
- * Clean up all boss resources
- */
- public void cleanup() {
- // Cancel tasks
- if (bossBarTask != null) bossBarTask.cancel();
- if (bossAITask != null) bossAITask.cancel();
- if (heightCheckTask != null) heightCheckTask.cancel();
- if (antiWallTask != null) antiWallTask.cancel();
- if (teleportTask != null) teleportTask.cancel();
-
- // Remove boss bar
- if (bossBar != null) {
- // Note: Adventure API may have different method names
- // bossBar.removeAllPlayers(); // Commented out for compatibility
- }
-
- // Remove boss entity
- if (bossEntity != null && bossEntity.isValid()) {
- bossEntity.remove();
- }
-
- // Clear collections
- playersAboveBoss.clear();
- playersNearBoss.clear();
- teleportCooldown.clear();
- playerArrowsShot.clear();
-
- // Remove reference
- bossEntity = null;
- }
-}
\ No newline at end of file
diff --git a/src/main/java/com/mmmm/story/listeners/Act2Listener.java b/src/main/java/com/mmmm/story/listeners/Act2Listener.java
index 6d41226..b33d68c 100644
--- a/src/main/java/com/mmmm/story/listeners/Act2Listener.java
+++ b/src/main/java/com/mmmm/story/listeners/Act2Listener.java
@@ -1,5 +1,6 @@
package com.mmmm.story.listeners;
+import com.mmmm.story.Cleanable;
import com.mmmm.story.MmmmStoryPlugin;
import com.mmmm.story.managers.ItemManager;
import com.mmmm.story.managers.MessageManager;
@@ -55,7 +56,7 @@
import java.util.Set;
import java.util.UUID;
-public class Act2Listener implements Listener {
+public class Act2Listener implements Listener, Cleanable {
private final MmmmStoryPlugin plugin;
private BossBar boss1BossBar;
@@ -3221,6 +3222,31 @@ public void onBoss1Death(EntityDeathEvent event) {
}
}
+ /**
+ * Release every runtime resource this listener owns.
+ *
+ * Called on plugin shutdown as well as on {@code /reload}. Bukkit cancels the
+ * scheduler tasks on its own, but without this the boss bars stay stuck on the
+ * screens of players who survive a reload.
+ */
+ @Override
+ public void cleanup() {
+ cleanupAllBossTasks();
+
+ // cleanupAllBossTasks() only clears the boss 2 bar - the boss 1 bar is normally
+ // removed by the death handler, which never runs when we shut down mid-fight.
+ if (boss1BossBar != null) {
+ for (Player player : plugin.getServer().getOnlinePlayers()) {
+ player.hideBossBar(boss1BossBar);
+ }
+ boss1BossBar = null;
+ }
+
+ boss1Entity = null;
+ boss1Phase = 1;
+ boss1Warriors.clear();
+ }
+
/**
* Clean up all boss-related tasks when boss dies
*/
diff --git a/src/main/java/com/mmmm/story/listeners/Act3Listener.java b/src/main/java/com/mmmm/story/listeners/Act3Listener.java
index 00a1819..292737e 100644
--- a/src/main/java/com/mmmm/story/listeners/Act3Listener.java
+++ b/src/main/java/com/mmmm/story/listeners/Act3Listener.java
@@ -162,7 +162,7 @@ private void summonBoss2(org.bukkit.entity.Item droppedItem) {
// Spawn Boss 2 (Enderman boss)
Location spawnLoc = location.clone().add(0, 3, 0);
Enderman boss = (Enderman) world.spawnEntity(spawnLoc, EntityType.ENDERMAN);
- boss.setCustomName(plugin.getMessageManager().getMessage("entities.end_guardian"));
+ boss.setCustomName(plugin.getMessageManager().getMessage("npc.entities.end_guardian"));
boss.setCustomNameVisible(true);
// Set attributes
diff --git a/src/main/java/com/mmmm/story/listeners/PlayerListener.java b/src/main/java/com/mmmm/story/listeners/PlayerListener.java
index efb91ce..47406e5 100644
--- a/src/main/java/com/mmmm/story/listeners/PlayerListener.java
+++ b/src/main/java/com/mmmm/story/listeners/PlayerListener.java
@@ -12,12 +12,14 @@
import org.bukkit.event.Listener;
import org.bukkit.event.entity.PlayerDeathEvent;
import org.bukkit.event.player.PlayerJoinEvent;
+import org.bukkit.event.player.PlayerQuitEvent;
import org.bukkit.event.player.PlayerRespawnEvent;
import org.bukkit.inventory.ItemStack;
import org.bukkit.scheduler.BukkitTask;
import java.util.ArrayList;
import java.util.List;
+import java.util.logging.Level;
public class PlayerListener implements Listener {
@@ -91,8 +93,7 @@ public void onPlayerJoin(PlayerJoinEvent event) {
plugin.getActManager().startCampaign();
plugin.getLogger().info("=== STORY STARTED SUCCESSFULLY ===");
} catch (Exception e) {
- plugin.getLogger().severe("=== ERROR STARTING STORY: " + e.getMessage());
- e.printStackTrace();
+ plugin.getLogger().log(Level.SEVERE, "=== ERROR STARTING STORY", e);
}
} else {
plugin.getLogger().info("Auto-start cancelled - conditions not met (Act: " + actNow + ", Players: " + playersNow + ")");
@@ -105,6 +106,17 @@ public void onPlayerJoin(PlayerJoinEvent event) {
}
}
+ /**
+ * Flush and evict the leaving player's cached profile.
+ *
+ *
{@code DataManager} loads profiles lazily and used to keep every one of them
+ * for the lifetime of the server, so the cache grew without bound.
+ */
+ @EventHandler
+ public void onPlayerQuit(PlayerQuitEvent event) {
+ plugin.getDataManager().unloadPlayer(event.getPlayer().getUniqueId());
+ }
+
@EventHandler
public void onPlayerDeath(PlayerDeathEvent event) {
Player player = event.getEntity();
diff --git a/src/main/java/com/mmmm/story/managers/DataManager.java b/src/main/java/com/mmmm/story/managers/DataManager.java
index 0eafe27..04f9f57 100644
--- a/src/main/java/com/mmmm/story/managers/DataManager.java
+++ b/src/main/java/com/mmmm/story/managers/DataManager.java
@@ -9,9 +9,12 @@
import java.io.File;
import java.io.IOException;
+import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.StandardCopyOption;
import java.util.*;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.logging.Level;
public class DataManager {
@@ -21,7 +24,8 @@ public class DataManager {
private File playersFolder;
private FileConfiguration globalData;
- private final Map playerData = new HashMap<>();
+ /** Concurrent: read from scheduler tasks as well as the main thread. */
+ private final Map playerData = new ConcurrentHashMap<>();
public DataManager(MmmmStoryPlugin plugin) {
this.plugin = plugin;
@@ -125,7 +129,52 @@ private void saveAllPlayers() {
savePlayerData(uuid);
}
}
-
+
+ /**
+ * Save everything without stalling the server tick.
+ *
+ * Bukkit configuration objects are not thread safe, so they are serialised to
+ * strings on the calling (main) thread; only the disk writes are handed to an
+ * async task. Used by the five-minute autosave, where the previous synchronous
+ * implementation wrote every cached player profile plus a backup copy inline.
+ */
+ public void saveAsync() {
+ Map pending = new LinkedHashMap<>();
+ pending.put(globalFile, globalData.saveToString());
+ for (Map.Entry entry : playerData.entrySet()) {
+ pending.put(new File(playersFolder, entry.getKey() + ".yml"), entry.getValue().saveToString());
+ }
+
+ plugin.getServer().getScheduler().runTaskAsynchronously(plugin, () -> {
+ for (Map.Entry entry : pending.entrySet()) {
+ writeFile(entry.getKey(), entry.getValue());
+ }
+ });
+ }
+
+ private void writeFile(File file, String contents) {
+ try {
+ if (file.equals(globalFile) && globalFile.exists()) {
+ Files.copy(globalFile.toPath(), new File(dataFolder, "global.yml.backup").toPath(),
+ StandardCopyOption.REPLACE_EXISTING);
+ }
+ Files.writeString(file.toPath(), contents, StandardCharsets.UTF_8);
+ } catch (IOException e) {
+ plugin.getLogger().log(Level.SEVERE, "Failed to write " + file.getName(), e);
+ }
+ }
+
+ /**
+ * Persist and drop a player's cached profile.
+ *
+ * Called from {@code PlayerQuitEvent}. Without this the {@code playerData} cache
+ * grows for every player who has ever joined and is never reclaimed.
+ */
+ public void unloadPlayer(UUID uuid) {
+ savePlayerData(uuid);
+ playerData.remove(uuid);
+ }
+
// Global data getters/setters
public int getCurrentAct() {
return globalData.getInt("act.current", 1);
diff --git a/src/main/java/com/mmmm/story/managers/DialogManager.java b/src/main/java/com/mmmm/story/managers/DialogManager.java
index 9fcedf7..5722450 100644
--- a/src/main/java/com/mmmm/story/managers/DialogManager.java
+++ b/src/main/java/com/mmmm/story/managers/DialogManager.java
@@ -266,7 +266,7 @@ public void run() {
if (matchesConfiguredDelay || matchesTriggerText) {
plugin.getLogger().info("[Dialog] Triggering messenger despawn for text: " + text);
- plugin.getNpcManager().despawnMessenger();
+ plugin.getNPCManager().despawnMessenger();
}
} catch (Exception e) {
plugin.getLogger().warning("[Dialog] Error while triggering messenger despawn: " + e.getMessage());
diff --git a/src/main/java/com/mmmm/story/managers/NPCManager.java b/src/main/java/com/mmmm/story/managers/NPCManager.java
index ddd5ab2..bf39285 100644
--- a/src/main/java/com/mmmm/story/managers/NPCManager.java
+++ b/src/main/java/com/mmmm/story/managers/NPCManager.java
@@ -1155,8 +1155,7 @@ public void enhancedDespawnMessenger() {
startEnhancedMessengerDespawn(npcId, npcLocation, animationDuration, particlesEnabled, cleanupRadius, finalCleanup, cleanupMethod);
} catch (Exception e) {
- plugin.getLogger().severe("[NPC] Error during enhanced despawn: " + e.getMessage());
- e.printStackTrace();
+ plugin.getLogger().log(java.util.logging.Level.SEVERE, "[NPC] Error during enhanced despawn", e);
// Fallback to simple removal
removeNPC(npcId);
diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml
index acb5849..29e7256 100644
--- a/src/main/resources/config.yml
+++ b/src/main/resources/config.yml
@@ -159,6 +159,9 @@ acts:
# Bug #4 & #5 Fix: Acts 4-5 portal blocking and spawn management
act4:
+ # When false, artifacts are obtained only by searching chests (Act4Listener stays
+ # unregistered). Previously this was toggled by commenting out code.
+ autoSpawnArtifacts: false
portalBlocking:
enabled: true
feedbackParticle: SMOKE # particle shown when portal blocked
diff --git a/src/main/resources/messages.yml b/src/main/resources/messages.yml
index 4279ae2..76cd753 100644
--- a/src/main/resources/messages.yml
+++ b/src/main/resources/messages.yml
@@ -52,6 +52,8 @@ boss1:
defeated_again: "§6§l⚔ Повелитель побежден снова!"
wither_skull_attack:
warning: "§5§l⚡ ПОВЕЛИТЕЛЬ ПРИЗЫВАЕТ ЧЕРЕПА ИССУШЕНИЯ!"
+ special_attack:
+ warning: "§5§l⚡ Босс готовит особую атаку! Отойдите от центра!"
# Boss 2 - Nether Fiend
boss2:
@@ -104,6 +106,7 @@ chest:
found_multiple: "§a§l✔ Найдено предметов: %count%!"
lore_updated: "§7Описание предмета обновлено!"
lore_added: "§7Добавлено описание предмета!"
+ lore_removed: "§7Описание предмета удалено!"
enchantment_added: "§7Добавлено зачарование предмета!"
enchantment_removed: "§7Удалено зачарование предмета!"
durability_changed: "§7Прочность предмета изменена!"
diff --git a/src/main/resources/messages_en.yml b/src/main/resources/messages_en.yml
index 4d40fa5..61048c6 100644
--- a/src/main/resources/messages_en.yml
+++ b/src/main/resources/messages_en.yml
@@ -6,6 +6,11 @@
npc:
messenger_name: "§6Messenger"
direction_marker: "§6You received a direction marker! (Lasts 5 minutes)"
+ entities:
+ skeleton_lord: "§4§lSkeleton Lord"
+ nether_fiend: "§4§lNether Fiend"
+ end_guardian: "§5§lEnd Guardian"
+ crystal_guardian: "§5§lCrystal Guardian"
# Act Manager
act:
@@ -133,6 +138,8 @@ chest:
hide_enchantments_removed: "§7Item enchantments no longer hidden!"
hide_tooltip_added: "§7Item tooltip hidden!"
hide_tooltip_removed: "§7Item tooltip no longer hidden!"
+ hide_flags_added: "§7Item flags hidden!"
+ hide_flags_removed: "§7Item flags no longer hidden!"
stored_success: "§a§l✔ Item stored in inventory!"
stored_failed: "§c✗ Failed to store item in inventory!"
retrieved_success: "§a§l✔ Item retrieved from inventory!"
@@ -167,6 +174,8 @@ chest:
enchanted_failed: "§c✗ Failed to enchant item!"
repaired_success: "§a§l✔ Item processed on anvil!"
repaired_failed: "§c✗ Failed to process item on anvil!"
+ anvil_success: "§a§l✔ Item processed on anvil!"
+ anvil_failed: "§c✗ Failed to process item on anvil!"
grindstone_success: "§a§l✔ Item processed on grindstone!"
grindstone_failed: "§c✗ Failed to process item on grindstone!"
brewing_stand_success: "§a§l✔ Item processed on brewing stand!"
@@ -250,7 +259,6 @@ chest:
- ""
- "▶ Drop (Q) on Dragon Egg"
- "in Boss 2 Arena for summoning"
- items:
overworld_portal_key:
name: "End Gates Key"
lore:
@@ -359,11 +367,6 @@ menu:
all_ready: "§6§lAll players ready! Starting story..."
waiting: "§eWaiting for player readiness..."
-# Additional messages from English file, now in Russian
-act5:
- all_artifacts_collected: "§a§l✔ All artifacts collected!"
- ritual_starting: "§d§l⚡ Ritual begins..."
-
# Spawn point management (Bug #5 Fix)
spawn:
saved: "§6Your respawn point has been saved."
@@ -380,6 +383,7 @@ boss:
events:
wave_incoming: "§c§lSkeleton wave incoming! Defend yourselves until dawn!"
wave_survived: "§aYou survived the skeleton warrior wave!"
+ phantom_burst: "§5§lThe Void has opened! Phantoms are approaching!"
# Achievement messages (Bug #1 Fix)
achievements:
@@ -416,15 +420,6 @@ structures:
line2: "§7End Portal"
line3: "§eAct 5"
-# NPC and Entity Names
-npc:
- messenger_name: "§6Messenger"
-entities:
- skeleton_lord: "§4§lSkeleton Lord"
- nether_fiend: "§4§lNether Fiend"
- end_guardian: "§5§lEnd Guardian"
- crystal_guardian: "§5§lCrystal Guardian"
-
# Server Start Menu
server_start_menu:
title: "§6§lDialog Settings"
diff --git a/src/test/java/com/mmmm/story/data/PlayerSettingsTest.java b/src/test/java/com/mmmm/story/data/PlayerSettingsTest.java
new file mode 100644
index 0000000..b87d103
--- /dev/null
+++ b/src/test/java/com/mmmm/story/data/PlayerSettingsTest.java
@@ -0,0 +1,63 @@
+package com.mmmm.story.data;
+
+import com.mmmm.story.data.PlayerSettings.DialogSpeed;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class PlayerSettingsTest {
+
+ @Test
+ void defaultsToVisibleDialogsAtNormalSpeed() {
+ PlayerSettings settings = new PlayerSettings();
+
+ assertTrue(settings.isShowDialogs());
+ assertEquals(DialogSpeed.NORMAL, settings.getDialogSpeed());
+ assertEquals(1.0, settings.getSpeedMultiplier());
+ }
+
+ @Test
+ void toggleDialogsFlipsBackAndForth() {
+ PlayerSettings settings = new PlayerSettings();
+
+ settings.toggleDialogs();
+ assertFalse(settings.isShowDialogs());
+
+ settings.toggleDialogs();
+ assertTrue(settings.isShowDialogs());
+ }
+
+ @Test
+ void cycleSpeedWrapsThroughEveryValue() {
+ PlayerSettings settings = new PlayerSettings(true, DialogSpeed.SLOW);
+
+ settings.cycleSpeed();
+ assertEquals(DialogSpeed.NORMAL, settings.getDialogSpeed());
+
+ settings.cycleSpeed();
+ assertEquals(DialogSpeed.FAST, settings.getDialogSpeed());
+
+ settings.cycleSpeed();
+ assertEquals(DialogSpeed.SLOW, settings.getDialogSpeed());
+ }
+
+ @Test
+ void speedMultiplierTracksTheSelectedSpeed() {
+ PlayerSettings settings = new PlayerSettings();
+
+ settings.setDialogSpeed(DialogSpeed.SLOW);
+ assertEquals(1.5, settings.getSpeedMultiplier());
+
+ settings.setDialogSpeed(DialogSpeed.FAST);
+ assertEquals(0.75, settings.getSpeedMultiplier());
+ }
+
+ @Test
+ void fromStringIsCaseInsensitiveAndFallsBackToNormal() {
+ assertEquals(DialogSpeed.FAST, DialogSpeed.fromString("fast"));
+ assertEquals(DialogSpeed.SLOW, DialogSpeed.fromString("SlOw"));
+ assertEquals(DialogSpeed.NORMAL, DialogSpeed.fromString("not-a-speed"));
+ }
+}
diff --git a/src/test/java/com/mmmm/story/managers/MessageManagerTest.java b/src/test/java/com/mmmm/story/managers/MessageManagerTest.java
index 9716d63..ed60378 100644
--- a/src/test/java/com/mmmm/story/managers/MessageManagerTest.java
+++ b/src/test/java/com/mmmm/story/managers/MessageManagerTest.java
@@ -4,12 +4,19 @@
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
+import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeSet;
import static org.junit.jupiter.api.Assertions.*;
@@ -48,4 +55,98 @@ public void testMessagesEnYmlSyntax(@TempDir Path tempDir) throws IOException {
assertTrue(config.getStringList("chest.items.end_artifact_4.lore").size() > 0, "end_artifact_4 lore should not be empty");
assertTrue(config.getStringList("chest.items.end_artifact_5.lore").size() > 0, "end_artifact_5 lore should not be empty");
}
+
+ /**
+ * The two locale files must expose exactly the same key set.
+ *
+ *
They had drifted apart in both directions: keys present only in Russian left
+ * English players on the Russian fallback, and {@code entities.end_guardian}
+ * existed only in English, so Russian players saw the raw key as the boss name.
+ */
+ @Test
+ public void testLocaleFilesHaveIdenticalKeys() throws IOException {
+ Set ru = leafKeys(load("messages.yml"));
+ Set en = leafKeys(load("messages_en.yml"));
+
+ Set missingFromEn = new TreeSet<>(ru);
+ missingFromEn.removeAll(en);
+ Set missingFromRu = new TreeSet<>(en);
+ missingFromRu.removeAll(ru);
+
+ assertTrue(missingFromEn.isEmpty(), "Keys missing from messages_en.yml: " + missingFromEn);
+ assertTrue(missingFromRu.isEmpty(), "Keys missing from messages.yml: " + missingFromRu);
+ }
+
+ /**
+ * A key defined twice silently discards the first definition. This is how the
+ * English {@code chest.items} block lost six story items and {@code act5} lost
+ * four messages.
+ */
+ @Test
+ public void testNoDuplicateKeysInLocaleFiles() throws IOException {
+ for (String file : new String[]{"messages.yml", "messages_en.yml"}) {
+ List lines = readResourceLines(file);
+ Map seen = new HashMap<>();
+ List path = new ArrayList<>();
+ List indents = new ArrayList<>();
+
+ for (int i = 0; i < lines.size(); i++) {
+ String line = lines.get(i);
+ String stripped = line.stripLeading();
+ if (stripped.isBlank() || stripped.startsWith("#") || stripped.startsWith("-")) {
+ continue;
+ }
+ int colon = stripped.indexOf(':');
+ if (colon < 0) {
+ continue;
+ }
+ String name = stripped.substring(0, colon).strip();
+ if (name.isEmpty() || name.contains("\"") || name.contains(" ")) {
+ continue;
+ }
+
+ // Pop every entry at the same or deeper indentation - those siblings and
+ // children are closed by this line.
+ int indent = line.length() - stripped.length();
+ while (!indents.isEmpty() && indents.get(indents.size() - 1) >= indent) {
+ indents.remove(indents.size() - 1);
+ path.remove(path.size() - 1);
+ }
+ path.add(name);
+ indents.add(indent);
+
+ String full = String.join(".", path);
+ Integer first = seen.put(full, i + 1);
+ assertNull(first, "Duplicate key '" + full + "' in " + file
+ + " at lines " + first + " and " + (i + 1));
+ }
+ }
+ }
+
+ private YamlConfiguration load(String resource) throws IOException {
+ try (InputStream in = getClass().getClassLoader().getResourceAsStream(resource)) {
+ assertNotNull(in, resource + " should exist");
+ return YamlConfiguration.loadConfiguration(
+ new InputStreamReader(in, StandardCharsets.UTF_8));
+ }
+ }
+
+ private List readResourceLines(String resource) throws IOException {
+ try (InputStream in = getClass().getClassLoader().getResourceAsStream(resource)) {
+ assertNotNull(in, resource + " should exist");
+ return new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8))
+ .lines().toList();
+ }
+ }
+
+ /** Every path that holds a value rather than a nested section. */
+ private Set leafKeys(YamlConfiguration config) {
+ Set leaves = new TreeSet<>();
+ for (String key : config.getKeys(true)) {
+ if (!config.isConfigurationSection(key)) {
+ leaves.add(key);
+ }
+ }
+ return leaves;
+ }
}
\ No newline at end of file