Compare commits

..
4 Commits
Author SHA1 Message Date
Eisi05 ac34ba181d Fixed NPE 2026-06-15 14:40:16 +02:00
Eisi05 66d58dd71c 2.3.3
- Performance Improvement
- Bugfixes
2026-06-15 14:07:34 +02:00
Eisi05 c861a13d5e Performance improvements 2026-06-13 19:55:00 +02:00
Eisi05 6b3b7c5e6f Fixed bugs 2026-06-11 14:05:40 +02:00
10 changed files with 272 additions and 84 deletions

No files matched your search

+4 -4
View File
@@ -44,7 +44,7 @@ This method requires [NpcPlugin](https://www.spigotmc.org/resources/npc-plugin-1
<dependency>
<groupId>com.github.Eisi05</groupId>
<artifactId>NpcApi</artifactId>
<version>2.2.2</version>
<version>2.3.3</version>
<scope>provided</scope>
</dependency>
```
@@ -60,7 +60,7 @@ dependencyResolutionManagement {
}
dependencies {
compileOnly 'com.github.Eisi05:NpcApi:2.2.2'
compileOnly 'com.github.Eisi05:NpcApi:2.3.3'
}
```
@@ -93,7 +93,7 @@ Add the repository and dependency to your `pom.xml`:
<dependency>
<groupId>com.github.Eisi05</groupId>
<artifactId>NpcApi</artifactId>
<version>2.2.2</version>
<version>2.3.3</version>
</dependency>
</dependencies>
```
@@ -110,7 +110,7 @@ dependencyResolutionManagement {
}
dependencies {
implementation 'com.github.Eisi05:NpcApi:2.2.2'
implementation 'com.github.Eisi05:NpcApi:2.3.3'
}
```
+1 -1
View File
@@ -6,7 +6,7 @@
<groupId>de.eisi05</groupId>
<artifactId>NpcApi</artifactId>
<version>2.3.2</version>
<version>2.3.3</version>
<packaging>jar</packaging>
<name>NpcApi</name>
@@ -177,6 +177,9 @@ public class NPC extends NpcHolder
@Override
public void save() throws IOException
{
if(getUUID() == null)
return;
npcPath.toFile().getParentFile().mkdirs();
new ObjectSaver(npcPath.toFile()).write(SerializedNPC.serializedNPC(this), false);
super.save();
@@ -694,6 +697,7 @@ public class NPC extends NpcHolder
npcPath.toFile().getParentFile().mkdirs();
Files.deleteIfExists(npcPath);
super.save();
}
/**
@@ -90,6 +90,12 @@ public class NpcConfig
*/
private boolean preciseSleepingHitbox = false;
/**
* If true, the NPC will asynchronously load unloaded chunks that it is walking into.
* Default: false
*/
private boolean loadChunksOnPath = false;
/**
* Sets the duration an NPC will look at a player after an interaction.
*
@@ -233,6 +239,18 @@ public class NpcConfig
return this;
}
/**
* Checks whether NPCs should load unloaded chunks on their path.
*
* @param loadChunksOnPath true to enable chunk loading, false otherwise
* @return This {@link NpcConfig} instance for method chaining. Never null.
*/
public @NotNull NpcConfig loadChunksOnPath(boolean loadChunksOnPath)
{
this.loadChunksOnPath = loadChunksOnPath;
return this;
}
/**
* Gets the configured duration an NPC will look at a player.
*
@@ -342,4 +360,14 @@ public class NpcConfig
{
return preciseSleepingHitbox;
}
/**
* Checks whether NPCs should load unloaded chunks on their path.
*
* @return true if chunks should be loaded, false otherwise
*/
public boolean loadChunksOnPath()
{
return loadChunksOnPath;
}
}
@@ -25,6 +25,7 @@ public abstract class NpcHolder implements InventoryHolder
{
protected static final UUID GLOBAL_UUID = UUID.fromString("00000000-0000-0000-0000-000000000000");
protected final Map<UUID, Map<NpcOption<?, ?>, Object>> options = new HashMap<>();
private final Map<UUID, NamespacedKey> playerKeyCache = new HashMap<>();
protected NpcName name;
@@ -235,7 +236,8 @@ public abstract class NpcHolder implements InventoryHolder
*/
protected @NotNull NamespacedKey getKey(@NotNull Player player)
{
return new NamespacedKey(NpcApi.plugin, getUUID() + "-" + player.getUniqueId());
return playerKeyCache.computeIfAbsent(player.getUniqueId(),
uuid -> new NamespacedKey(NpcApi.plugin, getUUID() + "-" + uuid));
}
/**
@@ -288,8 +288,9 @@ public class NpcOption<T, S extends Serializable>
boolean modified = WrappedPlayerTeam.exists(player, teamName) || playerTeam.getHandle() != null;
WrappedPlayerTeam wrappedPlayerTeam = playerTeam.getHandle() != null ? playerTeam : WrappedPlayerTeam.create(player, teamName);
wrappedPlayerTeam.setCanSeeFriendlyInvisible(true);
wrappedPlayerTeam.setNameTagVisibility(WrappedPlayerTeam.Visibility.HIDE_FOR_OWN_TEAM);
if(!wrappedPlayerTeam.canSeeFriendlyInvisible())
wrappedPlayerTeam.setCanSeeFriendlyInvisible(true);
var teamPacket = SetPlayerTeamPacket.createAddOrModifyPacket(wrappedPlayerTeam, !modified);
@@ -578,7 +579,8 @@ public class NpcOption<T, S extends Serializable>
var teamPair = getTeam(player, npc);
WrappedPlayerTeam team = teamPair.getFirst();
team.setNameTagVisibility(WrappedPlayerTeam.Visibility.NEVER);
if(team.getNameTagVisibility() != WrappedPlayerTeam.Visibility.NEVER)
team.setNameTagVisibility(WrappedPlayerTeam.Visibility.NEVER);
if(!team.getName().startsWith("trans"))
team.getPlayers().add(npc.getServerPlayer().getName());
@@ -1,5 +1,6 @@
package de.eisi05.npc.api.scheduler;
import de.eisi05.npc.api.NpcApi;
import de.eisi05.npc.api.enums.WalkingResult;
import de.eisi05.npc.api.events.NpcStopWalkingEvent;
import de.eisi05.npc.api.objects.NPC;
@@ -61,6 +62,7 @@ public class PathTask extends BukkitRunnable
private float previousYaw;
private double verticalVelocity = 0.0;
private int viewerRefreshTicks = 0;
private boolean isWaitingForChunkLoad = false;
/**
* Private constructor used by the Builder pattern.
@@ -103,6 +105,33 @@ public class PathTask extends BukkitRunnable
@Override
public void run()
{
World world = npc.getLocation().getWorld();
if(world == null) return;
int currentChunkX = currentPos.getBlockX() >> 4;
int currentChunkZ = currentPos.getBlockZ() >> 4;
if (!world.isChunkLoaded(currentChunkX, currentChunkZ))
{
handleUnloadedChunk(world, currentChunkX, currentChunkZ);
return;
}
if (index < pathPoints.size())
{
Location next = pathPoints.get(index);
int nextChunkX = next.getBlockX() >> 4;
int nextChunkZ = next.getBlockZ() >> 4;
if (!world.isChunkLoaded(nextChunkX, nextChunkZ))
{
handleUnloadedChunk(world, nextChunkX, nextChunkZ);
return;
}
}
isWaitingForChunkLoad = false;
if(autoManageWalkingViewers && viewerRefreshTicks++ >= 10)
{
viewerRefreshTicks = 0;
@@ -124,42 +153,85 @@ public class PathTask extends BukkitRunnable
return;
}
processDoors();
cleanupDoors();
int chunkX = currentPos.getBlockX() >> 4;
int chunkZ = currentPos.getBlockZ() >> 4;
boolean isChunkLoaded = world.isChunkLoaded(chunkX, chunkZ);
Vector movement = calculateHorizontalMovement(toTarget, target);
if(movement.lengthSquared() < 1e-6 && index < pathPoints.size() && currentPos.equals(target))
return;
PhysicsResult physics = applyPhysics(movement, toTarget);
if(physics.skipHorizontal)
if(isChunkLoaded)
{
movement.setX(0);
movement.setZ(0);
}
else if(physics.horizontalSlowdown < 1.0)
movement.multiply(physics.horizontalSlowdown);
processDoors();
cleanupDoors();
movement.setY(physics.yChange);
Vector movement = calculateHorizontalMovement(toTarget, target);
currentPos.add(movement);
if(movement.lengthSquared() < 1e-6 && index < pathPoints.size() && currentPos.equals(target))
return;
float yaw, pitch;
if(withRotation)
{
float[] rotation = calculateSmoothRotation();
yaw = rotation[0];
pitch = rotation[1];
PhysicsResult physics = applyPhysics(movement, toTarget);
if(physics.skipHorizontal)
{
movement.setX(0);
movement.setZ(0);
}
else if(physics.horizontalSlowdown < 1.0)
movement.multiply(physics.horizontalSlowdown);
movement.setY(physics.yChange);
currentPos.add(movement);
float yaw, pitch;
if(withRotation)
{
float[] rotation = calculateSmoothRotation();
yaw = rotation[0];
pitch = rotation[1];
}
else
{
yaw = npc.getLocation().getYaw();
pitch = npc.getLocation().getPitch();
}
sendMovePackets(movement, yaw, pitch, physics.isGrounded);
}
else
{
yaw = npc.getLocation().getYaw();
pitch = npc.getLocation().getPitch();
}
double distanceToTarget = toTarget.length();
double moveDist = Math.min(speed, distanceToTarget);
sendMovePackets(movement, yaw, pitch, physics.isGrounded);
if(distanceToTarget > 1e-6)
{
Vector movement = toTarget.normalize().multiply(moveDist);
currentPos.add(movement);
}
else
currentPos = target.clone();
if(withRotation)
{
float[] rotation = calculateSmoothRotation();
previousYaw = rotation[0];
previousPitch = rotation[1];
}
if(updateRealLocation)
npc.setLocation(currentPos.toLocation(world));
}
}
/**
* Handles the logic when an NPC encounters an unloaded chunk.
*/
private void handleUnloadedChunk(World world, int chunkX, int chunkZ)
{
if (NpcApi.config.loadChunksOnPath() && !isWaitingForChunkLoad)
{
isWaitingForChunkLoad = true;
world.getChunkAt(chunkX, chunkZ);
isWaitingForChunkLoad = false;
}
}
/**
@@ -1,7 +1,6 @@
package de.eisi05.npc.api.utils;
import com.google.common.primitives.Primitives;
import de.eisi05.npc.api.wrapper.Wrapper;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -13,14 +12,15 @@ import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
/**
* Utility class providing methods for reflective operations such as loading classes,
* invoking methods, and accessing fields dynamically at runtime.
* Utility class providing methods for reflective operations such as loading classes, invoking methods, and accessing fields dynamically at runtime.
*/
@SuppressWarnings("unchecked")
public class Reflections
{
private static final ConcurrentHashMap<MethodKey, Method> METHOD_CACHE = new ConcurrentHashMap<>();
private static final ConcurrentHashMap<FieldKey, Field> FIELD_CACHE = new ConcurrentHashMap<>();
private static final ConcurrentHashMap<ConstructorKey, Constructor<?>> CONSTRUCTOR_CACHE = new ConcurrentHashMap<>();
private static final ConcurrentHashMap<String, Class<?>> CLASS_CACHE = new ConcurrentHashMap<>();
/**
* Loads a class by its fully qualified name.
@@ -33,8 +33,16 @@ public class Reflections
{
try
{
return Optional.of((Class<T>) Class.forName(path));
} catch(ClassNotFoundException e)
Class<?> cached = CLASS_CACHE.get(path);
if(cached != null)
return Optional.of((Class<T>) cached);
Class<?> clazz = Class.forName(path);
CLASS_CACHE.put(path, clazz);
return Optional.of((Class<T>) clazz);
}
catch(ClassNotFoundException e)
{
return Optional.empty();
}
@@ -55,39 +63,35 @@ public class Reflections
public static <T> @NotNull Optional<T> getInstance(@NotNull Class<T> clazz, @Nullable Object... args)
{
final Object[] newArgs = Wrapper.checkArgs(args);
try
{
Class<?>[] argTypes = Arrays.stream(newArgs)
.map(Object::getClass)
.toArray(Class[]::new);
Class<?>[] argTypes = args == null ? new Class<?>[0] : Arrays.stream(args)
.map(o -> o == null ? Object.class : o.getClass())
.toArray(Class<?>[]::new);
for(Constructor<?> ctor : clazz.getDeclaredConstructors())
ConstructorKey key = new ConstructorKey(clazz, argTypes);
Constructor<?> ctor = CONSTRUCTOR_CACHE.get(key);
if(ctor == null)
{
Class<?>[] paramTypes = ctor.getParameterTypes();
if(paramTypes.length != argTypes.length)
continue;
boolean match = true;
for(int i = 0; i < paramTypes.length; i++)
for(Constructor<?> constructor : clazz.getDeclaredConstructors())
{
if(!paramTypes[i].isAssignableFrom(argTypes[i]))
if(isCompatible(argTypes, constructor.getParameterTypes(), constructor.isVarArgs()))
{
match = false;
ctor = constructor;
ctor.setAccessible(true);
CONSTRUCTOR_CACHE.put(key, ctor);
break;
}
}
if(match)
{
ctor.setAccessible(true);
return Optional.of((T) ctor.newInstance(newArgs));
}
}
return Optional.empty();
} catch(Exception e)
if(ctor == null)
throw new NoSuchMethodException("No compatible constructor found in class " + clazz.getName() + " for arguments " + Arrays.toString(argTypes));
return Optional.of((T) ctor.newInstance(args));
}
catch(Exception e)
{
return Optional.empty();
}
@@ -105,7 +109,7 @@ public class Reflections
private static @NotNull Method findMethod(@NotNull Class<?> clazz, @NotNull String name, @Nullable Object[] args) throws NoSuchMethodException
{
Class<?>[] argTypes = args == null ? new Class<?>[0] : Arrays.stream(args)
.map(Object::getClass)
.map(o -> o == null ? Object.class : o.getClass())
.toArray(Class<?>[]::new);
MethodKey key = new MethodKey(clazz, name, argTypes);
@@ -140,8 +144,7 @@ public class Reflections
/**
* Checks if a given set of argument types is compatible with the parameter types of a method.
* <p>
* This method supports both regular and varargs methods. It also handles primitive-to-wrapper
* type conversions (e.g., int to Integer).
* 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.
@@ -190,13 +193,13 @@ public class Reflections
*/
public static <V> @NotNull ReflectionChain<V> invokeMethod(@NotNull Object object, @NotNull String methodName, @Nullable Object... args)
{
args = Wrapper.checkArgs(args);
try
{
Method method = findMethod(object.getClass(), methodName, args);
method.setAccessible(true);
return new ReflectionChain<>((V) method.invoke(object, args));
} catch(Exception e)
}
catch(Exception e)
{
throw new RuntimeException(e);
}
@@ -213,14 +216,37 @@ public class Reflections
*/
public static <V> @NotNull ReflectionChain<V> invokeStaticMethod(@NotNull String classPath, @NotNull String methodName, @Nullable Object... args)
{
args = Wrapper.checkArgs(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)
}
catch(Exception e)
{
throw new RuntimeException(e);
}
}
/**
* Invokes a static method of a class given its fully qualified name.
*
* @param clazz the class of the method
* @param methodName name of the static method
* @param args method arguments
* @param <V> return type of the method
* @return a ReflectionChain wrapping the method's return value
*/
public static <V> @NotNull ReflectionChain<V> invokeStaticMethod(@NotNull Class<?> clazz, @NotNull String methodName, @Nullable Object... args)
{
try
{
Method method = findMethod(clazz, methodName, args);
method.setAccessible(true);
return new ReflectionChain<>((V) method.invoke(null, args));
}
catch(Exception e)
{
throw new RuntimeException(e);
}
@@ -249,7 +275,8 @@ public class Reflections
field.setAccessible(true);
FIELD_CACHE.put(key, field);
return field;
} catch(NoSuchFieldException ignored)
}
catch(NoSuchFieldException ignored)
{
clazz = clazz.getSuperclass();
}
@@ -271,7 +298,8 @@ public class Reflections
{
Field field = findField(object.getClass(), fieldName);
return new ReflectionChain<>((T) field.get(object));
} catch(Exception e)
}
catch(Exception e)
{
throw new RuntimeException(e);
}
@@ -292,7 +320,8 @@ public class Reflections
{
Field field = findField(clazz, fieldName);
return (V) field.get(null);
} catch(Exception e)
}
catch(Exception e)
{
throw new RuntimeException(e);
}
@@ -313,7 +342,8 @@ public class Reflections
Class<?> clazz = Class.forName(classPath);
Field field = findField(clazz, fieldName);
return (T) field.get(null);
} catch(Exception e)
}
catch(Exception e)
{
throw new RuntimeException(e);
}
@@ -332,7 +362,8 @@ public class Reflections
{
Field field = findField(object.getClass(), fieldName);
field.set(object, value);
} catch(Exception e)
}
catch(Exception e)
{
throw new RuntimeException(e);
}
@@ -352,7 +383,8 @@ public class Reflections
Class<?> clazz = Class.forName(classPath);
Field field = findField(clazz, fieldName);
field.set(null, value);
} catch(Exception e)
}
catch(Exception e)
{
throw new RuntimeException(e);
}
@@ -401,6 +433,27 @@ public class Reflections
}
}
private record ConstructorKey(Class<?> clazz, Class<?>[] paramTypes)
{
@Override
public boolean equals(Object o)
{
if(this == o)
return true;
if(!(o instanceof ConstructorKey constructorKey))
return false;
return clazz.equals(constructorKey.clazz) && Arrays.equals(paramTypes, constructorKey.paramTypes);
}
@Override
public int hashCode()
{
int result = clazz.hashCode();
result = 31 * result + Arrays.hashCode(paramTypes);
return result;
}
}
/**
* Helper class to chain reflection calls on the result of previous reflective operations.
*
@@ -432,13 +485,13 @@ public class Reflections
if(value == null)
return new ReflectionChain<>(null);
args = Wrapper.checkArgs(args);
try
{
Method method = findMethod(value.getClass(), methodName, args);
method.setAccessible(true);
return new ReflectionChain<>((V) method.invoke(value, args));
} catch(Exception e)
}
catch(Exception e)
{
throw new RuntimeException(e);
}
@@ -458,7 +511,8 @@ public class Reflections
{
Field field = findField(value.getClass(), fieldName);
return new ReflectionChain<>((V) field.get(value));
} catch(Exception e)
}
catch(Exception e)
{
throw new RuntimeException(e);
}
@@ -7,6 +7,7 @@ import de.eisi05.npc.api.wrapper.Wrapper;
import de.eisi05.npc.api.wrapper.enums.ChatFormat;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Collection;
import java.util.HashMap;
@@ -80,6 +81,21 @@ public class WrappedPlayerTeam extends Wrapper
return teams.getOrDefault(player.getUniqueId(), new HashMap<>()).containsKey(name);
}
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "getNameTagVisibility")
@Mapping(range = @Mapping.Range(from = Versions.V1_21_5, to = Versions.V1_21_11), path = "k")
@Mapping(range = @Mapping.Range(from = Versions.V1_18, to = Versions.V1_21_4), path = "j")
@Mapping(fixed = @Mapping.Fixed(Versions.V1_17), path = "getNameTagVisibility")
public @Nullable Visibility getNameTagVisibility()
{
Object enumHandle = getWrappedFieldValue();
for(Visibility visibility : Visibility.values())
{
if(visibility.getHandle().equals(enumHandle))
return visibility;
}
return null;
}
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "setNameTagVisibility")
@Mapping(range = @Mapping.Range(from = Versions.V1_18, to = Versions.V1_21_11), path = "a")
@Mapping(fixed = @Mapping.Fixed(Versions.V1_17), path = "setNameTagVisibility")
@@ -104,6 +120,15 @@ public class WrappedPlayerTeam extends Wrapper
invokeWrappedMethod(canSeeFriendlyInvisible);
}
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "canSeeFriendlyInvisibles")
@Mapping(range = @Mapping.Range(from = Versions.V1_21_5, to = Versions.V1_21_11), path = "j")
@Mapping(range = @Mapping.Range(from = Versions.V1_18, to = Versions.V1_21_4), path = "i")
@Mapping(fixed = @Mapping.Fixed(Versions.V1_17), path = "canSeeFriendlyInvisibles")
public boolean canSeeFriendlyInvisible()
{
return invokeWrappedMethod();
}
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "getPlayers")
@Mapping(range = @Mapping.Range(from = Versions.V1_21_5, to = Versions.V1_21_11), path = "h")
@Mapping(range = @Mapping.Range(from = Versions.V1_18, to = Versions.V1_21_4), path = "g")
@@ -139,7 +164,8 @@ public class WrappedPlayerTeam extends Wrapper
}
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "net.minecraft.world.scores.Team$Visibility")
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_11), path = "net.minecraft.world.scores.ScoreboardTeamBase$EnumNameTagVisibility")
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_11), path = "net.minecraft.world.scores" +
".ScoreboardTeamBase$EnumNameTagVisibility")
public enum Visibility implements EnumWrapper
{
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "ALWAYS")
@@ -11,8 +11,8 @@ import org.jetbrains.annotations.Nullable;
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_11), path = "net.minecraft.network.protocol.game.PacketPlayInUseEntity")
public class UseEntityPacketWrapper extends PacketWrapper.PacketHolder
{
@Mapping(range = @Mapping.Range(from = Versions.V1_21, to = Versions.V1_21_11), path = "e")
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_20_6), path = "d")
@Mapping(range = @Mapping.Range(from = Versions.V1_20_6, to = Versions.V1_21_11), path = "e")
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_20_4), path = "d")
public final static Action ATTACK_ACTION = new Action(getStaticWrappedFieldValue("ATTACK_ACTION").orElse(null));
protected UseEntityPacketWrapper(Object handle)
@@ -21,8 +21,8 @@ public class UseEntityPacketWrapper extends PacketWrapper.PacketHolder
}
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "entityId")
@Mapping(range = @Mapping.Range(from = Versions.V1_21, to = Versions.V1_21_11), path = "b")
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_20_6), path = "a")
@Mapping(range = @Mapping.Range(from = Versions.V1_20_6, to = Versions.V1_21_11), path = "b")
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_20_4), path = "a")
public int getId()
{
return getWrappedFieldValue();