Hey! So I've got a server where I spawn Human Entitys and I have autosave disabled. The problem is that (probably) because of the disabled autosave that the Humans despawn after around 1 minute if no player is on the server. Is this a bug or is there any way to solve that. The same thing happened to me when I was spawning an ItemEntity on a server with autosave disabled. Maybe someone has experienced the same. Thanks!
It makes sense for the entity to disappear. Once you're far enough, server will unload the chunks. Since they are not being saved, your entities aren't either. Therefore when chunks are loaded back in, no entities are there. Solution number one, would be to enable autosave. And the second, is to run the save manually /save-all (check /help). Last, but not least, spawn your entities when needed when chunks are loaded. It would be extremely helpful if we knew your intentions behind this and reasoning on why autosave is disabled in the first place.
Yea you're right. So the Human Entities are for my hub server where the minigame NPCs for example are located. Since they don't need to be saved in the world, I have autosave off and don't save anything in the entity NBT. I have now found a solution with NBT tags, but I would actually like it better if the garbage collector was disabled for entities on the lobby server.
Best solution for this would be to spawn them on demand. Garbage collector won't dump them if you keep a reference to them in your plugin.
By you mean to store them in a variable? Because I do so. The only thing now missing are the values which are not stored in NBT(Like my own callback functions for the interaction with the entity) and I now store these values in a seperate array and I am giving each Entity an ID which is also the key of the array. A bit difficult to explain but it works fine now. But I'm still a bit sad that I couldn't find a nicer solution.
I'm interested to see how your code looks. Especially the spawning and handling the interactions. I am no longer active developer for a long time, so I am unable to provide you a good enough solution from top of my head. But I am sure we can come to suitable solution if we slowly work through this.
So I spawn them with this function. You can see I am storing the data which doesn't get saved after a chunk unloading in the $npcData variable which I store in an array with the key value being the entity id. PHP: public function addNPC(string $skinName, Location $location, string $nametag = "", bool $isGeometry = false, bool $turnToPlayer = false, Closure $onPunch = null, Closure $onClick = null, Closure $onCollide = null, float $scale = 1.0, bool $danceToPlayer = false, array $emoteIds = [], string $skinPath = ""): NPC{ $nbt = Human::createBaseNBT($location->asVector3(), null, $location->getYaw(), $location->getPitch()); $nbt->setTag(new CompoundTag("Skin", [ "Name" => new StringTag("Name", $skinName), "Data" => new ByteArrayTag("Data", ManagerSystem::getAPI()->readImage(($skinPath === "" ? "/home/Datenbank/".($isGeometry ? "Geometries" : "NPCSkins")."/" : $skinPath).$skinName.".png")), "GeometryName" => new StringTag("GeometryName", $isGeometry ? "geometry.".$skinName : "geometry.humanoid.custom"), "GeometryData" => new ByteArrayTag("GeometryData", str_replace("\n", "", $isGeometry ? file_get_contents(($skinPath === "" ? "/home/Datenbank/Geometries/" : $skinPath).$skinName.".json") : "")) ])); $npc = new NPC($location->getLevel(), $nbt); ManagerSystem::$npcData[$npc->getId()] = $npcData = [$npc->getId(), $nametag, $turnToPlayer, $onPunch, $onClick, $onCollide, $scale, $danceToPlayer, $emoteIds]; $npc->initNPC(...$npcData); ManagerSystem::$npcs[$npc->getId()] = $npc; return $npc;} My NPC Object looks like this. I now store the entity id in the NBT Tag and after that load the data again when the server initializes the entity. PHP: <?phpnamespace ManagerSystem\utils;use Closure;use ManagerSystem\ManagerSystem;use pocketmine\entity\Human;use pocketmine\event\entity\EntityDamageByEntityEvent;use pocketmine\event\entity\EntityDamageEvent;use pocketmine\item\Item;use pocketmine\math\Vector2;use pocketmine\math\Vector3;use pocketmine\network\mcpe\protocol\MoveActorAbsolutePacket;use pocketmine\Player;use function array_keys;use function atan2;use function time;use const M_PI;class NPC extends Human { /** @var Closure|null */ public $onPunch = null, $onClick = null, $onCollide = null; /** @var bool */ public $turnToPlayer = false, $danceToPlayer = false; /** @var string[] */ private $emoteIds = []; /** @var int */ private $lastDanceTime = 0, $lastDanceIndex = 0, $npcId; public $width = 0, $scale = 1; protected function initEntity(): void{ $nbt = $this->namedtag; parent::initEntity(); $this->setImmobile(true); if($nbt->hasTag("NPC_ID")){ if(isset(ManagerSystem::$npcData[$nbt->getInt("NPC_ID")])) $this->initNPC(...ManagerSystem::$npcData[$nbt->getInt("NPC_ID")]); else $this->flagForDespawn(); } } public function saveNBT(): void{ parent::saveNBT(); $this->namedtag->setInt("NPC_ID", $this->npcId); } public function initNPC(int $npcId, string $nametag = "", bool $turnToPlayer = false, Closure $onPunch = null, Closure $onClick = null, Closure $onCollide = null, float $scale = 1.0, bool $danceToPlayer = false, $emoteIds = []): void{ $this->npcId = $npcId; $this->onPunch = $onPunch; $this->onClick = $onClick; $this->onCollide = $onCollide; $this->turnToPlayer = $turnToPlayer; $this->danceToPlayer = $danceToPlayer; $this->emoteIds = $emoteIds; $this->setNameTag($nametag); $this->scale = $scale; $this->setScale($scale); } public function onCollideWithPlayer(Player $player): void{ if($this->onCollide !== null) ($this->onCollide)($player, $this); } public function attack(EntityDamageEvent $source): void{ if($source instanceof EntityDamageByEntityEvent){ $damager = $source->getDamager(); if($damager instanceof Player && $this->onPunch !== null){ ($this->onPunch)($damager, $this); } } $source->setCancelled(); } public function onInteract(Player $player, Item $item, Vector3 $clickPos): bool{ if($this->onClick !== null) ($this->onClick)($player, $this); return true; } public function getWidth(): int{ return 0; } public function onUpdate(int $currentTick): bool{ $danceViewers = []; foreach($this->getLevel()->getNearbyEntities($this->getBoundingBox()->expandedCopy(7, 5, 7), $this) as $viewer){ if($viewer instanceof Player){ $danceViewers[] = $viewer; if($this->turnToPlayer){ $xdiff = $viewer->x - $this->x; $zdiff = $viewer->z - $this->z; $angle = atan2($zdiff, $xdiff); $yaw = (($angle * 180) / M_PI) - 90; $ydiff = $viewer->y - $this->y; $v = new Vector2($this->x, $this->z); $dist = $v->distance($viewer->x, $viewer->z); $angle = atan2($dist, $ydiff); $pitch = (($angle * 180) / M_PI) - 90; $pk = new MoveActorAbsolutePacket(); $pk->position = $this->getOffsetPosition($this); $pk->xRot = $pitch; $pk->yRot = $yaw; $pk->zRot = $yaw; $pk->entityRuntimeId = $this->getId(); $viewer->sendDataPacket($pk); } } } if($this->danceToPlayer){ $now = time(); if($this->lastDanceTime < $now){ if($this->lastDanceIndex === count($this->emoteIds)) $this->lastDanceIndex = 0; ManagerSystem::getAPI()->sendEmote($this, empty($this->emoteIds) ? array_keys(ManagerSystem::getAPI()::EMOTES)[$this->lastDanceIndex] : $this->emoteIds[$this->lastDanceIndex], $danceViewers); $this->lastDanceIndex++; $this->lastDanceTime = $now + 8; } } return parent::onUpdate($currentTick); } public function hasMovementUpdate(): bool{ return false; }}
This looks fine and would work flawlessly if it did save entities inside the chunk. However if you're determined to not use autosave, let's look for ways to spawn the entity back into level. This is unnecessary overhead for the server, but something like this might do the job. PHP: public function onChunkLoad(ChunkLoadEvent $event) { if($event->isNewChunk()) return; // Let's assume we're spawning entities in existing chunks $chunk = $event->getChunk(); foreach(ManagerSystem::$npcs as $npc) { if(($npc->x >> 4) === $chunk->x && ($npc->z >> 4) === $chunk->z) { // 1) Check if this particular entity is not present // 2) Spawn Chunk::addEntity($npc) NPC::spawnToAll() ?!? Not exactly sure, but you'll figure it out } }} So this possibly is one of the approaches. I am really confused on why won't you leave this mechanic in hands of pocketmine api.
I would leave it in the hands of the pocketmine api but as I said earlier I already have autosave enabled(after it didn't work at all with autsave disabled) Not sure if I undestand right what you meant by your code snippet, but I made a new Listener. PHP: public function onChunkLoad(ChunkLoadEvent $event) { if($event->isNewChunk()) return; $chunk = $event->getChunk(); foreach(ManagerSystem::$npcs as $npc) { if(($npc->getX() >> 4) === $chunk->getX() && ($npc->getZ() >> 4) === $chunk->getZ()) { if(($entity = $event->getLevel()->getEntity($npc->getId())) === null) $chunk->addEntity($npc); } } } After that I get this exception which kinda makes sense. But maybe I just didn't understand it right. Code: 08:22:05 Server|CRITICAL > InvalidArgumentException: "Attempted to add a garbage closed Entity to a chunk" (EXCEPTION) in "src/pocketmine/level/format/Chunk" at line 601 08:22:05 Server|CRITICAL > #0 plugins/ManagerSystem/src/ManagerSystem/listeners/ChunkLoadListener(19): pocketmine\level\format\Chunk->addEntity(object ManagerSystem\utils\NPC) 08:22:05 Server|CRITICAL > #1 src/pocketmine/plugin/MethodEventExecutor(42): ManagerSystem\listeners\ChunkLoadListener->onChunkLoad(object pocketmine\event\level\ChunkLoadEvent) 08:22:05 Server|CRITICAL > #2 src/pocketmine/plugin/RegisteredListener(80): pocketmine\plugin\MethodEventExecutor->execute(object ManagerSystem\listeners\ChunkLoadListener, object pocketmine\event\level\ChunkLoadEvent) 08:22:05 Server|CRITICAL > #3 src/pocketmine/event/Event(88): pocketmine\plugin\RegisteredListener->callEvent(object pocketmine\event\level\ChunkLoadEvent)
Okay, so apparently you can't use the same entity object which I suspected, hence the comment I left. But basically with that snippet you now have control over loading the npcs on demand, you just need to add that part in, instead of the Chunk::addEntity option I gave
Isn't it also loading on demand now? Because I load it when the initEntity() method is executed. I don't quite understand what I should do now in the listener so that the entities don't lose their properties. Do you mean that at that moment the entity is "deleted" and that's why I put the data from the old Entity into a newly created human entity?
What I currently understand is, that chunks are being unloaded, and entity data is not being saved, and your npc entities are being closed. Therefore spawning NPCs must be done persistently. My proposal was to do it whenever chunk loads, right as you put it Or modify NPC::isClosed() to something like this, and try re-adding the entity. PHP: /** @var NPC $this **/return isset(ManagerSystem::$npcs[$this->getId()]); As I mentioned, I am long retired developer, and this API might have changed significantly.
Try using Entity->setCanSaveWithChunk(), which should make it so that the entity will be saved when the chunk it's in is unloaded