Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/main/java/dansplugins/wildpets/WildPets.java
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ private void registerEventHandlers() {
listeners.add(new JoinAndQuitHandler(petListRepository, ephemeralData));
listeners.add(new MoveHandler(petListRepository));
listeners.add(new BreedEventHandler(petListRepository, petRecordRepository, configService, ephemeralData));
listeners.add(new ChunkLoadHandler(petListRepository));
EventHandlerRegistry eventHandlerRegistry = new EventHandlerRegistry();
eventHandlerRegistry.registerEventHandlers(listeners, this);
}
Expand Down
32 changes: 32 additions & 0 deletions src/main/java/dansplugins/wildpets/listeners/ChunkLoadHandler.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package dansplugins.wildpets.listeners;

import dansplugins.wildpets.pet.Pet;
import dansplugins.wildpets.pet.list.PetListRepository;
import org.bukkit.entity.Entity;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.world.ChunkLoadEvent;

/**
* Handles chunk load events to ensure pet entities retain their
* persistence flags, preventing them from despawning.
*
* @author Daniel McCoy Stephenson
*/
public class ChunkLoadHandler implements Listener {
private final PetListRepository petListRepository;

public ChunkLoadHandler(PetListRepository petListRepository) {
this.petListRepository = petListRepository;
}

@EventHandler()
public void handle(ChunkLoadEvent event) {
for (Entity entity : event.getChunk().getEntities()) {
Pet pet = petListRepository.getPet(entity);
if (pet != null) {
pet.ensurePersistence();
}
Comment on lines +24 to +29
Copy link

Copilot AI Mar 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ChunkLoadEvent can fire very frequently, and this handler does an O(entities_in_chunk × total_pets) scan because petListRepository.getPet(entity) iterates all pet lists and each list iterates its pets. On servers with many pets/players this can create noticeable lag spikes during chunk loads. Consider maintaining a UUID→Pet index in PetListRepository (updated on add/remove/load) so the lookup here is O(1), or otherwise avoid nested linear scans in this event handler.

Copilot uses AI. Check for mistakes.
}
}
}
27 changes: 27 additions & 0 deletions src/main/java/dansplugins/wildpets/pet/Pet.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import dansplugins.wildpets.location.WpLocation;
import org.bukkit.Server;
import org.bukkit.entity.Entity;
import org.bukkit.entity.LivingEntity;
import org.bukkit.entity.Mob;
import dansplugins.wildpets.data.Lockable;
import dansplugins.wildpets.data.Savable;
Expand Down Expand Up @@ -123,6 +124,32 @@ public void applyAIState() {
}
}

/**
* Ensures that, for a currently loaded pet entity, persistence and "do not remove when far away"
* flags are applied.
* This helps prevent the entity from being removed by distance-based despawn mechanics while it
* is loaded. It does not prevent chunk unloading itself; these flags may need to be re-applied
* after the entity is reloaded.
* Returns silently if the entity is not currently loaded.
*/
public void ensurePersistence() {
if (serverProvider == null) {
return;
}
Server server = serverProvider.get();
if (server == null) {
return;
}
Entity entity = server.getEntity(uniqueID);
if (entity == null) {
return;
}
entity.setPersistent(true);
if (entity instanceof LivingEntity) {
((LivingEntity) entity).setRemoveWhenFarAway(false);
Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

setRemoveWhenFarAway(...) is not a method on LivingEntity in the Spigot API (it’s on Mob). As written, this will not compile against spigot-api 1.17.1-R0.1-SNAPSHOT. Change the check/cast to entity instanceof Mob and call ((Mob) entity).setRemoveWhenFarAway(false) (and drop the LivingEntity import if no longer needed).

Suggested change
if (entity instanceof LivingEntity) {
((LivingEntity) entity).setRemoveWhenFarAway(false);
if (entity instanceof Mob) {
((Mob) entity).setRemoveWhenFarAway(false);

Copilot uses AI. Check for mistakes.
}
}

public String getMovementState() {
return movementState;
}
Expand Down
4 changes: 4 additions & 0 deletions src/main/java/dansplugins/wildpets/pet/list/PetList.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import org.bukkit.Bukkit;
import org.bukkit.ChatColor;
import org.bukkit.entity.Entity;
import org.bukkit.entity.LivingEntity;
import org.bukkit.entity.Player;

import dansplugins.wildpets.config.ConfigService;
Expand Down Expand Up @@ -62,6 +63,9 @@ public boolean removePet(Pet petToRemove) {
if (entity != null) {
entity.setCustomName("");
entity.setPersistent(false);
if (entity instanceof LivingEntity) {
((LivingEntity) entity).setRemoveWhenFarAway(true);
}
Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same issue as in Pet.ensurePersistence(): setRemoveWhenFarAway(...) is not on LivingEntity in Spigot 1.17.1. Use entity instanceof Mob and call ((Mob) entity).setRemoveWhenFarAway(true) so this compiles and only applies to entities that support the flag.

Copilot uses AI. Check for mistakes.
entity.setInvulnerable(false);
}
return getPets().remove(petToRemove);
Expand Down
34 changes: 26 additions & 8 deletions src/main/java/dansplugins/wildpets/pet/list/PetListRepository.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import org.bukkit.entity.Player;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.UUID;

/**
Expand All @@ -20,6 +21,7 @@ public class PetListRepository {
private final ConfigService configService;

private final ArrayList<PetList> petLists = new ArrayList<>();
private final HashMap<UUID, Pet> petsByEntityUUID = new HashMap<>();

public PetListRepository(ConfigService configService) {
this.configService = configService;
Expand All @@ -37,27 +39,23 @@ public boolean addNewPet(Player player, Entity entity) {
WpLocation wpLocation = new WpLocation(bukkitLocation.getX(), bukkitLocation.getY(), bukkitLocation.getZ());
newPet.setLastKnownLocation(wpLocation);
entity.setCustomName(ChatColor.GREEN + newPet.getName());
entity.setPersistent(true);
newPet.ensurePersistence();
entity.playEffect(EntityEffect.LOVE_HEARTS);

// add pet to pet list
PetList petList = getPetList(player.getUniqueId());
petList.addPet(newPet);
petsByEntityUUID.put(newPet.getUniqueID(), newPet);
return true;
}

public boolean removePet(Pet petToRemove) {
petsByEntityUUID.remove(petToRemove.getUniqueID());
return getPetList(petToRemove.getOwnerUUID()).removePet(petToRemove);
Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removePet() removes the entry from petsByEntityUUID before confirming the pet was actually removed from the owning PetList. If PetList.removePet(...) returns false (or the owner list is missing), the UUID index becomes inconsistent with petLists. Consider only removing from the map after a successful removal (and guarding against a null PetList).

Suggested change
petsByEntityUUID.remove(petToRemove.getUniqueID());
return getPetList(petToRemove.getOwnerUUID()).removePet(petToRemove);
PetList ownerPetList = getPetList(petToRemove.getOwnerUUID());
if (ownerPetList == null) {
return false;
}
boolean removed = ownerPetList.removePet(petToRemove);
if (removed) {
petsByEntityUUID.remove(petToRemove.getUniqueID());
}
return removed;

Copilot uses AI. Check for mistakes.
}

public Pet getPet(Entity entity) {
for (PetList petList : getPetLists()) {
Pet pet = petList.getPet(entity.getUniqueId());
if (pet != null) {
return pet;
}
}
return null;
return petsByEntityUUID.get(entity.getUniqueId());
}

public PetList getPetList(UUID playerUUID) {
Expand All @@ -74,6 +72,26 @@ public void createPetListForPlayer(UUID playerUUID) {
getPetLists().add(newPetList);
}

/**
* Adds an already-constructed Pet to the appropriate PetList and the UUID index.
* Used when loading pets from storage.
*/
public void addExistingPet(Pet pet) {
if (getPetList(pet.getOwnerUUID()) == null) {
createPetListForPlayer(pet.getOwnerUUID());
}
getPetList(pet.getOwnerUUID()).addPet(pet);
petsByEntityUUID.put(pet.getUniqueID(), pet);
}

/**
* Clears all pet lists and the UUID index.
*/
public void clearAll() {
petLists.clear();
petsByEntityUUID.clear();
}

public Pet getPlayersPet(Player player, Entity entity) {
PetList petList = getPetList(player.getUniqueId());
return petList.getPet(entity.getUniqueId());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ private void writeOutFiles(List<Map<String, String>> saveData, String fileName)

private void loadPets() {
// load each pet individually and reconstruct pet list objects
petListRepository.getPetLists().clear();
petListRepository.clearAll();

ArrayList<HashMap<String, String>> data = loadDataFromFilename(FILE_PATH + PETS_FILE_NAME);

Expand All @@ -103,11 +103,7 @@ private void loadPets() {
}

for (Pet pet : allPets) {
if (petListRepository.getPetList(pet.getOwnerUUID()) == null) {
petListRepository.createPetListForPlayer(pet.getOwnerUUID());

}
petListRepository.getPetList(pet.getOwnerUUID()).addPet(pet);
petListRepository.addExistingPet(pet);
petRecordRepository.addPetRecord(pet); // will not result in duplicates because petRecords is a hashset
}

Expand All @@ -134,6 +130,7 @@ private void applyAIStateToPets() {
wildPets.getServer().getScheduler().runTaskLater(wildPets, () -> {
for (Pet pet : petListRepository.getAllPets()) {
pet.applyAIState();
pet.ensurePersistence();
}
}, delayTicks); // Default: wait 100 ticks (5 seconds at 20 TPS) for entities to be loaded
}
Expand Down
56 changes: 56 additions & 0 deletions src/test/java/dansplugins/wildpets/pet/PetTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,62 @@ public void testApplyAIStateWithNullEntity() {
assertEquals("Staying", pet.getMovementState());
}

@Test
public void testEnsurePersistenceWithMob() {
// prepare - Mob extends LivingEntity
when(mockServer.getEntity(entityUniqueId)).thenReturn(mockMob);

// Create pet
Pet pet = new Pet(entityUniqueId, playerOwnerUniqueId, playerOwnerName, mockServerProvider);

// execute
pet.ensurePersistence();

// verify
verify(mockMob).setPersistent(true);
verify(mockMob).setRemoveWhenFarAway(false);
}

@Test
public void testEnsurePersistenceWithNonLivingEntity() {
// prepare - mockEntity is just Entity, not LivingEntity
when(mockServer.getEntity(entityUniqueId)).thenReturn(mockEntity);

// Create pet
Pet pet = new Pet(entityUniqueId, playerOwnerUniqueId, playerOwnerName, mockServerProvider);

// execute
pet.ensurePersistence();

// verify setPersistent is called, but setRemoveWhenFarAway is not (not a LivingEntity)
verify(mockEntity).setPersistent(true);
}

@Test
public void testEnsurePersistenceWithNullEntity() {
// prepare - entity not found (e.g., in unloaded chunk)
when(mockServer.getEntity(entityUniqueId)).thenReturn(null);

// Create pet
Pet pet = new Pet(entityUniqueId, playerOwnerUniqueId, playerOwnerName, mockServerProvider);

// execute - should not throw an exception
pet.ensurePersistence();

// verify no interactions attempted on null entity
}

@Test
public void testEnsurePersistenceWithNullServerProvider() {
// Create pet with null server provider
Pet pet = new Pet(entityUniqueId, playerOwnerUniqueId, playerOwnerName, null);

// execute - should not throw an exception
pet.ensurePersistence();

// verify no exception occurred
}

// Helper method to create test data
private Map<String, String> createBasicPetData(String movementState) {
Map<String, String> petData = new HashMap<>();
Expand Down
Loading