Dynamic spawns now create NPCs #150

This commit is contained in:
Ziggy
2020-11-22 21:06:05 +01:00
parent abb882aa62
commit 2155bb0b14
3 changed files with 132 additions and 136 deletions

View File

@@ -1,7 +1,35 @@
/***********************************************************************************
* Copyright (c) 2018 /// Project SWG /// www.projectswg.com *
* *
* ProjectSWG is the first NGE emulator for Star Wars Galaxies founded on *
* July 7th, 2011 after SOE announced the official shutdown of Star Wars Galaxies. *
* Our goal is to create an emulator which will provide a server for players to *
* continue playing a game similar to the one they used to play. We are basing *
* it on the final publish of the game prior to end-game events. *
* *
* This file is part of PSWGCommon. *
* *
* --------------------------------------------------------------------------------*
* *
* PSWGCommon is free software: you can redistribute it and/or modify *
* it under the terms of the GNU Affero General Public License as *
* published by the Free Software Foundation, either version 3 of the *
* License, or (at your option) any later version. *
* *
* PSWGCommon is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU Affero General Public License for more details. *
* *
* You should have received a copy of the GNU Affero General Public License *
* along with PSWGCommon. If not, see <http://www.gnu.org/licenses/>. *
***********************************************************************************/
package com.projectswg.holocore.resources.support.data.server_info.loader;
import com.projectswg.common.data.location.Terrain;
import com.projectswg.holocore.resources.support.data.server_info.SdbLoader;
import com.projectswg.holocore.resources.support.data.server_info.loader.npc.NpcStaticSpawnLoader;
import me.joshlarson.jlcommon.log.Log;
import org.jetbrains.annotations.NotNull;
import java.io.File;
@@ -47,33 +75,40 @@ public final class DynamicSpawnLoader extends DataLoader {
public static class DynamicSpawnInfo {
private String dynamicId;
private String lairTemplate;
private String npcBoss;
private String npcElite;
private String npcNormal1;
private String npcNormal2;
private String npcNormal3;
private String npcNormal4;
private final NpcStaticSpawnLoader.SpawnerFlag spawnerFlag;
public DynamicSpawnInfo(SdbLoader.SdbResultSet set) {
this.dynamicId = set.getText("dynamic_id");
this.lairTemplate = set.getText("lair_type");
this.npcBoss = set.getText("npc_boss");
this.npcElite = set.getText("npc_elite");
this.npcNormal1 = set.getText("npc_normal_1");
this.npcNormal2 = set.getText("npc_normal_2");
this.npcNormal3 = set.getText("npc_normal_3");
this.npcNormal4 = set.getText("npc_normal_4");
this.spawnerFlag = readSpawnerFlag(dynamicId, set);
}
private NpcStaticSpawnLoader.SpawnerFlag readSpawnerFlag(String id, SdbLoader.SdbResultSet set) {
String columnName = "attackable";
try {
return NpcStaticSpawnLoader.SpawnerFlag.valueOf(set.getText(columnName));
} catch (IllegalArgumentException e) {
Log.w("Unknown attackable flag for dynamic_id '%s': '%s'", id, set.getText(columnName));
return NpcStaticSpawnLoader.SpawnerFlag.INVULNERABLE;
}
}
public String getDynamicId() {
return dynamicId;
}
public String getLairTemplate() {
return lairTemplate;
}
public String getNpcBoss() {
return npcBoss;
}
@@ -97,5 +132,9 @@ public final class DynamicSpawnLoader extends DataLoader {
public String getNpcNormal4() {
return npcNormal4;
}
public NpcStaticSpawnLoader.SpawnerFlag getSpawnerFlag() {
return spawnerFlag;
}
}
}

View File

@@ -190,6 +190,7 @@ public class SimpleSpawnInfo implements SpawnInfo {
info.amount = 1;
info.minSpawnTime = (int) TimeUnit.SECONDS.convert(8, TimeUnit.MINUTES);
info.maxSpawnTime = (int) TimeUnit.SECONDS.convert(12, TimeUnit.MINUTES);
info.loiterRadius = 15;
}
public Builder withNpcId(String npcId) {
@@ -257,6 +258,12 @@ public class SimpleSpawnInfo implements SpawnInfo {
return this;
}
public Builder withBehavior(AIBehavior behavior) {
info.behavior = behavior;
return this;
}
public SimpleSpawnInfo build() {
return info;
}

View File

@@ -1,95 +1,81 @@
/***********************************************************************************
* Copyright (c) 2018 /// Project SWG /// www.projectswg.com *
* *
* ProjectSWG is the first NGE emulator for Star Wars Galaxies founded on *
* July 7th, 2011 after SOE announced the official shutdown of Star Wars Galaxies. *
* Our goal is to create an emulator which will provide a server for players to *
* continue playing a game similar to the one they used to play. We are basing *
* it on the final publish of the game prior to end-game events. *
* *
* This file is part of PSWGCommon. *
* *
* --------------------------------------------------------------------------------*
* *
* PSWGCommon is free software: you can redistribute it and/or modify *
* it under the terms of the GNU Affero General Public License as *
* published by the Free Software Foundation, either version 3 of the *
* License, or (at your option) any later version. *
* *
* PSWGCommon is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU Affero General Public License for more details. *
* *
* You should have received a copy of the GNU Affero General Public License *
* along with PSWGCommon. If not, see <http://www.gnu.org/licenses/>. *
***********************************************************************************/
package com.projectswg.holocore.services.support.npc.spawn;
import com.projectswg.common.data.location.Location;
import com.projectswg.common.data.location.Terrain;
import com.projectswg.holocore.intents.gameplay.world.spawn.CreateSpawnIntent;
import com.projectswg.holocore.intents.support.global.zone.PlayerTransformedIntent;
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.location.ClosestLocationReducer;
import com.projectswg.holocore.resources.support.data.server_info.StandardLog;
import com.projectswg.holocore.resources.support.data.server_info.loader.DynamicSpawnLoader;
import com.projectswg.holocore.resources.support.data.server_info.loader.NoSpawnZoneLoader;
import com.projectswg.holocore.resources.support.data.server_info.loader.ServerData;
import com.projectswg.holocore.resources.support.data.server_info.loader.TerrainLevelLoader;
import com.projectswg.holocore.resources.support.data.server_info.loader.npc.NpcStaticSpawnLoader;
import com.projectswg.holocore.resources.support.data.server_info.mongodb.PswgDatabase;
import com.projectswg.holocore.resources.support.global.player.Player;
import com.projectswg.holocore.resources.support.data.location.ClosestLocationReducer;
import com.projectswg.holocore.resources.support.objects.ObjectCreator;
import com.projectswg.holocore.resources.support.objects.swg.SWGObject;
import com.projectswg.holocore.resources.support.npc.spawn.SimpleSpawnInfo;
import com.projectswg.holocore.resources.support.npc.spawn.SpawnerType;
import com.projectswg.holocore.resources.support.objects.swg.creature.CreatureDifficulty;
import com.projectswg.holocore.resources.support.objects.swg.creature.CreatureObject;
import com.projectswg.holocore.resources.support.objects.swg.tangible.TangibleObject;
import me.joshlarson.jlcommon.concurrency.ScheduledThreadPool;
import com.projectswg.holocore.resources.support.objects.swg.custom.AIBehavior;
import me.joshlarson.jlcommon.control.IntentHandler;
import me.joshlarson.jlcommon.control.Service;
import me.joshlarson.jlcommon.log.Log;
import org.jetbrains.annotations.Nullable;
import java.util.*;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Optional;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
public class DynamicSpawnService extends Service {
private static final int SPAWN_DISTANCE_TO_PLAYER = 70; // Spawner is created 70m away from the player and NPCs are spawned around the spawner
private static final String EGG_TEMPLATE = "object/path_waypoint/shared_path_waypoint_patrol.iff";
private static final int SPAWN_DISTANCE_TO_PLAYER = 70; // Spawner is created 70m away from the player and NPCs are spawned around the spawner
private static final SpawnerType SPAWNER_TYPE = SpawnerType.RANDOM;
private final DynamicSpawnLoader dynamicSpawnLoader;
private final NoSpawnZoneLoader noSpawnZoneLoader;
private final TerrainLevelLoader terrainLevelLoader;
private final Map<Terrain, Collection<ActiveSpawn>> activeSpawnMap;
private final long destroyTimerMs;
private final long eggsPerArea;
private final ScheduledThreadPool executor;
private final long spawnsPerArea;
public DynamicSpawnService() {
dynamicSpawnLoader = ServerData.INSTANCE.getDynamicSpawns();
noSpawnZoneLoader = ServerData.INSTANCE.getNoSpawnZones();
terrainLevelLoader = ServerData.INSTANCE.getTerrainLevels();
activeSpawnMap = Collections.synchronizedMap(new HashMap<>());
long destroyTimer = PswgDatabase.INSTANCE.getConfig().getLong(this, "destroyTimer", 600); // Dynamic NPCs are despawned after 10 mins of inactivity
destroyTimerMs = TimeUnit.MILLISECONDS.convert(destroyTimer, TimeUnit.SECONDS);
eggsPerArea = PswgDatabase.INSTANCE.getConfig().getLong(this, "eggsPerArea", 4); // Amount of spawns in an area
executor = new ScheduledThreadPool(1, "dynamic-spawn-service");
}
@Override
public boolean start() {
long checkRate = 1000; // Attempt to delete old NPCs every 1000ms
executor.start();
executor.executeWithFixedRate(checkRate, checkRate, this::destroyOldNpcs);
return super.start();
}
@Override
public boolean stop() {
executor.stop();
return super.stop() && executor.awaitTermination(1000);
spawnsPerArea = PswgDatabase.INSTANCE.getConfig().getLong(this, "eggsPerArea", 4) * 3; // Amount of spawns in an area
}
@IntentHandler
private void handlePlayerTransformed(PlayerTransformedIntent intent) {
Location location = intent.getNewLocation();
updateTimestamps(location);
spawnNewNpcs(intent.getPlayer(), location);
}
private void updateTimestamps(Location location) {
Terrain terrain = location.getTerrain();
Collection<ActiveSpawn> activeSpawns = activeSpawnMap.get(terrain);
if (activeSpawns == null || activeSpawns.isEmpty()) {
// No active spawns for this terrain. Do nothing.
return;
}
for (ActiveSpawn activeSpawn : activeSpawns) {
SWGObject spawnerObject = activeSpawn.getEggObject();
Set<Player> observers = spawnerObject.getObservers();
if (!observers.isEmpty()) {
activeSpawn.setLastSeenTS(System.currentTimeMillis());
}
}
}
private void spawnNewNpcs(CreatureObject player, Location location) {
Terrain terrain = location.getTerrain();
Collection<DynamicSpawnLoader.DynamicSpawnInfo> spawnInfos = dynamicSpawnLoader.getSpawnInfos(terrain);
@@ -116,12 +102,8 @@ public class DynamicSpawnService extends Service {
if (!noSpawnZoneInfos.isEmpty()) {
Optional<Location> closestZoneOpt = noSpawnZoneInfos.stream()
.map(noSpawnZoneInfo -> Location.builder()
.setX(noSpawnZoneInfo.getX())
.setZ(noSpawnZoneInfo.getZ())
.setTerrain(location.getTerrain())
.build())
.reduce(new ClosestLocationReducer(location));
.map(noSpawnZoneInfo -> Location.builder().setX(noSpawnZoneInfo.getX()).setZ(noSpawnZoneInfo.getZ())
.setTerrain(location.getTerrain()).build()).reduce(new ClosestLocationReducer(location));
Location closestZoneLocation = closestZoneOpt.get();
@@ -133,11 +115,11 @@ public class DynamicSpawnService extends Service {
}
}
long nearbyEggs = player.getAware().stream()
.filter(swgObject -> EGG_TEMPLATE.equals(swgObject.getTemplate()))
.count();
String eggTemplate = SPAWNER_TYPE.getObjectTemplate();
if (nearbyEggs >= eggsPerArea) {
long nearbyEggs = player.getAware().stream().filter(swgObject -> eggTemplate.equals(swgObject.getTemplate())).count();
if (nearbyEggs >= spawnsPerArea) {
// Plenty spawns near this player already - do nothing
return;
}
@@ -148,82 +130,50 @@ public class DynamicSpawnService extends Service {
boolean usePositiveDirectionZ = random.nextBoolean();
double eggX = (usePositiveDirectionX ? SPAWN_DISTANCE_TO_PLAYER : -SPAWN_DISTANCE_TO_PLAYER) + location.getX();
double eggZ = (usePositiveDirectionZ ? SPAWN_DISTANCE_TO_PLAYER : -SPAWN_DISTANCE_TO_PLAYER) + location.getZ();
SWGObject eggObject = ObjectCreator.createObjectFromTemplate(EGG_TEMPLATE);
Location eggLocation = Location.builder(location)
.setX(eggX)
.setZ(eggZ)
.build(); // TODO y parameter should be set and calculated based on X and Z in relevant terrain. Currently they'll spawn in the air or below ground.
eggObject.moveToContainer(null, eggLocation); // Spawn egg in the world
ObjectCreatedIntent.broadcast(eggObject);
Location eggLocation = Location.builder(location).setX(eggX).setZ(eggZ)
.build(); // TODO y parameter should be set and calculated based on X and Z in relevant terrain. Currently they'll spawn in the air or below ground.
int randomSpawnInfoIndex = random.nextInt(0, spawnInfos.size());
DynamicSpawnLoader.DynamicSpawnInfo spawnInfo = new ArrayList<>(spawnInfos).get(randomSpawnInfoIndex);
eggObject.setObjectName(spawnInfo.getDynamicId());
long minLevel = terrainLevelInfo.getMinLevel();
long maxLevel = terrainLevelInfo.getMaxLevel();
int minLevel = (int) terrainLevelInfo.getMinLevel();
int maxLevel = (int) terrainLevelInfo.getMaxLevel();
// TODO spawn (loitering?) NPCs within the terrain level range up to 32m away from the egg
NpcStaticSpawnLoader.SpawnerFlag spawnerFlag = spawnInfo.getSpawnerFlag();
Collection<ActiveSpawn> terrainActiveSpawns = activeSpawnMap.computeIfAbsent(terrain, k -> new ArrayList<>());
terrainActiveSpawns.add(new ActiveSpawn(eggObject, Collections.emptyList()));
StandardLog.onPlayerEvent(this, player, "Spawning %s", spawnInfo.getDynamicId());
spawn(randomNpc(spawnInfo.getNpcBoss()), CreatureDifficulty.BOSS, spawnerFlag, minLevel, maxLevel, eggLocation);
spawn(randomNpc(spawnInfo.getNpcElite()), CreatureDifficulty.ELITE, spawnerFlag, minLevel, maxLevel, eggLocation);
spawn(randomNpc(spawnInfo.getNpcNormal1()), CreatureDifficulty.NORMAL, spawnerFlag, minLevel, maxLevel, eggLocation);
spawn(randomNpc(spawnInfo.getNpcNormal2()), CreatureDifficulty.NORMAL, spawnerFlag, minLevel, maxLevel, eggLocation);
spawn(randomNpc(spawnInfo.getNpcNormal3()), CreatureDifficulty.NORMAL, spawnerFlag, minLevel, maxLevel, eggLocation);
spawn(randomNpc(spawnInfo.getNpcNormal4()), CreatureDifficulty.NORMAL, spawnerFlag, minLevel, maxLevel, eggLocation);
}
private void destroyOldNpcs() {
Collection<Collection<ActiveSpawn>> globalActiveSpawns = activeSpawnMap.values();
for (Collection<ActiveSpawn> activeSpawns : globalActiveSpawns) {
for (ActiveSpawn activeSpawn : new ArrayList<>(activeSpawns)) {
long lastSeenTS = activeSpawn.getLastSeenTS();
long nowTS = System.currentTimeMillis();
long delta = nowTS - lastSeenTS;
SWGObject eggObject = activeSpawn.getEggObject();
boolean noPlayersNearby = !eggObject.getObservers().isEmpty();
if (delta >= destroyTimerMs && noPlayersNearby) {
// It's been too long since an active player last saw this spawn and no player is nearby - destroy it
if (activeSpawns.remove(activeSpawn)) {
Collection<TangibleObject> npcs = activeSpawn.getNpcs();
DestroyObjectIntent.broadcast(eggObject);
for (TangibleObject npc : npcs) {
DestroyObjectIntent.broadcast(npc);
}
Log.d("Destroyed inactive dynamic spawn at " + eggObject.getWorldLocation());
}
}
}
private void spawn(String npcId, CreatureDifficulty difficulty, NpcStaticSpawnLoader.SpawnerFlag spawnerFlag, int minLevel, int maxLevel, Location location) {
if (npcId == null) {
return;
}
SimpleSpawnInfo simpleSpawnInfo = SimpleSpawnInfo.builder().withNpcId(npcId).withDifficulty(difficulty).withSpawnerType(SpawnerType.RANDOM)
.withMinLevel(minLevel).withMaxLevel(maxLevel).withSpawnerFlag(spawnerFlag).withBehavior(AIBehavior.LOITER).withLocation(location)
.build();
CreateSpawnIntent.broadcast(simpleSpawnInfo);
}
private static class ActiveSpawn {
private final SWGObject eggObject;
private final Collection<TangibleObject> npcs;
private long lastSeenTS; // Timestamp in millis for when this object was last viewed by a player
public ActiveSpawn(SWGObject eggObject, Collection<TangibleObject> npcs) {
this.eggObject = eggObject;
this.npcs = npcs;
lastSeenTS = System.currentTimeMillis();
@Nullable
private String randomNpc(String npcString) {
if (npcString.isEmpty()) {
return null;
}
public SWGObject getEggObject() {
return eggObject;
}
String[] npcIds = npcString.split(";");
int npcIdCount = npcIds.length;
ThreadLocalRandom random = ThreadLocalRandom.current();
int randomIdx = random.nextInt(0, npcIdCount);
public Collection<TangibleObject> getNpcs() {
return npcs;
}
public long getLastSeenTS() {
return lastSeenTS;
}
public void setLastSeenTS(long lastSeenTS) {
this.lastSeenTS = lastSeenTS;
}
return npcIds[randomIdx];
}
}