diff --git a/src/main/java/com/projectswg/holocore/resources/support/npc/ai/NpcCombatMode.java b/src/main/java/com/projectswg/holocore/resources/support/npc/ai/NpcCombatMode.java index a4c62bc69..913a85187 100644 --- a/src/main/java/com/projectswg/holocore/resources/support/npc/ai/NpcCombatMode.java +++ b/src/main/java/com/projectswg/holocore/resources/support/npc/ai/NpcCombatMode.java @@ -4,7 +4,6 @@ import com.projectswg.common.data.encodables.tangible.Posture; import com.projectswg.common.data.location.Location; import com.projectswg.holocore.intents.support.global.command.QueueCommandIntent; import com.projectswg.holocore.resources.support.data.server_info.loader.DataLoader; -import com.projectswg.holocore.resources.support.npc.spawn.Spawner; import com.projectswg.holocore.resources.support.objects.swg.creature.CreatureObject; import com.projectswg.holocore.resources.support.objects.swg.custom.AIObject; import com.projectswg.holocore.resources.support.objects.swg.custom.NpcMode; @@ -27,25 +26,25 @@ public class NpcCombatMode extends NpcMode { private final AtomicReference returnLocation; private final Collection targets; private final AtomicInteger attackCountdown; - private final Spawner spawner; - public NpcCombatMode(Spawner spawner) { + public NpcCombatMode() { this.returnLocation = new AtomicReference<>(null); this.targets = new CopyOnWriteArraySet<>(); this.attackCountdown = new AtomicInteger(0); - this.spawner = spawner; } @Override public void onPlayerMoveInAware(CreatureObject player, double distance) { - if (distance < spawner.getAggressiveRadius()) { + if (distance < getSpawner().getAggressiveRadius()) { if (targets.add(player)) { requestAssistance(); if (!isExecuting()) requestModeStart(); } } else { - targets.remove(player); + // If out of aggressive range, and not actively fighting + if (!getAI().getDefenders().contains(player.getObjectId())) + targets.remove(player); } } @@ -68,7 +67,7 @@ public class NpcCombatMode extends NpcMode { } syncTargets(); - if (isInActiveCombat()) { + if (!targets.isEmpty()) { performCombatAction(); queueNextLoop(1000); } else if (isReturning()) { @@ -84,17 +83,6 @@ public class NpcCombatMode extends NpcMode { return getAI().isInCombat() || ret == null || getAI().getLocation().distanceTo(ret) >= RETURN_THRESHOLD; } - private boolean isInActiveCombat() { - if (getAI().isInCombat()) - return true; - - for (CreatureObject target : targets) { - if (!target.hasBuff("incapWeaken") || spawner.isDeathblow()) - return true; - } - return false; - } - private void performCombatAction() { CreatureObject target = getPrimaryTarget(); if (target == null) @@ -108,9 +96,9 @@ public class NpcCombatMode extends NpcMode { if (target.getPosture() != Posture.INCAPACITATED && target.getPosture() != Posture.DEAD && attackCountdown.decrementAndGet() <= 0) { attack(target, weapon); if (getAI().getPrimaryWeapons().contains(weapon)) - attackCountdown.set((int) spawner.getPrimaryWeaponSpeed()); + attackCountdown.set((int) getSpawner().getPrimaryWeaponSpeed()); else - attackCountdown.set((int) spawner.getSecondaryWeaponSpeed()); + attackCountdown.set((int) getSpawner().getSecondaryWeaponSpeed()); } } @@ -161,15 +149,8 @@ public class NpcCombatMode extends NpcMode { @Nullable private CreatureObject getPrimaryTarget() { - if (spawner.isDeathblow()) { - return targets.stream() - .filter(creo -> creo.getHealth() > 0) // Don't attack if they're already dead - .min(Comparator.comparingInt(CreatureObject::getHealth)).orElse(null); - } - return targets.stream() .filter(creo -> creo.getHealth() > 0) // Don't attack if they're already dead - .filter(creo -> !creo.hasBuff("incapWeaken")) // Don't attack if it'll be a DB .min(Comparator.comparingInt(CreatureObject::getHealth)).orElse(null); } @@ -184,7 +165,7 @@ public class NpcCombatMode extends NpcMode { private void requestAssistance() { Location myLocation = getAI().getWorldLocation(); - double assistRange = spawner.getAssistRadius(); + double assistRange = getSpawner().getAssistRadius(); getAI().getAware().stream() .filter(AIObject.class::isInstance) // get nearby AI .filter(ai -> ai.getWorldLocation().flatDistanceTo(myLocation) < assistRange) // that can assist diff --git a/src/main/java/com/projectswg/holocore/resources/support/npc/spawn/NPCCreator.java b/src/main/java/com/projectswg/holocore/resources/support/npc/spawn/NPCCreator.java index b45fba1e5..b913cb501 100644 --- a/src/main/java/com/projectswg/holocore/resources/support/npc/spawn/NPCCreator.java +++ b/src/main/java/com/projectswg/holocore/resources/support/npc/spawn/NPCCreator.java @@ -58,6 +58,7 @@ public class NPCCreator { public static long createNPC(Spawner spawner) { AIObject object = ObjectCreator.createObjectFromTemplate(spawner.getRandomIffTemplate(), AIObject.class); + object.setSpawner(spawner); object.setLocation(behaviorLocation(spawner)); object.setObjectName(spawner.getName()); object.setLevel(spawner.getCombatLevel()); @@ -110,7 +111,7 @@ public class NPCCreator { case ATTACKABLE: creature.setPvpFlags(PvpFlag.ATTACKABLE); creature.addOptionFlags(OptionFlag.HAM_BAR); - creature.setCombatMode(new NpcCombatMode(spawner)); + creature.setCombatMode(new NpcCombatMode()); break; case INVULNERABLE: creature.addOptionFlags(OptionFlag.INVULNERABLE); diff --git a/src/main/java/com/projectswg/holocore/resources/support/npc/spawn/Spawner.java b/src/main/java/com/projectswg/holocore/resources/support/npc/spawn/Spawner.java index 2b375cf45..bedbc3ae5 100644 --- a/src/main/java/com/projectswg/holocore/resources/support/npc/spawn/Spawner.java +++ b/src/main/java/com/projectswg/holocore/resources/support/npc/spawn/Spawner.java @@ -79,7 +79,8 @@ public final class Spawner { if (spawn.getPatrolId() < 1000) { this.waypoints = null; } else { - this.waypoints = DataLoader.npcPatrolRoutes().getPatrolRoute(spawn.getPatrolId()).stream().map(ResolvedPatrolWaypoint::new).collect(Collectors.toList()); + List waypoints = Objects.requireNonNull(DataLoader.npcPatrolRoutes().getPatrolRoute(spawn.getPatrolId()), "Invalid patrol route: " + spawn.getPatrolId()); + this.waypoints = waypoints.stream().map(ResolvedPatrolWaypoint::new).collect(Collectors.toList()); } this.egg = Objects.requireNonNull(egg, "egg"); this.random = new Random(); diff --git a/src/main/java/com/projectswg/holocore/resources/support/objects/swg/custom/AIObject.java b/src/main/java/com/projectswg/holocore/resources/support/objects/swg/custom/AIObject.java index 91ed027e2..efa6759d9 100644 --- a/src/main/java/com/projectswg/holocore/resources/support/objects/swg/custom/AIObject.java +++ b/src/main/java/com/projectswg/holocore/resources/support/objects/swg/custom/AIObject.java @@ -29,6 +29,7 @@ package com.projectswg.holocore.resources.support.objects.swg.custom; import com.projectswg.common.data.encodables.tangible.Posture; import com.projectswg.common.data.encodables.tangible.PvpFlag; import com.projectswg.common.network.packets.swg.zone.baselines.Baseline.BaselineType; +import com.projectswg.holocore.resources.support.npc.spawn.Spawner; import com.projectswg.holocore.resources.support.objects.ObjectCreator; import com.projectswg.holocore.resources.support.objects.swg.SWGObject; import com.projectswg.holocore.resources.support.objects.swg.creature.CreatureObject; @@ -49,6 +50,7 @@ public class AIObject extends CreatureObject { private final EnumMap modes; private final SWGObject hiddenInventory; + private Spawner spawner; private ScheduledThreadPool executor; private ScheduledFuture previousScheduled; private ScheduledMode mode; @@ -62,6 +64,7 @@ public class AIObject extends CreatureObject { this.modes = new EnumMap<>(ScheduledMode.class); this.hiddenInventory = ObjectCreator.createObjectFromTemplate("object/tangible/inventory/shared_character_inventory.iff"); + this.spawner = null; this.executor = null; this.previousScheduled = null; this.mode = ScheduledMode.DEFAULT; @@ -105,6 +108,10 @@ public class AIObject extends CreatureObject { return (theirPosture != Posture.INCAPACITATED || hasPvpFlag(PvpFlag.AGGRESSIVE)) && theirPosture != Posture.DEAD; } + public void setSpawner(Spawner spawner) { + this.spawner = spawner; + } + public void addPrimaryWeapon(WeaponObject weapon) { this.primaryWeapons.add(weapon); weapon.systemMove(hiddenInventory); @@ -124,6 +131,10 @@ public class AIObject extends CreatureObject { super.setEquippedWeapon(weapon); } + public Spawner getSpawner() { + return spawner; + } + public List getPrimaryWeapons() { return Collections.unmodifiableList(primaryWeapons); } diff --git a/src/main/java/com/projectswg/holocore/resources/support/objects/swg/custom/NpcMode.java b/src/main/java/com/projectswg/holocore/resources/support/objects/swg/custom/NpcMode.java index 3df1b545d..5e1d3ccba 100644 --- a/src/main/java/com/projectswg/holocore/resources/support/objects/swg/custom/NpcMode.java +++ b/src/main/java/com/projectswg/holocore/resources/support/objects/swg/custom/NpcMode.java @@ -2,6 +2,7 @@ package com.projectswg.holocore.resources.support.objects.swg.custom; import com.projectswg.common.data.location.Location; import com.projectswg.holocore.intents.support.objects.swg.MoveObjectIntent; +import com.projectswg.holocore.resources.support.npc.spawn.Spawner; import com.projectswg.holocore.resources.support.objects.swg.SWGObject; import com.projectswg.holocore.resources.support.objects.swg.creature.CreatureObject; import com.projectswg.holocore.resources.support.objects.swg.custom.AIObject.ScheduledMode; @@ -88,6 +89,10 @@ public abstract class NpcMode { return random; } + public final Spawner getSpawner() { + return obj.getSpawner(); + } + @Nullable public final NpcMode requestPeerMode(AIObject obj) { return obj.getMode(mode); diff --git a/src/main/java/com/projectswg/holocore/services/gameplay/combat/CombatDeathblowService.java b/src/main/java/com/projectswg/holocore/services/gameplay/combat/CombatDeathblowService.java index 4724b4ea3..1eb65bfb2 100644 --- a/src/main/java/com/projectswg/holocore/services/gameplay/combat/CombatDeathblowService.java +++ b/src/main/java/com/projectswg/holocore/services/gameplay/combat/CombatDeathblowService.java @@ -7,6 +7,7 @@ import com.projectswg.holocore.intents.gameplay.combat.*; import com.projectswg.holocore.intents.gameplay.combat.buffs.BuffIntent; import com.projectswg.holocore.intents.support.global.chat.SystemMessageIntent; import com.projectswg.holocore.resources.support.objects.swg.creature.CreatureObject; +import com.projectswg.holocore.resources.support.objects.swg.custom.AIObject; import me.joshlarson.jlcommon.concurrency.ScheduledThreadPool; import me.joshlarson.jlcommon.control.IntentHandler; import me.joshlarson.jlcommon.control.Service; @@ -91,11 +92,16 @@ public class CombatDeathblowService extends Service { corpse.setHealth(0); corpse.setTurnScale(0); corpse.setMovementScale(0); + CreatureObject killer = rcdi.getKiller(); - if (corpse.isPlayer() && !corpse.hasBuff("incapWeaken")) { - incapacitatePlayer(rcdi.getKiller(), corpse); - } else { + boolean deathblow = !corpse.isPlayer() || corpse.hasBuff("incapWeaken"); + if (!deathblow && killer instanceof AIObject) + deathblow = ((AIObject) killer).getSpawner().isDeathblow(); + + if (deathblow) { killCreature(rcdi.getKiller(), corpse); + } else { + incapacitatePlayer(rcdi.getKiller(), corpse); } ExitCombatIntent.broadcast(corpse); diff --git a/src/main/java/com/projectswg/holocore/services/support/npc/spawn/SpawnerService.java b/src/main/java/com/projectswg/holocore/services/support/npc/spawn/SpawnerService.java index 64d9f5e03..f0a705574 100644 --- a/src/main/java/com/projectswg/holocore/services/support/npc/spawn/SpawnerService.java +++ b/src/main/java/com/projectswg/holocore/services/support/npc/spawn/SpawnerService.java @@ -27,7 +27,6 @@ package com.projectswg.holocore.services.support.npc.spawn; import com.projectswg.common.data.location.Location; -import com.projectswg.holocore.intents.support.data.config.ConfigChangedIntent; import com.projectswg.holocore.intents.support.objects.swg.DestroyObjectIntent; import com.projectswg.holocore.intents.support.objects.swg.ObjectCreatedIntent; import com.projectswg.holocore.resources.support.data.config.ConfigFile; @@ -52,17 +51,13 @@ import me.joshlarson.jlcommon.control.IntentHandler; import me.joshlarson.jlcommon.control.Service; import me.joshlarson.jlcommon.log.Log; -import java.util.HashMap; import java.util.List; -import java.util.Map; public final class SpawnerService extends Service { - private final Map spawnerMap; private final ScheduledThreadPool executor; public SpawnerService() { - this.spawnerMap = new HashMap<>(); this.executor = new ScheduledThreadPool(1, "spawner-service"); } @@ -82,71 +77,45 @@ public final class SpawnerService extends Service { return true; } - @IntentHandler - private void handleConfigChangedIntent(ConfigChangedIntent cci) { - String newValue, oldValue; - - if (cci.getChangedConfig().equals(ConfigFile.FEATURES) && cci.getKey().equals("SPAWN-EGGS-ENABLED")) { - newValue = cci.getNewValue(); - oldValue = cci.getOldValue(); - - if (!newValue.equals(oldValue)) { - if (Boolean.valueOf(newValue) && spawnerMap.isEmpty()) { // If nothing's been spawned, create it. - loadSpawners(); - } else { // If anything's been spawned, delete it. - removeSpawners(); - } - } - } - } - @IntentHandler private void handleDestroyObjectIntent(DestroyObjectIntent doi) { SWGObject destroyedObject = doi.getObject(); if (!(destroyedObject instanceof AIObject)) return; - Spawner spawner = spawnerMap.remove(destroyedObject.getObjectId()); + Spawner spawner = ((AIObject) destroyedObject).getSpawner(); if (spawner == null) { Log.e("Killed AI object %s has no linked Spawner - it cannot respawn!", destroyedObject); return; } - executor.execute(spawner.getRespawnDelay() * 1000, () -> spawnNPC(spawner)); - } - - private void removeSpawners() { - spawnerMap.values().forEach(spawner -> new DestroyObjectIntent(spawner.getEgg()).broadcast()); - spawnerMap.clear(); - } - - private void spawnNPC(Spawner spawner) { - spawnerMap.put(NPCCreator.createNPC(spawner), spawner); + executor.execute(spawner.getRespawnDelay() * 1000, () -> NPCCreator.createNPC(spawner)); } private void loadSpawners() { long startTime = StandardLog.onStartLoad("spawners"); List forceNoGC = List.of(DataLoader.npcs(), DataLoader.npcWeapons(), DataLoader.npcStaticSpawns(), DataLoader.npcStats(), DataLoader.npcPatrolRoutes()); + int count = 0; for (StaticSpawnInfo spawn : DataLoader.npcStaticSpawns().getSpawns()) { try { spawn(spawn); + count++; } catch (Throwable t) { Log.e("Failed to load spawner. %s: %s", t.getClass().getName(), t.getMessage()); - Log.e(t); } } createPatrolRouteWaypoints(); - StandardLog.onEndLoad(spawnerMap.size(), "spawners", startTime); + StandardLog.onEndLoad(count, "spawners", startTime); } private void spawn(StaticSpawnInfo spawn) { Spawner spawner = new Spawner(spawn, createEgg(spawn)); for (int i = 0; i < spawner.getAmount(); i++) { - spawnNPC(spawner); + NPCCreator.createNPC(spawner); } }