package com.hypixel.hytale.server.spawning.controllers; import com.hypixel.hytale.component.ComponentAccessor; import com.hypixel.hytale.component.Ref; import com.hypixel.hytale.logger.HytaleLogger; import com.hypixel.hytale.server.core.entity.UUIDComponent; import com.hypixel.hytale.server.core.universe.PlayerRef; import com.hypixel.hytale.server.core.universe.world.World; import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; import com.hypixel.hytale.server.npc.NPCPlugin; import com.hypixel.hytale.server.spawning.assets.spawns.config.BeaconNPCSpawn; import com.hypixel.hytale.server.spawning.assets.spawns.config.RoleSpawnParameters; import com.hypixel.hytale.server.spawning.beacons.LegacySpawnBeaconEntity; import com.hypixel.hytale.server.spawning.jobs.NPCBeaconSpawnJob; import com.hypixel.hytale.server.spawning.wrappers.BeaconSpawnWrapper; import it.unimi.dsi.fastutil.ints.IntOpenHashSet; import it.unimi.dsi.fastutil.ints.IntSet; import it.unimi.dsi.fastutil.objects.Object2DoubleMap; import it.unimi.dsi.fastutil.objects.Object2DoubleOpenHashMap; import it.unimi.dsi.fastutil.objects.Object2IntMap; import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; import it.unimi.dsi.fastutil.objects.ObjectArrayList; import java.time.Duration; import java.util.Comparator; import java.util.List; import java.util.UUID; import java.util.concurrent.ThreadLocalRandom; import java.util.logging.Level; import javax.annotation.Nonnull; import javax.annotation.Nullable; public class BeaconSpawnController extends SpawnController { @Nonnull private static final HytaleLogger LOGGER = HytaleLogger.forEnclosingClass(); public static final int MAX_ATTEMPTS_PER_TICK = 5; public static final double ROUNDING_BREAK_POINT = 0.25; @Nonnull private final Ref ownerRef; private final List> spawnedEntities = new ObjectArrayList(); private final List playersInRegion = new ObjectArrayList(); private int nextPlayerIndex = 0; private final Object2IntMap entitiesPerPlayer = new Object2IntOpenHashMap(); private final Object2DoubleMap> entityTimeoutCounter = new Object2DoubleOpenHashMap(); private final IntSet unspawnableRoles = new IntOpenHashSet(); private final Comparator threatComparator = Comparator.comparingInt(playerRef -> this.entitiesPerPlayer.getOrDefault(playerRef.getUuid(), 0)); private int baseMaxTotalSpawns; private int currentScaledMaxTotalSpawns; private int[] baseMaxConcurrentSpawns; private int currentScaledMaxConcurrentSpawns; private int spawnsThisRound; private int remainingSpawns; private boolean roundStart = true; private double beaconRadiusSquared; private double spawnRadiusSquared; private double despawnNPCAfterTimeout; private Duration despawnBeaconAfterTimeout; private boolean despawnNPCsIfIdle; public BeaconSpawnController(@Nonnull World world, @Nonnull Ref ownerRef) { super(world); this.ownerRef = ownerRef; } @Override public int getMaxActiveJobs() { return Math.min(this.remainingSpawns, this.baseMaxActiveJobs); } @Nullable public NPCBeaconSpawnJob createRandomSpawnJob(@Nonnull ComponentAccessor componentAccessor) { LegacySpawnBeaconEntity legacySpawnBeaconComponent = componentAccessor.getComponent(this.ownerRef, LegacySpawnBeaconEntity.getComponentType()); assert legacySpawnBeaconComponent != null; BeaconSpawnWrapper wrapper = legacySpawnBeaconComponent.getSpawnWrapper(); RoleSpawnParameters spawn = wrapper.pickRole(ThreadLocalRandom.current()); String spawnId = spawn.getId(); int roleIndex = NPCPlugin.get().getIndex(spawnId); if (roleIndex >= 0 && !this.unspawnableRoles.contains(roleIndex)) { NPCBeaconSpawnJob job = null; int predictedTotal = this.spawnedEntities.size() + this.activeJobs.size(); if (this.activeJobs.size() < this.getMaxActiveJobs() && this.nextPlayerIndex < this.playersInRegion.size() && predictedTotal < this.currentScaledMaxTotalSpawns) { job = this.idleJobs.isEmpty() ? new NPCBeaconSpawnJob() : this.idleJobs.pop(); job.beginProbing(this.playersInRegion.get(this.nextPlayerIndex++), this.currentScaledMaxConcurrentSpawns, roleIndex, spawn.getFlockDefinition()); this.activeJobs.add(job); if (this.nextPlayerIndex >= this.playersInRegion.size()) { this.nextPlayerIndex = 0; } } return job; } else { return null; } } public void initialise(@Nonnull BeaconSpawnWrapper spawnWrapper) { BeaconNPCSpawn spawn = spawnWrapper.getSpawn(); this.baseMaxTotalSpawns = spawn.getMaxSpawnedNpcs(); this.baseMaxConcurrentSpawns = spawn.getConcurrentSpawnsRange(); double beaconRadius = spawn.getBeaconRadius(); this.beaconRadiusSquared = beaconRadius * beaconRadius; double spawnRadius = spawn.getSpawnRadius(); this.spawnRadiusSquared = spawnRadius * spawnRadius; this.despawnNPCAfterTimeout = spawn.getNpcIdleDespawnTimeSeconds(); this.despawnBeaconAfterTimeout = spawn.getBeaconVacantDespawnTime(); this.despawnNPCsIfIdle = spawn.getNpcSpawnState() != null; } public int getSpawnsThisRound() { return this.spawnsThisRound; } public void setRemainingSpawns(int remainingSpawns) { this.remainingSpawns = remainingSpawns; } public void addRoundSpawn() { this.spawnsThisRound++; this.remainingSpawns--; } public boolean isRoundStart() { return this.roundStart; } public void setRoundStart(boolean roundStart) { this.roundStart = roundStart; } public Ref getOwnerRef() { return this.ownerRef; } public int[] getBaseMaxConcurrentSpawns() { return this.baseMaxConcurrentSpawns; } public List getPlayersInRegion() { return this.playersInRegion; } public int getCurrentScaledMaxConcurrentSpawns() { return this.currentScaledMaxConcurrentSpawns; } public void setCurrentScaledMaxConcurrentSpawns(int currentScaledMaxConcurrentSpawns) { this.currentScaledMaxConcurrentSpawns = currentScaledMaxConcurrentSpawns; } public Duration getDespawnBeaconAfterTimeout() { return this.despawnBeaconAfterTimeout; } public double getSpawnRadiusSquared() { return this.spawnRadiusSquared; } public double getBeaconRadiusSquared() { return this.beaconRadiusSquared; } public int getBaseMaxTotalSpawns() { return this.baseMaxTotalSpawns; } public void setCurrentScaledMaxTotalSpawns(int currentScaledMaxTotalSpawns) { this.currentScaledMaxTotalSpawns = currentScaledMaxTotalSpawns; } public List> getSpawnedEntities() { return this.spawnedEntities; } public void setNextPlayerIndex(int nextPlayerIndex) { this.nextPlayerIndex = nextPlayerIndex; } public Object2DoubleMap> getEntityTimeoutCounter() { return this.entityTimeoutCounter; } public Object2IntMap getEntitiesPerPlayer() { return this.entitiesPerPlayer; } public boolean isDespawnNPCsIfIdle() { return this.despawnNPCsIfIdle; } public double getDespawnNPCAfterTimeout() { return this.despawnNPCAfterTimeout; } public Comparator getThreatComparator() { return this.threatComparator; } public void notifySpawnedEntityExists(@Nonnull Ref ref, @Nonnull ComponentAccessor componentAccessor) { this.spawnedEntities.add(ref); HytaleLogger.Api context = LOGGER.at(Level.FINE); if (context.isEnabled()) { UUIDComponent ownerUuidComponent = componentAccessor.getComponent(this.ownerRef, UUIDComponent.getComponentType()); assert ownerUuidComponent != null; context.log("Registering NPC with reference %s with Spawn Beacon %s", ref, ownerUuidComponent.getUuid()); } } public void onJobFinished(@Nonnull ComponentAccessor componentAccessor) { if (++this.spawnsThisRound >= this.currentScaledMaxConcurrentSpawns) { this.onAllConcurrentSpawned(componentAccessor); } } public void notifyNPCRemoval(@Nonnull Ref ref, @Nonnull ComponentAccessor componentAccessor) { this.spawnedEntities.remove(ref); this.entityTimeoutCounter.removeDouble(ref); if (this.spawnedEntities.size() == this.currentScaledMaxTotalSpawns - 1) { LegacySpawnBeaconEntity.prepareNextSpawnTimer(this.ownerRef, componentAccessor); } HytaleLogger.Api context = LOGGER.at(Level.FINE); if (context.isEnabled()) { UUIDComponent ownerUuidComponent = componentAccessor.getComponent(this.ownerRef, UUIDComponent.getComponentType()); assert ownerUuidComponent != null; context.log("Removing NPC with reference %s from Spawn Beacon %s", ref, ownerUuidComponent.getUuid()); } } public boolean hasSlots() { return this.spawnedEntities.size() < this.currentScaledMaxTotalSpawns; } public void markNPCUnspawnable(int roleIndex) { this.unspawnableRoles.add(roleIndex); } public void clearUnspawnableNPCs() { this.unspawnableRoles.clear(); } public void onAllConcurrentSpawned(@Nonnull ComponentAccessor componentAccessor) { this.spawnsThisRound = 0; this.remainingSpawns = 0; LegacySpawnBeaconEntity.prepareNextSpawnTimer(this.ownerRef, componentAccessor); this.roundStart = true; } }