Compare commits
19
Commits
2.2.2
...
old/java17
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ac34ba181d | ||
|
|
66d58dd71c | ||
|
|
c861a13d5e | ||
|
|
6b3b7c5e6f | ||
|
|
a5f2cc5378 | ||
|
|
9dd167219a | ||
|
|
5491ba198b | ||
|
|
991cea235a | ||
|
|
19fefc2fd3 | ||
|
|
24a50f7239 | ||
|
|
7489f7024b | ||
|
|
4d37ce07af | ||
|
|
d5df3e9ea5 | ||
|
|
66836699f5 | ||
|
|
d11cf4d95e | ||
|
|
ec0db9e578 | ||
|
|
e653f102e3 | ||
|
|
e2dded2988 | ||
|
|
692da7013d |
No files matched your search
@@ -7,10 +7,7 @@ on:
|
||||
jobs:
|
||||
notify_comment:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
# ⬇️ ADD THIS CONDITION TO THE *JOB*
|
||||
# This skips the *entire job* if the commenter is the user you want to ignore.
|
||||
# Replace 'YOUR-USERNAME-TO-IGNORE' with your actual GitHub login.
|
||||
|
||||
if: github.event.comment.user.login != 'Eisi05'
|
||||
|
||||
steps:
|
||||
@@ -22,15 +19,9 @@ jobs:
|
||||
const comment = context.payload.comment;
|
||||
const issue = context.payload.issue;
|
||||
|
||||
// ⬇️ REMOVE THIS LINE - the job-level 'if' handles this now.
|
||||
// if (comment.user.login === github.actor) return;
|
||||
|
||||
// 1. Comment body (truncate to 1024)
|
||||
let body = comment.body || 'No content provided.';
|
||||
if (body.length > 1024) body = body.substring(0, 1021) + '...';
|
||||
|
||||
// ... (rest of your script is good) ...
|
||||
|
||||
body = "\n" + body + "\n";
|
||||
function escapeJSON(str) {
|
||||
return str.replace(/\\/g, '\\\\')
|
||||
@@ -45,7 +36,6 @@ jobs:
|
||||
core.setOutput('title', escapeJSON(`Comment on #${issue.number}: ${issue.title}`));
|
||||
core.setOutput('description', body);
|
||||
core.setOutput('color', color);
|
||||
// ... (all your other core.setOutput lines) ...
|
||||
core.setOutput('labels', escapeJSON(labelsText));
|
||||
core.setOutput('issue_url', issue.html_url);
|
||||
core.setOutput('comment_author', comment.user.login);
|
||||
|
||||
@@ -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.1</version>
|
||||
<version>2.3.3</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
```
|
||||
@@ -60,7 +60,7 @@ dependencyResolutionManagement {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compileOnly 'com.github.Eisi05:NpcApi:2.2.1'
|
||||
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.1</version>
|
||||
<version>2.3.3</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
```
|
||||
@@ -110,7 +110,7 @@ dependencyResolutionManagement {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'com.github.Eisi05:NpcApi:2.2.1'
|
||||
implementation 'com.github.Eisi05:NpcApi:2.3.3'
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
<groupId>de.eisi05</groupId>
|
||||
<artifactId>NpcApi</artifactId>
|
||||
<version>2.2.2</version>
|
||||
<version>2.3.3</version>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<name>NpcApi</name>
|
||||
|
||||
@@ -31,17 +31,20 @@ import java.util.function.Function;
|
||||
*/
|
||||
public final class NpcApi
|
||||
{
|
||||
private static final List<Listener> listeners = List.of(new ChangeWorldListener(), new ConnectionListener(), new NpcInteractListener(),
|
||||
new WorldLoadListener(), new ServerReadyListener(), new ProjectileHitListener());
|
||||
private static final List<Listener> listeners = new ArrayList<>(List.of(new ChangeWorldListener(), new ConnectionListener(), new NpcInteractListener(),
|
||||
new WorldLoadListener(), new ServerReadyListener(), new ProjectileHitListener()));
|
||||
|
||||
/**
|
||||
* A static reference to the Bukkit plugin instance that is using this API. This is set during the API's initialization.
|
||||
*/
|
||||
public static Plugin plugin;
|
||||
public static Function<Player, String> DISABLED_MESSAGE_PROVIDER = player -> ChatColor.RED + "DISABLED";
|
||||
|
||||
/**
|
||||
* The configuration object for the NPC API, containing various settings like the look-at timer.
|
||||
*/
|
||||
public static NpcConfig config = new NpcConfig();
|
||||
private static Listener sleepListener;
|
||||
private static NpcApi npcApi;
|
||||
|
||||
/**
|
||||
@@ -56,6 +59,9 @@ public final class NpcApi
|
||||
NpcApi.plugin = plugin;
|
||||
NpcApi.config = config;
|
||||
|
||||
if(config.preciseSleepingHitbox())
|
||||
listeners.add(sleepListener = new NpcSleepListener());
|
||||
|
||||
listeners.forEach(listener -> Bukkit.getPluginManager().registerEvents(listener, plugin));
|
||||
|
||||
ConfigurationSerialization.registerClass(Path.class);
|
||||
@@ -129,6 +135,18 @@ public final class NpcApi
|
||||
npcApi = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reloads the sleep listener, unregistering the old one and registering a new one.
|
||||
*/
|
||||
public static void reloadSleepListener()
|
||||
{
|
||||
if(sleepListener != null && !config.preciseSleepingHitbox())
|
||||
HandlerList.unregisterAll(sleepListener);
|
||||
|
||||
if(config.preciseSleepingHitbox())
|
||||
Bukkit.getPluginManager().registerEvents(sleepListener = new NpcSleepListener(), plugin);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a function that provides the message shown when an NPC is disabled.
|
||||
*
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package de.eisi05.npc.api.ai;
|
||||
|
||||
import de.eisi05.npc.api.objects.NPC;
|
||||
import de.eisi05.npc.api.utils.SerializablePredicate;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
@@ -15,6 +17,7 @@ public abstract class Goal implements Serializable
|
||||
@Serial
|
||||
private final static long serialVersionUID = 1L;
|
||||
private Priority priority;
|
||||
private SerializablePredicate<NPC> condition;
|
||||
|
||||
/**
|
||||
* Creates a new goal with the specified priority.
|
||||
@@ -30,6 +33,26 @@ public abstract class Goal implements Serializable
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the condition for this goal. The condition is checked before the goal is executed.
|
||||
*
|
||||
* @return The condition for this goal
|
||||
*/
|
||||
public @Nullable SerializablePredicate<NPC> getCondition()
|
||||
{
|
||||
return condition;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the condition for this goal. The condition is checked before the goal is executed.
|
||||
*
|
||||
* @param condition The condition to set
|
||||
*/
|
||||
public void setCondition(@Nullable SerializablePredicate<NPC> condition)
|
||||
{
|
||||
this.condition = condition;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the priority of this goal.
|
||||
*
|
||||
@@ -56,7 +79,10 @@ public abstract class Goal implements Serializable
|
||||
* @param npc The NPC to check for
|
||||
* @return true if this goal can be used, false otherwise
|
||||
*/
|
||||
protected abstract boolean canUse(@NotNull NPC npc);
|
||||
protected boolean canUse(@NotNull NPC npc)
|
||||
{
|
||||
return condition == null || condition.test(npc);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when this goal starts executing. Use this to initialize any state needed for the goal.
|
||||
@@ -87,7 +113,7 @@ public abstract class Goal implements Serializable
|
||||
*/
|
||||
protected boolean canContinue(@NotNull NPC npc)
|
||||
{
|
||||
return canUse(npc);
|
||||
return condition == null || condition.test(npc);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -5,6 +5,7 @@ import de.eisi05.npc.api.ai.Goal;
|
||||
import de.eisi05.npc.api.objects.NPC;
|
||||
import de.eisi05.npc.api.objects.NpcOption;
|
||||
import de.eisi05.npc.api.utils.LocationUtils;
|
||||
import de.eisi05.npc.api.utils.Reflections;
|
||||
import de.eisi05.npc.api.utils.SerializableBiPredicate;
|
||||
import de.eisi05.npc.api.wrapper.enums.Pose;
|
||||
import de.eisi05.npc.api.wrapper.objects.WrappedEntityData;
|
||||
@@ -38,10 +39,12 @@ public class AttackEntityGoal extends Goal
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private static final double WEAPONMECHANICS_ATTACK_RANGE = 25.0;
|
||||
private static final double BOW_ATTACK_RANGE = 15.0;
|
||||
private static final double MELEE_ATTACK_RANGE = 3.0;
|
||||
private static final double LINE_OF_SIGHT_RANGE = 25.0;
|
||||
private static final int BOW_DRAW_DELAY_TICKS = 25;
|
||||
private static final int WEAPONMECHANICS_SHOOT_DELAY_TICKS = 5;
|
||||
private static final double KITING_DISTANCE = 3.0;
|
||||
private static final double OPTIMAL_RANGED_DISTANCE = 6.0;
|
||||
|
||||
@@ -179,6 +182,9 @@ public class AttackEntityGoal extends Goal
|
||||
if(targetFilter == null)
|
||||
return false;
|
||||
|
||||
if(!super.canUse(npc))
|
||||
return false;
|
||||
|
||||
if(target != null && target.isValid() && canContinue(npc))
|
||||
return true;
|
||||
|
||||
@@ -350,6 +356,9 @@ public class AttackEntityGoal extends Goal
|
||||
if(!isAttacking || target == null || !target.isValid())
|
||||
return false;
|
||||
|
||||
if(!super.canContinue(npc))
|
||||
return false;
|
||||
|
||||
Location npcLoc = npc.getLocation();
|
||||
Location targetLoc = target.getLocation();
|
||||
|
||||
@@ -450,8 +459,10 @@ public class AttackEntityGoal extends Goal
|
||||
if(mainHand == null)
|
||||
return MELEE_ATTACK_RANGE;
|
||||
|
||||
Material type = mainHand.getType();
|
||||
if(isRangedWeapon(type))
|
||||
if(getWeaponTitle(mainHand) != null)
|
||||
return WEAPONMECHANICS_ATTACK_RANGE;
|
||||
|
||||
if(isRangedWeapon(mainHand.getType()))
|
||||
return BOW_ATTACK_RANGE;
|
||||
else
|
||||
return MELEE_ATTACK_RANGE;
|
||||
@@ -468,6 +479,36 @@ public class AttackEntityGoal extends Goal
|
||||
if(mainHand == null || !mainHand.hasItemMeta())
|
||||
return 5;
|
||||
|
||||
String weaponTitle = getWeaponTitle(mainHand);
|
||||
if(weaponTitle != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
Object config = Reflections.invokeMethod("me.deecaad.weaponmechanics.WeaponMechanics", "getInstance")
|
||||
.thanInvoke("getWeaponConfigurations").get();
|
||||
if(config != null)
|
||||
{
|
||||
|
||||
int shotsPerSecond = (int) Reflections.invokeMethod(config, "getInt", weaponTitle + ".Shoot.Fully_Automatic_Shots_Per_Second", 0).get();
|
||||
if(shotsPerSecond > 0)
|
||||
return Math.max(1, 20 / shotsPerSecond);
|
||||
|
||||
|
||||
int burstDelay = (int) Reflections.invokeMethod(config, "getInt", weaponTitle + ".Shoot.Burst.Ticks_Between_Each_Shot", 0).get();
|
||||
if(burstDelay > 0)
|
||||
return burstDelay;
|
||||
|
||||
|
||||
int shootDelay = (int) Reflections.invokeMethod(config, "getInt", weaponTitle + ".Shoot.Delay_Between_Shots", 0).get();
|
||||
if(shootDelay > 0)
|
||||
return shootDelay;
|
||||
}
|
||||
}
|
||||
catch(NoClassDefFoundError | Exception ignored) {}
|
||||
|
||||
return 10;
|
||||
}
|
||||
|
||||
ItemMeta meta = mainHand.getItemMeta();
|
||||
if(meta == null)
|
||||
return 5;
|
||||
@@ -491,12 +532,66 @@ public class AttackEntityGoal extends Goal
|
||||
Map<EquipmentSlot, ItemStack> equipment = npc.getOption(NpcOption.EQUIPMENT);
|
||||
ItemStack mainHand = equipment != null ? equipment.get(EquipmentSlot.HAND) : null;
|
||||
|
||||
if(mainHand != null && isRangedWeapon(mainHand.getType()))
|
||||
String weaponTitle = getWeaponTitle(mainHand);
|
||||
if(weaponTitle != null)
|
||||
performWeaponMechanicsAttack(npc, weaponTitle, mainHand);
|
||||
else if(mainHand != null && isRangedWeapon(mainHand.getType()))
|
||||
performRangedAttack(npc);
|
||||
else
|
||||
performMeleeAttack(npc);
|
||||
}
|
||||
|
||||
/**
|
||||
* Utilizes WeaponMechanics API to gather weapon identification title safely.
|
||||
*/
|
||||
private String getWeaponTitle(ItemStack item)
|
||||
{
|
||||
if(item == null || item.getType() == Material.AIR)
|
||||
return null;
|
||||
|
||||
try
|
||||
{
|
||||
return (String) Reflections.invokeMethod("me.deecaad.weaponmechanics.WeaponMechanicsAPI", "getWeaponTitle", item).get();
|
||||
}
|
||||
catch (NoClassDefFoundError | Exception e)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes native programmatic WeaponMechanics shooting actions centered around the NPC.
|
||||
*/
|
||||
private void performWeaponMechanicsAttack(@NotNull NPC npc, String weaponTitle, ItemStack weaponStack)
|
||||
{
|
||||
if(target == null || !target.isValid())
|
||||
return;
|
||||
|
||||
Player npcPlayer = npc.getServerPlayer().getBukkitPlayer();
|
||||
if(npcPlayer == null)
|
||||
return;
|
||||
|
||||
if(target == null || !target.isValid())
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
Vector direction = target.getLocation().subtract(npc.getLocation()).toVector().normalize();
|
||||
Location location = npc.getLocation().clone().add(0, npc.getEyeHeight(null), 0);
|
||||
location.setDirection(direction);
|
||||
|
||||
Object entityWrapper = Reflections.invokeMethod("me.deecaad.weaponmechanics.WeaponMechanics", "getInstance")
|
||||
.thanInvoke("getEntityWrapper", npcPlayer).get();
|
||||
Reflections.invokeMethod("me.deecaad.weaponmechanics.WeaponMechanics", "getInstance")
|
||||
.thanInvoke("getWeaponHandler").thanInvoke("getShootHandler")
|
||||
.thanInvoke("shoot", entityWrapper, weaponTitle, weaponStack, location, true, true, false);
|
||||
}
|
||||
catch(NoClassDefFoundError | Exception e)
|
||||
{
|
||||
target.damage(getAttackDamage(npc), npcPlayer);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a ranged attack.
|
||||
*/
|
||||
@@ -624,7 +719,7 @@ public class AttackEntityGoal extends Goal
|
||||
{
|
||||
Map<EquipmentSlot, ItemStack> equipment = npc.getOption(NpcOption.EQUIPMENT);
|
||||
ItemStack mainHand = equipment != null ? equipment.get(EquipmentSlot.HAND) : null;
|
||||
return mainHand != null && isRangedWeapon(mainHand.getType());
|
||||
return (mainHand != null && isRangedWeapon(mainHand.getType())) || getWeaponTitle(mainHand) != null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -172,6 +172,9 @@ public class FollowEntityGoal extends Goal
|
||||
if(targetEntityId == null)
|
||||
return false;
|
||||
|
||||
if(!super.canUse(npc))
|
||||
return false;
|
||||
|
||||
LivingEntity potentialTarget = target;
|
||||
if(potentialTarget == null || !potentialTarget.isValid())
|
||||
{
|
||||
@@ -239,7 +242,7 @@ public class FollowEntityGoal extends Goal
|
||||
}
|
||||
}
|
||||
|
||||
Location targetLoc = target.getLocation();
|
||||
Location targetLoc = target.getLocation().clone();
|
||||
if(cachedViewers == null || cachedViewers.size() != npc.getViewers().size())
|
||||
updateCachedViewers(npc);
|
||||
|
||||
@@ -340,6 +343,9 @@ public class FollowEntityGoal extends Goal
|
||||
if(target == null || !target.isValid())
|
||||
return false;
|
||||
|
||||
if(!super.canContinue(npc))
|
||||
return false;
|
||||
|
||||
Location npcLoc = npc.getLocation();
|
||||
Location targetLoc = target.getLocation();
|
||||
|
||||
|
||||
@@ -123,7 +123,7 @@ public class LookAroundGoal extends Goal
|
||||
@Override
|
||||
public boolean canUse(@NotNull NPC npc)
|
||||
{
|
||||
return minDuration >= 0 && maxDuration >= 0;
|
||||
return minDuration >= 0 && maxDuration >= 0 && super.canUse(npc);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -208,7 +208,7 @@ public class LookAroundGoal extends Goal
|
||||
@Override
|
||||
public boolean canContinue(@NotNull NPC npc)
|
||||
{
|
||||
return isLooking;
|
||||
return isLooking && super.canContinue(npc);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -68,7 +68,7 @@ public class WaitGoal extends Goal
|
||||
@Override
|
||||
public boolean canUse(@NotNull NPC npc)
|
||||
{
|
||||
return ticksRemaining > 0 || durationTicks > 0;
|
||||
return (ticksRemaining > 0 || durationTicks > 0) && super.canUse(npc);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -113,7 +113,7 @@ public class WaitGoal extends Goal
|
||||
@Override
|
||||
public boolean canContinue(@NotNull NPC npc)
|
||||
{
|
||||
return ticksRemaining > 0;
|
||||
return ticksRemaining > 0 && super.canContinue(npc);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -0,0 +1,406 @@
|
||||
package de.eisi05.npc.api.ai.goals;
|
||||
|
||||
import de.eisi05.npc.api.ai.Goal;
|
||||
import de.eisi05.npc.api.enums.WalkingResult;
|
||||
import de.eisi05.npc.api.objects.NPC;
|
||||
import de.eisi05.npc.api.pathfinding.Path;
|
||||
import de.eisi05.npc.api.utils.SerializableConsumer;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* A goal that makes the NPC walk along a predefined path of waypoints.
|
||||
* The NPC will walk the entire path without recalculating, using the pre-calculated path.
|
||||
*/
|
||||
public class WalkPathGoal extends Goal
|
||||
{
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private static final double DEFAULT_SPEED = 0.25;
|
||||
private static final double START_LOCATION_THRESHOLD = 50.0 * 50.0;
|
||||
|
||||
private Path.SerializablePath serializablePath;
|
||||
private double speed;
|
||||
private SerializableConsumer<WalkingResult> completionCallback;
|
||||
private boolean withRotation;
|
||||
|
||||
private transient Path path;
|
||||
private transient boolean isWalking;
|
||||
private transient WalkToLocationGoal walkToStartGoal;
|
||||
private transient boolean walkingToStart;
|
||||
|
||||
/**
|
||||
* Creates a WalkPathGoal with full configuration options.
|
||||
*
|
||||
* @param builder the builder containing the configuration options
|
||||
*/
|
||||
public WalkPathGoal(@NotNull Builder builder)
|
||||
{
|
||||
super(Priority.MEDIUM);
|
||||
this.path = builder.path;
|
||||
this.serializablePath = path != null ? path.toSerializablePath() : null;
|
||||
this.speed = Math.max(0.1, Math.min(1.0, builder.speed));
|
||||
this.completionCallback = builder.completionCallback;
|
||||
this.withRotation = builder.withRotation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a copy of this goal.
|
||||
*
|
||||
* @param goal the goal to copy
|
||||
*/
|
||||
private WalkPathGoal(@NotNull WalkPathGoal goal)
|
||||
{
|
||||
super(goal.getPriority());
|
||||
this.serializablePath = goal.serializablePath;
|
||||
this.speed = goal.speed;
|
||||
this.completionCallback = goal.completionCallback;
|
||||
this.withRotation = goal.withRotation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the path for this goal.
|
||||
*
|
||||
* @return the path
|
||||
*/
|
||||
public @Nullable Path getPath()
|
||||
{
|
||||
return path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the path for this goal.
|
||||
*
|
||||
* @param path the new path
|
||||
*/
|
||||
public void setPath(@NotNull Path path)
|
||||
{
|
||||
this.path = path;
|
||||
this.serializablePath = path.toSerializablePath();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the speed for this goal.
|
||||
*
|
||||
* @return the speed
|
||||
*/
|
||||
public double getSpeed()
|
||||
{
|
||||
return speed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the speed for this goal.
|
||||
*
|
||||
* @param speed the new speed
|
||||
*/
|
||||
public void setSpeed(double speed)
|
||||
{
|
||||
this.speed = Math.max(0.1, Math.min(1.0, speed));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Gets the completion callback for the walk path goal.
|
||||
*
|
||||
* @return the completion callback
|
||||
*/
|
||||
public @Nullable SerializableConsumer<WalkingResult> getCompletionCallback()
|
||||
{
|
||||
return completionCallback;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the completion callback for the walk path goal.
|
||||
*
|
||||
* @param completionCallback the new completion callback
|
||||
*/
|
||||
public void setCompletionCallback(@Nullable SerializableConsumer<WalkingResult> completionCallback)
|
||||
{
|
||||
this.completionCallback = completionCallback;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets whether the NPC should rotate to face the target location.
|
||||
*
|
||||
* @return true if the NPC should rotate
|
||||
*/
|
||||
public boolean isWithRotation()
|
||||
{
|
||||
return withRotation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether the NPC should rotate to face the target location.
|
||||
*
|
||||
* @param withRotation the new rotation setting
|
||||
*/
|
||||
public void setWithRotation(boolean withRotation)
|
||||
{
|
||||
this.withRotation = withRotation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this goal can be used by the NPC.
|
||||
*
|
||||
* @param npc the NPC to check
|
||||
* @return true if the path is valid, start location is close enough, and not already walking
|
||||
*/
|
||||
@Override
|
||||
public boolean canUse(@NotNull NPC npc)
|
||||
{
|
||||
if(isWalking)
|
||||
return true;
|
||||
|
||||
if(!super.canUse(npc))
|
||||
return false;
|
||||
|
||||
if(path == null)
|
||||
return false;
|
||||
|
||||
List<Location> waypoints = path.asLocations();
|
||||
if(waypoints == null || waypoints.isEmpty())
|
||||
return false;
|
||||
|
||||
Location startLocation = waypoints.get(0);
|
||||
if(!startLocation.getWorld().equals(npc.getLocation().getWorld()))
|
||||
return false;
|
||||
|
||||
double distance = npc.getLocation().distanceSquared(startLocation);
|
||||
return distance <= START_LOCATION_THRESHOLD;
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the walk path goal, first walking to the start location if needed, then walking the entire path.
|
||||
*
|
||||
* @param npc the NPC starting this goal
|
||||
*/
|
||||
@Override
|
||||
public void start(@NotNull NPC npc)
|
||||
{
|
||||
if(path == null)
|
||||
return;
|
||||
|
||||
List<Location> waypoints = path.asLocations();
|
||||
if(waypoints == null || waypoints.isEmpty())
|
||||
return;
|
||||
|
||||
Location startLocation = waypoints.get(0);
|
||||
double distance = npc.getLocation().distance(startLocation);
|
||||
|
||||
isWalking = true;
|
||||
|
||||
if(distance > START_LOCATION_THRESHOLD)
|
||||
{
|
||||
walkingToStart = true;
|
||||
walkToStartGoal = new WalkToLocationGoal.Builder(startLocation)
|
||||
.speed(speed)
|
||||
.withRotation(withRotation)
|
||||
.completionCallback(result ->
|
||||
{
|
||||
walkToStartGoal = null;
|
||||
walkingToStart = false;
|
||||
|
||||
if(result == WalkingResult.SUCCESS)
|
||||
walkPath(npc);
|
||||
else
|
||||
{
|
||||
isWalking = false;
|
||||
if(completionCallback != null)
|
||||
completionCallback.accept(result);
|
||||
}
|
||||
})
|
||||
.build();
|
||||
walkToStartGoal.start(npc);
|
||||
}
|
||||
else
|
||||
walkPath(npc);
|
||||
}
|
||||
|
||||
/**
|
||||
* Walks the actual path after reaching the start location.
|
||||
*
|
||||
* @param npc the NPC to walk
|
||||
*/
|
||||
private void walkPath(@NotNull NPC npc)
|
||||
{
|
||||
if(path == null)
|
||||
return;
|
||||
|
||||
List<Location> waypoints = path.asLocations();
|
||||
if(waypoints == null || waypoints.isEmpty())
|
||||
return;
|
||||
|
||||
List<Player> viewers = npc.getViewers().stream()
|
||||
.map(Bukkit::getPlayer)
|
||||
.filter(Objects::nonNull)
|
||||
.toList();
|
||||
|
||||
Location finalLocation = waypoints.get(waypoints.size() - 1);
|
||||
|
||||
npc.walkTo(path, speed, true, result ->
|
||||
{
|
||||
isWalking = false;
|
||||
if(completionCallback != null)
|
||||
completionCallback.accept(result);
|
||||
if(result == WalkingResult.SUCCESS)
|
||||
npc.changeRealLocation(finalLocation);
|
||||
}, withRotation, viewers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ticks the walk path goal, handling both walking to start and walking the path.
|
||||
*
|
||||
* @param npc the NPC to update
|
||||
*/
|
||||
@Override
|
||||
public void tick(@NotNull NPC npc)
|
||||
{
|
||||
if(walkingToStart && walkToStartGoal != null)
|
||||
{
|
||||
walkToStartGoal.tick(npc);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops the walk path goal and cancels current walking.
|
||||
*
|
||||
* @param npc the NPC stopping this goal
|
||||
*/
|
||||
@Override
|
||||
public void stop(@NotNull NPC npc)
|
||||
{
|
||||
if(!isWalking)
|
||||
return;
|
||||
|
||||
if(walkToStartGoal != null)
|
||||
{
|
||||
walkToStartGoal.stop(npc);
|
||||
walkToStartGoal = null;
|
||||
}
|
||||
|
||||
List<Player> viewers = npc.getViewers().stream()
|
||||
.map(Bukkit::getPlayer)
|
||||
.filter(Objects::nonNull)
|
||||
.toList();
|
||||
|
||||
for(Player viewer : viewers)
|
||||
{
|
||||
if(npc.isWalking(viewer))
|
||||
npc.cancelWalking(viewer);
|
||||
}
|
||||
|
||||
isWalking = false;
|
||||
walkingToStart = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this goal should continue running.
|
||||
*
|
||||
* @param npc the NPC to check
|
||||
* @return true if still walking and has waypoints remaining
|
||||
*/
|
||||
@Override
|
||||
public boolean canContinue(@NotNull NPC npc)
|
||||
{
|
||||
return isWalking && super.canContinue(npc);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean canBeRemovedNow(@NotNull NPC npc)
|
||||
{
|
||||
if(super.canBeRemovedNow(npc))
|
||||
return true;
|
||||
|
||||
if(isWalking)
|
||||
{
|
||||
Location npcLoc = npc.getLocation();
|
||||
Location below = npcLoc.clone().subtract(0, 0.1, 0);
|
||||
|
||||
if(below.getBlock().getType().isAir())
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull Goal copy()
|
||||
{
|
||||
return new WalkPathGoal(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builder class for creating WalkPathGoal instances with a fluent API.
|
||||
*/
|
||||
public static class Builder
|
||||
{
|
||||
private final Path path;
|
||||
private double speed = DEFAULT_SPEED;
|
||||
private SerializableConsumer<WalkingResult> completionCallback;
|
||||
private boolean withRotation = true;
|
||||
|
||||
/**
|
||||
* Creates a new Builder with the required path.
|
||||
*
|
||||
* @param path The path to walk along
|
||||
*/
|
||||
public Builder(@NotNull Path path)
|
||||
{
|
||||
this.path = path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the walking speed.
|
||||
*
|
||||
* @param speed The walking speed (0.1 to 1.0)
|
||||
* @return this builder for chaining
|
||||
*/
|
||||
public Builder speed(double speed)
|
||||
{
|
||||
this.speed = Math.max(0.1, Math.min(1.0, speed));
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the completion callback.
|
||||
*
|
||||
* @param completionCallback Callback called when path walking completes
|
||||
* @return this builder for chaining
|
||||
*/
|
||||
public Builder completionCallback(@NotNull SerializableConsumer<WalkingResult> completionCallback)
|
||||
{
|
||||
this.completionCallback = completionCallback;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether rotation packets should be sent.
|
||||
*
|
||||
* @param withRotation If true, includes rotation packets in the movement; otherwise only position packets are sent.
|
||||
* @return this builder for chaining
|
||||
*/
|
||||
public Builder withRotation(boolean withRotation)
|
||||
{
|
||||
this.withRotation = withRotation;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the WalkPathGoal instance.
|
||||
*
|
||||
* @return A new WalkPathGoal instance
|
||||
*/
|
||||
public WalkPathGoal build()
|
||||
{
|
||||
return new WalkPathGoal(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,15 +4,15 @@ import de.eisi05.npc.api.NpcApi;
|
||||
import de.eisi05.npc.api.ai.Goal;
|
||||
import de.eisi05.npc.api.enums.WalkingResult;
|
||||
import de.eisi05.npc.api.objects.NPC;
|
||||
import de.eisi05.npc.api.objects.NpcOption;
|
||||
import de.eisi05.npc.api.pathfinding.AStarPathfinder;
|
||||
import de.eisi05.npc.api.pathfinding.Path;
|
||||
import de.eisi05.npc.api.pathfinding.PathfindingUtils;
|
||||
import de.eisi05.npc.api.scheduler.Tasks;
|
||||
import de.eisi05.npc.api.utils.SerializableConsumer;
|
||||
import de.eisi05.npc.api.wrapper.objects.WrappedEntity;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.block.BlockFace;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
@@ -30,13 +30,11 @@ import java.util.concurrent.CompletableFuture;
|
||||
*/
|
||||
public class WalkToLocationGoal extends Goal
|
||||
{
|
||||
static final int RECALCULATION_COOLDOWN = 20;
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public static final int DEFAULT_MAX_ITERATIONS = 5000;
|
||||
public static final double DEFAULT_SPEED = 0.25;
|
||||
static final int RECALCULATION_COOLDOWN = 20;
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
private static final int PATH_CHECK_AHEAD = 5;
|
||||
private static final long PATHABILITY_CHECK_INTERVAL_MS = 5000;
|
||||
|
||||
@@ -200,6 +198,9 @@ public class WalkToLocationGoal extends Goal
|
||||
if(isWalking)
|
||||
return true;
|
||||
|
||||
if(!super.canUse(npc))
|
||||
return false;
|
||||
|
||||
if(targetLocation == null || !targetLocation.getWorld().equals(npc.getLocation().getWorld()))
|
||||
return false;
|
||||
|
||||
@@ -279,7 +280,8 @@ public class WalkToLocationGoal extends Goal
|
||||
@Override
|
||||
public boolean canContinue(@NotNull NPC npc)
|
||||
{
|
||||
return isWalking && currentPath != null && targetLocation != null && npc.getLocation().distance(targetLocation) > 1.0;
|
||||
return isWalking && currentPath != null && targetLocation != null && npc.getLocation().distance(targetLocation) > 1.0
|
||||
&& super.canContinue(npc);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -292,7 +294,7 @@ public class WalkToLocationGoal extends Goal
|
||||
{
|
||||
Location npcLoc = npc.getLocation();
|
||||
Location below = npcLoc.clone().subtract(0, 0.1, 0);
|
||||
|
||||
|
||||
if(below.getBlock().getType().isAir())
|
||||
return false;
|
||||
}
|
||||
@@ -343,7 +345,7 @@ public class WalkToLocationGoal extends Goal
|
||||
return;
|
||||
}
|
||||
|
||||
CompletableFuture<Path> future = PathfindingUtils.findPathAsync(List.of(start, end), maxIterations, allowDiagonal, null);
|
||||
CompletableFuture<Path> future = npc.findPathAsync(List.of(start, end), maxIterations, allowDiagonal, null);
|
||||
Tasks.trackFuture(future);
|
||||
future.thenAcceptAsync(path -> pathable = path != null, task -> Bukkit.getScheduler().runTask(NpcApi.plugin, task))
|
||||
.exceptionally(e ->
|
||||
@@ -373,7 +375,7 @@ public class WalkToLocationGoal extends Goal
|
||||
|
||||
isWalking = true;
|
||||
|
||||
pathfindingFuture = PathfindingUtils.findPathAsync(List.of(start, end), maxIterations, allowDiagonal, null);
|
||||
pathfindingFuture = npc.findPathAsync(List.of(start, end), maxIterations, allowDiagonal, null);
|
||||
Tasks.trackFuture(pathfindingFuture);
|
||||
pathfindingFuture.thenAcceptAsync(path ->
|
||||
{
|
||||
@@ -442,6 +444,8 @@ public class WalkToLocationGoal extends Goal
|
||||
if(npcLoc.getWorld() == null)
|
||||
return true;
|
||||
|
||||
WrappedEntity.BoundingBox boundingBox = npc.entity.getBoundingBox();
|
||||
double scale = npc.getOption(NpcOption.SCALE);
|
||||
int checkAhead = Math.min(PATH_CHECK_AHEAD, currentPath.getWaypoints().size());
|
||||
for(int i = 0; i < checkAhead; i++)
|
||||
{
|
||||
@@ -449,14 +453,8 @@ public class WalkToLocationGoal extends Goal
|
||||
if(!waypoint.getWorld().equals(npcLoc.getWorld()))
|
||||
return true;
|
||||
|
||||
Block floor = waypoint.getBlock();
|
||||
Block spaceFeet = waypoint.getBlock().getRelative(BlockFace.UP);
|
||||
Block spaceHead = waypoint.getBlock().getRelative(BlockFace.UP).getRelative(BlockFace.UP);
|
||||
|
||||
if(!AStarPathfinder.isSafeFloor(floor))
|
||||
return true;
|
||||
|
||||
if(AStarPathfinder.isSolid(spaceFeet) || AStarPathfinder.isSolid(spaceHead))
|
||||
if(!AStarPathfinder.isPositionValid(npcLoc.getWorld(), waypoint.getX(), waypoint.getY(), waypoint.getZ(), boundingBox.getYSize() * scale,
|
||||
boundingBox.getXSize() * scale))
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -170,7 +170,7 @@ public class WanderGoal extends Goal
|
||||
@Override
|
||||
public boolean canUse(@NotNull NPC npc)
|
||||
{
|
||||
return radius >= 0 && minDelay >= 0 && maxDelay >= 0 && speed >= 0;
|
||||
return radius >= 0 && minDelay >= 0 && maxDelay >= 0 && speed >= 0 && super.canUse(npc);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -240,7 +240,7 @@ public class WanderGoal extends Goal
|
||||
@Override
|
||||
public boolean canContinue(@NotNull NPC npc)
|
||||
{
|
||||
return targetLocation != null;
|
||||
return targetLocation != null && super.canContinue(npc);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -23,7 +23,10 @@ public class ChangeWorldListener implements Listener
|
||||
NpcManager.getList().forEach(npc ->
|
||||
{
|
||||
if(npc.getVisibilityManager().shouldShowToPlayer(event.getPlayer().getUniqueId()))
|
||||
{
|
||||
npc.showNPCToPlayer(event.getPlayer());
|
||||
npc.addWalkingViewer(event.getPlayer());
|
||||
}
|
||||
});
|
||||
}
|
||||
}.runTaskLater(NpcApi.plugin, 10L);
|
||||
|
||||
@@ -40,6 +40,7 @@ public class ConnectionListener implements Listener
|
||||
continue;
|
||||
|
||||
npc.showNPCToPlayer(event.getPlayer());
|
||||
npc.addWalkingViewer(event.getPlayer());
|
||||
NpcSkin npcSkin = npc.getOption(NpcOption.SKIN, event.getPlayer());
|
||||
if(npcSkin == null || npcSkin.isStatic() || npcSkin.getPlaceholder() == null || npc.getOption(NpcOption.USE_PLAYER_SKIN, event.getPlayer()))
|
||||
continue;
|
||||
@@ -60,6 +61,7 @@ public class ConnectionListener implements Listener
|
||||
for(NPC npc : NpcManager.getList())
|
||||
{
|
||||
npc.nameCache.remove(event.getPlayer().getUniqueId());
|
||||
npc.removeWalkingViewer(event.getPlayer());
|
||||
|
||||
if(NpcApi.config.autoManageVisibility())
|
||||
npc.hideNpcFromPlayer(event.getPlayer());
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
package de.eisi05.npc.api.listeners;
|
||||
|
||||
import de.eisi05.npc.api.enums.ClickActionType;
|
||||
import de.eisi05.npc.api.manager.NpcManager;
|
||||
import de.eisi05.npc.api.objects.NPC;
|
||||
import de.eisi05.npc.api.objects.NpcOption;
|
||||
import de.eisi05.npc.api.utils.NpcHitboxUtil;
|
||||
import de.eisi05.npc.api.utils.PacketReader;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.entity.Pose;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.block.Action;
|
||||
import org.bukkit.event.player.PlayerInteractEvent;
|
||||
import org.bukkit.inventory.EquipmentSlot;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
public class NpcSleepListener implements Listener
|
||||
{
|
||||
private static boolean checkForRay(@NotNull Player player, @NotNull ClickActionType type)
|
||||
{
|
||||
double rangeSq = 25.0;
|
||||
Location eye = player.getEyeLocation();
|
||||
World world = eye.getWorld();
|
||||
|
||||
for(NPC npc : NpcManager.getList())
|
||||
{
|
||||
Location npcLoc = npc.getLocation();
|
||||
|
||||
if(npcLoc.getWorld() != world)
|
||||
continue;
|
||||
|
||||
if(npc.getOption(NpcOption.POSE, player) != Pose.SLEEPING)
|
||||
continue;
|
||||
|
||||
if(npcLoc.distanceSquared(eye) > rangeSq)
|
||||
continue;
|
||||
|
||||
if(NpcHitboxUtil.rayIntersectsNpc(npc, true, player))
|
||||
{
|
||||
PacketReader.callNpc(player, npc, type);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onInteract(PlayerInteractEvent event)
|
||||
{
|
||||
if(event.getHand() == EquipmentSlot.OFF_HAND)
|
||||
return;
|
||||
|
||||
Action action = event.getAction();
|
||||
Player player = event.getPlayer();
|
||||
if(action == Action.RIGHT_CLICK_BLOCK || action == Action.RIGHT_CLICK_AIR)
|
||||
event.setCancelled(checkForRay(player, ClickActionType.RIGHT));
|
||||
|
||||
if(action == Action.LEFT_CLICK_BLOCK || action == Action.LEFT_CLICK_AIR)
|
||||
event.setCancelled(checkForRay(player, ClickActionType.LEFT));
|
||||
}
|
||||
}
|
||||
@@ -38,7 +38,10 @@ public class WorldLoadListener implements Listener
|
||||
players.forEach(player ->
|
||||
{
|
||||
if(npc.getVisibilityManager().shouldShowToPlayer(player.getUniqueId()))
|
||||
{
|
||||
npc.showNPCToPlayer(player);
|
||||
npc.addWalkingViewer(player);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -56,11 +59,6 @@ public class WorldLoadListener implements Listener
|
||||
.stream()
|
||||
.filter(npc -> npc.getLocation().getWorld().getUID().equals(event.getWorld().getUID()))
|
||||
.filter(npc -> (npc.getLocation().getBlockX() >> 4) == chunkX && ((npc.getLocation().getBlockZ() >> 4) == chunkZ))
|
||||
.forEach(npc ->
|
||||
players.forEach(player ->
|
||||
{
|
||||
if(npc.getVisibilityManager().shouldShowToPlayer(player.getUniqueId()))
|
||||
npc.showNPCToPlayer(player);
|
||||
}));
|
||||
.forEach(npc -> players.forEach(npc::hideNpcFromPlayer));
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,7 @@ public class NpcVisibilityManager implements Serializable
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private final Set<UUID> specificPlayers = new HashSet<>();
|
||||
private boolean showToAllPlayers = false;
|
||||
private boolean showToAllPlayers = true;
|
||||
|
||||
/**
|
||||
* Sets whether this NPC should be shown to all players (including new ones joining).
|
||||
|
||||
@@ -11,6 +11,7 @@ import de.eisi05.npc.api.events.NpcStartWalkingEvent;
|
||||
import de.eisi05.npc.api.interfaces.NpcClickAction;
|
||||
import de.eisi05.npc.api.manager.NpcManager;
|
||||
import de.eisi05.npc.api.manager.NpcVisibilityManager;
|
||||
import de.eisi05.npc.api.pathfinding.PathfindingUtils;
|
||||
import de.eisi05.npc.api.scheduler.PathTask;
|
||||
import de.eisi05.npc.api.utils.ObjectSaver;
|
||||
import de.eisi05.npc.api.utils.Var;
|
||||
@@ -43,6 +44,8 @@ import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.time.Instant;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@@ -56,6 +59,7 @@ public class NPC extends NpcHolder
|
||||
private final List<UUID> viewers = new ArrayList<>();
|
||||
private final Path npcPath;
|
||||
private final Map<UUID, PathTask> pathTasks = new HashMap<>();
|
||||
private final Set<PathTask> walkingTasks = new LinkedHashSet<>();
|
||||
public WrappedEntity<?> entity;
|
||||
WrappedServerPlayer serverPlayer;
|
||||
|
||||
@@ -173,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();
|
||||
@@ -287,15 +294,26 @@ public class NPC extends NpcHolder
|
||||
*/
|
||||
public void reload()
|
||||
{
|
||||
boolean hasUnsavedChanges = hasUnsavedChanges();
|
||||
final List<UUID> viewers = new ArrayList<>(this.viewers);
|
||||
NpcVisibilityManager visibilityManager = getVisibilityManager();
|
||||
boolean shouldShowToAll = visibilityManager.shouldShowToAllPlayers();
|
||||
Set<UUID> specificPlayers = visibilityManager.getSpecificPlayers();
|
||||
hideNpcFromAllPlayers();
|
||||
WrappedPlayerTeam.clear(getServerPlayer().getName());
|
||||
viewers.stream().filter(uuid -> Bukkit.getPlayer(uuid) != null).forEach(uuid -> showNPCToPlayer(Bukkit.getPlayer(uuid)));
|
||||
visibilityManager.setShowToAllPlayers(shouldShowToAll);
|
||||
specificPlayers.forEach(visibilityManager::addSpecificPlayer);
|
||||
viewers.stream().filter(uuid -> Bukkit.getPlayer(uuid) != null).forEach(uuid -> showNPCToPlayer(Bukkit.getPlayer(uuid)));
|
||||
if(!hasUnsavedChanges)
|
||||
{
|
||||
try
|
||||
{
|
||||
super.save();
|
||||
}
|
||||
catch(IOException e)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -679,6 +697,22 @@ public class NPC extends NpcHolder
|
||||
|
||||
npcPath.toFile().getParentFile().mkdirs();
|
||||
Files.deleteIfExists(npcPath);
|
||||
super.save();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the eye height of the NPC for a specific viewer.
|
||||
*
|
||||
* @param viewer The player to get the eye height for. If null, the default eye height is returned.
|
||||
* @return The eye height of the NPC.
|
||||
*/
|
||||
public double getEyeHeight(@Nullable Player viewer)
|
||||
{
|
||||
org.bukkit.entity.Pose pose = viewer == null ? getOption(NpcOption.POSE) : getOption(NpcOption.POSE, viewer);
|
||||
double scale = viewer == null ? getOption(NpcOption.SCALE) : getOption(NpcOption.SCALE, viewer);
|
||||
double eyeHeight = (entity.getBukkitPlayer() instanceof LivingEntity le ? le.getEyeHeight() :
|
||||
entity.getBukkitPlayer().getHeight()) - (Pose.fromBukkit(pose) == Pose.SITTING ? 0.625 : 0);
|
||||
return eyeHeight * scale;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -718,7 +752,7 @@ public class NPC extends NpcHolder
|
||||
if(entity == null)
|
||||
return;
|
||||
|
||||
Location npcLoc = entity.getBukkitPlayer().getLocation();
|
||||
Location npcLoc = location;
|
||||
Location playerLoc = viewer.getLocation();
|
||||
|
||||
if(npcLoc.getWorld() != playerLoc.getWorld())
|
||||
@@ -792,6 +826,56 @@ public class NPC extends NpcHolder
|
||||
player.sendPacket(new MoveEntityPacket.Rot(entity.getId(), yawByte, pitchByte, serverPlayer.isOnGround()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Asynchronously calculates a path through the specified waypoints.
|
||||
* <p>
|
||||
* This method executes the pathfinding logic off the main thread using the Bukkit scheduler.
|
||||
* </p>
|
||||
*
|
||||
* @param waypoints the list of locations the path must pass through, must not be null
|
||||
* @param maxIterations the maximum number of iterations allowed for the pathfinding algorithm before giving up
|
||||
* @param allowDiagonalMovement {@code true} to allow diagonal movement between nodes, {@code false} for straight lines only
|
||||
* @param progressListener an optional listener to receive progress updates (current iteration, max iterations), can be null
|
||||
* @return a {@link CompletableFuture} that will complete with the calculated {@link de.eisi05.npc.api.pathfinding.Path}
|
||||
* @throws RuntimeException if an underlying {@link PathfindingUtils.PathfindingException} occurs during execution
|
||||
*/
|
||||
public @NotNull CompletableFuture<de.eisi05.npc.api.pathfinding.Path> findPathAsync(@NotNull List<Location> waypoints, int maxIterations,
|
||||
boolean allowDiagonalMovement,
|
||||
@Nullable BiConsumer<Integer, Integer> progressListener)
|
||||
{
|
||||
return CompletableFuture.supplyAsync(() ->
|
||||
{
|
||||
try
|
||||
{
|
||||
return findPath(waypoints, maxIterations, allowDiagonalMovement, progressListener);
|
||||
}
|
||||
catch(PathfindingUtils.PathfindingException e)
|
||||
{
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}, runnable -> Bukkit.getScheduler().runTaskAsynchronously(NpcApi.plugin, runnable));
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronously calculates a path through the specified waypoints based on the entity's current bounding box and scale.
|
||||
*
|
||||
* @param waypoints the list of locations the path must pass through, must not be null
|
||||
* @param maxIterations the maximum number of iterations allowed for the pathfinding algorithm before giving up
|
||||
* @param allowDiagonalMovement {@code true} to allow diagonal movement between nodes, {@code false} for straight lines only
|
||||
* @param progressListener an optional listener to receive progress updates (current iteration, max iterations), can be null
|
||||
* @return the calculated {@link de.eisi05.npc.api.pathfinding.Path}, must not be null
|
||||
* @throws PathfindingUtils.PathfindingException if the pathfinding algorithm fails to find a valid path or encounters an error
|
||||
*/
|
||||
public @NotNull de.eisi05.npc.api.pathfinding.Path findPath(@NotNull List<Location> waypoints, int maxIterations, boolean allowDiagonalMovement,
|
||||
@Nullable BiConsumer<Integer, Integer> progressListener)
|
||||
throws PathfindingUtils.PathfindingException
|
||||
{
|
||||
WrappedEntity.BoundingBox boundingBox = entity.getBoundingBox();
|
||||
double scale = getOption(NpcOption.SCALE);
|
||||
return PathfindingUtils.findPath(waypoints, maxIterations, allowDiagonalMovement, boundingBox.getYSize() * scale,
|
||||
boundingBox.getXSize() * scale, progressListener);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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).
|
||||
@@ -838,31 +922,53 @@ public class NPC extends NpcHolder
|
||||
* @return The {@link BukkitTask} representing the movement task.
|
||||
*/
|
||||
public @NotNull BukkitTask walkTo(@NotNull de.eisi05.npc.api.pathfinding.Path path, double walkSpeed,
|
||||
boolean changeRealLocation, @Nullable Consumer<WalkingResult> onEnd, boolean withRotation, @Nullable List<Player> viewers)
|
||||
boolean changeRealLocation, @Nullable Consumer<WalkingResult> onEnd,
|
||||
boolean withRotation, @Nullable List<Player> viewers)
|
||||
{
|
||||
viewers = viewers == null || viewers.isEmpty() ? this.viewers.stream().map(Bukkit::getPlayer).filter(Objects::nonNull).toList() : viewers;
|
||||
boolean explicitViewers = viewers != null && !viewers.isEmpty();
|
||||
|
||||
boolean autoManageWalkingViewers = !explicitViewers
|
||||
&& NpcApi.config.autoManageVisibility()
|
||||
&& NpcApi.config.autoManageWalkingViewers();
|
||||
|
||||
if(!explicitViewers)
|
||||
{
|
||||
if(autoManageWalkingViewers)
|
||||
{
|
||||
viewers = Bukkit.getOnlinePlayers().stream()
|
||||
.filter(player -> canShowWalkingTo(player, getLocation()))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
else
|
||||
{
|
||||
viewers = this.viewers.stream()
|
||||
.map(Bukkit::getPlayer)
|
||||
.filter(Objects::nonNull)
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
|
||||
for(Player player : viewers)
|
||||
{
|
||||
if(isWalking(player))
|
||||
cancelWalking(player);
|
||||
}
|
||||
|
||||
final double speed = Math.max(Math.min(walkSpeed, 1), 0.1);
|
||||
|
||||
NpcStartWalkingEvent event = new NpcStartWalkingEvent(this, path, speed, changeRealLocation);
|
||||
Bukkit.getPluginManager().callEvent(event);
|
||||
if(event.isCancelled())
|
||||
return null;
|
||||
|
||||
PathTask pathTask = new PathTask.Builder(this, path)
|
||||
.speed(event.getWalkSpeed())
|
||||
.viewers(viewers.toArray(new Player[0]))
|
||||
.updateRealLocation(event.isChangeRealLocation())
|
||||
.callback(onEnd)
|
||||
.withRotation(withRotation)
|
||||
.autoManageWalkingViewers(autoManageWalkingViewers)
|
||||
.build();
|
||||
|
||||
walkingTasks.add(pathTask);
|
||||
|
||||
for(Player player : viewers)
|
||||
pathTasks.put(player.getUniqueId(), pathTask);
|
||||
|
||||
@@ -870,31 +976,201 @@ public class NPC extends NpcHolder
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the NPC is currently walking.
|
||||
* <p>
|
||||
* The NPC is considered walking if a path task exists and has not yet finished.
|
||||
* Checks whether this NPC has any active walking task.
|
||||
*
|
||||
* @param viewer Player who is viewing the NPC
|
||||
* @return true if the NPC is still walking, false otherwise
|
||||
* @return true if this NPC is walking; false otherwise
|
||||
*/
|
||||
public boolean isWalking(@NotNull Player viewer)
|
||||
public boolean isWalking()
|
||||
{
|
||||
return pathTasks.containsKey(viewer.getUniqueId()) && !pathTasks.get(viewer.getUniqueId()).isFinished();
|
||||
return walkingTasks.stream().anyMatch(task -> !task.isFinished());
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels the NPC's current walking task if one is active.
|
||||
* <p>
|
||||
* If the NPC is walking, the path task is canceled and cleared. If no walking task is active, this method has no effect.
|
||||
* Checks whether this NPC is currently walking for the specified viewer.
|
||||
*
|
||||
* @param viewer Player who is viewing the NPC
|
||||
* @param viewer the viewer to check
|
||||
* @return true if this NPC has an active walking task for the viewer; false otherwise
|
||||
*/
|
||||
public boolean isWalking(@NotNull Player viewer)
|
||||
{
|
||||
PathTask task = pathTasks.get(viewer.getUniqueId());
|
||||
return task != null && !task.isFinished();
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels this NPC's walking task for the specified viewer if one is active.
|
||||
* <p>
|
||||
* For automatically managed walking tasks, this only removes the viewer from the task and does not cancel the shared walking task for other viewers.
|
||||
* <p>
|
||||
* For manually created viewer-specific walking tasks, the task itself is cancelled.
|
||||
*
|
||||
* @param viewer the viewer whose walking task should be cancelled
|
||||
*/
|
||||
public void cancelWalking(@NotNull Player viewer)
|
||||
{
|
||||
if(!pathTasks.containsKey(viewer.getUniqueId()))
|
||||
PathTask task = pathTasks.remove(viewer.getUniqueId());
|
||||
if(task == null)
|
||||
return;
|
||||
|
||||
pathTasks.remove(viewer.getUniqueId()).cancel();
|
||||
task.removeViewer(viewer);
|
||||
|
||||
if(task.isAutoManageWalkingViewers())
|
||||
return;
|
||||
|
||||
task.cancel();
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels all active walking tasks for this NPC.
|
||||
* <p>
|
||||
* If the NPC is currently walking, each unique path task is cancelled and cleared. If no walking task is active, this method has no effect.
|
||||
*/
|
||||
public void cancelWalking()
|
||||
{
|
||||
for(PathTask task : new HashSet<>(walkingTasks))
|
||||
task.cancel();
|
||||
|
||||
walkingTasks.clear();
|
||||
pathTasks.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether a player is allowed to receive packets for this NPC's current walking position.
|
||||
* <p>
|
||||
* A player is eligible only if:
|
||||
* <ul>
|
||||
* <li>the player is online,</li>
|
||||
* <li>the player is in the same world as the walking NPC,</li>
|
||||
* <li>the player passes this NPC's visibility rules,</li>
|
||||
* <li>the player is within {@link NpcConfig#walkingViewerDistance()} blocks, unless disabled.</li>
|
||||
* </ul>
|
||||
*
|
||||
* @param player the player to check
|
||||
* @param currentLocation the NPC's current walking location
|
||||
* @return true if the player should receive walking packets; false otherwise
|
||||
*/
|
||||
public boolean canShowWalkingTo(@NotNull Player player, @NotNull Location currentLocation)
|
||||
{
|
||||
if(!player.isOnline())
|
||||
return false;
|
||||
|
||||
if(currentLocation.getWorld() == null)
|
||||
return false;
|
||||
|
||||
if(!player.getWorld().equals(currentLocation.getWorld()))
|
||||
return false;
|
||||
|
||||
if(!getVisibilityManager().shouldShowToPlayer(player.getUniqueId()))
|
||||
return false;
|
||||
|
||||
double distance = NpcApi.config.walkingViewerDistance();
|
||||
if(distance >= 0 && player.getLocation().distanceSquared(currentLocation) > distance * distance)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a player to this NPC's active automatically managed walking task, if possible.
|
||||
* <p>
|
||||
* This method does not start or cancel any walking task. It only attaches the player to an already running task that allows automatic viewer management.
|
||||
*
|
||||
* @param player the player to add as a walking viewer
|
||||
*/
|
||||
public void addWalkingViewer(@NotNull Player player)
|
||||
{
|
||||
if(!NpcApi.config.autoManageVisibility() || !NpcApi.config.autoManageWalkingViewers())
|
||||
return;
|
||||
|
||||
for(PathTask task : new ArrayList<>(walkingTasks))
|
||||
{
|
||||
if(task.isFinished())
|
||||
{
|
||||
clearWalkingTask(task);
|
||||
continue;
|
||||
}
|
||||
|
||||
if(!task.isAutoManageWalkingViewers())
|
||||
continue;
|
||||
|
||||
if(!canShowWalkingTo(player, task.getCurrentLocation()))
|
||||
continue;
|
||||
|
||||
PathTask currentTask = pathTasks.get(player.getUniqueId());
|
||||
|
||||
if(currentTask == task && task.hasViewer(player))
|
||||
return;
|
||||
|
||||
if(currentTask != null && currentTask != task)
|
||||
removeWalkingViewer(player);
|
||||
|
||||
boolean newlyAdded = !task.hasViewer(player);
|
||||
|
||||
pathTasks.put(player.getUniqueId(), task);
|
||||
|
||||
if(newlyAdded)
|
||||
showNPCToPlayer(player);
|
||||
|
||||
task.addViewer(player);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a player from all active walking tasks for this NPC.
|
||||
* <p>
|
||||
* This does not cancel the walking task itself. It only stops sending movement packets to the specified player.
|
||||
*
|
||||
* @param player the player to remove
|
||||
*/
|
||||
public void removeWalkingViewer(@NotNull Player player)
|
||||
{
|
||||
PathTask task = pathTasks.remove(player.getUniqueId());
|
||||
if(task != null)
|
||||
task.removeViewer(player);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all references to a finished or cancelled walking task.
|
||||
*
|
||||
* @param task the task to clear
|
||||
*/
|
||||
public void clearWalkingTask(@NotNull PathTask task)
|
||||
{
|
||||
walkingTasks.remove(task);
|
||||
pathTasks.entrySet().removeIf(entry -> entry.getValue() == task);
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-checks all online players for active automatically managed walking tasks.
|
||||
* <p>
|
||||
* Eligible players are added to the walking task. Players who are no longer eligible, for example because they changed worlds or moved too far away, are
|
||||
* removed.
|
||||
*/
|
||||
public void refreshWalkingViewers()
|
||||
{
|
||||
if(!NpcApi.config.autoManageVisibility() || !NpcApi.config.autoManageWalkingViewers())
|
||||
return;
|
||||
|
||||
for(PathTask task : new ArrayList<>(walkingTasks))
|
||||
{
|
||||
if(task.isFinished())
|
||||
{
|
||||
clearWalkingTask(task);
|
||||
continue;
|
||||
}
|
||||
|
||||
if(!task.isAutoManageWalkingViewers())
|
||||
continue;
|
||||
|
||||
for(Player player : Bukkit.getOnlinePlayers())
|
||||
{
|
||||
if(canShowWalkingTo(player, task.getCurrentLocation()))
|
||||
addWalkingViewer(player);
|
||||
else if(pathTasks.get(player.getUniqueId()) == task)
|
||||
removeWalkingViewer(player);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1023,7 +1299,7 @@ public class NPC extends NpcHolder
|
||||
*
|
||||
* @param excludedPlayers Players who should NOT see the respawn/refresh.
|
||||
*/
|
||||
public void changeRealLocation(Location location, @Nullable Player... excludedPlayers)
|
||||
public void changeRealLocation(@NotNull Location location, @Nullable Player... excludedPlayers)
|
||||
{
|
||||
if(serverPlayer == null)
|
||||
return;
|
||||
@@ -1033,33 +1309,35 @@ public class NPC extends NpcHolder
|
||||
Set<UUID> excluded = excludedPlayers == null ? Collections.emptySet() :
|
||||
Arrays.stream(excludedPlayers).filter(Objects::nonNull).map(Player::getUniqueId).collect(Collectors.toSet());
|
||||
|
||||
float baseYaw = location.getYaw();
|
||||
float baseYaw = location.getYaw() + 360F;
|
||||
float pitch = location.getPitch();
|
||||
|
||||
float renderYaw = baseYaw;
|
||||
|
||||
if(getOption(NpcOption.POSE) == org.bukkit.entity.Pose.SLEEPING)
|
||||
{
|
||||
renderYaw = 180.0F - baseYaw + 90.0F;
|
||||
pitch = 0F;
|
||||
}
|
||||
|
||||
PacketWrapper teleport1 = new TeleportEntityPacket(serverPlayer, new TeleportEntityPacket.PositionMoveRotation(location.toVector(), new Vector(0, 0, 0),
|
||||
renderYaw, pitch), Set.of(), true);
|
||||
|
||||
byte renderYawByte = (byte) (renderYaw * 256 / 360);
|
||||
PacketWrapper rotPacket;
|
||||
if(getOption(NpcOption.POSE) == org.bukkit.entity.Pose.SLEEPING)
|
||||
rotPacket = new BundlePacket(
|
||||
new MoveEntityPacket.Rot(entity.getId(), renderYawByte, (byte) (pitch * 256 / 360), entity.getBukkitPlayer().isOnGround()),
|
||||
new MoveEntityPacket.Rot(entity.getId(), (byte) (renderYawByte - 35), (byte) (pitch * 256 / 360), entity.getBukkitPlayer().isOnGround()),
|
||||
new RotateHeadPacket(entity, renderYawByte));
|
||||
else
|
||||
{
|
||||
byte yawByte = (byte) (baseYaw * 256 / 360);
|
||||
rotPacket = new BundlePacket(new MoveEntityPacket.Rot(entity.getId(), yawByte, (byte) (pitch * 256 / 360), entity.getBukkitPlayer().isOnGround()),
|
||||
new RotateHeadPacket(entity, yawByte));
|
||||
rotPacket = new BundlePacket(new MoveEntityPacket.Rot(entity.getId(), (byte) (yawByte - 35), (byte) (pitch * 256 / 360),
|
||||
entity.getBukkitPlayer().isOnGround()), new RotateHeadPacket(entity, yawByte));
|
||||
}
|
||||
|
||||
PacketWrapper teleport1 = new TeleportEntityPacket(serverPlayer, new TeleportEntityPacket.PositionMoveRotation(location.toVector(), new Vector(0, 0, 0),
|
||||
0, pitch), Set.of(), true);
|
||||
|
||||
TeleportEntityPacket teleport2 = entity.equals(serverPlayer) ? null : new TeleportEntityPacket(entity,
|
||||
new TeleportEntityPacket.PositionMoveRotation(location.toVector(), new Vector(0, 0, 0), baseYaw, pitch),
|
||||
Set.of(), true);
|
||||
new TeleportEntityPacket.PositionMoveRotation(location.toVector(), new Vector(0, 0, 0), baseYaw, pitch), Set.of(), true);
|
||||
|
||||
for(UUID uuid : viewers)
|
||||
{
|
||||
@@ -1081,6 +1359,61 @@ public class NPC extends NpcHolder
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates this NPC's location and rotation for a specific player by sending the appropriate teleport and rotation packets.
|
||||
* <p>
|
||||
* Sleeping NPCs require special handling because the client renders their orientation differently from standing entities. In that case, a modified yaw is
|
||||
* used and the pitch is forced to {@code 0}.
|
||||
*
|
||||
* @param location the location to display the NPC at
|
||||
* @param player the player to send the location update to
|
||||
*/
|
||||
public void updateLocationForPlayer(@NotNull Location location, @NotNull Player player)
|
||||
{
|
||||
if(serverPlayer == null)
|
||||
return;
|
||||
|
||||
float baseYaw = location.getYaw() + 360F;
|
||||
float pitch = location.getPitch();
|
||||
|
||||
float renderYaw = baseYaw;
|
||||
|
||||
if(getOption(NpcOption.POSE, player) == org.bukkit.entity.Pose.SLEEPING)
|
||||
{
|
||||
renderYaw = 180.0F - baseYaw + 90.0F;
|
||||
pitch = 0F;
|
||||
}
|
||||
|
||||
PacketWrapper teleport1 = new TeleportEntityPacket(serverPlayer, new TeleportEntityPacket.PositionMoveRotation(location.toVector(), new Vector(0, 0, 0),
|
||||
renderYaw, pitch), Set.of(), true);
|
||||
|
||||
byte renderYawByte = (byte) (renderYaw * 256 / 360);
|
||||
PacketWrapper rotPacket;
|
||||
if(getOption(NpcOption.POSE, player) == org.bukkit.entity.Pose.SLEEPING)
|
||||
rotPacket = new BundlePacket(
|
||||
new MoveEntityPacket.Rot(entity.getId(), (byte) (renderYawByte - 35), (byte) (pitch * 256 / 360),
|
||||
entity.getBukkitPlayer().isOnGround()),
|
||||
new RotateHeadPacket(entity, renderYawByte));
|
||||
else
|
||||
{
|
||||
byte yawByte = (byte) (baseYaw * 256 / 360);
|
||||
rotPacket = new BundlePacket(new MoveEntityPacket.Rot(entity.getId(), (byte) (yawByte - 35), (byte) (pitch * 256 / 360),
|
||||
entity.getBukkitPlayer().isOnGround()), new RotateHeadPacket(entity, yawByte));
|
||||
}
|
||||
|
||||
TeleportEntityPacket teleport2 = entity.equals(serverPlayer) ? null : new TeleportEntityPacket(entity,
|
||||
new TeleportEntityPacket.PositionMoveRotation(location.toVector(), new Vector(0, 0, 0), baseYaw, pitch), Set.of(), true);
|
||||
|
||||
WrappedServerPlayer serverPlayer1 = WrappedServerPlayer.fromPlayer(player);
|
||||
serverPlayer1.sendPacket(teleport1);
|
||||
|
||||
if(teleport2 != null)
|
||||
serverPlayer1.sendPacket(teleport2);
|
||||
|
||||
if(rotPacket != null)
|
||||
serverPlayer1.sendPacket(rotPacket);
|
||||
}
|
||||
|
||||
/**
|
||||
* A serializable representation of an NPC that can be stored and later restored. This class handles the conversion between in-memory {@link NPC} objects
|
||||
* and their persistent storage format.
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package de.eisi05.npc.api.objects;
|
||||
|
||||
import de.eisi05.npc.api.utils.ApiOnly;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
/**
|
||||
@@ -48,11 +49,53 @@ public class NpcConfig
|
||||
private boolean checkValidPath = true;
|
||||
|
||||
/**
|
||||
* If true, the API will automatically manage NPC visibility for players across various events.
|
||||
* This includes showing NPCs when players join, change worlds, or load chunks, and hiding NPCs when players quit.
|
||||
* If true, the API will automatically manage NPC visibility for players across various events. This includes showing NPCs when players join, change worlds,
|
||||
* or load chunks, and hiding NPCs when players quit.
|
||||
*/
|
||||
@ApiOnly
|
||||
private boolean autoManageVisibility = true;
|
||||
|
||||
/**
|
||||
* If true, walking NPCs automatically manage viewers while a path task is active.
|
||||
* <p>
|
||||
* When enabled, players who join, change worlds, respawn, or load nearby chunks can be added to the active walking task and synced to the NPC's current
|
||||
* walking position.
|
||||
* <p>
|
||||
* This only applies to walking tasks created without an explicit viewer list.
|
||||
* <p>
|
||||
* Default: true
|
||||
*/
|
||||
@ApiOnly
|
||||
private boolean autoManageWalkingViewers = true;
|
||||
|
||||
/**
|
||||
* Maximum distance in blocks at which walking NPC movement is shown to players.
|
||||
* <p>
|
||||
* Players farther than this distance from the NPC's current walking position will not receive walking movement packets. Players in another world are never
|
||||
* shown walking movement.
|
||||
* <p>
|
||||
* Set to {@code -1} to disable the distance check.
|
||||
* <p>
|
||||
* Default: 64 blocks.
|
||||
*/
|
||||
private double walkingViewerDistance = 64.0;
|
||||
|
||||
/**
|
||||
* If true, precise hitbox detection is used for sleeping NPCs.
|
||||
* <p>
|
||||
* When enabled, the {@link de.eisi05.npc.api.listeners.NpcSleepListener} will use accurate ray intersection calculations to detect interactions with
|
||||
* sleeping NPCs. This provides more precise click detection but may be more computationally expensive.
|
||||
* <p>
|
||||
* Default: false
|
||||
*/
|
||||
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.
|
||||
*
|
||||
@@ -150,6 +193,64 @@ public class NpcConfig
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether walking NPCs should automatically manage viewers during active path tasks.
|
||||
* <p>
|
||||
* This only applies when {@link #autoManageVisibility()} is enabled and the walking task was created without an explicit viewer list.
|
||||
*
|
||||
* @param autoManageWalkingViewers true to automatically manage walking viewers, false otherwise
|
||||
* @return This {@link NpcConfig} instance for method chaining. Never null.
|
||||
*/
|
||||
public @NotNull NpcConfig autoManageWalkingViewers(boolean autoManageWalkingViewers)
|
||||
{
|
||||
this.autoManageWalkingViewers = autoManageWalkingViewers;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the maximum distance in blocks at which walking NPC movement is shown to players.
|
||||
* <p>
|
||||
* Players farther than this distance from the NPC's current walking position will not receive walking packets. Players in another world are never shown
|
||||
* walking movement.
|
||||
* <p>
|
||||
* Use {@code -1} to disable the distance check.
|
||||
*
|
||||
* @param walkingViewerDistance the maximum distance in blocks, or -1 to disable
|
||||
* @return This {@link NpcConfig} instance for method chaining. Never null.
|
||||
*/
|
||||
public @NotNull NpcConfig walkingViewerDistance(double walkingViewerDistance)
|
||||
{
|
||||
this.walkingViewerDistance = walkingViewerDistance;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether precise hitbox detection is used for sleeping NPCs.
|
||||
* <p>
|
||||
* When enabled, the {@link de.eisi05.npc.api.listeners.NpcSleepListener} will use accurate ray intersection calculations to detect interactions with
|
||||
* sleeping NPCs.
|
||||
*
|
||||
* @param preciseSleepingHitbox true to enable precise hitbox detection, false otherwise
|
||||
* @return This {@link NpcConfig} instance for method chaining. Never null.
|
||||
*/
|
||||
public @NotNull NpcConfig preciseSleepingHitbox(boolean preciseSleepingHitbox)
|
||||
{
|
||||
this.preciseSleepingHitbox = preciseSleepingHitbox;
|
||||
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.
|
||||
*
|
||||
@@ -229,4 +330,44 @@ public class NpcConfig
|
||||
{
|
||||
return autoManageVisibility;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether walking NPCs automatically manage viewers during active path tasks.
|
||||
*
|
||||
* @return true if automatic walking viewer management is enabled; false otherwise
|
||||
*/
|
||||
public boolean autoManageWalkingViewers()
|
||||
{
|
||||
return autoManageWalkingViewers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the maximum distance in blocks at which walking NPC movement is shown to players.
|
||||
*
|
||||
* @return the maximum walking viewer distance in blocks, or -1 if disabled
|
||||
*/
|
||||
public double walkingViewerDistance()
|
||||
{
|
||||
return walkingViewerDistance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether precise hitbox detection is enabled for sleeping NPCs.
|
||||
*
|
||||
* @return true if precise hitbox detection is enabled, false otherwise
|
||||
*/
|
||||
public boolean preciseSleepingHitbox()
|
||||
{
|
||||
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));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -17,13 +17,13 @@ import de.eisi05.npc.api.wrapper.enums.ChatFormat;
|
||||
import de.eisi05.npc.api.wrapper.objects.*;
|
||||
import de.eisi05.npc.api.wrapper.packets.*;
|
||||
import org.bukkit.ChatColor;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.entity.EntityType;
|
||||
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.bukkit.util.Vector;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
@@ -32,7 +32,6 @@ import java.lang.reflect.Field;
|
||||
import java.util.*;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Represents a configurable option for an NPC. Each option has a path, a default value, serialization/deserialization logic, and a function to generate a
|
||||
@@ -205,12 +204,38 @@ public class NpcOption<T, S extends Serializable>
|
||||
* Serialized form uses item base64 strings.
|
||||
*/
|
||||
public static final NpcOption<Map<EquipmentSlot, ItemStack>, HashMap<EquipmentSlot, String>> EQUIPMENT = new NpcOption<>("equipment", HashMap::new, HashMap::new,
|
||||
map -> map.entrySet().stream().collect(
|
||||
Collectors.toMap(Map.Entry::getKey,
|
||||
entry -> ItemSerializer.itemStackToBase64(entry.getValue()), (a, b) -> b, HashMap::new)),
|
||||
serializedMap -> serializedMap.entrySet().stream().collect(
|
||||
Collectors.toMap(Map.Entry::getKey,
|
||||
entry -> ItemSerializer.itemStackFromBase64(entry.getValue()), (a, b) -> b, HashMap::new)),
|
||||
map ->
|
||||
{
|
||||
HashMap<EquipmentSlot, String> serializedMap = new HashMap<>();
|
||||
map.forEach((slot, item) ->
|
||||
{
|
||||
if(item == null || item.getType().isAir())
|
||||
return;
|
||||
|
||||
String serialized = ItemSerializer.itemStackToBase64(item);
|
||||
if(serialized == null)
|
||||
return;
|
||||
|
||||
serializedMap.put(slot, serialized);
|
||||
});
|
||||
return serializedMap;
|
||||
},
|
||||
serializedMap ->
|
||||
{
|
||||
HashMap<EquipmentSlot, ItemStack> map = new HashMap<>();
|
||||
serializedMap.forEach((slot, string) ->
|
||||
{
|
||||
if(string == null || string.isEmpty())
|
||||
return;
|
||||
|
||||
ItemStack item = ItemSerializer.itemStackFromBase64(string);
|
||||
if(item == null)
|
||||
return;
|
||||
|
||||
map.put(slot, item);
|
||||
});
|
||||
return map;
|
||||
},
|
||||
(map, npc, player) ->
|
||||
{
|
||||
if(map.isEmpty())
|
||||
@@ -245,6 +270,123 @@ public class NpcOption<T, S extends Serializable>
|
||||
distance -> distance, distance -> distance, distance -> distance,
|
||||
(distance, npc, player) -> null);
|
||||
|
||||
/**
|
||||
* NPC option to control visibility with three states: fully visible, transparent, or invisible.
|
||||
* <p>
|
||||
* <b>Important Notes:</b>
|
||||
* <ul>
|
||||
* <li>When TRANSPARENT is enabled, it affects other team-based options (COLLISION, GLOWING) by forcing them
|
||||
* to use the same transparent team naming convention</li>
|
||||
* </ul>
|
||||
* <p>
|
||||
*/
|
||||
public static final NpcOption<NpcVisibility, NpcVisibility> VISIBILITY = new NpcOption<>("visibility", () -> NpcVisibility.FULLY_VISIBLE,
|
||||
visibility -> visibility, visibility -> visibility, visibility -> visibility, (visibility, npc, player) ->
|
||||
{
|
||||
String teamName = "trans-" + player.getEntityId();
|
||||
WrappedPlayerTeam playerTeam = WrappedPlayerTeam.getPlayersTeam(player);
|
||||
|
||||
boolean modified = WrappedPlayerTeam.exists(player, teamName) || playerTeam.getHandle() != null;
|
||||
WrappedPlayerTeam wrappedPlayerTeam = playerTeam.getHandle() != null ? playerTeam : WrappedPlayerTeam.create(player, teamName);
|
||||
|
||||
if(!wrappedPlayerTeam.canSeeFriendlyInvisible())
|
||||
wrappedPlayerTeam.setCanSeeFriendlyInvisible(true);
|
||||
|
||||
var teamPacket = SetPlayerTeamPacket.createAddOrModifyPacket(wrappedPlayerTeam, !modified);
|
||||
|
||||
WrappedEntityData entityData = npc.entity.getEntityData();
|
||||
WrappedEntityData.EntityDataAccessor<Byte> accessor = WrappedEntityData.EntityDataSerializers.BYTE.create(0);
|
||||
Byte value = entityData.get(accessor);
|
||||
byte flags = value == null ? 0 : value;
|
||||
|
||||
byte modifier = 0x20;
|
||||
|
||||
List<PacketWrapper> packets = new ArrayList<>(List.of(teamPacket));
|
||||
|
||||
var removeNpc = SetPlayerTeamPacket.createPlayerPacket(wrappedPlayerTeam, npc.getServerPlayer().getName(), SetPlayerTeamPacket.Action.REMOVE);
|
||||
|
||||
if(wrappedPlayerTeam.getPlayers().remove(npc.getServerPlayer().getName()))
|
||||
packets.add(removeNpc);
|
||||
|
||||
if(visibility == NpcVisibility.FULLY_VISIBLE)
|
||||
{
|
||||
entityData.set(accessor, (byte) (flags & ~modifier));
|
||||
packets.add(SetEntityDataPacket.create(npc.entity.getId(), entityData));
|
||||
return new BundlePacket(packets.toArray(new PacketWrapper[0]));
|
||||
}
|
||||
|
||||
entityData.set(accessor, (byte) (flags | modifier));
|
||||
|
||||
if(visibility == NpcVisibility.INVISIBLE)
|
||||
{
|
||||
packets.add(SetEntityDataPacket.create(npc.entity.getId(), entityData));
|
||||
return new BundlePacket(packets.toArray(new PacketWrapper[0]));
|
||||
}
|
||||
|
||||
wrappedPlayerTeam.getPlayers().add(npc.getServerPlayer().getName());
|
||||
var addNpc = SetPlayerTeamPacket.createPlayerPacket(wrappedPlayerTeam, npc.getServerPlayer().getName(), SetPlayerTeamPacket.Action.ADD);
|
||||
var addPlayer = SetPlayerTeamPacket.createPlayerPacket(wrappedPlayerTeam, player.getName(), SetPlayerTeamPacket.Action.ADD);
|
||||
|
||||
return new BundlePacket(teamPacket, SetEntityDataPacket.create(npc.entity.getId(), entityData), addNpc, addPlayer);
|
||||
});
|
||||
|
||||
/**
|
||||
* NPC option to make the NPC glow with a specific color. If null, the glowing effect is removed.
|
||||
* <p>
|
||||
* <b>Important Notes:</b>
|
||||
* <ul>
|
||||
* <li>If TRANSPARENT is enabled, this option will change glowing for all npc with TRANSPARENT enabled</li>
|
||||
* </ul>
|
||||
* </p>
|
||||
*/
|
||||
public static final NpcOption<ChatColor, ChatColor> GLOWING = new NpcOption<>("glowing", () -> null,
|
||||
color -> color, color -> color, color -> color,
|
||||
(color, npc, player) ->
|
||||
{
|
||||
WrappedEntityData entityData = npc.entity.getEntityData();
|
||||
WrappedEntityData.EntityDataAccessor<Byte> accessor = WrappedEntityData.EntityDataSerializers.BYTE.create(0);
|
||||
Byte value = entityData.get(accessor);
|
||||
byte flags = value == null ? 0 : value;
|
||||
|
||||
byte modifier = 0x40;
|
||||
|
||||
if(color == null)
|
||||
{
|
||||
entityData.set(accessor, (byte) (flags & ~modifier));
|
||||
return SetEntityDataPacket.create(npc.entity.getId(), entityData);
|
||||
}
|
||||
|
||||
var teamPair = getTeam(player, npc);
|
||||
WrappedPlayerTeam team = teamPair.getFirst();
|
||||
|
||||
team.setColor(ChatFormat.fromChatColor(color));
|
||||
|
||||
var teamPacket = SetPlayerTeamPacket.createAddOrModifyPacket(team, !teamPair.getSecond());
|
||||
|
||||
entityData.set(accessor, (byte) (flags | modifier));
|
||||
|
||||
return new BundlePacket(teamPacket, SetEntityDataPacket.create(npc.entity.getId(), entityData));
|
||||
});
|
||||
|
||||
/**
|
||||
* NPC option to control collision behavior between players and NPCs. When enabled, players can push and collide with NPCs. When disabled, players pass
|
||||
* through NPCs without collision.
|
||||
* <p>
|
||||
* <b>Important Notes:</b>
|
||||
* <ul>
|
||||
* <li>If TRANSPARENT is enabled, this option will change collisions for all npc with TRANSPARENT enabled</li>
|
||||
* </ul>
|
||||
* <p>
|
||||
*/
|
||||
public static final NpcOption<Boolean, Boolean> COLLISION = new NpcOption<>("collision", () -> true,
|
||||
aBoolean -> aBoolean, aBoolean -> aBoolean, aBoolean -> aBoolean, (collision, npc, player) ->
|
||||
{
|
||||
var teamPair = getTeam(player, npc);
|
||||
WrappedPlayerTeam team = teamPair.getFirst();
|
||||
team.setCollisionRule(collision ? WrappedPlayerTeam.CollisionRule.ALWAYS : WrappedPlayerTeam.CollisionRule.NEVER);
|
||||
return SetPlayerTeamPacket.createAddOrModifyPacket(team, !teamPair.getSecond());
|
||||
});
|
||||
|
||||
/**
|
||||
* NPC option to set the pose of the NPC (e.g., standing, sleeping, swimming). For a full list look at {@link Pose}.
|
||||
*/
|
||||
@@ -294,10 +436,43 @@ public class NpcOption<T, S extends Serializable>
|
||||
data.set(WrappedEntityData.EntityDataSerializers.BYTE.create(8), (byte) (handFlag & ~0x04));
|
||||
}
|
||||
|
||||
if(pose == Pose.SLEEPING)
|
||||
{
|
||||
new BukkitRunnable()
|
||||
{
|
||||
int counter = 255;
|
||||
final Location startLocation = npc.getLocation().clone();
|
||||
final float startYaw = startLocation.getYaw();
|
||||
|
||||
@Override
|
||||
public void run()
|
||||
{
|
||||
startLocation.setYaw((startYaw + counter) % 360);
|
||||
npc.updateLocationForPlayer(startLocation, player);
|
||||
if(counter == 360)
|
||||
cancel();
|
||||
|
||||
counter += 35;
|
||||
}
|
||||
}.runTaskTimer(NpcApi.plugin, 10, 5);
|
||||
}
|
||||
else
|
||||
{
|
||||
new BukkitRunnable()
|
||||
{
|
||||
@Override
|
||||
public void run()
|
||||
{
|
||||
npc.updateLocationForPlayer(npc.getLocation(), player);
|
||||
}
|
||||
}.runTaskLater(NpcApi.plugin, 1);
|
||||
}
|
||||
|
||||
Integer oldId = npc.toDeleteEntities.remove("sit");
|
||||
if(nmsPose == de.eisi05.npc.api.wrapper.enums.Pose.SITTING)
|
||||
{
|
||||
WrappedTextDisplay textDisplay = WrappedTextDisplay.create(npc.entity.getBukkitPlayer().getLocation().getWorld());
|
||||
textDisplay.moveTo(npc.entity.getBukkitPlayer().getLocation());
|
||||
WrappedTextDisplay textDisplay = WrappedTextDisplay.create(npc.getLocation().getWorld());
|
||||
textDisplay.moveTo(npc.getLocation());
|
||||
npc.toDeleteEntities.put("sit", textDisplay.getId());
|
||||
|
||||
PacketWrapper addEntityPacket = textDisplay.getAddEntityPacket();
|
||||
@@ -309,98 +484,25 @@ public class NpcOption<T, S extends Serializable>
|
||||
textDisplay.setPassengers(npc.entity);
|
||||
|
||||
SetPassengerPacket passengerPacket = new SetPassengerPacket(textDisplay);
|
||||
RotateHeadPacket rotateHeadPacket = new RotateHeadPacket(npc.entity,
|
||||
(byte) (npc.getLocation().getYaw() * 256 / 360));
|
||||
RotateHeadPacket rotateHeadPacket = new RotateHeadPacket(npc.entity, (byte) (npc.getLocation().getYaw() * 256 / 360));
|
||||
|
||||
return new BundlePacket(addEntityPacket, entityDataPacket, passengerPacket, rotateHeadPacket);
|
||||
if(oldId == null)
|
||||
return new BundlePacket(addEntityPacket, entityDataPacket, passengerPacket, rotateHeadPacket);
|
||||
|
||||
return new BundlePacket(new RemoveEntityPacket(oldId), addEntityPacket, entityDataPacket, passengerPacket, rotateHeadPacket);
|
||||
}
|
||||
else
|
||||
{
|
||||
float yaw = npc.getLocation().getYaw();
|
||||
float pitch = npc.getLocation().getPitch();
|
||||
if(pose == Pose.SLEEPING)
|
||||
yaw = 180.0F - yaw + 90.0F;
|
||||
if(oldId == null)
|
||||
return packetWrapper == null ? new BundlePacket(SetEntityDataPacket.create(npc.entity.getId(), data)) :
|
||||
new BundlePacket(packetWrapper, SetEntityDataPacket.create(npc.entity.getId(), data));
|
||||
|
||||
PacketWrapper packet1 = new TeleportEntityPacket(npc.entity, new TeleportEntityPacket.PositionMoveRotation(npc.getLocation().toVector(),
|
||||
new Vector(0, 0, 0), yaw, pitch), Set.of(), true);
|
||||
PacketWrapper packet2 = new MoveEntityPacket.Rot(npc.entity.getId(), (byte) (yaw * 256 / 360), (byte) (pitch * 256 / 360),
|
||||
npc.getServerPlayer().isOnGround());
|
||||
PacketWrapper packet3 = new RotateHeadPacket(npc.entity, (byte) (yaw * 256 / 360));
|
||||
|
||||
PacketWrapper bundle = new BundlePacket(packet1, packet2, packet3);
|
||||
|
||||
Integer toDelete = npc.toDeleteEntities.get("sit");
|
||||
|
||||
if(toDelete == null)
|
||||
return packetWrapper == null ? new BundlePacket(SetEntityDataPacket.create(npc.entity.getId(), data), bundle) :
|
||||
new BundlePacket(SetEntityDataPacket.create(npc.entity.getId(), data), packetWrapper, bundle);
|
||||
|
||||
npc.toDeleteEntities.remove("sit");
|
||||
return packetWrapper == null ?
|
||||
new BundlePacket(new RemoveEntityPacket(toDelete), SetEntityDataPacket.create(npc.entity.getId(), data), bundle) :
|
||||
new BundlePacket(new RemoveEntityPacket(toDelete), SetEntityDataPacket.create(npc.entity.getId(), data), packetWrapper, bundle);
|
||||
new BundlePacket(new RemoveEntityPacket(oldId), SetEntityDataPacket.create(npc.entity.getId(), data)) :
|
||||
new BundlePacket(packetWrapper, new RemoveEntityPacket(oldId), SetEntityDataPacket.create(npc.entity.getId(), data));
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* NPC option to control visibility with three states: fully visible, transparent, or invisible.
|
||||
* <p>
|
||||
* <b>Important Notes:</b>
|
||||
* <ul>
|
||||
* <li>When TRANSPARENT is enabled, it affects other team-based options (COLLISION, GLOWING) by forcing them
|
||||
* to use the same transparent team naming convention</li>
|
||||
* </ul>
|
||||
* <p>
|
||||
*/
|
||||
public static final NpcOption<NpcVisibility, NpcVisibility> VISIBILITY = new NpcOption<>("visibility", () -> NpcVisibility.FULLY_VISIBLE,
|
||||
visibility -> visibility, visibility -> visibility, visibility -> visibility, (visibility, npc, player) ->
|
||||
{
|
||||
String teamName = "trans-" + player.getEntityId();
|
||||
WrappedPlayerTeam playerTeam = WrappedPlayerTeam.getPlayersTeam(player);
|
||||
|
||||
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);
|
||||
|
||||
var teamPacket = SetPlayerTeamPacket.createAddOrModifyPacket(wrappedPlayerTeam, !modified);
|
||||
|
||||
WrappedEntityData entityData = npc.entity.getEntityData();
|
||||
WrappedEntityData.EntityDataAccessor<Byte> accessor = WrappedEntityData.EntityDataSerializers.BYTE.create(0);
|
||||
Byte value = entityData.get(accessor);
|
||||
byte flags = value == null ? 0 : value;
|
||||
|
||||
byte modifier = 0x20;
|
||||
|
||||
List<PacketWrapper> packets = new ArrayList<>(List.of(teamPacket));
|
||||
|
||||
var removeNpc = SetPlayerTeamPacket.createPlayerPacket(wrappedPlayerTeam, npc.getServerPlayer().getName(), SetPlayerTeamPacket.Action.REMOVE);
|
||||
|
||||
if(wrappedPlayerTeam.getPlayers().remove(npc.getServerPlayer().getName()))
|
||||
packets.add(removeNpc);
|
||||
|
||||
if(visibility == NpcVisibility.FULLY_VISIBLE)
|
||||
{
|
||||
entityData.set(accessor, (byte) (flags & ~modifier));
|
||||
packets.add(SetEntityDataPacket.create(npc.entity.getId(), entityData));
|
||||
return new BundlePacket(packets.toArray(new PacketWrapper[0]));
|
||||
}
|
||||
|
||||
entityData.set(accessor, (byte) (flags | modifier));
|
||||
|
||||
if(visibility == NpcVisibility.INVISIBLE)
|
||||
{
|
||||
packets.add(SetEntityDataPacket.create(npc.entity.getId(), entityData));
|
||||
return new BundlePacket(packets.toArray(new PacketWrapper[0]));
|
||||
}
|
||||
|
||||
wrappedPlayerTeam.getPlayers().add(npc.getServerPlayer().getName());
|
||||
var addNpc = SetPlayerTeamPacket.createPlayerPacket(wrappedPlayerTeam, npc.getServerPlayer().getName(), SetPlayerTeamPacket.Action.ADD);
|
||||
var addPlayer = SetPlayerTeamPacket.createPlayerPacket(wrappedPlayerTeam, player.getName(), SetPlayerTeamPacket.Action.ADD);
|
||||
|
||||
return new BundlePacket(teamPacket, SetEntityDataPacket.create(npc.entity.getId(), entityData), addNpc, addPlayer);
|
||||
});
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
@@ -418,44 +520,6 @@ public class NpcOption<T, S extends Serializable>
|
||||
return new UpdateAttributesPacket(npc.entity.getId(), instance);
|
||||
}).since(Versions.V1_20_6);
|
||||
|
||||
/**
|
||||
* NPC option to make the NPC glow with a specific color. If null, the glowing effect is removed.
|
||||
* <p>
|
||||
* <b>Important Notes:</b>
|
||||
* <ul>
|
||||
* <li>If TRANSPARENT is enabled, this option will change glowing for all npc with TRANSPARENT enabled</li>
|
||||
* </ul>
|
||||
* </p>
|
||||
*/
|
||||
public static final NpcOption<ChatColor, ChatColor> GLOWING = new NpcOption<>("glowing", () -> null,
|
||||
color -> color, color -> color, color -> color,
|
||||
(color, npc, player) ->
|
||||
{
|
||||
WrappedEntityData entityData = npc.entity.getEntityData();
|
||||
WrappedEntityData.EntityDataAccessor<Byte> accessor = WrappedEntityData.EntityDataSerializers.BYTE.create(0);
|
||||
Byte value = entityData.get(accessor);
|
||||
byte flags = value == null ? 0 : value;
|
||||
|
||||
byte modifier = 0x40;
|
||||
|
||||
if(color == null)
|
||||
{
|
||||
entityData.set(accessor, (byte) (flags & ~modifier));
|
||||
return SetEntityDataPacket.create(npc.entity.getId(), entityData);
|
||||
}
|
||||
|
||||
var teamPair = getTeam(player, npc);
|
||||
WrappedPlayerTeam team = teamPair.getFirst();
|
||||
|
||||
team.setColor(ChatFormat.fromChatColor(color));
|
||||
|
||||
var teamPacket = SetPlayerTeamPacket.createAddOrModifyPacket(team, !teamPair.getSecond());
|
||||
|
||||
entityData.set(accessor, (byte) (flags | modifier));
|
||||
|
||||
return new BundlePacket(teamPacket, SetEntityDataPacket.create(npc.entity.getId(), entityData));
|
||||
});
|
||||
|
||||
/**
|
||||
* NPC option to control the position of the NPC in the TAB list.
|
||||
* <p>
|
||||
@@ -474,25 +538,6 @@ public class NpcOption<T, S extends Serializable>
|
||||
return new PlayerInfoUpdatePacket(PlayerInfoUpdatePacket.Action.UPDATE_LIST_ORDER, npc.getServerPlayer());
|
||||
}).since(Versions.V1_21_2);
|
||||
|
||||
/**
|
||||
* NPC option to control collision behavior between players and NPCs. When enabled, players can push and collide with NPCs. When disabled, players pass
|
||||
* through NPCs without collision.
|
||||
* <p>
|
||||
* <b>Important Notes:</b>
|
||||
* <ul>
|
||||
* <li>If TRANSPARENT is enabled, this option will change collisions for all npc with TRANSPARENT enabled</li>
|
||||
* </ul>
|
||||
* <p>
|
||||
*/
|
||||
public static final NpcOption<Boolean, Boolean> COLLISION = new NpcOption<>("collision", () -> true,
|
||||
aBoolean -> aBoolean, aBoolean -> aBoolean, aBoolean -> aBoolean, (collision, npc, player) ->
|
||||
{
|
||||
var teamPair = getTeam(player, npc);
|
||||
WrappedPlayerTeam team = teamPair.getFirst();
|
||||
team.setCollisionRule(collision ? WrappedPlayerTeam.CollisionRule.ALWAYS : WrappedPlayerTeam.CollisionRule.NEVER);
|
||||
return SetPlayerTeamPacket.createAddOrModifyPacket(team, !teamPair.getSecond());
|
||||
});
|
||||
|
||||
/**
|
||||
* NPC option to change the entity type of the NPC. This allows transforming the NPC into any Minecraft entity type. The default is a PLAYER entity. When
|
||||
* changed, the NPC will be recreated as the new entity type.
|
||||
@@ -534,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());
|
||||
@@ -558,7 +604,7 @@ public class NpcOption<T, S extends Serializable>
|
||||
{
|
||||
if(Versions.isCurrentVersionSmallerThan(Versions.V1_21))
|
||||
npc.serverPlayer.getNameTag()
|
||||
.moveTo(entity.getBukkitPlayer().getLocation().clone().add(0,
|
||||
.moveTo(npc.getLocation().clone().add(0,
|
||||
(entity.getBoundingBox().getYSize() * npc.getOption(NpcOption.SCALE, player)), 0));
|
||||
|
||||
packets.add(npc.serverPlayer.getNameTag().getAddEntityPacket());
|
||||
@@ -592,11 +638,9 @@ public class NpcOption<T, S extends Serializable>
|
||||
if(enabled)
|
||||
return null;
|
||||
|
||||
WrappedArmorStand armorStand = WrappedArmorStand.create(npc.entity.getBukkitPlayer().getLocation().getWorld());
|
||||
armorStand.moveTo(npc.entity.getBukkitPlayer()
|
||||
.getLocation()
|
||||
.clone()
|
||||
.add(0, (npc.getServerPlayer().getBoundingBox().getYSize() * npc.getOption(SCALE, player)), 0));
|
||||
WrappedArmorStand armorStand = WrappedArmorStand.create(npc.getLocation().getWorld());
|
||||
armorStand.moveTo(npc.getLocation().clone()
|
||||
.add(0, (npc.entity.getBoundingBox().getYSize() * npc.getOption(SCALE, player)), 0));
|
||||
|
||||
PacketWrapper addPacket = armorStand.getAddEntityPacket();
|
||||
|
||||
@@ -628,7 +672,7 @@ public class NpcOption<T, S extends Serializable>
|
||||
* deserialization, goals must be re-instantiated by the plugin.
|
||||
*/
|
||||
static final NpcOption<ArrayList<Goal>, ArrayList<Goal>> GOALS = new NpcOption<>("goals", ArrayList::new,
|
||||
goals -> new ArrayList<>(goals.stream().map(goal -> goal.copy()).toList()),goals -> goals,
|
||||
goals -> new ArrayList<>(goals.stream().map(Goal::copy).toList()), goals -> goals,
|
||||
goals -> goals,
|
||||
(data, npc, player) -> null);
|
||||
|
||||
@@ -651,7 +695,13 @@ public class NpcOption<T, S extends Serializable>
|
||||
newManager.setShowToAllPlayers(npcVisibilityManager.shouldShowToAllPlayers());
|
||||
npcVisibilityManager.getSpecificPlayers().forEach(newManager::addSpecificPlayer);
|
||||
return newManager;
|
||||
},visibilityManager -> visibilityManager, visibilityManager -> visibilityManager,
|
||||
},visibilityManager -> visibilityManager, visibilityManager ->
|
||||
{
|
||||
if(visibilityManager.getSpecificPlayers().isEmpty() && !visibilityManager.shouldShowToAllPlayers())
|
||||
visibilityManager.setShowToAllPlayers(true);
|
||||
|
||||
return visibilityManager;
|
||||
},
|
||||
(visibilityManager, npc, player) -> null);
|
||||
|
||||
private final String path;
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
package de.eisi05.npc.api.pathfinding;
|
||||
|
||||
import de.eisi05.npc.api.NpcApi;
|
||||
import de.eisi05.npc.api.utils.Var;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.block.BlockFace;
|
||||
import org.bukkit.block.data.Openable;
|
||||
import org.bukkit.util.BoundingBox;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
@@ -36,19 +35,23 @@ public class AStarPathfinder
|
||||
|
||||
private final int maxIterations;
|
||||
private final boolean allowDiagonal;
|
||||
private final double entityHeight;
|
||||
private final double entityWidth;
|
||||
private final PriorityQueue<Node> openSet = new PriorityQueue<>();
|
||||
private final Set<Long> openSetIds = new HashSet<>();
|
||||
private final Map<Long, Node> allNodes = new HashMap<>();
|
||||
private World world;
|
||||
|
||||
public AStarPathfinder(int maxIterations, boolean allowDiagonal)
|
||||
public AStarPathfinder(int maxIterations, boolean allowDiagonal, double entityHeight, double entityWidth)
|
||||
{
|
||||
this.maxIterations = maxIterations;
|
||||
this.allowDiagonal = allowDiagonal;
|
||||
this.entityHeight = entityHeight;
|
||||
this.entityWidth = entityWidth;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a block is valid to stand ON.
|
||||
* Checks if a block is valid to stand on.
|
||||
*/
|
||||
public static boolean isSafeFloor(Block block)
|
||||
{
|
||||
@@ -59,41 +62,72 @@ public class AStarPathfinder
|
||||
if(type.isAir() || block.isLiquid())
|
||||
return false;
|
||||
|
||||
if(block.isPassable())
|
||||
return false;
|
||||
|
||||
return true;
|
||||
return !block.isPassable();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a block obstructs movement (is a wall).
|
||||
* Checks if a position is valid (not inside a solid block).
|
||||
*
|
||||
* @param world The world to check in
|
||||
* @param tx The x coordinate of the position
|
||||
* @param ty The y coordinate of the position
|
||||
* @param tz The z coordinate of the position
|
||||
* @param entityHeight The height of the entity
|
||||
* @param entityWidth The width of the entity
|
||||
* @return true if the position is valid, false otherwise
|
||||
*/
|
||||
public static boolean isSolid(Block block)
|
||||
public static boolean isPositionValid(@NotNull World world, double tx, double ty, double tz, double entityHeight, double entityWidth)
|
||||
{
|
||||
if(block == null)
|
||||
return false;
|
||||
double radius = entityWidth / 2.0;
|
||||
double minX = tx - radius;
|
||||
double maxX = tx + radius;
|
||||
double maxY = ty + entityHeight;
|
||||
double minZ = tz - radius;
|
||||
double maxZ = tz + radius;
|
||||
|
||||
Material type = block.getType();
|
||||
if(type.isAir())
|
||||
return false;
|
||||
BoundingBox entityBox = new BoundingBox(minX, ty, minZ, maxX, maxY, maxZ);
|
||||
|
||||
if(Var.isCarpet(type) && Var.isCarpet(block.getRelative(BlockFace.UP).getType()))
|
||||
return true;
|
||||
int minBlockX = (int) Math.floor(minX);
|
||||
int maxBlockX = (int) Math.floor(maxX);
|
||||
int minBlockY = (int) Math.floor(ty);
|
||||
int maxBlockY = (int) Math.floor(maxY);
|
||||
if(maxY > maxBlockY && maxBlockY == minBlockY)
|
||||
maxBlockY++;
|
||||
int minBlockZ = (int) Math.floor(minZ);
|
||||
int maxBlockZ = (int) Math.floor(maxZ);
|
||||
|
||||
if(Var.isCarpet(type))
|
||||
return false;
|
||||
for(int x = minBlockX; x <= maxBlockX; x++)
|
||||
{
|
||||
for(int y = minBlockY; y <= maxBlockY; y++)
|
||||
{
|
||||
for(int z = minBlockZ; z <= maxBlockZ; z++)
|
||||
{
|
||||
Block block = world.getBlockAt(x, y, z);
|
||||
if(block.getBlockData() instanceof Openable)
|
||||
continue;
|
||||
|
||||
if(block.isPassable())
|
||||
return false;
|
||||
Collection<BoundingBox> blockBoxes = block.getCollisionShape().getBoundingBoxes();
|
||||
if(blockBoxes.isEmpty())
|
||||
continue;
|
||||
|
||||
if(block.getBlockData() instanceof Openable)
|
||||
return false;
|
||||
for(BoundingBox blockBox : blockBoxes)
|
||||
{
|
||||
BoundingBox absoluteBox = blockBox.clone().shift(x, y, z);
|
||||
if(entityBox.overlaps(absoluteBox))
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public @Nullable List<Location> getPath(@NotNull Location start, @NotNull Location end) throws PathfindingUtils.PathfindingException
|
||||
{
|
||||
if(start.getWorld() == null || end.getWorld() == null)
|
||||
return null;
|
||||
|
||||
if(!start.getWorld().equals(end.getWorld()))
|
||||
return null;
|
||||
|
||||
@@ -102,17 +136,18 @@ public class AStarPathfinder
|
||||
allNodes.clear();
|
||||
this.world = start.getWorld();
|
||||
|
||||
int startOffset = Math.abs(start.getY() - start.getBlockY()) > 0 ? 0 : 1;
|
||||
Block startFloor = findSafeFloor(start.getBlockX(), start.getBlockY() - startOffset, start.getBlockZ());
|
||||
if(NpcApi.config.checkValidPath() && startFloor == null)
|
||||
int startFloorY = resolveFloorY(start);
|
||||
int endFloorY = resolveFloorY(end);
|
||||
|
||||
Block startFloor = world.getBlockAt(start.getBlockX(), startFloorY, start.getBlockZ());
|
||||
if(NpcApi.config.checkValidPath() && !isSafeFloor(startFloor))
|
||||
throw new PathfindingUtils.PathfindingException("Start not on a valid floor: " + start);
|
||||
|
||||
int endOffset = Math.abs(end.getY() - end.getBlockY()) > 0 ? 0 : 1;
|
||||
Block endFloor = findSafeFloor(end.getBlockX(), end.getBlockY() - endOffset, end.getBlockZ());
|
||||
if(NpcApi.config.checkValidPath() && endFloor == null)
|
||||
Block endFloor = world.getBlockAt(end.getBlockX(), endFloorY, end.getBlockZ());
|
||||
if(NpcApi.config.checkValidPath() && !isSafeFloor(endFloor))
|
||||
throw new PathfindingUtils.PathfindingException("End not on a valid floor: " + end);
|
||||
|
||||
Node startNode = new Node(start.getBlockX(), start.getBlockY(), start.getBlockZ(), null);
|
||||
Node startNode = new Node(start.getBlockX(), startFloorY, start.getBlockZ(), null);
|
||||
startNode.gCost = 0;
|
||||
startNode.calculateH(end);
|
||||
|
||||
@@ -132,7 +167,7 @@ public class AStarPathfinder
|
||||
Node current = openSet.poll();
|
||||
openSetIds.remove(current.id);
|
||||
|
||||
if(distance(current, end) < 4)
|
||||
if(distanceSq(current, end) < 1.0)
|
||||
return retracePath(current);
|
||||
|
||||
current.closed = true;
|
||||
@@ -146,7 +181,7 @@ public class AStarPathfinder
|
||||
if(x == 0 && y == 0 && z == 0)
|
||||
continue;
|
||||
|
||||
if(!allowDiagonal && (Math.abs(x) + Math.abs(z) > 1.5))
|
||||
if(!allowDiagonal && (Math.abs(x) + Math.abs(z) > 1))
|
||||
continue;
|
||||
|
||||
int targetX = current.x + x;
|
||||
@@ -160,13 +195,15 @@ public class AStarPathfinder
|
||||
Node neighbor = allNodes.get(id);
|
||||
|
||||
if(neighbor == null)
|
||||
{
|
||||
neighbor = new Node(targetX, targetY, targetZ, id);
|
||||
allNodes.put(id, neighbor);
|
||||
}
|
||||
|
||||
if(neighbor.closed)
|
||||
continue;
|
||||
|
||||
double moveCost = MOVE_COSTS[x + 1][y + 1][z + 1];
|
||||
double newGCost = current.gCost + moveCost;
|
||||
double newGCost = current.gCost + MOVE_COSTS[x + 1][y + 1][z + 1];
|
||||
|
||||
if(newGCost < neighbor.gCost || !openSetIds.contains(id))
|
||||
{
|
||||
@@ -178,7 +215,6 @@ public class AStarPathfinder
|
||||
{
|
||||
openSet.add(neighbor);
|
||||
openSetIds.add(id);
|
||||
allNodes.put(id, neighbor);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -189,25 +225,40 @@ public class AStarPathfinder
|
||||
}
|
||||
|
||||
/**
|
||||
* Advanced physics check. Checks if we can move from (fx, fy, fz) to (tx, ty, tz).
|
||||
* Advanced physics check. Checks whether we can move from one floor block to another.
|
||||
* <p>
|
||||
* The {@code fy} and {@code ty} values are floor-block Y coordinates. Entity feet and headspace are checked at {@code ty + 1} and {@code ty + 2}.
|
||||
*/
|
||||
private boolean canWalk(int fx, int fy, int fz, int tx, int ty, int tz)
|
||||
{
|
||||
Block floor = world.getBlockAt(tx, ty, tz);
|
||||
Block spaceFeet = world.getBlockAt(tx, ty + 1, tz);
|
||||
Block spaceHead = world.getBlockAt(tx, ty + 2, tz);
|
||||
|
||||
if(!isSafeFloor(floor))
|
||||
return false;
|
||||
|
||||
if(isSolid(spaceFeet) || isSolid(spaceHead))
|
||||
double absoluteFeetY = feetYAt(tx, ty, tz);
|
||||
|
||||
if(!isPositionValid(world, tx + 0.5, absoluteFeetY, tz + 0.5, entityHeight, entityWidth))
|
||||
return false;
|
||||
|
||||
if(fx != tx && fz != tz)
|
||||
{
|
||||
Block checkA = world.getBlockAt(fx, ty + 1, tz);
|
||||
Block checkB = world.getBlockAt(tx, ty + 1, fz);
|
||||
if(isSolid(checkA) || isSolid(checkB))
|
||||
double currentFeetY = feetYAt(fx, fy, fz);
|
||||
double checkY = Math.max(currentFeetY, absoluteFeetY);
|
||||
|
||||
if(!isPositionValid(world, fx + 0.5, checkY, tz + 0.5, entityHeight, entityWidth))
|
||||
return false;
|
||||
|
||||
if(!isPositionValid(world, tx + 0.5, checkY, fz + 0.5, entityHeight, entityWidth))
|
||||
return false;
|
||||
}
|
||||
|
||||
if(fy != ty)
|
||||
{
|
||||
double currentFeetY = feetYAt(fx, fy, fz);
|
||||
double highestFloorY = Math.max(currentFeetY, absoluteFeetY);
|
||||
|
||||
if(!isPositionValid(world, tx + 0.5, highestFloorY, tz + 0.5, entityHeight, entityWidth) ||
|
||||
!isPositionValid(world, fx + 0.5, highestFloorY, fz + 0.5, entityHeight, entityWidth))
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -215,48 +266,52 @@ public class AStarPathfinder
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a safe floor block at or below the given coordinates. Searches downward from the starting Y level to find a solid, passable block. Includes edge
|
||||
* detection to handle cases where the entity is standing near a block edge.
|
||||
* Resolves the block Y coordinate of the floor beneath a feet-based location. This keeps path nodes aligned with partial collision blocks such as slabs and
|
||||
* stairs.
|
||||
*
|
||||
* @param x The X coordinate
|
||||
* @param y The Y coordinate to start searching from
|
||||
* @param z The Z coordinate
|
||||
* @return The safe floor block, or null if none found
|
||||
* @param loc the feet-based location to inspect
|
||||
* @return the Y coordinate of the floor block
|
||||
*/
|
||||
private @Nullable Block findSafeFloor(int x, int y, int z)
|
||||
private int resolveFloorY(@NotNull Location loc)
|
||||
{
|
||||
Block directBlock = findSafeFloorDirect(x, y, z);
|
||||
if(directBlock != null)
|
||||
return directBlock;
|
||||
World w = loc.getWorld();
|
||||
if(w == null)
|
||||
return loc.getBlockY() - 1;
|
||||
|
||||
int[][] offsets = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}};
|
||||
for(int[] offset : offsets)
|
||||
int bx = loc.getBlockX();
|
||||
int bz = loc.getBlockZ();
|
||||
int startY = loc.getBlockY();
|
||||
|
||||
double lx = loc.getX() - bx;
|
||||
double lz = loc.getZ() - bz;
|
||||
|
||||
for(int y = startY + 1; y >= startY - 6; y--)
|
||||
{
|
||||
Block neighborBlock = findSafeFloorDirect(x + offset[0], y, z + offset[1]);
|
||||
if(neighborBlock != null)
|
||||
return neighborBlock;
|
||||
Block block = w.getBlockAt(bx, y, bz);
|
||||
|
||||
if(block.getBlockData() instanceof Openable)
|
||||
continue;
|
||||
|
||||
if(block.isLiquid())
|
||||
continue;
|
||||
|
||||
if(!block.getType().isSolid() || block.isPassable())
|
||||
continue;
|
||||
|
||||
Collection<BoundingBox> boxes = block.getCollisionShape().getBoundingBoxes();
|
||||
if(boxes.isEmpty())
|
||||
return y;
|
||||
|
||||
for(BoundingBox bb : boxes)
|
||||
{
|
||||
if(lx >= bb.getMinX() && lx <= bb.getMaxX() && lz >= bb.getMinZ() && lz <= bb.getMaxZ())
|
||||
return y;
|
||||
}
|
||||
|
||||
return y;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a safe floor block directly at the given coordinates by searching downward.
|
||||
*
|
||||
* @param x The X coordinate
|
||||
* @param y The Y coordinate to start searching from
|
||||
* @param z The Z coordinate
|
||||
* @return The safe floor block, or null if none found
|
||||
*/
|
||||
private @Nullable Block findSafeFloorDirect(int x, int y, int z)
|
||||
{
|
||||
for(int searchY = y; searchY >= y - 3; searchY--)
|
||||
{
|
||||
Block block = world.getBlockAt(x, searchY, z);
|
||||
if(isSafeFloor(block))
|
||||
return block;
|
||||
}
|
||||
return null;
|
||||
return loc.getBlockY() - 1;
|
||||
}
|
||||
|
||||
private @NotNull List<Location> retracePath(@NotNull Node current)
|
||||
@@ -264,19 +319,51 @@ public class AStarPathfinder
|
||||
List<Location> path = new ArrayList<>();
|
||||
while(current != null)
|
||||
{
|
||||
path.add(new Location(world, current.x + 0.5, current.y, current.z + 0.5));
|
||||
double feetY = feetYAt(current.x, current.y, current.z);
|
||||
path.add(new Location(world, current.x + 0.5, feetY, current.z + 0.5));
|
||||
current = current.parent;
|
||||
}
|
||||
Collections.reverse(path);
|
||||
return path;
|
||||
}
|
||||
|
||||
private double distance(@NotNull Node n, @NotNull Location l)
|
||||
private double feetYAt(int x, int floorY, int z)
|
||||
{
|
||||
double dx = (n.x + 0.5) - l.getBlockX();
|
||||
double dy = n.y - l.getBlockY();
|
||||
double dz = (n.z + 0.5) - l.getBlockZ();
|
||||
Block floor = world.getBlockAt(x, floorY, z);
|
||||
return floorY + topSurfaceAt(floor, 0.5, 0.5);
|
||||
}
|
||||
|
||||
private double topSurfaceAt(@NotNull Block block, double lx, double lz)
|
||||
{
|
||||
Collection<BoundingBox> boxes = block.getCollisionShape().getBoundingBoxes();
|
||||
if(boxes.isEmpty())
|
||||
return 1.0;
|
||||
|
||||
double bestTop = -1.0;
|
||||
|
||||
for(BoundingBox bb : boxes)
|
||||
{
|
||||
if(lx >= bb.getMinX() && lx <= bb.getMaxX() && lz >= bb.getMinZ() && lz <= bb.getMaxZ())
|
||||
bestTop = Math.max(bestTop, bb.getMaxY());
|
||||
}
|
||||
|
||||
if(bestTop < 0.0)
|
||||
{
|
||||
for(BoundingBox bb : boxes)
|
||||
bestTop = Math.max(bestTop, bb.getMaxY());
|
||||
}
|
||||
|
||||
if(bestTop <= 0.0)
|
||||
return 1.0;
|
||||
|
||||
return bestTop;
|
||||
}
|
||||
|
||||
private double distanceSq(@NotNull Node n, @NotNull Location l)
|
||||
{
|
||||
double dx = (n.x + 0.5) - l.getX();
|
||||
double dy = feetYAt(n.x, n.y, n.z) - l.getY();
|
||||
double dz = (n.z + 0.5) - l.getZ();
|
||||
return dx * dx + dy * dy + dz * dz;
|
||||
}
|
||||
|
||||
@@ -305,9 +392,9 @@ public class AStarPathfinder
|
||||
|
||||
public void calculateH(@NotNull Location end)
|
||||
{
|
||||
double dx = x - end.getBlockX();
|
||||
double dy = y - end.getBlockY();
|
||||
double dz = z - end.getBlockZ();
|
||||
double dx = (x + 0.5) - end.getX();
|
||||
double dy = (y + 1.0) - end.getY();
|
||||
double dz = (z + 0.5) - end.getZ();
|
||||
this.hCost = Math.sqrt(dx * dx + dy * dy + dz * dz);
|
||||
}
|
||||
|
||||
|
||||
@@ -12,17 +12,15 @@ 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.
|
||||
* 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.
|
||||
* <p>
|
||||
* 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 PathfindingException} occurs.
|
||||
* 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 PathfindingException} occurs.
|
||||
*
|
||||
* @param waypoints the ordered list of locations to traverse
|
||||
* @param maxIterations the maximum number of iterations the A* algorithm will attempt per segment
|
||||
@@ -31,14 +29,46 @@ public class PathfindingUtils
|
||||
* @return a {@link CompletableFuture} that completes with the calculated {@link Path}
|
||||
*/
|
||||
public static @NotNull CompletableFuture<Path> findPathAsync(@NotNull List<Location> waypoints, int maxIterations, boolean allowDiagonalMovement,
|
||||
@Nullable BiConsumer<Integer, Integer> progressListener)
|
||||
@Nullable BiConsumer<Integer, Integer> progressListener)
|
||||
{
|
||||
return CompletableFuture.supplyAsync(() ->
|
||||
{
|
||||
try
|
||||
{
|
||||
return findPath(waypoints, maxIterations, allowDiagonalMovement, progressListener);
|
||||
} catch(PathfindingException e)
|
||||
}
|
||||
catch(PathfindingException e)
|
||||
{
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}, runnable -> Bukkit.getScheduler().runTaskAsynchronously(NpcApi.plugin, runnable));
|
||||
}
|
||||
|
||||
/**
|
||||
* Asynchronously calculates a path through a list of waypoints.
|
||||
* <p>
|
||||
* 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 PathfindingException} 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 entityHeight the height of the entity traversing the path
|
||||
* @param entityWidth the width of the entity traversing the path
|
||||
* @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<Path> findPathAsync(@NotNull List<Location> waypoints, int maxIterations, boolean allowDiagonalMovement,
|
||||
double entityHeight, double entityWidth,
|
||||
@Nullable BiConsumer<Integer, Integer> progressListener)
|
||||
{
|
||||
return CompletableFuture.supplyAsync(() ->
|
||||
{
|
||||
try
|
||||
{
|
||||
return findPath(waypoints, maxIterations, allowDiagonalMovement, entityHeight, entityWidth, progressListener);
|
||||
}
|
||||
catch(PathfindingException e)
|
||||
{
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
@@ -48,8 +78,8 @@ public class PathfindingUtils
|
||||
/**
|
||||
* Synchronously calculates a path through a list of waypoints.
|
||||
* <p>
|
||||
* 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}.
|
||||
* 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
|
||||
@@ -59,14 +89,36 @@ public class PathfindingUtils
|
||||
* @throws PathfindingException if any segment's start or end location is invalid/unwalkable
|
||||
*/
|
||||
public static @NotNull Path findPath(@NotNull List<Location> waypoints, int maxIterations, boolean allowDiagonalMovement,
|
||||
@Nullable BiConsumer<Integer, Integer> progressListener) throws PathfindingException
|
||||
@Nullable BiConsumer<Integer, Integer> progressListener) throws PathfindingException
|
||||
{
|
||||
return findPath(waypoints, maxIterations, allowDiagonalMovement, 1.8, 0.6, progressListener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronously calculates a path through a list of waypoints.
|
||||
* <p>
|
||||
* 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 entityHeight the height of the entity traversing the path
|
||||
* @param entityWidth the width of the entity traversing the path
|
||||
* @param progressListener a progress listener with the signature (segmentIndex, totalSegments)
|
||||
* @return the calculated {@link Path} containing all intermediate locations
|
||||
* @throws PathfindingException if any segment's start or end location is invalid/unwalkable
|
||||
*/
|
||||
public static @NotNull Path findPath(@NotNull List<Location> waypoints, int maxIterations, boolean allowDiagonalMovement,
|
||||
double entityHeight, double entityWidth, @Nullable BiConsumer<Integer, Integer> progressListener)
|
||||
throws PathfindingException
|
||||
{
|
||||
if(waypoints.size() < 2)
|
||||
throw new IllegalArgumentException("Waypoints list must contain at least 2 locations.");
|
||||
|
||||
List<Location> fullPathPoints = new ArrayList<>();
|
||||
|
||||
AStarPathfinder aStar = new AStarPathfinder(maxIterations, allowDiagonalMovement);
|
||||
AStarPathfinder aStar = new AStarPathfinder(maxIterations, allowDiagonalMovement, entityHeight, entityWidth);
|
||||
for(int i = 0; i < waypoints.size() - 1; i++)
|
||||
{
|
||||
Location start = waypoints.get(i);
|
||||
@@ -77,12 +129,10 @@ public class PathfindingUtils
|
||||
if(segment == null)
|
||||
throw new PathfindingException("Could not find path between waypoint " + i + " and " + (i + 1));
|
||||
|
||||
if(!fullPathPoints.isEmpty() && !segment.isEmpty() && isAbove(segment.get(0), fullPathPoints.get(fullPathPoints.size() - 1)))
|
||||
if(!fullPathPoints.isEmpty() && !segment.isEmpty())
|
||||
segment.remove(0);
|
||||
else if(!segment.isEmpty())
|
||||
segment.set(0, segment.get(0).subtract(0, 1, 0));
|
||||
|
||||
fullPathPoints.addAll(segment.stream().map(location -> location.clone().add(0, 1, 0)).toList());
|
||||
fullPathPoints.addAll(segment.stream().map(Location::clone).toList());
|
||||
|
||||
if(progressListener != null)
|
||||
progressListener.accept(i + 1, waypoints.size() - 1);
|
||||
@@ -91,11 +141,6 @@ public class PathfindingUtils
|
||||
return new Path(fullPathPoints, waypoints);
|
||||
}
|
||||
|
||||
private static boolean isAbove(@NotNull Location l1, @NotNull Location l2)
|
||||
{
|
||||
return l1.getX() == l2.getX() && l1.getY() - 1 == l2.getY() && l1.getZ() == l2.getZ();
|
||||
}
|
||||
|
||||
public static class PathfindingException extends Exception
|
||||
{
|
||||
public PathfindingException(String message)
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
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;
|
||||
import de.eisi05.npc.api.objects.NpcOption;
|
||||
import de.eisi05.npc.api.pathfinding.AStarPathfinder;
|
||||
import de.eisi05.npc.api.pathfinding.Path;
|
||||
import de.eisi05.npc.api.utils.Var;
|
||||
import de.eisi05.npc.api.wrapper.objects.WrappedEntity;
|
||||
import de.eisi05.npc.api.wrapper.packets.MoveEntityPacket;
|
||||
import de.eisi05.npc.api.wrapper.packets.RotateHeadPacket;
|
||||
@@ -37,9 +39,12 @@ public class PathTask extends BukkitRunnable
|
||||
private static final double stepHeight = 0.55;
|
||||
|
||||
private final NPC npc;
|
||||
private final double entityHeight;
|
||||
private final double entityWidth;
|
||||
private final Path path;
|
||||
private final List<Location> pathPoints;
|
||||
private final Player[] viewers;
|
||||
private final Set<UUID> viewerIds = new HashSet<>();
|
||||
private final boolean autoManageWalkingViewers;
|
||||
private final WrappedEntity<?> serverEntity;
|
||||
private final Consumer<WalkingResult> callback;
|
||||
private final boolean withRotation;
|
||||
@@ -53,8 +58,11 @@ public class PathTask extends BukkitRunnable
|
||||
private boolean finished = false;
|
||||
private int index = 0;
|
||||
private Vector currentPos;
|
||||
private float previousPitch;
|
||||
private float previousYaw;
|
||||
private double verticalVelocity = 0.0;
|
||||
private int viewerRefreshTicks = 0;
|
||||
private boolean isWaitingForChunkLoad = false;
|
||||
|
||||
/**
|
||||
* Private constructor used by the Builder pattern.
|
||||
@@ -64,9 +72,20 @@ public class PathTask extends BukkitRunnable
|
||||
private PathTask(@NotNull Builder builder)
|
||||
{
|
||||
this.npc = builder.npc;
|
||||
double scale = npc.getOption(NpcOption.SCALE);
|
||||
this.entityHeight = npc.entity.getBoundingBox().getYSize() * scale;
|
||||
this.entityWidth = npc.entity.getBoundingBox().getXSize() * scale;
|
||||
this.path = builder.path;
|
||||
this.pathPoints = new ArrayList<>(builder.path.asLocations());
|
||||
this.viewers = builder.viewers;
|
||||
if(builder.viewers != null)
|
||||
{
|
||||
for(Player viewer : builder.viewers)
|
||||
{
|
||||
if(viewer != null)
|
||||
this.viewerIds.add(viewer.getUniqueId());
|
||||
}
|
||||
}
|
||||
this.autoManageWalkingViewers = builder.autoManageWalkingViewers;
|
||||
this.callback = builder.callback;
|
||||
this.withRotation = builder.withRotation;
|
||||
|
||||
@@ -74,6 +93,7 @@ public class PathTask extends BukkitRunnable
|
||||
this.updateRealLocation = builder.updateRealLocation;
|
||||
|
||||
this.currentPos = npc.getLocation().toVector();
|
||||
this.previousPitch = npc.getLocation().getPitch();
|
||||
this.previousYaw = npc.getLocation().getYaw();
|
||||
this.previousMoveDir = npc.getLocation().getDirection();
|
||||
this.serverEntity = npc.entity;
|
||||
@@ -85,6 +105,39 @@ 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;
|
||||
npc.refreshWalkingViewers();
|
||||
}
|
||||
|
||||
if(index >= pathPoints.size())
|
||||
{
|
||||
if(finishPath())
|
||||
@@ -100,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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -263,9 +359,10 @@ public class PathTask extends BukkitRunnable
|
||||
if(event.changeRealLocation())
|
||||
{
|
||||
Location loc = path.getWaypoints().isEmpty() ? pathPoints.get(pathPoints.size() - 1) : path.getWaypoints().get(path.getWaypoints().size() - 1);
|
||||
npc.changeRealLocation(loc, viewers);
|
||||
npc.changeRealLocation(loc, getViewers());
|
||||
}
|
||||
|
||||
npc.clearWalkingTask(this);
|
||||
cancel();
|
||||
return true;
|
||||
}
|
||||
@@ -287,8 +384,8 @@ public class PathTask extends BukkitRunnable
|
||||
TeleportEntityPacket teleport = new TeleportEntityPacket(serverEntity,
|
||||
new TeleportEntityPacket.PositionMoveRotation(loc.toVector(), new Vector(0, 0, 0), loc.getYaw(), loc.getPitch()), Set.of(), true);
|
||||
|
||||
npc.sendNpcMovePackets(teleport, head, viewers);
|
||||
npc.sendNpcBodyPackets(body, viewers);
|
||||
npc.sendNpcMovePackets(teleport, head, getViewers());
|
||||
npc.sendNpcBodyPackets(body, getViewers());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -299,7 +396,9 @@ public class PathTask extends BukkitRunnable
|
||||
*/
|
||||
private boolean hasReachedWaypoint(@NotNull Vector toTarget)
|
||||
{
|
||||
return toTarget.lengthSquared() < 0.04 && Math.abs(toTarget.getY()) < 0.2;
|
||||
double horizontalDistSq = (toTarget.getX() * toTarget.getX()) + (toTarget.getZ() * toTarget.getZ());
|
||||
double verticalDiff = Math.abs(toTarget.getY());
|
||||
return horizontalDistSq <= 0.04 && verticalDiff < 0.5;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -432,54 +531,15 @@ public class PathTask extends BukkitRunnable
|
||||
*/
|
||||
private boolean isPositionValid(@NotNull World world, @NotNull Vector pos)
|
||||
{
|
||||
double entityWidth = 0.6;
|
||||
double entityHeight = 1.8;
|
||||
|
||||
double minX = pos.getX() - entityWidth / 2;
|
||||
double maxX = pos.getX() + entityWidth / 2;
|
||||
double minY = pos.getY();
|
||||
double maxY = pos.getY() + entityHeight;
|
||||
double minZ = pos.getZ() - entityWidth / 2;
|
||||
double maxZ = pos.getZ() + entityWidth / 2;
|
||||
|
||||
int minBlockX = (int) Math.floor(minX);
|
||||
int maxBlockX = (int) Math.floor(maxX);
|
||||
int minBlockY = (int) Math.floor(minY);
|
||||
int maxBlockY = (int) Math.floor(maxY);
|
||||
int minBlockZ = (int) Math.floor(minZ);
|
||||
int maxBlockZ = (int) Math.floor(maxZ);
|
||||
|
||||
for(int x = minBlockX; x <= maxBlockX; x++)
|
||||
{
|
||||
for(int y = minBlockY; y <= maxBlockY; y++)
|
||||
{
|
||||
for(int z = minBlockZ; z <= maxBlockZ; z++)
|
||||
{
|
||||
Block block = world.getBlockAt(x, y, z);
|
||||
if(block.getBlockData() instanceof Openable)
|
||||
continue;
|
||||
|
||||
if(block.getType().isSolid() && !block.isPassable())
|
||||
{
|
||||
BoundingBox blockBox = block.getBoundingBox();
|
||||
Vector min = new Vector(minX, minY, minZ);
|
||||
Vector max = new Vector(maxX, maxY, maxZ);
|
||||
if(blockBox.overlaps(min, max))
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
return AStarPathfinder.isPositionValid(world, pos.getX(), pos.getY(), pos.getZ(), entityHeight, entityWidth);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the Y-coordinate of the ground at a given position.
|
||||
* Calculates the feet Y-coordinate of the ground at a given position.
|
||||
*
|
||||
* @param world The world to check in
|
||||
* @param pos The position to check
|
||||
* @return The Y-coordinate of the ground
|
||||
* @return The Y-coordinate where the NPC's feet should be
|
||||
*/
|
||||
private double getGroundY(@NotNull World world, @NotNull Vector pos)
|
||||
{
|
||||
@@ -487,24 +547,35 @@ public class PathTask extends BukkitRunnable
|
||||
int bz = pos.getBlockZ();
|
||||
int startY = pos.getBlockY();
|
||||
|
||||
for(int y = startY; y >= startY - 3; y--)
|
||||
for(int y = startY; y >= startY - 4; y--)
|
||||
{
|
||||
Block block = world.getBlockAt(bx, y, bz);
|
||||
|
||||
if(block.getBlockData() instanceof Openable)
|
||||
if(block.getBlockData() instanceof Openable || block.isLiquid())
|
||||
continue;
|
||||
|
||||
if(Var.isCarpet(block.getType()) && Var.isCarpet(block.getRelative(BlockFace.UP).getType()))
|
||||
return ++y;
|
||||
Collection<BoundingBox> boxes = block.getCollisionShape().getBoundingBoxes();
|
||||
if(boxes.isEmpty())
|
||||
continue;
|
||||
|
||||
if(!block.getType().isSolid() || block.isPassable())
|
||||
return y;
|
||||
double lx = pos.getX() - bx;
|
||||
double lz = pos.getZ() - bz;
|
||||
|
||||
OptionalDouble maxY = block.getCollisionShape().getBoundingBoxes().stream().mapToDouble(BoundingBox::getMaxY).max();
|
||||
OptionalDouble minY = block.getCollisionShape().getBoundingBoxes().stream().mapToDouble(BoundingBox::getMinY).min();
|
||||
double bestTop = -1.0;
|
||||
|
||||
if(minY.isPresent() && maxY.isPresent())
|
||||
return y + minY.getAsDouble() + (maxY.getAsDouble() - minY.getAsDouble());
|
||||
for(BoundingBox bb : boxes)
|
||||
{
|
||||
if(lx >= bb.getMinX() && lx <= bb.getMaxX() && lz >= bb.getMinZ() && lz <= bb.getMaxZ())
|
||||
bestTop = Math.max(bestTop, bb.getMaxY());
|
||||
}
|
||||
|
||||
if(bestTop < 0.0)
|
||||
{
|
||||
for(BoundingBox bb : boxes)
|
||||
bestTop = Math.max(bestTop, bb.getMaxY());
|
||||
}
|
||||
|
||||
return y + bestTop;
|
||||
}
|
||||
|
||||
return world.getHighestBlockYAt(bx, bz);
|
||||
@@ -563,6 +634,100 @@ public class PathTask extends BukkitRunnable
|
||||
return angle;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether this task allows automatic walking viewer management.
|
||||
*
|
||||
* @return true if viewers can be added automatically; false otherwise
|
||||
*/
|
||||
public boolean isAutoManageWalkingViewers()
|
||||
{
|
||||
return autoManageWalkingViewers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the NPC's current walking location.
|
||||
*
|
||||
* @return the current walking location
|
||||
*/
|
||||
public @NotNull Location getCurrentLocation()
|
||||
{
|
||||
World world = npc.getLocation().getWorld();
|
||||
return currentPos.toLocation(world);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a viewer to this path task and immediately syncs the NPC's current walking position.
|
||||
*
|
||||
* @param player the player to add
|
||||
* @return true if the player was newly added; false if the player was already a viewer
|
||||
*/
|
||||
public boolean addViewer(@NotNull Player player)
|
||||
{
|
||||
if(finished)
|
||||
return false;
|
||||
|
||||
if(!viewerIds.add(player.getUniqueId()))
|
||||
return false;
|
||||
|
||||
sendCurrentPosition(player);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the specified player is already attached to this path task.
|
||||
*
|
||||
* @param player the player to check
|
||||
* @return true if the player is already a viewer of this walking task; false otherwise
|
||||
*/
|
||||
public boolean hasViewer(@NotNull Player player)
|
||||
{
|
||||
return viewerIds.contains(player.getUniqueId());
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a viewer from this path task.
|
||||
*
|
||||
* @param player the player to remove
|
||||
*/
|
||||
public void removeViewer(@NotNull Player player)
|
||||
{
|
||||
viewerIds.remove(player.getUniqueId());
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all online viewers currently attached to this path task.
|
||||
*
|
||||
* @return online viewers
|
||||
*/
|
||||
private Player @NotNull [] getViewers()
|
||||
{
|
||||
return viewerIds.stream()
|
||||
.map(Bukkit::getPlayer)
|
||||
.filter(Objects::nonNull)
|
||||
.toArray(Player[]::new);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends the NPC's current walking position and rotation to a viewer.
|
||||
*
|
||||
* @param player the viewer to sync
|
||||
*/
|
||||
private void sendCurrentPosition(@NotNull Player player)
|
||||
{
|
||||
|
||||
if(serverEntity == null)
|
||||
return;
|
||||
|
||||
RotateHeadPacket head = new RotateHeadPacket(serverEntity, (byte) (previousYaw * 256 / 360));
|
||||
|
||||
Vector currentVec = new Vector(currentPos.getX(), currentPos.getY(), currentPos.getZ());
|
||||
|
||||
TeleportEntityPacket teleport = new TeleportEntityPacket(serverEntity,
|
||||
new TeleportEntityPacket.PositionMoveRotation(currentVec, new Vector(0, 0, 0), previousYaw, previousPitch), Set.of(), true);
|
||||
|
||||
npc.sendNpcMovePackets(teleport, head, player);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends movement and rotation packets to update the NPC's position for viewers.
|
||||
*
|
||||
@@ -576,11 +741,14 @@ public class PathTask extends BukkitRunnable
|
||||
if(serverEntity == null)
|
||||
return;
|
||||
|
||||
previousYaw = yaw;
|
||||
previousPitch = pitch;
|
||||
|
||||
RotateHeadPacket head = new RotateHeadPacket(serverEntity, (byte) (yaw * 256 / 360));
|
||||
TeleportEntityPacket teleport = new TeleportEntityPacket(serverEntity, new TeleportEntityPacket.PositionMoveRotation(currentPos, movement, yaw, pitch),
|
||||
Set.of(), onGround);
|
||||
|
||||
npc.sendNpcMovePackets(teleport, head, viewers);
|
||||
npc.sendNpcMovePackets(teleport, head, getViewers());
|
||||
|
||||
if(updateRealLocation)
|
||||
npc.setLocation(currentPos.toLocation(npc.getLocation().getWorld()));
|
||||
@@ -615,9 +783,9 @@ public class PathTask extends BukkitRunnable
|
||||
World world = path.getWaypoints().isEmpty() ? pathPoints.get(pathPoints.size() - 1).getWorld() :
|
||||
path.getWaypoints().get(path.getWaypoints().size() - 1).getWorld();
|
||||
Location loc = new Location(world, currentPos.getX(), currentPos.getY(), currentPos.getZ());
|
||||
npc.changeRealLocation(loc, viewers);
|
||||
npc.changeRealLocation(loc, getViewers());
|
||||
}
|
||||
|
||||
npc.clearWalkingTask(this);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -655,6 +823,7 @@ public class PathTask extends BukkitRunnable
|
||||
private double speed = 1.0;
|
||||
private boolean updateRealLocation = false;
|
||||
private boolean withRotation = true;
|
||||
private boolean autoManageWalkingViewers = false;
|
||||
|
||||
/**
|
||||
* Creates a new Builder for a PathTask.
|
||||
@@ -680,6 +849,20 @@ public class PathTask extends BukkitRunnable
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether this path task should allow automatic walking viewer management.
|
||||
* <p>
|
||||
* When enabled, external library listeners may add eligible players to this task while the NPC is already walking.
|
||||
*
|
||||
* @param autoManageWalkingViewers true to allow automatic walking viewer management, false otherwise
|
||||
* @return This builder instance for method chaining
|
||||
*/
|
||||
public @NotNull Builder autoManageWalkingViewers(boolean autoManageWalkingViewers)
|
||||
{
|
||||
this.autoManageWalkingViewers = autoManageWalkingViewers;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the movement speed of the NPC.
|
||||
*
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
package de.eisi05.npc.api.utils;
|
||||
|
||||
import java.lang.annotation.*;
|
||||
|
||||
/**
|
||||
* Indicates that a configuration option or field is strictly reserved for third-party API integration and is not actively utilized by the internal plugin
|
||||
* logic.
|
||||
* <p/>
|
||||
* {@link de.eisi05.npc.api.objects.NpcConfig}
|
||||
*/
|
||||
@Documented
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
@Target({ElementType.FIELD, ElementType.METHOD})
|
||||
public @interface ApiOnly
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
package de.eisi05.npc.api.utils;
|
||||
|
||||
import de.eisi05.npc.api.objects.NPC;
|
||||
import de.eisi05.npc.api.objects.NpcOption;
|
||||
import de.eisi05.npc.api.wrapper.objects.WrappedEntity;
|
||||
import org.bukkit.FluidCollisionMode;
|
||||
import org.bukkit.GameMode;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.attribute.Attribute;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.util.RayTraceResult;
|
||||
import org.bukkit.util.Vector;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
public class NpcHitboxUtil
|
||||
{
|
||||
/**
|
||||
* Checks whether a player's line of sight intersects the Oriented Bounding Box (OBB) of an NPC using an optimized RaySlab intersection algorithm.
|
||||
* <p>
|
||||
* This method accounts for the NPC's custom scale, bounding box dimensions, and spatial orientation (yaw rotation). It supports specialized poses such as
|
||||
* {@link org.bukkit.entity.Pose#SLEEPING}, adjusting the bounding box extents and center offset dynamically to match the modified client-side model.
|
||||
* </p>
|
||||
* <p>
|
||||
* To prevent clipping exploits, the method executes a fast-fail line-of-sight raytrace via Bukkit's world physics engine up to the point of structural
|
||||
* intersection, ensuring that solid blocks or other valid entities do not obstruct the interaction.
|
||||
* </p>
|
||||
*
|
||||
* @param npc the {@link NPC} instance whose bounding box is being evaluated
|
||||
* @param sleeping {@code true} if the NPC is currently rendered in a sleeping pose, triggering specialized horizontal bounding box transformations
|
||||
* @param player the {@link Player} executing the interaction, used to determine ray origin, direction vector, and maximum interaction range attributes
|
||||
* @return {@code true} if the player's ray safely intersects the NPC's bounding box within their maximum interaction range without being obstructed by
|
||||
* blocks or other entities; {@code false} otherwise * @see org.bukkit.util.Vector
|
||||
* @see org.bukkit.util.RayTraceResult
|
||||
*/
|
||||
public static boolean rayIntersectsNpc(@NotNull NPC npc, boolean sleeping, @NotNull Player player)
|
||||
{
|
||||
Location eyeLocation = player.getEyeLocation();
|
||||
Vector origin = eyeLocation.toVector();
|
||||
Vector dir = eyeLocation.getDirection();
|
||||
|
||||
double maxDistance;
|
||||
try
|
||||
{
|
||||
maxDistance = player.getAttribute(Attribute.valueOf("GENERIC_ENTITY_INTERACTION_RANGE")).getValue();
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
maxDistance = player.getGameMode() == GameMode.CREATIVE ? 5.0 : 3.0;
|
||||
}
|
||||
|
||||
Location base = npc.getLocation();
|
||||
float yaw = base.getYaw();
|
||||
double yawRad = Math.toRadians(yaw);
|
||||
double cos = Math.cos(yawRad);
|
||||
double sin = Math.sin(yawRad);
|
||||
|
||||
WrappedEntity.BoundingBox bb = npc.entity.getDefaultBoundingBox();
|
||||
double scale = npc.getOption(NpcOption.SCALE, player);
|
||||
|
||||
double width, height, length;
|
||||
Vector centerOffset = new Vector(0, 0, 0);
|
||||
|
||||
if(sleeping)
|
||||
{
|
||||
width = bb.getXSize() * scale;
|
||||
height = bb.getZSize() * scale;
|
||||
length = bb.getYSize() * scale;
|
||||
|
||||
// Inlined forward vector rotation to save allocations
|
||||
Vector forward = new Vector(-sin, 0, cos).normalize();
|
||||
centerOffset.copy(forward).multiply(-length / 2.0);
|
||||
centerOffset.setY(height / 2.0);
|
||||
}
|
||||
else
|
||||
{
|
||||
width = bb.getXSize() * scale;
|
||||
height = bb.getYSize() * scale;
|
||||
length = bb.getZSize() * scale;
|
||||
centerOffset.setY(height / 2.0);
|
||||
}
|
||||
|
||||
Vector center = base.toVector().add(centerOffset);
|
||||
Vector oc = origin.clone().subtract(center);
|
||||
|
||||
// Inverse rotation to bring ray into local OBB space
|
||||
double ox = oc.getX() * cos + oc.getZ() * sin;
|
||||
double oz = -oc.getX() * sin + oc.getZ() * cos;
|
||||
double oy = oc.getY();
|
||||
|
||||
double dx = dir.getX() * cos + dir.getZ() * sin;
|
||||
double dz = -dir.getX() * sin + dir.getZ() * cos;
|
||||
double dy = dir.getY();
|
||||
|
||||
double minX = -width / 2.0, maxX = width / 2.0;
|
||||
double minY = -height / 2.0, maxY = height / 2.0;
|
||||
double minZ = -length / 2.0, maxZ = length / 2.0;
|
||||
|
||||
double tMin = 0.0;
|
||||
double tMax = maxDistance;
|
||||
|
||||
// X Slab
|
||||
if(Math.abs(dx) < 1e-8)
|
||||
{
|
||||
if(ox < minX || ox > maxX)
|
||||
return false;
|
||||
}
|
||||
else
|
||||
{
|
||||
double invDx = 1.0 / dx;
|
||||
double t1 = (minX - ox) * invDx;
|
||||
double t2 = (maxX - ox) * invDx;
|
||||
if(t1 > t2)
|
||||
{
|
||||
double tmp = t1;
|
||||
t1 = t2;
|
||||
t2 = tmp;
|
||||
}
|
||||
tMin = Math.max(tMin, t1);
|
||||
tMax = Math.min(tMax, t2);
|
||||
if(tMin > tMax)
|
||||
return false;
|
||||
}
|
||||
|
||||
// Y Slab
|
||||
if(Math.abs(dy) < 1e-8)
|
||||
{
|
||||
if(oy < minY || oy > maxY)
|
||||
return false;
|
||||
}
|
||||
else
|
||||
{
|
||||
double invDy = 1.0 / dy;
|
||||
double t1 = (minY - oy) * invDy;
|
||||
double t2 = (maxY - oy) * invDy;
|
||||
if(t1 > t2)
|
||||
{
|
||||
double tmp = t1;
|
||||
t1 = t2;
|
||||
t2 = tmp;
|
||||
}
|
||||
tMin = Math.max(tMin, t1);
|
||||
tMax = Math.min(tMax, t2);
|
||||
if(tMin > tMax)
|
||||
return false;
|
||||
}
|
||||
|
||||
// Z Slab
|
||||
if(Math.abs(dz) < 1e-8)
|
||||
{
|
||||
if(oz < minZ || oz > maxZ)
|
||||
return false;
|
||||
}
|
||||
else
|
||||
{
|
||||
double invDz = 1.0 / dz;
|
||||
double t1 = (minZ - oz) * invDz;
|
||||
double t2 = (maxZ - oz) * invDz;
|
||||
if(t1 > t2)
|
||||
{
|
||||
double tmp = t1;
|
||||
t1 = t2;
|
||||
t2 = tmp;
|
||||
}
|
||||
tMin = Math.max(tMin, t1);
|
||||
tMax = Math.min(tMax, t2);
|
||||
if(tMin > tMax)
|
||||
return false;
|
||||
}
|
||||
|
||||
RayTraceResult blockHit = eyeLocation.getWorld().rayTraceBlocks(eyeLocation, dir, tMin, FluidCollisionMode.NEVER, true);
|
||||
if(blockHit != null && blockHit.getHitBlock() != null)
|
||||
return false;
|
||||
|
||||
RayTraceResult entityHit = eyeLocation.getWorld().rayTraceEntities(eyeLocation, dir, tMin, 0.1,
|
||||
(entity) -> !entity.equals(player) && entity.isValid());
|
||||
if(entityHit != null && entityHit.getHitEntity() != null)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -116,8 +116,7 @@ public class PacketReader
|
||||
if(npc == null)
|
||||
return;
|
||||
|
||||
Bukkit.getScheduler().scheduleSyncDelayedTask(NpcApi.plugin,
|
||||
() -> Bukkit.getPluginManager().callEvent(new NpcInteractEvent(player, npc, ClickActionType.LEFT)), 0);
|
||||
callNpc(player, npc, ClickActionType.LEFT);
|
||||
cancelUseUntilTick.put(player.getUniqueId(), currentTick + 10);
|
||||
return;
|
||||
}
|
||||
@@ -138,8 +137,7 @@ public class PacketReader
|
||||
if(useEntityPacketWrapper.usingSecondaryAction() || useEntityPacketWrapper.getHand() == InteractionHand.OFF_HAND)
|
||||
return;
|
||||
|
||||
Bukkit.getScheduler().scheduleSyncDelayedTask(NpcApi.plugin,
|
||||
() -> Bukkit.getPluginManager().callEvent(new NpcInteractEvent(player, npc, ClickActionType.RIGHT)), 0);
|
||||
callNpc(player, npc, ClickActionType.RIGHT);
|
||||
cancelUseUntilTick.put(player.getUniqueId(), currentTick + 10);
|
||||
return;
|
||||
}
|
||||
@@ -147,18 +145,28 @@ public class PacketReader
|
||||
UseEntityPacketWrapper.Action action = useEntityPacketWrapper.getAction();
|
||||
if(action.getActionType() == UseEntityPacketWrapper.ActionType.ATTACK)
|
||||
{
|
||||
Bukkit.getScheduler().scheduleSyncDelayedTask(NpcApi.plugin,
|
||||
() -> Bukkit.getPluginManager().callEvent(new NpcInteractEvent(player, npc, ClickActionType.LEFT)), 0);
|
||||
callNpc(player, npc, ClickActionType.LEFT);
|
||||
cancelUseUntilTick.put(player.getUniqueId(), currentTick + 10);
|
||||
}
|
||||
else if(action.getActionType() == UseEntityPacketWrapper.ActionType.INTERACT && action.getHand() == InteractionHand.MAIN_HAND)
|
||||
{
|
||||
Bukkit.getScheduler().scheduleSyncDelayedTask(NpcApi.plugin,
|
||||
() -> Bukkit.getPluginManager().callEvent(new NpcInteractEvent(player, npc, ClickActionType.RIGHT)), 0);
|
||||
callNpc(player, npc, ClickActionType.RIGHT);
|
||||
cancelUseUntilTick.put(player.getUniqueId(), currentTick + 10);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls the {@link NpcInteractEvent} for the specified player and NPC.
|
||||
*
|
||||
* @param player The {@link Player} who interacted with the NPC. Must not be {@code null}.
|
||||
* @param npc The {@link NPC} that was interacted with. Must not be {@code null}.
|
||||
* @param type The type of interaction (left or right click). Must not be {@code null}.
|
||||
*/
|
||||
public static void callNpc(@NotNull Player player, @NotNull NPC npc, @NotNull ClickActionType type)
|
||||
{
|
||||
Bukkit.getScheduler().runTask(NpcApi.plugin, () -> Bukkit.getPluginManager().callEvent(new NpcInteractEvent(player, npc, type)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Uninjects the custom {@link ChannelDuplexHandler} from the specified player's Netty pipeline. This stops the interception of packets for that player.
|
||||
*
|
||||
|
||||
@@ -1,24 +1,27 @@
|
||||
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;
|
||||
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Arrays;
|
||||
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.
|
||||
*
|
||||
@@ -30,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();
|
||||
}
|
||||
@@ -52,46 +63,42 @@ 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();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a declared method in the specified class that matches the given name and parameter types.
|
||||
* Finds a declared method in the specified class that matches the given name and parameter types. Uses caching to avoid repeated reflection overhead.
|
||||
*
|
||||
* @param clazz the class to search in
|
||||
* @param name the name of the method
|
||||
@@ -101,11 +108,16 @@ public class Reflections
|
||||
*/
|
||||
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);
|
||||
Class<?>[] argTypes = args == null ? new Class<?>[0] : Arrays.stream(args)
|
||||
.map(o -> o == null ? Object.class : o.getClass())
|
||||
.toArray(Class<?>[]::new);
|
||||
|
||||
MethodKey key = new MethodKey(clazz, name, argTypes);
|
||||
Method cached = METHOD_CACHE.get(key);
|
||||
if(cached != null)
|
||||
return cached;
|
||||
|
||||
Class<?> current = clazz;
|
||||
while(current != null)
|
||||
{
|
||||
for(Method method : current.getDeclaredMethods())
|
||||
@@ -119,6 +131,7 @@ public class Reflections
|
||||
if(isCompatible(argTypes, paramTypes, isVarArgs))
|
||||
{
|
||||
method.setAccessible(true);
|
||||
METHOD_CACHE.put(key, method);
|
||||
return method;
|
||||
}
|
||||
}
|
||||
@@ -131,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.
|
||||
@@ -181,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);
|
||||
}
|
||||
@@ -204,21 +216,44 @@ 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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a declared field in the specified class by its name and makes it accessible.
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a declared field in the specified class by its name and makes it accessible. Uses caching to avoid repeated reflection overhead.
|
||||
*
|
||||
* @param clazz the class to search in
|
||||
* @param fieldName the name of the field
|
||||
@@ -227,14 +262,21 @@ public class Reflections
|
||||
*/
|
||||
private static @NotNull Field findField(@NotNull Class<?> clazz, @NotNull String fieldName) throws NoSuchFieldException
|
||||
{
|
||||
FieldKey key = new FieldKey(clazz, fieldName);
|
||||
Field cached = FIELD_CACHE.get(key);
|
||||
if(cached != null)
|
||||
return cached;
|
||||
|
||||
while(clazz != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
Field field = clazz.getDeclaredField(fieldName);
|
||||
field.setAccessible(true);
|
||||
FIELD_CACHE.put(key, field);
|
||||
return field;
|
||||
} catch(NoSuchFieldException ignored)
|
||||
}
|
||||
catch(NoSuchFieldException ignored)
|
||||
{
|
||||
clazz = clazz.getSuperclass();
|
||||
}
|
||||
@@ -256,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);
|
||||
}
|
||||
@@ -277,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);
|
||||
}
|
||||
@@ -298,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);
|
||||
}
|
||||
@@ -317,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);
|
||||
}
|
||||
@@ -337,12 +383,77 @@ 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);
|
||||
}
|
||||
}
|
||||
|
||||
private record MethodKey(Class<?> clazz, String methodName, Class<?>[] paramTypes)
|
||||
{
|
||||
@Override
|
||||
public boolean equals(Object o)
|
||||
{
|
||||
if(this == o)
|
||||
return true;
|
||||
if(!(o instanceof MethodKey methodKey))
|
||||
return false;
|
||||
return clazz.equals(methodKey.clazz) && methodName.equals(methodKey.methodName) && Arrays.equals(paramTypes, methodKey.paramTypes);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode()
|
||||
{
|
||||
int result = clazz.hashCode();
|
||||
result = 31 * result + methodName.hashCode();
|
||||
result = 31 * result + Arrays.hashCode(paramTypes);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
private record FieldKey(Class<?> clazz, String fieldName)
|
||||
{
|
||||
@Override
|
||||
public boolean equals(Object o)
|
||||
{
|
||||
if(this == o)
|
||||
return true;
|
||||
if(!(o instanceof FieldKey fieldKey))
|
||||
return false;
|
||||
return clazz.equals(fieldKey.clazz) && fieldName.equals(fieldKey.fieldName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode()
|
||||
{
|
||||
int result = clazz.hashCode();
|
||||
result = 31 * result + fieldName.hashCode();
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
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.
|
||||
*
|
||||
@@ -374,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);
|
||||
}
|
||||
@@ -400,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);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
package de.eisi05.npc.api.utils;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
/**
|
||||
* A serializable version of {@link Predicate} that can be used in contexts where the predicate needs to be serialized.
|
||||
*
|
||||
* @param <T> the type of the first argument to the predicate
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface SerializablePredicate<T> extends Predicate<T>, Serializable
|
||||
{
|
||||
}
|
||||
@@ -2,7 +2,6 @@ package de.eisi05.npc.api.utils;
|
||||
|
||||
import de.eisi05.npc.api.wrapper.objects.WrappedComponent;
|
||||
import de.eisi05.npc.api.wrapper.objects.WrappedEntitySnapshot;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.entity.Entity;
|
||||
import org.bukkit.entity.EntityType;
|
||||
@@ -219,9 +218,4 @@ public class Var
|
||||
|
||||
return flags;
|
||||
}
|
||||
|
||||
public static boolean isCarpet(@NotNull Material material)
|
||||
{
|
||||
return material.name().contains("CARPET");
|
||||
}
|
||||
}
|
||||
@@ -20,10 +20,12 @@ import java.util.Arrays;
|
||||
public class WrappedEntity<T extends Entity> extends Wrapper
|
||||
{
|
||||
public String data = "{}";
|
||||
private final BoundingBox defaultBoundingBox;
|
||||
|
||||
WrappedEntity(Object handle)
|
||||
{
|
||||
super(handle);
|
||||
defaultBoundingBox = getBoundingBox();
|
||||
}
|
||||
|
||||
public static <T extends Entity, V extends WrappedEntity<T>> @NotNull V fromEntity(@NotNull T entity, @NotNull Class<V> clazz)
|
||||
@@ -156,6 +158,11 @@ public class WrappedEntity<T extends Entity> extends Wrapper
|
||||
return new BoundingBox(invokeWrappedMethod());
|
||||
}
|
||||
|
||||
public @NotNull BoundingBox getDefaultBoundingBox()
|
||||
{
|
||||
return defaultBoundingBox;
|
||||
}
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "setId")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_21_9, to = Versions.V1_21_11), path = "e")
|
||||
public void setId(int id)
|
||||
@@ -253,12 +260,26 @@ public class WrappedEntity<T extends Entity> extends Wrapper
|
||||
super(handle);
|
||||
}
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "getXsize")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_11), path = "b")
|
||||
public double getXSize()
|
||||
{
|
||||
return invokeWrappedMethod();
|
||||
}
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "getYsize")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_11), path = "c")
|
||||
public double getYSize()
|
||||
{
|
||||
return invokeWrappedMethod();
|
||||
}
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "getZsize")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_11), path = "d")
|
||||
public double getZSize()
|
||||
{
|
||||
return invokeWrappedMethod();
|
||||
}
|
||||
}
|
||||
|
||||
public abstract static class WrappedNameTag<T extends Entity> extends WrappedEntity<T>
|
||||
|
||||
@@ -29,6 +29,7 @@ public class WrappedEntityData extends Wrapper
|
||||
public <T> void set(@NotNull EntityDataAccessor<T> accessor, @Nullable T value)
|
||||
{
|
||||
invokeWrappedMethod(accessor, value);
|
||||
markDirty(accessor);
|
||||
}
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "get")
|
||||
@@ -46,6 +47,12 @@ public class WrappedEntityData extends Wrapper
|
||||
return invokeWrappedMethod();
|
||||
}
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V26_1), path = "markDirty")
|
||||
public <T> void markDirty(@NotNull EntityDataAccessor<T> accessor)
|
||||
{
|
||||
invokeWrappedMethod(accessor);
|
||||
}
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "getNonDefaultValues")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_18, to = Versions.V1_21_11), path = "c")
|
||||
@Mapping(fixed = @Mapping.Fixed(Versions.V1_17), path = "getAll")
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -206,7 +206,7 @@ public class WrappedServerPlayer extends WrappedEntity<Player>
|
||||
return new WrappedCommandSourceStack(invokeWrappedMethod());
|
||||
}
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_21_11, to = Versions.V1_21_11), path = "stopUsingItem")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "stopUsingItem")
|
||||
@Mapping(fixed = @Mapping.Fixed(Versions.V1_21_11), path = "gf")
|
||||
@Mapping(fixed = @Mapping.Fixed(Versions.V1_21_9), path = "fU")
|
||||
@Mapping(fixed = @Mapping.Fixed(Versions.V1_21_6), path = "fM")
|
||||
|
||||
@@ -17,15 +17,16 @@ public class WrappedVec2F extends Wrapper
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "x")
|
||||
@Mapping(fixed = @Mapping.Fixed(Versions.V1_21_11), path = "j")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_9), path = "i")
|
||||
public float getYaw()
|
||||
{
|
||||
return getWrappedFieldValue();
|
||||
}
|
||||
|
||||
@Mapping(fixed = @Mapping.Fixed(Versions.V1_21_11), path = "y")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_9), path = "j")
|
||||
public float getPitch()
|
||||
{
|
||||
return getWrappedFieldValue();
|
||||
}
|
||||
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V26_1, to = Versions.V26_1), path = "y")
|
||||
@Mapping(fixed = @Mapping.Fixed(Versions.V1_21_11), path = "k")
|
||||
@Mapping(range = @Mapping.Range(from = Versions.V1_17, to = Versions.V1_21_9), path = "j")
|
||||
public float getYaw()
|
||||
{
|
||||
return getWrappedFieldValue();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
Reference in new issue
Block a user