diff --git a/data/conf/zones.xsd b/data/conf/zones.xsd
index 1fd34be1f91..569b5553765 100644
--- a/data/conf/zones.xsd
+++ b/data/conf/zones.xsd
@@ -25,6 +25,7 @@
maxOccurs="unbounded"/>
+
@@ -178,4 +179,27 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/doc/CHANGES.txt b/doc/CHANGES.txt
index 0782d4d184c..03b4eb0ae12 100644
--- a/doc/CHANGES.txt
+++ b/doc/CHANGES.txt
@@ -46,6 +46,7 @@ Changelog
- new achievements:
- 30 Minutes or Less: Deliver 25 hot pizzas
- Balduin allows requesting different item for Ultimate Collector quest
+- support added for dynamic creature spawning
*web client*
- fixed text extending past edge of notification bubbles
diff --git a/src/games/stendhal/client/entity/factory/EntityMap.java b/src/games/stendhal/client/entity/factory/EntityMap.java
index fed4c152be2..e9a8c09a6f7 100644
--- a/src/games/stendhal/client/entity/factory/EntityMap.java
+++ b/src/games/stendhal/client/entity/factory/EntityMap.java
@@ -1,5 +1,5 @@
/***************************************************************************
- * (C) Copyright 2003-2023 - Marauroa *
+ * (C) Copyright 2003-2024 - Marauroa *
***************************************************************************
***************************************************************************
* *
@@ -17,7 +17,6 @@
import org.apache.log4j.Logger;
-import games.stendhal.client.Triple;
import games.stendhal.client.entity.Block;
import games.stendhal.client.entity.Blood;
import games.stendhal.client.entity.BossCreature;
@@ -53,6 +52,7 @@
import games.stendhal.client.entity.UseableRing;
import games.stendhal.client.entity.WalkBlocker;
import games.stendhal.client.entity.Wall;
+import games.stendhal.common.Triple;
/**
* Registers the relationship between Type, eclass and java class of entity
diff --git a/src/games/stendhal/client/gui/j2d/entity/EntityViewFactory.java b/src/games/stendhal/client/gui/j2d/entity/EntityViewFactory.java
index 758a1c02adf..f04686ae778 100644
--- a/src/games/stendhal/client/gui/j2d/entity/EntityViewFactory.java
+++ b/src/games/stendhal/client/gui/j2d/entity/EntityViewFactory.java
@@ -1,6 +1,6 @@
/* $Id$ */
/***************************************************************************
- * (C) Copyright 2003-2023 - Stendhal *
+ * (C) Copyright 2003-2024 - Stendhal *
***************************************************************************
***************************************************************************
* *
@@ -17,9 +17,9 @@
import org.apache.log4j.Logger;
-import games.stendhal.client.Triple;
import games.stendhal.client.entity.IEntity;
import games.stendhal.client.gui.wt.core.WtWindowManager;
+import games.stendhal.common.Triple;
/*
* The entity views are generic, but we don't simply have sufficient data to
diff --git a/src/games/stendhal/client/Triple.java b/src/games/stendhal/common/Triple.java
similarity index 96%
rename from src/games/stendhal/client/Triple.java
rename to src/games/stendhal/common/Triple.java
index 3736ec76d4d..2bbd80e75e2 100644
--- a/src/games/stendhal/client/Triple.java
+++ b/src/games/stendhal/common/Triple.java
@@ -1,6 +1,6 @@
/* $Id$ */
/***************************************************************************
- * (C) Copyright 2003-2023 - Stendhal *
+ * (C) Copyright 2003-2024 - Stendhal *
***************************************************************************
***************************************************************************
* *
@@ -10,7 +10,7 @@
* (at your option) any later version. *
* *
***************************************************************************/
-package games.stendhal.client;
+package games.stendhal.common;
import java.util.Objects;
diff --git a/src/games/stendhal/server/core/config/ZonesXMLLoader.java b/src/games/stendhal/server/core/config/ZonesXMLLoader.java
index c199e83f749..118fad77b53 100644
--- a/src/games/stendhal/server/core/config/ZonesXMLLoader.java
+++ b/src/games/stendhal/server/core/config/ZonesXMLLoader.java
@@ -35,6 +35,7 @@
import games.stendhal.common.tiled.StendhalMapStructure;
import games.stendhal.server.core.config.zone.AttributesXMLReader;
import games.stendhal.server.core.config.zone.ConfiguratorXMLReader;
+import games.stendhal.server.core.config.zone.CreatureSpawnsXMLReader;
import games.stendhal.server.core.config.zone.EntitySetupXMLReader;
import games.stendhal.server.core.config.zone.PortalSetupXMLReader;
import games.stendhal.server.core.config.zone.RegionNameSubstitutionHelper;
@@ -57,6 +58,8 @@ public final class ZonesXMLLoader {
/** Zone attributes reader. */
private static final SetupXMLReader attributesReader = new AttributesXMLReader();
+ /** Dynamically spawned creatures. */
+ private static final SetupXMLReader creatureSpawnsReader = new CreatureSpawnsXMLReader();
/**
* The ConfiguratorDescriptor XML reader.
*/
@@ -416,6 +419,13 @@ public ZoneDesc readZone(final Element element) {
continue;
} else if (tag.equals("associated")) {
desc.setAssociatedZones(child.getAttribute("zones"));
+ } else if (tag.equals("spawns")) {
+ for (final Element spawn: XMLUtil.getElements(child)) {
+ final String spawnType = spawn.getTagName();
+ if ("creatures".equals(spawnType)) {
+ setupDesc = creatureSpawnsReader.read(spawn);
+ }
+ }
} else {
logger.warn("Zone [" + name + "] has unknown element: " + tag);
continue;
diff --git a/src/games/stendhal/server/core/config/zone/CreatureSpawnsXMLReader.java b/src/games/stendhal/server/core/config/zone/CreatureSpawnsXMLReader.java
new file mode 100644
index 00000000000..e022d93c166
--- /dev/null
+++ b/src/games/stendhal/server/core/config/zone/CreatureSpawnsXMLReader.java
@@ -0,0 +1,56 @@
+/***************************************************************************
+ * Copyright © 2024 - Faiumoni e. V. *
+ ***************************************************************************
+ ***************************************************************************
+ * *
+ * This program is free software; you can redistribute it and/or modify *
+ * it under the terms of the GNU General Public License as published by *
+ * the Free Software Foundation; either version 2 of the License, or *
+ * (at your option) any later version. *
+ * *
+ ***************************************************************************/
+package games.stendhal.server.core.config.zone;
+
+import java.util.Map;
+
+import org.apache.log4j.Logger;
+import org.w3c.dom.Element;
+
+import games.stendhal.common.MathHelper;
+import games.stendhal.server.core.engine.StendhalRPZone;
+import games.stendhal.server.entity.mapstuff.spawner.DynamicCreatureSpawner;
+
+
+/**
+ * Parses info for dynamically spawned creatures.
+ */
+public class CreatureSpawnsXMLReader extends SetupXMLReader {
+
+ private static Logger logger = Logger.getLogger(CreatureSpawnsXMLReader.class);
+
+
+ @Override
+ public SetupDescriptor read(final Element element) {
+ final CreatureSpawnsDescriptor desc = new CreatureSpawnsDescriptor();
+ readParameters(desc, element);
+ return desc;
+ }
+
+ private static class CreatureSpawnsDescriptor extends SetupDescriptor {
+ @Override
+ public void setup(final StendhalRPZone zone) {
+ final DynamicCreatureSpawner spawner = new DynamicCreatureSpawner(zone);
+ final Map params = getParameters();
+ for (final String name: params.keySet()) {
+ final int max = MathHelper.parseIntDefault(params.get(name), 0);
+ if (max < 0) {
+ logger.warn("Max must be a positive integer, not registering dynamic spawn for creature \"" + name + "\"");
+ continue;
+ }
+ spawner.register(name, max);
+ }
+ // NOTE: does zone need to know about dynamic spawner?
+ zone.setDynamicSpawner(spawner);
+ }
+ }
+}
diff --git a/src/games/stendhal/server/core/engine/StendhalRPZone.java b/src/games/stendhal/server/core/engine/StendhalRPZone.java
index 99df957d232..c2314002068 100644
--- a/src/games/stendhal/server/core/engine/StendhalRPZone.java
+++ b/src/games/stendhal/server/core/engine/StendhalRPZone.java
@@ -65,6 +65,7 @@
import games.stendhal.server.entity.mapstuff.portal.OneWayPortalDestination;
import games.stendhal.server.entity.mapstuff.portal.Portal;
import games.stendhal.server.entity.mapstuff.spawner.CreatureRespawnPoint;
+import games.stendhal.server.entity.mapstuff.spawner.DynamicCreatureSpawner;
import games.stendhal.server.entity.mapstuff.spawner.PassiveEntityRespawnPoint;
import games.stendhal.server.entity.mapstuff.spawner.PassiveEntityRespawnPointFactory;
import games.stendhal.server.entity.mapstuff.spawner.SheepFood;
@@ -114,8 +115,12 @@ public class StendhalRPZone extends MarauroaRPZone {
*/
private final List sheepFoods;
+ /** Statically spawned creatures. */
private final List respawnPoints;
+ /** Dynamically spawned creatures. */
+ private DynamicCreatureSpawner dynamicSpawner;
+
private final List plantGrowers;
private final List playersAndFriends;
@@ -312,6 +317,30 @@ public void remove(final CreatureRespawnPoint point) {
respawnPoints.remove(point);
}
+ /**
+ * Retrieves the dynamic creature spawner associated with this zone.
+ *
+ * @return
+ * Creature spawner instance.
+ */
+ public DynamicCreatureSpawner getDynamicSpawner() {
+ return dynamicSpawner;
+ }
+
+ /**
+ * Sets the dynamic creature spawner associated with this zone.
+ *
+ * @param spawner
+ * Creature spawner instance.
+ */
+ public void setDynamicSpawner(final DynamicCreatureSpawner spawner) {
+ if (dynamicSpawner != null) {
+ dynamicSpawner.remove();
+ }
+ dynamicSpawner = spawner;
+ dynamicSpawner.init();
+ }
+
/**
* Retrieves growers in this zone.
*/
@@ -1534,6 +1563,7 @@ public void nextTurn() {
os.append("playersAndFriends: " + playersAndFriends.size() + "\n");
os.append("portals: " + portals.size() + "\n");
os.append("respawnPoints: " + respawnPoints.size() + "\n");
+ os.append("dynamicRespawns: " + dynamicSpawner.maxTotal() + "\n");
os.append("sheepFoods: " + sheepFoods.size() + "\n");
os.append("objects: " + objects.size() + "\n");
logger.info(os);
diff --git a/src/games/stendhal/server/core/rule/defaultruleset/DefaultCreature.java b/src/games/stendhal/server/core/rule/defaultruleset/DefaultCreature.java
index 8066baff781..32a6e0aa279 100644
--- a/src/games/stendhal/server/core/rule/defaultruleset/DefaultCreature.java
+++ b/src/games/stendhal/server/core/rule/defaultruleset/DefaultCreature.java
@@ -1,6 +1,6 @@
/* $Id$ */
/***************************************************************************
- * (C) Copyright 2003-2013 - Marauroa *
+ * (C) Copyright 2003-2024 - Marauroa *
***************************************************************************
***************************************************************************
* *
@@ -382,6 +382,16 @@ public int compare(final DropItem o1, final DropItem o2) {
return creature;
}
+ /**
+ * Creates a creature instance with randomized stats.
+ *
+ * @return
+ * New creature instance.
+ */
+ public Creature getCreatureRandomizeStats() {
+ return getCreature().getNewInstanceRandomizeStats();
+ }
+
/** @return the tileid. */
public String getTileId() {
return tileid;
diff --git a/src/games/stendhal/server/entity/creature/Creature.java b/src/games/stendhal/server/entity/creature/Creature.java
index ca58989cce3..d84c3dbc0dd 100644
--- a/src/games/stendhal/server/entity/creature/Creature.java
+++ b/src/games/stendhal/server/entity/creature/Creature.java
@@ -28,6 +28,7 @@
import games.stendhal.common.Rand;
import games.stendhal.common.constants.Nature;
import games.stendhal.common.constants.SoundLayer;
+import games.stendhal.common.constants.Testing;
import games.stendhal.server.core.engine.SingletonRepository;
import games.stendhal.server.core.engine.StendhalRPRuleProcessor;
import games.stendhal.server.core.engine.StendhalRPZone;
@@ -49,7 +50,7 @@
import games.stendhal.server.entity.item.Corpse;
import games.stendhal.server.entity.item.Item;
import games.stendhal.server.entity.item.StackableItem;
-import games.stendhal.server.entity.mapstuff.spawner.CreatureRespawnPoint;
+import games.stendhal.server.entity.mapstuff.spawner.CreatureSpawner;
import games.stendhal.server.entity.npc.NPC;
import games.stendhal.server.entity.player.Player;
import games.stendhal.server.entity.slot.EntitySlot;
@@ -122,7 +123,7 @@ public class Creature extends NPC {
private int corpseWidth = 1;
private int corpseHeight = 1;
- private CreatureRespawnPoint point;
+ private CreatureSpawner spawner;
/** Respawn time in turns */
private int respawnTime;
@@ -368,6 +369,24 @@ public Creature getNewInstance() {
return new Creature(this);
}
+ /**
+ * Creates a new creature with randomized stats using this instance as a template.
+ *
+ * @return
+ * New creature instance.
+ */
+ public Creature getNewInstanceRandomizeStats() {
+ final Creature newInstance = getNewInstance();
+ // A bit of randomization to make Joan and Snaketails a bit happier.
+ // :)
+ newInstance.setAtk(Rand.randGaussian(newInstance.getAtk(), newInstance.getAtk() / 10));
+ newInstance.setDef(Rand.randGaussian(newInstance.getDef(), newInstance.getDef() / 10));
+ if (Testing.COMBAT) {
+ newInstance.setRatk(Rand.randGaussian(newInstance.getRatk(), newInstance.getRatk() / 10));
+ }
+ return newInstance;
+ }
+
/**
* Sets the sound played at creature's death
*
@@ -528,18 +547,40 @@ public Map getAIProfiles() {
return aiProfiles;
}
- public void setRespawnPoint(final CreatureRespawnPoint point) {
- this.point = point;
+ /**
+ * Registers this creature with a spawner.
+ *
+ * @param spawner
+ * Spawner instance.
+ */
+ public void setSpawner(final CreatureSpawner spawner) {
+ this.spawner = spawner;
setRespawned(true);
}
+ @Deprecated
+ public void setRespawnPoint(final CreatureSpawner spawner) {
+ setSpawner(spawner);
+ }
+
+ /**
+ * Gets the spawner of this creature.
+ *
+ * @return
+ * Spawner instance.
+ */
+ public CreatureSpawner getRespawner() {
+ return spawner;
+ }
+
/**
* gets the respan point of this create
*
* @return CreatureRespawnPoint
*/
- public CreatureRespawnPoint getRespawnPoint() {
- return point;
+ @Deprecated
+ public CreatureSpawner getRespawnPoint() {
+ return getRespawner();
}
/**
@@ -664,8 +705,8 @@ public void onDead(final Killer killer, final boolean remove) {
notifyRegisteredObjects();
- if (this.point != null) {
- this.point.notifyDead(this);
+ if (this.spawner != null) {
+ this.spawner.onRemoved(this);
}
super.onDead(killer, remove);
diff --git a/src/games/stendhal/server/entity/mapstuff/spawner/CreatureRespawnPoint.java b/src/games/stendhal/server/entity/mapstuff/spawner/CreatureRespawnPoint.java
index 7d71e7fa533..56dfc1e427f 100644
--- a/src/games/stendhal/server/entity/mapstuff/spawner/CreatureRespawnPoint.java
+++ b/src/games/stendhal/server/entity/mapstuff/spawner/CreatureRespawnPoint.java
@@ -21,7 +21,6 @@
import games.stendhal.common.Rand;
import games.stendhal.server.core.engine.SingletonRepository;
import games.stendhal.server.core.engine.StendhalRPZone;
-import games.stendhal.server.core.events.TurnListener;
import games.stendhal.server.core.rp.StendhalRPAction;
import games.stendhal.server.entity.creature.Creature;
import games.stendhal.server.util.Observer;
@@ -38,12 +37,7 @@
* pattern is used; the prototypeCreature will be copied to create new
* creatures.
*/
-public class CreatureRespawnPoint implements TurnListener {
- /** longest possible respawn time in turns. half a year - should be longer than the
- * server is up in one phase */
- private static final int MAX_RESPAWN_TIME = 200 * 60 * 24 * 30 * 6;
- /** minimum respawn time in turns. about 10s */
- private static final int MIN_RESPAWN_TIME = 33;
+public class CreatureRespawnPoint implements CreatureSpawner {
/** the logger instance. */
private static final Logger logger = Logger.getLogger(CreatureRespawnPoint.class);
@@ -143,23 +137,34 @@ public void setRespawnTime(final int respawnTime) {
this.respawnTime = respawnTime;
}
- /**
- * Notifies this respawn point about the death of a creature that was
- * spawned here.
- *
- * @param dead
- * The creature that has died
- */
- public void notifyDead(final Creature dead) {
+ @Override
+ public void onSpawned(Creature spawned) {
+ spawned.init();
+ spawned.setRespawnPoint(this);
+ creatures.add(spawned);
+ }
+ @Override
+ public void onRemoved(Creature removed) {
if (!respawning) {
// start respawning a new creature
respawning = true;
SingletonRepository.getTurnNotifier().notifyInTurns(
calculateNextRespawnTurn(), this);
}
+ creatures.remove(removed);
+ }
- creatures.remove(dead);
+ /**
+ * Notifies this respawn point about the death of a creature that was
+ * spawned here.
+ *
+ * @param dead
+ * The creature that has died
+ */
+ @Deprecated
+ public void notifyDead(final Creature dead) {
+ onRemoved(dead);
}
/**
@@ -226,6 +231,7 @@ public void setPrototypeCreature(final Creature creature) {
* add observer to observers list
* @param observer - observer to add
*/
+ @Override
public void addObserver(final Observer observer) {
observers.add(observer);
}
@@ -234,6 +240,7 @@ public void addObserver(final Observer observer) {
* remove observer from list
* @param observer - observer to remove
*/
+ @Override
public void removeObserver(final Observer observer) {
observers.remove(observer);
}
@@ -250,25 +257,13 @@ public StendhalRPZone getZone() {
* Pops up a new creature.
*/
protected void respawn() {
-
try {
// clone the prototype creature
- final Creature newentity = prototypeCreature.getNewInstance();
-
- // A bit of randomization to make Joan and Snaketails a bit happier.
- // :)
- newentity.setAtk(Rand.randGaussian(newentity.getAtk(),
- newentity.getAtk() / 10));
- newentity.setDef(Rand.randGaussian(newentity.getDef(),
- newentity.getDef() / 10));
-
+ final Creature newentity = prototypeCreature.getNewInstanceRandomizeStats();
newentity.registerObjectsForNotification(observers);
if (StendhalRPAction.placeat(zone, newentity, x, y)) {
- newentity.init();
- newentity.setRespawnPoint(this);
-
- creatures.add(newentity);
+ onSpawned(newentity);
} else {
// Could not place the creature anywhere.
// Treat it like it just had died.
diff --git a/src/games/stendhal/server/entity/mapstuff/spawner/CreatureSpawner.java b/src/games/stendhal/server/entity/mapstuff/spawner/CreatureSpawner.java
new file mode 100644
index 00000000000..85f348f0249
--- /dev/null
+++ b/src/games/stendhal/server/entity/mapstuff/spawner/CreatureSpawner.java
@@ -0,0 +1,47 @@
+/***************************************************************************
+ * Copyright © 2024 - Faiumoni e. V. *
+ ***************************************************************************
+ ***************************************************************************
+ * *
+ * This program is free software; you can redistribute it and/or modify *
+ * it under the terms of the GNU General Public License as published by *
+ * the Free Software Foundation; either version 2 of the License, or *
+ * (at your option) any later version. *
+ * *
+ ***************************************************************************/
+package games.stendhal.server.entity.mapstuff.spawner;
+
+import games.stendhal.server.core.events.TurnListener;
+import games.stendhal.server.entity.creature.Creature;
+import games.stendhal.server.util.Observer;
+
+
+public interface CreatureSpawner extends TurnListener {
+
+ /** Longest possible respawn time in turns (half a year - should be longer than the server is up
+ * in one phase). */
+ static final int MAX_RESPAWN_TIME = 200 * 60 * 24 * 30 * 6;
+ /** Minimum respawn time in turns (about 10s) */
+ static final int MIN_RESPAWN_TIME = 33;
+
+
+ void addObserver(Observer observer);
+
+ void removeObserver(Observer observer);
+
+ /**
+ * Notifies this spawner that a creature was spawned.
+ *
+ * @param spawned
+ * The new creature.
+ */
+ void onSpawned(Creature spawned);
+
+ /**
+ * Notifies this spawner about the death of a creature that was spawned with it.
+ *
+ * @param removed
+ * The creature that was removed.
+ */
+ void onRemoved(Creature removed);
+}
diff --git a/src/games/stendhal/server/entity/mapstuff/spawner/DynamicCreatureSpawner.java b/src/games/stendhal/server/entity/mapstuff/spawner/DynamicCreatureSpawner.java
new file mode 100644
index 00000000000..f785642b05a
--- /dev/null
+++ b/src/games/stendhal/server/entity/mapstuff/spawner/DynamicCreatureSpawner.java
@@ -0,0 +1,344 @@
+/***************************************************************************
+ * Copyright © 2024 - Faiumoni e. V. *
+ ***************************************************************************
+ ***************************************************************************
+ * *
+ * This program is free software; you can redistribute it and/or modify *
+ * it under the terms of the GNU General Public License as published by *
+ * the Free Software Foundation; either version 2 of the License, or *
+ * (at your option) any later version. *
+ * *
+ ***************************************************************************/
+package games.stendhal.server.entity.mapstuff.spawner;
+
+import java.awt.Point;
+import java.util.LinkedHashMap;
+import java.util.LinkedList;
+import java.util.List;
+
+import org.apache.log4j.Logger;
+
+import games.stendhal.common.MathHelper;
+import games.stendhal.common.Rand;
+import games.stendhal.common.Triple;
+import games.stendhal.server.core.engine.SingletonRepository;
+import games.stendhal.server.core.engine.StendhalRPZone;
+import games.stendhal.server.core.rp.StendhalRPAction;
+import games.stendhal.server.core.rule.defaultruleset.DefaultCreature;
+import games.stendhal.server.entity.creature.Creature;
+import games.stendhal.server.util.Observer;
+import marauroa.common.Pair;
+
+
+/**
+ * Class that spawn multiple creatures at random points in a zone.
+ */
+public class DynamicCreatureSpawner extends LinkedHashMap> implements CreatureSpawner {
+
+ private static final Logger logger = Logger.getLogger(DynamicCreatureSpawner.class);
+
+ /** Zone associated with this spawner. */
+ private final StendhalRPZone zone;
+ /** Creatures to be spawned at turn reached. */
+ final List> queued;
+
+ final List observers;
+
+ /** Denotes whether turn notifier is active. */
+ private int spawnsAt = -1;
+
+
+ /**
+ * Creates a new spawner.
+ *
+ * @param zone
+ */
+ public DynamicCreatureSpawner(final StendhalRPZone zone) {
+ this.zone = zone;
+ this.observers = new LinkedList();
+ this.queued = new LinkedList>();
+ }
+
+ /**
+ * Starts the spawning cycle of all registered creatures.
+ */
+ public void init() {
+ for (final String name: keySet()) {
+ queue(name);
+ }
+ // NOTE: Do we want to queue all possible spawns of a creature simultaneously instead of
+ // sequentially? E.g. if max active = 10 queue up all 10 instances instead of only the
+ // first.
+ startNotifier();
+ }
+
+ /**
+ * Disables associated turn notifiers for clean removal.
+ */
+ public void remove() {
+ SingletonRepository.getTurnNotifier().dontNotify(this);
+ // NOTE: should active spawned creatures also be removed?
+ }
+
+ /**
+ * Adds a creature to spawn list.
+ *
+ * Registration is represented in a map indexed by creature name. The value is a triple of
+ * consisting of the following integer representations:
+ * - first: respawn time
+ * - second: max active
+ * - third: actual acitve
+ *
+ * @param name
+ * Creature name.
+ * @param max
+ * Maximum instances allowed at one time from this spawner.
+ */
+ public void register(final String name, final int max) {
+ if (max < 1) {
+ logger.warn("Max must be a positive integer");
+ return;
+ }
+ final DefaultCreature creature = SingletonRepository.getEntityManager().getDefaultCreature(name);
+ if (creature == null) {
+ logger.warn("Creature \"" + name + "\" not found");
+ return;
+ }
+ put(name, new Triple(creature.getRespawnTime(), max, 0));
+ }
+
+ /**
+ * Retrieves respawn time value of a creature.
+ *
+ * @param name
+ * Creature name.
+ * @return
+ * Rate at which creature respawns.
+ */
+ private int getRespawnTime(final String name) {
+ if (containsKey(name)) {
+ return get(name).getFirst();
+ }
+ return 0;
+ }
+
+ /**
+ * Retrieves maximum allowed active spawned instances.
+ *
+ * @param name
+ * Creature name.
+ * @return
+ * Maximum allowed creatures at one time.
+ */
+ private int getActiveMax(final String name) {
+ if (containsKey(name)) {
+ return get(name).getSecond();
+ }
+ return 0;
+ }
+
+ /**
+ * Retrieves the current number of active creatures from this spawner.
+ *
+ * @param name
+ * Creature name.
+ * @return
+ * Active creatures.
+ */
+ private int getActiveCount(final String name) {
+ if (containsKey(name)) {
+ return get(name).getThird();
+ }
+ return 0;
+ }
+
+ /**
+ * Retrieves the total number of active creatures that can be spawned at the same time.
+ *
+ * @return
+ * Max total active creatures.
+ */
+ public int maxTotal() {
+ int total = 0;
+ for (final Triple tr: values()) {
+ total += tr.getSecond();
+ }
+ return total;
+ }
+
+ /**
+ * Adds a new creature instance to zone at a random position.
+ *
+ * @param name
+ * Creature name.
+ */
+ private boolean spawn(final String name) {
+ final DefaultCreature defaultCreature = SingletonRepository.getEntityManager().getDefaultCreature(name);
+ if (defaultCreature == null) {
+ logger.error("Default creature \"" + name + "\" not found");
+ return false;
+ }
+ final Creature creature = defaultCreature.getCreatureRandomizeStats();
+ try {
+ // find a suitable location on map for spawning
+ final Point pos = zone.getRandomSpawnPosition(creature);
+ if (pos == null) {
+ throw new Exception("No suitable position for dynamic spawning available in zone " + zone.getName());
+ }
+ // NOTE: behavior copied from `CreatureRespawnPoint`, not sure what it does
+ creature.registerObjectsForNotification(observers);
+ if (StendhalRPAction.placeat(zone, creature, pos.x, pos.y)) {
+ onSpawned(creature);
+ return true;
+ } else {
+ // could not place the creature anywhere so treat it like it died
+ onRemoved(creature);
+ logger.warn("Could not spawn " + creature.getName() + " near " + zone.getName() + " "
+ + pos.x + " " + pos.y);
+ }
+ } catch (final Exception e) {
+ logger.error("Error spawning entity " + creature, e);
+ }
+ return false;
+ }
+
+ /**
+ * Adds creature to spawn queue.
+ *
+ * @param name
+ * Creature name.
+ */
+ private void queue(final String name) {
+ if (getActiveCount(name) >= getActiveMax(name)) {
+ // maximum number of creatures is active so don't spawn more
+ return;
+ }
+ final int nextTurn = MathHelper.clamp(Rand.randExponential(getRespawnTime(name)), MIN_RESPAWN_TIME, MAX_RESPAWN_TIME);
+ queued.add(new Pair<>(nextTurn, name));
+ }
+
+ /**
+ * Removes next entry from the queue.
+ */
+ private void unqueue() {
+ final int idx = queued.indexOf(getNextQueued(false));
+ if (idx > -1) {
+ queued.remove(idx);
+ }
+ }
+
+ /**
+ * Gets the next entry in the queue.
+ *
+ * @param copy
+ * If {@code true} make a copy before returning.
+ * @return
+ * Attributes of next entry.
+ */
+ private Pair getNextQueued(final boolean copy) {
+ Pair next = null;
+ // NOTE: might be more efficient to get first value of sorted list
+ for (final Pair p: queued) {
+ final int t = p.first();
+ if (next == null || t < next.first()) {
+ next = p;
+ }
+ }
+ if (copy && next != null) {
+ return new Pair<>(next.first(), next.second());
+ }
+ return next;
+ }
+
+ /**
+ * Increments the known number of active creature instances.
+ *
+ * @param name
+ * Creature name.
+ * @param amount
+ * Number to adjust by (usually 1 or -1).
+ */
+ private void incActive(final String name, final short amount) {
+ if (!containsKey(name)) {
+ logger.warn("Creature \"" + name + "\" not registered");
+ return;
+ }
+ final Triple oldValue = get(name);
+ put(name, new Triple(oldValue.getFirst(), oldValue.getSecond(), oldValue.getThird() + amount));
+ }
+
+ @Override
+ public void onSpawned(final Creature spawned) {
+ spawned.init();
+ spawned.setSpawner(this);
+ final String name = spawned.getName();
+ incActive(name, (short) 1);
+ // spawning succeeded so reset active spawning property
+ spawnsAt = -1;
+ // add another to queue
+ queue(name);
+ // don't restart notifier here as it will be done in {@code TurnListener.onTurnReached}
+ }
+
+ @Override
+ public void onRemoved(final Creature removed) {
+ final String name = removed.getName();
+ incActive(name, (short) -1);
+ // add another to queue
+ queue(name);
+ // restart notifier if needed
+ startNotifier();
+ }
+
+ /**
+ * Starts turn notifier for next queued entry.
+ */
+ private void startNotifier() {
+ final Pair next = getNextQueued(false);
+ if (next == null) {
+ return;
+ }
+ final int turns = next.first();
+ if (spawnsAt > -1 && turns >= spawnsAt) {
+ // don't override earlier spawning
+ return;
+ }
+ spawnsAt = turns;
+ SingletonRepository.getTurnNotifier().notifyInTurns(turns, this);
+ }
+
+ @Override
+ public void onTurnReached(final int currentTurn) {
+ final Pair next = getNextQueued(true);
+ if (next != null) {
+ final String nextName = next.second();
+ /* this check doesn't work because currentTurn is total turns since server started &
+ * next.first() is turns since time of queue
+ if (next.first() != currentTurn) {
+ logger.warn("Creature \"" + nextName + "\" scheduled to spawn at turn " + nextTurn + " but spawning at " + currentTurn);
+ }
+ */
+ // remove from queue
+ unqueue();
+ spawn(nextName);
+ } else {
+ logger.warn("Turn reached but nothing queued for spawn");
+ }
+
+ // restart notifier if another entry is in queue
+ startNotifier();
+ if (spawnsAt > -1 && queued.size() > 0) {
+ logger.warn("Queue not empty but not scheduled for spawn");
+ }
+ }
+
+ @Override
+ public void addObserver(final Observer observer) {
+ observers.add(observer);
+ }
+
+ @Override
+ public void removeObserver(final Observer observer) {
+ observers.remove(observer);
+ }
+}
diff --git a/tests/games/stendhal/client/gui/j2d/entity/EntityViewFactoryTest.java b/tests/games/stendhal/client/gui/j2d/entity/EntityViewFactoryTest.java
index 5f828b6170f..f355f1c8b97 100644
--- a/tests/games/stendhal/client/gui/j2d/entity/EntityViewFactoryTest.java
+++ b/tests/games/stendhal/client/gui/j2d/entity/EntityViewFactoryTest.java
@@ -24,7 +24,7 @@
import org.junit.Before;
import org.junit.Test;
-import games.stendhal.client.Triple;
+import games.stendhal.common.Triple;
import marauroa.common.Log4J;
public class EntityViewFactoryTest {