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 {