mirror of
https://github.com/ProjectSWGCore/Holocore.git
synced 2026-01-17 00:06:00 -05:00
Dynamic spawns now create NPCs #150
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user